diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e455d43ce..c531b4c623 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,7 @@ repos: - id: check-toml - id: check-ast - id: check-added-large-files + exclude: ^third_party/ascend/backend/lib/libdevice\.10\.bc$ - id: check-merge-conflict - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable diff --git a/CMakeLists.txt b/CMakeLists.txt index b1db0614ba..beded2abec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,12 @@ if(FLAGTREE_BACKEND AND EXISTS ${BACKEND_INCLUDE_DIR}) # flagtree: include directories include_directories(${PROJECT_SOURCE_DIR}/third_party/${FLAGTREE_BACKEND}/include) include_directories(${PROJECT_BINARY_DIR}/third_party/${FLAGTREE_BACKEND}/include) # Tablegen'd files + if(FLAGTREE_BACKEND STREQUAL "ascend") + # The ascend backend builds the core include/lib from its own CMakeLists, so + # only the search paths are needed here. + include_directories(${PROJECT_SOURCE_DIR}/include) + include_directories(${PROJECT_BINARY_DIR}/include) # Tablegen'd files + endif() else() include_directories(${PROJECT_SOURCE_DIR}/include) include_directories(${PROJECT_BINARY_DIR}/include) # Tablegen'd files @@ -212,6 +218,8 @@ if(TRITON_BUILD_PYTHON_MODULE) # FlagPrism: register component targets before backend codegen captures TRITON_LIBS. flagtree_add_flagprism_components() flagtree_configure_flir_dependency() + # Provide the BiShengIR targets before the backend subdirectory links them. + flagtree_configure_ascend_npu_ir_dependency() foreach(CODEGEN_BACKEND ${TRITON_CODEGEN_BACKENDS}) add_subdirectory(third_party/${CODEGEN_BACKEND}) @@ -257,6 +265,7 @@ if(TRITON_BUILD_PYTHON_MODULE) ${triton_plugins} # mlir + MLIRFuncInlinerExtension MLIRAMDGPUDialect MLIRNVVMDialect MLIRNVVMToLLVMIRTranslation diff --git a/cmake/FlagTreeOptions.cmake b/cmake/FlagTreeOptions.cmake index a5392d4428..4fb605b259 100644 --- a/cmake/FlagTreeOptions.cmake +++ b/cmake/FlagTreeOptions.cmake @@ -37,6 +37,11 @@ macro(flagtree_configure_options) add_compile_definitions(__FLAGTREE_COMMON_IR__) endif() + # Ascend always requires CommonIR, without a C/C++ feature gate. + if(FLAGTREE_BACKEND STREQUAL "ascend") + set(FLAGTREE_COMMON_IR_ENABLED ON) + endif() + set(FLAGCX_ENABLED OFF) set(FLAGCX_SUPPORT_BACKENDS nvidia iluvatar) if(NOT FLAGTREE_BACKEND OR @@ -82,6 +87,29 @@ macro(flagtree_configure_options) set(CMAKE_CXX_COMPILER clang++-16) add_definitions(-D__NVIDIA__) add_definitions(-D__AMD__) + elseif(FLAGTREE_BACKEND STREQUAL "ascend") + set(CMAKE_C_COMPILER clang) + set(CMAKE_CXX_COMPILER clang++) + # AscendNPU-IR gates its sources on the LLVM major version. cmake/llvm-hash.txt + # pins LLVM 22 here. + # + # Deliberately leave LLVM_MAJOR_VERSION_21_COMPATIBLE unset: LLVM 22 renamed + # the Mesh dialect to Shard (only libMLIRShardDialect.a ships in the pinned + # package), so turning it on would reference a missing target. + set(LLVM_MAJOR_VERSION_22_OR_HIGHER ON) + set(LLVM_MAJOR_VERSION_22_COMPATIBLE ON CACHE BOOL + "AscendNPU-IR build with llvm major version 22 or later" FORCE) + add_definitions(-D__LLVM_MAJOR_VERSION_22_COMPATIBLE__) + add_compile_options("-Wno-deprecated-declarations") + add_compile_options("-Wno-error=deprecated-declarations") + # AscendNPU-IR: build only the standalone IR, and keep it out of the + # backend's own CMakeLists so the subdirectory is added exactly once. + set(BISHENGIR_ENABLE_A5_UNPUBLISHED_FEATURES ON) + set(BISHENGIR_BUILD_STANDALONE_IR_ONLY ON) + set(ASCENDNPU_IR_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/ascend/AscendNPU-IR) + set(ASCENDNPU_IR_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/third_party/ascend/AscendNPU-IR) + include_directories(${ASCENDNPU_IR_SRC_DIR}/bishengir/include) + include_directories(${ASCENDNPU_IR_BINARY_DIR}/bishengir/include) elseif(FLAGTREE_BACKEND STREQUAL "tsingmicro") set(CMAKE_C_COMPILER clang) set(CMAKE_CXX_COMPILER clang++) @@ -230,7 +258,7 @@ macro(flagtree_configure_core_source) endif() if(FLAGTREE_BACKEND MATCHES - "^(xpu|cambricon|aipu|tsingmicro|enflame|rpu|thrive|metax|tileir|ppu|spacemit|sunrise)$") + "^(xpu|cambricon|aipu|tsingmicro|enflame|rpu|thrive|metax|tileir|ppu|spacemit|sunrise|ascend)$") include_directories(${PROJECT_SOURCE_DIR}/include) include_directories(${PROJECT_BINARY_DIR}/include) # Tablegen'd files if(FLAGTREE_BACKEND STREQUAL "xpu") @@ -320,7 +348,7 @@ endmacro() macro(flagtree_configure_flir_dependency) if(FLAGTREE_COMMON_IR_ENABLED) if(NOT EXISTS "${PROJECT_SOURCE_DIR}/third_party/flir/CMakeLists.txt") - message(FATAL_ERROR "FLAGTREE_COMMON_IR requires third_party/flir") + message(FATAL_ERROR "CommonIR support requires third_party/flir") endif() include_directories(${PROJECT_SOURCE_DIR}/third_party/flir/include) include_directories(${PROJECT_BINARY_DIR}/third_party/flir/include) @@ -346,6 +374,21 @@ macro(flagtree_configure_flir_dependency) endmacro() +# AscendNPU-IR provides the BiShengIR dialects the ascend backend links against. +# Added from the top level (not from the backend's own CMakeLists) so the source +# directory is only ever associated with one binary directory. +macro(flagtree_configure_ascend_npu_ir_dependency) + if(FLAGTREE_BACKEND STREQUAL "ascend") + if(NOT EXISTS "${ASCENDNPU_IR_SRC_DIR}/CMakeLists.txt") + message(FATAL_ERROR + "The ascend backend requires AscendNPU-IR at ${ASCENDNPU_IR_SRC_DIR}. " + "Run the Python package build to download third-party dependencies.") + endif() + add_subdirectory(${ASCENDNPU_IR_SRC_DIR} ${ASCENDNPU_IR_BINARY_DIR}) + endif() +endmacro() + + macro(flagtree_configure_tle_plugin append_tle_plugin) if(FLAGTREE_TLE) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/tle/CMakeLists.txt") @@ -575,6 +618,10 @@ macro(flagtree_configure_tools_and_tests) flagtree_add_tle_generated_header_dependencies() endif() add_subdirectory(test) + elseif(FLAGTREE_BACKEND STREQUAL "ascend") + if(FLAGTREE_TLE) + flagtree_add_tle_generated_header_dependencies() + endif() elseif(FLAGTREE_BACKEND STREQUAL "iluvatar") option(FLAGTREE_ILUVATAR_BUILD_BIN "Build third_party/iluvatar/bin tools and lit tests" OFF) @@ -683,6 +730,13 @@ macro(flagtree_added_python_src) endforeach() add_library(triton SHARED ${_flagtree_python_sources}) + if(FLAGTREE_BACKEND STREQUAL "ascend") + # third_party/ascend/triton_ascend.cc registers the buffer_ir submodule, so + # its implementation must be part of the triton module. + target_sources(triton PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/ascend/python/triton/extension/buffer/src/buffer_ir.cc) + endif() + if(FLAGTREE_BACKEND STREQUAL "xpu") target_sources(triton PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/third_party/xpu/python/src/mlir_pass_abi_shim.cc) add_dependencies(triton TritonAMDGPUTableGen TritonAMDGPUAttrDefsIncGen) diff --git a/python/requirements-ascend.txt b/python/requirements-ascend.txt new file mode 100644 index 0000000000..9d3f182fde --- /dev/null +++ b/python/requirements-ascend.txt @@ -0,0 +1,3 @@ +--extra-index-url https://ascend.devcloud.huaweicloud.com/cann/pypi/simple/ + +cann-shmem diff --git a/python/setup_tools/setup_helper.py b/python/setup_tools/setup_helper.py index 73df70aef7..fd46dcbd80 100644 --- a/python/setup_tools/setup_helper.py +++ b/python/setup_tools/setup_helper.py @@ -68,7 +68,8 @@ def get_long_description(): def init_backends(backend_installer): if flagtree_backend: - if flagtree_backend in ("aipu", "tsingmicro", "enflame", "rpu", "thrive", "sunrise", "tileir", "ppu", "amd"): + if flagtree_backend in ("aipu", "tsingmicro", "enflame", "rpu", "thrive", "sunrise", "tileir", "ppu", "amd", + "ascend"): backends = [ *backend_installer.copy(configs.default_backends + tuple(configs.extend_backends)), *backend_installer.copy_externals(), @@ -233,6 +234,12 @@ def get_hook_instance(hook_name): return hook_instance if callable(hook_instance) else None +def finalize_backend_build_tree(build_lib): + hook = get_hook_instance("finalize_build_tree") + if hook: + hook(build_lib) + + def enable_flagtree_third_party(name): return os.environ.get(f"USE_{name.upper()}", 'ON') == 'ON' @@ -888,7 +895,14 @@ def uninstall_triton(): cache = FlagTreeCache() -download_flagtree_third_party("flir", condition=(flagtree_backend == "tsingmicro"), required=True) +# FLIR supplies the CommonIR dialect, which the ascend backend requires for its +# CommonIRToHIVM lowering and FLIR's triton-lane-vectorize pass. +download_flagtree_third_party("flir", condition=(flagtree_backend in ("tsingmicro", "ascend")), required=True) + +# The ascend backend links the BiShengIR dialects built from AscendNPU-IR. +if flagtree_backend == "ascend": + configs.activated_module.warn_stale_ascend_npu_ir_checkout(utils.flagtree_submodules["AscendNPU-IR"]) + download_flagtree_third_party("AscendNPU-IR", condition=True, required=True) ''' FlagCX is a third-party library adopted by the tle distributed system, refer to https://github.com/flagos-ai/FlagCX diff --git a/python/setup_tools/utils/__init__.py b/python/setup_tools/utils/__init__.py index 3931c53025..26eb20f6ad 100644 --- a/python/setup_tools/utils/__init__.py +++ b/python/setup_tools/utils/__init__.py @@ -60,6 +60,10 @@ def _register_submodule(self, submodule): }, {"name": "flir", "url": "https://github.com/flagos-ai/flir.git"}, {"name": "FlagPrism", "url": "https://github.com/flagos-ai/FlagPrism.git"}, + { + "name": "AscendNPU-IR", "url": "https://gitcode.com/Ascend/AscendNPU-IR.git", "relative_path": + "ascend/AscendNPU-IR", "commit_id": "efb7141581c009bbd1d676b18941ed15ede62a68" + }, { "name": "flagcx", "url": "https://github.com/flagos-ai/FlagCX.git", "relative_path": "tle/third_party/flagcx", "commit_id": "c93438decaba8d935e7aa6ae8377e56fd3910b32" diff --git a/python/setup_tools/utils/ascend.py b/python/setup_tools/utils/ascend.py new file mode 100644 index 0000000000..f0be98766b --- /dev/null +++ b/python/setup_tools/utils/ascend.py @@ -0,0 +1,48 @@ +import importlib.util +import os +import subprocess +from pathlib import Path + + +def install_extension(*args, **kargs): + """Copy producer tools next to libtriton, then strip only the copies.""" + from python.build_helpers import get_cmake_dir + + build_ext = kargs.get("build_ext") or (args[0] if args else None) + if build_ext is None: + raise RuntimeError("Ascend install_extension requires the build_ext command") + + # Match setup.py's CMakeBuild.build_extension: ext.path, not ext.name. + ext = next(ext for ext in build_ext.extensions if ext.name == "triton") + extdir = Path(build_ext.get_ext_fullpath(ext.path)).resolve().parent + source = Path(__file__).resolve().parents[3] / "third_party/ascend/build/packaging.py" + _load_packaging(source)._copy_ascend_tools(extdir, get_cmake_dir()) + + +def _load_packaging(source): + spec = importlib.util.spec_from_file_location("_flagtree_ascend_packaging", source) + if spec is None or spec.loader is None: + raise RuntimeError(f"Cannot load Ascend packaging hook: {source}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def finalize_build_tree(build_lib): + """Delegate packaging to the backend without importing its runtime.""" + source = Path(__file__).resolve().parents[3] / "third_party/ascend/build/packaging.py" + _load_packaging(source).finalize_build_tree(build_lib) + + +def warn_stale_ascend_npu_ir_checkout(module): + # DownloadManager reuses dst_path as-is when it already exists, so a + # checkout left on a different commit would silently shadow the pin + # registered for this build. + if not module.commit_id or not os.path.isdir(os.path.join(module.dst_path, ".git")): + return + head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=module.dst_path, capture_output=True, text=True) + if head.returncode != 0 or head.stdout.strip().startswith(module.commit_id): + return + print(f"[WARNING] {module.dst_path} is at {head.stdout.strip()[:8]} but this build pins " + f"AscendNPU-IR to {module.commit_id[:8]}; remove the directory (or run " + f"'git checkout {module.commit_id}' inside it) before rebuilding") diff --git a/python/src/ir.cc b/python/src/ir.cc index f5a60bb78f..9495b8cb26 100644 --- a/python/src/ir.cc +++ b/python/src/ir.cc @@ -382,13 +382,19 @@ void init_triton_ir(py::module &&m) { .value("NONE", PropagateNan::NONE) .value("ALL", PropagateNan::ALL); - py::enum_(m, "INPUT_PRECISION", py::module_local()) + auto inputPrecision = + py::enum_(m, "INPUT_PRECISION", py::module_local()); + inputPrecision .value("TF32", InputPrecision::TF32) .value("TF32x3", InputPrecision::TF32x3) .value("IEEE", InputPrecision::IEEE) .value("BF16x3", InputPrecision::BF16x3) - .value("BF16x6", InputPrecision::BF16x6) - .export_values(); + .value("BF16x6", InputPrecision::BF16x6); + + // Backend-specialized enum definitions may additionally support HF32. + if (auto hf32 = symbolizeInputPrecision("hf32")) + inputPrecision.value("HF32", *hf32); + inputPrecision.export_values(); py::enum_(m, "ScaleDotElemTypeTY", py::module_local()) .value("E4M3", ScaleDotElemType::E4M3) diff --git a/python/triton/_utils.py b/python/triton/_utils.py index 9f4ab1a48c..98594abdb3 100644 --- a/python/triton/_utils.py +++ b/python/triton/_utils.py @@ -26,6 +26,11 @@ from typing import Any, Callable, TYPE_CHECKING, Union, List, Dict from triton._flagtree_spec import spec_func +from triton._flagtree_backend import FLAGTREE_BACKEND + +# The ascend backend lowers tensors through a SIMD pipeline that does not +# require power-of-2 block shapes (see validate_block_shape below). +_SKIP_POW2_BLOCK_SHAPE = FLAGTREE_BACKEND == "ascend" if TYPE_CHECKING: from .language import core @@ -80,7 +85,13 @@ def validate_block_shape(shape: List[int]): for i, d in enumerate(shape): if not isinstance(d, int): raise TypeError(f"Shape element {i} must have type `constexpr[int]`, got `constexpr[{type(d)}]") - if not is_power_of_two(d): + # A negative dimension is invalid for every backend. + if d < 0: + raise ValueError(f"Shape element {i} must be non-negative") + # Backends that lower through a SIMD pipeline (e.g. ascend) support + # non-power-of-2 block shapes; this check has no builder in scope, so + # gate it on the active backend. + if not is_power_of_two(d) and not _SKIP_POW2_BLOCK_SHAPE: raise ValueError(f"Shape element {i} must be a power of 2") numel *= d diff --git a/python/triton/backends/compiler.py b/python/triton/backends/compiler.py index 4b9e371734..734aa5e3e0 100644 --- a/python/triton/backends/compiler.py +++ b/python/triton/backends/compiler.py @@ -95,6 +95,17 @@ def get_tensor_descriptor_specialization(arg): """Return backend-specific TensorDescriptor cache-key attributes.""" return None + @staticmethod + def use_alignment_specialization(options: dict) -> bool: + """Whether divisibility-by-16 specialization applies for these options. + + Receives raw launch keyword options before parse_options, once per + binder call when alignment specialization can apply. The default + preserves existing cache keys. Overrides must agree with the options + used for compilation; parameter-level opt-outs still take precedence. + """ + return True + @staticmethod @abstractmethod def supports_target(target: GPUTarget): diff --git a/python/triton/compiler/code_generator.py b/python/triton/compiler/code_generator.py index bb78ac03f1..1a9e4b5e03 100644 --- a/python/triton/compiler/code_generator.py +++ b/python/triton/compiler/code_generator.py @@ -390,6 +390,12 @@ def __init__(self, context, prototype, gscope, function_name, jit_fn: JITFunctio # convert_custom_types(input_tensor, dtype, fp_downcast_rounding=None, _builder=None) self.builder.codegen_fns = codegen_fns self.builder.module_map = {} if module_map is None else module_map + # Backend extensions are instance-local and must be ready before IR emission. + # Gluon keeps its own builder and semantics. + if not is_gluon: + initialize_builder = codegen_fns.get("initialize_builder") + if initialize_builder is not None: + initialize_builder(self.builder, context, options) self.module = self.builder.create_module() if module is None else module self.function_ret_types = {} if function_types is None else function_types self.prototype = prototype @@ -1107,6 +1113,23 @@ def visit_IfExp(self, node): return self.visit(node.orelse) def visit_With(self, node): + # Let backends lower specialized context managers before invoking + # the standard context manager protocol. + with_handlers = self.builder.codegen_fns.get("with_handlers", {}) + if with_handlers and len(node.items) == 1: + item = node.items[0] + if isinstance(item.context_expr, ast.Call): + context_type = self.visit(item.context_expr.func) + handler = with_handlers.get(context_type) + if handler is not None: + if item.optional_vars is not None: + raise self._unsupported( + node, "`as` bindings are not supported for backend-specific `with` statements") + if ContainsReturnChecker(self.gscope).visit(node): + raise self._unsupported( + node, "Cannot have `return` statements inside `with` statements in triton ") + return handler(self, node) + # Lower `with` statements by constructing context managers and calling their enter/exit hooks # Instantiate each context manager with builder injection cm_list = [] diff --git a/python/triton/compiler/compiler.py b/python/triton/compiler/compiler.py index 75fa1d4b18..d454b0e8fa 100644 --- a/python/triton/compiler/compiler.py +++ b/python/triton/compiler/compiler.py @@ -469,8 +469,12 @@ def __init__(self, src, metadata_group, hash): # stores the text of each level of IR that was generated during compilation asm_files = [Path(p) for c, p in metadata_group.items() if not c.endswith(".json")] binary_ext = backend.binary_ext + # A backend may emit more than one binary artifact (e.g. the ascend + # backend caches the MLIR bytecode stage as .mlirbc alongside .npubin); + # those must be read as bytes, not decoded as UTF-8 text. + binary_exts = set(getattr(backend, "binary_extensions", None) or {binary_ext}) self.asm = AsmDict({ - file.suffix[1:]: file.read_bytes() if file.suffix[1:] == binary_ext else file.read_text() + file.suffix[1:]: file.read_bytes() if file.suffix[1:] in binary_exts else file.read_text() for file in asm_files }) self.metadata_group = metadata_group @@ -516,8 +520,16 @@ def raise_(err): if knobs.runtime.kernel_load_start_hook is not None: knobs.runtime.kernel_load_start_hook(self.module, self.function, self.name, self.metadata_group, self.hash) # TODO: n_regs, n_spills should be metadata generated when calling `ptxas` - self.module, self.function, self.n_regs, self.n_spills, self.n_max_threads = kernel_driver.utils.load_binary( - self.name, self.kernel, self.metadata.shared, device) + # The ascend backend's load_binary needs the kernel's mix_mode (aiv/aic/mix) + # to pick the launch path, and keys the binary by the mangled kernel_name + # rather than the Python function name. + if hasattr(self.metadata, "mix_mode"): + self.module, self.function, self.n_regs, self.n_spills, self.n_max_threads = \ + kernel_driver.utils.load_binary(self.metadata.kernel_name, self.kernel, self.metadata.shared, device, + self.metadata.mix_mode) + else: + self.module, self.function, self.n_regs, self.n_spills, self.n_max_threads = \ + kernel_driver.utils.load_binary(self.name, self.kernel, self.metadata.shared, device) warp_size = self.metadata.target.warp_size if self.metadata.num_warps * warp_size > self.n_max_threads \ and self.metadata.target.backend != "maca": # flagtree metax diff --git a/python/triton/compiler/errors.py b/python/triton/compiler/errors.py index 955f8762ed..81d8f79609 100644 --- a/python/triton/compiler/errors.py +++ b/python/triton/compiler/errors.py @@ -71,3 +71,28 @@ class CompileTimeAssertionFailure(CompilationError): class UnsupportedLanguageConstruct(CompilationError): pass + + +class MLIRCompilationError(TritonError): + """Failure in an Ascend IR or binary compilation stage.""" + + def __init__(self, stage_name: Optional[str], message: Optional[str] = None): + self.stage_name = stage_name + self.error_message = "" if message is None else message + # Preserve the complete diagnostic, but omit the unsymbolized stack + # dump from the displayed message. + detail = self.error_message.split("Stack dump without symbol names", 1)[0] + if detail and not detail.endswith("\n"): + detail += "\n" + self.message = ( + "\n" + "///------------------[ERROR][Triton][BEG]------------------\n" + f"[{self.stage_name}] encounters error:\n" + f"{detail}" + "///------------------[ERROR][Triton][END]------------------\n" + ) + super().__init__(self.message) + + def __reduce__(self): + # Reconstruct from the original diagnostic, not the formatted message. + return type(self), (self.stage_name, self.error_message) diff --git a/python/triton/extension/__init__.py b/python/triton/extension/__init__.py new file mode 100644 index 0000000000..ac05b013db --- /dev/null +++ b/python/triton/extension/__init__.py @@ -0,0 +1,4 @@ +# flagtree backend path specialization +from triton._flagtree_spec import spec_path + +spec_path(__path__) diff --git a/python/triton/language/semantic.py b/python/triton/language/semantic.py index 77dbd8fc1c..294d48e952 100644 --- a/python/triton/language/semantic.py +++ b/python/triton/language/semantic.py @@ -44,6 +44,18 @@ def __init__(self, type_a, type_b): super(IncompatibleTypeErrorImpl, self).__init__(self.message) +# flagtree: arange's power-of-2 restriction is a SIMT lowering constraint. +# Prefer the builder's mode query when available (e.g. Ascend interpreter). +# Otherwise use compilation options, keeping the strict default. +def _requires_power_of_two_range(builder) -> bool: + is_simt_mode = getattr(builder, "is_simt_mode", None) + if callable(is_simt_mode): + return is_simt_mode() + + options = getattr(builder, "options", None) + return getattr(options, "parallel_mode", "simt") != "simd" + + class TritonSemantic(Generic[TensorTy]): tensor: Type[TensorTy] = tl.tensor lang = tl @@ -601,7 +613,10 @@ def arange(self, start: int, end: int, *, ret_ty: tl.block_type = None) -> Tenso if end <= start: raise ValueError("arange's end argument must be greater than the start argument") range = end - start - if (range & (range - 1)) != 0: + # The power-of-2 restriction only applies to SIMT lowering. Backends that + # lower through a SIMD pipeline (e.g. ascend, whose options report + # parallel_mode == "simd") accept arbitrary ranges. + if (range & (range - 1)) != 0 and _requires_power_of_two_range(self.builder): raise ValueError("arange's range must be a power of 2") shape = [range] if ret_ty is None: diff --git a/python/triton/runtime/jit.py b/python/triton/runtime/jit.py index 70c48bd546..3dad931087 100644 --- a/python/triton/runtime/jit.py +++ b/python/triton/runtime/jit.py @@ -428,6 +428,13 @@ def create_function_from_signature(sig, kparams, backend): assert len(sig.parameters) == len(kparams) # Create the function argument list and the dict entries for the return statement specialization = [] + # Whether alignment specialization applies can depend on the launch options + # so the backend is asked once per binder call rather than baked in when + # the binder is generated. + uses_alignment_specialization = False + alignment_specialization_name = "__triton_alignment_specialization" + while alignment_specialization_name in sig.parameters: + alignment_specialization_name += "_" # signature for name, kp in zip(sig.parameters.keys(), kparams): if kp.is_constexpr: @@ -435,14 +442,20 @@ def create_function_from_signature(sig, kparams, backend): else: is_const = 'True' if kp.is_const else 'False' specialize = 'False' if kp.do_not_specialize else 'True' - align = 'False' if kp.do_not_specialize_on_alignment else 'True' - ret = f"specialize_impl(backend, {name}, {is_const}, {specialize}, {align})" + # `specialize` is emitted into generated source, so the string + # "False" is truthy here; keep the decision on a real boolean. + specializes_value = not kp.do_not_specialize + align = not kp.do_not_specialize_on_alignment + effective_align = alignment_specialization_name if align and specializes_value else 'False' + ret = f"specialize_impl(backend, {name}, {is_const}, {specialize}, {effective_align})" if kp.annotation_type: if isinstance(kp.annotation_type, str): if kp.annotation_type == "u1" or kp.annotation_type[:2] in ["fp", "bf"]: # we do not specialize non-constexpr floats and bools: specialize = False if specialize: + uses_alignment_specialization = (uses_alignment_specialization + or (align and specializes_value)) # Prefer constexpr over annotation type to avoid downgrading # compile-time constants to runtime-specialized types specialization.append( @@ -451,13 +464,18 @@ def create_function_from_signature(sig, kparams, backend): # skip runtime specialization: specialization.append(f'("{kp.annotation_type}", None)') else: + uses_alignment_specialization = (uses_alignment_specialization + or (align and specializes_value)) specialization.append(f"{ret}") # compute argument string for a given parameter arg = lambda x: x[0] if x[1].default is inspect.Parameter.empty else f"{x[0]}=default_{x[0]}" + alignment_specialization_init = (f" {alignment_specialization_name} = " + "backend.use_alignment_specialization(options)\n" + if uses_alignment_specialization else "") func_body = f""" def dynamic_func({", ".join(list(map(arg, sig.parameters.items())) + ["**options"])}): - params = {{{', '.join([f"'{name}': {name}" for name in sig.parameters.keys()])}}} +{alignment_specialization_init} params = {{{', '.join([f"'{name}': {name}" for name in sig.parameters.keys()])}}} specialization = [{','.join(specialization)}] return params, specialization, options """ diff --git a/python/triton/spec/ascend b/python/triton/spec/ascend new file mode 120000 index 0000000000..4233a96de7 --- /dev/null +++ b/python/triton/spec/ascend @@ -0,0 +1 @@ +../../../third_party/ascend/spec/ \ No newline at end of file diff --git a/setup.py b/setup.py index 678a438bba..f716db9f68 100644 --- a/setup.py +++ b/setup.py @@ -475,6 +475,7 @@ def run(self) -> None: helper.write_backend_site_pth(self.build_lib) # flagtree # FlagPrism: finalize external component packaging after build_py. FLAGPRISM_SETUP.finalize_build_tree(self.build_lib) + helper.finalize_backend_build_tree(self.build_lib) return ret diff --git a/third_party/ascend/.gitignore b/third_party/ascend/.gitignore new file mode 100644 index 0000000000..a277bb8c49 --- /dev/null +++ b/third_party/ascend/.gitignore @@ -0,0 +1 @@ +ascendnpu_ir/ diff --git a/third_party/ascend/CMakeLists.txt b/third_party/ascend/CMakeLists.txt new file mode 100644 index 0000000000..bbbe2547e5 --- /dev/null +++ b/third_party/ascend/CMakeLists.txt @@ -0,0 +1,189 @@ +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/include) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# Triton Ascend is dependent on AscendNPU IR. The subdirectory is added from the +# top-level CMakeLists (flagtree_configure_ascend_npu_ir_dependency), which also +# sets ASCENDNPU_IR_SRC_DIR / ASCENDNPU_IR_BINARY_DIR and the BISHENGIR_* options, +# so that a source dir is only ever bound to one binary dir. +if(NOT ASCENDNPU_IR_SRC_DIR) + message(FATAL_ERROR + "ASCENDNPU_IR_SRC_DIR is unset; the ascend backend expects the top-level " + "CMakeLists to have configured AscendNPU-IR already.") +endif() + +set(TRITON_DISTRIBUTED_ASCEND_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Triton-distributed-ascend" CACHE PATH + "Path to Triton-distributed-ascend source root") +set(TRITON_DISTRIBUTED_ASCEND_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/Triton-distributed-ascend") + +include_directories(${ASCENDNPU_IR_SRC_DIR}/bishengir/include) +include_directories(${ASCENDNPU_IR_BINARY_DIR}/bishengir/include) # Tablegen'd files + +add_subdirectory(include) +add_subdirectory(lib) +add_subdirectory(bin) + +if(TRITON_BUILD_TD) + if(NOT EXISTS "${TRITON_DISTRIBUTED_ASCEND_SRC_DIR}/CMakeLists.txt") + message(FATAL_ERROR "Triton-Distributed submodule is not initialized: ${TRITON_DISTRIBUTED_ASCEND_SRC_DIR}") + endif() + + set(TRITON_USE_ASCEND ON) + add_compile_definitions(TRITON_USE_ASCEND=1) + + if(TRITON_BUILD_PYTHON_MODULE) + # Python3::Module / pybind11::headers targets are created in the root + # CMakeLists.txt *after* this directory is processed, so resolve them here + # when the distributed plugin needs them. + if(NOT Python3_FOUND OR NOT TARGET Python3::Module) + find_package(Python3 REQUIRED COMPONENTS Development Interpreter) + endif() + if(NOT pybind11_FOUND) + find_package(pybind11 CONFIG REQUIRED HINTS "${Python3_SITELIB}") + endif() + include_directories(${Python3_INCLUDE_DIRS}) + include_directories(${pybind11_INCLUDE_DIR}) + endif() + + include_directories(${TRITON_DISTRIBUTED_ASCEND_SRC_DIR}/include) + include_directories(${TRITON_DISTRIBUTED_ASCEND_BINARY_DIR}/include) + add_subdirectory(${TRITON_DISTRIBUTED_ASCEND_SRC_DIR}/include/TritonDistributed + ${TRITON_DISTRIBUTED_ASCEND_BINARY_DIR}/include/TritonDistributed) + add_subdirectory(${TRITON_DISTRIBUTED_ASCEND_SRC_DIR}/lib + ${TRITON_DISTRIBUTED_ASCEND_BINARY_DIR}/lib) + + if(TRITON_BUILD_PYTHON_MODULE) + set(PYTHON_DIST_SRC_PATH ${TRITON_DISTRIBUTED_ASCEND_SRC_DIR}/python/src) + add_triton_plugin(TritonDistributed + ${PYTHON_DIST_SRC_PATH}/triton_distributed.cc + ${PYTHON_DIST_SRC_PATH}/ascend_passes.cc + ${PYTHON_DIST_SRC_PATH}/ir.cc) + target_link_libraries(TritonDistributed PRIVATE DistributedIR Python3::Module pybind11::headers) + endif() +endif() + +option(TRITON_ASCEND_ENABLE_COSTMODEL_INPROC "Enable in-process costmodel C++ bridge" ON) +if(TRITON_ASCEND_ENABLE_COSTMODEL_INPROC) + set(TRITON_ASCEND_COSTMODEL_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/costmodel" CACHE PATH "Path to vendored vTriton source root") + set(_costmodel_include_dir "${TRITON_ASCEND_COSTMODEL_SOURCE_DIR}/include/AscendModel") + set(_costmodel_lib_dir "${TRITON_ASCEND_COSTMODEL_SOURCE_DIR}/lib/AscendModel") + + if(EXISTS "${_costmodel_include_dir}/CMakeLists.txt" AND EXISTS "${_costmodel_lib_dir}/CMakeLists.txt") + # Generate HardwareParams.h required by AscendModel analysis/transforms. + set(ASCEND_CUBE_TFLOPS 320 CACHE STRING "Cube Core FP16 TFLOPS") + set(ASCEND_CUBE_M 16 CACHE STRING "Cube Core M dimension") + set(ASCEND_CUBE_N 16 CACHE STRING "Cube Core N dimension") + set(ASCEND_CUBE_K 16 CACHE STRING "Cube Core K dimension") + set(ASCEND_VECTOR_TFLOPS 10 CACHE STRING "Vector Core FP32 TFLOPS") + set(ASCEND_VECTOR_WIDTH 256 CACHE STRING "Vector width in bits") + set(ASCEND_HBM_BW 1555 CACHE STRING "HBM Bandwidth GB/s") + set(ASCEND_L2_SIZE 192 CACHE STRING "L2 Cache Size MB") + set(ASCEND_L1_SIZE 1024 CACHE STRING "L1 Buffer Size KB") + set(_costmodel_gen_include "${CMAKE_BINARY_DIR}/third_party/ascend/costmodel_build/include") + file(MAKE_DIRECTORY "${_costmodel_gen_include}/AscendModel") + configure_file( + "${_costmodel_include_dir}/HardwareParams.h.in" + "${_costmodel_gen_include}/AscendModel/HardwareParams.h" + @ONLY + ) + include_directories("${_costmodel_gen_include}") + # TableGen outputs (*.inc) from AscendModel/include land in this binary dir. + include_directories("${CMAKE_BINARY_DIR}/third_party/ascend/costmodel_build/include_AscendModel") + include_directories("${TRITON_ASCEND_COSTMODEL_SOURCE_DIR}/include") + + add_subdirectory(${_costmodel_include_dir} ${CMAKE_BINARY_DIR}/third_party/ascend/costmodel_build/include_AscendModel) + add_subdirectory(${_costmodel_lib_dir} ${CMAKE_BINARY_DIR}/third_party/ascend/costmodel_build/lib_AscendModel) + set(TRITON_ASCEND_HAS_INPROC_COSTMODEL 1) + else() + message(WARNING "Minimal costmodel sources not found under ${TRITON_ASCEND_COSTMODEL_SOURCE_DIR}; in-process costmodel bridge disabled") + set(TRITON_ASCEND_HAS_INPROC_COSTMODEL 0) + endif() +else() + set(TRITON_ASCEND_HAS_INPROC_COSTMODEL 0) +endif() + +if(TRITON_ASCEND_HAS_INPROC_COSTMODEL) + target_include_directories(TritonToGraph PRIVATE + "${_costmodel_gen_include}" + "${CMAKE_BINARY_DIR}/third_party/ascend/costmodel_build/include_AscendModel" + "${TRITON_ASCEND_COSTMODEL_SOURCE_DIR}/include" + ) + target_compile_definitions(TritonToGraph PRIVATE TRITON_ASCEND_HAS_INPROC_COSTMODEL=1) + target_link_libraries(TritonToGraph PUBLIC AscendModelAnalysis) +else() + target_compile_definitions(TritonToGraph PRIVATE TRITON_ASCEND_HAS_INPROC_COSTMODEL=0) +endif() + +add_triton_plugin(TritonAscend + ${CMAKE_CURRENT_SOURCE_DIR}/triton_ascend.cc + ${CMAKE_CURRENT_SOURCE_DIR}/ascend_ir.cc + LINK_LIBS + TritonToGraph + TritonToLinalg + BiShengIRScopeDialect + BiShengIRHIVMDialect +) + +target_link_libraries(TritonAscend PRIVATE CommonIRToHIVM CommonIRTransforms) + +option(TRITON_ENABLE_COVERAGE_LLVM_COV "Enable code llvm-cov coverage tool for Ascend plugin " OFF) +if(TRITON_ENABLE_COVERAGE_LLVM_COV) + message(STATUS "Enabling llvm-cov coverage tool flags for TritonAscend") + target_compile_options(TritonAscend PRIVATE + -fprofile-arcs + -ftest-coverage + -O0 + -fprofile-update=atomic + --coverage + ) + target_link_options(TritonAscend PRIVATE + --coverage + -lgcov + ) + # branch coverage + target_compile_definitions(TritonAscend PRIVATE + COVERAGE_ENABLED=1 + ) +endif() + + +# To enable hitest coverage tool +if(TRITON_ENABLE_COVERAGE_HITEST) + set_target_properties(TritonAscend PROPERTIES + RULE_LAUNCH_COMPILE "hitestwrapper" + RULE_LAUNCH_LINK "hitestwrapper" + ) +endif() + +if(TRITON_BUILD_UT) + add_subdirectory(unittest) +endif() + +if(TRITON_BUILD_TD) + add_subdirectory(Triton-distributed-ascend/csrc) +endif() + +if(TRITON_ASCEND_HAS_INPROC_COSTMODEL) + target_compile_definitions(TritonAscend PRIVATE TRITON_ASCEND_HAS_INPROC_COSTMODEL=1) + target_link_libraries(TritonAscend PRIVATE + AscendModelIR + AscendModelTransforms + AscendModelAnalysis + MLIROptLib + MLIRIR + MLIRParser + MLIRPass + MLIRSupport + MLIRTransforms + MLIRFuncDialect + MLIRSCFDialect + MLIRArithDialect + MLIRMathDialect + MLIRTensorDialect + LLVMSupport + ) +else() + target_compile_definitions(TritonAscend PRIVATE TRITON_ASCEND_HAS_INPROC_COSTMODEL=0) +endif() diff --git a/third_party/ascend/ascend_ir.cc b/third_party/ascend/ascend_ir.cc new file mode 100644 index 0000000000..051864697d --- /dev/null +++ b/third_party/ascend/ascend_ir.cc @@ -0,0 +1,1579 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ir.h" +#include "pybind11/pybind11.h" +#include +#include + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "ascend/include/TritonToGraph/ProgramAxisDependenceAnalysis.h" +#include "ascend/include/TritonToGraph/ProgramGridTransform.h" +#include "ascend/include/TritonToGraph/ProgramMappingScalarSpecialization.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" + +#include "mlir/AsmParser/AsmParser.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/Func/Extensions/InlinerExtension.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/AffineExpr.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Types.h" +#include "mlir/Support/LLVM.h" +#include "llvm/IR/Instructions.h" +#include +#include +#include + +using namespace mlir; +namespace py = pybind11; + +struct AscendNPUIROpBuilder : public TritonOpBuilder { + std::string target; + std::string compile_mode; + static constexpr char kTarget910_95[] = "Ascend910_95"; + static constexpr char kTarget950[] = "Ascend950"; + + explicit AscendNPUIROpBuilder(MLIRContext *context, std::string target = "", + std::string compile_mode = "simd") + : TritonOpBuilder(context), target(target), compile_mode(compile_mode) {} + + bool isSimtMode() const { return compile_mode == "simt"; } + + bool is_910_95() const { + // TODO: Use enum instead of strings after enabling HACC in satandalone + // build + constexpr size_t kLen910 = sizeof(kTarget910_95) - 1; + bool match_910 = target.size() >= kLen910 && + target.compare(0, kLen910, kTarget910_95) == 0; + + constexpr size_t kLen950 = sizeof(kTarget950) - 1; + bool match_950 = + target.size() >= kLen950 && target.compare(0, kLen950, kTarget950) == 0; + + return match_910 || match_950; + } +}; + +namespace { +MLIRContext *gDefaultAscendContext = nullptr; + +MLIRContext *resolveContext(const py::object &contextObj) { + if (!contextObj.is_none()) { + return &py::cast(contextObj); + } + if (gDefaultAscendContext) { + return gDefaultAscendContext; + } + throw std::invalid_argument( + "No default MLIR context. Pass context explicitly or call " + "ascend_ir.load_dialects(context) first."); +} + +struct ModeAndPipes { + hivm::SyncBlockModeAttr modeAttr = {}; + hivm::PipeAttr cubePipe = {}; + hivm::PipeAttr vectorPipe = {}; +}; + +hivm::TCoreTypeAttr GetCore(MLIRContext *ctx, llvm::StringRef opName, + llvm::StringRef sender) { + // Decide core type + hivm::TCoreTypeAttr core; + if (sender == "cube") { + if (opName == "sync_block_set") + core = hivm::TCoreTypeAttr::get(ctx, hivm::TCoreType::CUBE); + else + core = hivm::TCoreTypeAttr::get(ctx, hivm::TCoreType::VECTOR); + } else { + if (sender != "vector") { + throw std::runtime_error( + "sync_block_set/wait only supports 'cube' or 'vector' as sender"); + } + if (opName == "sync_block_set") + core = hivm::TCoreTypeAttr::get(ctx, hivm::TCoreType::VECTOR); + else + core = hivm::TCoreTypeAttr::get(ctx, hivm::TCoreType::CUBE); + } + + return core; +} + +void buildSyncBlockOp(AscendNPUIROpBuilder &self, const std::string &opName, + std::string &sender, std::string &receiver, Value id, + hivm::PIPE senderPipe, hivm::PIPE receiverPipe) { + auto *ctx = self.getBuilder().getContext(); + hivm::TCoreTypeAttr coreAttr = GetCore(ctx, opName, sender); + hivm::PipeAttr prodPipe = hivm::PipeAttr::get(ctx, senderPipe); + hivm::PipeAttr consPipe = hivm::PipeAttr::get(ctx, receiverPipe); + const size_t I64 = 64; + auto i64Ty = IntegerType::get(ctx, I64); + Value idI64 = id; + if (!id.getType().isInteger(I64)) { + idI64 = mlir::convertScalarToDtype(self.getBuilder(), id.getLoc(), id, + i64Ty, true); + } + if (opName == "sync_block_set") { + self.create(coreAttr, prodPipe, consPipe, idI64); + } else if (opName == "sync_block_wait") { + self.create(coreAttr, prodPipe, consPipe, idI64); + } else { + throw std::runtime_error("Unsupported operation name for SyncBlockOp"); + } +} + +ModeAndPipes GetSyncBlockModeAndPipes(MLIRContext *ctx, + const std::string &mode) { + hivm::SyncBlockModeAttr modeAttr = {}; + hivm::PipeAttr cubePipe = {}; + hivm::PipeAttr vectorPipe = {}; + + if (mode == "all_cube") { + modeAttr = hivm::SyncBlockModeAttr::get(ctx, hivm::SyncBlockMode::ALL_CUBE); + cubePipe = hivm::PipeAttr::get(ctx, hivm::PIPE::PIPE_ALL); + vectorPipe = hivm::PipeAttr{}; + } else if (mode == "all_vector") { + modeAttr = + hivm::SyncBlockModeAttr::get(ctx, hivm::SyncBlockMode::ALL_VECTOR); + cubePipe = hivm::PipeAttr{}; + vectorPipe = hivm::PipeAttr::get(ctx, hivm::PIPE::PIPE_ALL); + } else if (mode == "all") { + modeAttr = hivm::SyncBlockModeAttr::get(ctx, hivm::SyncBlockMode::ALL); + cubePipe = hivm::PipeAttr::get(ctx, hivm::PIPE::PIPE_ALL); + vectorPipe = hivm::PipeAttr::get(ctx, hivm::PIPE::PIPE_ALL); + } else if (mode == "all_sub_vector") { + modeAttr = + hivm::SyncBlockModeAttr::get(ctx, hivm::SyncBlockMode::ALL_SUB_VECTOR); + cubePipe = hivm::PipeAttr{}; + vectorPipe = hivm::PipeAttr::get(ctx, hivm::PIPE::PIPE_ALL); + } else { + llvm::report_fatal_error( + llvm::StringRef("Invalid sync-block mode: " + mode)); + } + return {modeAttr, cubePipe, vectorPipe}; +} + +// Extend triton.ir.context with a context manager protocol for ascend tests +// and examples, without modifying upstream python/src/ir.cc. +void installTritonContextManager() { + static bool installed = false; + if (installed) { + return; + } + installed = true; + + py::module_ tritonIr = py::module_::import("triton._C.libtriton.ir"); + py::object ctxClass = tritonIr.attr("context"); + if (py::hasattr(ctxClass, "__enter__")) { + return; + } + + const char *patch = R"PY( +import triton._C.libtriton.ir as _triton_ir +if not hasattr(_triton_ir.context, '__enter__'): + def _mlir_context_enter(self): + return self + def _mlir_context_exit(self, exc_type, exc_val, exc_tb): + return False + _triton_ir.context.__enter__ = _mlir_context_enter + _triton_ir.context.__exit__ = _mlir_context_exit +)PY"; + PyGILState_STATE gil = PyGILState_Ensure(); + if (PyRun_SimpleString(patch) != 0) { + PyGILState_Release(gil); + throw std::runtime_error("failed to install MLIRContext context manager"); + } + PyGILState_Release(gil); +} + +} // namespace + +void init_ascend_ir(py::module &&m) { + auto affineExprClass = + py::class_(m, "affine_expr", py::module_local()); + affineExprClass + .def("__str__", + [](AffineExpr self) { + std::string str; + llvm::raw_string_ostream os(str); + self.print(os); + return os.str(); + }) + .def("__repr__", + [](AffineExpr self) { + std::string str; + llvm::raw_string_ostream os(str); + self.print(os); + return ""; + }) + .def("is_symbolic_or_constant", &AffineExpr::isSymbolicOrConstant) + .def("is_pure_affine", &AffineExpr::isPureAffine) + .def("is_function_of_dim", &AffineExpr::isFunctionOfDim) + .def("compose", + [](AffineExpr self, AffineMap map) { return self.compose(map); }) + .def("get_largest_known_divisor", &AffineExpr::getLargestKnownDivisor) + .def("floordiv", [](AffineExpr self, + AffineExpr other) { return self.floorDiv(other); }) + .def("ceildiv", [](AffineExpr self, + AffineExpr other) { return self.ceilDiv(other); }) + .def("mod", + [](AffineExpr self, AffineExpr other) { return self % other; }) + .def("__hash__", + [](AffineExpr self) { + return py::int_(static_cast(mlir::hash_value(self))); + }) + .def("__eq__", [](AffineExpr lhs, AffineExpr rhs) { return lhs == rhs; }) + .def(py::self + py::self) + .def(py::self - py::self) + .def(py::self * py::self) + .def(py::self % py::self); + affineExprClass + .def_static( + "get_constant", + [](int64_t val, py::object contextObj) { + auto *context = resolveContext(contextObj); + return getAffineConstantExpr(val, context); + }, + py::arg("value"), py::arg("context") = py::none()) + .def_static( + "get_dim", + [](uint32_t pos, py::object contextObj) { + auto *context = resolveContext(contextObj); + return getAffineDimExpr(pos, context); + }, + py::arg("pos"), py::arg("context") = py::none()) + .def_static( + "get_symbol", + [](uint32_t pos, py::object contextObj) { + auto *context = resolveContext(contextObj); + return getAffineSymbolExpr(pos, context); + }, + py::arg("pos"), py::arg("context") = py::none()); + + affineExprClass.attr("__doc__") = + "An MLIR affine expression representing a linear combination of " + "dimensions and symbols with a constant offset.\n\n" + "Affine expressions model loop bounds, array indices, and memory " + "access patterns in the Ascend NPU compiler. They support addition, " + "subtraction, multiplication by constants, floor division, ceiling " + "division, and modulo operations.\n\n" + "Create via static methods: get_constant(value), get_dim(pos), " + "get_symbol(pos)."; + + py::class_(m, "affine_constant_expr", + py::module_local()) + .def("get_value", &AffineConstantExpr::getValue) + .attr("__doc__") = + "An affine expression that is a constant integer value. " + "This is the simplest form of an affine expression, " + "representing just a numeric constant."; + py::class_(m, "affine_dim_expr", + py::module_local()) + .def("get_position", &AffineDimExpr::getPosition) + .attr("__doc__") = + "An affine expression representing a single dimension variable. " + "Dimensions typically correspond to loop induction variables."; + py::class_(m, "affine_symbol_expr", + py::module_local()) + .def("get_position", &AffineSymbolExpr::getPosition) + .attr("__doc__") = + "An affine expression representing a single symbol variable. " + "Symbols represent unknown but constant values (e.g., tile sizes)."; + py::class_(m, "affine_binary_op_expr", + py::module_local()) + .def("get_lhs", &AffineBinaryOpExpr::getLHS) + .def("get_rhs", &AffineBinaryOpExpr::getRHS) + .attr("__doc__") = + "An affine expression composed of two sub-expressions combined by " + "an operator (add, sub, mul, mod, floordiv, ceildiv)."; + + auto affineMapClass = + py::class_(m, "affine_map", py::module_local()); + affineMapClass + .def("__str__", + [](AffineMap &self) { + std::string str; + llvm::raw_string_ostream os(str); + self.print(os); + return os.str(); + }) + .def("__repr__", + [](AffineMap &self) { + std::string str; + llvm::raw_string_ostream os(str); + self.print(os); + return ""; + }) + .def("is_identity", &AffineMap::isIdentity) + .def("is_permutation", &AffineMap::isPermutation) + .def("get_num_dims", &AffineMap::getNumDims) + .def("get_num_symbols", &AffineMap::getNumSymbols) + .def("get_num_results", &AffineMap::getNumResults) + .def("is_empty", &AffineMap::isEmpty) + .def("is_single_constant", &AffineMap::isSingleConstant) + .def("is_constant", &AffineMap::isConstant) + .def("get_constant_result", + [](AffineMap &self) -> int64_t { + if (!self.isSingleConstant()) { + throw std::runtime_error( + "affine map is not a single constant map"); + } + return self.getSingleConstantResult(); + }) + .def("get_result", + [](AffineMap &self, uint32_t pos) { + if (pos >= self.getNumResults()) { + throw py::index_error("result index out of range"); + } + return self.getResult(pos); + }) + .def("get_sub_map", + [](AffineMap &self, const std::vector &resultPos) { + return self.getSubMap(resultPos); + }) + .def("replace", + [](AffineMap &self, AffineExpr expr, AffineExpr replacement, + uint32_t numResultDims, uint32_t numResultSymbols) { + return self.replace(expr, replacement, numResultDims, + numResultSymbols); + }) + .def("compose", + [](AffineMap &self, AffineMap map) { return self.compose(map); }) + .def("get_results", + [](AffineMap &self) -> std::vector { + auto results = self.getResults(); + return std::vector(results.begin(), results.end()); + }) + .def("__hash__", + [](AffineMap &self) { + return py::int_(static_cast(mlir::hash_value(self))); + }) + .def("__eq__", [](AffineMap &lhs, AffineMap &rhs) { return lhs == rhs; }) + .def("inverse_permutation", + [](AffineMap &self) -> py::object { + // Validate it's a permutation first + if (!self.isPermutation()) { + throw py::value_error( + "AffineMap must be a valid permutation to compute inverse"); + } + + // Returns AffineMap directly, not a pointer + AffineMap inverse = mlir::inversePermutation(self); + + // Check if result is valid (null AffineMap) + if (!inverse) { + throw py::value_error("Failed to compute inverse permutation"); + } + + return py::cast(inverse); + }) + .def("to_dict", [](AffineMap &self) -> py::dict { + py::list results; + for (AffineExpr result : self.getResults()) { + if (auto dimExpr = dyn_cast(result)) { + results.append(dimExpr.getPosition()); + } else { + std::string exprStr; + llvm::raw_string_ostream os(exprStr); + result.print(os); + results.append(py::str(exprStr)); + } + } + + py::dict ret; + ret["num_dims"] = self.getNumDims(); + ret["num_symbols"] = self.getNumSymbols(); + ret["results"] = std::move(results); + return ret; + }); + affineMapClass + .def_static( + "get", + [](int64_t numDims, int64_t numSymbols, const py::iterable &resultsIn, + py::object contextObj) -> AffineMap { + MLIRContext *context = nullptr; + if (numDims < 0 || numSymbols < 0) { + throw std::invalid_argument( + "num_dims and num_symbols must be non-negative"); + } + llvm::SmallVector results; + for (const auto &item : resultsIn) { + if (py::isinstance(item)) { + auto expr = py::cast(item); + if (!context) { + context = expr.getContext(); + } + results.push_back(expr); + continue; + } + if (py::isinstance(item)) { + if (!context) { + context = resolveContext(contextObj); + } + int64_t pos = py::cast(item); + if (pos < 0 || pos >= numDims) { + throw std::invalid_argument( + "result dim index is out of range for num_dims"); + } + results.push_back(getAffineDimExpr(pos, context)); + continue; + } + throw std::invalid_argument( + "results must contain affine_expr or int dim indices"); + } + if (!context) { + context = resolveContext(contextObj); + } + return AffineMap::get(numDims, numSymbols, results, context); + }, + py::arg("num_dims"), py::arg("num_symbols"), py::arg("result_dims"), + py::arg("context") = py::none()) + .def_static( + "get_identity", + [](int64_t numDims, py::object contextObj) -> AffineMap { + auto *context = resolveContext(contextObj); + if (numDims < 0) { + throw std::invalid_argument("num_dims must be non-negative"); + } + return AffineMap::getMultiDimIdentityMap(numDims, context); + }, + py::arg("num_dims"), py::arg("context") = py::none()) + .def_static( + "get_minor_identity", + [](int64_t dims, int64_t results, py::object contextObj) { + auto *context = resolveContext(contextObj); + if (dims < 0 || results < 0) { + throw std::invalid_argument("dims/results must be non-negative"); + } + return AffineMap::getMinorIdentityMap(dims, results, context); + }, + py::arg("dims"), py::arg("results"), py::arg("context") = py::none()) + .def_static( + "get_empty", + [](py::object contextObj) { + auto *context = resolveContext(contextObj); + return AffineMap::get(0, 0, {}, context); + }, + py::arg("context") = py::none()) + .def_static( + "get_permutation", + [](const std::vector &permutation, py::object contextObj) { + auto *context = resolveContext(contextObj); + return AffineMap::getPermutationMap(permutation, context); + }, + py::arg("permutation"), py::arg("context") = py::none()) + .def_static( + "get_constant", + [](int64_t value, py::object contextObj) { + auto *context = resolveContext(contextObj); + return AffineMap::getConstantMap(value, context); + }, + py::arg("value"), py::arg("context") = py::none()); + + affineMapClass.attr("__doc__") = + "An MLIR affine map representing a mapping from a set of " + "dimensions and symbols to a list of affine expressions.\n\n" + "Affine maps encode transformations like indexing, layout " + "permutations, and memory access patterns. They are the " + "fundamental abstraction for describing data movement and " + "computation placement in the Ascend NPU compiler."; + + py::enum_(m, "AddressSpace", py::module_local()) + .value("L1", hivm::AddressSpace::L1) + .value("UB", hivm::AddressSpace::UB) + .value("L0A", hivm::AddressSpace::L0A) + .value("L0B", hivm::AddressSpace::L0B) + .value("L0C", hivm::AddressSpace::L0C) + .export_values(); + + py::enum_(m, "CoreType", py::module_local()) + .value("CUBE", hivm::TCoreType::CUBE) + .value("VECTOR", hivm::TCoreType::VECTOR) + .value("CUBE_OR_VECTOR", hivm::TCoreType::CUBE_OR_VECTOR) + .value("CUBE_AND_VECTOR", hivm::TCoreType::CUBE_AND_VECTOR) + .export_values(); + + py::enum_(m, "PIPE", py::module_local()) + .value("PIPE_S", hivm::PIPE::PIPE_S) + .value("PIPE_V", hivm::PIPE::PIPE_V) + .value("PIPE_M", hivm::PIPE::PIPE_M) + .value("PIPE_MTE1", hivm::PIPE::PIPE_MTE1) + .value("PIPE_MTE2", hivm::PIPE::PIPE_MTE2) + .value("PIPE_MTE3", hivm::PIPE::PIPE_MTE3) + .value("PIPE_ALL", hivm::PIPE::PIPE_ALL) + .value("PIPE_FIX", hivm::PIPE::PIPE_FIX) + .export_values(); + + py::enum_(m, "SYNC_HINT", py::module_local()) + .value("wait", hivm::SyncEventSlotMacroSync::wait) + .value("set", hivm::SyncEventSlotMacroSync::set) + .value("internal", hivm::SyncEventSlotMacroSync::internal) + .export_values(); + + py::enum_(m, "EVENT", py::module_local()) + .value("EVENT_ID0", hivm::EVENT::EVENT_ID0) + .value("EVENT_ID1", hivm::EVENT::EVENT_ID1) + .value("EVENT_ID2", hivm::EVENT::EVENT_ID2) + .value("EVENT_ID3", hivm::EVENT::EVENT_ID3) + .value("EVENT_ID4", hivm::EVENT::EVENT_ID4) + .value("EVENT_ID5", hivm::EVENT::EVENT_ID5) + .value("EVENT_ID6", hivm::EVENT::EVENT_ID6) + .value("EVENT_ID7", hivm::EVENT::EVENT_ID7) + .export_values(); + + py::enum_(m, "MODE", py::module_local()) + .value("SIMD", hivm::VFMode::SIMD) + .value("SIMT", hivm::VFMode::SIMT) + .value("MIX", hivm::VFMode::MIX) + .export_values(); + + py::enum_(m, "IteratorType", py::module_local()) + .value("Parallel", hivm::IteratorType::kParallel) + .value("Broadcast", hivm::IteratorType::kBroadcast) + .value("Transpose", hivm::IteratorType::kTranspose) + .value("Reduction", hivm::IteratorType::kReduction) + .value("Interleave", hivm::IteratorType::kInterleave) + .value("Deinterleave", hivm::IteratorType::kDeinterleave) + .value("Inverse", hivm::IteratorType::kInverse) + .value("Pad", hivm::IteratorType::kPad) + .value("Concat", hivm::IteratorType::kConcat) + .value("Gather", hivm::IteratorType::kGather) + .value("Cumulative", hivm::IteratorType::kCumulative) + .value("Opaque", hivm::IteratorType::kOpaque) + .export_values(); + + py::enum_(m, "FixpipeDMAMode", py::module_local()) + .value("NZ2DN", hivm::FixpipeDMAMode::NZ2DN) + .value("NZ2ND", hivm::FixpipeDMAMode::NZ2ND) + .value("NZ2NZ", hivm::FixpipeDMAMode::NZ2NZ) + .export_values(); + + py::enum_(m, "FixpipeDualDstMode", + py::module_local()) + .value("NO_DUAL", hivm::FixpipeDualDstMode::NO_DUAL) + .value("COLUMN_SPLIT", hivm::FixpipeDualDstMode::COLUMN_SPLIT) + .value("ROW_SPLIT", hivm::FixpipeDualDstMode::ROW_SPLIT) + .export_values(); + + py::enum_(m, "FixpipePreQuantMode", + py::module_local()) + .value("NO_QUANT", hivm::FixpipePreQuantMode::NO_QUANT) + .value("F322BF16", hivm::FixpipePreQuantMode::F322BF16) + .value("F322F16", hivm::FixpipePreQuantMode::F322F16) + .value("S322I8", hivm::FixpipePreQuantMode::S322I8) + .export_values(); + + py::enum_(m, "FixpipePreReluMode", + py::module_local()) + .value("LEAKY_RELU", hivm::FixpipePreReluMode::LEAKY_RELU) + .value("NO_RELU", hivm::FixpipePreReluMode::NO_RELU) + .value("NORMAL_RELU", hivm::FixpipePreReluMode::NORMAL_RELU) + .value("P_RELU", hivm::FixpipePreReluMode::P_RELU) + .export_values(); + py::enum_(m, "DataLayout", py::module_local()) + .value("nZ", hivm::DataLayout::nZ) + .value("zN", hivm::DataLayout::zN) + .export_values(); + + m.def("load_dialects", [](MLIRContext &context) { + gDefaultAscendContext = &context; + DialectRegistry registry; + registry.insert(); + // Standalone callers also use this loader before running the Ascend + // lowering pipeline. + mlir::func::registerInlinerExtension(registry); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + }); + m.def("get_int_attr", [](OpState &op, std::string &name) -> py::object { + auto ret = op->getAttrOfType(name); + if (!ret) { + return py::none(); + } + return py::cast(ret.getInt()); + }); + m.def("get_program_grid_transforms", [](OpState &op) -> py::object { + Attribute attribute = + op->getAttr(mlir::triton::cfg::kProgramGridTransformsAttr); + if (!attribute) + return py::none(); + FailureOr contract = + mlir::triton::cfg::parseProgramGridTransformContract(attribute); + if (failed(contract)) + throw std::runtime_error("invalid hacc.program_grid_transforms contract"); + + py::dict result; + result["version"] = contract->version; + result["extent_source"] = "runtime_original_grid"; + py::list hiddenExtentAxes; + hiddenExtentAxes.append(0); + hiddenExtentAxes.append(1); + result["hidden_extent_axes"] = std::move(hiddenExtentAxes); + py::list hiddenArgumentOrder; + hiddenArgumentOrder.append("originalGridX"); + hiddenArgumentOrder.append("originalGridY"); + result["hidden_argument_order"] = std::move(hiddenArgumentOrder); + py::list hiddenArgumentTypes; + hiddenArgumentTypes.append("i32"); + hiddenArgumentTypes.append("i32"); + result["hidden_argument_types"] = std::move(hiddenArgumentTypes); + py::list transforms; + for (const mlir::triton::cfg::ProgramGridTransform &transform : + contract->transforms) { + py::dict item; + item["order"] = transform.order; + item["kind"] = "ceil_div"; + item["axis"] = transform.axis; + item["factor"] = transform.factor; + item["persistent_coverage"] = transform.persistentCoverage; + item["grid_stride_abi_verified"] = transform.gridStrideAbiVerified; + transforms.append(std::move(item)); + } + result["transforms"] = std::move(transforms); + return std::move(result); + }); + m.def("get_program_mapping_scalar_specialization", + [](OpState &op) -> py::object { + Attribute attribute = op->getAttr( + mlir::triton::cfg::kProgramMappingScalarSpecializationAttr); + if (!attribute) + return py::none(); + FailureOr + specialization = + mlir::triton::cfg::parseProgramMappingScalarSpecialization( + attribute); + if (failed(specialization)) + throw std::runtime_error( + "invalid hacc.program_mapping_scalar_specialization contract"); + + py::dict result; + result["version"] = specialization->version; + py::list arguments; + for (const mlir::triton::cfg::ProgramMappingScalarArgument &argument : + specialization->arguments) { + py::dict item; + item["index"] = argument.index; + if (specialization->version == + mlir::triton::cfg::kProgramMappingScalarSpecializationVersion) + item["name"] = argument.name; + item["value"] = argument.value; + arguments.append(std::move(item)); + } + result["arguments"] = std::move(arguments); + return std::move(result); + }); + m.def( + "set_program_mapping_scalar_specialization", + [](OpState &op, int64_t version, const py::iterable &arguments) { + auto module = dyn_cast(op.getOperation()); + if (!module) + throw std::invalid_argument("set_program_mapping_scalar_" + "specialization expects an MLIR module"); + mlir::triton::cfg::ProgramMappingScalarSpecialization specialization; + specialization.version = version; + const bool isLegacy = + version == + mlir::triton::cfg::kProgramMappingScalarSpecializationLegacyVersion; + const bool isNamed = + version == + mlir::triton::cfg::kProgramMappingScalarSpecializationVersion; + if (!isLegacy && !isNamed) + throw std::invalid_argument( + "unsupported program_mapping_scalar_specialization version"); + for (const py::handle &rawArgument : arguments) { + if (!py::isinstance(rawArgument) || + py::isinstance(rawArgument)) + throw std::invalid_argument("program_mapping_scalar_specialization " + "argument must be a sequence"); + py::sequence argument = + py::reinterpret_borrow(rawArgument); + const size_t expectedSize = isLegacy ? 2 : 3; + if (argument.size() != expectedSize) + throw std::invalid_argument("program_mapping_scalar_specialization " + "argument has invalid arity"); + uint32_t index = argument[0].cast(); + if (isLegacy) { + specialization.arguments.push_back( + {index, "", argument[1].cast()}); + } else { + specialization.arguments.push_back({index, + argument[1].cast(), + argument[2].cast()}); + } + } + if (failed(mlir::triton::cfg::setProgramMappingScalarSpecialization( + module, specialization))) + throw std::runtime_error( + "invalid hacc.program_mapping_scalar_specialization contract"); + }, + py::arg("module"), py::arg("version"), py::arg("arguments")); + m.def("clear_program_mapping_scalar_specialization", [](OpState &op) { + auto module = dyn_cast(op.getOperation()); + if (!module) + throw std::invalid_argument( + "clear_program_mapping_scalar_specialization expects an MLIR module"); + mlir::triton::cfg::clearProgramMappingScalarSpecialization(module); + }); + m.def( + "analyze_program_axis_dependence", + [](OpState &op) -> py::list { + auto function = llvm::dyn_cast(op.getOperation()); + if (!function) + throw std::invalid_argument( + "analyze_program_axis_dependence expects a tt.func operation"); + + mlir::triton::cfg::ProgramAxisDependenceAnalysis analysis(function); + py::list axes; + for (int32_t axis = 0; axis < 3; ++axis) { + const mlir::triton::cfg::ProgramAxisDependence &facts = + analysis.get(axis); + py::dict result; + result["axis"] = facts.axis; + result["program_id_count"] = facts.programIds.size(); + result["escapes"] = facts.escapes; + result["has_side_effects"] = facts.hasSideEffects; + result["has_unsupported_side_effects"] = + facts.hasUnsupportedSideEffects; + result["reads_num_programs"] = facts.readsNumPrograms; + result["is_independent_axis_transform_candidate"] = + facts.isIndependentAxisTransformCandidate(); + result["is_program_mapping_transform_candidate"] = + facts.isProgramMappingTransformCandidate(); + + py::list closure; + for (Operation *operation : facts.dependenceClosure) + closure.append(operation->getName().getStringRef().str()); + result["dependence_closure"] = std::move(closure); + + py::list reductions; + for (int32_t reductionAxis : facts.reductionAxes) + reductions.append(reductionAxis); + result["reduction_axes"] = std::move(reductions); + + py::list stores; + for (const mlir::triton::cfg::StoreAddressDependence &store : + facts.stores) { + py::dict summary; + summary["pointer_depends_on_axis"] = store.pointerDependsOnAxis; + switch (store.independence) { + case mlir::triton::cfg::StoreAddressIndependence:: + NotProgramDependent: + summary["address_independence"] = "not_program_dependent"; + break; + case mlir::triton::cfg::StoreAddressIndependence::ProvenDisjoint: + summary["address_independence"] = "proven_disjoint"; + break; + case mlir::triton::cfg::StoreAddressIndependence:: + CanonicalDynamicStride: + summary["address_independence"] = "canonical_dynamic_stride"; + break; + case mlir::triton::cfg::StoreAddressIndependence::Unknown: + summary["address_independence"] = "unknown"; + break; + } + stores.append(std::move(summary)); + } + result["stores"] = std::move(stores); + axes.append(std::move(result)); + } + return axes; + }, + py::arg("function"), + "Return conservative per-axis ProgramAxisDependenceAnalysis facts for a " + "tt.func."); + m.def("remove_attr", + [](OpState &op, std::string &name) -> void { op->removeAttr(name); }); + py::class_(m, "str_attr", py::module_local()); + py::class_(m, "array_attr", py::module_local()); + + py::class_( + m, "ascendnpu_ir_builder", py::module_local(), py::dynamic_attr()) + .def(py::init(), + py::arg("context"), py::arg("target") = "", + py::arg("compile_mode") = "simd", + "Create a TritonOpBuilder with optional compile_mode (simt or simd, " + "default: simd)") + .def("is_simt_mode", &AscendNPUIROpBuilder::isSimtMode, + "Check if the compile mode is simt") + .def("get_i64_array_attr", + [](AscendNPUIROpBuilder &self, const std::vector &array) { + return self.getBuilder().getI64ArrayAttr(array); + }) + .def("get_buffer_ty", + [](AscendNPUIROpBuilder &self, std::vector &shape, + Type &elementType, const Attribute &memorySpace) -> Type { + return MemRefType::get(shape, elementType, + MemRefLayoutAttrInterface{}, memorySpace); + }) + .def("get_buffer_ty_with_strides", + [](AscendNPUIROpBuilder &self, std::vector &shape, + Type &elementType, const std::vector &strides, + const Attribute &memorySpace) -> Type { + // create a layout with strides, using dynamic offset + auto layout = StridedLayoutAttr::get( + self.getBuilder().getContext(), ShapedType::kDynamic, strides); + return MemRefType::get(shape, elementType, layout, memorySpace); + }) + .def("create_extract_scalar", + [](AscendNPUIROpBuilder &self, Value &src, + std::vector &indices) -> Value { + llvm::SmallVector arg_indices; + for (const auto &i : indices) { + auto iTy = i.getType(); + if (!iTy.isIndex()) { + auto v = self.create( + self.getBuilder().getIndexType(), i); + arg_indices.push_back(v); + } else { + arg_indices.push_back(i); + } + } + auto ret = self.create(src, arg_indices); + return ret; + }) + .def("create_extract_slice", + [](AscendNPUIROpBuilder &self, Value &ful, + std::vector &offs_vec, std::vector &sizs_vec, + std::vector &strd_vec) -> Value { + llvm::SmallVector offsets; + llvm::SmallVector staticOffsets; + for (const auto &o : offs_vec) { + auto oTy = o.getType(); + if (!oTy.isIndex()) { + auto v = self.create( + self.getBuilder().getIndexType(), o); + offsets.push_back(v); + } else { + offsets.push_back(o); + } + staticOffsets.push_back(ShapedType::kDynamic); + } + llvm::SmallVector sizes; + llvm::SmallVector staticSizes; + llvm::SmallVector retSizes; + for (const auto &s : sizs_vec) { + // auto v = self.create(s); + // sizes.push_back(v); + staticSizes.push_back(s); + retSizes.push_back(s); + } + llvm::SmallVector strides; + llvm::SmallVector staticStrides; + for (const auto &s : strd_vec) { + auto v = self.create(s); + strides.push_back(v); + staticStrides.push_back(ShapedType::kDynamic); + } + auto retTy = RankedTensorType::get( + retSizes, + cast(ful.getType()).getElementType()); + + return self.create( + retTy, ful, offsets, sizes, strides, staticOffsets, + staticSizes, staticStrides); + }) + .def("create_insert_slice", + [](AscendNPUIROpBuilder &self, Value &ful, Value &sub, + std::vector &offs_vec, std::vector &sizs_vec, + std::vector &strd_vec) -> Value { + llvm::SmallVector offsets; + llvm::SmallVector staticOffsets; + for (const auto &o : offs_vec) { + auto oTy = o.getType(); + if (!oTy.isIndex()) { + auto v = self.create( + self.getBuilder().getIndexType(), o); + offsets.push_back(v); + } else { + offsets.push_back(o); + } + staticOffsets.push_back(ShapedType::kDynamic); + } + llvm::SmallVector sizes; + llvm::SmallVector staticSizes; + llvm::SmallVector retSizes; + for (const auto &s : sizs_vec) { + // auto v = self.create(s); + // sizes.push_back(v); + staticSizes.push_back(s); + retSizes.push_back(s); + } + llvm::SmallVector strides; + llvm::SmallVector staticStrides; + for (const auto &s : strd_vec) { + auto v = self.create(s); + strides.push_back(v); + staticStrides.push_back(ShapedType::kDynamic); + } + auto retTy = RankedTensorType::get( + retSizes, + cast(ful.getType()).getElementType()); + auto ret = self.create( + sub, ful, offsets, sizes, strides, staticOffsets, staticSizes, + staticStrides); + return ret; + }) + .def("create_custom_op_for_inter_core_sync", + [](AscendNPUIROpBuilder &self, std::string &op_name, + std::string &mode_or_sender, int id) -> void { + auto args = self.getBuilder().getArrayAttr( + {self.getBuilder().getStringAttr(mode_or_sender), + self.getBuilder().getI32IntegerAttr(id)}); + self.create(op_name, args, ValueRange()); + }) + .def("create_index_select_simd", + [](AscendNPUIROpBuilder &self, Value &src, Value &index, int32_t dim, + std::vector &srcShape, std::vector &srcOffset, + std::vector &readShape, + std::vector &returnShape) -> Value { + auto &builder = self.getBuilder(); + auto loc = self.getLastLoc(); + + // Get element type from source pointer + Type elemType; + if (auto ptrTy = dyn_cast(src.getType())) { + elemType = ptrTy.getPointeeType(); + } else { + llvm::report_fatal_error( + "index_select_simd: src must be pointer type"); + } + + // Create return tensor type + llvm::SmallVector retShape; + for (const auto &s : returnShape) { + retShape.push_back(s); + } + auto retTensorType = RankedTensorType::get(retShape, elemType); + + // Convert srcShape and srcOffset values to index type if needed + llvm::SmallVector srcShapeIndex; + for (auto val : srcShape) { + if (!val.getType().isIndex()) { + val = self.create(builder.getIndexType(), + val); + } + srcShapeIndex.push_back(val); + } + + llvm::SmallVector srcOffsetIndex; + for (auto val : srcOffset) { + if (!val.getType().isIndex()) { + val = self.create(builder.getIndexType(), + val); + } + srcOffsetIndex.push_back(val); + } + + // Create attributes + auto dimAttr = builder.getI32IntegerAttr(dim); + auto readShapeAttr = builder.getDenseI32ArrayAttr(readShape); + + // Create the IndexSelectSimdOp + // Parameter order must match TritonOps.td definition: + // src, index, dim, src_shape, src_offset, read_shape + auto indexSelectSimdOp = + builder.create( + loc, + retTensorType, // result type + src, // src pointer + index, // index tensor + dimAttr, // dim attribute + srcShapeIndex, // src_shape (variadic, index type) + srcOffsetIndex, // src_offset (variadic, index type) + readShapeAttr // read_shape attribute + ); + + return indexSelectSimdOp.getResult(); + }) + .def("create_index_put", + [](AscendNPUIROpBuilder &self, Value &ptr, Value &index, + Value &value, const int32_t dim, const int64_t indexBoundary, + std::vector &endOffset, std::vector &startOffset, + std::vector &dstStride) -> void { + // dim need to be i32 type + auto dimI32Ty = self.getBuilder().getI32Type(); + auto dim_val = self.create(dimI32Ty, dim); + // indexBoundary need to be i64 type + auto BoundI64Ty = self.getBuilder().getI64Type(); + auto bound_val = + self.create(BoundI64Ty, indexBoundary); + + self.create(ptr, index, value, dim_val, + bound_val, endOffset, + startOffset, dstStride); + }) + .def("create_gather_out_to_ub", + [](AscendNPUIROpBuilder &self, Value &src, Value &index, + const int64_t indexBoundary, const int32_t dim, + std::vector &srcStride, std::vector &endOffset, + std::vector &startOffset, + std::optional &other) -> Value { + auto elemTy = + cast(src.getType()).getPointeeType(); + auto idxTy = cast(index.getType()); + auto idxShape = idxTy.getShape(); + std::vector retShape(idxShape.begin(), idxShape.end()); + auto resType = RankedTensorType::get(retShape, elemTy); + + // indexBoundary need to be i64 type + auto BoundI64Ty = self.getBuilder().getI64Type(); + auto bound_val = + self.create(BoundI64Ty, indexBoundary); + // dim need to be i32 type + auto dimI32Ty = self.getBuilder().getI32Type(); + auto dim_val = self.create(dimI32Ty, dim); + return self.create( + resType, src, index, bound_val, dim_val, srcStride, endOffset, + startOffset, other.value_or(Value())); + }) + .def("create_scatter_ub_to_out", + [](AscendNPUIROpBuilder &self, Value &ptr, Value &value, + Value &index, const int64_t indexBoundary, const int32_t dim, + std::vector &dstStride, std::vector &endOffset, + std::vector &startOffset) -> void { + auto idxTy = cast(index.getType()); + + // indexBoundary need to be i64 type + auto BoundI64Ty = self.getBuilder().getI64Type(); + auto bound_val = + self.create(BoundI64Ty, indexBoundary); + // dim need to be i32 type + auto dimI32Ty = self.getBuilder().getI32Type(); + auto dim_val = self.create(dimI32Ty, dim); + + self.create( + ptr, value, index, bound_val, dim_val, dstStride, endOffset, + startOffset); + }) + // conv1d operation + .def( + "create_conv1d", + [](AscendNPUIROpBuilder &self, Value input, Value weight, + py::object bias, int64_t stride, int64_t padding_size, + int64_t dilation, int64_t groups, Type output_type) -> Value { + Value biasValue; + if (!bias.is_none()) { + biasValue = bias.cast(); + } else { + biasValue = Value(); + } + auto &builder = self.getBuilder(); + auto strideAttr = builder.getI64IntegerAttr(stride); + auto paddingSizeAttr = builder.getI64IntegerAttr(padding_size); + auto dilationAttr = builder.getI64IntegerAttr(dilation); + auto groupsAttr = builder.getI64IntegerAttr(groups); + auto op = self.create( + output_type, input, weight, biasValue, strideAttr, + paddingSizeAttr, dilationAttr, groupsAttr); + return op.getResult(); + }, + py::arg("input"), py::arg("weight"), py::arg("bias"), + py::arg("stride"), py::arg("padding_size"), py::arg("dilation"), + py::arg("groups"), py::arg("output_type")) + // dot: D = A * B with per-operand fractal (zN) flags; result type + // inferred from A, B and the flags. + .def( + "create_dot", + [](AscendNPUIROpBuilder &self, Value &a, Value &b, bool fractalA, + bool fractalB, bool fractalC) -> Value { + auto &builder = self.getBuilder(); + auto op = self.create( + a, b, builder.getBoolAttr(fractalA), + builder.getBoolAttr(fractalB), builder.getBoolAttr(fractalC)); + return op.getResult(); + }, + py::arg("a"), py::arg("b"), py::arg("fractal_a"), + py::arg("fractal_b"), py::arg("fractal_c")) + // conv2d operation + .def( + "create_conv2d", + [](AscendNPUIROpBuilder &self, Value input, Value weight, + py::object bias, py::object stride, py::object padding, + py::object dilation, int64_t groups, Type output_type) -> Value { + Value biasValue; + if (!bias.is_none()) { + biasValue = bias.cast(); + } else { + biasValue = Value(); + } + auto &builder = self.getBuilder(); + // Keep ints as IntegerAttr and sequences as DenseI32ArrayAttr. + // padding = [pad_top, pad_bottom, pad_left, pad_right] or a + // 2-element [pad_h, pad_w]. + auto buildConvParamAttr = [&](py::object param, + const std::string &name, + bool allowFour) -> Attribute { + if (py::isinstance(param)) { + return builder.getI64IntegerAttr(param.cast()); + } + py::sequence seq = param.cast(); + size_t len = py::len(seq); + SmallVector values; + for (auto item : seq) + values.push_back(item.cast()); + return builder.getDenseI32ArrayAttr(values); + }; + auto strideAttr = buildConvParamAttr(stride, "stride", false); + auto paddingAttr = buildConvParamAttr(padding, "padding", true); + auto dilationAttr = buildConvParamAttr(dilation, "dilation", false); + auto groupsAttr = builder.getI64IntegerAttr(groups); + auto op = self.create( + output_type, input, weight, biasValue, strideAttr, paddingAttr, + dilationAttr, groupsAttr); + return op.getResult(); + }, + py::arg("input"), py::arg("weight"), py::arg("bias"), + py::arg("stride"), py::arg("padding"), py::arg("dilation"), + py::arg("groups"), py::arg("output_type")) + // Add sort + .def("create_sort", + [](AscendNPUIROpBuilder &self, Value src, int64_t dim, + bool descending) -> Value { + auto &builder = self.getBuilder(); + auto loc = self.getLastLoc(); + + auto dimAttr = builder.getI64IntegerAttr(dim); + auto descendingAttr = builder.getBoolAttr(descending); + + auto op = builder.create(loc, src, dimAttr, + descendingAttr); + + return op->getResult(0); + }) + // Add flip + .def("create_flip", + [](AscendNPUIROpBuilder &self, Value src, int64_t dim) -> Value { + auto &builder = self.getBuilder(); + auto loc = self.getLastLoc(); + + auto dimAttr = builder.getI64IntegerAttr(dim); + + auto op = + builder.create(loc, src, dimAttr); + + return op->getResult(0); + }) + .def("create_tanh", + [](AscendNPUIROpBuilder &self, Value &val) -> Value { + return self.create(val); + }) + // Add an annotation + .def("create_annotation", + [](AscendNPUIROpBuilder &self, Value &ptr, + const std::string &attrKey, Attribute &attrVal) { + auto annotationOp = self.create(ptr); + annotationOp->setAttr(self.getBuilder().getStringAttr(attrKey), + attrVal); + }) + .def("get_int_attr", + [](AscendNPUIROpBuilder &self, int64_t value) -> Attribute { + return IntegerAttr::get(self.getBuilder().getI64Type(), value); + }) + .def("get_type_array_attr", + [](AscendNPUIROpBuilder &self, + const std::vector &array) -> Attribute { + return self.getBuilder().getTypeArrayAttr(array); + }) + .def("get_core_type_attr", + [](AscendNPUIROpBuilder &self, + hivm::TCoreType core_type) -> Attribute { + return self.getBuilder().getAttr(core_type); + }) + .def("get_pipe_attr", + [](AscendNPUIROpBuilder &self, hivm::PIPE pipe) -> Attribute { + return self.getBuilder().getAttr(pipe); + }) + .def("get_event_attr", + [](AscendNPUIROpBuilder &self, hivm::EVENT event) -> Attribute { + return hivm::EventAttr::get(self.getBuilder().getContext(), event); + }) + .def( + "get_sync_event_slot_attr", + [](AscendNPUIROpBuilder &self, py::object setPipe, + py::object waitPipe, hivm::SyncEventSlotMacroSync macroSync, + py::object event) -> Attribute { + auto *ctx = self.getBuilder().getContext(); + hivm::PipeAttr setPipeAttr; + hivm::PipeAttr waitPipeAttr; + hivm::EventAttr eventAttr; + if (!setPipe.is_none()) + setPipeAttr = + hivm::PipeAttr::get(ctx, py::cast(setPipe)); + if (!waitPipe.is_none()) + waitPipeAttr = + hivm::PipeAttr::get(ctx, py::cast(waitPipe)); + if (!event.is_none()) + eventAttr = + hivm::EventAttr::get(ctx, py::cast(event)); + return hivm::SyncEventSlotAttr::get(ctx, setPipeAttr, waitPipeAttr, + macroSync, eventAttr); + }, + py::arg("set_pipe") = py::none(), py::arg("wait_pipe") = py::none(), + py::arg("macro_sync") = hivm::SyncEventSlotMacroSync::wait, + py::arg("event") = py::none()) + .def("get_vf_mode_attr", + [](AscendNPUIROpBuilder &self, hivm::VFMode mode) -> Attribute { + return self.getBuilder().getAttr(mode); + }) + .def("get_iterator_types_attr", + [](AscendNPUIROpBuilder &self, + const std::vector &array) { + auto attrs = llvm::to_vector( + llvm::map_range(array, [&self](hivm::IteratorType type) { + return cast( + self.getBuilder().getAttr(type)); + })); + return self.getBuilder().getArrayAttr(attrs); + }) + .def("get_array_attr", + [](AscendNPUIROpBuilder &self, const std::vector &attrs) + -> Attribute { return self.getBuilder().getArrayAttr(attrs); }) + .def("get_t_core_type_attr_name", + [](AscendNPUIROpBuilder &self) -> std::string { + return hivm::TCoreTypeAttr::name.str(); + }) + .def("get_t_core_type_cube_attr", + [](AscendNPUIROpBuilder &self) -> Attribute { + return hivm::TCoreTypeAttr::get(self.getBuilder().getContext(), + hivm::TCoreType::CUBE); + }) + .def("get_t_core_type_vector_attr", + [](AscendNPUIROpBuilder &self) -> Attribute { + return hivm::TCoreTypeAttr::get(self.getBuilder().getContext(), + hivm::TCoreType::VECTOR); + }) + .def("parse_attr", + [](TritonOpBuilder &self, std::string value) -> Attribute { + auto *ctx = self.getBuilder().getContext(); + // Enable parsing of HACC attributes by allowing unregistered + // dialects. + ctx->allowUnregisteredDialects(); + return mlir::parseAttribute(value, ctx); + }) + .def("get_affine_map_attr", + [](AscendNPUIROpBuilder &self, AffineMap affineMap) -> Attribute { + return AffineMapAttr::get(affineMap); + }) + .def("get_affine_map_array_attr", + [](AscendNPUIROpBuilder &self, + const std::vector &affineMaps) -> Attribute { + auto *ctx = self.getBuilder().getContext(); + llvm::SmallVector attrs; + attrs.reserve(affineMaps.size()); + for (const auto &map : affineMaps) { + attrs.push_back(AffineMapAttr::get(map)); + } + return ArrayAttr::get(ctx, attrs); + }) + .def("get_buffer_ty_with_affine_map", + [](AscendNPUIROpBuilder &self, std::vector &shape, + Type &elementType, AffineMap affineMap, + const Attribute &memorySpace) -> Type { + auto layout = AffineMapAttr::get(affineMap); + return MemRefType::get(shape, elementType, layout, memorySpace); + }) + .def("create_fixpipe", + [](AscendNPUIROpBuilder &self, Value src, py::object dst_obj, + hivm::FixpipeDMAMode dma_mode, + hivm::FixpipeDualDstMode dual_dst_mode, + hivm::FixpipePreQuantMode pre_quant_mode, + hivm::FixpipePreReluMode pre_relu_mode) -> py::object { + if (!dyn_cast(src.getType())) { + llvm_unreachable("src is not of RankedTensorType"); + } + + auto *ctx = self.getBuilder().getContext(); + auto loc = self.getLastLoc(); + + Value dstValue; + bool needCreateDst = dst_obj.is_none(); + + if (needCreateDst) { + auto srcType = dyn_cast(src.getType()); + auto srcShape = srcType.getShape(); + + llvm::SmallVector dstShape(srcShape.begin(), + srcShape.end()); + + if (dual_dst_mode == hivm::FixpipeDualDstMode::ROW_SPLIT) { + if (dstShape.size() >= 1 && dstShape[0] > 0) { + dstShape[0] = dstShape[0] / 2; + } + } else if (dual_dst_mode == + hivm::FixpipeDualDstMode::COLUMN_SPLIT) { + if (dstShape.size() >= 2 && dstShape[1] > 0) { + dstShape[1] = dstShape[1] / 2; + } + } + + auto dstType = + RankedTensorType::get(dstShape, srcType.getElementType()); + auto emptyTensor = self.create( + dstType.getShape(), dstType.getElementType()); + dstValue = emptyTensor.getResult(); + } else { + dstValue = py::cast(dst_obj); + if (!dyn_cast(dstValue.getType())) { + llvm_unreachable("dst is not of ShapedType"); + } + } + + auto dma_mode_attr = + mlir::hivm::FixpipeDMAModeAttr::get(ctx, dma_mode); + auto dual_dst_mode_attr = + mlir::hivm::FixpipeDualDstModeAttr::get(ctx, dual_dst_mode); + auto pre_quant_mode_attr = + mlir::hivm::FixpipePreQuantModeAttr::get(ctx, pre_quant_mode); + auto pre_relu_mode_attr = + mlir::hivm::FixpipePreReluModeAttr::get(ctx, pre_relu_mode); + auto channel_split = BoolAttr::get(ctx, false); + if (needCreateDst) { + return py::cast( + self.create( + mlir::TypeRange{dstValue.getType()}, src, dstValue, + dma_mode_attr, dual_dst_mode_attr, nullptr, + pre_quant_mode_attr, pre_relu_mode_attr, + channel_split) + .getResult(0)); + + } else { + self.create(mlir::TypeRange{}, src, dstValue, + dma_mode_attr, dual_dst_mode_attr, + nullptr, pre_quant_mode_attr, + pre_relu_mode_attr, channel_split); + return py::none(); + } + }) + .def("create_annotation_mark", + [](TritonOpBuilder &self, Value &ptr, const std::string &attrKey, + Attribute &attrVal) { + auto annotationOp = self.create(ptr); + annotationOp->setAttr(self.getBuilder().getStringAttr(attrKey), + attrVal); + }) + .def("create_bind_buffer", + [](TritonOpBuilder &self, Value &src, Value &alloc) -> void { + auto ctx = self.getBuilder().getContext(); + auto bind = StringAttr::get(ctx, "bind_buffer"); + self.create(src, ValueRange{alloc}, + ArrayAttr::get(ctx, bind)); + }) + .def("create_debug_barrier", + [](TritonOpBuilder &self, Value &ptr, const std::string &attrKey, + Attribute &attrVal) { + auto annotationOp = self.create(ptr); + annotationOp->setAttr(self.getBuilder().getStringAttr(attrKey), + attrVal); + }) + .def("create_custom_op", + [](AscendNPUIROpBuilder &self, const std::string &name, + const py::dict &attrs, const std::vector &ins, + const std::vector &outs, + const std::vector &arg_attrs) -> std::vector { + ValueRange inputs{ins}; + ValueRange outputs{outs}; + ValueRange temp_buffers{}; + TypeRange res_types{outputs}; + auto op = self.create(res_types, name, inputs, + outputs, temp_buffers); + for (auto &attr : attrs) { + std::string attr_name = py::cast(attr.first); + Attribute attr_value = py::cast(attr.second); + op->setAttr(attr_name, attr_value); + } + + SmallVector dictAttrs(arg_attrs.size()); + Attribute emptyDict = self.getBuilder().getDictionaryAttr({}); + for (const auto &[idx, attrs] : llvm::enumerate(arg_attrs)) { + if (idx >= op.getNumOperands()) + continue; + + if (attrs.is_none()) { + dictAttrs[idx] = emptyDict; + continue; + } + + llvm::SmallVector namedAttrs; + for (const auto &attr : attrs) { + std::string attr_name = py::cast(attr.first); + Attribute attr_value = py::cast(attr.second); + namedAttrs.push_back(NamedAttribute( + self.getBuilder().getStringAttr(attr_name), attr_value)); + } + + dictAttrs[idx] = self.getBuilder().getDictionaryAttr(namedAttrs); + } + + ArrayAttr arg_attrs_array = + self.getBuilder().getArrayAttr(dictAttrs); + op->setAttr("arg_attrs", arg_attrs_array); + + auto results = op->getResults(); + return std::vector(results.begin(), results.end()); + }) + .def("create_custom_macro_op", + [](AscendNPUIROpBuilder &self, const std::string &name, + const py::dict &attrs, const std::vector &ins, + const std::vector &outs, + const std::vector &arg_attrs) -> std::vector { + ValueRange inputs{ins}; + ValueRange outputs{outs}; + ValueRange temp_buffers{}; + ValueRange syncArgs{}; + TypeRange res_types{outputs}; + auto op = self.create( + res_types, name, inputs, outputs, temp_buffers, syncArgs); + for (auto &attr : attrs) { + std::string attr_name = py::cast(attr.first); + Attribute attr_value = py::cast(attr.second); + op->setAttr(attr_name, attr_value); + } + + SmallVector dictAttrs(arg_attrs.size()); + Attribute emptyDict = self.getBuilder().getDictionaryAttr({}); + for (const auto &[idx, attrs] : llvm::enumerate(arg_attrs)) { + if (idx >= op.getNumOperands()) + continue; + + if (attrs.is_none()) { + dictAttrs[idx] = emptyDict; + continue; + } + + llvm::SmallVector namedAttrs; + for (const auto &attr : attrs) { + std::string attr_name = py::cast(attr.first); + Attribute attr_value = py::cast(attr.second); + namedAttrs.push_back(NamedAttribute( + self.getBuilder().getStringAttr(attr_name), attr_value)); + } + + dictAttrs[idx] = self.getBuilder().getDictionaryAttr(namedAttrs); + } + + ArrayAttr arg_attrs_array = + self.getBuilder().getArrayAttr(dictAttrs); + op->setAttr("arg_attrs", arg_attrs_array); + + auto results = op->getResults(); + return std::vector(results.begin(), results.end()); + }) + .def("create_scope_op", + [](AscendNPUIROpBuilder &self, py::dict &scopeAttrs, + std::vector resultTypes) -> OpState { + llvm::SmallVector attrs; + for (auto item : scopeAttrs) { + std::string key = py::cast(item.first); + Attribute value = py::cast(item.second); + attrs.push_back( + NamedAttribute(self.getBuilder().getStringAttr(key), value)); + } + auto scopeOp = self.create(TypeRange(resultTypes)); + scopeOp->setAttrs(attrs); + return OpState(scopeOp); + }) + .def("scope_return", + [](AscendNPUIROpBuilder &self, + std::vector operands) -> OpState { + return self.create(ValueRange(operands)); + }) + .def("sync_block_set", + [](AscendNPUIROpBuilder &self, std::string &sender, + std::string &receiver, Value id, hivm::PIPE senderPipe, + hivm::PIPE receiverPipe) -> void { + buildSyncBlockOp(self, "sync_block_set", sender, receiver, id, + senderPipe, receiverPipe); + }) + .def("sync_block_wait", + [](AscendNPUIROpBuilder &self, std::string &sender, + std::string &receiver, Value id, hivm::PIPE senderPipe, + hivm::PIPE receiverPipe) -> void { + buildSyncBlockOp(self, "sync_block_wait", sender, receiver, id, + senderPipe, receiverPipe); + }) + .def("get_target_attribute", + [](AscendNPUIROpBuilder &self, + hivm::AddressSpace &addressSpace) -> Attribute { + return hivm::AddressSpaceAttr::get(self.getBuilder().getContext(), + addressSpace); + }) + .def("create_get_sub_vec_id", + [](AscendNPUIROpBuilder &self) -> Value { + auto subBlockIdxOp = self.create(); + auto moduleOp = subBlockIdxOp->getParentOfType(); + auto *ctx = self.getBuilder().getContext(); + // If user explicitly uses sub.block idx, add attribute to module. + // NPU compiler will parse this attribute and disable auto tile and + // bind subblock pass. + moduleOp->setAttr("hivm.disable_auto_tile_and_bind_subblock", + mlir::UnitAttr::get(ctx)); + return subBlockIdxOp; + }) + .def("sync_block_all", + [](AscendNPUIROpBuilder &self, std::string &mode, int id) -> void { + auto *ctx = self.getBuilder().getContext(); + auto [modeAttr, cubePipe, vectorPipe] = + GetSyncBlockModeAndPipes(ctx, mode); + mlir::IndexType indexType = mlir::IndexType::get(ctx); + mlir::IntegerAttr indexAttribute = + mlir::IntegerAttr::get(indexType, static_cast(id)); + self.create( + modeAttr, indexAttribute, mlir::Value{}, cubePipe, vectorPipe); + }) + .def("is_910_95", + [](AscendNPUIROpBuilder &self) -> bool { return self.is_910_95(); }) + .def("create_copy_buffer", + [](AscendNPUIROpBuilder &self, Value src, Value dst) { + self.create(mlir::TypeRange{}, src, dst); + }) + .def("create_copy_tensor", + [](AscendNPUIROpBuilder &self, Value src, Value dst) { + return self + .create(mlir::TypeRange{dst.getType()}, src, dst) + .getResult(0); + }) + .def("create_convert_layout", + [](AscendNPUIROpBuilder &self, Value src, Type memrefType) -> Value { + // src is a memref + // the layout is incorrect (temporarily) + auto *ctx = self.getBuilder().getContext(); + return self + .create( + memrefType, src, + hivm::DataLayoutAttr::get(ctx, hivm::DataLayout::ND), + hivm::DataLayoutAttr::get(ctx, hivm::DataLayout::ND)) + .getResult(); + }); + + installTritonContextManager(); +} diff --git a/third_party/ascend/backend/__init__.py b/third_party/ascend/backend/__init__.py new file mode 100644 index 0000000000..a4d8f3cc7b --- /dev/null +++ b/third_party/ascend/backend/__init__.py @@ -0,0 +1,204 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +__version__ = '3.6.0' + +import logging +from triton._C.libtriton.ascend import ir as ascend_ir + +from .testing import do_bench_npu + + +def _apply_ascend_patch(): + from triton.compiler.code_generator import CodeGenerator + + if not getattr(CodeGenerator, "_ascend_patch_applied", False): + _original_cg_init = CodeGenerator.__init__ + + def _patched_cg_init(self, *args, **kwargs): + """ + Monkey Patch for Ascend: + Injects 'hacc.target' attribute into the module after initilization of module. + """ + _original_cg_init(self, *args, **kwargs) + options = self.builder.options + context = self.context + if hasattr(options, "arch") and options.arch: + try: + builder = ascend_ir.ascendnpu_ir_builder(context, options.arch) + + target_attr_str = f'#hacc.target<"{options.arch}">' + self.module.set_attr("hacc.target", builder.parse_attr(target_attr_str)) + except Exception as e: + logging.warning(f"[Ascend Patch] Failed to set hacc.target: {e}") + + CodeGenerator.__init__ = _patched_cg_init + CodeGenerator._ascend_patch_applied = True + + # Patch compiler.parse to support Ascend-specific IR extensions + # for ir_override and TRITON_KERNEL_OVERRIDE features. + from triton.compiler import compiler as _compiler_module + + if not getattr(_compiler_module, "_ascend_parse_patch_applied", False): + from pathlib import Path as _Path + from triton._C.libtriton import ir as _ir + + # Keep the original community logic intact, adding Ascend extensions + # to the existing text/binary branches. + def _patched_parse(full_name, ext, context): + if ext == "ttir" or ext == "ttgir": + module = _ir.parse_mlir_module(full_name, context) + module.context = context + return module + if ext in ("llir", "ptx", "amdgcn", "ttadapter", "bcmlir"): + return _Path(full_name).read_text() + if ext in ("cubin", "hsaco", "mlirbc", "npubin"): + return _Path(full_name).read_bytes() + + _compiler_module.parse = _patched_parse + _compiler_module._ascend_parse_patch_applied = True + + # Patch TritonSemantic.dot for Ascend-specific HF32 guard and + # max_num_imprecise_acc warning. + from triton.language.semantic import TritonSemantic + + if not getattr(TritonSemantic, "_ascend_dot_patch_applied", False): + _original_dot = TritonSemantic.dot + + def _patched_dot(self, lhs, rhs, acc, input_precision, max_num_imprecise_acc, out_dtype): + """ + Monkey Patch for Ascend: + - HF32 precision only works for fp32 x fp32. + When either input is not fp32, silently fall back + to default precision (ieee). + - Warn when max_num_imprecise_acc is explicitly set, since + Ascend NPU does not support imprecise accumulation. + """ + # Ascend does not support tf32, tf32 is converted to hf32. Avoid tf32 verification interception + if input_precision == "tf32": + input_precision = "hf32" + # Ascend NPU does not support imprecise accumulation. + # Force max_num_imprecise_acc to None so the upstream None + # branch handles it (via max_num_imprecise_acc_default = 0), + # avoiding the fp8 ValueError path which is NVIDIA-only. + if max_num_imprecise_acc is not None: + print("max_num_imprecise_acc in tl.dot is not supported on Ascend yet. " + "Thus it is ignored.") + max_num_imprecise_acc = None + + return _original_dot(self, lhs, rhs, acc, input_precision, max_num_imprecise_acc, out_dtype) + + TritonSemantic.dot = _patched_dot + TritonSemantic._ascend_dot_patch_applied = True + + # Install the unified, non-intrusive dtype interception for operators + # whose dtypes are unsupported on Ascend. The rules live in + # triton.backends.ascend.op_dtype_config, next to this backend. + if not getattr(CodeGenerator, "_ascend_dtype_guard_applied", False): + try: + from triton.backends.ascend.dtype_guard import install_dtype_guard + + install_dtype_guard() + CodeGenerator._ascend_dtype_guard_applied = True + except Exception as e: + logging.warning(f"[Ascend Patch] Failed to install dtype guard: {e}") + + # Hand the math builtins' dtype admission over to the Ascend rules. + # + # ``triton.language.math`` wraps these builtins in ``_check_dtype``, whose + # whitelist describes libdevice on GPUs. It is both redundant and wrong + # here: ``exp`` is listed as ``["fp32", "fp64"]`` while Ascend supports + # fp16 and rejects fp64 (see the ``exp`` rule in op_dtype_config), so + # ``tl.exp`` on an fp16 tensor is refused by the core whitelist before the + # backend ever sees it. The core check is also bypassed for the member-call + # form: ``_tensor_member_fn`` binds the undecorated function onto + # ``tensor``, so ``x.exp()`` already compiles on fp16 while ``tl.exp(x)`` + # does not. + # + # Unwrapping ``_check_dtype`` removes that inconsistent second gate and + # leaves dtype admission to the single Ascend source of truth. dtype_guard + # intercepts both the named-builtin and the member-call form, and keeps + # rejecting every dtype listed as unsupported (fp64 included). Note that + # its decision is a blacklist: ``validate`` only rejects dtypes present in + # ``unsupported``, and the ``supported`` list merely formats the message. + # So for ops whose rule omits fp16 from both lists (``log``, ``sqrt``, + # ``floor``, ``erf``), unwrapping does admit fp16. Add fp16 to those rules' + # ``unsupported`` list if the hardware cannot run them, rather than relying + # on the core whitelist to reject them as a side effect. + # + # Only builtins that have an Ascend rule are unwrapped, so every op keeps + # at least one gate; ``div_rn`` has no rule and therefore keeps the core + # check. + if not getattr(CodeGenerator, "_ascend_math_dtype_unwrapped", False): + try: + import functools + from triton import language as _language + from triton.language import core as _core + from triton.language import math as _math + from triton.backends.ascend.op_dtype_config import RULES_BY_NAME + + def _unwrap_check_dtype(fn): + """Return the builtin with the ``_check_dtype`` layer removed. + + ``math`` builds each op as ``core.builtin(_check_dtype(...)( + _add_math_*_docstr(...)(fn)))``. Only the ``_check_dtype`` frame + is dropped; the ``@core.builtin`` contract (the ``_semantic`` + keyword requirement and the ``TRITON_BUILTIN`` marker the AST + tracer dispatches on) is rebuilt around the inner function. + Builtins without both wrapper layers (``fma``) are left alone. + """ + inner = getattr(fn, "__wrapped__", None) + if inner is None: + return None + inner = getattr(inner, "__wrapped__", None) + if inner is None: + return None + + @functools.wraps(inner) + def unchecked(*args, **kwargs): + if kwargs.get("_semantic") is None: + raise ValueError("Did you forget to add @triton.jit ? " + "(`_semantic` argument must be provided " + "outside of JIT functions.)") + return inner(*args, **kwargs) + + setattr(unchecked, _core.TRITON_BUILTIN, True) + return unchecked + + for _name in sorted(set(dir(_math))): + if f"triton.language.{_name}" not in RULES_BY_NAME: + continue + _original = getattr(_math, _name, None) + if not callable(_original) or not getattr(_original, _core.TRITON_BUILTIN, False): + continue + _unchecked = _unwrap_check_dtype(_original) + if _unchecked is None: + continue + # ``triton.language`` re-exports the same object; rebind both so + # ``tl.exp`` and ``triton.language.math.exp`` stay identical. + setattr(_math, _name, _unchecked) + if getattr(_language, _name, None) is _original: + setattr(_language, _name, _unchecked) + + CodeGenerator._ascend_math_dtype_unwrapped = True + except Exception as e: + logging.warning(f"[Ascend Patch] Failed to defer math dtype checks: {e}") + + +__all__ = ["do_bench_npu"] diff --git a/third_party/ascend/backend/ascend_interpreter.py b/third_party/ascend/backend/ascend_interpreter.py new file mode 100644 index 0000000000..eea5c2500e --- /dev/null +++ b/third_party/ascend/backend/ascend_interpreter.py @@ -0,0 +1,921 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Ascend-specific interpreter builder extensions. + +This module extends the base InterpreterBuilder with Ascend-specific operations +(extension ops) without modifying the public base class. All Ascend-related +features are isolated here and can be extended independently. + +Author: Triton-Ascend Contributors +""" + +import warnings +import contextlib +import numpy as np +import triton.language as tl +from triton.runtime.interpreter import InterpreterBuilder, TensorHandle, ReduceOps, ScanOps, _get_np_dtype, _patch_builtin, _patch_attr, _LangPatchScope +from triton._C.libtriton import interpreter as _interpreter, ir as _ir + + +class AscendReduceOps(ReduceOps): + """ + Ascend reduce operations that override only the apply_impl logic. + All other methods (sum, min_max, generic_reduce, etc.) are inherited from ReduceOps. + """ + + def apply_impl(self, input_param): + if self.combine_fn == tl.standard._argmin_combine_tie_break_left: + return self.min_max(input_param[0], val_reduce_op=np.min, idx_reduce_op=np.argmin) + elif self.combine_fn == tl.standard._argmax_combine_tie_break_left: + return self.min_max(input_param[0], val_reduce_op=np.max, idx_reduce_op=np.argmax) + elif self.combine_fn == tl.standard._elementwise_max: + return self.min_max(input_param[0], val_reduce_op=np.nanmax, idx_reduce_op=None) + # TA add function _elementwise_max_propagate_nan + elif self.combine_fn == tl.standard._elementwise_max_propagate_nan: + return self.min_max(input_param[0], val_reduce_op=np.max, idx_reduce_op=None) + elif self.combine_fn == tl.standard._elementwise_min: + return self.min_max(input_param[0], val_reduce_op=np.nanmin, idx_reduce_op=None) + elif self.combine_fn == tl.standard._sum_combine: + return self.sum(input_param[0]) + else: + # Fall back to the slow mode + return self.generic_reduce(input_param) + + def sum(self, input): + # Align with compiler optimization: promote fp16 to fp32 + data = input.handle.data + if data.dtype == np.float16: + result = np.sum(data.astype(np.float32), axis=self.axis, keepdims=self.keep_dims).astype(np.float16) + else: + result = np.sum(data, axis=self.axis, keepdims=self.keep_dims) + return self.to_tensor(result, input.dtype) + + def generic_reduce(self, input): + original_axis = self.axis + input, axis = self.unravel(input, self.axis) + input_data = [] + output_data = [] + input_shape = input[0].handle.data.shape + output_shape = input_shape[0:axis] + input_shape[axis + 1:] + for arg in input: + input_data.append(arg.handle.data) + output_data.append(np.zeros(output_shape, dtype=arg.handle.data.dtype)) + # Reduce on axis + for i in range(input_data[0].size): + # Recover input_index from i using input_shape + input_index = np.unravel_index(i, input_shape) + output_index = input_index[0:axis] + input_index[axis + 1:] + # For tuple input (value, index), the index input should use local + # index along the reduce axis instead of the stored global index, + # so that the result matches torch.min/torch.max which return + # per-axis local indices. Only apply this conversion for the + # canonical (value, index) two-input form to avoid side effects on + # other custom combine functions. + is_indexed_reduce = len(input_data) == 2 + input_tuple = [] + for ii, d in enumerate(input_data): + if is_indexed_reduce and ii == 1: + # Second input is the index parameter - use local index along the reduce axis + val = input_index[axis] + else: + val = d[input_index] + input_tuple.append(self.to_tensor(val, input[ii].dtype)) + input_tuple = tuple(input_tuple) + if input_index[axis] == 0: + # First element + for j in range(len(output_data)): + output_data[j][output_index] = input_tuple[j].handle.data.item() + else: + acc_tuple = tuple(self.to_tensor(o[output_index], input[oi].dtype) for oi, o in enumerate(output_data)) + combine_fn_ret = self.combine_fn.fn(*acc_tuple, *input_tuple) + acc_tuple = (combine_fn_ret, ) if not isinstance(combine_fn_ret, tuple) else combine_fn_ret + for j in range(len(output_data)): + output_data[j][output_index] = acc_tuple[j].handle.data.item() if isinstance( + acc_tuple[j], tl.core.tensor) else acc_tuple[j] + # Pack output + ret = [] + for i, data in enumerate(output_data): + if self.keep_dims: + if original_axis is not None: + data = np.expand_dims(data, axis) + else: + for _ in range(len(input_shape)): + data = np.expand_dims(data, 0) + + elif original_axis is None: + # Take a scalar + data = data.item() + ret.append(self.to_tensor(data, input[i].dtype)) + return ret + + +class AscendScanOps(ScanOps): + + def cumsum(self, input): + # Align with compiler optimization: promote fp16 to fp32 + data = input.handle.data + if data.dtype == np.float16: + result = np.cumsum(data.astype(np.float32), axis=self.axis).astype(np.float16) + else: + result = np.cumsum(data, axis=self.axis) + return [self.to_tensor(result, dtype=input.dtype)] + + +class _GlobalsProxy: + """Wraps fn.__globals__ (a dict) so _LangPatchScope.set_attr (which uses + getattr/setattr/delattr) can patch directly-imported builtin functions. + + When a kernel does ``from ...extension import gather_out_to_ub``, the name + is bound in fn.__globals__ as a direct reference to the original function. + Patching the source module attribute does not update this binding, so we + need to patch the dict entry itself. + """ + + def __init__(self, d): + object.__setattr__(self, '_d', d) + + def __getattr__(self, name): + try: + return self._d[name] + except KeyError: + raise AttributeError(name) + + def __setattr__(self, name, value): + self._d[name] = value + + def __delattr__(self, name): + try: + del self._d[name] + except KeyError: + pass + + +def _compute_strides(shape): + strides = [1] * len(shape) + for i in range(len(shape) - 2, -1, -1): + strides[i] = strides[i + 1] * shape[i + 1] + return strides + + +class AscendInterpreterBuilder(InterpreterBuilder): + """ + Extended InterpreterBuilder with Ascend-specific extension operations. + + This class inherits from InterpreterBuilder and adds support for: + - get_element (extract_scalar): Extract scalar from tensor using indices + - insert_slice: Insert sub-tensor into full tensor + - extract_slice: Extract slice from tensor + - index_select_simd: SIMD gather operation + - get_sub_vec_id: Get vector core ID for 1:2 ratio emulation + - Synchronization operations: sync_block_set/wait/all + + All extension operations handle both TensorHandle and Python int types + for interpreter mode compatibility. + """ + + def __init__(self) -> None: + super().__init__() + # Sub-vector core ID for simulating 1:2 hardware ratio + self.sub_vec_id = 0 + # Flag to track if sub_vec_id simulation is needed + self._sub_vec_simulation_enabled = False + # Alias so semantic.py can access builder._ascend_builder.create_* + self._ascend_builder = self + + def to_int_val(self, val): + """ + Convert a value (int or TensorHandle) to Python int. + + :param val: Value to convert (int, TensorHandle, or other) + :return: Python integer + """ + if isinstance(val, TensorHandle): + return int(val.data.item()) + return int(val) + + def _patch_lang_ascend(self, fn, scope: _LangPatchScope): + + def _new_range(arg1, arg2=None, step=None, **kwargs): + if step is None: + step = 1 + if arg2 is None: + start, end = 0, arg1 + else: + start, end = arg1, arg2 + return range(start, end, step) + + def _new_reduce(input_param, axis, combine_fn, keep_dims=False, **kwargs): + return AscendReduceOps(axis, combine_fn, keep_dims).apply(input_param) + + def _new_scan(input, axis, combine_fn, reverse=False, **kwargs): + return AscendScanOps(axis, combine_fn, reverse).apply(input) + + @contextlib.contextmanager + def _dummpy_scope(*args, **kwargs): + yield + + scope.set_attr(tl.extra.cann.extension, "scope", _dummpy_scope) + scope.set_attr(tl.extra.cann.extension, "parallel", _new_range) + scope.set_attr(tl, "reduce", _new_reduce) + scope.set_attr(tl.core, "reduce", _new_reduce) + scope.set_attr(tl, "associative_scan", _new_scan) + scope.set_attr(tl.core, "associative_scan", _new_scan) + + def patch_extensions(self, fn, scope: _LangPatchScope): + """ + Patch Ascend extension modules for the given function. + + This method handles all Ascend-specific extension module patching, + including CANN extensions and any other extension modules found in + the function's global namespace. + + :param fn: The kernel function to patch extensions for + :param scope: The language patch scope to use for patching + """ + self._patch_lang_ascend(fn, scope) + + # Patch all modules in fn's globals that might be extension modules + # and directly-imported builtin functions (e.g. `from ...extension import gather_out_to_ub`) + globals_proxy = _GlobalsProxy(fn.__globals__) + for name, value in list(fn.__globals__.items()): + if value is None: + continue + try: + if tl.core.is_builtin(value): + # Direct import of a builtin function: patch the dict entry + # itself so the kernel resolves to the interpreter-patched version. + _patch_attr(globals_proxy, name, value, self, scope) + elif hasattr(value, '__name__') and 'extension' in str(value.__name__): + _patch_builtin(value, self, scope) + # Also try patching any module-like object that might have builtin functions + # Skip non-triton modules (torch/numpy/etc.) to avoid inspect.getmembers + # side-effects on C extensions that can cause heap corruption. + elif (hasattr(value, '__dict__') and not isinstance(value, type) and hasattr(value, '__name__') + and 'triton' in str(value.__name__)): + try: + _patch_builtin(value, self, scope) + except Exception: + pass + except Exception: + pass + + # Also try importing extension directly as fallback + try: + import triton.language.extra.cann.extension as extension + _patch_builtin(extension, self, scope) + except (ImportError, AttributeError): + # Extension module not available (e.g., non-Ascend backend) + pass + + def execute_with_sub_vec_simulation(self, fn, args, grid): + """ + Execute function with optional 1:2 sub-vector core simulation. + + Sub-vector simulation is only activated when create_get_sub_vec_id() is + actually called during execution. This avoids unnecessary double execution + for code that doesn't use sub_vec_id functionality. + + :param fn: The kernel function to execute + :param args: Function arguments + :param grid: Grid dimensions (nx, ny, nz) + """ + # Reset simulation flag at the beginning of each execution + self._sub_vec_simulation_enabled = False + self.sub_vec_id = 0 + + # First, try a single execution to see if sub_vec_id is used + for x in range(grid[0]): + for y in range(grid[1]): + for z in range(grid[2]): + self.set_grid_idx(x, y, z) + fn(**args) + + # If sub_vec_id was accessed during execution, run again with sub_vec_id=1 + if self._sub_vec_simulation_enabled: + self.sub_vec_id = 1 + for x in range(grid[0]): + for y in range(grid[1]): + for z in range(grid[2]): + self.set_grid_idx(x, y, z) + fn(**args) + + # ======================================================================== + # Extension ops for Ascend + # ======================================================================== + + def create_extract_scalar(self, tensor_handle, indices): + """ + Extract a scalar from a tensor using indices (equivalent to get_element). + + Handles mixed types: Python int (from loops) and TensorHandle (from other ops). + + :param tensor_handle: The tensor to extract from (TensorHandle) + :param indices: List of scalar indices (can be TensorHandle or Python int) + :return: Scalar value as TensorHandle + """ + # Convert indices from TensorHandle or Python int to integers + index_values = [] + for idx in indices: + if isinstance(idx, int): + # Python int passed directly (e.g., from loop counter) + index_values.append(idx) + elif isinstance(idx, TensorHandle): + # Interpreter TensorHandle + index_values.append(int(idx.data.item()) if hasattr(idx.data, 'item') else int(idx.data)) + else: + # Fallback: try to extract data + index_values.append( + int(idx.data.item()) if hasattr(idx, 'data') and hasattr(idx.data, 'item') else + int(idx.data) if hasattr(idx, 'data') else int(idx)) + + # Extract the scalar value + scalar_data = tensor_handle.data[tuple(index_values)] + return TensorHandle(np.array([scalar_data]), tensor_handle.dtype.scalar) + + def create_insert_slice(self, full_tensor, sub_tensor, offsets, sizes, strides): + """ + Insert a sub-tensor into a full tensor at specified offsets. + + Handles mixed types: Python int and TensorHandle for offsets. + + :param full_tensor: The full tensor (destination, TensorHandle) + :param sub_tensor: The sub-tensor to insert (TensorHandle) + :param offsets: List of offset TensorHandle objects or Python ints + :param sizes: List of size integers + :param strides: List of stride integers + :return: Modified tensor with sub_tensor inserted (TensorHandle) + """ + result = full_tensor.data.copy() + + # Convert offsets from TensorHandle or Python int to integers + offset_values = [] + for off in offsets: + if isinstance(off, int): + # Python int passed directly + offset_values.append(off) + elif isinstance(off, TensorHandle): + # Interpreter TensorHandle + offset_values.append(int(off.data.item()) if hasattr(off.data, 'item') else int(off.data)) + else: + # Fallback + offset_values.append( + int(off.data.item()) if hasattr(off, 'data') and hasattr(off.data, 'item') else + int(off.data) if hasattr(off, 'data') else int(off)) + + # Build slices for insertion + slices = [] + for i, (offset, size, stride) in enumerate(zip(offset_values, sizes, strides)): + end = offset + size * stride + if stride == 1: + slices.append(slice(offset, end)) + else: + slices.append(slice(offset, end, stride)) + + # Insert the sub-tensor + result[tuple(slices)] = sub_tensor.data + + return TensorHandle(result, full_tensor.dtype.scalar) + + def create_extract_slice(self, full_tensor, offsets, sizes, strides): + """ + Extract a slice from a full tensor. + + Handles mixed types: Python int and TensorHandle for offsets. + + :param full_tensor: The full tensor (TensorHandle) + :param offsets: List of offset TensorHandle objects or Python ints + :param sizes: List of size integers + :param strides: List of stride integers + :return: Extracted sub-tensor (TensorHandle) + """ + # Convert offsets from TensorHandle or Python int to integers + offset_values = [] + for off in offsets: + if isinstance(off, int): + # Python int passed directly + offset_values.append(off) + elif isinstance(off, TensorHandle): + # Interpreter TensorHandle + offset_values.append(int(off.data.item()) if hasattr(off.data, 'item') else int(off.data)) + else: + # Fallback + offset_values.append( + int(off.data.item()) if hasattr(off, 'data') and hasattr(off.data, 'item') else + int(off.data) if hasattr(off, 'data') else int(off)) + + # Build slices for extraction + slices = [] + for i, (offset, size, stride) in enumerate(zip(offset_values, sizes, strides)): + end = offset + size * stride + if stride == 1: + slices.append(slice(offset, end)) + else: + slices.append(slice(offset, end, stride)) + + # Extract the slice + extracted = full_tensor.data[tuple(slices)] + + return TensorHandle(extracted, full_tensor.dtype.scalar) + + def create_index_select_simd(self, src_ptr, index_tensor, dim, src_shape, src_offset, read_shape, result_shape): + """ + SIMD index_select operation (gather with indices along a dimension). + + This is a hardware-accelerated gather operation that selects elements + from a tensor using a set of indices along a specified dimension. + + :param src_ptr: Source tensor pointer (TensorHandle), just ptr address, not value + :param index_tensor: 1D tensor of indices (TensorHandle or array) + :param dim: Dimension to select from (int) + :param src_shape: List of source shape (int or TensorHandle) + :param src_offset: List of source offset (int or TensorHandle) + :param read_shape: List of read shape (int or TensorHandle) + :param result_shape: List of result shape (int or TensorHandle) + :return: Result tensor with selected indices (TensorHandle) + """ + # Convert src_shape, src_offset, read_shape to integers + src_shape_vals = [self.to_int_val(s) for s in src_shape] + src_offset_vals = [self.to_int_val(s) if s != -1 else -1 for s in src_offset] + read_shape_vals = [self.to_int_val(r) if r != -1 else -1 for r in read_shape] + result_shape_vals = [self.to_int_val(r) for r in result_shape] + + # Get index values - handle both array and TensorHandle + if isinstance(index_tensor, TensorHandle): + indices = index_tensor.data.flatten() + else: + indices = np.asarray(index_tensor).flatten() + + # Ensure indices are integers + if indices.dtype not in [np.int32, np.int64]: + indices = indices.astype(np.int32) + + # Element type + dtype_tt = src_ptr.get_element_ty() + dtype_np = _get_np_dtype(dtype_tt) + src_strides = _compute_strides(src_shape_vals) + base_addr = int(src_ptr.data.item()) + + # Create result tensor + result = np.empty(result_shape_vals, dtype=dtype_np) + + # Perform index_select: for each index, read the specified data + for out_idx, in_idx in enumerate(indices): + in_idx = int(in_idx) + # Generate all coordinates in the tile + ranges = [] + for d in range(len(src_shape_vals)): + if d == dim: + ranges.append([in_idx]) + else: + offset = src_offset_vals[d] if src_offset_vals[d] != -1 else 0 + read_size = read_shape_vals[d] if read_shape_vals[d] != -1 else src_shape_vals[d] + # Clamp to valid range + offset = max(0, min(offset, src_shape_vals[d] - 1)) + read_size = min(read_size, src_shape_vals[d] - offset) + ranges.append(list(range(offset, offset + read_size))) + from itertools import product + coords = list(product(*ranges)) + + # Compute address for each element in the tile + addresses = [] + for coord in coords: + offset = sum(coord[i] * src_strides[i] for i in range(len(coord))) + addr = base_addr + offset * np.dtype(dtype_np).itemsize + addresses.append(addr) + # load data + addr_array = np.array(addresses, dtype=np.uint64) + mask_array = np.ones_like(addr_array, dtype=bool) + other_array = np.zeros_like(addr_array, dtype=dtype_np) + tile_data = _interpreter.load(addr_array, mask_array, other_array, dtype_np) + # Reshape tile_data to match read_shape with dim=1 at dim + tile_shape = [] + for d in range(len(src_shape_vals)): + if d == dim: + tile_shape.append(1) + else: + offset = src_offset_vals[d] + read_size = read_shape_vals[d] + offset = max(0, min(offset, src_shape_vals[d] - 1)) + read_size = min(read_size, src_shape_vals[d] - offset) + tile_shape.append(read_size) + tile_data = tile_data.reshape(tile_shape) + + # Build result slice + result_slices = [] + for d in range(len(result_shape_vals)): + if d == dim: + result_slices.append(slice(out_idx, out_idx + 1)) + else: + result_slices.append(slice(None)) + result[tuple(result_slices)] = tile_data + + return TensorHandle(result, dtype_tt) + + def create_get_sub_vec_id(self): + """ + Get the Vector Core index on the AI Core. + + In Interpreter mode, simulate multiple vector cores by maintaining + a sub_vec_id counter. This is used for 1:2 hardware ratio emulation + where different vector cores process different partitions of the data. + + The first call to this method enables sub_vec_simulation, causing + the kernel to be executed twice (once for each sub_vec_id value). + + :return: Vector Core ID as TensorHandle (int64, scalar) + """ + # Enable sub_vec_id simulation when this method is called + self._sub_vec_simulation_enabled = True + + # Return the current sub_vec_id + vec_id = np.int64(self.sub_vec_id) + return TensorHandle(np.array([vec_id], dtype=np.int64), tl.int64) + + def sync_block_set(self, sender, receiver, event_id, sender_pipe_value, receiver_pipe_value): + """ + Set synchronization event between compute and vector units. + + In Interpreter mode, this is a no-op since we execute single-threaded. + Synchronization is not needed in CPU emulation. + + :param sender: Source unit ("cube" or "vector") + :param receiver: Destination unit ("cube" or "vector") + :param event_id: Event ID (TensorHandle) + :param sender_pipe_value: Sender pipe value + :param receiver_pipe_value: Receiver pipe value + """ + # No-op in interpreter mode: single-threaded execution doesn't need sync + pass + + def sync_block_wait(self, sender, receiver, event_id, sender_pipe_value, receiver_pipe_value): + """ + Wait for synchronization event between compute and vector units. + + In Interpreter mode, this is a no-op since we execute single-threaded. + Synchronization is not needed in CPU emulation. + + :param sender: Source unit ("cube" or "vector") + :param receiver: Destination unit ("cube" or "vector") + :param event_id: Event ID (TensorHandle) + :param sender_pipe_value: Sender pipe value + :param receiver_pipe_value: Receiver pipe value + """ + # No-op in interpreter mode: single-threaded execution doesn't need sync + pass + + def sync_block_all(self, mode, event_id): + """ + Synchronize all compute or vector units globally. + + In Interpreter mode, this is a no-op since we execute single-threaded. + Synchronization is not needed in CPU emulation. + + :param mode: Sync mode ("all_cube", "all_vector", "all", "all_sub_vector") + :param event_id: Event ID (int, constexpr, or TensorHandle) + """ + # No-op in interpreter mode: single-threaded execution doesn't need sync + pass + + def get_int1_ty(self): + return tl.int1 + + def get_all_ones_value(self, tt_type): + np_type = _get_np_dtype(tt_type) + if "int" in np_type.name: + return TensorHandle(np.full(1, -1, dtype=np_type), tt_type.scalar) + elif np_type == np.bool_: + return TensorHandle(np.full(1, True, dtype=np_type), tt_type.scalar) + else: + raise TypeError(f"unsupported type {tt_type}") + + def is_simt_mode(self): + return False + + def create_sort(self, ptr_data, dim: int, descending: bool): + ndim = ptr_data.data.ndim + norm_dim = dim if dim >= 0 else dim + ndim + if not (0 <= norm_dim < ndim): + raise IndexError(f"Dimension out of range(expected to be in range of [{-ndim}, {ndim - 1}], but got {dim})") + + if descending: + sorted_asc = np.sort(ptr_data.data, axis=norm_dim) + sorted_desc = np.flip(sorted_asc, axis=norm_dim) + return TensorHandle(sorted_desc, ptr_data.dtype.scalar) + else: + return TensorHandle(np.sort(ptr_data.data, axis=norm_dim), ptr_data.dtype.scalar) + + def create_flip(self, ptr_data, dim): + ndim = ptr_data.data.ndim + norm_dim = dim if dim >= 0 else dim + ndim + if not (0 <= norm_dim < ndim): + raise IndexError(f"Dimension out of range(expected to be in range of [{-ndim}, {ndim - 1}], but got {dim})") + return TensorHandle(np.flip(ptr_data.data, axis=norm_dim), ptr_data.dtype.scalar) + + def create_gather_out_to_ub(self, src_ptr, index_tensor, index_boundary, dim, src_stride, end_offset, start_offset, + other=None): + # Convert src_stride, start_offset, end_offset to integers + src_stride_vals = [self.to_int_val(s) for s in src_stride] + start_offset_vals = [self.to_int_val(s) for s in start_offset] + + # Element type + dtype_tt = src_ptr.get_element_ty() + dtype_np = _get_np_dtype(dtype_tt) + element_size = np.dtype(dtype_np).itemsize + base_addr = int(src_ptr.data.item()) + index_shape = index_tensor.data.shape + index_rank = len(index_shape) + total_elements = np.prod(index_shape) + + # Generate coordinates + all_coords = [] + for idx in range(total_elements): + coord = np.unravel_index(idx, index_shape) + all_coords.append(coord) + + # Compute the source tensor coordinates for each position in all_coords + src_coords = [] + for coord in all_coords: + src_coord = [] + for d in range(index_rank): + if d == dim: + index_value = index_tensor.data[coord] + if index_value >= index_boundary: + src_coord.append(-1) + else: + src_coord.append(start_offset_vals[d] + index_value) + else: + src_coord.append(start_offset_vals[d] + coord[d]) + src_coords.append(src_coord) + + # Compute address and mask + addresses = [] + valid_mask = [] + for _, src_coord in enumerate(src_coords): + if -1 in src_coord: + addresses.append(0) + valid_mask.append(False) + else: + offset = 0 + for d in range(index_rank): + offset += src_coord[d] * src_stride_vals[d] + address = base_addr + offset * element_size + addresses.append(address) + valid_mask.append(True) + + addr_array = np.array(addresses, dtype=np.uint64) + mask_array = np.array(valid_mask, dtype=bool) + + # Create other value array + if other is not None: + if isinstance(other, TensorHandle): + other_value = other.data.item() + else: + other_value = other + other_array = np.full(addr_array.shape, other_value, dtype=dtype_np) + else: + other_array = np.zeros(addr_array.shape, dtype=dtype_np) + + # Load data + flat_result = _interpreter.load(addr_array, mask_array, other_array, dtype_np) + result = flat_result.reshape(index_shape) + return TensorHandle(result, dtype_tt) + + def create_scatter_ub_to_out(self, dst_ptr, value_tensor, index_tensor, index_boundary, dim, dst_stride, end_offset, + start_offset): + # Convert dst_stride, start_offset, end_offset to integers + dst_stride_vals = [self.to_int_val(s) for s in dst_stride] + start_offset_vals = [self.to_int_val(s) for s in start_offset] + + # Element type + dtype_tt = dst_ptr.get_element_ty() + dtype_np = _get_np_dtype(dtype_tt) + element_size = np.dtype(dtype_np).itemsize + base_addr = int(dst_ptr.data.item()) + + index_shape = index_tensor.data.shape + index_rank = len(index_shape) + total_elements = np.prod(index_shape) + flat_values = value_tensor.data.flatten() + + # Generate coordinates + all_coords = [] + for idx in range(total_elements): + coord = np.unravel_index(idx, index_shape) + all_coords.append(coord) + + # Compute address and mask + addresses = [] + valid_mask = [] + for _, coord in enumerate(all_coords): + index_value = index_tensor.data[coord] + if index_value >= index_boundary: + addresses.append(0) + valid_mask.append(False) + else: + dst_coord = [] + for d in range(index_rank): + if d == dim: + dst_coord.append(start_offset_vals[d] + index_value) + else: + dst_coord.append(start_offset_vals[d] + coord[d]) + offset = 0 + for d in range(index_rank): + offset += dst_coord[d] * dst_stride_vals[d] + address = base_addr + offset * element_size + addresses.append(address) + valid_mask.append(True) + + addr_array = np.array(addresses, dtype=np.uint64) + mask_array = np.array(valid_mask, dtype=bool) + + _interpreter.store(addr_array, flat_values, mask_array) + + def create_index_put(self, dst_ptr, index_tensor, value_tensor, dim, index_boundary, end_offset, start_offset, + dst_stride): + # Convert dst_stride, start_offset, end_offset_ to integers + dst_stride_vals = [self.to_int_val(s) for s in dst_stride] + start_offset_vals = [self.to_int_val(s) for s in start_offset] + end_offset_vals = [self.to_int_val(s) for s in end_offset] + + # Element type + dtype_tt = dst_ptr.get_element_ty() + dtype_np = _get_np_dtype(dtype_tt) + element_size = np.dtype(dtype_np).itemsize + base_addr = int(dst_ptr.data.item()) + + value_shape = value_tensor.data.shape + value_rank = len(value_shape) + + flat_values = value_tensor.data.flatten() + total_elements = flat_values.size + + # Generate coordinates + all_coords = [] + for idx in range(total_elements): + coord = np.unravel_index(idx, value_shape) + all_coords.append(coord) + + read_ranges = [] + for d in range(value_rank): + start = start_offset_vals[d] + end = end_offset_vals[d] + read_ranges.append((start, end)) + + #Compute address + addresses = [] + valid_mask = [] + values_to_store = [] + for i, coord in enumerate(all_coords): + index_pos = coord[dim] + index_value = index_tensor.data[index_pos] + if index_value >= index_boundary: + addresses.append(0) + valid_mask.append(False) + else: + dst_coord = [] + for d in range(value_rank): + if d == dim: + dst_coord.append(index_value) + else: + dst_coord.append(start_offset_vals[d] + coord[d]) + offset = 0 + for d in range(value_rank): + offset += dst_coord[d] * dst_stride_vals[d] + address = base_addr + offset * element_size + addresses.append(address) + values_to_store.append(flat_values[i]) + valid_mask.append(True) + + addr_array = np.array(addresses, dtype=np.uint64) + mask_array = np.array(valid_mask, dtype=bool) + values_array = np.array(values_to_store, dtype=dtype_np) + + _interpreter.store(addr_array, values_array, mask_array) + + def get_bool_attr(self, val): + return bool(val) + + def get_unit_attr(self): + return None # None valule in compile_hint return uint + + def get_int32_attr(self, val): + return int(val) + + def get_str_attr(self, val): + return str(val) + + def get_i64_array_attr(self, val): + return [int(x) for x in val] + + def create_annotation_mark(self, ptr_data, hint_name: str, hint_val): + if hint_name == "overflow_mode": + raise ValueError("overflow_mode is not supported in interpreter mode, may have accuracy issues") + else: + warnings.warn(f"compile_hint '{hint_name}' is not supported in interpreter mode, just pass it", UserWarning, + stacklevel=2) + + def create_idiv(self, lhs, rhs): + # NPU hardware saturates on integer division by zero: + # positive // 0 → dtype max (acts as +inf) + # negative // 0 → dtype min (acts as -inf) + # 0 // 0 → 0 + # numpy returns 0 for all these cases. Match NPU behaviour. + result = (lhs.data - np.fmod(lhs.data, rhs.data)) // rhs.data + if lhs.data.dtype.kind in ('i', 'u'): + zero_mask = (rhs.data == 0) + if np.any(zero_mask): + result = result.copy() + dt_info = np.iinfo(lhs.data.dtype) + pos_mask = zero_mask & (lhs.data > 0) + neg_mask = zero_mask & (lhs.data < 0) + if np.any(pos_mask): + result[pos_mask] = dt_info.max + if np.any(neg_mask): + result[neg_mask] = dt_info.min + return TensorHandle(result, lhs.dtype.scalar) + + def create_histogram(self, data, bins, mask): + # Override to ensure histogram result is always int32, regardless of + # input dtype (base impl uses input dtype for weights, causing int64 + # results that fail TensorHandle validation for int32 return type). + if mask is None: + mask = TensorHandle(np.ones_like(data.data, dtype=bool), tl.int1) + dummy_weights = np.ones_like(data.data, dtype=np.int32) + masked_data = np.where(mask.data, data.data, np.zeros_like(data.data)) + histogram = np.histogram(masked_data, bins=bins, range=(0, bins), weights=dummy_weights)[0] + histogram[0] -= np.logical_not(mask.data).sum() + return TensorHandle(histogram.astype(np.int32), tl.int32) + + def create_atomic_rmw(self, rmwOp, ptr, val, mask, sem, scope): + """ + Override create_atomic_rmw to support float32/float64 MAX/MIN. + + The community C++ interpreter only instantiates MAX/MIN over int32/int64 + (and UMAX/UMIN over uint32/uint64), so We reuse the GPU triton trick : bitcast the float + to int/uint of the same width and dispatch onto the existing integer atomic ops. + When the intrusive modification of atomic_max/atomic_min op is fallback, this function can be deleted. + """ + + sca_ty = val.dtype.scalar + is_float_max = rmwOp == _ir.ATOMIC_OP.MAX and sca_ty in {tl.float32, tl.float64} + is_float_min = rmwOp == _ir.ATOMIC_OP.MIN and sca_ty in {tl.float32, tl.float64} + + if not (is_float_max or is_float_min): + return super().create_atomic_rmw(rmwOp, ptr, val, mask, sem, scope) + + # Map float dtype to int/uint (numpy + triton) of the same bit-width. + if sca_ty == tl.float32: + int_np, uint_np = np.int32, np.uint32 + triton_int_type, triton_uint_type = tl.int32, tl.uint32 + else: # tl.float64 + int_np, uint_np = np.int64, np.uint64 + triton_int_type, triton_uint_type = tl.int64, tl.uint64 + + # Bitcast float val/ptr to int / uint (zero-copy view; ascontiguousarray + # guards against non-C-contiguous inputs which would break .view()). + int_val = TensorHandle(np.ascontiguousarray(val.data).view(int_np), triton_int_type) + uint_val = TensorHandle(np.ascontiguousarray(val.data).view(uint_np), triton_uint_type) + int_ptr = TensorHandle(ptr.data, tl.pointer_type(triton_int_type, 1)) + uint_ptr = TensorHandle(ptr.data, tl.pointer_type(triton_uint_type, 1)) + + # Split mask by sign of val (matches GPU semantic._signbit: 1 == negative). + # np.signbit handles NaN (signbit=0 -> treated as positive, matching IEEE 754). + is_negative = np.signbit(val.data) + is_positive = ~is_negative + pos_mask = TensorHandle(np.logical_and(mask.data, is_positive), mask.dtype) + neg_mask = TensorHandle(np.logical_and(mask.data, is_negative), mask.dtype) + + # Pick the integer ops that preserve float ordering. + if is_float_max: + pos_op, neg_op = _ir.ATOMIC_OP.MAX, _ir.ATOMIC_OP.UMIN + else: # MIN + pos_op, neg_op = _ir.ATOMIC_OP.MIN, _ir.ATOMIC_OP.UMAX + + # Two disjoint atomic ops (pos_mask & neg_mask do not overlap). + pos_ret = super().create_atomic_rmw(pos_op, int_ptr, int_val, pos_mask, sem, scope) + neg_ret = super().create_atomic_rmw(neg_op, uint_ptr, uint_val, neg_mask, sem, scope) + + pos_ret_int = np.ascontiguousarray(pos_ret.data).view(int_np) + neg_ret_int = np.ascontiguousarray(neg_ret.data).view(int_np) + combined_int = np.where(is_positive, pos_ret_int, neg_ret_int) + + result = np.ascontiguousarray(combined_int).view(_get_np_dtype(sca_ty)) + return TensorHandle(result, sca_ty) diff --git a/third_party/ascend/backend/backend_register.py b/third_party/ascend/backend/backend_register.py new file mode 100644 index 0000000000..183b9ab792 --- /dev/null +++ b/third_party/ascend/backend/backend_register.py @@ -0,0 +1,335 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import importlib.util +import os +from typing import Callable, Dict + + +class BackendStrategyRegistry: + + def __init__(self): + self.strategies: Dict[str:Dict[str, Callable]] = {} + + def register(self, category: str, method: str): + + def decorator(func: Callable): + if category not in self.strategies: + self.strategies[category] = {} + if method in self.strategies[category]: + raise ValueError(f"Strategy {method} already registered") + self.strategies[category][method] = func + return func + + return decorator + + def execute_func(self, category, method, *args, **kwargs): + if category not in self.strategies: + raise ValueError(f"Strategy {category} not registered") + if method not in self.strategies[category]: + raise ValueError(f"Strategy {method} not registered") + return self.strategies[category][method](*args, **kwargs) + + def list_categories(self): + return list(self.strategies.keys()) + + def list_methods(self, category): + if category not in self.strategies: + raise ValueError(f"Strategy {category} not registered") + return list(self.strategies[category].keys()) + + +class _LazyBackendStrategyRegister: + + def __init__(self): + self._instance = None + + def _get_instance(self): + if self._instance is None: + self._instance = BackendStrategyRegistry() + return self._instance + + def register(self, *args, **kwargs): + return self._get_instance().register(*args, **kwargs) + + def execute_func(self, *args, **kwargs): + return self._get_instance().execute_func(*args, **kwargs) + + +backend_strategy_registry = _LazyBackendStrategyRegister() + + +@backend_strategy_registry.register("mindspore", "cxx_abi") +def get_mindspore_cxx_abi(): + return 0 + + +@backend_strategy_registry.register("torch_npu", "cxx_abi") +def get_torch_cxx_abi(): + import torch + return 1 if torch._C._GLIBCXX_USE_CXX11_ABI else 0 + + +@backend_strategy_registry.register("mindspore", "type_convert") +def type_convert(): + import mindspore + import numpy as np + MINDSPORE_TO_NUMPY_DTYPE = { + mindspore.float32: np.float32, + mindspore.float64: np.float64, + mindspore.float16: np.float16, + mindspore.int8: np.int8, + mindspore.uint8: np.uint8, + mindspore.int16: np.int16, + mindspore.int32: np.int32, + mindspore.int64: np.int64, + mindspore.bool: np.bool_, + mindspore.complex64: np.complex64, + mindspore.complex128: np.complex128, + } + return MINDSPORE_TO_NUMPY_DTYPE + + +@backend_strategy_registry.register("torch_npu", "type_convert") +def type_convert(): + import torch + import numpy as np + TORCH_TO_NUMPY_DTYPE = { + torch.float32: np.float32, + torch.float64: np.float64, + torch.float16: np.float16, + torch.int8: np.int8, + torch.uint8: np.uint8, + torch.int16: np.int16, + torch.int32: np.int32, + torch.int64: np.int64, + torch.bool: np.bool_, + torch.complex64: np.complex64, + torch.complex128: np.complex128, + } + return TORCH_TO_NUMPY_DTYPE + + +@backend_strategy_registry.register("mindspore", "get_device_interface") +def get_device_interface(): + import mindspore + return mindspore + + +@backend_strategy_registry.register("torch_npu", "get_device_interface") +def get_device_interface(): + import torch + return torch.npu + + +@backend_strategy_registry.register("mindspore", "get_empty_tensor") +def get_empty_tensor(size): + import mindspore + return mindspore.mint.empty(size, dtype=mindspore.int32) + + +@backend_strategy_registry.register("torch_npu", "get_empty_tensor") +def get_empty_tensor(size): + import torch + return torch.empty(size, dtype=torch.int32, device='npu') + + +@backend_strategy_registry.register("mindspore", "get_cc_cmd") +def get_cc_cmd(): + import mindspore + mindspore_path = os.path.dirname(os.path.realpath(mindspore.__file__)) + cc_cmd = [ + f"-I{mindspore_path}", + f"-I{os.path.join(mindspore_path, 'include/')}", + f"-I{os.path.join(mindspore_path, 'include/third_party')}", + f"-I{os.path.join(mindspore_path, 'include/third_party/robin_hood_hashing')}", + f"-I{os.path.join(mindspore_path, 'include/mindspore/core')}", + f"-I{os.path.join(mindspore_path, 'include/mindspore/core/include')}", + f"-I{os.path.join(mindspore_path, 'include/mindspore/core/mindrt/include')}", + f"-I{os.path.join(mindspore_path, 'include/mindspore/ccsrc')}", + f"-I{os.path.join(mindspore_path, 'include/mindspore/ccsrc/include')}", + f"-I{os.path.join(mindspore_path, 'include/mindspore/ops')}", + f"-I{os.path.join(mindspore_path, 'include/mindspore/ops/include')}", + f"-D_GLIBCXX_USE_CXX11_ABI={get_mindspore_cxx_abi()}", + "-DENABLE_FAST_HASH_TABLE=1", + f"-L{os.path.join(mindspore_path, 'lib')}", + f"-lmindspore_pynative_utils", + ] + return cc_cmd + + +@backend_strategy_registry.register("torch_npu", "get_cc_cmd") +def get_cc_cmd(): + return [ + f"-D_GLIBCXX_USE_CXX11_ABI={get_torch_cxx_abi()}", + "-ldl", + ] + + +def _get_package_dir(package_name): + spec = importlib.util.find_spec(package_name) + if spec is None: + raise ModuleNotFoundError(f"No module named '{package_name}'") + if spec.submodule_search_locations: + return os.path.realpath(next(iter(spec.submodule_search_locations))) + if spec.origin is None: + raise ModuleNotFoundError(f"Cannot locate module '{package_name}'") + return os.path.dirname(os.path.realpath(spec.origin)) + + +@backend_strategy_registry.register("torch_npu", "get_cc_cmd_npu_utils") +def get_cc_cmd_npu_utils(): + torch_path = _get_package_dir("torch") + torch_npu_path = _get_package_dir("torch_npu") + cc_cmd = [ + f"-I{os.path.join(torch_path, 'include')}", + f"-I{os.path.join(torch_npu_path, 'include')}", + f"-D_GLIBCXX_USE_CXX11_ABI={get_torch_cxx_abi()}", + f"-L{os.path.join(torch_npu_path, 'lib')}", + f"-ltorch_npu", + "-DUSE_TORCH_NPU", + ] + return cc_cmd + + +@backend_strategy_registry.register("mindspore", "get_current_device") +def get_current_device(): + import mindspore + return mindspore.get_current_device().device_id + + +@backend_strategy_registry.register("torch_npu", "get_current_device") +def get_current_device(): + import torch + import torch_npu + return torch.npu.current_device() + + +@backend_strategy_registry.register("mindspore", "set_current_device") +def set_current_device(device_id): + import mindspore + return mindspore.set_device("Ascend", device_id) + + +@backend_strategy_registry.register("torch_npu", "set_current_device") +def set_current_device(device_id): + import torch + import torch_npu + return torch.npu.set_device(device_id) + + +@backend_strategy_registry.register("mindspore", "get_current_stream") +def get_current_stream(device): + import mindspore + try: + return mindspore.current_stream().stream_ptr() + except Exception: + return mindspore.current_stream().id + + +@backend_strategy_registry.register("torch_npu", "get_current_stream") +def get_current_stream(device): + import torch + import torch_npu + if device is None: + device = torch.npu.current_device() + if hasattr(torch_npu._C, "_npu_getCurrentRawStreamNoWait"): + from torch_npu._C import _npu_getCurrentRawStreamNoWait + return _npu_getCurrentRawStreamNoWait(device) + else: + from torch_npu._C import _npu_getCurrentRawStream + return _npu_getCurrentRawStream(device) + + +@backend_strategy_registry.register("mindspore", "header_file") +def header_file(enable_taskqueue): + return f'''#include "include/utils/device_manager_conf.h" +#include "include/runtime/hardware_abstract/device_context/device_context_manager.h" +#include "include/mindspore/ops/kernel/ascend/aclnn/pyboost_impl/aclnn_utils.h" +{'#include "include/pynative/utils/runtime/op_executor.h"' if {enable_taskqueue} else ''} +{'#include "include/runtime/pipeline/pipeline.h"' if {enable_taskqueue} else ''}''' + + +@backend_strategy_registry.register("torch_npu", "header_file") +def header_file(enable_taskqueue): + return '#include \n#include ' + + +@backend_strategy_registry.register("mindspore", "allocate_memory") +def allocate_memory(size, stream): + return f'''auto work_ptr = std::make_shared(device_context, {size}, reinterpret_cast({stream})); + workspace_addr_ptr = work_ptr->ptr_;''' + + +@backend_strategy_registry.register("torch_npu", "allocate_memory") +def allocate_memory(size, stream): + return f'''init_npu_utils(); + if (!g_allocate_workspace) {{ + fprintf(stderr, "Error: triton_allocate_workspace is unavailable\\n"); + workspace_addr_ptr = nullptr; + }} else {{ + workspace_addr_ptr = g_allocate_workspace({size}, &workspace_handle); + }}''' + + +@backend_strategy_registry.register("mindspore", "allocate_sync_block_lock") +def allocate_sync_block_lock(size, stream): + return f'''auto sync_ptr = std::make_shared(device_context, {size}, reinterpret_cast({stream})); + syncBlockLock_ptr = sync_ptr->ptr_;''' + + +@backend_strategy_registry.register("torch_npu", "allocate_sync_block_lock") +def allocate_sync_block_lock(size, stream): + return f'''init_npu_utils(); + if (!g_allocate_sync_block_lock) {{ + fprintf(stderr, "Error: triton_allocate_sync_block_lock is unavailable\\n"); + syncBlockLock_ptr = nullptr; + }} else {{ + syncBlockLock_ptr = g_allocate_sync_block_lock({size}, {stream}, &syncBlockLock_handle); + }}''' + + +@backend_strategy_registry.register("mindspore", "pre_launch") +def pre_launch(first_call): + if first_call: + return '''static auto device_context = mindspore::device::DeviceContextManager::GetInstance().GetOrCreateDeviceContext({mindspore::device::DeviceType::kAscend, mindspore::DeviceManagerConf::GetInstance()->device_id()}); + device_context->device_res_manager_->BindDeviceToCurrentThread(false);''' + else: + return '''device_context->device_res_manager_->BindDeviceToCurrentThread(false);''' + + +@backend_strategy_registry.register("torch_npu", "pre_launch") +def pre_launch(first_call): + return "" + + +@backend_strategy_registry.register("mindspore", "async_launch") +def async_launch(func): + return f'''mindspore::runtime::OpExecutor::DispatchLaunchTask({func});''' + + +@backend_strategy_registry.register("torch_npu", "async_launch") +def async_launch(func): + return f'''init_npu_utils(); + if (!g_async_launch) {{ + fprintf(stderr, "Error: triton_async_launch is unavailable\\n"); + return; + }} + g_async_launch(static_cast(&{func}), kernelName);''' diff --git a/third_party/ascend/backend/compiler.py b/third_party/ascend/backend/compiler.py new file mode 100644 index 0000000000..e409f27d06 --- /dev/null +++ b/third_party/ascend/backend/compiler.py @@ -0,0 +1,1820 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import ctypes +import functools +import hashlib +import glob +import json +import os +import re +import shlex +import subprocess +import tempfile +import warnings +from dataclasses import InitVar, dataclass, field +from pathlib import Path +from types import ModuleType +from typing import Any, Dict, Optional, Tuple, Union +from .debug_line_rewriter import rewrite_debug_line + +from triton._C.libtriton import ir, passes, ascend, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir +try: + from triton._C.libtriton import distributed +except ImportError: + distributed = None +from triton.backends.ascend.utils import ( + _multibuffer_mode_to_tuple, + _check_bishengir_api_change, + _check_bishengir_able_save_ir, + _check_bishengir_is_regbased, + _enable_print_ub_bits, + _enable_dump_memory_info, + _enable_msdebug, + _get_kernel_target, + _get_npucompiler_path, + _get_triton_adapter_opt_path, + _get_triton_mlir_opt_path, + _get_triton_opt_path, + _get_bishengir_opt_path, + _is_ascend_sanitizer_enabled, + _is_debug_line_info_disabled, + _is_auto_map_parallel_blocks_enabled, + _get_auto_blockify_blacklist_reasons, + _warn_auto_blockify_disabled, + _remove_deprecated_npu_options, + _warn_deprecated_npu_option, + _warn_deprecated_ascend_env_vars, + downgrade_llir, + force_disable_ffts, + graph_ub_budget_bytes_for_arch, + ub_size_in_kbytes_for_arch, + get_cann_version_file_hash, + is_cann_version_at_least, +) +from triton.backends.ascend.driver import (NPUUtils) +from triton.backends.ascend.program_grid import ( + DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK, + PROGRAM_GRID_TRANSFORMS_ATTR, + ProgramGridContractError, + get_persistent_transform, + normalize_graph_optimization_rule_mask, + normalize_program_grid_transforms, +) +from triton.backends.compiler import ( + BaseBackend, + GPUTarget, +) +from triton.runtime.cache import get_dump_manager + + +# TODO: materialize the concrete min shape +def min_dot_size(target: GPUTarget): + return lambda lhsType, rhsType: (1, 1, 1) + + +# Get result code saved in module {attr_name = rc} +def _get_then_remove_rc(mod, attr_name: str) -> int: + get_int_attr = getattr(ascend.ir, "get_int_attr", None) + remove_attr = getattr(ascend.ir, "remove_attr", None) + + if get_int_attr is None: + return -1 + try: + attr_value = get_int_attr(mod, attr_name) + except TypeError: + return -1 + + if remove_attr: + remove_attr(mod, attr_name) + + if not isinstance(attr_value, int): + return -1 + + return attr_value + + +def _get_then_remove_program_grid_transforms(mod): + get_transforms = getattr(ascend.ir, "get_program_grid_transforms", None) + remove_attr = getattr(ascend.ir, "remove_attr", None) + if get_transforms is None: + if PROGRAM_GRID_TRANSFORMS_ATTR in str(mod): + raise RuntimeError("hacc.program_grid_transforms requires the matching Ascend C++ binding") + return None + try: + raw = get_transforms(mod) + except TypeError: + if PROGRAM_GRID_TRANSFORMS_ATTR in str(mod): + raise RuntimeError("hacc.program_grid_transforms requires an MLIR module accepted by " + "the Ascend C++ binding") + return None + if raw is not None and remove_attr: + remove_attr(mod, PROGRAM_GRID_TRANSFORMS_ATTR) + return raw + + +def _export_coalesce_metadata(mod, metadata, *, require_row_contract=False): + # Tile/strided coalescing (TritonToLinalg) records the chosen coalesce factor + # H and the grid axis it applies to as module attrs hacc.coalesce_factor / + # hacc.coalesce_axis. In the full-TA design the *launcher* (driver.py) owns + # the grid division: it divides grid[axis] by H before launch, and bishengir + # no longer interprets the attrs. Read them into metadata here and strip them + # from the module so the hacc.* attrs never reach hivmc (which rejects + # unknown module attrs). Absent attrs -> factor 1 (no-op) / axis -1. + # RowCoalescing also records whether the launcher should use ceil-div when + # shrinking the grid, because its runtime valid-count guard handles tails. + # A Row result is only valid as the complete triple. The regular T2L + # Axis/Chunk ABI predates ceil-div and deliberately remains compatible with + # a missing ceil-div attr. + factor = _get_then_remove_rc(mod, "hacc.coalesce_factor") + axis = _get_then_remove_rc(mod, "hacc.coalesce_axis") + ceil_div = _get_then_remove_rc(mod, "hacc.coalesce_grid_ceil_div") + has_any_contract_attr = any(value != -1 for value in (factor, axis, ceil_div)) + valid_factor = isinstance(factor, int) and factor > 1 + valid_axis = isinstance(axis, int) and axis in (0, 1, 2) + valid_ceil_div = isinstance(ceil_div, int) and ceil_div > 0 + if has_any_contract_attr and (not valid_factor or not valid_axis): + raise RuntimeError("invalid hacc.coalesce launch contract") + if require_row_contract and has_any_contract_attr and not valid_ceil_div: + raise RuntimeError("RowCoalescing requires hacc.coalesce_grid_ceil_div") + + metadata["coalesce_factor"] = factor if valid_factor else 1 + metadata["coalesce_axis"] = axis if valid_axis else -1 + metadata["coalesce_grid_ceil_div"] = valid_ceil_div + metadata["row_coalescing_applied"] = metadata["coalesce_factor"] > 1 + + +def _export_program_grid_metadata(mod, metadata, *, require_row_contract=False): + raw_transforms = _get_then_remove_program_grid_transforms(mod) + if raw_transforms is not None: + factor = _get_then_remove_rc(mod, "hacc.coalesce_factor") + axis = _get_then_remove_rc(mod, "hacc.coalesce_axis") + ceil_div = _get_then_remove_rc(mod, "hacc.coalesce_grid_ceil_div") + has_legacy_attrs = any(value != -1 for value in (factor, axis, ceil_div)) + if has_legacy_attrs: + raise RuntimeError("hacc.program_grid_transforms conflicts with legacy hacc.coalesce_* metadata") + try: + transforms = normalize_program_grid_transforms(raw_transforms) + except ProgramGridContractError as error: + raise RuntimeError(f"invalid hacc.program_grid_transforms: {error}") from error + metadata["program_grid_transforms"] = transforms + metadata["program_grid_mapping_applied"] = True + metadata["coalesce_factor"] = 1 + metadata["coalesce_axis"] = -1 + metadata["coalesce_grid_ceil_div"] = False + metadata["row_coalescing_applied"] = False + return + + metadata["program_grid_transforms"] = None + metadata["program_grid_mapping_applied"] = False + _export_coalesce_metadata( + mod, + metadata, + require_row_contract=require_row_contract, + ) + + +def _finalize_program_launch_policy(metadata, opt): + required_fields = ( + "program_grid_transforms", + "program_grid_mapping_applied", + "row_coalescing_applied", + "has_auto_blockify_blacklist_op", + "mix_mode", + ) + missing = [name for name in required_fields if name not in metadata] + if missing: + raise RuntimeError("cannot finalize program launch policy; missing metadata: " + ", ".join(missing)) + + raw_transforms = metadata["program_grid_transforms"] + mapping_applied = metadata["program_grid_mapping_applied"] + row_coalescing_applied = metadata["row_coalescing_applied"] + has_auto_blockify_blacklist_op = metadata["has_auto_blockify_blacklist_op"] + if not isinstance(mapping_applied, bool): + raise RuntimeError("program_grid_mapping_applied must be a boolean") + if not isinstance(row_coalescing_applied, bool): + raise RuntimeError("row_coalescing_applied must be a boolean") + if not isinstance(has_auto_blockify_blacklist_op, bool): + raise RuntimeError("has_auto_blockify_blacklist_op must be a boolean") + + transforms = None + if raw_transforms is not None: + try: + transforms = normalize_program_grid_transforms(raw_transforms) + except ProgramGridContractError as error: + raise RuntimeError(f"invalid exported program_grid_transforms: {error}") from error + if mapping_applied != (transforms is not None): + raise RuntimeError("program_grid_mapping_applied disagrees with program_grid_transforms") + if mapping_applied and row_coalescing_applied: + raise RuntimeError("program-grid mapping conflicts with legacy RowCoalescing") + + persistent_transform = get_persistent_transform(transforms) if transforms is not None else None + ptsm_cap_authorized = False + if persistent_transform is not None: + if metadata["mix_mode"] != "aiv": + raise RuntimeError("persistent program-grid transform requires final mix_mode=aiv") + if not (persistent_transform["persistent_coverage"] and persistent_transform["grid_stride_abi_verified"]): + raise RuntimeError("persistent program-grid transform lacks coverage/ABI verification") + ptsm_cap_authorized = True + + if opt.is_pure_simt: + auto_blockify_enabled = _is_auto_map_parallel_blocks_enabled() and not row_coalescing_applied + else: + auto_blockify_enabled = (_is_auto_map_parallel_blocks_enabled() and not has_auto_blockify_blacklist_op + and not ptsm_cap_authorized) + + if auto_blockify_enabled and ptsm_cap_authorized: + raise RuntimeError("AutoBlockify and persistent-grid cap cannot both be enabled") + + metadata["auto_blockify_enabled"] = auto_blockify_enabled + metadata["ptsm_cap_authorized"] = ptsm_cap_authorized + + +def _adjust_metadata_by_module_result(mod, metadata, opt, **kwargs): + rc = _get_then_remove_rc(mod, "triton_ascend.dynamic_cv_pipeline.rc") + if rc == 3: + metadata["disable_vf_operand_substitution"] = True + return + if rc != -1 and rc > 0: + # When the option dynamic_cv_pipeline is set to False, + # these options should also reverted. + metadata["enable_dynamic_cv_pipeline"] = False + metadata["enable_mixed_cv"] = kwargs["enable_mixed_cv"] + metadata["disable_auto_inject_block_sync"] = kwargs["disable_auto_inject_block_sync"] + metadata["set_workspace_multibuffer"] = kwargs["set_workspace_multibuffer"] + if opt.debug: + print(f"SSBUFFER return code={rc}, will fallback to enable_dynamic_cv_pipeline=False") + + +def _get_dump_paths(hash_key: str, src_path: str, dst_path: str) -> Tuple[str, str]: + dump_manager = get_dump_manager(hash_key) + return (dump_manager._make_path(os.path.basename(src_path)), dump_manager._make_path(os.path.basename(dst_path))) + + +def _with_debug_line(npubin_stage, options): + """Wrap an npubin-producing stage so the emitted kernel binary gets + .debug_line cleanup for msdebug stepping. No-op unless + LLVM_EXTRACT_DI_LOCAL_VARIABLES is set; never raises (failure returns the + artifact unchanged), so it cannot break a build.""" + + def stage(src, metadata): + artifact = npubin_stage(src, metadata) + return rewrite_debug_line(artifact, metadata=metadata, options=options) + + return stage + + +# Stage extensions named after the BiShengIR step they drive, for diagnostics. +_STAGE_NAMES = { + "ttadapter": "ConvertTritonIRToLinalgIR", + "npubin": "ConvertLinalgIRToBinary", + "bcmlir": "BytecodeToLinalgIRByBishengirOpt", + "mlirbc": "LinalgIRToBytecodeByTritonMLIROpt", +} + + +def _kernel_cache_hint(ext, metadata): + """The cache location upstream appends to a stage failure. + + Core compile() has fn_cache_manager in scope; a backend stage does not, so + rebuild it from metadata["hash"] through the same get_cache_manager used at + compiler.py:279 (knobs.cache.manager_class or FileCacheManager), which + yields the same cache_dir. A custom manager class need not expose one, and + the core's file_name is not reachable here, so that case names the stage + artifact only. + """ + from triton.runtime.cache import FileCacheManager, get_cache_manager + + hash_key = (metadata or {}).get("hash") + if hash_key: + try: + manager = get_cache_manager(hash_key) + except Exception: + # A cache manager that cannot be built must not replace the real + # compilation diagnostic with its own failure. + manager = None + if isinstance(manager, FileCacheManager) and manager.cache_dir: + return f"\n\n[INFO]: The compiled kernel cache is in {manager.cache_dir}\n\n" + return f"\n\n[INFO]: The compiled kernel cache is kernel.{ext}\n\n" + + +def _with_tool_diagnostics(ext, stage): + """Wrap an Ascend compilation stage so a tool failure keeps its diagnostic. + + These stages shell out, and CalledProcessError.str() carries only the + command line while the diagnostic sits in stderr. The tuners decide + whether a failure is recoverable by matching the compiler's message + (runtime/ubtuner.py matches "ub overflow") and autotuner classifies + failures by isinstance(exc, MLIRCompilationError), so both the text and + the type have to travel with the exception. + + triton-ascend does this by patching the stage loop in core compiler.py to + catch Exception. FlagTree does not apply that patch, so the handling lives + here, leaving core Triton untouched the way the nvidia backend formats its + own ptxas failures. The catch is deliberately narrowed to SubprocessError + rather than Exception: autotuner skips a config on MLIRCompilationError, so + wrapping an in-process Python defect would let it pass for "this config + does not compile" and be visible only through the exception chain (the risk + flagged in docs/backend/ascend-mlir-compilation-error.md). SubprocessError + still covers every external-tool failure, including TimeoutExpired. + """ + # Imported lazily: triton.compiler imports triton.backends, which loads + # this module, so a module-level import would be circular. + from triton.compiler.errors import MLIRCompilationError + + stage_name = _STAGE_NAMES.get(ext, "MLIRCompile") + + def run(src, metadata): + try: + return stage(src, metadata) + except subprocess.SubprocessError as e: + detail = getattr(e, "stderr", None) + if detail: + if isinstance(detail, bytes): + detail = detail.decode("utf-8", errors="replace") + else: + detail = str(detail) + else: + # A tool that failed without captured stderr still belongs to + # the stage; upstream reports str(e) for exactly this case. + detail = str(e) + detail += _kernel_cache_hint(ext, metadata) + raise MLIRCompilationError(stage_name, detail) from e + + return run + + +def _graph_optimize_kwargs(opt): + kwargs = { + "ub_capacity_bytes": graph_ub_budget_bytes_for_arch(opt.target_arch), + "compile_mode": opt.compile_mode, + } + rule_mask = getattr(opt, "rule_mask", DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK) + if rule_mask != DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK: + kwargs["rule_mask"] = rule_mask + kwargs["ub_safety_percent"] = 80 + kwargs["reserved_ub_bytes"] = 0 + kwargs["mapping_ub_capacity_bytes"] = (ub_size_in_kbytes_for_arch(opt.target_arch) * 1024) + return kwargs + + +def _serialize_module(mod, opt): + if getattr(opt, "debug", False) or not _is_debug_line_info_disabled() or _enable_msdebug(): + return str(mod) + return mod.str_nodebug() + + +def make_ttir(mod, metadata, opt): + if "hash" not in metadata: + metadata["hash"] = hashlib.sha256(f"{mod}-{metadata}".encode()).hexdigest() + # the same optimize pass for triton-ir as all other backends + pm = ir.pass_manager(mod.context) + pm.enable_debug() + passes.common.add_inliner(pm) + # passes.ttir.add_rewrite_tensor_pointer(pm) + passes.ttir.add_rewrite_tensor_descriptor_to_pointer(pm) + passes.ttir.add_combine(pm) + passes.common.add_canonicalizer(pm) + passes.ttir.add_reorder_broadcast(pm) + # commonir: lane-vectorize merges per-lane tensor computations into a single + # tensor with a synthesized leading lane dimension. + ascend.passes.ttir.add_lane_vectorize(pm) + passes.common.add_cse(pm) + passes.common.add_licm(pm) + passes.common.add_symbol_dce(pm) + passes.ttir.add_loop_unroll(pm) + if opt.enable_graph_optimize: + ascend.passes.ttir.add_graph_optimize(pm, **_graph_optimize_kwargs(opt)) + pm.run(mod, 'make_ttir') + if opt.debug: + dump_manager = get_dump_manager(metadata["hash"]) + print(f"Dumping intermediate results to {dump_manager.cache_dir}") + dump_manager.put(str(mod), "kernel.ttir.mlir", binary=False) + + return mod + + +def ttir_to_linalg(mod, metadata, opt, *, named_ops=True): + # use triton_adapter to lower Triton-MLIR to linalg + # Get Triton-MLIR as string + ttir_code = _serialize_module(mod, opt) + auto_map_parallel_blocks_enabled = _is_auto_map_parallel_blocks_enabled() + # This is compiler-derived safety metadata, never a user compile option. + # Derive it even when the feature is currently disabled so a later runtime + # environment change cannot enable AutoBlockify for unsafe TTIR. + blacklist_reasons = _get_auto_blockify_blacklist_reasons(ttir_code) + has_auto_blockify_blacklist_op = bool(blacklist_reasons) + metadata["has_auto_blockify_blacklist_op"] = has_auto_blockify_blacklist_op + if auto_map_parallel_blocks_enabled and has_auto_blockify_blacklist_op and blacklist_reasons: + kernel_name = re.search(r"tt\.func\spublic\s+@(\w+)", ttir_code).group(1) + _warn_auto_blockify_disabled(kernel_name or "", blacklist_reasons) + with tempfile.TemporaryDirectory() as tmpdir: + src_path = os.path.join(tmpdir, "kernel.ttir.mlir") + dst_path = os.path.join(tmpdir, "kernel.ttadapter.mlir") + if opt.debug: + Path(src_path).write_text(ttir_code) + triton_adapter_opt_path = _get_triton_adapter_opt_path() + + # Select analysis is a fixed lowering policy, not a user compile option. + enable_select_analysis = True + compile_on_910_95 = metadata["compile_on_910_95"] + compile_mode = opt.compile_mode + metadata["compile_mode"] = compile_mode + enable_mixed_cv = metadata.get("enable_mixed_cv") + disable_auto_inject_block_sync = metadata.get("disable_auto_inject_block_sync") + set_workspace_multibuffer = metadata.get("set_workspace_multibuffer") + + # Inject grid tile-count hint for ChunkCoalescing. When the kernel + # has no boundary mask but grid[axis] is known at compile time (e.g. + # from constexpr nchunks), the pass uses this to safely choose H. + grid_num_tiles = metadata.get("grid_num_tiles") + if isinstance(grid_num_tiles, int) and grid_num_tiles > 0: + try: + _builder = ascend.ir.ascendnpu_ir_builder(mod.context, opt.target_arch) + mod.set_attr("hacc.grid_num_tiles", _builder.parse_attr(f"{grid_num_tiles} : i32")) + except Exception: + pass # graceful fallback: pass runs without hint + + pm = ir.pass_manager(mod.context) + pm.enable_debug() + if distributed is not None: + distributed.ascend_passes.ttgpuir.add_convert_triton_distributed_to_hivm(pm) + + ascend.passes.ttir.add_triton_control_flow_opt(pm) + ascend.passes.ttir.add_triton_to_structure(pm, False, False) + ascend.passes.ttir.add_discrete_mask_access_conversion(pm, compile_on_910_95, compile_mode) + ascend.passes.ttir.add_triton_to_annotation(pm) + ascend.passes.ttir.add_triton_to_unstructure(pm, compile_on_910_95, compile_mode) + ascend.passes.ttir.add_triton_to_hivm(pm) + ascend.passes.ttir.add_triton_to_hfusion(pm, compile_on_910_95) + ascend.passes.ttir.add_triton_to_llvm(pm) + ascend.passes.ttir.add_bubble_up_operation(pm) + ascend.passes.ttir.add_triton_to_structure(pm, False, False) + + # commonir: fold tile.* ops away before the linalg lowering, which does + # not know the CommonIR dialect. Followed by the inliner because the + # lowering can leave unresolved !tile.buf function signatures. + ascend.passes.ttir.add_commonir_to_hivm(pm) + passes.common.add_inliner(pm) + passes.common.add_canonicalizer(pm) + + ascend.passes.ttir.add_reduce_sum_strength(pm, opt.enable_reduce_sum_strength, 2) + + ascend.passes.ttir.add_triton_to_linalg(pm, False, named_ops, False, enable_select_analysis, + compile_on_910_95, compile_mode) + + # commonir: a second run, since the linalg lowering can reintroduce + # tile.* ops from buffer-typed values. + ascend.passes.ttir.add_commonir_to_hivm(pm) + passes.common.add_canonicalizer(pm) + passes.common.add_cse(pm) + # Restricted to 910_95/950. The merged buffer is written by two disjoint + # memref.copy ops, and on 910_9362 the generated code only makes the + # copy next to the surviving to_tensor visible, so the other half of the + # tile reads back the padding value. The IR is unchanged through + # bishengir-opt, so the loss happens in code generation. + if compile_on_910_95: + ascend.passes.ttir.add_merge_concat_load_buffer(pm) + if metadata["enable_dynamic_cv_pipeline"]: + metadata["set_workspace_multibuffer"] = 0 + metadata["enable_mixed_cv"] = True + metadata["disable_auto_inject_block_sync"] = True + + # Must run before add_dynamic_cv_pipeline because the driven + # AddMultiBufferInnerScope pass reads the module-level + # `ssbuffer.insertionOptimization` attribute (set here) at run time. + # Keep the existing default-on buffer insertion behavior. + ascend.passes.ttir.set_enable_buffer_insert_optimization(mod) + ascend.passes.ttir.add_dynamic_cv_pipeline(pm, compile_on_910_95) + + if _enable_msdebug(): + ascend.passes.ttir.add_normalize_debug_line_locations(pm) + + _intra_val = metadata.get("buf_slot_num_of_veccore") + if _intra_val is not None: + ascend.passes.ttir.set_buffer_count(mod, "INTRA", _intra_val) + + _inter_val = metadata.get("buf_slot_num_of_crosscore") + if _inter_val is not None: + ascend.passes.ttir.set_buffer_count(mod, "INTER", _inter_val) + + _load_val = metadata.get("buf_slot_num_of_gm") + if _load_val is not None: + ascend.passes.ttir.set_buffer_count(mod, "LOAD", _load_val) + + if opt.debug: + # Print a command for replaying the pass pipeline outside Python. + # Module attributes set above are not carried by --pass-pipeline. + print_src_path, print_dst_path = _get_dump_paths(metadata["hash"], src_path, dst_path) + try: + triton_opt_path = _get_triton_opt_path() + except EnvironmentError: + # Wheels bundle this tool, but it is only named here, not + # executed. A missing debug tool must not abort compilation. + triton_opt_path = "triton-opt" + cmd = [ + triton_opt_path, + print_src_path, + f"--pass-pipeline={pm.get_pipeline_str()}", + "--mlir-print-debuginfo", + "-o", + print_dst_path, + ] + print(f"[DEBUG] cmd list: {shlex.join(cmd)}") + + pm.run(mod, 'ttir_to_linalg') + _adjust_metadata_by_module_result(mod, metadata, opt, enable_mixed_cv=enable_mixed_cv, + disable_auto_inject_block_sync=disable_auto_inject_block_sync, + set_workspace_multibuffer=set_workspace_multibuffer) + _export_program_grid_metadata(mod, metadata) + + if opt.debug: + dump_manager = get_dump_manager(metadata["hash"]) + dump_manager.put(str(mod), "kernel.ttadapter.mlir", binary=False) + + return _serialize_module(mod, opt) + + +def linalg_to_bc_by_triton_mlir_opt(linalg: str, metadata, opt): + """ + Convert Linalg IR to MLIR Bytecode format using triton-mlir-opt. + This function supports both MLIR and BishengIR ops. + Args: + linalg: Linalg IR in text format + metadata: Compilation metadata + opt: Compilation options + Returns: + Bytecode data as bytes (not file path, to avoid temp directory cleanup issues) + """ + with tempfile.TemporaryDirectory() as tmpdir: + ttadapter_path = os.path.join(tmpdir, "kernel.ttadapter.mlir") + bc_path = os.path.join(tmpdir, "kernel.mlirbc") + Path(ttadapter_path).write_text(linalg) + + triton_mlir_opt_path = _get_triton_mlir_opt_path() + + # The --emit-bytecode flag ensures output is in BC format. + # Pin v4 for the LLVM 22 producer / LLVM 19 consumer bridge: + # the consumer cannot read the producer's default v6 location encoding. + # Override only when the selected consumer supports that bytecode version. + bytecode_version = os.environ.get("TRITON_ASCEND_BYTECODE_VERSION", "4") + subprocess.run([ + triton_mlir_opt_path, + ttadapter_path, + "--emit-bytecode", + f"--emit-bytecode-version={bytecode_version}", + "-o", + bc_path, + ], check=True, capture_output=True, text=True) + + # Read bytecode as binary before temp directory is cleaned up + with open(bc_path, "rb") as f: + bc_data = f.read() + + if opt.debug: + dump_manager = get_dump_manager(metadata["hash"]) + dump_manager.put(bc_data, "kernel.mlirbc", binary=True) + + return bc_data + + +def bc_to_linalg_by_bishengir_opt(bc_data: bytes, metadata, opt): + """ + Convert MLIR Bytecode to MLIR text format using bishengir-opt. + Args: + bc_data: Bytecode data as bytes + metadata: Compilation metadata + opt: Compilation options + Returns: + MLIR text as string + """ + with tempfile.TemporaryDirectory() as tmpdir: + bc_path = os.path.join(tmpdir, "kernel.mlirbc") + mlir_path = os.path.join(tmpdir, "kernel.mlir") + + # Write bytecode data to temporary file + with open(bc_path, "wb") as f: + f.write(bc_data) + + bishengir_opt_path, env = _get_bishengir_opt_path() + + cmd = [ + bishengir_opt_path, + bc_path, + ] + if opt.debug: + cmd += ["--mlir-print-debuginfo"] + cmd += ["-o", mlir_path] + subprocess.run(cmd, env=env, capture_output=True, check=True, text=True) + + # Read the generated MLIR text + linalg_text = Path(mlir_path).read_text() + + if opt.debug: + dump_manager = get_dump_manager(metadata["hash"]) + dump_manager.put(linalg_text, "kernel.mlir", binary=False) + + return linalg_text + + +def __get_metadata_attr_by_callback(lib, postfix: str, metadata, meta_key: str): + func_symbol = metadata["kernel_name"] + postfix + if hasattr(lib, func_symbol): + callback_func = getattr(lib, func_symbol) + callback_func.restype = ctypes.c_int64 + callback_func.argtypes = [] + metadata[meta_key] = callback_func() + + +def _parse_linalg_metadata(linalg: str, metadata: dict): + """ + Parse Linalg IR to extract metadata required for NPU compilation. + Extracts and updates the following fields in metadata: + - mix_mode + - kernel_name + - tensor_kinds + - shared (currently hardcoded) + - name (kernel_name) + + Additionally, removes the mix_mode attribute from the IR. + """ + # --- Regular expressions and examples --- + + DISABLE_AUTO_TILE_AND_BIND_SUBBLOCK_REGEX = r'hivm.disable_auto_tile_and_bind_subblock' + + # Inserted by DiscreteMaskAccessConversionPass / MemOpConverter when discrete + # masked stores need cross-block exclusion (e.g. hivm.sync_block_lock). + SYNC_BLOCK_LOCK_REGEX = r'\bsync_block_lock\b' + + # Example: mix_mode = "aiv" -> aiv + MIX_MODE_REGEX = r'mix_mode\s*=\s*"([^"]+)"' + + # Example: parallel_mode = "mix_simd_simt" -> mix_simd_simt + PARALLEL_MODE_REGEX = r'parallel_mode\s*=\s*"([^"]+)"' + + # Example: func.func @gather_sorted_kernel(%arg0: ...) -> gather_sorted_kernel + KERNEL_NAME_REGEX = r"func\.func\s+@(\w+)" + + # Example: %arg1: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32} -> ('1', '0') + TENSOR_KIND_REGEX = r'%arg(\d+):[^,)]*?\{[^}]*?tt\.tensor_kind\s*=\s*([^:\s}]+)\s*:[^}]*?\}' + + # Example: bitcode = "a.bc" + BITCODES_REGEX = r'bitcode\s*=\s*(?:"([^"]+)"|\'([^\']+)\'|(\w+))' + + # Note: Compiled Kernel requires to estimate size of shared memory to occupy + # Currently, NPU backend does not limit on shared memory + metadata["shared"] = 1 + module_attrs = re.search(r'\bmodule(?:\s+@\w+)?\s+attributes\s*\{([^}]*)\}', linalg) + metadata["has_full_row_copy"] = bool(module_attrs and re.search(r'\btt\.full_row_copy\b', module_attrs.group(1))) + # Force disable auto tile and bind subblock if attribute is present in module + metadata["auto_tile_and_bind_subblock"] = not re.search(DISABLE_AUTO_TILE_AND_BIND_SUBBLOCK_REGEX, linalg) + # Turn off auto-blockify only for the ORDERED (token-ring) sync_block_lock: + if re.search(SYNC_BLOCK_LOCK_REGEX, linalg) and not re.search(r"sync_block_lock_unordered", linalg): + metadata["has_auto_blockify_blacklist_op"] = True + # Mixed kernels use (block, subblock) as the unordered-lock participant + # identity. Pure AIV kernels keep subblock tiling disabled: they neither + # need it nor necessarily have the FFTS runtime resource it requires. + has_unordered_sync_block_lock = re.search(r"sync_block_lock_unordered", linalg) is not None + metadata["has_unordered_sync_block_lock"] = has_unordered_sync_block_lock + if has_unordered_sync_block_lock: + mix_mode = re.search(MIX_MODE_REGEX, linalg).group(1) + if mix_mode != "mix": + metadata["auto_tile_and_bind_subblock"] = False + # One metadata cache line for runtime participant_num, plus one + # choosing and one ticket cache line per participant. Each cache line is + # 8 i64. This fallback is for one lock; the bishengir callback supplies + # the exact total after lowering. + metadata["sync_block_lock_layout"] = 1 << 32 + metadata["lock_init_val"] = 0 + # the mix mode is also encoded into metadata['name'] for runtime to distinguish + metadata["mix_mode"] = re.search(MIX_MODE_REGEX, linalg).group(1) + metadata["parallel_mode"] = re.search(PARALLEL_MODE_REGEX, linalg).group(1) + metadata["kernel_name"] = re.search(KERNEL_NAME_REGEX, linalg).group(1) + # Check the function load_binary in npu_driver.py. + metadata["name"] = metadata["kernel_name"] + # Parse all tensor kinds from arguments + metadata["tensor_kinds"] = [int(kind) for _, kind in re.findall(TENSOR_KIND_REGEX, linalg)] + # init the ub bits of triton kernel for inductor autotune using + metadata["required_ub_bits"] = 0 + + # Parse all bitcode paths + bitcodes = re.findall(BITCODES_REGEX, linalg) + metadata["bitcodes"] = [val for group in bitcodes for val in group if val] + return linalg, metadata + + +def _parse_ttir_metadata(ttir: str, metadata: dict): + """ + Parse TTIR to extract metadata required for NPU compilation. + Extracts and updates the following fields in metadata: + - kernel_name + - shared (currently hardcoded) + """ + # --- Regular expressions and examples --- + # Example: tt.func @gather_sorted_kernel(%arg0: ...) -> gather_sorted_kernel + KERNEL_NAME_REGEX = r"tt\.func\spublic\s+@(\w+)" + + # Example: %arg1: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32} -> ('1', '0') + TENSOR_KIND_REGEX = r'%arg(\d+):[^,)]*?\{[^}]*?tt\.tensor_kind\s*=\s*([^:\s}]+)\s*:[^}]*?\}' + + # Note: Compiled Kernel requires to estimate size of shared memory to occupy + # Currently, NPU backend does not limit on shared memory + metadata["shared"] = 1 + # Note: Currently, for TTIR inputs, we only support vector kernels. + metadata["mix_mode"] = "aiv" + metadata["kernel_name"] = re.search(KERNEL_NAME_REGEX, ttir).group(1) + metadata["name"] = metadata["kernel_name"] + # Keep this as compiler-derived safety metadata. In particular, do not + # trust a caller-provided False value to override an unsafe TTIR pattern. + metadata["has_auto_blockify_blacklist_op"] = bool(_get_auto_blockify_blacklist_reasons(ttir)) + # Parse all tensor kinds from arguments + metadata["tensor_kinds"] = [int(kind) for _, kind in re.findall(TENSOR_KIND_REGEX, ttir)] + return metadata + + +def get_common_bishengir_compile_options(metadata): + bishengir_target = metadata['target'].arch + bishengir_target_opt = f"--target={bishengir_target}" + return [bishengir_target_opt] + + +@functools.lru_cache() +def _supports_full_row_copy_inlining(compiler_path): + try: + result = subprocess.run([compiler_path, "--help"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + timeout=10, check=False) + except (OSError, subprocess.SubprocessError): + return False + return result.returncode == 0 and "--enable-lib-call-no-inline" in result.stdout + + +def _full_row_copy_compile_options(metadata, compiler_path): + # A shared noinline load helper loses constant-width specialization when + # the masked tail also calls it. Scope this to compiler-versioned A5 SIMD + # loads; other kernels retain the NPU compiler's default inlining policy. + if (metadata.get("has_full_row_copy", False) and metadata["target"].arch.startswith("Ascend950") + and metadata.get("mix_mode") == "aiv" and metadata.get("parallel_mode") == "simd" + and _supports_full_row_copy_inlining(compiler_path)): + return ["--enable-lib-call-no-inline=false"] + return [] + + +def get_auto_bind_sub_block_option(metadata): + # auto_tile_and_bind_subblock is read from the module. + # enable_auto_bind_sub_block is set by the user and has a higher priority. + enable_auto_bind_sub_block = metadata["enable_auto_bind_sub_block"] + return (metadata["auto_tile_and_bind_subblock"] + if enable_auto_bind_sub_block is None else enable_auto_bind_sub_block) + + +def _save_npuir_debug_output(stdout_bytes: bytes, stderr_bytes: bytes, tmpdir: str, metadata_hash: str): + stdout = stdout_bytes.decode('utf-8') if stdout_bytes else '' + stderr = stderr_bytes.decode('utf-8') if stderr_bytes else '' + combined = stdout + stderr + if not combined.strip(): + combined = "No output captured." + output_path = os.path.join(tmpdir, "kernel.npuir.mlir") + with open(output_path, 'w', encoding='utf-8') as f: + f.write(combined) + + dump_manager = get_dump_manager(metadata_hash) + dump_manager.put(Path(output_path).read_text(encoding='utf-8'), "kernel.npuir.mlir", binary=False) + + +def _dump_kernel_binary(metadata_hash: str, bin_path: str): + """Copy the compiled kernel object into the TRITON_DEBUG dump directory.""" + dump_manager = get_dump_manager(metadata_hash) + dump_manager.put(Path(bin_path).read_bytes(), os.path.basename(bin_path), binary=True) + + +def try_compile_with_config(linalg: str, ub_config: Dict[str, Any], metadata: dict, opt) -> Tuple[bool, str]: + """ + Try to compile with given UB config, return (success, error_msg). + If compilation fails due to UB overflow or other errors, returns (False, error_msg). + If compilation succeeds, returns (True, ""). + """ + # Must import from ubtuner.py - this is the single source of truth + from triton.backends.ascend.runtime.ubtuner import UB_TO_NPU_OPTION_MAP + option_mapping = UB_TO_NPU_OPTION_MAP + + metadata = dict(metadata) + + for ub_key, meta_key in option_mapping.items(): + if ub_key in ub_config: + metadata[meta_key] = ub_config[ub_key] + + # Choose compile function based on options + if opt.compile_on_910_95: + compile_func = linalg_to_bin_enable_npu_compile_910_95 + else: + compile_func = linalg_to_bin_enable_npu_compile_A2_A3 + + try: + compile_func(linalg, metadata, opt) + return (True, "") + except subprocess.CalledProcessError as e: + error_msg = e.stderr.decode('utf-8') if e.stderr else str(e) + return (False, error_msg) + except Exception as e: + return (False, str(e)) + + +def linalg_to_bin_enable_npu_compile_910_95(linalg: str, metadata, opt): + linalg, metadata = _parse_linalg_metadata(linalg, metadata) + _finalize_program_launch_policy(metadata, opt) + with tempfile.TemporaryDirectory() as tmpdir: + tmp_file_name = "kernel.mlir" + ttadapter_path = os.path.join(tmpdir, tmp_file_name) + Path(ttadapter_path).write_text(linalg) + bin_file = os.path.join(tmpdir, "kernel") + if _check_bishengir_api_change(): + bin_file_with_ext = "kernel.o" + else: + bin_file_with_ext = "kernel_reloc.o" + bin_path = os.path.join(tmpdir, bin_file_with_ext) + callback_path = os.path.join(tmpdir, "libkernel.so") + _compile_option_list = get_common_bishengir_compile_options(metadata) + + multibuffer_mode = metadata.get("multibuffer_mode") + if multibuffer_mode is not None: + mode_arg = "[" + ",".join(f"({level},{count})" for level, count in multibuffer_mode) + "]" + _compile_option_list.append(f"--multibuffer-mode={mode_arg}") + + multibuffer = metadata.get("multibuffer") + num_stages = metadata.get("num_stages") + + if multibuffer is not None or num_stages is not None: + multi_buffer_value = True + if multibuffer is not None and not multibuffer: + multi_buffer_value = False + elif num_stages is not None and num_stages == 1: + multi_buffer_value = False + _compile_option_list += [ + f"--enable-auto-multi-buffer={multi_buffer_value}", + ] + + vf_fusion_mode = metadata["vf_fusion_mode"] + if vf_fusion_mode is not None: + _compile_option_list += [ + f"--vf-fusion-mode={vf_fusion_mode}", + ] + + enable_preload = metadata["enable_preload"] + if enable_preload is not None: + _compile_option_list += [ + f"--enable-preload={enable_preload}", + ] + + disable_tightly_coupled_buffer_reuse = metadata["disable_tightly_coupled_buffer_reuse"] + if disable_tightly_coupled_buffer_reuse: + _compile_option_list += ["--disable-tightly-coupled-buffer-reuse"] + + _compile_option_list += [ + f"--enable-auto-bind-sub-block={get_auto_bind_sub_block_option(metadata)}", + ] + + _compile_option_list += [f"--enable-optimized-metaop={is_cann_version_at_least(9, 2, 0)}"] + + npu_utils = NPUUtils() + if npu_utils.has_device_limit(): + _compile_option_list += [ + f"--custom-aic-number={npu_utils.get_aicore_num()}", + ] + _compile_option_list += [ + f"--custom-aiv-number={npu_utils.get_aivector_core_num()}", + ] + + if force_disable_ffts(opt.target_arch): + _compile_option_list += ["--disable-ffts"] + if _is_ascend_sanitizer_enabled(): + _compile_option_list += ["--enable-sanitizer=true"] + if not _is_debug_line_info_disabled(): + _compile_option_list += ["--enable-debug-info=true"] + if _enable_msdebug(): + _compile_option_list += ["--enable-debug-variables=true"] + if _enable_print_ub_bits(): + _compile_option_list += ["--enable-print-memory-allocated-size"] + + enable_hivm_auto_cv_balance = metadata["enable_hivm_auto_cv_balance"] + if enable_hivm_auto_cv_balance is not None: + _compile_option_list += \ + [f"--enable-hivm-auto-cv-balance={enable_hivm_auto_cv_balance}"] + + sync_solver = metadata["sync_solver"] + if sync_solver is not None: + _compile_option_list += \ + [f"--enable-hivm-graph-sync-solver={sync_solver}"] + + unit_flag = metadata["unit_flag"] + if unit_flag is not None: + _compile_option_list += \ + [f"--enable-hivm-unit-flag-sync={unit_flag}"] + + inject_barrier_all = metadata["inject_barrier_all"] + if inject_barrier_all is not None: + _compile_option_list += \ + [f"--enable-hivm-inject-barrier-all-sync={inject_barrier_all}"] + + inject_block_all = metadata["inject_block_all"] + if inject_block_all is not None: + _compile_option_list += \ + [f"--enable-hivm-inject-block-all-sync={inject_block_all}"] + + limit_auto_multi_buffer_only_for_local_buffer = metadata["limit_auto_multi_buffer_only_for_local_buffer"] + if limit_auto_multi_buffer_only_for_local_buffer is not None: + _compile_option_list += \ + [f"--limit-auto-multi-buffer-only-for-local-buffer={limit_auto_multi_buffer_only_for_local_buffer}"] + + set_workspace_multibuffer = metadata["set_workspace_multibuffer"] + if set_workspace_multibuffer is not None: + _compile_option_list += \ + [f"--set-workspace-multibuffer={set_workspace_multibuffer}"] + + auto_multi_buffer = metadata["limit_auto_multi_buffer_of_local_buffer"] + if auto_multi_buffer is None: + auto_multi_buffer = "no-limit" + _compile_option_list += \ + [f"--limit-auto-multi-buffer-of-local-buffer={auto_multi_buffer}"] + auto_multi_buffer_buffer = metadata["limit_auto_multi_buffer_buffer"] + if auto_multi_buffer_buffer is not None: + _compile_option_list += \ + [f"--limit-auto-multi-buffer-buffer={auto_multi_buffer_buffer}"] + + enable_mixed_cv = metadata["enable_mixed_cv"] + if enable_mixed_cv is not None: + _compile_option_list += \ + [f"--enable-mixed-cv={enable_mixed_cv}"] + + enable_dynamic_cv_pipeline = metadata["enable_dynamic_cv_pipeline"] + if enable_dynamic_cv_pipeline == True and not metadata.get("disable_vf_operand_substitution", False): + _compile_option_list += [f"--enable-vf-operand-substitution=True"] + + enable_flatten = metadata["enable_flatten"] + if enable_flatten is not None: + _compile_option_list += \ + [f"--enable-flatten={enable_flatten}"] + + enable_auto_vectorize_v2 = metadata["enable_auto_vectorize_v2"] + if enable_auto_vectorize_v2 is not None: + _compile_option_list += \ + [f"--enable-auto-vectorize-v2={enable_auto_vectorize_v2}"] + auto_vectorize_v2_max_fused_ops_num = metadata["auto_vectorize_v2_max_fused_ops_num"] + if auto_vectorize_v2_max_fused_ops_num is not None: + _compile_option_list += \ + [f"--hfusion-max-fused-ops-in-auto-vectorize-v2={auto_vectorize_v2_max_fused_ops_num}"] + prevec_max_fused_ops_num = metadata["prevec_max_fused_ops_num"] + if prevec_max_fused_ops_num is not None: + _compile_option_list += \ + [f"--hfusion-max-fused-elementwise-ops={prevec_max_fused_ops_num}"] + + disable_auto_inject_block_sync = metadata["disable_auto_inject_block_sync"] + if disable_auto_inject_block_sync is not None: + _compile_option_list += \ + [f"--disable-auto-inject-block-sync={disable_auto_inject_block_sync}"] + + bitcodes = metadata["bitcodes"] + if bitcodes is not None: + for bitcode in bitcodes: + _compile_option_list += \ + [f"--link-aicore-bitcode={bitcode}"] + + if metadata["auto_blockify_enabled"]: + _compile_option_list += ["--enable-auto-blockify-loop"] + npu_compiler_path, env = _get_npucompiler_path() + if npu_compiler_path.endswith("bishengir-compile"): + _compile_option_list += [ + "--enable-hfusion-compile=true", + "--enable-triton-kernel-compile=true", + ] + _compile_option_list += _full_row_copy_compile_options(metadata, npu_compiler_path) + # Temporary until the NPU compiler enables batch matmul by default in Q4. + if metadata.get("enable_hivm_batch_matmul"): + _compile_option_list += ["--enable-hivm-batch-matmul"] + if metadata.get("enable_vf_stack_limit"): + _compile_option_list += ["--enable-vf-stack-limit"] + bisheng_options = metadata["bisheng_options"] + if bisheng_options is not None: + _compile_option_list += [f"--append-bisheng-options={bisheng_options}"] + if opt.debug: + _compile_option_list += ["--mlir-print-ir-after-failure"] + _compile_option_list += ["--mlir-print-stacktrace-on-diagnostic"] + _compile_option_list += ["--bishengir-print-ir-after=hivm-graph-sync-solver"] + + vf_merge_level = metadata["vf_merge_level"] + if vf_merge_level is not None: + _compile_option_list += [f"--enable-vf-merge-level={vf_merge_level}"] + + hfusion_enable_multiple_consumer_fusion = metadata["hfusion_enable_multiple_consumer_fusion"] + if hfusion_enable_multiple_consumer_fusion is not None: + _compile_option_list += [ + f"--hfusion-enable-multiple-consumer-fusion={hfusion_enable_multiple_consumer_fusion}" + ] + + plan_memory_strategy = metadata["plan_memory_strategy"] + if plan_memory_strategy is not None: + _compile_option_list += [f"--plan-memory-strategy={plan_memory_strategy}"] + + cmd_list = ([npu_compiler_path, ttadapter_path] + _compile_option_list + ["-o", bin_file]) + + if opt.debug or os.getenv("TRITON_PRINT_AUTOTUNING", None) == "1": + print_cmd_list = cmd_list.copy() + print_cmd_list[1], print_cmd_list[-1] = _get_dump_paths(metadata["hash"], ttadapter_path, bin_path) + print(f"[DEBUG] cmd_list: {shlex.join(print_cmd_list)}") + + try: + ret = subprocess.run(cmd_list, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + if opt.debug: + _save_npuir_debug_output(e.stdout, e.stderr, tmpdir, metadata["hash"]) + raise + + if opt.debug: + _save_npuir_debug_output(ret.stdout, ret.stderr, tmpdir, metadata["hash"]) + + stdout_str = ret.stdout.decode('utf-8') if ret.stdout else '' + match = re.search(r'UB\s+size\s*=\s*(\d+)\s*bits', stdout_str) + if match: + # get the ub bits of triton kernel from bisheng for inductor autotune using + metadata["required_ub_bits"] = int(match.group(1)) + + if not Path(bin_path).exists(): + error_msg = ret.stderr.decode('utf-8') if ret.stderr else '' + print(f"[DEBUG] {bin_path} is not found") + print(f"[DEBUG] Stderr:\n{error_msg}") + raise subprocess.CalledProcessError(ret.returncode, cmd_list, ret.stdout, ret.stderr) + + if opt.debug: + _dump_kernel_binary(metadata["hash"], bin_path) + + if Path(callback_path).is_file(): + lib = ctypes.CDLL(callback_path) + __get_metadata_attr_by_callback(lib, "_infer_task_type_function", metadata, "bs_task_type") + __get_metadata_attr_by_callback(lib, "_infer_workspace_shape_function", metadata, "workspace_size") + __get_metadata_attr_by_callback(lib, "_infer_sync_block_lock_num_function", metadata, + "sync_block_lock_layout") + __get_metadata_attr_by_callback(lib, "_infer_sync_block_lock_init_function", metadata, "lock_init_val") + + return Path(bin_path).read_bytes() + + +def linalg_to_bin_enable_npu_compile_A2_A3(linalg: str, metadata, opt): + linalg, metadata = _parse_linalg_metadata(linalg, metadata) + _finalize_program_launch_policy(metadata, opt) + with tempfile.TemporaryDirectory() as tmpdir: + tmp_file_name = "kernel.mlir" + ttadapter_path = os.path.join(tmpdir, tmp_file_name) + Path(ttadapter_path).write_text(linalg) + bin_file = os.path.join(tmpdir, "kernel") + if _check_bishengir_api_change(): + bin_file_with_ext = "kernel.o" + else: + bin_file_with_ext = "kernel_reloc.o" + if _check_bishengir_is_regbased(): + bishengir_hivm_opt = "--reg-based=true" + else: + bishengir_hivm_opt = "--enable-hivm-compile=true" + bin_path = os.path.join(tmpdir, bin_file_with_ext) + callback_path = os.path.join(tmpdir, "libkernel.so") + _compile_option_list = [ + f"--target={NPUUtils().get_arch()}", + ] + + multibuffer_mode = metadata.get("multibuffer_mode") + if multibuffer_mode is not None: + mode_arg = "[" + ",".join(f"({level},{count})" for level, count in multibuffer_mode) + "]" + _compile_option_list.append(f"--multibuffer-mode={mode_arg}") + + multibuffer = metadata.get("multibuffer") + num_stages = metadata.get("num_stages") + + if multibuffer is not None or num_stages is not None: + multi_buffer_value = True + if multibuffer is not None and not multibuffer: + multi_buffer_value = False + elif num_stages is not None and num_stages == 1: + multi_buffer_value = False + _compile_option_list.append(f"--enable-auto-multi-buffer={multi_buffer_value}") + + enable_ubuf_saving = metadata["enable_ubuf_saving"] + if enable_ubuf_saving is not None: + _compile_option_list += [ + f"--enable-ubuf-saving={enable_ubuf_saving}", + ] + + disable_size_align_for_cast = metadata["disable_size_align_for_cast"] + if disable_size_align_for_cast is not None: + _compile_option_list += [ + f"--disable-size-align-for-cast={disable_size_align_for_cast}", + ] + + enable_preload = metadata["enable_preload"] + if enable_preload is not None: + _compile_option_list += [ + f"--enable-preload={enable_preload}", + ] + + _compile_option_list += [ + f"--enable-auto-bind-sub-block={get_auto_bind_sub_block_option(metadata)}", + ] + + _compile_option_list += [f"--enable-optimized-metaop=false"] + + if _is_ascend_sanitizer_enabled(): + _compile_option_list += ["--enable-sanitizer=true"] + if not _is_debug_line_info_disabled(): + _compile_option_list += ["--enable-debug-info=true"] + + if _enable_print_ub_bits(): + _compile_option_list += ["--enable-print-memory-allocated-size"] + + if _enable_dump_memory_info(): + _compile_option_list += ["--enable-memory-display=true"] + + if _enable_msdebug(): + _compile_option_list += ["--enable-debug-variables=true"] + + enable_hivm_auto_cv_balance = metadata["enable_hivm_auto_cv_balance"] + if enable_hivm_auto_cv_balance is not None: + _compile_option_list += \ + [f"--enable-hivm-auto-cv-balance={enable_hivm_auto_cv_balance}"] + + sync_solver = metadata["sync_solver"] + if sync_solver is not None: + _compile_option_list += [ + f"--enable-hivm-graph-sync-solver={sync_solver}", + f"--enable-hivm-cross-core-gss={sync_solver}", + ] + + unit_flag = metadata["unit_flag"] + if unit_flag is not None: + _compile_option_list += \ + [f"--enable-hivm-unit-flag-sync={unit_flag}"] + + enable_flatten = metadata["enable_flatten"] + if enable_flatten is not None: + _compile_option_list += \ + [f"--enable-flatten={enable_flatten}"] + + enable_auto_vectorize_v2 = metadata["enable_auto_vectorize_v2"] + if enable_auto_vectorize_v2 is not None: + _compile_option_list += \ + [f"--enable-auto-vectorize-v2={enable_auto_vectorize_v2}"] + + inject_barrier_all = metadata["inject_barrier_all"] + if inject_barrier_all is not None: + _compile_option_list += \ + [f"--enable-hivm-inject-barrier-all-sync={inject_barrier_all}"] + + inject_block_all = metadata["inject_block_all"] + if inject_block_all is not None: + _compile_option_list += \ + [f"--enable-hivm-inject-block-all-sync={inject_block_all}"] + + limit_auto_multi_buffer_only_for_local_buffer = metadata["limit_auto_multi_buffer_only_for_local_buffer"] + if limit_auto_multi_buffer_only_for_local_buffer is not None: + _compile_option_list += \ + [f"--limit-auto-multi-buffer-only-for-local-buffer={limit_auto_multi_buffer_only_for_local_buffer}"] + + set_workspace_multibuffer = metadata["set_workspace_multibuffer"] + if set_workspace_multibuffer is not None: + _compile_option_list += \ + [f"--set-workspace-multibuffer={set_workspace_multibuffer}"] + + tile_mix_vector_loop = metadata["tile_mix_vector_loop"] + if tile_mix_vector_loop is not None: + _compile_option_list += \ + [f"--tile-mix-vector-loop={tile_mix_vector_loop}"] + + tile_mix_cube_loop = metadata["tile_mix_cube_loop"] + if tile_mix_cube_loop is not None: + _compile_option_list += \ + [f"--tile-mix-cube-loop={tile_mix_cube_loop}"] + + auto_multi_buffer = metadata["limit_auto_multi_buffer_of_local_buffer"] + if auto_multi_buffer is not None: + _compile_option_list += \ + [f"--limit-auto-multi-buffer-of-local-buffer={auto_multi_buffer}"] + + disable_auto_inject_block_sync = metadata["disable_auto_inject_block_sync"] + if disable_auto_inject_block_sync is not None: + _compile_option_list += \ + [f"--disable-auto-inject-block-sync={disable_auto_inject_block_sync}"] + + bitcodes = metadata["bitcodes"] + if bitcodes is not None: + for bitcode in bitcodes: + _compile_option_list += \ + [f"--link-aicore-bitcode={bitcode}"] + + enable_libdevice = os.getenv("TRITON_ENABLE_LIBDEVICE", False) + if enable_libdevice: + _compile_option_list += [f"--link-aicore-bitcode={get_libdevice()}"] + + if metadata["auto_blockify_enabled"]: + _compile_option_list += ["--enable-auto-blockify-loop"] + npu_compiler_path, env = _get_npucompiler_path() + if npu_compiler_path.endswith("bishengir-compile"): + _compile_option_list += [ + "--enable-hfusion-compile=true", + bishengir_hivm_opt, + "--enable-triton-kernel-compile=true", + ] + + if opt.debug: + _compile_option_list += ["--mlir-print-ir-after-failure"] + _compile_option_list += ["--mlir-print-stacktrace-on-diagnostic"] + _compile_option_list += ["--bishengir-print-ir-after=hivm-graph-sync-solver"] + + cmd_list = ([npu_compiler_path, ttadapter_path] + _compile_option_list + ["-o", bin_file]) + + if opt.debug or os.getenv("TRITON_PRINT_AUTOTUNING", None) == "1": + print_cmd_list = cmd_list.copy() + print_cmd_list[1], print_cmd_list[-1] = _get_dump_paths(metadata["hash"], ttadapter_path, bin_path) + print(f"[DEBUG] cmd_list: {shlex.join(print_cmd_list)}") + + try: + ret = subprocess.run(cmd_list, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + except subprocess.CalledProcessError as e: + if opt.debug: + _save_npuir_debug_output(e.stdout, e.stderr, tmpdir, metadata["hash"]) + raise + + if opt.debug: + _save_npuir_debug_output(ret.stdout, ret.stderr, tmpdir, metadata["hash"]) + + stdout_str = ret.stdout.decode('utf-8') if ret.stdout else '' + match = re.search(r'UB\s+size\s*=\s*(\d+)\s*bits', stdout_str) + if match: + metadata["required_ub_bits"] = int(match.group(1)) + + if not Path(bin_path).exists(): + error_msg = ret.stderr.decode('utf-8') if ret.stderr else '' + print(f"[DEBUG] {bin_path} is not found") + print(f"[DEBUG] Stderr:\n{error_msg}") + raise subprocess.CalledProcessError(ret.returncode, cmd_list, ret.stdout, ret.stderr) + + if opt.debug: + _dump_kernel_binary(metadata["hash"], bin_path) + + if Path(callback_path).is_file(): + lib = ctypes.CDLL(callback_path) + __get_metadata_attr_by_callback(lib, "_infer_task_type_function", metadata, "bs_task_type") + __get_metadata_attr_by_callback(lib, "_infer_workspace_shape_function", metadata, "workspace_size") + __get_metadata_attr_by_callback(lib, "_infer_sync_block_lock_num_function", metadata, + "sync_block_lock_layout") + __get_metadata_attr_by_callback(lib, "_infer_sync_block_lock_init_function", metadata, "lock_init_val") + + return Path(bin_path).read_bytes() + + +def get_libdevice(): + current = os.path.dirname(__file__) + return os.path.join(current, "lib/libdevice.10.bc") + + +def _is_a5_target_arch(arch: str) -> bool: + return isinstance(arch, str) and arch.startswith(("Ascend910_95", "Ascend950")) + + +def _get_libdevice_compile_state(arch: str) -> tuple[bool, bool]: + return ( + bool(os.getenv("TRITON_ENABLE_LIBDEVICE", False)), + bool(os.getenv("TRITON_ENABLE_LIBDEVICE_SIMT", False)) and _is_a5_target_arch(arch), + ) + + +_CANONICAL_COMPILE_MODES = ("simd", "simd_simt_template", "simt_only") +_COMPILE_MODE_ALIASES = { + "unstructured_in_simt": "simd_simt_template", +} + + +def _normalize_compile_mode(compile_mode, arch: str) -> str: + """Validate and canonicalize the public compile-mode contract. + + ``simd_simt_template`` is the portable default: it retains the ordinary + SIMD pipeline on A2/A3 and enables the existing template-SIMT subpaths on + Ascend 950. ``unstructured_in_simt`` is an equivalent spelling of that mode. + Pure-SIMT remains an Ascend 950-only explicit compile mode. + """ + if not isinstance(compile_mode, str): + raise ValueError("compile_mode must be a string; expected one of: " + ", ".join(_CANONICAL_COMPILE_MODES)) + + canonical_mode = _COMPILE_MODE_ALIASES.get(compile_mode, compile_mode) + if canonical_mode not in _CANONICAL_COMPILE_MODES: + raise ValueError(f"invalid compile_mode={compile_mode!r}; expected one of: " + + ", ".join(_CANONICAL_COMPILE_MODES)) + + if canonical_mode == "simt_only" and not _is_a5_target_arch(arch): + raise ValueError('compile_mode="simt_only" is supported only on Ascend 950 targets.') + return canonical_mode + + +@dataclass(frozen=True) +class NPUOptions: + debug: bool = False + sanitize_overflow: bool = True + # Backend-only construction input. AscendBackend.parse_options injects + # GPUTarget.arch and never forwards a user-supplied compile option. + arch: InitVar[str] = "" + rule_mask: InitVar[int] = DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK + # This becomes compiler metadata, so its name must also be valid for the + # namedtuple constructed by CompiledKernel on Python 3.10. + target_arch: str = field(init=False, repr=False) + + cluster_dims: tuple = (1, 1, 1) + num_warps: int = 32 + num_ctas: int = 1 + # Keep an omitted stage count distinct from an explicit mode. + num_stages: Optional[int] = None + # Ascend threads-per-warp is a backend capability, not a compile option. + warp_size: int = field(default=32, init=False) + ir_override: Optional[str] = None # filename of a user-defined IR (*.{ttir|ttadapter|mlirbc|bcmlir|npubin}) + + # Deprecated constructor-only compatibility input. The supplied value is + # ignored and replaced with the lowering selector derived from GPUTarget.arch. + compile_on_910_95: Optional[bool] = field(default=None, repr=False, kw_only=True) + enable_warp_specialization: bool = False + enable_persistent: bool = False + optimize_epilogue: bool = False + enable_fp_fusion: bool = True + launch_cooperative_grid: bool = False + backend_name: str = 'cann' + instrumentation_mode: str = "" + enable_graph_optimize: bool = True + supported_fp8_dtypes: Tuple[str] = ("fp8e5", "fp8e4b15", "fp8e4nv", "fp8e4b8", "fp8e5b16") + deprecated_fp8_dtypes: Tuple[str] = () + vf_merge_level: int = 1 + default_dot_input_precision: str = "ieee" + allowed_dot_input_precisions: Tuple[str] = ("ieee", "hf32") + max_num_imprecise_acc_default: int = 0 + extern_libs: dict = None + # Backend-managed BiSheng arguments. Keep the name discoverable so legacy + # launch keywords are accepted, but never forward a user-supplied value. + bisheng_options: str = field(default="-cce-link-aicore-ll-module " + get_libdevice(), init=False) + multibuffer: bool = True + # Public dictionaries are stored as sorted pairs for hashing and JSON caches. + multibuffer_mode: Optional[Union[Dict[str, int], Tuple[Tuple[str, int], ...]]] = None + vf_fusion_mode: str = None + enable_ubuf_saving: bool = None + disable_size_align_for_cast: bool = None + enable_preload: bool = None + enable_auto_bind_sub_block: bool = None + disable_tightly_coupled_buffer_reuse: bool = False + enable_hivm_auto_cv_balance: bool = None + # Temporary 910_95 switch; the NPU compiler plans to make this default in Q4. + enable_hivm_batch_matmul: bool = False + # Only takes effect on the Ascend 950 non-pure-SIMT BiShengIR compilation path. + enable_vf_stack_limit: bool = False + sync_solver: bool = None + unit_flag: bool = None + enable_flatten: bool = None + enable_auto_vectorize_v2: bool = None + auto_vectorize_v2_max_fused_ops_num: int = None + prevec_max_fused_ops_num: int = None + inject_barrier_all: bool = None + inject_block_all: bool = None + limit_auto_multi_buffer_only_for_local_buffer: bool = None + limit_auto_multi_buffer_of_local_buffer: str = None + limit_auto_multi_buffer_buffer: str = None + set_workspace_multibuffer: int = None + tile_mix_vector_loop: int = None + tile_mix_cube_loop: int = None + disable_auto_inject_block_sync: bool = None + enable_mixed_cv: bool = None + enable_dynamic_cv_pipeline: bool = None + enable_cube_block_merge: bool = False + hfusion_enable_multiple_consumer_fusion: bool = None + buf_slot_num_of_veccore: int = None + buf_slot_num_of_crosscore: int = None + buf_slot_num_of_gm: int = None + + # plan memory strategy: "default" (default) or "largest-first" + plan_memory_strategy: str = None + # Internal launch metadata. The mode is initially derived from + # compile_mode, then replaced with the mode emitted by TritonToLinalg. + parallel_mode: str = field(default="simd", init=False) + # Internal pure-SIMT state, derived from the effective lowering mode. + is_pure_simt: bool = field(default=False, init=False) + # Only takes effect on the pure-SIMT path. + shared_mem_dynamic_size: int = None + # Ascend 950 pure-SIMT-only option passed as -simt-optimization-mode + # to bishengir-compile. Its value grammar belongs to the toolchain. + # Individual digits are passed to various passes to control behavior, + # and are parsed right-to-left. + # For example, a value of 101 is interpreted as 0000101. + # If left as 0, bishengir-compile sets this to 900101 + simt_optimization_mode: int = 0000000 + # Canonical modes: SIMD (D), SIMD with template-SIMT (P), and pure-SIMT + # (T). ``unstructured_in_simt`` is an equivalent P spelling. + compile_mode: str = "simd_simt_template" + # disable simt fma optimization to get high precision + disable_fma: bool = False + + # superblocking factor + superblock_factor: int = 1 + + # ChunkCoalescing: number of tiles along the outermost grid axis. + # Auto-injected from static grid tuples; enables safe coalescing for + # unmasked kernels whose grid dims are compile-time known. + grid_num_tiles: int = None + + # Deprecated names remain visible to callers that use parse_options({}) + # to distinguish compile options from kernel arguments. Their public + # values are ignored (or routed to canonical options) before construction. + add_auto_scheduling: bool = field(default=False, init=False) + allow_fp8e4nv: bool = field(default=False, init=False) + auto_blockify_size: int = field(default=1, init=False) + auto_tile_and_bind_subblock: bool = field(default=True, init=False) + code_motion: Optional[bool] = field(default=None, init=False) + enable_auto_blockify: Optional[bool] = field(default=None, init=False) + enable_bishengir_simt_optimization: int = field(default=0, init=False) + enable_buffer_insert_optimization: bool = field(default=True, init=False) + enable_cce_vf_auto_sync: Optional[bool] = field(default=None, init=False) + enable_cce_vf_remove_membar: Optional[bool] = field(default=None, init=False) + enable_cross_if_fusion: bool = field(default=False, init=False) + enable_drop_unit_dims: Optional[bool] = field(default=None, init=False) + enable_linearize: Optional[bool] = field(default=None, init=False) + enable_mask_fallback_conversion: bool = field(default=False, init=False) + enable_nd2nz_on_vector: bool = field(default=False, init=False) + enable_reduce_sum_strength: bool = field(default=False, init=False) + enable_select_analysis: bool = field(default=True, init=False) + enable_simt_reorder_instruction: bool = field(default=False, init=False) + enable_sync_block_lock: Optional[bool] = field(default=None, init=False) + enable_ub_refine_opt: bool = field(default=False, init=False) + enable_vf_fusion: Optional[bool] = field(default=None, init=False) + force_simt_only: bool = field(default=False, init=False) + force_simt_template: bool = field(default=False, init=False) + graph_optimize_emit_remarks: bool = field(default=False, init=False) + graph_optimize_max_rewrites_per_function: int = field(default=64, init=False) + graph_optimize_rule_mask: int = field(default=DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK, init=False) + graph_optimize_ub_capacity_bytes: Optional[int] = field(default=None, init=False) + has_auto_blockify_blacklist_op: Optional[bool] = field(default=None, init=False) + inter_cache_num: Optional[int] = field(default=None, init=False) + intra_cache_num: Optional[int] = field(default=None, init=False) + kernel_name: str = field(default="triton_", init=False) + llvm_version: int = field(default=15, init=False) + load_cache_num: Optional[int] = field(default=None, init=False) + mix_mode: str = field(default="", init=False) + ops_reorder: Optional[bool] = field(default=None, init=False) + optimize_dynamic_offset: bool = field(default=False, init=False) + simt_reorder_instruction: bool = field(default=False, init=False) + storage_align: Optional[bool] = field(default=None, init=False) + stream: Optional[int] = field(default=None, init=False) + use_bytecode: bool = field(default=True, init=False) + + def __post_init__(self, arch, rule_mask): + from triton.backends.ascend import _apply_ascend_patch + + _apply_ascend_patch() + object.__setattr__(self, "target_arch", arch) + # Plain init=False defaults live on the class. Materialize them in + # the instance because both Inductor and JIT inspect options.__dict__. + for name, option_field in self.__dataclass_fields__.items(): + if not option_field.init and name not in self.__dict__: + object.__setattr__(self, name, option_field.default) + # Keep the legacy name discoverable while its property and all + # compiler decisions continue to use the injected target architecture. + self.__dict__["arch"] = arch + object.__setattr__(self, "multibuffer_mode", _multibuffer_mode_to_tuple(self.multibuffer_mode)) + if self.multibuffer_mode is not None and self.num_stages is not None: + raise ValueError("num_stages and multibuffer_mode cannot be specified together") + if self.multibuffer_mode is None and self.num_stages is None: + object.__setattr__(self, "num_stages", 2) + if self.compile_on_910_95 is not None: + _warn_deprecated_npu_option("compile_on_910_95") + object.__setattr__( + self, + "compile_on_910_95", + isinstance(arch, str) and arch.startswith(("Ascend910_95", "Ascend950")), + ) + try: + normalized_rule_mask = normalize_graph_optimization_rule_mask(rule_mask) + except ProgramGridContractError as error: + raise ValueError(f"invalid GraphOptimize rule_mask: {error}") from error + if normalized_rule_mask != DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK: + object.__setattr__(self, "rule_mask", normalized_rule_mask) + + compile_mode = str(_normalize_compile_mode(self.compile_mode, arch)) + object.__setattr__(self, "compile_mode", compile_mode) + + if compile_mode == "simt_only": + object.__setattr__(self, "is_pure_simt", True) + object.__setattr__(self, "parallel_mode", "simt") + else: + object.__setattr__(self, "parallel_mode", "simd") + + if self.is_pure_simt: + if self.shared_mem_dynamic_size is None: + object.__setattr__(self, "shared_mem_dynamic_size", 122880) + else: + object.__setattr__(self, "shared_mem_dynamic_size", 221184) + + def hash(self): + key = "_".join([f"{name}-{val}" for name, val in self.__dict__.items()]) + key = "_".join([key, get_cann_version_file_hash()]) + return hashlib.sha256(key.encode("utf-8")).hexdigest() + + +def _initialize_ascend_builder(main_builder, context, options): + """Attach the ascend and buffer builder methods onto the core builder. + + Registered as the ``initialize_builder`` codegen function so that + ``CodeGenerator`` can bridge the extension methods onto the plain + ``ir.builder`` it constructs. ``options`` is an :class:`NPUOptions` on the + real compile path, but callers that drive ``ast_to_ttir`` directly (the + unittests under ``third_party/ascend/unittest``) pass a minimal stand-in, + so the two fields read here are resolved defensively rather than as + attributes: ``arch`` is the public alias of ``target_arch`` (see + ``_get_npu_options_arch``) and ``parallel_mode`` defaults to the same + ``"simd"`` as the dataclass field. + """ + from triton.language.extra.cann.extension.builder import setup_unified_builder + from triton.extension.buffer.language.builder import setup_unified_builder_with_buffer_builder + + target_arch = getattr(options, "target_arch", None) + if target_arch is None: + # Match the C++ binding's optional target="" for minimal callers. + # This leaves the target unspecified; it does not infer a device arch. + target_arch = getattr(options, "arch", None) or "" + # The C++ builder expects "simd" or "simt", not "simt_only". + parallel_mode = getattr(options, "parallel_mode", "simd") + ascend_builder = ascend.ir.ascendnpu_ir_builder(context, target_arch, parallel_mode) + setup_unified_builder(main_builder, ascend_builder) + setup_unified_builder_with_buffer_builder(main_builder, buffer_ir.buffer_builder(context)) + + +def _get_npu_options_arch(options: NPUOptions) -> str: + """Expose the injected target to the established lowering builder API. + + ``arch`` is an ``InitVar`` rather than a user compile option. Its legacy + name is also serialized for option discovery, while this view always + returns the target injected by ``AscendBackend.parse_options``. + """ + return options.target_arch + + +NPUOptions.arch = property(_get_npu_options_arch) + +_INTERNAL_NPU_OPTION_MARKERS = frozenset({ + "target_arch", + "compile_on_910_95", + "parallel_mode", + "is_pure_simt", +}) + + +def _is_internal_npu_options(options, target_arch: str) -> bool: + """Recognize an NPUOptions metadata round trip by backend-only fields.""" + return (_INTERNAL_NPU_OPTION_MARKERS <= options.keys() and options.get("target_arch") == target_arch + and options.get("compile_on_910_95") == _is_a5_target_arch(target_arch)) + + +def _normalize_bishengir_simt_optimization_for_context(options: NPUOptions, raw_options) -> None: + """Restrict the vendor SIMT optimization switch to its Ascend 950 pure-SIMT path.""" + option_name = "simt_optimization_mode" + if option_name not in raw_options: + return + + if _is_a5_target_arch(options.target_arch) and options.is_pure_simt: + # The BiShengIR toolchain owns the option's value grammar; keep the + # Python boundary free of numeric or range validation. + return + + warnings.warn( + "simt_optimization_mode only takes effect for Ascend 950 " + "pure-SIMT compilation; ignoring the explicit value.", + UserWarning, + stacklevel=3, + ) + object.__setattr__(options, option_name, 0) + + +def ttir_to_npubin(mod, metadata, opt): + _export_program_grid_metadata(mod, metadata, require_row_contract=True) + ttir_code = _serialize_module(mod, opt) + metadata = _parse_ttir_metadata(ttir_code, metadata) + _finalize_program_launch_policy(metadata, opt) + + with tempfile.TemporaryDirectory() as tmpdir: + # prepare input + src_path = os.path.join(tmpdir, "kernel.ttir.mlir") + Path(src_path).write_text(ttir_code) + # prepare output + bin_file = os.path.join(tmpdir, "kernel") + bin_path = os.path.join(tmpdir, "kernel.o") + # build compile options + _compile_option_list = get_common_bishengir_compile_options(metadata) + if opt.is_pure_simt: + _compile_option_list += ["--enable-hivm-compile=false"] + _compile_option_list += ["--enable-triton-ir-compile"] + _compile_option_list += ["--pure-simt"] + _compile_option_list += [f"--num-warps={opt.num_warps}"] + _compile_option_list += [f"--threads-per-warp={opt.warp_size}"] + if opt.simt_optimization_mode != 0000000: + _compile_option_list += [f"--simt-optimization-mode={opt.simt_optimization_mode}"] + _compile_option_list += [f"--simt-stack-limit={get_simt_stack_limit()}"] + if opt.shared_mem_dynamic_size is not None: + _compile_option_list += [f"--shared-mem-dynamic-size={opt.shared_mem_dynamic_size}"] + if opt.disable_fma: + _compile_option_list += [f"--disable-fma"] + if opt.compile_on_910_95: + npu_utils = NPUUtils() + if npu_utils.has_device_limit(): + _compile_option_list += [ + f"--custom-aic-number={npu_utils.get_aicore_num()}", + f"--custom-aiv-number={npu_utils.get_aivector_core_num()}", + ] + + bisheng_options = metadata["bisheng_options"] + if bisheng_options is not None: + _compile_option_list += [f"--append-bisheng-options={bisheng_options}"] + + if _is_auto_map_parallel_blocks_enabled() and not metadata.get("row_coalescing_applied", False): + _compile_option_list += ["--enable-auto-blockify-loop"] + if opt.superblock_factor > 1: + _compile_option_list += [f"--super-block-factor={opt.superblock_factor}"] + + npu_compiler_path, env = _get_npucompiler_path() + cmd_list = ([npu_compiler_path, src_path] + _compile_option_list + ["-o", bin_file]) + ret = subprocess.run(cmd_list, env=env, capture_output=True, check=True) + if not Path(bin_path).exists(): + error_msg = ret.stderr.decode('utf-8') + print(f"[DEBUG] {bin_path} is not found") + print(f"[DEBUG] Stderr:\n{error_msg}") + raise subprocess.CalledProcessError(ret.returncode, cmd_list, ret.stdout, ret.stderr) + return Path(bin_path).read_bytes() + + +def get_simt_stack_limit(): + # simt_stack_limit resolution precedence: + # 1. torch_npu's acl_default.json "StackSize":{"simt_stack_size":N}. + # 2. The kernel-time default simt_stack_limit=1152. + _simt_stack_limit = 1152 + try: + import torch_npu + torch_npu_basic_path = os.path.dirname(torch_npu.__file__) + _acl_cfg_path = os.path.join(torch_npu_basic_path, "acl_default.json") + with open(_acl_cfg_path, "r") as f: + _acl_cfg = json.load(f) + _cfg_stack = _acl_cfg.get("StackSize", {}).get("simt_stack_size", None) + if isinstance(_cfg_stack, int) and not isinstance(_cfg_stack, bool) and _cfg_stack > 0: + _simt_stack_limit = _cfg_stack + except Exception as e: + print(f"[DEBUG] read acl_default.json failed: {e}") + return _simt_stack_limit + + +class AscendBackend(BaseBackend): + + @staticmethod + def supports_target(target: GPUTarget): + return target.backend == "npu" + + @staticmethod + def use_alignment_specialization(options: dict) -> bool: + # The binder runs before parse_options. Normalize its option copy so + # legacy selectors affect alignment specialization and the cache key. + normalized = _remove_deprecated_npu_options(options, in_place=True) + return normalized.get("compile_mode") == "simt_only" + + def __init__(self, target: GPUTarget) -> None: + super().__init__(target) + if target.backend == "npu": + self.binary_ext = "npubin" + # Include all binary file extensions (mlirbc is always emitted for normal kernels). + self.binary_extensions = {"npubin", "mlirbc"} + + def parse_options(self, opts) -> Any: + # TODO: get available targets when building options? + if self.target.backend == "npu": + _warn_deprecated_ascend_env_vars() + option_names = { + name + for name, option_field in NPUOptions.__dataclass_fields__.items() + if option_field.init and name not in {"arch", "compile_on_910_95"} + } + # Serialized NPUOptions include backend-only derived fields. Use + # those provenance markers instead of depending on every public + # field being present, which changes whenever the dataclass evolves. + internal_options = _is_internal_npu_options(opts, self.target.arch) + # Normalize a copy, except for the legacy launch keyword below. + # NPUOptions retains compatibility defaults for legacy names so + # JIT and Inductor can still recognize them as compile options. + normalized_opts = opts if internal_options else _remove_deprecated_npu_options(opts) + args = {k: normalized_opts[k] for k in option_names if k in normalized_opts} + options = NPUOptions(arch=self.target.arch, **args) + # Lazy init enable_dynamic_cv_pipeline if not provided. + # compile_on_910_95 is already resolved from the requested target. + if options.enable_dynamic_cv_pipeline is None: + object.__setattr__(options, "enable_dynamic_cv_pipeline", options.compile_on_910_95) + if not internal_options: + _normalize_bishengir_simt_optimization_for_context(options, normalized_opts) + # Community JIT rejects the legacy launch keyword "stream" + # even when options.__dict__ recognizes it. Consume it at + # the Ascend boundary; the returned compatibility field stays + # None and the runtime still selects the current stream. + opts.pop("stream", None) + else: + raise NotImplementedError(f"Backend '{self.target.backend}' is not supported. " + "Please ensure the target backend is set to 'npu'.") + return options + + def pack_metadata(self, metadata): + # collect necessary metadata to launch kernels + # TORCHINDUCTOR_UNIQUE_KERNEL_NAMES=1 could set unique name. + # Get this name as the kernel_name to CANN runtime. + # kernel_name is unique to Ascend backend and should not be public. + # CANN runtime limits the length of kernel name <= 50. + # Considering '\n' is appended, thus the real kernel name <= 49. + KERNEL_NAME_MAX_LEN = 49 + kernel_name_orig = metadata.kernel_name + if len(kernel_name_orig) > KERNEL_NAME_MAX_LEN: + kernel_name = kernel_name_orig[-KERNEL_NAME_MAX_LEN:] + else: + kernel_name = kernel_name_orig + return { + "kernel_name": kernel_name, + "hash": metadata.hash, + "debug": metadata.debug, + "tensor_kinds": metadata.tensor_kinds, + } + + def get_codegen_implementation(self, options): + from triton.language.extra.cann.extension.scope import scope + from triton.language.extra.cann.extension.code_generator import handle_scope_with + + # Note: a dict of functions is required to generate vendor-specific code piecies + # e.g. convert custom types like fp8e4b15 + codegen_fns = { + "min_dot_size": min_dot_size(self.target), + "initialize_builder": _initialize_ascend_builder, + "with_handlers": { + scope: handle_scope_with, + }, + } + return codegen_fns + + def load_dialects(self, ctx): + from triton._C.libtriton import buffer_ir + from triton._C.libtriton.ascend import ir as ascend_ir + if distributed is not None: + distributed.ir.load_dialects(ctx) + buffer_ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + ascend.load_dialects(ctx) + + def add_stages(self, stages, options, language): + if self.target.backend != "npu": + raise NotImplementedError(f"Backend '{self.target.backend}' is not supported. " + "Please ensure the target backend is set to 'npu'.") + stages["ttir"] = lambda src, metadata: make_ttir(src, metadata, options) + if options.is_pure_simt: + stages["npubin"] = (lambda src, metadata: ttir_to_npubin(src, metadata, options)) + else: + stages["ttadapter"] = lambda src, metadata: ttir_to_linalg(src, metadata, options) + # Normal kernels always convert Linalg IR to bytecode and back to MLIR text. + stages["mlirbc"] = lambda src, metadata: linalg_to_bc_by_triton_mlir_opt(src, metadata, options) + stages["bcmlir"] = lambda src, metadata: bc_to_linalg_by_bishengir_opt(src, metadata, options) + if options.compile_on_910_95: + stages["npubin"] = ( + lambda src, metadata: linalg_to_bin_enable_npu_compile_910_95(src, metadata, options)) + else: + stages["npubin"] = ( + lambda src, metadata: linalg_to_bin_enable_npu_compile_A2_A3(src, metadata, options)) + stages["npubin"] = _with_debug_line(stages["npubin"], options) + # Only the stages that shell out to BiShengIR tools. ttir runs the pass + # manager in process, and stages the caller supplied are not ours to + # rewrite. _STAGE_NAMES covers both paths, pure SIMT npubin included. + for ext in _STAGE_NAMES: + if ext in stages: + stages[ext] = _with_tool_diagnostics(ext, stages[ext]) + + @functools.lru_cache() + def hash(self): + # TODO fetch compiler version + version_key = (self.target, _get_libdevice_compile_state(self.target.arch)) + return str(version_key) + + def get_module_map(self) -> Dict[str, ModuleType]: + from triton.language.extra.cann import libdevice + + return {"triton.language.extra.libdevice": libdevice} diff --git a/third_party/ascend/backend/cpu_driver.py b/third_party/ascend/backend/cpu_driver.py new file mode 100644 index 0000000000..86ff502436 --- /dev/null +++ b/third_party/ascend/backend/cpu_driver.py @@ -0,0 +1,203 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from triton.runtime.cache import get_cache_manager, get_dump_manager +from pathlib import Path +import tempfile +import os +import sysconfig +import subprocess +import importlib +from triton.backends.ascend.utils import _get_cxx + + +# TODO: temporarily fake CPUUtils class +class CPUUtils(object): + + def __new__(cls): + if not hasattr(cls, 'instance'): + cls.instance = super(CPUUtils, cls).__new__(cls) + return cls.instance + + def __init__(self): + pass + + def get_device_properties(self, device): + # temperoarily added properties to avoid triton-compiler complain + # fetch available memory at runtime + return {"max_shared_mem": 1} + + def load_binary(self, name, kernel, shared, device, mix_mode=None): + # TODO (temperoarily fake function) load a binary from binary object to device + # return value are: (mod, funcptr/handle, n_regs, n_spills) + return None, kernel, 0, 0 + + +class CPULauncher(object): + + def __init__(self, src, metadata): + kernel_name = metadata.name.split()[0] + signature = src.signature + constants = src.constants + launcher_src = generate_cpu_wrapper_src(constants, signature, kernel_name) + self.launch = compile_module(launcher_src) + + def __call__(self, *args, **kwargs): + self.launch(*args, **kwargs) + + +class CPUDriver: + + def __init__(self): + self.utils = CPUUtils() + self.launcher_cls = CPULauncher + super().__init__() + + def get_current_target(self): + # TODO: do we rely on CPU arch? + return ("cpu", "arm-64") + + def get_current_device(self): + """ + Get current device + """ + # TODO: dummy device-getter for cpu backend + return 0 + + def set_current_device(self, device): + """ + Set current device as the given device + """ + # TODO: dummy device-setter for cpu backend + return + + def get_current_stream(self, device): + """ + Get stream for current device + """ + # TODO: dummy stream api for cpu backend. + return 0 + + +# the template is from triton-adapter HEAD. Wrapping the generated kernel assembly into a python module +def generate_cpu_wrapper_src(constants, signature, kernel_name): + + def _ty_to_cpp(ty): + if ty[0] == '*': + return "void*" + return { + "i1": "int32_t", + "i8": "int8_t", + "i16": "int16_t", + "i32": "int32_t", + "i64": "int64_t", + "u32": "uint32_t", + "u64": "uint64_t", + "fp16": "float", + "bf16": "float", + "fp32": "float", + "f32": "float", + "fp64": "double", + }[ty] + + def _extracted_ty(ty): + if ty[0] == '*': + return "PyObject*" + return { + 'i1': 'int32_t', + 'i32': 'int32_t', + 'i64': 'int64_t', + 'u32': 'uint32_t', + 'u64': 'uint64_t', + 'fp16': 'float', + 'bf16': 'float', + 'fp32': 'float', + 'f32': 'float', + 'fp64': 'double', + }[ty] + + def _format_of(ty): + return { + "PyObject*": "O", + "float": "f", + "double": "d", + "long": "l", + "uint32_t": "I", + "int32_t": "i", + "uint64_t": "K", + "int64_t": "L", + }[ty] + + def _generate_launcher(constants, signature, kernel_name): + arg_decls = ', '.join(f"{_ty_to_cpp(ty)} arg{i}" for i, ty in signature.items()) + format = "iiiOOO" + ''.join([_format_of(_extracted_ty(ty)) for ty in signature.values()]) + # to be filled + return f""" + """ + + launcher_src = _generate_launcher(constants, signature, kernel_name) + return launcher_src + + +def compile_module(launcher_src): + # This function was renamed and made public in Python 3.10 + if hasattr(sysconfig, 'get_default_scheme'): + scheme = sysconfig.get_default_scheme() + else: + scheme = sysconfig._get_default_scheme() + # 'posix_local' is a custom scheme on Debian. However, starting Python 3.10, the default install + # path changes to include 'local'. This change is required to use triton with system-wide python. + if scheme == 'posix_local': + scheme = 'posix_prefix' + py_include_dir = sysconfig.get_paths(scheme=scheme)["include"] + + def launch(gridX, gridY, gridZ, stream, cu_function, packed_metadata, launch_metadata, launch_enter_hook, + launch_exit_hook, *args): + kernel_name = packed_metadata["kernel_name"] + cache = get_cache_manager(packed_metadata["hash"]) + filename = f"{kernel_name}_cpu_launcher.so" + cache_path = cache.get_file(filename) + if cache_path is None: + asm_src = cu_function + with tempfile.TemporaryDirectory() as tmpdir: + asm_src_path = os.path.join(tmpdir, "kernel.s") + launcher_src_path = os.path.join(tmpdir, "main.cxx") + if packed_metadata["debug"]: + dump_manager = get_dump_manager(packed_metadata["hash"]) + dump_manager.put(launcher_src, "kernel_cpu_launcher.cxx", binary=False) + so_path = os.path.join(tmpdir, "kernel.so") + Path(asm_src_path).write_bytes(asm_src) + Path(launcher_src_path).write_text(launcher_src) + # Compile it together. + subprocess.check_call([ + _get_cxx(), launcher_src_path, asm_src_path, f"-I{py_include_dir}", + f"-I{Path(__file__).resolve().parent}", "-shared", "-fPIC", "-o", so_path + ]) + + with open(so_path, "rb") as f: + cache_path = cache.put(f.read(), filename, binary=True) + + # Load and launch the compiled kernel. + spec = importlib.util.spec_from_file_location("__triton_adapter_ref_cpu_kernel_launcher", cache_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.launch(gridX, gridY, gridZ, launch_enter_hook, launch_exit_hook, packed_metadata, *args) + + return launch diff --git a/third_party/ascend/backend/debug_line_rewriter.py b/third_party/ascend/backend/debug_line_rewriter.py new file mode 100644 index 0000000000..2603b9c537 --- /dev/null +++ b/third_party/ascend/backend/debug_line_rewriter.py @@ -0,0 +1,724 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Post-process the DWARF ``.debug_line`` table of a compiled Ascend kernel so +that ``msdebug`` steps once per user source line. + +bishengir emits one ``is_stmt`` row per *static* occurrence of a source line. +Instruction scheduling and inlining duplicate those rows (one statement lowered +to several scattered instruction groups, loop bodies, inlined device-library +frames), so plain ``next`` stepping stops repeatedly on the same line and dives +into compiler-generated code. This module removes the redundant stops while +preserving genuine re-entries (loop back-edges). + +It complements the MLIR passes ``CanonicalizeDebugLocationsPass`` and +``DeduplicateDebugNopsPass``: those clean up locations *before* bishengir; this +cleans up the residual scatter bishengir introduces *after* them, at the binary +level. + +Mechanism (length-preserving, relocation-safe) + bishengir encodes the line program as a single relocated + ``DW_LNE_set_address`` followed by ``DW_LNS_fixed_advance_pc`` per row, + whose operands are symbol differences patched by ``.rela.debug_line``. + Regenerating the program would reorder those relocations and corrupt the + table, so instead each dropped stop has its ``DW_LNS_copy`` opcode (0x01) + overwritten with ``DW_LNS_set_basic_block`` (0x07) -- both one-byte, zero + argument. ``set_basic_block`` emits no row, so the stop disappears and its + address folds into the previous row. No byte shifts, so ``.rela.debug_line`` + is never touched and stays valid. + +Demotion rules, applied to ``is_stmt=1`` rows in program order: + 1. Scatter -- within a sequence keep the first row per ``(file, line)``, + drop the rest. + 2. Loops -- keep every occurrence of a ``for``/``while`` header line + (detected from the source AST), so the back-edge re-stops each iteration. + 3a. Foreign -- drop rows whose file is not the user source file. + 3b. line==0 -- drop compiler-generated rows. + 3c. Over-len -- drop rows whose line exceeds the source length (safety net + for binaries built before ``CanonicalizeDebugLocationsPass`` or without + ``--enable-ms-debug``; normally a no-op). + +Rows emitted by a DWARF special opcode (e.g. the line-0 function-entry row) +cannot be edited length-preserving and are left as-is; this is harmless because +execution begins there and ``next`` never returns to it. + +Public API + rewrite_debug_line(artifact, metadata=None, options=None) + Pipeline entry. No-op unless ``LLVM_EXTRACT_DI_LOCAL_VARIABLES`` is set. + Accepts the ``bytes`` (or path) produced by the ``npubin`` stage and + returns the rewritten ``bytes`` (or the same path, patched in place). + Exception-safe: any failure logs and returns the artifact unchanged. + + rewrite_debug_line_blob(blob, src_path=None) -> (bytes, RewriteResult) + Pure in-memory transform (no env gate); the unit-testable core. + + rewrite_debug_line_file(path, src_path=None, out_path=None) -> RewriteResult + File convenience used by the CLI; writes atomically. +""" + +from __future__ import annotations + +import argparse +import ast +import copy +import io +import logging +import os +import struct +import sys +import tempfile +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Tuple + +try: + from elftools.elf.elffile import ELFFile +except ImportError: # surfaced lazily; importing this module must never fail + ELFFile = None + +log = logging.getLogger(__name__) + +ENV_FLAG = "LLVM_EXTRACT_DI_LOCAL_VARIABLES" + +# ── DWARF line-program opcodes ──────────────────────────────────────────────── +DW_LNS_COPY = 0x01 +DW_LNS_ADVANCE_PC = 0x02 +DW_LNS_ADVANCE_LINE = 0x03 +DW_LNS_SET_FILE = 0x04 +DW_LNS_SET_COLUMN = 0x05 +DW_LNS_NEGATE_STMT = 0x06 +DW_LNS_SET_BASIC_BLOCK = 0x07 +DW_LNS_CONST_ADD_PC = 0x08 +DW_LNS_FIXED_ADVANCE_PC = 0x09 +DW_LNS_SET_PROLOGUE_END = 0x0A +DW_LNS_SET_EPILOGUE_BEGIN = 0x0B +DW_LNS_SET_ISA = 0x0C +DW_LNE_END_SEQUENCE = 0x01 +DW_LNE_SET_ADDRESS = 0x02 +DW_LNE_DEFINE_FILE = 0x03 +DW_LNE_SET_DISCRIMINATOR = 0x04 + + +@dataclass +class RewriteResult: + """Outcome of a rewrite attempt.""" + changed: bool + demoted: int = 0 + before: List[int] = field(default_factory=list) + after: List[int] = field(default_factory=list) + counts: Dict[str, int] = field(default_factory=dict) + reason: str = "" + + +# ── LEB128 ──────────────────────────────────────────────────────────────────── + + +def _uleb(data: bytes, off: int, limit: Optional[int] = None) -> Tuple[int, int]: + """Decode ULEB128 without reading past ``limit``.""" + limit = len(data) if limit is None else min(limit, len(data)) + result = shift = 0 + while True: + if off >= limit: + raise ValueError(f"truncated ULEB128 at .debug_line+{off:#x} (limit {limit:#x})") + byte = data[off] + off += 1 + result |= (byte & 0x7F) << shift + shift += 7 + if not byte & 0x80: + return result, off + if shift > 128: + raise ValueError(f"overlong ULEB128 ending near .debug_line+{off:#x}") + + +def _sleb(data: bytes, off: int, limit: Optional[int] = None) -> Tuple[int, int]: + """Decode SLEB128 without reading past ``limit``.""" + limit = len(data) if limit is None else min(limit, len(data)) + result = shift = 0 + while True: + if off >= limit: + raise ValueError(f"truncated SLEB128 at .debug_line+{off:#x} (limit {limit:#x})") + byte = data[off] + off += 1 + result |= (byte & 0x7F) << shift + shift += 7 + if not byte & 0x80: + if shift < 64 and byte & 0x40: + result |= -(1 << shift) + return result, off + if shift > 128: + raise ValueError(f"overlong SLEB128 ending near .debug_line+{off:#x}") + + +# ── ELF access (navigation only; the DWARF resolver is never invoked because it +# chokes on the .npubin's AArch64 relocations and address_size=0) ─────────── + + +def _read_sections(stream) -> Tuple[Dict[str, dict], bool]: + if ELFFile is None: + raise RuntimeError("pyelftools is required (pip install pyelftools)") + elf = ELFFile(stream) + little = elf.little_endian + sections: Dict[str, dict] = {} + for name in (".debug_line", ".rela.debug_line"): + sec = elf.get_section_by_name(name) + if sec is not None: + sections[name] = { + "data": sec.data(), + "offset": sec["sh_offset"], + "size": sec["sh_size"], + } + return sections, little + + +# ── line-program header ─────────────────────────────────────────────────────── + + +def _require(data: bytes, off: int, size: int, limit: int, what: str) -> None: + if off < 0 or size < 0 or off + size > min(limit, len(data)): + raise ValueError(f"truncated {what} at .debug_line+{off:#x}: " + f"need {size} byte(s), limit {min(limit, len(data)):#x}") + + +def _cstring(data: bytes, off: int, limit: int, what: str) -> Tuple[str, int]: + _require(data, off, 1, limit, what) + end = data.find(b"\0", off, limit) + if end < 0: + raise ValueError(f"unterminated {what} at .debug_line+{off:#x}") + return data[off:end].decode("utf-8", "replace"), end + 1 + + +def _parse_header(data: bytes, little: bool, unit_offset: int = 0) -> dict: + """Parse one line-table unit beginning at ``unit_offset``. + + The initial length supplies a hard ``unit_end`` boundary. Linked Ascend + kernels commonly contain several line units (user code plus libdevice), so + replaying until the end of the whole section is incorrect. + """ + endian = "<" if little else ">" + off = unit_offset + _require(data, off, 4, len(data), "initial length") + unit_length, = struct.unpack_from(endian + "I", data, off) + off += 4 + is_dwarf64 = unit_length == 0xFFFFFFFF + if is_dwarf64: + _require(data, off, 8, len(data), "64-bit unit length") + unit_length, = struct.unpack_from(endian + "Q", data, off) + off += 8 + elif unit_length >= 0xFFFFFFF0: + raise ValueError(f"reserved DWARF initial length {unit_length:#x} at .debug_line+{unit_offset:#x}") + + unit_end = off + unit_length + if unit_end > len(data): + raise ValueError(f"line unit at .debug_line+{unit_offset:#x} ends at {unit_end:#x}, " + f"past section size {len(data):#x}") + + _require(data, off, 2, unit_end, "DWARF version") + version, = struct.unpack_from(endian + "H", data, off) + off += 2 + if version < 2 or version > 5: + raise ValueError(f"unsupported DWARF line version {version} at .debug_line+{unit_offset:#x}") + + address_size = 8 + segment_selector_size = 0 + if version >= 5: + _require(data, off, 2, unit_end, "DWARF v5 address sizes") + address_size = data[off] + off += 1 + segment_selector_size = data[off] + off += 1 + + length_size = 8 if is_dwarf64 else 4 + _require(data, off, length_size, unit_end, "header length") + header_length, = struct.unpack_from(endian + ("Q" if is_dwarf64 else "I"), data, off) + off += length_size + program_start = off + header_length + if program_start > unit_end: + raise ValueError(f"line header at .debug_line+{unit_offset:#x} ends at {program_start:#x}, " + f"past unit end {unit_end:#x}") + + _require(data, off, 1, program_start, "minimum_instruction_length") + min_inst_len = data[off] + off += 1 + if version >= 4: + _require(data, off, 1, program_start, "maximum_operations_per_instruction") + off += 1 + _require(data, off, 4, program_start, "line header fields") + default_is_stmt = data[off] + off += 1 + line_base = struct.unpack_from("b", data, off)[0] + off += 1 + line_range = data[off] + off += 1 + opcode_base = data[off] + off += 1 + if line_range == 0: + raise ValueError(f"zero line_range in unit at .debug_line+{unit_offset:#x}") + if opcode_base == 0: + raise ValueError(f"zero opcode_base in unit at .debug_line+{unit_offset:#x}") + _require(data, off, opcode_base - 1, program_start, "standard opcode lengths") + standard_opcode_lengths = [0] + [data[off + i] for i in range(opcode_base - 1)] + off += opcode_base - 1 + + include_dirs = [""] + file_names = [""] + if version <= 4: + while True: + directory, off = _cstring(data, off, program_start, "include directory") + if not directory: + break + include_dirs.append(directory) + while True: + filename, off = _cstring(data, off, program_start, "file name") + if not filename: + break + file_names.append(filename) + _, off = _uleb(data, off, program_start) + _, off = _uleb(data, off, program_start) + _, off = _uleb(data, off, program_start) + # DWARF v5 has format-described directory/file tables. Their extent is + # already known from header_length; do not parse them as legacy C strings. + + return { + "unit_start": unit_offset, + "unit_end": unit_end, + "is_dwarf64": is_dwarf64, + "little": little, + "version": version, + "address_size": address_size, + "segment_selector_size": segment_selector_size, + "min_inst_len": min_inst_len, + "default_is_stmt": default_is_stmt, + "line_base": line_base, + "line_range": line_range, + "opcode_base": opcode_base, + "standard_opcode_lengths": standard_opcode_lengths, + "include_dirs": include_dirs, + "file_names": file_names, + "program_start": program_start, + } + + +# ── line-number state machine ───────────────────────────────────────────────── + + +def _simulate(data: bytes, hdr: dict) -> List[List[dict]]: + """Replay the line program. Each row records ``emit_off`` (byte offset of the + opcode that produced it) and ``emit_kind`` ('copy' | 'special' | 'end').""" + ob = hdr["opcode_base"] + lb = hdr["line_base"] + lr = hdr["line_range"] + mil = hdr["min_inst_len"] + sol = hdr["standard_opcode_lengths"] + default_stmt = bool(hdr["default_is_stmt"]) + unit_end = hdr.get("unit_end", len(data)) + little = hdr.get("little", True) + endian = "<" if little else ">" + + def fresh() -> dict: + return dict(addr=0, file=1, line=1, col=0, is_stmt=default_stmt, end_sequence=False) + + sequences: List[List[dict]] = [] + current: List[dict] = [] + state = fresh() + off = hdr["program_start"] + + while off < unit_end: + op_off = off + opcode = data[off] + off += 1 + + if opcode == 0: # extended + length, off = _uleb(data, off, unit_end) + if length < 1: + raise ValueError(f"zero-length extended opcode at .debug_line+{op_off:#x}") + payload_end = off + length + _require(data, off, length, unit_end, "extended opcode payload") + sub = data[off] + off += 1 + if sub == DW_LNE_END_SEQUENCE: + row = copy.copy(state) + row["end_sequence"] = True + row["emit_off"] = op_off + row["emit_kind"] = "end" + current.append(row) + sequences.append(current) + current = [] + state = fresh() + elif sub == DW_LNE_SET_ADDRESS: + # Trust the extended-op length. Bisheng may advertise + # address_size=0 while carrying an 8-byte relocated operand. + operand_size = payload_end - off + if operand_size <= 0: + raise ValueError(f"empty set_address at .debug_line+{op_off:#x}") + advertised_size = hdr["address_size"] + if advertised_size not in (0, operand_size): + log.debug("debug-line: unit %#x set_address has %d byte(s), header advertises %d", + hdr.get("unit_start", 0), operand_size, advertised_size) + state["addr"] = int.from_bytes(data[off:payload_end], "little" if little else "big") + elif sub == DW_LNE_SET_DISCRIMINATOR: + _, off = _uleb(data, off, payload_end) + elif sub == DW_LNE_DEFINE_FILE: + _, off = _cstring(data, off, payload_end, "DW_LNE_define_file name") + for _ in range(3): + _, off = _uleb(data, off, payload_end) + # Known and unknown extended opcodes both end at the declared + # payload boundary; any producer padding is skipped here. + off = payload_end + elif opcode < ob: # standard + if opcode == DW_LNS_COPY: + row = copy.copy(state) + row["emit_off"] = op_off + row["emit_kind"] = "copy" + current.append(row) + elif opcode == DW_LNS_ADVANCE_PC: + operand, off = _uleb(data, off, unit_end) + state["addr"] += operand * mil + elif opcode == DW_LNS_ADVANCE_LINE: + operand, off = _sleb(data, off, unit_end) + state["line"] += operand + elif opcode == DW_LNS_SET_FILE: + state["file"], off = _uleb(data, off, unit_end) + elif opcode == DW_LNS_SET_COLUMN: + state["col"], off = _uleb(data, off, unit_end) + elif opcode == DW_LNS_NEGATE_STMT: + state["is_stmt"] = not state["is_stmt"] + elif opcode == DW_LNS_SET_BASIC_BLOCK: + pass + elif opcode == DW_LNS_CONST_ADD_PC: + state["addr"] += ((255 - ob) // lr) * mil + elif opcode == DW_LNS_FIXED_ADVANCE_PC: + _require(data, off, 2, unit_end, "fixed_advance_pc operand") + operand, = struct.unpack_from(endian + "H", data, off) + off += 2 + state["addr"] += operand + elif opcode in (DW_LNS_SET_PROLOGUE_END, DW_LNS_SET_EPILOGUE_BEGIN): + pass + elif opcode == DW_LNS_SET_ISA: + _, off = _uleb(data, off, unit_end) + else: # unknown standard opcode: skip its ULEB operands + for _ in range(sol[opcode] if opcode < len(sol) else 0): + _, off = _uleb(data, off, unit_end) + else: # special + adj = opcode - ob + state["line"] += lb + (adj % lr) + state["addr"] += (adj // lr) * mil + row = copy.copy(state) + row["emit_off"] = op_off + row["emit_kind"] = "special" + current.append(row) + + if current: + sequences.append(current) + return sequences + + +def _decode_units(data: bytes, little: bool) -> List[Tuple[dict, List[List[dict]]]]: + """Parse and replay every DWARF line unit in section order.""" + units: List[Tuple[dict, List[List[dict]]]] = [] + off = 0 + while off < len(data): + # Some linkers leave all-zero padding at the end of debug sections. + if not any(data[off:]): + break + hdr = _parse_header(data, little, off) + units.append((hdr, _simulate(data, hdr))) + next_off = hdr["unit_end"] + if next_off <= off: + raise ValueError(f"non-advancing line unit at .debug_line+{off:#x}") + off = next_off + return units + + +# ── source analysis ─────────────────────────────────────────────────────────── + + +def _loop_header_lines(src_path: str) -> set: + with open(src_path, "r", encoding="utf-8") as handle: + tree = ast.parse(handle.read(), filename=src_path) + return {node.lineno for node in ast.walk(tree) if isinstance(node, (ast.For, ast.AsyncFor, ast.While))} + + +def _source_length(src_path: str) -> int: + with open(src_path, "r", encoding="utf-8") as handle: + return sum(1 for _ in handle) + + +def _auto_detect_source(file_names: List[str]) -> Optional[str]: + """First user (.py, not site-packages) file from the line table that exists + on disk, resolved relative to the current working directory.""" + for path in file_names: + if path.endswith(".py") and "/site-packages/" not in path and os.path.isfile(path): + return path + return None + + +def _user_file_indices(file_names: List[str], src_path: str) -> set: + base = os.path.basename(src_path) + return {i for i, p in enumerate(file_names) if os.path.basename(p) == base} + + +# ── demotion planning ───────────────────────────────────────────────────────── + + +def _plan_demotions(sequences, protected, user_files, src_lines): + demote: List[dict] = [] + kept: List[int] = [] + counts = {"dup": 0, "foreign": 0, "line0": 0, "over": 0, "special_skip": 0} + for sequence in sequences: + seen = set() + for row in sequence: + if row["end_sequence"] or not row["is_stmt"]: + continue + reason = None + if user_files is not None and row["file"] not in user_files: + reason = "foreign" + elif row["line"] == 0: + reason = "line0" + elif src_lines and row["line"] > src_lines: + reason = "over" + elif row["line"] in protected: + reason = None # keep every occurrence of a loop header + else: + key = (row["file"], row["line"]) + if key in seen: + reason = "dup" + else: + seen.add(key) + if reason and row["emit_kind"] != "special": + demote.append(row) + counts[reason] += 1 + else: + if reason: # wanted but special -> survives unpatched + counts["special_skip"] += 1 + kept.append(row["line"]) + return demote, kept, counts + + +def _surviving_is_stmt_lines(data: bytes, hdr: dict) -> List[int]: + return [ + row["line"] + for sequence in _simulate(data, hdr) + for row in sequence + if not row["end_sequence"] and row["is_stmt"] + ] + + +# ── core transform (in-memory, no env gate) ────────────────────────────────── + + +def rewrite_debug_line_blob(blob: bytes, src_path: Optional[str] = None) -> Tuple[bytes, RewriteResult]: + """Rewrite the ``.debug_line`` table inside an ELF ``blob``. + + Returns ``(new_blob, result)``. ``new_blob`` is the original ``blob`` when + no change is made or verification fails (the input is never corrupted). + """ + sections, little = _read_sections(io.BytesIO(blob)) + if ".debug_line" not in sections: + return blob, RewriteResult(False, reason="no .debug_line section") + + debug_line = sections[".debug_line"]["data"] + units = _decode_units(debug_line, little) + if not units: + return blob, RewriteResult(False, reason="empty .debug_line section") + log.debug("debug-line: parsed %d unit(s): %s", len(units), [ + f"{hdr['unit_start']:#x}-{hdr['unit_end']:#x}/v{hdr['version']}/addr{hdr['address_size']}" for hdr, _ in units + ]) + + src = src_path + if not src: + for hdr, _ in units: + src = _auto_detect_source(hdr["file_names"]) + if src: + break + protected = _loop_header_lines(src) if src and os.path.isfile(src) else set() + src_lines = _source_length(src) if src and os.path.isfile(src) else None + + before: List[int] = [] + demote: List[dict] = [] + kept: List[int] = [] + counts = {"dup": 0, "foreign": 0, "line0": 0, "over": 0, "special_skip": 0} + for hdr, sequences in units: + user_files = _user_file_indices(hdr["file_names"], src) if src else None + # A source mismatch (or an unresolved v5 file table) must not cause all + # rows in this unit to be classified as foreign. + if user_files is not None and not user_files: + log.warning("debug-line: source %s matches no file in unit %#x table %s; skipping foreign-file rule", src, + hdr["unit_start"], hdr["file_names"][1:]) + user_files = None + before.extend(row["line"] + for sequence in sequences + for row in sequence + if row["is_stmt"] and not row["end_sequence"]) + unit_demote, unit_kept, unit_counts = _plan_demotions(sequences, protected, user_files, src_lines) + demote.extend(unit_demote) + kept.extend(unit_kept) + for key in counts: + counts[key] += unit_counts[key] + + if not demote: + return blob, RewriteResult(False, before=before, after=kept, counts=counts, reason="nothing to demote") + + base = sections[".debug_line"]["offset"] + patched = bytearray(blob) + for row in demote: + file_off = base + row["emit_off"] + if patched[file_off] != DW_LNS_COPY: + return blob, RewriteResult( + False, before=before, after=kept, counts=counts, + reason=f"expected Copy(0x01) at .debug_line+{row['emit_off']:#x}, " + f"found {patched[file_off]:#04x}") + patched[file_off] = DW_LNS_SET_BASIC_BLOCK + + # Verify against the patched bytes (length-preserving, so offsets are stable). + verify_sections, verify_little = _read_sections(io.BytesIO(bytes(patched))) + verify_debug_line = verify_sections[".debug_line"]["data"] + survivors = [ + line for verify_hdr, _ in _decode_units(verify_debug_line, verify_little) + for line in _surviving_is_stmt_lines(verify_debug_line, verify_hdr) + ] + if survivors != kept: + return blob, RewriteResult(False, before=before, after=kept, counts=counts, + reason=f"verify mismatch: survivors={survivors} kept={kept}") + + return bytes(patched), RewriteResult(True, demoted=len(demote), before=before, after=kept, counts=counts, + reason="ok") + + +# ── file convenience (CLI / manual use) ─────────────────────────────────────── + + +def rewrite_debug_line_file(path: str, src_path: Optional[str] = None, out_path: Optional[str] = None) -> RewriteResult: + """Rewrite ``path``; write to ``out_path`` (or in place when ``None``). + Writes atomically via a temp file + ``os.replace``.""" + with open(path, "rb") as handle: + blob = handle.read() + new_blob, result = rewrite_debug_line_blob(blob, src_path) + if not result.changed: + return result + + dest = out_path or path + dest_dir = os.path.dirname(os.path.abspath(dest)) or "." + fd, tmp = tempfile.mkstemp(prefix=".debugline_", suffix=".npubin", dir=dest_dir) + try: + with os.fdopen(fd, "wb") as handle: + handle.write(new_blob) + os.replace(tmp, dest) + tmp = None + finally: + if tmp and os.path.exists(tmp): + os.unlink(tmp) + return result + + +# ── pipeline entry (env-gated, polymorphic, exception-safe) ─────────────────── + + +def _enabled() -> bool: + return os.environ.get(ENV_FLAG, "").strip().lower() in ("1", "true", "yes", "on") + + +def _resolve_source(metadata) -> Optional[str]: + """Best-effort source path from compiler metadata; ``None`` falls back to + auto-detection from the line table's file_names.""" + if metadata is None: + return None + for attr in ("src_path", "source_path", "filename"): + value = getattr(metadata, attr, None) + if isinstance(value, str) and os.path.isfile(value): + return value + if isinstance(metadata, dict): + for key in ("src_path", "source_path", "filename"): + value = metadata.get(key) + if isinstance(value, str) and os.path.isfile(value): + return value + return None + + +def rewrite_debug_line(artifact, metadata=None, options=None): + """``npubin``-stage post-processor. Returns ``artifact`` unchanged unless + ``LLVM_EXTRACT_DI_LOCAL_VARIABLES`` is set. Accepts the stage's ``bytes`` (or + a path) and returns the rewritten ``bytes`` (or the same path, patched in + place). Never raises: failures are logged and the artifact is returned as-is. + """ + if not _enabled(): + return artifact + + src = _resolve_source(metadata) + try: + if isinstance(artifact, (bytes, bytearray)): + new_blob, result = rewrite_debug_line_blob(bytes(artifact), src) + if result.changed: + log.info("debug-line: demoted %d stop(s) %s -> %s", result.demoted, result.before, result.after) + else: + log.debug("debug-line: no change (%s)", result.reason) + return new_blob + if isinstance(artifact, str): + result = rewrite_debug_line_file(artifact, src_path=src) + if result.changed: + log.info("debug-line: demoted %d stop(s) in %s", result.demoted, artifact) + return artifact + log.warning("debug-line: unexpected npubin artifact type %r; skipping", type(artifact)) + return artifact + except Exception: # never break the build over debug-info cleanup + log.exception("debug-line: rewrite skipped due to error") + return artifact + + +# ── CLI ─────────────────────────────────────────────────────────────────────── + + +def _main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser(description="Rewrite a .npubin's .debug_line for clean msdebug stepping.") + parser.add_argument("binary") + parser.add_argument("--src", help="kernel .py (loop detection + user-file id)") + group = parser.add_mutually_exclusive_group() + group.add_argument("--out", help="write patched copy here") + group.add_argument("--in-place", action="store_true", help="patch in place") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("-v", "--verbose", action="store_true") + args = parser.parse_args(argv) + + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, format="%(message)s") + + with open(args.binary, "rb") as handle: + blob = handle.read() + + if args.dry_run: + _, result = rewrite_debug_line_blob(blob, args.src) + log.info("demote %d (dup:%d foreign:%d line0:%d over_src_len:%d special_skipped:%d)", result.demoted, + result.counts.get("dup", 0), result.counts.get("foreign", 0), result.counts.get("line0", 0), + result.counts.get("over", 0), result.counts.get("special_skip", 0)) + log.info("before: %s", result.before) + log.info("after: %s", result.after) + log.info("(dry run — no write; reason=%s)", result.reason) + return 0 + + out = None if args.in_place else (args.out or args.binary + ".patched.npubin") + result = rewrite_debug_line_file(args.binary, src_path=args.src, out_path=out) + if result.changed: + log.info("demoted %d stop(s): %s -> %s", result.demoted, result.before, result.after) + log.info("written: %s", out or args.binary) + if out: + log.info("install: cp %s %s", out, args.binary) + return 0 + log.info("no change (%s)", result.reason) + return 1 + + +if __name__ == "__main__": + sys.exit(_main()) diff --git a/third_party/ascend/backend/driver.py b/third_party/ascend/backend/driver.py new file mode 100644 index 0000000000..28338d5b0d --- /dev/null +++ b/third_party/ascend/backend/driver.py @@ -0,0 +1,1662 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import importlib.metadata +from pathlib import Path +import tempfile +import os +import os.path +import re +import subprocess +import sysconfig +from typing import Optional +import functools +import hashlib +from triton.runtime.cache import get_cache_manager, get_dump_manager +from triton.backends.driver import DriverBase +from triton.backends.compiler import GPUTarget +from triton.backends.ascend.utils import (_build_npu_ext, _check_cxx11_abi, convert_sigtype_to_int, is_ffts_supported, + force_disable_ffts, get_backend_func, get_cann_version) +from triton.backends.ascend.program_grid import ( + PROGRAM_GRID_TRANSFORMS_VERSION, + ProgramGridContractError, + get_persistent_transform, + normalize_program_grid_transforms, +) +# Bind the already-imported utils module once so the launch hot path can write +# TRITON_PROFILER_REGISTERED without a per-launch `import triton` + attribute walk. +import triton.backends.ascend.utils as _ascend_utils + + +class NPUUtils(object): + + def __new__(cls): + if not hasattr(cls, 'instance'): + cls.instance = super(NPUUtils, cls).__new__(cls) + return cls.instance + + def __init__(self): + # NPUUtils is a singleton, but __init__ must refresh the cached shared + # object path on every construction. PyTorch Inductor may set + # TRITON_CACHE_DIR after the driver first initializes, so keeping the + # first path would make the launcher look for npu_utils.so in a newer + # cache root where it was never built. + self._cache_path = self._build_or_get_cached_so() + if not hasattr(self, "npu_utils_mod"): + self.npu_utils_mod = None + + def get_so_path(self): + if self._cache_path is None: + self._cache_path = self._build_or_get_cached_so() + return self._cache_path + + def _build_or_get_cached_so(self): + dirname = os.path.dirname(os.path.realpath(__file__)) + src_path = os.path.join(dirname, "npu_utils.cpp") + src = Path(src_path).read_text() + cann_version = get_cann_version() + cann_version_str = ".".join(map(str, cann_version)) if cann_version else "" + torch_npu_version = importlib.metadata.version("torch_npu") + key_parts = [cann_version_str, torch_npu_version, src] + key = hashlib.md5("\0".join(key_parts).encode("utf-8")).hexdigest() + cache = get_cache_manager(key) + fname = "npu_utils.so" + cache_path = cache.get_file(fname) + if cache_path is not None and os.path.exists(cache_path): + return cache_path + + with tempfile.TemporaryDirectory() as tmpdir: + tmp_src_path = os.path.join(tmpdir, "npu_utils.cpp") + with open(tmp_src_path, "w") as f: + f.write(src) + so = _build_npu_ext("npu_utils", tmp_src_path) + with open(so, "rb") as f: + cache_path = cache.put(f.read(), fname, binary=True) + return cache_path + + def _load_mod(self): + if self.npu_utils_mod is not None: + return self.npu_utils_mod + + import importlib.util + spec = importlib.util.spec_from_file_location("npu_utils", self.get_so_path()) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + self.npu_utils_mod = mod + return self.npu_utils_mod + + def load_binary(self, name, kernel, shared, device, mix_mode): + return self._load_mod().load_kernel_binary(name, kernel, shared, device, mix_mode) + + def _get_npu_device_limit_form_env(self) -> tuple[int, int]: + """Read and validate the NPU_DEVICE_LIMIT env var, return the capped AICore and AIVector counts. + + The env var format is ``cube_core_num,vector_core_num`` (e.g. ``"14,28"``), + used to reduce the core count visible to Triton in multi-tenant sharding, + performance tuning, and resource isolation scenarios. + When unset, the hardware actual values are returned (AIVector = AICore x 2). + + Validation rules (any failure raises ValueError): + 1. Format must match ``^\\d+(,\\d+)$``; leading/trailing whitespace allowed, + but no space after the comma; + 2. Both values must be positive; + 3. Neither value may exceed the hardware limit (AICore cap = device actual, + AIVector cap = AICore x 2). + + Returns: + tuple[int, int]: (num_aic, num_aiv) the capped AICore and AIVector counts. + + Raises: + ValueError: when the env var is malformed, contains non-positive values, + or exceeds the hardware limit; the error message includes the raw + input and the hardware actual caps. + """ + npu_device_limit_str = os.getenv("NPU_DEVICE_LIMIT") + num_aic, num_aiv = self.get_device_core() + if npu_device_limit_str is None: + return num_aic, num_aiv + + is_valid = re.match(r'^\d+ *, *\d+$', npu_device_limit_str.strip()) + if is_valid: + parts = [part.strip() for part in npu_device_limit_str.split(",")] + num_aic_env = int(parts[0]) + num_aiv_env = int(parts[1]) + + if num_aic_env <= 0 or num_aiv_env <= 0: + raise ValueError(f"[ERROR]NPU_DEVICE_LIMIT={npu_device_limit_str}, which has non-positive value," + f"both cube_core_num and vector_core_num must be positive.") + elif num_aic_env > num_aic or num_aiv_env > num_aiv: + raise ValueError( + f"[ERROR]NPU_DEVICE_LIMIT={npu_device_limit_str}, both cube_core_num and vector_core_num " + f"must be less than or equal to device properties ({num_aic},{num_aiv}).") + elif num_aic_env * (num_aiv / num_aic) != num_aiv_env: + env_quotient = num_aiv_env / num_aic_env + env_quotient_decimal = round(env_quotient, 1) + quotient = num_aiv / num_aic + quotient_decimal = round(quotient, 1) + raise ValueError( + f"[ERROR]NPU_DEVICE_LIMIT={npu_device_limit_str}; expected ratio is consistent, actual, " + f"the ratio of vector_core_num/cube_core_num({num_aiv_env}/{num_aic_env}={env_quotient_decimal}) does " + f"not equal device properties vector_core_num/cube_core_num({num_aiv}/{num_aic}={quotient_decimal}) ratio." + ) + else: + debug = os.getenv("TRITON_DEBUG", 'false').lower() in ('true', '1') + if debug: + print( + f"[DEBUG]NPU_DEVICE_LIMIT from env: cube_core_num={num_aic_env},vector_core_num={num_aiv_env})." + ) + return num_aic_env, num_aiv_env + else: + raise ValueError(f"[ERROR]NPU_DEVICE_LIMIT={npu_device_limit_str}, which has invalid format: " + f"It should be like '14,28' (cube_core_num,vector_core_num) " + f"and it must be a positive number.") + + @functools.lru_cache() + def get_device_core(self): + import torch + device = torch.npu.current_device() + prop = torch.npu.get_device_properties(device) + cube_core_num, vector_core_num = prop.cube_core_num, prop.vector_core_num + return cube_core_num, vector_core_num + + def has_device_limit(self): + num_aic, num_aiv = self.get_device_core() + try: + return num_aic != self.get_aicore_num() or num_aiv != self.get_aivector_core_num() + except ValueError: + return False + + def get_device_properties(self, device): + # temperoarily added "max_shared_mem" properties to avoid triton-compiler complain + # fetch available memory at runtime + num_aic, num_aiv = self._get_npu_device_limit_form_env() + return {"max_shared_mem": 1, "num_aicore": num_aic, "num_vectorcore": num_aiv} + + def get_arch(self): + # temporarily return empty arch descriptor + return self._load_mod().get_arch() + + def get_aicore_num(self): + # temporarily return empty arch descriptor + return self.get_device_properties("npu")["num_aicore"] + + def get_aivector_core_num(self): + return self.get_device_properties("npu")["num_vectorcore"] + + +class NPULauncher(object): + + def __init__(self, src, metadata): + self.compile_only = os.getenv("TRITON_COMPILE_ONLY", 'false').lower() in ('true', '1') + self.src = src + self.metadata = metadata + self.so_launcher_path = self._make_launcher_stub_path() + # setup for remote run + # TODO: use a var to pack all vars required to run on a remote machine + self.mix_mode = metadata.mix_mode + self.shared = metadata.shared + import importlib.util + spec = importlib.util.spec_from_file_location("__triton_launcher", self.so_launcher_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + cst_key = lambda i: self.src.fn.arg_names.index(i) if isinstance(i, str) else i + signature = {cst_key(key): value for key, value in self.src.signature.items()} + self.launch = wrap_handle_tensordesc(getattr(mod, "launch"), signature) + + def _make_launcher_stub_path(self): + header_src = generate_npu_header_src() + constants = self.src.constants if hasattr(self.src, "constants") else dict() + cst_key = lambda i: self.src.fn.arg_names.index(i) if isinstance(i, str) else i + constants = {cst_key(key): value for key, value in constants.items()} + signature = {cst_key(key): value for key, value in self.src.signature.items()} + wrapper_src = make_launcher(constants, signature, self.metadata) + return make_npu_launcher_stub(header_src, wrapper_src, self.metadata.debug) + + def get_launcher_so_path(self): + return self.so_launcher_path + + def __call__(self, *args, **kwargs): + _ascend_utils._warn_deprecated_ascend_env_var("TRITON_REGISTER_TENSOR_MSPROF") + if self.compile_only: + cache_manager = get_cache_manager(args[5]['hash']) + print("[INFO]: skip running kernel") + print(f"[INFO]: The compiled kernel cache is in {cache_manager.cache_dir}") + return + profiler_registered = self.launch(*args, **kwargs) + _ascend_utils.TRITON_PROFILER_REGISTERED = (profiler_registered == 1) + + +class NPUDriver(DriverBase): + + def __init__(self): + self.utils = NPUUtils() + self.launcher_cls = NPULauncher + super().__init__() + + @classmethod + def is_active(cls): + + def test_npucompiler(): + from triton.backends.ascend.utils import _get_bisheng_path + npucompiler = _get_bisheng_path() + targets = subprocess.check_output([npucompiler, "-print-targets"]).decode().strip().split() + return "hiipu64" in targets + + try: + return test_npucompiler() + except Exception as e_npucompiler: + import warnings + red = "\x1b[31;20m" + reset = "\x1b[0m" + warnings.warn(red + str(e_npucompiler) + reset) + return False + + def map_python_to_cpp_type(self, ty: str) -> str: + return ty_to_cpp(ty) + + def get_current_target(self): + backend = "npu" + arch = self.utils.get_arch() + warp_size = 0 + return GPUTarget(backend, arch, warp_size) + + def get_current_device(self): + """ + Get current device + """ + import torch + return torch.npu.current_device() + + def get_active_torch_device(self): + import torch + return torch.device("npu", self.get_current_device()) + + def set_current_device(self, device): + """ + Set current device as the given device + """ + import torch + return torch.npu.set_device(device) + + def get_current_stream(self, device: Optional[int] = None) -> int: + """ + Get stream for current device + """ + import torch + import torch_npu + if device is None: + device = torch.npu.current_device() + if hasattr(torch_npu._C, "_npu_getCurrentRawStreamNoWait"): + from torch_npu._C import _npu_getCurrentRawStreamNoWait + return _npu_getCurrentRawStreamNoWait(device) + else: + from torch_npu._C import _npu_getCurrentRawStream + return _npu_getCurrentRawStream(device) + + def get_benchmarker(self): + from triton.testing import do_bench + return do_bench + + def get_device_interface(self): + return get_backend_func("get_device_interface") + + def get_empty_cache_for_benchmark(self): + cache_size = 192 * 1024 * 1024 + return get_backend_func("get_empty_tensor", cache_size // 4) + + def clear_cache(self, cache): + cache.zero_() + + +def make_npu_launcher_stub(header_src, wrapper_src, debug=False): + """ + Generate the launcher stub to launch the kernel + """ + so_cache_key = hashlib.sha256((header_src + "\0" + wrapper_src).encode("utf-8")).hexdigest() + so_cache_manager = get_cache_manager(so_cache_key) + use_cxx11_abi = _check_cxx11_abi() + name = f"launcher_cxx11abi{use_cxx11_abi}" + suffix = sysconfig.get_config_var('EXT_SUFFIX') + so_name = f"{name}{suffix}" + + if debug: + dump_manager = get_dump_manager(so_cache_key) + print(f"Dumping precompiled.h to {dump_manager.cache_dir}") + dump_manager.put(header_src, "precompiled.h", binary=False) + print(f"Dumping {name}.cxx to {dump_manager.cache_dir}") + dump_manager.put(wrapper_src, f"{name}.cxx", binary=False) + + cache_path = so_cache_manager.get_file(so_name) + if cache_path is not None: + return cache_path + + kernel_launcher_type = "torch" + import fcntl + cache_dir = os.getenv("TRITON_CACHE_DIR", os.path.join(os.path.expanduser("~/.triton"), "cache")) + lock_dir = os.path.join(cache_dir, "lock") + if not (os.path.exists(lock_dir)): + os.makedirs(lock_dir, exist_ok=True) + with open(os.path.join(lock_dir, f"{so_cache_key}.lock"), "w") as lock_file: + try: + fcntl.flock(lock_file, fcntl.LOCK_EX) + cache_path = so_cache_manager.get_file(so_name) + if cache_path is not None: + return cache_path + with tempfile.TemporaryDirectory() as tmpdir: + src_path = os.path.join(tmpdir, f"{name}.cxx") + with open(src_path, "w") as f: + f.write(wrapper_src) + so_path = _build_npu_ext(name, src_path, kernel_launcher=kernel_launcher_type) + if debug: + with open(so_path, "rb") as f: + dump_manager.put(f.read(), so_name, binary=True) + with open(so_path, "rb") as f: + so_cache_path = so_cache_manager.put(f.read(), so_name, binary=True) + return so_cache_path + finally: + fcntl.flock(lock_file, fcntl.LOCK_UN) + + +def extract_device_print_code_from_cann(): + from triton.backends.ascend.utils import _get_bisheng_path + ccec_compiler_bin_folder, _ = os.path.split(os.path.realpath(_get_bisheng_path())) + ccec_compiler_folder, _ = os.path.split(ccec_compiler_bin_folder) + clang_version = os.listdir(os.path.join(ccec_compiler_folder, "lib/clang/"))[0] + ccelib_path = os.path.join(ccec_compiler_folder, f"lib/clang/{clang_version}/include/ccelib") + + def read_header(header_path): + with open(os.path.join(ccelib_path, header_path), 'r') as f: + code = f.read() + + # remove all #include "..." + lines = code.splitlines() + purged_lines = [] + for line in lines: + normalized_line = ' '.join(line.split()) + if not normalized_line.startswith('#include "'): + purged_lines.append(line) + code = '\n'.join(purged_lines) + + # remove [aicore] functions + aicore_positions = [] + for m in re.finditer(r'\[aicore\]', code): + aicore_positions.append(m.start()) + + def find_aicore_function_span(src, pos): + for i in range(pos - 1, -1, -1): + if src[i] == '}': # this relies on that all [aicore] functions come after normal functions + left = i + 1 + break + n = len(src) + brace_nest = 0 + for j in range(pos, n, 1): + if src[j] == '{': + brace_nest += 1 + elif src[j] == '}': + brace_nest -= 1 + if brace_nest == 0: + right = j + break + return left, right + + new_code = '' + segment_start = 0 + for pos in aicore_positions: + left, right = find_aicore_function_span(code, pos) + new_code += code[segment_start:left] + segment_start = right + 1 + new_code += code[segment_start:] + + # remove __gm__ and rename macros + new_code = new_code.replace('__gm__', ' ') + new_code = new_code.replace('__CCELIB_RT_ERROR_NONE', 'RT_ERROR_NONE') + new_code = new_code.replace('__CCELIB_RT_MEMORY_HBM', 'RT_MEMORY_HBM') + new_code = new_code.replace('__CCELIB_RT_MEMCPY_HOST_TO_DEVICE', 'RT_MEMCPY_HOST_TO_DEVICE') + new_code = new_code.replace('__CCELIB_RT_MEMCPY_DEVICE_TO_HOST', 'RT_MEMCPY_DEVICE_TO_HOST') + return new_code + + # the following headers should be included in this order + return '\n'.join([ + read_header('common/common_impl.h'), + read_header('internal/debug_tunnel/payload.h'), + read_header('internal/debug_tunnel/payload_impl.h'), + read_header('internal/debug_tunnel/tunnel.h'), + read_header('internal/debug_tunnel/tunnel_impl.h') + ]) + + +def generate_npu_header_src(): + enable_taskqueue = os.getenv("TRITON_ENABLE_TASKQUEUE", 'true').lower() in ('true', '1') + return f""" +#ifndef TRITON_NPU_HEADERS +#define TRITON_NPU_HEADERS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "runtime/runtime/rt.h" +#include +{get_backend_func("header_file", enable_taskqueue)} +#endif + +// Compatibility shim for CANN runtime API transition (rt -> aclrt in 9.0.0). +#ifdef TRITON_CANN_910 +using cann_error = aclError; +using cann_stream = aclrtStream; +using cann_func_handle = aclrtFuncHandle; +using cann_memcpy_kind = aclrtMemcpyKind; +static constexpr cann_error CANN_SUCCESS = ACL_SUCCESS; +static constexpr cann_memcpy_kind CANN_MEMCPY_HOST_TO_DEVICE = ACL_MEMCPY_HOST_TO_DEVICE; +static inline cann_error cann_malloc_host(void **ptr, size_t size) {{ return aclrtMallocHost(ptr, size); }} +static inline cann_error cann_free_host(void *ptr) {{ return aclrtFreeHost(ptr); }} +static inline cann_error cann_memcpy_async(void *dst, size_t destMax, const void *src, size_t count, cann_memcpy_kind kind, cann_stream stream) {{ return aclrtMemcpyAsync(dst, destMax, src, count, kind, stream); }} +static inline cann_error cann_memset_async(void *dst, size_t destMax, int32_t value, size_t count, cann_stream stream) {{ return aclrtMemsetAsync(dst, destMax, value, count, stream); }} +static inline cann_error cann_synchronize_stream(cann_stream stream) {{ return aclrtSynchronizeStream(stream); }} +static inline cann_error cann_get_hardware_sync_addr(void **addr) {{ return aclrtGetHardwareSyncAddr(addr); }} +static inline cann_error cann_launch_kernel(cann_func_handle func, uint32_t block_dim, cann_stream stream, void *cfg, void *args, size_t arg_size) {{ + return aclrtLaunchKernelWithHostArgs(func, block_dim, stream, static_cast(cfg), args, arg_size, nullptr, 0); +}} +static inline void* cann_get_launch_kernel_cfg(uint32_t shared_mem_dynamic_size) {{ + // thread_local storage: launch is synchronous on the launcher thread, so the + // returned pointer remains valid until cann_launch_kernel returns. + static thread_local aclrtLaunchKernelAttr attrInfo; + static thread_local aclrtLaunchKernelCfg cfgCfgInfo; + attrInfo.id = ACL_RT_LAUNCH_KERNEL_ATTR_DYN_UBUF_SIZE; + aclrtLaunchKernelAttrValue attrValue; + attrValue.localMemorySize = shared_mem_dynamic_size; + attrInfo.value = attrValue; + cfgCfgInfo.attrs = &attrInfo; + cfgCfgInfo.numAttrs = 1; + return &cfgCfgInfo; +}} +#else +using cann_error = rtError_t; +using cann_stream = rtStream_t; +using cann_func_handle = const void*; +using cann_memcpy_kind = rtMemcpyKind_t; +static constexpr cann_error CANN_SUCCESS = RT_ERROR_NONE; +static constexpr cann_memcpy_kind CANN_MEMCPY_HOST_TO_DEVICE = RT_MEMCPY_HOST_TO_DEVICE; +static inline cann_error cann_malloc_host(void **ptr, size_t size) {{ return rtMallocHost(ptr, size, RT_MEMORY_HOST); }} +static inline cann_error cann_free_host(void *ptr) {{ return rtFreeHost(ptr); }} +static inline cann_error cann_memcpy_async(void *dst, size_t destMax, const void *src, size_t count, cann_memcpy_kind kind, cann_stream stream) {{ return rtMemcpyAsync(dst, destMax, src, count, kind, stream); }} +static inline cann_error cann_memset_async(void *dst, size_t destMax, int32_t value, size_t count, cann_stream stream) {{ return rtMemsetAsync(dst, destMax, value, count, stream); }} +static inline cann_error cann_synchronize_stream(cann_stream stream) {{ return rtStreamSynchronize(stream); }} +static inline cann_error cann_get_hardware_sync_addr(void **addr) {{ uint32_t len = 0; return rtGetC2cCtrlAddr(reinterpret_cast(addr), &len); }} +static inline cann_error cann_launch_kernel(cann_func_handle func, uint32_t block_dim, cann_stream stream, void *cfg, void *args, size_t arg_size) {{ + if (cfg != nullptr) {{ + rtArgsEx_t argsInfo = {{}}; + argsInfo.args = args; + argsInfo.argsSize = arg_size; + return rtKernelLaunchWithFlagV2(func, block_dim, &argsInfo, NULL, stream, 0, static_cast(cfg)); + }} + return rtKernelLaunch(func, block_dim, args, arg_size, NULL, stream); +}} +static inline void* cann_get_launch_kernel_cfg(uint32_t shared_mem_dynamic_size) {{ + // thread_local storage: launch is synchronous on the launcher thread, so the + // returned pointer remains valid until cann_launch_kernel returns. + static thread_local rtTaskCfgInfo_t cfgInfo; + cfgInfo.localMemorySize = shared_mem_dynamic_size; + return &cfgInfo; +}} +#endif + +// Initialize the reusable lock workspace asynchronously on the compute stream. +// Synchronous cann_memcpy does not synchronize with the stream, so a prior +// kernel still consuming the lock buffer could be clobbered. Cache pinned +// host templates for the lifetime of the process: queued copies and captured +// graphs may outlive the launch that first used them. +static inline cann_error initializeLocks(uint64_t orderedLocks, + uint64_t unorderedLocks, + uint64_t participants, + int64_t lockInitValue, + uint64_t bytes, + uint64_t elements, + uint64_t orderedElements, + uint64_t unorderedStride, + void *dst, cann_stream stream) {{ + using Key = std::tuple; + static std::mutex mutex; + static std::map templates; + const void *src; + {{ + std::lock_guard guard(mutex); + auto &cached = templates[Key(orderedLocks, unorderedLocks, participants, + lockInitValue)]; + if (!cached) {{ + void *data = nullptr; + cann_error ret = cann_malloc_host(&data, bytes); + if (ret != CANN_SUCCESS) + return ret; + auto *init = static_cast(data); + for (uint64_t i = 0; i < elements; ++i) + init[i] = unorderedLocks ? 0 : lockInitValue; + for (uint64_t i = 0; i < unorderedLocks; ++i) + init[orderedElements + i * unorderedStride] = + static_cast(participants); + cached = data; + }} + src = cached; + }} + return cann_memcpy_async(dst, bytes, src, bytes, CANN_MEMCPY_HOST_TO_DEVICE, + stream); +}} +""" + + +# ------------------------ +# Launcher +# ------------------------ + + +def ty_to_cpp(ty): + if ty[0] == '*': + return "void*" + if ty.startswith("tensordesc"): + return "void*" + return { + "i1": "int32_t", + "i8": "int8_t", + "i16": "int16_t", + "i32": "int32_t", + "i64": "int64_t", + "u1": "uint32_t", + "u8": "uint8_t", + "u16": "uint16_t", + "u32": "uint32_t", + "u64": "uint64_t", + "fp16": "float", + "bf16": "float", + "fp32": "float", + "f32": "float", + "fp64": "double", + }[ty] + + +_BASE_ARGS_FORMAT = "iiiKKOOOO" +_BASE_ARGS_FORMAT_LEN = len(_BASE_ARGS_FORMAT) + + +def make_tensordesc_arg(arg): + return [arg.base, *arg.shape, *arg.strides, arg.padding == "nan", *arg.shape, *arg.strides] + + +def wrap_handle_tensordesc(launcher, signature): + has_tensor_desc_arg = any(isinstance(sig, str) and sig.startswith("tensordesc") for sig in signature.values()) + if not has_tensor_desc_arg: + return launcher + + tensordesc_indices = set( + [i for i, sig in enumerate(signature.values()) if isinstance(sig, str) and sig.startswith("tensordesc")]) + + def inner(*args): + final_args = list(args[:_BASE_ARGS_FORMAT_LEN]) + for i, arg in enumerate(args[_BASE_ARGS_FORMAT_LEN:]): + if i in tensordesc_indices: + final_args.extend(make_tensordesc_arg(arg)) + else: + final_args.append(arg) + return launcher(*final_args) + + return inner + + +_CPP_DEVICE_POINTER = r""" +typedef struct _DevicePtrInfo { + void* dev_ptr; + bool valid; +} DevicePtrInfo; + +static inline DevicePtrInfo getPointer(PyObject* obj, int idx) { + DevicePtrInfo ptr_info; + ptr_info.dev_ptr = nullptr; + ptr_info.valid = true; + if (PyLong_Check(obj)) { + ptr_info.dev_ptr = reinterpret_cast(PyLong_AsUnsignedLongLong(obj)); + return ptr_info; + } + if (obj == Py_None) { + return ptr_info; + } + // Cache the interned "data_ptr" key once instead of rebuilding a temporary + // PyUnicode on every call. Function-local static init is thread-safe in C++11 + // and the GIL is held here, so the one-time init is safe. + static PyObject* data_ptr_str = PyUnicode_InternFromString("data_ptr"); + // PyObject_CallMethodNoArgs avoids creating a temporary tuple and a temporary + // method-name PyUnicode on every call (Python 3.9+). + PyObject* ret = PyObject_CallMethodNoArgs(obj, data_ptr_str); + if (ret) { + if (!PyLong_Check(ret)) { + PyErr_SetString(PyExc_TypeError, "data_ptr method of Pointer object must return 64-bit int"); + ptr_info.valid = false; + Py_DECREF(ret); + return ptr_info; + } + ptr_info.dev_ptr = reinterpret_cast(PyLong_AsUnsignedLongLong(ret)); + Py_DECREF(ret); + if (!ptr_info.dev_ptr) { + return ptr_info; + } + return ptr_info; + } + PyErr_SetString(PyExc_TypeError, "Pointer argument must be either uint64 or have data_ptr method"); + ptr_info.valid = false; + return ptr_info; +} +""" + +_CPP_MSPROF_EXTERN = r""" +extern "C" { +typedef int (*callback)(unsigned int type, void* data, unsigned int len); +extern int MsprofReportApi(unsigned int agingFlag, const MsprofApi* api); +extern unsigned long int MsprofSysCycleTime(); +extern int MsprofRegisterCallback(unsigned int moduleId, callback handle); +static unsigned int __MsprofFlagL0 = 0; +static unsigned int __MsprofFlagL1 = 0; +static std::vector tensorKinds; + +int ProfCtrlHandle(unsigned int CtrlType, void* CtrlData, unsigned int DataLen) { + if ((CtrlData == nullptr) || (DataLen == 0U)) { + return 1; + } + if (CtrlType == 1) { + MsprofCommandHandle* handle = (MsprofCommandHandle*)(CtrlData); + if (handle->type >= 6) { + return 1; + } + if (handle->type == 1) { + __MsprofFlagL0 = ((0x00000800ULL & handle->profSwitch) == 0x00000800ULL) ? 1 : 0; + __MsprofFlagL1 = ((0x00000002ULL & handle->profSwitch) == 0x00000002ULL) ? 1 : 0; + } + } + return 0; +} +} +""" + +_CPP_MSPROF_CALLBACK = r""" + MsprofRegisterCallback(8, ProfCtrlHandle); +""" + +_CPP_MSPROF_BEFORE_LAUNCH = r""" + unsigned long int beginTime = 0; + unsigned long int endTime = 0; + unsigned long int opNameHashID = 0; + unsigned int threadId = 0; + char* _kernelName = const_cast(kernelName); + size_t length = kernelName ? strlen(kernelName) : 0; + if (__MsprofFlagL0 || __MsprofFlagL1) { + beginTime = MsprofSysCycleTime(); + } +""" + +_CPP_ALIGN_LAUNCH_OFFSET = r""" +static inline size_t _align_launch_offset(size_t offset, size_t alignment) { + return (offset + alignment - 1) & ~(alignment - 1); +} + +// cann_get_hardware_sync_addr returns a per-process per-stream constant address; +// re-querying it on every kernel launch is pure overhead. Cache the most +// recently observed (stream, ffts_addr) pair on the calling thread. +// Thread-safety: launch_call is invoked synchronously from the launcher thread +// by cann_async_launch (see npu_utils.cpp), so thread_local is safe. +static thread_local cann_stream g_last_ffts_stream = nullptr; +static thread_local void* g_last_ffts_addr = nullptr; +static inline cann_error get_ffts_addr(cann_stream stream, void** out_addr) { + if (stream == g_last_ffts_stream && g_last_ffts_addr) { + *out_addr = g_last_ffts_addr; + return CANN_SUCCESS; + } + void* ffts_addr = nullptr; + cann_error ret = cann_get_hardware_sync_addr(&ffts_addr); + if (ret == CANN_SUCCESS) { + g_last_ffts_stream = stream; + g_last_ffts_addr = ffts_addr; + *out_addr = ffts_addr; + } + return ret; +} +""" + +_CPP_GET_TENSOR_SHAPE = r""" +static std::vector _get_tensor_shape(PyObject* tensor) { + std::vector shape; + if (!tensor || tensor == Py_None) { + return shape; + } + // Cache the interned "size" key once; avoid temporary PyUnicode/tuple per call. + static PyObject* size_str = PyUnicode_InternFromString("size"); + // PyObject_CallMethodNoArgs avoids building "size" PyUnicode and an empty + // tuple on every launch (Python 3.9+). + PyObject* size_result = PyObject_CallMethodNoArgs(tensor, size_str); + if (!size_result) { + // Defensive: profiling-only path; swallow attribute errors so subsequent + // PyErr_Occurred() checks in launch() are not poisoned. + PyErr_Clear(); + return shape; + } + PyObject* seq = PySequence_Fast(size_result, "Expected a sequence from tensor.size()"); + if (seq) { + Py_ssize_t len = PySequence_Fast_GET_SIZE(seq); + PyObject** items = PySequence_Fast_ITEMS(seq); + for (Py_ssize_t i = 0; i < len; ++i) { + PyObject* dim = items[i]; + if (PyLong_Check(dim)) { + shape.push_back(PyLong_AsLong(dim)); + } + } + } + Py_DECREF(seq); + Py_DECREF(size_result); + return shape; +} +""" + + +# the template is from triton-adapter HEAD. Wrapping the generated kernel binary into a python module +def make_launcher(constants, signature, metadata): + import os + workspace_size = int(metadata.workspace_size) \ + if hasattr(metadata, 'workspace_size') else -1 + lock_init_value = int(metadata.lock_init_value if hasattr(metadata, 'lock_init_value') else metadata. + lock_init_val if hasattr(metadata, 'lock_init_val') else 0) + sync_block_lock_layout = int(getattr(metadata, "sync_block_lock_layout", 0)) + ordered_sync_block_lock_count = sync_block_lock_layout & 0xFFFFFFFF + unordered_sync_block_lock_count = (sync_block_lock_layout >> 32) & 0xFFFFFFFF + has_sync_block_lock = (ordered_sync_block_lock_count + unordered_sync_block_lock_count) > 0 + unordered_sync_block_participant_factor = (2 if unordered_sync_block_lock_count > 0 and metadata.mix_mode == "mix" + and getattr(metadata, "auto_tile_and_bind_subblock", False) else 1) + sync_block_lock_layout_stmt = f""" + constexpr uint64_t syncBlockLockCacheLineI64 = 8; + constexpr uint64_t syncBlockLockOrderedCount = {ordered_sync_block_lock_count}; + constexpr uint64_t syncBlockLockUnorderedCount = {unordered_sync_block_lock_count}; + constexpr uint64_t syncBlockLockParticipantFactor = {unordered_sync_block_participant_factor}; + const uint64_t syncBlockLockParticipantNum = + blockNum * syncBlockLockParticipantFactor; + const uint64_t syncBlockLockUnorderedStrideI64 = + (1 + 2 * syncBlockLockParticipantNum) * syncBlockLockCacheLineI64; + const uint64_t syncBlockLockOrderedI64Count = + syncBlockLockOrderedCount * syncBlockLockCacheLineI64; + const uint64_t syncBlockLockI64Count = syncBlockLockOrderedI64Count + + syncBlockLockUnorderedCount * syncBlockLockUnorderedStrideI64; + const uint64_t syncBlockLockSize = syncBlockLockI64Count * sizeof(int64_t);""" + # Initialize on the compute stream. Synchronous memcpy is not ordered + # against the stream and could overwrite lock state still consumed by a + # previous kernel, so non-zero patterns go through initializeLocks which + # copies a cached pinned host template asynchronously on the stream. + if not unordered_sync_block_lock_count and not lock_init_value: + lock_init_stmt = ("ret = cann_memset_async(syncBlockLock_ptr, syncBlockLockSize, 0, " + "syncBlockLockSize, stream);") + else: + lock_init_stmt = ("ret = initializeLocks(" + "syncBlockLockOrderedCount, syncBlockLockUnorderedCount, " + "syncBlockLockParticipantNum, " + f"{lock_init_value}, " + "syncBlockLockSize, syncBlockLockI64Count, " + "syncBlockLockOrderedI64Count, syncBlockLockUnorderedStrideI64, " + "syncBlockLock_ptr, stream);") + bs_task_type = metadata.bs_task_type if hasattr(metadata, 'bs_task_type') else 0 + mix_mode = metadata.mix_mode + compile_on_910_95 = metadata.compile_on_910_95 + parallel_mode = metadata.parallel_mode + enable_simt = ("simt" in parallel_mode) or metadata.is_pure_simt + + def _expand_signature(signature): + output = [] + # Expand tensor descriptor arguments into base pointer, shape and + # strides. Ascend always rewrites tensordesc to pointer (no TMA). + for sig in signature: + if isinstance(sig, str) and sig.startswith("tensordesc"): + match = re.match("tensordesc<([^[>]*)\\[([^]]*)\\]", sig) + dtype = match.group(1) + shape = match.group(2) + ndim = shape.count(",") + 1 + + output.append("*" + dtype) + # Currently the host side tensor descriptors get passed in as a + # tensor desc, shape, and strides. We have no way to use these + # shape and strides when processing tensor descriptors which is + # why we provide our own decomposition above. Sadly this means + # we have to pass the shape and strides twice. + for _ in range(2 * ndim): + output.append("i64") + output.append("i1") + + for _ in range(ndim): + output.append("i32") + for _ in range(ndim): + output.append("i64") + else: + output.append(sig) + + return output + + def _flatten_signature(sig, output): + # Flatten tuples + if isinstance(sig, tuple): + for x in sig: + _flatten_signature(x, output) + else: + output.append(sig) + + def _extracted_type(ty): + if isinstance(ty, tuple): + val = ','.join(map(_extracted_type, ty)) + return f"[{val}]" + if ty[0] == '*': + return "PyObject*" + if ty.startswith("tensordesc"): + return "PyObject*" + if ty in ("constexpr"): + return "PyObject*" + return ty_to_cpp(ty) + + def format_of(ty): + if isinstance(ty, tuple): + val = ''.join(map(format_of, ty)) + return f"({val})" + if ty[0] == '*': + return "O" + if ty.startswith("tensordesc"): + return "O" + if ty in ("constexpr"): + return "O" + if ty == "void*": + return "O" + return { + "float": "f", + "double": "d", + "long": "l", + "int8_t": "b", + "int16_t": "h", + "int32_t": "i", + "int64_t": "L", + "uint8_t": "B", + "uint16_t": "H", + "uint32_t": "I", + "uint64_t": "K", + }[ty_to_cpp(ty)] + + def _format_to_fastcall_stmt(ty, var, idx): + """Generate C statement to parse argument at index idx from METH_FASTCALL args array.""" + fmt = format_of(ty) + if fmt == "O": + return f"{var} = args[{idx}];" + elif fmt == "i": + return f"{var} = (int)PyLong_AsLong(args[{idx}]);" + elif fmt == "L": + return f"{var} = (int64_t)PyLong_AsLongLong(args[{idx}]);" + elif fmt == "K": + return f"{var} = (uint64_t)PyLong_AsUnsignedLongLong(args[{idx}]);" + elif fmt == "I": + return f"{var} = (uint32_t)PyLong_AsUnsignedLong(args[{idx}]);" + elif fmt == "H": + return f"{var} = (uint16_t)PyLong_AsUnsignedLong(args[{idx}]);" + elif fmt == "B": + return f"{var} = (uint8_t)PyLong_AsUnsignedLong(args[{idx}]);" + elif fmt == "h": + return f"{var} = (int16_t)PyLong_AsLong(args[{idx}]);" + elif fmt == "b": + return f"{var} = (int8_t)PyLong_AsLong(args[{idx}]);" + elif fmt == "l": + return f"{var} = (long)PyLong_AsLong(args[{idx}]);" + elif fmt == "f": + return f"{var} = (float)PyFloat_AsDouble(args[{idx}]);" + elif fmt == "d": + return f"{var} = PyFloat_AsDouble(args[{idx}]);" + else: + raise ValueError(f"Unsupported format: {fmt} for type {ty}") + + def _format_of_msprof_task_type_ratio(bs_task_type, mix_mode): + # Default fallback based on mix_mode + default_task_type = "MSPROF_GE_TASK_TYPE_AIV" if mix_mode == "aiv" else "MSPROF_GE_TASK_TYPE_AI_CORE" + + if not bs_task_type: + return default_task_type, 0 + + task_type_num, mix_block_dim_ratio = divmod(int(bs_task_type), 10) + task_type_map = { + 1: "MSPROF_GE_TASK_TYPE_AIV", + 2: "MSPROF_GE_TASK_TYPE_AI_CORE", + 3: "MSPROF_GE_TASK_TYPE_MIX_AIC", + 4: "MSPROF_GE_TASK_TYPE_MIX_AIV", + } + + task_type = task_type_map.get(task_type_num, default_task_type) + return task_type, mix_block_dim_ratio + + """ + args: + int gridX, gridY, gridZ; + aclrtStream stream; + aclrtFuncHandle functon; + PyObject* packed_metadata, *launch_metadata; + PyObject* launch_enter_hook, *launch_exit_hook; + *args_expand + """ + + expand_signature = _expand_signature(signature.values()) + signature = {i: s for i, s in enumerate(expand_signature)} + + args_format = ''.join([format_of(ty) for ty in signature.values()]) + format = _BASE_ARGS_FORMAT + args_format + + flat_signature = [] + for sig in signature.values(): + _flatten_signature(sig, flat_signature) + signature = {i: s for i, s in enumerate(flat_signature)} + args_list = ', ' + ', '.join(f"&_arg{i}" for i, ty in signature.items()) if len(signature) > 0 else '' + # Total expected argument count for METH_FASTCALL arity check. + total_nargs = _BASE_ARGS_FORMAT_LEN + len(signature) + # Generate manual parsing statements for signature args (indices 9..) used by + # the METH_FASTCALL fast path in launch(). + fastcall_sig_parse_stmts = '\n '.join( + _format_to_fastcall_stmt(ty, f"_arg{i}", _BASE_ARGS_FORMAT_LEN + i) for i, ty in signature.items()) + # Record the end of regular arguments; + # subsequent arguments are architecture-specific descriptors. + arg_decls = ', '.join(f"{ty_to_cpp(ty)} arg{i}" for i, ty in signature.items() if ty != "constexpr") + internal_args_list = [] + for i, ty in signature.items(): + if ty[0] == "*": + internal_args_list.append(f"ptr_info{i}.dev_ptr") + elif ty != "constexpr": + internal_args_list.append(f"_arg{i}") + + # generate glue code + newline = '\n ' + ptr_decls = [ + f"DevicePtrInfo ptr_info{i} = getPointer(_arg{i}, {i}); if (!ptr_info{i}.valid) return nullptr;" + for i, ty in signature.items() + if ty[0] == "*" + ] + grid_info = {'X': 'i32', 'Y': 'i32', 'Z': 'i32'} + # TODO: automatically check if gather load ops are used. + + arch = metadata.target.arch + target_support_ffts = is_ffts_supported(arch) and (not force_disable_ffts(arch)) + enable_device_print = os.getenv("TRITON_DEVICE_PRINT", 'false').lower() in ('true', '1') + enable_taskqueue = os.getenv("TRITON_ENABLE_TASKQUEUE", 'true').lower() in ('true', '1') + enable_grid_warn_print = os.getenv("TRITON_GRID_WARN_PRINT", 'false').lower() in ('true', '1') + npu_utils = NPUUtils() + num_physical_blocks = npu_utils.get_aivector_core_num() if mix_mode == "aiv" else npu_utils.get_aicore_num() + task_type, mix_block_dim_ratio = _format_of_msprof_task_type_ratio(bs_task_type, mix_mode) + is_mix_task_type = "true" if ("MIX" in task_type) else "false" + LINE_CHANGE_CHAR = '\n' + alloc_success_code = 'return 1;' + sync_lock_fail_code = 'fprintf(stderr, "Error: syncBlockLock allocation failed\\n"); return;' + workspace_fail_code = 'fprintf(stderr, "Error: workspace allocation failed\\n"); return;' + npu_utils_so_path = NPUUtils().get_so_path() + # The generated launcher source is part of its cache key. Preserve only the + # deterministic cache-key directory so the launcher can be reused after the + # cache root changes. + npu_utils_cache_relative = os.path.join( + os.path.basename(os.path.dirname(npu_utils_so_path)), + os.path.basename(npu_utils_so_path), + ) + cpp_npu_utils_dlopen = f""" +typedef void* (*triton_allocate_workspace_t)(uint64_t, void**); +typedef void* (*triton_allocate_sync_block_lock_t)(uint64_t, void*, void**); +typedef void (*triton_async_launch_t)(void*, const char*); +typedef void (*triton_release_retained_tensor_t)(void*); + +static triton_allocate_workspace_t g_allocate_workspace = nullptr; +static triton_allocate_sync_block_lock_t g_allocate_sync_block_lock = nullptr; +static triton_async_launch_t g_async_launch = nullptr; +static triton_release_retained_tensor_t g_release_retained_tensor = nullptr; + +static bool npu_utils_ready() {{ + return g_allocate_workspace && + g_allocate_sync_block_lock && + g_async_launch && + g_release_retained_tensor; +}} + +static void init_npu_utils() {{ + if (npu_utils_ready()) return; + const char* cache_root = std::getenv("TRITON_CACHE_DIR"); + std::string npu_utils_path; + if (cache_root && cache_root[0] != '\\0') {{ + npu_utils_path = std::string(cache_root) + "/{npu_utils_cache_relative}"; + }} else {{ + const char* triton_home = std::getenv("TRITON_HOME"); + const char* home = std::getenv("HOME"); + const char* base = triton_home && triton_home[0] != '\\0' ? triton_home : home; + if (!base || base[0] == '\\0') {{ + fprintf(stderr, "Error: neither TRITON_CACHE_DIR nor TRITON_HOME/HOME is set\\n"); + return; + }} + npu_utils_path = std::string(base) + "/.triton/cache/{npu_utils_cache_relative}"; + }} + void* handle = dlopen(npu_utils_path.c_str(), RTLD_LAZY); + if (!handle) {{ + fprintf(stderr, "Error: dlopen %s failed: %s\\n", npu_utils_path.c_str(), dlerror()); + return; + }} + g_allocate_workspace = (triton_allocate_workspace_t)dlsym(handle, "triton_allocate_workspace"); + g_allocate_sync_block_lock = (triton_allocate_sync_block_lock_t)dlsym(handle, "triton_allocate_sync_block_lock"); + g_async_launch = (triton_async_launch_t)dlsym(handle, "triton_async_launch"); + g_release_retained_tensor = (triton_release_retained_tensor_t)dlsym(handle, "triton_release_retained_tensor"); +}} + +static void release_npu_tensor_handle(void* handle) {{ + if (!handle) return; + if (!g_release_retained_tensor) {{ + fprintf(stderr, "Error: triton_release_retained_tensor is unavailable\\n"); + return; + }} + g_release_retained_tensor(handle); +}} +""" + + mapping_applied = getattr(metadata, "program_grid_mapping_applied", None) + row_coalescing_applied = getattr(metadata, "row_coalescing_applied", None) + auto_blockify_enabled = getattr(metadata, "auto_blockify_enabled", None) + ptsm_cap_authorized = getattr(metadata, "ptsm_cap_authorized", None) + if not isinstance(mapping_applied, bool): + raise RuntimeError("compiler metadata missing program_grid_mapping_applied") + if not isinstance(row_coalescing_applied, bool): + raise RuntimeError("compiler metadata missing row_coalescing_applied") + if not isinstance(auto_blockify_enabled, bool): + raise RuntimeError("compiler metadata missing auto_blockify_enabled") + if not isinstance(ptsm_cap_authorized, bool): + raise RuntimeError("compiler metadata missing ptsm_cap_authorized") + raw_program_grid_transforms = getattr(metadata, "program_grid_transforms", None) + if not mapping_applied: + if raw_program_grid_transforms is not None: + raise RuntimeError("program_grid_mapping_applied disagrees with program_grid_transforms") + program_grid_transforms = None + else: + if raw_program_grid_transforms is None: + raise RuntimeError("program_grid_mapping_applied requires program_grid_transforms") + try: + program_grid_transforms = normalize_program_grid_transforms(raw_program_grid_transforms) + except ProgramGridContractError as error: + raise RuntimeError(f"invalid program_grid_transforms launcher metadata: {error}") from error + + persistent_transform = (get_persistent_transform(program_grid_transforms) + if program_grid_transforms is not None else None) + if program_grid_transforms is not None and row_coalescing_applied: + raise RuntimeError("program-grid transforms conflict with legacy RowCoalescing") + if auto_blockify_enabled and ptsm_cap_authorized: + raise RuntimeError("auto_blockify_enabled and ptsm_cap_authorized cannot both be true") + if persistent_transform is None: + if ptsm_cap_authorized: + raise RuntimeError("ptsm_cap_authorized requires a persistent transform") + elif not ptsm_cap_authorized: + raise RuntimeError("persistent program-grid transform lacks PTSM cap authorization") + elif mix_mode != "aiv": + raise RuntimeError("persistent program-grid transform requires final mix_mode=aiv") + + launcher_cap_enabled = auto_blockify_enabled + + program_grid_finalization = "" + if program_grid_transforms is not None: + finalization_lines = [ + f"// hacc.program_grid_transforms v{PROGRAM_GRID_TRANSFORMS_VERSION}: dynamic original-grid ABI.", + "uint32_t originalGridX = static_cast(gridX);", + "uint32_t originalGridY = static_cast(gridY);", + ] + axis_names = {0: ("gridX", "originalGridX"), 1: ("gridY", "originalGridY")} + for transform in program_grid_transforms["transforms"]: + grid_name, original_name = axis_names[transform["axis"]] + factor = transform["factor"] + finalization_lines.append( + f"{grid_name} = (uint32_t)(((uint64_t){original_name} + {factor - 1}u) / {factor}u);") + if persistent_transform is not None: + axis = persistent_transform["axis"] + grid_name = {0: "gridX", 1: "gridY", 2: "gridZ"}[axis] + other_grid_names = tuple(name for index, name in enumerate(("gridX", "gridY", "gridZ")) if index != axis) + finalization_lines.extend(( + f"uint64_t otherPrograms = (uint64_t){other_grid_names[0]} * (uint64_t){other_grid_names[1]};", + "uint32_t axisCap = std::max((uint32_t)1,", + f" (uint32_t)((uint64_t){num_physical_blocks} / std::max((uint64_t)1, otherPrograms)));", + f"{grid_name} = std::min({grid_name}, static_cast(axisCap));", + )) + if program_grid_transforms is not None: + program_grid_finalization = "\n ".join(finalization_lines) + + # Full-TA tile/strided coalescing: the compiler recorded a coalesce factor H + # and the program-id/grid axis it applies to. Each program now covers H tiles + # along that axis, so the host shrinks the matching grid dim by H here (the + # equivalent of what bishengir AutoBlockify used to do via hacc.coalesce_factor; + # bishengir no longer touches it). RowCoalescing can request ceil-div because + # its generated row mask handles tail rows. + coalesce_factor = int(getattr(metadata, "coalesce_factor", 1) or 1) + coalesce_axis = int(getattr(metadata, "coalesce_axis", -1)) + coalesce_grid_ceil_div = bool(getattr(metadata, "coalesce_grid_ceil_div", False)) + if coalesce_factor > 1 and coalesce_axis in (0, 1, 2): + _coalesce_grid_var = {0: "gridX", 1: "gridY", 2: "gridZ"}[coalesce_axis] + _coalesce_grid_expr = (f"({_coalesce_grid_var} + {coalesce_factor} - 1) / {coalesce_factor}" + if coalesce_grid_ceil_div else f"{_coalesce_grid_var} / {coalesce_factor}") + coalesce_grid_div = ( + f"// coalescing: each program covers {coalesce_factor} tiles along " + f"axis {coalesce_axis}; shrink that grid dim.\n" + + ("" if coalesce_grid_ceil_div else f" assert({_coalesce_grid_var} % {coalesce_factor} == 0 && " + f"\"ChunkCoalescing: grid[{coalesce_axis}] not divisible by coalesce_factor {coalesce_factor}\");\n") + + f" {_coalesce_grid_var} = {_coalesce_grid_expr};") + else: + coalesce_grid_div = "" + if program_grid_transforms is not None and coalesce_factor > 1: + raise RuntimeError("program-grid transforms conflict with legacy coalesce metadata") + if row_coalescing_applied: + if coalesce_factor <= 1 or coalesce_axis not in (0, 1, 2): + raise RuntimeError("row_coalescing_applied requires valid coalesce metadata") + elif coalesce_factor != 1 or coalesce_axis != -1 or coalesce_grid_ceil_div: + raise RuntimeError("unmatched RowCoalescing must retain default coalesce metadata") + has_dynamic_program_grid_abi = program_grid_transforms is not None + + cpp_device_pointer = _CPP_DEVICE_POINTER + cpp_msprof_extern = _CPP_MSPROF_EXTERN + cpp_msprof_callback = _CPP_MSPROF_CALLBACK + cpp_msprof_call_before_launch = _CPP_MSPROF_BEFORE_LAUNCH + + cpp_msprof_call_after_launch = f""" + if (__MsprofFlagL0 || __MsprofFlagL1) + {{ + endTime = MsprofSysCycleTime(); + opNameHashID = MsprofGetHashId(_kernelName, length); + threadId = (unsigned int)(syscall(SYS_gettid)); + MsprofApi info; + info.level = MSPROF_REPORT_NODE_LEVEL; + info.magicNumber = 0x5a5a; //MSPROF_REPORT_DATA_MAGIC_NUM + info.type = MSPROF_REPORT_NODE_LAUNCH_TYPE; + info.threadId = threadId; + info.reserve = 0; + info.beginTime = beginTime; + info.endTime = endTime; + info.itemId = opNameHashID; + MsprofReportApi(false, &info); + }} + if (__MsprofFlagL1) + {{ + MsprofCompactInfo nodeBasicInfo; + nodeBasicInfo.level = MSPROF_REPORT_NODE_LEVEL; + nodeBasicInfo.magicNumber = 0x5a5a; //MSPROF_REPORT_DATA_MAGIC_NUM + nodeBasicInfo.type = MSPROF_REPORT_NODE_BASIC_INFO_TYPE; + nodeBasicInfo.threadId = threadId; + nodeBasicInfo.timeStamp = endTime; + nodeBasicInfo.data.nodeBasicInfo.opName = opNameHashID; + nodeBasicInfo.data.nodeBasicInfo.opType = opNameHashID; + nodeBasicInfo.data.nodeBasicInfo.taskType = {task_type}; + nodeBasicInfo.data.nodeBasicInfo.blockDim = nodeBasicBlockDim; + MsprofReportCompactInfo(0, static_cast(&nodeBasicInfo), sizeof(MsprofCompactInfo)); + + // 'mix' kernel need to report the ctxID + if ({is_mix_task_type} > 0) {{ + MsprofAdditionalInfo info; + info.level = MSPROF_REPORT_NODE_LEVEL; + info.type = MSPROF_REPORT_NODE_CONTEXT_ID_INFO_TYPE; + info.threadId = threadId; + info.timeStamp = endTime; + MsprofContextIdInfo ctxId; + ctxId.opName = opNameHashID; + ctxId.ctxIdNum = 1; + for (uint32_t i = 0; i < ctxId.ctxIdNum; i++) {{ + ctxId.ctxIds[i] = i; + }} + size_t copyLen = sizeof(MsprofContextIdInfo); + if (copyLen > MSPROF_ADDTIONAL_INFO_DATA_LENGTH) {{ + copyLen = MSPROF_ADDTIONAL_INFO_DATA_LENGTH; + }} + memcpy(info.data, &ctxId, copyLen); + MsprofReportAdditionalInfo(false, static_cast(&info), sizeof(MsprofAdditionalInfo)); + }} + + // Report tensor info + int max_tensors_num = tensorShapes.size() < MSPROF_GE_TENSOR_DATA_NUM ? tensorShapes.size() : MSPROF_GE_TENSOR_DATA_NUM; + MsprofAdditionalInfo tensorInfo; + tensorInfo.level = MSPROF_REPORT_NODE_LEVEL; + tensorInfo.type = MSPROF_REPORT_NODE_TENSOR_INFO_TYPE; + tensorInfo.threadId = threadId; + tensorInfo.timeStamp = endTime; + auto profTensorData = reinterpret_cast(tensorInfo.data); + profTensorData->opName = opNameHashID; + int tensorCount = 0; + int dataTypes[MSPROF_GE_TENSOR_DATA_NUM]; + if (tensorShapes.size() > 0) {{ + {LINE_CHANGE_CHAR.join( + f'dataTypes[{idx}] = {convert_sigtype_to_int(ty[1:])};' + for idx, (_, ty) in enumerate( + (k, v) for k, v in signature.items() if v.startswith("*") + ) + if idx < 5 + )} + }} + for (int i = 0; i < tensorShapes.size() && tensorCount < MSPROF_GE_TENSOR_DATA_NUM; i++) {{ + auto fillTensorData = [&](int index, int tensorType) {{ + profTensorData->tensorData[index].tensorType = tensorType; + profTensorData->tensorData[index].format = 2; // GeDataFormat: ND = 2 + profTensorData->tensorData[index].dataType = dataTypes[i]; + int nDim = tensorShapes[i].size(); + nDim = nDim < MSPROF_GE_TENSOR_DATA_SHAPE_LEN ? nDim : MSPROF_GE_TENSOR_DATA_SHAPE_LEN; + for (int j = 0; j < nDim; j++) {{ + profTensorData->tensorData[index].shape[j] = tensorShapes[i][j]; + }} + for (int j = nDim; j < MSPROF_GE_TENSOR_DATA_SHAPE_LEN; j++) {{ + profTensorData->tensorData[index].shape[j] = 0; + }} + }}; + int tensorType = (i < tensorKinds.size()) ? tensorKinds[i] : 0; // DeFault tensor type is input + if (tensorType == TENSOR_KIND_INPUT || tensorType == TENSOR_KIND_INPUT_OUTPUT) {{ + fillTensorData(tensorCount, MSPROF_GE_TENSOR_TYPE_INPUT); + tensorCount++; + }} + if ((tensorType == TENSOR_KIND_OUTPUT || tensorType == TENSOR_KIND_INPUT_OUTPUT) && tensorCount < MSPROF_GE_TENSOR_DATA_NUM){{ + fillTensorData(tensorCount, MSPROF_GE_TENSOR_TYPE_OUTPUT); + tensorCount++; + }} + }} + profTensorData->tensorNum = tensorCount; + MsprofReportAdditionalInfo(false, static_cast(&tensorInfo), sizeof(MsprofAdditionalInfo)); + }} +""" + + def _make_kernel_launch(args_ptr, args_size, indent=" "): + cfg_setup = "" + if compile_on_910_95 and enable_simt: + cfg_setup = f"{indent}void *kernel_cfg = cann_get_launch_kernel_cfg({metadata.shared_mem_dynamic_size});\n" + else: + cfg_setup = f"{indent}void *kernel_cfg = nullptr;\n" + return f"""{cfg_setup}{indent}ret = cann_launch_kernel(func, blockNum, stream, kernel_cfg, {args_ptr}, {args_size}); +""" + + cpp_kernel_launch = _make_kernel_launch("static_cast(launch_args.data())", "launch_args.size()") + cpp_kernel_launch_local = _make_kernel_launch("&args", "sizeof(args)", indent=" ") + + npu_headers = generate_npu_header_src() + + if not mapping_applied and not row_coalescing_applied: + _launch_preamble = f""" + void* workspace_addr_ptr = nullptr; + void* workspace_handle = nullptr; + {coalesce_grid_div} + uint32_t blockNum4Workspace = gridX * gridY * gridZ; + {get_backend_func("pre_launch", True)} + {f''' + uint64_t totalWorkSpaceSize = (uint64_t){workspace_size} * blockNum4Workspace; + {get_backend_func("allocate_memory", "totalWorkSpaceSize", "stream")} + std::shared_ptr workspace_handle_guard(workspace_handle, release_npu_tensor_handle); + if (!workspace_addr_ptr) {{ + {workspace_fail_code} + }} + ''' if workspace_size > 0 else ''}""" + _python_launch_preamble = _launch_preamble + else: + _program_grid_launch_preamble = f""" + {program_grid_finalization} + {coalesce_grid_div if program_grid_transforms is None else ''}""" + _launch_preamble = f""" + void* workspace_addr_ptr = nullptr; + void* workspace_handle = nullptr; +{_program_grid_launch_preamble} + uint32_t blockNum4Workspace = gridX * gridY * gridZ; + {get_backend_func("pre_launch", True)} + {f''' + uint64_t totalWorkSpaceSize = (uint64_t){workspace_size} * blockNum4Workspace; + {get_backend_func("allocate_memory", "totalWorkSpaceSize", "stream")} + std::shared_ptr workspace_handle_guard(workspace_handle, release_npu_tensor_handle); + if (!workspace_addr_ptr) {{ + {workspace_fail_code} + }} + ''' if workspace_size > 0 else ''}""" + _python_launch_preamble = f""" + void* workspace_addr_ptr = nullptr; + void* workspace_handle = nullptr; +{_program_grid_launch_preamble} + uint32_t blockNum4Workspace = gridX * gridY * gridZ; + {get_backend_func("pre_launch", True)} + {f''' + uint64_t totalWorkSpaceSize = (uint64_t){workspace_size} * blockNum4Workspace; + {get_backend_func("allocate_memory", "totalWorkSpaceSize", "stream")} + std::shared_ptr workspace_handle_guard(workspace_handle, release_npu_tensor_handle); + if (!workspace_addr_ptr) {{ + {workspace_fail_code} + }} + ''' if workspace_size > 0 else ''}""" + + preserve_legacy_launcher_layout = mapping_applied or row_coalescing_applied + original_grid_offset_decls = (" size_t original_grid_x_offset = reserve_slot(sizeof(uint32_t), 4); " + "size_t original_grid_y_offset = reserve_slot(sizeof(uint32_t), 4);\n" if + has_dynamic_program_grid_abi else " \n" if preserve_legacy_launcher_layout else "") + original_grid_copy = (" memcpy(launch_args.data() + original_grid_x_offset, &originalGridX, sizeof(uint32_t)); " + "memcpy(launch_args.data() + original_grid_y_offset, &originalGridY, sizeof(uint32_t));\n" + if has_dynamic_program_grid_abi else " \n" if preserve_legacy_launcher_layout else "") + original_grid_struct_fields = (" uint32_t originalGridX __attribute__((aligned(4))); " + "uint32_t originalGridY __attribute__((aligned(4)));\n" + if has_dynamic_program_grid_abi else + " \n" if preserve_legacy_launcher_layout else "") + original_grid_struct_values = (" static_cast(originalGridX), static_cast(originalGridY),\n" + if has_dynamic_program_grid_abi else + " \n" if preserve_legacy_launcher_layout else "") + + _launch_lambda_pre = f""" {'std::function launch_call = [=]() -> cann_error' if enable_taskqueue else ''} {{ + {get_backend_func("pre_launch", False)} + uint32_t blockNum = gridX * gridY * gridZ; + + #ifdef ENABLE_GRID_WARN_PRINT + static bool warned = false; + if (!warned && blockNum > (uint32_t){num_physical_blocks}) {{ + printf("WARNING: Grid %u > physical limit {num_physical_blocks}, performance maybe reduced.\\n",blockNum); + warned = true; + }} + #endif + {'blockNum = std::min(blockNum, (uint32_t)' + str(num_physical_blocks) + ');' if launcher_cap_enabled else ''} + // set mixBlockNumRation for nodeBasicBlockDim for msprof report + uint32_t mixBlockNumRation = {mix_block_dim_ratio}; + uint32_t nodeBasicBlockDim = (mixBlockNumRation << 16) + blockNum; + + {'cce::internal::DebugTunnelData *DTData = cce::internal::DebugTunnel::Open(blockNum);' if enable_device_print else ''} + cann_error ret = CANN_SUCCESS; + {'void *ffts_addr = nullptr; ret = get_ffts_addr(stream, &ffts_addr);' if target_support_ffts else ''} + {'if (ret != CANN_SUCCESS) return ret;' if (target_support_ffts and enable_taskqueue) else 'if (ret != CANN_SUCCESS) return;' if (target_support_ffts and (not enable_taskqueue)) else ''} + // stub argument for workspace + void *syncBlockLock_ptr = nullptr; + void *syncBlockLock_handle = nullptr; + uint16_t ModuleId = 0; + {f''' + {sync_block_lock_layout_stmt} + {get_backend_func("allocate_sync_block_lock", "syncBlockLockSize", "stream")} + std::shared_ptr syncBlockLock_handle_guard(syncBlockLock_handle, release_npu_tensor_handle); + if (!syncBlockLock_ptr) {{ + {alloc_success_code if enable_taskqueue else sync_lock_fail_code} + }} + {lock_init_stmt} + if (ret != CANN_SUCCESS) {{ + return {'ret' if enable_taskqueue else ''}; + }} + ''' if has_sync_block_lock else ''} + {'if (ret != CANN_SUCCESS) {{ return ret; }}' if (workspace_size > 0 and enable_taskqueue) else 'if (ret != CANN_SUCCESS) {{ return; }}' if (workspace_size > 0 and not enable_taskqueue) else ''}""" + + _launch_lambda_post = f""" + {cpp_msprof_call_before_launch} + __KERNEL_LAUNCH_CALL__ + {'void*& stream_ref = const_cast(stream);' if enable_device_print else ''} + {'cce::internal::DebugTunnel::Close(DTData, stream_ref);' if enable_device_print else ''} + {cpp_msprof_call_after_launch} + {'return ret;' if enable_taskqueue else 'ret = cann_synchronize_stream(stream);'} + }}; + {f'''{get_backend_func("async_launch", "launch_call") if enable_taskqueue else ''}'''} + return; +}}""" + + return f""" +{npu_headers} +{'#define __CCE_ENABLE_PRINT__' if enable_device_print else ''} +{extract_device_print_code_from_cann() if enable_device_print else ''} +#define PY_SSIZE_T_CLEAN +{'#define ENABLE_GRID_WARN_PRINT' if enable_grid_warn_print else ''} +#define TENSOR_KIND_INPUT 0 +#define TENSOR_KIND_OUTPUT 1 +#define TENSOR_KIND_INPUT_OUTPUT 2 + +{cpp_msprof_extern} + +{cpp_npu_utils_dlopen} + +{cpp_device_pointer} + +{_CPP_ALIGN_LAUNCH_OFFSET} + +extern "C" {{ +void triton_launch_kernel(const char* kernelName, cann_func_handle func, cann_stream stream, + int gridX, int gridY, int gridZ, + const int64_t* shapes_data, const int* shape_dims, int num_tensors, + const int* tensor_kinds, + const void* const* kernel_args, const size_t* arg_sizes, int num_args) {{ + if (gridX <= 0 || gridY <= 0 || gridZ <= 0) {{ + printf("WARNING: Skipping launch for kernel '%s' due to empty grid (gridX=%d, gridY=%d, gridZ=%d).\\n", + kernelName, gridX, gridY, gridZ); + return; + }} + std::vector> tensorShapes; + if (shapes_data != nullptr && shape_dims != nullptr) {{ + int shapes_idx = 0; + for (int tensor_idx = 0; tensor_idx < num_tensors; ++tensor_idx) {{ + std::vector tensorShape; + for (int dim_idx = 0; dim_idx < shape_dims[tensor_idx]; ++dim_idx) {{ + tensorShape.push_back(shapes_data[shapes_idx++]); + }} + tensorShapes.push_back(tensorShape); + }} + }} + std::vector tensorKinds; + if (tensor_kinds != nullptr && num_tensors > 0) {{ + tensorKinds.assign(tensor_kinds, tensor_kinds + num_tensors); + }} + if (num_args > 0 && (kernel_args == nullptr || arg_sizes == nullptr)) {{ + return; + }} + std::vector launch_arg_sizes; + launch_arg_sizes.reserve(num_args); + std::vector> copied_kernel_args; + copied_kernel_args.reserve(num_args); + for (int arg_idx = 0; arg_idx < num_args; ++arg_idx) {{ + launch_arg_sizes.push_back(arg_sizes[arg_idx]); + copied_kernel_args.emplace_back(arg_sizes[arg_idx]); + memcpy(copied_kernel_args.back().data(), kernel_args[arg_idx], arg_sizes[arg_idx]); + }} + + // Only 1D parallelization is supported for NPU. + // Pointer type becomes flattened 1-D Memref tuple: base_ptr, data_ptr, + // offset, shape, stride. base_ptr offset shape and stride are not used, + // arbitrarily set for now. +{_launch_preamble} +{_launch_lambda_pre} + + size_t args_offset = 0; + auto reserve_slot = [&](size_t size, size_t alignment) -> size_t {{ + args_offset = _align_launch_offset(args_offset, alignment); + size_t current_offset = args_offset; + args_offset += size; + return current_offset; + }}; + {'size_t ffts_offset = reserve_slot(sizeof(void*), 8);' if target_support_ffts else ''} + {'size_t sync_block_lock_offset = reserve_slot(sizeof(void*), 8);' if not metadata.is_pure_simt else ''} + {'size_t workspace_offset = reserve_slot(sizeof(void*), 8);' if not metadata.is_pure_simt else ''} + size_t kernel_args_offset = args_offset; + for (int arg_idx = 0; arg_idx < num_args; ++arg_idx) {{ + size_t alignment = launch_arg_sizes[arg_idx] >= 8 ? 8 : (launch_arg_sizes[arg_idx] >= 4 ? 4 : 1); + args_offset = _align_launch_offset(args_offset, alignment); + args_offset += launch_arg_sizes[arg_idx]; + }} +{original_grid_offset_decls} size_t grid_offset = reserve_slot(sizeof(int32_t), 4); + reserve_slot(sizeof(int32_t), 4); + reserve_slot(sizeof(int32_t), 4); + {'reserve_slot(sizeof(void*), 8);' if metadata.is_pure_simt else ''} + {'reserve_slot(sizeof(void*), 8);' if metadata.is_pure_simt else ''} + {'size_t dtdata_offset = reserve_slot(sizeof(void*), 8);' if enable_device_print else 'reserve_slot(sizeof(void*), 8);'} + size_t total_size = args_offset; + + std::vector launch_args(total_size, 0); + {'memcpy(launch_args.data() + ffts_offset, &ffts_addr, sizeof(void*));' if target_support_ffts else ''} + {f'memcpy(launch_args.data() + sync_block_lock_offset, &syncBlockLock_ptr, sizeof(void*));' if not metadata.is_pure_simt else ''} + {f'memcpy(launch_args.data() + workspace_offset, &workspace_addr_ptr, sizeof(void*));' if not metadata.is_pure_simt else ''} + size_t kernel_arg_offset = kernel_args_offset; + for (int arg_idx = 0; arg_idx < num_args; ++arg_idx) {{ + size_t alignment = launch_arg_sizes[arg_idx] >= 8 ? 8 : (launch_arg_sizes[arg_idx] >= 4 ? 4 : 1); + kernel_arg_offset = _align_launch_offset(kernel_arg_offset, alignment); + memcpy(launch_args.data() + kernel_arg_offset, copied_kernel_args[arg_idx].data(), launch_arg_sizes[arg_idx]); + kernel_arg_offset += launch_arg_sizes[arg_idx]; + }} +{original_grid_copy} memcpy(launch_args.data() + grid_offset, &gridX, sizeof(int32_t)); + memcpy(launch_args.data() + grid_offset + sizeof(int32_t), &gridY, sizeof(int32_t)); + memcpy(launch_args.data() + grid_offset + 2 * sizeof(int32_t), &gridZ, sizeof(int32_t)); + {'memcpy(launch_args.data() + dtdata_offset, &DTData, sizeof(void*));' if enable_device_print else ''} + +{_launch_lambda_post.replace('__KERNEL_LAUNCH_CALL__', cpp_kernel_launch)} +}} // extern "C" + +static void _launch(const char* kernelName, cann_func_handle func, cann_stream stream, + int gridX, int gridY, int gridZ, + std::vector> &tensorShapes, std::vector &tensorKinds{(', ' + arg_decls) if len(arg_decls) > 0 else ''}) {{ + // Keep Python launcher on the stable local packing path. + if (gridX <=0 || gridY <=0 || gridZ <=0) {{ + printf("WARNING: Skipping launch for kernel '%s' due to empty grid (gridX=%d, gridY=%d, gridZ=%d).\\n", kernelName, gridX, gridY, gridZ); + return; + }} +{_python_launch_preamble} +{_launch_lambda_pre} + struct __attribute__((packed)) {{ + {'void* ffts_addr __attribute__((aligned(8)));' if target_support_ffts else ''} + {'void* syncBlockLock __attribute__((aligned(8)));' if not metadata.is_pure_simt else ''} + {'void* workspace_addr __attribute__((aligned(8)));' if not metadata.is_pure_simt else ''} + {' '.join(f'{ty_to_cpp(ty)} arg{i} __attribute__((aligned({4 if ty[0] != "*" and ty[-2:] != "64" else 8})));' for i, ty in signature.items() if ty != "constexpr")} +{original_grid_struct_fields} {' '.join(f'{ty_to_cpp(ty)} grid{mark} __attribute__((aligned(4)));' for mark, ty in grid_info.items())} + {'void* global_scratch __attribute__((aligned(8)));' if metadata.is_pure_simt else ''} + {'void* profile_scratch __attribute__((aligned(8)));' if metadata.is_pure_simt else ''} + {'void* DTData __attribute__((aligned(8)));'} + }} args = {{ + {'static_cast(ffts_addr),' if target_support_ffts else ''} + {('static_cast(syncBlockLock_ptr),' if has_sync_block_lock else 'nullptr,') if not metadata.is_pure_simt else ''} + {('static_cast(workspace_addr_ptr),' if workspace_size > 0 else 'nullptr,') if not metadata.is_pure_simt else ''} + {(lambda _rt: (', '.join(_rt) + ',') if _rt else '')( + [f'static_cast<{ty_to_cpp(ty)}>(arg{i})' for i, ty in signature.items() if ty != "constexpr"] + )} +{original_grid_struct_values} {', '.join(f'static_cast<{ty_to_cpp(ty)}>(grid{mark})' for mark, ty in grid_info.items())} + {', static_cast(nullptr)' if metadata.is_pure_simt else ''} + {', static_cast(nullptr)' if metadata.is_pure_simt else ''} + {', static_cast(DTData)' if enable_device_print else ', static_cast(nullptr)'} + }}; +{_launch_lambda_post.replace('__KERNEL_LAUNCH_CALL__', cpp_kernel_launch_local)} + +{_CPP_GET_TENSOR_SHAPE} + +static PyObject* launch(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {{ + int gridX, gridY, gridZ; + cann_stream stream; + cann_func_handle function; + PyObject *packedMetadata = nullptr; + PyObject *launch_metadata = nullptr; + PyObject *launch_enter_hook = nullptr; + PyObject *launch_exit_hook = nullptr; + std::vector> tensorShapes; + + {newline.join([f"{_extracted_type(ty)} _arg{i};" for i, ty in signature.items()])} + // METH_FASTCALL fast path: avoid per-call tuple allocation (METH_VARARGS) and + // skip PyArg_ParseTuple's format-string interpreter by parsing manually. + // Borrowed-reference semantics match PyArg_ParseTuple("O"). + if (nargs != {total_nargs}) {{ + PyErr_Format(PyExc_TypeError, "launch expects %d arguments, got %zd", {total_nargs}, nargs); + return nullptr; + }} + gridX = (int)PyLong_AsLong(args[0]); + gridY = (int)PyLong_AsLong(args[1]); + gridZ = (int)PyLong_AsLong(args[2]); + stream = reinterpret_cast(PyLong_AsUnsignedLongLong(args[3])); + function = reinterpret_cast(PyLong_AsUnsignedLongLong(args[4])); + packedMetadata = args[5]; + launch_metadata = args[6]; + launch_enter_hook = args[7]; + launch_exit_hook = args[8]; + {fastcall_sig_parse_stmts} + if (PyErr_Occurred()) {{ + return nullptr; + }} + if (__MsprofFlagL1) {{ + { + LINE_CHANGE_CHAR.join( + f"{{ auto tmp = _get_tensor_shape(_arg{i}); if (!tmp.empty()) tensorShapes.push_back(tmp); }}" + for i, ty in signature.items() if ty[0] == "*" + ) + } + }} + + if (launch_enter_hook != Py_None){{ + PyObject* hook_args = Py_BuildValue("(O)", launch_metadata); + PyObject* hook_ret = PyObject_CallObject(launch_enter_hook, hook_args); + Py_DECREF(hook_args); + if (!hook_ret) + return nullptr; + }} + + // get kernel_name (use interned key to avoid temporary PyUnicode per call) + static PyObject* key_kernel_name = PyUnicode_InternFromString("kernel_name"); + PyObject* kernelNameObj = PyDict_GetItemWithError(packedMetadata, key_kernel_name); + if (!kernelNameObj) {{ + PyErr_SetString(PyExc_KeyError, "packedMetadata missing 'kernel_name'"); + return nullptr; + }} + const char* kernelName = PyUnicode_AsUTF8(kernelNameObj); + // get tensor_kinds (use interned key, cache result in tensorKinds) + if (tensorKinds.empty()) {{ + static PyObject* key_tensor_kinds = PyUnicode_InternFromString("tensor_kinds"); + PyObject* tensorKindList = PyDict_GetItemWithError(packedMetadata, key_tensor_kinds); + if (tensorKindList) {{ + Py_ssize_t size = PySequence_Size(tensorKindList); + for (Py_ssize_t i = 0; i < size; ++i) {{ + PyObject* kind = PySequence_GetItem(tensorKindList, i); + tensorKinds.push_back(PyLong_AsLong(kind)); + Py_DECREF(kind); + }} + }} + }} + + // raise exception asap + {newline.join(ptr_decls)} + _launch(kernelName, function, stream, + gridX, gridY, gridZ, + tensorShapes, tensorKinds + {', ' + ', '.join(internal_args_list) if len(internal_args_list) > 0 else ''}); + if (PyErr_Occurred()) {{ + return nullptr; + }} + if(launch_exit_hook != Py_None){{ + PyObject* hook_args = Py_BuildValue("(O)", launch_metadata); + PyObject* hook_ret = PyObject_CallObject(launch_exit_hook, hook_args); + Py_DECREF(hook_args); + if (!hook_ret) + return nullptr; + }} + Py_RETURN_NONE; +}} + +static PyMethodDef ModuleMethods[] = {{ + {{"launch", (PyCFunction)launch, METH_FASTCALL, "Entry point for all kernels with this signature"}}, + {{nullptr, nullptr, 0, nullptr}} // sentinel +}}; + +static struct PyModuleDef ModuleDef = {{ + PyModuleDef_HEAD_INIT, + \"__triton_launcher\", + nullptr, //documentation + -1, //size + ModuleMethods +}}; + +PyMODINIT_FUNC PyInit___triton_launcher(void) {{ + PyObject *m = PyModule_Create(&ModuleDef); + if(m == nullptr) {{ + return nullptr; + }} + PyModule_AddFunctions(m, ModuleMethods); + {cpp_msprof_callback} + // One-time initialization of NPU utils (dlsym lookup for g_async_launch etc.) + // Moved here from the per-call async_launch path to avoid repeated dlsym work. + init_npu_utils(); + return m; +}} +""" diff --git a/third_party/ascend/backend/dtype_guard.py b/third_party/ascend/backend/dtype_guard.py new file mode 100644 index 0000000000..e015efecdb --- /dev/null +++ b/third_party/ascend/backend/dtype_guard.py @@ -0,0 +1,564 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Unified, non-intrusive dtype interception for unsupported Ascend operators. +# +# Two thin monkey-patch layers are installed (see ``install()``): +# +# * The *builtin-dunder layer* wraps the operator-overload tensor dunders of +# ``triton.language.core.tensor`` (``__add__``/``__rshift__``/``__and__`` +# ...). Operator syntax (``a + b``, ``a >> b``, ``-a``, ``a == b`` ...) is +# turned by the AST tracer into a direct dunder builtin call and never +# passes through ``call_Function``, so this is the only place where the +# operator form can be intercepted. The corresponding ``TritonSemantic`` +# leaf methods are deliberately NOT patched: they are reused by internal +# lowering (``load`` builds its mask with ``and_``/``or_``/``not_``, +# ``sub`` lowers through ``minus``+``add``, ``atomic_min/max`` use +# ``not_equal``/``where`` ...), so wrapping them would misfire. The dunder +# builtin is reached only from the user's operator syntax. +# +# * The *entry layer* wraps ``CodeGenerator.call_Function`` and validates +# named builtins (``tl.*``), ``@jit`` composite ops (``tl.max``, +# ``tl.sum`` ...) and cann extension builtins before they run. +# +# The rules themselves live in ``op_dtype_config.py``. Both layers are driven +# entirely by that configuration: a rule opts into the dunder layer with a +# ``"dunder"`` list and into the entry layer with ``"names"``. + +import inspect +import contextvars +from functools import wraps + +from .op_dtype_config import ( + OP_DTYPE_RULES, + RULES_BY_NAME, + ALL_SCALAR_DTYPES, + FP8E4_DTYPES, + FP8E5_DTYPES, +) + +_LANG_MODULE_PREFIX = "triton.language" +_EXTRA_MARKER = ".extra" + +# While a triton-internal composite @jit helper (e.g. the Philox code behind +# ``tl.rand`` / ``tl.randint`` in ``triton/language/random.py``) is being +# traced, this counter is > 0. Those helpers legitimately mix uint32/uint64 +# arithmetic even though the standalone ``mul``/``add``/``xor``/``umulhi`` +# rules document those dtypes as unsupported: the documented public contract +# is that ``rand``/``randint`` accept integer offsets, and their internals +# must not be validated as if each inner op were a user-facing call. +_INTERNAL_COMPOSITE_TRACE = contextvars.ContextVar("ascend_internal_composite_trace", default=0) + + +def _in_internal_composite(): + return _INTERNAL_COMPOSITE_TRACE.get() > 0 + + +def _is_triton_internal_fn(fn): + target = _unwrap_callable(fn) + module = getattr(target, "__module__", "") or "" + return module.startswith("triton.") + + +class UnsupportedDtypeError(TypeError): + """Raised at the front-end when an Ascend operator gets an unsupported dtype.""" + + +def arch_bucket(arch): + """Classify an ``options.arch`` value into an ``"a2"`` / ``"a5"`` bucket.""" + try: + from triton.backends.ascend.utils import is_compile_on_910_95 + return "a5" if is_compile_on_910_95(arch) else "a2" + except Exception: + return "a2" + + +def _bucket_from_semantic(semantic): + options = getattr(getattr(semantic, "builder", None), "options", None) + arch = getattr(options, "arch", None) + return arch_bucket(arch) + + +def _normalize_dtype_name(name): + return "int1" if name == "bool" else name + + +# Friendly dtype spellings shown in the front-end rejection message. Keys cover +# both the canonical rule tokens and the concrete/internal runtime names. +DTYPE_DISPLAY_NAMES = { + "uint8": "uint8", + "int8": "int8", + "uint16": "uint16", + "int16": "int16", + "uint32": "uint32", + "int32": "int32", + "uint64": "uint64", + "int64": "int64", + "fp16": "float16", + "bf16": "bfloat16", + "fp32": "float32", + "fp64": "float64", + "fp8e4": "float8e4m3", + "fp8e5": "float8e5m2", + "bool": "bool", + "int1": "bool", + "fp8e4nv": "float8e4m3fn", + "fp8e4b8": "float8e4m3fnuz", + "fp8e4b15": "float8e4m3b11us", + "fp8e5b16": "float8e5m2fnuz", +} + + +def _display_dtype(name): + return DTYPE_DISPLAY_NAMES.get(name, name) + + +def _canonical_token(name): + """Map an extracted runtime dtype name onto a rule token. + + ``bool`` arrives as ``int1`` and the concrete fp8 variants collapse onto + their documented column (``fp8e4`` / ``fp8e5``). + """ + if name == "int1": + return "bool" + if name in FP8E4_DTYPES: + return "fp8e4" + if name in FP8E5_DTYPES: + return "fp8e5" + return name + + +def _extract_scalar_name(value, read_pointer=False): + """Return the scalar dtype ``name`` of a traced value, or ``None``. + + Understands: + * a ``tl.dtype`` itself (destination dtype of ``cast`` / ``full`` ...) + * a tensor / block (``value.type.scalar``) + * ``constexpr`` wrapping one of the above + * a pointer tensor: its pointee element dtype is returned only when + ``read_pointer`` is True (i.e. the position is declared in the rule's + ``pointer_args``). Otherwise pointers are skipped, because dunder + builtins are reused for address computation (``ptr + offsets``) and a + pointee dtype there does not describe the op itself. + Python scalars, shapes, strings and ``None`` yield ``None``. + """ + # Unwrap constexpr without importing the (compile-time) class directly. + value = getattr(value, "value", value) if value.__class__.__name__ == "constexpr" else value + + # A bare dtype object (e.g. the ``dtype`` argument of ``cast`` / ``full``). + name = getattr(value, "name", None) + if isinstance(name, str) and hasattr(value, "primitive_bitwidth") and not hasattr(value, "handle"): + return _normalize_dtype_name(name) + + type_obj = getattr(value, "type", None) + if type_obj is None: + return None + + # Unwrap a ``block_type`` (e.g. a vector of pointers produced by + # ``ptr + offsets``) before the pointer test. ``block_type`` does not + # override ``is_ptr`` and its ``scalar`` is the inner element type, so + # without unwrapping a pointer *block* would read back as the + # ``pointer`` spelling instead of its pointee dtype (or instead of + # being skipped when the position is not a declared pointer argument). + is_block = getattr(type_obj, "is_block", None) + if callable(is_block) and is_block(): + type_obj = getattr(type_obj, "element_ty", type_obj) + + is_ptr = getattr(type_obj, "is_ptr", None) + if callable(is_ptr) and is_ptr(): + if not read_pointer: + return None + element_ty = getattr(type_obj, "element_ty", None) + element_is_block = getattr(element_ty, "is_block", None) + if callable(element_is_block) and element_is_block(): + element_ty = getattr(element_ty, "element_ty", element_ty) + return _normalize_dtype_name(getattr(element_ty, "name", None)) + + scalar = type_obj + scalar_prop = getattr(type_obj, "scalar", None) + if scalar_prop is not None and not callable(scalar_prop): + scalar = scalar_prop + return _normalize_dtype_name(getattr(scalar, "name", None)) + + +def _unsupported_tokens(rule, bucket): + """Return the unsupported-dtype blacklist for a bucket, or ``None``. + + ``None`` means "no dtype restriction on that architecture" (a missing + bucket, see the rule schema in op_dtype_config.py); it must NOT fall back + to the other bucket's list. An empty list means nothing is rejected. + """ + unsupported = rule.get("unsupported") + if not isinstance(unsupported, dict) or bucket not in unsupported: + return None + return unsupported[bucket] + + +def _supported_tokens(rule, bucket): + """Return the accepted-dtype list for a bucket as rule tokens. + + Used only to build the user-facing error message. By default the list is + NOT stored: it is derived as the complement of the bucket's ``unsupported`` + blacklist over ``ALL_SCALAR_DTYPES``, preserving universe order, which is + exactly the whitelist the configuration used to carry by hand. + + A rule may still spell out an explicit ``supported`` list when its + advertised dtypes are intentionally narrower than the blacklist complement + (currently only ``dot_scaled``); that explicit value takes precedence. + + A missing bucket in ``unsupported`` means "no dtype restriction on that + architecture", so every dtype is accepted there. + """ + unsupported = _unsupported_tokens(rule, bucket) + if unsupported is None: + return list(ALL_SCALAR_DTYPES) + explicit = rule.get("supported") + if isinstance(explicit, dict) and bucket in explicit: + return explicit[bucket] + blocked = set(unsupported) + return [token for token in ALL_SCALAR_DTYPES if token not in blocked] + + +def _format_supported_list(tokens): + if not tokens: + return "none" + names = list(dict.fromkeys(_display_dtype(t) for t in tokens)) + return ", ".join(names) + + +def _align_operands(fn, args, kws): + """Map runtime args/kws onto the public builtin positional parameters. + + This returns a list indexed exactly like the documented builtin signature + (self of a member-style call included), so that ``tensor_args`` positions + are stable regardless of whether an operand was passed positionally or by + keyword (e.g. ``cast(x, dtype=...)`` / ``zeros(shape, dtype=...)``). + Falls back to the raw args when introspection is unavailable. + """ + try: + signature_target = _unwrap_callable(fn) + try: + sig = inspect.signature(signature_target) + except (TypeError, ValueError): + return list(args) + try: + bound = sig.bind_partial(*args, **dict(kws or ())) + except TypeError: + return list(args) + arguments = bound.arguments + ordered = [] + for name, param in sig.parameters.items(): + if name in ("_semantic", "_generator"): + continue + if name in arguments: + value = arguments[name] + if param.kind == inspect.Parameter.VAR_POSITIONAL: + ordered.extend(value) + else: + ordered.append(value) + return ordered + except Exception: + return list(args) + + +def validate(rule_id, args, bucket, op_label=None, kws=None, fn=None): + """Validate selected operands against one rule. + + ``args`` are the raw runtime positional arguments. When ``fn``/``kws`` are + provided (the entry layer), operands are aligned onto the builtin + signature so keyword dst-dtypes land on their configured position. + """ + rule = OP_DTYPE_RULES[rule_id] + tokens = _unsupported_tokens(rule, bucket) + # A missing bucket means "no dtype restriction" for this architecture. + if tokens is None: + return + blocked = set(tokens) + + operands = _align_operands(fn, args, kws) if fn is not None else list(args) + + positions = rule.get("tensor_args") + if positions is None: + positions = range(len(operands)) + pointer_positions = set(rule.get("pointer_args") or ()) + + # Operator symbol (``>>`` for a dunder) or the concise rule/function name. + label = op_label or rule_id + arch_name = "Ascend 950PR&950DT" if bucket == "a5" else "Atlas A2 products/Atlas A3 products" + # The interception decision uses the blacklist above; the message tells + # the user which dtypes ARE supported, so it is built from the complement + # of that blacklist derived on the fly. + supported_list = _format_supported_list(_supported_tokens(rule, bucket)) + + for index in positions: + if index >= len(operands): + continue + name = _extract_scalar_name(operands[index], read_pointer=index in pointer_positions) + if name is None: + continue + token = _canonical_token(name) + if token in blocked: + raise UnsupportedDtypeError( + f"operator '{label}' does not support dtype '{_display_dtype(name)}'" + f" (operand #{index + 1}); supported dtypes on {arch_name}: {supported_list}." + f" Please cast the operand to a supported dtype via tl.cast before calling '{label}'.") + + +# --------------------------------------------------------------------------- +# Builtin-dunder layer +# --------------------------------------------------------------------------- +# +# A rule opts into this layer with a ``"dunder"`` list (the tensor dunder +# names, reflected variants included) and a ``"dunder_label"`` (the operator +# shown in error messages), e.g.:: +# +# "rshift": { +# "dunder": ["__rshift__", "__rrshift__"], "dunder_label": ">>", +# ... +# } +# +# Wrapping the builtin dunder itself intercepts only the user operator; +# internal lowering that calls a ``TritonSemantic`` leaf directly never +# touches it. +def _configured_tensor_dunders(): + mapping = {} + for rule_id, rule in OP_DTYPE_RULES.items(): + label = rule.get("dunder_label", rule_id) + for dunder_name in rule.get("dunder", ()): + # A dunder belongs to exactly one operator; guard against + # accidental double registration in the configuration. + assert dunder_name not in mapping, f"dunder {dunder_name} registered twice" + mapping[dunder_name] = (rule_id, label) + return mapping + + +def _install_builtin_dunder_patches(): + from triton.language.core import TRITON_BUILTIN, tensor + + if getattr(tensor, "_ascend_dtype_dunder_patch", False): + return + + for dunder_name, (rule_id, label) in _configured_tensor_dunders().items(): + original = getattr(tensor, dunder_name, None) + if original is None or getattr(original, "_ascend_guard_wrapped", False): + continue + + def make_wrapper(orig, rid, op_label): + + @wraps(orig) + def wrapper(self, *args, **kwargs): + semantic = kwargs.get("_semantic") + # Only run while tracing (the builtin wrapper is what outside + # users hit; inside a kernel ``_semantic`` is always supplied). + # Skip while tracing a triton-internal composite helper (see + # ``_INTERNAL_COMPOSITE_TRACE``). + if semantic is not None and not _in_internal_composite(): + try: + bucket = _bucket_from_semantic(semantic) + validate(rid, [self, *args], bucket, op_label=op_label) + except UnsupportedDtypeError: + raise + except Exception: + # Never let guard bookkeeping break compilation. + pass + return orig(self, *args, **kwargs) + + wrapper._ascend_guard_wrapped = True + # Keep the builtin marker so the tracer still treats it as such. + setattr(wrapper, TRITON_BUILTIN, True) + return wrapper + + setattr(tensor, dunder_name, make_wrapper(original, rule_id, label)) + + tensor._ascend_dtype_dunder_patch = True + + +# --------------------------------------------------------------------------- +# Entry layer +# --------------------------------------------------------------------------- + + +def _build_short_name_index(): + lang_index = {} + ext_index = {} + for full_name in RULES_BY_NAME: + short = full_name.rsplit(".", 1)[-1] + if _EXTRA_MARKER in full_name: + ext_index.setdefault(short, RULES_BY_NAME[full_name]) + else: + lang_index.setdefault(short, RULES_BY_NAME[full_name]) + return lang_index, ext_index + + +_LANG_SHORT_INDEX, _EXT_SHORT_INDEX = _build_short_name_index() + + +def _unwrap_callable(fn): + seen = set() + for _ in range(8): + if id(fn) in seen: + break + seen.add(id(fn)) + func = getattr(fn, "__func__", None) + if func is not None: + fn = func + continue + # JITFunction / ConstexprFunction wrap the raw Python function. + inner = getattr(fn, "fn", None) + if callable(inner): + fn = inner + continue + wrapped = getattr(fn, "__wrapped__", None) + if callable(wrapped): + fn = wrapped + continue + # ``_tensor_member_fn`` registers a closure wrapper named "wrapper". + if getattr(fn, "__name__", None) == "wrapper" and getattr(fn, "__closure__", None): + candidates = [] + for cell in fn.__closure__: + try: + contents = cell.cell_contents + except ValueError: + continue + if callable(contents) and getattr(contents, "__name__", None) not in (None, "wrapper"): + candidates.append(contents) + if candidates: + fn = candidates[0] + continue + break + return fn + + +def _resolve_entry_rule(fn): + target = _unwrap_callable(fn) + module = getattr(target, "__module__", "") or "" + qualname = getattr(target, "__qualname__", "") or getattr(target, "__name__", "") or "" + if not qualname: + return None + + basename = qualname.rsplit(".", 1)[-1] + + # Exact match, progressively dropping trailing module components: + # triton.language.standard.max -> triton.language.max + # triton.language.extra.cann.extension.vec_ops.insert_slice + # -> triton.language.extra.cann.extension.insert_slice + parts = module.split(".") + for cut in range(len(parts), 0, -1): + candidate = ".".join(parts[:cut]) + "." + basename + rule_id = RULES_BY_NAME.get(candidate) + if rule_id is not None: + return rule_id + + # Short-name fallback, constrained to the right operator family so that + # e.g. the extension cube ``dot`` can never shadow the regular ``tl.dot``. + if module.startswith(_LANG_MODULE_PREFIX): + if _EXTRA_MARKER in module: + return _EXT_SHORT_INDEX.get(basename) + return _LANG_SHORT_INDEX.get(basename) + return None + + +def _install_entry_patch(): + from triton.compiler.code_generator import CodeGenerator, _is_triton_value + + if getattr(CodeGenerator, "_ascend_dtype_entry_patch", False): + return + + # These bound-call wrappers live at module scope and have moved between + # modules across Triton versions (BoundConstexprFunction is re-exported + # from triton.runtime.jit). Resolve them opportunistically; a missing + # wrapper must not abort the whole entry layer and silently disable dtype + # interception for every builtin. + import triton.compiler.code_generator as _cg + + bound_method_types = [] + for symbol in ("BoundJITMethod", "BoundConstexprFunction"): + obj = getattr(_cg, symbol, None) + if obj is None: + try: + import triton.runtime.jit as _jit + + obj = getattr(_jit, symbol, None) + except Exception: + obj = None + if obj is not None and obj not in bound_method_types: + bound_method_types.append(obj) + bound_method_types = tuple(bound_method_types) + + original_call_function = CodeGenerator.call_Function + + def call_function(self, node, fn, args, kws): + # Skip while tracing a triton-internal composite helper (see + # ``_INTERNAL_COMPOSITE_TRACE``): its inner ops are library internals, + # not user-facing calls. + if not _in_internal_composite(): + try: + check_fn = fn + check_args = args + # Member-style calls (``x.sum()``, ``x.exp()``) bind the tensor as + # the first operand outside of ``args``; restore it logically for + # validation without mutating the arguments handed to the original + # implementation (which performs the same insertion itself). + if bound_method_types and isinstance(fn, bound_method_types): + check_fn = fn.__func__ + check_args = [fn.__self__, *args] + else: + self_obj = getattr(fn, "__self__", None) + if self_obj is not None and _is_triton_value(self_obj): + check_fn = getattr(fn, "__func__", fn) + check_args = [self_obj, *args] + + rule_id = _resolve_entry_rule(check_fn) + if rule_id is not None: + bucket = arch_bucket(getattr(getattr(self.builder, "options", None), "arch", None)) + validate(rule_id, list(check_args), bucket, fn=check_fn, kws=kws) + except UnsupportedDtypeError as exc: + # Re-raise the actual rejection attached to the user source node, + # mirroring how ``call_Function`` wraps builtin failures so the + # message points at the offending call rather than the compiler. + from triton import knobs + + if knobs.compilation.front_end_debugging: + raise + from triton.compiler.errors import CompilationError + + raise CompilationError(self.jit_fn.src, node, str(exc)) from None + except Exception: + # Validation must never break compilation; only explicit dtype + # rejections are allowed to propagate. + pass + return original_call_function(self, node, fn, args, kws) + + CodeGenerator.call_Function = call_function + CodeGenerator._ascend_dtype_entry_patch = True + + original_call_jit_function = CodeGenerator.call_JitFunction + + def call_jit_function(self, fn, args, kwargs, caller_context=None): + # Nested @jit helpers that ship inside triton itself (the Philox + # implementation behind tl.rand/tl.randint lives in + # triton/language/random.py) are library internals. Raise the internal + # flag for the duration of their trace so the uint32/uint64 arithmetic + # they are built from is not rejected as a standalone user op. The + # user-facing call itself (``tl.rand(...)`` in user code) is validated + # by the entry layer before this flag is raised. + if _is_triton_internal_fn(fn): + token = _INTERNAL_COMPOSITE_TRACE.set(_INTERNAL_COMPOSITE_TRACE.get() + 1) + try: + return original_call_jit_function(self, fn, args, kwargs, caller_context) + finally: + _INTERNAL_COMPOSITE_TRACE.reset(token) + return original_call_jit_function(self, fn, args, kwargs, caller_context) + + CodeGenerator.call_JitFunction = call_jit_function + + +def install_dtype_guard(): + """Install all interception layers. Idempotent. + + This is the single public entry point the Ascend backend calls while + patching ``CodeGenerator`` (see ``third_party/ascend/backend/__init__.py``). + It is a real, importable symbol (not a lazy alias) so it can be resolved + statically and located via ``from ... import install_dtype_guard``. + """ + _install_builtin_dunder_patches() + _install_entry_patch() diff --git a/third_party/ascend/backend/lib/libdevice.10.bc b/third_party/ascend/backend/lib/libdevice.10.bc new file mode 100644 index 0000000000..aa92cc6564 Binary files /dev/null and b/third_party/ascend/backend/lib/libdevice.10.bc differ diff --git a/third_party/ascend/backend/name.conf b/third_party/ascend/backend/name.conf new file mode 100644 index 0000000000..037e3b658a --- /dev/null +++ b/third_party/ascend/backend/name.conf @@ -0,0 +1 @@ +ascend diff --git a/third_party/ascend/backend/npu_utils.cpp b/third_party/ascend/backend/npu_utils.cpp new file mode 100644 index 0000000000..7e160ab197 --- /dev/null +++ b/third_party/ascend/backend/npu_utils.cpp @@ -0,0 +1,497 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#define PY_SSIZE_T_CLEAN +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef TRITON_CANN_910 +#include +#else +#include "runtime/runtime/rt.h" +#endif + +#ifdef USE_TORCH_NPU +#include +#include +#include +#include +#include +#endif + +// Compatibility shim for CANN runtime API transition (rt -> aclrt in 9.1.0). +#ifdef TRITON_CANN_910 +using cann_error = aclError; +using cann_stream = aclrtStream; +static constexpr int CANN_MEMCPY_HOST_TO_DEVICE = ACL_MEMCPY_HOST_TO_DEVICE; +static constexpr int CANN_MEMCPY_DEVICE_TO_HOST = ACL_MEMCPY_DEVICE_TO_HOST; +static constexpr cann_error CANN_SUCCESS = ACL_SUCCESS; + +static inline cann_error cann_set_device(int32_t device) { + return aclrtSetDevice(device); +} +static inline cann_error cann_create_stream(cann_stream *stream) { + return aclrtCreateStream(stream); +} +static inline cann_error cann_destroy_stream(cann_stream stream) { + return aclrtDestroyStream(stream); +} +static inline cann_error cann_malloc_host(void **ptr, size_t size) { + return aclrtMallocHost(ptr, size); +} +static inline cann_error cann_free_host(void *ptr) { + return aclrtFreeHost(ptr); +} +static inline cann_error cann_malloc(void **ptr, size_t size) { + aclrtMemMallocPolicy policy = static_cast( + ACL_MEM_MALLOC_HUGE_FIRST | ACL_MEM_TYPE_HIGH_BAND_WIDTH); + return aclrtMalloc(ptr, size, policy); +} +static inline cann_error cann_free(void *ptr) { return aclrtFree(ptr); } +static inline cann_error cann_memcpy(void *dst, size_t destMax, const void *src, + size_t count, int kind) { + return aclrtMemcpy(dst, destMax, src, count, + static_cast(kind)); +} +static inline const char *cann_get_soc_name() { return aclrtGetSocName(); } +static inline std::tuple +cann_register_kernel(const char *name, const void *data, size_t data_size, + const char *kernel_mode_str) { + uint32_t magic; + const std::string kernel_mode{kernel_mode_str}; + if (kernel_mode == "aiv") + magic = ACL_RT_BINARY_MAGIC_ELF_VECTOR_CORE; + else + magic = ACL_RT_BINARY_MAGIC_ELF_AICORE; + + aclrtBinaryLoadOption optArr[] = { + {.type = ACL_RT_BINARY_LOAD_OPT_LAZY_LOAD, .value = {.isLazyLoad = 0}}, + {.type = ACL_RT_BINARY_LOAD_OPT_MAGIC, .value = {.magic = magic}}}; + aclrtBinaryLoadOptions loadOptions = {.options = optArr, .numOpt = 2}; + aclrtBinHandle binHandle = nullptr; + aclError aclRet = + aclrtBinaryLoadFromData(data, data_size, &loadOptions, &binHandle); + if (aclRet != ACL_SUCCESS) { + printf("aclrtBinaryLoadFromData failed, 0x%x\n", aclRet); + return {nullptr, nullptr}; + } + aclrtFuncHandle funcHandle = nullptr; + aclRet = aclrtBinaryGetFunction(binHandle, name, &funcHandle); + if (aclRet != ACL_SUCCESS) { + printf("aclrtBinaryGetFunction failed(name = %s), 0x%x\n", name, aclRet); + return {nullptr, nullptr}; + } + return std::make_tuple(binHandle, funcHandle); +} +#else +using cann_error = rtError_t; +using cann_stream = rtStream_t; +static constexpr int CANN_MEMCPY_HOST_TO_DEVICE = RT_MEMCPY_HOST_TO_DEVICE; +static constexpr int CANN_MEMCPY_DEVICE_TO_HOST = RT_MEMCPY_DEVICE_TO_HOST; +static constexpr cann_error CANN_SUCCESS = RT_ERROR_NONE; + +static inline cann_error cann_set_device(int32_t device) { + return rtSetDevice(device); +} +static inline cann_error cann_create_stream(cann_stream *stream) { + return rtStreamCreate(stream, 0); +} +static inline cann_error cann_destroy_stream(cann_stream stream) { + return rtStreamDestroy(stream); +} +static inline cann_error cann_malloc_host(void **ptr, size_t size) { + return rtMallocHost(ptr, size, RT_MEMORY_HOST); +} +static inline cann_error cann_free_host(void *ptr) { return rtFreeHost(ptr); } +static inline cann_error cann_malloc(void **ptr, size_t size) { + return rtMalloc(ptr, size, RT_MEMORY_HBM, 0); +} +static inline cann_error cann_free(void *ptr) { return rtFree(ptr); } +static inline cann_error cann_memcpy(void *dst, size_t destMax, const void *src, + size_t count, int kind) { + return rtMemcpy(dst, destMax, src, count, static_cast(kind)); +} +static inline const char *cann_get_soc_name() { + static thread_local char name[64] = {}; + if (rtGetSocVersion(name, sizeof(name)) != RT_ERROR_NONE) + return nullptr; + return name; +} +// Use map to differentiate same name functions from different binary +static std::unordered_map registered_names; +static std::unordered_map> func_stubs; +static inline std::tuple +cann_register_kernel(const char *name, const void *data, size_t data_size, + const char *kernel_mode_str) { + rtError_t rtRet; + + rtDevBinary_t devbin; + devbin.data = data; + devbin.length = data_size; + const std::string kernel_mode{kernel_mode_str}; + if (kernel_mode == "aiv") + devbin.magic = RT_DEV_BINARY_MAGIC_ELF_AIVEC; + else + devbin.magic = RT_DEV_BINARY_MAGIC_ELF; + devbin.version = 0; + + void *devbinHandle = nullptr; + rtRet = rtDevBinaryRegister(&devbin, &devbinHandle); + if (rtRet != RT_ERROR_NONE) { + printf("rtDevBinaryRegister failed, 0x%x\n", rtRet); + return {nullptr, nullptr}; + } + + std::string stubName = name; + stubName += "_" + std::to_string(registered_names[name]); + registered_names[name]++; + auto registered = func_stubs.emplace(stubName, std::make_unique(0)); + void *func_stub_handle = registered.first->second.get(); + rtRet = rtFunctionRegister(devbinHandle, func_stub_handle, stubName.c_str(), + (void *)name, 0); + if (rtRet != RT_ERROR_NONE) { + printf("rtFunctionRegister failed(stubName = %s), 0x%x\n", stubName.c_str(), + rtRet); + return {nullptr, nullptr}; + } + + return std::make_tuple(devbinHandle, func_stub_handle); +} +#endif + +static std::tuple registerKernel(const char *name, + const void *data, + size_t data_size, int device, + const char *kernel_mode_str) { + cann_error rtRet = cann_set_device(device); + if (rtRet != CANN_SUCCESS) { + printf("cann_set_device failed, 0x%x\n", rtRet); + return {nullptr, nullptr}; + } + return cann_register_kernel(name, data, data_size, kernel_mode_str); +} + +static PyObject *loadKernelBinary(PyObject *self, PyObject *args) { + const char *name; // kernel name + const char *data; // binary pointer + Py_ssize_t data_size; // binary size + int shared; // shared_memory(meaningless now) + int device; // device ID + const char *kernel_mode; // kernel mode + int32_t n_max_threads = 0; + + if (!PyArg_ParseTuple(args, "ss#iis", &name, &data, &data_size, &shared, + &device, &kernel_mode)) { + return nullptr; + } + auto [module_handle, func_handle] = + registerKernel(name, data, data_size, device, kernel_mode); + uint64_t mod = reinterpret_cast(module_handle); + uint64_t func = reinterpret_cast(func_handle); + if (PyErr_Occurred()) { + return nullptr; + } + + return Py_BuildValue("(KKiii)", mod, func, 0, 0, n_max_threads); +} + +static PyObject *getArch(PyObject *self, PyObject *args) { + const char *socName = cann_get_soc_name(); + + if (socName == nullptr) { + printf("cann_get_soc_name failed."); + return nullptr; + } + if (PyErr_Occurred()) { + return nullptr; + } + return Py_BuildValue("s", socName); +} + +static PyObject *createStream(PyObject *self, PyObject *args) { + cann_stream stream; + cann_error rtRet = cann_create_stream(&stream); + if (rtRet != CANN_SUCCESS) { + printf("cann_create_stream failed, 0x%x", rtRet); + return nullptr; + } + if (PyErr_Occurred()) { + return nullptr; + } + uint64_t stream_uint64 = reinterpret_cast(stream); + PyObject *result = Py_BuildValue("K", stream_uint64); + + if (result == nullptr) { + cann_destroy_stream(stream); + } + + return result; +} + +/** + * Read binary data from a file into a vector. + * + * @param filename Path to the binary file + * @return Vector of floats read from the file + * @throws std::runtime_error if file cannot be opened or read + */ +std::vector readDataFromBinaryFile(const std::string &filename) { + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + + file.seekg(0, std::ios::end); + const size_t fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + // Read the data into a vector + std::vector data(fileSize); + file.read(data.data(), fileSize); + + // Check if the read was successful + if (!file) { + throw std::runtime_error("Failed to read entire file"); + } + + return data; +} + +static PyObject *readDataFromBinaryFileWrapper(PyObject *self, PyObject *args) { + const char *filename; + uint64_t arr_ptr; + if (!PyArg_ParseTuple(args, "sK", &filename, &arr_ptr)) { + return nullptr; + } + + try { + std::vector data = readDataFromBinaryFile(filename); + char *arr = reinterpret_cast(arr_ptr); + std::copy(data.begin(), data.end(), arr); + return Py_None; + } catch (const std::exception &e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return nullptr; + } +} + +void writeDataToBinaryFile(const std::string &filename, const char *data, + size_t num_bytes) { + std::ofstream file(filename, std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + file.write(data, num_bytes); + if (!file) { + throw std::runtime_error("Failed to write to file"); + } +} + +static PyObject *writeDataToBinaryFileWrapper(PyObject *self, PyObject *args) { + const char *filename; + uint64_t arr_ptr; + size_t num_bytes; + + if (!PyArg_ParseTuple(args, "sKn", &filename, &arr_ptr, &num_bytes)) { + return nullptr; + } + + try { + const char *data = reinterpret_cast(arr_ptr); + writeDataToBinaryFile(filename, data, num_bytes); + return Py_None; + } catch (const std::exception &e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return nullptr; + } +} + +static PyObject *allocateHostMemory(PyObject *self, PyObject *args) { + uint64_t num_bytes; + if (!PyArg_ParseTuple(args, "K", &num_bytes)) { + return nullptr; + } + + void *host_ptr = nullptr; + cann_error error = cann_malloc_host(&host_ptr, num_bytes); + if (error != CANN_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, + "cann_malloc_host failed with error code: 0x%x", error); + return nullptr; + } + + PyObject *result = Py_BuildValue("K", (uint64_t)host_ptr); + if (result == nullptr) { + cann_free_host(host_ptr); + } + return result; +} + +static PyObject *allocateDeviceMemory(PyObject *self, PyObject *args) { + uint64_t num_bytes; + if (!PyArg_ParseTuple(args, "K", &num_bytes)) { + return nullptr; + } + + void *device_ptr = nullptr; + cann_error error = cann_malloc(&device_ptr, num_bytes); + if (error != CANN_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "cann_malloc failed with error code: 0x%x", + error); + return nullptr; + } + + PyObject *result = Py_BuildValue("K", (uint64_t)device_ptr); + if (result == nullptr) { + cann_free(device_ptr); + } + return result; +} + +static PyObject *copyMemory(PyObject *self, PyObject *args) { + uint64_t dst_ptr; + uint64_t src_ptr; + size_t count; + const char *direction_str; + int copy_direction; + + if (!PyArg_ParseTuple(args, "KKns", &dst_ptr, &src_ptr, &count, + &direction_str)) { + return nullptr; + } + if (strcmp(direction_str, "H2D") == 0) { + copy_direction = CANN_MEMCPY_HOST_TO_DEVICE; + } else if (strcmp(direction_str, "D2H") == 0) { + copy_direction = CANN_MEMCPY_DEVICE_TO_HOST; + } else { + PyErr_SetString(PyExc_ValueError, + "Invalid copy direction. Must be 'H2D' or 'D2H'."); + return nullptr; + } + + void *dst = (void *)dst_ptr; + void *src = (void *)src_ptr; + cann_error error = cann_memcpy(dst, count, src, count, copy_direction); + if (error != CANN_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "cann_memcpy failed with error code: 0x%x", + error); + return nullptr; + } + Py_INCREF(Py_None); + return Py_None; +} + +#ifdef USE_TORCH_NPU +struct RetainedTensorHandle { + RetainedTensorHandle(at::Tensor tensor, std::string kind, uint64_t size) + : tensor(std::move(tensor)), kind(std::move(kind)), size(size), + data(const_cast(this->tensor.storage().data())) {} + + at::Tensor tensor; + std::string kind; + uint64_t size; + void *data; +}; + +static void *retainTensor(at::Tensor tensor, void **handle, const char *kind, + uint64_t size) { + if (handle == nullptr) { + return nullptr; + } + auto *retained = new RetainedTensorHandle(std::move(tensor), kind, size); + *handle = retained; + return retained->data; +} + +extern "C" void *triton_allocate_workspace(uint64_t size, void **handle) { + if (handle == nullptr) { + return nullptr; + } + *handle = nullptr; + auto tensor = at::empty( + size, at::TensorOptions().device(at::kPrivateUse1).dtype(at::kByte)); + return retainTensor(std::move(tensor), handle, "workspace", size); +} + +extern "C" void *triton_allocate_sync_block_lock(uint64_t size, void *stream, + void **handle) { + if (handle == nullptr) { + return nullptr; + } + *handle = nullptr; + auto tensor = at_npu::native::allocate_workspace( + size, reinterpret_cast(stream)); + return retainTensor(std::move(tensor), handle, "sync_block_lock", size); +} + +extern "C" void triton_release_retained_tensor(void *handle) { + auto *retained = static_cast(handle); + delete retained; +} + +extern "C" void triton_async_launch(void *func_obj, const char *name) { + auto &func = *static_cast *>(func_obj); + at_npu::native::OpCommand cmd; + cmd.Name(name).SetCustomHandler(func).Run(); +} +#endif + +static PyMethodDef NpuUtilsMethods[] = { + {"load_kernel_binary", loadKernelBinary, METH_VARARGS, + "Load NPU kernel binary into NPU driver"}, + {"get_arch", getArch, METH_VARARGS, "Get soc version of NPU"}, + {"create_stream", createStream, METH_VARARGS, "Create a stream"}, + {"read_data_from_file", readDataFromBinaryFileWrapper, METH_VARARGS, + "Read binary file into the array already allocated"}, + {"write_data_to_file", writeDataToBinaryFileWrapper, METH_VARARGS, + "Write an array to a binary file"}, + {"allocate_device_memory", allocateDeviceMemory, METH_VARARGS, + "Allocate device memory"}, + {"allocate_host_memory", allocateHostMemory, METH_VARARGS, + "Allocate host memory"}, + {"copy_memory", copyMemory, METH_VARARGS, + "Copy data between host and device"}, + {nullptr, nullptr, 0, nullptr}}; + +static PyModuleDef ModuleDef = { + PyModuleDef_HEAD_INIT, "npu_utils", + "Utilities for fetching NPU device info and preparing kernel binary", -1, + NpuUtilsMethods}; + +PyMODINIT_FUNC PyInit_npu_utils(void) { + PyObject *m = PyModule_Create(&ModuleDef); + if (m == nullptr) { + return nullptr; + } + + PyModule_AddFunctions(m, NpuUtilsMethods); + return m; +} diff --git a/third_party/ascend/backend/op_dtype_config.py b/third_party/ascend/backend/op_dtype_config.py new file mode 100644 index 0000000000..ae31807713 --- /dev/null +++ b/third_party/ascend/backend/op_dtype_config.py @@ -0,0 +1,2024 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Machine-readable operator dtype configuration for the Ascend backend. +# +# Source of truth: docs/zh/python-api/_ascend_constraints.py. Each documented +# operator carries an ASCII ``dtype_support`` table (one column per scalar +# dtype, one row per Ascend platform); the rules below transcribe that table +# for every operator whose support differs across platforms. Fully supported +# operators have no rule. +# +# --------------------------------------------------------------------------- +# Rule schema +# --------------------------------------------------------------------------- +# "": { +# # Fully qualified names matched by the *entry layer* (direct +# # ``tl.(...)`` / @jit composite calls). Only direct +# # ``triton.language`` operators are guarded. +# "names": ["triton.language.permute"], +# +# # Tensor dunder builtins matched by the *builtin-dunder layer* +# # (``a + b``, ``a >> b``, ``a & b`` ...); reflected dunders included. +# "dunder": ["__add__", "__radd__"], +# "dunder_label": "+", +# +# # Positions (0-based) of tensor-like operands to inspect. When +# # omitted, every positional operand is inspected (non-tensors and +# # pointers are skipped automatically). +# "tensor_args": [0, 1], +# +# # Positions that are pointers: the pointee element dtype is read. +# "pointer_args": [0], +# +# # The dtype blacklist per architecture bucket: +# # "a2" -> Ascend A2/A3 ; "a5" -> Ascend 950 +# # * "unsupported": blacklist -- the interception decision uses this: +# # a dtype listed here is rejected at the front end. +# # The accepted ("supported") dtypes are normally NOT stored; they are +# # derived at runtime as the complement of this list over +# # ALL_SCALAR_DTYPES and used in the error message. A rule whose +# # advertised dtypes are intentionally narrower than that complement +# # (currently only "dot_scaled") may spell out an explicit "supported" +# # list, which then overrides the derivation. A missing bucket means +# # "no dtype restriction on that architecture" (every dtype accepted). +# "unsupported": {"a2": [...], "a5": [...]}, +# } +# +# Dtype tokens (matched against ``dtype.name``): +# * regular names: int8/int16/int32/int64/uint8/uint16/uint32/uint64/ +# fp16/bf16/fp32/fp64/bool +# * "fp8e4" : the E4M3 column, matching fp8e4nv/fp8e4b8/fp8e4b15 at runtime +# * "fp8e5" : the E5M2 column, matching fp8e5/fp8e5b16 at runtime +# (``bool`` is represented internally as ``int1``; the matching layer +# normalizes both. The concrete fp8 variant names live in FP8*_DTYPES.) + +FP8E4_DTYPES = ["fp8e4nv", "fp8e4b8", "fp8e4b15"] +FP8E5_DTYPES = ["fp8e5", "fp8e5b16"] + +# The 15 scalar-dtype columns of the documented ``dtype_support`` tables, in +# table order. Each rule's "unsupported" list is a subset of this universe; the +# accepted dtypes for a rule are the complement of that list. +ALL_SCALAR_DTYPES = [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + "fp16", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + "bool", +] + +OP_DTYPE_RULES = { + # "abs": { + # "names": ["triton.language.abs"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "add": { + # "names": ["triton.language.add"], + # "dunder": ["__add__", "__radd__"], + # "dunder_label": "+", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "advance": { + # "names": ["triton.language.advance"], + # "pointer_args": [0], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5", "bool"], + # "a5": ["fp64", "bool"], + # }, + # }, + # "and": { + # "dunder": ["__and__", "__rand__"], + # "dunder_label": "&", + # "unsupported": { + # "a2": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": ["fp16", "fp32", "fp64", "bf16", "fp8e4", "fp8e5"], + # }, + # }, + # "argmax": { + # "names": ["triton.language.argmax"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + # "argmin": { + # "names": ["triton.language.argmin"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + # "associative_scan": { + # "names": ["triton.language.associative_scan"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "bf16", "fp8e4", "fp8e5"], + # }, + # }, + "assume": { + "names": ["triton.language.assume"], + "unsupported": { + "a2": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + "fp16", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + "fp16", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + ], + }, + }, + "condition": { + "names": ["triton.language.condition"], + "unsupported": {"a2": ["uint64"], "a5": ["uint64"]}, + "supported": { + "a2": ["uint8", "int8", "int16", "int32", "int64", "fp16", "fp32", "bf16", "bool"], "a5": + ["uint8", "int8", "int16", "int32", "int64", "fp16", "fp32", "bf16", "bool"] + }, + }, + # "atomic_add": { + # "names": ["triton.language.atomic_add"], + # "tensor_args": [0, 1], + # "pointer_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "uint64", + # "fp64", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "fp64", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # }, + # }, + # "atomic_and": { + # "names": ["triton.language.atomic_and"], + # "tensor_args": [0, 1], + # "pointer_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # }, + # }, + # "atomic_cas": { + # "names": ["triton.language.atomic_cas"], + # "tensor_args": [0, 1, 2], + # "pointer_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "uint32", + # "uint64", + # "fp64", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": ["uint8", "int8", "fp64", "fp8e4", "fp8e5", "bool"], + # }, + # }, + # "atomic_max": { + # "names": ["triton.language.atomic_max"], + # "tensor_args": [0, 1], + # "pointer_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "uint64", + # "fp16", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "fp16", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # }, + # }, + # "atomic_min": { + # "names": ["triton.language.atomic_min"], + # "tensor_args": [0, 1], + # "pointer_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "uint64", + # "fp16", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "fp16", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # }, + # }, + # "atomic_or": { + # "names": ["triton.language.atomic_or"], + # "tensor_args": [0, 1], + # "pointer_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # }, + # }, + # "atomic_xchg": { + # "names": ["triton.language.atomic_xchg"], + # "tensor_args": [0, 1], + # "pointer_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "uint64", + # "fp16", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "fp16", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # }, + # }, + # "atomic_xor": { + # "names": ["triton.language.atomic_xor"], + # "tensor_args": [0, 1], + # "pointer_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # }, + # }, + "bitonic_merge": { + "names": ["triton.language.bitonic_merge"], + "unsupported": { + "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + "a5": ["uint16", "uint32", "uint64", "fp64"], + }, + }, + # "broadcast": { + # "names": ["triton.language.broadcast"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "broadcast_to": { + # "names": ["triton.language.broadcast_to"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "cast": { + # "names": ["triton.language.cast"], + # "tensor_args": [1], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + "cat": { + "names": ["triton.language.cat"], + "unsupported": { + "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + "a5": ["fp64"], + }, + }, + "cdiv": { + "names": ["triton.language.cdiv"], + "unsupported": { + "a2": [ + "uint16", + "uint32", + "uint64", + "fp16", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + "bool", + ], + "a5": ["fp16", "fp32", "fp64", "bf16", "fp8e4", "fp8e5", "bool"], + }, + }, + "ceil": { + "names": ["triton.language.ceil"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": ["fp32"], + "a5": [ + "fp32", + ], + }, + }, + "clamp": { + "names": ["triton.language.clamp"], + "unsupported": { + "a2": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "uint64", + "int64", + "fp64", + "fp8e4", + "fp8e5", + "bool", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "uint64", + "int64", + "fp64", + "bool", + ], + }, + "supported": { + "a2": [ + "fp16", + "fp32", + "bf16", + ], + "a5": [ + "fp16", + "fp32", + "bf16", + "fp8e4", + "fp8e5", + ], + }, + }, + "cos": { + "names": ["triton.language.cos"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "cumprod": { + "names": ["triton.language.cumprod"], + "unsupported": { + "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + "a5": ["fp64", "fp8e4", "fp8e5"], + }, + }, + # "cumsum": { + # "names": ["triton.language.cumsum"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "device_assert": { + # "names": ["triton.language.device_assert"], + # "tensor_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # }, + # }, + # "device_print": { + # "names": ["triton.language.device_print"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "div": { + # "dunder": ["__truediv__", "__rtruediv__"], + # "dunder_label": "/", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "dot": { + # "names": ["triton.language.dot"], + # "tensor_args": [0, 1], + # "unsupported": { + # "a2": [ + # "uint8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp64", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp64", + # "bool", + # ], + # }, + # }, + # "dot_scaled": { + # "names": ["triton.language.dot_scaled"], + # "tensor_args": [0, 3], + # "unsupported": { + # "a2": [ + # "fp8e4", + # "fp8e5", + # ], + # "a5": [], + # }, + # # Exception to the complement rule: the advertised input dtypes for + # # dot_scaled are narrower than the blacklist alone, so the message + # # list is kept explicit instead of being derived. + # "supported": { + # "a2": ["fp16", "bf16"], + # # uint = e2m1 / fp8 as uint8 + # "a5": ["fp8e4", "fp8e5", "fp16", "bf16", "uint8"], + # }, + # }, + # "eq": { + # "dunder": ["__eq__", "__req__"], + # "dunder_label": "==", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + "erf": { + "names": ["triton.language.erf"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "exp": { + "names": ["triton.language.exp"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp16", + "fp32", + ], + "a5": [ + "fp16", + "fp32", + ], + }, + }, + "exp2": { + "names": ["triton.language.exp2"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + # "expand_dims": { + # "names": ["triton.language.expand_dims"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "fdiv": { + # "names": ["triton.language.fdiv"], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp64", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp64", + # "bool", + # ], + # }, + # }, + "flip": { + "names": ["triton.language.flip"], + "unsupported": { + "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + "a5": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + }, + }, + "floor": { + "names": ["triton.language.floor"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + # "floordiv": { + # "dunder": ["__floordiv__", "__rfloordiv__"], + # "dunder_label": "//", + # "unsupported": { + # "a2": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": ["fp16", "fp32", "fp64", "bf16", "fp8e4", "fp8e5", "bool"], + # }, + # }, + "fma": { + "names": ["triton.language.fma"], + "unsupported": { + "a2": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + "fp64", + "fp8e4", + "fp8e5", + "bool", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + "fp64", + "bool", + ], + }, + }, + # "full": { + # "names": ["triton.language.full"], + # "tensor_args": [1, 2], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + # "gather": { + # "names": ["triton.language.gather"], + # "tensor_args": [0], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp64", + # "fp8e4", + # "fp8e5", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp64", + # ], + # }, + # }, + # "ge": { + # "dunder": ["__ge__", "__rge__"], + # "dunder_label": ">=", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "gt": { + # "dunder": ["__gt__", "__rgt__"], + # "dunder_label": ">", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + "histogram": { + "names": ["triton.language.histogram"], + "tensor_args": [0], + "unsupported": { + "a2": [ + "uint16", + "fp16", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + "bool", + ], + "a5": ["fp16", "fp32", "fp64", "bf16", "fp8e4", "fp8e5", "bool"], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "int64", + "uint64", + ], + }, + }, + # "inline_asm_elementwise": { + # "names": ["triton.language.inline_asm_elementwise"], + # "tensor_args": [3], + # "unsupported": { + # "a2": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": ["uint64", "fp16", "fp64", "bf16", "fp8e4", "fp8e5", "bool"], + # }, + # }, + # "interleave": { + # "names": ["triton.language.interleave"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "invert": { + # "dunder": ["__invert__"], + # "dunder_label": "~", + # "unsupported": { + # "a2": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # }, + # }, + # "join": { + # "names": ["triton.language.join"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "le": { + # "dunder": ["__le__", "__rle__"], + # "dunder_label": "<=", + # "unsupported": { + # "a2": ["uint8", "uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "load": { + # "names": ["triton.language.load"], + # "tensor_args": [0, 2], + # "pointer_args": [0], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + "load_tensor_descriptor": { + "names": ["triton.language.load_tensor_descriptor"], + "unsupported": { + "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5", "bool"], + "a5": ["fp64", "bool"], + }, + }, + "log": { + "names": ["triton.language.log"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "log2": { + "names": ["triton.language.log2"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "logical_and": { + "names": ["triton.language.tensor.logical_and"], + "unsupported": { + "a2": [ + "uint16", + "uint32", + "uint64", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + ], + "a5": ["fp32", "fp64", "bf16", "fp8e4", "fp8e5"], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + "bool", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + "bool", + ], + }, + }, + "logical_or": { + "names": ["triton.language.tensor.logical_or"], + "unsupported": { + "a2": [ + "uint16", + "uint32", + "uint64", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + ], + "a5": ["fp32", "fp64", "bf16", "fp8e4", "fp8e5"], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + "bool", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + "bool", + ], + }, + }, + "lshift": { + "dunder": ["__lshift__", "__rlshift__"], + "dunder_label": "<<", + "unsupported": { + "a2": [ + "fp64", + "fp8e4", + "fp8e5", + ], + "a5": [ + "fp64", + "fp8e4", + "fp8e5", + ], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + "bool", + ], + "a5": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + "bool", + ], + }, + }, + # "lt": { + # "dunder": ["__lt__", "__rlt__"], + # "dunder_label": "<", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "make_block_ptr": { + # "names": ["triton.language.make_block_ptr"], + # "pointer_args": [0], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + "make_tensor_descriptor": { + "names": ["triton.language.make_tensor_descriptor"], + "pointer_args": [0], + "unsupported": { + "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5", "bool"], + "a5": ["fp64", "bool"], + }, + }, + # "max": { + # "names": ["triton.language.max"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + # "max_constancy": { + # "names": ["triton.language.max_constancy"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "max_contiguous": { + # "names": ["triton.language.max_contiguous"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "maximum": { + # "names": ["triton.language.maximum"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["uint16", "uint32", "fp64"], + # }, + # }, + # "min": { + # "names": ["triton.language.min"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + # "minimum": { + # "names": ["triton.language.minimum"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["uint16", "uint32", "fp64"], + # }, + # }, + # "mod": { + # "dunder": ["__mod__", "__rmod__"], + # "dunder_label": "%", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "mul": { + # "names": ["triton.language.mul"], + # "dunder": ["__mul__", "__rmul__"], + # "dunder_label": "*", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "multiple_of": { + # "names": ["triton.language.multiple_of"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "ne": { + # "dunder": ["__ne__", "__rne__"], + # "dunder_label": "!=", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "neg": { + # "dunder": ["__neg__"], + # "dunder_label": "unary -", + # "unsupported": { + # "a2": [ + # "uint8", + # "uint16", + # "uint32", + # "uint64", + # "fp64", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": [ + # "uint8", + # "uint16", + # "uint32", + # "uint64", + # "fp64", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # }, + # }, + # "not": { + # "dunder": ["__not__"], + # "dunder_label": "not", + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # }, + # }, + # "or": { + # "dunder": ["__or__", "__ror__"], + # "dunder_label": "|", + # "unsupported": { + # "a2": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # }, + # }, + # "permute": { + # "names": ["triton.language.permute"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "ravel": { + # "names": ["triton.language.ravel"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "reduce": { + # "names": ["triton.language.reduce"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + "reduce_or": { + "names": ["triton.language.reduce_or"], + "unsupported": { + "a2": [ + "fp16", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + ], + "a5": ["fp16", "fp32", "fp64", "bf16", "fp8e4", "fp8e5"], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "int32", + "uint64", + "int64", + ], + }, + }, + # "reshape": { + # "names": ["triton.language.reshape"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + "rshift": { + "dunder": ["__rshift__", "__rrshift__"], + "dunder_label": ">>", + "unsupported": { + "a2": [ + "uint8", + "fp64", + "fp8e4", + "fp8e5", + ], + "a5": [ + "fp64", + "fp8e4", + "fp8e5", + ], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + "bool", + ], + "a5": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + "bool", + ], + }, + }, + "rsqrt": { + "names": ["triton.language.rsqrt"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "sigmoid": { + "names": ["triton.language.sigmoid"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "sin": { + "names": ["triton.language.sin"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "softmax": { + "names": ["triton.language.softmax"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "sort": { + "names": ["triton.language.sort"], + "unsupported": { + "a2": [ + "uint16", + "uint32", + "int32", + "uint64", + "int64", + "fp64", + "fp8e4", + "fp8e5", + "bool", + ], + "a5": ["uint16", "uint32", "uint64", "fp64"], + }, + "supported": { + "a2": [ + "int8", + "int16", + "fp16", + "fp32", + "bf16", + ], + "a5": [ + "int8", + "int16", + "int32", + "int64", + "fp16", + "fp32", + "bf16", + "fp8e4", + "fp8e5", + "bool", + ], + }, + }, + # "split": { + # "names": ["triton.language.split"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + "sqrt": { + "names": ["triton.language.sqrt"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "sqrt_rn": { + "names": ["triton.language.sqrt_rn"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + # "static_assert": { + # "names": ["triton.language.static_assert"], + # "unsupported": { + # "a2": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": [ + # "uint8", + # "int8", + # "uint16", + # "int16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # }, + # }, + # "static_print": { + # "names": ["triton.language.static_print"], + # "unsupported": { + # "a2": ["uint8", "uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["uint8", "uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # }, + # }, + # "store": { + # "names": ["triton.language.store"], + # "tensor_args": [0, 1], + # "pointer_args": [0], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + "store_tensor_descriptor": { + "names": ["triton.language.store_tensor_descriptor"], + "unsupported": { + "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5", "bool"], + "a5": ["fp64", "bool"], + }, + }, + # "sub": { + # "names": ["triton.language.sub"], + # "dunder": ["__sub__", "__rsub__"], + # "dunder_label": "-", + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "sum": { + # "names": ["triton.language.sum"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["uint8", "uint16", "uint32", "fp64"], + # }, + # }, + "swizzle2d": { + "names": ["triton.language.swizzle2d"], + "unsupported": { + "a2": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "uint64", + "fp16", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + "bool", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "uint64", + "fp16", + "fp32", + "fp64", + "bf16", + "fp8e4", + "fp8e5", + "bool", + ], + }, + }, + # "topk": { + # "names": ["triton.language.topk"], + # "unsupported": { + # "a2": [ + # "uint8", + # "uint16", + # "uint32", + # "int32", + # "uint64", + # "int64", + # "fp64", + # "fp8e4", + # "fp8e5", + # "bool", + # ], + # "a5": ["uint8", "uint16", "uint32", "uint64", "fp64"], + # }, + # }, + # "trans": { + # "names": ["triton.language.trans"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + "umulhi": { + "names": ["triton.language.umulhi"], + "unsupported": { + "a2": [ + "fp64", + ], + "a5": [ + "fp64", + ], + }, + "supported": { + "a2": [ + "fp32", + ], + "a5": [ + "fp32", + ], + }, + }, + "arange": { + "names": ["triton.language.arange"], + "unsupported": { + "a2": ["bf16", "fp16", "fp32", "fp64", "bool"], + "a5": ["bf16", "fp16", "fp32", "fp64", "bool"], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "int32", + ], + }, + }, + "range": { + "names": ["triton.language.range"], + "unsupported": { + "a2": ["bf16", "fp16", "fp32", "fp64", "bool"], + "a5": ["bf16", "fp16", "fp32", "fp64", "bool"], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + ], + }, + }, + "static_range": { + "names": ["triton.language.static_range"], + "unsupported": { + "a2": ["bf16", "fp16", "fp32", "fp64", "bool"], + "a5": ["bf16", "fp16", "fp32", "fp64", "bool"], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + ], + "a5": [ + "uint8", + "int8", + "uint16", + "int16", + "uint32", + "int32", + "uint64", + "int64", + ], + }, + }, + "rand": { + "names": ["triton.language.rand"], + "unsupported": { + "a2": [ + "bf16", + "fp16", + "fp32", + "fp64", + ], + "a5": [ + "bf16", + "fp16", + "fp32", + "fp64", + ], + }, + "supported": { + "a2": ["uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64", "bool"], + "a5": ["uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64", "bool"], + }, + }, + "randn": { + "names": ["triton.language.randn"], + "unsupported": { + "a2": [ + "bf16", + "fp16", + "fp32", + "fp64", + ], + "a5": [ + "bf16", + "fp16", + "fp32", + "fp64", + ], + }, + "supported": { + "a2": ["uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64", "bool"], + "a5": ["uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64", "bool"], + }, + }, + # "view": { + # "names": ["triton.language.view"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64"], + # }, + # }, + # "where": { + # "names": ["triton.language.where"], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + # "xor": { + # "dunder": ["__xor__", "__rxor__"], + # "dunder_label": "^", + # "unsupported": { + # "a2": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": [ + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # }, + # }, + # "xor_sum": { + # "names": ["triton.language.xor_sum"], + # "unsupported": { + # "a2": [ + # "uint8", + # "uint16", + # "uint32", + # "uint64", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # "a5": [ + # "uint8", + # "uint16", + # "uint32", + # "fp16", + # "fp32", + # "fp64", + # "bf16", + # "fp8e4", + # "fp8e5", + # ], + # }, + # }, + # "zeros": { + # "names": ["triton.language.zeros"], + # "tensor_args": [1], + # "unsupported": { + # "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + # "a5": ["fp64", "fp8e4", "fp8e5"], + # }, + # }, + "zeros_like": { + "names": ["triton.language.zeros_like"], + "unsupported": { + "a2": ["uint16", "uint32", "uint64", "fp64", "fp8e4", "fp8e5"], + "a5": ["fp64", "fp8e4", "fp8e5"], + }, + "supported": { + "a2": [ + "uint8", + "int8", + "int16", + "int32", + "int64", + "fp16", + "fp32", + "bf16", + ], + "a5": [ + "uint8", + "int8", + "int16", + "uint16", + "int32", + "uint32", + "int64", + "uint64", + "fp16", + "fp32", + "bf16", + ], + }, + }, +} + + +def _build_lookup_indexes(): + by_name = {} + for rule_id, rule in OP_DTYPE_RULES.items(): + for name in rule.get("names", []): + by_name[name] = rule_id + return by_name + + +RULES_BY_NAME = _build_lookup_indexes() diff --git a/third_party/ascend/backend/program_grid.py b/third_party/ascend/backend/program_grid.py new file mode 100644 index 0000000000..c640db1491 --- /dev/null +++ b/third_party/ascend/backend/program_grid.py @@ -0,0 +1,225 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +from __future__ import annotations + +import json +from typing import Any, Mapping, Sequence + +PROGRAM_GRID_TRANSFORMS_ATTR = "hacc.program_grid_transforms" +PROGRAM_GRID_TRANSFORMS_VERSION = 2 + +GRAPH_OPTIMIZATION_KNOWN_RULE_MASK = 0xFBFF +INDEPENDENT_AXIS_TENSORIZE_RULE_BIT = 1 << 9 +PERSISTENT_TASK_STRIP_MINING_RULE_BIT = 1 << 11 +DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK = (511 | INDEPENDENT_AXIS_TENSORIZE_RULE_BIT + | PERSISTENT_TASK_STRIP_MINING_RULE_BIT) + +RUNTIME_ORIGINAL_GRID = "runtime_original_grid" +HIDDEN_EXTENT_AXES = (0, 1) +HIDDEN_ARGUMENT_ORDER = ("originalGridX", "originalGridY") +HIDDEN_ARGUMENT_TYPES = ("i32", "i32") + + +class ProgramGridContractError(ValueError): + pass + + +_TOP_LEVEL_KEYS = frozenset(( + "version", + "extent_source", + "hidden_extent_axes", + "hidden_argument_order", + "hidden_argument_types", + "transforms", +)) +_TRANSFORM_KEYS = frozenset(( + "order", + "kind", + "axis", + "factor", + "persistent_coverage", + "grid_stride_abi_verified", +)) +_SUPPORTED_SEQUENCES = frozenset(( + ((1, 16, False, False), ), + ((1, 16, False, False), (0, 4, True, True)), + ((0, 64, True, True), ), + ((0, 2, False, False), ), + ((0, 4, False, False), ), + ((0, 8, False, False), ), + ((0, 16, False, False), ), + ((0, 32, False, False), ), +)) + + +def _integer(value: Any, name: str, *, minimum: int | None = None) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ProgramGridContractError(f"{name} must be an integer") + if minimum is not None and value < minimum: + raise ProgramGridContractError(f"{name} must be >= {minimum}") + return value + + +def _boolean(value: Any, name: str) -> bool: + if not isinstance(value, bool): + raise ProgramGridContractError(f"{name} must be a boolean") + return value + + +def _mapping(value: Any, name: str) -> Mapping[str, Any]: + if not isinstance(value, Mapping): + raise ProgramGridContractError(f"{name} must be a mapping") + return value + + +def _exact_sequence(value: Any, expected: tuple[Any, ...], name: str) -> None: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise ProgramGridContractError(f"{name} must be a sequence") + if tuple(value) != expected: + raise ProgramGridContractError(f"{name} must equal {list(expected)!r}") + + +def normalize_graph_optimization_rule_mask(raw: Any) -> int: + mask = _integer(raw, "rule_mask", minimum=0) + if mask & ~GRAPH_OPTIMIZATION_KNOWN_RULE_MASK: + raise ProgramGridContractError("rule_mask contains unsupported graph-optimization bits") + return mask + + +def normalize_program_grid_transforms(raw: Any) -> dict[str, Any]: + if isinstance(raw, str): + try: + raw = json.loads(raw) + except json.JSONDecodeError as error: + raise ProgramGridContractError("program_grid_transforms must be valid JSON when encoded as text") from error + + contract = _mapping(raw, "program_grid_transforms") + if set(contract) != _TOP_LEVEL_KEYS: + missing = sorted(_TOP_LEVEL_KEYS - set(contract)) + unknown = sorted(set(contract) - _TOP_LEVEL_KEYS) + detail = [] + if missing: + detail.append("missing " + ", ".join(missing)) + if unknown: + detail.append("unknown " + ", ".join(unknown)) + raise ProgramGridContractError("program_grid_transforms has an invalid top-level schema" + + (": " + "; ".join(detail) if detail else "")) + + version = _integer(contract["version"], "version", minimum=1) + if version != PROGRAM_GRID_TRANSFORMS_VERSION: + raise ProgramGridContractError(f"unsupported program_grid_transforms version {version}; " + f"expected {PROGRAM_GRID_TRANSFORMS_VERSION}") + if contract["extent_source"] != RUNTIME_ORIGINAL_GRID: + raise ProgramGridContractError("extent_source must be runtime_original_grid") + _exact_sequence(contract["hidden_extent_axes"], HIDDEN_EXTENT_AXES, "hidden_extent_axes") + _exact_sequence(contract["hidden_argument_order"], HIDDEN_ARGUMENT_ORDER, "hidden_argument_order") + _exact_sequence(contract["hidden_argument_types"], HIDDEN_ARGUMENT_TYPES, "hidden_argument_types") + + transforms_raw = contract["transforms"] + if isinstance(transforms_raw, (str, bytes)) or not isinstance(transforms_raw, Sequence): + raise ProgramGridContractError("transforms must be a sequence") + if len(transforms_raw) not in (1, 2): + raise ProgramGridContractError("transforms must contain one or two entries") + + transforms: list[dict[str, Any]] = [] + sequence: list[tuple[int, int, bool, bool]] = [] + seen_axes: set[int] = set() + persistent_count = 0 + for expected_order, raw_transform in enumerate(transforms_raw): + transform = _mapping(raw_transform, f"transforms[{expected_order}]") + if set(transform) != _TRANSFORM_KEYS: + missing = sorted(_TRANSFORM_KEYS - set(transform)) + unknown = sorted(set(transform) - _TRANSFORM_KEYS) + detail = [] + if missing: + detail.append("missing " + ", ".join(missing)) + if unknown: + detail.append("unknown " + ", ".join(unknown)) + raise ProgramGridContractError(f"transforms[{expected_order}] has an invalid schema" + + (": " + "; ".join(detail) if detail else "")) + order = _integer(transform["order"], f"transforms[{expected_order}].order", minimum=0) + if order != expected_order: + raise ProgramGridContractError("transform order must be contiguous from zero") + if transform["kind"] != "ceil_div": + raise ProgramGridContractError(f"transforms[{expected_order}].kind must be 'ceil_div'") + axis = _integer(transform["axis"], f"transforms[{expected_order}].axis", minimum=0) + if axis not in HIDDEN_EXTENT_AXES: + raise ProgramGridContractError("dynamic program-grid transforms support only axes 0 and 1") + if axis in seen_axes: + raise ProgramGridContractError("dynamic program-grid transforms may not repeat an axis") + seen_axes.add(axis) + factor = _integer(transform["factor"], f"transforms[{expected_order}].factor", minimum=2) + persistent = _boolean(transform["persistent_coverage"], f"transforms[{expected_order}].persistent_coverage") + grid_stride = _boolean(transform["grid_stride_abi_verified"], + f"transforms[{expected_order}].grid_stride_abi_verified") + if persistent != grid_stride: + raise ProgramGridContractError("persistent coverage and grid-stride ABI verification must agree") + persistent_count += int(persistent) + if persistent_count > 1: + raise ProgramGridContractError("at most one transform may be persistent") + sequence.append((axis, factor, persistent, grid_stride)) + transforms.append({ + "order": order, + "kind": "ceil_div", + "axis": axis, + "factor": factor, + "persistent_coverage": persistent, + "grid_stride_abi_verified": grid_stride, + }) + + if tuple(sequence) not in _SUPPORTED_SEQUENCES: + raise ProgramGridContractError("unsupported dynamic IAT/PTSM transform sequence") + return { + "version": PROGRAM_GRID_TRANSFORMS_VERSION, + "extent_source": RUNTIME_ORIGINAL_GRID, + "hidden_extent_axes": list(HIDDEN_EXTENT_AXES), + "hidden_argument_order": list(HIDDEN_ARGUMENT_ORDER), + "hidden_argument_types": list(HIDDEN_ARGUMENT_TYPES), + "transforms": transforms, + } + + +def canonical_program_grid_transforms_json(raw: Any) -> str: + return json.dumps(normalize_program_grid_transforms(raw), sort_keys=True, separators=(",", ":")) + + +def get_persistent_transform(contract: Mapping[str, Any]) -> Mapping[str, Any] | None: + for transform in normalize_program_grid_transforms(contract)["transforms"]: + if transform["persistent_coverage"]: + return transform + return None + + +def apply_program_grid_transforms( + grid: Sequence[int], + contract: Mapping[str, Any], + *, + physical_core_count: int | None = None, +) -> tuple[int, int, int]: + if len(grid) != 3: + raise ProgramGridContractError("grid must contain exactly three dimensions") + launch_grid = [_integer(value, f"grid[{axis}]", minimum=1) for axis, value in enumerate(grid)] + normalized = normalize_program_grid_transforms(contract) + for transform in normalized["transforms"]: + axis = transform["axis"] + launch_grid[axis] = (launch_grid[axis] + transform["factor"] - 1) // transform["factor"] + persistent = get_persistent_transform(normalized) + if persistent is not None: + cores = _integer(physical_core_count, "physical_core_count", minimum=1) + axis = persistent["axis"] + other_programs = 1 + for other_axis, extent in enumerate(launch_grid): + if other_axis != axis: + other_programs *= extent + launch_grid[axis] = min(launch_grid[axis], max(1, cores // max(1, other_programs))) + return tuple(launch_grid) diff --git a/third_party/ascend/backend/runtime/__init__.py b/third_party/ascend/backend/runtime/__init__.py new file mode 100644 index 0000000000..2d52f74b04 --- /dev/null +++ b/third_party/ascend/backend/runtime/__init__.py @@ -0,0 +1,92 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os + +from .autotuner import get_max_configs, max_autotune + + +def _patch_autotune(): + try: + import triton + except ImportError: + return + + from .autotuner import autotune + + triton.autotune = autotune + + +def _patch_config_init(): + """Allow Config to round-trip the ubtune_cfg attribute through the disk cache. + + UB-tuner dynamically injects ubtune_cfg onto Config objects via setattr to + fix UB-overflow compile failures. The disk cache serializes config.__dict__ + to JSON, and on cache hit rebuilds configs with Config(**config_dict). + The upstream Config.__init__ does not accept ubtune_cfg, so the attribute + would be lost (or crash) on a disk-cache round-trip. This patch restores it. + """ + from triton.runtime.autotuner import Config + + _original_config_init = Config.__init__ + _default_num_stages = object() + + def _patched_config_init(self, kwargs, num_warps=4, num_stages=_default_num_stages, num_ctas=1, maxnreg=None, + pre_hook=None, ir_override=None, **extra): + has_mode = kwargs.get("multibuffer_mode") is not None + if num_stages is _default_num_stages: + num_stages = None if has_mode else 3 + if has_mode: + if num_stages is not None: + raise ValueError("num_stages and multibuffer_mode cannot be specified together") + from triton.backends.ascend.utils import _multibuffer_mode_to_tuple + kwargs = dict(kwargs, multibuffer_mode=_multibuffer_mode_to_tuple(kwargs["multibuffer_mode"])) + _original_config_init(self, kwargs, num_warps, num_stages, num_ctas, maxnreg, pre_hook, ir_override) + if "ubtune_cfg" in extra: + setattr(self, "ubtune_cfg", extra["ubtune_cfg"]) + + Config.__init__ = _patched_config_init + + +def _patch_cache_invalidating_env_vars(): + """Include TRITON_ENABLE_UBTUNER in the autotune disk cache key. + + UB-tuner may rescue configs that would otherwise fail to compile, changing + the benchmark outcome. Toggling it must therefore invalidate cached + autotune results; otherwise a cache hit from a run without UB-tuner would + silently skip configs that UB-tuner could have rescued. + """ + import triton.runtime.autotuner as _autotuner_module + + _original_get_env_vars = _autotuner_module.get_cache_invalidating_env_vars + + def _patched_get_env_vars(): + env_vars = _original_get_env_vars() + ubtuner_mode = os.environ.get("TRITON_ENABLE_UBTUNER", "") + if ubtuner_mode: + env_vars["TRITON_ENABLE_UBTUNER"] = ubtuner_mode + return env_vars + + _autotuner_module.get_cache_invalidating_env_vars = _patched_get_env_vars + + +_patch_autotune() +_patch_config_init() +_patch_cache_invalidating_env_vars() diff --git a/third_party/ascend/backend/runtime/autoparser.py b/third_party/ascend/backend/runtime/autoparser.py new file mode 100644 index 0000000000..c1925b138e --- /dev/null +++ b/third_party/ascend/backend/runtime/autoparser.py @@ -0,0 +1,939 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import ast +from typing import Dict, List, Union + + +class AutoParser(ast.NodeVisitor): + """ + Base class for parsing triton dsl kernel code using AST analysis. + + Provides common functionality for traversing the AST (abstract syntax tree) + of a triton dsl kernel function and identifying specific elements in the code. + + Subclassed should implement specific parsing logic by overriding the relevant + node visit methods. + """ + + def __init__(self, func_ast: ast.AST): + self.func_ast = func_ast + + def parse(self): + self.visit(self.func_ast) + + def contains_target_var(self, node, var): + """ + Recursively checks if a given AST node or its children contain a reference + to the specified variable. + + :param node: the AST node to check + :type node: ast.AST + :param var: the variable name to search for + :type var: str + :return: True if the variable is found, False otherwise + """ + if isinstance(node, ast.Name) and node.id == var: + return True + for _, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + if self.contains_target_var(item, var): + return True + elif isinstance(value, ast.AST): + if self.contains_target_var(value, var): + return True + return False + + +class AxesKeyParser(AutoParser): + """ + A parser for extracting axis information from a given function's AST. + This class is designed to handle specific patterns in the function's code to + determine the axis associated with a given variable. It is particularly useful + for parsing triton DSL kernel code and identifying axis information. + It recursively processes assignment nodes and lessthan nodes to obtain the axes + corresponding to the specified var in the given function. + """ + + def __init__(self, func_ast: ast.AST, keys: Dict[str, str]): + super().__init__(func_ast) + self.keys = keys + self.checked_vars = list() + + def get_axis(self, var: str, node=None): + """ + Traverse the AST using the provided variable name and mask-based less-than + operations to obtain the corresponding axis name. + + :param var: the variable name to get the corresponding axis. + :type var: str + """ + if var in self.checked_vars: + return None + axis = None + if not node: + node = self.func_ast + for child_node in ast.walk(node): + # handle compare node + if isinstance(child_node, ast.Compare): + axis = self.handle_lt_node(var, child_node) + elif isinstance(child_node, ast.Assign): + axis = self.handle_assign_node(var, child_node) + + elif isinstance(child_node, ast.BinOp) and \ + isinstance(child_node.op, ast.BitAnd): + + axis = self.handle_lt_node(var, child_node.left) + if axis is None: + axis = self.handle_lt_node(var, child_node.right) + + if axis is not None: + return axis + self.checked_vars.append(var) + return None + + def handle_assign_node(self, var, node): + if not isinstance(node, ast.Assign) or not isinstance(node.targets, list): + return None + if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name): + return None + + target = node.targets[0].id + if target in self.checked_vars: + return None + # Prevent cyclic assignment. + if var == target or not self.contains_target_var(node.value, var): + return None + + axis = self.get_axis(var, node.value) + if axis: + return axis + + axis = self.get_axis(target) + return axis + + def handle_lt_node(self, var, node): + if not isinstance(node, ast.Compare) or not isinstance(node.ops, list): + return None + if len(node.ops) != 1 or not isinstance(node.ops[0], ast.Lt): + return None + if not isinstance(node.comparators, list) or len(node.comparators) != 1: + return None + if not isinstance(node.left, ast.Name) or var != node.left.id: + return None + + comparator = node.comparators[0] + if not isinstance(comparator, ast.Name) and \ + not (isinstance(comparator, ast.Call) and \ + isinstance(comparator.func, ast.Name) and \ + comparator.func.id == 'min'): + return None + + for k, v in self.keys.items(): + if self.contains_target_var(comparator, v): + return k + return None + + +class SplitAxesParser(AxesKeyParser): + """ + Extracts the split axis parameters from triton kernel code. The parsing is based on the + `tl.program_id` statement. This class identifies potential split axes by analyzing the usage + of the `tl.program_id` variable in the program and its multiplication operations with other + variables(currently supporting scenarios where multiplication is either direct or indirect via + intermediate variables). It then filters these candidates based on a list of candidate parameters + (parameters not provided by the user). After that, it confirms the split axis corresponding to + the current parameter using mask comparison and the `keys` passed in `autotune`. + + Note: + 1. Split axis parameters must be multiplied with `tl.program_id`. + 2. Without mask comparision, it is impossible to confirm the exact split axis, which would lead + to parameter parsing failure. (eg. mask = offsets < n_elements) + 3. The identified split axes are limited to the list of candidated parameters, ensuring that + only those parameters that can be dynamically adjusted through the autotune process are considered. + """ + + def __init__(self, func_ast: ast.AST, keys: Dict[str, str], candidates_params: List[str]): + """ + :param func_ast: Abstract syntax tree of the triton kernel function + :type func_ast: ast.AST + :param keys: a dict of axis name: argument name, used to confirm the split axis corresponding to + the split axis parameters. + :type keys: Dict[str, str] + :param candidates_params: a list of parameters names that were not provided by the user when calling + triton kernel function. The parser will only consider these parameters as potential split axis + parameters. + :type candidates_params: List[str] + """ + super().__init__(func_ast, keys) + self.split_axes = dict() + self.program_id_vars = list() + self.program_id_var_dims = dict() + self.num_programs_var_dims = dict() + self.grid_stride_tiling_only = dict() + # axis_name -> program_id axis dim + self.split_axis_pid_dims = dict() + # axis_name -> program_id axis dim (includes axes inferred without split params) + self.axis_pid_dims = dict() + self.candidates_params = candidates_params + + def parse(self) -> Dict[str, str]: + super().parse() + return self.split_axes + + def visit_Assign(self, node): + pid_dim = self._get_program_id_dim(node.value) + if pid_dim is not None: + if (len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) + and node.targets[0].id not in self.program_id_vars): + self.program_id_vars.append(node.targets[0].id) + self.program_id_var_dims[node.targets[0].id] = pid_dim + num_programs_dim = self._get_num_programs_dim(node.value) + if num_programs_dim is not None: + if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): + self.num_programs_var_dims[node.targets[0].id] = num_programs_dim + self.generic_visit(node) + + def visit_BinOp(self, node): + if isinstance(node.op, ast.Mult): + split_axes_val = None + split_axis_pid_dim = None + if isinstance(node.left, ast.Name) and node.left.id in self.program_id_vars: + if isinstance(node.right, ast.Name): + split_axes_val = node.right.id + split_axis_pid_dim = self.program_id_var_dims.get(node.left.id) + elif isinstance(node.left, ast.Call) and isinstance(node.left.func, ast.Attribute): + if (isinstance(node.left.func.value, ast.Name) and node.left.func.value.id == "tl" + and node.left.func.attr == "program_id"): + if isinstance(node.right, ast.Name): + split_axes_val = node.right.id + split_axis_pid_dim = self._get_program_id_dim(node.left) + + if isinstance(node.right, ast.Name) and node.right.id in self.program_id_vars: + if isinstance(node.left, ast.Name): + split_axes_val = node.left.id + split_axis_pid_dim = self.program_id_var_dims.get(node.right.id) + elif isinstance(node.right, ast.Call) and isinstance(node.right.func, ast.Attribute): + if (isinstance(node.right.func.value, ast.Name) and node.right.func.value.id == "tl" + and node.right.func.attr == "program_id"): + if isinstance(node.left, ast.Name): + split_axes_val = node.left.id + split_axis_pid_dim = self._get_program_id_dim(node.right) + + if split_axes_val in self.candidates_params and \ + split_axes_val not in self.split_axes.values(): + split_axes_key = self.get_axis(split_axes_val) + if split_axes_key and not self._is_tiling_only_split(split_axes_key, split_axes_val): + self.split_axes[split_axes_key] = split_axes_val + if split_axis_pid_dim is not None: + self._record_axis_pid_dim(split_axes_key, split_axis_pid_dim) + self.generic_visit(node) + + def visit_For(self, node): + if not isinstance(node.iter, ast.Call): + self.generic_visit(node) + return + + iter_fn = node.iter.func + is_range = isinstance(iter_fn, ast.Name) and iter_fn.id == "range" + is_tl_range = (isinstance(iter_fn, ast.Attribute) and isinstance(iter_fn.value, ast.Name) + and iter_fn.value.id == "tl" and iter_fn.attr == "range") + if not (is_range or is_tl_range): + self.generic_visit(node) + return + + if len(node.iter.args) == 0: + self.generic_visit(node) + return + + start = node.iter.args[0] if len(node.iter.args) >= 2 else None + stop = node.iter.args[1] if len(node.iter.args) >= 2 else node.iter.args[0] + pid_dim = self._extract_pid_dim_from_expr(start) + axis = self._axis_from_expr(stop) + if axis is not None and pid_dim is not None: + self._record_axis_pid_dim(axis, pid_dim) + if len(node.iter.args) >= 3: + step = node.iter.args[2] + loop_tiling_only_param = self._extract_grid_stride_split_param(start, step, pid_dim) + if loop_tiling_only_param is not None: + self._mark_tiling_only_param(axis, loop_tiling_only_param) + + self.generic_visit(node) + + def _get_program_id_dim(self, node): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and isinstance( + node.func.value, ast.Name) and node.func.value.id == "tl" and node.func.attr == "program_id"): + return None + + axis_dim = 0 + if len(node.args) > 0: + if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, int): + axis_dim = node.args[0].value + else: + return None + + for kw in node.keywords: + if kw.arg == "axis": + if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, int): + axis_dim = kw.value.value + else: + return None + break + return axis_dim + + def _get_num_programs_dim(self, node): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and isinstance( + node.func.value, ast.Name) and node.func.value.id == "tl" and node.func.attr == "num_programs"): + return None + + axis_dim = 0 + if len(node.args) > 0: + if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, int): + axis_dim = node.args[0].value + else: + return None + + for kw in node.keywords: + if kw.arg == "axis": + if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, int): + axis_dim = kw.value.value + else: + return None + break + return axis_dim + + def _extract_pid_dim_from_expr(self, node): + if node is None: + return None + for child in ast.walk(node): + if isinstance(child, ast.Name) and child.id in self.program_id_var_dims: + return self.program_id_var_dims[child.id] + pid_dim = self._get_program_id_dim(child) + if pid_dim is not None: + return pid_dim + return None + + def _contains_pid_dim(self, node, pid_dim): + if node is None: + return False + for child in ast.walk(node): + if isinstance(child, ast.Name): + if self.program_id_var_dims.get(child.id, None) == pid_dim: + return True + if self._get_program_id_dim(child) == pid_dim: + return True + return False + + def _contains_num_programs_dim(self, node, pid_dim): + if node is None: + return False + for child in ast.walk(node): + if isinstance(child, ast.Name): + if self.num_programs_var_dims.get(child.id, None) == pid_dim: + return True + if self._get_num_programs_dim(child) == pid_dim: + return True + return False + + def _is_candidate_name(self, node, candidate_name): + return (isinstance(node, ast.Name) and node.id == candidate_name and candidate_name in self.candidates_params) + + def _extract_pid_multiplied_candidate(self, node, pid_dim): + if node is None: + return None + candidates = set() + for child in ast.walk(node): + if not isinstance(child, ast.BinOp) or not isinstance(child.op, ast.Mult): + continue + left = child.left + right = child.right + if isinstance(left, ast.Name) and left.id in self.candidates_params and \ + self._contains_pid_dim(right, pid_dim): + candidates.add(left.id) + if isinstance(right, ast.Name) and right.id in self.candidates_params and \ + self._contains_pid_dim(left, pid_dim): + candidates.add(right.id) + if len(candidates) == 1: + return next(iter(candidates)) + return None + + def _contains_num_programs_multiplied_candidate(self, node, candidate_name, pid_dim): + if node is None: + return False + for child in ast.walk(node): + if not isinstance(child, ast.BinOp) or not isinstance(child.op, ast.Mult): + continue + if self._is_candidate_name(child.left, candidate_name): + if self._contains_num_programs_dim(child.right, pid_dim): + return True + if self._is_candidate_name(child.right, candidate_name): + if self._contains_num_programs_dim(child.left, pid_dim): + return True + return False + + def _extract_grid_stride_split_param(self, start, step, pid_dim): + if start is None or step is None: + return None + candidate_name = self._extract_pid_multiplied_candidate(start, pid_dim) + if candidate_name is None: + return None + if self._contains_num_programs_multiplied_candidate(step, candidate_name, pid_dim): + return candidate_name + return None + + def _mark_tiling_only_param(self, axis, candidate_name): + self.grid_stride_tiling_only.setdefault(axis, set()).add(candidate_name) + if self.split_axes.get(axis, None) == candidate_name: + del self.split_axes[axis] + self.split_axis_pid_dims.pop(axis, None) + + def _is_tiling_only_split(self, axis, candidate_name): + return candidate_name in self.grid_stride_tiling_only.get(axis, set()) + + def _axis_from_expr(self, node): + if node is None: + return None + for k, v in self.keys.items(): + if self.contains_target_var(node, v): + return k + return None + + def _record_axis_pid_dim(self, axis, pid_dim): + self.axis_pid_dims[axis] = pid_dim + if axis in self.split_axes: + self.split_axis_pid_dims[axis] = pid_dim + + +class TilingAxesParser(AxesKeyParser): + """ + Extracts the tiling axis parameters from triton kernel code. The parsing is based on the + `tl.arange`, `tl.range` and `range()` statement. This class identifies potential tiling axes by analyzing + the usage of the `range` and `tl.range` within `for` loop in the program. Common parameters + between `range()` or `tl.range` and `tl.arange` are extracted. It then filters these candidates based on a + list of candidate parameters (parameters not provided by the user). After that, it confirms the + tiling axis corresponding to the current parameter using mask comparison and the `keys` passed + in `autotune`. + + Note: + 1. Tiling axis parameters must be calculated within the `tl.arange` function and the `for` loop + using `tl.range`. + 2. Without mask comparision, it is impossible to confirm the exact tiling axis, which would lead + to parameter parsing failure. (eg. mask = offsets < n_elements). + 3. The identified tiling axes are limited to the list of candidated parameters, ensuring that + only those parameters that can be dynamically adjusted through the autotune process are considered. + """ + + def __init__(self, func_ast: ast.AST, keys: Dict[str, str], candidates_params: List[str]): + """ + :param func_ast: Abstract syntax tree of the triton kernel function + :type func_ast: ast.AST + :param keys: a dict of axis name: argument name, used to confirm the tiling axis corresponding to + the tiling axis parameters. + :type keys: Dict[str, str] + :param candidates_params: a list of parameters names that were not provided by the user when calling + triton kernel function. The parser will only consider these parameters as potential tiling axis + parameters. + :type candidates_params: List[str] + """ + super().__init__(func_ast, keys) + self.tiling_axes = dict() + self.candidates_params = candidates_params + self.candidates_params_for_loop = list() + + def parse(self) -> Dict[str, str]: + super().parse() + return self.tiling_axes + + def visit_For(self, node): + if isinstance(node.iter, ast.Call) and len(node.iter.args) == 3: + step_expr = node.iter.args[2] + for_loop_param = self._extract_unique_candidate(step_expr) + if (for_loop_param is not None and for_loop_param not in self.candidates_params_for_loop): + self.candidates_params_for_loop.append(for_loop_param) + self.generic_visit(node) + + def visit_Assign(self, node): + if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): + # handle FloorDiv + if isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.FloorDiv): + denominator = node.value.right + denominator_param = self._extract_unique_candidate(denominator) + if denominator_param is not None and \ + denominator_param not in self.candidates_params_for_loop: + self.candidates_params_for_loop.append(denominator_param) + self.visit(self.func_ast) + + tiling_axes_val = self.get_tiling_axes_val(node.value) + if tiling_axes_val is not None and \ + tiling_axes_val in self.candidates_params_for_loop: + tiling_axes_key = self.get_axis(tiling_axes_val) + if tiling_axes_key: + self.tiling_axes[tiling_axes_key] = tiling_axes_val + self.generic_visit(node) + + def get_tiling_axes_val(self, node): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): + if node.func.attr == 'arange' and \ + isinstance(node.func.value, ast.Name) and \ + node.func.value.id == 'tl': + if isinstance(node.args, list) and len(node.args) == 2: + for param in self.candidates_params_for_loop: + if self.contains_target_var(node.args[1], param): + return param + + for _, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + val = self.get_tiling_axes_val(item) + if val: + return val + elif isinstance(value, ast.AST): + val = self.get_tiling_axes_val(value) + if val: + return val + return None + + def _extract_unique_candidate(self, expr): + """ + Extract a unique tiling candidate from an expression. + Return None when no candidate or ambiguous (more than one candidate) appears. + """ + if expr is None: + return None + candidates = [param for param in self.candidates_params if self.contains_target_var(expr, param)] + if len(candidates) == 1: + return candidates[0] + return None + + +class ReductionAxesParser(AxesKeyParser): + """ + Extracts the reduction axis from triton kernel code. The parsing is based on the + reduction function (eg. tl.max, tl.min, tl.sum, ...). This class identifies the + dimensions of reduction operations by analyzing the reduction function calls in + the program. After that, It confirms the reduction axis corresponding to the current + parameter using mask comparison and the keys passed in autotune. + + Note: + 1. The call to the reduction function must start with 'tl', meaning the function must + be a function from triton.language + 2. It's preferable to specify the reduction axis dimension in the reduction function + using keyword arguments(eg. axis=xxx). Otherwise, specifying it via positional + arguments may lead to errors. + 3. Mask comparison must be performed on the potential reduction axis length parameters, + and the comparison parameters or target parameters of the comparison expression + must be sliced. Otherwise, the correspondence between dimensions and axes cannot + be confirmed, which will lead to failure in parsing the reduction axis. + 4. The identified reduction axes are limited to the candidate list provided in the keys. + """ + + def __init__(self, func_ast: ast.AST, keys: Dict[str, str]): + """ + :param func_ast: Abstract syntax tree of the triton kernel function + :type func_ast: ast.AST + :param keys: a dict of axis name: argument name, used to confirm the reduction axis. + :type keys: Dict[str, str] + """ + super().__init__(func_ast, keys) + self.reduction_axes = list() + self.reduction_func = ('sum', 'xor_sum', 'max', 'min', 'argmax', 'argmin') # tl.xxx + self.ndim = 1 + + def parse(self) -> List[str]: + super().parse() + return self.reduction_axes + + def visit_Assign(self, node): + self._scan_subscripts(node.value) + self.generic_visit(node) + + def _scan_subscripts(self, node): + if isinstance(node, ast.Subscript): + ndim = self._get_subscripts_ndim(node) + if ndim > self.ndim: + self.ndim = ndim + + for child in ast.iter_child_nodes(node): + self._scan_subscripts(child) + + def _get_subscripts_ndim(self, subscript_node): + slice_node = subscript_node.slice + + if isinstance(slice_node, ast.Tuple): + # e.g. [:, None] -> Tuple(elts=[Slice(), Constant(None)]) + return len(slice_node.elts) + elif isinstance(slice_node, (ast.Slice, ast.Constant, ast.Name, ast.UnaryOp, ast.BinOp)): + # e.g. [0], [:], [i], [-1], [i+1] + return 1 + else: + # Fallback: treat as 1D + return 1 + + def visit_Call(self, node): + if not isinstance(node.func, ast.Attribute): + return + func = node.func + if not isinstance(func.value, ast.Name) or func.value.id != 'tl': + self.generic_visit(node) + return + if func.attr not in self.reduction_func: + return + + axis_dim = None + args = node.args + if len(args) == 1: + # Axis passed as keyword argument + for keyword in node.keywords: + if keyword.arg == 'axis': + axis_dim = self.get_axis_dim(keyword.value) + break + + elif len(args) == 2: + # Axis passed as positional argument. Check the second param + axis_dim = self.get_axis_dim(args[1]) + + else: + raise ValueError("Reduction funtions args error") + + if axis_dim is not None: + reduction_axis = self.get_axis(axis_dim) + if reduction_axis and reduction_axis not in self.reduction_axes: + self.reduction_axes.append(reduction_axis) + + def get_axis_dim(self, node): + if isinstance(node, ast.Constant): + axis_dim = node.value + elif isinstance(node, ast.UnaryOp) and \ + isinstance(node.op, ast.USub): + operand = node.operand + if isinstance(operand, ast.Constant): + axis_dim = self.ndim - operand.value + else: + raise ValueError(f"Reduction function axis error, got: {ast.dump(node)}") + + if not isinstance(axis_dim, int): + raise ValueError("Reduction function axis must be an integer, " + f"got {type(node.value).__name__}: {node.value}") + return axis_dim + + def get_axis(self, axis_dim: int): + """ + Override the parent class method to accept an integer axis dimension + instead of a string. + + :param axis_dim: + :type axis_dim: int + """ + if axis_dim in self.checked_vars: + return None + self.checked_vars.append(axis_dim) + for node in ast.walk(self.func_ast): + if not isinstance(node, ast.Assign): + continue + reduction_axis = self.handle_assign_node(axis_dim, node) + if reduction_axis: + return reduction_axis + return None + + def handle_assign_node(self, axis_dim: int, node): + if not isinstance(node.value, ast.Compare): + return None + + # only support less than + if len(node.value.ops) != 1 or not isinstance(node.value.ops[0], ast.Lt): + return None + + target_axis_len = None + for axis_len in self.keys.values(): + if self.contains_target_var(node.value, axis_len): + target_axis_len = axis_len + break + if not target_axis_len: + return None + + # handel compare left var + if isinstance(node.value.left, ast.Name): + if self.check_compare_left(node.value.left.id, axis_dim): + reduction_axis = next((k for k, v in self.keys.items() if target_axis_len == v), None) + if reduction_axis and reduction_axis not in self.reduction_axes: + return reduction_axis + # handel compare target var + if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): + if self.check_compare_target(node.targets[0].id, axis_dim): + reduction_axis = next((k for k, v in self.keys.items() if target_axis_len == v), None) + if reduction_axis and reduction_axis not in self.reduction_axes: + return reduction_axis + return None + + def check_compare_left(self, var, axis_dim): + for node in ast.walk(self.func_ast): + if not isinstance(node, ast.Assign): + continue + if len(node.targets) != 1 or \ + not isinstance(node.targets[0], ast.Name) or \ + node.targets[0].id != var: + continue + if self.is_current_dim_slice(node.value, axis_dim): + return True + return False + + def check_compare_target(self, var, axis_dim, node=None): + if not node: + node = self.func_ast + for _, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + if self.check_compare_target(var, axis_dim, item): + return True + elif isinstance(value, ast.AST): + if isinstance(value, ast.Subscript): + if not isinstance(value.value, ast.Name) or value.value.id != var: + continue + if self.is_current_dim_slice(value, axis_dim): + return True + else: + if self.check_compare_target(var, axis_dim, value): + return True + return False + + def is_current_dim_slice(self, node, dim): + for node in ast.walk(node): + if not isinstance(node, ast.Subscript) or not isinstance(node.slice, ast.Tuple): + continue + elts = node.slice.elts + if len(elts) != 0 and isinstance(elts[dim], ast.Slice): + return True + return False + + +class LowDimsAxesParser(AxesKeyParser): + """ + Extracts the low dimensions axis from triton kernel code. The parsing is based on the + `tl.arange` statement. This class identifies low dimensions axis by analyzing the usage + of the `tl.arange` in the program and extracts the variables computed by `tl.arange` and + their associated operations. Then it checks if these variables are involved in slicing + operations to determine dimension expansion and filters out variables that are expanded + in non-lowest dimensions. After that, it compares the extracted variables with the provided + `keys` to map them to specific low-dimensional axis. + + Note: + 1. low dimensions axis must be calculated within the `tl.arange` function and involved in + slicing operations to be identified. + 2. Without mask comparision, it is impossible to confirm the exact low dimensions axis, which + would lead to parameter parsing failure. (eg. mask = offsets < n_elements). + """ + + def __init__(self, func_ast: ast.AST, keys: Dict[str, str]): + """ + :param func_ast: Abstract syntax tree of the triton kernel function + :type func_ast: ast.AST + :param keys: a dict of axis name: argument name, used to confirm the low-dimensional axis. + :type keys: Dict[str, str] + """ + super().__init__(func_ast, keys) + self.low_dims_axis = list() + self.keys = keys + self.checked_slice_vars = list() + + def parse(self) -> List[str]: + super().parse() + return self.low_dims_axis + + def visit_Assign(self, node): + if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): + tl_arange_node = self.get_tl_arange_node(node) + low_dims_axis = None + if isinstance(tl_arange_node, ast.Call): + partin_other_slice = [False] + if self.is_partin_low_dim_slice(node.targets[0].id, partin_other_slice): + low_dims_axis = self.get_axis(node.targets[0].id) + elif not partin_other_slice[0]: + low_dims_axis = self.get_axis(node.targets[0].id) + elif isinstance(tl_arange_node, ast.Subscript) and \ + self.is_low_dim_slice(tl_arange_node, [False]): + low_dims_axis = self.get_axis(node.targets[0].id) + + if low_dims_axis and low_dims_axis not in self.low_dims_axis: + self.low_dims_axis.append(low_dims_axis) + self.generic_visit(node) + + def get_tl_arange_node(self, node): + for _, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + if self.is_tl_arange_call(item): + return item + node = self.get_tl_arange_node(item) + if node: + return node + elif isinstance(value, ast.AST): + if self.is_tl_arange_call(value): + return value + node = self.get_tl_arange_node(value) + if node: + return node + return None + + def is_tl_arange_call(self, node): + """ + Checks if the given AST node is a call to `tl.arange` or a subscript of `tl.arange`. + It supports direct calls to `tl.arange` and subscripts of `tl.arange`, such as + `tl.arange()[None, :]` + """ + if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): + if node.func.attr == 'arange' and \ + isinstance(node.func.value, ast.Name) and \ + node.func.value.id == 'tl': + return True + elif isinstance(node, ast.Subscript): + return self.is_tl_arange_call(node.value) + return False + + def is_low_dim_slice(self, node: ast.Subscript, partin_other_slice): + if not isinstance(node.slice, ast.Tuple) or not isinstance(node.slice.elts, list): + return False + elts = node.slice.elts + if len(elts) != 0 and not isinstance(elts[-1], ast.Slice): + partin_other_slice[0] = True + return False + return True + + def is_partin_low_dim_slice(self, var, partin_other_slice, node=None): + if not node: + node = self.func_ast + for child_node in ast.walk(node): + if isinstance(child_node, ast.Subscript) and isinstance(child_node.value, ast.Name): + if var == child_node.value.id and self.is_low_dim_slice(child_node, partin_other_slice): + return True + elif isinstance(child_node, ast.Assign): + if len(child_node.targets) == 1 and \ + isinstance(child_node.targets[0], ast.Name) and \ + var != child_node.targets[0].id: # Prevent cyclic assignment. + if not self.contains_target_var(child_node.value, var): + continue + target_var = child_node.targets[0].id + if target_var in self.checked_slice_vars: + continue + + if self.is_partin_low_dim_slice(var, partin_other_slice, child_node.value): + return True + if self.is_partin_low_dim_slice(target_var, partin_other_slice): + return True + + self.checked_slice_vars.append(var) + return False + + +class PtrNumsParser(AutoParser): + """ + Counts the number of pointer parameters from triton kernel code. The parsing of pointer-type + parameters is determined based on whether these parameters participate in memory access + statements such as `tl.load` and `tl.store`. + First, all input parameters in the kernel function are parsed, and then recursively, all variables + involved in the computation of each input parameter are identified. + If an input parameter directly participates in the computation of the first argument of `tl.load` + or `tl.store`, or if an intermediate variable computed from this input parameter indirectly + participates in the computation of the first argument of `tl.load` or `tl.store`, then this + parameter is considered a pointer-type parameter. + + Note: + 1. Variables modified with `tl.constexpr` are not pointer-type variables and will not be + further parsed. + 2. Only memory access statementes where the input parameter is directly involved or indirectly + involved through one level of computation are counted. Intermediate variables computed from + the input parameter through two or more levels of computation are not counted. + """ + + def __init__(self, func_ast: ast.AST, keys: Dict[str, str], miss_params: List[str]): + """ + :param func_ast: Abstract syntax tree of the triton kernel function + :type func_ast: ast.AST + :param keys: a dict of axis name: argument name, used to exclude potential ptr params. + :type keys: Dict[str, str] + :param miss_params: a list of parameters names that were not provided by the user when calling triton + kernel function. + :type miss_params: List[str] + """ + super().__init__(func_ast) + self.checked_vars = list() + self.ptr_nums = 0 + self.ptr_params = list() + self.keys = keys + self.miss_params = miss_params + self.constexpr_params = list() + + def parse(self): + super().parse() + return self.ptr_nums, self.ptr_params + + def visit_FunctionDef(self, node): + if isinstance(node.args, ast.arguments): + for arg in node.args.args: + if not isinstance(arg, ast.arg): + continue + + if isinstance(arg.annotation, ast.Attribute): + # var modified by tl.constexpr are not pointer type var, passed + is_tl = isinstance(arg.annotation.value, ast.Name) and \ + arg.annotation.value.id == 'tl' + if is_tl and arg.annotation.attr == 'constexpr': + if arg.arg not in self.constexpr_params: + self.constexpr_params.append(arg.arg) + continue + + if self.is_in_addr_calc(arg.arg) and arg.arg not in self.keys.values(): + self.ptr_params.append(arg.arg) + self.ptr_nums += 1 + + for miss_param in self.miss_params: + if miss_param not in self.constexpr_params: + print(f"[WARNING] The parameter '{miss_param}' needs to be declared as tl.constexpr!") + self.generic_visit(node) + + def is_in_addr_calc(self, var): + for node in ast.walk(self.func_ast): + if isinstance(node, ast.Call): + if isinstance(node.func, ast.Attribute) and \ + isinstance(node.func.value, ast.Name): + if node.func.value.id == "tl" and \ + (node.func.attr == "load" or node.func.attr == "store"): + if [arg for arg in node.args if self.contains_target_var(arg, var)]: + return True + + elif isinstance(node, ast.Assign): + if len(node.targets) == 1 and \ + isinstance(node.targets[0], ast.Name) and \ + var != node.targets[0].id: # Prevent cyclic assignment. + target_var = node.targets[0].id + if target_var in self.checked_vars: + continue + if isinstance(node.value, ast.BinOp) and \ + isinstance(node.value.op, ast.Add): + if isinstance(node.value.left, ast.Name) and \ + node.value.left.id == var: + if self.is_in_addr_calc(node.targets[0].id): + return True + elif isinstance(node.value.right, ast.Name) and \ + node.value.right.id == var: + if self.is_in_addr_calc(node.targets[0].id): + return True + self.checked_vars.append(var) + return False diff --git a/third_party/ascend/backend/runtime/autotuner.py b/third_party/ascend/backend/runtime/autotuner.py new file mode 100644 index 0000000000..9b73a7c408 --- /dev/null +++ b/third_party/ascend/backend/runtime/autotuner.py @@ -0,0 +1,3388 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import builtins +import copy +import functools +import ast +import gc +import inspect +import math +import os +import pprint +import threading +import time +import warnings +from concurrent.futures import ThreadPoolExecutor +import itertools +from collections.abc import Sequence +from dataclasses import asdict, is_dataclass +from typing import Any, Dict, List, Optional, Tuple + +from torch import Tensor + +import triton +from triton import knobs +from triton._C.libtriton import get_cache_invalidating_env_vars +from triton.runtime.autotuner import Autotuner, Config +from triton.runtime.jit import compute_cache_key +from triton.backends.ascend.utils import (_InternalNPUOptionInt, _remove_deprecated_npu_options, + _RESERVED_NPU_OPTION_NAMES, is_compile_on_910_95, _multibuffer_mode_to_tuple) + +from .autoparser import (LowDimsAxesParser, PtrNumsParser, ReductionAxesParser, SplitAxesParser, TilingAxesParser) +from .dsl_analysis.cv_param_parser import parse_cv_params +from .dsl_analysis.entry import analyze_dot_site_mnk, analyze_signature_and_missing_tunable +from .dsl_analysis.kernel_classifier import resolve_kernel_type +from .dsl_analysis.vv_config_adapter import adapt_vv_v2_to_vector_inputs +from .dsl_analysis.vv_param_parser_v2 import parse_vv_axis_info_v2 +from .dsl_analysis.vv_parser_options import (resolve_vv_parser_v2_enabled, resolve_vv_parser_v2_mode) +from .utils import get_byte_per_numel, is_valid_axis_name, valid_axis_names +from .ubtuner import UBTuner, get_origin_fn +from .vector_axes import VectorAxes + +# Import CV autotune components +from .cv_autotune.generators.cv_tile_generator import CVTileGenerator +from .cv_autotune.hardware_consts import get_default_hardware_constraints + +_RESERVED_HINT_KEYS = { + "split_params", + "tiling_params", + "low_dim_axes", + "reduction_axes", + "axes", + "auto_gen_config", + "kernel_type", + "tunable_parameter", + "vv_parser_v2_mode", +} +_DEFAULT_HINT_NUM_STAGES = [1, 2] +_DEFAULT_COMPILE_MODE = "simd_simt_template" +_FAILURE_DETAIL_MAX_LENGTH = 512 + + +def _format_autotune_failure(exc) -> str: + """Summarize a failure without retaining its exception or traceback.""" + try: + detail = getattr(exc, "error_message", None) or str(exc) + detail = " ".join(str(detail).split()) + except Exception: + detail = "" + summary = type(exc).__name__ + (f": {detail}" if detail else "") + if len(summary) > _FAILURE_DETAIL_MAX_LENGTH: + summary = summary[:_FAILURE_DETAIL_MAX_LENGTH - 3] + "..." + return summary + + +class _UserConfigDiagnostics: + """Track explicit configs for one invocation, including hints expansion.""" + + _REPORTED = "reported" + _PENDING = {"not_evaluated", "compiling", "benchmarking", "running"} + _NON_FAILURES = {_REPORTED, "benchmarked", "selected", "skipped_cache_hit", "pruned"} + + def __init__(self, configs): + self.reset_configs(configs) + + def reset_configs(self, configs): + # An equal generated config may be the key retained by the benchmark dict. + self.results = [[config, "not_evaluated", "No benchmark result was produced."] for config in configs] + + def record(self, config, status, detail="", exc=None): + matched = False + for result in self.results: + if result[0] is config or result[0] == config: + if status == "pruned" and result[1] == "rough_benchmark_failed": + # Keep the actual error when time-budget pruning follows it. + result[2] += f" {detail}" + matched = True + continue + result[1] = status + result[2] = detail + (f" {_format_autotune_failure(exc)}" if exc is not None else "") + matched = True + return matched + + def abort(self, exc): + for result in self.results: + if result[1] in self._PENDING: + result[2] += f" Autotuning interrupted: {_format_autotune_failure(exc)}" + + def report(self): + incomplete = { + "compiling": "compilation_incomplete", + "benchmarking": "benchmark_incomplete", + "running": "evaluation_incomplete", + } + for config, status, detail in self.results: + # Keep existing success logs; only add failures and interrupted work. + if status in self._NON_FAILURES: + continue + status = incomplete.get(status, status) + print(f"Triton autotuning: config={config}; {status}; reason={detail.strip()}") + + +def _format_autotune_timing(timing) -> str: + """Format the timing returned by the active autotune benchmarker.""" + if isinstance(timing, (tuple, list)): + labels = ("p50", "p20", "p80") + return ", ".join(f"{label}={value:.4f} ms" for label, value in zip(labels, timing)) + return f"mean={timing:.4f} ms" + + +def _get_constexpr_candidates_from_fn(fn) -> List[str]: + """ + Returns all constexpr parameter names from the kernel function definition. + """ + current_fn = fn + visited = set() + while current_fn is not None and id(current_fn) not in visited: + visited.add(id(current_fn)) + parse = getattr(current_fn, "parse", None) + if callable(parse): + func_ast = parse() + break + jit_function = getattr(current_fn, "jit_function", None) + current_fn = jit_function if jit_function is not None else getattr(current_fn, "fn", None) + else: + return [] + + constexpr_names = [] + for node in ast.walk(func_ast): + if not isinstance(node, ast.FunctionDef): + continue + if not isinstance(node.args, ast.arguments): + continue + for arg in node.args.args: + if not isinstance(arg, ast.arg): + continue + ann = arg.annotation + if (isinstance(ann, ast.Attribute) and isinstance(ann.value, ast.Name) and ann.value.id == "tl" + and ann.attr == "constexpr"): + constexpr_names.append(arg.arg) + break + return constexpr_names + + +def _clone_config_with_kwargs( + config: Config, + kwargs: Dict[str, object], + num_stages: Optional[int] = None, +) -> Config: + return Config( + kwargs=kwargs, + num_warps=config.num_warps, + num_stages=config.num_stages if num_stages is None else num_stages, + num_ctas=config.num_ctas, + maxnreg=config.maxnreg, + pre_hook=config.pre_hook, + ir_override=config.ir_override, + ) + + +def _normalize_user_configs(configs): + if not configs: + return configs + normalized_configs = [] + for config in configs: + normalized_kwargs = _remove_deprecated_npu_options(config.kwargs) + if normalized_kwargs == config.kwargs: + normalized_configs.append(config) + continue + normalized_config = copy.copy(config) + normalized_config.kwargs = normalized_kwargs + normalized_configs.append(normalized_config) + return normalized_configs + + +def _validate_reserved_npu_option_names(arg_names) -> None: + conflicts = sorted(set(arg_names) & _RESERVED_NPU_OPTION_NAMES) + if conflicts: + raise ValueError("Kernel parameters cannot use reserved Ascend compile-option names: {}.".format( + ", ".join(conflicts))) + + +def _split_hints(hints: Optional[Dict[str, object]]): + hints = {} if hints is None else dict(hints) + reserved_hints = {k: v for k, v in hints.items() if k in _RESERVED_HINT_KEYS} + config_hints = {k: v for k, v in hints.items() if k not in _RESERVED_HINT_KEYS} + return reserved_hints, config_hints + + +def _validate_user_hints(fn, hints: Optional[Dict[str, object]]) -> None: + if not hints: + return + + constexpr_names = set(_get_constexpr_candidates_from_fn(fn)) + legal_keys = set(_RESERVED_HINT_KEYS) | set(_ALL_PARAMS) | constexpr_names + invalid_keys = sorted(key for key in hints.keys() if key not in legal_keys) + if invalid_keys: + raise ValueError("Unsupported hints keys: {}. " + "Legal keys must be reserved hints, tl.constexpr kernel parameters, " + "or supported compile options.".format(invalid_keys)) + + +def _multibuffer_to_num_stages(multibuffer: bool) -> int: + if not isinstance(multibuffer, bool): + raise ValueError("multibuffer must be a boolean when mapped to num_stages. " + f"Got: {type(multibuffer)}") + return 2 if multibuffer else 1 + + +def _normalize_config_hints( + config_hints: Optional[Dict[str, object]], + inject_default_num_stages: bool = False, + has_multibuffer_mode: bool = False, +): + normalized_hints = {} if config_hints is None else dict(config_hints) + has_explicit_num_stages = "num_stages" in normalized_hints + + has_mode = has_multibuffer_mode or "multibuffer_mode" in normalized_hints + if has_mode and has_explicit_num_stages: + raise ValueError("num_stages and multibuffer_mode cannot be specified together") + multibuffer_values = (normalized_hints.get("multibuffer") if has_mode else normalized_hints.pop( + "multibuffer", None)) + if multibuffer_values is not None: + if not isinstance(multibuffer_values, (list, tuple)) or len(multibuffer_values) == 0: + raise ValueError("hints['multibuffer'] must be a non-empty list/tuple when used for config expansion.") + if not all(isinstance(value, bool) for value in multibuffer_values): + raise ValueError("hints['multibuffer'] must contain only boolean values.") + if not has_explicit_num_stages and not has_mode: + normalized_hints["num_stages"] = [_multibuffer_to_num_stages(value) for value in multibuffer_values] + + if inject_default_num_stages and "num_stages" not in normalized_hints and not has_mode: + normalized_hints["num_stages"] = list(_DEFAULT_HINT_NUM_STAGES) + + return normalized_hints + + +def _validate_config_hint_values(config_hints: Dict[str, object], constexpr_names: List[str]): + invalid_keys = sorted(key for key in config_hints if key not in constexpr_names and key not in _ALL_PARAMS) + if invalid_keys: + raise ValueError( + "Invalid hints keys for config expansion: {}. " + "Keys must be tl.constexpr kernel parameters or supported compile options.".format(invalid_keys)) + + for key, value in config_hints.items(): + if not isinstance(value, (list, tuple)) or len(value) == 0: + raise ValueError(f"hints['{key}'] must be a non-empty list/tuple when used for config expansion.") + if key in _VALIDATION_RULES: + rule = _VALIDATION_RULES[key] + if not rule["check"](value, key): + raise ValueError(f"Invalid value for '{key}': {value}. Expected: {rule['desc']}") + + +def _expand_configs_with_hints(fn, configs, config_hints: Dict[str, object]): + if not config_hints: + return configs + + constexpr_names = _get_constexpr_candidates_from_fn(fn) + _validate_config_hint_values(config_hints, constexpr_names) + + if not configs: + return configs + + keys = list(config_hints.keys()) + values = [config_hints[key] for key in keys] + expanded_configs = [] + for config in configs: + for combo in itertools.product(*values): + new_kwargs = config.kwargs.copy() + num_stages = config.num_stages + for key, value in zip(keys, combo): + if key == "num_stages": + num_stages = value + else: + new_kwargs[key] = value + expanded_configs.append(_clone_config_with_kwargs(config, new_kwargs, num_stages)) + return expanded_configs + + +class AutoTilingTuner(Autotuner): + """ + Automatic generateing candidate tiling configs and evaluating their performance to get the best config. + """ + + def __init__( + self, + fn, + arg_names, + configs, + key, + reset_to_zero, + restore_value, + pre_hook=None, + post_hook=None, + prune_configs_by: Dict = None, + warmup=None, + rep=None, + use_cuda_graph=False, + do_bench=None, + cache_results=False, + auto_profile_dir=None, + hints=None, + ): + """ + :param key: a list of argument names whose runtime value changes invalidate the autotune cache + and trigger re-evaluation of candidate configs. + :type key: List[str] + """ + _validate_reserved_npu_option_names(arg_names) + hints = _remove_deprecated_npu_options(hints or {}) + configs = _normalize_user_configs(configs) + _validate_user_hints(fn, hints) + reserved_hints, raw_config_hints = _split_hints(hints) + config_hints = _normalize_config_hints( + raw_config_hints, + inject_default_num_stages=bool(configs) and bool(raw_config_hints), + has_multibuffer_mode=any(config.kwargs.get("multibuffer_mode") is not None for config in (configs or ())), + ) + + super().__init__( + fn, + arg_names, + configs, + key, + reset_to_zero, + restore_value, + pre_hook, + post_hook, + prune_configs_by, + warmup, + rep, + use_cuda_graph, + do_bench, + cache_results=cache_results, + ) + self.user_defined_do_bench = do_bench is not None + self.hints = reserved_hints + # Retain user hints to reconsider defaults when a launch supplies a mode. + self._raw_config_hints = raw_config_hints + self.config_hints = config_hints + self.vv_parser_v2_mode = resolve_vv_parser_v2_mode(self.hints) + self.enable_vv_parser_v2 = resolve_vv_parser_v2_enabled(self.hints) + self.explicit_tunable_params = self._parse_explicit_tunable_params(self.hints.get("tunable_parameter", None)) + + split_params = self.hints.get("split_params", None) + tiling_params = self.hints.get("tiling_params", None) + low_dim_axes = self.hints.get("low_dim_axes", None) + reduction_axes = self.hints.get("reduction_axes", None) + hints_axes = self.hints.get("axes", None) + self._init_axis_params( + key, + split_params, + tiling_params, + low_dim_axes, + reduction_axes, + hints_axes, + ) + + self.auto_gen_config = not configs or self.hints.get("auto_gen_config", False) + self.gen_configs = [] # generated configs from TileGenerator + self.auto_profile_dir = auto_profile_dir + if not configs: + self.user_configs = [] + else: + self.user_configs = configs + self.is_simt_mode = False + self.user_specified_warps = None + self.user_specified_num_stages = None + self.user_specified_multibuffer_mode = None + self.user_specified_multibuffer = None + target_arch = triton.runtime.driver.active.get_current_target().arch + self.default_multibuffer = not is_compile_on_910_95(target_arch) + self.print_autotuning = os.getenv("TRITON_PRINT_AUTOTUNING", None) == "1" + + # Mark the original function so ubtuner can detect autotune was applied + ori_fn = get_origin_fn(fn) + setattr(ori_fn, '_triton_autotuned', True) + + self.enable_ubtuner = os.getenv("TRITON_ENABLE_UBTUNER", "").lower() in ('compile', 'run') or getattr( + ori_fn, '_ubtuned', False) + if self.enable_ubtuner: + self.ubtuner = UBTuner(fn, key) + + # Compile kernels in parallel by default for triton.runtime.JITFunction or UBTuner, + # but not for others, e.g., LibEntry, since it's not compatible with AsyncCompileMode + self.compile_parallel = (isinstance(self.fn, (triton.runtime.JITFunction, UBTuner)) + and os.getenv("TRITON_AUTOTUNE_PARALLEL_COMPILE", "1") == "1") + self._source_module_ast_cache: Optional[ast.Module] = None + self._source_module_ast_resolved = False + + # Negative cache for deterministic compile failures. This is keyed by + # the JIT compilation identity rather than the autotune tuning key, so + # a failed compile can be reused when only the tuning key changes while + # the actual compiler inputs remain identical. + self._compile_failure_cache: Dict[Tuple, Dict[str, str]] = {} + self._compile_failure_cache_lock = threading.Lock() + self._cached_compile_failed_configs: List[Config] = [] + + @staticmethod + def _parse_explicit_tunable_params(raw_value) -> List[str]: + if raw_value is None: + return [] + if not isinstance(raw_value, list): + raise ValueError("hints.tunable_parameter must be a list, got: {}".format(type(raw_value))) + values = list(raw_value) + parsed = [] + seen = set() + for value in values: + if value in seen: + continue + parsed.append(value) + seen.add(value) + return parsed + + def _expand_simt_num_warps_configs(self, base_configs: List[Config]) -> List[Config]: + _default_cand_num_warps = [8, 16, 32, 64] + cand_num_warps = (_default_cand_num_warps if self.user_specified_warps is None else [self.user_specified_warps]) + + simt_configs = [] + for base_cfg in base_configs: + for num_warps in cand_num_warps: + new_cfg = copy.deepcopy(base_cfg) + new_cfg.num_warps = num_warps + simt_configs.append(new_cfg) + + if self.print_autotuning: + print(f"Triton autotuning: Expanded to {len(simt_configs)} SIMT configs (with warps: {cand_num_warps})") + return simt_configs + + def _get_non_simt_num_stages_candidates(self) -> List[int]: + if self.user_specified_num_stages is not None: + return [self.user_specified_num_stages] + if self.user_specified_multibuffer is not None: + return [_multibuffer_to_num_stages(self.user_specified_multibuffer)] + return list(_DEFAULT_HINT_NUM_STAGES) + + def _expand_num_stages_configs( + self, + base_configs: List[Config], + num_stages_candidates: List[int], + ) -> List[Config]: + expanded_configs = [] + for base_cfg in base_configs: + for num_stages in num_stages_candidates: + expanded_configs.append( + _clone_config_with_kwargs( + base_cfg, + base_cfg.kwargs.copy(), + num_stages=num_stages, + )) + + if self.print_autotuning: + print("Triton autotuning: Expanded to " + f"{len(expanded_configs)} configs (with num_stages: {num_stages_candidates})") + return expanded_configs + + def _expand_simd_multibuffer_configs(self, base_configs: List[Config]) -> List[Config]: + if self.user_specified_multibuffer_mode is not None or "multibuffer_mode" in self.config_hints: + return base_configs + if (self.user_specified_multibuffer is not None or self.user_specified_num_stages is not None): + normalized_configs = [] + for base_cfg in base_configs: + if self.user_specified_num_stages is not None: + base_cfg.num_stages = self.user_specified_num_stages + # Keep compiler-visible multibuffer consistent when only num_stages is specified. + if self.user_specified_multibuffer is None: + base_cfg.kwargs["multibuffer"] = (self.user_specified_num_stages != 1) + else: + base_cfg.num_stages = _multibuffer_to_num_stages(self.user_specified_multibuffer) + + normalized_configs.append(base_cfg) + if self.print_autotuning: + print("Triton autotuning: Skip SIMD multibuffer expansion because user " + f"specified multibuffer={self.user_specified_multibuffer}, " + f"num_stages={self.user_specified_num_stages}") + return normalized_configs + + opposite_default_multibuffer = not self.default_multibuffer + simd_configs = [] + for base_cfg in base_configs: + if base_cfg.kwargs.get("multibuffer_mode") is not None: + simd_configs.append(base_cfg) + continue + base_multibuffer = base_cfg.kwargs.get("multibuffer", self.default_multibuffer) + base_cfg.kwargs["multibuffer"] = base_multibuffer + base_cfg.num_stages = _multibuffer_to_num_stages(base_multibuffer) + simd_configs.append(base_cfg) + + new_cfg = _clone_config_with_kwargs(base_cfg, base_cfg.kwargs.copy()) + new_cfg.kwargs["multibuffer"] = opposite_default_multibuffer + new_cfg.num_stages = _multibuffer_to_num_stages(opposite_default_multibuffer) + simd_configs.append(new_cfg) + + if self.print_autotuning: + print("Triton autotuning: Expanded to " + f"{len(simd_configs)} SIMD configs (toggle multibuffer={opposite_default_multibuffer})") + return simd_configs + + def _parse_hints_axes(self, hints_axes): + if hints_axes is None: + return {} + if not isinstance(hints_axes, dict): + raise ValueError("hints.axes must be a dict, got: {}".format(type(hints_axes))) + if not set(hints_axes.keys()).issubset(set(valid_axis_names)): + raise ValueError("All keys in 'hints.axes' must be valid axis names. Got unexpected keys.") + for axis, arg_name in hints_axes.items(): + if not isinstance(arg_name, str): + raise ValueError("All values in 'hints.axes' must be non-empty argument names. " + "Got {} for axis '{}'.".format(type(arg_name), axis)) + if not arg_name: + raise ValueError("All values in 'hints.axes' must be non-empty argument names.") + if arg_name.isidentifier() and arg_name not in self._get_runtime_arg_names_for_hints_axes(): + raise ValueError("All values in 'hints.axes' must reference runtime non-constexpr argument names. " + "Got '{}' for axis '{}'.".format(arg_name, axis)) + return dict(hints_axes) + + def _normalize_autotuning_debug_value(self, value): + if is_dataclass(value) and not isinstance(value, type): + return self._normalize_autotuning_debug_value(asdict(value)) + if isinstance(value, dict): + return {str(key): self._normalize_autotuning_debug_value(val) for key, val in value.items()} + if isinstance(value, (list, tuple)): + return [self._normalize_autotuning_debug_value(item) for item in value] + if isinstance(value, set): + return [self._normalize_autotuning_debug_value(item) for item in sorted(value, key=repr)] + if hasattr(value, "__dict__") and not isinstance(value, type): + public_attrs = {key: val for key, val in vars(value).items() if not key.startswith("_")} + if public_attrs: + return self._normalize_autotuning_debug_value(public_attrs) + if isinstance(value, (str, int, float, bool)) or value is None: + return value + return repr(value) + + def _print_autotuning_parse_result(self, label: str, result) -> None: + if not self.print_autotuning: + return + normalized_result = self._normalize_autotuning_debug_value(result) + print("{}: {}".format( + label, + pprint.pformat(normalized_result, sort_dicts=True), + )) + + def _collect_vector_parse_debug_result( + self, + required_missing_params: List[str], + missing_tunable_params: List[str], + ) -> Dict[str, object]: + vector_axes = getattr(self, "vector_axes", None) + if self.vv_parse_result_v2 is not None: + return {"vector_axes": vector_axes} + + debug_result = {"vector_axes": vector_axes} + fixed_split_params = dict(getattr(self, "fixed_split_params", {}) or {}) + if fixed_split_params: + debug_result["fixed_split_params"] = fixed_split_params + if missing_tunable_params: + debug_result["missing_tunable_params"] = list(missing_tunable_params) + if required_missing_params: + debug_result["required_missing_params"] = list(required_missing_params) + num_buffers = getattr(self, "num_buffers", -1) + if num_buffers != -1: + debug_result["num_buffers"] = num_buffers + if getattr(self, "persistent_reduction", False): + debug_result["persistent_reduction"] = True + if getattr(self, "dual_reduction", False): + debug_result["dual_reduction"] = True + return debug_result + + def _get_runtime_arg_names_for_hints_axes(self): + constexpr_names = set() + if hasattr(self, "_get_constexpr_candidates"): + try: + constexpr_names = set(self._get_constexpr_candidates()) + except Exception: + constexpr_names = set() + return [arg_name for arg_name in getattr(self, "arg_names", []) if arg_name not in constexpr_names] + + def _rebuild_vector_axes(self) -> VectorAxes: + vector_axes = VectorAxes.from_hints_axes(self.hints_axes) + + existing_vector_axes = getattr(self, "vector_axes", None) + if existing_vector_axes is not None: + vector_axes.apply_semantic_fields( + axis_length_exprs=self._normalize_axis_name_mapping( + getattr(existing_vector_axes, "axis_length_exprs", {}), ), + fixed_tiling_exprs=self._normalize_axis_name_mapping( + getattr(existing_vector_axes, "fixed_tiling_exprs", {}), ), + axis_dynamic_sources=self._normalize_axis_name_mapping( + getattr(existing_vector_axes, "axis_dynamic_sources", {}), ), + ) + + vector_axes.apply_semantic_fields( + split_params=self.split_params, + tiling_params=self.tiling_params, + low_dim_axes=self.low_dim_axes, + reduction_axes=self.reduction_axes, + axis_pid_dims=self.axis_pid_dims, + ) + return vector_axes + + def _refresh_vector_axes(self) -> None: + self.vector_axes = self._rebuild_vector_axes() + self.axis_arg_names = self._get_parser_axis_arg_names() + + def _reset_vector_parse_derived_state(self) -> None: + self.fixed_split_params = {} + self.fixed_grid_dims = set() + self.fixed_grid_dim_values = {} + self.split_axis_pid_dims = {} + self.axis_pid_dims = {} + self.dual_reduction = False + self.persistent_reduction = False + + def _get_parser_axis_arg_names(self) -> Dict[str, str]: + axis_arg_names = {} + if self.vector_axes is not None: + axis_arg_names.update({ + axis: arg_name + for axis, arg_name in self.vector_axes.axis_length_exprs.items() + if self._is_direct_runtime_length_arg_name(arg_name) + }) + + return axis_arg_names + + def _promote_axis_arg_name_to_reduction(self, axis): + axis = self._get_axis_base_name(axis) + if not isinstance(axis, str) or not axis: + return + if self.vector_axes is None: + self.vector_axes = self._rebuild_vector_axes() + axis_arg_name = self.vector_axes.axis_length_exprs.get(axis, None) + self.vector_axes.ensure_axis(axis) + if axis_arg_name is not None: + self.vector_axes.ensure_axis(axis).length_expr = axis_arg_name + self.vector_axes.apply_semantic_fields(reduction_axes=[axis]) + self.axis_arg_names = self._get_parser_axis_arg_names() + + def _init_axis_params(self, key, split_params, tiling_params, low_dim_axes, reduction_axes, hints_axes=None): + if not isinstance(key, Sequence) or isinstance(key, (str, bytes, dict)): + raise ValueError("key must be a list, got: {}".format(type(key))) + self.keys = list(key) + self.hints_axes = self._parse_hints_axes(hints_axes) + + axis_related_params = [split_params, tiling_params, low_dim_axes, reduction_axes] + if any(item is not None for item in axis_related_params): + if not self.hints_axes: + raise ValueError("hints.axes must be provided when axis metadata " + "(split_params, tiling_params, low_dim_axes, reduction_axes) is supplied.") + if not isinstance(split_params, dict): + raise ValueError("split_params must be a dict, got: {}".format(type(split_params))) + if not isinstance(tiling_params, dict): + raise ValueError("tiling_params must be a dict, got: {}".format(type(tiling_params))) + if not isinstance(low_dim_axes, list): + raise ValueError("low_dim_axes must be a list, got: {}".format(type(low_dim_axes))) + if not isinstance(reduction_axes, list): + raise ValueError("reduction_axes must be a list, got: {}".format(type(reduction_axes))) + used_axes = set(split_params.keys()).union( + tiling_params.keys(), + low_dim_axes, + reduction_axes, + ) + if not used_axes.issubset(self.hints_axes.keys()): + raise ValueError( + "The following axes are used in axis metadata but missing from 'hints.axes': {}".format( + used_axes - set(self.hints_axes.keys()))) + elif not self.hints_axes: + defer_legacy_fallback = (getattr(self, "enable_vv_parser_v2", False) + and getattr(self, "vv_parser_v2_mode", "off") in ("assist", "authoritative")) + if not defer_legacy_fallback: + # Compatibility fallback for the legacy vector path: infer x/y/z... + # from the ordered key list when no explicit axis metadata is supplied. + self.hints_axes = self._infer_hints_axes_from_key() + + self.split_params = split_params + self.all_split_params = {} + self.fixed_split_params = {} + self.tiling_params = tiling_params + self.low_dim_axes = low_dim_axes + self.reduction_axes = reduction_axes + self.fixed_grid_dims = set() + self.fixed_grid_dim_values = {} + self.split_axis_pid_dims = {} + self.axis_pid_dims = {} + self.dual_reduction = False + self.persistent_reduction = False + self.num_buffers = -1 + self.parser_mode = None + self.missing_tunable_params = [] + self.cv_parse_result = None + self.vv_parse_result_v2 = None + self.vv_adapter_result_v2 = None + self.vv_gap_fill_report_v2 = None + self.vector_axes = self._rebuild_vector_axes() + self.axis_arg_names = self._get_parser_axis_arg_names() + + def _autoparse_axis_params(self, all_args): + if self.parser_mode is None: + self.parser_mode = self._resolve_kernel_type() + + if self.parser_mode in ("cube", "mix"): + result = self._autoparse_axis_params_cube_mix(all_args) + self.cv_parse_result = result + return result + + return self._autoparse_axis_params_vector(all_args) + + @staticmethod + def _find_function_by_name( + tree: ast.AST, + func_name: Optional[str], + ) -> Optional[ast.AST]: + if not func_name: + return None + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == func_name: + return node + return None + + @staticmethod + def _find_first_function(tree: ast.AST) -> Optional[ast.AST]: + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + return node + return None + + @staticmethod + def _collect_direct_name_callees(function_node: ast.AST) -> List[str]: + callee_names = set() + for node in ast.walk(function_node): + if not isinstance(node, ast.Call): + continue + if not isinstance(node.func, ast.Name): + continue + callee_names.add(node.func.id) + return sorted(callee_names) + + def _has_direct_importable_callee(self, function_node: ast.AST) -> bool: + globals_dict = getattr(self.base_fn, "__globals__", {}) + for callee_name in self._collect_direct_name_callees(function_node): + callee_obj = globals_dict.get(callee_name, None) + if isinstance(callee_obj, triton.runtime.JITFunction): + return True + if inspect.isfunction(callee_obj): + return True + return False + + def _should_expand_vv_ast_context( + self, + parsed_ast: ast.AST, + entry_function_name: Optional[str], + ) -> bool: + entry_function = self._find_function_by_name(parsed_ast, entry_function_name) + if entry_function is None: + if isinstance(parsed_ast, (ast.FunctionDef, ast.AsyncFunctionDef)): + entry_function = parsed_ast + elif isinstance(parsed_ast, ast.Module): + entry_function = self._find_first_function(parsed_ast) + + if not isinstance(entry_function, (ast.FunctionDef, ast.AsyncFunctionDef)): + return False + return self._has_direct_importable_callee(entry_function) + + def _augment_module_ast_with_imported_callees( + self, + module_ast: ast.Module, + entry_function_name: Optional[str], + ) -> ast.Module: + if not entry_function_name: + return module_ast + + entry_function = self._find_function_by_name(module_ast, entry_function_name) + if not isinstance(entry_function, (ast.FunctionDef, ast.AsyncFunctionDef)): + return module_ast + + known_function_names = { + node.name + for node in ast.walk(module_ast) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + } + + globals_dict = getattr(self.base_fn, "__globals__", {}) + appended = 0 + for callee_name in self._collect_direct_name_callees(entry_function): + if callee_name in known_function_names: + continue + + callee_obj = globals_dict.get(callee_name, None) + if isinstance(callee_obj, triton.runtime.JITFunction): + callee_obj = callee_obj.fn + if not inspect.isfunction(callee_obj): + continue + + source_file = None + try: + source_file = inspect.getsourcefile(callee_obj) or inspect.getfile(callee_obj) + except Exception: + source_file = None + if not source_file or not os.path.isfile(source_file): + continue + + try: + with open(source_file, "r", encoding="utf-8") as source_fp: + source = source_fp.read() + callee_module_ast = ast.parse(source, filename=source_file) + except Exception: + continue + + callee_def = self._find_function_by_name(callee_module_ast, getattr(callee_obj, "__name__", None)) + if not isinstance(callee_def, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if callee_def.name in known_function_names: + continue + + module_ast.body.append(callee_def) + known_function_names.add(callee_def.name) + appended += 1 + + return module_ast + + def _load_source_module_ast(self) -> Optional[ast.Module]: + if self._source_module_ast_resolved: + return self._source_module_ast_cache + self._source_module_ast_resolved = True + + source_file = None + try: + source_file = inspect.getsourcefile(self.base_fn) or inspect.getfile(self.base_fn) + except Exception: + source_file = None + + if not source_file or not os.path.isfile(source_file): + return None + + try: + with open(source_file, "r", encoding="utf-8") as source_fp: + source = source_fp.read() + module_ast = ast.parse(source, filename=source_file) + except Exception: + return None + + self._source_module_ast_cache = module_ast + return self._source_module_ast_cache + + def _build_cv_parse_ast_context( + self, + parsed_ast: ast.AST, + entry_function_name: Optional[str], + ) -> Tuple[ast.AST, Optional[ast.Module], Optional[str], str]: + module_ast = self._load_source_module_ast() + if isinstance(module_ast, ast.Module): + module_ast = self._augment_module_ast_with_imported_callees( + module_ast, + entry_function_name, + ) + entry_from_source = self._find_function_by_name(module_ast, entry_function_name) + if entry_from_source is not None: + return entry_from_source, module_ast, entry_function_name, "source_file" + + if isinstance(parsed_ast, (ast.FunctionDef, ast.AsyncFunctionDef)): + module_ast = ast.Module(body=[parsed_ast], type_ignores=[]) + if not entry_function_name: + entry_function_name = parsed_ast.name + return parsed_ast, module_ast, entry_function_name, "jit_parse_function" + + if isinstance(parsed_ast, ast.Module): + entry_from_parse = self._find_function_by_name(parsed_ast, entry_function_name) + if entry_from_parse is None: + entry_from_parse = self._find_first_function(parsed_ast) + if entry_from_parse is not None and not entry_function_name: + entry_function_name = getattr(entry_from_parse, "name", None) + if entry_from_parse is not None: + return entry_from_parse, parsed_ast, entry_function_name, "jit_parse_module" + return parsed_ast, parsed_ast, entry_function_name, "jit_parse_module_no_entry" + + return parsed_ast, None, entry_function_name, "jit_parse_unknown" + + def _is_auto_kernel_hint(self) -> bool: + kernel_type_hint = self.hints.get("kernel_type", "auto") + if isinstance(kernel_type_hint, str): + kernel_type_hint = kernel_type_hint.strip().lower() + return kernel_type_hint == "auto" + + def _resolve_kernel_type(self) -> str: + parsed_ast = None + try: + parsed_ast = self.fn.parse() + except Exception: + parsed_ast = None + + kernel_type = resolve_kernel_type(self.hints, parsed_ast) + if kernel_type != "vector" or not self._is_auto_kernel_hint() or parsed_ast is None: + return kernel_type + + entry_function_name = getattr(self.base_fn, "__name__", None) or getattr(self.fn, "__name__", None) + entry_ast, module_ast, entry_function_name, ast_context = self._build_cv_parse_ast_context( + parsed_ast, + entry_function_name, + ) + if not isinstance(module_ast, ast.Module) or not entry_function_name: + return kernel_type + + try: + dot_result = analyze_dot_site_mnk( + func_ast=entry_ast, + parser_mode="mix", + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + except Exception: + return kernel_type + + if dot_result.dot_sites: + return "mix" + return kernel_type + + def _autoparse_axis_params_cube_mix(self, all_args): + parsed_ast = self.fn.parse() + entry_function_name = getattr(self.base_fn, "__name__", None) or getattr(self.fn, "__name__", None) + func_ast, module_ast, entry_function_name, ast_context = self._build_cv_parse_ast_context( + parsed_ast, + entry_function_name, + ) + cv_parse_result = parse_cv_params( + func_ast=func_ast, + parser_mode=self.parser_mode or "mix", + arg_names=self.arg_names, + provided_args=all_args, + explicit_tunable_params=self.explicit_tunable_params, + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + self.cv_parse_result = cv_parse_result + self.missing_tunable_params = list(cv_parse_result.missing_tunable_params) + self._print_autotuning_parse_result( + "Ascend autotuning cv parse result", + cv_parse_result, + ) + if cv_parse_result.status != "ok": + diagnostics = "; ".join(cv_parse_result.diagnostics).strip() + if not diagnostics: + diagnostics = "no diagnostics" + raise RuntimeError("cv parser status {}: {}".format(cv_parse_result.status, diagnostics)) + return cv_parse_result + + def _autoparse_axis_params_vector(self, all_args): + self.vv_gap_fill_report_v2 = None + self._reset_vector_parse_derived_state() + # Normalize vector axis containers for vv-assist flow. In list-key mode + # these fields are initialized as None, and vv may legitimately fill only + # split/tiling/low_dim without reduction axes. + if self.split_params is None: + self.split_params = {} + if self.tiling_params is None: + self.tiling_params = {} + if self.low_dim_axes is None: + self.low_dim_axes = [] + if self.reduction_axes is None: + self.reduction_axes = [] + self._refresh_vector_axes() + + required_missing_params = [arg for arg in self.arg_names if arg not in all_args.keys()] + vv_injected_split_axes = set() + vv_injected_tiling_axes = set() + existing_split_axes = set(self.split_params.keys()) + existing_tiling_axes = set(self.tiling_params.keys()) + vv_result = self._autoparse_axis_info_v2_for_vector(all_args) + self._apply_assist_legacy_hints_axes_fallback(all_args, vv_result) + use_vv_semantics = False + fallback_to_vector_all = self._should_fallback_to_vector_from_v2(vv_result) + if not fallback_to_vector_all and self._is_vv_semantic_usable(vv_result, required_missing_params): + use_vv_semantics = self._apply_vv_axis_semantic_result() + if use_vv_semantics: + vv_injected_split_axes = set(self.split_params.keys()) - existing_split_axes + vv_injected_tiling_axes = set(self.tiling_params.keys()) - existing_tiling_axes + + missing_tunable_params = self._detect_missing_tunable_params( + all_args, + required_missing_params, + ) + self._filter_vv_semantic_tunable_params_by_cv_policy( + required_missing_params=required_missing_params, + vv_injected_split_axes=vv_injected_split_axes, + vv_injected_tiling_axes=vv_injected_tiling_axes, + ) + self.missing_tunable_params = list(missing_tunable_params) + vector_gap_fill_applied = [] + self._prepare_vv_gap_fill_report( + required_missing_params=required_missing_params, + missing_tunable_params=missing_tunable_params, + ) + # parse pointer params nums + if self.num_buffers == -1: + self.num_buffers = self._autoparse_ptr_nums(all_args) + + # parse autotiling axes + # reduction axis must be parsed before other axes. it will alter the key + if not self.reduction_axes: + if self._should_skip_vv_vector_gap_fill(): + self.reduction_axes = [] + else: + vector_gap_fill_applied.append("reduction_axes") + self.reduction_axes = self._parse_reduction_axes_with_fallback() + if len(self.reduction_axes) >= 2: + self.dual_reduction = True + + if not self.low_dim_axes: + if self._should_skip_vv_vector_gap_fill(): + self.low_dim_axes = [] + else: + vector_gap_fill_applied.append("low_dim_axes") + self.low_dim_axes = self._parse_low_dim_axes_with_fallback() + + if len(self.reduction_axes) == 1: + reduction_axis = self.reduction_axes[0] + reduction_param = self._resolve_axis_length_arg_name(reduction_axis) + reduction_numel = all_args.get(reduction_param, float("inf")) + persistent_threshold = self._get_persistent_reduction_threshold(reduction_axis) + if reduction_numel <= persistent_threshold: + self.persistent_reduction = True + + if not self.split_params: + if self._should_skip_vv_vector_gap_fill(): + self.all_split_params = {} + else: + vector_gap_fill_applied.append("split_params") + all_split_params = self._parse_split_params_with_fallback(self._get_constexpr_candidates()) + self.all_split_params = dict(all_split_params) + self.split_params = { + axis: param + for axis, param in all_split_params.items() + if param in missing_tunable_params + } + elif not self.axis_pid_dims: + # When split axes are provided by hints, parse axis->program_id mapping + # independently for fixed-grid semantics and diagnostics. + if not self._should_skip_vv_vector_gap_fill(): + vector_gap_fill_applied.append("axis_pid_dims") + self._parse_axis_pid_dims_with_fallback() + self._apply_fixed_grid_semantics(all_args) + missing_tunable_params = [arg for arg in missing_tunable_params if arg not in self.split_params.values()] + if not self.tiling_params: + if self._should_skip_vv_vector_gap_fill(): + self.tiling_params = {} + else: + vector_gap_fill_applied.append("tiling_params") + self.tiling_params = self._parse_tiling_params_with_fallback(missing_tunable_params) + auto_fallback_report = self._fallback_unresolved_tunable_params_to_split_tiling(missing_tunable_params) + if self.vv_gap_fill_report_v2 is not None: + self.vv_gap_fill_report_v2["auto_fallback_split_tiling"] = auto_fallback_report + missing_tunable_params = [arg for arg in missing_tunable_params if arg not in self.split_params.values()] + missing_tunable_params = [arg for arg in missing_tunable_params if arg not in self.tiling_params.values()] + required_missing_params = [arg for arg in required_missing_params if arg not in self.split_params.values()] + required_missing_params = [arg for arg in required_missing_params if arg not in self.tiling_params.values()] + required_missing_params = self._filter_hard_missing_required_params(required_missing_params) + self._finalize_vv_gap_fill_report( + vector_gap_fill_applied=vector_gap_fill_applied, + required_missing_params=required_missing_params, + missing_tunable_params=missing_tunable_params, + ) + self._refresh_vector_axes() + self._print_autotuning_parse_result( + "Ascend autotuning vector final parse result", + self._collect_vector_parse_debug_result( + required_missing_params=required_missing_params, + missing_tunable_params=missing_tunable_params, + ), + ) + if required_missing_params: + raise ValueError(f"Missing required arguments: {required_missing_params}. " + f"These arguments must be explicitly provided and cannot be automatically tuned. " + f"Please ensure that these arguments are passed when calling the function.") + + @staticmethod + def _require_base_axis_name(axis: Optional[str]) -> Optional[str]: + if not isinstance(axis, str) or not axis: + return None + if axis.startswith("r"): + raise ValueError("r-prefixed axis names are not supported; " + f"use the base axis name instead of '{axis}'.") + if axis not in valid_axis_names: + raise ValueError(f"Unsupported axis name '{axis}'.") + return axis + + @staticmethod + def _normalize_reduction_axis_name(axis: Optional[str]) -> Optional[str]: + if not isinstance(axis, str) or not axis: + return None + if axis.startswith("r"): + raise ValueError("r-prefixed axis names are not supported; " + f"use the base axis name instead of '{axis}'.") + if axis not in valid_axis_names: + raise ValueError(f"Unsupported axis name '{axis}'.") + return axis + + def _normalize_reduction_axes(self, reduction_axes: List[str]) -> List[str]: + normalized = [] + for axis in reduction_axes: + canonical_axis = self._normalize_reduction_axis_name(axis) + if canonical_axis is not None: + normalized.append(canonical_axis) + return normalized + + def _normalize_vv_reduction_axes(self, reduction_axes: List[str]) -> List[str]: + return self._normalize_reduction_axes(reduction_axes) + + def _normalize_axis_name_mapping( + self, + axis_mapping: Optional[Dict[str, Any]], + ) -> Dict[str, Any]: + normalized = {} + for axis, value in dict(axis_mapping or {}).items(): + base_axis = self._get_axis_base_name(axis) + if not isinstance(base_axis, str) or not base_axis: + continue + normalized.setdefault(base_axis, value) + return normalized + + def _normalize_axis_name_list( + self, + axis_names: Optional[List[str]], + ) -> List[str]: + normalized = [] + for axis in list(axis_names or []): + base_axis = self._get_axis_base_name(axis) + if not isinstance(base_axis, str) or not base_axis: + continue + if base_axis not in normalized: + normalized.append(base_axis) + return normalized + + @staticmethod + def _get_axis_base_name(axis: Optional[str]) -> Optional[str]: + if not isinstance(axis, str) or not axis: + return None + if axis.startswith("r"): + raise ValueError("r-prefixed axis names are not supported; " + f"use the base axis name instead of '{axis}'.") + if axis not in valid_axis_names: + raise ValueError(f"Unsupported axis name '{axis}'.") + return axis + + def _apply_vv_axis_semantic_result(self) -> bool: + + def resolve_base_axis(axis_name): + resolver = getattr(self, "_get_axis_base_name", None) + if callable(resolver): + return resolver(axis_name) + if not isinstance(axis_name, str) or not axis_name: + return None + if axis_name.startswith("r"): + raise ValueError("r-prefixed axis names are not supported; " + f"use the base axis name instead of '{axis_name}'.") + return axis_name + + def normalize_axis_name_mapping(axis_mapping): + normalizer = getattr(self, "_normalize_axis_name_mapping", None) + if callable(normalizer): + return normalizer(axis_mapping) + normalized = {} + for axis, value in dict(axis_mapping or {}).items(): + base_axis = resolve_base_axis(axis) + if not isinstance(base_axis, str) or not base_axis: + continue + normalized.setdefault(base_axis, value) + return normalized + + def normalize_axis_name_list(axis_names): + normalizer = getattr(self, "_normalize_axis_name_list", None) + if callable(normalizer): + return normalizer(axis_names) + normalized = [] + for axis in list(axis_names or []): + base_axis = resolve_base_axis(axis) + if not isinstance(base_axis, str) or not base_axis: + continue + if base_axis not in normalized: + normalized.append(base_axis) + return normalized + + if self.vv_adapter_result_v2 is None: + return False + adapter_status = getattr(self.vv_adapter_result_v2, "status", "ok") + if adapter_status == "failed": + return False + + applied = False + raw_adapter_reduction_axes = list(getattr(self.vv_adapter_result_v2, "reduction_axes", []) or []) + adapter_reduction_axes = self._normalize_vv_reduction_axes(raw_adapter_reduction_axes) + raw_adapter_low_dim_axes = list(getattr(self.vv_adapter_result_v2, "low_dim_axes", []) or []) + if adapter_reduction_axes or self.reduction_axes: + self.reduction_axes = [] + for reduction_axis in adapter_reduction_axes: + base_axis = resolve_base_axis(reduction_axis) + self._promote_axis_arg_name_to_reduction(base_axis) + self.reduction_axes = list(adapter_reduction_axes) + applied = True + + adapter_low_dim_axes = normalize_axis_name_list(raw_adapter_low_dim_axes) + if adapter_low_dim_axes or self.low_dim_axes: + self.low_dim_axes = list(adapter_low_dim_axes) + applied = True + + adapter_split_params = normalize_axis_name_mapping(getattr(self.vv_adapter_result_v2, "split_params", {}) or {}) + if adapter_split_params or self.split_params: + self.split_params = dict(adapter_split_params) + applied = True + + adapter_tiling_params = normalize_axis_name_mapping( + getattr(self.vv_adapter_result_v2, "tiling_params", {}) or {}) + if adapter_tiling_params or self.tiling_params: + self.tiling_params = dict(adapter_tiling_params) + applied = True + + adapter_axis_pid_dims = normalize_axis_name_mapping( + getattr(self.vv_adapter_result_v2, "axis_pid_dims", {}) or {}) + if adapter_axis_pid_dims or self.axis_pid_dims: + self.axis_pid_dims = dict(adapter_axis_pid_dims) + applied = True + + if len(self.reduction_axes or []) >= 2: + self.dual_reduction = True + return applied + + def _should_fallback_to_vector_from_v2(self, vv_result) -> bool: + mode = getattr(self, "vv_parser_v2_mode", "off") + if mode == "off": + return True + if mode == "observe": + return True + if vv_result is None: + return True + vv_status = getattr(vv_result, "status", "ok") + if vv_status == "failed": + return True + if mode == "assist" and not self._vv_result_has_resolved_axes(vv_result): + return True + return False + + @staticmethod + def _vv_result_has_resolved_axes(vv_result) -> bool: + if vv_result is None: + return False + semantic_fields = ( + "axis_length_exprs", + "inferred_keys", + "split_params", + "tiling_params", + "fixed_tiling_exprs", + "axis_pid_dims", + ) + for field_name in semantic_fields: + field_value = getattr(vv_result, field_name, None) + if isinstance(field_value, dict) and field_value: + return True + + for axis_info in list(getattr(vv_result, "axes", []) or []): + if getattr(axis_info, "length_expr", None): + return True + if getattr(axis_info, "split_param", None): + return True + if getattr(axis_info, "tiling_param", None): + return True + if getattr(axis_info, "fixed_tiling_expr", None): + return True + if getattr(axis_info, "tunable_param", None): + return True + + return False + + def _infer_hints_axes_from_key( + self, + all_args: Optional[Dict[str, object]] = None, + *, + int_only: bool = False, + ) -> Dict[str, str]: + inferred_hints_axes = {} + for axis, arg_name in zip(valid_axis_names, self.keys): + if not (isinstance(arg_name, str) and arg_name): + continue + if int_only: + if all_args is None or arg_name not in all_args: + continue + arg_value = all_args[arg_name] + if isinstance(arg_value, bool): + continue + if not isinstance(arg_value, int): + continue + inferred_hints_axes[axis] = arg_name + return inferred_hints_axes + + def _apply_assist_legacy_hints_axes_fallback( + self, + all_args: Dict[str, object], + vv_result, + ) -> None: + if not self.enable_vv_parser_v2: + return + if getattr(self, "vv_parser_v2_mode", "off") != "assist": + return + if self.hints_axes: + return + if self._vv_result_has_resolved_axes(vv_result): + return + legacy_hints_axes = self._infer_hints_axes_from_key(all_args, int_only=True) + if not legacy_hints_axes: + return + self.hints_axes = legacy_hints_axes + self._refresh_vector_axes() + + def _is_vv_semantic_usable( + self, + vv_result, + required_missing_params: List[str], + ) -> bool: + if vv_result is None or self.vv_adapter_result_v2 is None: + return False + + adapter_status = getattr(self.vv_adapter_result_v2, "status", "ok") + if adapter_status == "failed": + return False + + adapter_split_params = dict(getattr(self.vv_adapter_result_v2, "split_params", {}) or {}) + adapter_tiling_params = dict(getattr(self.vv_adapter_result_v2, "tiling_params", {}) or {}) + mapped_params = set(adapter_split_params.values()) | set(adapter_tiling_params.values()) + if not mapped_params: + return False + + constexpr_candidates = set(self._get_constexpr_candidates()) + missing_constexpr = set(required_missing_params) & constexpr_candidates + if not missing_constexpr: + return True + if mapped_params & missing_constexpr: + return True + + if "x" in adapter_split_params or "x" in adapter_tiling_params: + return True + return False + + def _should_skip_vv_vector_gap_fill(self) -> bool: + return (self.enable_vv_parser_v2 and (self.parser_mode or "vector") == "vector" + and getattr(self, "vv_parser_v2_mode", "off") == "authoritative") + + @staticmethod + def _is_direct_runtime_length_arg_name(expr: Optional[str]) -> bool: + return isinstance(expr, str) and expr.isidentifier() + + def _resolve_axis_length_arg_name(self, axis: str) -> Optional[str]: + axis_length_exprs = {} + if self.vector_axes is not None: + axis_length_exprs = dict(getattr(self.vector_axes, "axis_length_exprs", {}) or {}) + elif self.vv_adapter_result_v2 is not None: + axis_length_exprs = dict(getattr(self.vv_adapter_result_v2, "axis_length_exprs", {}) or {}) + axis_expr = axis_length_exprs.get(axis, None) + if self._is_direct_runtime_length_arg_name(axis_expr): + return axis_expr + parser_axis_arg_names = self._get_parser_axis_arg_names() + return parser_axis_arg_names.get(axis, None) + + def _materialize_vector_tile_inputs( + self, + kv_dict: Dict[str, object], + all_args: Optional[Dict[str, Any]] = None, + ) -> Dict[str, object]: + vector_axes = VectorAxes.from_hints_axes(self.hints_axes) + + if self.vector_axes is not None: + vector_axes.apply_semantic_fields( + split_params=self.vector_axes.split_params, + tiling_params=self.vector_axes.tiling_params, + low_dim_axes=self.vector_axes.low_dim_axes, + reduction_axes=self.vector_axes.reduction_axes, + axis_pid_dims=self.vector_axes.axis_pid_dims, + axis_length_exprs=self.vector_axes.axis_length_exprs, + fixed_tiling_exprs=self.vector_axes.fixed_tiling_exprs, + axis_dynamic_sources=self.vector_axes.axis_dynamic_sources, + ) + + if self.split_params is not None: + for axis in vector_axes.axes.values(): + axis.split_param = None + vector_axes.apply_semantic_fields(split_params=self.split_params) + if self.tiling_params is not None: + for axis in vector_axes.axes.values(): + axis.tiling_param = None + vector_axes.apply_semantic_fields(tiling_params=self.tiling_params) + if self.low_dim_axes is not None: + vector_axes._low_dim_axis_names = [] + for axis in vector_axes.axes.values(): + axis.is_low_dim = False + vector_axes.apply_semantic_fields(low_dim_axes=self.low_dim_axes) + if self.reduction_axes is not None: + vector_axes._reduction_axis_names = [] + for axis in vector_axes.axes.values(): + axis.is_reduction = False + vector_axes.apply_semantic_fields(reduction_axes=self.reduction_axes) + if self.axis_pid_dims is not None: + for axis in vector_axes.axes.values(): + axis.pid_dim = None + vector_axes.apply_semantic_fields(axis_pid_dims=self.axis_pid_dims) + + runtime_view = dict(all_args or {}) + for axis, value in dict(kv_dict or {}).items(): + if not is_valid_axis_name(axis): + continue + if not isinstance(value, int): + raise ValueError(f"Not supported dim type: {type(value)}, `int` is the only supported type") + vector_axes.ensure_axis(axis) + runtime_view[axis] = value + + axis_sizes, diagnostics = vector_axes.materialize_axis_sizes(runtime_view) + self.vector_axes = vector_axes + return { + "axis_sizes": axis_sizes, + "split_params": vector_axes.split_params, + "tiling_params": vector_axes.tiling_params, + "low_dim_axes": vector_axes.low_dim_axes, + "reduction_axes": vector_axes.reduction_axes, + "axis_pid_dims": vector_axes.axis_pid_dims, + "diagnostics": diagnostics, + } + + def _apply_fixed_grid_semantics( + self, + all_args: Dict[str, object], + ) -> None: + self.fixed_split_params = {} + self.fixed_grid_dim_values = self._get_fixed_grid_dim_values( + all_args.get("grid", None), + all_args, + ) + self.fixed_grid_dims = set(self.fixed_grid_dim_values.keys()) + if not self.fixed_grid_dims or not self.axis_pid_dims: + return + + fixed_grid_axes = {axis for axis, pid_dim in self.axis_pid_dims.items() if pid_dim in self.fixed_grid_dims} + if not fixed_grid_axes: + return + + for axis, pid_dim in self.axis_pid_dims.items(): + if pid_dim not in self.fixed_grid_dims: + continue + core_num = self.fixed_grid_dim_values.get(pid_dim, 0) + axis_len_name = self._resolve_axis_length_arg_name(axis) + axis_len = all_args.get(axis_len_name, None) + if not isinstance(core_num, int) or core_num <= 0: + continue + if not isinstance(axis_len, int) or axis_len <= 0: + continue + self.fixed_split_params[axis] = (axis_len + core_num - 1) // core_num + + def _collect_vv_vector_gap_fill_needed(self) -> List[str]: + needed = [] + if not self.reduction_axes: + needed.append("reduction_axes") + if not self.low_dim_axes: + needed.append("low_dim_axes") + if not self.split_params: + needed.append("split_params") + if not self.tiling_params: + needed.append("tiling_params") + if self.split_params and not self.axis_pid_dims: + needed.append("axis_pid_dims") + return needed + + def _prepare_vv_gap_fill_report( + self, + required_missing_params: List[str], + missing_tunable_params: List[str], + ) -> None: + if not self.enable_vv_parser_v2 or (self.parser_mode or "vector") != "vector": + return + if getattr(self, "vv_parser_v2_mode", "off") not in ("observe", "authoritative"): + return + self.vv_gap_fill_report_v2 = { + "mode": + getattr(self, "vv_parser_v2_mode", "off"), + "vv_status": + (None if self.vv_parse_result_v2 is None else getattr(self.vv_parse_result_v2, "status", None)), + "adapter_status": + (None if self.vv_adapter_result_v2 is None else getattr(self.vv_adapter_result_v2, "status", None)), + "required_missing_params_before_vector": + list(required_missing_params), + "missing_tunable_params_before_vector": + list(missing_tunable_params), + "vector_gap_fill_needed": + self._collect_vv_vector_gap_fill_needed(), + "vector_gap_fill_applied": [], + "vv_diagnostics": ([] if self.vv_parse_result_v2 is None else list( + getattr(self.vv_parse_result_v2, "diagnostics", []) or [])), + "adapter_diagnostics": ([] if self.vv_adapter_result_v2 is None else list( + getattr(self.vv_adapter_result_v2, "diagnostics", []) or [])), + } + + def _finalize_vv_gap_fill_report( + self, + vector_gap_fill_applied: List[str], + required_missing_params: List[str], + missing_tunable_params: List[str], + ) -> None: + if self.vv_gap_fill_report_v2 is None: + return + self.vv_gap_fill_report_v2.update({ + "vector_gap_fill_applied": list(vector_gap_fill_applied), + "required_missing_params_final": list(required_missing_params), + "missing_tunable_params_final": list(missing_tunable_params), + "final_split_params": dict(self.split_params or {}), + "final_tiling_params": dict(self.tiling_params or {}), + "final_low_dim_axes": list(self.low_dim_axes or []), + "final_reduction_axes": list(self.reduction_axes or []), + "final_axis_pid_dims": dict(self.axis_pid_dims or {}), + }) + + def _run_vector_parser_with_fallback( + self, + parser_name: str, + parser_fn, + fallback_value, + ): + try: + return parser_fn() + except Exception: + if self.print_autotuning: + print("[WARNING] Failed to parse {}, fallback to {}.".format(parser_name, fallback_value)) + return fallback_value + + def _parse_reduction_axes_with_fallback(self) -> List[str]: + result = self._run_vector_parser_with_fallback( + "reduction_axes", + self._autoparse_reduction_axes, + [], + ) + return list(result or []) + + def _parse_low_dim_axes_with_fallback(self) -> List[str]: + result = self._run_vector_parser_with_fallback( + "low_dim_axes", + self._autoparse_low_dim_axes, + [], + ) + return list(result or []) + + def _parse_split_params_with_fallback( + self, + candidates_params: List[str], + ) -> Dict[str, str]: + result = self._run_vector_parser_with_fallback( + "split_params", + lambda: self._autoparse_split_params(candidates_params), + {}, + ) + return dict(result or {}) + + def _parse_tiling_params_with_fallback( + self, + candidates_params: List[str], + ) -> Dict[str, str]: + result = self._run_vector_parser_with_fallback( + "tiling_params", + lambda: self._autoparse_tiling_params(candidates_params), + {}, + ) + return dict(result or {}) + + def _parse_axis_pid_dims_with_fallback(self) -> Dict[str, int]: + result = self._run_vector_parser_with_fallback( + "axis_pid_dims", + self._autoparse_axis_pid_dims, + {}, + ) + if not result: + self.axis_pid_dims = {} + self.split_axis_pid_dims = {} + return {} + return dict(result) + + def _get_defaulted_argument_names(self) -> List[str]: + func_ast = self.fn.parse() + func_node = self._find_first_function(func_ast) + if not isinstance(func_node, (ast.FunctionDef, ast.AsyncFunctionDef)): + return [] + if not isinstance(func_node.args, ast.arguments): + return [] + + defaulted = set() + + positional_args = list(func_node.args.args) + positional_defaults = list(func_node.args.defaults) + if positional_defaults: + default_count = len(positional_defaults) + for arg in positional_args[-default_count:]: + if isinstance(arg, ast.arg): + defaulted.add(arg.arg) + + for kw_arg, kw_default in zip( + func_node.args.kwonlyargs, + func_node.args.kw_defaults, + ): + if kw_default is None: + continue + if isinstance(kw_arg, ast.arg): + defaulted.add(kw_arg.arg) + return list(defaulted) + + def _filter_hard_missing_required_params( + self, + required_missing_params: List[str], + ) -> List[str]: + if not required_missing_params: + return [] + defaulted_args = set(self._get_defaulted_argument_names()) + return [arg_name for arg_name in required_missing_params if arg_name not in defaulted_args] + + def _resolve_vv_tunable_candidates_by_cv_policy( + self, + required_missing_params: List[str], + ) -> Tuple[set, set, set, set]: + constexpr_candidates = set(self._get_constexpr_candidates()) + defaulted_args = set(self._get_defaulted_argument_names()) + explicit_tunable_params = set(self.explicit_tunable_params or []) + candidate_tunable_params = set() + + for arg_name in required_missing_params: + if arg_name not in constexpr_candidates: + continue + if arg_name in defaulted_args and arg_name not in explicit_tunable_params: + continue + candidate_tunable_params.add(arg_name) + + return ( + candidate_tunable_params, + constexpr_candidates, + defaulted_args, + explicit_tunable_params, + ) + + def _filter_vv_semantic_tunable_params_by_cv_policy( + self, + required_missing_params: List[str], + vv_injected_split_axes: set, + vv_injected_tiling_axes: set, + ) -> None: + if not vv_injected_split_axes and not vv_injected_tiling_axes: + return + + ( + candidate_tunable_params, + constexpr_candidates, + defaulted_args, + explicit_tunable_params, + ) = self._resolve_vv_tunable_candidates_by_cv_policy(required_missing_params) + + removed_split_params = {} + for axis in list(vv_injected_split_axes): + param = self.split_params.get(axis, None) + if not isinstance(param, str): + continue + if param in candidate_tunable_params: + continue + removed_split_params[axis] = param + self.split_params.pop(axis, None) + + removed_tiling_params = {} + for axis in list(vv_injected_tiling_axes): + param = self.tiling_params.get(axis, None) + if not isinstance(param, str): + continue + if param in candidate_tunable_params: + continue + removed_tiling_params[axis] = param + self.tiling_params.pop(axis, None) + + @staticmethod + def _axis_name_affinity_score(param_name: str, axis_name: str) -> int: + if not isinstance(param_name, str) or not isinstance(axis_name, str): + return 0 + param_upper = param_name.upper() + if not param_upper: + return 0 + + axis_token = axis_name.upper() + if not axis_token: + return 0 + + if param_upper.startswith(axis_token): + return 100 + if "_{}_".format(axis_token) in "_{}_".format(param_upper): + return 80 + return 0 + + @staticmethod + def _param_anchor_affinity_score(param_name: str, anchor_name: Optional[str]) -> int: + if not isinstance(param_name, str) or not isinstance(anchor_name, str): + return 0 + if not param_name or not anchor_name: + return 0 + param_upper = param_name.upper() + anchor_upper = anchor_name.upper() + if param_upper == anchor_upper: + return 100 + if param_upper.startswith(anchor_upper) or anchor_upper.startswith(param_upper): + return 90 + if (param_upper.replace("_SUB", "") == anchor_upper or anchor_upper.replace("_SUB", "") == param_upper): + return 88 + common_prefix = 0 + for lhs, rhs in zip(param_upper, anchor_upper): + if lhs != rhs: + break + common_prefix += 1 + if common_prefix <= 0: + return 0 + return int((common_prefix * 100) / max(len(param_upper), len(anchor_upper))) + + def _fallback_unresolved_tunable_params_to_split_tiling( + self, + missing_tunable_params: List[str], + ) -> Dict[str, object]: + report = { + "mode": getattr(self, "vv_parser_v2_mode", "off"), + "initial_unresolved_params": [], + "assigned_tiling_params": {}, + "assigned_split_params": {}, + "remaining_unresolved_params": [], + "decisions": [], + "applied": False, + } + if not (self.enable_vv_parser_v2 and (self.parser_mode or "vector") == "vector" + and getattr(self, "vv_parser_v2_mode", "off") in ("assist", "authoritative")): + return report + + consumed_params = set(self.split_params.values()) | set(self.tiling_params.values()) + unresolved = [] + seen_unresolved = set() + for param in missing_tunable_params: + if param in consumed_params or param in seen_unresolved: + continue + unresolved.append(param) + seen_unresolved.add(param) + if not unresolved: + return report + report["initial_unresolved_params"] = list(unresolved) + + ordered_axes = [ + axis for axis in self.axis_arg_names.keys() if isinstance(axis, str) and is_valid_axis_name(axis) + ] + if not ordered_axes: + report["remaining_unresolved_params"] = list(unresolved) + return report + axis_order = {axis: idx for idx, axis in enumerate(ordered_axes)} + + empty_axes = [axis for axis in ordered_axes if axis not in self.split_params and axis not in self.tiling_params] + split_only_axes = [ + axis for axis in ordered_axes if axis in self.split_params and axis not in self.tiling_params + ] + tiling_only_axes = [ + axis for axis in ordered_axes if axis in self.tiling_params and axis not in self.split_params + ] + + def _pick_best_axis(param_name: str, candidate_axes: List[str], anchor_map: Dict[str, str]): + best_axis = None + best_score = None + for axis in candidate_axes: + axis_name_score = self._axis_name_affinity_score(param_name, axis) + anchor_score = self._param_anchor_affinity_score( + param_name, + anchor_map.get(axis, None), + ) + order_score = -axis_order.get(axis, 0) + score = ( + axis_name_score, + anchor_score, + order_score, + ) + if best_score is None or score > best_score: + best_axis = axis + best_score = score + return best_axis, best_score + + phase_specs = [ + { + "phase": "empty_axis_to_tiling", + "role": "tiling", + "candidate_axes": empty_axes, + "anchor_map": {}, + }, + { + "phase": "split_only_axis_to_tiling", + "role": "tiling", + "candidate_axes": split_only_axes, + "anchor_map": dict(self.split_params), + }, + { + "phase": "tiling_only_axis_to_split", + "role": "split", + "candidate_axes": tiling_only_axes, + "anchor_map": dict(self.tiling_params), + }, + ] + + pending = list(unresolved) + for phase_spec in phase_specs: + if not pending: + break + candidate_axes = phase_spec["candidate_axes"] + if not candidate_axes: + continue + next_pending = [] + for param_name in pending: + if not candidate_axes: + next_pending.append(param_name) + continue + axis, score = _pick_best_axis( + param_name, + candidate_axes, + phase_spec["anchor_map"], + ) + if axis is None: + next_pending.append(param_name) + continue + if phase_spec["role"] == "tiling": + if axis in self.tiling_params: + next_pending.append(param_name) + continue + self.tiling_params[axis] = param_name + report["assigned_tiling_params"][axis] = param_name + else: + if axis in self.split_params: + next_pending.append(param_name) + continue + self.split_params[axis] = param_name + report["assigned_split_params"][axis] = param_name + candidate_axes.remove(axis) + report["decisions"].append({ + "phase": phase_spec["phase"], + "axis": axis, + "param": param_name, + "score": list(score) if score is not None else [], + }) + pending = next_pending + + report["remaining_unresolved_params"] = list(pending) + report["applied"] = bool(report["assigned_tiling_params"] or report["assigned_split_params"]) + return report + + def _detect_missing_tunable_params(self, all_args, required_missing_params: List[str]) -> List[str]: + try: + func_ast = self.fn.parse() + dsl_result = analyze_signature_and_missing_tunable( + func_ast, + arg_names=self.arg_names, + provided_args=all_args, + split_params=self.split_params or {}, + tiling_params=self.tiling_params or {}, + explicit_tunable_params=self.explicit_tunable_params, + parser_mode=self.parser_mode or "vector", + ) + return list(dsl_result.missing_tunable) + except ValueError: + raise + except Exception: + constexpr_names = set(self._get_constexpr_candidates()) + fallback = [arg_name for arg_name in required_missing_params if arg_name in constexpr_names] + return fallback + + def _autoparse_axis_info_v2_for_vector(self, all_args=None): + if not self.enable_vv_parser_v2: + return None + if (self.parser_mode or "vector") != "vector": + return None + try: + parsed_ast = self.fn.parse() + entry_function_name = getattr(self.base_fn, "__name__", None) or getattr(self.fn, "__name__", None) + func_ast = parsed_ast + module_ast = None + ast_context = "jit_parse_direct" + + if self._should_expand_vv_ast_context(parsed_ast, entry_function_name): + ( + func_ast, + module_ast, + entry_function_name, + ast_context, + ) = self._build_cv_parse_ast_context( + parsed_ast, + entry_function_name, + ) + + result = parse_vv_axis_info_v2( + func_ast, + provided_args=dict(all_args or {}), + hints=self.hints, + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + self.vv_parse_result_v2 = result + self.vv_adapter_result_v2 = adapt_vv_v2_to_vector_inputs( + key_axis_to_param=self.axis_arg_names, + vv_parse_result=result, + ) + self.vector_axes = VectorAxes.from_hints_axes(self.hints_axes) + if self.vv_adapter_result_v2 is not None: + self.vector_axes.apply_semantic_fields( + split_params=getattr(self.vv_adapter_result_v2, "split_params", None), + tiling_params=getattr(self.vv_adapter_result_v2, "tiling_params", None), + low_dim_axes=getattr(self.vv_adapter_result_v2, "low_dim_axes", None), + reduction_axes=getattr(self.vv_adapter_result_v2, "reduction_axes", None), + axis_pid_dims=getattr(self.vv_adapter_result_v2, "axis_pid_dims", None), + axis_length_exprs=getattr(self.vv_adapter_result_v2, "axis_length_exprs", None), + fixed_tiling_exprs=getattr(self.vv_adapter_result_v2, "fixed_tiling_exprs", None), + axis_dynamic_sources=getattr(self.vv_adapter_result_v2, "axis_dynamic_sources", None), + ) + return result + except Exception as exc: + if self.print_autotuning: + print("Ascend autotuning vector v2 parse failed: {}: {}".format( + type(exc).__name__, + exc, + )) + self.vv_parse_result_v2 = None + self.vv_adapter_result_v2 = None + return None + + def _gen_tile_configs(self, kv_dict: Dict[str, int], dtype: torch.dtype, + all_args: Dict[str, Any] = None) -> List[Config]: + # Use CVTileGenerator for cube/mix parser modes. + if self.parser_mode in ("cube", "mix") and self.cv_parse_result is not None: + return self._gen_tile_configs_cv(kv_dict, dtype, all_args) + + # Use the legacy TileGenerator for vector mode. + from .tile_generator import KernelMeta, TileGenerator + + vector_tile_inputs = self._materialize_vector_tile_inputs(kv_dict, all_args) + if self.print_autotuning: + print("Ascend autotuning vector tile inputs: " + f"axis_sizes={vector_tile_inputs['axis_sizes']}, " + f"split_params={vector_tile_inputs['split_params']}, " + f"fixed_split_params={self.fixed_split_params}, " + f"tiling_params={vector_tile_inputs['tiling_params']}, " + f"low_dim_axes={vector_tile_inputs['low_dim_axes']}, " + f"reduction_axes={vector_tile_inputs['reduction_axes']}, " + f"persistent_reduction={self.persistent_reduction}, " + f"dual_reduction={self.dual_reduction}, " + f"num_buffers={self.num_buffers}") + + kernel_meta = KernelMeta( + vector_tile_inputs["axis_sizes"], + vector_tile_inputs["split_params"], + self.fixed_split_params, + vector_tile_inputs["tiling_params"], + vector_tile_inputs["low_dim_axes"], + vector_tile_inputs["reduction_axes"], + dtype, + self.persistent_reduction, + self.dual_reduction, + self.num_buffers, + self.is_simt_mode, + ) + tile_gen = TileGenerator(kernel_meta=kernel_meta) + tile_gen.descend_split_tiling() + + self.gen_configs.clear() + self.gen_configs = tile_gen.configs + + if self.is_simt_mode: + self.gen_configs = self._expand_simt_num_warps_configs(self.gen_configs) + else: + self.gen_configs = self._expand_simd_multibuffer_configs(self.gen_configs) + + if len(self.gen_configs) == 0: + print("[WARNING] The generated candidate tiling configs are empty based on provided parameters!") + + if self.print_autotuning: + print("Generated configs number: {}".format(len(self.gen_configs))) + + def _gen_tile_configs_cv(self, kv_dict: Dict[str, int], dtype: torch.dtype, + all_args: Dict[str, Any] = None) -> List[Config]: + """ + Generate candidate configs with CVTileGenerator for cube/mix mode. + + This path primarily consumes data from ``self.cv_parse_result``: + - ``dot_sites``: source of the M/N/K dimension metadata + - ``tunable_params``: parameters selected for autotuning + - ``missing_tunable_params``: tunable parameters still unresolved + """ + if self.cv_parse_result is None: + return [] + + dot_sites = self.cv_parse_result.dot_sites + if not dot_sites: + return [] + + cv_tile_gen = CVTileGenerator( + cv_parse_result=self.cv_parse_result, + all_args=all_args, + dtype=dtype, + ) + + configs = cv_tile_gen.generate_configs() + self.gen_configs = configs + + return configs + + def _rough_bench_once(self, fn, config=None) -> float: + di = triton.runtime.driver.active.get_device_interface() + di.synchronize() + try: + start_event = di.Event(enable_timing=True) + end_event = di.Event(enable_timing=True) + start_event.record() + fn() + end_event.record() + di.synchronize() + return start_event.elapsed_time(end_event) + except Exception as exc: + if config is not None: + self._record_user_config(config, "rough_benchmark_failed", exc=exc) + print("Triton autotuning compile debug: " + f"one-shot rough benchmark failed, reason={type(exc).__name__}: {exc}") + return float("inf") + + def _prune_by_time_limit(self, run_fns: Dict[Config, Any]) -> Dict[Config, Any]: + time_limit = 200 + + if len(run_fns) <= 1: + return run_fns + + rough_timings = {} + for config, fn in run_fns.items(): + rough_timings[config] = self._rough_bench_once(fn, config=config) + + sorted_configs = sorted(rough_timings.keys(), key=lambda c: rough_timings[c]) + + fastest_time = rough_timings[sorted_configs[0]] + n_warmup = max(1, int(25 / fastest_time)) + n_repeat = max(1, int(100 / fastest_time)) + self.cv_warmup = n_warmup + self.cv_repeat = n_repeat + + min_valid = min(10, len(sorted_configs)) + valid_configs = [] + cumulative_time = 0.0 + + for config in sorted_configs: + estimate_s = rough_timings[config] / 1000.0 * (n_warmup + n_repeat) + cumulative_time += estimate_s + if len(valid_configs) >= min_valid and cumulative_time > time_limit: + break + valid_configs.append(config) + + if self.print_autotuning: + print(f"Triton autotuning: estimate n_warmup={n_warmup}, n_repeat={n_repeat}, " + f"cumulative={cumulative_time:.4f}s/{time_limit}s, " + f"valid={len(valid_configs)}/{len(run_fns)}") + for config in sorted_configs: + is_valid = config in valid_configs + status = "valid" if is_valid else "pruned" + print(f"Triton autotuning compile debug: " + f"[{status}] config={config}, rough_time={rough_timings[config]:.4f}ms") + + for config in run_fns: + if config not in valid_configs: + self._record_user_config( + config, "pruned", + f"Excluded by the {time_limit}s benchmark time budget; rough_time={rough_timings[config]} ms.") + return {cfg: run_fns[cfg] for cfg in valid_configs} + + def generate_key_and_configs(self, *args, **kwargs): + self.nargs = dict(zip(self.arg_names, args)) + compile_mode = kwargs.get("compile_mode", _DEFAULT_COMPILE_MODE) + self.is_simt_mode = compile_mode == "simt_only" + if 'num_warps' in kwargs and kwargs['num_warps'] is not None: + self.user_specified_warps = kwargs['num_warps'] + else: + self.user_specified_warps = None + self.user_specified_multibuffer_mode = _multibuffer_mode_to_tuple(kwargs.get("multibuffer_mode")) + if 'num_stages' in kwargs and kwargs['num_stages'] is not None: + self.user_specified_num_stages = kwargs['num_stages'] + else: + self.user_specified_num_stages = None + if 'multibuffer' in kwargs and kwargs['multibuffer'] is not None: + self.user_specified_multibuffer = kwargs['multibuffer'] + else: + self.user_specified_multibuffer = None + + has_mode = self.user_specified_multibuffer_mode is not None or "multibuffer_mode" in self._raw_config_hints + config_has_mode = any(config.kwargs.get("multibuffer_mode") is not None for config in self.user_configs) + if (has_mode or config_has_mode) and self.user_specified_num_stages is not None: + raise ValueError("num_stages and multibuffer_mode cannot be specified together") + if has_mode and any(config.num_stages is not None for config in self.user_configs): + raise ValueError("num_stages and multibuffer_mode cannot be specified together") + self.config_hints = _normalize_config_hints( + self._raw_config_hints, + inject_default_num_stages=bool(self.user_configs) and bool(self._raw_config_hints), + has_multibuffer_mode=has_mode or config_has_mode, + ) + + # generate key + all_args = {**self.nargs, **kwargs} + _args = {k: v for (k, v) in all_args.items() if k in self.arg_names} + key = [_args[arg_name] for arg_name in self.keys if arg_name in _args] + + # Currently, we use the dtype with maximum byte length + dtype = None + for _, arg in _args.items(): + if hasattr(arg, "dtype"): + key.append(str(arg.dtype)) + dtype = (arg.dtype if get_byte_per_numel(arg.dtype) >= get_byte_per_numel(dtype) else dtype) + if dtype is None: + raise NotImplementedError("Not support for non-Tensor inputs") + key.append(("compile_mode", compile_mode)) + if self.user_specified_multibuffer_mode is not None: + key.append(("multibuffer_mode", self.user_specified_multibuffer_mode)) + + key = tuple(key) + if key not in self.cache: + if self.auto_gen_config: + self.cv_parse_result = self._autoparse_axis_params(all_args) + _kv_dict = { + axis: _args[arg_name] + for axis, arg_name in self.axis_arg_names.items() + if arg_name in _args + } + self._gen_tile_configs(_kv_dict, dtype, all_args) + if has_mode: + # Tile generators supply legacy stage defaults, not user options. + for config in self.gen_configs: + config.num_stages = None + self.gen_configs = _expand_configs_with_hints( + self.fn, + self.gen_configs, + self.config_hints, + ) + expanded_user_configs = _expand_configs_with_hints( + self.fn, + self.user_configs, + self.config_hints, + ) + self._expanded_user_configs = expanded_user_configs + if getattr(self, "_user_config_diagnostics", None) is not None: + self._user_config_diagnostics.reset_configs(expanded_user_configs) + if len(self.gen_configs) == 0 and len(self.user_configs) == 0: + self.configs = _expand_configs_with_hints( + self.fn, + [Config({}, num_warps=4, num_stages=None if has_mode else 2, num_ctas=1)], + self.config_hints if has_mode else {}, + ) + else: + self.configs = self.gen_configs + expanded_user_configs + return key + + @staticmethod + def _inject_grid_num_tiles(kwargs): + # ChunkCoalescing hint: when the launch grid is a static tuple, the + # outermost grid dim is the tile count along the chunk axis. Exposing it + # as grid_num_tiles lets the compiler safely coalesce small chunks for + # unmasked kernels (compile-time known bound). Skipped for callable grids + # (dynamic, unknown at compile time) so the pass safely bails. + if "grid_num_tiles" in kwargs: + return + grid = kwargs.get("grid", None) + if callable(grid) or not isinstance(grid, (tuple, list)): + return + glen = min(len(grid), 3) + if glen <= 0: + return + outermost = grid[glen - 1] + if isinstance(outermost, int) and outermost > 1: + kwargs["grid_num_tiles"] = _InternalNPUOptionInt(outermost) + + def _record_user_config(self, config, status, detail="", exc=None): + diagnostics = getattr(self, "_user_config_diagnostics", None) + return diagnostics is not None and diagnostics.record(config, status, detail, exc) + + def _is_user_config(self, config): + configs = getattr(self, "_expanded_user_configs", getattr(self, "user_configs", [])) + return any(candidate is config or candidate == config for candidate in configs) + + def _record_user_timings(self, timings, source="benchmark"): + if getattr(self, "_user_config_diagnostics", None) is None: + return + for config, timing in timings.items(): + if not self._is_user_config(config): + continue + values = timing if isinstance(timing, (tuple, list)) else (timing, ) + if values and all(math.isfinite(value) for value in values): + self._record_user_config(config, "benchmarked", f"source={source}; {_format_autotune_timing(timing)}") + else: + self._record_user_config(config, "invalid_timing", f"source={source}; No finite benchmark timing.") + + def run(self, *args, **kwargs): + configs = getattr(self, "_expanded_user_configs", getattr(self, "user_configs", [])) + self._user_config_diagnostics = _UserConfigDiagnostics(configs) if self.print_autotuning and configs else None + try: + return self._run_autotune(*args, **kwargs) + except Exception as exc: + if self._user_config_diagnostics is not None: + self._user_config_diagnostics.abort(exc) + raise + finally: + if self._user_config_diagnostics is not None: + self._user_config_diagnostics.report() + self._user_config_diagnostics = None + + def _run_autotune(self, *args, **kwargs): + kwargs = _remove_deprecated_npu_options(kwargs) + self._inject_grid_num_tiles(kwargs) + key = self.generate_key_and_configs(*args, **kwargs) + cache_miss = key not in self.cache + did_benchmark = False + disk_cache_hit = False + single_config_cache_pending = False + if cache_miss: + # prune configs + pruned_configs = self.prune_configs(kwargs) + diagnostics = self._user_config_diagnostics + if diagnostics is not None: + for config, _, _ in diagnostics.results: + if config not in pruned_configs: + diagnostics.record(config, "pruned", "Removed by early_config_prune or perf_model/top_k.") + if self.enable_ubtuner or len(pruned_configs) > 1: + + def benchmark(): + nonlocal did_benchmark + did_benchmark = True + bench_start = time.time() + timings = self._batch_bench(*args, configs=pruned_configs, **kwargs) + bench_end = time.time() + self.bench_time = bench_end - bench_start + self.cache[key] = builtins.min(timings, key=timings.get) + full_nargs = {**self.nargs, **kwargs, **self.cache[key].all_kwargs()} + self.pre_hook(full_nargs, reset_only=True) + self.configs_timings = timings + self._record_user_timings(timings) + + if self.cache_results: + disk_cache_hit = self.check_disk_cache(key, pruned_configs, benchmark) + if disk_cache_hit and self._user_config_diagnostics is not None: + for config in pruned_configs: + self._record_user_config(config, "skipped_cache_hit") + else: + benchmark() + + config = self.cache[key] + else: + config = pruned_configs[0] + single_config_cache_pending = True + else: + config = self.cache[key] + if self._user_config_diagnostics is not None: + for user_config, _, _ in self._user_config_diagnostics.results: + self._record_user_config(user_config, "skipped_cache_hit") + + self.best_config = config + + if self.print_autotuning and did_benchmark: + all_args = {**self.nargs, **kwargs} + key_args = {name: arg for name, arg in all_args.items() if name in self.arg_names} + key_names = [name for name in self.keys if name in key_args] + key_names.extend(f"{name}.dtype" for name, arg in key_args.items() if hasattr(arg, "dtype")) + key_names.append("compile_mode") + if self.user_specified_multibuffer_mode is not None: + key_names.append("multibuffer_mode") + key_fields = ", ".join(f"{name}={value[1]!r}" if isinstance(value, tuple) and len(value) == 2 + and value[0] == name else f"{name}={value!r}" for name, value in zip(key_names, key)) + print(f"Triton autotuning for function {self.base_fn.__name__},\n" + f"with key fields as {key_fields},\n" + f"finished after {self.bench_time:.2f}s; best config selected: {self.best_config};") + self._print_benchmark_results(self.configs_timings) + + if did_benchmark and self.auto_profile_dir is not None: + self._profile(*args, config=self.best_config, **kwargs) + ub_cfg = dict(getattr(config, "ubtune_cfg", {})) + final_kwargs = dict(config.all_kwargs(), **kwargs) + final_kwargs.update(ub_cfg) + if single_config_cache_pending: + self._record_user_config(config, "running", "Single-config execution did not complete.") + if config.pre_hook is not None: + config.pre_hook({**self.nargs, **final_kwargs}) + try: + ret = self.fn.run( + *args, + **final_kwargs, + ) + if single_config_cache_pending: + self.cache[key] = config + self._record_user_config(config, "selected", "Only one config remains; executed without benchmarking.") + return ret + except Exception as exc: + from triton.compiler.errors import CompileTimeAssertionFailure, MLIRCompilationError + from triton.runtime.errors import OutOfResources + + status = "compile_failed" if isinstance(exc, (CompileTimeAssertionFailure, MLIRCompilationError, + OutOfResources)) else "evaluation_failed" + self._record_user_config(config, status, exc=exc) + raise + finally: + self.nargs = None + if did_benchmark and not disk_cache_hit: + # workaround for memory leak when some configs fail to compile + gc.collect() + + def _try_ubtuner(self, *args, config, excp, run_fns, **kwargs): + if not (self.enable_ubtuner and "ub overflow" in str(excp).lower()): + return + + try: + # Extract compile options from the current config and forward them + # to ubtuner as the search starting point. + compile_opts = config.all_kwargs() + ub_cfg = self.ubtuner.get_best_config(*args, compile_opts=compile_opts, **kwargs) + if self.print_autotuning: + print(f"ub_cfg {ub_cfg}") + if ub_cfg: + setattr(config, 'ubtune_cfg', ub_cfg) + ub_fn = self._make_kernel_call(*args, config=config, **kwargs) + run_fns[config] = functools.partial(ub_fn, warmup=False) + except Exception as e: + if self.print_autotuning: + print(f"[WARN] encounter exception when try ubtune, Details: {e}") + + def _handle_compile_failure(self, *args, config, excp, run_fns, compile_key, **kwargs): + self._try_ubtuner(*args, config=config, excp=excp, run_fns=run_fns, **kwargs) + self._compile_failed_configs.append(config) + if config not in run_fns: + self._remember_compile_failure(compile_key, excp) + self._record_user_config(config, "compile_failed", exc=excp) + else: + self._record_user_config(config, "benchmarking", + "UBTuner returned a fallback config; no benchmark result was produced.") + + def _bench_config(self, config, fn): + self._record_user_config(config, "benchmarking", "Benchmark did not complete.") + try: + timing = self.do_bench(fn, quantiles=(0.5, 0.2, 0.8)) + except Exception as exc: + self._record_user_config(config, "benchmark_failed", exc=exc) + raise + self._record_user_timings({config: timing}) + return timing + + def _print_benchmark_results(self, timings) -> None: + if not self.print_autotuning: + return + + print(f"Triton autotuning benchmark results for function {self.base_fn.__name__}:") + for config, timing in timings.items(): + selected = " [selected]" if config == self.best_config else "" + print(f" config={config}; {_format_autotune_timing(timing)}{selected}") + values = timing if isinstance(timing, (tuple, list)) else (timing, ) + if values and all(math.isfinite(value) for value in values): + self._record_user_config(config, "reported") + + def _get_jit_compile_cache_key(self, *args, config, **meta): + """Return the JIT compilation identity for one autotune Config. + + The autotune tuning key intentionally does not participate in this + identity. If a changed tuning key produces the same JIT + specialization and backend options, a previous deterministic compile + failure can be reused safely. If constexpr values or compile options + change, ``compute_cache_key`` returns a different identity and the + Config is compiled again. + + User hooks may mutate arguments before JIT specialization, so use a + conservative fallback and disable negative caching for those Configs. + """ + if config.pre_hook is not None or getattr(self, "user_defined_pre_hook", False): + return None + + try: + current = dict(meta, **config.all_kwargs()) + ub_cfg = dict(getattr(config, "ubtune_cfg", {})) + if ub_cfg: + current.update(ub_cfg) + + # Match the first call made by _batch_bench. Heuristics.run sees + # grid and warmup before forwarding the remaining arguments to + # JITFunction.run, so resolve heuristics before removing them. + current["warmup"] = bool(getattr(self, "compile_parallel", False)) + jit_fn = getattr(self, "fn", None) + visited = set() + while isinstance(jit_fn, triton.runtime.Heuristics): + if id(jit_fn) in visited: + return None + visited.add(id(jit_fn)) + for name, heuristic in jit_fn.values.items(): + heuristic_args = {**dict(zip(jit_fn.arg_names, args)), **current} + current[name] = heuristic(heuristic_args) + jit_fn = jit_fn.fn + + # Unknown wrappers may transform arguments before JITFunction.run. + # Never bypass them when constructing a negative-cache key. + if not isinstance(jit_fn, triton.runtime.JITFunction): + return None + if getattr(jit_fn, "pre_run_hooks", None): + return None + + # These two arguments are consumed by JITFunction.run and are not + # passed to the generated binder. + current.pop("grid", None) + current.pop("warmup", None) + + # Keep this in sync with JITFunction.run before it invokes binder. + current["debug"] = current.get("debug", jit_fn.debug) or knobs.runtime.debug + current["instrumentation_mode"] = knobs.compilation.instrumentation_mode + + device = triton.runtime.driver.active.get_current_device() + _, kernel_key_cache, target, _, binder = jit_fn.device_caches[device] + _, specialization, options = binder(*args, **current) + jit_key = compute_cache_key(kernel_key_cache, specialization, options) + + env_vars = get_cache_invalidating_env_vars() + ubtuner_mode = os.environ.get("TRITON_ENABLE_UBTUNER", "") + if ubtuner_mode: + env_vars["TRITON_ENABLE_UBTUNER"] = ubtuner_mode + + return ( + jit_fn.cache_key, + repr(target), + str(sorted(env_vars.items())), + device, + jit_key, + ) + except Exception: + # Failure-key calculation must never break normal autotuning. + return None + + def _get_cached_compile_failure(self, compile_key): + if compile_key is None: + return None + with self._compile_failure_cache_lock: + return self._compile_failure_cache.get(compile_key) + + def _remember_compile_failure(self, compile_key, exc) -> None: + if compile_key is None: + return + failure = { + "exception_type": type(exc).__name__, + } + with self._compile_failure_cache_lock: + self._compile_failure_cache.setdefault(compile_key, failure) + + def _filter_cached_compile_failures(self, *args, configs, **kwargs): + active_configs = [] + cached_configs = [] + compile_keys = {} + + for config in configs: + compile_key = self._get_jit_compile_cache_key(*args, config=config, **kwargs) + compile_keys[config] = compile_key + failure = self._get_cached_compile_failure(compile_key) + if failure is None: + active_configs.append(config) + continue + + cached_configs.append(config) + if self.print_autotuning: + print("Triton autotuning: skip cached compile-failed config " + f"{config}; previous failure: {failure['exception_type']}") + self._record_user_config(config, "reported") + + self._cached_compile_failed_configs = cached_configs + return active_configs, compile_keys + + def _batch_bench(self, *args, configs, **kwargs): + from triton.compiler.errors import CompileTimeAssertionFailure, MLIRCompilationError + from triton.runtime.errors import OutOfResources + + kernels_call = {} + for config in configs: + try: + kernels_call[config] = self._make_kernel_call(*args, config=config, **kwargs) + except Exception as exc: + self._record_user_config(config, "configuration_failed", exc=exc) + raise + active_configs, compile_keys = self._filter_cached_compile_failures( + *args, + configs=configs, + **kwargs, + ) + kernels_call = {config: kernels_call[config] for config in active_configs} + + if not kernels_call: + details = [] + for config in configs: + failure = self._get_cached_compile_failure(compile_keys.get(config)) + if failure is not None: + details.append(f"config={config}: {failure['exception_type']}") + raise RuntimeError("All triton configs are cached compile failures.\n" + "\n".join(details)) + + run_fns = {} + self._compile_failed_configs = [] + exc = None + exc_stack = "" + + if self.compile_parallel: + import psutil + + max_workers = min(psutil.cpu_count(logical=False) * 3 // 4, len(kernels_call)) + future_kernels = [] + failed_submission = None + try: + with ( + ThreadPoolExecutor(max_workers=max_workers) as executor, + triton.AsyncCompileMode(executor), + ): + for config, fn in kernels_call.items(): + self._record_user_config(config, "compiling", "Compilation did not complete.") + try: + future_kernels.append((config, fn(warmup=True))) + except (CompileTimeAssertionFailure, MLIRCompilationError, OutOfResources) as e: + failed_submission = config + self._record_user_config(config, "compile_failed", exc=e) + raise + + for config, fut in future_kernels: + try: + if hasattr(fut, "result"): + fut = fut.result() + if hasattr(fut, "packed_metadata"): + kernels_call[config].target_kernel_name = fut.packed_metadata.get("kernel_name") + run_fns[config] = functools.partial(kernels_call[config], warmup=False) + self._record_user_config(config, "benchmarking", + "Compilation completed; no benchmark result was produced.") + except (CompileTimeAssertionFailure, MLIRCompilationError, OutOfResources) as e: + import traceback + exc_stack = traceback.format_exc() + exc = e + self._handle_compile_failure(*args, config=config, excp=e, run_fns=run_fns, + compile_key=compile_keys.get(config), **kwargs) + except Exception as e: + # ignore exception from __exit__() of AsyncCompileMode + triton.runtime._async_compile.active_mode.set(None) + for pending in kernels_call: + if (pending not in run_fns and pending not in self._compile_failed_configs + and pending is not failed_submission): + # __exit__ can re-raise an earlier future's exception; + # it cannot identify a failure in this pending config. + self._record_user_config(pending, "not_evaluated", "Parallel compilation interrupted:", exc=e) + else: + for config, fn in kernels_call.items(): + self._record_user_config(config, "compiling", "Compilation or initial execution did not complete.") + try: + compiled_kernel = fn(warmup=False) + if hasattr(compiled_kernel, "packed_metadata"): + fn.target_kernel_name = compiled_kernel.packed_metadata.get("kernel_name") + run_fns[config] = functools.partial(fn, warmup=False) + self._record_user_config(config, "benchmarking", + "Compilation completed; no benchmark result was produced.") + except (CompileTimeAssertionFailure, MLIRCompilationError, OutOfResources) as e: + import traceback + exc_stack = traceback.format_exc() + exc = e + self._handle_compile_failure(*args, config=config, excp=e, run_fns=run_fns, + compile_key=compile_keys.get(config), **kwargs) + except Exception as e: + self._record_user_config(config, "evaluation_failed", exc=e) + raise + + if len(run_fns) == 0: + raise RuntimeError(f"No valid triton configs. {type(exc).__name__}: {exc} \nStack trace: {exc_stack}") + + if len(run_fns) == 1: + # we ignore expensive profiling method when only single config is left + return {config: self._bench_config(config, fn) for config, fn in run_fns.items()} + + parser_mode = getattr(self, "parser_mode", None) or "vector" + cv_parse_result = getattr(self, "cv_parse_result", None) + if parser_mode in ("cube", "mix") and cv_parse_result is not None: + run_fns = self._prune_by_time_limit(run_fns) + + use_profiling = os.getenv("TRITON_BENCH_METHOD", "default").lower() == "npu" + # Respect user-provided benchmarkers even when NPU profiling mode is enabled. + use_npu_profiling = use_profiling and not self.user_defined_do_bench + if use_npu_profiling: + from ..testing import ProfilerResultMismatchError, do_bench_npu + + cv_mode = parser_mode in ("cube", "mix") and cv_parse_result is not None + warmup = getattr(self, "cv_warmup", 5) if cv_mode else 5 + active = getattr(self, "cv_repeat", 30) if cv_mode else 30 + target_kernel_name = self._resolve_target_kernel_name(kernels_call, run_fns.keys()) + try: + time_cost = do_bench_npu( + list(run_fns.values()), + warmup=warmup, + active=active, + clear_l2_cache=True, + target_kernel_name=target_kernel_name, + ) + assert len(time_cost) == len(run_fns) + timings = dict(zip(run_fns.keys(), time_cost)) + self._record_user_timings(timings) + return timings + except ProfilerResultMismatchError as exc: + warnings.warn( + "Filtered profiler rows do not match the expected count for autotune benchmarking; " + f"target_kernel_name={exc.target_kernel_name!r}, expected_rows={exc.expected_rows}, " + f"actual_rows={exc.actual_rows}. fallback to default do_bench.", + RuntimeWarning, + stacklevel=2, + ) + return {config: self._bench_config(config, fn) for config, fn in run_fns.items()} + else: + return {config: self._bench_config(config, fn) for config, fn in run_fns.items()} + + def _resolve_target_kernel_name(self, kernels_call, configs) -> Optional[str]: + for config in configs: + kernel_call = kernels_call.get(config) + if kernel_call is None: + continue + kernel_name = getattr(kernel_call, "target_kernel_name", None) + if kernel_name: + return kernel_name + return None + + def _make_kernel_call(self, *args, config, **meta): + # check for conflicts, i.e. meta-parameters both provided + # as kwargs and by the autotuner + conflicts = meta.keys() & config.kwargs.keys() + if conflicts: + raise ValueError(f"Conflicting meta-parameters: {', '.join(conflicts)}." + " Make sure that you don't re-define auto-tuned symbols.") + # augment meta-parameters with tunable ones + current = dict(meta, **config.all_kwargs()) + ub_cfg = dict(getattr(config, "ubtune_cfg", {})) + if ub_cfg: + current.update(ub_cfg) + full_nargs = {**self.nargs, **current} + + def kernel_call(warmup): + if config.pre_hook: + config.pre_hook(full_nargs) + self.pre_hook(full_nargs) + try: + current.update({"warmup": warmup}) + res = self.fn.run( + *args, + **current, + ) + if isinstance(res, tuple): + res = res[0] + packed_metadata = getattr(res, "packed_metadata", None) + if isinstance(packed_metadata, dict): + kernel_call.target_kernel_name = packed_metadata.get("kernel_name") + except Exception as e: + try: + self.post_hook(full_nargs, exception=e) + finally: + # Throw exception raised by `self.fn.run` + raise + + if not warmup: + self.post_hook(full_nargs, exception=None) + return res + + kernel_call.target_kernel_name = None + return kernel_call + + def warmup(self, *args, **kwargs): + kwargs = _remove_deprecated_npu_options(kwargs) + self._inject_grid_num_tiles(kwargs) + _ = self.generate_key_and_configs(*args, **kwargs) + pruned_configs = self.prune_configs(kwargs) + ret = [] + + def warmup_config(config): + compile_options = dict(config.all_kwargs(), **kwargs) + return self.fn.warmup(*args, **compile_options) + + if self.compile_parallel: + import psutil + + max_workers = min(psutil.cpu_count(logical=False) // 2, len(pruned_configs)) + with ( + ThreadPoolExecutor(max_workers=max_workers) as executor, + triton.AsyncCompileMode(executor), + ): + for config in pruned_configs: + ret.append(warmup_config(config)) + else: + for config in pruned_configs: + ret.append(warmup_config(config)) + self.nargs = None + return ret + + def _profile(self, *args, config, **meta): + from ..testing import do_bench_npu + + kernel_call = self._make_kernel_call(*args, config=config, **meta) + fn = functools.partial(kernel_call, warmup=False) + do_bench_npu(fn, prof_dir=self.auto_profile_dir, keep_res=True) + + def _autoparse_split_params(self, candidates_params: List[str]) -> Dict[str, str]: + """ + Extracts the split axis parameters from triton kernel code. + """ + func_ast = self.fn.parse() + parser = SplitAxesParser(func_ast, self._get_parser_axis_arg_names(), candidates_params) + split_axes = parser.parse() + self.split_axis_pid_dims = dict(getattr(parser, "split_axis_pid_dims", {})) + self.axis_pid_dims = dict(getattr(parser, "axis_pid_dims", {})) + self.split_params = dict(split_axes) + self._refresh_vector_axes() + if self.print_autotuning: + print(f"Ascend autotuning parse split axes: {split_axes}, " + f"split axis pid dims: {self.split_axis_pid_dims}, " + f"axis pid dims: {self.axis_pid_dims}") + return split_axes + + def _autoparse_axis_pid_dims(self) -> Dict[str, int]: + """ + Extract axis -> program_id dim mapping without relying on split-parameter + classification, so fixed-grid semantics can always consume it. + """ + func_ast = self.fn.parse() + parser = SplitAxesParser( + func_ast, + self._get_parser_axis_arg_names(), + self._get_constexpr_candidates(), + ) + _ = parser.parse() + self.axis_pid_dims = dict(getattr(parser, "axis_pid_dims", {})) + self.split_axis_pid_dims = dict(getattr(parser, "split_axis_pid_dims", {})) + self._refresh_vector_axes() + if self.print_autotuning: + print("Ascend autotuning parse axis pid dims (independent): " + f"{self.axis_pid_dims}") + return self.axis_pid_dims + + def _get_constexpr_candidates(self) -> List[str]: + """ + Returns all constexpr parameter names from the kernel function definition. + """ + func_ast = self.fn.parse() + constexpr_names = [] + for node in ast.walk(func_ast): + if not isinstance(node, ast.FunctionDef): + continue + if not isinstance(node.args, ast.arguments): + continue + for arg in node.args.args: + if not isinstance(arg, ast.arg): + continue + ann = arg.annotation + if (isinstance(ann, ast.Attribute) and isinstance(ann.value, ast.Name) and ann.value.id == "tl" + and ann.attr == "constexpr"): + constexpr_names.append(arg.arg) + break + return constexpr_names + + def _get_fixed_grid_dim_values(self, grid, all_args: Dict[str, object] = None) -> Dict[int, int]: + """ + Returns fixed grid dim -> value. + - Static tuple/list grid: direct extraction + - Callable grid: infer fixed dims by perturbing missing constexpr params + """ + if grid is None: + return {} + if callable(grid): + return self._infer_fixed_dims_from_callable_grid(grid, all_args or {}) + return self._extract_fixed_grid_dims(grid) + + def _extract_fixed_grid_dims(self, grid) -> Dict[int, int]: + if isinstance(grid, int): + grid = (grid, ) + if not isinstance(grid, (tuple, list)): + return {} + fixed_dims = {} + for idx, dim in enumerate(grid): + if isinstance(dim, int) and dim > 0: + fixed_dims[idx] = dim + return fixed_dims + + def _normalize_grid_tuple(self, grid_out): + if isinstance(grid_out, int): + return (grid_out, ) + if isinstance(grid_out, (tuple, list)): + return tuple(grid_out) + return None + + def _infer_fixed_dims_from_callable_grid(self, grid_fn, all_args: Dict[str, object]) -> Dict[int, int]: + constexpr_candidates = self._get_constexpr_candidates() + base_meta = dict(all_args or {}) + + # Fill missing constexpr with stable probe defaults so grid(meta) can execute. + for name in constexpr_candidates: + if name not in base_meta: + base_meta[name] = 128 + + try: + base_grid_raw = grid_fn(dict(base_meta)) + except Exception: + return {} + + base_grid = self._normalize_grid_tuple(base_grid_raw) + if base_grid is None: + return {} + + dynamic_dims = set() + # Missing constexpr are tunable candidates. + tunable_probe_names = [name for name in constexpr_candidates if name not in (all_args or {})] + probe_values = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + + for name in tunable_probe_names: + baseline = base_meta.get(name, 128) + for probe in probe_values: + if probe == baseline: + continue + probe_meta = dict(base_meta) + probe_meta[name] = probe + try: + probe_grid_raw = grid_fn(probe_meta) + except Exception: + continue + probe_grid = self._normalize_grid_tuple(probe_grid_raw) + if probe_grid is None: + continue + if len(probe_grid) != len(base_grid): + dynamic_dims.update(range(min(len(probe_grid), len(base_grid)))) + continue + for idx, (base_dim, probe_dim) in enumerate(zip(base_grid, probe_grid)): + if not (isinstance(base_dim, int) and isinstance(probe_dim, int)): + dynamic_dims.add(idx) + continue + if base_dim != probe_dim: + dynamic_dims.add(idx) + + fixed_dims = {} + for idx, dim in enumerate(base_grid): + if idx in dynamic_dims: + continue + if isinstance(dim, int) and dim > 0: + fixed_dims[idx] = dim + return fixed_dims + + def _autoparse_tiling_params(self, candidates_params: List[str]) -> Dict[str, str]: + """ + Extracts the tiling axis parameters from triton kernel code. + """ + func_ast = self.fn.parse() + parser_axis_arg_names = self._normalize_axis_name_mapping(self._get_parser_axis_arg_names()) + parser = TilingAxesParser(func_ast, parser_axis_arg_names, candidates_params) + tiling_axes = parser.parse() + self.tiling_params = dict(tiling_axes) + self._refresh_vector_axes() + if self.print_autotuning: + print(f"Ascend autotuning parse tiling axes: {tiling_axes}") + return tiling_axes + + def _autoparse_reduction_axes(self) -> List[str]: + """ + Extracts the reduction axis parameters from triton kernel code. + """ + func_ast = self.fn.parse() + axis_arg_names_before = self._get_parser_axis_arg_names() + parser = ReductionAxesParser(func_ast, axis_arg_names_before) + raw_reduction_axes = parser.parse() + for axis in raw_reduction_axes: + base_axis = self._get_axis_base_name(axis) + self._promote_axis_arg_name_to_reduction(base_axis) + reduction_axes = self._normalize_reduction_axes(raw_reduction_axes) + self.reduction_axes = list(reduction_axes) + self._refresh_vector_axes() + + if self.print_autotuning: + print(f"Ascend autotuning parse axis arg names: {self.axis_arg_names} \n" + f"Ascend autotuning parse reduction axes: {reduction_axes}") + return reduction_axes + + def _autoparse_low_dim_axes(self) -> List[str]: + """ + Extracts the low dimension axis from triton kernel code. + """ + func_ast = self.fn.parse() + parser_axis_arg_names = self._normalize_axis_name_mapping(self._get_parser_axis_arg_names()) + parser = LowDimsAxesParser(func_ast, parser_axis_arg_names) + low_dim_axes = parser.parse() + if len(low_dim_axes) < 1: + if self.print_autotuning: + print("[WARNING] Failed to parse low-dimensional axes, fallback to empty low_dim_axes.") + return [] + self.low_dim_axes = list(low_dim_axes) + self._refresh_vector_axes() + if self.print_autotuning: + print(f"Ascend autotuning parse low dimensional axes: {low_dim_axes}") + return low_dim_axes + + def _autoparse_ptr_nums(self, all_args: dict) -> int: + """ + Counts the number of pointer parameters from triton kernel code. + """ + ptr_nums = 0 + ptr_params = list() + for k, v in all_args.items(): + if isinstance(v, Tensor): + ptr_nums += 1 + ptr_params.append(k) + + if self.print_autotuning: + print(f"Ascend autotuning parse pointer params: {ptr_params}, pointer nums: {ptr_nums}") + return ptr_nums + + def _get_persistent_reduction_threshold(self, reduction_axis: str) -> int: + # Keep this heuristic aligned with inductor-style policy: + # inner reduction axis uses a larger threshold than other axes. + # Reduction axes are stored as base axis names; compare directly with + # the low-dim base axis. + reduction_axis_base = self._get_axis_base_name(reduction_axis) + low_dim_axis_base = (self._get_axis_base_name(self.low_dim_axes[0]) if self.low_dim_axes else None) + if reduction_axis_base is not None and reduction_axis_base == low_dim_axis_base: + return 1024 + return 64 + + +def autotune(configs, key, prune_configs_by=None, reset_to_zero=None, restore_value=None, pre_hook=None, post_hook=None, + warmup=None, rep=None, use_cuda_graph=False, do_bench=None, cache_results=False, *, auto_prof_dir=None, + hints=None): + """ + Decorator for auto-tuning a :code:`triton.jit`'d function. + + .. highlight:: python + .. code-block:: python + + @triton.autotune(configs=[ + triton.Config(kwargs={'BLOCK_SIZE': 128}, num_warps=4), + triton.Config(kwargs={'BLOCK_SIZE': 1024}, num_warps=8), + ], + key=['x_size'] # the two above configs will be evaluated anytime + # the value of x_size changes + ) + @triton.jit + def kernel(x_ptr, x_size, **META): + BLOCK_SIZE = META['BLOCK_SIZE'] + :note: When all the configurations are evaluated, the kernel will run multiple times. + This means that whatever value the kernel updates will be updated multiple times. + To avoid this undesired behavior, you can use the `reset_to_zero` argument, which + resets the value of the provided tensor to `zero` before running any configuration. + + If the environment variable :code:`TRITON_PRINT_AUTOTUNING` is set to + :code:`"1"`, Triton will print a message to stdout after autotuning each + kernel, including the benchmark timing for each valid configuration, the + time spent autotuning, and the best configuration. + Additional feedback for user-provided configurations (including hints + expansion) reports failures or interrupted evaluation without repeating + successful benchmark output. Pruning, successful single-config execution, + and memory/disk tuning-cache hits do not add diagnostics. + + :param configs: a list of :code:`triton.Config` objects + :type configs: list[triton.Config] + :param key: a list of argument names whose change in value will trigger the evaluation of all provided configs. + :type key: list[str] + :param prune_configs_by: a dict of functions that are used to prune configs, fields: + 'perf_model': performance model used to predicate running time with different configs, returns running time + 'top_k': number of configs to bench + 'early_config_prune'(optional): a function used to do early prune (eg, num_stages). It takes configs:List[Config] as its input, and returns pruned configs. + :param reset_to_zero: a list of argument names whose value will be reset to zero before evaluating any configs. + :type reset_to_zero: list[str] + :param restore_value: a list of argument names whose value will be restored after evaluating any configs. + :type restore_value: list[str] + :param pre_hook: a function that will be called before the kernel is called. + This overrides the default pre_hook used for 'reset_to_zero' and 'restore_value'. + 'kwargs': a dict of all arguments passed to the kernel. + 'reset_only': a boolean indicating whether the pre_hook is called to reset the values only, without a corresponding post_hook. + :type pre_hook: lambda args, reset_only + :param post_hook: a function that will be called after the kernel is called. + This overrides the default post_hook used for 'restore_value'. + 'kwargs': a dict of all arguments passed to the kernel. + 'exception': the exception raised by the kernel in case of a compilation or runtime error. + :type post_hook: lambda args, exception + :param warmup: warmup time (in ms) to pass to benchmarking (deprecated). + :type warmup: int + :param rep: repetition time (in ms) to pass to benchmarking (deprecated). + :type rep: int + :param do_bench: a benchmark function to measure the time of each run. + :type do_bench: lambda fn, quantiles + :param cache_results: whether to cache autotune timings to disk. Defaults to False. + :type cache_results: bool + :param auto_prof_dir: the specified directory to store the profiling results of the best config. + If this parameter is None or the best config is retrieved from cache, the profiling process will be ignored. + :type auto_prof_dir: str + :param hints: a dict of autotune hint auguments passed to AutoTilingTuner. + """ + + def decorator(fn): + return AutoTilingTuner(fn, fn.arg_names, configs, key, reset_to_zero, restore_value, pre_hook=pre_hook, + post_hook=post_hook, prune_configs_by=prune_configs_by, warmup=warmup, rep=rep, + use_cuda_graph=use_cuda_graph, do_bench=do_bench, cache_results=cache_results, + auto_profile_dir=auto_prof_dir, hints=hints) + + return decorator + + +_ALL_PARAMS = { + "num_stages", + "multibuffer_mode", + "unit_flag", + "multibuffer", + "limit_auto_multi_buffer_only_for_local_buffer", + "limit_auto_multi_buffer_of_local_buffer", + "set_workspace_multibuffer", + "enable_hivm_auto_cv_balance", + "tile_mix_vector_loop", + "tile_mix_cube_loop", + "enable_ubuf_saving", +} + +_DEFAULTS = { + "num_stages": [2], + "unit_flag": [False], + "limit_auto_multi_buffer_only_for_local_buffer": [False], + "limit_auto_multi_buffer_of_local_buffer": ["no-l0c"], + "set_workspace_multibuffer": [2, 4], + "enable_hivm_auto_cv_balance": [True], + "tile_mix_vector_loop": [2, 4], + "tile_mix_cube_loop": [2, 4], + "enable_ubuf_saving": [True], +} + +_VALID_VALUES = { + "num_stages": [1, 2], + "limit_auto_multi_buffer_of_local_buffer": ["no-limit", "no-l0c"], + "set_workspace_multibuffer": [2, 4], + "tile_mix_vector_loop": [2, 4, 8], + "tile_mix_cube_loop": [2, 4, 8], +} + +_CUBE_PARAMS = {"num_stages", "multibuffer_mode", "unit_flag", "limit_auto_multi_buffer_of_local_buffer"} + +_MIXCV_PARAMS = { + "num_stages", + "multibuffer_mode", + "unit_flag", + "limit_auto_multi_buffer_only_for_local_buffer", + "limit_auto_multi_buffer_of_local_buffer", + "set_workspace_multibuffer", + "enable_hivm_auto_cv_balance", + "tile_mix_vector_loop", + "tile_mix_cube_loop", + "enable_ubuf_saving", +} + +_VECTOR_PARAMS = { + "num_stages", + "multibuffer_mode", + "enable_ubuf_saving", +} + + +def _check_boolean_list(val, param_name): + return isinstance(val, (list, tuple)) and len(val) > 0 and all(isinstance(x, bool) for x in val) + + +def _check_string_in_set(val, valid_set, param_name): + return isinstance(val, (list, tuple)) and len(val) > 0 and all(v in valid_set for v in val) + + +def _check_int_in_set(val, valid_set, param_name): + return isinstance(val, (list, tuple)) and len(val) > 0 and all(isinstance(v, int) and v in valid_set for v in val) + + +def _check_multibuffer_mode_list(values, param_name): + if not isinstance(values, (list, tuple)) or not values: + return False + try: + return all(value is not None and _multibuffer_mode_to_tuple(value) is not None for value in values) + except TypeError: + return False + + +_VALIDATION_RULES = { + "multibuffer_mode": { + "desc": "must be a non-empty list/tuple of mode dictionaries (or cached pairs) with str keys and int counts", + "check": _check_multibuffer_mode_list, + }, + "num_stages": { + "desc": f"must be one or more of: {_VALID_VALUES['num_stages']}", "check": + lambda val, p: _check_int_in_set(val, _VALID_VALUES['num_stages'], p) + }, + "unit_flag": {"desc": "must be non-empty list/tuple of boolean values", "check": _check_boolean_list}, + "limit_auto_multi_buffer_only_for_local_buffer": + {"desc": "must be non-empty list/tuple of boolean values", "check": _check_boolean_list}, + "limit_auto_multi_buffer_of_local_buffer": { + "desc": f"must be one or more of: {_VALID_VALUES['limit_auto_multi_buffer_of_local_buffer']}", "check": + lambda val, p: _check_string_in_set(val, _VALID_VALUES['limit_auto_multi_buffer_of_local_buffer'], p) + }, + "set_workspace_multibuffer": { + "desc": f"must be one or more of: {_VALID_VALUES['set_workspace_multibuffer']}", "check": + lambda val, p: _check_int_in_set(val, _VALID_VALUES['set_workspace_multibuffer'], p) + }, + "enable_hivm_auto_cv_balance": + {"desc": "must be non-empty list/tuple of boolean values", "check": _check_boolean_list}, + "tile_mix_vector_loop": { + "desc": f"must be one or more of: {_VALID_VALUES['tile_mix_vector_loop']}", "check": + lambda val, p: _check_int_in_set(val, _VALID_VALUES['tile_mix_vector_loop'], p) + }, + "tile_mix_cube_loop": { + "desc": f"must be one or more of: {_VALID_VALUES['tile_mix_cube_loop']}", "check": + lambda val, p: _check_int_in_set(val, _VALID_VALUES['tile_mix_cube_loop'], p) + }, + "enable_ubuf_saving": {"desc": "must be non-empty list/tuple of boolean values", "check": _check_boolean_list}, +} + + +class BaseAutotuner: + """ + Base class for generating auto-tuning configurations without block dimensions. + Users must provide fixed dimension parameters when calling the kernel. + """ + + def __init__(self, operator_name, supported_params, default_params, validation_rules): + self.operator_name = operator_name + self.supported_params = supported_params + self.default_params = default_params + self.validation_rules = validation_rules + + def validate_parameters(self, **kwargs): + # Check for unsupported parameters + invalid_params = [k for k in kwargs.keys() if k not in _ALL_PARAMS] + if invalid_params: + print(f"[ERROR] Invalid parameters for {self.operator_name}: {invalid_params}") + return False + + for param, rule in self.validation_rules.items(): + if param in kwargs: + if not rule["check"](kwargs[param], param): + print(f"[ERROR] Invalid value for '{param}' in {self.operator_name}: {kwargs[param]}") + print(f" Expected: {rule['desc']}") + return False + return True + + def get_configs(self, **kwargs): + """ + Generate a list of Config objects. + Each parameter must be provided as a list (even for a single value). + The function produces the Cartesian product of all parameter lists. + - num_stages: each value will be set as Config.num_stages (not placed in kwargs) + - other parameters: each value will be placed in Config.kwargs + Returns a list of Config objects. + """ + if "multibuffer_mode" in kwargs and "num_stages" in kwargs: + raise ValueError("num_stages and multibuffer_mode cannot be specified together") + if not self.validate_parameters(**kwargs): + return [] + + # Collect parameter values, using defaults for missing ones + param_values = {} + for p in sorted(self.supported_params): + if p == "multibuffer_mode" and p not in kwargs: + continue + if p == "num_stages" and "multibuffer_mode" in kwargs: + continue + if p in kwargs: + param_values[p] = kwargs[p] + else: + param_values[p] = self.default_params.get(p, [None]) + + keys = list(param_values.keys()) + values = list(param_values.values()) + combos = list(itertools.product(*values)) + + configs = [] + for combo in combos: + config_kwargs = {} + num_stages_val = None + for i, pname in enumerate(keys): + val = combo[i] + if pname == "num_stages": + num_stages_val = val + else: + config_kwargs[pname] = val + + configs.append(Config(kwargs=config_kwargs, num_stages=num_stages_val)) + return configs + + +CubeAutotuner = BaseAutotuner(operator_name="cube", supported_params=_CUBE_PARAMS, default_params=_DEFAULTS, + validation_rules=_VALIDATION_RULES) + +MixcvAutotuner = BaseAutotuner(operator_name="mixcv", supported_params=_MIXCV_PARAMS, default_params=_DEFAULTS, + validation_rules=_VALIDATION_RULES) + +VectorAutotuner = BaseAutotuner(operator_name="vector", supported_params=_VECTOR_PARAMS, default_params=_DEFAULTS, + validation_rules=_VALIDATION_RULES) + + +def get_autotune_cube_config(**kwargs: Any) -> List[triton.Config]: + """ + Generate autotune configuration for the cube operator. + Supported parameters: num_stages, unit_flag, limit_auto_multi_buffer_of_local_buffer. + """ + import triton + return CubeAutotuner.get_configs(**kwargs) + + +def get_autotune_cv_config(**kwargs: Any) -> List[triton.Config]: + """ + Generate autotune configuration for the mixcv operator. + Supported parameters: num_stages, unit_flag, limit_auto_multi_buffer_only_for_local_buffer, + limit_auto_multi_buffer_of_local_buffer, set_workspace_multibuffer, + enable_hivm_auto_cv_balance, tile_mix_vector_loop, tile_mix_cube_loop, enable_ubuf_saving + """ + import triton + return MixcvAutotuner.get_configs(**kwargs) + + +def get_autotune_vector_config(**kwargs: Any) -> List[triton.Config]: + """ + Generate autotune configuration for the vector operator. + Supported parameters: num_stages, enable_ubuf_saving + """ + import triton + return VectorAutotuner.get_configs(**kwargs) + + +def get_max_configs(config, kernel_type="mixcv", **kwargs): + """ + Expand a single base Config by combining it with tuning parameters. + + :param config: A triton.Config object serving as the base. + :param kernel_type: Operator type, one of "cube", "mixcv", "vector". Default "mixcv". + :param kwargs: Tuning parameters, each provided as a list (e.g., enable_hivm_auto_cv_balance=[True, False]). + If a parameter is not provided, its value is taken from the base config (if present) + or from the defaults. + :return: List of expanded Config objects. + """ + kwargs = _remove_deprecated_npu_options(kwargs) + + # Determine the set of parameters supported by the current kernel_type + if kernel_type == "cube": + supported = _CUBE_PARAMS + elif kernel_type == "vector": + supported = _VECTOR_PARAMS + else: + supported = _MIXCV_PARAMS + + # Warn about unsupported parameters provided in kwargs + unsupported = [k for k in kwargs if k not in supported and k in _ALL_PARAMS] + if unsupported: + print( + f"[WARNING] The following parameters are not supported for kernel_type '{kernel_type}': {unsupported}. They will be ignored." + ) + + # Build value lists for each parameter (priority: kwargs > base config > defaults) + param_values = {} + base_kwargs = config.kwargs + base_num_stages = config.num_stages + + has_mode = "multibuffer_mode" in kwargs or base_kwargs.get("multibuffer_mode") is not None + if has_mode and ("num_stages" in kwargs or base_num_stages is not None): + raise ValueError("num_stages and multibuffer_mode cannot be specified together") + for param in sorted(supported): + if param == "multibuffer_mode" and param not in kwargs and param not in base_kwargs: + continue + if has_mode and param == "num_stages" and param not in kwargs: + continue + if param in kwargs: + # User-provided list via tuning_params takes precedence + val_list = kwargs[param] + elif param == "num_stages": + # num_stages is an attribute of Config, not part of kwargs. + # If not provided in tuning_params, use the value from the base config as a fixed single-element list. + val_list = [base_num_stages] + elif param in base_kwargs: + # Parameter present in base config's kwargs -> fix to that single value + val_list = [base_kwargs[param]] + else: + # Otherwise fall back to defaults + val_list = _DEFAULTS.get(param, [None]) + + # Validate the value list + if param in _VALIDATION_RULES: + rule = _VALIDATION_RULES[param] + if not rule["check"](val_list, param): + raise ValueError(f"Invalid value for '{param}': {val_list}. Expected: {rule['desc']}") + param_values[param] = val_list + + # Cartesian product of all parameter lists + keys = list(param_values.keys()) + values = list(param_values.values()) + combos = list(itertools.product(*values)) + + new_configs = [] + for combo in combos: + # Start with a copy of the original config's kwargs + new_kwargs = config.kwargs.copy() + num_stages_val = None + + for i, pname in enumerate(keys): + val = combo[i] + if pname == "num_stages": + num_stages_val = val + else: + # Overwrite or add the parameter to kwargs + new_kwargs[pname] = val + + new_config = Config(kwargs=new_kwargs, num_warps=config.num_warps, + num_stages=num_stages_val if num_stages_val is not None else config.num_stages, + num_ctas=config.num_ctas, maxnreg=config.maxnreg, pre_hook=config.pre_hook, + ir_override=config.ir_override) + new_configs.append(new_config) + + return new_configs + + +def max_autotune(configs, key, kernel_type="mixcv", prune_configs_by=None, reset_to_zero=None, restore_value=None, + pre_hook=None, post_hook=None, warmup=None, rep=None, use_cuda_graph=False, do_bench=None, + **tuning_params): + """ + Decorator that expands each base Config with tuning parameters before auto-tuning. + + Usage is similar to @triton.autotune, but allows automatic expansion of + additional tuning parameters (e.g., enable_hivm_auto_cv_balance, tile_mix_vector_loop, ...) + for each provided base configuration. + + :param configs: List of base triton.Config objects. + :param key: List of argument names whose change triggers re-tuning. + :param kernel_type: Operator type, one of "cube", "mixcv", "vector". Default "mixcv". + :param prune_configs_by: Same as in autotune. + :param reset_to_zero: Same as in autotune. + :param restore_value: Same as in autotune. + :param pre_hook: Same as in autotune. + :param post_hook: Same as in autotune. + :param warmup: Deprecated. + :param rep: Deprecated. + :param use_cuda_graph: Deprecated. + :param do_bench: Same as in autotune. + :param tuning_params: Additional tuning parameters as keyword arguments. + Each value must be a list; the Cartesian product of these lists + will be combined with each base config. + """ + + tuning_params = _remove_deprecated_npu_options(tuning_params) + + def decorator(fn): + if not configs or len(configs) == 0: + raise ValueError("[max_autotune] The argument 'configs' cannot be empty. " + "Please provide at least one base config. ") + # Expand each base config with the provided tuning parameters + expanded_configs = [] + for cfg in configs: + expanded = get_max_configs(cfg, kernel_type=kernel_type, **tuning_params) + expanded_configs.extend(expanded) + + # Call the original autotune decorator with the expanded configs + return autotune(configs=expanded_configs, key=key, prune_configs_by=prune_configs_by, + reset_to_zero=reset_to_zero, restore_value=restore_value, pre_hook=pre_hook, + post_hook=post_hook, warmup=warmup, rep=rep, use_cuda_graph=use_cuda_graph, + do_bench=do_bench)(fn) + + return decorator diff --git a/third_party/ascend/backend/runtime/costmodel_runtime.py b/third_party/ascend/backend/runtime/costmodel_runtime.py new file mode 100644 index 0000000000..30e29bdf3b --- /dev/null +++ b/third_party/ascend/backend/runtime/costmodel_runtime.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import hashlib +import json +import os +import warnings +from typing import Dict, List, Optional + +from triton.runtime.cache import get_cache_manager + +try: + from triton.runtime.cache import triton_key as _triton_key +except ImportError: + _triton_key = None + + +def _costmodel_cache_namespace() -> str: + """Return a stable hex key accepted by triton.runtime.cache manager.""" + if _triton_key is None: + raw = "costmodel_release322" + else: + try: + raw = f"costmodel_{_triton_key()}" + except Exception: + raw = "costmodel_release322" + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + +_COSTMODEL_MEM_CACHE: Dict[str, float] = {} + + +def run_costmodel(ttir_or_path, extra_args=None, dump_ir_on_error=False): + args = list(extra_args or []) + if "-allow-unregistered-dialect" not in args: + args.append("-allow-unregistered-dialect") + + from triton._C.libtriton import ascend as ascend_capi + + if os.path.exists(str(ttir_or_path)): + with open(str(ttir_or_path), "r", encoding="utf-8") as f: + mlir_text = f.read() + else: + mlir_text = ttir_or_path + + try: + return ascend_capi.run_costmodel_inproc(mlir_text, args) + except Exception as exc: + if dump_ir_on_error and os.path.exists(str(ttir_or_path)): + print(f"IR 文件: {ttir_or_path}") + print(f"in-process costmodel failed: {exc}") + return None + + +def make_costmodel_cache_key(ttir: str, extra_args: Optional[List[str]]) -> str: + h = hashlib.sha256() + h.update(ttir.encode("utf-8")) + h.update(b"|") + if extra_args: + h.update(" ".join(extra_args).encode("utf-8")) + h.update(b"|") + h.update(b"inproc_costmodel_v2_loop_weighted") + return h.hexdigest() + + +def load_costmodel_latency(cache_key: str) -> Optional[float]: + cached = _COSTMODEL_MEM_CACHE.get(cache_key) + if cached is not None: + return cached + + cache_manager = get_cache_manager(_costmodel_cache_namespace()) + file_name = f"{cache_key}.json" + payload_path = cache_manager.get_file(file_name) + if payload_path is None: + return None + + try: + with open(payload_path, "r", encoding="utf-8") as f: + parsed = json.load(f) + latency = float(parsed["latency"]) + _COSTMODEL_MEM_CACHE[cache_key] = latency + return latency + except Exception: + return None + + +def store_costmodel_latency(cache_key: str, latency: float) -> None: + _COSTMODEL_MEM_CACHE[cache_key] = latency + cache_manager = get_cache_manager(_costmodel_cache_namespace()) + file_name = f"{cache_key}.json" + cache_manager.put(json.dumps({"latency": latency}), file_name, binary=False) + + +def parse_latency(stdout: str) -> float: + import re + + match = re.search(r"Estimated Time:\s+([0-9.]+)\s*us", stdout) + return float(match.group(1)) if match else float("inf") + + +def _warn_costmodel(msg: str) -> None: + warnings.warn(f"costmodel_bench: {msg}", RuntimeWarning) + + +def _resolve_default_hardware_config() -> str: + candidates = [ + os.path.join(os.path.dirname(__file__), "../../../costmodel/configs/ascend_910b.json"), + os.path.join(os.path.dirname(__file__), "../../../../third_party/ascend/costmodel/configs/ascend_910b.json"), + os.path.join(os.path.dirname(__file__), "../../../../../third_party/ascend/costmodel/configs/ascend_910b.json"), + ] + for candidate in candidates: + path = os.path.abspath(candidate) + if os.path.exists(path): + return path + return "" + + +def _build_costmodel_extra_args(arg_bindings: str, hardware_config: str = ""): + base = "-ascend-perf-model" + resolved_hardware_config = hardware_config or _resolve_default_hardware_config() + # NOTE: Current inproc parser in installed runtime consumes only one payload + # token after `-ascend-perf-model`. Frontend only forwards arg-bindings now. + if arg_bindings: + return [base, f"arg-bindings={arg_bindings}"] + if resolved_hardware_config: + return [base, f"hardware-config={resolved_hardware_config}"] + return [base] + + +def _normalize_costmodel_item(item): + if not isinstance(item, dict): + return None, float("inf"), None + + config = item.get("config") + if config is None: + return None, float("inf"), None + + ttir = item.get("ttir") + if not ttir: + return config, float("inf"), None + + arg_bindings = item.get("arg_bindings", "") + hardware_config = item.get("hardware_config", "") + return config, None, (config, ttir, arg_bindings, hardware_config) + + +def _eval_one_costmodel_item(item): + config, ttir, arg_bindings, hardware_config = item + extra_args = _build_costmodel_extra_args(arg_bindings, hardware_config) + cache_key = make_costmodel_cache_key(ttir, extra_args) + cached = load_costmodel_latency(cache_key) + if cached is not None: + return config, cached + + output = run_costmodel(ttir_or_path=ttir, extra_args=extra_args) + latency = float("inf") if output is None else parse_latency(output) + store_costmodel_latency(cache_key, latency) + return config, latency + + +def costmodel_bench(config_ttir_item): + """Evaluate one candidate config with costmodel from a prebuilt TTIR payload. + + Args: + config_ttir_item (dict): Per-config payload containing: + - ``config``: config object returned as the result key. + - ``ttir`` (str): TTIR text for costmodel evaluation. + - ``arg_bindings`` (str, optional): Runtime bindings string passed + to costmodel (for example ``"arg3=98432,pid_x=0"``). + + Returns: + tuple: ``(config, latency_us)``. ``latency_us`` is ``float("inf")`` + when TTIR is missing/invalid or evaluation fails. Invalid items + without a config return ``(None, float("inf"))``. + """ + try: + config, fallback_latency, pending_item = _normalize_costmodel_item(config_ttir_item) + if config is None: + return None, fallback_latency + if pending_item is None: + return config, fallback_latency + return _eval_one_costmodel_item(pending_item) + except Exception as exc: + _warn_costmodel(f"unexpected failure: {exc}") + if isinstance(config_ttir_item, dict) and config_ttir_item.get("config") is not None: + return config_ttir_item["config"], float("inf") + return None, float("inf") diff --git a/third_party/ascend/backend/runtime/cv_autotune/__init__.py b/third_party/ascend/backend/runtime/cv_autotune/__init__.py new file mode 100644 index 0000000000..094740a5bb --- /dev/null +++ b/third_party/ascend/backend/runtime/cv_autotune/__init__.py @@ -0,0 +1,62 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +CV Fusion Operator Autotuning Framework. + +This package provides autotuning functionality for CV fusion operators +on NPU hardware. +""" + +# Configuration generation +from .generators.cv_tile_generator import ( + CVTileGenerator, + ParamSpace, +) + +# Pruning +from .pruning.heuristic_pruner import HeuristicPruner + +# Hardware constraints +from .hardware_consts import HardwareConstraints, get_default_hardware_constraints + +# Parameter normalization +from .normalization import ( + ParamNameNormalizer, + CvAutotuneParam, + create_cv_autotune_param, +) + +__all__ = [ + # Configuration generation + 'CVTileGenerator', + 'ParamSpace', + + # Pruning + 'HeuristicPruner', + + # Hardware + 'HardwareConstraints', + 'get_default_hardware_constraints', + + # Parameter normalization + 'ParamNameNormalizer', + 'CvAutotuneParam', + 'create_cv_autotune_param', +] diff --git a/third_party/ascend/backend/runtime/cv_autotune/generators/cv_tile_generator.py b/third_party/ascend/backend/runtime/cv_autotune/generators/cv_tile_generator.py new file mode 100644 index 0000000000..841d2e5b0b --- /dev/null +++ b/third_party/ascend/backend/runtime/cv_autotune/generators/cv_tile_generator.py @@ -0,0 +1,441 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +CV Tile configuration generator for autotune. + +This module implements a tree-based configuration generator that creates +all possible tile configurations for CV fusion operators based on +CvParseResult. +""" + +# Types used to describe the search space. +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional + +import torch + +from triton.runtime.autotuner import Config + +from ...dsl_analysis.schema import (AXIS_LENGTH_STATE_TUNABLE, CvAxisLengthInfo, CvParseResult) +from ..hardware_consts import (HardwareConstraints, get_default_hardware_constraints) +from ..pruning.heuristic_pruner import HeuristicPruner + +ParamType = Literal["NUM", "ENUM", "BOOL"] + + +@dataclass +class ParamSpace: + """Definition of a single autotune parameter search space.""" + name: str + type: ParamType + min: int = 0 + max: int = 0 + stride: int = 1 + values: List[Any] = None + + def count_values(self) -> int: + if self.type == "NUM": + return (self.max - self.min) // self.stride + 1 + elif self.type == "ENUM": + return len(self.values) if self.values else 0 + elif self.type == "BOOL": + return 2 + return 0 + + def get_values(self) -> List[Any]: + if self.type == "NUM": + return list(range(self.min, self.max + 1, self.stride)) + elif self.type == "ENUM": + return self.values if self.values else [] + elif self.type == "BOOL": + return [True, False] + return [] + + def check_constraint(self, config: Dict[str, Any]) -> bool: + """Check parameter constraints.""" + return True + + +class CVTileGenerator: + """ + CV Tile configuration generator. + + Builds the search space from ``CvParseResult`` and enumerates valid + configurations with a BFS-style traversal. + + Core logic: + 1. Extract M/N/K tunable parameters from ``cv_parse_result.dot_sites``. + 2. Use the real axis length as the upper bound of the search space. + 3. Traverse the search space and prune invalid configs with + ``HeuristicPruner``. + """ + + # Default values for M/N dimensions. + _MN_MIN = 16 + _MN_DEFAULT_MAX = 256 + _MN_STRIDE = 16 + + # Default values for the K dimension. + _K_MIN = 16 + _K_DEFAULT_MAX = 128 + _K_STRIDE = 16 + + _STEP_SMALL = 16 + _STEP_LARGE = 32 + _STEP_THRESHOLD = 64 + + def __init__(self, cv_parse_result: CvParseResult, hardware_constraints: Optional[HardwareConstraints] = None, + all_args: Optional[Dict[str, Any]] = None, dtype: Optional[torch.dtype] = None): + """ + Initialize the generator. + + Args: + cv_parse_result: CV parse result. + hardware_constraints: Optional hardware constraints. Defaults to + the built-in constraint set. + all_args: Full runtime argument map, including tensors, used to + resolve tensor shapes from ``length_expr``. + """ + self.cv_parse_result = cv_parse_result + self.all_args = all_args or {} + self.hw_constraints = hardware_constraints or get_default_hardware_constraints() + self.pruner = HeuristicPruner(self.hw_constraints) + self.dtype = dtype or torch.float16 + + # Build the search space directly from dot-site metadata. + self.search_space = self._build_param_space() + + # Convert search space to ordered list for deterministic generation + self.params = list(self.search_space.values()) + + # Statistics + self.stats = { + 'generated': 0, + 'pruned': 0, + 'total_possible': 1, + } + + # Calculate total possible configurations + for param in self.params: + self.stats['total_possible'] *= param.count_values() + + def _resolve_axis_max(self, axis_info: CvAxisLengthInfo, default_max: int) -> int: + """ + Resolve the maximum tile value from ``CvAxisLengthInfo``. + + Priority: + 1. ``const_value``: compile-time constant, use directly. + 2. ``length_expr`` as a key in ``all_args``. + 3. ``length_expr`` parsed as an integer literal. + 4. Fallback to the provided default. + + Args: + axis_info: Axis length metadata. + default_max: Default upper bound when resolution fails. + + Returns: + int: Maximum tile size for the axis. + """ + # Prefer the compile-time constant when available. + if axis_info.const_value is not None: + return axis_info.const_value + + # Try to resolve length_expr through runtime arguments. + if axis_info.length_expr is not None and axis_info.length_expr in self.all_args: + value = self.all_args[axis_info.length_expr] + if isinstance(value, (int, float)): + return int(value) + + # Fall back to parsing length_expr as a plain integer literal. + if axis_info.length_expr is not None: + try: + return int(axis_info.length_expr) + except (ValueError, TypeError): + pass + + return default_max + + def _make_param(self, name: str, max_val: int, min_val: int, stride: int) -> ParamSpace: + """ + Build a parameter search space with segmented strides. + + Supports dynamic step sizes: + - Step = 16 when current value <= 64. + - Step = 32 when current value > 64. + Always includes `min_val` and `max_val`. Returns an ENUM ParamSpace. + + Args: + name: Parameter name. + max_val: Upper bound. + min_val: Lower bound. + stride: (Ignored, kept for API compatibility.) + + Returns: + ParamSpace: ENUM type containing the generated tile sizes. + """ + # Case 1: max < stride, so max itself is the only legal value. + values = [] + current = min_val + while current <= max_val: + values.append(current) + if current < self._STEP_THRESHOLD: + current += self._STEP_SMALL + else: + current += self._STEP_LARGE + if values[-1] != max_val: + values.append(max_val) + values = sorted(set(values)) + return ParamSpace(name=name, type="ENUM", values=values) + + @staticmethod + def _extract_single_tunable_symbol(symbol: Optional[str]) -> Optional[str]: + """Extract the unique tunable symbol from a composite token.""" + if not isinstance(symbol, str): + return None + + candidates = [] + for item in symbol.split("|"): + token = item.strip() + if not token or not token.isidentifier(): + continue + if token in candidates: + continue + candidates.append(token) + if len(candidates) == 1: + return candidates[0] + return None + + def _build_param_space(self) -> Dict[str, ParamSpace]: + """ + Build the M/N/K parameter search space from ``cv_parse_result``. + + Strategy, in priority order: + 1. Extract tunable params and axis lengths from dot sites. + - Visit every dot site and collect all tunable params. + - Treat repeated parameter names as the same coupled parameter and + keep the largest axis length across all sites. + - Treat different parameter names as independent parameters. + 2. If dot sites do not provide enough data, fall back to + ``tunable_params``. + 3. If ``tunable_params`` is still insufficient, infer missing + parameters from ``signature_params``. + 4. If all inference fails, use the default search space. + + Returns: + Dict[str, ParamSpace]: Mapping from parameter name to search range. + """ + param_space = {} + dot_sites = self.cv_parse_result.dot_sites + + # Strategy 1: extract parameters from all dot sites. + # Deduplicate by parameter name; identical names indicate coupling. + if dot_sites: + # Collect the largest resolved axis length for each parameter. + param_max_values: Dict[str, int] = {} # param_name -> resolved max + param_roles: Dict[str, str] = {} # param_name -> role (M/N/K) + + for site in dot_sites: + for role, axis_info in [("M", site.m), ("N", site.n), ("K", site.k)]: + param_name = axis_info.tunable_param + if not param_name and axis_info.state == AXIS_LENGTH_STATE_TUNABLE: + param_name = self._extract_single_tunable_symbol(axis_info.axis_symbol) + if param_name is None: + param_name = self._extract_single_tunable_symbol(axis_info.length_expr) + if not param_name: + continue + if axis_info.state != AXIS_LENGTH_STATE_TUNABLE: + continue + + resolved_max = self._resolve_axis_max(axis_info, self._MN_DEFAULT_MAX) + + if param_name not in param_max_values: + # First occurrence: record the resolved axis length. + param_max_values[param_name] = resolved_max + param_roles[param_name] = role + else: + # Coupled occurrence: keep the largest axis length. + old_max = param_max_values[param_name] + if resolved_max > old_max: + param_max_values[param_name] = resolved_max + for param_name, max_val in param_max_values.items(): + role = param_roles[param_name] + if role == "K": + param_space[param_name] = self._make_param(param_name, max_val, self._K_MIN, self._K_STRIDE) + else: + param_space[param_name] = self._make_param(param_name, max_val, self._MN_MIN, self._MN_STRIDE) + + # Strategy 2: if dot-site extraction produced nothing, fall back to + # tunable_params. + if not param_space and self.cv_parse_result.tunable_params: + for param_info in self.cv_parse_result.tunable_params: + name = param_info.name + if name in param_space: + continue + # Infer the axis role from the parameter name. + role = self._infer_role_from_name(name) + if role == "K": + param_space[name] = self._make_param(name, self._K_DEFAULT_MAX, self._K_MIN, self._K_STRIDE) + else: + param_space[name] = self._make_param(name, self._MN_DEFAULT_MAX, self._MN_MIN, self._MN_STRIDE) + + if not param_space: + return {} + + return param_space + + @staticmethod + def _infer_role_from_name(name: str) -> str: + """Infer the axis role from the parameter name.""" + name_upper = name.upper() + if "K" in name_upper and "BLOCK_K" in name_upper: + return "K" + return "M" # Default to the M/N class. + + def _get_default_param_space(self) -> Dict[str, ParamSpace]: + """Return the default parameter search space.""" + return { + 'BLOCK_M': + ParamSpace(name='BLOCK_M', type='NUM', min=self._MN_MIN, max=self._MN_DEFAULT_MAX, stride=self._MN_STRIDE), + 'BLOCK_N': + ParamSpace(name='BLOCK_N', type='NUM', min=self._MN_MIN, max=self._MN_DEFAULT_MAX, stride=self._MN_STRIDE), + 'BLOCK_K': + ParamSpace(name='BLOCK_K', type='NUM', min=self._K_MIN, max=self._K_DEFAULT_MAX, stride=self._K_STRIDE), + } + + def _is_memory_limit_exceeded(self, reason: str) -> bool: + """ + Check if pruning reason is memory-related. + + This is used to determine if we should break the parameter loop, + since larger values will also exceed memory limits. + + Args: + reason: Pruning reason string + + Returns: + True if the reason indicates memory limit exceeded + """ + memory_keywords = [ + "Memory limit", + "L0c", + "UB", + "L0A", + "L0B", + "memory", + "Memory:", + ] + return any(keyword in reason for keyword in memory_keywords) + + def generate_configs(self) -> List[Config]: + """ + Generate the full tile-config space recursively and prune afterward. + + Returns: + List[Config]: Valid ``Config`` objects after pruning. + """ + + if not self.params: + return [] + + # Step 1: generate the full Cartesian product without pruning. + raw_configs: List[Dict[str, int]] = [] + self._generate_all_recursive(param_idx=0, partial={}, result=raw_configs) + + configs = [] + for cfg_dict in raw_configs: + is_valid = True + for site in self.cv_parse_result.dot_sites: + site_full_config = cfg_dict.copy() + for role, axis_info in [("M", site.m), ("N", site.n), ("K", site.k)]: + target_key = f"standard_block_{role.lower()}" + + if target_key in site_full_config: + continue + + resolved_val = None + + if axis_info.tunable_param and axis_info.tunable_param in cfg_dict: + resolved_val = cfg_dict[axis_info.tunable_param] + elif axis_info.const_value is not None: + resolved_val = axis_info.const_value + # TODO: replace this fallback with a parser-derived default. + else: + resolved_val = 128 if role != "K" else 64 + + site_full_config[target_key] = resolved_val + + should_prune, reason = self.pruner.should_prune(site_full_config, self.dtype) + + if should_prune: + is_valid = False + self.stats['pruned'] += 1 + break + if is_valid: + configs.append(Config(kwargs=cfg_dict, num_warps=1, num_stages=1)) + + return configs + + def _generate_all_recursive(self, param_idx: int, partial: Dict[str, int], result: List[Dict[str, int]]): + """Recursively enumerate all parameter combinations without pruning.""" + if param_idx >= len(self.params): + result.append(partial.copy()) + return + + param = self.params[param_idx] + if param.type == "NUM": + value = param.min + while value <= param.max: + partial[param.name] = value + self._generate_all_recursive(param_idx + 1, partial, result) + value += param.stride + del partial[param.name] + + elif param.type == "ENUM": + for value in param.values: + partial[param.name] = value + self._generate_all_recursive(param_idx + 1, partial, result) + del partial[param.name] + + elif param.type == "BOOL": + for value in [True, False]: + partial[param.name] = value + self._generate_all_recursive(param_idx + 1, partial, result) + del partial[param.name] + + def estimate_search_space_size(self) -> Dict[str, int]: + """ + Estimate the size of the search space. + + Returns: + Dictionary with size statistics + """ + total = 1 + param_counts = {} + + for param in self.params: + count = param.count_values() + param_counts[param.name] = count + total *= count + + return { + 'total_combinations': total, + 'num_parameters': len(self.params), + 'parameter_counts': param_counts, + } diff --git a/third_party/ascend/backend/runtime/cv_autotune/hardware_consts.py b/third_party/ascend/backend/runtime/cv_autotune/hardware_consts.py new file mode 100644 index 0000000000..727fa80eb1 --- /dev/null +++ b/third_party/ascend/backend/runtime/cv_autotune/hardware_consts.py @@ -0,0 +1,124 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, or/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Hardware constraints for CV autotune. + +This module defines hardware-specific constraints for NPU hardware, +including memory limits, cache sizes, and other resource constraints. +""" + +from dataclasses import dataclass, field +from typing import Optional, Dict, Any + +from .. import utils as runtime_utils + + +def _arch_default(legacy_value: int, a5_value: int) -> int: + # Only called for an omitted constructor argument, never during import. + arch = runtime_utils.target.arch + if arch.startswith(("Ascend910_95", "Ascend950")): + return a5_value + return legacy_value + + +@dataclass +class HardwareConstraints: + """ + Hardware constraint parameters for NPU. + + These constraints are used during configuration generation and pruning + to ensure generated configurations are feasible on the target hardware. + + Attributes: + l0a_size: L0A cache size in bytes (for matrix A) + l0b_size: L0B cache size in bytes (for matrix B) + l0c_size: L0C cache size in bytes (for matrix C) + max_l0_size: Maximum L0 cache size in bytes + max_l1_size: Maximum L1 cache size in bytes + max_l2_size: Maximum L2 cache size in bytes + ub_size: Unified Buffer size in bytes + """ + + # Default ArchType : A2/A3 + l0a_size: int = 64 * 1024 # 64 KB - for matrix A + l0b_size: int = 64 * 1024 # 64 KB - for matrix B + l0c_size: int = field(default_factory=lambda: _arch_default(128 * 1024, 256 * 1024)) + + # Memory limits (in bytes) + max_l0_size: int = 128 * 1024 # 128 KB + max_l1_size: int = 512 * 1024 # 512 KB + max_l2_size: int = field(default_factory=lambda: _arch_default(192 * 1024 * 1024, 128 * 1024 * 1024)) + ub_size: int = field(default_factory=lambda: _arch_default(192 * 1024, 248 * 1024)) + + def __post_init__(self): + """Validate hardware constraints after initialization.""" + + if self.l0a_size <= 0: + raise ValueError(f"Invalid l0a_size: {self.l0a_size}") + if self.l0b_size <= 0: + raise ValueError(f"Invalid l0b_size: {self.l0b_size}") + if self.l0c_size <= 0: + raise ValueError(f"Invalid l0c_size: {self.l0c_size}") + if self.max_l0_size <= 0: + raise ValueError(f"Invalid max_l0_size: {self.max_l0_size}") + if self.max_l1_size <= 0: + raise ValueError(f"Invalid max_l1_size: {self.max_l1_size}") + if self.ub_size <= 0: + raise ValueError(f"Invalid ub_size: {self.ub_size}") + + def to_dict(self) -> Dict[str, Any]: + """ + Convert hardware constraints to dictionary. + + Returns: + Dictionary representation of hardware constraints + """ + return { + 'l0a_size': self.l0a_size, + 'l0b_size': self.l0b_size, + 'l0c_size': self.l0c_size, + 'max_l0_size': self.max_l0_size, + 'max_l1_size': self.max_l1_size, + 'max_l2_size': self.max_l2_size, + 'ub_size': self.ub_size, + } + + +def get_default_hardware_constraints(device_type: str = "ascend", + custom_constraints: Optional[HardwareConstraints] = None) -> HardwareConstraints: + """ + Get default hardware constraints for the specified device type. + + Args: + device_type: Type of device (e.g., "ascend", "nvidia") + custom_constraints: Optional custom constraints to override defaults + + Returns: + HardwareConstraints instance + + Example: + >>> from cv_autotune.hardware_consts import get_default_hardware_constraints + >>> constraints = get_default_hardware_constraints() + >>> print(f"Max L0 size: {constraints.max_l0_size}") + """ + if custom_constraints is not None: + return custom_constraints + + return HardwareConstraints() diff --git a/third_party/ascend/backend/runtime/cv_autotune/normalization/__init__.py b/third_party/ascend/backend/runtime/cv_autotune/normalization/__init__.py new file mode 100644 index 0000000000..aebbef2c9e --- /dev/null +++ b/third_party/ascend/backend/runtime/cv_autotune/normalization/__init__.py @@ -0,0 +1,31 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Parameter normalization for CV autotune. +""" + +from .param_normalizer import ParamNameNormalizer +from .cv_autotune_param import CvAutotuneParam, create_cv_autotune_param + +__all__ = [ + 'ParamNameNormalizer', + 'CvAutotuneParam', + 'create_cv_autotune_param', +] diff --git a/third_party/ascend/backend/runtime/cv_autotune/normalization/cv_autotune_param.py b/third_party/ascend/backend/runtime/cv_autotune/normalization/cv_autotune_param.py new file mode 100644 index 0000000000..135172400e --- /dev/null +++ b/third_party/ascend/backend/runtime/cv_autotune/normalization/cv_autotune_param.py @@ -0,0 +1,358 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Unified parameter structure for CV autotune algorithms. + +This module defines the CvAutotuneParam class, which serves as the unified +input parameter structure for all CV autotune algorithms. +""" + +from dataclasses import dataclass, field +from typing import Dict, Any, Optional, List +from ...dsl_analysis.schema import CvParseResult, CvAxisRole + + +@dataclass +class CvAutotuneParam: + """ + Unified parameter container for CV autotune. + + This class gathers all inputs required by the algorithms: + 1. CV parse result (``CvParseResult``) + 2. Runtime parameters (the original ``kv_dict``) + 3. Data type (``dtype``) + 4. Other optional configuration + + Design principles: + - Provide a single input interface for autotune algorithms + - Carry all required information in one place + - Offer convenient conversion helpers + - Support optional hardware constraints + """ + + # ==================== Core inputs ==================== + + cv_parse_result: CvParseResult + """CV parse result imported from ``schema.py``.""" + + runtime_params: Dict[str, Any] + """Runtime parameter dictionary (the original ``kv_dict``).""" + + # ==================== Optional config ==================== + + dtype: Optional[str] = None + """Data type such as ``float16`` or ``float32``.""" + + parser_mode: Optional[str] = None + """Parser mode (``cube`` or ``mix``). Defaults to ``cv_parse_result.mode``.""" + + # ==================== Optional hardware constraints ==================== + + hardware_constraints: Optional[Dict[str, Any]] = None + """Hardware constraints such as memory or cache sizes.""" + + # ==================== Optional algorithm config ==================== + + enable_pruning: bool = True + """Whether pruning is enabled.""" + + enable_profiling: bool = False + """Whether profiling is enabled.""" + + # ==================== Internal caches (derived automatically) ==================== + + _cached_axis_sizes: Dict[str, int] = field(default_factory=dict) + """Cached axis-size mapping.""" + + _cached_tiling_params: Dict[str, str] = field(default_factory=dict) + """Cached tiling-parameter mapping.""" + + _cached_fixed_params: Dict[str, int] = field(default_factory=dict) + """Cached fixed-parameter mapping.""" + + _cached_low_dims: List[str] = field(default_factory=list) + """Cached low-dimension list.""" + + # ==================== Convenience helpers ==================== + + def __post_init__(self): + """Perform post-init normalization.""" + # If parser_mode is omitted, derive it from cv_parse_result. + if self.parser_mode is None: + self.parser_mode = self.cv_parse_result.mode + + # Precompute commonly used mappings. + self._precompute_mappings() + + def _precompute_mappings(self): + """Precompute commonly used mappings.""" + # 1. Build tiling_params from tunable parameters with M/N/K roles. + for param_info in self.cv_parse_result.tunable_params: + if param_info.role in (CvAxisRole.M, CvAxisRole.N, CvAxisRole.K): + # This should eventually map axis names to parameter names. + # For now, reuse the parameter name directly and refine later + # with dot-site information if needed. + self._cached_tiling_params[param_info.role] = param_info.name + + # 2. Build fixed_params from scalar runtime parameters. + for param_name, param_value in self.runtime_params.items(): + if isinstance(param_value, (int, float)): + self._cached_fixed_params[param_name] = param_value + + # 3. Derive axis_sizes from dot-site length expressions. + if self.cv_parse_result.dot_sites: + first_site = self.cv_parse_result.dot_sites[0] + for axis_name, axis_info in [('m', first_site.m), ('n', first_site.n), ('k', first_site.k)]: + if axis_info.length_expr is not None: + try: + size = int(axis_info.length_expr) + self._cached_axis_sizes[axis_name] = size + except (ValueError, TypeError): + pass + + # 4. Infer low_dims from the smallest length_expr values. + if self.cv_parse_result.dot_sites: + first_site = self.cv_parse_result.dot_sites[0] + axis_sizes = [] + for axis_name, axis_info in [('m', first_site.m), ('n', first_site.n), ('k', first_site.k)]: + if axis_info.length_expr is not None: + try: + size = int(axis_info.length_expr) + axis_sizes.append((axis_name, size)) + except (ValueError, TypeError): + pass + + # Use the smallest dimensions as low_dim candidates. + if axis_sizes: + axis_sizes.sort(key=lambda x: x[1]) + # Keep the smallest one or two dimensions. + num_low_dims = min(2, len(axis_sizes)) + self._cached_low_dims = [name for name, _ in axis_sizes[:num_low_dims]] + + # ==================== Conversion helpers ==================== + + def to_kernel_meta_dict(self) -> Dict[str, Any]: + """ + Convert to the dictionary format expected by ``KernelMeta``. + + Returns: + Dict: All fields required to initialize ``KernelMeta``. + """ + return { + 'axis_sizes': self.axis_sizes, 'split_params': self.split_params, 'fixed_split_params': + self.fixed_split_params, 'tiling_params': self.tiling_params, 'low_dims': self.low_dims, 'dtype': + self.dtype, 'persistent_reduction': False, # Default value. + 'dual_reduction': False, # Default value. + 'num_buffers': 3, # Default value. + 'is_simt_mode': False, # Default value. + } + + def to_cv_tile_generator_input(self) -> CvParseResult: + """ + Convert to the input expected by ``CVTileGenerator``. + + ``CVTileGenerator`` consumes ``CvParseResult`` directly, so return it + as-is. + + Returns: + CvParseResult: CV parse result. + """ + return self.cv_parse_result + + def to_pruner_input(self) -> Dict[str, Any]: + """ + Convert to the input expected by the pruner. + + Returns: + Dict: Parameter dictionary consumed by the pruner. + """ + return { + 'cv_parse_result': self.cv_parse_result, + 'runtime_params': self.runtime_params, + 'hardware_constraints': self.hardware_constraints, + } + + # ==================== Accessors ==================== + + @property + def axis_sizes(self) -> Dict[str, int]: + """Return the axis-size mapping.""" + return self._cached_axis_sizes + + @property + def tiling_params(self) -> Dict[str, str]: + """Return the tiling-parameter mapping.""" + return self._cached_tiling_params + + @property + def split_params(self) -> Dict[str, str]: + """ + Return the split-parameter mapping. + + This should eventually infer axis-to-parameter mapping from dot sites. + It currently returns an empty dict and is left for future work. + """ + return {} + + @property + def fixed_split_params(self) -> Dict[str, int]: + """Return the fixed split parameters.""" + return self._cached_fixed_params + + @property + def low_dims(self) -> List[str]: + """Return the low-dimension list.""" + return self._cached_low_dims + + @property + def tunable_params(self) -> List[str]: + """Return the list of tunable parameter names.""" + return [p.name for p in self.cv_parse_result.tunable_params] + + @property + def missing_tunable_params(self) -> List[str]: + """Return unresolved tunable parameters.""" + return self.cv_parse_result.missing_tunable_params + + @property + def dot_sites_count(self) -> int: + """Return the number of dot sites.""" + return len(self.cv_parse_result.dot_sites) + + @property + def is_mix_mode(self) -> bool: + """Whether the parser is in mix mode.""" + return self.parser_mode == 'mix' + + @property + def is_cube_mode(self) -> bool: + """Whether the parser is in cube mode.""" + return self.parser_mode == 'cube' + + # ==================== Validation ==================== + + def validate(self) -> tuple[bool, List[str]]: + """ + Validate the parameter bundle. + + Returns: + tuple: (is_valid, error_messages) + """ + errors = [] + + # 1. Check cv_parse_result. + if self.cv_parse_result is None: + errors.append("cv_parse_result cannot be None") + elif self.cv_parse_result.status != "ok": + errors.append(f"cv_parse_result status is not OK: {self.cv_parse_result.status}") + + # 2. Check runtime_params. + if not self.runtime_params: + errors.append("runtime_params cannot be empty") + + # 3. Check unresolved tunable parameters. + missing = set(self.missing_tunable_params) - set(self.runtime_params.keys()) + if missing: + errors.append(f"Missing required tunable parameters: {missing}") + + # 4. Check dot_sites. + if not self.cv_parse_result.dot_sites: + errors.append("cv_parse_result does not contain any dot_sites") + + return (len(errors) == 0, errors) + + # ==================== Utilities ==================== + + def get_param_value(self, param_name: str, default: Any = None) -> Any: + """ + Return the runtime value for a parameter. + + Args: + param_name: Parameter name. + default: Default value. + + Returns: + Parameter value, or ``default`` if it is absent. + """ + return self.runtime_params.get(param_name, default) + + def get_tunable_param_by_role(self, role: CvAxisRole) -> Optional[str]: + """ + Return the tunable parameter name for a given role. + + Args: + role: Parameter role (M/N/K/OTHER). + + Returns: + Parameter name, or ``None`` if it does not exist. + """ + for param_info in self.cv_parse_result.tunable_params: + if param_info.role == role: + return param_info.name + return None + + def get_dot_site(self, site_id: int) -> Optional[Any]: + """ + Return the dot site for a given site ID. + + Args: + site_id: Dot site ID + + Returns: + DotSiteInfo, or ``None`` if it does not exist. + """ + for site in self.cv_parse_result.dot_sites: + if site.site_id == site_id: + return site + return None + + def __repr__(self) -> str: + """Return a readable string representation.""" + return (f"CvAutotuneParam(\n" + f" mode={self.parser_mode},\n" + f" tunable_params={len(self.tunable_params)},\n" + f" dot_sites={self.dot_sites_count},\n" + f" runtime_params={len(self.runtime_params)},\n" + f" dtype={self.dtype}\n" + f")") + + +# ==================== Factory ==================== + + +def create_cv_autotune_param(cv_parse_result: CvParseResult, runtime_params: Dict[str, Any], + dtype: Optional[str] = None, parser_mode: Optional[str] = None, + hardware_constraints: Optional[Dict[str, Any]] = None, **kwargs) -> CvAutotuneParam: + """ + Factory for creating ``CvAutotuneParam`` instances. + + Args: + cv_parse_result: CV parse result. + runtime_params: Runtime parameter dictionary. + dtype: Optional data type. + parser_mode: Optional parser mode. + hardware_constraints: Optional hardware constraints. + **kwargs: Additional optional arguments. + + Returns: + CvAutotuneParam instance. + """ + return CvAutotuneParam(cv_parse_result=cv_parse_result, runtime_params=runtime_params, dtype=dtype, + parser_mode=parser_mode, hardware_constraints=hardware_constraints, **kwargs) diff --git a/third_party/ascend/backend/runtime/cv_autotune/normalization/param_normalizer.py b/third_party/ascend/backend/runtime/cv_autotune/normalization/param_normalizer.py new file mode 100644 index 0000000000..7972ba131e --- /dev/null +++ b/third_party/ascend/backend/runtime/cv_autotune/normalization/param_normalizer.py @@ -0,0 +1,116 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Parameter name normalization for CV autotune. + +This module provides functionality to normalize parameter names and +configurations to a common format for processing and pruning. +""" + +from typing import Dict, Any +import copy + + +class ParamNameNormalizer: + """ + Normalize parameter names and configurations. + + This class handles conversion between different parameter naming + conventions and ensures consistent format for processing. + """ + + def __init__(self): + """Initialize the normalizer with default mappings.""" + # Common parameter name mappings + self.param_mappings = { + # Block size mappings + 'BLOCK_M': 'BLOCK_M', + 'BLOCK_N': 'BLOCK_N', + 'BLOCK_K': 'BLOCK_K', + 'block_m': 'BLOCK_M', + 'block_n': 'BLOCK_N', + 'block_k': 'BLOCK_K', + 'standard_block_m': 'BLOCK_M', + 'standard_block_n': 'BLOCK_N', + 'standard_block_k': 'BLOCK_K', + + # Other mappings + 'num_warps': 'num_warps', + 'num_stages': 'num_stages', + 'GROUP_M': 'GROUP_M', + 'GROUP_N': 'GROUP_N', + } + + def normalize_param_name(self, name: str) -> str: + """ + Normalize a parameter name to standard format. + + Args: + name: Parameter name to normalize + + Returns: + Normalized parameter name + + Example: + >>> normalizer = ParamNameNormalizer() + >>> normalizer.normalize_param_name('block_m') + 'BLOCK_M' + """ + return self.param_mappings.get(name, name) + + def normalize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Normalize a configuration dictionary. + + This method: + 1. Normalizes all parameter names + 2. Creates a new dictionary with normalized names + 3. Preserves the original config (does not modify in-place) + + Args: + config: Configuration dictionary to normalize + + Returns: + New configuration dictionary with normalized parameter names + + Example: + >>> normalizer = ParamNameNormalizer() + >>> config = {'block_m': 128, 'num_warps': 4} + >>> normalized = normalizer.normalize_config(config) + >>> normalized + {'BLOCK_M': 128, 'num_warps': 4} + """ + normalized = {} + for key, value in config.items(): + normalized_key = self.normalize_param_name(key) + normalized[normalized_key] = value + return normalized + + def normalize_configs(self, configs: list) -> list: + """ + Normalize a list of configurations. + + Args: + configs: List of configuration dictionaries + + Returns: + List of normalized configuration dictionaries + """ + return [self.normalize_config(config) for config in configs] diff --git a/third_party/ascend/backend/runtime/cv_autotune/pruning/heuristic_pruner.py b/third_party/ascend/backend/runtime/cv_autotune/pruning/heuristic_pruner.py new file mode 100644 index 0000000000..b871cd494a --- /dev/null +++ b/third_party/ascend/backend/runtime/cv_autotune/pruning/heuristic_pruner.py @@ -0,0 +1,136 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Heuristic pruning for CV fusion operator autotune. + +This module implements heuristic pruning strategies to reduce the search space +by filtering out invalid or suboptimal configurations. +""" + +from typing import Dict, Tuple + +import torch + +from ..hardware_consts import HardwareConstraints +from ..normalization.param_normalizer import ParamNameNormalizer + + +class HeuristicPruner: + """ + Heuristic pruner for configuration space. + + This class implements conservative pruning strategies that only remove + configurations that are definitely invalid (memory limits, alignment, etc.). + It does NOT prune configurations that might be optimal. + """ + + def __init__(self, hardware_constraints: HardwareConstraints): + """ + Initialize the pruner. + + Args: + hardware_constraints: Hardware constraint parameters + """ + self.hw = hardware_constraints + self.normalizer = ParamNameNormalizer() + self.L1Stages: int = 2 + self.L0AStages: int = 2 + self.L0BStages: int = 2 + self.L0CStages: int = 1 + + def should_prune(self, config: Dict, dtype) -> Tuple[bool, str]: + """ + Check if a configuration should be pruned. + + This implements conservative pruning: + - Only prunes configurations that violate hard constraints + - Does NOT prune based on efficiency thresholds (might miss optimal configs) + + Args: + config: Configuration dictionary + + Returns: + (should_prune, reason): Tuple of boolean and reason string + """ + # Normalize parameter names + normalized_config = self.normalizer.normalize_config(config) + + # Check : Memory limits + should_prune, reason = self._check_memory_limits(normalized_config, dtype) + if should_prune: + return True, f"Memory limit: {reason}" + + # Configuration passes all checks + return False, "" + + def _check_memory_limits(self, config: Dict, dtype) -> Tuple[bool, str]: + """ + Check memory limits (conservative). + + This checks if the tile size exceeds hardware memory limits: + - L0A/L0B/L0C Buffers + - L1 Buffer + + Args: + config: Normalized configuration dictionary + + Returns: + (exceeded, reason): Tuple of boolean and reason string + """ + # Get tile dimensions + tile_m = config.get('BLOCK_M', 128) + tile_n = config.get('BLOCK_N', 128) + tile_k = config.get('BLOCK_K', 64) + + try: + if dtype.is_floating_point: + bits = torch.finfo(dtype).bits + else: + bits = torch.iinfo(dtype).bits + typesize = bits // 8 + except Exception: + typesize = 2 + + # Calculate tile size (assuming fp16, adjust based on dtype) + + l1_size = getattr(self.hw, 'max_l1_size', None) // self.L1Stages + l0a_size = getattr(self.hw, 'l0a_size', None) // self.L0AStages + l0b_size = getattr(self.hw, 'l0b_size', None) // self.L0BStages + l0c_size = getattr(self.hw, 'l0c_size', None) // self.L0CStages + + # # Check L1 limit + tile_numel = tile_m * tile_k + tile_k * tile_n + tile_bytes = tile_numel * typesize + if l1_size is not None and tile_bytes > l1_size: + return True, f"L1: {tile_bytes} > {l1_size}" + # Check L0a limit + tile_bytes = tile_m * tile_k * typesize + if l0a_size is not None and tile_bytes > l0a_size: + return True, f"L0a: {tile_bytes} > {l0a_size}" + # Check L0b limit + tile_bytes = tile_k * tile_n * typesize + if l0b_size is not None and tile_bytes > l0b_size: + return True, f"L0a: {tile_bytes} > {l0b_size}" + # Check L0c limit + tile_bytes = tile_m * tile_n * 4 + if l0c_size is not None and tile_bytes > l0c_size: + return True, f"L0a: {tile_bytes} > {l0c_size}" + + return False, "" diff --git a/third_party/ascend/backend/runtime/dsl_analysis/__init__.py b/third_party/ascend/backend/runtime/dsl_analysis/__init__.py new file mode 100644 index 0000000000..4742c88bde --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/__init__.py @@ -0,0 +1,104 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from .entry import (SignatureMissingTunableResult, DotSiteMNKResult, analyze_axis_length_state, analyze_dot_site_mnk, + analyze_signature_and_missing_tunable, resolve_missing_tunable_policy) +from .axis_length_resolver import classify_length_symbol, resolve_axis_length_states +from .axis_semantic_schema import (AxisExtent, AxisSemanticInfo, AxisSemanticResult, AxisSplit, AxisTiling) +from .call_graph import (CallSite, FunctionMeta, build_call_sites, build_function_index, find_function_name_by_node) +from .cv_param_parser import parse_cv_params +from .dot_role_annotator import TensorRole, annotate_tensor_roles +from .kernel_classifier import classify_kernel_type_from_dsl, resolve_kernel_type +from .launch_analyzer import (LaunchSemantics, analyze_launch_semantics, looks_like_cube_mix_tunable) +from .missing_tunable_detector import MissingTunableDetector +from .missing_tunable_policies import CubeMixPolicy, VectorPolicy +from .schema import (AXIS_LENGTH_STATE_FIXED_COMPILE_TIME, AXIS_LENGTH_STATE_RUNTIME_NON_TUNABLE, + AXIS_LENGTH_STATE_TUNABLE, CV_PARSE_STATUS_FAILED, CV_PARSE_STATUS_OK, CV_PARSE_STATUS_PARTIAL, + CV_PARSER_VERSION_V1, AxisLengthStateInfo, AxisLengthStateResult, CvAxisLengthInfo, CvDotSiteInfo, + CvParseResult, CvTunableParamInfo, MissingTunableContext, ParameterSpec, SignatureInfo) +from .shape_inference import (DotSiteMNK, infer_dot_sites_mnk, infer_dot_sites_mnk_interprocedural) +from .signature_analyzer import (extract_constexpr_name_set, extract_constexpr_names, extract_signature_info) +from .symbolic_expr import SymbolicExpr +from .vv_config_adapter import (VvConfigAdapterResult, adapt_vv_v2_to_vector_inputs) +from .vv_parser_options import (resolve_vv_parser_v2_enabled, resolve_vv_parser_v2_mode) +from .vv_param_parser_v2 import (VvAxisInfoV2, VvAxisParseResultV2, parse_vv_axis_semantic_v2, parse_vv_axis_info_v2) + +__all__ = [ + "CubeMixPolicy", + "CallSite", + "CvAxisLengthInfo", + "CvDotSiteInfo", + "CvParseResult", + "CvTunableParamInfo", + "DotSiteMNK", + "DotSiteMNKResult", + "FunctionMeta", + "VectorPolicy", + "LaunchSemantics", + "AxisExtent", + "AxisSemanticInfo", + "AxisSemanticResult", + "AxisSplit", + "AxisTiling", + "AXIS_LENGTH_STATE_FIXED_COMPILE_TIME", + "AXIS_LENGTH_STATE_RUNTIME_NON_TUNABLE", + "AXIS_LENGTH_STATE_TUNABLE", + "CV_PARSER_VERSION_V1", + "CV_PARSE_STATUS_OK", + "CV_PARSE_STATUS_PARTIAL", + "CV_PARSE_STATUS_FAILED", + "AxisLengthStateInfo", + "AxisLengthStateResult", + "MissingTunableContext", + "MissingTunableDetector", + "ParameterSpec", + "SignatureInfo", + "SignatureMissingTunableResult", + "SymbolicExpr", + "TensorRole", + "VvConfigAdapterResult", + "VvAxisInfoV2", + "VvAxisParseResultV2", + "analyze_axis_length_state", + "analyze_launch_semantics", + "analyze_dot_site_mnk", + "analyze_signature_and_missing_tunable", + "annotate_tensor_roles", + "build_call_sites", + "build_function_index", + "classify_kernel_type_from_dsl", + "classify_length_symbol", + "extract_constexpr_name_set", + "extract_constexpr_names", + "extract_signature_info", + "infer_dot_sites_mnk", + "infer_dot_sites_mnk_interprocedural", + "find_function_name_by_node", + "looks_like_cube_mix_tunable", + "adapt_vv_v2_to_vector_inputs", + "parse_cv_params", + "resolve_axis_length_states", + "resolve_missing_tunable_policy", + "resolve_kernel_type", + "resolve_vv_parser_v2_enabled", + "resolve_vv_parser_v2_mode", + "parse_vv_axis_semantic_v2", + "parse_vv_axis_info_v2", +] diff --git a/third_party/ascend/backend/runtime/dsl_analysis/axis_length_resolver.py b/third_party/ascend/backend/runtime/dsl_analysis/axis_length_resolver.py new file mode 100644 index 0000000000..cb33705e6e --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/axis_length_resolver.py @@ -0,0 +1,279 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from typing import Dict, Iterable, List, Mapping, Optional, Set, Tuple + +from .load_semantics import ( + build_assignment_expr_map, + collect_axis_candidates, + extract_axis_block_symbol_map, +) +from .schema import (AXIS_LENGTH_STATE_FIXED_COMPILE_TIME, AXIS_LENGTH_STATE_RUNTIME_NON_TUNABLE, + AXIS_LENGTH_STATE_TUNABLE, AxisLengthStateInfo, SignatureInfo) +from .symbolic_expr import SymbolicExpr + + +def _is_tl_arange_call(node: ast.AST) -> bool: + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) and node.func.value.id == "tl" and node.func.attr == "arange") + + +def _get_arange_stop_expr(call_node: ast.Call) -> Optional[ast.AST]: + if len(call_node.args) >= 2: + return call_node.args[1] + if len(call_node.args) == 1: + return call_node.args[0] + for keyword in call_node.keywords: + if keyword.arg in ("end", "stop"): + return keyword.value + return None + + +def _is_static_number(value: object) -> bool: + # bool is a subclass of int in Python; keep it explicit for readability. + return isinstance(value, (bool, int, float)) + + +def _evaluate_static_expr(node: ast.AST, provided_args: Mapping[str, object]) -> Optional[object]: + if isinstance(node, ast.Constant): + value = node.value + if _is_static_number(value): + return value + return None + + if isinstance(node, ast.Name): + value = provided_args.get(node.id, None) + if _is_static_number(value): + return value + return None + + if isinstance(node, ast.UnaryOp): + operand = _evaluate_static_expr(node.operand, provided_args) + if operand is None: + return None + if isinstance(node.op, ast.USub): + return -operand + if isinstance(node.op, ast.UAdd): + return +operand + if isinstance(node.op, ast.Not): + return not operand + return None + + if isinstance(node, ast.BinOp): + lhs = _evaluate_static_expr(node.left, provided_args) + rhs = _evaluate_static_expr(node.right, provided_args) + if lhs is None or rhs is None: + return None + try: + if isinstance(node.op, ast.Add): + return lhs + rhs + if isinstance(node.op, ast.Sub): + return lhs - rhs + if isinstance(node.op, ast.Mult): + return lhs * rhs + if isinstance(node.op, ast.Div): + return lhs / rhs + if isinstance(node.op, ast.FloorDiv): + return lhs // rhs + if isinstance(node.op, ast.Mod): + return lhs % rhs + if isinstance(node.op, ast.Pow): + return lhs**rhs + except Exception: + return None + return None + + if isinstance(node, ast.BoolOp): + values = [] + for item in node.values: + evaluated = _evaluate_static_expr(item, provided_args) + if evaluated is None: + return None + values.append(bool(evaluated)) + if isinstance(node.op, ast.And): + return all(values) + if isinstance(node.op, ast.Or): + return any(values) + return None + + if isinstance(node, ast.Compare): + left = _evaluate_static_expr(node.left, provided_args) + if left is None: + return None + comparisons = [] + cur_left = left + for op, comparator in zip(node.ops, node.comparators): + right = _evaluate_static_expr(comparator, provided_args) + if right is None: + return None + if isinstance(op, ast.Lt): + comparisons.append(cur_left < right) + elif isinstance(op, ast.LtE): + comparisons.append(cur_left <= right) + elif isinstance(op, ast.Gt): + comparisons.append(cur_left > right) + elif isinstance(op, ast.GtE): + comparisons.append(cur_left >= right) + elif isinstance(op, ast.Eq): + comparisons.append(cur_left == right) + elif isinstance(op, ast.NotEq): + comparisons.append(cur_left != right) + else: + return None + cur_left = right + return all(comparisons) + + return None + + +def _resolve_conditional_expr(node: ast.AST, provided_args: Mapping[str, object]) -> ast.AST: + resolved = node + while isinstance(resolved, ast.IfExp): + cond_value = _evaluate_static_expr(resolved.test, provided_args) + if not isinstance(cond_value, bool): + break + resolved = resolved.body if cond_value else resolved.orelse + return resolved + + +def _normalize_arange_stop_symbol(stop_expr: ast.AST, provided_args: Mapping[str, object]) -> str: + resolved_expr = _resolve_conditional_expr(stop_expr, provided_args) + return str(SymbolicExpr.from_ast(resolved_expr)) + + +def _extract_axis_length_symbol_map( + func_ast: ast.AST, + provided_args: Mapping[str, object], +) -> Dict[str, str]: + assignment_expr_map = build_assignment_expr_map( + func_ast, + provided_args=provided_args, + ) + axis_candidates = collect_axis_candidates(func_ast) + raw_symbol_map = extract_axis_block_symbol_map( + func_ast, + axis_candidates, + assignment_expr_map=assignment_expr_map, + ) + + symbol_map = {} + for symbol, raw_value in raw_symbol_map.items(): + try: + raw_node = ast.parse(raw_value, mode="eval").body + except Exception: + symbol_map[symbol] = raw_value + continue + symbol_map[symbol] = _normalize_arange_stop_symbol(raw_node, provided_args) + return symbol_map + + +def _parse_literal_int(text: str) -> Optional[int]: + try: + node = ast.parse(text, mode="eval").body + except Exception: + return None + if isinstance(node, ast.Constant) and isinstance(node.value, int): + return int(node.value) + return None + + +def _extract_expr_names(text: str) -> Set[str]: + try: + expr = ast.parse(text, mode="eval") + except Exception: + return set() + names = set() + for node in ast.walk(expr): + if isinstance(node, ast.Name): + names.add(node.id) + return names + + +def _classify_length_symbol( + length_symbol: Optional[str], + signature: SignatureInfo, + provided_args: Mapping[str, object], +) -> Tuple[str, Optional[int]]: + if length_symbol is None: + return AXIS_LENGTH_STATE_RUNTIME_NON_TUNABLE, None + + literal_value = _parse_literal_int(length_symbol) + if literal_value is not None: + return AXIS_LENGTH_STATE_FIXED_COMPILE_TIME, literal_value + + if signature.is_constexpr(length_symbol): + if length_symbol in provided_args: + value = provided_args[length_symbol] + if isinstance(value, int): + return AXIS_LENGTH_STATE_FIXED_COMPILE_TIME, int(value) + return AXIS_LENGTH_STATE_FIXED_COMPILE_TIME, None + return AXIS_LENGTH_STATE_TUNABLE, None + + expr_names = _extract_expr_names(length_symbol) + if not expr_names: + return AXIS_LENGTH_STATE_RUNTIME_NON_TUNABLE, None + + all_constexpr = all(signature.is_constexpr(name) for name in expr_names) + if all_constexpr: + if all(name in provided_args for name in expr_names): + return AXIS_LENGTH_STATE_FIXED_COMPILE_TIME, None + return AXIS_LENGTH_STATE_TUNABLE, None + return AXIS_LENGTH_STATE_RUNTIME_NON_TUNABLE, None + + +def classify_length_symbol( + length_symbol: Optional[str], + signature: SignatureInfo, + provided_args: Mapping[str, object], +) -> Tuple[str, Optional[int]]: + return _classify_length_symbol(length_symbol, signature, provided_args) + + +def resolve_axis_length_states( + func_ast: ast.AST, + axis_symbols: Iterable[str], + signature: SignatureInfo, + provided_args: Mapping[str, object], +) -> Dict[str, AxisLengthStateInfo]: + axis_length_symbol_map = _extract_axis_length_symbol_map( + func_ast, + provided_args=provided_args, + ) + resolved = {} + seen = set() + for axis_symbol in axis_symbols: + if axis_symbol in seen: + continue + seen.add(axis_symbol) + # When axis symbols are already direct length parameters + # (e.g., BLOCK_M/BLOCK_N from block_ptr-based inference), classify them + # directly instead of forcing a tl.arange indirection. + length_symbol = axis_length_symbol_map.get(axis_symbol, axis_symbol) + state, value = _classify_length_symbol(length_symbol, signature, provided_args) + resolved[axis_symbol] = AxisLengthStateInfo( + axis_symbol=axis_symbol, + length_symbol=length_symbol, + state=state, + value=value, + ) + return resolved diff --git a/third_party/ascend/backend/runtime/dsl_analysis/axis_semantic_schema.py b/third_party/ascend/backend/runtime/dsl_analysis/axis_semantic_schema.py new file mode 100644 index 0000000000..8ab357935b --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/axis_semantic_schema.py @@ -0,0 +1,77 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List, Optional + + +@dataclass(frozen=True) +class AxisExtent: + expr: Optional[str] + state: str + const_value: Optional[int] + source: str + confidence: float + dynamic_source: str = "none" + + +@dataclass(frozen=True) +class AxisSplit: + param: Optional[str] + pid_dim: Optional[int] + source: str + confidence: float + + +@dataclass(frozen=True) +class AxisTiling: + param: Optional[str] + loop_var: Optional[str] + source: str + confidence: float + fixed_expr: Optional[str] = None + + +@dataclass(frozen=True) +class AxisSemanticInfo: + axis_name: str + extent: AxisExtent + split: AxisSplit + tiling: AxisTiling + is_low_dim: bool + is_reduction: bool + diagnostics: List[str] + + +@dataclass(frozen=True) +class AxisSemanticResult: + axes: Dict[str, AxisSemanticInfo] + axis_length_exprs: Dict[str, str] + fixed_tiling_exprs: Dict[str, str] + axis_pid_dims: Dict[str, int] + inferred_keys: Dict[str, str] + split_params: Dict[str, str] + tiling_params: Dict[str, str] + low_dim_axes: List[str] + reduction_axes: List[str] + status: str + diagnostics: List[str] diff --git a/third_party/ascend/backend/runtime/dsl_analysis/call_graph.py b/third_party/ascend/backend/runtime/dsl_analysis/call_graph.py new file mode 100644 index 0000000000..8fce4e83fe --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/call_graph.py @@ -0,0 +1,105 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from dataclasses import dataclass +from typing import Dict, List, Optional, Sequence, Union + +FunctionLike = Union[ast.FunctionDef, ast.AsyncFunctionDef] + + +@dataclass(frozen=True) +class FunctionMeta: + name: str + node: FunctionLike + param_names: Sequence[str] + + +@dataclass(frozen=True) +class CallSite: + caller: str + callee: str + args: Sequence[Optional[ast.AST]] + lineno: int + + +def _extract_param_names(function_node: FunctionLike) -> List[str]: + args_node = function_node.args + ordered_args = list(args_node.posonlyargs) + list(args_node.args) + list(args_node.kwonlyargs) + return [arg.arg for arg in ordered_args if isinstance(arg, ast.arg)] + + +def build_function_index(tree: ast.AST) -> Dict[str, FunctionMeta]: + functions = {} + for node in ast.walk(tree): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if node.name in functions: + continue + functions[node.name] = FunctionMeta( + name=node.name, + node=node, + param_names=_extract_param_names(node), + ) + return functions + + +def build_call_sites(functions: Dict[str, FunctionMeta]) -> List[CallSite]: + call_sites = [] + for caller_meta in functions.values(): + for node in ast.walk(caller_meta.node): + if not isinstance(node, ast.Call): + continue + if not isinstance(node.func, ast.Name): + continue + callee_name = node.func.id + if callee_name not in functions: + continue + callee_meta = functions[callee_name] + bound_args: List[Optional[ast.AST]] = [None] * len(callee_meta.param_names) + for idx, arg_node in enumerate(node.args): + if idx >= len(bound_args): + break + bound_args[idx] = arg_node + keyword_map = {kw.arg: kw.value for kw in node.keywords if kw.arg is not None} + for idx, param_name in enumerate(callee_meta.param_names): + if bound_args[idx] is not None: + continue + if param_name not in keyword_map: + continue + bound_args[idx] = keyword_map[param_name] + call_sites.append( + CallSite( + caller=caller_meta.name, + callee=callee_name, + args=bound_args, + lineno=getattr(node, "lineno", -1), + )) + call_sites.sort(key=lambda site: (site.caller, site.lineno, site.callee)) + return call_sites + + +def find_function_name_by_node(functions: Dict[str, FunctionMeta], node: ast.AST) -> Optional[str]: + for name, meta in functions.items(): + if meta.node is node: + return name + return None diff --git a/third_party/ascend/backend/runtime/dsl_analysis/cv_param_parser.py b/third_party/ascend/backend/runtime/dsl_analysis/cv_param_parser.py new file mode 100644 index 0000000000..e0f0aae317 --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/cv_param_parser.py @@ -0,0 +1,280 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from typing import Dict, List, Mapping, Optional, Sequence, Set + +from .dynamic_source_utils import resolve_dynamic_source +from .entry import (analyze_axis_length_state, analyze_dot_site_mnk, analyze_signature_and_missing_tunable) +from .load_dependency_analyzer import collect_load_derived_symbols +from .schema import (AXIS_LENGTH_STATE_TUNABLE, CV_PARSE_STATUS_FAILED, CV_PARSE_STATUS_OK, CV_PARSE_STATUS_PARTIAL, + CV_PARSER_VERSION_V1, CvAxisLengthInfo, CvAxisRole, CvDotSiteInfo, CvParseResult, CvParserMode, + CvTunableParamInfo) + + +def _resolve_axis_state_info(axis_symbol: Optional[str], axis_states: Mapping[str, object]): + if axis_symbol is None: + return None + + state_info = axis_states.get(axis_symbol, None) + if state_info is not None: + return state_info + + if "|" not in axis_symbol: + return None + for symbol in axis_symbol.split("|"): + state_info = axis_states.get(symbol, None) + if state_info is not None: + return state_info + return None + + +def _normalize_tunable_param_symbol(symbol: Optional[str]) -> Optional[str]: + if symbol is None: + return None + if not isinstance(symbol, str): + return None + if not symbol.isidentifier(): + if "|" not in symbol: + return None + candidates = [] + for item in symbol.split("|"): + token = item.strip() + if not token or not token.isidentifier(): + continue + if token in candidates: + continue + candidates.append(token) + if len(candidates) == 1: + return candidates[0] + return None + return symbol + + +def _to_cv_axis_length_info( + axis_symbol: Optional[str], + axis_states: Mapping[str, object], + *, + override_length_expr: Optional[str] = None, + load_derived_symbols: Optional[Set[str]] = None, +) -> CvAxisLengthInfo: + load_derived_symbols = load_derived_symbols or set() + state_info = _resolve_axis_state_info(axis_symbol, axis_states) + if state_info is None: + length_expr = override_length_expr + return CvAxisLengthInfo( + axis_symbol=axis_symbol, + length_expr=length_expr, + state="unknown", + tunable_param=None, + const_value=None, + dynamic_source=resolve_dynamic_source( + axis_symbol=axis_symbol, + length_expr=length_expr, + load_derived_symbols=load_derived_symbols, + ), + ) + + state = getattr(state_info, "state", "unknown") + base_tunable_symbol = _normalize_tunable_param_symbol(getattr(state_info, "length_symbol", None)) + tunable_param = base_tunable_symbol if state == AXIS_LENGTH_STATE_TUNABLE else None + length_expr = None + has_effective_override = (override_length_expr is not None and override_length_expr != axis_symbol) + # If we cannot infer a separate/global length expression and the axis symbol + # equals the tunable split parameter itself (e.g. BLOCK_M/BLOCK_N/BLOCK_K), + # keep length_expr as None to let downstream fallback logic decide defaults. + suppress_axis_self_length = (not has_effective_override and axis_symbol is not None and tunable_param is not None + and axis_symbol == tunable_param) + if override_length_expr is not None and override_length_expr != axis_symbol: + length_expr = override_length_expr + if length_expr is None and not suppress_axis_self_length: + length_expr = getattr(state_info, "length_symbol", None) + value = getattr(state_info, "value", None) + # Keep tunable parameter as the original tunable length symbol even when + # we override length_expr to show logical/global axis length. + return CvAxisLengthInfo( + axis_symbol=axis_symbol, + length_expr=length_expr, + state=state, + tunable_param=tunable_param, + const_value=value, + dynamic_source=resolve_dynamic_source( + axis_symbol=axis_symbol, + length_expr=length_expr, + load_derived_symbols=load_derived_symbols, + ), + ) + + +def _collect_role_param_names(dot_sites: Sequence[CvDotSiteInfo]) -> Dict[CvAxisRole, Set[str]]: + role_map: Dict[CvAxisRole, Set[str]] = {"M": set(), "N": set(), "K": set(), "OTHER": set()} + for site in dot_sites: + if site.m.tunable_param: + role_map["M"].add(site.m.tunable_param) + if site.n.tunable_param: + role_map["N"].add(site.n.tunable_param) + if site.k.tunable_param: + role_map["K"].add(site.k.tunable_param) + return role_map + + +def _infer_role(name: str, role_param_names: Mapping[CvAxisRole, Set[str]]) -> CvAxisRole: + if name in role_param_names["M"]: + return "M" + if name in role_param_names["N"]: + return "N" + if name in role_param_names["K"]: + return "K" + return "OTHER" + + +def parse_cv_params( + func_ast: ast.AST, + parser_mode: str, + arg_names: Sequence[str], + provided_args: Mapping[str, object], + explicit_tunable_params: Optional[Sequence[str]] = None, + module_ast: Optional[ast.AST] = None, + entry_function_name: Optional[str] = None, +) -> CvParseResult: + mode = (parser_mode or "").lower() + if mode not in ("cube", "mix"): + raise ValueError("parse_cv_params only supports parser_mode='cube' or 'mix'.") + + diagnostics = [] + signature_params: List[str] = [] + missing_tunable_params: List[str] = [] + load_derived_symbols: Set[str] = set() + + try: + sig_result = analyze_signature_and_missing_tunable( + func_ast, + arg_names=arg_names, + provided_args=dict(provided_args), + explicit_tunable_params=explicit_tunable_params, + parser_mode=mode, + ) + signature_params = list(sig_result.signature.parameter_names()) + missing_tunable_params = list(sig_result.missing_tunable) + except ValueError: + raise + except Exception as exc: + diagnostics.append("signature/missing_tunable analysis failed: {}".format(type(exc).__name__)) + + dot_sites_raw = [] + try: + dot_result = analyze_dot_site_mnk( + func_ast, + parser_mode=mode, + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + dot_sites_raw = list(dot_result.dot_sites) + except Exception as exc: + diagnostics.append("dot-site analysis failed: {}".format(type(exc).__name__)) + + axis_states = {} + try: + axis_state_result = analyze_axis_length_state( + func_ast=func_ast, + parser_mode=mode, + arg_names=arg_names, + provided_args=dict(provided_args), + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + axis_states = dict(axis_state_result.axis_states) + except Exception as exc: + diagnostics.append("axis-length-state analysis failed: {}".format(type(exc).__name__)) + + try: + analysis_root = module_ast if module_ast is not None else func_ast + load_derived_symbols = collect_load_derived_symbols( + analysis_root, + provided_args=dict(provided_args), + entry_function_name=entry_function_name, + ) + except Exception as exc: + diagnostics.append("load-dependency analysis failed: {}".format(type(exc).__name__)) + + dot_sites = [] + for site in dot_sites_raw: + dot_sites.append( + CvDotSiteInfo( + site_id=int(getattr(site, "site_id", len(dot_sites))), + lhs=getattr(site, "lhs", None), + rhs=getattr(site, "rhs", None), + m=_to_cv_axis_length_info( + getattr(site, "m", None), + axis_states, + override_length_expr=getattr(site, "m_total", None), + load_derived_symbols=load_derived_symbols, + ), + n=_to_cv_axis_length_info( + getattr(site, "n", None), + axis_states, + override_length_expr=getattr(site, "n_total", None), + load_derived_symbols=load_derived_symbols, + ), + k=_to_cv_axis_length_info( + getattr(site, "k", None), + axis_states, + override_length_expr=getattr(site, "k_total", None), + load_derived_symbols=load_derived_symbols, + ), + scope=None, + )) + + if not dot_sites: + diagnostics.append("no tl.dot/tl.conv site resolved in cube/mix parser.") + + role_param_names = _collect_role_param_names(dot_sites) + tunable_params: List[CvTunableParamInfo] = [] + seen = set() + for name in missing_tunable_params: + role = _infer_role(name, role_param_names) + tunable_params.append(CvTunableParamInfo(name=name, role=role, source="signature")) + seen.add(name) + + for role in ("M", "N", "K"): + for name in sorted(role_param_names[role]): + if name in seen: + continue + tunable_params.append(CvTunableParamInfo(name=name, role=role, source="axis_length")) + seen.add(name) + + status = CV_PARSE_STATUS_OK + if diagnostics: + status = CV_PARSE_STATUS_PARTIAL + if not signature_params and not dot_sites and not tunable_params: + status = CV_PARSE_STATUS_FAILED + + return CvParseResult( + version=CV_PARSER_VERSION_V1, + mode=mode, # type: ignore[arg-type] + status=status, # type: ignore[arg-type] + signature_params=signature_params, + dot_sites=dot_sites, + tunable_params=tunable_params, + missing_tunable_params=missing_tunable_params, + diagnostics=diagnostics, + ) diff --git a/third_party/ascend/backend/runtime/dsl_analysis/dot_role_annotator.py b/third_party/ascend/backend/runtime/dsl_analysis/dot_role_annotator.py new file mode 100644 index 0000000000..35ee0aec80 --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/dot_role_annotator.py @@ -0,0 +1,144 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from typing import Dict, Mapping, Optional + +from .role_engine import TensorRole, infer_value_states +from .symbolic_expr import SymbolicExpr + + +def _is_slice_all(node: ast.AST) -> bool: + return (isinstance(node, ast.Slice) and node.lower is None and node.upper is None and node.step is None) + + +def _is_tl_arange_call(node: ast.AST) -> bool: + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) and node.func.value.id == "tl" and node.func.attr == "arange") + + +def _get_arange_stop_expr(call_node: ast.Call) -> Optional[ast.AST]: + if len(call_node.args) >= 2: + return call_node.args[1] + if len(call_node.args) == 1: + return call_node.args[0] + for keyword in call_node.keywords: + if keyword.arg in ("end", "stop"): + return keyword.value + return None + + +def _extract_axis_symbol_from_subscript_base(base_node: ast.AST) -> Optional[str]: + if isinstance(base_node, ast.Name): + return base_node.id + if _is_tl_arange_call(base_node) and isinstance(base_node, ast.Call): + stop_expr = _get_arange_stop_expr(base_node) + if stop_expr is not None: + return str(SymbolicExpr.from_ast(stop_expr)) + + arange_stop_symbols = set() + for node in ast.walk(base_node): + if not _is_tl_arange_call(node): + continue + if not isinstance(node, ast.Call): + continue + stop_expr = _get_arange_stop_expr(node) + if stop_expr is None: + continue + arange_stop_symbols.add(str(SymbolicExpr.from_ast(stop_expr))) + if len(arange_stop_symbols) == 1: + return next(iter(arange_stop_symbols)) + return None + + +def _extract_subscript_axes(node: ast.Subscript) -> Optional[tuple]: + symbol_name = _extract_axis_symbol_from_subscript_base(node.value) + if symbol_name is None: + return None + slice_node = node.slice + if not isinstance(slice_node, ast.Tuple): + return None + if len(slice_node.elts) != 2: + return None + first_dim = slice_node.elts[0] + second_dim = slice_node.elts[1] + # var[:, None] => row axis + if _is_slice_all(first_dim) and isinstance(second_dim, ast.Constant) and second_dim.value is None: + return symbol_name, 0 + # var[None, :] => col axis + if isinstance(first_dim, ast.Constant) and first_dim.value is None and _is_slice_all(second_dim): + return symbol_name, 1 + return None + + +def infer_tensor_role_from_expr(expr: ast.AST) -> TensorRole: + row_symbol = None + col_symbol = None + for node in ast.walk(expr): + if not isinstance(node, ast.Subscript): + continue + axes = _extract_subscript_axes(node) + if axes is None: + continue + symbol_name, axis_idx = axes + if axis_idx == 0 and row_symbol is None: + row_symbol = SymbolicExpr(text=symbol_name) + if axis_idx == 1 and col_symbol is None: + col_symbol = SymbolicExpr(text=symbol_name) + return TensorRole( + row_symbol=row_symbol, + col_symbol=col_symbol, + total_row_symbol=row_symbol, + total_col_symbol=col_symbol, + ) + + +def annotate_tensor_roles( + func_ast: ast.AST, + inherited_roles: Optional[Mapping[str, TensorRole]] = None, +) -> Dict[str, TensorRole]: + states = infer_value_states(func_ast, inherited_roles=inherited_roles) + roles: Dict[str, TensorRole] = {} + for name, state in states.items(): + if state.kind != "tensor" or state.role is None: + continue + if state.role.row_symbol is None and state.role.col_symbol is None: + continue + roles[name] = state.role + + # Keep legacy fallback from explicit subscript patterns to avoid regressions + # in kernels that are not yet covered by role_engine rules. + for node in ast.walk(func_ast): + if not isinstance(node, ast.Assign): + continue + if len(node.targets) != 1: + continue + target = node.targets[0] + if not isinstance(target, ast.Name): + continue + if target.id in roles: + continue + role = infer_tensor_role_from_expr(node.value) + if role.row_symbol is None or role.col_symbol is None: + continue + roles[target.id] = role + return roles diff --git a/third_party/ascend/backend/runtime/dsl_analysis/dynamic_source_utils.py b/third_party/ascend/backend/runtime/dsl_analysis/dynamic_source_utils.py new file mode 100644 index 0000000000..6d35f007cb --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/dynamic_source_utils.py @@ -0,0 +1,56 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from typing import Optional, Set + + +def extract_expr_names_from_text(expr_text: Optional[str]) -> Set[str]: + if not expr_text: + return set() + try: + parsed = ast.parse(expr_text, mode="eval") + except Exception: + try: + parsed = ast.parse(expr_text, mode="exec") + except Exception: + return set() + return {node.id for node in ast.walk(parsed) if isinstance(node, ast.Name)} + + +def resolve_dynamic_source( + axis_symbol: Optional[str], + length_expr: Optional[str], + load_derived_symbols: Set[str], +) -> str: + if not load_derived_symbols: + return "none" + + names = extract_expr_names_from_text(length_expr) + if not names and axis_symbol is not None: + names = extract_expr_names_from_text(axis_symbol) + if not names and axis_symbol.isidentifier(): + names = {axis_symbol} + + if names.intersection(load_derived_symbols): + return "dynamic_from_load" + return "none" diff --git a/third_party/ascend/backend/runtime/dsl_analysis/entry.py b/third_party/ascend/backend/runtime/dsl_analysis/entry.py new file mode 100644 index 0000000000..41e3715bc3 --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/entry.py @@ -0,0 +1,164 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from dataclasses import dataclass +from typing import Dict, List, Optional, Sequence + +from .axis_length_resolver import resolve_axis_length_states +from .launch_analyzer import analyze_launch_semantics +from .missing_tunable_detector import MissingTunableDetector +from .missing_tunable_policies import CubeMixPolicy, VectorPolicy +from .schema import (AxisLengthStateResult, MissingTunableContext, MissingTunablePolicy, SignatureInfo) +from .shape_inference import (DotSiteMNK, infer_dot_sites_mnk, infer_dot_sites_mnk_interprocedural) +from .signature_analyzer import extract_signature_info + + +@dataclass(frozen=True) +class SignatureMissingTunableResult: + signature: SignatureInfo + missing_tunable: List[str] + + +@dataclass(frozen=True) +class DotSiteMNKResult: + dot_sites: List[DotSiteMNK] + + +def resolve_missing_tunable_policy(parser_mode: Optional[str]) -> MissingTunablePolicy: + mode = (parser_mode or "vector").lower() + if mode in ("cube", "mix"): + return CubeMixPolicy() + return VectorPolicy() + + +def analyze_signature_and_missing_tunable( + func_ast: ast.AST, + arg_names: Sequence[str], + provided_args: Dict[str, object], + split_params: Optional[Dict[str, str]] = None, + tiling_params: Optional[Dict[str, str]] = None, + explicit_tunable_params: Optional[Sequence[str]] = None, + parser_mode: Optional[str] = None, + policy: Optional[MissingTunablePolicy] = None, +) -> SignatureMissingTunableResult: + split_params = split_params or {} + tiling_params = tiling_params or {} + mode = (parser_mode or "vector").lower() + + signature = extract_signature_info(func_ast) + launch_info = analyze_launch_semantics(func_ast) + context = MissingTunableContext( + arg_names=arg_names, + provided_args=provided_args, + signature=signature, + split_params=split_params, + tiling_params=tiling_params, + launch_info=launch_info, + ) + + resolved_policy = policy or resolve_missing_tunable_policy(parser_mode) + detector = MissingTunableDetector(resolved_policy) + missing_tunable = detector.detect(context) + + explicit_candidates = list(explicit_tunable_params or []) + if explicit_candidates: + signature_names = set(signature.parameter_names()) + invalid_names = [ + name for name in explicit_candidates if not isinstance(name, str) or name not in signature_names + ] + if invalid_names: + raise ValueError("hints.tunable_parameter contains unknown parameters: {}".format(invalid_names)) + non_constexpr = [name for name in explicit_candidates if not signature.is_constexpr(name)] + if non_constexpr: + raise ValueError( + "hints.tunable_parameter must reference constexpr parameters only: {}".format(non_constexpr)) + provided = set(provided_args.keys()) + for name in explicit_candidates: + if name in provided: + continue + if name in missing_tunable: + continue + missing_tunable.append(name) + + return SignatureMissingTunableResult( + signature=signature, + missing_tunable=missing_tunable, + ) + + +def analyze_dot_site_mnk( + func_ast: ast.AST, + parser_mode: Optional[str] = None, + module_ast: Optional[ast.AST] = None, + entry_function_name: Optional[str] = None, +) -> DotSiteMNKResult: + mode = (parser_mode or "vector").lower() + if mode not in ("cube", "mix"): + return DotSiteMNKResult(dot_sites=[]) + if module_ast is not None and entry_function_name: + dot_sites = infer_dot_sites_mnk_interprocedural( + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + return DotSiteMNKResult(dot_sites=dot_sites) + dot_sites = infer_dot_sites_mnk(func_ast) + return DotSiteMNKResult(dot_sites=dot_sites) + + +def analyze_axis_length_state( + func_ast: ast.AST, + parser_mode: Optional[str], + arg_names: Sequence[str], + provided_args: Dict[str, object], + module_ast: Optional[ast.AST] = None, + entry_function_name: Optional[str] = None, +) -> AxisLengthStateResult: + del arg_names + mode = (parser_mode or "vector").lower() + if mode not in ("cube", "mix"): + return AxisLengthStateResult(axis_states={}) + + dot_result = analyze_dot_site_mnk( + func_ast=func_ast, + parser_mode=mode, + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + dot_sites = dot_result.dot_sites + axis_symbols = [] + for site in dot_sites: + if site.m is not None: + axis_symbols.append(site.m) + if site.n is not None: + axis_symbols.append(site.n) + if site.k is not None: + axis_symbols.append(site.k) + + signature = extract_signature_info(func_ast) + axis_states = resolve_axis_length_states( + func_ast=func_ast, + axis_symbols=axis_symbols, + signature=signature, + provided_args=provided_args, + ) + return AxisLengthStateResult(axis_states=axis_states) diff --git a/third_party/ascend/backend/runtime/dsl_analysis/kernel_classifier.py b/third_party/ascend/backend/runtime/dsl_analysis/kernel_classifier.py new file mode 100644 index 0000000000..69aedeef8c --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/kernel_classifier.py @@ -0,0 +1,78 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from typing import Any, Dict, Optional + +SUPPORTED_KERNEL_TYPES = {"vector", "cube", "mix", "auto"} +_MIX_OPERATOR_ATTRS = {"dot"} + + +def _get_hint_kernel_type(hints: Optional[Dict[str, Any]]) -> str: + if not hints: + return "auto" + + kernel_type = hints.get("kernel_type", "auto") + if isinstance(kernel_type, str): + kernel_type = kernel_type.strip().lower() + + if kernel_type not in SUPPORTED_KERNEL_TYPES: + raise ValueError(f"Unsupported kernel_type '{kernel_type}', expected one of: " + f"{sorted(SUPPORTED_KERNEL_TYPES)}") + return kernel_type + + +def _base_name(expr: ast.AST) -> Optional[str]: + node = expr + while isinstance(node, ast.Attribute): + node = node.value + if isinstance(node, ast.Name): + return node.id + return None + + +def classify_kernel_type_from_dsl(func_ast: Optional[ast.AST]) -> str: + if func_ast is None: + return "vector" + try: + for node in ast.walk(func_ast): + if not isinstance(node, ast.Call): + continue + if not isinstance(node.func, ast.Attribute): + continue + if node.func.attr not in _MIX_OPERATOR_ATTRS: + continue + if _base_name(node.func) == "tl": + return "mix" + except Exception: + return "vector" + return "vector" + + +def resolve_kernel_type( + hints: Optional[Dict[str, Any]], + func_ast: Optional[ast.AST] = None, +) -> str: + kernel_type = _get_hint_kernel_type(hints) + if kernel_type != "auto": + return kernel_type + return classify_kernel_type_from_dsl(func_ast) diff --git a/third_party/ascend/backend/runtime/dsl_analysis/launch_analyzer.py b/third_party/ascend/backend/runtime/dsl_analysis/launch_analyzer.py new file mode 100644 index 0000000000..f58c1e4c7a --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/launch_analyzer.py @@ -0,0 +1,55 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from dataclasses import dataclass +from typing import Set + +_CUBE_MIX_TUNABLE_KEYWORDS = ( + "BLOCK", + "TILE", + "CHUNK", + "SPLIT", + "GROUP", + "WARP", + "STAGE", +) + + +@dataclass(frozen=True) +class LaunchSemantics: + launch_related_names: Set[str] + + +def analyze_launch_semantics(func_ast: ast.AST) -> LaunchSemantics: + launch_related_names = set() + for node in ast.walk(func_ast): + if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load): + launch_related_names.add(node.id) + return LaunchSemantics(launch_related_names=launch_related_names) + + +def looks_like_cube_mix_tunable(name: str) -> bool: + upper_name = name.upper() + if upper_name in {"M", "N", "K"}: + return True + return any(keyword in upper_name for keyword in _CUBE_MIX_TUNABLE_KEYWORDS) diff --git a/third_party/ascend/backend/runtime/dsl_analysis/load_dependency_analyzer.py b/third_party/ascend/backend/runtime/dsl_analysis/load_dependency_analyzer.py new file mode 100644 index 0000000000..700c8c1bf4 --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/load_dependency_analyzer.py @@ -0,0 +1,340 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from typing import Dict, Iterable, List, Mapping, Optional, Set, Tuple + + +def _is_tl_load_call(node: ast.AST) -> bool: + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) and node.func.value.id == "tl" and node.func.attr == "load") + + +def _expr_has_tl_load(expr: ast.AST) -> bool: + for node in ast.walk(expr): + if _is_tl_load_call(node): + return True + return False + + +def _extract_target_names(target: ast.AST) -> List[str]: + if isinstance(target, ast.Name): + return [target.id] + if isinstance(target, (ast.Tuple, ast.List)): + names: List[str] = [] + for elt in target.elts: + names.extend(_extract_target_names(elt)) + return names + return [] + + +def _find_function_by_name(tree: ast.AST, function_name: str) -> Optional[ast.AST]: + for node in ast.walk(tree): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if node.name == function_name: + return node + return None + + +def _iter_assignments(tree: ast.AST) -> Iterable[Tuple[List[str], ast.AST]]: + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + if node.value is None: + continue + target_names: List[str] = [] + for target in node.targets: + target_names.extend(_extract_target_names(target)) + if target_names: + yield target_names, node.value + continue + + if isinstance(node, ast.AnnAssign): + if node.value is None: + continue + target_names = _extract_target_names(node.target) + if target_names: + yield target_names, node.value + continue + + if isinstance(node, ast.NamedExpr): + target_names = _extract_target_names(node.target) + if target_names: + yield target_names, node.value + + +def _expr_uses_symbols(expr: ast.AST, symbols: Set[str]) -> bool: + if not symbols: + return False + for node in ast.walk(expr): + if isinstance(node, ast.Name) and node.id in symbols: + return True + return False + + +def _legacy_collect_load_derived_symbols(tree: ast.AST) -> Set[str]: + assignments = list(_iter_assignments(tree)) + load_derived: Set[str] = set() + + for target_names, value in assignments: + if not _is_tl_load_call(value): + continue + for name in target_names: + load_derived.add(name) + + changed = True + while changed: + changed = False + for target_names, value in assignments: + if not _expr_uses_symbols(value, load_derived): + continue + for name in target_names: + if name in load_derived: + continue + load_derived.add(name) + changed = True + + return load_derived + + +def _to_static_bool(value: object) -> Optional[bool]: + if isinstance(value, bool): + return value + if isinstance(value, int): + return bool(value) + return None + + +def _resolve_static_scalar(expr: ast.AST, known_values: Mapping[str, object]) -> Optional[object]: + if isinstance(expr, ast.Constant): + if isinstance(expr.value, (bool, int, float)): + return expr.value + return None + if isinstance(expr, ast.Name): + value = known_values.get(expr.id, None) + if isinstance(value, (bool, int, float)): + return value + return None + if isinstance(expr, ast.UnaryOp): + operand = _resolve_static_scalar(expr.operand, known_values) + if operand is None: + return None + if isinstance(expr.op, ast.Not): + operand_bool = _to_static_bool(operand) + if operand_bool is None: + return None + return not operand_bool + if isinstance(expr.op, ast.USub): + if isinstance(operand, (int, float)): + return -operand + return None + if isinstance(expr.op, ast.UAdd): + if isinstance(operand, (int, float)): + return +operand + return None + return None + if isinstance(expr, ast.BoolOp): + if isinstance(expr.op, ast.And): + maybe_unknown = False + for value_expr in expr.values: + value = _resolve_static_bool_expr(value_expr, known_values) + if value is False: + return False + if value is None: + maybe_unknown = True + return None if maybe_unknown else True + if isinstance(expr.op, ast.Or): + maybe_unknown = False + for value_expr in expr.values: + value = _resolve_static_bool_expr(value_expr, known_values) + if value is True: + return True + if value is None: + maybe_unknown = True + return None if maybe_unknown else False + return None + if isinstance(expr, ast.Compare): + if len(expr.ops) != 1 or len(expr.comparators) != 1: + return None + lhs = _resolve_static_scalar(expr.left, known_values) + rhs = _resolve_static_scalar(expr.comparators[0], known_values) + if lhs is None or rhs is None: + return None + op = expr.ops[0] + if isinstance(op, ast.Eq): + return lhs == rhs + if isinstance(op, ast.NotEq): + return lhs != rhs + if isinstance(op, ast.Lt): + return lhs < rhs + if isinstance(op, ast.LtE): + return lhs <= rhs + if isinstance(op, ast.Gt): + return lhs > rhs + if isinstance(op, ast.GtE): + return lhs >= rhs + return None + return None + + +def _resolve_static_bool_expr(expr: ast.AST, known_values: Mapping[str, object]) -> Optional[bool]: + scalar = _resolve_static_scalar(expr, known_values) + if scalar is None: + return None + return _to_static_bool(scalar) + + +def _assign_target_value( + target_names: List[str], + value_expr: ast.AST, + load_derived: Set[str], +) -> None: + is_derived = _expr_has_tl_load(value_expr) or _expr_uses_symbols(value_expr, load_derived) + if is_derived: + for name in target_names: + load_derived.add(name) + return + for name in target_names: + load_derived.discard(name) + + +def _visit_stmt( + stmt: ast.stmt, + load_derived: Set[str], + known_values: Mapping[str, object], +) -> None: + if isinstance(stmt, ast.Assign): + if stmt.value is None: + return + target_names: List[str] = [] + for target in stmt.targets: + target_names.extend(_extract_target_names(target)) + if target_names: + _assign_target_value(target_names, stmt.value, load_derived) + return + + if isinstance(stmt, ast.AnnAssign): + if stmt.value is None: + return + target_names = _extract_target_names(stmt.target) + if target_names: + _assign_target_value(target_names, stmt.value, load_derived) + return + + if isinstance(stmt, ast.AugAssign): + target_names = _extract_target_names(stmt.target) + if not target_names: + return + is_derived = (_expr_has_tl_load(stmt.value) or _expr_uses_symbols(stmt.value, load_derived) + or any(name in load_derived for name in target_names)) + if is_derived: + for name in target_names: + load_derived.add(name) + return + + if isinstance(stmt, ast.If): + cond = _resolve_static_bool_expr(stmt.test, known_values) + if cond is True: + for child in stmt.body: + _visit_stmt(child, load_derived, known_values) + return + if cond is False: + for child in stmt.orelse: + _visit_stmt(child, load_derived, known_values) + return + body_load_derived = set(load_derived) + else_load_derived = set(load_derived) + for child in stmt.body: + _visit_stmt(child, body_load_derived, known_values) + for child in stmt.orelse: + _visit_stmt(child, else_load_derived, known_values) + load_derived.clear() + load_derived.update(body_load_derived.union(else_load_derived)) + return + + if isinstance(stmt, ast.For): + body_load_derived = set(load_derived) + for child in stmt.body: + _visit_stmt(child, body_load_derived, known_values) + for child in stmt.orelse: + _visit_stmt(child, body_load_derived, known_values) + load_derived.update(body_load_derived) + return + + if isinstance(stmt, ast.While): + body_load_derived = set(load_derived) + for child in stmt.body: + _visit_stmt(child, body_load_derived, known_values) + for child in stmt.orelse: + _visit_stmt(child, body_load_derived, known_values) + load_derived.update(body_load_derived) + return + + if isinstance(stmt, ast.With): + for child in stmt.body: + _visit_stmt(child, load_derived, known_values) + return + + +def _collect_load_derived_from_function( + function_node: ast.AST, + provided_args: Optional[Mapping[str, object]] = None, +) -> Set[str]: + known_values: Dict[str, object] = {} + for name, value in (provided_args or {}).items(): + if isinstance(value, (bool, int, float)): + known_values[name] = value + + load_derived: Set[str] = set() + body = getattr(function_node, "body", None) + if not isinstance(body, list): + return load_derived + + for stmt in body: + if isinstance(stmt, ast.stmt): + _visit_stmt(stmt, load_derived, known_values) + return load_derived + + +def collect_load_derived_symbols( + tree: ast.AST, + *, + provided_args: Optional[Mapping[str, object]] = None, + entry_function_name: Optional[str] = None, +) -> Set[str]: + if isinstance(tree, (ast.FunctionDef, ast.AsyncFunctionDef)): + return _collect_load_derived_from_function( + tree, + provided_args=provided_args, + ) + + if isinstance(tree, ast.Module): + if entry_function_name: + entry_func = _find_function_by_name(tree, entry_function_name) + if isinstance(entry_func, (ast.FunctionDef, ast.AsyncFunctionDef)): + return _collect_load_derived_from_function( + entry_func, + provided_args=provided_args, + ) + return _legacy_collect_load_derived_symbols(tree) + + return set() diff --git a/third_party/ascend/backend/runtime/dsl_analysis/load_semantics.py b/third_party/ascend/backend/runtime/dsl_analysis/load_semantics.py new file mode 100644 index 0000000000..12a5c0a59e --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/load_semantics.py @@ -0,0 +1,662 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from typing import Dict, List, Mapping, Optional, Sequence, Set, Tuple + + +def ast_to_text(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Constant): + return str(node.value) + return ast.unparse(node) + + +def is_tl_call(node: ast.AST, name: str) -> bool: + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) and node.func.value.id == "tl" and node.func.attr == name) + + +def iter_walk_in_order(node: ast.AST): + yield node + for child in ast.iter_child_nodes(node): + yield from iter_walk_in_order(child) + + +def get_arange_stop_expr(call_node: ast.Call) -> Optional[ast.AST]: + if len(call_node.args) >= 2: + return call_node.args[1] + if len(call_node.args) == 1: + return call_node.args[0] + for keyword in call_node.keywords: + if keyword.arg in ("end", "stop"): + return keyword.value + return None + + +def collect_arange_stop_texts(expr: ast.AST) -> List[str]: + values: List[str] = [] + seen = set() + for node in iter_walk_in_order(expr): + if not is_tl_call(node, "arange"): + continue + stop_expr = get_arange_stop_expr(node) + if stop_expr is None: + continue + text = ast_to_text(stop_expr) + if text in seen: + continue + seen.add(text) + values.append(text) + return values + + +def is_slice_all(node: ast.AST) -> bool: + return (isinstance(node, ast.Slice) and node.lower is None and node.upper is None and node.step is None) + + +def is_none_const(node: ast.AST) -> bool: + return isinstance(node, ast.Constant) and node.value is None + + +def extract_axis_symbol_from_subscript_base(base_node: ast.AST) -> Optional[str]: + if isinstance(base_node, ast.Name): + return base_node.id + if is_tl_call(base_node, "arange") and isinstance(base_node, ast.Call): + stop_expr = get_arange_stop_expr(base_node) + if stop_expr is not None: + return ast_to_text(stop_expr) + arange_lengths = collect_arange_stop_texts(base_node) + if len(arange_lengths) == 1: + return arange_lengths[0] + return None + + +def extract_subscript_axis_info(node: ast.Subscript) -> Optional[Tuple[int, str, int]]: + symbol = extract_axis_symbol_from_subscript_base(node.value) + if symbol is None: + return None + if not isinstance(node.slice, ast.Tuple): + return None + dims = node.slice.elts + if len(dims) < 2 or len(dims) > 4: + return None + axis_index = None + for idx, dim in enumerate(dims): + if is_slice_all(dim): + if axis_index is not None: + return None + axis_index = idx + continue + if is_none_const(dim): + continue + return None + if axis_index is None: + return None + return axis_index, symbol, len(dims) + + +def extract_target_names(target: ast.AST) -> List[str]: + if isinstance(target, ast.Name): + return [target.id] + if isinstance(target, (ast.Tuple, ast.List)): + names: List[str] = [] + for item in target.elts: + names.extend(extract_target_names(item)) + return names + return [] + + +def to_static_bool(value: object) -> Optional[bool]: + if isinstance(value, bool): + return value + if isinstance(value, int): + return bool(value) + return None + + +def resolve_static_scalar(expr: ast.AST, known_values: Mapping[str, object]) -> Optional[object]: + if isinstance(expr, ast.Constant): + if isinstance(expr.value, (bool, int, float)): + return expr.value + return None + if isinstance(expr, ast.Name): + value = known_values.get(expr.id, None) + if isinstance(value, (bool, int, float)): + return value + return None + if isinstance(expr, ast.UnaryOp): + operand = resolve_static_scalar(expr.operand, known_values) + if operand is None: + return None + if isinstance(expr.op, ast.Not): + operand_bool = to_static_bool(operand) + if operand_bool is None: + return None + return not operand_bool + if isinstance(expr.op, ast.USub): + if isinstance(operand, (int, float)): + return -operand + return None + if isinstance(expr.op, ast.UAdd): + if isinstance(operand, (int, float)): + return +operand + return None + return None + if isinstance(expr, ast.BoolOp): + if isinstance(expr.op, ast.And): + maybe_unknown = False + for child in expr.values: + value = resolve_static_bool_expr(child, known_values) + if value is False: + return False + if value is None: + maybe_unknown = True + return None if maybe_unknown else True + if isinstance(expr.op, ast.Or): + maybe_unknown = False + for child in expr.values: + value = resolve_static_bool_expr(child, known_values) + if value is True: + return True + if value is None: + maybe_unknown = True + return None if maybe_unknown else False + return None + if isinstance(expr, ast.Compare): + if len(expr.ops) != 1 or len(expr.comparators) != 1: + return None + lhs = resolve_static_scalar(expr.left, known_values) + rhs = resolve_static_scalar(expr.comparators[0], known_values) + if lhs is None or rhs is None: + return None + op = expr.ops[0] + if isinstance(op, ast.Eq): + return lhs == rhs + if isinstance(op, ast.NotEq): + return lhs != rhs + if isinstance(op, ast.Lt): + return lhs < rhs + if isinstance(op, ast.LtE): + return lhs <= rhs + if isinstance(op, ast.Gt): + return lhs > rhs + if isinstance(op, ast.GtE): + return lhs >= rhs + return None + return None + + +def resolve_static_bool_expr(expr: ast.AST, known_values: Mapping[str, object]) -> Optional[bool]: + scalar = resolve_static_scalar(expr, known_values) + if scalar is None: + return None + return to_static_bool(scalar) + + +def clone_assignment_expr_map(src: Mapping[str, Sequence[ast.AST]]) -> Dict[str, List[ast.AST]]: + return {name: list(exprs) for name, exprs in src.items()} + + +def append_unique_expr(values: List[ast.AST], expr: ast.AST) -> None: + expr_dump = ast.dump(expr) + for current in values: + if ast.dump(current) == expr_dump: + return + values.append(expr) + + +def merge_assignment_expr_map( + dst: Dict[str, List[ast.AST]], + src: Mapping[str, Sequence[ast.AST]], +) -> None: + for name, exprs in src.items(): + bucket = dst.setdefault(name, []) + for expr in exprs: + append_unique_expr(bucket, expr) + + +def set_assignment_expr( + assignment_expr_map: Dict[str, List[ast.AST]], + target_names: Sequence[str], + value_expr: ast.AST, +) -> None: + for name in target_names: + assignment_expr_map[name] = [value_expr] + + +def visit_stmt_for_assignment_map( + stmt: ast.stmt, + assignment_expr_map: Dict[str, List[ast.AST]], + known_values: Mapping[str, object], +) -> None: + if isinstance(stmt, ast.Assign): + target_names: List[str] = [] + for target in stmt.targets: + target_names.extend(extract_target_names(target)) + if target_names: + set_assignment_expr(assignment_expr_map, target_names, stmt.value) + return + + if isinstance(stmt, ast.AnnAssign): + if stmt.value is None: + return + target_names = extract_target_names(stmt.target) + if target_names: + set_assignment_expr(assignment_expr_map, target_names, stmt.value) + return + + if isinstance(stmt, ast.If): + cond = resolve_static_bool_expr(stmt.test, known_values) + if cond is True: + for child in stmt.body: + visit_stmt_for_assignment_map(child, assignment_expr_map, known_values) + return + if cond is False: + for child in stmt.orelse: + visit_stmt_for_assignment_map(child, assignment_expr_map, known_values) + return + body_map = clone_assignment_expr_map(assignment_expr_map) + else_map = clone_assignment_expr_map(assignment_expr_map) + for child in stmt.body: + visit_stmt_for_assignment_map(child, body_map, known_values) + for child in stmt.orelse: + visit_stmt_for_assignment_map(child, else_map, known_values) + assignment_expr_map.clear() + merge_assignment_expr_map(assignment_expr_map, body_map) + merge_assignment_expr_map(assignment_expr_map, else_map) + return + + if isinstance(stmt, ast.For): + loop_map = clone_assignment_expr_map(assignment_expr_map) + for child in stmt.body: + visit_stmt_for_assignment_map(child, loop_map, known_values) + for child in stmt.orelse: + visit_stmt_for_assignment_map(child, loop_map, known_values) + merge_assignment_expr_map(assignment_expr_map, loop_map) + return + + if isinstance(stmt, ast.While): + loop_map = clone_assignment_expr_map(assignment_expr_map) + for child in stmt.body: + visit_stmt_for_assignment_map(child, loop_map, known_values) + for child in stmt.orelse: + visit_stmt_for_assignment_map(child, loop_map, known_values) + merge_assignment_expr_map(assignment_expr_map, loop_map) + return + + if isinstance(stmt, ast.With): + for child in stmt.body: + visit_stmt_for_assignment_map(child, assignment_expr_map, known_values) + return + + +def build_assignment_expr_map( + func_node: ast.AST, + provided_args: Optional[Mapping[str, object]] = None, +) -> Dict[str, List[ast.AST]]: + known_values: Dict[str, object] = {} + for name, value in (provided_args or {}).items(): + if isinstance(value, (bool, int, float)): + known_values[name] = value + + result: Dict[str, List[ast.AST]] = {} + body = getattr(func_node, "body", None) + if not isinstance(body, list): + return result + for stmt in body: + if not isinstance(stmt, ast.stmt): + continue + visit_stmt_for_assignment_map(stmt, result, known_values) + return result + + +def extract_name_ids(expr: ast.AST) -> Set[str]: + return {node.id for node in ast.walk(expr) if isinstance(node, ast.Name)} + + +def extract_name_depths_transitive( + expr: ast.AST, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], +) -> Dict[str, int]: + name_depths = {name: 0 for name in extract_name_ids(expr)} + queue = list(name_depths.keys()) + while queue: + name = queue.pop() + base_depth = name_depths[name] + for assigned_expr in assignment_expr_map.get(name, ()): + for sub_name in extract_name_ids(assigned_expr): + next_depth = base_depth + 1 + cur_depth = name_depths.get(sub_name, None) + if cur_depth is not None and cur_depth <= next_depth: + continue + name_depths[sub_name] = next_depth + queue.append(sub_name) + return name_depths + + +def extract_name_ids_transitive( + expr: ast.AST, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], +) -> Set[str]: + return set(extract_name_depths_transitive(expr, assignment_expr_map).keys()) + + +def collect_symbol_dependency_names( + symbol: str, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], +) -> Set[str]: + names = {symbol} + for expr in assignment_expr_map.get(symbol, ()): + names.update(extract_name_ids_transitive(expr, assignment_expr_map)) + return names + + +def build_symbol_user_expr_map(assignment_expr_map: Mapping[str, Sequence[ast.AST]], ) -> Dict[str, List[ast.AST]]: + result: Dict[str, List[ast.AST]] = {} + for exprs in assignment_expr_map.values(): + for expr in exprs: + seen = set() + for node in ast.walk(expr): + if not isinstance(node, ast.Subscript): + continue + axis_info = extract_subscript_axis_info(node) + if axis_info is None: + continue + _, symbol, _ = axis_info + if symbol in seen: + continue + seen.add(symbol) + result.setdefault(symbol, []).append(expr) + return result + + +def pick_param_candidate( + expr: ast.AST, + *, + param_names: Set[str], + exclude: Set[str], + assignment_expr_map: Mapping[str, Sequence[ast.AST]], +) -> Optional[str]: + name_depths = extract_name_depths_transitive(expr, assignment_expr_map) + candidates = [name for name, depth in name_depths.items() if name in param_names and name not in exclude] + if not candidates: + return None + min_depth = min(name_depths[name] for name in candidates) + candidates = sorted(name for name in candidates if name_depths[name] == min_depth) + for preferred in ("M", "N", "K"): + if preferred in candidates: + return preferred + return candidates[0] + + +def extract_axis_symbols_from_expr(expr: ast.AST, axis_candidates: Set[str]) -> Set[str]: + symbols: Set[str] = set() + for node in ast.walk(expr): + if isinstance(node, ast.Name) and node.id in axis_candidates: + symbols.add(node.id) + continue + if not isinstance(node, ast.Subscript): + continue + axis_info = extract_subscript_axis_info(node) + if axis_info is None: + continue + _, symbol, _ = axis_info + if symbol in axis_candidates: + symbols.add(symbol) + return symbols + + +def extract_range_stop_and_step(node: ast.For) -> Tuple[Optional[ast.AST], Optional[ast.AST]]: + if not isinstance(node.iter, ast.Call): + return None, None + iter_fn = node.iter.func + is_range = isinstance(iter_fn, ast.Name) and iter_fn.id == "range" + is_tl_range = (isinstance(iter_fn, ast.Attribute) and isinstance(iter_fn.value, ast.Name) + and iter_fn.value.id == "tl" and iter_fn.attr == "range") + if not (is_range or is_tl_range): + return None, None + + args = list(node.iter.args) + if len(args) == 0: + return None, None + if len(args) == 1: + return args[0], None + if len(args) == 2: + return args[1], None + return args[1], args[2] + + +def extract_param_names(func_node: ast.AST) -> Set[str]: + if not isinstance(func_node, (ast.FunctionDef, ast.AsyncFunctionDef)): + return set() + args_node = func_node.args + ordered_args = list(args_node.posonlyargs) + list(args_node.args) + list(args_node.kwonlyargs) + return {arg.arg for arg in ordered_args if isinstance(arg, ast.arg)} + + +def collect_axis_candidates(func_node: ast.AST, param_names: Optional[Set[str]] = None) -> Set[str]: + axis_candidates: Set[str] = set() + for node in ast.walk(func_node): + if not isinstance(node, ast.Assign): + continue + if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name): + continue + target_name = node.targets[0].id + if is_tl_call(node.value, "arange"): + axis_candidates.add(target_name) + continue + arange_lengths = collect_arange_stop_texts(node.value) + if len(arange_lengths) == 1: + axis_candidates.add(target_name) + continue + if isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.Mod): + axis_candidates.add(target_name) + continue + for sub_node in ast.walk(node.value): + if not isinstance(sub_node, ast.Subscript): + continue + axis_info = extract_subscript_axis_info(sub_node) + if axis_info is None: + continue + axis_candidates.add(target_name) + break + + if param_names: + for node in ast.walk(func_node): + if not isinstance(node, ast.Subscript): + continue + axis_info = extract_subscript_axis_info(node) + if axis_info is None: + continue + _, symbol, _ = axis_info + if symbol in param_names: + axis_candidates.add(symbol) + return axis_candidates + + +def extract_axis_block_symbol_map( + func_node: ast.AST, + axis_candidates: Set[str], + assignment_expr_map: Optional[Mapping[str, Sequence[ast.AST]]] = None, +) -> Dict[str, str]: + axis_block_symbol_map: Dict[str, str] = {} + for node in ast.walk(func_node): + if not isinstance(node, ast.Assign): + continue + if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name): + continue + target_name = node.targets[0].id + if target_name not in axis_candidates: + continue + arange_lengths = collect_arange_stop_texts(node.value) + if len(arange_lengths) == 1: + axis_block_symbol_map[target_name] = arange_lengths[0] + + if not assignment_expr_map: + return axis_block_symbol_map + + changed = True + while changed: + changed = False + for symbol, exprs in assignment_expr_map.items(): + if symbol in axis_block_symbol_map: + continue + candidate_blocks = set() + for expr in exprs: + for dep_name in extract_name_ids_transitive(expr, assignment_expr_map): + if dep_name == symbol: + continue + block_symbol = axis_block_symbol_map.get(dep_name) + if block_symbol is None: + continue + candidate_blocks.add(block_symbol) + if len(candidate_blocks) != 1: + continue + axis_block_symbol_map[symbol] = next(iter(candidate_blocks)) + changed = True + return axis_block_symbol_map + + +def extract_axis_total_symbol_map( + func_node: ast.AST, + *, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], + axis_candidates: Set[str], + param_names: Set[str], +) -> Dict[str, str]: + axis_block_symbol_map = extract_axis_block_symbol_map( + func_node, + axis_candidates, + assignment_expr_map=assignment_expr_map, + ) + axis_total_symbol_map: Dict[str, str] = {} + + for node in ast.walk(func_node): + if not isinstance(node, ast.Assign): + continue + if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name): + continue + target_name = node.targets[0].id + if target_name not in axis_candidates: + continue + if not isinstance(node.value, ast.BinOp) or not isinstance(node.value.op, ast.Mod): + continue + candidate = pick_param_candidate( + node.value.right, + param_names=param_names, + exclude={target_name}, + assignment_expr_map=assignment_expr_map, + ) + if candidate is not None: + axis_total_symbol_map[target_name] = candidate + else: + axis_total_symbol_map[target_name] = ast_to_text(node.value.right) + + for node in ast.walk(func_node): + if not isinstance(node, ast.Compare): + continue + if len(node.ops) != 1 or len(node.comparators) != 1: + continue + left_expr = node.left + right_expr = node.comparators[0] + + left_axes = extract_axis_symbols_from_expr(left_expr, axis_candidates=axis_candidates) + right_axes = extract_axis_symbols_from_expr(right_expr, axis_candidates=axis_candidates) + + left_only = left_axes - right_axes + if left_only: + candidate = pick_param_candidate( + right_expr, + param_names=param_names, + exclude=set(left_only), + assignment_expr_map=assignment_expr_map, + ) + if candidate is not None: + for axis_name in left_only: + axis_total_symbol_map.setdefault(axis_name, candidate) + + right_only = right_axes - left_axes + if right_only: + candidate = pick_param_candidate( + left_expr, + param_names=param_names, + exclude=set(right_only), + assignment_expr_map=assignment_expr_map, + ) + if candidate is not None: + for axis_name in right_only: + axis_total_symbol_map.setdefault(axis_name, candidate) + + for node in ast.walk(func_node): + if not isinstance(node, ast.For): + continue + stop_expr, step_expr = extract_range_stop_and_step(node) + if stop_expr is None: + continue + total_candidate = pick_param_candidate( + stop_expr, + param_names=param_names, + exclude=set(), + assignment_expr_map=assignment_expr_map, + ) + if total_candidate is None: + continue + stop_names = extract_name_ids_transitive(stop_expr, assignment_expr_map) + step_names = (extract_name_ids_transitive(step_expr, assignment_expr_map) if step_expr is not None else set()) + if not stop_names: + if not step_names: + continue + for axis_name, block_symbol in axis_block_symbol_map.items(): + if axis_name in axis_total_symbol_map: + continue + if block_symbol not in stop_names and block_symbol not in step_names: + continue + axis_total_symbol_map[axis_name] = total_candidate + + changed = True + while changed: + changed = False + for symbol, exprs in assignment_expr_map.items(): + if symbol in axis_total_symbol_map: + continue + symbol_block = axis_block_symbol_map.get(symbol) + if symbol_block is None: + continue + candidate_totals = set() + for expr in exprs: + for dep_name in extract_name_ids_transitive(expr, assignment_expr_map): + if dep_name == symbol: + continue + total_symbol = axis_total_symbol_map.get(dep_name) + if total_symbol is None: + continue + dep_block = axis_block_symbol_map.get(dep_name) + if dep_block is not None and dep_block != symbol_block: + continue + candidate_totals.add(total_symbol) + if len(candidate_totals) != 1: + continue + axis_total_symbol_map[symbol] = next(iter(candidate_totals)) + changed = True + return axis_total_symbol_map diff --git a/third_party/ascend/backend/runtime/dsl_analysis/missing_tunable_detector.py b/third_party/ascend/backend/runtime/dsl_analysis/missing_tunable_detector.py new file mode 100644 index 0000000000..ec2b186fdd --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/missing_tunable_detector.py @@ -0,0 +1,44 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +from typing import List + +from .schema import MissingTunableContext, MissingTunablePolicy + + +class MissingTunableDetector: + + def __init__(self, policy: MissingTunablePolicy): + if policy is None: + raise ValueError("MissingTunableDetector requires a policy instance.") + self.policy = policy + + def detect(self, context: MissingTunableContext) -> List[str]: + raw_candidates = list(self.policy.select_missing_tunable(context)) + deduped = [] + seen = set() + for name in raw_candidates: + if name in seen: + continue + deduped.append(name) + seen.add(name) + return deduped diff --git a/third_party/ascend/backend/runtime/dsl_analysis/missing_tunable_policies.py b/third_party/ascend/backend/runtime/dsl_analysis/missing_tunable_policies.py new file mode 100644 index 0000000000..807dd181c9 --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/missing_tunable_policies.py @@ -0,0 +1,65 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +from typing import List + +from .launch_analyzer import looks_like_cube_mix_tunable +from .schema import MissingTunableContext + + +def _select_missing_constexpr(context: MissingTunableContext) -> List[str]: + missing = set(context.missing_arg_names()) + fixed = context.fixed_tunable_names() + candidates = [] + for name in context.signature.constexpr_names(): + if name not in missing: + continue + if context.signature.has_default(name): + continue + if name in fixed: + continue + candidates.append(name) + return candidates + + +class VectorPolicy: + + def select_missing_tunable(self, context: MissingTunableContext) -> List[str]: + return _select_missing_constexpr(context) + + +class CubeMixPolicy: + + def select_missing_tunable(self, context: MissingTunableContext) -> List[str]: + candidates = _select_missing_constexpr(context) + launch_related_names = set() + if context.launch_info is not None: + launch_related_names = set(getattr(context.launch_info, "launch_related_names", set())) + + filtered = [] + for name in candidates: + if looks_like_cube_mix_tunable(name): + filtered.append(name) + continue + if name in launch_related_names: + filtered.append(name) + return filtered diff --git a/third_party/ascend/backend/runtime/dsl_analysis/role_engine.py b/third_party/ascend/backend/runtime/dsl_analysis/role_engine.py new file mode 100644 index 0000000000..360c3aa56a --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/role_engine.py @@ -0,0 +1,762 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from dataclasses import dataclass +from typing import Callable, Dict, List, Mapping, Optional + +from .load_semantics import ( + build_assignment_expr_map, + collect_axis_candidates, + extract_axis_total_symbol_map as extract_axis_total_symbol_map_impl, + extract_name_depths_transitive, + extract_name_ids, + extract_name_ids_transitive, + extract_param_names, + pick_param_candidate, +) +from .symbolic_expr import SymbolicExpr + + +@dataclass(frozen=True) +class TensorRole: + row_symbol: Optional[SymbolicExpr] + col_symbol: Optional[SymbolicExpr] + total_row_symbol: Optional[SymbolicExpr] = None + total_col_symbol: Optional[SymbolicExpr] = None + + +@dataclass(frozen=True) +class ValueState: + kind: str # tensor/scalar/unknown + role: Optional[TensorRole] = None + + +@dataclass(frozen=True) +class RuleContext: + call_node: ast.Call + + +TransferFn = Callable[[RuleContext, List[ValueState]], ValueState] + + +class RoleTransferRegistry: + + def __init__(self): + self._handlers: Dict[str, TransferFn] = {} + + def register(self, op_key: str, fn: TransferFn) -> None: + self._handlers[op_key] = fn + + def get(self, op_key: str) -> Optional[TransferFn]: + return self._handlers.get(op_key, None) + + +def _tensor(role: Optional[TensorRole]) -> ValueState: + return ValueState(kind="tensor", role=role) + + +def _scalar() -> ValueState: + return ValueState(kind="scalar", role=None) + + +def _unknown() -> ValueState: + return ValueState(kind="unknown", role=None) + + +def _is_tensor(state: ValueState) -> bool: + return state.kind == "tensor" and state.role is not None + + +def _merge_tensor_role(lhs: Optional[TensorRole], rhs: Optional[TensorRole]) -> Optional[TensorRole]: + if lhs is not None and rhs is not None: + if lhs == rhs: + return lhs + # Prefer preserving row from lhs and col from rhs when compatible. + row = lhs.row_symbol if lhs.row_symbol is not None else rhs.row_symbol + col = lhs.col_symbol if lhs.col_symbol is not None else rhs.col_symbol + total_row = lhs.total_row_symbol if lhs.total_row_symbol is not None else rhs.total_row_symbol + total_col = lhs.total_col_symbol if lhs.total_col_symbol is not None else rhs.total_col_symbol + return TensorRole( + row_symbol=row, + col_symbol=col, + total_row_symbol=total_row, + total_col_symbol=total_col, + ) + if lhs is not None: + return lhs + if rhs is not None: + return rhs + return None + + +def _swap_role(role: Optional[TensorRole]) -> Optional[TensorRole]: + if role is None: + return None + return TensorRole( + row_symbol=role.col_symbol, + col_symbol=role.row_symbol, + total_row_symbol=role.total_col_symbol, + total_col_symbol=role.total_row_symbol, + ) + + +def _to_op_key(call_node: ast.Call) -> str: + + def _extract_tl_attr_tail(node: ast.AST) -> Optional[str]: + attrs = [] + cur = node + while isinstance(cur, ast.Attribute): + attrs.append(cur.attr) + cur = cur.value + if isinstance(cur, ast.Name) and cur.id == "tl" and attrs: + return attrs[0] + return None + + if isinstance(call_node.func, ast.Attribute): + tl_attr = _extract_tl_attr_tail(call_node.func) + if tl_attr is not None: + return "tl.{}".format(tl_attr) + return "method.{}".format(call_node.func.attr) + if isinstance(call_node.func, ast.Name): + return "call.{}".format(call_node.func.id) + return "unknown" + + +def _is_tl_namespace_attr(node: ast.AST) -> bool: + cur = node + saw_attr = False + while isinstance(cur, ast.Attribute): + saw_attr = True + cur = cur.value + return saw_attr and isinstance(cur, ast.Name) and cur.id == "tl" + + +def _extract_make_block_ptr_role(call_node: ast.Call) -> Optional[TensorRole]: + # Triton make_block_ptr supports both keyword and positional style: + # make_block_ptr(base, shape, strides, offsets, block_shape, order) + shape_node = None + block_shape_node = None + keyword_map = {} + for keyword in call_node.keywords: + if keyword.arg is None: + continue + keyword_map[keyword.arg] = keyword.value + if keyword.arg == "shape": + shape_node = keyword.value + continue + if keyword.arg == "block_shape": + block_shape_node = keyword.value + continue + if shape_node is None: + shape_node = keyword_map.get("shape", None) + if block_shape_node is None: + block_shape_node = keyword_map.get("block_shape", None) + if shape_node is None and len(call_node.args) >= 2: + shape_node = call_node.args[1] + if block_shape_node is None and len(call_node.args) >= 5: + block_shape_node = call_node.args[4] + if not isinstance(shape_node, ast.Tuple): + return None + if len(shape_node.elts) != 2: + return None + if not isinstance(block_shape_node, ast.Tuple): + return None + if len(block_shape_node.elts) != 2: + return None + return TensorRole( + row_symbol=SymbolicExpr.from_ast(block_shape_node.elts[0]), + col_symbol=SymbolicExpr.from_ast(block_shape_node.elts[1]), + total_row_symbol=SymbolicExpr.from_ast(shape_node.elts[0]), + total_col_symbol=SymbolicExpr.from_ast(shape_node.elts[1]), + ) + + +def _extract_shape_constructor_role(call_node: ast.Call) -> Optional[TensorRole]: + shape_node = None + for keyword in call_node.keywords: + if keyword.arg == "shape": + shape_node = keyword.value + break + if shape_node is None and call_node.args: + shape_node = call_node.args[0] + if not isinstance(shape_node, (ast.Tuple, ast.List)): + return None + if len(shape_node.elts) != 2: + return None + # Constructor shapes represent local tile extents. Keep total symbols empty + # so later merge steps can upgrade to global axis totals from block pointers. + return TensorRole( + row_symbol=SymbolicExpr.from_ast(shape_node.elts[0]), + col_symbol=SymbolicExpr.from_ast(shape_node.elts[1]), + total_row_symbol=None, + total_col_symbol=None, + ) + + +def _rule_make_block_ptr(ctx: RuleContext, _: List[ValueState]) -> ValueState: + return _tensor(_extract_make_block_ptr_role(ctx.call_node)) + + +def _rule_shape_constructor(ctx: RuleContext, _: List[ValueState]) -> ValueState: + role = _extract_shape_constructor_role(ctx.call_node) + if role is None: + return _unknown() + return _tensor(role) + + +def _rule_load(_: RuleContext, args: List[ValueState]) -> ValueState: + if args and _is_tensor(args[0]): + return args[0] + return _unknown() + + +def _rule_transpose(_: RuleContext, args: List[ValueState]) -> ValueState: + if not args or not _is_tensor(args[0]): + return _unknown() + return _tensor(_swap_role(args[0].role)) + + +def _rule_where(_: RuleContext, args: List[ValueState]) -> ValueState: + # tl.where(cond, a, b) + if len(args) < 3: + return _unknown() + role = _merge_tensor_role( + args[1].role if _is_tensor(args[1]) else None, + args[2].role if _is_tensor(args[2]) else None, + ) + if role is None: + return _unknown() + return _tensor(role) + + +def _rule_dot(_: RuleContext, args: List[ValueState]) -> ValueState: + if len(args) < 2: + return _unknown() + lhs = args[0] + rhs = args[1] + if not _is_tensor(lhs) or not _is_tensor(rhs): + return _unknown() + lhs_role = lhs.role + rhs_role = rhs.role + if lhs_role is None or rhs_role is None: + return _unknown() + return _tensor( + TensorRole( + row_symbol=lhs_role.row_symbol, + col_symbol=rhs_role.col_symbol, + total_row_symbol=lhs_role.total_row_symbol or lhs_role.row_symbol, + total_col_symbol=rhs_role.total_col_symbol or rhs_role.col_symbol, + )) + + +def _rule_unary_passthrough(_: RuleContext, args: List[ValueState]) -> ValueState: + if not args or not _is_tensor(args[0]): + return _unknown() + return args[0] + + +def _rule_binary_elementwise(_: RuleContext, args: List[ValueState]) -> ValueState: + if len(args) < 2: + return _unknown() + role = _merge_tensor_role( + args[0].role if _is_tensor(args[0]) else None, + args[1].role if _is_tensor(args[1]) else None, + ) + if role is None: + return _unknown() + return _tensor(role) + + +def _build_registry() -> RoleTransferRegistry: + registry = RoleTransferRegistry() + registry.register("tl.make_block_ptr", _rule_make_block_ptr) + registry.register("tl.full", _rule_shape_constructor) + registry.register("tl.zeros", _rule_shape_constructor) + registry.register("tl.zeros_like", _rule_unary_passthrough) + registry.register("tl.load", _rule_load) + registry.register("tl.trans", _rule_transpose) + registry.register("tl.transpose", _rule_transpose) + registry.register("tl.where", _rule_where) + registry.register("tl.dot", _rule_dot) + registry.register("tl.conv", _rule_dot) + registry.register("method.to", _rule_unary_passthrough) + return registry + + +_REGISTRY = _build_registry() + +_TL_UNARY_ELEMENTWISE = { + "abs", + "exp", + "exp2", + "log", + "sqrt", + "cos", + "sin", + "sigmoid", +} + +_TL_BINARY_ELEMENTWISE = { + "maximum", + "minimum", +} + + +def _is_slice_all(node: ast.AST) -> bool: + return (isinstance(node, ast.Slice) and node.lower is None and node.upper is None and node.step is None) + + +def _get_arange_stop_expr(call_node: ast.Call) -> Optional[ast.AST]: + if len(call_node.args) >= 2: + return call_node.args[1] + if len(call_node.args) == 1: + return call_node.args[0] + for keyword in call_node.keywords: + if keyword.arg in ("end", "stop"): + return keyword.value + return None + + +def _extract_axis_symbol_from_subscript_base(base_node: ast.AST) -> Optional[str]: + if isinstance(base_node, ast.Name): + return base_node.id + if _is_tl_arange_call(base_node) and isinstance(base_node, ast.Call): + stop_expr = _get_arange_stop_expr(base_node) + if stop_expr is not None: + return str(SymbolicExpr.from_ast(stop_expr)) + + arange_stop_symbols = set() + for node in ast.walk(base_node): + if not _is_tl_arange_call(node): + continue + if not isinstance(node, ast.Call): + continue + stop_expr = _get_arange_stop_expr(node) + if stop_expr is None: + continue + arange_stop_symbols.add(str(SymbolicExpr.from_ast(stop_expr))) + if len(arange_stop_symbols) == 1: + return next(iter(arange_stop_symbols)) + return None + + +def _extract_subscript_axes(node: ast.Subscript) -> Optional[tuple]: + symbol_name = _extract_axis_symbol_from_subscript_base(node.value) + if symbol_name is None: + return None + slice_node = node.slice + if not isinstance(slice_node, ast.Tuple): + return None + if len(slice_node.elts) != 2: + return None + first_dim = slice_node.elts[0] + second_dim = slice_node.elts[1] + if _is_slice_all(first_dim) and isinstance(second_dim, ast.Constant) and second_dim.value is None: + return symbol_name, 0 + if isinstance(first_dim, ast.Constant) and first_dim.value is None and _is_slice_all(second_dim): + return symbol_name, 1 + return None + + +def _extract_subscript_axis_kind(node: ast.Subscript) -> Optional[int]: + slice_node = node.slice + if not isinstance(slice_node, ast.Tuple): + return None + if len(slice_node.elts) != 2: + return None + first_dim = slice_node.elts[0] + second_dim = slice_node.elts[1] + if _is_slice_all(first_dim) and isinstance(second_dim, ast.Constant) and second_dim.value is None: + return 0 + if isinstance(first_dim, ast.Constant) and first_dim.value is None and _is_slice_all(second_dim): + return 1 + return None + + +def _is_tl_arange_call(node: ast.AST) -> bool: + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) and node.func.value.id == "tl" and node.func.attr == "arange") + + +def _extract_axis_symbols_from_expr(expr: ast.AST, axis_candidates: set) -> set: + symbols = set() + for node in ast.walk(expr): + if isinstance(node, ast.Name) and node.id in axis_candidates: + symbols.add(node.id) + continue + if not isinstance(node, ast.Subscript): + continue + axes = _extract_subscript_axes(node) + if axes is not None: + symbol_name, _ = axes + if symbol_name in axis_candidates: + symbols.add(symbol_name) + continue + if isinstance(node.value, ast.Name) and node.value.id in axis_candidates: + symbols.add(node.value.id) + return symbols + + +def _infer_role_from_subscript_expr( + expr: ast.AST, + axis_total_symbol_map: Optional[Mapping[str, str]] = None, +) -> Optional[TensorRole]: + row_symbol = None + col_symbol = None + total_row_symbol = None + total_col_symbol = None + total_symbol_map = axis_total_symbol_map or {} + for node in ast.walk(expr): + if not isinstance(node, ast.Subscript): + continue + axes = _extract_subscript_axes(node) + if axes is None: + continue + symbol_name, axis_idx = axes + total_name = total_symbol_map.get(symbol_name, symbol_name) + if axis_idx == 0 and row_symbol is None: + row_symbol = SymbolicExpr(text=symbol_name) + total_row_symbol = SymbolicExpr(text=total_name) + if axis_idx == 1 and col_symbol is None: + col_symbol = SymbolicExpr(text=symbol_name) + total_col_symbol = SymbolicExpr(text=total_name) + # Keep partial-axis role so binary expression merging can reconstruct + # full row/col roles through pointer-hoisted forms. + if row_symbol is None and col_symbol is None: + return None + return TensorRole( + row_symbol=row_symbol, + col_symbol=col_symbol, + total_row_symbol=total_row_symbol, + total_col_symbol=total_col_symbol, + ) + + +def _infer_axis_alias_role_for_assign( + target_name: str, + expr: ast.AST, + axis_total_symbol_map: Optional[Mapping[str, str]] = None, +) -> Optional[TensorRole]: + row_symbol = None + col_symbol = None + total_row_symbol = None + total_col_symbol = None + total_name = (axis_total_symbol_map or {}).get(target_name, target_name) + for node in ast.walk(expr): + if not isinstance(node, ast.Subscript): + continue + axis_kind = _extract_subscript_axis_kind(node) + if axis_kind is None: + continue + if axis_kind == 0 and row_symbol is None: + row_symbol = SymbolicExpr(text=target_name) + total_row_symbol = SymbolicExpr(text=total_name) + if axis_kind == 1 and col_symbol is None: + col_symbol = SymbolicExpr(text=target_name) + total_col_symbol = SymbolicExpr(text=total_name) + + if row_symbol is None and col_symbol is None: + return None + + return TensorRole( + row_symbol=row_symbol, + col_symbol=col_symbol, + total_row_symbol=total_row_symbol, + total_col_symbol=total_col_symbol, + ) + + +def _extract_axis_total_symbol_map(func_ast: ast.AST) -> Dict[str, str]: + param_names = extract_param_names(func_ast) + assignment_expr_map = build_assignment_expr_map(func_ast) + axis_candidates = collect_axis_candidates(func_ast, param_names=param_names) + return extract_axis_total_symbol_map_impl( + func_ast, + assignment_expr_map=assignment_expr_map, + axis_candidates=axis_candidates, + param_names=param_names, + ) + + +def _infer_expr_state( + expr: ast.AST, + env: Mapping[str, ValueState], + axis_total_symbol_map: Optional[Mapping[str, str]] = None, +) -> ValueState: + if isinstance(expr, ast.Name): + return env.get(expr.id, _unknown()) + + if isinstance(expr, ast.Constant): + return _scalar() + + if isinstance(expr, ast.Subscript): + if isinstance(expr.value, ast.Name): + base = env.get(expr.value.id, _unknown()) + if _is_tensor(base): + return base + role = _infer_role_from_subscript_expr( + expr, + axis_total_symbol_map=axis_total_symbol_map, + ) + if role is not None: + return _tensor(role) + return _unknown() + + if isinstance(expr, ast.UnaryOp): + operand = _infer_expr_state( + expr.operand, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + if _is_tensor(operand): + return operand + return _unknown() + + if isinstance(expr, ast.BinOp): + lhs = _infer_expr_state( + expr.left, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + rhs = _infer_expr_state( + expr.right, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + return _rule_binary_elementwise( + RuleContext(call_node=ast.Call(func=ast.Name(id="binop"), args=[], keywords=[])), + [lhs, rhs], + ) + + if isinstance(expr, ast.IfExp): + body_state = _infer_expr_state( + expr.body, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + else_state = _infer_expr_state( + expr.orelse, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + return _rule_binary_elementwise( + RuleContext(call_node=ast.Call(func=ast.Name(id="ifexp"), args=[], keywords=[])), + [body_state, else_state], + ) + + if isinstance(expr, ast.Attribute): + base_state = _infer_expr_state( + expr.value, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + if expr.attr == "T" and _is_tensor(base_state): + return _tensor(_swap_role(base_state.role)) + if expr.attr == "dtype": + return _scalar() + return _unknown() + + if isinstance(expr, ast.Call): + op_key = _to_op_key(expr) + arg_states: List[ValueState] = [] + if (isinstance(expr.func, ast.Attribute) and not _is_tl_namespace_attr(expr.func)): + # Method calls (e.g. tensor.to(...)) should propagate from receiver. + arg_states.append(_infer_expr_state( + expr.func.value, + env, + axis_total_symbol_map=axis_total_symbol_map, + )) + arg_states.extend( + _infer_expr_state( + arg, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) for arg in expr.args) + + handler = _REGISTRY.get(op_key) + if handler is not None: + state = handler(RuleContext(call_node=expr), arg_states) + if state.kind != "unknown": + return state + + # Fallback by operator family. + if op_key.startswith("tl."): + op_name = op_key[len("tl."):] + if op_name in _TL_UNARY_ELEMENTWISE: + return _rule_unary_passthrough(RuleContext(call_node=expr), arg_states) + if op_name in _TL_BINARY_ELEMENTWISE: + return _rule_binary_elementwise(RuleContext(call_node=expr), arg_states) + + if arg_states and _is_tensor(arg_states[0]): + return arg_states[0] + return _unknown() + + return _unknown() + + +def _assign_target(target: ast.AST, state: ValueState, env: Dict[str, ValueState]) -> None: + if not isinstance(target, ast.Name): + return + env[target.id] = state + + +def _merge_env(dst: Dict[str, ValueState], src: Mapping[str, ValueState]) -> None: + for name, state in src.items(): + if name not in dst: + dst[name] = state + continue + cur = dst[name] + if cur == state: + continue + if _is_tensor(cur) and _is_tensor(state): + merged = _merge_tensor_role(cur.role, state.role) + dst[name] = _tensor(merged) + continue + if _is_tensor(cur): + continue + if _is_tensor(state): + dst[name] = state + + +def _is_single_axis_role(role: Optional[TensorRole]) -> bool: + if role is None: + return False + has_row = role.row_symbol is not None + has_col = role.col_symbol is not None + return has_row != has_col + + +def _visit_stmt( + stmt: ast.stmt, + env: Dict[str, ValueState], + axis_total_symbol_map: Optional[Mapping[str, str]] = None, +) -> None: + if isinstance(stmt, ast.Assign): + state = _infer_expr_state( + stmt.value, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + # Broadcasted axis aliases like: + # offs_m = ... + tl.arange(... )[:, None] + # offs_k = tl.arange(... )[None, :] + # should keep alias symbols (offs_m/offs_k) for downstream axis tracking. + if len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name): + alias_role = _infer_axis_alias_role_for_assign( + stmt.targets[0].id, + stmt.value, + axis_total_symbol_map=axis_total_symbol_map, + ) + if alias_role is not None and (not _is_tensor(state) or + (_is_single_axis_role(alias_role) and _is_single_axis_role(state.role))): + state = _tensor(alias_role) + for target in stmt.targets: + _assign_target(target, state, env) + return + + if isinstance(stmt, ast.AnnAssign): + if stmt.value is None: + return + state = _infer_expr_state( + stmt.value, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + _assign_target(stmt.target, state, env) + return + + if isinstance(stmt, ast.AugAssign): + target_state = _infer_expr_state( + stmt.target, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + value_state = _infer_expr_state( + stmt.value, + env, + axis_total_symbol_map=axis_total_symbol_map, + ) + state = _rule_binary_elementwise( + RuleContext(call_node=ast.Call(func=ast.Name(id="augassign"), args=[], keywords=[])), + [target_state, value_state], + ) + if state.kind == "unknown": + if target_state.kind != "unknown": + state = target_state + elif value_state.kind != "unknown": + state = value_state + _assign_target(stmt.target, state, env) + return + + if isinstance(stmt, ast.For): + inner_env = dict(env) + for child in stmt.body: + _visit_stmt(child, inner_env, axis_total_symbol_map=axis_total_symbol_map) + _merge_env(env, inner_env) + orelse_env = dict(env) + for child in stmt.orelse: + _visit_stmt(child, orelse_env, axis_total_symbol_map=axis_total_symbol_map) + _merge_env(env, orelse_env) + return + + if isinstance(stmt, ast.While): + inner_env = dict(env) + for child in stmt.body: + _visit_stmt(child, inner_env, axis_total_symbol_map=axis_total_symbol_map) + _merge_env(env, inner_env) + return + + if isinstance(stmt, ast.If): + body_env = dict(env) + for child in stmt.body: + _visit_stmt(child, body_env, axis_total_symbol_map=axis_total_symbol_map) + else_env = dict(env) + for child in stmt.orelse: + _visit_stmt(child, else_env, axis_total_symbol_map=axis_total_symbol_map) + _merge_env(env, body_env) + _merge_env(env, else_env) + return + + if isinstance(stmt, ast.With): + inner_env = dict(env) + for child in stmt.body: + _visit_stmt(child, inner_env, axis_total_symbol_map=axis_total_symbol_map) + _merge_env(env, inner_env) + return + + +def infer_value_states( + func_ast: ast.AST, + inherited_roles: Optional[Mapping[str, TensorRole]] = None, +) -> Dict[str, ValueState]: + axis_total_symbol_map = _extract_axis_total_symbol_map(func_ast) + env: Dict[str, ValueState] = {name: _tensor(role) for name, role in (inherited_roles or {}).items()} + + if isinstance(func_ast, (ast.FunctionDef, ast.AsyncFunctionDef)): + body = func_ast.body + else: + body = [node for node in ast.walk(func_ast) if isinstance(node, ast.stmt)] + + for stmt in body: + _visit_stmt(stmt, env, axis_total_symbol_map=axis_total_symbol_map) + return env diff --git a/third_party/ascend/backend/runtime/dsl_analysis/schema.py b/third_party/ascend/backend/runtime/dsl_analysis/schema.py new file mode 100644 index 0000000000..1880e9fca6 --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/schema.py @@ -0,0 +1,153 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import (Any, Dict, List, Literal, Mapping, Optional, Protocol, Sequence, Set) + + +@dataclass(frozen=True) +class ParameterSpec: + name: str + is_constexpr: bool = False + has_default: bool = False + + +@dataclass(frozen=True) +class SignatureInfo: + parameters: Sequence[ParameterSpec] + + def parameter_names(self) -> List[str]: + return [param.name for param in self.parameters] + + def constexpr_names(self) -> List[str]: + return [param.name for param in self.parameters if param.is_constexpr] + + def is_constexpr(self, parameter_name: str) -> bool: + for param in self.parameters: + if param.name == parameter_name: + return param.is_constexpr + return False + + def has_default(self, parameter_name: str) -> bool: + for param in self.parameters: + if param.name == parameter_name: + return param.has_default + return False + + +@dataclass(frozen=True) +class MissingTunableContext: + arg_names: Sequence[str] + provided_args: Mapping[str, object] + signature: SignatureInfo + split_params: Mapping[str, str] + tiling_params: Mapping[str, str] + launch_info: Optional[Any] = None + + def missing_arg_names(self) -> List[str]: + provided = set(self.provided_args.keys()) + return [name for name in self.arg_names if name not in provided] + + def fixed_tunable_names(self) -> Set[str]: + return set(self.split_params.values()) | set(self.tiling_params.values()) + + +class MissingTunablePolicy(Protocol): + + def select_missing_tunable(self, context: MissingTunableContext) -> List[str]: + ... + + +AXIS_LENGTH_STATE_TUNABLE = "tunable" +AXIS_LENGTH_STATE_FIXED_COMPILE_TIME = "fixed_compile_time" +AXIS_LENGTH_STATE_RUNTIME_NON_TUNABLE = "runtime_non_tunable" + + +@dataclass(frozen=True) +class AxisLengthStateInfo: + axis_symbol: str + length_symbol: Optional[str] + state: str + value: Optional[int] = None + + +@dataclass(frozen=True) +class AxisLengthStateResult: + axis_states: Dict[str, AxisLengthStateInfo] + + +CV_PARSER_VERSION_V1 = "cv-parse-v1" +CV_PARSE_STATUS_OK = "ok" +CV_PARSE_STATUS_PARTIAL = "partial" +CV_PARSE_STATUS_FAILED = "failed" + +CvParserMode = Literal["cube", "mix"] +CvParseStatus = Literal["ok", "partial", "failed"] +CvAxisRole = Literal["M", "N", "K", "OTHER"] +CvTunableSource = Literal["signature", "axis_length", "launch_semantics"] +CvAxisLengthState = Literal[ + AXIS_LENGTH_STATE_TUNABLE, + AXIS_LENGTH_STATE_FIXED_COMPILE_TIME, + AXIS_LENGTH_STATE_RUNTIME_NON_TUNABLE, + "unknown", +] +CvAxisDynamicSource = Literal["none", "dynamic_from_load"] + + +@dataclass(frozen=True) +class CvAxisLengthInfo: + axis_symbol: Optional[str] + length_expr: Optional[str] + state: CvAxisLengthState + tunable_param: Optional[str] + const_value: Optional[int] = None + dynamic_source: CvAxisDynamicSource = "none" + + +@dataclass(frozen=True) +class CvDotSiteInfo: + site_id: int + lhs: Optional[str] + rhs: Optional[str] + m: CvAxisLengthInfo + n: CvAxisLengthInfo + k: CvAxisLengthInfo + scope: Optional[str] = None + + +@dataclass(frozen=True) +class CvTunableParamInfo: + name: str + role: CvAxisRole + source: CvTunableSource + + +@dataclass(frozen=True) +class CvParseResult: + version: str + mode: CvParserMode + status: CvParseStatus + signature_params: Sequence[str] + dot_sites: List[CvDotSiteInfo] + tunable_params: List[CvTunableParamInfo] + missing_tunable_params: List[str] + diagnostics: List[str] diff --git a/third_party/ascend/backend/runtime/dsl_analysis/shape_inference.py b/third_party/ascend/backend/runtime/dsl_analysis/shape_inference.py new file mode 100644 index 0000000000..5f73d42a95 --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/shape_inference.py @@ -0,0 +1,396 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from dataclasses import dataclass, replace +from typing import Dict, List, Optional, Set + +from .call_graph import build_call_sites, build_function_index +from .dot_role_annotator import TensorRole, annotate_tensor_roles +from .symbolic_expr import SymbolicExpr + + +def _is_tl_mix_call(node: ast.AST) -> bool: + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) and node.func.value.id == "tl" + and node.func.attr in ("dot", "conv")) + + +def _is_tl_transpose_call(node: ast.AST) -> bool: + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) and node.func.value.id == "tl" + and node.func.attr in ("trans", "transpose")) + + +def _merge_tensor_name_candidates(*candidates: Optional[str]) -> Optional[str]: + names = [] + for candidate in candidates: + if candidate is None or candidate in names: + continue + names.append(candidate) + if len(names) == 1: + return names[0] + return None + + +def _extract_tensor_name_from_expr(expr: ast.AST) -> Optional[str]: + if isinstance(expr, ast.Name): + return expr.id + + if isinstance(expr, ast.Subscript): + if isinstance(expr.value, ast.Name): + return expr.value.id + return _extract_tensor_name_from_expr(expr.value) + + if isinstance(expr, ast.Call): + if isinstance(expr.func, ast.Attribute): + if isinstance(expr.func.value, ast.Name): + # tl.xxx(tensor) wrappers should resolve from positional args, + # while tensor.xxx(...) methods can use receiver name directly. + if expr.func.value.id != "tl": + return expr.func.value.id + elif not (isinstance(expr.func.value, ast.Name) and expr.func.value.id == "tl"): + base = _extract_tensor_name_from_expr(expr.func.value) + if base is not None: + return base + if expr.args: + return _extract_tensor_name_from_expr(expr.args[0]) + return None + + if isinstance(expr, ast.UnaryOp): + return _extract_tensor_name_from_expr(expr.operand) + + if isinstance(expr, ast.BinOp): + return _merge_tensor_name_candidates( + _extract_tensor_name_from_expr(expr.left), + _extract_tensor_name_from_expr(expr.right), + ) + + if isinstance(expr, ast.IfExp): + return _merge_tensor_name_candidates( + _extract_tensor_name_from_expr(expr.body), + _extract_tensor_name_from_expr(expr.orelse), + ) + + if isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name): + return expr.value.id + + return None + + +@dataclass(frozen=True) +class DotSiteMNK: + site_id: int + m: Optional[str] + n: Optional[str] + k: Optional[str] + lhs: Optional[str] + rhs: Optional[str] + m_total: Optional[str] = None + n_total: Optional[str] = None + k_total: Optional[str] = None + + +def _dedup_key(site: DotSiteMNK): + return ( + site.lhs, + site.rhs, + site.m, + site.n, + site.k, + site.m_total, + site.n_total, + site.k_total, + ) + + +def _deduplicate_dot_sites(dot_sites: List[DotSiteMNK]) -> List[DotSiteMNK]: + seen = set() + deduped: List[DotSiteMNK] = [] + for site in dot_sites: + key = _dedup_key(site) + if key in seen: + continue + seen.add(key) + deduped.append(site) + return [replace(site, site_id=idx) for idx, site in enumerate(deduped)] + + +class _DotSiteCollector(ast.NodeVisitor): + + def __init__(self, tensor_roles): + self.tensor_roles = tensor_roles + self.dot_sites = [] + self._site_id = 0 + + @staticmethod + def _swap_role(role: Optional[TensorRole]) -> Optional[TensorRole]: + if role is None: + return None + return TensorRole( + row_symbol=role.col_symbol, + col_symbol=role.row_symbol, + total_row_symbol=role.total_col_symbol, + total_col_symbol=role.total_row_symbol, + ) + + def _resolve_tensor_name_and_role(self, expr: ast.AST): + transposed = _is_tl_transpose_call(expr) + base_expr = expr.args[0] if transposed and isinstance(expr, ast.Call) and expr.args else expr + tensor_name, role = self._extract_tensor_name_and_role(base_expr) + if transposed: + role = self._swap_role(role) + return tensor_name, role + + def _extract_tensor_name_and_role(self, expr: ast.AST): + if isinstance(expr, ast.BinOp): + left_name, left_role = self._extract_tensor_name_and_role(expr.left) + right_name, right_role = self._extract_tensor_name_and_role(expr.right) + if left_role is not None and right_role is None: + return left_name, left_role + if right_role is not None and left_role is None: + return right_name, right_role + if left_role is not None and right_role is not None and left_name == right_name: + return left_name, left_role + return None, None + + if isinstance(expr, ast.UnaryOp): + return self._extract_tensor_name_and_role(expr.operand) + + if isinstance(expr, ast.IfExp): + body_name, body_role = self._extract_tensor_name_and_role(expr.body) + else_name, else_role = self._extract_tensor_name_and_role(expr.orelse) + if body_role is not None and else_role is None: + return body_name, body_role + if else_role is not None and body_role is None: + return else_name, else_role + if body_role is not None and else_role is not None and body_name == else_name: + return body_name, body_role + return None, None + + tensor_name = self._extract_tensor_name(expr) + if tensor_name is None: + return None, None + return tensor_name, self.tensor_roles.get(tensor_name, None) + + def _extract_tensor_name(self, expr: ast.AST) -> Optional[str]: + return _extract_tensor_name_from_expr(expr) + + def visit_Call(self, node: ast.Call): + if _is_tl_mix_call(node): + lhs = None + rhs = None + lhs_role = None + rhs_role = None + if len(node.args) >= 2: + lhs, lhs_role = self._resolve_tensor_name_and_role(node.args[0]) + rhs, rhs_role = self._resolve_tensor_name_and_role(node.args[1]) + + m = str(lhs_role.row_symbol) if lhs_role and lhs_role.row_symbol else None + n = str(rhs_role.col_symbol) if rhs_role and rhs_role.col_symbol else None + k_lhs = str(lhs_role.col_symbol) if lhs_role and lhs_role.col_symbol else None + k_rhs = str(rhs_role.row_symbol) if rhs_role and rhs_role.row_symbol else None + k = k_lhs + if k is None: + k = k_rhs + elif k_rhs is not None and k != k_rhs: + k = "{}|{}".format(k_lhs, k_rhs) + + m_total = str(lhs_role.total_row_symbol) if lhs_role and lhs_role.total_row_symbol else None + n_total = str(rhs_role.total_col_symbol) if rhs_role and rhs_role.total_col_symbol else None + k_lhs_total = (str(lhs_role.total_col_symbol) if lhs_role and lhs_role.total_col_symbol else None) + k_rhs_total = (str(rhs_role.total_row_symbol) if rhs_role and rhs_role.total_row_symbol else None) + k_total = k_lhs_total + if k_total is None: + k_total = k_rhs_total + elif k_rhs_total is not None and k_total != k_rhs_total: + k_total = "{}|{}".format(k_lhs_total, k_rhs_total) + + self.dot_sites.append( + DotSiteMNK( + site_id=self._site_id, + m=m, + n=n, + k=k, + lhs=lhs, + rhs=rhs, + m_total=m_total, + n_total=n_total, + k_total=k_total, + )) + self._site_id += 1 + + self.generic_visit(node) + + +def infer_dot_sites_mnk( + func_ast: ast.AST, + inherited_roles: Optional[Dict[str, TensorRole]] = None, +) -> List[DotSiteMNK]: + tensor_roles = annotate_tensor_roles(func_ast, inherited_roles=inherited_roles) + collector = _DotSiteCollector(tensor_roles) + collector.visit(func_ast) + return _deduplicate_dot_sites(collector.dot_sites) + + +def _substitute_symbol(symbol: Optional[str], symbol_map: Dict[str, str]) -> Optional[str]: + if symbol is None: + return None + return symbol_map.get(symbol, symbol) + + +def _expr_to_text(arg_node: ast.AST, symbol_map: Dict[str, str]) -> Optional[str]: + if _is_tl_transpose_call(arg_node) and arg_node.args: + arg_text = _expr_to_text(arg_node.args[0], symbol_map) + if arg_text is None: + return None + return "tl.trans({})".format(arg_text) + if isinstance(arg_node, ast.Name): + return symbol_map.get(arg_node.id, arg_node.id) + try: + text = ast.unparse(arg_node) + except Exception: + return None + return symbol_map.get(text, text) + + +def _substitute_role( + role: Optional[TensorRole], + symbol_map: Dict[str, str], +) -> Optional[TensorRole]: + if role is None: + return None + + def _map_symbol(sym: Optional[SymbolicExpr]) -> Optional[SymbolicExpr]: + if sym is None: + return None + text = symbol_map.get(str(sym), str(sym)) + return SymbolicExpr(text=text) + + return TensorRole( + row_symbol=_map_symbol(role.row_symbol), + col_symbol=_map_symbol(role.col_symbol), + total_row_symbol=_map_symbol(role.total_row_symbol), + total_col_symbol=_map_symbol(role.total_col_symbol), + ) + + +def _resolve_role_for_expr( + arg_node: ast.AST, + tensor_roles: Dict[str, TensorRole], +) -> Optional[TensorRole]: + transposed = _is_tl_transpose_call(arg_node) + base_node = arg_node.args[0] if transposed and isinstance(arg_node, ast.Call) and arg_node.args else arg_node + tensor_name = _extract_tensor_name_from_expr(base_node) + if tensor_name is None: + return None + role = tensor_roles.get(tensor_name, None) + if role is None: + return None + if not transposed: + return role + return TensorRole( + row_symbol=role.col_symbol, + col_symbol=role.row_symbol, + total_row_symbol=role.total_col_symbol, + total_col_symbol=role.total_row_symbol, + ) + + +def infer_dot_sites_mnk_interprocedural(module_ast: ast.AST, entry_function_name: str) -> List[DotSiteMNK]: + functions = build_function_index(module_ast) + if entry_function_name not in functions: + return [] + + call_sites = build_call_sites(functions) + call_sites_by_caller = {} + for site in call_sites: + call_sites_by_caller.setdefault(site.caller, []).append(site) + + emitted = [] + next_site_id = 0 + + def walk( + function_name: str, + symbol_map: Dict[str, str], + param_roles: Dict[str, TensorRole], + active: Set[str], + ): + nonlocal next_site_id + if function_name in active: + return + active.add(function_name) + + function_meta = functions.get(function_name, None) + if function_meta is None: + active.remove(function_name) + return + + local_tensor_roles = annotate_tensor_roles( + function_meta.node, + inherited_roles=param_roles, + ) + local_dot_sites = infer_dot_sites_mnk( + function_meta.node, + inherited_roles=param_roles, + ) + for site in local_dot_sites: + emitted.append( + DotSiteMNK( + site_id=next_site_id, + m=_substitute_symbol(site.m, symbol_map), + n=_substitute_symbol(site.n, symbol_map), + k=_substitute_symbol(site.k, symbol_map), + lhs=_substitute_symbol(site.lhs, symbol_map), + rhs=_substitute_symbol(site.rhs, symbol_map), + m_total=_substitute_symbol(site.m_total, symbol_map), + n_total=_substitute_symbol(site.n_total, symbol_map), + k_total=_substitute_symbol(site.k_total, symbol_map), + )) + next_site_id += 1 + + for call_site in call_sites_by_caller.get(function_name, []): + callee_meta = functions.get(call_site.callee, None) + if callee_meta is None: + continue + callee_symbol_map = {} + callee_param_roles: Dict[str, TensorRole] = {} + for idx, param_name in enumerate(callee_meta.param_names): + if idx >= len(call_site.args): + break + arg_node = call_site.args[idx] + if arg_node is None: + continue + arg_text = _expr_to_text(arg_node, symbol_map) + if arg_text is None: + continue + callee_symbol_map[param_name] = arg_text + arg_role = _resolve_role_for_expr(arg_node, local_tensor_roles) + arg_role = _substitute_role(arg_role, symbol_map) + if arg_role is not None: + callee_param_roles[param_name] = arg_role + walk(call_site.callee, callee_symbol_map, callee_param_roles, active) + + active.remove(function_name) + + walk(entry_function_name, {}, {}, set()) + return _deduplicate_dot_sites(emitted) diff --git a/third_party/ascend/backend/runtime/dsl_analysis/signature_analyzer.py b/third_party/ascend/backend/runtime/dsl_analysis/signature_analyzer.py new file mode 100644 index 0000000000..ac80647bba --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/signature_analyzer.py @@ -0,0 +1,91 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from typing import List, Set, Union + +from .schema import ParameterSpec, SignatureInfo + + +def _is_tl_constexpr(annotation: ast.AST) -> bool: + return (isinstance(annotation, ast.Attribute) and isinstance(annotation.value, ast.Name) + and annotation.value.id == "tl" and annotation.attr == "constexpr") + + +def _find_function_node(func_ast: ast.AST) -> Union[ast.FunctionDef, ast.AsyncFunctionDef]: + if isinstance(func_ast, (ast.FunctionDef, ast.AsyncFunctionDef)): + return func_ast + + for node in ast.walk(func_ast): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + return node + + raise ValueError("Unable to locate function definition in DSL AST.") + + +def _extract_default_flags(function_node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> dict: + args_node = function_node.args + positional_args = list(args_node.posonlyargs) + list(args_node.args) + kwonly_args = list(args_node.kwonlyargs) + + default_flags = {} + positional_defaults = list(args_node.defaults or []) + positional_default_count = len(positional_defaults) + positional_arg_count = len(positional_args) + positional_default_start = positional_arg_count - positional_default_count + for idx, arg in enumerate(positional_args): + default_flags[arg.arg] = idx >= positional_default_start + + kw_defaults = list(args_node.kw_defaults or []) + for idx, arg in enumerate(kwonly_args): + has_default = idx < len(kw_defaults) and kw_defaults[idx] is not None + default_flags[arg.arg] = has_default + return default_flags + + +def extract_signature_info(func_ast: ast.AST) -> SignatureInfo: + function_node = _find_function_node(func_ast) + args_node = function_node.args + if not isinstance(args_node, ast.arguments): + return SignatureInfo(parameters=[]) + + ordered_args = list(args_node.posonlyargs) + list(args_node.args) + list(args_node.kwonlyargs) + default_flags = _extract_default_flags(function_node) + parameters = [] + for arg in ordered_args: + if not isinstance(arg, ast.arg): + continue + parameters.append( + ParameterSpec( + name=arg.arg, + is_constexpr=_is_tl_constexpr(arg.annotation), + has_default=bool(default_flags.get(arg.arg, False)), + )) + return SignatureInfo(parameters=parameters) + + +def extract_constexpr_names(func_ast: ast.AST) -> List[str]: + return extract_signature_info(func_ast).constexpr_names() + + +def extract_constexpr_name_set(func_ast: ast.AST) -> Set[str]: + return set(extract_constexpr_names(func_ast)) diff --git a/third_party/ascend/backend/runtime/dsl_analysis/symbolic_expr.py b/third_party/ascend/backend/runtime/dsl_analysis/symbolic_expr.py new file mode 100644 index 0000000000..cf17577575 --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/symbolic_expr.py @@ -0,0 +1,40 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from dataclasses import dataclass + + +@dataclass(frozen=True) +class SymbolicExpr: + text: str + + @classmethod + def from_ast(cls, node: ast.AST) -> "SymbolicExpr": + if isinstance(node, ast.Name): + return cls(text=node.id) + if isinstance(node, ast.Constant): + return cls(text=str(node.value)) + return cls(text=ast.unparse(node)) + + def __str__(self) -> str: + return self.text diff --git a/third_party/ascend/backend/runtime/dsl_analysis/vv_config_adapter.py b/third_party/ascend/backend/runtime/dsl_analysis/vv_config_adapter.py new file mode 100644 index 0000000000..b1fdfdeacb --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/vv_config_adapter.py @@ -0,0 +1,190 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Sequence, Tuple + +_DEFAULT_KEY_AXES = ("x", "y", "z", "w", "v", "t") + + +@dataclass(frozen=True) +class VvConfigAdapterResult: + split_params: Dict[str, str] + tiling_params: Dict[str, str] + low_dim_axes: List[str] = field(default_factory=list) + reduction_axes: List[str] = field(default_factory=list) + axis_length_exprs: Dict[str, str] = field(default_factory=dict) + fixed_tiling_exprs: Dict[str, str] = field(default_factory=dict) + axis_dynamic_sources: Dict[str, str] = field(default_factory=dict) + axis_pid_dims: Dict[str, int] = field(default_factory=dict) + inferred_keys: Dict[str, str] = field(default_factory=dict) + status: str = "ok" + diagnostics: List[str] = field(default_factory=list) + + +def _dedup_tunable_by_first_axis(axis_tunable_pairs: Sequence[Tuple[int, str]], ) -> List[Tuple[int, str]]: + ordered: List[Tuple[int, str]] = [] + seen = set() + for axis_index, tunable_name in axis_tunable_pairs: + if tunable_name in seen: + continue + ordered.append((axis_index, tunable_name)) + seen.add(tunable_name) + return ordered + + +def _resolve_key_axis_name(ordered_key_axes: List[str], axis_index: int) -> str: + if not ordered_key_axes: + if axis_index < 0: + return _DEFAULT_KEY_AXES[0] + if axis_index >= len(_DEFAULT_KEY_AXES): + return _DEFAULT_KEY_AXES[-1] + return _DEFAULT_KEY_AXES[axis_index] + if axis_index < 0: + return ordered_key_axes[0] + if axis_index >= len(ordered_key_axes): + return ordered_key_axes[-1] + return ordered_key_axes[axis_index] + + +def adapt_vv_v2_to_vector_inputs( + key_axis_to_param: Dict[str, str], + vv_parse_result: object, +) -> Optional[VvConfigAdapterResult]: + if vv_parse_result is None: + return None + + direct_split = dict(getattr(vv_parse_result, "split_params", {}) or {}) + direct_tiling = dict(getattr(vv_parse_result, "tiling_params", {}) or {}) + direct_low_dim = list(getattr(vv_parse_result, "low_dim_axes", []) or []) + direct_reduction = list(getattr(vv_parse_result, "reduction_axes", []) or []) + direct_axis_length_exprs = dict(getattr(vv_parse_result, "axis_length_exprs", {}) or {}) + direct_fixed_tiling_exprs = dict(getattr(vv_parse_result, "fixed_tiling_exprs", {}) or {}) + direct_axis_dynamic_sources = dict(getattr(vv_parse_result, "axis_dynamic_sources", {}) or {}) + direct_axis_pid_dims = dict(getattr(vv_parse_result, "axis_pid_dims", {}) or {}) + direct_inferred_keys = dict(getattr(vv_parse_result, "inferred_keys", {}) or {}) + direct_status = getattr(vv_parse_result, "status", "ok") + direct_diagnostics = list(getattr(vv_parse_result, "diagnostics", []) or []) + + if direct_status == "failed": + return None + + if (direct_split or direct_tiling or direct_low_dim or direct_reduction or direct_axis_length_exprs + or direct_fixed_tiling_exprs or direct_axis_dynamic_sources or direct_axis_pid_dims + or direct_inferred_keys): + return VvConfigAdapterResult( + split_params=direct_split, + tiling_params=direct_tiling, + low_dim_axes=direct_low_dim, + reduction_axes=direct_reduction, + axis_length_exprs=direct_axis_length_exprs, + fixed_tiling_exprs=direct_fixed_tiling_exprs, + axis_dynamic_sources=direct_axis_dynamic_sources, + axis_pid_dims=direct_axis_pid_dims, + inferred_keys=direct_inferred_keys, + status=direct_status, + diagnostics=direct_diagnostics, + ) + + axes = list(getattr(vv_parse_result, "axes", [])) + if not axes: + return None + + axis_tunable_pairs: List[Tuple[int, str]] = [] + for item in axes: + split_param = getattr(item, "split_param", None) + if isinstance(split_param, str) and split_param: + axis_index = getattr(item, "axis_index", None) + if isinstance(axis_index, int): + axis_tunable_pairs.append((axis_index, split_param)) + continue + tunable = getattr(item, "tunable_param", None) + if not isinstance(tunable, str) or not tunable: + continue + axis_index = getattr(item, "axis_index", None) + if not isinstance(axis_index, int): + continue + axis_tunable_pairs.append((axis_index, tunable)) + + axis_tunable_pairs = _dedup_tunable_by_first_axis(axis_tunable_pairs) + if not axis_tunable_pairs: + if direct_status != "ok": + return VvConfigAdapterResult( + split_params={}, + tiling_params={}, + low_dim_axes=direct_low_dim, + reduction_axes=direct_reduction, + axis_length_exprs=direct_axis_length_exprs, + fixed_tiling_exprs=direct_fixed_tiling_exprs, + axis_dynamic_sources=direct_axis_dynamic_sources, + axis_pid_dims=direct_axis_pid_dims, + inferred_keys=direct_inferred_keys, + status=direct_status, + diagnostics=direct_diagnostics, + ) + return None + + ordered_key_axes = list(key_axis_to_param.keys()) + split_params: Dict[str, str] = {} + tiling_params: Dict[str, str] = {} + used_axes = set() + + for idx, (axis_index, tunable_name) in enumerate(axis_tunable_pairs): + key_axis = _resolve_key_axis_name(ordered_key_axes, axis_index) + if idx == 0: + split_params[key_axis] = tunable_name + used_axes.add(key_axis) + continue + if key_axis in used_axes: + continue + tiling_params[key_axis] = tunable_name + used_axes.add(key_axis) + + if not split_params and not tiling_params: + if direct_status != "ok": + return VvConfigAdapterResult( + split_params={}, + tiling_params={}, + low_dim_axes=direct_low_dim, + reduction_axes=direct_reduction, + axis_length_exprs=direct_axis_length_exprs, + fixed_tiling_exprs=direct_fixed_tiling_exprs, + axis_dynamic_sources=direct_axis_dynamic_sources, + axis_pid_dims=direct_axis_pid_dims, + inferred_keys=direct_inferred_keys, + status=direct_status, + diagnostics=direct_diagnostics, + ) + return None + return VvConfigAdapterResult( + split_params=split_params, + tiling_params=tiling_params, + low_dim_axes=direct_low_dim, + reduction_axes=direct_reduction, + axis_length_exprs=direct_axis_length_exprs, + fixed_tiling_exprs=direct_fixed_tiling_exprs, + axis_dynamic_sources=direct_axis_dynamic_sources, + axis_pid_dims=direct_axis_pid_dims, + inferred_keys=direct_inferred_keys, + status=direct_status, + diagnostics=direct_diagnostics, + ) diff --git a/third_party/ascend/backend/runtime/dsl_analysis/vv_param_parser_v2.py b/third_party/ascend/backend/runtime/dsl_analysis/vv_param_parser_v2.py new file mode 100644 index 0000000000..96af5e50da --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/vv_param_parser_v2.py @@ -0,0 +1,1904 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import ast +from dataclasses import dataclass, field +from typing import Dict, List, Mapping, Optional, Sequence, Set, Tuple + +from .axis_length_resolver import classify_length_symbol +from .axis_semantic_schema import (AxisExtent, AxisSemanticInfo, AxisSemanticResult, AxisSplit, AxisTiling) +from .dynamic_source_utils import resolve_dynamic_source +from .load_semantics import ( + build_assignment_expr_map, + build_symbol_user_expr_map, + collect_axis_candidates, + extract_axis_block_symbol_map, + extract_axis_total_symbol_map, + extract_name_depths_transitive, + extract_name_ids, + extract_name_ids_transitive, + pick_param_candidate, +) +from .load_dependency_analyzer import collect_load_derived_symbols +from .schema import (AXIS_LENGTH_STATE_FIXED_COMPILE_TIME, AXIS_LENGTH_STATE_TUNABLE) +from .signature_analyzer import extract_signature_info + +_VALID_AXIS_NAMES = ("x", "y", "z", "w", "v", "t") +_REDUCTION_FUNCS = ("sum", "xor_sum", "max", "min", "argmax", "argmin") + + +@dataclass(frozen=True) +class VvAxisInfoV2: + axis_index: int + length_expr: Optional[str] + axis_symbol: Optional[str] = None + state: str = "unknown" + tunable_param: Optional[str] = None + const_value: Optional[int] = None + split_param: Optional[str] = None + tiling_param: Optional[str] = None + fixed_tiling_expr: Optional[str] = None + is_low_dim: bool = False + is_reduction: bool = False + dynamic_source: str = "none" + + +@dataclass(frozen=True) +class VvAxisParseResultV2: + axis_count: int + axes: List[VvAxisInfoV2] + source: str + diagnostics: List[str] + axis_length_exprs: Dict[str, str] = field(default_factory=dict) + fixed_tiling_exprs: Dict[str, str] = field(default_factory=dict) + axis_pid_dims: Dict[str, int] = field(default_factory=dict) + inferred_keys: Dict[str, str] = field(default_factory=dict) + split_params: Dict[str, str] = field(default_factory=dict) + tiling_params: Dict[str, str] = field(default_factory=dict) + axis_dynamic_sources: Dict[str, str] = field(default_factory=dict) + low_dim_axes: List[str] = field(default_factory=list) + reduction_axes: List[str] = field(default_factory=list) + status: str = "ok" + + +@dataclass +class _SemanticAxisEvidence: + axis_index: int + axis_symbol: Optional[str] = None + extent_candidates: List[Tuple[str, str, float]] = field(default_factory=list) + split_candidates: List[Tuple[str, int, str, float]] = field(default_factory=list) + tiling_candidates: List[Tuple[str, Optional[str], str, float]] = field(default_factory=list) + fixed_tiling_candidates: List[Tuple[str, str, float]] = field(default_factory=list) + is_low_dim: bool = False + diagnostics: List[str] = field(default_factory=list) + + +@dataclass(frozen=True) +class _MakeBlockPtrSpec: + shape_dims: List[str] + block_shape_exprs: List[ast.AST] + offset_exprs: List[ast.AST] + + +def _ast_to_text(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Constant): + return str(node.value) + return ast.unparse(node) + + +def _normalize_tunable_param_symbol(symbol: Optional[str]) -> Optional[str]: + if symbol is None: + return None + if not isinstance(symbol, str): + return None + if not symbol.isidentifier(): + return None + return symbol + + +def _is_tl_call(node: ast.AST, name: str) -> bool: + return (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) and node.func.value.id == "tl" and node.func.attr == name) + + +def _find_function_node(tree: ast.AST) -> ast.AST: + if isinstance(tree, (ast.FunctionDef, ast.AsyncFunctionDef)): + return tree + if isinstance(tree, ast.Module): + for node in tree.body: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + return node + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + return node + raise ValueError("Unable to locate function definition in VV parser v2 input AST.") + + +def _find_function_by_name(tree: ast.AST, function_name: Optional[str]) -> Optional[ast.AST]: + if not function_name: + return None + for node in ast.walk(tree): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if node.name == function_name: + return node + return None + + +def _resolve_function_node( + func_ast: ast.AST, + module_ast: Optional[ast.AST] = None, + entry_function_name: Optional[str] = None, +) -> ast.AST: + if isinstance(module_ast, ast.AST) and entry_function_name: + resolved = _find_function_by_name(module_ast, entry_function_name) + if resolved is not None: + return resolved + + if isinstance(func_ast, ast.Module) and entry_function_name: + resolved = _find_function_by_name(func_ast, entry_function_name) + if resolved is not None: + return resolved + + return _find_function_node(func_ast) + + +def _iter_walk_in_order(node: ast.AST): + yield node + for child in ast.iter_child_nodes(node): + yield from _iter_walk_in_order(child) + + +def _get_arange_stop_expr(call_node: ast.Call) -> Optional[ast.AST]: + if len(call_node.args) >= 2: + return call_node.args[1] + if len(call_node.args) == 1: + return call_node.args[0] + for keyword in call_node.keywords: + if keyword.arg in ("end", "stop"): + return keyword.value + return None + + +def _collect_arange_stop_texts(expr: ast.AST) -> List[str]: + values: List[str] = [] + seen = set() + for node in _iter_walk_in_order(expr): + if not _is_tl_call(node, "arange"): + continue + stop_expr = _get_arange_stop_expr(node) + if stop_expr is None: + continue + text = _ast_to_text(stop_expr) + if text in seen: + continue + seen.add(text) + values.append(text) + return values + + +def _extract_call_arg( + call_node: ast.Call, + keyword_name: str, + positional_index: int, +) -> Optional[ast.AST]: + for keyword in call_node.keywords: + if keyword.arg == keyword_name: + return keyword.value + if len(call_node.args) > positional_index: + return call_node.args[positional_index] + return None + + +def _extract_tuple_items(node: Optional[ast.AST]) -> List[ast.AST]: + if not isinstance(node, (ast.Tuple, ast.List)): + return [] + return list(node.elts) + + +def _extract_make_block_ptr_spec(call_node: ast.Call) -> Optional[_MakeBlockPtrSpec]: + shape_items = _extract_tuple_items(_extract_call_arg(call_node, "shape", 1)) + if not shape_items: + return None + block_shape_items = _extract_tuple_items(_extract_call_arg(call_node, "block_shape", 4)) + offset_items = _extract_tuple_items(_extract_call_arg(call_node, "offsets", 3)) + return _MakeBlockPtrSpec( + shape_dims=[_ast_to_text(item) for item in shape_items], + block_shape_exprs=block_shape_items, + offset_exprs=offset_items, + ) + + +def _extract_make_block_ptr_shape(call_node: ast.Call) -> List[str]: + spec = _extract_make_block_ptr_spec(call_node) + if spec is None: + return [] + return list(spec.shape_dims) + + +def _collect_make_block_ptr_specs(func_node: ast.AST) -> Dict[str, _MakeBlockPtrSpec]: + result: Dict[str, _MakeBlockPtrSpec] = {} + for node in _iter_walk_in_order(func_node): + if not isinstance(node, ast.Assign) or len(node.targets) != 1: + continue + target = node.targets[0] + if not isinstance(target, ast.Name): + continue + if not _is_tl_call(node.value, "make_block_ptr"): + continue + spec = _extract_make_block_ptr_spec(node.value) + if spec is None: + continue + result[target.id] = spec + return result + + +def _extract_target_names(target: ast.AST) -> List[str]: + if isinstance(target, ast.Name): + return [target.id] + if isinstance(target, (ast.Tuple, ast.List)): + names: List[str] = [] + for item in target.elts: + names.extend(_extract_target_names(item)) + return names + return [] + + +def _to_static_bool(value: object) -> Optional[bool]: + if isinstance(value, bool): + return value + if isinstance(value, int): + return bool(value) + return None + + +def _resolve_static_scalar(expr: ast.AST, known_values: Mapping[str, object]) -> Optional[object]: + if isinstance(expr, ast.Constant): + if isinstance(expr.value, (bool, int, float)): + return expr.value + return None + if isinstance(expr, ast.Name): + value = known_values.get(expr.id, None) + if isinstance(value, (bool, int, float)): + return value + return None + if isinstance(expr, ast.UnaryOp): + operand = _resolve_static_scalar(expr.operand, known_values) + if operand is None: + return None + if isinstance(expr.op, ast.Not): + operand_bool = _to_static_bool(operand) + if operand_bool is None: + return None + return not operand_bool + if isinstance(expr.op, ast.USub): + if isinstance(operand, (int, float)): + return -operand + return None + if isinstance(expr.op, ast.UAdd): + if isinstance(operand, (int, float)): + return +operand + return None + return None + if isinstance(expr, ast.BoolOp): + if isinstance(expr.op, ast.And): + maybe_unknown = False + for child in expr.values: + value = _resolve_static_bool_expr(child, known_values) + if value is False: + return False + if value is None: + maybe_unknown = True + return None if maybe_unknown else True + if isinstance(expr.op, ast.Or): + maybe_unknown = False + for child in expr.values: + value = _resolve_static_bool_expr(child, known_values) + if value is True: + return True + if value is None: + maybe_unknown = True + return None if maybe_unknown else False + return None + if isinstance(expr, ast.Compare): + if len(expr.ops) != 1 or len(expr.comparators) != 1: + return None + lhs = _resolve_static_scalar(expr.left, known_values) + rhs = _resolve_static_scalar(expr.comparators[0], known_values) + if lhs is None or rhs is None: + return None + op = expr.ops[0] + if isinstance(op, ast.Eq): + return lhs == rhs + if isinstance(op, ast.NotEq): + return lhs != rhs + if isinstance(op, ast.Lt): + return lhs < rhs + if isinstance(op, ast.LtE): + return lhs <= rhs + if isinstance(op, ast.Gt): + return lhs > rhs + if isinstance(op, ast.GtE): + return lhs >= rhs + return None + return None + + +def _resolve_static_bool_expr(expr: ast.AST, known_values: Mapping[str, object]) -> Optional[bool]: + scalar = _resolve_static_scalar(expr, known_values) + if scalar is None: + return None + return _to_static_bool(scalar) + + +def _clone_assignment_expr_map(src: Mapping[str, Sequence[ast.AST]]) -> Dict[str, List[ast.AST]]: + return {name: list(exprs) for name, exprs in src.items()} + + +def _append_unique_expr(values: List[ast.AST], expr: ast.AST) -> None: + expr_dump = ast.dump(expr) + for current in values: + if ast.dump(current) == expr_dump: + return + values.append(expr) + + +def _merge_assignment_expr_map( + dst: Dict[str, List[ast.AST]], + src: Mapping[str, Sequence[ast.AST]], +) -> None: + for name, exprs in src.items(): + bucket = dst.setdefault(name, []) + for expr in exprs: + _append_unique_expr(bucket, expr) + + +def _set_assignment_expr( + assignment_expr_map: Dict[str, List[ast.AST]], + target_names: Sequence[str], + value_expr: ast.AST, +) -> None: + for name in target_names: + assignment_expr_map[name] = [value_expr] + + +def _visit_stmt_for_assignment_map( + stmt: ast.stmt, + assignment_expr_map: Dict[str, List[ast.AST]], + known_values: Mapping[str, object], +) -> None: + if isinstance(stmt, ast.Assign): + target_names: List[str] = [] + for target in stmt.targets: + target_names.extend(_extract_target_names(target)) + if target_names: + _set_assignment_expr(assignment_expr_map, target_names, stmt.value) + return + + if isinstance(stmt, ast.AnnAssign): + if stmt.value is None: + return + target_names = _extract_target_names(stmt.target) + if target_names: + _set_assignment_expr(assignment_expr_map, target_names, stmt.value) + return + + if isinstance(stmt, ast.If): + cond = _resolve_static_bool_expr(stmt.test, known_values) + if cond is True: + for child in stmt.body: + _visit_stmt_for_assignment_map(child, assignment_expr_map, known_values) + return + if cond is False: + for child in stmt.orelse: + _visit_stmt_for_assignment_map(child, assignment_expr_map, known_values) + return + body_map = _clone_assignment_expr_map(assignment_expr_map) + else_map = _clone_assignment_expr_map(assignment_expr_map) + for child in stmt.body: + _visit_stmt_for_assignment_map(child, body_map, known_values) + for child in stmt.orelse: + _visit_stmt_for_assignment_map(child, else_map, known_values) + assignment_expr_map.clear() + _merge_assignment_expr_map(assignment_expr_map, body_map) + _merge_assignment_expr_map(assignment_expr_map, else_map) + return + + if isinstance(stmt, ast.For): + loop_map = _clone_assignment_expr_map(assignment_expr_map) + for child in stmt.body: + _visit_stmt_for_assignment_map(child, loop_map, known_values) + for child in stmt.orelse: + _visit_stmt_for_assignment_map(child, loop_map, known_values) + _merge_assignment_expr_map(assignment_expr_map, loop_map) + return + + if isinstance(stmt, ast.While): + loop_map = _clone_assignment_expr_map(assignment_expr_map) + for child in stmt.body: + _visit_stmt_for_assignment_map(child, loop_map, known_values) + for child in stmt.orelse: + _visit_stmt_for_assignment_map(child, loop_map, known_values) + _merge_assignment_expr_map(assignment_expr_map, loop_map) + return + + if isinstance(stmt, ast.With): + for child in stmt.body: + _visit_stmt_for_assignment_map(child, assignment_expr_map, known_values) + return + + +def _build_axis_symbol_to_length_map(func_node: ast.AST) -> Dict[str, str]: + symbol_to_length: Dict[str, str] = {} + assignment_expr: Dict[str, ast.AST] = {} + for node in _iter_walk_in_order(func_node): + if not isinstance(node, ast.Assign) or len(node.targets) != 1: + continue + target = node.targets[0] + if not isinstance(target, ast.Name): + continue + assignment_expr[target.id] = node.value + if _is_tl_call(node.value, "arange"): + stop_expr = _get_arange_stop_expr(node.value) + if stop_expr is None: + continue + symbol_to_length[target.id] = _ast_to_text(stop_expr) + continue + arange_lengths = _collect_arange_stop_texts(node.value) + if len(arange_lengths) == 1: + symbol_to_length[target.id] = arange_lengths[0] + + changed = True + while changed: + changed = False + for name, expr in assignment_expr.items(): + if name in symbol_to_length: + continue + if isinstance(expr, ast.Name) and expr.id in symbol_to_length: + symbol_to_length[name] = symbol_to_length[expr.id] + changed = True + return symbol_to_length + + +def _iter_expr_closure(expr: ast.AST, assignment_expr_map: Mapping[str, Sequence[ast.AST]]): + queue = [expr] + seen = set() + while queue: + current = queue.pop(0) + key = id(current) + if key in seen: + continue + seen.add(key) + yield current + for node in _iter_walk_in_order(current): + if not isinstance(node, ast.Name): + continue + assigned_exprs = assignment_expr_map.get(node.id, ()) + for assigned in assigned_exprs: + queue.append(assigned) + + +def _is_slice_all(node: ast.AST) -> bool: + return (isinstance(node, ast.Slice) and node.lower is None and node.upper is None and node.step is None) + + +def _is_none_const(node: ast.AST) -> bool: + return isinstance(node, ast.Constant) and node.value is None + + +def _extract_axis_symbol_from_subscript_base(base_node: ast.AST) -> Optional[str]: + if isinstance(base_node, ast.Name): + return base_node.id + if _is_tl_call(base_node, "arange") and isinstance(base_node, ast.Call): + stop_expr = _get_arange_stop_expr(base_node) + if stop_expr is not None: + return _ast_to_text(stop_expr) + arange_lengths = _collect_arange_stop_texts(base_node) + if len(arange_lengths) == 1: + return arange_lengths[0] + return None + + +def _extract_subscript_axis_info(node: ast.Subscript) -> Optional[Tuple[int, str, int]]: + symbol = _extract_axis_symbol_from_subscript_base(node.value) + if symbol is None: + return None + if not isinstance(node.slice, ast.Tuple): + return None + dims = node.slice.elts + if len(dims) < 2 or len(dims) > 4: + return None + axis_index = None + for idx, dim in enumerate(dims): + if _is_slice_all(dim): + if axis_index is not None: + return None + axis_index = idx + continue + if _is_none_const(dim): + continue + return None + if axis_index is None: + return None + return axis_index, symbol, len(dims) + + +def _extract_memory_pointer_expr(memory_node: ast.Call) -> Optional[ast.AST]: + if memory_node.args: + return memory_node.args[0] + for keyword in memory_node.keywords: + if keyword.arg in ("pointer", "ptr", "input"): + return keyword.value + return None + + +def _get_program_id_dim(node: ast.AST) -> Optional[int]: + if not _is_tl_call(node, "program_id"): + return None + axis_dim = 0 + if len(node.args) > 0: + if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, int): + axis_dim = node.args[0].value + else: + return None + for kw in node.keywords: + if kw.arg != "axis": + continue + if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, int): + axis_dim = kw.value.value + else: + return None + break + return axis_dim + + +def _build_program_id_var_dims(func_node: ast.AST) -> Dict[str, int]: + result: Dict[str, int] = {} + for node in _iter_walk_in_order(func_node): + if not isinstance(node, ast.Assign): + continue + if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name): + continue + pid_dim = _get_program_id_dim(node.value) + if pid_dim is None: + continue + result[node.targets[0].id] = pid_dim + return result + + +def _extract_pid_dim_from_expr(expr: ast.AST, program_id_var_dims: Dict[str, int]) -> Optional[int]: + for node in ast.walk(expr): + if isinstance(node, ast.Name) and node.id in program_id_var_dims: + return program_id_var_dims[node.id] + pid_dim = _get_program_id_dim(node) + if pid_dim is not None: + return pid_dim + return None + + +def _expr_has_axis_primitive(expr: ast.AST, program_id_var_dims: Dict[str, int]) -> bool: + for node in ast.walk(expr): + if _is_tl_call(node, "arange"): + return True + if isinstance(node, ast.Name) and node.id in program_id_var_dims: + return True + if _get_program_id_dim(node) is not None: + return True + return False + + +def _collect_offset_symbols_from_expr( + ptr_expr: ast.AST, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], + program_id_var_dims: Dict[str, int], +) -> List[str]: + symbols: List[str] = [] + seen = set() + queue: List[Tuple[str, int]] = [] + + for node in _iter_walk_in_order(ptr_expr): + if not isinstance(node, ast.Name): + continue + if node.id in seen: + continue + seen.add(node.id) + queue.append((node.id, 0)) + + while queue: + name, depth = queue.pop(0) + assigned_exprs = list(assignment_expr_map.get(name, ())) + if not assigned_exprs: + continue + if any(_expr_has_axis_primitive(assigned, program_id_var_dims) for assigned in assigned_exprs): + symbols.append(name) + continue + if depth >= 1: + continue + for assigned in assigned_exprs: + for child in _iter_walk_in_order(assigned): + if not isinstance(child, ast.Name): + continue + if child.id in seen: + continue + seen.add(child.id) + queue.append((child.id, depth + 1)) + return symbols + + +def _collect_ptr_axis_symbols( + ptr_expr: ast.AST, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], + program_id_var_dims: Dict[str, int], + axis_candidates: Optional[Set[str]] = None, + value_axis_provenance: Optional[Dict[str, Dict[int, str]]] = None, + provenance_visiting: Optional[Set[Tuple[str, int]]] = None, +) -> Tuple[List[int], Dict[int, str], Dict[int, bool]]: + symbol_by_index: Dict[int, str] = {} + is_low_dim_by_index: Dict[int, bool] = {} + expr_owner_by_id = {} + for name, exprs in assignment_expr_map.items(): + for expr in exprs: + expr_owner_by_id[id(expr)] = name + root_ptr_name = ptr_expr.id if isinstance(ptr_expr, ast.Name) else None + axis_candidates = set(axis_candidates or ()) + value_axis_provenance = value_axis_provenance if value_axis_provenance is not None else {} + provenance_visiting = provenance_visiting if provenance_visiting is not None else set() + + def _resolve_value_axis_symbol(symbol: str, axis_index: int) -> str: + if symbol in axis_candidates: + return symbol + + cached = value_axis_provenance.get(symbol, None) + if cached is not None and axis_index in cached: + return cached[axis_index] + + visiting_key = (symbol, axis_index) + if visiting_key in provenance_visiting: + return symbol + + provenance_visiting.add(visiting_key) + try: + for expr in assignment_expr_map.get(symbol, ()): + base_expr = expr + if isinstance(expr, ast.Call): + memory_expr = _extract_memory_pointer_expr(expr) + if memory_expr is not None: + base_expr = memory_expr + axis_indices, resolved_symbols, _ = _collect_ptr_axis_symbols( + base_expr, + assignment_expr_map, + program_id_var_dims, + axis_candidates=axis_candidates, + value_axis_provenance=value_axis_provenance, + provenance_visiting=provenance_visiting, + ) + if axis_index not in axis_indices: + continue + resolved = resolved_symbols.get(axis_index, symbol) + value_axis_provenance.setdefault(symbol, {})[axis_index] = resolved + return resolved + finally: + provenance_visiting.discard(visiting_key) + return symbol + + for current_expr in _iter_expr_closure(ptr_expr, assignment_expr_map): + owner_symbol = expr_owner_by_id.get(id(current_expr)) + for node in _iter_walk_in_order(current_expr): + if not isinstance(node, ast.Subscript): + continue + axis_info = _extract_subscript_axis_info(node) + if axis_info is None: + continue + axis_index, symbol, ndim = axis_info + if axis_index in symbol_by_index: + continue + if isinstance(symbol, str): + symbol = _resolve_value_axis_symbol(symbol, axis_index) + # For patterns like: + # row_offsets = ... + tl.arange(0, XBLOCK_SUB)[:, None] + # prefer the assignment owner (`row_offsets`) over the inline + # arange stop symbol (`XBLOCK_SUB`), so downstream split/tiling + # extraction can trace program_id/loop evidence from the offset var. + if (_is_tl_call(node.value, "arange") and isinstance(owner_symbol, str) and owner_symbol != root_ptr_name): + symbol = owner_symbol + elif (isinstance(owner_symbol, str) and owner_symbol != root_ptr_name and axis_candidates + and owner_symbol in axis_candidates and symbol not in axis_candidates): + symbol = owner_symbol + symbol_by_index[axis_index] = symbol + is_low_dim_by_index[axis_index] = axis_index == (ndim - 1) + + if symbol_by_index: + sorted_indices = sorted(symbol_by_index.keys()) + return sorted_indices, symbol_by_index, is_low_dim_by_index + + fallback_symbols = _collect_offset_symbols_from_expr( + ptr_expr, + assignment_expr_map, + program_id_var_dims, + ) + if not fallback_symbols: + return [], {}, {} + + # Prefer high-confidence axis candidates collected from arange/mod/subscript + # patterns when fallback symbols mix real vector offsets with base-pointer + # symbols (e.g. `base + pid * stride + offset`). + if axis_candidates: + preferred_symbols = [symbol for symbol in fallback_symbols if symbol in axis_candidates] + if preferred_symbols: + fallback_symbols = preferred_symbols + + symbol_by_index = {idx: symbol for idx, symbol in enumerate(fallback_symbols)} + is_low_dim_by_index = {idx: idx == (len(fallback_symbols) - 1) for idx in range(len(fallback_symbols))} + return list(range(len(fallback_symbols))), symbol_by_index, is_low_dim_by_index + + +def _normalize_mask_extent_candidate_expr( + rhs_expr: ast.AST, + *, + symbol: str, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], +) -> Optional[str]: + if not (isinstance(rhs_expr, ast.Call) and isinstance(rhs_expr.func, ast.Name) and rhs_expr.func.id == "min"): + return None + + args = list(rhs_expr.args) + if len(args) < 2: + return None + + dependency_names = _collect_symbol_dependency_names(symbol, assignment_expr_map) + preferred_args: List[ast.AST] = [] + for arg in args: + arg_names = extract_name_ids_transitive(arg, assignment_expr_map) + if arg_names.intersection(dependency_names): + continue + preferred_args.append(arg) + + if len(preferred_args) != 1: + return None + return _ast_to_text(preferred_args[0]) + + +def _collect_mask_extent_candidates_for_symbol( + symbol: str, + func_node: ast.AST, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], + load_derived_symbols: Optional[Set[str]] = None, +) -> List[Tuple[str, str, float]]: + candidates: List[Tuple[str, str, float]] = [] + seen = set() + load_derived_symbols = load_derived_symbols or set() + + def _contains_symbol_direct(expr: ast.AST, target: str) -> bool: + for child in ast.walk(expr): + if isinstance(child, ast.Name) and child.id == target: + return True + return False + + for node in ast.walk(func_node): + if not isinstance(node, ast.Compare): + continue + if len(node.ops) != 1 or not isinstance(node.ops[0], ast.Lt): + continue + if len(node.comparators) != 1: + continue + + hit = False + for current_expr in _iter_expr_closure(node.left, assignment_expr_map): + for child in ast.walk(current_expr): + if isinstance(child, ast.Name) and child.id == symbol: + hit = True + break + if hit: + break + if not hit: + continue + + # Reject value-domain comparisons (e.g. logits < 0) whose left side is + # already load-derived. Such predicates should not be treated as axis + # extent masks. + if _expr_uses_symbols(node.left, load_derived_symbols): + continue + + # Prefer direct offset-mask patterns (symbol appears directly on left). + # Indirect dataflow hits are still kept but with lower confidence. + is_direct_hit = _contains_symbol_direct(node.left, symbol) + + rhs_expr = _normalize_mask_extent_candidate_expr( + node.comparators[0], + symbol=symbol, + assignment_expr_map=assignment_expr_map, + ) or _ast_to_text(node.comparators[0]) + if rhs_expr in seen: + continue + seen.add(rhs_expr) + source = "mask_direct" if is_direct_hit else "mask_indirect" + confidence = 1.0 if is_direct_hit else 0.6 + candidates.append((rhs_expr, source, confidence)) + return candidates + + +def _extract_range_stop_and_step(node: ast.For) -> Tuple[Optional[ast.AST], Optional[ast.AST]]: + if not isinstance(node.iter, ast.Call): + return None, None + iter_fn = node.iter.func + is_range = isinstance(iter_fn, ast.Name) and iter_fn.id == "range" + is_tl_range = (isinstance(iter_fn, ast.Attribute) and isinstance(iter_fn.value, ast.Name) + and iter_fn.value.id == "tl" and iter_fn.attr == "range") + if not (is_range or is_tl_range): + return None, None + + args = list(node.iter.args) + if len(args) == 0: + return None, None + if len(args) == 1: + return args[0], None + if len(args) == 2: + return args[1], None + return args[1], args[2] + + +def _collect_loop_stop_candidates_for_symbol( + symbol: str, + func_node: ast.AST, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], +) -> List[Tuple[str, str, float]]: + candidates: List[Tuple[str, str, float]] = [] + seen = set() + dependency_names = _collect_symbol_dependency_names(symbol, assignment_expr_map) + + for node in ast.walk(func_node): + if not isinstance(node, ast.For): + continue + if not isinstance(node.target, ast.Name): + continue + if node.target.id not in dependency_names: + continue + stop_expr, _ = _extract_range_stop_and_step(node) + if stop_expr is None: + continue + + stop_text = _ast_to_text(stop_expr) + if stop_text in seen: + continue + seen.add(stop_text) + candidates.append((stop_text, "loop", 0.7)) + return candidates + + +def _collect_loop_steps(func_node: ast.AST) -> Dict[str, str]: + result: Dict[str, str] = {} + for node in ast.walk(func_node): + if not isinstance(node, ast.For): + continue + if not isinstance(node.target, ast.Name): + continue + _, step_expr = _extract_range_stop_and_step(node) + if step_expr is None: + continue + result[node.target.id] = _ast_to_text(step_expr) + return result + + +def _extract_param_names(func_node: ast.AST) -> Set[str]: + if not isinstance(func_node, (ast.FunctionDef, ast.AsyncFunctionDef)): + return set() + args_node = func_node.args + ordered_args = list(args_node.posonlyargs) + list(args_node.args) + list(args_node.kwonlyargs) + return {arg.arg for arg in ordered_args if isinstance(arg, ast.arg)} + + +def _expr_uses_symbols(expr: ast.AST, symbols: Set[str]) -> bool: + if not symbols: + return False + for node in ast.walk(expr): + if isinstance(node, ast.Name) and node.id in symbols: + return True + return False + + +def _collect_symbol_dependency_names( + symbol: str, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], +) -> Set[str]: + names = {symbol} + for expr in assignment_expr_map.get(symbol, ()): + names.update(extract_name_ids_transitive(expr, assignment_expr_map)) + return names + + +def _extract_axis_symbols_from_expr(expr: ast.AST, axis_candidates: Set[str]) -> Set[str]: + symbols: Set[str] = set() + for node in ast.walk(expr): + if isinstance(node, ast.Name) and node.id in axis_candidates: + symbols.add(node.id) + continue + if not isinstance(node, ast.Subscript): + continue + axis_info = _extract_subscript_axis_info(node) + if axis_info is None: + continue + _, symbol, _ = axis_info + if symbol in axis_candidates: + symbols.add(symbol) + return symbols + + +def _extract_split_candidates_for_symbol( + symbol: str, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], + program_id_var_dims: Dict[str, int], + tunable_params: Set[str], + symbol_user_expr_map: Optional[Mapping[str, Sequence[ast.AST]]] = None, +) -> List[Tuple[str, int, str, float]]: + exprs = list(assignment_expr_map.get(symbol, ())) + if symbol_user_expr_map is not None: + exprs.extend(symbol_user_expr_map.get(symbol, ())) + if not exprs: + return [] + + result: List[Tuple[str, int, str, float]] = [] + seen = set() + for expr in exprs: + for current_expr in _iter_expr_closure(expr, assignment_expr_map): + for node in ast.walk(current_expr): + if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.Mult): + continue + sides = ((node.left, node.right), (node.right, node.left)) + for param_node, pid_side in sides: + if not isinstance(param_node, ast.Name): + continue + param_name = param_node.id + if param_name not in tunable_params: + continue + pid_dim = _extract_pid_dim_from_expr(pid_side, program_id_var_dims) + if pid_dim is None: + continue + key = (param_name, pid_dim) + if key in seen: + continue + seen.add(key) + result.append((param_name, pid_dim, "program_id", 0.95)) + return result + + +def _extract_split_candidates_from_expr( + expr: Optional[ast.AST], + program_id_var_dims: Dict[str, int], + tunable_params: Set[str], +) -> List[Tuple[str, int, str, float]]: + if expr is None: + return [] + result: List[Tuple[str, int, str, float]] = [] + seen = set() + for node in ast.walk(expr): + if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.Mult): + continue + sides = ((node.left, node.right), (node.right, node.left)) + for param_node, pid_side in sides: + if not isinstance(param_node, ast.Name): + continue + param_name = param_node.id + if param_name not in tunable_params: + continue + pid_dim = _extract_pid_dim_from_expr(pid_side, program_id_var_dims) + if pid_dim is None: + continue + key = (param_name, pid_dim) + if key in seen: + continue + seen.add(key) + result.append((param_name, pid_dim, "program_id", 0.95)) + return result + + +def _extract_make_block_ptr_tiling_param( + expr: Optional[ast.AST], + tunable_params: Set[str], +) -> Optional[str]: + if expr is None: + return None + text = _normalize_tunable_param_symbol(_ast_to_text(expr)) + if text is None or text not in tunable_params: + return None + return text + + +def _extract_make_block_ptr_fixed_tiling_expr( + expr: Optional[ast.AST], + signature, + provided_args: Mapping[str, object], +) -> Optional[str]: + if expr is None: + return None + text = _ast_to_text(expr) + state, _ = classify_length_symbol( + text, + signature=signature, + provided_args=provided_args, + ) + if state == AXIS_LENGTH_STATE_TUNABLE: + return None + if text and state == AXIS_LENGTH_STATE_FIXED_COMPILE_TIME: + return text + return None + + +def _extract_tiling_candidates_for_symbol( + symbol: str, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], + tunable_params: Set[str], + loop_steps: Dict[str, str], +) -> List[Tuple[str, Optional[str], str, float]]: + exprs = list(assignment_expr_map.get(symbol, ())) + if not exprs: + return [] + + result: List[Tuple[str, Optional[str], str, float]] = [] + seen = set() + + for expr in exprs: + for current_expr in _iter_expr_closure(expr, assignment_expr_map): + for stop_text in _collect_arange_stop_texts(current_expr): + if stop_text not in tunable_params: + continue + key = (stop_text, None, "arange") + if key in seen: + continue + seen.add(key) + result.append((stop_text, None, "arange", 0.9)) + + for node in ast.walk(current_expr): + if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.Mult): + continue + if isinstance(node.left, ast.Name) and isinstance(node.right, ast.Name): + left_name = node.left.id + right_name = node.right.id + if left_name in loop_steps and right_name in tunable_params: + key = (right_name, left_name, "range") + if key not in seen: + seen.add(key) + result.append((right_name, left_name, "range", 0.75)) + if right_name in loop_steps and left_name in tunable_params: + key = (left_name, right_name, "range") + if key not in seen: + seen.add(key) + result.append((left_name, right_name, "range", 0.75)) + return result + + +def _extract_fixed_tiling_candidates_for_symbol( + symbol: str, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], + signature, + provided_args: Mapping[str, object], +) -> List[Tuple[str, str, float]]: + exprs = list(assignment_expr_map.get(symbol, ())) + if not exprs: + return [] + + result: List[Tuple[str, str, float]] = [] + seen = set() + + for expr in exprs: + for current_expr in _iter_expr_closure(expr, assignment_expr_map): + for stop_text in _collect_arange_stop_texts(current_expr): + state, _ = classify_length_symbol( + stop_text, + signature=signature, + provided_args=provided_args, + ) + if state != AXIS_LENGTH_STATE_FIXED_COMPILE_TIME: + continue + key = (stop_text, "arange") + if key in seen: + continue + seen.add(key) + result.append((stop_text, "arange", 0.9)) + return result + + +def _extract_range_start_expr(node: ast.For) -> Optional[ast.AST]: + if not isinstance(node.iter, ast.Call): + return None + args = list(node.iter.args) + if len(args) < 2: + return None + return args[0] + + +def _infer_axis_pid_dim_for_symbol( + symbol: str, + func_node: ast.AST, + assignment_expr_map: Mapping[str, Sequence[ast.AST]], + program_id_var_dims: Dict[str, int], +) -> Optional[int]: + candidates: List[int] = [] + seen = set() + + for expr in assignment_expr_map.get(symbol, ()): + pid_dim = _extract_pid_dim_from_expr(expr, program_id_var_dims) + if pid_dim is None or pid_dim in seen: + continue + seen.add(pid_dim) + candidates.append(pid_dim) + + dependency_names = _collect_symbol_dependency_names(symbol, assignment_expr_map) + for node in ast.walk(func_node): + if not isinstance(node, ast.For): + continue + if not isinstance(node.target, ast.Name): + continue + if node.target.id not in dependency_names: + continue + start_expr = _extract_range_start_expr(node) + pid_dim = _extract_pid_dim_from_expr(start_expr, program_id_var_dims) + if pid_dim is None or pid_dim in seen: + continue + seen.add(pid_dim) + candidates.append(pid_dim) + + if len(candidates) == 1: + return candidates[0] + return None + + +def _select_best_split(candidates: Sequence[Tuple[str, int, str, float]], ) -> Tuple[AxisSplit, List[str]]: + if not candidates: + return AxisSplit(param=None, pid_dim=None, source="infer", confidence=0.0), [] + + ordered = sorted(candidates, key=lambda item: (-item[3], item[0], item[1])) + best = ordered[0] + diagnostics: List[str] = [] + distinct = {(param, pid_dim) for param, pid_dim, _, _ in ordered} + if len(distinct) > 1: + diagnostics.append("multiple split candidates resolved, selected {}".format(best[0])) + return AxisSplit(param=best[0], pid_dim=best[1], source=best[2], confidence=best[3]), diagnostics + + +def _select_best_tiling( + candidates: Sequence[Tuple[str, Optional[str], str, float]], + split_param: Optional[str], +) -> Tuple[AxisTiling, List[str]]: + if not candidates: + return AxisTiling(param=None, loop_var=None, source="infer", confidence=0.0), [] + + ordered = sorted(candidates, key=lambda item: (-item[3], item[0])) + diagnostics: List[str] = [] + filtered = [item for item in ordered if item[0] != split_param] + if filtered: + best = filtered[0] + else: + best = ordered[0] + diagnostics.append("tiling candidate {} equals split candidate, use single-parameter mode".format(best[0])) + return AxisTiling(param=None, loop_var=best[1], source=best[2], confidence=best[3]), diagnostics + + distinct = {param for param, _, _, _ in ordered} + if len(distinct) > 1: + diagnostics.append("multiple tiling candidates resolved, selected {}".format(best[0])) + return AxisTiling(param=best[0], loop_var=best[1], source=best[2], confidence=best[3]), diagnostics + + +def _select_best_fixed_tiling_expr(candidates: Sequence[Tuple[str, str, float]], ) -> Tuple[Optional[str], List[str]]: + if not candidates: + return None, [] + + ordered = sorted(candidates, key=lambda item: (-item[2], item[0])) + best = ordered[0] + diagnostics: List[str] = [] + distinct = {expr for expr, _, _ in ordered} + if len(distinct) > 1: + diagnostics.append("multiple fixed tiling candidates resolved, selected {}".format(best[0])) + return best[0], diagnostics + + +def _extent_priority(source: str) -> int: + if source == "mask_direct": + return 1 + if source == "mask_indirect": + return 2 + if source == "infer_total": + return 3 + if source == "loop": + return 4 + if source == "make_block_ptr": + return 5 + return 5 + + +def _select_best_extent( + candidates: Sequence[Tuple[str, str, float]], + split_param: Optional[str], + tiling_param: Optional[str], + signature, + provided_args: Mapping[str, object], + axis_symbol: Optional[str] = None, + load_derived_symbols: Optional[Set[str]] = None, +) -> Tuple[AxisExtent, List[str]]: + load_derived_symbols = load_derived_symbols or set() + + if not candidates: + dynamic_source = resolve_dynamic_source( + axis_symbol=axis_symbol, + length_expr=None, + load_derived_symbols=load_derived_symbols, + ) + return ( + AxisExtent( + expr=None, + state="unknown", + const_value=None, + source="infer", + confidence=0.0, + dynamic_source=dynamic_source, + ), + ["no extent candidate found"], + ) + + dedup: Dict[str, Tuple[str, str, float]] = {} + for expr, source, confidence in candidates: + old = dedup.get(expr) + if old is None or confidence > old[2]: + dedup[expr] = (expr, source, confidence) + ordered = sorted( + dedup.values(), + key=lambda item: (_extent_priority(item[1]), -item[2], item[0]), + ) + + tile_params = {item for item in (split_param, tiling_param) if item} + filtered = [item for item in ordered if item[0] not in tile_params] + + diagnostics: List[str] = [] + if filtered: + best = filtered[0] + else: + best = ordered[0] + diagnostics.append("extent fallback to tile parameter {} because no better candidate".format(best[0])) + + state, const_value = classify_length_symbol( + best[0], + signature=signature, + provided_args=provided_args, + ) + dynamic_source = resolve_dynamic_source( + axis_symbol=axis_symbol, + length_expr=best[0], + load_derived_symbols=load_derived_symbols, + ) + return ( + AxisExtent( + expr=best[0], + state=state, + const_value=const_value, + source=best[1], + confidence=best[2], + dynamic_source=dynamic_source, + ), + diagnostics, + ) + + +def _resolve_tunable_params(signature, provided_args: Mapping[str, object], + hints: Optional[Mapping[str, object]]) -> Set[str]: + constexpr_names = set(signature.constexpr_names()) + missing_constexpr = {name for name in constexpr_names if name not in dict(provided_args or {}).keys()} + + explicit_tunable = set() + if hints and isinstance(hints.get("tunable_parameter"), list): + for item in hints.get("tunable_parameter"): + if isinstance(item, str): + explicit_tunable.add(item) + + return missing_constexpr | explicit_tunable + + +def _collect_reduction_axis_indices(func_node: ast.AST, axis_count: int) -> List[int]: + if axis_count <= 0: + return [] + + result: List[int] = [] + seen = set() + for node in ast.walk(func_node): + if not isinstance(node, ast.Call): + continue + if not (isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) + and node.func.value.id == "tl" and node.func.attr in _REDUCTION_FUNCS): + continue + + axis_node = None + if len(node.args) >= 2: + axis_node = node.args[1] + else: + for kw in node.keywords: + if kw.arg == "axis": + axis_node = kw.value + break + if axis_node is None: + continue + + axis_idx = None + if isinstance(axis_node, ast.Constant) and isinstance(axis_node.value, int): + axis_idx = axis_node.value + elif (isinstance(axis_node, ast.UnaryOp) and isinstance(axis_node.op, ast.USub) + and isinstance(axis_node.operand, ast.Constant) and isinstance(axis_node.operand.value, int)): + axis_idx = -axis_node.operand.value + + if axis_idx is None: + continue + if axis_idx < 0: + axis_idx = axis_count + axis_idx + if axis_idx < 0 or axis_idx >= axis_count: + continue + if axis_idx in seen: + continue + seen.add(axis_idx) + result.append(axis_idx) + return result + + +def _axis_sort_key(axis_name: str) -> Tuple[int, str]: + try: + axis_index = _VALID_AXIS_NAMES.index(axis_name) + except ValueError: + axis_index = len(_VALID_AXIS_NAMES) + return (axis_index, axis_name) + + +def _choose_best_site_evidence( + site_evidences: Sequence[Dict[int, _SemanticAxisEvidence]]) -> Dict[int, _SemanticAxisEvidence]: + if not site_evidences: + return {} + + extent_source_score = { + "mask": 5, + "infer_total": 4, + "loop": 3, + "make_block_ptr": 2, + "load": 1, + } + + def _best_extent_quality(evidence: _SemanticAxisEvidence) -> Tuple[int, float]: + if not evidence.extent_candidates: + return (0, 0.0) + best_source_score = 0 + best_confidence = 0.0 + for _, source, confidence in evidence.extent_candidates: + source_score = extent_source_score.get(source, 0) + if (source_score, confidence) > (best_source_score, best_confidence): + best_source_score = source_score + best_confidence = confidence + return (best_source_score, best_confidence) + + def _score(site: Dict[int, _SemanticAxisEvidence]) -> Tuple[int, int, float, int]: + axis_count = len(site) + resolved_extent_axes = sum(1 for evidence in site.values() if len(evidence.extent_candidates) > 0) + extent_quality_score = sum(_best_extent_quality(evidence)[0] for evidence in site.values()) + extent_confidence_score = sum(_best_extent_quality(evidence)[1] for evidence in site.values()) + return ( + resolved_extent_axes, + extent_quality_score, + extent_confidence_score, + axis_count, + ) + + best = site_evidences[0] + best_score = _score(best) + for site in site_evidences[1:]: + score = _score(site) + if score > best_score: + best = site + best_score = score + return best + + +def parse_vv_axis_semantic_v2( + func_ast: ast.AST, + provided_args: Optional[Mapping[str, object]] = None, + hints: Optional[Mapping[str, object]] = None, + module_ast: Optional[ast.AST] = None, + entry_function_name: Optional[str] = None, +) -> AxisSemanticResult: + """ + Unified axis semantic parser (v2): extent/split/tiling/low_dim/reduction. + """ + func_node = _resolve_function_node( + func_ast, + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + provided_args = dict(provided_args or {}) + load_derived_symbols: Set[str] = set() + try: + analysis_root = (module_ast if isinstance(module_ast, ast.AST) and entry_function_name else func_node) + load_derived_symbols = collect_load_derived_symbols( + analysis_root, + provided_args=provided_args, + entry_function_name=entry_function_name, + ) + except Exception as exc: + load_derived_symbols = set() + diagnostics = ["load-dependency analysis failed: {}".format(type(exc).__name__)] + else: + diagnostics: List[str] = [] + + signature = extract_signature_info(func_node) + tunable_params = _resolve_tunable_params(signature, provided_args, hints) + param_names = _extract_param_names(func_node) + + make_block_ptr_specs = _collect_make_block_ptr_specs(func_node) + assignment_expr_map = build_assignment_expr_map( + func_node, + provided_args=provided_args, + ) + symbol_user_expr_map = build_symbol_user_expr_map(assignment_expr_map) + symbol_to_length = _build_axis_symbol_to_length_map(func_node) + program_id_var_dims = _build_program_id_var_dims(func_node) + loop_steps = _collect_loop_steps(func_node) + axis_candidates = collect_axis_candidates(func_node, param_names=param_names) + axis_total_symbol_map = extract_axis_total_symbol_map( + func_node, + assignment_expr_map=assignment_expr_map, + axis_candidates=axis_candidates, + param_names=param_names, + ) + + source_tags = set() + site_evidences: List[Dict[int, _SemanticAxisEvidence]] = [] + + def _ensure_evidence( + site: Dict[int, _SemanticAxisEvidence], + axis_index: int, + ) -> _SemanticAxisEvidence: + if axis_index not in site: + site[axis_index] = _SemanticAxisEvidence(axis_index=axis_index) + return site[axis_index] + + def _append_extent( + site: Dict[int, _SemanticAxisEvidence], + axis_index: int, + expr: str, + source: str, + confidence: float, + ) -> None: + if not expr: + return + evidence = _ensure_evidence(site, axis_index) + key = (expr, source) + for item in evidence.extent_candidates: + if (item[0], item[1]) == key: + return + evidence.extent_candidates.append((expr, source, confidence)) + + def _append_split( + site: Dict[int, _SemanticAxisEvidence], + axis_index: int, + param: str, + pid_dim: int, + source: str, + confidence: float, + ) -> None: + evidence = _ensure_evidence(site, axis_index) + key = (param, pid_dim, source) + for item in evidence.split_candidates: + if (item[0], item[1], item[2]) == key: + return + evidence.split_candidates.append((param, pid_dim, source, confidence)) + + def _append_tiling( + site: Dict[int, _SemanticAxisEvidence], + axis_index: int, + param: str, + loop_var: Optional[str], + source: str, + confidence: float, + ) -> None: + evidence = _ensure_evidence(site, axis_index) + key = (param, loop_var, source) + for item in evidence.tiling_candidates: + if (item[0], item[1], item[2]) == key: + return + evidence.tiling_candidates.append((param, loop_var, source, confidence)) + + def _append_fixed_tiling( + site: Dict[int, _SemanticAxisEvidence], + axis_index: int, + expr: str, + source: str, + confidence: float, + ) -> None: + if not expr: + return + evidence = _ensure_evidence(site, axis_index) + key = (expr, source) + for item in evidence.fixed_tiling_candidates: + if (item[0], item[1]) == key: + return + evidence.fixed_tiling_candidates.append((expr, source, confidence)) + + def _append_make_block_ptr_spec( + site: Dict[int, _SemanticAxisEvidence], + spec: _MakeBlockPtrSpec, + ) -> None: + if spec.shape_dims: + source_tags.add("make_block_ptr") + for axis_index, dim in enumerate(spec.shape_dims): + _append_extent(site, axis_index, dim, "make_block_ptr", 0.6) + for axis_index, offset_expr in enumerate(spec.offset_exprs): + for param, pid_dim, source, confidence in _extract_split_candidates_from_expr( + offset_expr, + program_id_var_dims, + tunable_params, + ): + _append_split(site, axis_index, param, pid_dim, source, confidence) + for axis_index, block_shape_expr in enumerate(spec.block_shape_exprs): + param = _extract_make_block_ptr_tiling_param( + block_shape_expr, + tunable_params, + ) + if param is not None: + _append_tiling(site, axis_index, param, None, "make_block_ptr", 0.85) + continue + fixed_expr = _extract_make_block_ptr_fixed_tiling_expr( + block_shape_expr, + signature, + provided_args, + ) + if fixed_expr is not None: + _append_fixed_tiling( + site, + axis_index, + fixed_expr, + "make_block_ptr", + 0.8, + ) + + for node in _iter_walk_in_order(func_node): + if _is_tl_call(node, "make_block_ptr"): + current_site: Dict[int, _SemanticAxisEvidence] = {} + spec = _extract_make_block_ptr_spec(node) + if spec is not None: + _append_make_block_ptr_spec(current_site, spec) + if current_site: + site_evidences.append(current_site) + continue + + is_load = _is_tl_call(node, "load") + is_store = _is_tl_call(node, "store") + if not is_load and not is_store: + continue + + current_site: Dict[int, _SemanticAxisEvidence] = {} + ptr_expr = _extract_memory_pointer_expr(node) + if ptr_expr is None: + continue + + source_tags.add("load" if is_load else "store") + + if isinstance(ptr_expr, ast.Name) and ptr_expr.id in make_block_ptr_specs: + _append_make_block_ptr_spec(current_site, make_block_ptr_specs[ptr_expr.id]) + if current_site: + site_evidences.append(current_site) + continue + + if _is_tl_call(ptr_expr, "make_block_ptr"): + spec = _extract_make_block_ptr_spec(ptr_expr) + if spec is not None: + _append_make_block_ptr_spec(current_site, spec) + if current_site: + site_evidences.append(current_site) + continue + + axis_indices, symbol_by_index, is_low_dim_by_index = _collect_ptr_axis_symbols( + ptr_expr, + assignment_expr_map, + program_id_var_dims, + axis_candidates=axis_candidates, + ) + + if not axis_indices: + arange_lengths: List[str] = [] + seen = set() + for current_expr in _iter_expr_closure(ptr_expr, assignment_expr_map): + for stop_text in _collect_arange_stop_texts(current_expr): + if stop_text in seen: + continue + seen.add(stop_text) + arange_lengths.append(stop_text) + for axis_index, length_expr in enumerate(arange_lengths): + _append_extent(current_site, axis_index, length_expr, "load", 0.5) + if current_site: + site_evidences.append(current_site) + continue + + for axis_index in axis_indices: + evidence = _ensure_evidence(current_site, axis_index) + symbol = symbol_by_index.get(axis_index) + if symbol and evidence.axis_symbol is None: + evidence.axis_symbol = symbol + evidence.is_low_dim = evidence.is_low_dim or is_low_dim_by_index.get(axis_index, False) + + if not symbol: + continue + + inferred_total_expr = axis_total_symbol_map.get(symbol, None) + if inferred_total_expr: + _append_extent(current_site, axis_index, inferred_total_expr, "infer_total", 0.85) + + for expr, source, confidence in _collect_mask_extent_candidates_for_symbol( + symbol, + func_node, + assignment_expr_map, + load_derived_symbols, + ): + _append_extent(current_site, axis_index, expr, source, confidence) + + for expr, source, confidence in _collect_loop_stop_candidates_for_symbol( + symbol, + func_node, + assignment_expr_map, + ): + _append_extent(current_site, axis_index, expr, source, confidence) + + fallback_expr = symbol_to_length.get(symbol) + if fallback_expr: + _append_extent(current_site, axis_index, fallback_expr, "load", 0.4) + + for param, pid_dim, source, confidence in _extract_split_candidates_for_symbol( + symbol, + assignment_expr_map, + program_id_var_dims, + tunable_params, + symbol_user_expr_map=symbol_user_expr_map, + ): + _append_split(current_site, axis_index, param, pid_dim, source, confidence) + + for param, loop_var, source, confidence in _extract_tiling_candidates_for_symbol( + symbol, + assignment_expr_map, + tunable_params, + loop_steps, + ): + _append_tiling(current_site, axis_index, param, loop_var, source, confidence) + + for expr, source, confidence in _extract_fixed_tiling_candidates_for_symbol( + symbol, + assignment_expr_map, + signature, + provided_args, + ): + _append_fixed_tiling(current_site, axis_index, expr, source, confidence) + + if current_site: + site_evidences.append(current_site) + + evidence_by_index = _choose_best_site_evidence(site_evidences) + if not evidence_by_index: + return AxisSemanticResult( + axes={}, + axis_length_exprs={}, + fixed_tiling_exprs={}, + axis_pid_dims={}, + inferred_keys={}, + split_params={}, + tiling_params={}, + low_dim_axes=[], + reduction_axes=[], + status="failed", + diagnostics=["no axis information resolved from tl.load/tl.store/make_block_ptr"], + ) + + sorted_axis_indices = sorted(evidence_by_index.keys()) + axis_name_by_index: Dict[int, str] = {} + for order, axis_index in enumerate(sorted_axis_indices): + if order < len(_VALID_AXIS_NAMES): + axis_name_by_index[axis_index] = _VALID_AXIS_NAMES[order] + else: + axis_name_by_index[axis_index] = "x{}".format(order) + + reduction_indices = _collect_reduction_axis_indices(func_node, len(sorted_axis_indices)) + reduction_base_axes = { + axis_name_by_index[sorted_axis_indices[item]] + for item in reduction_indices + if 0 <= item < len(sorted_axis_indices) + } + + axes: Dict[str, AxisSemanticInfo] = {} + axis_length_exprs: Dict[str, str] = {} + fixed_tiling_exprs: Dict[str, str] = {} + axis_pid_dims: Dict[str, int] = {} + inferred_keys: Dict[str, str] = {} + split_params: Dict[str, str] = {} + tiling_params: Dict[str, str] = {} + low_dim_axes: List[str] = [] + reduction_axes: List[str] = [] + + for axis_index in sorted_axis_indices: + evidence = evidence_by_index[axis_index] + base_axis_name = axis_name_by_index[axis_index] + is_reduction = base_axis_name in reduction_base_axes + axis_name = base_axis_name + + split, split_diag = _select_best_split(evidence.split_candidates) + tiling, tiling_diag = _select_best_tiling(evidence.tiling_candidates, split.param) + fixed_tiling_expr, fixed_tiling_diag = _select_best_fixed_tiling_expr(evidence.fixed_tiling_candidates) + extent, extent_diag = _select_best_extent( + evidence.extent_candidates, + split.param, + tiling.param, + signature, + provided_args, + axis_symbol=evidence.axis_symbol, + load_derived_symbols=load_derived_symbols, + ) + + axis_diags: List[str] = [] + axis_diags.extend(split_diag) + axis_diags.extend(tiling_diag) + axis_diags.extend(fixed_tiling_diag) + axis_diags.extend(extent_diag) + axis_diags.extend(evidence.diagnostics) + + if extent.expr is not None: + axis_length_exprs[axis_name] = extent.expr + inferred_keys[axis_name] = extent.expr + pid_dim = split.pid_dim + if pid_dim is None and evidence.axis_symbol is not None: + pid_dim = _infer_axis_pid_dim_for_symbol( + evidence.axis_symbol, + func_node, + assignment_expr_map, + program_id_var_dims, + ) + if pid_dim is not None: + axis_pid_dims[axis_name] = pid_dim + if split.param is not None: + split_params[axis_name] = split.param + if tiling.param is not None: + tiling_params[axis_name] = tiling.param + if fixed_tiling_expr is not None: + fixed_tiling_exprs[axis_name] = fixed_tiling_expr + + if evidence.is_low_dim: + low_dim_axes.append(axis_name) + if is_reduction: + reduction_axes.append(axis_name) + + axes[axis_name] = AxisSemanticInfo( + axis_name=axis_name, + extent=extent, + split=split, + tiling=AxisTiling( + param=tiling.param, + loop_var=tiling.loop_var, + source=tiling.source, + confidence=tiling.confidence, + fixed_expr=fixed_tiling_expr, + ), + is_low_dim=evidence.is_low_dim, + is_reduction=is_reduction, + diagnostics=axis_diags, + ) + + if len(low_dim_axes) != len(set(low_dim_axes)): + diagnostics.append("duplicate low_dim axis detected") + low_dim_axes = list(dict.fromkeys(low_dim_axes)) + if len(reduction_axes) != len(set(reduction_axes)): + diagnostics.append("duplicate reduction axis detected") + reduction_axes = list(dict.fromkeys(reduction_axes)) + + for axis_info in axes.values(): + if axis_info.diagnostics: + diagnostics.extend(["{}: {}".format(axis_info.axis_name, item) for item in axis_info.diagnostics]) + + status = "ok" + if diagnostics or any(info.extent.expr is None for info in axes.values()): + status = "partial" + + if not axes: + status = "failed" + + return AxisSemanticResult( + axes=axes, + axis_length_exprs=axis_length_exprs, + fixed_tiling_exprs=fixed_tiling_exprs, + axis_pid_dims=axis_pid_dims, + inferred_keys=inferred_keys, + split_params=split_params, + tiling_params=tiling_params, + low_dim_axes=low_dim_axes, + reduction_axes=reduction_axes, + status=status, + diagnostics=diagnostics, + ) + + +def parse_vv_axis_info_v2( + func_ast: ast.AST, + provided_args: Optional[Mapping[str, object]] = None, + hints: Optional[Mapping[str, object]] = None, + module_ast: Optional[ast.AST] = None, + entry_function_name: Optional[str] = None, +) -> VvAxisParseResultV2: + semantic_result = parse_vv_axis_semantic_v2( + func_ast, + provided_args=provided_args, + hints=hints, + module_ast=module_ast, + entry_function_name=entry_function_name, + ) + + ordered_axis_names = sorted(semantic_result.axes.keys(), key=_axis_sort_key) + legacy_axes: List[VvAxisInfoV2] = [] + axis_dynamic_sources: Dict[str, str] = {} + + for axis_index, axis_name in enumerate(ordered_axis_names): + semantic_axis = semantic_result.axes[axis_name] + extent = semantic_axis.extent + tunable_param = None + if extent.state == AXIS_LENGTH_STATE_TUNABLE: + tunable_param = _normalize_tunable_param_symbol(extent.expr) + axis_dynamic_sources[axis_name] = extent.dynamic_source + + legacy_axes.append( + VvAxisInfoV2( + axis_index=axis_index, + length_expr=extent.expr, + axis_symbol=None, + state=extent.state, + tunable_param=tunable_param, + const_value=extent.const_value, + split_param=semantic_axis.split.param, + tiling_param=semantic_axis.tiling.param, + fixed_tiling_expr=semantic_axis.tiling.fixed_expr, + is_low_dim=semantic_axis.is_low_dim, + is_reduction=semantic_axis.is_reduction, + dynamic_source=extent.dynamic_source, + )) + + if not semantic_result.axes: + source = "none" + else: + extent_sources = {axis_info.extent.source for axis_info in semantic_result.axes.values()} + if extent_sources == {"make_block_ptr"}: + source = "make_block_ptr" + elif extent_sources.intersection({"mask", "infer_total", "loop", "load"}): + source = "load" + else: + source = "axis_semantic" + + return VvAxisParseResultV2( + axis_count=len(legacy_axes), + axes=legacy_axes, + source=source, + diagnostics=list(semantic_result.diagnostics), + axis_length_exprs=dict(semantic_result.axis_length_exprs), + fixed_tiling_exprs=dict(semantic_result.fixed_tiling_exprs), + axis_pid_dims=dict(semantic_result.axis_pid_dims), + inferred_keys=dict(semantic_result.inferred_keys), + split_params=dict(semantic_result.split_params), + tiling_params=dict(semantic_result.tiling_params), + axis_dynamic_sources=axis_dynamic_sources, + low_dim_axes=list(semantic_result.low_dim_axes), + reduction_axes=list(semantic_result.reduction_axes), + status=semantic_result.status, + ) diff --git a/third_party/ascend/backend/runtime/dsl_analysis/vv_parser_options.py b/third_party/ascend/backend/runtime/dsl_analysis/vv_parser_options.py new file mode 100644 index 0000000000..dca83bef5e --- /dev/null +++ b/third_party/ascend/backend/runtime/dsl_analysis/vv_parser_options.py @@ -0,0 +1,53 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +from typing import Any, Mapping, Optional + +_VV_PARSER_V2_MODES = ("observe", "assist", "authoritative") + + +def resolve_vv_parser_v2_mode(hints: Optional[Mapping[str, Any]]) -> str: + """ + Resolve vv parser v2 mode from hints. + + Priority: + 1. hints["vv_parser_v2_mode"] in {"observe", "assist", "authoritative"} + 2. hints missing -> "assist" + 3. invalid mode -> "off" + """ + if not hints: + return "assist" + + raw_mode = hints.get("vv_parser_v2_mode") + if isinstance(raw_mode, str): + mode = raw_mode.strip().lower() + if mode in _VV_PARSER_V2_MODES: + return mode + + if "vv_parser_v2_mode" not in hints: + return "assist" + + return "off" + + +def resolve_vv_parser_v2_enabled(hints: Optional[Mapping[str, Any]]) -> bool: + return resolve_vv_parser_v2_mode(hints) != "off" diff --git a/third_party/ascend/backend/runtime/tile_generator.py b/third_party/ascend/backend/runtime/tile_generator.py new file mode 100644 index 0000000000..faf9ba4c1d --- /dev/null +++ b/third_party/ascend/backend/runtime/tile_generator.py @@ -0,0 +1,493 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations + +import functools +import sys +from dataclasses import dataclass +from typing import ( + Dict, + List, + Tuple, +) + +from triton.runtime.autotuner import Config + +from . import utils as runtime_utils +from .utils import ( + get_byte_per_numel, + is_valid_axis_name, + next_power_of_2, +) + + +@dataclass +class AxisInfo: + name: str + index: int + length: int + + split_name: str = "" + tiling_name: str = "" + is_split_axis: bool = False + is_tunable_split_axis: bool = False + is_tiling_axis: bool = False + is_reduction: bool = False + fixed_split_size: int = 0 + + +class KernelMeta: + + def __init__( + self, + axis_sizes: Dict[str, int], + split_params: Dict[str, str], + fixed_split_params: Dict[str, int], + tiling_params: Dict[str, str], + low_dims: List[str], + reduction_axes: List[str], + dtype: torch.dtype, + persistent_reduction: bool, + dual_reduction: bool, + num_buffers: int, + is_simt_mode: bool, + ): + """ + :param split_params: a dict of axis name: argument name, the argument is an adjustable parameter in a split axis, such as 'XBLOCK'. + The axis name must be in key's axis names. Do not add prefix 'r' before the axis name. + This param can be empty. Note that the auto tiling feature will be disabled when the split_params and tiling_params are both empty. + The split axis can usually be identified according to `tl.program_id()` expression. + :type split_params: Dict[str, str] + :param tiling_params: a dict of axis name: argument name, the argument is an adjustable parameter in a tiling axis, such as 'XBLOCK_SUB'. + The axis name must be in key's axis names. Do not add prefix 'r' before the axis name. + This param can be empty. Note that the auto tiling feature will be disabled when the split_params and tiling_params are both empty. + The tiling axis can usually be identified according to `tl.arange()` expression. + :type tiling_params: Dict[str, str] + :param low_dims: a list of axis name in which the corresponding axis is low dim aixs. + The axis name must be in key's axis names. Do not add prefix 'r' before the axis name. + :type low_dims: List[str] + :param reduction_axes: a list of base axis names that are reduction axes. + :type reduction_axes: List[str] + :param dual_reduction: performing reduction on more than one axis. + :param persistent_reduction: there is no splitting in reduction axis. + """ + self._validate_axis(axis_sizes, split_params, fixed_split_params, tiling_params, low_dims, reduction_axes) + + reduction_axis_names = set(reduction_axes or []) + axis_dict = {} + idx = 0 + for name, length in axis_sizes.items(): + is_tunable_split_axis = name in split_params + fixed_split_size = fixed_split_params.get(name, 0) + is_split_axis = is_tunable_split_axis or fixed_split_size > 0 + is_tiling_axis = name in tiling_params + is_reduction = name in reduction_axis_names + split_name = "" if not is_tunable_split_axis else split_params[name] + tiling_name = "" if name not in tiling_params else tiling_params[name] + + axis_dict[name] = AxisInfo( + name=name, + index=idx, + length=length, + split_name=split_name, + tiling_name=tiling_name, + is_split_axis=is_split_axis, + is_tunable_split_axis=is_tunable_split_axis, + is_tiling_axis=is_tiling_axis, + is_reduction=is_reduction, + fixed_split_size=fixed_split_size, + ) + idx += 1 + + self.axis_info = list(axis_dict.values()) + self.split_axis = [x for x in axis_dict.values() if x.is_split_axis] + self.tunable_split_axis = [x for x in axis_dict.values() if x.is_tunable_split_axis] + self.tiling_axis = [x for x in axis_dict.values() if x.is_tiling_axis] + self.low_dims_axis = [x for x in axis_dict.values() if x.name in low_dims] + self.dtype = dtype + self.persistent_reduction = persistent_reduction + self.dual_reduction = dual_reduction + self.num_buffers = num_buffers + self.is_simt_mode = is_simt_mode + + @classmethod + def _validate_axis( + cls, + axis_sizes: Dict[str, int], + split_params: Dict[str, str], + fixed_split_params: Dict[str, int], + tiling_params: Dict[str, str], + low_dims: List[str], + reduction_axes: List[str], + ) -> None: + for axis_name in axis_sizes.keys(): + if not is_valid_axis_name(axis_name): + raise ValueError(f"Invalid axis name '{axis_name}'. Axis names must be base axes.") + + def check_keys(params: List[str], context="parameter"): + for k in params: + if k not in axis_sizes: + raise KeyError(f"{context} '{k}' not found in known axes: {axis_sizes.keys()}") + + check_keys(split_params.keys(), "split axis") + check_keys(fixed_split_params.keys(), "fixed split axis") + check_keys(tiling_params.keys(), "tiling axis") + check_keys(low_dims, "low dim axis") + for axis_name in list(reduction_axes or []): + if isinstance(axis_name, str) and axis_name.startswith("r"): + raise ValueError(f"r-prefixed reduction axis '{axis_name}' is not supported; " + "use the base axis name and pass it through reduction_axes.") + check_keys(reduction_axes or [], "reduction axis") + + +@dataclass +class BlockInfo: + block_name: str # e.g., XBLOCK + sub_block_name: str # e.g., XBLOCK_SUB + block_size: int + sub_block_size: int + + +""" +Generate possible candidate tiling configs for benchmarking +""" + + +class TileGenerator: + num_warps = 1 + num_stages = 1 + + def __init__(self, kernel_meta: KernelMeta): + self.kernel_meta = kernel_meta + self.persistent_reduction = self.kernel_meta.persistent_reduction + self.dual_reduction = self.kernel_meta.dual_reduction + + self.blocks = self.init_blocks_info(kernel_meta) + self.numels = [axis.length for axis in kernel_meta.axis_info] + self.candidate_blocks = [] + self.configs = [] + self.dtype_bytes = get_byte_per_numel(kernel_meta.dtype) + + self.num_buffers = 3 if kernel_meta.num_buffers == 0 else min(kernel_meta.num_buffers, 3) + self.is_simt_mode = kernel_meta.is_simt_mode + # Snapshot device-dependent values when generating configs, not on import. + self.num_vector_core = runtime_utils.num_vector_core + local_mem_size = (runtime_utils.rf_size_in_kbytes if self.is_simt_mode else runtime_utils.ub_size_in_kbytes) + self.max_numel_threshold = local_mem_size * 1024 // self.dtype_bytes // self.num_buffers + self.max_total_numel = functools.reduce(lambda x, y: x * y, [x.block_size + for x in self.blocks]) if self.blocks else 1 + self.small_kernel = self.max_total_numel < 128 * 1024 + self.tiny_kernel = self.max_total_numel <= 32 * 1024 + self.stop_numel = min(1024 // self.dtype_bytes, self.max_total_numel // + (self.num_vector_core * 2)) if self.small_kernel else 1024 // self.dtype_bytes + self.max_programs_num = 65535 + self.tiny_program_threshold = self.num_vector_core // 8 + self.tiny_per_program_cap = 1 + self.tiny_low_program_hist = {p: 0 for p in range(1, self.tiny_program_threshold + 1)} + self.tiny_low_program_active = False + self.tiny_low_program_tile_floor = 0 + + @classmethod + def init_blocks_info(cls, kernel_meta: KernelMeta) -> List[BlockInfo]: + blocks = [] + for axis in kernel_meta.axis_info: + block_name = axis.split_name + sub_block_name = axis.tiling_name + block_size = axis.fixed_split_size if axis.fixed_split_size > 0 else axis.length + sub_block_size = block_size + blocks.append(BlockInfo(block_name, sub_block_name, block_size, sub_block_size)) + + return blocks + + @classmethod + def get_key_from_dict(cls, kwargs: Dict[str, int]): + return tuple(sorted(kwargs.items())) + + def calcu_last_split_blocks(self, axis_idx): + splits = 1 + for x in self.kernel_meta.split_axis: + if x.index != axis_idx: + splits = splits * ( + (self.numels[x.index] + self.blocks[x.index].block_size - 1) // self.blocks[x.index].block_size) + else: + break + + last_splits = self.num_vector_core // splits + last_splits = max(1, last_splits) + last_blocks = (self.numels[axis_idx] + last_splits - 1) // last_splits + return last_blocks + + def aligned_numel(self, numel, align_bytes=32): + if self.is_simt_mode: + return next_power_of_2(numel) + + align_numel = align_bytes // self.dtype_bytes + if numel <= align_numel: + return numel + return ((numel + align_numel - 1) // align_numel) * align_numel + + def calculate_tile_numel(self): + tile_numel = 1 + for axis in self.kernel_meta.axis_info: + if axis.is_tiling_axis: + tile_numel *= self.blocks[axis.index].sub_block_size + else: + tile_numel *= self.blocks[axis.index].block_size + + return tile_numel + + def fill_config(self, cfg, candi_block): + for axis in self.kernel_meta.axis_info: + if not (axis.is_split_axis or axis.is_tiling_axis): + continue + block_info = self.blocks[axis.index] + if axis.is_split_axis: + curr_numel = candi_block[axis.index] + if not axis.is_tiling_axis: + curr_numel = self.aligned_numel(curr_numel) + if block_info.block_name: + cfg[block_info.block_name] = curr_numel + if axis.is_tiling_axis: + tiling_numel = self.aligned_numel(block_info.sub_block_size) + cfg[block_info.sub_block_name] = (tiling_numel if self.is_simt_mode else min( + tiling_numel, candi_block[axis.index])) + + def find_config(self, cfg): + for config_var in self.configs: + if config_var.kwargs == cfg: + return True + return False + + def _try_add_tiny_low_program_config(self, total_programs): + if (not self.tiny_kernel or total_programs < 1 or total_programs > self.tiny_program_threshold): + return + + if self.tiny_low_program_hist.get(total_programs, 0) >= self.tiny_per_program_cap: + return + + candi_block = tuple([x.block_size for x in self.blocks]) + if self.add_to_configs(list(candi_block)): + if candi_block not in self.candidate_blocks: + self.candidate_blocks.append(candi_block) + if not self.tiny_low_program_active: + self.tiny_low_program_active = True + self.tiny_low_program_tile_floor = self.calculate_tile_numel() + self.tiny_low_program_hist[total_programs] = (self.tiny_low_program_hist.get(total_programs, 0) + 1) + + def _calc_total_programs(self, candi_block=None): + grids = [] + for axis in self.kernel_meta.split_axis: + numel = self.numels[axis.index] + block_size = (self.blocks[axis.index].block_size if candi_block is None else candi_block[axis.index]) + programs = (numel + block_size - 1) // block_size + grids.append(programs) + + total_programs = functools.reduce(lambda x, y: x * y, grids) if grids else 1 + return total_programs + + def add_to_configs(self, candi_block): + newcfg = {} + self.fill_config(newcfg, candi_block) + tile_numel = self.calculate_tile_numel() + stop_numel_threshold = 0 if len(self.configs) < 10 or self.small_kernel else self.stop_numel + 100 + if self.tiny_low_program_active and self.tiny_low_program_tile_floor > 0: + total_programs = self._calc_total_programs(candi_block) + program_threshold = self.tiny_program_threshold if self.small_kernel else self.num_vector_core // 2 + if total_programs <= program_threshold: + tiny_low_program_threshold = max(self.stop_numel, self.tiny_low_program_tile_floor // 2) + stop_numel_threshold = max(stop_numel_threshold, tiny_low_program_threshold) + if (tile_numel <= self.max_numel_threshold and tile_numel >= stop_numel_threshold + and not self.find_config(newcfg)): + self.configs.append(Config(newcfg, num_warps=1, num_stages=1)) + return True + return False + + def desecnd_all_low_dims_with_all_blocks(self): + restore_sub_blocks = {} + for axis in self.kernel_meta.low_dims_axis: + restore_sub_blocks[axis.index] = self.blocks[axis.index].sub_block_size + self.descend_all_low_dims() + for axis in self.kernel_meta.low_dims_axis: + self.blocks[axis.index].sub_block_size = restore_sub_blocks[axis.index] + + def descend_one_axis(self, axis_idx: int, is_split=False): + + def calc_total_programs(): + grids = [] + for axis in self.kernel_meta.split_axis: + numel = self.numels[axis.index] + block_size = self.blocks[axis.index].block_size + programs = (numel + block_size - 1) // block_size + grids.append(programs) + + total_programs = functools.reduce(lambda x, y: x * y, grids) if grids else 1 + return total_programs + + reached_stop_numel = False + slow_decend_split = False + num_vector_core_tile = self.num_vector_core + max_programs_num = num_vector_core_tile if self.kernel_meta.tiling_axis else self.max_programs_num + if not is_split and len(self.candidate_blocks) == 0: + self.candidate_blocks.append(tuple([x.block_size for x in self.blocks])) + + axis = self.kernel_meta.axis_info[axis_idx] + while True: + for candi_block in self.candidate_blocks: + if self.add_to_configs(candi_block): + self.desecnd_all_low_dims_with_all_blocks() + + # tile numel reached threshold + tile_numel = self.calculate_tile_numel() + if tile_numel <= self.stop_numel: + if self.add_to_configs([x.block_size for x in self.blocks]): + self.desecnd_all_low_dims_with_all_blocks() + reached_stop_numel = True + break + + numel = (self.blocks[axis_idx].block_size if is_split else self.blocks[axis_idx].sub_block_size) + if numel == 1: + if self.add_to_configs([x.block_size for x in self.blocks]): + self.desecnd_all_low_dims_with_all_blocks() + break + + if is_split: + if self.persistent_reduction and axis.is_reduction: + reached_stop_numel = True + break + total_programs = calc_total_programs() + if total_programs > num_vector_core_tile: + if len(self.configs) == 0: + num_vector_core_tile = max_programs_num + slow_decend_split = (total_programs > num_vector_core_tile // 2) + if total_programs > num_vector_core_tile: + last_blocks = self.calcu_last_split_blocks(axis_idx) + if last_blocks != self.blocks[axis_idx].block_size: + self.blocks[axis_idx].block_size = last_blocks + self.candidate_blocks.append(tuple([x.block_size for x in self.blocks])) + break + + program_threshold = self.tiny_program_threshold if self.small_kernel else self.num_vector_core // 2 + if self.tiny_kernel and total_programs <= program_threshold: + self._try_add_tiny_low_program_config(total_programs) + if total_programs > program_threshold or self.dual_reduction: + if len(self.candidate_blocks) > 2: + self.candidate_blocks.pop(0) + self.candidate_blocks.append(tuple([x.block_size for x in self.blocks])) + if self.small_kernel: + self.add_to_configs(list(tuple([x.block_size for x in self.blocks]))) + slow_decend_split = (total_programs > num_vector_core_tile // 2) + + if not slow_decend_split: + self.blocks[axis_idx].block_size = (numel + 1) // 2 + else: + step = (numel + 3) // 4 if (numel + 3) // 4 > 1 else 1 + self.blocks[axis_idx].block_size = numel - step + self.blocks[axis_idx].sub_block_size = self.blocks[axis_idx].block_size + total_programs = calc_total_programs() + if self.blocks[axis_idx].block_size == 1 and (total_programs > program_threshold + or self.dual_reduction): + self.candidate_blocks.append(tuple([x.block_size for x in self.blocks])) + else: + if numel >= 32: + self.blocks[axis_idx].sub_block_size = next_power_of_2(numel // 2) + else: + self.blocks[axis_idx].sub_block_size = numel - 1 + return reached_stop_numel + + def descend_all_low_dims(self): + low_dim_numels = [self.blocks[x.index].sub_block_size for x in self.kernel_meta.low_dims_axis] + if not low_dim_numels: + return False + + def descend_all_axis(min_numel): + + for axis in self.kernel_meta.low_dims_axis: + if axis.is_reduction and self.persistent_reduction: + continue + + numel = self.blocks[axis.index].sub_block_size + if numel == 1: + continue + if min_numel > 1 and abs(numel - min_numel) / min_numel < 0.2: + continue + if numel >= 128: + self.blocks[axis.index].sub_block_size = next_power_of_2(numel // 2) + else: + numel = self.blocks[axis.index].sub_block_size + numel = numel // 2 + self.blocks[axis.index].sub_block_size = min(self.aligned_numel(numel), next_power_of_2(numel)) + + if len(self.candidate_blocks) == 0: + # means there is no split axis and tiling_not_low_dim axis + # so we need to init the candidates_blk_sizes + self.candidate_blocks.append(tuple([x.block_size for x in self.blocks])) + + count = 0 + tile_numel = self.calculate_tile_numel() + while tile_numel > self.stop_numel and count < 100: + count += 1 + tile_numel = self.calculate_tile_numel() + for candi_block in self.candidate_blocks: + self.add_to_configs(candi_block) + min_numel = min(low_dim_numels) + descend_all_axis(min_numel) + new_tile_numel = self.calculate_tile_numel() + if tile_numel == new_tile_numel: + descend_all_axis(0) + + return tile_numel < self.stop_numel + + def descend_split_tiling(self): + + tiling_not_low_dims = [x for x in self.kernel_meta.tiling_axis if x not in self.kernel_meta.low_dims_axis] + + def descend_split_axis(): + for axis in self.kernel_meta.tunable_split_axis: + if self.descend_one_axis(axis.index, is_split=True): + return True + + return self.calculate_tile_numel() <= self.stop_numel + + def descend_tiling_not_low_dims(): + for axis in tiling_not_low_dims: + if axis.is_reduction and self.persistent_reduction: + continue + + if self.descend_one_axis(axis.index): + return True + return self.calculate_tile_numel() <= self.stop_numel + + while True: + # descend split axis + if descend_split_axis(): + break + if len(self.candidate_blocks) > 0: + candi_block = self.candidate_blocks[0] + for i, blk_size in enumerate(candi_block): + self.blocks[i].sub_block_size = blk_size + # descend tiling but not low dims + if descend_tiling_not_low_dims(): + break + # descend low dims, need to descend all axis at the same time + self.descend_all_low_dims() + break diff --git a/third_party/ascend/backend/runtime/ubtuner.py b/third_party/ascend/backend/runtime/ubtuner.py new file mode 100644 index 0000000000..781ea549fc --- /dev/null +++ b/third_party/ascend/backend/runtime/ubtuner.py @@ -0,0 +1,657 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. +""" +UB Tuner for Triton-Ascend + +This module provides @ubtuner decorator that automatically tries different +bisheng-compiler options to avoid UB (Unified Buffer) overflow, without actually +running the kernel on the device. + +Unlike @autotuner which tunes performance parameters and requires running on +device, @ubtuner only needs to compile the kernel and check if there's a UB +overflow error. +""" + +from __future__ import annotations + +import os +import functools +from dataclasses import dataclass, field +from itertools import product +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from triton.runtime.jit import KernelInterface + + +# ============================================================================= +# Configuration Constants +# ============================================================================= +class _Config: + """Global configuration constants for UB Tuner.""" + + # Environment variable names + ENV_MODE = "TRITON_ENABLE_UBTUNER" # 调优模式,可选值: "compile"(仅编译)、"run"(编译+运行测量耗时) (默认: 未设置) + ENV_EANBLE_PRINT = "TRITON_PRINT_AUTOTUNING" # 调优日志开关, 复用autotune的环境变量,设为"1"时打印调优过程详细信息 (默认: 未设置/不打印) + ENV_ENABLE_PRINT_UB_BITS = "ENABLE_PRINT_UB_BITS" # 运行时打印UB占用位数,run模式下临时设为"true"以获取required_ub_bits (默认: 未设置) + + # Default values + DEFAULT_MODE = "run" + DEFAULT_UB_MEMORY_LIMIT = 100 # 1 unit = 256KB, so 100 = 25MB effective limit + + # Error patterns + UB_OVERFLOW_ERROR = "ub overflow" + + # Supported values + SUPPORTED_ALGORITHMS = {"greedy"} + SUPPORTED_MODES = {"compile", "run"} + + # Logging format + LOG_PREFIX = "[UBTuner]" + + +# ============================================================================= +# UB Option Metadata - Single source of truth for all UB tuning options +# ============================================================================= +UB_OPTION_METADATA: Dict[str, Dict[str, Any]] = { + 'auto_multi_buffer': {'type': bool, 'default': False, 'npu_option': 'multibuffer'}, + 'vf_fusion_mode': + {'type': str, 'default': 'ub-aware-op', 'npu_option': 'vf_fusion_mode', 'values': ['ub-aware-op', 'max-parallel']}, +} + +# Derived lists for convenience +BOOL_OPTIONS = [k for k, v in UB_OPTION_METADATA.items() if v['type'] == bool] +INT_OPTIONS = [k for k, v in UB_OPTION_METADATA.items() if v['type'] == int] +STR_OPTIONS = [k for k, v in UB_OPTION_METADATA.items() if v['type'] == str] + +# Mapping from UB config keys to NPUOptions keys +UB_TO_NPU_OPTION_MAP = {k: v['npu_option'] for k, v in UB_OPTION_METADATA.items() if v['npu_option'] is not None} + +# ============================================================================= +# Cost Model for UB Config Selection +# ============================================================================= +DEFAULT_UB_MEMORY_LIMIT = _Config.DEFAULT_UB_MEMORY_LIMIT + +UB_OPTION_COST_BENEFIT: Dict[str, Dict[str, float]] = { + 'auto_multi_buffer': {'cost': 8, 'benefit': 1.5}, + + # Str options: each possible value has its own cost/benefit entry + 'vf_fusion_mode#ub-aware-op': {'cost': 2, 'benefit': 1.0}, + 'vf_fusion_mode#max-parallel': {'cost': 3, 'benefit': 2.0}, +} + + +# ============================================================================= +# Utility Functions +# ============================================================================= +def _log_debug(message: str) -> None: + """Print debug message if TRITON_PRINT_UBTUNING=1.""" + if os.getenv(_Config.ENV_EANBLE_PRINT) == "1": + print(f"{_Config.LOG_PREFIX} {message}") + + +def _log_info(message: str) -> None: + """Print info message with green color.""" + if os.getenv(_Config.ENV_EANBLE_PRINT) == "1": + print(f"\033[1;32m{message}\033[0m") + + +def _get_option_value(option: str, enabled: bool) -> bool: + """Get the value for an option based on enabled state and default.""" + if option not in UB_OPTION_METADATA: + return enabled + default = UB_OPTION_METADATA[option].get('default', False) + return not default if enabled else default + + +def get_mode_choice() -> str: + """Get the mode to use based on environment variable.""" + mode_env = os.getenv(_Config.ENV_MODE, "None").lower() + if mode_env in _Config.SUPPORTED_MODES: + return mode_env + _log_debug(f"Unknown mode: {mode_env}, using '{_Config.DEFAULT_MODE}' as default") + return _Config.DEFAULT_MODE + + +def _try_compile_option(option: str, current_config: Dict[str, bool], linalg_ir: str, metadata: Dict[str, Any], + npu_options: Any, try_value: Union[bool, str]) -> Tuple[bool, Optional[str]]: + """Try compiling with a single option enabled/disabled.""" + from triton.backends.ascend.compiler import try_compile_with_config + + test_config = dict(current_config) + test_config[option] = try_value + + config = UBConfig(kwargs=test_config) + return try_compile_with_config(linalg_ir, config.all_kwargs(), metadata, npu_options) + + +def _build_sorted_option_list(benefit_cost: Dict[str, Dict[str, float]], options: List[str]) -> List[Tuple[str, Any]]: + """ + Build a flat list of (option_key, value) tuples sorted by benefit/cost ratio. + + For bool options: key is the option name, value is 'not Default_value' + For str options: key format is "option#value", value is the string value + """ + items = [] + + for opt in options: + opt_meta = UB_OPTION_METADATA.get(opt, {}) + opt_type = opt_meta.get('type', bool) + + if opt_type == str: + # Str option: add each possible value as a separate entry + values = opt_meta.get('values', [opt_meta.get('default', '')]) + for val in values: + key = f"{opt}#{val}" + bc = benefit_cost.get(key, {'cost': 0, 'benefit': -1e100}) + ratio = bc.get('benefit', 1.0) / max(bc.get('cost', 1), 1) + items.append((key, val, opt, ratio)) + else: + # Bool option: + bc = benefit_cost.get(opt, {'cost': 0, 'benefit': -1e100}) + ratio = bc.get('benefit', 1.0) / max(bc.get('cost', 1), 1) + items.append((opt, (not opt_meta.get('default', False)), opt, ratio)) + + # Sort by benefit/cost ratio descending + items.sort(key=lambda x: x[3], reverse=True) + return items + + +def greedy_optimize(benefit_cost: Dict[str, Dict[str, float]], options: List[str], linalg_ir: str, metadata: Dict[str, + Any], + npu_options: Any, _memory_limit: Optional[int] = None # Unused but kept for compatibility + ) -> Dict[str, Any]: + """ + Greedy strategy: sort options by benefit/cost ratio, try each option. + + Args: + benefit_cost: Dict mapping option (or "option#value" for str) to {'cost': float, 'benefit': float} + options: List of option names to consider + linalg_ir: Linalg IR string for compilation test + metadata: Compilation metadata + npu_options: NPUOptions object + memory_limit: Optional memory limit (unused, for compatibility) + + Returns: + Dict of option_name -> value (bool for bool options, str for str options) + """ + if not options: + return {} + + # Build flat sorted list of all options and their values + sorted_items = _build_sorted_option_list(benefit_cost, options) + + if os.getenv(_Config.ENV_EANBLE_PRINT) == "1": + print(f"{_Config.LOG_PREFIX} Greedy strategy - sorted options by benefit/cost:") + for key, _, _, ratio in sorted_items: + bc = benefit_cost.get(key, {'cost': 0, 'benefit': 1.0}) + print(f" {key}: cost={bc.get('cost', 0)}, benefit={bc.get('benefit', 1.0)}, ratio={ratio}") + + result: Dict[str, Any] = {} + applied_options: Set[str] = set() # Track which options have been applied + + for _, val, opt, _ in sorted_items: + is_str_opt = opt in STR_OPTIONS + is_bool_opt = opt in BOOL_OPTIONS + # Already found a successful value for this str option, skip + if is_str_opt and opt in applied_options: + continue + + _log_debug(f"Greedy: trying config with {opt}={val}") + try: + success, error_msg = _try_compile_option(opt, result, linalg_ir, metadata, npu_options, val) + if success: + result[opt] = val + applied_options.add(opt) + _log_debug(f"Greedy: {opt}={val} succeeded, keeping it") + else: + if is_bool_opt: + result[opt] = _get_option_value(opt, False) + err_preview = error_msg[:100] if error_msg else "Unknown" + _log_debug(f"Greedy: {opt}={val} failed: {err_preview}..., skipping") + except Exception as e: + _log_debug(f"Greedy: {opt}={val} exception: {e}, skipping") + + return result + + +def _measure_option_cost_onboard(fn: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any], opt: str, + opt_value: Any) -> Tuple[float, float]: + """ + Measure the cost (UB bits) for a single option configuration. + + Returns: + Tuple of (cost, benefit) where benefit is negative execution time in ms + """ + test_kwargs = dict(kwargs) + test_config = UBConfig(kwargs={opt: opt_value}) + + for ub_key, npu_key in UB_TO_NPU_OPTION_MAP.items(): + if ub_key in test_config.all_kwargs(): + test_kwargs[npu_key] = test_config.all_kwargs()[ub_key] + + compiled_kernel = fn.run(*args, **test_kwargs) + + metadata = dict(compiled_kernel.metadata._asdict()) if hasattr(compiled_kernel, 'metadata') else None + if metadata is None: + raise RuntimeError("No metadata found in kernel") + + from ..testing import do_bench_npu + test_kwargs.update({"warmup": False}) + bench_fn = functools.partial(fn.run, *args, **test_kwargs) + opt_time = do_bench_npu(bench_fn, clear_l2_cache=False) + + return metadata.get('required_ub_bits', 0), -opt_time * 1000 + + +def run_mode_get_benefit_cost(available_options: List[str], fn: Callable, args: Tuple[Any, ...], + kwargs: Dict[str, Any]) -> Dict[str, Dict[str, float]]: + """ + Run mode: measure actual execution time for each option. + + Args: + available_options: List of option names to measure + fn: The kernel function + args: Positional arguments for the kernel + kwargs: Keyword arguments for the kernel + + Returns: + Dict of {option: {'cost': float, 'benefit': float}} + For str options, key format is "option#value" + """ + result = {} + + # Build flat list of (key, opt, value) to measure + items_to_measure = [] + for opt in available_options: + opt_meta = UB_OPTION_METADATA.get(opt, {}) + opt_type = opt_meta.get('type', bool) + + if opt_type == str: + values = opt_meta.get('values', [opt_meta.get('default', '')]) + for val in values: + items_to_measure.append((f"{opt}#{val}", opt, val)) + else: + items_to_measure.append((opt, opt, _get_option_value(opt, True))) + + # Measure each option + for key, opt, opt_value in items_to_measure: + _log_debug(f"======== {key} ========") + try: + cost, benefit = _measure_option_cost_onboard(fn, args, kwargs, opt, opt_value) + result[key] = {'cost': cost, 'benefit': benefit} + except Exception as e: + _log_debug(f"Run-mode {key} failed: {e}") + result[key] = {'cost': 0, 'benefit': -1e100} + + return result + + +# Legacy Option weights (kept for backward compatibility) +OPTION_WEIGHTS: Dict[str, float] = { + 'auto_multi_buffer': 5.0, +} + + +def compute_config_score(config: 'UBConfig') -> float: + """ + Compute cost model score for a UB config. + + Higher score = more aggressive config, higher UB usage risk. + Conservative configs get lower scores. + """ + kwargs = config.all_kwargs() + score = 0.0 + + for opt_name, weight in OPTION_WEIGHTS.items(): + if opt_name in kwargs: + value = kwargs[opt_name] + if isinstance(value, bool) and value: + score += weight + elif isinstance(value, int): + score += weight * value + + return score + + +def _generate_all_configs() -> List[UBConfig]: + """Generate all possible UB configurations.""" + configs = [] + bool_combinations = product([False, True], repeat=len(BOOL_OPTIONS)) + + for bool_vals in bool_combinations: + kwargs = {opt: val for opt, val in zip(BOOL_OPTIONS, bool_vals)} + kwargs.update({opt: 0 for opt in INT_OPTIONS}) + configs.append(UBConfig(kwargs=kwargs)) + + return configs + + +def _sort_configs_by_score(configs: List[UBConfig]) -> List[Tuple[UBConfig, float]]: + """Sort configs by cost model score (descending).""" + scored_configs = [(cfg, compute_config_score(cfg)) for cfg in configs] + scored_configs.sort(key=lambda x: x[1], reverse=True) + return scored_configs + + +_SORTED_CONFIGS: Optional[List[Tuple[UBConfig, float]]] = None + + +def get_sorted_configs() -> List[Tuple[UBConfig, float]]: + """Get all configs sorted by cost model score (cached).""" + global _SORTED_CONFIGS + + if _SORTED_CONFIGS is None: + _SORTED_CONFIGS = _sort_configs_by_score(_generate_all_configs()) + + return _SORTED_CONFIGS + + +def print_sorted_configs() -> None: + """Print all configs sorted by cost model score (for debugging).""" + print("UB Configs sorted by cost model score:") + print("-" * 80) + for i, (cfg, score) in enumerate(get_sorted_configs()): + print(f"{i+1:3d}. score={score:5.2f} -> {cfg}") + print("-" * 80) + print(f"Total: {len(get_sorted_configs())} configs") + + +def get_origin_fn(fn): + original_fn = fn + while hasattr(original_fn, 'fn'): + original_fn = original_fn.fn + return original_fn + + +@dataclass +class UBConfig: + """ + UB Configuration - similar to triton.Config + + Represents a set of bisheng-compiler options to try for avoiding UB overflow. + Each field corresponds to a compiler option that affects UB usage. + """ + + # Compiler options that affect UB usage (generated from UB_OPTION_METADATA) + auto_multi_buffer: Optional[bool] = None + ubuf_saving: Optional[bool] = None + auto_bind_sub_block: Optional[bool] = None + auto_blockify_loop: Optional[bool] = None + drop_unit_dims: Optional[bool] = None + tile_mix_vector_loop: Optional[int] = None + tile_mix_cube_loop: Optional[int] = None + enable_hivm_auto_cv_balance: Optional[bool] = None + sync_solver: Optional[bool] = None + unit_flag: Optional[bool] = None + vf_fusion_mode: Optional[str] = None + + # Additional kwargs for extensibility + kwargs: Dict[str, Any] = field(default_factory=dict) + + def __post_init__(self): + if self.kwargs is None: + self.kwargs = {} + + # Collect all non-None fields into kwargs (using UB_OPTION_METADATA keys) + for field_name in UB_OPTION_METADATA.keys(): + val = getattr(self, field_name, None) + if val is not None: + self.kwargs[field_name] = val + + def all_kwargs(self) -> Dict[str, Any]: + """Return all configuration options as a dictionary.""" + return self.kwargs.copy() + + def __repr__(self): + return f"UBConfig({self.kwargs})" + + +class UBTuner(KernelInterface): + """ + UB Configuration Tuner - similar to AutoTilingTuner + + Automatically tries different bisheng-compiler options to find a configuration + that doesn't cause UB overflow. Unlike autotuner, this doesn't require running + on the device - it only needs to compile and check for errors. + + Uses cost model to prioritize configs with lower UB usage risk first. + """ + + def __init__( + self, + fn: Callable, + key: Optional[List[str]] = None, + ): + self.fn = fn + self.arg_names = fn.arg_names if hasattr(fn, 'arg_names') else [] + self.configs: List[UBConfig] = [] + self.key = key or [] + self.cache: Dict[Tuple, UBConfig] = {} + self._searched_configs: Set[int] = set() + self.print_ubtuning = os.getenv(_Config.ENV_EANBLE_PRINT) == "1" + self.config_iter = self._iter_configs() if not self.configs else iter(self.configs) + + def _iter_configs(self): + """Iterator to generate candidate UB configurations ordered by cost model score.""" + for config, score in get_sorted_configs(): + config_id = hash(frozenset(config.all_kwargs().items())) + if config_id not in self._searched_configs: + self._searched_configs.add(config_id) + if self.print_ubtuning: + print(f"{_Config.LOG_PREFIX} Next config: {config}, score={score:.2f}") + yield config + + def generate_key_and_configs(self, *args: Any, **kwargs: Any) -> Tuple: + """Generate cache key based on arguments.""" + nargs = dict(zip(self.arg_names, args)) + nargs.update(kwargs) + key_args = {k: nargs[k] for k in self.key if k in nargs} + return tuple(sorted(key_args.items())) + + def run(self, *args: Any, **kwargs: Any): + """Run UB tuning: first try without tuner, if UB overflow, tune and retry.""" + try: + return self.fn.run(*args, **kwargs) + except Exception as e: + # the overflow problem not autotuned will do ubtune + autotuned = getattr(get_origin_fn(self.fn), '_triton_autotuned', False) + if _Config.UB_OVERFLOW_ERROR not in str(e).lower() or autotuned: + raise e + print(f"{e}, Ub overflow try ub tuner.") + + self.nargs = dict(zip(self.arg_names, args)) + key = self.generate_key_and_configs(*args, **kwargs) + + best_config = self._get_or_find_config(key, *args, **kwargs) + + kwargs_with_ub = self._apply_ub_config(kwargs, best_config) + + _log_debug(f"Running kernel with options: {kwargs_with_ub}") + + ret = self.fn.run(*args, **kwargs_with_ub) + self.nargs = None + return ret + + def _set_ub_optimal_config(self, kwargs: Any): + """set compiler config that use minimum ub space""" + for option in BOOL_OPTIONS: + if option not in UB_OPTION_METADATA: + continue + default = UB_OPTION_METADATA[option].get('default', False) + npu_option = UB_OPTION_METADATA[option].get('npu_option', '') + if npu_option: + kwargs[npu_option] = default + for option in STR_OPTIONS: + if option not in UB_OPTION_METADATA: + continue + default = UB_OPTION_METADATA[option].get('default', '') + npu_option = UB_OPTION_METADATA[option].get('npu_option', '') + if npu_option: + kwargs[npu_option] = default + + def _get_or_find_config(self, key: Tuple, *args: Any, **kwargs: Any) -> UBConfig: + """Get cached config or find a new one.""" + if key in self.cache: + _log_debug(f"Using cached config for key: {key}") + return self.cache[key] + + self._set_ub_optimal_config(kwargs) + best_config = self._find_best_config(*args, **kwargs) + self.cache[key] = best_config + + _log_debug(f"Cached config for key: {key}, config: {best_config}") + + return best_config + + def _apply_ub_config(self, kwargs: Dict[str, Any], config: UBConfig) -> Dict[str, Any]: + """Apply UB config to kwargs, mapping UB keys to NPU options.""" + kwargs_with_ub = dict(kwargs) + ub_kwargs = config.all_kwargs() + + for ub_key, npu_key in UB_TO_NPU_OPTION_MAP.items(): + if ub_key in ub_kwargs: + kwargs_with_ub[npu_key] = ub_kwargs[ub_key] + + return kwargs_with_ub + + def _find_best_config(self, *args: Any, **kwargs: Any) -> UBConfig: + """Find the best UB config using configured algorithm.""" + mode = get_mode_choice() + _log_debug(f"Using cost mode: {mode}") + os.environ[_Config.ENV_ENABLE_PRINT_UB_BITS] = 'true' if mode == _Config.DEFAULT_MODE else 'false' + try: + compiled_kernel = self.fn.run(*args, **kwargs) + metadata = dict(compiled_kernel.metadata._asdict()) if hasattr(compiled_kernel, 'metadata') else {} + npu_options = self._create_npu_options(metadata) + if npu_options is None: + return UBConfig() + + asm_key = 'bcmlir' + linalg_ir = compiled_kernel.asm.get(asm_key) if hasattr(compiled_kernel, 'asm') else None + if linalg_ir is None: + _log_debug("Could not get linalg IR, using default config") + return UBConfig() + + available_options = list(UB_OPTION_METADATA.keys()) + + benefit_cost = self._get_benefit_cost(mode, available_options, args, kwargs, npu_options) + finally: + os.environ.pop(_Config.ENV_ENABLE_PRINT_UB_BITS, None) + + for config in self._try_algorithms(['greedy'], benefit_cost, available_options, linalg_ir, metadata, + npu_options): + return config + + return UBConfig() + + def _create_npu_options(self, metadata: Dict[str, Any]): + """Create NPUOptions from metadata.""" + from triton.backends.ascend.compiler import NPUOptions + try: + npu_option_keys = { + field.name + for field in NPUOptions.__dataclass_fields__.values() + if field.init and field.name not in {"arch", "compile_on_910_95"} + } + opts_dict = {k: v for k, v in metadata.items() if k in npu_option_keys} + target_arch = metadata.get("target_arch", metadata.get("arch", "")) + return NPUOptions(arch=target_arch, **opts_dict) + except Exception as e: + _log_debug(f"Failed to create NPUOptions: {e}") + return None + + def _get_benefit_cost(self, mode: str, available_options: List[str], args: Tuple[Any, ...], kwargs: Dict[str, Any], + npu_options: Any) -> Dict[str, Dict[str, float]]: + """Get benefit/cost data based on mode.""" + if mode == "run": + _log_debug("Running in run-mode, measuring execution time...") + return run_mode_get_benefit_cost(available_options, self.fn, args, kwargs) + return UB_OPTION_COST_BENEFIT + + def _try_algorithms(self, algorithm_list: List[str], benefit_cost: Dict[str, Dict[str, float]], + available_options: List[str], linalg_ir: str, metadata: Dict[str, Any], + npu_options: Any) -> List[UBConfig]: + """Try each algorithm in order until one succeeds.""" + for algo in algorithm_list: + config = self._try_single_algorithm(algo, benefit_cost, available_options, linalg_ir, metadata, npu_options) + if config is not None: + return [config] + return [] + + def _try_single_algorithm(self, algo: str, benefit_cost: Dict[str, Dict[str, float]], available_options: List[str], + linalg_ir: str, metadata: Dict[str, Any], npu_options: Any) -> Optional[UBConfig]: + """Try a single algorithm and return config if successful.""" + _log_debug(f"Running {algo} optimization...") + + if algo == "greedy": + config_dict = greedy_optimize(benefit_cost, available_options, linalg_ir, metadata, npu_options) + _log_info(f"{_Config.LOG_PREFIX} Greedy optimized config: {config_dict}") + config = UBConfig(kwargs=config_dict) + + success, error_msg = self._try_compile(config, linalg_ir, metadata, npu_options) + + if success: + _log_debug(f"{algo.capitalize()} config works!") + return config + + err_preview = error_msg[:200] if error_msg else "Unknown error" + _log_debug(f"{algo.capitalize()} config failed: {err_preview}...") + return None + + def _try_compile(self, config: UBConfig, linalg_ir: str, metadata: Dict[str, Any], + npu_options: Any) -> Tuple[bool, Optional[str]]: + """Try to compile the kernel with the given UB config.""" + from triton.backends.ascend.compiler import try_compile_with_config + try: + return try_compile_with_config(linalg_ir, config.all_kwargs(), metadata, npu_options) + except Exception as e: + _log_debug(f"Exception during try_compile: {e}") + return (False, str(e)) + + def get_best_config(self, *args: Any, compile_opts=None, **kwargs: Any): + """Get the best UB config for the given arguments.""" + _log_debug(f"kwargs {kwargs}") + _log_debug(f"compile_opts {compile_opts}") + if compile_opts: + kwargs = {**kwargs, **compile_opts} + self._set_ub_optimal_config(kwargs) + cfg = self._find_best_config(*args, **kwargs) + if cfg.kwargs: + ub_cfg = self._apply_ub_config({}, cfg) + return ub_cfg + return {} + + +def ubtuner(key=None): + + def decorator(fn): + # Check if fn (or its underlying function) has been decorated with autotuner + # When autotune is applied, it marks the original function with '_triton_autotuned' attribute + # Check fn itself and its underlying 'fn' attribute (for decorator chains) + original_fn = get_origin_fn(fn) + # Mark the original function so ubtuner can detect autotune was applied + setattr(original_fn, '_ubtuned', True) + if getattr(original_fn, '_triton_autotuned', False): + raise ValueError(f"Cannot apply @ubtuner decorator on a function that already has @autotune decorator. " + f"The @autotune decorator should be the outer decorator (applied first), not @ubtuner. " + f"Please change the decorator order: put @autotune outside @ubtuner.") + return UBTuner(fn, key) + + return decorator diff --git a/third_party/ascend/backend/runtime/utils.py b/third_party/ascend/backend/runtime/utils.py new file mode 100644 index 0000000000..789362da13 --- /dev/null +++ b/third_party/ascend/backend/runtime/utils.py @@ -0,0 +1,136 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch + +_cached_params = None + + +def _init_npu_params(): + global _cached_params + if _cached_params is not None: + return _cached_params + + from triton.runtime.driver import driver + from triton.backends.ascend.utils import ub_size_in_kbytes_for_arch + + target = driver.active.get_current_target() + device = driver.active.get_current_device() + prop = driver.active.utils.get_device_properties(device) + + num_cube_core = prop["num_aicore"] + num_vector_core = prop["num_aicore"] + # Keep the runtime's historical fallback for a device descriptor that is + # not yet listed in the compiler-side architecture table. Graph + # optimization intentionally treats that case as zero budget instead. + ub_size_in_kbytes = ub_size_in_kbytes_for_arch(target.arch) or 192 + rf_size_in_kbytes = None + + ASCEND_VARIANTS = ["Ascend910B", "Ascend910_93", "Ascend910_95", "Ascend950"] + if any(variant in target.arch for variant in ASCEND_VARIANTS): + num_vector_core = num_cube_core * 2 + + if target.arch.startswith("Ascend910_95") or target.arch.startswith("Ascend950"): + rf_size_in_kbytes = 128 + + _cached_params = { + 'target': target, + 'device': device, + 'prop': prop, + 'num_cube_core': num_cube_core, + 'num_vector_core': num_vector_core, + 'ub_size_in_kbytes': ub_size_in_kbytes, + 'rf_size_in_kbytes': rf_size_in_kbytes, + } + return _cached_params + + +def __getattr__(name): + if name in [ + 'target', 'device', 'prop', 'num_cube_core', 'num_vector_core', 'ub_size_in_kbytes', 'rf_size_in_kbytes' + ]: + return _init_npu_params()[name] + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +# wrapper npu 32 bytes align, get and pass unalign info to triton meta +# then autotune choose tiling param and send them to bishengIR +byte_per_numel = { + torch.float32: 4, # torch.float32 or torch.float + torch.float64: 8, # torch.float64 or torch.double + torch.float16: 2, # torch.float16 or torch.half + torch.bfloat16: 2, # torch.bfloat16 + torch.int32: 4, # torch.int32 or torch.int + torch.int64: 8, # torch.int64 or torch.long + torch.int16: 2, # torch.int16 or torch.short + torch.int8: 1, # torch.int8 + torch.uint8: 1, # torch.uint8 + torch.uint16: 2, # torch.uint16 + torch.uint32: 4, # torch.uint32 + torch.uint64: 8, # torch.uint64 + torch.bool: 1, # torch.bool + torch.complex32: 4, # torch.complex32 (not yet available in PyTorch as of the latest stable release) + torch.complex64: 8, # torch.complex64 + torch.complex128: 16, # torch.complex128 +} + +# Some PyTorch versions expose extra fp8 dtypes. Register them when available. +for fp8_dtype_name in ( + "float8_e4m3fn", + "float8_e4m3fnuz", + "float8_e5m2", + "float8_e5m2fnuz", +): + fp8_dtype = getattr(torch, fp8_dtype_name, None) + if fp8_dtype is not None: + byte_per_numel[fp8_dtype] = 1 + +valid_axis_names = [ + "x", + "y", + "z", + "w", + "v", + "t", +] + + +def get_byte_per_numel(dtype: torch.dtype) -> int: + return 1 if dtype is None else byte_per_numel[dtype] + + +def is_valid_axis_name(name: str) -> bool: + return name in valid_axis_names + + +# move to an appropriate place, currently duplicated with triton.__init__.py +def next_power_of_2(n: int): + """Return the smallest power of 2 greater than or equal to n""" + n -= 1 + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + n |= n >> 32 + n += 1 + return n diff --git a/third_party/ascend/backend/runtime/vector_axes.py b/third_party/ascend/backend/runtime/vector_axes.py new file mode 100644 index 0000000000..d47f27628f --- /dev/null +++ b/third_party/ascend/backend/runtime/vector_axes.py @@ -0,0 +1,336 @@ +from __future__ import annotations + +import ast +from collections import OrderedDict +from dataclasses import dataclass, field +from typing import Dict, Iterable, List, Mapping, Optional, Tuple + +SPLIT_AXIS_FALLBACK_SIZE = 4096 +DEFAULT_AXIS_FALLBACK_SIZE = 512 + + +@dataclass +class VectorAxis: + name: str + length_expr: Optional[str] = None + fixed_tiling_expr: Optional[str] = None + dynamic_source: str = "none" + split_param: Optional[str] = None + tiling_param: Optional[str] = None + is_low_dim: bool = False + is_reduction: bool = False + pid_dim: Optional[int] = None + + +@dataclass +class VectorAxes: + axes: "OrderedDict[str, VectorAxis]" = field(default_factory=OrderedDict) + _low_dim_axis_names: List[str] = field(default_factory=list) + _reduction_axis_names: List[str] = field(default_factory=list) + + @classmethod + def from_hints_axes( + cls, + hints_axes: Optional[Mapping[str, str]] = None, + ) -> "VectorAxes": + result = cls() + for axis_name, length_expr in dict(hints_axes or {}).items(): + axis = result.ensure_axis(axis_name) + axis.length_expr = length_expr + return result + + def ensure_axis(self, name: str) -> VectorAxis: + axis = self.axes.get(name) + if axis is None: + axis = VectorAxis(name=name) + self.axes[name] = axis + self._apply_axis_property_flags(axis) + return axis + + def apply_semantic_fields( + self, + *, + split_params: Optional[Mapping[str, str]] = None, + tiling_params: Optional[Mapping[str, str]] = None, + low_dim_axes: Optional[Iterable[str]] = None, + reduction_axes: Optional[Iterable[str]] = None, + axis_pid_dims: Optional[Mapping[str, int]] = None, + axis_length_exprs: Optional[Mapping[str, str]] = None, + fixed_tiling_exprs: Optional[Mapping[str, str]] = None, + axis_dynamic_sources: Optional[Mapping[str, str]] = None, + ) -> None: + for axis_name, param in dict(split_params or {}).items(): + self.ensure_axis(axis_name).split_param = param + for axis_name, param in dict(tiling_params or {}).items(): + self.ensure_axis(axis_name).tiling_param = param + + for axis_name in list(low_dim_axes or []): + self._record_axis_property(self._low_dim_axis_names, axis_name) + axis = self.axes.get(axis_name) + if axis is not None: + axis.is_low_dim = True + + for axis_name in list(reduction_axes or []): + self._record_axis_property(self._reduction_axis_names, axis_name) + axis = self.axes.get(axis_name) + if axis is not None: + axis.is_reduction = True + + for axis_name, pid_dim in dict(axis_pid_dims or {}).items(): + self.ensure_axis(axis_name).pid_dim = pid_dim + for axis_name, expr in dict(axis_length_exprs or {}).items(): + self.ensure_axis(axis_name).length_expr = expr + for axis_name, expr in dict(fixed_tiling_exprs or {}).items(): + self.ensure_axis(axis_name).fixed_tiling_expr = expr + for axis_name, source in dict(axis_dynamic_sources or {}).items(): + self.ensure_axis(axis_name).dynamic_source = source + + def materialize_axis_sizes( + self, + all_args: Optional[Mapping[str, object]] = None, + ) -> Tuple[Dict[str, int], Dict[str, Dict[str, object]]]: + all_args = dict(all_args or {}) + axis_sizes: Dict[str, int] = {} + diagnostics: Dict[str, Dict[str, object]] = {} + + for axis in self.axes.values(): + if not self._should_materialize_axis(axis): + continue + + axis_sizes[axis.name], diagnostics[axis.name] = self._materialize_axis_size( + axis, + all_args, + ) + + return axis_sizes, diagnostics + + @property + def split_params(self) -> Dict[str, str]: + return { + axis.name: axis.split_param + for axis in self.axes.values() + if isinstance(axis.split_param, str) and axis.split_param + } + + @property + def tiling_params(self) -> Dict[str, str]: + return { + axis.name: axis.tiling_param + for axis in self.axes.values() + if isinstance(axis.tiling_param, str) and axis.tiling_param + } + + @property + def low_dim_axes(self): + return list(self._low_dim_axis_names) + + @property + def reduction_axes(self): + return list(self._reduction_axis_names) + + @property + def axis_pid_dims(self) -> Dict[str, int]: + return {axis.name: axis.pid_dim for axis in self.axes.values() if isinstance(axis.pid_dim, int)} + + @property + def axis_length_exprs(self) -> Dict[str, str]: + return { + axis.name: axis.length_expr + for axis in self.axes.values() + if isinstance(axis.length_expr, str) and axis.length_expr + } + + @property + def fixed_tiling_exprs(self) -> Dict[str, str]: + return { + axis.name: axis.fixed_tiling_expr + for axis in self.axes.values() + if isinstance(axis.fixed_tiling_expr, str) and axis.fixed_tiling_expr + } + + @property + def axis_dynamic_sources(self) -> Dict[str, str]: + return { + axis.name: axis.dynamic_source + for axis in self.axes.values() + if (isinstance(axis.dynamic_source, str) and axis.dynamic_source and axis.dynamic_source != "none") + } + + @staticmethod + def _should_materialize_axis(axis: VectorAxis) -> bool: + return bool(axis.split_param or axis.tiling_param or axis.length_expr or axis.fixed_tiling_expr) + + @staticmethod + def _record_axis_property(store: List[str], axis_name: str) -> None: + if axis_name not in store: + store.append(axis_name) + + def _apply_axis_property_flags(self, axis: VectorAxis) -> None: + axis.is_low_dim = axis.name in self._low_dim_axis_names + axis.is_reduction = axis.name in self._reduction_axis_names + + @staticmethod + def _materialize_axis_size( + axis: VectorAxis, + all_args: Mapping[str, object], + ) -> Tuple[int, Dict[str, object]]: + if not axis.split_param and not axis.tiling_param: + fixed_tiling_value, resolved_by = VectorAxes._resolve_positive_int_expr( + axis.fixed_tiling_expr, + all_args, + ) + if fixed_tiling_value is not None: + return fixed_tiling_value, { + "axis": axis.name, + "length_expr": axis.length_expr, + "fixed_tiling_expr": axis.fixed_tiling_expr, + "dynamic_source": axis.dynamic_source or "none", + "resolved_by": resolved_by, + "resolved_value": fixed_tiling_value, + } + + runtime_value = all_args.get(axis.name) + if VectorAxes._is_positive_int(runtime_value): + return runtime_value, { + "axis": axis.name, + "length_expr": axis.length_expr, + "dynamic_source": axis.dynamic_source or "none", + "resolved_by": "axis_name_arg", + "resolved_value": runtime_value, + } + + if isinstance(axis.length_expr, str) and axis.length_expr: + runtime_value = all_args.get(axis.length_expr) + if VectorAxes._is_positive_int(runtime_value): + return runtime_value, { + "axis": axis.name, + "length_expr": axis.length_expr, + "dynamic_source": axis.dynamic_source or "none", + "resolved_by": "length_expr_arg", + "resolved_value": runtime_value, + } + + literal_value = VectorAxes._parse_positive_int_literal(axis.length_expr) + if literal_value is not None: + return literal_value, { + "axis": axis.name, + "length_expr": axis.length_expr, + "dynamic_source": axis.dynamic_source or "none", + "resolved_by": "literal", + "resolved_value": literal_value, + } + + if axis.split_param: + return SPLIT_AXIS_FALLBACK_SIZE, { + "axis": axis.name, + "length_expr": axis.length_expr, + "dynamic_source": axis.dynamic_source or "none", + "resolved_by": "default_split", + "resolved_value": SPLIT_AXIS_FALLBACK_SIZE, + } + + return DEFAULT_AXIS_FALLBACK_SIZE, { + "axis": axis.name, + "length_expr": axis.length_expr, + "dynamic_source": axis.dynamic_source or "none", + "resolved_by": "default_tiling", + "resolved_value": DEFAULT_AXIS_FALLBACK_SIZE, + } + + @staticmethod + def _parse_positive_int_literal(value: str) -> Optional[int]: + try: + parsed = int(value, 10) + except (TypeError, ValueError): + return None + if parsed > 0: + return parsed + return None + + @staticmethod + def _is_positive_int(value: object) -> bool: + return isinstance(value, int) and not isinstance(value, bool) and value > 0 + + @staticmethod + def _resolve_positive_int_expr( + expr: Optional[str], + all_args: Mapping[str, object], + ) -> Tuple[Optional[int], Optional[str]]: + if not isinstance(expr, str) or not expr: + return None, None + + runtime_value = all_args.get(expr) + if VectorAxes._is_positive_int(runtime_value): + return runtime_value, "fixed_tiling_expr_arg" + + literal_value = VectorAxes._parse_positive_int_literal(expr) + if literal_value is not None: + return literal_value, "fixed_tiling_expr_literal" + + static_value = VectorAxes._evaluate_positive_int_expr(expr, all_args) + if static_value is not None: + return static_value, "fixed_tiling_expr_static" + return None, None + + @staticmethod + def _evaluate_positive_int_expr( + expr: str, + all_args: Mapping[str, object], + ) -> Optional[int]: + try: + node = ast.parse(expr, mode="eval").body + except Exception: + return None + + value = VectorAxes._evaluate_static_expr_node(node, all_args) + if VectorAxes._is_positive_int(value): + return value + return None + + @staticmethod + def _evaluate_static_expr_node( + node: ast.AST, + all_args: Mapping[str, object], + ) -> Optional[int]: + if isinstance(node, ast.Constant): + if isinstance(node.value, int) and not isinstance(node.value, bool): + return node.value + return None + + if isinstance(node, ast.Name): + value = all_args.get(node.id) + return value if isinstance(value, int) and not isinstance(value, bool) else None + + if isinstance(node, ast.UnaryOp): + operand = VectorAxes._evaluate_static_expr_node(node.operand, all_args) + if operand is None: + return None + if isinstance(node.op, ast.UAdd): + return operand + if isinstance(node.op, ast.USub): + return -operand + return None + + if isinstance(node, ast.BinOp): + left = VectorAxes._evaluate_static_expr_node(node.left, all_args) + right = VectorAxes._evaluate_static_expr_node(node.right, all_args) + if left is None or right is None: + return None + try: + if isinstance(node.op, ast.Add): + return left + right + if isinstance(node.op, ast.Sub): + return left - right + if isinstance(node.op, ast.Mult): + return left * right + if isinstance(node.op, ast.FloorDiv): + if right == 0: + return None + return left // right + if isinstance(node.op, ast.Mod): + if right == 0: + return None + return left % right + except Exception: + return None + return None diff --git a/third_party/ascend/backend/testing.py b/third_party/ascend/backend/testing.py new file mode 100644 index 0000000000..02b4062136 --- /dev/null +++ b/third_party/ascend/backend/testing.py @@ -0,0 +1,404 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import builtins +import multiprocessing +import os +from datetime import datetime, timezone +from typing import Optional +import fnmatch + +import triton.runtime as runtime +from triton.knobs import cache + + +class ProfilerResultMismatchError(RuntimeError): + + def __init__(self, target_kernel_name: str, expected_rows: int, actual_rows: int): + self.target_kernel_name = target_kernel_name + self.expected_rows = expected_rows + self.actual_rows = actual_rows + super().__init__( + "Profiler rows filtered by target kernel name do not match the expected count. " + f"target_kernel_name={target_kernel_name!r}, expected_rows={expected_rows}, actual_rows={actual_rows}") + + +def do_bench_npu_profiler( + funcs, + warmup=5, + active=30, + clear_l2_cache=False, + prof_dir=None, + keep_res=False, + target_kernel_name: Optional[str] = None, + _raise_on_mismatch: bool = False, +): + import torch + import torch_npu + if not isinstance(funcs, list): + funcs = [funcs] + + # warmup kernel + for fn in funcs: + fn() + torch.npu.synchronize() + + experimental_config = torch_npu.profiler._ExperimentalConfig( + aic_metrics=torch_npu.profiler.AiCMetrics.PipeUtilization, + profiler_level=torch_npu.profiler.ProfilerLevel.Level1, + l2_cache=False, + data_simplification=False, + ) + + if prof_dir is not None: + torch_path = prof_dir + else: + process = multiprocessing.current_process() + pid = process.pid + process_name = process.name + timestamp = datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S") + base_path = cache.get_triton_dir("profile_results") + torch_path = os.path.join(base_path, f"prof_{timestamp}_{process_name}-{pid}") + + if clear_l2_cache: + buffer = runtime.driver.active.get_empty_cache_for_benchmark() + buffer = buffer.float() # to avoid type cast + buffer.sum() + torch.npu.synchronize() # shake out of any npu error + + total = warmup + active + with torch_npu.profiler.profile( + activities=[torch_npu.profiler.ProfilerActivity.NPU], + on_trace_ready=torch_npu.profiler.tensorboard_trace_handler(torch_path), + record_shapes=False, + profile_memory=False, + with_stack=False, + with_flops=False, + with_modules=False, + experimental_config=experimental_config, + ) as prof: + for fn in funcs: + for _ in builtins.range(total): + if clear_l2_cache: + buffer.sum() # use buffer read to clear l2 cache + torch.npu.synchronize() + fn() + torch.npu.synchronize() + if clear_l2_cache: + del buffer + + try: + return _collect_prof_result( + torch_path, + funcs, + warmup, + active, + target_kernel_name=target_kernel_name, + clear_l2_cache=clear_l2_cache, + _raise_on_mismatch=_raise_on_mismatch, + ) + finally: + _rm_dic(keep_res, torch_path) + + +def _rm_dic(keep_res, torch_path): + if keep_res: + return + import shutil + + if os.path.exists(torch_path): + shutil.rmtree(torch_path) + + +def _collect_prof_result( + base_dir: str, + funcs, + num_warmup: int, + num_active: int, + target_kernel_name: Optional[str] = None, + clear_l2_cache: bool = False, + _raise_on_mismatch: bool = False, +): + """ + Collect kernel performance from task_time*.csv or kernel_details.csv, returned in millisecond. + Uses task_time*.csv by default. If target_kernel_name is provided, scans for kernel_details.csv as a fallback for accuracy. + The first `num_warmup` rows of each function are warmup data and will be ignored, the next `num_active` rows will be averaged. + + :param base_dir: the profiler path + :type base_dir: str + :param funcs: a list of Callable being profiled + :type funcs: List[Callable] + :param num_warmup: warmup count in task_time*.csv or kernel_details.csv of each fn + :type num_warmup: int + :param num_active: active count in task_time*.csv or kernel_details.csv of each fn + :type num_active: int + :param target_kernel_name: target triton kernel name reported by profiler + :type target_kernel_name: Optional[str] + """ + + import numpy as np + import pandas as pd + use_task_time = (target_kernel_name is None) + kernel_details_file = None + for root, _, files in os.walk(base_dir): + for file in files: + if use_task_time and fnmatch.fnmatch(file, "task_time*.csv"): + kernel_details_file = os.path.join(root, file) + break + elif not use_task_time and file == "kernel_details.csv": + kernel_details_file = os.path.join(root, file) + break + num_funcs = len(funcs) + + def _error(msg: str): + print(f"[Error] {msg}") + if num_funcs == 1: + return float("inf") + return [float("inf")] * num_funcs + + if kernel_details_file is None: + return _error( + f"No profiling data found under {base_dir}. The profiler may have failed to collect device tasks or stopped abnormally." + ) + + df = pd.read_csv(kernel_details_file, keep_default_na=False) + if use_task_time: + # The first and last lines of the task_time*.csv file are PROFILING_DISABLE, which should be deleted. + df = df[1:-1] + + if df.empty: + print("[WARNING] No profiling data on the device side.") + if num_funcs == 1: + return float("0") + return [float("0")] * num_funcs + + if use_task_time: + col_time = "task_time(us)" + filter_cond = (not clear_l2_cache) | ~df["kernel_name"].str.contains(r"^ReduceSum", case=False, na=False) + else: + col_time = "Duration(us)" + filter_cond = (not clear_l2_cache) | ~df["Type"].str.contains(r"^ReduceSum$", case=False, na=False) + # filter out l2 cache clearing operation + filter_df = df[filter_cond] + if target_kernel_name is not None: + filter_df = filter_df[filter_df["Name"] == target_kernel_name] + + expected_rows = num_funcs * (num_warmup + num_active) + actual_rows = len(filter_df) + + mul = 1 + if num_funcs == 1: + if actual_rows % expected_rows != 0: + if target_kernel_name is not None and _raise_on_mismatch: + raise ProfilerResultMismatchError(target_kernel_name, expected_rows, actual_rows) + return _error( + f"Expected the actual row count to be a multiple of {expected_rows}, but got {actual_rows}. The expected row count and the actual row count do not match." + ) + mul = actual_rows // expected_rows + num_warmup = num_warmup * mul + num_active = num_active * mul + else: + if actual_rows != expected_rows: + print( + "[WARNING] Passing a list of functions where a function may contain multiple kernels or launch no kernel is not supported and may lead to incorrect results." + ) + + time_cost = [0] * num_funcs + for func_idx in np.arange(0, num_funcs): + for active_index in np.arange(0, num_active): + row_index = func_idx * (num_warmup + num_active) + num_warmup + active_index + time_cost[func_idx] += filter_df.iloc[row_index][col_time] + time_cost = [x * mul / num_active / 1e3 for x in time_cost] + + if num_funcs == 1: + return time_cost[0] + else: + return time_cost + + +try: + from mspti import KernelMonitor +except ImportError: + KernelMonitor = None + +try: + from triton.backends.ascend.utils import is_cann_version_at_least + CANN_VERSION_AVAILABLE = True +except ImportError: + CANN_VERSION_AVAILABLE = False + print("[WARNING] triton.backends.ascend.utils not found. CANN version check skipped.") + + +# If the CANN version is earlier than 9.1.0, it needs to set libmspti.so in LD_PRELOAD to use mspti. +def do_bench_npu_mspti( + funcs, + warmup=5, + active=30, + clear_l2_cache=False, + target_kernel_name: Optional[str] = None, +): + import torch + import torch_npu + if not isinstance(funcs, list): + funcs = [funcs] + + for fn in funcs: + fn() + torch.npu.synchronize() + + if clear_l2_cache: + buffer = runtime.driver.active.get_empty_cache_for_benchmark() + buffer = buffer.float() # to avoid type cast + buffer.sum() + torch.npu.synchronize() # shake out of any npu error + else: + buffer = None + + all_kernel_durations = [] + + def callback(data): + if clear_l2_cache and ('reducesum' in data.name.lower()): + return + if target_kernel_name is not None and target_kernel_name not in data.name: + return + all_kernel_durations.append(data.end - data.start) + + monitor = KernelMonitor() + torch.npu.synchronize() + + monitor.start(callback) + + try: + total = warmup + active + for fn in funcs: + for _ in builtins.range(total): + if clear_l2_cache: + buffer.sum() + torch.npu.synchronize() + fn() + torch.npu.synchronize() + finally: + torch.npu.synchronize() + monitor.stop() + + num_funcs = len(funcs) + duration_per_kernel = [] + + expected_rows = num_funcs * total + actual_rows = len(all_kernel_durations) + if actual_rows < expected_rows: + if num_funcs == 1: + return float("inf") + return [float("inf")] * num_funcs + + mul = 1 + if num_funcs == 1: + if actual_rows % expected_rows != 0: + return float("inf") + mul = actual_rows // expected_rows + warmup = warmup * mul + total = actual_rows + else: + if actual_rows != expected_rows: + print( + "[WARNING] Passing a list of functions containing multiple kernels is not fully supported and may lead to inaccurate results." + ) + + current_idx = 0 + for i in range(num_funcs): + current_func_records = all_kernel_durations[current_idx:current_idx + total] + current_idx += total + current_active_records = current_func_records[warmup:total] + avg_time = sum(current_active_records) * mul / len(current_active_records) + avg_time_ms = avg_time / 1000000.0 + duration_per_kernel.append(avg_time_ms) + + if num_funcs == 1: + return duration_per_kernel[0] + else: + return duration_per_kernel + + +def _check_mspti_env(): + if CANN_VERSION_AVAILABLE: + if not is_cann_version_at_least(9, 1, 0): + if 'libmspti.so' not in os.getenv('LD_PRELOAD', ''): + print("[WARNING] libmspti.so not set in LD_PRELOAD, please set libmspti.so in LD_PRELOAD to use mspti.") + + +def do_bench_npu( + funcs, + warmup=5, + active=30, + clear_l2_cache=False, + prof_dir=None, + keep_res=False, + target_kernel_name: Optional[str] = None, +): + """ + Benchmark the runtime of the provided function on NPU. + this function utilizes NPU profiling tools (mspti or torch_npu.profiler) to capture pure Device-side kernel execution time. + By default, it returns the mean runtime in milliseconds of the provided function(s) based on `active` iterations. + + :param funcs: Function (or list of functions) to benchmark. If a list is provided, returns a list of mean runtimes. + :type funcs: Callable or List[Callable] + :param warmup: Warmup iterations. Runs the function `warmup` times before actual timing to stabilize performance. Defaults to 5. + :type warmup: int + :param active: Active iterations to record for timing. The final result is the average over these iterations. Defaults to 30. + :type active: int + :param clear_l2_cache: Whether to clear the L2 cache before each function execution. Defaults to False. + :type clear_l2_cache: bool, optional + :param prof_dir: Directory to save profiler results. If None, defaults to a temporary directory under triton cache. If specified, forces fallback to torch_npu.profiler. + :type prof_dir: str, optional + :param keep_res: Whether to keep the raw profiler result files (e.g., CSVs) after parsing. Defaults to False. If True, forces fallback to torch_npu.profiler. + :type keep_res: bool, optional + :param target_kernel_name: Specific NPU kernel name to filter and benchmark. If None, benchmarks the entire function. If specified, returns the execution time of only that kernel. Defaults to None. Only used when falling back to torch_npu.profiler. + :type target_kernel_name: str, optional + """ + import math + import os + mspti_available = True + if KernelMonitor is None: + mspti_available = False + print(f"[WARNING] mspti package not found. Falling back to torch_npu.profiler.") + _check_mspti_env() + + if not isinstance(funcs, list): + funcs = [funcs] + use_autotune = False + else: + use_autotune = os.getenv("TRITON_BENCH_METHOD", "default").lower() == "npu" + + results = None + need_fallback = True + force_fallback = (prof_dir is not None) or keep_res or (target_kernel_name is not None and not use_autotune) + + if mspti_available and not force_fallback: + try: + results = do_bench_npu_mspti(funcs, warmup, active, clear_l2_cache, target_kernel_name) + first_val = results[0] if isinstance(results, list) else results + if not math.isinf(first_val): + need_fallback = False + except Exception: + pass + if need_fallback: + results = do_bench_npu_profiler(funcs, warmup, active, clear_l2_cache, prof_dir, keep_res, target_kernel_name, + use_autotune) + return results diff --git a/third_party/ascend/backend/utils.py b/third_party/ascend/backend/utils.py new file mode 100644 index 0000000000..d0fff81341 --- /dev/null +++ b/third_party/ascend/backend/utils.py @@ -0,0 +1,944 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import functools +import hashlib +import os +import re +import shutil +import subprocess +import sysconfig +import warnings +from pathlib import Path +import logging +import platform +from triton.backends.ascend.backend_register import backend_strategy_registry + +import pybind11 + +_is_compile_on_910_95 = None + + +def _multibuffer_mode_to_tuple(value): + """Freeze a mode dictionary, or restore its pairs from a JSON cache. + + Sort keys for stable hashing regardless of dictionary insertion order. + NPU IR owns level names, count semantics and legacy-option handling. + """ + if value is None: + return None + if isinstance(value, dict): + value = tuple(value.items()) + if not isinstance(value, (list, tuple)) or any(not isinstance(pair, (list, tuple)) or len(pair) != 2 + or not isinstance(pair[0], str) or type(pair[1]) is not int + for pair in value): + raise TypeError("multibuffer_mode must be a dict[str, int], cached (str, int) pairs, or None") + return tuple(sorted((level, count) for level, count in value)) + + +# Compatibility boundary for compile-option cleanup. Public dictionaries +# route renamed options and discard backend-managed options before community +# JIT validates the remaining keys. +_DEPRECATED_NPU_OPTIONS = frozenset({ + "add_auto_scheduling", + "allow_fp8e4nv", + "arch", + "auto_blockify_size", + "auto_tile_and_bind_subblock", + "bisheng_options", + "code_motion", + "compile_on_910_95", + "enable_auto_blockify", + "enable_bishengir_simt_optimization", + "enable_buffer_insert_optimization", + "enable_cce_vf_auto_sync", + "enable_cce_vf_remove_membar", + "enable_cross_if_fusion", + "enable_drop_unit_dims", + "enable_linearize", + "enable_mask_fallback_conversion", + "enable_nd2nz_on_vector", + "enable_select_analysis", + "enable_simt_reorder_instruction", + "enable_sync_block_lock", + "enable_ub_refine_opt", + "enable_vf_fusion", + "force_simt_only", + "force_simt_template", + "graph_optimize_emit_remarks", + "graph_optimize_max_rewrites_per_function", + "graph_optimize_rule_mask", + "graph_optimize_ub_capacity_bytes", + "grid_num_tiles", + "has_auto_blockify_blacklist_op", + "inter_cache_num", + "intra_cache_num", + "kernel_name", + "load_cache_num", + "llvm_version", + "mix_mode", + "ops_reorder", + "optimize_dynamic_offset", + "parallel_mode", + "simt_reorder_instruction", + "storage_align", + "stream", + "use_bytecode", + "vf_merge_level", + "warp_size", +}) + +# These names remain reserved while the compatibility layer accepts them as +# deprecated compile options. Kernel parameters, including tl.constexpr +# parameters supplied through autotune configs, must not reuse them. +_RESERVED_NPU_OPTION_NAMES = _DEPRECATED_NPU_OPTIONS +_WARNED_DEPRECATED_NPU_OPTIONS = set() + +# Boolean compatibility switches route only their enabled state. An enabled +# legacy force switch overrides compile_mode to preserve its historical force +# semantics; False keeps the canonical mode or backend default unchanged. +_DEPRECATED_NPU_OPTION_ROUTES = { + "force_simt_only": ("compile_mode", "simt_only"), + "force_simt_template": ("compile_mode", "simd_simt_template"), +} + +# Apply weaker selectors first so pure SIMT wins if both legacy force switches +# are enabled together. +_DEPRECATED_NPU_OPTION_ROUTE_PRECEDENCE = ( + "force_simt_template", + "force_simt_only", +) + +# Renamed value options preserve the complete user value. A simultaneously +# supplied canonical option wins via setdefault below. +_DEPRECATED_NPU_OPTION_ALIASES = { + "intra_cache_num": "buf_slot_num_of_veccore", + "inter_cache_num": "buf_slot_num_of_crosscore", + "load_cache_num": "buf_slot_num_of_gm", + "enable_bishengir_simt_optimization": "simt_optimization_mode", +} + +# Removed no-op options keep using the compatibility path above. This table +# only specializes their migration guidance; route and alias options retain +# the behavior and messages defined by PR #1729. +_DEPRECATED_NPU_OPTION_DETAILS = { + "add_auto_scheduling": "it is ignored; the removed DAG scheduling switch has no replacement.", + "allow_fp8e4nv": "it is ignored; this option has no replacement because it had no effective consumer.", + "arch": "it is ignored; the target architecture is injected from GPUTarget.arch.", + "auto_blockify_size": "it is ignored; this option has no replacement because it had no effective consumer.", + "auto_tile_and_bind_subblock": + "it is ignored; tiling and subblock binding are derived from Linalg IR and lock semantics.", + "bisheng_options": "it is ignored; BiSheng compiler arguments and libdevice linkage are backend-managed.", + "code_motion": "it is ignored; the removed vendor compiler control has no replacement.", + "enable_auto_blockify": "it is ignored; automatic block mapping and its safety blacklist are backend-managed.", + "enable_buffer_insert_optimization": + "it is ignored; DynamicCV keeps buffer insertion optimization enabled internally.", + "enable_cce_vf_auto_sync": "it is ignored; the removed vendor compiler control has no replacement.", + "enable_cce_vf_remove_membar": "it is ignored; the removed vendor compiler control has no replacement.", + "enable_cross_if_fusion": "it is ignored; the removed vendor compiler control has no replacement.", + "enable_drop_unit_dims": "it is ignored; consider 'enable_flatten' when flattening is intended.", + "enable_linearize": "it is ignored; this option has no replacement because it had no effective consumer.", + "enable_mask_fallback_conversion": "it is ignored; the backend fixes mask fallback conversion to False.", + "enable_nd2nz_on_vector": "it is ignored; the backend fixes vector ND2NZ conversion to False.", + "enable_select_analysis": "it is ignored; the backend fixes select analysis to True.", + "enable_simt_reorder_instruction": "it is ignored; instruction reordering is controlled by simt_optimization_mode.", + "enable_sync_block_lock": "it is ignored; this option has no replacement because it had no effective consumer.", + "enable_ub_refine_opt": "it is ignored; the backend keeps UB refine optimization disabled.", + "enable_vf_fusion": "it is ignored; this switch is no longer forwarded to the NPU compiler.", + "graph_optimize_emit_remarks": "it is ignored; the backend fixes graph-optimization remarks to False.", + "graph_optimize_max_rewrites_per_function": + "it is ignored; the backend fixes the maximum rewrites per function to 64.", + "graph_optimize_rule_mask": + "it is ignored; the backend fixes the graph-optimization rule mask to 3071 (legacy + IAT + PTSM).", + "graph_optimize_ub_capacity_bytes": + "it is ignored; the backend derives an 80%-of-raw-UB graph budget from the target architecture.", + "has_auto_blockify_blacklist_op": "it is ignored; the safety flag is derived by scanning TTIR.", + "kernel_name": "it is ignored; the kernel name is derived from TTIR.", + "llvm_version": "it is ignored; this option has no replacement because it had no effective consumer.", + "mix_mode": "it is ignored; mix mode is derived from Linalg IR as internal metadata.", + "ops_reorder": "it is ignored; the removed vendor compiler control has no replacement.", + "optimize_dynamic_offset": "it is ignored; the backend fixes dynamic-offset optimization to False.", + "parallel_mode": "it is ignored; parallel mode is derived from compile_mode and Linalg IR.", + "simt_reorder_instruction": + "it is ignored; this option has been moved into the first digit of simt_optimization_mode.", + "storage_align": "it is ignored; the removed vendor compiler control has no replacement.", + "stream": "it is ignored; launch streams are managed by the runtime and driver.", + "use_bytecode": "it is ignored; the bytecode pipeline is always enabled.", + "warp_size": "it is ignored; the Ascend backend fixes warp_size to 32.", +} + +_DEPRECATED_ASCEND_ENV_VARS = frozenset({ + "LLVM_ROOT", + "MLIR_ROOT", + "TRITON_ALL_BLOCKS_PARALLEL", + "TRITON_ASCEND_ARCH", + "TRITON_ASCEND_COMPILE_SPEED_OPT", + "TRITON_BACKEND", + "TRITON_DISABLE_FFTS", + "TRITON_REGISTER_TENSOR_MSPROF", +}) +_DEPRECATED_ASCEND_ENV_VAR_DETAILS = { + "LLVM_ROOT": + "it is ignored; set CC to select the CPU launcher compiler.", + "MLIR_ROOT": + "it is ignored; packaged or PATH-discovered MLIR tools are used instead.", + "TRITON_ALL_BLOCKS_PARALLEL": + "it is ignored; automatic block mapping is managed by backend policy.", + "TRITON_ASCEND_ARCH": + "use an explicit GPUTarget.arch instead; host environment overrides are ignored.", + "TRITON_ASCEND_COMPILE_SPEED_OPT": + "it is ignored; this variable has no replacement because it had no effective consumer.", + "TRITON_BACKEND": + "it is ignored; the backend policy is fixed to 'torch_npu'.", + "TRITON_DISABLE_FFTS": + "it is ignored; FFTS policy is derived from the explicit target architecture.", + "TRITON_REGISTER_TENSOR_MSPROF": + "it is ignored; tensor-shape msprof registration is no longer controlled by an environment variable.", +} +_WARNED_DEPRECATED_ASCEND_ENV_VARS = set() + + +class _InternalNPUOptionInt(int): + """Marks an integer option value produced by the Ascend backend itself.""" + + +def _is_internal_npu_option_value(value) -> bool: + return isinstance(value, _InternalNPUOptionInt) + + +def _get_deprecated_npu_options(options) -> set[str]: + return { + name + for name in options.keys() & _DEPRECATED_NPU_OPTIONS + if not _is_internal_npu_option_value(options[name]) + } + + +def _warn_deprecated_npu_option(name: str) -> None: + if name in _WARNED_DEPRECATED_NPU_OPTIONS: + return + _WARNED_DEPRECATED_NPU_OPTIONS.add(name) + route = _DEPRECATED_NPU_OPTION_ROUTES.get(name) + alias = _DEPRECATED_NPU_OPTION_ALIASES.get(name) + detail = _DEPRECATED_NPU_OPTION_DETAILS.get(name) + if route is not None: + replacement_name, replacement_value = route + message = (f"Ascend compile option '{name}' is deprecated; " + f"use {replacement_name}={replacement_value!r} instead.") + elif alias is not None: + message = f"Ascend compile option '{name}' is deprecated; use '{alias}' instead." + elif detail is not None: + message = (f"Ascend compile option '{name}' is deprecated and will be removed in a future release; " + f"{detail}") + else: + message = (f"Ascend compile option '{name}' is deprecated and ignored; " + "the backend-managed/default behavior is used instead.") + warnings.warn( + message, + FutureWarning, + stacklevel=3, + ) + + +def _remove_deprecated_npu_options(options, *, in_place=False): + """Normalize reserved legacy NPU options, copying by default.""" + normalized = options if in_place else dict(options) + deprecated = _get_deprecated_npu_options(normalized) + active_routes = [ + _DEPRECATED_NPU_OPTION_ROUTES[name] + for name in _DEPRECATED_NPU_OPTION_ROUTE_PRECEDENCE + if name in deprecated and normalized[name] + ] + for name in sorted(deprecated): + _warn_deprecated_npu_option(name) + alias = _DEPRECATED_NPU_OPTION_ALIASES.get(name) + if alias is not None: + normalized.setdefault(alias, normalized[name]) + normalized.pop(name) + for replacement_name, replacement_value in active_routes: + normalized[replacement_name] = replacement_value + return normalized + + +def _warn_deprecated_ascend_env_var(name: str) -> None: + if (name not in _DEPRECATED_ASCEND_ENV_VARS or name not in os.environ + or name in _WARNED_DEPRECATED_ASCEND_ENV_VARS): + return + _WARNED_DEPRECATED_ASCEND_ENV_VARS.add(name) + detail = _DEPRECATED_ASCEND_ENV_VAR_DETAILS.get(name) + if detail is None: + message = (f"Ascend environment variable '{name}' is deprecated and ignored; " + "the default behavior is used instead.") + else: + message = (f"Ascend environment variable '{name}' is deprecated and will be removed in a future release; " + f"{detail}") + warnings.warn( + message, + FutureWarning, + stacklevel=3, + ) + + +def _warn_deprecated_ascend_env_vars() -> None: + for name in sorted(_DEPRECATED_ASCEND_ENV_VARS & os.environ.keys()): + _warn_deprecated_ascend_env_var(name) + + +def is_compile_on_910_95(arch: str = None) -> bool: + """Return whether the compilation target belongs to the Ascend 950 generation.""" + if arch is not None: + return isinstance(arch, str) and arch.startswith(("Ascend910_95", "Ascend950")) + + global _is_compile_on_910_95 + if _is_compile_on_910_95 is None: + try: + import acl + name_lower = acl.get_soc_name().lower() + _is_compile_on_910_95 = ("ascend910_95" in name_lower or "ascend950" in name_lower + or "910_958b" in name_lower) + except (ImportError, AttributeError): + _is_compile_on_910_95 = False + return _is_compile_on_910_95 + + +AUTO_BLOCKIFY_BLACKLIST_RULES = ( + (re.compile(r"\btt\.atomic_(?:rmw|cas)\b"), "atomic operations"), + (re.compile(r"\btt\.elementwise_inline_asm\b"), "inline elementwise assembly"), + ( + re.compile(r"\btt\.load\b[^\n]*\bisVolatile\s*=\s*true\b"), + "loads with volatile", + ), + ( + re.compile(r"\btt\.(?:load|store)\b[^\n]*\bcacheModifier\s*="), + "loads or stores with cache modifiers", + ), +) + +backend_policy = None + + +def get_backend_func(name, *args, **kwargs): + global backend_policy + if backend_policy is None: + _warn_deprecated_ascend_env_var("TRITON_BACKEND") + backend_policy = "torch_npu" + return backend_strategy_registry.execute_func(backend_policy, name, *args, **kwargs) + + +def get_logger(logger_name, logger_level_str): + ''' + ''' + logging_level_mapping = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + } + logger = logging.getLogger(logger_name) + logger.propagate = False + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + logger.setLevel(logging_level_mapping.get(logger_level_str.upper(), "INFO")) + logger.addHandler(console_handler) + return logger + + +def downgrade_llir(llir): + llir = _downgrade_mem_attrs(llir) + llir = _downgrade_stacksaverestore_intrinsics(llir) + return llir + + +def _downgrade_mem_attrs(llir: str): + memory_pattern = r"memory\([^()]*\)" + + def replace_mem_attr(m): + attrs = m[0][7:-1].split(",") + if len(attrs) == 0: + return "readnone" + loc_map = {"argmem": 1, "inaccessiblemem": 2, "other": 4} + loc_attr = 0 + rw_map = {"readwrite": 3, "write": 2, "read": 1, "none": 0} + rw_attr = 0 + for attr_pair in attrs: + pair = attr_pair.split(":") + assert len(pair) <= 2 + if len(pair) == 1: + rw = rw_map[pair[0].strip()] + loc = loc_map["other"] # all location + else: + rw = rw_map[pair[1].strip()] + loc_str = pair[0].strip() + if loc_str == "argmem" or loc_str == "inaccessiblemem": + loc = loc_map[loc_str] + else: + loc = loc_map["other"] + if rw > 0: + loc_attr = loc_attr | loc + rw_attr = rw_attr | rw + rev_rw_map = {0: "readnone", 1: "readonly", 2: "writeonly"} + if rw_attr in rev_rw_map: + rw_attr_str = rev_rw_map[rw_attr] + else: + rw_attr_str = "" + rev_loc_map = { + 1: "argmemonly", + 2: "inaccessiblememonly", + 3: "inaccessiblemem_or_argmemonly", + } + if loc_attr in rev_loc_map: + loc_attr_str = rev_loc_map[loc_attr] + else: + loc_attr_str = "" + return rw_attr_str + " " + loc_attr_str + + return re.sub(memory_pattern, replace_mem_attr, llir) + + +def _downgrade_stacksaverestore_intrinsics(llir: str): + llir = re.sub(r"llvm\.stacksave\.\w+", "llvm.stacksave", llir) + llir = re.sub(r"llvm\.stackrestore\.\w+", "llvm.stackrestore", llir) + return llir + + +def _get_triton_adapter_opt_path() -> str: + path = os.path.dirname(__file__) + path = os.path.join(path, "triton-adapter-opt") + return path + + +def _get_default_tool_path(path: str, *paths) -> str: + relative_path = os.path.join(path, *paths) + try: + import triton._C.libtriton as libtriton + tool_path = os.path.join(os.path.dirname(libtriton.__file__), relative_path) + if os.path.exists(tool_path) and os.access(tool_path, os.X_OK): + return tool_path + except (ImportError, AttributeError): + pass + return _get_tool_path(os.path.basename(relative_path)) + + +def _get_mlir_path(path: str, *paths) -> str: + _warn_deprecated_ascend_env_var("MLIR_ROOT") + return _get_default_tool_path(path, *paths) + + +def _get_llvm_path(path: str, *paths) -> str: + _warn_deprecated_ascend_env_var("LLVM_ROOT") + return _get_default_tool_path(path, *paths) + + +def _get_tool_path(tool_name: str) -> str: + """ + Get the path to a Triton tool binary. + Search order: + 1. Installed package location (triton/_C/) + 2. TRITON_BUILD_DIR environment variable + 3. System PATH + """ + try: + import triton._C.libtriton as libtriton + tool_path = os.path.join(os.path.dirname(libtriton.__file__), tool_name) + if os.path.exists(tool_path) and os.access(tool_path, os.X_OK): + return tool_path + except (ImportError, AttributeError): + pass + + build_path = os.getenv("TRITON_BUILD_DIR", "") + if build_path: + tool_path = os.path.join(build_path, "bin", tool_name) + if os.path.exists(tool_path) and os.access(tool_path, os.X_OK): + return tool_path + + tool_path = shutil.which(tool_name) + if tool_path: + return tool_path + + raise EnvironmentError(f"Could not find {tool_name} tool. " + f"It should be installed in triton/_C/ directory or available in PATH.") + + +def _get_triton_opt_path() -> str: + """ + Get the path to triton-opt tool. + This tool is used to convert ttir to ttadapter. + """ + return _get_tool_path("triton-opt") + + +def _get_triton_mlir_opt_path() -> str: + """ + Get the path to triton-mlir-opt tool. + This tool is used to convert MLIR to Bytecode format, supporting both + MLIR native ops and AscendNPU-IR custom ops. + """ + return _get_tool_path("triton-mlir-opt") + + +def _get_bishengir_payload_tool(tool: str): + """Use bundled tools as a pair, or leave resolution to PATH/CANN.""" + root = Path(__file__).resolve().parent / "bishengir" + if not os.path.lexists(root): + return None + for name in ("bishengir-opt", "bishengir-compile"): + executable = root / "bin" / name + if not executable.is_file() or not os.access(executable, os.X_OK): + raise EnvironmentError(f"Incomplete BiShengIR payload {root}: missing executable {executable}") + env = os.environ.copy() + bin_dir = str(root / "bin") + env["PATH"] = bin_dir + os.pathsep + env.get("PATH", os.defpath) + return str(root / "bin" / tool), env + + +def _get_bishengir_opt_path() -> str: + payload_tool = _get_bishengir_payload_tool("bishengir-opt") + if payload_tool is not None: + return payload_tool + env = os.environ.copy() + path = shutil.which("bishengir-opt") + if path is None: + root = os.getenv("TRITON_NPU_COMPILER_PATH") + if not root: + raise EnvironmentError("Couldn't find executable bishengir-opt or TRITON_NPU_COMPILER_PATH") + path = os.path.join(root, "bishengir-opt") + return path, env + + +def _get_npucompiler_path() -> str: + payload_tool = _get_bishengir_payload_tool("bishengir-compile") + if payload_tool is not None: + return payload_tool + env = os.environ.copy() + path = shutil.which("bishengir-compile") + if path is None: + root = os.getenv("TRITON_NPU_COMPILER_PATH") + if not root: + raise EnvironmentError("Couldn't find executable bishengir-compile or TRITON_NPU_COMPILER_PATH.") + path = os.path.join(root, "npuc") + return path, env + + +def _get_bisheng_path() -> str: + bisheng_path = shutil.which("bisheng") + if bisheng_path is None: + npu_compiler_root = os.getenv("TRITON_NPU_COMPILER_PATH", None) + if npu_compiler_root is None: + raise EnvironmentError("Couldn't find executable bisheng or TRITON_NPU_COMPILER_PATH") + bisheng_path = os.path.join(npu_compiler_root, "ccec") + return bisheng_path + + +def _is_valid_bishengir_path(path: str) -> bool: + if not path or not isinstance(path, str): + return False + if os.path.basename(path) != "bishengir-compile": + return False + if not os.path.isfile(path) or not os.access(path, os.X_OK): + return False + return True + + +# grep bishengir-compile's option limit-auto-multi-buffer-buffer to check +# if bishengir-compile is a newer version which does not generate kernel_reloc.o +# any more. +def _check_bishengir_api_change() -> bool: + bishengir_path, _ = _get_npucompiler_path() + if not _is_valid_bishengir_path(bishengir_path): + print(f"ERROR: Invalid bishengir path format: {bishengir_path}") + return False + try: + result = subprocess.run( + [bishengir_path, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode == 0 and 'limit-auto-multi-buffer-buffer' in result.stdout: + # bishengir-compile is newer version + return True + else: + # bishengir-compile is older version + return False + except Exception as e: + print(f"ERROR: {e}") + return False + + +def _check_bishengir_is_regbased() -> bool: + bishengir_path, _ = _get_npucompiler_path() + if not _is_valid_bishengir_path(bishengir_path): + print(f"ERROR: Invalid bishengir path format: {bishengir_path}") + return False + try: + result = subprocess.run( + [bishengir_path, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode == 0 and 'reg-based' in result.stdout: + # bishengir-compile is regbased version + return True + else: + # bishengir-compile is membased version + return False + except Exception as e: + print(f"ERROR: {e}") + return False + + +@functools.lru_cache(None) +def _get_ascend_path() -> Path: + path = os.getenv("ASCEND_HOME_PATH", "") + if path == "": + raise EnvironmentError("ASCEND_HOME_PATH is not set, source /set_env.sh first") + return Path(path) + + +def _is_ascend_sanitizer_enabled() -> bool: + return os.getenv("TRITON_ENABLE_SANITIZER", "false").lower() in ("true", "1") + + +def _is_debug_line_info_disabled() -> bool: + return os.getenv("TRITON_DISABLE_LINE_INFO", "true").lower() in ("true", "1") + + +def _is_auto_map_parallel_blocks_enabled() -> bool: + """Return the fixed backend policy for automatic block mapping.""" + _warn_deprecated_ascend_env_var("TRITON_ALL_BLOCKS_PARALLEL") + return True + + +def _get_auto_blockify_blacklist_reasons(ir_text: str): + return [description for pattern, description in AUTO_BLOCKIFY_BLACKLIST_RULES if pattern.search(ir_text)] + + +def _warn_auto_blockify_disabled(kernel_name: str, blacklist_reasons) -> None: + if not blacklist_reasons: + return + reasons = ", ".join(blacklist_reasons) + print(f"[WARNING] AutoBlockify disabled for kernel '{kernel_name}'. " + f"Unsafe ops: {reasons}. Enabling may cause correctness issues. " + "The compiler keeps AutoBlockify disabled for this kernel.") + + +def _enable_print_ub_bits() -> bool: + return os.getenv("ENABLE_PRINT_UB_BITS", "false").lower() in ("true", "1") + + +def _enable_dump_memory_info() -> bool: + return os.getenv("TRITON_MEMORY_DISPLAY", "false").lower() in ("true", "1") + + +def _enable_msdebug() -> bool: + return os.getenv("LLVM_EXTRACT_DI_LOCAL_VARIABLES", "false").lower() in ("true", "1") + + +def _get_cxx(): + cxx = os.environ.get("CC") + if cxx is None: + clangxx = shutil.which("clang++") + gxx = shutil.which("g++") + cxx = clangxx if clangxx is not None else gxx + if cxx is None: + raise RuntimeError("Failed to find C++ compiler") + return cxx + + +def _build_npu_ext(obj_name: str, header_or_src_path, src_path=None, *, kernel_launcher="torch", + precompile=False) -> str: + header_path = None + if src_path is None: + src_path = header_or_src_path + else: + header_path = header_or_src_path + + suffix = sysconfig.get_config_var("EXT_SUFFIX") + src_dir = os.path.dirname(src_path) + so_path = os.path.join(src_dir, f"{obj_name}{suffix}") + cxx = _get_cxx() + cc_cmd = [cxx, src_path] + cc_cmd += [f"-w"] + if hasattr(sysconfig, "get_default_scheme"): + scheme = sysconfig.get_default_scheme() + else: + scheme = sysconfig._get_default_scheme() + if scheme == "posix_local": + scheme = "posix_prefix" + py_include_dir = sysconfig.get_paths(scheme=scheme)["include"] + cc_cmd += [f"-I{py_include_dir}"] + cc_cmd += [f"-I{os.path.dirname(os.path.realpath(__file__))}"] + asc_path = _get_ascend_path() + if header_path is not None: + cc_cmd += [f"-I{os.path.dirname(header_path)}"] + + rt_path = os.path.join(asc_path, "include/experiment/runtime/runtime/rt.h") + if not os.path.exists(rt_path): + cc_cmd += [ + f"-I{os.path.join(asc_path, 'pkg_inc')}", + f"-I{os.path.join(asc_path, 'pkg_inc/profiling')}", + ] + + cc_cmd += [ + f"-I{os.path.join(asc_path, 'include')}", + f"-I{os.path.join(asc_path, 'include/experiment')}", + f"-I{os.path.join(asc_path, 'include/experiment/msprof')}", + f"-I{pybind11.get_include()}", + f"-L{os.path.join(asc_path, 'lib64')}", + "-lruntime", + "-lascendcl", + ] + if kernel_launcher == "torch": + if obj_name == "npu_utils": + cc_cmd += get_backend_func("get_cc_cmd_npu_utils") + else: + cc_cmd += get_backend_func("get_cc_cmd") + + cc_cmd += cann_version_compile_args() + cc_cmd += ["-std=c++17", "-shared", "-fPIC", "-o", so_path] + + result = subprocess.run(cc_cmd, capture_output=True, text=True) + + if result.returncode == 0: + return so_path + else: + raise RuntimeError(f"Failed to compile {src_path}, error: {result.stderr},cmd={cc_cmd}") + + +def _get_kernel_target(metadata: dict): + if "target" not in metadata: + raise Exception("No target provided!") + sub_target = metadata["target"].arch + assert isinstance(sub_target, str) + if sub_target.startswith("Ascend910B"): + mix_mode = metadata["mix_mode"] + if mix_mode.lower().strip("_").startswith("aiv"): + return "ascend_910b_vec", "c220-vec", "aiv" + elif mix_mode.lower().strip("_").startswith("aic"): + return "ascend_910b_cube", "c220-cube", "aic" + else: + return "ascend_910b", "c220", "mix" + elif sub_target.startswith("Ascend910"): + return "ascend_910", "c100", "mix" + else: + raise NotImplementedError(f"NPU subtarget {sub_target} not supported yet") + + +def _check_cxx11_abi(): + return get_backend_func("cxx_abi") + + +def convert_sigtype_to_int(sigty: str): + MAP_SIGTYPE_TO_INT = { + # Boolean + "i1": 12, # BOOL + # Integer types + "i4": 29, # INT4 + "i8": 2, # INT8 + "i16": 6, # INT16 + "i32": 3, # INT32 + "i64": 9, # INT64 + # Unsigned integer types + "u1": 30, # UINT1 + "u8": 4, # UINT8 + "u16": 7, # UINT16 + "u32": 8, # UINT32 + "u64": 10, # UINT64 + # Floating point types + "fp16": 1, # FLOAT16 + "bf16": 27, # DT_BF16 + "fp32": 0, # FLOAT + "fp64": 11, # DOUBLE + "fp8e5": 35, # FLOAT8_E5M2 + "fp8e4nv": 36, # FLOAT8_E4M3FN + } + if sigty not in MAP_SIGTYPE_TO_INT: + raise ValueError(f"Unsupported data type: {sigty}") + + return MAP_SIGTYPE_TO_INT[sigty] + + +def convert_dtype_to_numpy(dtype): + return get_backend_func("type_convert")[dtype] + + +def _check_bishengir_able_save_ir() -> bool: + bishengir_path, _ = _get_npucompiler_path() + if not _is_valid_bishengir_path(bishengir_path): + print(f"ERROR: Invalid bishengir path format: {bishengir_path}") + return False + try: + result = subprocess.run( + [bishengir_path, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode == 0 and 'save-linked-ir' in result.stdout: + return True + else: + return False + except Exception as e: + print(f"ERROR: {e}") + return False + + +def get_ascend_arch_from_env(): + _warn_deprecated_ascend_env_var("TRITON_ASCEND_ARCH") + return "" + + +def ub_size_in_kbytes_for_arch(arch: str) -> int: + """Return the raw UB capacity for a supported Ascend target architecture. + + This resolver is deliberately independent of the active runtime device so + compiler options can be normalized for an explicit compilation target. + Unknown and empty targets fail closed instead of guessing a UB capacity. + """ + if not isinstance(arch, str) or not arch: + return 0 + if arch.startswith(("Ascend910_95", "Ascend950")): + return 256 + if arch.startswith(("Ascend910A", "Ascend910B", "Ascend910D", "Ascend910_93", "Ascend310B")): + return 192 + return 0 + + +def graph_ub_budget_bytes_for_arch(arch: str) -> int: + """Return the conservative graph-optimization UB budget in bytes. + + StoreCoalescing does not model all live local buffers, so its budget is + capped at 80 percent of the target's raw UB capacity. + """ + return ub_size_in_kbytes_for_arch(arch) * 1024 * 80 // 100 + + +def is_ffts_supported(arch: str): + """Return whether the specified compilation target supports FFTS.""" + if not isinstance(arch, str) or not arch: + return False + if is_compile_on_910_95(arch) or arch in ["Ascend910A", "Ascend310B4"]: + return False + return True + + +def force_disable_ffts(arch: str) -> bool: + """Return whether the selected target requires FFTS to be disabled.""" + _warn_deprecated_ascend_env_var("TRITON_DISABLE_FFTS") + return is_compile_on_910_95(arch) + + +def _parse_cann_version(line: str): + m = re.search(r'(\d+)\.(\d+)(?:\.(\d+))?', line) + if m: + major = int(m.group(1)) + minor = int(m.group(2)) + patch = int(m.group(3)) if m.group(3) is not None else 0 + return (major, minor, patch) + return None + + +def _find_cann_version_file(): + ascend_path = str(_get_ascend_path()) + arch = get_machine_arch() + candidates = [ + os.path.join(ascend_path, arch + "-linux", "ascend_toolkit_install.info"), + os.path.join(ascend_path, arch + "-linux", "ascend_all_cann_install.info"), + ] + for p in candidates: + if os.path.exists(p): + return p + return None + + +@functools.lru_cache(None) +def get_cann_version(): + _cann_version = None + try: + version_file = _find_cann_version_file() + if version_file is None: + _cann_version = None + return None + with open(version_file, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.strip() + if "version" in line.lower(): + parsed = _parse_cann_version(line) + if parsed is not None: + _cann_version = parsed + return _cann_version + _cann_version = None + except Exception: + raise EnvironmentError("Could not parse CANN version file") + + +def is_cann_version_at_least(major: int, minor: int = 0, patch: int = 0) -> bool: + v = get_cann_version() + if v is None: + return False + return v >= (major, minor, patch) + + +def cann_version_compile_args(): + if is_cann_version_at_least(9, 1, 0): + return ["-DTRITON_CANN_910"] + return [] + + +def triton_enable_libdevice_simt(arch: str = None) -> bool: + """Return whether the environment switch selects SIMT libdevice.""" + return bool(os.getenv("TRITON_ENABLE_LIBDEVICE_SIMT", False)) and is_compile_on_910_95(arch) + + +def get_cann_version_file_hash(): + cann_version_file_path = _find_cann_version_file() + return get_file_hash256(cann_version_file_path) + + +def get_file_hash256(file_path): + if file_path is None: + raise ValueError("file_path is None") + sha256 = hashlib.sha256() + try: + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256.update(byte_block) + return sha256.hexdigest() + except FileNotFoundError as e: + raise FileNotFoundError(f"Can't find file {os.path.basename(file_path)}") from e + + +def get_machine_arch(): + ARCHITECTURE_ALIASES = { + "x86_64": "x86_64", + "amd64": "x86_64", + "i386": "x86_64", + "i686": "x86_64", + "arm64": "aarch64", + "aarch64": "aarch64", + "armv7l": "aarch64", + "armv8l": "aarch64", + "arm": "aarch64", + } + system_arch = platform.machine() + return ARCHITECTURE_ALIASES[system_arch] diff --git a/third_party/ascend/bin/CMakeLists.txt b/third_party/ascend/bin/CMakeLists.txt new file mode 100644 index 0000000000..b00a4af523 --- /dev/null +++ b/third_party/ascend/bin/CMakeLists.txt @@ -0,0 +1,73 @@ +# Added after third_party/ascend/lib, so TRITON_LIBS already holds the ascend +# conversion libraries; the tools below do not have to list them one by one. +get_property(triton_libs GLOBAL PROPERTY TRITON_LIBS) + +# triton-opt: replays the ttir->ttadapter pipeline that backend/compiler.py +# prints under `if opt.debug:`. Its registry is self-contained (see the comment +# at the top of triton-opt.cpp for why core bin/RegisterTritonDialects.h is not +# reused). +add_llvm_executable(triton-opt triton-opt.cpp PARTIAL_SOURCES_INTENDED) +llvm_update_compile_flags(triton-opt) + +target_link_libraries(triton-opt PRIVATE + ${triton_libs} + # MLIR core + MLIROptLib + MLIRPass + MLIRRegisterAllDialects + MLIRRegisterAllExtensions + MLIRRegisterAllPasses + MLIRTransforms + # The pipeline runs `inline` on func.func right after commonir-to-hivm. + MLIRFuncInlinerExtension + # Core BishengIR dialect libraries + BiShengIRAnnotationDialect + BiShengIRHACCDialect + BiShengIRHFusionDialect + BiShengIRHIVMDialect + BiShengIRMathExtDialect + BiShengIRMemRefExtDialect + BiShengIRScopeDialect + BiShengIRSymbolDialect + BiShengIRTensorDialect +) + +mlir_check_all_link_libraries(triton-opt) + +# triton-mlir-opt: Triton-Ascend optimizer combining MLIR and BishengIR dialects +add_llvm_executable(triton-mlir-opt triton-mlir-opt.cpp PARTIAL_SOURCES_INTENDED) +llvm_update_compile_flags(triton-mlir-opt) + +target_link_libraries(triton-mlir-opt PRIVATE + ${triton_libs} + # MLIR core + MLIROptLib + MLIRPass + MLIRRegisterAllDialects + MLIRRegisterAllPasses + MLIRTransforms + # Core BishengIR dialect libraries + BiShengIRAnnotationDialect + BiShengIRHACCDialect + BiShengIRHFusionDialect + BiShengIRHIVMDialect + BiShengIRMathExtDialect + BiShengIRMemRefExtDialect + BiShengIRScopeDialect + BiShengIRSymbolDialect + BiShengIRTensorDialect +) + +mlir_check_all_link_libraries(triton-mlir-opt) + + +# Emit into the build tree, NOT into CMAKE_LIBRARY_OUTPUT_DIRECTORY (the wheel's +# triton/_C). python/setup_tools/utils/ascend.py install_extension copies these +# into triton/_C and strips the copies, which keeps a full-symbol original here +# for source-level debugging. Writing straight to triton/_C would leave only one +# file: stripping it would discard the symbols for good, and because strip does +# not make the target look out of date, an incremental build would not restore +# them either. +set_target_properties(triton-opt triton-mlir-opt PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/third_party/ascend/bin" +) diff --git a/third_party/ascend/bin/triton-mlir-opt.cpp b/third_party/ascend/bin/triton-mlir-opt.cpp new file mode 100644 index 0000000000..efc9212f84 --- /dev/null +++ b/third_party/ascend/bin/triton-mlir-opt.cpp @@ -0,0 +1,42 @@ +//===- triton-mlir-opt.cpp - Triton-Ascend Optimizer Driver -----*- C++ -*-===// +// +// Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//===----------------------------------------------------------------------===// +// +// Main entry function for triton-mlir-opt built as standalone binary. +// This tool combines MLIR dialects and BishengIR dialects. +// +//===----------------------------------------------------------------------===// + +// BishengIR includes +#include "bishengir/InitAllDialects.h" + +#include "mlir/InitAllDialects.h" +#include "mlir/InitAllExtensions.h" +#include "mlir/InitAllPasses.h" +#include "mlir/Tools/mlir-opt/MlirOptMain.h" +#include "llvm/Support/InitLLVM.h" + +int main(int argc, char **argv) { + // Register all MLIR dialects and passes + mlir::DialectRegistry registry; + mlir::registerAllDialects(registry); + + // Register BishengIR dialects and passes + bishengir::registerAllDialects(registry); + + return mlir::asMainReturnCode(mlir::MlirOptMain( + argc, argv, "Triton-Ascend optimizer driver\n", registry)); +} diff --git a/third_party/ascend/bin/triton-opt.cpp b/third_party/ascend/bin/triton-opt.cpp new file mode 100644 index 0000000000..dda8bbb774 --- /dev/null +++ b/third_party/ascend/bin/triton-opt.cpp @@ -0,0 +1,146 @@ +//===- triton-opt.cpp - Triton-Ascend Pass Pipeline Driver ------*- C++ -*-===// +// +// Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//===----------------------------------------------------------------------===// +// +// Reproduces the backend's ttir->ttadapter pass pipeline outside of Python. +// `third_party/ascend/backend/compiler.py` prints the equivalent command line +// under `if opt.debug:`, and this driver is what runs it. +// +// Deliberately does NOT reuse the core `bin/RegisterTritonDialects.h`: +// - it registers test-only passes needing TritonTestAnalysis / +// TritonTestDialect / TritonTestProton, which an ascend configuration +// does not build (only amd's TritonAMDGPUTestAnalysis is available), +// - its CommonIR dialect registration sits behind __FLAGTREE_COMMON_IR__, +// a macro the ascend build never defines, so `tile.*` would not parse, +// - it carries no HIVM/HFusion/Scope/HACC/Annotation registration. +// Keeping a self-contained registry here also means this file does not have +// to be kept in sync with a header shared by every other backend. +// +//===----------------------------------------------------------------------===// + +#include "bishengir/InitAllDialects.h" + +#include "mlir/Dialect/Func/Extensions/InlinerExtension.h" +#include "mlir/InitAllDialects.h" +#include "mlir/InitAllExtensions.h" +#include "mlir/InitAllPasses.h" +#include "mlir/Tools/mlir-opt/MlirOptMain.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "ascend/include/Dialect/TritonStructured/IR/TritonStructuredDialect.h" +#include "mlir-ext/Dialect/CommonIR/IR/CommonIRDialect.h" + +#include "ascend/include/AutoBlockify/Passes.h" +#include "ascend/include/CommonIRToHIVM/Passes.h" +#include "ascend/include/DiscreteMaskAccessConversion/Passes.h" +#include "ascend/include/TritonControlFlowOpt/Passes.h" +#include "ascend/include/TritonToAnnotation/Passes.h" +#include "ascend/include/TritonToGraph/Passes.h" +#include "ascend/include/TritonToHFusion/Passes.h" +#include "ascend/include/TritonToHIVM/Passes.h" +#include "ascend/include/TritonToLLVM/Passes.h" +#include "ascend/include/TritonToLinalg/Passes.h" +#include "ascend/include/TritonToStructured/Passes.h" +#include "ascend/include/TritonToUnstructure/Passes.h" + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" +#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.h" +#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.h" +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Passes.h" +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.h" +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromComputePass.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.h" +#include "ascend/include/DynamicCVPipeline/StandardizeOp.h" + +static void registerAscendPasses() { + // Groups behind GEN_PASS_REGISTRATION. Between them these cover every pass + // the backend names in the printed pipeline: triton-control-flow-opt, + // triton-to-structured, discrete-mask-access-conversion, + // triton-to-annotation, triton-to-unstructure (+ bubble-up-operation), + // triton-to-hivm, triton-to-hfusion, triton-to-llvm, commonir-to-hivm + // (+ reduce-sum-strength), triton-to-linalg, merge-concat-load-buffer + // (via TritonToGraph) and add_dynamic_cv_pipeline. + mlir::triton::registerTritonControlFlowOptPasses(); + mlir::triton::registerTritonToStructuredPasses(); + mlir::triton::registerDiscreteMaskAccessConversionPasses(); + mlir::triton::registerTritonToAnnotationPasses(); + mlir::triton::registerTritonToUnstructurePasses(); + mlir::triton::registerTritonToHIVMPasses(); + mlir::triton::registerTritonToHFusionPasses(); + mlir::triton::registerTritonToLLVMPasses(); + mlir::triton::registerCommonIRToHIVMPasses(); + mlir::triton::registerTritonToLinalgPasses(); + mlir::triton::registerAutoBlockifyPasses(); + mlir::triton::cfg::registerTritonToGraphPasses(); + mlir::triton::registerAddDynamicCVPipelinePasses(); + + // DynamicCVPipeline sub-passes declared by hand rather than in Passes.td. + // add_dynamic_cv_pipeline drives these through a nested pass manager, so + // they are not required to replay the printed pipeline, but registering + // them lets a single stage be run in isolation. Passes also covered by + // registerAddDynamicCVPipelinePasses above resolve to the same allocator, + // which the pass registry accepts. + mlir::triton::registerStandardizeOpPasses(); + mlir::triton::registerPreCheckAvailablePasses(); + mlir::triton::registerAddControlFlowConditionPasses(); + mlir::triton::registerAddMultiBufferOuterScopePasses(); + mlir::triton::registerAddMultiBufferInnerScopePasses(); + mlir::triton::registerUBOverflowCheckerPasses(); + mlir::triton::registerSeparateMemoryFromComputePasses(); + mlir::triton::registerAnalyzeDataFlowPasses(); + mlir::triton::registerComputeBlockOptPasses(); + mlir::triton::registerPlanComputeBlockPasses(); + mlir::triton::registerAddOpClassifierPasses(); + mlir::triton::registerRefineArgsBlockIdPasses(); + // Not registered: registerAsyncLoadHoistingPasses. It is declared in + // DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoistingPass.h and + // defined in the matching .cpp, but lib/DynamicCVPipeline/CMakeLists.txt + // does not glob DecoupleComputeAndMemory/*.cpp, so that directory is not + // compiled into libDynamicCVPipeline and the symbol does not exist. +} + +int main(int argc, char **argv) { + mlir::DialectRegistry registry; + + // Upstream MLIR: supplies the dialects the lowering targets (linalg, memref, + // tensor, bufferization, func, scf, ...) plus canonicalize / cse / inline / + // symbol-dce, which the printed pipeline interleaves with ascend passes. + mlir::registerAllDialects(registry); + mlir::registerAllExtensions(registry); + mlir::registerAllPasses(); + + // mlir::registerAllExtensions does not pull in the func dialect's inliner + // interface. The pipeline runs `inline` right after commonir-to-hivm, and + // the Inliner aborts on func.func without it. This mirrors the in-process + // path, which registers the same extension in triton_ascend.cc load_dialects. + mlir::func::registerInlinerExtension(registry); + + // BiShengIR: hivm / hfusion / scope / hacc / annotation dialects and their + // bufferization and tiling external models. + bishengir::registerAllDialects(registry); + + registry.insert(); + + registerAscendPasses(); + + return mlir::asMainReturnCode(mlir::MlirOptMain( + argc, argv, "Triton-Ascend pass pipeline driver\n", registry)); +} diff --git a/third_party/ascend/build/packaging.py b/third_party/ascend/build/packaging.py new file mode 100644 index 0000000000..ad46ff94d3 --- /dev/null +++ b/third_party/ascend/build/packaging.py @@ -0,0 +1,132 @@ +"""Ascend tool and consumer wheel staging; loaded without importing triton. + +This module is loaded by file path so the legacy build package __init__ and +its setup patch machinery are not executed. +""" +import os +import subprocess +from pathlib import Path +import shutil + + +def _build_npuir_enabled(): + value = os.environ.get("TRITON_BUILD_NPUIR", "OFF").strip().upper() + if value in {"ON", "1", "YES", "TRUE", "Y"}: + return True + if value in {"OFF", "0", "NO", "FALSE", "N"}: + return False + raise RuntimeError(f"Invalid TRITON_BUILD_NPUIR value: {value!r}") + + +def _remove_generated_directory(path): + if path.is_symlink(): + raise RuntimeError(f"Refusing to clean symlinked build directory: {path}") + if path.exists(): + if not path.is_dir(): + raise RuntimeError(f"Expected a generated directory: {path}") + shutil.rmtree(path) + + +def _copy_ascend_tools(extdir, cmake_dir): + """Mirror triton-ascend's copy-then-strip hook; retain build-tree symbols.""" + source_dir = Path(cmake_dir) / "third_party/ascend/bin" + destination_dir = Path(extdir) + destination_dir.mkdir(parents=True, exist_ok=True) + for name in ("triton-opt", "triton-mlir-opt"): + source = source_dir / name + if not source.is_file(): + print(f"[ascend] {name} was not built; skipping", flush=True) + continue + destination = destination_dir / name + shutil.copy2(source, destination) + destination.chmod(0o755) + print(f"[ascend] Copied {name} to {destination}", flush=True) + try: + subprocess.check_call(["strip", "--strip-all", str(destination)]) + except (subprocess.CalledProcessError, FileNotFoundError) as error: + print(f"[ascend] WARNING: could not strip {name}: {error}", flush=True) + else: + print(f"[ascend] Stripped {name} to reduce size", flush=True) + + +def _validate_bishengir_payload(payload): + for name in ("bishengir-opt", "bishengir-compile", "hivmc", "hivmc-a5"): + executable = payload / "bin" / name + if not executable.is_file() or not os.access(executable, os.X_OK): + raise RuntimeError(f"Missing BiShengIR executable: {executable}") + # Template bitcode is matched by stem rather than by exact file name: an + # AscendNPU-IR revision that supports --enable-optimized-metaop emits + # per-optimization-level variants (meta_op.aic.c220.O2.bc) instead of the + # single meta_op.aic.c220.bc, and both layouts are valid payloads. + stems = [f"meta_op.{kind}.{arch}" for arch in ("c220", "c310") + for kind in ("aic", "aiv", "mix.aic", "mix.aiv")] + for stem in (*stems, "host"): + found = [path for path in payload.glob(f"lib/{stem}.*bc") if path.stat().st_size] + if not found: + raise RuntimeError(f"Missing or empty BiShengIR template: {payload / 'lib' / (stem + '*.bc')}") + + +def _build_bishengir(): + # setup.py loads the registry under python.setup_tools, not setup_tools. + from python.setup_tools.utils import get_submodules + + module = get_submodules("AscendNPU-IR") + source = Path(module.dst_path).resolve() + script = source / "build-tools" / "build.sh" + if not script.is_file(): + raise RuntimeError(f"AscendNPU-IR source is not prepared: {source}") + head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip() + if head != module.commit_id: + raise RuntimeError(f"AscendNPU-IR is at {head}, expected {module.commit_id}; prepare the pinned checkout") + cann_home = os.environ.get("ASCEND_HOME_PATH") + if not cann_home: + raise RuntimeError("TRITON_BUILD_NPUIR=ON requires ASCEND_HOME_PATH; source CANN set_env.sh first") + bisheng_bin = Path(cann_home).expanduser().resolve() / "tools" / "bisheng_compiler" / "bin" + for name in ("ccec", "llvm-link"): + executable = bisheng_bin / name + if not executable.is_file() or not os.access(executable, os.X_OK): + raise RuntimeError(f"Missing CANN build tool: {executable}") + for name in ("git", "cmake", "ninja", "clang", "clang++"): + if shutil.which(name) is None: + raise RuntimeError(f"Required build tool is not on PATH: {name}") + env = os.environ.copy() + for name in list(env): + if name.startswith(("LLVM_", "MLIR_", "CMAKE_")): + env.pop(name) + for name in ("CC", "CXX", "CFLAGS", "CXXFLAGS", "CPPFLAGS", "LDFLAGS"): + env.pop(name, None) + subprocess.check_call(["git", "submodule", "update", "--init", "--recursive"], cwd=source, env=env) + build_root = source / "build" + _remove_generated_directory(build_root) + build_root.mkdir() + payload = build_root / "bishengir-output" + cmake_options = ["-DLLVM_ENABLE_LLD=" + ("ON" if shutil.which("ld.lld") else "OFF")] + if shutil.which("ccache"): + cmake_options.extend(["-DCMAKE_C_COMPILER_LAUNCHER=ccache", "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache"]) + command = [ + "bash", str(script), "--build-type=Release", "-o", str(build_root / "bishengir"), + "-t", "-j", "32", "-r", f"--bisheng-compiler={bisheng_bin}", + "--build-triton", "--build-torch-mlir", "--build-shmem-template", + "--bishengir-publish", "ON", "--collect-binary", str(payload), + "--add-cmake-options", " ".join(cmake_options), + ] + print(f"[ascend] Building BiShengIR under {build_root}", flush=True) + subprocess.check_call(command, cwd=source, env=env) + _validate_bishengir_payload(payload) + return payload + + +def finalize_build_tree(build_lib): + enabled = _build_npuir_enabled() + build_root = Path(build_lib).resolve() + destination = build_root / "triton" / "backends" / "ascend" / "bishengir" + if not destination.parent.resolve().is_relative_to(build_root): + raise RuntimeError(f"Backend staging escapes build_lib: {destination}") + _remove_generated_directory(destination) + if not enabled: + return + payload = _build_bishengir() + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(payload, destination, copy_function=shutil.copy2) + _validate_bishengir_payload(destination) + print(f"[ascend] Bundled BiShengIR into {destination}", flush=True) diff --git a/third_party/ascend/costmodel/configs/ascend_910b.json b/third_party/ascend/costmodel/configs/ascend_910b.json new file mode 100644 index 0000000000..1df9fb5a59 --- /dev/null +++ b/third_party/ascend/costmodel/configs/ascend_910b.json @@ -0,0 +1,770 @@ +{ + "$schema": "./hardware_schema.json", + "name": "Ascend 910B", + "vendor": "Huawei", + "version": "1.0", + + "clock": { + "frequency_ghz": 1.85, + "cycles_per_us": 1850 + }, + + "memory_spaces": { + "hbm": { + "type": "off_chip", + "size_gb": 32, + "bandwidth_tbps": 1.6, + "latency_cycles": 200 + }, + "l2": { + "type": "on_chip_shared", + "size_mb": 192, + "bandwidth_gbps": 3200, + "latency_cycles": 50 + }, + "l1": { + "type": "on_chip_local", + "size_kb": 512, + "bandwidth_gbps": 6400, + "latency_cycles": 10, + "description": "Cube data staging buffer" + }, + "l0a": { + "type": "register_file", + "size_kb": 64, + "description": "Left matrix input for Cube" + }, + "l0b": { + "type": "register_file", + "size_kb": 64, + "description": "Right matrix input for Cube" + }, + "l0c": { + "type": "register_file", + "size_kb": 128, + "description": "Cube output accumulator" + }, + "ub": { + "type": "on_chip_local", + "size_kb": 192, + "bandwidth_gbps": 3200, + "latency_cycles": 5, + "description": "Unified Buffer for Vector compute" + } + }, + + "compute_units": { + "cube": { + "type": "matrix_engine", + "tflops_fp16": 320, + "tflops_fp32": 160, + "tflops_int8": 640, + "fractal_sizes": { + "fp16": { + "m": 16, + "k": 16, + "n": 16 + }, + "bf16": { + "m": 16, + "k": 16, + "n": 16 + }, + "fp32": { + "m": 16, + "k": 8, + "n": 16 + }, + "int8": { + "m": 16, + "k": 32, + "n": 16 + } + }, + "tile_m": 16, + "tile_n": 16, + "tile_k": 16, + "input_spaces": [ + "l0a", + "l0b" + ], + "output_space": "l0c", + "supported_dtypes": [ + "fp16", + "bf16", + "fp32", + "int8" + ] + }, + "vector": { + "type": "simd_engine", + "width_elements": 128, + "width_bytes": 256, + "tflops_fp32": 10, + "tflops_fp16": 20, + "compute_space": "ub", + "supported_ops": [ + "add", + "sub", + "mul", + "div", + "max", + "min", + "exp", + "log", + "sqrt", + "rsqrt", + "tanh", + "sigmoid", + "relu", + "reduce_sum", + "reduce_max", + "reduce_min", + "cast", + "broadcast" + ], + "supported_dtypes": [ + "fp32", + "fp16", + "bf16", + "int32", + "int8" + ] + } + }, + + "data_movers": { + "cube_mte2": { + "description": "Cube input: HBM to L1", + "src_space": "hbm", + "dst_space": "l1", + "bandwidth_gbps": 200, + "max_burst_bytes": 65536, + "alignment_bytes": 32 + }, + "mte1": { + "description": "Cube input: L1 to L0A/L0B", + "src_space": "l1", + "dst_spaces": [ + "l0a", + "l0b" + ], + "bandwidth_gbps": 400, + "max_burst_bytes": 32768, + "alignment_bytes": 32 + }, + "fixpipe": { + "description": "Cube output: L0C to HBM", + "src_space": "l0c", + "dst_space": "hbm", + "bandwidth_gbps": 200, + "max_burst_bytes": 65536, + "alignment_bytes": 32, + "supports_accumulate": true, + "supports_cast": true + }, + "vector_mte2": { + "description": "Vector input: HBM to UB", + "src_space": "hbm", + "dst_space": "ub", + "bandwidth_gbps": 200, + "max_burst_bytes": 65536, + "alignment_bytes": 32 + }, + "mte3": { + "description": "Vector output: UB to HBM", + "src_space": "ub", + "dst_space": "hbm", + "bandwidth_gbps": 200, + "max_burst_bytes": 65536, + "alignment_bytes": 32 + } + }, + + "pipeline": { + "cube_path": { + "stages": [ + "cube_mte2", + "mte1", + "cube", + "fixpipe" + ], + "description": "Matrix multiplication data flow", + "fully_pipelined": true + }, + "vector_path": { + "stages": [ + "vector_mte2", + "vector", + "mte3" + ], + "description": "Vector computation data flow", + "fully_pipelined": true + }, + "hardware_units": { + "cube_mte2": { + "path": "cube", + "can_overlap": true + }, + "mte1": { + "path": "cube", + "can_overlap": true + }, + "cube": { + "path": "cube", + "can_overlap": true + }, + "fixpipe": { + "path": "cube", + "can_overlap": true + }, + "vector_mte2": { + "path": "vector", + "can_overlap": true + }, + "vector": { + "path": "vector", + "can_overlap": true + }, + "mte3": { + "path": "vector", + "can_overlap": true + } + }, + "parallelism": { + "inter_path": { + "cube_path_and_vector_path": true, + "description": "Cube and Vector paths execute in parallel" + }, + "intra_path": { + "cube_mte2_mte1_cube_fixpipe": true, + "vector_mte2_vector_mte3": true, + "description": "All units within each path can execute in parallel (pipelined)" + } + } + }, + + "performance_model": { + "cube_cycles_per_tile": { + "formula": "ceil(M/16) * ceil(N/16) * ceil(K/16)", + "base_cycles": 1 + }, + "vector_cycles_per_op": { + "formula": "ceil(elements / 128)", + "base_cycles": 1 + }, + "memory_cycles": { + "formula": "bytes / bandwidth_per_cycle", + "include_latency": true + } + }, + + "calibration": { + "description": "Parameters calibrated from _attn_fwd (flash attention) profiling on Ascend 910B. Input: Z=1,H=8,N_CTX=192,HEAD_DIM=128. BLOCK_M in {16,32,48,64}, BLOCK_N=64 fixed. Hardware: Block Dim=20 (AIC) + 20 AIV = Mix Block Dim 40.", + + "startup_latencies": { + "vector_startup_cycles": 35, + "mte2_startup_cycles": 50, + "mte3_startup_cycles": 40, + "cube_startup_cycles": 20, + "fixpipe_startup_cycles": 30, + "notes": "vector: was 10, increased to reflect UB read-after-write stall in serial dependency chains (sub->exp->reduce). mte2: restored to 50 (actual DMA startup); pipe_barrier overhead now modeled separately in estimateDuration." + }, + + "vector_op_cycles_per_vec_instruction": { + "exp": 4, + "log": 3, + "tanh": 3, + "sigmoid": 3, + "sqrt": 2, + "rsqrt": 2, + "div": 3, + "simple_ops_add_sub_mul_etc": 1, + "notes": "Calibrated ordering for vector ALU ops: vadd/vsub/vmul remain the cheapest, vreduce is only slightly heavier, and vexp is typically about 4x the cost of simple arithmetic. Other transcendental and divide-like ops stay between those extremes." + }, + + "scalar_overhead": { + "aiv_scalar_to_vec_ratio_steady_state": 1.27, + "aic_scalar_to_mac_ratio_steady_state": 8.4, + "aiv_vec_ratio_observed": 0.211, + "aiv_scalar_overhead_factor": 3.74, + "description": "aiv_vec_ratio converges to 0.211 for BM>=48. Total AIV time = vec_time * (1/0.211) = vec_time * 4.74, equivalently vec_time * (1 + 3.74). Scalar fraction covers loop control, pointer arithmetic, and pipe_barrier sync between AIC and AIV. Completely absent from prior model." + }, + + "parallelism": { + "num_aic_cores": 20, + "num_aiv_cores": 40, + "description": "From profiling: Block Dim=20, Mix Block Dim=40 (2 AIV per AIC block)." + }, + + "pipe_barrier": { + "cycles_per_inner_iteration": 7500, + "description": "Calibrated from BM=64 (1 AIV wave, 24 programs). AIV wall time=31.993us=59187 cycles. Active fraction (vec+scalar+mte) sums to 61.1%, leaving 38.9% idle=23044 cycles. With N_iter=3 inner iterations per program: 23044/3 ~ 7500 cycles per pipe_barrier." + }, + + "wave_overhead": { + "description": "Observed aiv_time decreases monotonically with fewer AIV waves: BM=16(3 waves)->BM=32(2 waves)->BM=48(1 wave). BM=64 also 1 wave but with lower program count, making BM=48 and BM=64 near-equal (exception to the monotonic trend). The wave count multiplier N_waves=ceil(N_programs/N_AIV) is applied to per-program cycles to produce kernel-level time." + } + }, + + "tilesim": { + "description": "Knobs for the tilesim-migrated micro-architecture model (root cause 1/2).", + "active_bandwidth_cores": 48, + "enable_small_packet_bw": true, + "small_packet": { + "description": "Global small-packet bandwidth fitting coefficients (tilesim 910B1.yaml pkt_param). Applied to EVERY mover when enable_small_packet_bw is on, mirroring tilesim's single global pkt_param dict; a per-table bandwidth_tables[*].small_packet entry overrides these. Bucket selection: <64B -> 64B, <128B -> 128B, <256B -> 256B; formula bw = b/(a*b + pktBytes/(1024^3)).", + "threshold_bytes": 256, + "64B": [ + 0.295, + 12.061 + ], + "128B": [ + 0.272, + 16.820 + ], + "256B": [ + 0.322, + 24.004 + ] + }, + "mutex_groups": { + "description": "Hardware units that share a physical pipeline and cannot execute in parallel (tilesim MutexComponents / pipe_exclusive_config). Each clique is an array of costmodel unit names (cube/cube_mte2/fixpipe/vector/vec_mte2/mte3/scalar). 910B: AIV MTE2 (vec load) and MTE3 (vec store) share one pipeline -> they serialize, so the vector-path roofline uses max(vector, vec_mte2 + mte3) instead of max(vector, vec_mte2, mte3).", + "cliques": [ + [ + "vec_mte2", + "mte3" + ] + ] + } + }, + + "cube_model": { + "description": "Cube (GEMM) L1-pipe roofline parameters (tilesim 910B1 cube_config).", + "throughput": [ + 16, + 32, + 16 + ], + "repeat_cycles": { + "fp32": 2, + "fp16": 1, + "bf16": 1, + "int8": 1 + }, + "l0_tile_limit_kb": 32 + }, + + "bandwidth_tables": { + "description": "Per-(src:dst[,corenum,pkt]) bandwidth in GB/s, migrated from tilesim bandwidth_910B1.csv (GM->hbm, SHM->l2). Costmodel space names: hbm/l2/l1/l0a/l0b/l0c/ub. Core-dependent movers carry per_core_gbps (1..48); others use bandwidth_gbps.", + "hbm:ub": { + "per_core_gbps": { + "1": 100.9, + "2": 64.5, + "3": 88.29666667, + "4": 77.945, + "5": 84.852, + "6": 80.82666667, + "7": 73.31571429, + "8": 68.93375, + "9": 73.22888889, + "10": 67.586, + "11": 71.13727273, + "12": 68.08833333, + "13": 69.14615385, + "14": 72.60785714, + "15": 74.99866667, + "16": 73.26375, + "17": 72.05058824, + "18": 70.66833333, + "19": 73.27, + "20": 68.7705, + "21": 68.72190476, + "22": 66.07909091, + "23": 66.33826087, + "24": 65.08041667, + "25": 65.3448, + "26": 62.61884615, + "27": 62.15148148, + "28": 58.95964286, + "29": 58.23, + "30": 56.146, + "31": 56.48, + "32": 53.3059375, + "33": 51.55151515, + "34": 49.97264706, + "35": 48.51342857, + "36": 46.525, + "37": 46.06621622, + "38": 43.74315789, + "39": 42.97435897, + "40": 41.5595, + "41": 40.44609756, + "42": 39.31904762, + "43": 38.47302326, + "44": 37.03886364, + "45": 36.33777778, + "46": 35.40717391, + "47": 34.83446809, + "48": 33.68666667 + } + }, + "ub:hbm": { + "per_core_gbps": { + "1": 188.46, + "2": 210.53, + "3": 117.2733333, + "4": 130.295, + "5": 108.964, + "6": 119.54, + "7": 117.280446425, + "8": 115.02089285, + "9": 112.761339275, + "10": 110.5017857, + "11": 108.242232125, + "12": 105.98267855, + "13": 103.723124975, + "14": 101.4635714, + "15": 100.0593333, + "16": 100.373125, + "17": 96.81470588, + "18": 98.78555556, + "19": 90.44578947, + "20": 88.896, + "21": 83.38571429, + "22": 83.77727273, + "23": 77.92608696, + "24": 76.65166667, + "25": 71.0792, + "26": 70.58615385, + "27": 66.58407407, + "28": 66.42357143, + "29": 61.56655172, + "30": 59.854, + "31": 56.1083871, + "32": 55.9315625, + "33": 53.0, + "34": 51.53, + "35": 49.45828571, + "36": 48.11111111, + "37": 46.14540541, + "38": 45.26947368, + "39": 43.51512821, + "40": 42.56075, + "41": 40.9797561, + "42": 40.19857143, + "43": 38.46488372, + "44": 38.07454545, + "45": 36.684, + "46": 36.17608696, + "47": 34.70893617, + "48": 34.34291667 + } + }, + "hbm:l1": { + "bandwidth_gbps": 135 + }, + "hbm:l2": { + "bandwidth_gbps": 33.68666667 + }, + "l2:hbm": { + "bandwidth_gbps": 34.34291667 + }, + "l2:ub": { + "bandwidth_gbps": 137 + }, + "ub:l2": { + "bandwidth_gbps": 93 + }, + "l2:l1": { + "bandwidth_gbps": 221 + }, + "l1:l2": { + "bandwidth_gbps": 221 + }, + "l1:l0a": { + "bandwidth_gbps": 441 + }, + "l1:l0b": { + "bandwidth_gbps": 220.5 + }, + "hbm:l0b": { + "bandwidth_gbps": 104.5 + }, + "l0c:hbm": { + "bandwidth_gbps": 70 + }, + "l0c:l2": { + "bandwidth_gbps": 70 + }, + "l0c:l1": { + "bandwidth_gbps": 70 + }, + "hbm:l2_agg": { + "bandwidth_gbps": 1638.4 + } + }, + + "vec_cycle_tables": { + "description": "Per-(intrinsic,dtype) vector cycle triplets {compute,head,interval}, migrated from tilesim vec_cycle_910B1.csv. dtype keys fp16 (covers fp16/bf16) and fp32. cycles = compute * repeats + startup, repeats = ceil(numElems*elemBytes/256).", + "VADD": { + "fp16": { + "compute": 2, + "head": 13, + "interval": 18 + }, + "fp32": { + "compute": 2, + "head": 13, + "interval": 18 + } + }, + "VSUB": { + "fp16": { + "compute": 2, + "head": 13, + "interval": 18 + }, + "fp32": { + "compute": 2, + "head": 13, + "interval": 18 + } + }, + "VMUL": { + "fp16": { + "compute": 2, + "head": 13, + "interval": 19 + }, + "fp32": { + "compute": 2, + "head": 13, + "interval": 19 + } + }, + "VDIV": { + "fp16": { + "compute": 8, + "head": 13, + "interval": 25 + }, + "fp32": { + "compute": 4, + "head": 13, + "interval": 25 + } + }, + "VMAX": { + "fp16": { + "compute": 2, + "head": 14, + "interval": 16 + }, + "fp32": { + "compute": 2, + "head": 14, + "interval": 16 + } + }, + "VMIN": { + "fp16": { + "compute": 2, + "head": 14, + "interval": 16 + }, + "fp32": { + "compute": 2, + "head": 14, + "interval": 16 + } + }, + "VEXP": { + "fp16": { + "compute": 4, + "head": 13, + "interval": 24 + }, + "fp32": { + "compute": 2, + "head": 13, + "interval": 24 + } + }, + "VSQRT": { + "fp16": { + "compute": 2, + "head": 14, + "interval": 25 + }, + "fp32": { + "compute": 2, + "head": 14, + "interval": 25 + } + }, + "VABS": { + "fp16": { + "compute": 1, + "head": 13, + "interval": 16 + }, + "fp32": { + "compute": 1, + "head": 14, + "interval": 16 + } + }, + "RELU": { + "fp16": { + "compute": 1, + "head": 14, + "interval": 16 + }, + "fp32": { + "compute": 1, + "head": 14, + "interval": 16 + } + }, + "LOG": { + "fp16": { + "compute": 2, + "head": 26, + "interval": 14 + }, + "fp32": { + "compute": 2, + "head": 26, + "interval": 14 + } + }, + "VSEL": { + "fp16": { + "compute": 2, + "head": 13, + "interval": 13 + }, + "fp32": { + "compute": 2, + "head": 13, + "interval": 14 + } + }, + "VBRCB": { + "fp16": { + "compute": 0, + "head": 0, + "interval": 18 + }, + "fp32": { + "compute": 0, + "head": 0, + "interval": 18 + } + }, + "VCMPV_NE": { + "fp16": { + "compute": 2, + "head": 14, + "interval": 22 + }, + "fp32": { + "compute": 2, + "head": 14, + "interval": 22 + } + }, + "VCMPV_GE": { + "fp16": { + "compute": 2, + "head": 14, + "interval": 22 + }, + "fp32": { + "compute": 2, + "head": 14, + "interval": 22 + } + }, + "CMPV_EQ": { + "fp16": { + "compute": 2, + "head": 13, + "interval": 22 + }, + "fp32": { + "compute": 2, + "head": 13, + "interval": 22 + } + }, + "VCGADD": { + "fp32": { + "compute": 1, + "head": 14, + "interval": 24 + } + }, + "VCGMAX": { + "fp32": { + "compute": 1, + "head": 14, + "interval": 17 + } + }, + "VCGMIN": { + "fp32": { + "compute": 1, + "head": 14, + "interval": 17 + } + }, + "VREDUCEV2": { + "fp32": { + "compute": 14, + "head": 14, + "interval": 20 + } + }, + "VCOPY": { + "fp16": { + "compute": 1, + "head": 11, + "interval": 13 + }, + "fp32": { + "compute": 1, + "head": 11, + "interval": 13 + } + }, + "CONV_F322F16": { + "fp32": { + "compute": 1, + "head": 16, + "interval": 14 + } + }, + "CONV_F162F32": { + "fp16": { + "compute": 2, + "head": 13, + "interval": 15 + } + }, + "CONV_F322BF16": { + "fp32": { + "compute": 1, + "head": 14, + "interval": 16 + } + }, + "CONV_BF162F32": { + "fp16": { + "compute": 2, + "head": 14, + "interval": 18 + } + } + } +} diff --git a/third_party/ascend/costmodel/configs/ascend_davidv100.json b/third_party/ascend/costmodel/configs/ascend_davidv100.json new file mode 100644 index 0000000000..e0617328fe --- /dev/null +++ b/third_party/ascend/costmodel/configs/ascend_davidv100.json @@ -0,0 +1,305 @@ +{ + "$schema": "./hardware_schema.json", + "name": "Ascend davidV100", + "vendor": "Huawei", + "version": "1.0", + + "clock": { + "frequency_ghz": 1.65, + "cycles_per_us": 1650 + }, + + "memory_spaces": { + "hbm": { + "type": "off_chip", + "size_gb": 32, + "bandwidth_tbps": 1.6, + "latency_cycles": 200 + }, + "l2": { + "type": "on_chip_shared", + "size_mb": 192, + "bandwidth_gbps": 3200, + "latency_cycles": 50 + }, + "l1": { + "type": "on_chip_local", + "size_kb": 512, + "bandwidth_gbps": 6400, + "latency_cycles": 10, + "description": "Cube data staging buffer" + }, + "l0a": { + "type": "register_file", + "size_kb": 64, + "description": "Left matrix input for Cube" + }, + "l0b": { + "type": "register_file", + "size_kb": 64, + "description": "Right matrix input for Cube" + }, + "l0c": { + "type": "register_file", + "size_kb": 128, + "description": "Cube output accumulator" + }, + "ub": { + "type": "on_chip_local", + "size_kb": 512, + "bandwidth_gbps": 3200, + "latency_cycles": 5, + "description": "Unified Buffer for Vector compute" + } + }, + + "compute_units": { + "cube": { + "type": "matrix_engine", + "tflops_fp16": 448, + "tflops_fp32": 224, + "tflops_int8": 896, + "fractal_sizes": { + "fp16": { + "m": 16, + "k": 16, + "n": 16 + }, + "bf16": { + "m": 16, + "k": 16, + "n": 16 + }, + "fp32": { + "m": 16, + "k": 8, + "n": 16 + }, + "int8": { + "m": 16, + "k": 32, + "n": 16 + } + }, + "tile_m": 16, + "tile_n": 16, + "tile_k": 16, + "input_spaces": [ + "l0a", + "l0b" + ], + "output_space": "l0c", + "supported_dtypes": [ + "fp16", + "bf16", + "fp32", + "int8" + ] + }, + "vector": { + "type": "simd_engine", + "width_elements": 128, + "width_bytes": 256, + "tflops_fp32": 14, + "tflops_fp16": 28, + "compute_space": "ub", + "supported_ops": [ + "add", + "sub", + "mul", + "div", + "max", + "min", + "exp", + "log", + "sqrt", + "rsqrt", + "tanh", + "sigmoid", + "relu", + "reduce_sum", + "reduce_max", + "reduce_min", + "cast", + "broadcast" + ], + "supported_dtypes": [ + "fp32", + "fp16", + "bf16", + "int32", + "int8" + ] + } + }, + + "data_movers": { + "cube_mte2": { + "description": "Cube input: HBM to L1", + "src_space": "hbm", + "dst_space": "l1", + "bandwidth_gbps": 200, + "max_burst_bytes": 65536, + "alignment_bytes": 32 + }, + "mte1": { + "description": "Cube input: L1 to L0A/L0B", + "src_space": "l1", + "dst_spaces": [ + "l0a", + "l0b" + ], + "bandwidth_gbps": 400, + "max_burst_bytes": 32768, + "alignment_bytes": 32 + }, + "fixpipe": { + "description": "Cube output: L0C to HBM", + "src_space": "l0c", + "dst_space": "hbm", + "bandwidth_gbps": 200, + "max_burst_bytes": 65536, + "alignment_bytes": 32, + "supports_accumulate": true, + "supports_cast": true + }, + "vector_mte2": { + "description": "Vector input: HBM to UB", + "src_space": "hbm", + "dst_space": "ub", + "bandwidth_gbps": 75, + "max_burst_bytes": 65536, + "alignment_bytes": 32 + }, + "mte3": { + "description": "Vector output: UB to HBM", + "src_space": "ub", + "dst_space": "hbm", + "bandwidth_gbps": 71, + "max_burst_bytes": 65536, + "alignment_bytes": 32 + } + }, + + "pipeline": { + "cube_path": { + "stages": [ + "cube_mte2", + "mte1", + "cube", + "fixpipe" + ], + "description": "Matrix multiplication data flow", + "fully_pipelined": true + }, + "vector_path": { + "stages": [ + "vector_mte2", + "vector", + "mte3" + ], + "description": "Vector computation data flow", + "fully_pipelined": true + }, + "hardware_units": { + "cube_mte2": { + "path": "cube", + "can_overlap": true + }, + "mte1": { + "path": "cube", + "can_overlap": true + }, + "cube": { + "path": "cube", + "can_overlap": true + }, + "fixpipe": { + "path": "cube", + "can_overlap": true + }, + "vector_mte2": { + "path": "vector", + "can_overlap": true + }, + "vector": { + "path": "vector", + "can_overlap": true + }, + "mte3": { + "path": "vector", + "can_overlap": true + } + }, + "parallelism": { + "inter_path": { + "cube_path_and_vector_path": true, + "description": "Cube and Vector paths execute in parallel" + }, + "intra_path": { + "cube_mte2_mte1_cube_fixpipe": true, + "vector_mte2_vector_mte3": true, + "description": "All units within each path can execute in parallel (pipelined)" + } + } + }, + + "performance_model": { + "cube_cycles_per_tile": { + "formula": "ceil(M/16) * ceil(N/16) * ceil(K/16)", + "base_cycles": 1 + }, + "vector_cycles_per_op": { + "formula": "ceil(elements / 128)", + "base_cycles": 1 + }, + "memory_cycles": { + "formula": "bytes / bandwidth_per_cycle", + "include_latency": true + } + }, + + "calibration": { + "description": "davidV100 profile adapted from tilesim arc_config (clock=1650MHz, UB=512KB, core_config: cube=28, vector=56). Remaining model parameters follow Ascend 910B baseline and should be re-calibrated with davidV100 profiling data.", + + "startup_latencies": { + "vector_startup_cycles": 35, + "mte2_startup_cycles": 50, + "mte3_startup_cycles": 40, + "cube_startup_cycles": 20, + "fixpipe_startup_cycles": 30 + }, + + "vector_op_cycles_per_vec_instruction": { + "exp": 4, + "log": 3, + "tanh": 3, + "sigmoid": 3, + "sqrt": 2, + "rsqrt": 2, + "div": 3, + "simple_ops_add_sub_mul_etc": 1 + }, + + "scalar_overhead": { + "aiv_scalar_to_vec_ratio_steady_state": 1.27, + "aic_scalar_to_mac_ratio_steady_state": 8.4, + "aiv_vec_ratio_observed": 0.211, + "aiv_scalar_overhead_factor": 3.74 + }, + + "parallelism": { + "num_aic_cores": 28, + "num_aiv_cores": 56, + "description": "From tilesim davidV100 core_config" + }, + + "pipe_barrier": { + "cycles_per_inner_iteration": 7500 + }, + + "wave_overhead": { + "description": "Baseline wave-overhead model inherited from 910B; requires davidV100 calibration" + } + } +} diff --git a/third_party/ascend/costmodel/configs/hardware_schema.json b/third_party/ascend/costmodel/configs/hardware_schema.json new file mode 100644 index 0000000000..b663ea3a16 --- /dev/null +++ b/third_party/ascend/costmodel/configs/hardware_schema.json @@ -0,0 +1,220 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "hardware_schema.json", + "title": "TritonSim Hardware Configuration", + "description": "Schema for defining NPU/accelerator hardware parameters", + "type": "object", + "required": [ + "name", + "clock", + "memory_spaces", + "compute_units", + "data_movers" + ], + + "properties": { + "name": { + "type": "string", + "description": "Hardware name (e.g., Ascend 910B)" + }, + "vendor": { + "type": "string" + }, + "version": { + "type": "string" + }, + + "clock": { + "type": "object", + "required": [ + "frequency_ghz" + ], + "properties": { + "frequency_ghz": { + "type": "number", + "minimum": 0.1, + "description": "Core clock frequency in GHz" + }, + "cycles_per_us": { + "type": "integer", + "description": "Derived: cycles per microsecond" + } + } + }, + + "memory_spaces": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "off_chip", + "on_chip_shared", + "on_chip_local", + "register_file" + ] + }, + "size_gb": { + "type": "number" + }, + "size_mb": { + "type": "number" + }, + "size_kb": { + "type": "number" + }, + "bandwidth_tbps": { + "type": "number" + }, + "bandwidth_gbps": { + "type": "number" + }, + "latency_cycles": { + "type": "integer" + }, + "description": { + "type": "string" + } + } + } + }, + + "compute_units": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "matrix_engine", + "simd_engine", + "scalar_engine" + ] + }, + "tflops_fp16": { + "type": "number" + }, + "tflops_fp32": { + "type": "number" + }, + "tflops_int8": { + "type": "number" + }, + "tile_m": { + "type": "integer" + }, + "tile_n": { + "type": "integer" + }, + "tile_k": { + "type": "integer" + }, + "width_elements": { + "type": "integer" + }, + "width_bytes": { + "type": "integer" + }, + "input_spaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "output_space": { + "type": "string" + }, + "compute_space": { + "type": "string" + }, + "supported_ops": { + "type": "array", + "items": { + "type": "string" + } + }, + "supported_dtypes": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + + "data_movers": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "src_space" + ], + "properties": { + "description": { + "type": "string" + }, + "src_space": { + "type": "string" + }, + "dst_space": { + "type": "string" + }, + "dst_spaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "bandwidth_gbps": { + "type": "number" + }, + "max_burst_bytes": { + "type": "integer" + }, + "alignment_bytes": { + "type": "integer" + }, + "supports_accumulate": { + "type": "boolean" + }, + "supports_cast": { + "type": "boolean" + } + } + } + }, + + "pipeline": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "stages": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + } + } + } + }, + + "performance_model": { + "type": "object", + "description": "Custom performance estimation formulas" + } + } +} diff --git a/third_party/ascend/costmodel/include/AscendModel/Analysis/HIVMAnalysis.h b/third_party/ascend/costmodel/include/AscendModel/Analysis/HIVMAnalysis.h new file mode 100644 index 0000000000..6d1c055502 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/Analysis/HIVMAnalysis.h @@ -0,0 +1,119 @@ +//===- HIVMAnalysis.h - HIVM performance analysis --------------*- C++ -*-===// +// +// Optional HIVM-based analysis path for TritonSim. +// +// This analyzer consumes dumped `.npuir.mlir`, normalizes the subset of +// HIVM HIR that is relevant to performance modeling, and schedules the +// resulting execution graph on Ascend execution pipes through MLIR-native +// ingestion. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCENDMODEL_ANALYSIS_HIVMANALYSIS_H +#define ASCENDMODEL_ANALYSIS_HIVMANALYSIS_H + +#include "AscendModel/Analysis/HardwareConfig.h" + +#include "mlir/IR/BuiltinOps.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include + +namespace mlir { +namespace ascend { + +enum class HIVMSchedulerMode { Static, DES }; + +enum class HIVMPipe { + Unknown, + Vector, + VectorMTE2, + CubeMTE2, + MTE3, + Scalar, + FixPipe, + Cube, + MTE1, + All +}; + +struct HIVMOp { + size_t id = 0; + std::string opName; + std::string text; + HIVMPipe pipe = HIVMPipe::Unknown; + HIVMPipe senderPipe = HIVMPipe::Unknown; + HIVMPipe receiverPipe = HIVMPipe::Unknown; + std::string coreType; + std::string eventId; + int64_t eventGeneration = 0; + int64_t duration = 0; + int64_t bytes = 0; + int64_t elements = 0; + int64_t multiBufferSlots = 1; + int64_t startCycle = 0; + int64_t endCycle = 0; + int64_t loopMultiplier = 1; + int lineNumber = 0; + bool isSyncOp = false; + bool isBarrier = false; + std::vector readBuffers; + std::vector writeBuffers; + std::vector readBufferVersions; + std::vector writeBufferVersions; + std::vector dependsOn; +}; + +struct HIVMAnalysisReport { + std::string sourcePath; + std::string sourceMode; + HIVMSchedulerMode schedulerMode = HIVMSchedulerMode::Static; + int64_t oneIterationCycles = 0; + int64_t weightedCycles = 0; + int64_t totalBusyCycles = 0; + int64_t syncCycles = 0; + int64_t barrierCycles = 0; + size_t opCount = 0; + size_t syncOpCount = 0; + size_t barrierCount = 0; + size_t unknownOpCount = 0; + int64_t maxLoopMultiplier = 1; + std::map pipeBusyCycles; + std::map weightedPipeCycles; + std::vector operations; + + void print(llvm::raw_ostream &os, const HardwareConfig &config) const; + void emitPerfettoTrace(llvm::raw_ostream &os, + const HardwareConfig &config) const; + void emitDESGraph(llvm::raw_ostream &os, const HardwareConfig &config) const; +}; + +class HIVMAnalyzer { +public: + HIVMAnalyzer(const HardwareConfig &config, + llvm::StringRef argBindings = llvm::StringRef(), + HIVMSchedulerMode schedulerMode = HIVMSchedulerMode::Static); + + bool analyzeModule(mlir::ModuleOp module, HIVMAnalysisReport &report, + std::string &error) const; + + bool analyzeFile(llvm::StringRef path, HIVMAnalysisReport &report, + std::string &error) const; + + static llvm::StringRef stringifyPipe(HIVMPipe pipe); + static llvm::StringRef stringifySchedulerMode(HIVMSchedulerMode mode); + +private: + const HardwareConfig &config; + std::string argBindingsStr; + HIVMSchedulerMode schedulerMode; +}; + +} // namespace ascend +} // namespace mlir + +#endif // ASCENDMODEL_ANALYSIS_HIVMANALYSIS_H diff --git a/third_party/ascend/costmodel/include/AscendModel/Analysis/HardwareConfig.h b/third_party/ascend/costmodel/include/AscendModel/Analysis/HardwareConfig.h new file mode 100644 index 0000000000..c15763190a --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/Analysis/HardwareConfig.h @@ -0,0 +1,401 @@ +//===- HardwareConfig.h - Hardware Configuration Interface ------*- C++ -*-===// +// +// This file defines the HardwareConfig class for loading and querying +// hardware parameters from JSON configuration files. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCENDMODEL_HARDWARECONFIG_H +#define ASCENDMODEL_HARDWARECONFIG_H + +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/JSON.h" +#include +#include +#include +#include +#include + +namespace mlir { +namespace ascend { + +//===----------------------------------------------------------------------===// +// Memory Space +//===----------------------------------------------------------------------===// + +enum class MemoryType { + OffChip, // e.g., HBM + OnChipShared, // e.g., L2 + OnChipLocal, // e.g., L1, UB + RegisterFile // e.g., L0A, L0B, L0C +}; + +struct MemorySpace { + std::string name; + MemoryType type; + size_t sizeBytes; // Total size in bytes + double bandwidthBytesPerCycle; + int latencyCycles; + std::string description; + + // Convenience methods + double sizeKB() const { return sizeBytes / 1024.0; } + double sizeMB() const { return sizeBytes / (1024.0 * 1024.0); } + double sizeGB() const { return sizeBytes / (1024.0 * 1024.0 * 1024.0); } +}; + +//===----------------------------------------------------------------------===// +// Compute Unit +//===----------------------------------------------------------------------===// + +enum class ComputeUnitType { + MatrixEngine, // e.g., Cube + SIMDEngine, // e.g., Vector + ScalarEngine +}; + +/// Fractal (tile) dimensions for matrix operations +/// Ascend Cube Core computes C[m,n] += A[m,k] * B[k,n] +struct FractalSize { + int m = 16; // Output rows + int k = 16; // Reduction dimension + int n = 16; // Output columns +}; + +struct ComputeUnit { + std::string name; + ComputeUnitType type; + + // Performance specs + double tflopsFP16; + double tflopsFP32; + double tflopsINT8; + + // Matrix engine specific - default tile size (for backward compatibility) + int tileM, tileN, tileK; + + // Fractal sizes per data type (key: "fp16", "bf16", "fp32", "int8") + llvm::StringMap fractalSizes; + + std::vector inputSpaces; + std::string outputSpace; + + // SIMD engine specific + int widthElements; // Number of elements processed per cycle + int widthBytes; + std::string computeSpace; + + std::vector supportedOps; + std::vector supportedDtypes; +}; + +//===----------------------------------------------------------------------===// +// Data Mover +//===----------------------------------------------------------------------===// + +struct DataMover { + std::string name; + std::string description; + std::string srcSpace; + std::vector dstSpaces; + double bandwidthBytesPerCycle; + int maxBurstBytes; + int alignmentBytes; + bool supportsAccumulate; + bool supportsCast; +}; + +//===----------------------------------------------------------------------===// +// tilesim-style micro-architecture tables (migrated from tilesim 910B1) +//===----------------------------------------------------------------------===// + +/// One row of the per-(intrinsic, dtype) vector instruction cycle table. +/// Mirrors tilesim VecIntrinsics: computing/head/interval cycles per repeat. +struct VecCycleEntry { + int compute = 0; + int head = 0; + int interval = 0; +}; + +/// Small-packet bandwidth fitting coefficients (tilesim pkt_param). +/// Applied when the transfer packet size is below ``thresholdBytes``. +struct SmallPacketCoeffs { + bool enabled = false; + int thresholdBytes = 256; + // Bucket selection mirrors tilesim: <64B -> "64B", <128B -> "128B", + // <256B -> "256B". Each bucket has (a, b) fitting coefficients. + double a64 = 0, b64 = 0; + double a128 = 0, b128 = 0; + double a256 = 0, b256 = 0; +}; + +/// A bandwidth entry keyed by "src:dst" memory spaces. +/// Either core-independent (singleGbps) or per-core (degrades as more cores +/// contend for the same off-chip port, e.g. GM<->UB on 910B). +struct BandwidthTable { + bool hasPerCore = false; + double singleGbps = 0.0; // core-independent bandwidth (GB/s) + std::map perCoreGbps; // core_num -> GB/s (1..N) + SmallPacketCoeffs smallPacket; +}; + +/// Cube (GEMM) micro-architecture parameters migrated from tilesim +/// (cube_throughput, cube_repeat_cycles, L0 tile limit for best_k0). +struct CubeModelConfig { + // basic throughput per repeat: [basicM, basicK, basicN]. + // tilesim stores cube_throughput as [16, 32, 16]; the K value is a + // numerator divided by the element byte size, i.e. basicK = 32/elemBytes + // (fp16->16, fp32->8, int8->32), which equals the fractal K dimension. + int basicM = 16; + int basicKNumerator = 32; // basicK = basicKNumerator / (elemBytes) + int basicN = 16; + // repeat cycles per dtype key ("fp32","fp16","bf16","int8") + llvm::StringMap repeatCycles; + // L0 tile footprint limit (bytes) controlling best_k0 selection. + int l0TileLimitKb = 32; +}; + +//===----------------------------------------------------------------------===// +// Pipeline Stage +//===----------------------------------------------------------------------===// + +struct PipelinePath { + std::string name; + std::vector stages; + std::string description; +}; + +//===----------------------------------------------------------------------===// +// HardwareConfig +//===----------------------------------------------------------------------===// + +/// Result of a bandwidth lookup, mirroring tilesim ``lookup_bw``. +/// ``bwGBs`` is the per-(src,dst,corenum,pkt) bandwidth in GB/s using the +/// tilesim GiB convention (1 GB = 1024^3 B). ``isSmallPacket`` records +/// whether the small-packet fitting branch was taken. +struct BandwidthEntry { + double bwGBs = 0.0; + bool isSmallPacket = false; +}; + +class HardwareConfig { +public: + HardwareConfig(); + ~HardwareConfig(); + + // Factory methods + static std::unique_ptr loadFromFile(llvm::StringRef path); + static std::unique_ptr + loadFromJSON(const llvm::json::Value &json); + static std::unique_ptr getDefault910B(); + +private: + static std::unique_ptr createHardcodedDefault910B(); + +public: + // Basic info + llvm::StringRef getName() const { return name; } + llvm::StringRef getVendor() const { return vendor; } + + // Clock + double getClockFrequencyGHz() const { return clockFreqGHz; } + int getCyclesPerMicrosecond() const { + return static_cast(clockFreqGHz * 1000); + } + double cyclesToMicroseconds(int64_t cycles) const { + return static_cast(cycles) / (clockFreqGHz * 1000.0); + } + + // Memory spaces + const MemorySpace *getMemorySpace(llvm::StringRef name) const; + double getMemoryBandwidthTBps(llvm::StringRef name) const; + double getMemoryBandwidthBytesPerCycle(llvm::StringRef name) const; + int getMemoryLatencyCycles(llvm::StringRef name) const; + size_t getMemorySizeBytes(llvm::StringRef name) const; + std::vector getMemorySpaceNames() const; + + // Compute units + const ComputeUnit *getComputeUnit(llvm::StringRef name) const; + + // Cube (matrix engine) + double getCubeTFlopsFP16() const; + double getCubeTFLOPS() const; // Alias for getCubeTFlopsFP16 + void getCubeTileSize(int &m, int &n, int &k) const; + void getCubeFractalSize(int elementBits, int &m, int &n, int &k) const; + llvm::StringRef getCubeOutputSpace() const; + + // Vector (SIMD engine) + double getVectorTFlopsFP32() const; + double getVectorTFLOPS() const; // Alias for getVectorTFlopsFP32 + int getVectorWidthElements() const; + int getVectorWidthBytes() const; + llvm::StringRef getVectorComputeSpace() const; + int getVectorOpCyclesPerInstruction(llvm::StringRef opName) const; + + // HBM bandwidth (convenience) + double getHBMBandwidthGBs() const; + double getHBMBandwidthTBs() const; + + // Startup latencies (from hardware params, or reasonable defaults) + int getMTE2StartupLatency() const; + int getMTE3StartupLatency() const; + int getFixPipeStartupLatency() const; + int getCubeStartupLatency() const; + int getVectorStartupLatency() const; + + // Calibrated overhead parameters (derived from profiling _attn_fwd, + // BLOCK_M={16,32,48,64} on Ascend 910B with 20 AIC + 20 AIV cores). + // + // Scalar overhead: loop control, pointer arithmetic, and pipe_barrier + // synchronisation account for 27-36% of AIV wall time and 42-48% of AIC + // wall time. The factor below converts pure vector/MAC cycles to the + // estimated total path time: total_aiv ≈ vec_cycles * (1 + scalar_factor). + // Calibrated to aiv_vec_ratio = 0.211 in steady state → factor = 3.74. + double getAIVScalarOverheadFactor() const; + + // Number of AIC / AIV execution cores per block. + // Profiling was run with Block Dim=20 (AIC) + 20 AIV = Mix Block Dim 40. + int getNumAICCores() const; + int getNumAIVCores() const; + + // Cycles spent in pipe_barrier per inner-loop iteration (AIC↔AIV sync). + // Calibrated from the 39% idle fraction observed on AIV for BM=64, 1 wave: + // idle_cycles ≈ 23 000 cycles / 3 iterations ≈ 7 500 cycles per iteration. + int getPipeBarrierCyclesPerIter() const; + + // Data movers + const DataMover *getDataMover(llvm::StringRef name) const; + std::vector getDataMoverNames() const; + + // Performance estimation + int64_t estimateCubeCycles(int64_t M, int64_t N, int64_t K) const; + int64_t estimateVectorCycles(int64_t numElements) const; + int64_t estimateMemoryCycles(llvm::StringRef moverName, int64_t bytes) const; + int64_t estimateMemoryCyclesWithLatency(llvm::StringRef space, + int64_t bytes) const; + + //===--------------------------------------------------------------------===// + // tilesim-migrated micro-architecture queries (root cause ①/②) + //===--------------------------------------------------------------------===// + + /// Look up the per-(src,dst,corenum,pkt) bandwidth in GB/s, mirroring + /// tilesim ``ArcConfig.lookup_bw``. ``coreNum<=0`` falls back to the + /// hardware default active core count (see ``getActiveBandwidthCores``). + /// ``pktBytes>0`` enables the small-packet fitting branch when configured. + /// Unknown (src,dst) pairs fall back to the aggregate HBM bandwidth so the + /// estimate degrades gracefully instead of failing. + BandwidthEntry lookupBandwidth(llvm::StringRef src, llvm::StringRef dst, + int coreNum, int64_t pktBytes) const; + + /// Look up the per-(intrinsic,dtype) vector cycle triplet + /// {compute, head, interval}, mirroring tilesim ``lookup_vec_cycle``. + /// ``elementBits`` selects the dtype bucket; unknown (intrinsic,dtype) + /// falls back to fp32 then to a sensible per-intrinsic default. + VecCycleEntry lookupVecCycle(llvm::StringRef intrinsic, + int elementBits) const; + + /// Estimate transfer cycles for moving ``bytes`` from ``src`` to ``dst`` + /// across ``coreNum`` concurrent cores, using the tilesim GiB bandwidth + /// convention and clock frequency. Includes small-packet fitting. + int64_t estimateTransferCycles(llvm::StringRef src, llvm::StringRef dst, + int64_t bytes, int coreNum) const; + + /// Default number of concurrent cores used for bandwidth degradation of + /// vector transfers (GM<->UB). Defaults to the 910B1 saturation point (48). + int getActiveBandwidthCores() const { return activeBandwidthCores; } + void setActiveBandwidthCores(int cores) { activeBandwidthCores = cores; } + + /// Whether small-packet bandwidth fitting is enabled (tilesim + /// ``enable_small_package``). Defaults to false to match tilesim. + bool getEnableSmallPacketBw() const { return enableSmallPacketBw; } + void setEnableSmallPacketBw(bool v) { enableSmallPacketBw = v; } + + /// Whether two hardware units (by costmodel name, e.g. "vec_mte2"/"mte3") + /// are mutex partners: they share a physical pipeline and cannot execute in + /// parallel (tilesim ``MutexComponents`` / ``pipe_exclusive_config``). On + /// 910B the AIV MTE2 (vector load) and MTE3 (vector store) share one + /// pipeline. + bool areMutexUnits(llvm::StringRef a, llvm::StringRef b) const; + + // Cube (GEMM) micro-architecture: migrated from tilesim cube_config. + void getCubeModelThroughput(int elementBits, int &basicM, int &basicK, + int &basicN) const; + int getCubeModelRepeatCycles(int elementBits) const; + int getCubeModelL0TileLimitBytes() const; + + // Pipeline info + const PipelinePath *getPipelinePath(llvm::StringRef name) const; + bool canRunInParallel(llvm::StringRef path1, llvm::StringRef path2) const; + + // Validation + bool validate(std::string &error) const; + + // Debug + +private: + bool parseJSON(const llvm::json::Value &json, std::string &error); + + /// Populate the tilesim-migrated tables with 910B1 measured values + /// (hardcoded-fallback path; JSON is authoritative in production). + void populateTilesimDefaults910B(); + + std::string name; + std::string vendor; + std::string version; + + double clockFreqGHz; + + llvm::StringMap memorySpaces; + llvm::StringMap computeUnits; + llvm::StringMap dataMovers; + llvm::StringMap pipelinePaths; + llvm::StringMap vectorOpCyclesPerInstruction; + + // Parallelism info + llvm::StringMap parallelismFlags; + + // tilesim-migrated micro-architecture tables. + // Bandwidth keyed by "src:dst" (costmodel space names: hbm/l2/l1/l0a/l0b/ + // l0c/ub). Vector instruction cycles keyed by intrinsic -> dtype -> entry. + llvm::StringMap bandwidthTables; + llvm::StringMap> vecCycleTables; + CubeModelConfig cubeModel; + int activeBandwidthCores = 48; // 910B1 vec_core_num saturation point + bool enableSmallPacketBw = false; + // Global small-packet coefficients (tilesim pkt_param): a single (a,b) set + // per size bucket applied to ANY mover lookup when enableSmallPacketBw is on + // and the mover's own table has no per-table small_packet entry. tilesim + // keeps one global pkt_param dict on ArcConfig (910B1.yaml); per-table + // overrides it. + SmallPacketCoeffs globalSmallPacket; + bool hasGlobalSmallPacket = false; + // Mutex unit groups (tilesim MutexComponents): each group is a clique of + // unit-name strings that share a pipeline and cannot run in parallel. + // Default 910B: {"vec_mte2", "mte3"} (AIV MTE2<->MTE3). + std::vector> mutexGroups; +}; + +//===----------------------------------------------------------------------===// +// Global Hardware Config Access +//===----------------------------------------------------------------------===// + +/// Get the current hardware configuration. +/// Returns default 910B config if not set. +HardwareConfig &getHardwareConfig(); + +/// Set the global hardware configuration. +void setHardwareConfig(std::unique_ptr config); + +/// Load and set hardware config from file. +bool loadHardwareConfigFromFile(llvm::StringRef path, std::string &error); + +/// Load an independent hardware configuration for one analysis invocation. +/// Returns the default 910B config when path is empty. +std::shared_ptr +loadHardwareConfigForAnalysis(llvm::StringRef path, std::string &error); + +} // namespace ascend +} // namespace mlir + +#endif // ASCENDMODEL_HARDWARECONFIG_H diff --git a/third_party/ascend/costmodel/include/AscendModel/Analysis/PipelineAnalysis.h b/third_party/ascend/costmodel/include/AscendModel/Analysis/PipelineAnalysis.h new file mode 100644 index 0000000000..97e9b97588 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/Analysis/PipelineAnalysis.h @@ -0,0 +1,281 @@ +//===- PipelineAnalysis.h - Pipeline Scheduling Analysis --------*- C++ -*-===// +// +// This file declares the pipeline analysis for Ascend NPU performance modeling. +// The analysis uses HardwareConfig for configurable hardware parameters. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_ANALYSIS_PIPELINE_ANALYSIS_H +#define ASCEND_MODEL_ANALYSIS_PIPELINE_ANALYSIS_H + +#include "AscendModel/HardwareConfig.h" +#include "AscendModel/IR/AscendModelDialect.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include +#include +#include + +namespace mlir { +namespace ascend { + +//===----------------------------------------------------------------------===// +// Pipeline Stage Representation +//===----------------------------------------------------------------------===// + +/// Represents a single operation in the pipeline. +struct PipelineOp { + int64_t opId; // Unique operation ID + HWUnit hwUnit; // Hardware unit (cube, vector, mte2, etc.) + int64_t startCycle; // Scheduled start cycle + int64_t duration; // Duration in cycles + int64_t endCycle; // Computed end cycle + Operation *mlirOp; // Reference to MLIR operation + std::string opName; // Human-readable name + + // Performance attributes from HardwareConfig + int64_t bytes; // Memory transfer bytes (for MTE operations) + int64_t flops; // FLOPs (for compute operations) + + // Loop handling + int64_t loopMultiplier; // Number of times this op executes (due to loops) + + // Dependencies + llvm::SmallVector dependsOn; // Op IDs this depends on + + PipelineOp() + : opId(-1), hwUnit(HWUnit::Scalar), startCycle(0), duration(0), + endCycle(0), mlirOp(nullptr), bytes(0), flops(0), loopMultiplier(1) {} +}; + +//===----------------------------------------------------------------------===// +// Hardware Unit Pipeline +//===----------------------------------------------------------------------===// + +/// Represents the pipeline for a single hardware unit. +/// In Ascend 910B, all units are fully pipelined and can run in parallel. +class HWUnitPipeline { +public: + HWUnitPipeline(HWUnit unit) : unit(unit), currentCycle(0) {} + + /// Schedule an operation on this unit. + /// Returns the actual start time (may be later than earliestStart if unit is + /// busy). + void scheduleOp(PipelineOp &op, int64_t earliestStart); + + /// Get the next available cycle for scheduling. + int64_t getNextAvailableCycle() const { return currentCycle; } + + /// Get all scheduled operations. + const std::vector &getScheduledOps() const { + return scheduledOps; + } + + /// Get total busy cycles. + int64_t getTotalBusyCycles() const; + + /// Get utilization percentage. + double getUtilization(int64_t totalCycles) const; + + HWUnit getUnit() const { return unit; } + +private: + HWUnit unit; + int64_t currentCycle; + std::vector scheduledOps; +}; + +//===----------------------------------------------------------------------===// +// Dependency Graph +//===----------------------------------------------------------------------===// + +/// Manages operation dependencies for scheduling. +class DependencyGraph { +public: + /// Add an operation to the graph. + void addOp(int64_t opId, Operation *op); + + /// Add a dependency edge. + void addDependency(int64_t fromId, int64_t toId); + + /// Get all operations that must complete before given op. + llvm::SmallVector getDependencies(int64_t opId) const; + + /// Compute topological order for scheduling. + std::vector getTopologicalOrder() const; + + /// Check if graph has cycles. + bool hasCycle() const; + +private: + llvm::DenseMap ops; + llvm::DenseMap> edges; + llvm::DenseMap> reverseEdges; +}; + +//===----------------------------------------------------------------------===// +// Pipeline Scheduler +//===----------------------------------------------------------------------===// + +/// Main scheduler for Ascend NPU pipeline analysis. +/// Uses HardwareConfig for configurable hardware parameters. +/// +/// Key insight: In Ascend 910B, all hardware units are fully pipelined: +/// - Cube path: cube_mte2 | mte1 | cube | fixpipe can ALL run in parallel +/// - Vector path: vector_mte2 | vector | mte3 can ALL run in parallel +/// - Cube path and Vector path can run in parallel +/// +/// The only constraints are DATA DEPENDENCIES between operations. +class PipelineScheduler { +public: + /// Construct with optional hardware config (uses default 910B if null). + PipelineScheduler(const HardwareConfig *config = nullptr); + + /// Add an operation to be scheduled. + void addOperation(PipelineOp op); + + /// Add a dependency between operations. + void addDependency(int64_t fromId, int64_t toId); + + /// Perform scheduling using ASAP (As Soon As Possible) algorithm. + /// Operations are scheduled as early as possible given: + /// 1. Data dependencies (must wait for producers) + /// 2. Hardware unit availability (same unit can only run one op at a time) + bool schedule(); + + /// Get scheduled operations for a specific hardware unit. + const HWUnitPipeline &getPipeline(HWUnit unit) const; + + /// Get total execution time in cycles (single-program critical path, + /// no scalar overhead applied). + int64_t getTotalCycles() const { return totalCycles; } + + /// Get estimated kernel wall-clock cycles accounting for: + /// 1. Scalar overhead (loop control, pipe_barrier sync) via + /// HardwareConfig::getAIVScalarOverheadFactor(). + /// 2. Wave serialisation: ceil(numPrograms / numParallelUnits) waves. + /// + /// numPrograms – total programs launched (e.g. Z*H*ceil(N_CTX/BLOCK_M)). + /// numParallelUnits – hardware cores executing in parallel (e.g. N_AIV). + /// numInnerIters – inner-loop iterations per program (for pipe_barrier cost). + int64_t getKernelCycles(int64_t numPrograms, int64_t numParallelUnits, + int64_t numInnerIters = 0) const; + + /// Get all operations in scheduled order. + const std::vector &getAllOps() const { return operations; } + + /// Get hardware config. + const HardwareConfig &getConfig() const { return *hwConfig; } + + /// Print timeline visualization. + void printTimeline(llvm::raw_ostream &os) const; + + /// Print utilization report. + void printUtilizationReport(llvm::raw_ostream &os) const; + +private: + const HardwareConfig *hwConfig; + bool ownsConfig; + + std::vector operations; + DependencyGraph depGraph; + + // Pipelines for each hardware unit + std::map pipelines; + + int64_t totalCycles; + + /// Get the earliest start time considering dependencies. + int64_t getEarliestStartTime(const PipelineOp &op); + + /// Initialize hardware pipelines based on config. + void initPipelines(); +}; + +//===----------------------------------------------------------------------===// +// Performance Report +//===----------------------------------------------------------------------===// + +/// Holds the complete performance analysis results. +struct PerformanceReport { + // Hardware info + std::string hardwareName; + double clockFreqGHz; + + // Timing information + int64_t totalCycles; // Critical-path cycles (single program, no overhead) + double totalTimeUs; // Corresponding wall time + int64_t kernelTotalCycles = 0; // With scalar overhead + wave serialisation + int64_t numWaves = 0; // ceil(numPrograms / numParallelUnits) + + // Per-unit utilization + std::map unitUtilization; + std::map unitBusyCycles; + + // Bottleneck analysis + HWUnit bottleneckUnit; + double bottleneckUtilization; + + // Roofline metrics + double arithmeticIntensity; // FLOP/Byte + double achievedTFLOPS; + double peakTFLOPS; + double achievedBandwidth; // GB/s + double peakBandwidth; // GB/s (from HardwareConfig) + bool isComputeBound; + + // Detailed operation breakdown + struct OpStat { + std::string opType; + int64_t count; + int64_t totalCycles; + double percentage; + }; + std::vector opStats; + + /// Print the report. + void print(llvm::raw_ostream &os) const; + + /// Export as JSON. + std::string toJSON() const; +}; + +//===----------------------------------------------------------------------===// +// Roofline Analyzer +//===----------------------------------------------------------------------===// + +/// Performs roofline model analysis using HardwareConfig. +class RooflineAnalyzer { +public: + RooflineAnalyzer(const PipelineScheduler &scheduler); + + /// Analyze and generate performance report. + PerformanceReport analyze(); + + /// Get arithmetic intensity. + double getArithmeticIntensity() const; + + /// Check if compute-bound or memory-bound. + bool isComputeBound() const; + + /// Get theoretical peak performance. + double getTheoreticalPeak() const; + + /// Get achieved performance. + double getAchievedPerformance() const; + +private: + const PipelineScheduler &scheduler; + const HardwareConfig &config; + + int64_t totalFLOPs; + int64_t totalBytes; + + void computeMetrics(); +}; + +} // namespace ascend +} // namespace mlir + +#endif // ASCEND_MODEL_ANALYSIS_PIPELINE_ANALYSIS_H diff --git a/third_party/ascend/costmodel/include/AscendModel/Analysis/Utils.h b/third_party/ascend/costmodel/include/AscendModel/Analysis/Utils.h new file mode 100644 index 0000000000..e70e65ffcd --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/Analysis/Utils.h @@ -0,0 +1,1000 @@ +//===- Utils.h - Common utility functions for AscendModel --------*- C++ +//-*-===// +// +// This file defines common utility functions used across multiple passes. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCENDMODEL_UTILS_H +#define ASCENDMODEL_UTILS_H + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/Error.h" +#include +#include +#include + +namespace mlir { +namespace ascend { +namespace utils { + +//===----------------------------------------------------------------------===// +// Tensor/Type Utilities +//===----------------------------------------------------------------------===// + +/// Get the number of elements from a tensor type. +/// Returns 1024 for dynamic dimensions as a default estimate. +inline int64_t getNumElements(Type type) { + if (auto tensorType = dyn_cast(type)) { + int64_t count = 1; + for (int64_t dim : tensorType.getShape()) { + if (dim == ShapedType::kDynamic) + return 1024; // Default estimate for dynamic shapes + count *= dim; + } + return count; + } + return 1; +} + +/// Get the byte size of a tensor type. +/// Returns 0 if the type is not a ranked tensor or has dynamic dimensions. +inline int64_t getByteSize(Type type) { + auto tensorType = dyn_cast(type); + if (!tensorType) + return 0; + + int64_t count = 1; + for (int64_t dim : tensorType.getShape()) { + if (dim == ShapedType::kDynamic) + return 0; + count *= dim; + } + + Type elemType = tensorType.getElementType(); + int64_t elemBits = 32; // Default + if (elemType.isF16() || elemType.isBF16()) + elemBits = 16; + else if (elemType.isF32()) + elemBits = 32; + else if (elemType.isF64()) + elemBits = 64; + else if (auto intType = dyn_cast(elemType)) + elemBits = intType.getWidth(); + + return count * elemBits / 8; +} + +/// Get the element type bit width. +inline int64_t getElementBitWidth(Type type) { + if (auto tensorType = dyn_cast(type)) { + Type elemType = tensorType.getElementType(); + if (elemType.isF16() || elemType.isBF16()) + return 16; + else if (elemType.isF32()) + return 32; + else if (elemType.isF64()) + return 64; + else if (auto intType = dyn_cast(elemType)) + return intType.getWidth(); + } + return 32; // Default +} + +//===----------------------------------------------------------------------===// +// Constant Extraction Utilities +//===----------------------------------------------------------------------===// + +/// Try to extract a constant integer value from a Value. +/// Uses MLIR's matchPattern and fallback to arith.constant. +inline std::optional getConstantIntValue(Value v) { + // Try using MLIR's matchPattern for constant matching + IntegerAttr intAttr; + if (matchPattern(v, m_Constant(&intAttr))) { + return intAttr.getInt(); + } + + // Fallback: manually check for arith.constant + if (auto constOp = v.getDefiningOp()) { + if (auto attr = dyn_cast(constOp.getValue())) { + return attr.getInt(); + } + } + + return std::nullopt; +} + +//===----------------------------------------------------------------------===// +// Dynamic Loop Bound Analysis +//===----------------------------------------------------------------------===// + +/// Represents a symbolic expression for loop bounds. +/// Can be a constant, a function argument, or a simple expression. +struct SymbolicBound { + enum Kind { + Constant, // A compile-time constant + Argument, // A function argument (e.g., %arg2) + ProgramId, // A tt.get_program_id + Expression, // A computed expression depending on arguments + Unknown // Cannot be determined + }; + + Kind kind = Unknown; + int64_t constantValue = 0; // Valid if kind == Constant + unsigned argIndex = 0; // Valid if kind == Argument + std::string programIdDim; // Valid if kind == ProgramId ("x", "y", "z") + SmallVector dependentArgs; // Arguments this expression depends on + SmallVector dependentProgramIds; // Program IDs this depends on + std::string description; // Human-readable description + + bool isConstant() const { return kind == Constant; } + bool isArgument() const { return kind == Argument; } + bool isProgramId() const { return kind == ProgramId; } + bool hasDependencies() const { + return !dependentArgs.empty() || !dependentProgramIds.empty(); + } +}; + +/// Analyze a value to determine its symbolic representation. +/// Returns information about whether it's constant, an argument, or depends on +/// arguments. +inline SymbolicBound analyzeValue(Value v, Operation *funcOp = nullptr) { + SymbolicBound result; + + // Check if it's a constant + if (auto constVal = getConstantIntValue(v)) { + result.kind = SymbolicBound::Constant; + result.constantValue = *constVal; + result.description = std::to_string(*constVal); + return result; + } + + // Check if it's a block argument (function parameter) + if (auto blockArg = dyn_cast(v)) { + result.kind = SymbolicBound::Argument; + result.argIndex = blockArg.getArgNumber(); + result.dependentArgs.push_back(result.argIndex); + result.description = "%arg" + std::to_string(result.argIndex); + return result; + } + + // Check if it's computed from arguments + if (Operation *defOp = v.getDefiningOp()) { + result.kind = SymbolicBound::Expression; + + // Recursively find all argument and program_id dependencies + SmallVector worklist; + llvm::DenseSet visited; + worklist.push_back(v); + + while (!worklist.empty()) { + Value current = worklist.pop_back_val(); + if (!visited.insert(current).second) + continue; + + if (auto blockArg = dyn_cast(current)) { + result.dependentArgs.push_back(blockArg.getArgNumber()); + continue; + } + + if (Operation *op = current.getDefiningOp()) { + // Check for tt.get_program_id + if (op->getName().getStringRef() == "tt.get_program_id") { + if (auto axisAttr = op->getAttrOfType("axis")) { + int axis = axisAttr.getInt(); + std::string dim = (axis == 0) ? "x" : (axis == 1) ? "y" : "z"; + result.dependentProgramIds.push_back(dim); + } else { + // Try to get from operand or other attribute + result.dependentProgramIds.push_back("x"); // Default + } + continue; + } + + for (Value operand : op->getOperands()) { + worklist.push_back(operand); + } + } + } + + // Sort and deduplicate + llvm::sort(result.dependentArgs); + result.dependentArgs.erase( + std::unique(result.dependentArgs.begin(), result.dependentArgs.end()), + result.dependentArgs.end()); + + llvm::sort(result.dependentProgramIds); + result.dependentProgramIds.erase( + std::unique(result.dependentProgramIds.begin(), + result.dependentProgramIds.end()), + result.dependentProgramIds.end()); + + // Build description + if (result.dependentArgs.empty() && result.dependentProgramIds.empty()) { + result.kind = SymbolicBound::Unknown; + result.description = ""; + } else { + result.description = "expr("; + bool first = true; + for (unsigned arg : result.dependentArgs) { + if (!first) + result.description += ", "; + first = false; + result.description += "%arg" + std::to_string(arg); + } + for (const auto &pid : result.dependentProgramIds) { + if (!first) + result.description += ", "; + first = false; + result.description += "program_id_" + pid; + } + result.description += ")"; + } + + return result; + } + + result.kind = SymbolicBound::Unknown; + result.description = ""; + return result; +} + +/// Try to evaluate a value given argument and program_id bindings. +/// Returns std::nullopt if evaluation fails. +inline std::optional +evaluateValue(Value v, const llvm::DenseMap &argBindings, + const llvm::StringMap &programIdBindings = {}) { + + // Check if it's a constant + if (auto constVal = getConstantIntValue(v)) { + return *constVal; + } + + // Check if it's a bound argument + if (auto blockArg = dyn_cast(v)) { + auto it = argBindings.find(blockArg.getArgNumber()); + if (it != argBindings.end()) { + return it->second; + } + return std::nullopt; + } + + // Try to evaluate the defining operation + Operation *defOp = v.getDefiningOp(); + if (!defOp) + return std::nullopt; + + // Handle tt.get_program_id + if (defOp->getName().getStringRef() == "tt.get_program_id") { + std::string dim = "x"; + if (auto axisAttr = defOp->getAttrOfType("axis")) { + int axis = axisAttr.getInt(); + dim = (axis == 0) ? "x" : (axis == 1) ? "y" : "z"; + } + auto it = programIdBindings.find(dim); + if (it != programIdBindings.end()) { + return it->second; + } + // Also try "program_id_x" format + auto it2 = programIdBindings.find("program_id_" + dim); + if (it2 != programIdBindings.end()) { + return it2->second; + } + return std::nullopt; + } + + // Handle tt.get_num_programs + if (defOp->getName().getStringRef() == "tt.get_num_programs") { + std::string dim = "x"; + if (auto axisAttr = defOp->getAttrOfType("axis")) { + int axis = axisAttr.getInt(); + dim = (axis == 0) ? "x" : (axis == 1) ? "y" : "z"; + } + auto it = programIdBindings.find("num_programs_" + dim); + if (it != programIdBindings.end()) { + return it->second; + } + auto it2 = programIdBindings.find("num_programs" + dim); + if (it2 != programIdBindings.end()) { + return it2->second; + } + return std::nullopt; + } + + // Handle common arithmetic operations + if (auto addOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(addOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(addOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return *lhs + *rhs; + } else if (auto subOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(subOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(subOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return *lhs - *rhs; + } else if (auto mulOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(mulOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(mulOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return *lhs * *rhs; + } else if (auto divOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(divOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(divOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs && *rhs != 0) + return *lhs / *rhs; + } else if (auto divOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(divOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(divOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs && *rhs != 0) + return static_cast(static_cast(*lhs) / + static_cast(*rhs)); + } else if (auto minOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(minOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(minOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return std::min(*lhs, *rhs); + } else if (auto maxOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(maxOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(maxOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return std::max(*lhs, *rhs); + } else if (auto minOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(minOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(minOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return static_cast( + std::min(static_cast(*lhs), static_cast(*rhs))); + } else if (auto maxOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(maxOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(maxOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return static_cast( + std::max(static_cast(*lhs), static_cast(*rhs))); + } else if (auto extOp = dyn_cast(defOp)) { + return evaluateValue(extOp.getIn(), argBindings, programIdBindings); + } else if (auto extOp = dyn_cast(defOp)) { + return evaluateValue(extOp.getIn(), argBindings, programIdBindings); + } else if (auto truncOp = dyn_cast(defOp)) { + return evaluateValue(truncOp.getIn(), argBindings, programIdBindings); + } else if (auto remOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(remOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(remOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs && *rhs != 0) + return *lhs % *rhs; + } else if (auto remOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(remOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(remOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs && *rhs != 0) + return static_cast(static_cast(*lhs) % + static_cast(*rhs)); + } else if (auto selectOp = dyn_cast(defOp)) { + auto cond = + evaluateValue(selectOp.getCondition(), argBindings, programIdBindings); + auto trueVal = + evaluateValue(selectOp.getTrueValue(), argBindings, programIdBindings); + auto falseVal = + evaluateValue(selectOp.getFalseValue(), argBindings, programIdBindings); + if (cond && trueVal && falseVal) { + return *cond ? *trueVal : *falseVal; + } + } else if (auto cmpOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(cmpOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(cmpOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) { + switch (cmpOp.getPredicate()) { + case arith::CmpIPredicate::eq: + return *lhs == *rhs ? 1 : 0; + case arith::CmpIPredicate::ne: + return *lhs != *rhs ? 1 : 0; + case arith::CmpIPredicate::slt: + return *lhs < *rhs ? 1 : 0; + case arith::CmpIPredicate::sle: + return *lhs <= *rhs ? 1 : 0; + case arith::CmpIPredicate::sgt: + return *lhs > *rhs ? 1 : 0; + case arith::CmpIPredicate::sge: + return *lhs >= *rhs ? 1 : 0; + case arith::CmpIPredicate::ult: + return static_cast(*lhs) < static_cast(*rhs) ? 1 + : 0; + case arith::CmpIPredicate::ule: + return static_cast(*lhs) <= static_cast(*rhs) ? 1 + : 0; + case arith::CmpIPredicate::ugt: + return static_cast(*lhs) > static_cast(*rhs) ? 1 + : 0; + case arith::CmpIPredicate::uge: + return static_cast(*lhs) >= static_cast(*rhs) ? 1 + : 0; + } + } + } else if (auto andOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(andOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(andOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return *lhs & *rhs; + } else if (auto orOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(orOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(orOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return *lhs | *rhs; + } else if (auto xorOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(xorOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(xorOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return *lhs ^ *rhs; + } else if (auto shlOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(shlOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(shlOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return *lhs << *rhs; + } else if (auto shrOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(shrOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(shrOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return *lhs >> *rhs; + } else if (auto shrOp = dyn_cast(defOp)) { + auto lhs = evaluateValue(shrOp.getLhs(), argBindings, programIdBindings); + auto rhs = evaluateValue(shrOp.getRhs(), argBindings, programIdBindings); + if (lhs && rhs) + return static_cast(static_cast(*lhs) >> *rhs); + } else if (auto indexCastOp = dyn_cast(defOp)) { + return evaluateValue(indexCastOp.getIn(), argBindings, programIdBindings); + } else if (auto indexCastOp = dyn_cast(defOp)) { + return evaluateValue(indexCastOp.getIn(), argBindings, programIdBindings); + } + + return std::nullopt; +} + +//===----------------------------------------------------------------------===// +// Loop Trip Count Analysis (with dynamic bound support) +//===----------------------------------------------------------------------===// + +/// Result of loop trip count analysis. +struct LoopTripCountResult { + bool isStatic = false; // True if trip count is compile-time constant + int64_t staticTripCount = 1; // Valid if isStatic + SmallVector dependentArgs; // Arguments affecting trip count + std::string errorMsg; // Error message if analysis fails + + // Detailed bound info + SymbolicBound lowerBound; + SymbolicBound upperBound; + SymbolicBound step; +}; + +/// Analyze scf.for loop to determine trip count or required argument bindings. +inline LoopTripCountResult analyzeScfForTripCount(scf::ForOp forOp) { + LoopTripCountResult result; + + result.lowerBound = analyzeValue(forOp.getLowerBound()); + result.upperBound = analyzeValue(forOp.getUpperBound()); + result.step = analyzeValue(forOp.getStep()); + + // Collect all dependent arguments + for (unsigned arg : result.lowerBound.dependentArgs) + result.dependentArgs.push_back(arg); + for (unsigned arg : result.upperBound.dependentArgs) + result.dependentArgs.push_back(arg); + for (unsigned arg : result.step.dependentArgs) + result.dependentArgs.push_back(arg); + + // Sort and deduplicate + llvm::sort(result.dependentArgs); + result.dependentArgs.erase( + std::unique(result.dependentArgs.begin(), result.dependentArgs.end()), + result.dependentArgs.end()); + + // Check if all bounds are static + if (result.lowerBound.isConstant() && result.upperBound.isConstant() && + result.step.isConstant()) { + result.isStatic = true; + int64_t lb = result.lowerBound.constantValue; + int64_t ub = result.upperBound.constantValue; + int64_t step = result.step.constantValue; + if (step != 0 && ub > lb) { + result.staticTripCount = (ub - lb + step - 1) / step; + } else { + result.staticTripCount = 0; + } + } + + return result; +} + +/// Get trip count for scf.for loop with optional argument and program_id +/// bindings. If bindings are provided, tries to evaluate dynamic bounds. If +/// bindings are missing for required args/program_ids, returns error in result. +inline LoopTripCountResult getScfForTripCountWithBindings( + scf::ForOp forOp, + const llvm::DenseMap &argBindings = + llvm::DenseMap(), + const llvm::StringMap &programIdBindings = + llvm::StringMap()) { + + LoopTripCountResult result = analyzeScfForTripCount(forOp); + + // If already static, we're done + if (result.isStatic) { + return result; + } + + // Check if we have all required bindings + SmallVector missingBindings; + for (unsigned argIdx : result.dependentArgs) { + if (argBindings.find(argIdx) == argBindings.end()) { + missingBindings.push_back("%arg" + std::to_string(argIdx)); + } + } + for (const auto &pid : result.lowerBound.dependentProgramIds) { + if (programIdBindings.find(pid) == programIdBindings.end() && + programIdBindings.find("program_id_" + pid) == + programIdBindings.end()) { + missingBindings.push_back("pid_" + pid); + } + } + for (const auto &pid : result.upperBound.dependentProgramIds) { + if (programIdBindings.find(pid) == programIdBindings.end() && + programIdBindings.find("program_id_" + pid) == + programIdBindings.end()) { + // Avoid duplicates + std::string name = "pid_" + pid; + bool found = false; + for (const auto &m : missingBindings) { + if (m == name) { + found = true; + break; + } + } + if (!found) + missingBindings.push_back(name); + } + } + + if (!missingBindings.empty()) { + result.errorMsg = "Loop trip count depends on unbound values: "; + for (size_t i = 0; i < missingBindings.size(); ++i) { + if (i > 0) + result.errorMsg += ", "; + result.errorMsg += missingBindings[i]; + } + result.errorMsg += ". Please provide via --arg-bindings option."; + return result; + } + + // Try to evaluate with bindings + auto lbVal = + evaluateValue(forOp.getLowerBound(), argBindings, programIdBindings); + auto ubVal = + evaluateValue(forOp.getUpperBound(), argBindings, programIdBindings); + auto stepVal = evaluateValue(forOp.getStep(), argBindings, programIdBindings); + + if (lbVal && ubVal && stepVal && *stepVal != 0) { + result.isStatic = true; + int64_t lb = *lbVal; + int64_t ub = *ubVal; + int64_t step = *stepVal; + if (ub > lb) { + result.staticTripCount = (ub - lb + step - 1) / step; + } else { + result.staticTripCount = 0; + } + result.errorMsg.clear(); + } else { + result.errorMsg = "Failed to evaluate loop bounds with provided bindings"; + } + + return result; +} + +/// Simple wrapper that returns trip count (1 if unknown). +/// For backwards compatibility. +inline int64_t getScfForTripCount(scf::ForOp forOp) { + auto result = analyzeScfForTripCount(forOp); + return result.isStatic ? result.staticTripCount : 1; +} + +/// Get the loop multiplier for an operation by walking up the parent chain. +/// This handles nested loops correctly by multiplying all enclosing loop trip +/// counts. +inline int64_t getLoopMultiplier(Operation *op) { + int64_t multiplier = 1; + Operation *parent = op->getParentOp(); + + while (parent) { + if (auto forOp = dyn_cast(parent)) { + // Check if trip_count was already computed and stored + if (auto tripCountAttr = + forOp->getAttrOfType("ascend.trip_count")) { + multiplier *= tripCountAttr.getInt(); + } else { + multiplier *= getScfForTripCount(forOp); + } + } + parent = parent->getParentOp(); + } + + return multiplier; +} + +/// Check if an operation is inside any loop. +inline bool isInsideLoop(Operation *op) { + return op->getParentOfType() != nullptr; +} + +/// Get the innermost enclosing loop for an operation. +inline scf::ForOp getInnermostLoop(Operation *op) { + return op->getParentOfType(); +} + +/// Get all enclosing loops for an operation (innermost first). +inline SmallVector getEnclosingLoops(Operation *op) { + SmallVector loops; + Operation *parent = op->getParentOp(); + + while (parent) { + if (auto forOp = dyn_cast(parent)) { + loops.push_back(forOp); + } + parent = parent->getParentOp(); + } + + return loops; +} + +//===----------------------------------------------------------------------===// +// Argument Binding Parsing (Generic Programming) +//===----------------------------------------------------------------------===// + +/// Generic binding value that can hold different types. +/// Uses std::variant for type-safe value storage. +class BindingValue { +public: + using ValueType = std::variant; + +private: + ValueType value_; + +public: + BindingValue() : value_(int64_t(0)) {} + BindingValue(int64_t v) : value_(v) {} + BindingValue(double v) : value_(v) {} + BindingValue(const std::string &v) : value_(v) {} + BindingValue(llvm::StringRef v) : value_(v.str()) {} + + /// Check type + template bool is() const { + return std::holds_alternative(value_); + } + + bool isInt() const { return is(); } + bool isFloat() const { return is(); } + bool isString() const { return is(); } + + /// Get value with type conversion + template T get() const { + if constexpr (std::is_same_v) { + if (auto *v = std::get_if(&value_)) + return *v; + if (auto *v = std::get_if(&value_)) + return static_cast(*v); + return 0; + } else if constexpr (std::is_same_v) { + if (auto *v = std::get_if(&value_)) + return *v; + if (auto *v = std::get_if(&value_)) + return static_cast(*v); + return 0.0; + } else if constexpr (std::is_same_v) { + if (auto *v = std::get_if(&value_)) + return *v; + if (auto *v = std::get_if(&value_)) + return std::to_string(*v); + if (auto *v = std::get_if(&value_)) + return std::to_string(*v); + return ""; + } else { + static_assert(sizeof(T) == 0, "Unsupported type for BindingValue::get()"); + } + } + + /// Convenience accessors + int64_t asInt() const { return get(); } + double asFloat() const { return get(); } + std::string asString() const { return get(); } + + /// Convert to string representation + std::string toString() const { + return std::visit( + [](auto &&v) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return v; + } else { + return std::to_string(v); + } + }, + value_); + } +}; + +/// Generic bindings container with type-safe access. +class Bindings { +public: + using MapType = llvm::StringMap; + +private: + MapType bindings_; + +public: + Bindings() = default; + + /// Set a binding + template void set(llvm::StringRef name, T value) { + bindings_[name] = BindingValue(value); + } + + /// Get a binding with type conversion + template std::optional get(llvm::StringRef name) const { + auto it = bindings_.find(name); + if (it == bindings_.end()) + return std::nullopt; + return it->second.get(); + } + + /// Get raw BindingValue + std::optional getValue(llvm::StringRef name) const { + auto it = bindings_.find(name); + if (it == bindings_.end()) + return std::nullopt; + return it->second; + } + + /// Check if binding exists + bool has(llvm::StringRef name) const { + return bindings_.find(name) != bindings_.end(); + } + + /// Get with default value + template T getOr(llvm::StringRef name, T defaultValue) const { + auto it = bindings_.find(name); + if (it == bindings_.end()) + return defaultValue; + return it->second.get(); + } + + /// Iterate over all bindings + auto begin() const { return bindings_.begin(); } + auto end() const { return bindings_.end(); } + size_t size() const { return bindings_.size(); } + bool empty() const { return bindings_.empty(); } + + /// Get argument binding (by index) + std::optional getArg(unsigned idx) const { + return get("arg" + std::to_string(idx)); + } + + /// Get program_id binding + std::optional getProgramId(llvm::StringRef dim) const { + // Try multiple formats + if (auto v = get(dim)) + return v; + if (auto v = get("pid_" + dim.str())) + return v; + if (auto v = get("program_id_" + dim.str())) + return v; + return std::nullopt; + } + + /// Export to legacy formats for backwards compatibility + void exportArgBindings(llvm::DenseMap &out) const { + for (const auto &kv : bindings_) { + llvm::StringRef key = kv.first(); + if (key.starts_with("arg")) { + unsigned idx; + if (!key.substr(3).getAsInteger(10, idx)) { + out[idx] = kv.second.asInt(); + } + } + } + } + + void exportProgramIdBindings(llvm::StringMap &out) const { + for (const auto &kv : bindings_) { + llvm::StringRef key = kv.first(); + if (key.starts_with("pid_") || key.starts_with("program_id_") || + key.starts_with("num_programs") || key == "x" || key == "y" || + key == "z") { + out[key] = kv.second.asInt(); + } + } + } +}; + +/// Parse bindings from string format. +/// Supports: "arg2=100,arg3=1.5,pid_x=0,scale=0.125,name=hello" +/// Returns Bindings object on success, error message on failure. +inline llvm::Expected parseBindings(llvm::StringRef input) { + Bindings result; + + if (input.empty()) + return result; + + SmallVector pairs; + input.split(pairs, ','); + + for (StringRef pair : pairs) { + pair = pair.trim(); + if (pair.empty()) + continue; + + size_t eqPos = pair.find('='); + if (eqPos == StringRef::npos) { + return llvm::createStringError( + std::errc::invalid_argument, + "Invalid binding format: '%s' (expected 'key=value')", + pair.str().c_str()); + } + + StringRef key = pair.substr(0, eqPos).trim(); + StringRef val = pair.substr(eqPos + 1).trim(); + + // Determine value type and parse + BindingValue bindingVal; + + // Try integer first + int64_t intVal; + if (!val.getAsInteger(10, intVal)) { + bindingVal = BindingValue(intVal); + } + // Try float (contains '.', 'e', or 'E') + else if (val.contains('.') || val.contains('e') || val.contains('E')) { + std::string valStr = val.str(); + char *endPtr = nullptr; + double floatVal = std::strtod(valStr.c_str(), &endPtr); + if (endPtr != valStr.c_str() && *endPtr == '\0') { + bindingVal = BindingValue(floatVal); + } else { + return llvm::createStringError(std::errc::invalid_argument, + "Invalid float value: '%s'", + val.str().c_str()); + } + } + // Treat as string + else { + bindingVal = BindingValue(val.str()); + } + + // Normalize key and store + std::string normalizedKey = key.str(); + result.set(normalizedKey, bindingVal); + + // Also store normalized versions for common patterns + if (key.starts_with("arg")) { + // Already in correct format + } else if (key[0] >= '0' && key[0] <= '9') { + // Numeric key -> argN + result.set("arg" + key.str(), bindingVal); + } else if (key == "pid_x" || key == "pidx" || key == "program_id_x") { + result.set("x", bindingVal); + result.set("pid_x", bindingVal); + result.set("program_id_x", bindingVal); + } else if (key == "pid_y" || key == "pidy" || key == "program_id_y") { + result.set("y", bindingVal); + result.set("pid_y", bindingVal); + result.set("program_id_y", bindingVal); + } else if (key == "pid_z" || key == "pidz" || key == "program_id_z") { + result.set("z", bindingVal); + result.set("pid_z", bindingVal); + result.set("program_id_z", bindingVal); + } else if (key == "num_programs_x" || key == "num_programsx") { + result.set("num_programs_x", bindingVal); + result.set("num_programsx", bindingVal); + } else if (key == "num_programs_y" || key == "num_programsy") { + result.set("num_programs_y", bindingVal); + result.set("num_programsy", bindingVal); + } else if (key == "num_programs_z" || key == "num_programsz") { + result.set("num_programs_z", bindingVal); + result.set("num_programsz", bindingVal); + } + } + + return result; +} + +/// Legacy parseBindings for backwards compatibility. +inline bool parseBindings(llvm::StringRef input, + llvm::DenseMap &argBindings, + llvm::StringMap &programIdBindings, + std::string &error) { + auto result = parseBindings(input); + if (!result) { + error = llvm::toString(result.takeError()); + return false; + } + + result->exportArgBindings(argBindings); + result->exportProgramIdBindings(programIdBindings); + return true; +} + +/// Backwards-compatible wrapper that only parses arg bindings. +inline bool parseArgBindings(llvm::StringRef input, + llvm::DenseMap &bindings, + std::string &error) { + llvm::StringMap programIdBindings; // Ignored + return parseBindings(input, bindings, programIdBindings, error); +} + +/// Parse loop trip counts from string format: "4,100,6588" (in loop order) +/// Returns true on success, false on parse error. +inline bool parseLoopTripCounts(llvm::StringRef input, + SmallVector &tripCounts, + std::string &error) { + if (input.empty()) + return true; + + SmallVector parts; + input.split(parts, ','); + + for (StringRef part : parts) { + part = part.trim(); + if (part.empty()) + continue; + + int64_t value; + if (part.getAsInteger(10, value)) { + error = "Invalid trip count: " + part.str(); + return false; + } + + if (value <= 0) { + error = "Trip count must be positive: " + part.str(); + return false; + } + + tripCounts.push_back(value); + } + + return true; +} + +//===----------------------------------------------------------------------===// +// Operation Classification +//===----------------------------------------------------------------------===// + +/// Compute path classification for operations. +enum class ComputePath { + Cube, // Matrix operations: matmul + Vector, // SIMD operations: elementwise, reduce, etc. + Memory, // Memory operations: load/store + Unknown +}; + +/// Get a string name for ComputePath. +inline StringRef getComputePathName(ComputePath path) { + switch (path) { + case ComputePath::Cube: + return "Cube"; + case ComputePath::Vector: + return "Vector"; + case ComputePath::Memory: + return "Memory"; + default: + return "Unknown"; + } +} + +} // namespace utils +} // namespace ascend +} // namespace mlir + +#endif // ASCENDMODEL_UTILS_H diff --git a/third_party/ascend/costmodel/include/AscendModel/CMakeLists.txt b/third_party/ascend/costmodel/include/AscendModel/CMakeLists.txt new file mode 100644 index 0000000000..6421d98529 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/CMakeLists.txt @@ -0,0 +1,31 @@ +# Add IR/ subdirectory to TableGen include path so .td files can find siblings +set(LLVM_TABLEGEN_FLAGS "-I${CMAKE_CURRENT_SOURCE_DIR}/IR") + +# TableGen for interfaces (must come before ops since ops depend on interfaces) +set(LLVM_TARGET_DEFINITIONS IR/AscendModelInterfaces.td) +mlir_tablegen(AscendModel/IR/AscendModelInterfaces.h.inc -gen-op-interface-decls) +mlir_tablegen(AscendModel/IR/AscendModelInterfaces.cpp.inc -gen-op-interface-defs) +add_public_tablegen_target(AscendModelInterfacesIncGen) + +# TableGen for dialect and ops +set(LLVM_TARGET_DEFINITIONS IR/AscendModelOps.td) +mlir_tablegen(AscendModel/IR/AscendModelOps.h.inc -gen-op-decls) +mlir_tablegen(AscendModel/IR/AscendModelOps.cpp.inc -gen-op-defs) +mlir_tablegen(AscendModel/IR/AscendModelOpsEnums.h.inc -gen-enum-decls) +mlir_tablegen(AscendModel/IR/AscendModelOpsEnums.cpp.inc -gen-enum-defs) +mlir_tablegen(AscendModel/IR/AscendModelDialect.h.inc -gen-dialect-decls) +mlir_tablegen(AscendModel/IR/AscendModelDialect.cpp.inc -gen-dialect-defs) +mlir_tablegen(AscendModel/IR/AscendModelOpsAttrDefs.h.inc -gen-attrdef-decls) +mlir_tablegen(AscendModel/IR/AscendModelOpsAttrDefs.cpp.inc -gen-attrdef-defs) +mlir_tablegen(AscendModel/IR/AscendModelOpsTypes.h.inc -gen-typedef-decls) +mlir_tablegen(AscendModel/IR/AscendModelOpsTypes.cpp.inc -gen-typedef-defs) +add_public_tablegen_target(AscendModelOpsIncGen) +add_dependencies(AscendModelOpsIncGen AscendModelInterfacesIncGen) + +# TableGen for passes +set(LLVM_TARGET_DEFINITIONS Transforms/Passes.td) +mlir_tablegen(AscendModel/Transforms/Passes.h.inc -gen-pass-decls -name AscendModel) +add_public_tablegen_target(AscendModelTransformsPassIncGen) + +# Reset flags +set(LLVM_TABLEGEN_FLAGS "") diff --git a/third_party/ascend/costmodel/include/AscendModel/HardwareConfig.h b/third_party/ascend/costmodel/include/AscendModel/HardwareConfig.h new file mode 100644 index 0000000000..c86b363a94 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/HardwareConfig.h @@ -0,0 +1,3 @@ +// Forwarding header — actual implementation in Analysis/HardwareConfig.h +#pragma once +#include "AscendModel/Analysis/HardwareConfig.h" diff --git a/third_party/ascend/costmodel/include/AscendModel/HardwareParams.h.in b/third_party/ascend/costmodel/include/AscendModel/HardwareParams.h.in new file mode 100644 index 0000000000..b27bbe26a3 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/HardwareParams.h.in @@ -0,0 +1,31 @@ +//===- HardwareParams.h - Ascend Hardware Parameters (Generated) ----------===// +// +// This file is generated by CMake. Do not edit directly. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_HARDWARE_PARAMS_H +#define ASCEND_MODEL_HARDWARE_PARAMS_H + +namespace mlir { +namespace ascend { + +// Cube Core Parameters +constexpr double CUBE_TFLOPS = @ASCEND_CUBE_TFLOPS@; +constexpr int CUBE_M = @ASCEND_CUBE_M@; +constexpr int CUBE_N = @ASCEND_CUBE_N@; +constexpr int CUBE_K = @ASCEND_CUBE_K@; + +// Vector Core Parameters +constexpr double VECTOR_TFLOPS = @ASCEND_VECTOR_TFLOPS@; +constexpr int VECTOR_WIDTH = @ASCEND_VECTOR_WIDTH@; + +// Memory Parameters +constexpr double HBM_BANDWIDTH_GBS = @ASCEND_HBM_BW@; +constexpr int L2_SIZE_MB = @ASCEND_L2_SIZE@; +constexpr int L1_SIZE_KB = @ASCEND_L1_SIZE@; + +} // namespace ascend +} // namespace mlir + +#endif // ASCEND_MODEL_HARDWARE_PARAMS_H diff --git a/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelBase.td b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelBase.td new file mode 100644 index 0000000000..365880dd94 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelBase.td @@ -0,0 +1,133 @@ +//===- AscendModelBase.td - Base definitions for AscendModel dialect ------===// +// +// Defines the AscendModel dialect for Ascend 910B hardware modeling. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_BASE_TD +#define ASCEND_MODEL_BASE_TD + +include "mlir/IR/OpBase.td" +include "mlir/IR/AttrTypeBase.td" +include "mlir/IR/EnumAttr.td" +include "mlir/Interfaces/SideEffectInterfaces.td" +include "mlir/Interfaces/InferTypeOpInterface.td" + +//===----------------------------------------------------------------------===// +// AscendModel Dialect Definition +//===----------------------------------------------------------------------===// + +def AscendModel_Dialect : Dialect { + let name = "ascend"; + let summary = "Ascend 910B Hardware Modeling Dialect"; + let description = [{ + This dialect provides operations for modeling Ascend 910B NPU hardware + behavior and performance characteristics. It supports: + + - Cube Core operations (matrix multiply) + - Vector Core operations (elementwise, reduction, etc.) + - Memory transfer operations (MTE2, MTE3, FixPipe) + - Pipeline modeling and scheduling + + The dialect is designed for performance analysis and roofline modeling. + }]; + + let cppNamespace = "::mlir::ascend"; +} + +//===----------------------------------------------------------------------===// +// Hardware Unit Enum +//===----------------------------------------------------------------------===// + +def HWUnit_Cube : I32EnumAttrCase<"Cube", 0, "cube">; +def HWUnit_CubeMTE2 : I32EnumAttrCase<"CubeMTE2", 1, "cube_mte2">; +def HWUnit_FixPipe : I32EnumAttrCase<"FixPipe", 2, "fixpipe">; +def HWUnit_Vector : I32EnumAttrCase<"Vector", 3, "vector">; +def HWUnit_VecMTE2 : I32EnumAttrCase<"VecMTE2", 4, "vec_mte2">; +def HWUnit_MTE3 : I32EnumAttrCase<"MTE3", 5, "mte3">; +def HWUnit_Scalar : I32EnumAttrCase<"Scalar", 6, "scalar">; + +def HWUnitAttr : I32EnumAttr<"HWUnit", "Hardware execution unit", [ + HWUnit_Cube, HWUnit_CubeMTE2, HWUnit_FixPipe, + HWUnit_Vector, HWUnit_VecMTE2, HWUnit_MTE3, HWUnit_Scalar +]> { + let cppNamespace = "::mlir::ascend"; +} + +//===----------------------------------------------------------------------===// +// Memory Space Enum +//===----------------------------------------------------------------------===// + +def MemSpace_HBM : I32EnumAttrCase<"HBM", 0, "hbm">; +def MemSpace_L2 : I32EnumAttrCase<"L2", 1, "l2">; +def MemSpace_L1 : I32EnumAttrCase<"L1", 2, "l1">; +def MemSpace_UB : I32EnumAttrCase<"UB", 3, "ub">; // Unified Buffer + +def MemSpaceAttr : I32EnumAttr<"MemSpace", "Memory space", [ + MemSpace_HBM, MemSpace_L2, MemSpace_L1, MemSpace_UB +]> { + let cppNamespace = "::mlir::ascend"; +} + +//===----------------------------------------------------------------------===// +// Data Type for Operations +//===----------------------------------------------------------------------===// + +def DataType_FP32 : I32EnumAttrCase<"FP32", 0, "fp32">; +def DataType_FP16 : I32EnumAttrCase<"FP16", 1, "fp16">; +def DataType_BF16 : I32EnumAttrCase<"BF16", 2, "bf16">; +def DataType_INT8 : I32EnumAttrCase<"INT8", 3, "int8">; +def DataType_INT32 : I32EnumAttrCase<"INT32", 4, "int32">; + +def DataTypeAttr : I32EnumAttr<"DataType", "Data type for operations", [ + DataType_FP32, DataType_FP16, DataType_BF16, DataType_INT8, DataType_INT32 +]> { + let cppNamespace = "::mlir::ascend"; +} + +//===----------------------------------------------------------------------===// +// Vector Operation Kind +//===----------------------------------------------------------------------===// + +def VecOpKind_Add : I32EnumAttrCase<"Add", 0, "add">; +def VecOpKind_Sub : I32EnumAttrCase<"Sub", 1, "sub">; +def VecOpKind_Mul : I32EnumAttrCase<"Mul", 2, "mul">; +def VecOpKind_Div : I32EnumAttrCase<"Div", 3, "div">; +def VecOpKind_Exp : I32EnumAttrCase<"Exp", 4, "exp">; +def VecOpKind_Log : I32EnumAttrCase<"Log", 5, "log">; +def VecOpKind_Max : I32EnumAttrCase<"Max", 6, "max">; +def VecOpKind_Min : I32EnumAttrCase<"Min", 7, "min">; +def VecOpKind_Select : I32EnumAttrCase<"Select", 8, "select">; +def VecOpKind_Cast : I32EnumAttrCase<"Cast", 9, "cast">; + +def VecOpKindAttr : I32EnumAttr<"VecOpKind", "Vector operation kind", [ + VecOpKind_Add, VecOpKind_Sub, VecOpKind_Mul, VecOpKind_Div, + VecOpKind_Exp, VecOpKind_Log, VecOpKind_Max, VecOpKind_Min, + VecOpKind_Select, VecOpKind_Cast +]> { + let cppNamespace = "::mlir::ascend"; +} + +//===----------------------------------------------------------------------===// +// Reduction Kind +//===----------------------------------------------------------------------===// + +def ReduceKind_Sum : I32EnumAttrCase<"Sum", 0, "sum">; +def ReduceKind_Max : I32EnumAttrCase<"Max", 1, "max">; +def ReduceKind_Min : I32EnumAttrCase<"Min", 2, "min">; +def ReduceKind_Prod : I32EnumAttrCase<"Prod", 3, "prod">; + +def ReduceKindAttr : I32EnumAttr<"ReduceKind", "Reduction kind", [ + ReduceKind_Sum, ReduceKind_Max, ReduceKind_Min, ReduceKind_Prod +]> { + let cppNamespace = "::mlir::ascend"; +} + +//===----------------------------------------------------------------------===// +// Base Operation Class +//===----------------------------------------------------------------------===// + +class AscendModel_Op traits = []> : + Op; + +#endif // ASCEND_MODEL_BASE_TD diff --git a/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelDialect.h b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelDialect.h new file mode 100644 index 0000000000..cca4e24b81 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelDialect.h @@ -0,0 +1,45 @@ +//===- AscendModelDialect.h - AscendModel dialect definition -----*- C++ +//-*-===// +// +// This file defines the AscendModel dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_DIALECT_H +#define ASCEND_MODEL_DIALECT_H + +#include "mlir/Bytecode/BytecodeOpInterface.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Dialect.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/OpImplementation.h" +#include "mlir/Interfaces/InferTypeOpInterface.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" + +// Forward declarations +namespace mlir { +namespace ascend { +class HardwareConfig; +} // namespace ascend +} // namespace mlir + +// Include interface header (contains include guard for .inc) +#include "AscendModel/IR/AscendModelInterfaces.h" + +// Include generated dialect declaration +#include "AscendModel/IR/AscendModelDialect.h.inc" + +// Include generated enum declarations +#include "AscendModel/IR/AscendModelOpsEnums.h.inc" + +#define GET_ATTRDEF_CLASSES +#include "AscendModel/IR/AscendModelOpsAttrDefs.h.inc" + +#define GET_TYPEDEF_CLASSES +#include "AscendModel/IR/AscendModelOpsTypes.h.inc" + +// Include generated op declarations +#define GET_OP_CLASSES +#include "AscendModel/IR/AscendModelOps.h.inc" + +#endif // ASCEND_MODEL_DIALECT_H diff --git a/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelInterfaces.h b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelInterfaces.h new file mode 100644 index 0000000000..e2c8b92ecc --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelInterfaces.h @@ -0,0 +1,30 @@ +//===- AscendModelInterfaces.h - AscendModel Op Interfaces -------*- C++ +//-*-===// +// +// Defines operation interfaces for the AscendModel dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_IR_INTERFACES_H +#define ASCEND_MODEL_IR_INTERFACES_H + +#include "mlir/IR/OpDefinition.h" + +namespace mlir { +namespace ascend { + +// Forward declarations +class HardwareConfig; +enum class HWUnit : uint32_t; + +} // namespace ascend +} // namespace mlir + +/// Include the generated interface declarations. +/// Use include guard to prevent multiple inclusion. +#ifndef ASCEND_MODEL_INTERFACES_INC_ +#define ASCEND_MODEL_INTERFACES_INC_ +#include "AscendModel/IR/AscendModelInterfaces.h.inc" +#endif // ASCEND_MODEL_INTERFACES_INC_ + +#endif // ASCEND_MODEL_IR_INTERFACES_H diff --git a/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelInterfaces.td b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelInterfaces.td new file mode 100644 index 0000000000..69a01dbafd --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelInterfaces.td @@ -0,0 +1,131 @@ +//===- AscendModelInterfaces.td - AscendModel Op Interfaces ----*- tablegen -*-===// +// +// Defines operation interfaces for the AscendModel dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_INTERFACES_TD +#define ASCEND_MODEL_INTERFACES_TD + +include "mlir/IR/OpBase.td" + +//===----------------------------------------------------------------------===// +// EstimateCyclesOpInterface +//===----------------------------------------------------------------------===// + +def EstimateCyclesOpInterface : OpInterface<"EstimateCyclesOpInterface"> { + let description = [{ + Interface for operations that can estimate their execution cycles + on Ascend NPU hardware. + + Operations implementing this interface provide: + - Cycle estimation based on hardware configuration + - Hardware unit assignment (Cube, Vector, MTE, etc.) + - Transfer bytes (for memory operations) + - FLOP count (for compute operations) + + Example implementation: + ```cpp + int64_t MyOp::estimateCycles(const HardwareConfig &config) { + int64_t numElements = getNumElements(); + return utils::estimateVectorCycles(numElements, 1, 16, config.getVectorStartupLatency()); + } + ``` + }]; + + let cppNamespace = "::mlir::ascend"; + + let methods = [ + InterfaceMethod< + /*desc=*/[{ + Estimate the execution cycles for this operation given hardware config. + Returns 0 if cycles cannot be estimated. + }], + /*retTy=*/"int64_t", + /*methodName=*/"estimateCycles", + /*args=*/(ins "const HardwareConfig &":$config) + >, + InterfaceMethod< + /*desc=*/[{ + Get the hardware unit this operation executes on. + }], + /*retTy=*/"HWUnit", + /*methodName=*/"getHWUnit", + /*args=*/(ins) + >, + InterfaceMethod< + /*desc=*/[{ + Get the number of bytes transferred (for memory operations). + Returns 0 for non-memory operations. + }], + /*retTy=*/"int64_t", + /*methodName=*/"getTransferBytes", + /*args=*/(ins), + /*methodBody=*/"", + /*defaultImplementation=*/"return 0;" + >, + InterfaceMethod< + /*desc=*/[{ + Get the number of floating-point operations. + Returns 0 for non-compute operations. + }], + /*retTy=*/"int64_t", + /*methodName=*/"getFlops", + /*args=*/(ins), + /*methodBody=*/"", + /*defaultImplementation=*/"return 0;" + >, + InterfaceMethod< + /*desc=*/[{ + Get the cycles per vector operation (for vector ops). + 1 for simple ops (add, mul), 2-4 for complex ops (exp, log). + }], + /*retTy=*/"int", + /*methodName=*/"getCyclesPerVectorOp", + /*args=*/(ins), + /*methodBody=*/"", + /*defaultImplementation=*/"return 1;" + >, + InterfaceMethod< + /*desc=*/[{ + Get the source memory space for a transfer operation, used to select + the per-(src,dst,corenum) bandwidth (tilesim migration, root cause 1). + Transfer ops override this from their optional ``src_space`` attribute; + the default ("hbm") preserves legacy behaviour when absent. Named + ``getSrcMemSpace`` (not ``getSrcSpace``) to avoid colliding with the + MLIR auto-generated ``StrAttr`` value accessor ``getSrcSpace()``. + }], + /*retTy=*/"llvm::StringRef", + /*methodName=*/"getSrcMemSpace", + /*args=*/(ins), + /*methodBody=*/"", + /*defaultImplementation=*/[{ + return "hbm"; + }] + >, + InterfaceMethod< + /*desc=*/[{ + Get the destination memory space for a transfer operation. + Transfer ops override this; default "ub" preserves legacy behaviour. + Named ``getDstMemSpace`` to avoid the ``getDstSpace()`` attr collision. + }], + /*retTy=*/"llvm::StringRef", + /*methodName=*/"getDstMemSpace", + /*args=*/(ins), + /*methodBody=*/"", + /*defaultImplementation=*/[{ + return "ub"; + }] + > + ]; + + let extraClassDeclaration = [{ + /// Helper to get element bit width from first operand. + int getElementBits(); + + /// Helper to get number of elements from first operand. + int64_t getNumElements(); + }]; +} + +#endif // ASCEND_MODEL_INTERFACES_TD diff --git a/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelOps.td b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelOps.td new file mode 100644 index 0000000000..654990ac29 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/IR/AscendModelOps.td @@ -0,0 +1,443 @@ +//===- AscendModelOps.td - AscendModel dialect operations -----------------===// +// +// Defines operations for the AscendModel dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_OPS_TD +#define ASCEND_MODEL_OPS_TD + +include "AscendModelBase.td" +include "AscendModelInterfaces.td" +include "mlir/Interfaces/CallInterfaces.td" +include "mlir/Interfaces/ControlFlowInterfaces.td" + +//===----------------------------------------------------------------------===// +// Cube Core Operations (矩阵计算) +//===----------------------------------------------------------------------===// + +def Ascend_MatmulOp : AscendModel_Op<"matmul", [Pure, DeclareOpInterfaceMethods]> { + let summary = "Matrix multiplication on Cube Core"; + let description = [{ + Performs matrix multiplication C = A @ B on the Cube Core. + This operation is the primary compute-bound operation on Ascend 910B. + The Cube Core executes in 16x16x16 blocks for optimal performance. + }]; + + let arguments = (ins + AnyTensor:$lhs, + AnyTensor:$rhs, + I64Attr:$M, + I64Attr:$N, + I64Attr:$K, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyTensor:$result); + + let assemblyFormat = [{ + $lhs `,` $rhs attr-dict `:` `(` type($lhs) `,` type($rhs) `)` `->` type($result) + }]; +} + +//===----------------------------------------------------------------------===// +// Memory Transfer Operations +//===----------------------------------------------------------------------===// + +def Ascend_CubeLoadOp : AscendModel_Op<"cube_load", [DeclareOpInterfaceMethods]> { + let summary = "Load data for Cube Core via MTE2"; + let description = [{ + Loads tensor data from memory to Cube Core's local buffer via MTE2. + ``src_space``/``dst_space`` select the per-(src,dst,corenum) bandwidth + (tilesim migration). Defaults: hbm -> l1. + }]; + + let arguments = (ins + AnyType:$source, + I64Attr:$bytes, + OptionalAttr:$src_space, + OptionalAttr:$dst_space, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyType:$result); + + let assemblyFormat = [{ + $source attr-dict `:` type($source) `->` type($result) + }]; +} + +def Ascend_CubeStoreOp : AscendModel_Op<"cube_store", [DeclareOpInterfaceMethods]> { + let summary = "Store Cube Core result via FixPipe"; + let description = [{ + Stores Cube Core computation results to memory via FixPipe. + ``src_space``/``dst_space`` select the bandwidth (tilesim migration). + Defaults: l0c -> hbm. + }]; + + let arguments = (ins + AnyType:$data, + I64Attr:$bytes, + OptionalAttr:$src_space, + OptionalAttr:$dst_space, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs); + + let assemblyFormat = [{ + $data attr-dict `:` type($data) + }]; +} + +def Ascend_VectorLoadOp : AscendModel_Op<"vector_load", [DeclareOpInterfaceMethods]> { + let summary = "Load data for Vector Core via VecMTE2"; + let description = [{ + Loads tensor data for Vector Core operations via dedicated MTE2. + ``src_space``/``dst_space`` select the per-(src,dst,corenum) bandwidth + (tilesim migration). Defaults: hbm -> ub. + }]; + + let arguments = (ins + AnyType:$source, + I64Attr:$bytes, + OptionalAttr:$src_space, + OptionalAttr:$dst_space, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyType:$result); + + let assemblyFormat = [{ + $source attr-dict `:` type($source) `->` type($result) + }]; +} + +def Ascend_VectorStoreOp : AscendModel_Op<"vector_store", [DeclareOpInterfaceMethods]> { + let summary = "Store Vector Core result via MTE3"; + let description = [{ + Stores Vector Core computation results to memory via MTE3. + ``src_space``/``dst_space`` select the bandwidth (tilesim migration). + Defaults: ub -> hbm. + }]; + + let arguments = (ins + AnyType:$data, + I64Attr:$bytes, + OptionalAttr:$src_space, + OptionalAttr:$dst_space, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs); + + let assemblyFormat = [{ + $data attr-dict `:` type($data) + }]; +} + +//===----------------------------------------------------------------------===// +// Vector Core Binary Operations (向量二元运算) +//===----------------------------------------------------------------------===// + +// Base class for simple vector binary ops (1 cycle per vector op) +class Ascend_VectorBinarySimple + : AscendModel_Op]> { + let arguments = (ins + AnyTensor:$lhs, + AnyTensor:$rhs, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyTensor:$result); + + let assemblyFormat = [{ + $lhs `,` $rhs attr-dict `:` `(` type($lhs) `,` type($rhs) `)` `->` type($result) + }]; +} + +// Base class for complex vector binary ops (multiple cycles per vector op) +class Ascend_VectorBinaryComplex + : AscendModel_Op]> { + int cyclesPerOp = cycles_per_op; + + let arguments = (ins + AnyTensor:$lhs, + AnyTensor:$rhs, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyTensor:$result); + + let assemblyFormat = [{ + $lhs `,` $rhs attr-dict `:` `(` type($lhs) `,` type($rhs) `)` `->` type($result) + }]; +} + +// Simple binary ops (1 cycle) +def Ascend_AddOp : Ascend_VectorBinarySimple<"add"> { + let summary = "Element-wise addition on Vector Core"; +} + +def Ascend_SubOp : Ascend_VectorBinarySimple<"sub"> { + let summary = "Element-wise subtraction on Vector Core"; +} + +def Ascend_MulOp : Ascend_VectorBinarySimple<"mul"> { + let summary = "Element-wise multiplication on Vector Core"; +} + +def Ascend_MaxOp : Ascend_VectorBinarySimple<"max"> { + let summary = "Element-wise maximum on Vector Core"; +} + +def Ascend_MinOp : Ascend_VectorBinarySimple<"min"> { + let summary = "Element-wise minimum on Vector Core"; +} + +// Complex binary ops (4 cycles - division is expensive) +def Ascend_DivOp : Ascend_VectorBinaryComplex<"div", 4> { + let summary = "Element-wise division on Vector Core"; +} + +//===----------------------------------------------------------------------===// +// Vector Core Comparison Operations (向量比较运算) +//===----------------------------------------------------------------------===// + +// Base class for comparison ops (result is i1 tensor, 1 cycle per vector op) +class Ascend_VectorCmpOp + : AscendModel_Op]> { + let arguments = (ins + AnyTensor:$lhs, + AnyTensor:$rhs, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyTensor:$result); // i1 tensor + + let assemblyFormat = [{ + $lhs `,` $rhs attr-dict `:` `(` type($lhs) `,` type($rhs) `)` `->` type($result) + }]; +} + +def Ascend_CmpEqOp : Ascend_VectorCmpOp<"cmp_eq"> { + let summary = "Element-wise equal comparison on Vector Core"; +} + +def Ascend_CmpNeOp : Ascend_VectorCmpOp<"cmp_ne"> { + let summary = "Element-wise not-equal comparison on Vector Core"; +} + +def Ascend_CmpLtOp : Ascend_VectorCmpOp<"cmp_lt"> { + let summary = "Element-wise less-than comparison on Vector Core"; +} + +def Ascend_CmpLeOp : Ascend_VectorCmpOp<"cmp_le"> { + let summary = "Element-wise less-or-equal comparison on Vector Core"; +} + +def Ascend_CmpGtOp : Ascend_VectorCmpOp<"cmp_gt"> { + let summary = "Element-wise greater-than comparison on Vector Core"; +} + +def Ascend_CmpGeOp : Ascend_VectorCmpOp<"cmp_ge"> { + let summary = "Element-wise greater-or-equal comparison on Vector Core"; +} + +//===----------------------------------------------------------------------===// +// Vector Core Unary Operations (向量一元运算) +//===----------------------------------------------------------------------===// + +// Base class for simple vector unary ops (1 cycle) +class Ascend_VectorUnarySimple + : AscendModel_Op]> { + let arguments = (ins + AnyTensor:$input, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyTensor:$result); + + let assemblyFormat = [{ + $input attr-dict `:` type($input) `->` type($result) + }]; +} + +// Base class for complex vector unary ops (multiple cycles) +class Ascend_VectorUnaryComplex + : AscendModel_Op]> { + int cyclesPerOp = cycles_per_op; + + let arguments = (ins + AnyTensor:$input, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyTensor:$result); + + let assemblyFormat = [{ + $input attr-dict `:` type($input) `->` type($result) + }]; +} + +// Simple unary ops (1 cycle) +def Ascend_NegOp : Ascend_VectorUnarySimple<"neg"> { + let summary = "Element-wise negation on Vector Core"; +} + +def Ascend_AbsOp : Ascend_VectorUnarySimple<"abs"> { + let summary = "Element-wise absolute value on Vector Core"; +} + +def Ascend_ReluOp : Ascend_VectorUnarySimple<"relu"> { + let summary = "Element-wise ReLU activation on Vector Core"; +} + +def Ascend_CastOp : Ascend_VectorUnarySimple<"cast"> { + let summary = "Element-wise type cast on Vector Core (includes extsi/extui/trunci/sitofp/uitofp/fptosi/fptoui/extf/truncf/bitcast)"; +} + +// Medium complexity unary ops (2 cycles) +def Ascend_SqrtOp : Ascend_VectorUnaryComplex<"sqrt", 2> { + let summary = "Element-wise square root on Vector Core"; +} + +def Ascend_RsqrtOp : Ascend_VectorUnaryComplex<"rsqrt", 2> { + let summary = "Element-wise reciprocal square root on Vector Core"; +} + +// Complex unary ops — differentiated cycle costs based on 910B Vector unit +// exp: 3 cycles (hardware-accelerated exponential) +// log: 4 cycles (iterative approximation) +// tanh: 6 cycles (computed via multiple exp operations internally) +// sigmoid: 5 cycles (computed as 1/(1+exp(-x)), reuses exp hardware) +def Ascend_ExpOp : Ascend_VectorUnaryComplex<"exp", 3> { + let summary = "Element-wise exponential on Vector Core"; +} + +def Ascend_LogOp : Ascend_VectorUnaryComplex<"log", 4> { + let summary = "Element-wise natural logarithm on Vector Core"; +} + +def Ascend_TanhOp : Ascend_VectorUnaryComplex<"tanh", 6> { + let summary = "Element-wise hyperbolic tangent on Vector Core"; +} + +def Ascend_SigmoidOp : Ascend_VectorUnaryComplex<"sigmoid", 5> { + let summary = "Element-wise sigmoid activation on Vector Core"; +} + +//===----------------------------------------------------------------------===// +// Vector Core Reduce Operations (向量归约运算) +//===----------------------------------------------------------------------===// + +class Ascend_ReduceOp + : AscendModel_Op]> { + let arguments = (ins + AnyTensor:$input, + I64Attr:$axis, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyType:$result); + + let assemblyFormat = [{ + $input `axis` $axis attr-dict `:` type($input) `->` type($result) + }]; +} + +def Ascend_ReduceSumOp : Ascend_ReduceOp<"reduce_sum"> { + let summary = "Sum reduction on Vector Core"; +} + +def Ascend_ReduceMaxOp : Ascend_ReduceOp<"reduce_max"> { + let summary = "Max reduction on Vector Core"; +} + +def Ascend_ReduceMinOp : Ascend_ReduceOp<"reduce_min"> { + let summary = "Min reduction on Vector Core"; +} + +def Ascend_ReduceProdOp : Ascend_ReduceOp<"reduce_prod"> { + let summary = "Product reduction on Vector Core"; +} + +//===----------------------------------------------------------------------===// +// Vector Core Other Operations +//===----------------------------------------------------------------------===// + +def Ascend_BroadcastOp : AscendModel_Op<"broadcast", [Pure, DeclareOpInterfaceMethods]> { + let summary = "Broadcast tensor on Vector Core"; + let description = [{ + Broadcasts a tensor to a larger shape. + }]; + + let arguments = (ins + AnyTensor:$input, + DenseI64ArrayAttr:$shape, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyTensor:$result); + + let assemblyFormat = [{ + $input `to` $shape attr-dict `:` type($input) `->` type($result) + }]; +} + +def Ascend_SelectOp : AscendModel_Op<"select", [Pure, DeclareOpInterfaceMethods]> { + let summary = "Conditional select on Vector Core"; + let description = [{ + Selects elements from two tensors based on a condition tensor. + }]; + + let arguments = (ins + AnyTensor:$condition, + AnyTensor:$true_value, + AnyTensor:$false_value, + OptionalAttr:$estimated_cycles, + OptionalAttr:$op_id + ); + + let results = (outs AnyTensor:$result); + + let assemblyFormat = [{ + $condition `,` $true_value `,` $false_value attr-dict `:` + type($condition) `,` type($true_value) `,` type($false_value) `->` type($result) + }]; +} + +//===----------------------------------------------------------------------===// +// Synchronization Operations +//===----------------------------------------------------------------------===// + +def Ascend_SyncOp : AscendModel_Op<"sync", []> { + let summary = "Synchronization barrier"; + let description = [{ + Synchronization point between different hardware units. + }]; + + let arguments = (ins + StrAttr:$sync_type + ); + + let results = (outs); + + let assemblyFormat = [{ + $sync_type attr-dict + }]; +} + +#endif // ASCEND_MODEL_OPS_TD diff --git a/third_party/ascend/costmodel/include/AscendModel/Transforms/Passes.h b/third_party/ascend/costmodel/include/AscendModel/Transforms/Passes.h new file mode 100644 index 0000000000..46f1538019 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/Transforms/Passes.h @@ -0,0 +1,46 @@ +//===- Passes.h - AscendModel Transformation Passes -------------*- C++ -*-===// +// +// This file declares transformation passes for the AscendModel dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_TRANSFORMS_PASSES_H +#define ASCEND_MODEL_TRANSFORMS_PASSES_H + +#include "mlir/Pass/Pass.h" +#include + +namespace mlir { +namespace ascend { + +//===----------------------------------------------------------------------===// +// Pass Declarations (TableGen generated) +//===----------------------------------------------------------------------===// + +#define GEN_PASS_DECL +#include "AscendModel/Transforms/Passes.h.inc" + +//===----------------------------------------------------------------------===// +// Pass Registration (TableGen generated) +//===----------------------------------------------------------------------===// + +#define GEN_PASS_REGISTRATION +#include "AscendModel/Transforms/Passes.h.inc" + +//===----------------------------------------------------------------------===// +// Pipeline Registration +//===----------------------------------------------------------------------===// + +/// Register the performance modeling pipeline. +void registerAscendModelPipeline(); + +/// Register all AscendModel passes and pipelines. +inline void registerAllAscendModelPasses() { + registerAscendModelPasses(); // TableGen generated + registerAscendModelPipeline(); // Custom pipeline +} + +} // namespace ascend +} // namespace mlir + +#endif // ASCEND_MODEL_TRANSFORMS_PASSES_H diff --git a/third_party/ascend/costmodel/include/AscendModel/Transforms/Passes.td b/third_party/ascend/costmodel/include/AscendModel/Transforms/Passes.td new file mode 100644 index 0000000000..ff2d4b2cbd --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/Transforms/Passes.td @@ -0,0 +1,187 @@ +//===- Passes.td - AscendModel transformation passes -------*- tablegen -*-===// +// +// This file declares the passes for the AscendModel dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef ASCEND_MODEL_TRANSFORMS_PASSES_TD +#define ASCEND_MODEL_TRANSFORMS_PASSES_TD + +include "mlir/Pass/PassBase.td" + +//===----------------------------------------------------------------------===// +// ConvertTritonToAscend +//===----------------------------------------------------------------------===// + +def ConvertTritonToAscendPass : Pass<"convert-triton-to-ascend", "ModuleOp"> { + let summary = "Convert Triton IR to AscendModel IR"; + let description = [{ + This pass converts Triton dialect operations to AscendModel dialect + operations for performance modeling on Ascend 910B hardware. + }]; + let dependentDialects = [ + "mlir::ascend::AscendModelDialect", + "mlir::arith::ArithDialect", + "mlir::math::MathDialect", + "mlir::scf::SCFDialect", + "mlir::tensor::TensorDialect" + ]; +} + +//===----------------------------------------------------------------------===// +// InsertDataTransfers +//===----------------------------------------------------------------------===// + +def InsertDataTransfersPass : Pass<"insert-data-transfers", "ModuleOp"> { + let summary = "Insert memory transfer operations between Cube and Vector"; + let description = [{ + This pass analyzes data dependencies between Cube (matrix) and Vector + (SIMD) operations and inserts the necessary memory transfer operations. + + On Ascend 910B, Cube and Vector have separate memory hierarchies: + - Cube: HBM → L1 → L0A/L0B → Cube → L0C → HBM + - Vector: HBM → UB → Vector → UB → HBM + + When data flows between them, explicit transfers are needed: + - Vector → Cube: insert vector_store (UB→HBM) + cube_load (HBM→L1) + - Cube → Vector: insert cube_store (L0C→HBM) + vector_load (HBM→UB) + }]; +} + +//===----------------------------------------------------------------------===// +// AssignOpIDs +//===----------------------------------------------------------------------===// + +def AssignOpIDsPass : Pass<"assign-op-ids", "ModuleOp"> { + let summary = "Assign unique IDs to operations"; + let description = [{ + This pass assigns unique operation IDs to all AscendModel operations + for tracking and scheduling purposes. + }]; +} + +//===----------------------------------------------------------------------===// +// EstimateCycles +//===----------------------------------------------------------------------===// + +def EstimateCyclesPass : Pass<"estimate-cycles", "ModuleOp"> { + let summary = "Estimate execution cycles for operations"; + let description = [{ + This pass estimates the execution cycles for each AscendModel operation + based on hardware parameters from JSON config file. + + For loops with dynamic bounds, provide bindings via arg-bindings option: + + Example for Triton kernels (options are space-separated): + inproc-costmodel --estimate-cycles="arg-bindings=arg2=100,pid_x=0" input.mlir + + Supported binding formats: + - argN=value or N=value : function argument %argN + - pid_x=value : tt.get_program_id x + - pid_y=value : tt.get_program_id y + - pid_z=value : tt.get_program_id z + + Alternatively, use loop-trip-counts to directly override trip counts: + inproc-costmodel --estimate-cycles="loop-trip-counts=4,6588" input.mlir + + To use custom hardware config: + inproc-costmodel --estimate-cycles="hardware-config=path/to/config.json" input.mlir + + Multiple options (use space, not comma, to separate): + inproc-costmodel --estimate-cycles="hardware-config=config.json arg-bindings=pid_x=0" input.mlir + }]; + + let options = [ + Option<"argBindingsStr", "arg-bindings", "std::string", + /*default=*/"\"\"", + "Bindings for args and program_ids (e.g., 'arg2=100,pid_x=0')">, + Option<"loopTripCountsStr", "loop-trip-counts", "std::string", + /*default=*/"\"\"", + "Direct loop trip count overrides in order (e.g., '4,6588')">, + Option<"hardwareConfigPath", "hardware-config", "std::string", + /*default=*/"\"\"", + "Path to hardware configuration JSON file"> + ]; +} + +//===----------------------------------------------------------------------===// +// PipelineAnalysis +//===----------------------------------------------------------------------===// + +def PipelineAnalysisPass : Pass<"analyze-pipeline", "ModuleOp"> { + let summary = "Analyze pipeline scheduling"; + let description = [{ + This pass performs pipeline scheduling analysis for AscendModel operations, + considering hardware unit parallelism and dependencies. + + For loops with dynamic bounds (options are space-separated): + inproc-costmodel --analyze-pipeline="arg-bindings=arg2=100,pid_x=0" input.mlir + + To use custom hardware config: + inproc-costmodel --analyze-pipeline="hardware-config=path/to/config.json" input.mlir + + Multiple options (use space, not comma, to separate): + inproc-costmodel --analyze-pipeline="hardware-config=config.json arg-bindings=pid_x=0" input.mlir + }]; + + let options = [ + Option<"argBindingsStr", "arg-bindings", "std::string", + /*default=*/"\"\"", + "Bindings for args and program_ids (e.g., 'arg2=100,pid_x=0')">, + Option<"loopTripCountsStr", "loop-trip-counts", "std::string", + /*default=*/"\"\"", + "Direct loop trip count overrides in order (e.g., '4,6588')">, + Option<"hardwareConfigPath", "hardware-config", "std::string", + /*default=*/"\"\"", + "Path to hardware configuration JSON file"> + ]; +} + +//===----------------------------------------------------------------------===// +// HIVMAnalysis +//===----------------------------------------------------------------------===// + +def HIVMAnalysisPass : Pass<"analyze-hivm", "ModuleOp"> { + let summary = "Analyze native HIVM IR scheduling and synchronization"; + let description = [{ + This pass runs the HIVM-native scheduler directly on parsed HIVM IR. + It expects `hivm.hir.*` ops to be available as native MLIR ops, and + reports per-pipe utilization, sync costs, and critical-path timing. + + Example: + inproc-costmodel --analyze-hivm input.npuir.mlir + inproc-costmodel --analyze-hivm="hardware-config=configs/ascend_910b.json arg-bindings=arg10=128" input.npuir.mlir + }]; + + let options = [ + Option<"schedulerMode", "scheduler", "std::string", + /*default=*/"\"static\"", + "Scheduler backend: static or des">, + Option<"argBindingsStr", "arg-bindings", "std::string", + /*default=*/"\"\"", + "Bindings for dynamic HIVM values (e.g., 'arg10=128,arg11=64')">, + Option<"hardwareConfigPath", "hardware-config", "std::string", + /*default=*/"\"\"", + "Path to hardware configuration JSON file">, + Option<"reportFile", "report-file", "std::string", + /*default=*/"\"\"", + "Optional file to write the textual HIVM report to">, + Option<"perfettoTraceFile", "perfetto-trace-file", "std::string", + /*default=*/"\"\"", + "Optional Perfetto-compatible JSON trace output file"> + ]; +} + +//===----------------------------------------------------------------------===// +// PerfReport +//===----------------------------------------------------------------------===// +def PerfReportPass : Pass<"perf-report", "ModuleOp"> { + let summary = "Generate performance report"; + let description = [{ + This pass generates a comprehensive performance report including + roofline analysis, utilization metrics, and bottleneck identification. + }]; +} + + +#endif // ASCEND_MODEL_TRANSFORMS_PASSES_TD diff --git a/third_party/ascend/costmodel/include/AscendModel/Utils.h b/third_party/ascend/costmodel/include/AscendModel/Utils.h new file mode 100644 index 0000000000..6f15a1e687 --- /dev/null +++ b/third_party/ascend/costmodel/include/AscendModel/Utils.h @@ -0,0 +1,2 @@ +// Forwarding header — actual implementation in Analysis/Utils.h +#include "AscendModel/Analysis/Utils.h" diff --git a/third_party/ascend/costmodel/lib/AscendModel/Analysis/CMakeLists.txt b/third_party/ascend/costmodel/lib/AscendModel/Analysis/CMakeLists.txt new file mode 100644 index 0000000000..bce37c2ae8 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Analysis/CMakeLists.txt @@ -0,0 +1,42 @@ +add_mlir_library(AscendModelAnalysis + PARTIAL_SOURCES_INTENDED + HardwareConfig.cpp + HIVMAnalysis.cpp + PipelineAnalysis.cpp + + ADDITIONAL_HEADER_DIRS + ${PROJECT_SOURCE_DIR}/include/AscendModel/Analysis + + DEPENDS + AscendModelOpsIncGen + + LINK_LIBS PUBLIC + AscendModelIR + MLIRArithDialect + MLIRFuncDialect + MLIRIR + MLIRParser + MLIRSCFDialect + MLIRSupport + LLVMSupport +) + +if(TRITONSIM_HAS_BISHENGIR_HIVM) + target_include_directories(AscendModelAnalysis SYSTEM PUBLIC + ${TRITONSIM_BISHENGIR_INCLUDE_DIRS} + ) + target_link_libraries(AscendModelAnalysis PUBLIC + ${TRITONSIM_BISHENGIR_HIVM_LIBS} + MLIRBufferizationDialect + MLIRDLTIDialect + MLIRDestinationStyleOpInterface + MLIRLinalgDialect + MLIRLLVMCommonConversion + MLIRLLVMDialect + MLIRMemRefDialect + MLIRSideEffectInterfaces + MLIRTensorDialect + MLIRTransformUtils + MLIRViewLikeInterface + ) +endif() diff --git a/third_party/ascend/costmodel/lib/AscendModel/Analysis/HIVMAnalysis.cpp b/third_party/ascend/costmodel/lib/AscendModel/Analysis/HIVMAnalysis.cpp new file mode 100644 index 0000000000..5d51ce629e --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Analysis/HIVMAnalysis.cpp @@ -0,0 +1,3142 @@ +//===- HIVMAnalysis.cpp - HIVM performance analysis ----------------------===// +// +// This implementation consumes HIVM through MLIR-native ingestion and +// schedules the resulting execution graph with either a static scheduler or +// a discrete-event simulator. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/Analysis/HIVMAnalysis.h" + +#ifdef TRITONSIM_HAS_BISHENGIR_HIVM +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HACC/IR/HACC.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#endif + +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/AsmState.h" +#include "mlir/IR/BuiltinDialect.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/Operation.h" +#include "mlir/Parser/Parser.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Format.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::ascend; + +namespace { + +struct ParsedOp { + HIVMOp op; + std::string definedValue; + std::vector operandValues; + std::vector mlirResults; + std::vector barrierPipes; + std::string syncCoreType; + std::string senderEvent; + std::string receiverEvent; + std::string eventId; + mlir::Value syncIdValue; + HIVMPipe senderPipe = HIVMPipe::Unknown; + HIVMPipe receiverPipe = HIVMPipe::Unknown; +}; + +static int64_t estimateMmadL1MTE1Cycles(const ParsedOp &parsed, + const HardwareConfig &config); + +static int64_t estimateND2NZCycles(const ParsedOp &parsed, + const HardwareConfig &config); + +static std::vector expandMacroOp(const ParsedOp &parsed, + const HardwareConfig &config) { + std::vector ops; + llvm::StringRef name = parsed.op.opName; + if (name == "mmadL1") { + // MTE1 (L1 -> L0A/L0B) and Cube (compute on L0) are on different pipes + // and overlap in the hardware pipeline. Each gets its own duration. + HIVMOp mte = parsed.op; + mte.opName = "mmadL1.mte1"; + mte.pipe = HIVMPipe::MTE1; + mte.duration = estimateMmadL1MTE1Cycles(parsed, config); + mte.isSyncOp = false; + mte.isBarrier = false; + + HIVMOp cube = parsed.op; + cube.opName = "mmadL1.cube"; + cube.pipe = HIVMPipe::Cube; + // Cube gets the full compute duration (startup + tile cycles), NOT the + // leftover after subtracting MTE1. The two pipes overlap. + cube.duration = std::max(1, parsed.op.duration); + cube.isSyncOp = false; + cube.isBarrier = false; + ops.push_back(std::move(mte)); + ops.push_back(std::move(cube)); + return ops; + } + + if (name == "matmul" || name == "mix_matmul" || name == "mix_group_matmul") { + int64_t preload = std::max(1, config.getMTE2StartupLatency()); + int64_t drain = std::max(1, config.getMTE3StartupLatency()); + int64_t compute = + std::max(1, parsed.op.duration - preload - drain); + + HIVMOp mte2 = parsed.op; + mte2.opName = name.str() + ".mte2"; + mte2.pipe = HIVMPipe::CubeMTE2; + mte2.duration = preload; + mte2.isSyncOp = false; + mte2.isBarrier = false; + + HIVMOp cube = parsed.op; + cube.opName = name.str() + ".cube"; + cube.pipe = HIVMPipe::Cube; + cube.duration = compute; + cube.isSyncOp = false; + cube.isBarrier = false; + + HIVMOp mte3 = parsed.op; + mte3.opName = name.str() + ".mte3"; + mte3.pipe = HIVMPipe::MTE3; + mte3.duration = drain; + mte3.isSyncOp = false; + mte3.isBarrier = false; + + ops.push_back(std::move(mte2)); + ops.push_back(std::move(cube)); + ops.push_back(std::move(mte3)); + return ops; + } + + ops.push_back(parsed.op); + return ops; +} + +static void attachSyncMetadata(ParsedOp &parsed) { + parsed.op.senderPipe = parsed.senderPipe; + parsed.op.receiverPipe = parsed.receiverPipe; + parsed.op.eventId = parsed.eventId; +} + +struct EventKey { + HIVMPipe sender = HIVMPipe::Unknown; + HIVMPipe receiver = HIVMPipe::Unknown; + std::string eventId; + + bool operator<(const EventKey &other) const { + return std::tie(sender, receiver, eventId) < + std::tie(other.sender, other.receiver, other.eventId); + } +}; + +struct EventInstanceKey { + EventKey key; + int64_t generation = 0; + + bool operator<(const EventInstanceKey &other) const { + return std::tie(key.sender, key.receiver, key.eventId, generation) < + std::tie(other.key.sender, other.key.receiver, other.key.eventId, + other.generation); + } +}; + +struct LoopFrame { + int braceDepth = 0; + int64_t tripCount = 1; +}; + +struct AnalysisState { + llvm::DenseMap constants; + llvm::DenseMap boundValues; + llvm::DenseMap valueProducers; + llvm::DenseMap bufferRoots; + std::map bufferSlots; + std::map bufferVersions; + std::map eventProducers; + std::map eventGenerations; + std::map latestPipeProducer; + std::map argBindings; +}; + +static llvm::StringRef trim(llvm::StringRef s) { return s.trim(); } + +static int64_t ceilDiv(int64_t num, int64_t den) { + if (den <= 0) + return 0; + return (num + den - 1) / den; +} + +/// Resolve an ambiguous core type (empty or "CUBE_OR_VECTOR") by inspecting the +/// enclosing func.func's name for AIC/AIV markers. +static llvm::StringRef resolveCoreTypeFromFunc(mlir::Operation *op, + llvm::StringRef current) { + if (current == "CUBE" || current == "AIC" || current == "VECTOR" || + current == "AIV") + return current; + if (auto parentFunc = op->getParentOfType()) { + llvm::StringRef funcName = parentFunc.getName(); + if (funcName.contains("aic") || funcName.contains("AIC") || + funcName.contains("cube")) + return "CUBE"; + if (funcName.contains("aiv") || funcName.contains("AIV") || + funcName.contains("vector") || funcName.contains("mix")) + return "VECTOR"; + } + return current; +} + +static bool isVectorDomainPipe(HIVMPipe pipe) { + return pipe == HIVMPipe::Vector || pipe == HIVMPipe::VectorMTE2 || + pipe == HIVMPipe::MTE3; +} + +static bool isCubeDomainPipe(HIVMPipe pipe) { + return pipe == HIVMPipe::Cube || pipe == HIVMPipe::CubeMTE2 || + pipe == HIVMPipe::MTE1 || pipe == HIVMPipe::FixPipe; +} + +static HIVMPipe disambiguateMTE2Pipe(HIVMPipe pipe, HIVMPipe peerPipe, + llvm::StringRef coreType) { + if (pipe != HIVMPipe::VectorMTE2) + return pipe; + if (isVectorDomainPipe(peerPipe)) + return HIVMPipe::VectorMTE2; + if (isCubeDomainPipe(peerPipe)) + return HIVMPipe::CubeMTE2; + if (coreType == "VECTOR" || coreType == "AIV") + return HIVMPipe::VectorMTE2; + if (coreType == "CUBE" || coreType == "AIC") + return HIVMPipe::CubeMTE2; + return HIVMPipe::VectorMTE2; +} + +static HIVMPipe selectMTE2PipeForSpaces(llvm::StringRef srcSpace, + llvm::StringRef dstSpace, + llvm::StringRef coreType) { + if (dstSpace == "ub" || srcSpace == "ub") + return HIVMPipe::VectorMTE2; + if (dstSpace == "l1" || dstSpace == "l0a" || dstSpace == "l0b" || + dstSpace == "l0c" || srcSpace == "l1" || srcSpace == "l0a" || + srcSpace == "l0b" || srcSpace == "l0c") + return HIVMPipe::CubeMTE2; + if (coreType == "CUBE" || coreType == "AIC") + return HIVMPipe::CubeMTE2; + return HIVMPipe::VectorMTE2; +} + +static int64_t getElementByteWidth(llvm::StringRef typeToken) { + llvm::StringRef t = trim(typeToken); + if (t == "f16" || t == "bf16" || t == "i16") + return 2; + if (t == "f32" || t == "i32") + return 4; + if (t == "f64" || t == "i64") + return 8; + if (t == "i8" || t == "ui8" || t == "si8") + return 1; + return 0; +} + +static int64_t parseMemRefElementCount(llvm::StringRef line) { + size_t memrefPos = line.find("memref<"); + if (memrefPos == llvm::StringRef::npos) + return 0; + size_t addrPos = line.find(", #hivm.address_space<", memrefPos); + if (addrPos == llvm::StringRef::npos) + return 0; + llvm::StringRef shapeAndType = line.slice(memrefPos + 7, addrPos); + llvm::SmallVector parts; + shapeAndType.split(parts, 'x', -1, false); + if (parts.empty()) + return 0; + + int64_t count = 1; + for (size_t i = 0; i + 1 < parts.size(); ++i) { + llvm::StringRef dim = trim(parts[i]); + if (dim == "?" || dim.empty()) + return 0; + int64_t value = 0; + if (dim.getAsInteger(10, value)) + return 0; + count *= value; + } + return count; +} + +static int64_t parseMemRefBytes(llvm::StringRef line) { + size_t memrefPos = line.find("memref<"); + if (memrefPos == llvm::StringRef::npos) + return 0; + size_t addrPos = line.find(", #hivm.address_space<", memrefPos); + if (addrPos == llvm::StringRef::npos) + return 0; + llvm::StringRef shapeAndType = line.slice(memrefPos + 7, addrPos); + llvm::SmallVector parts; + shapeAndType.split(parts, 'x', -1, false); + if (parts.empty()) + return 0; + + llvm::StringRef elemType = trim(parts.back()); + int64_t elemBytes = getElementByteWidth(elemType); + if (elemBytes <= 0) + return 0; + + int64_t count = 1; + for (size_t i = 0; i + 1 < parts.size(); ++i) { + llvm::StringRef dim = trim(parts[i]); + if (dim == "?" || dim.empty()) + return 0; + int64_t value = 0; + if (dim.getAsInteger(10, value)) + return 0; + count *= value; + } + + return count * elemBytes; +} + +static int64_t estimateMmadL1MTE1Cycles(const ParsedOp &parsed, + const HardwareConfig &config) { + int64_t inputBytes = 0; + for (const std::string &buffer : parsed.op.readBuffers) + inputBytes += parseMemRefBytes(buffer); + if (inputBytes <= 0) + inputBytes = std::max(parsed.op.bytes, 1); + + double bandwidth = config.getMemoryBandwidthBytesPerCycle("mte1"); + if (bandwidth <= 0.0) + bandwidth = std::max(1.0, config.getMemoryBandwidthBytesPerCycle("l1")); + int64_t transferCycles = std::max( + 1, static_cast(std::ceil(inputBytes / bandwidth))); + int64_t startupCycles = + std::max(4, config.getMTE2StartupLatency() / 5); + return startupCycles + transferCycles; +} + +static int64_t estimateND2NZCycles(const ParsedOp &parsed, + const HardwareConfig &config) { + int64_t bytes = std::max(parsed.op.bytes, 1); + // nd2nz is not a plain HBM->L1 DMA. The layout conversion runs on the + // cube-side transfer path but sustains lower throughput than a normal + // cube_mte2 transport, so keep it on a dedicated calibration path. + double baseBandwidth = config.getMemoryBandwidthBytesPerCycle("cube_mte2"); + if (baseBandwidth <= 0.0) + baseBandwidth = + std::max(1.0, config.getMemoryBandwidthBytesPerCycle("hbm")); + double effectiveBandwidth = std::max(1.0, baseBandwidth * 0.5); + int64_t transferCycles = std::max( + 1, static_cast(std::ceil(bytes / effectiveBandwidth))); + int64_t startupCycles = + std::max(16, config.getMTE2StartupLatency() / 3); + return startupCycles + transferCycles; +} + +static std::string canonicalizeAddressSpace(llvm::StringRef space) { + llvm::StringRef s = trim(space); + if (s == "gm") + return "gm"; + if (s == "ub") + return "ub"; + if (s == "l1" || s == "cbuf") + return "l1"; + if (s == "ca" || s == "l0a") + return "l0a"; + if (s == "cb" || s == "l0b") + return "l0b"; + if (s == "cc" || s == "l0c") + return "l0c"; + return s.str(); +} + +#ifdef TRITONSIM_HAS_BISHENGIR_HIVM +static std::string getCanonicalTypeAddressSpace(mlir::Type type) { + auto memref = llvm::dyn_cast(type); + if (!memref) + return ""; + mlir::Attribute memSpace = memref.getMemorySpace(); + auto addrAttr = + llvm::dyn_cast_or_null(memSpace); + if (!addrAttr) + return ""; + switch (addrAttr.getAddressSpace()) { + case mlir::hivm::AddressSpace::GM: + return "gm"; + case mlir::hivm::AddressSpace::L1: + return "l1"; + case mlir::hivm::AddressSpace::L0A: + return "l0a"; + case mlir::hivm::AddressSpace::L0B: + return "l0b"; + case mlir::hivm::AddressSpace::L0C: + return "l0c"; + case mlir::hivm::AddressSpace::UB: + return "ub"; + default: + return ""; + } +} +#endif + +static int64_t getTypeByteWidth(mlir::Type type) { + if (!type) + return 0; + if (auto intType = llvm::dyn_cast(type)) + return std::max(1, intType.getWidth() / 8); + if (auto floatType = llvm::dyn_cast(type)) + return std::max(1, floatType.getWidth() / 8); + return 0; +} + +static int64_t getShapedTypeElementCount(mlir::Type type) { + auto shaped = llvm::dyn_cast(type); + if (!shaped || !shaped.hasStaticShape()) + return 0; + int64_t count = 1; + for (int64_t dim : shaped.getShape()) + count *= dim; + return count; +} + +static int64_t getShapedTypeBytes(mlir::Type type) { + auto shaped = llvm::dyn_cast(type); + if (!shaped) + return 0; + int64_t count = getShapedTypeElementCount(type); + if (count <= 0) + return 0; + return count * getTypeByteWidth(shaped.getElementType()); +} + +static int64_t inferValueBytes(mlir::Value value) { + if (!value) + return 0; + return getShapedTypeBytes(value.getType()); +} + +static int64_t inferValueElements(mlir::Value value) { + if (!value) + return 0; + return getShapedTypeElementCount(value.getType()); +} + +static int getLineNumberFromLocation(mlir::Location loc) { + return llvm::TypeSwitch(loc) + .Case( + [](auto fileLoc) { return static_cast(fileLoc.getLine()); }) + .Case([](auto nameLoc) { + return getLineNumberFromLocation(nameLoc.getChildLoc()); + }) + .Case([](auto fusedLoc) { + for (mlir::Location child : fusedLoc.getLocations()) { + int line = getLineNumberFromLocation(child); + if (line > 0) + return line; + } + return 0; + }) + .Default([](mlir::Location) { return 0; }); +} + +static std::string renderOperation(mlir::Operation *op) { + std::string storage; + llvm::raw_string_ostream os(storage); + mlir::OpPrintingFlags flags; + flags.elideLargeElementsAttrs(); + op->print(os, flags); + os.flush(); + return storage; +} + +static void eraseAttributeAssignment(std::string &line, llvm::StringRef name) { + std::string needle = name.str(); + for (;;) { + size_t pos = line.find(needle); + if (pos == std::string::npos) + return; + + size_t valueStart = line.find('#', pos + needle.size()); + if (valueStart == std::string::npos) + return; + size_t valueEnd = line.find('>', valueStart); + if (valueEnd == std::string::npos) + return; + ++valueEnd; + + size_t eraseStart = pos; + while (eraseStart > 0 && line[eraseStart - 1] == ' ') + --eraseStart; + if (eraseStart > 0 && line[eraseStart - 1] == ',') { + --eraseStart; + while (eraseStart > 0 && line[eraseStart - 1] == ' ') + --eraseStart; + } else { + while (valueEnd < line.size() && line[valueEnd] == ' ') + ++valueEnd; + if (valueEnd < line.size() && line[valueEnd] == ',') + ++valueEnd; + while (valueEnd < line.size() && line[valueEnd] == ' ') + ++valueEnd; + } + + line.erase(eraseStart, valueEnd - eraseStart); + } +} + +static std::string sanitizeMlirBuffer(llvm::StringRef buffer) { + // Pre-process to remove custom dialect attributes/types that require + // registered dialects. When built without BiShengIR, the parser cannot + // handle #hivm.address_space<...>, #hacc.arg_type<...>, etc. + std::string preprocessed; + { + llvm::SmallVector lines; + buffer.split(lines, '\n'); + llvm::raw_string_ostream os(preprocessed); + for (llvm::StringRef line : lines) { + llvm::StringRef trimmed = line.trim(); + if (trimmed.starts_with("warning: ") || + trimmed.ends_with("warning generated.")) + break; + if (trimmed.starts_with("ld.lld:") || trimmed.starts_with("[ERROR]") || + trimmed.starts_with("[WARNING]") || trimmed.starts_with("[INFO]")) + continue; + std::string l = line.str(); + // Replace #hivm.address_space with integer memory space + while (auto pos = l.find("#hivm.address_space<")) { + auto end = l.find('>', pos); + if (end == std::string::npos) + break; + auto space = llvm::StringRef(l).slice(pos + 20, end); + int num = 0; + if (space == "gm") + num = 0; + else if (space == "ub") + num = 1; + else if (space == "l1") + num = 2; + else if (space == "l0a") + num = 3; + else if (space == "l0b") + num = 4; + else if (space == "l0c") + num = 5; + else if (space == "cbuf") + num = 6; + l.replace(pos, end - pos + 1, std::to_string(num)); + } + // Strip whole custom attribute assignments whose values require + // external dialect parsers. + eraseAttributeAssignment(l, "hacc.arg_type"); + eraseAttributeAssignment(l, "hivm.func_core_type"); + eraseAttributeAssignment(l, "hacc.function_kind"); + os << l << "\n"; + } + os.flush(); + } + + return preprocessed; +} + +static bool startsWithHivmOp(mlir::Operation *op) { + return op->getName().getStringRef().starts_with("hivm.hir."); +} + +static llvm::StringRef getLeafOpName(mlir::Operation *op) { + llvm::StringRef fullName = op->getName().getStringRef(); + size_t dot = fullName.rfind('.'); + if (dot == llvm::StringRef::npos) + return fullName; + return fullName.drop_front(dot + 1); +} + +#ifdef TRITONSIM_HAS_BISHENGIR_HIVM +static HIVMPipe convertTypedPipe(mlir::hivm::PIPE pipe) { + switch (pipe) { + case mlir::hivm::PIPE::PIPE_V: + return HIVMPipe::Vector; + case mlir::hivm::PIPE::PIPE_MTE2: + case mlir::hivm::PIPE::VIRTUAL_PIPE_MTE2_L1A: + case mlir::hivm::PIPE::VIRTUAL_PIPE_MTE2_L1B: + return HIVMPipe::VectorMTE2; + case mlir::hivm::PIPE::PIPE_MTE3: + return HIVMPipe::MTE3; + case mlir::hivm::PIPE::PIPE_S: + return HIVMPipe::Scalar; + case mlir::hivm::PIPE::PIPE_FIX: + return HIVMPipe::FixPipe; + case mlir::hivm::PIPE::PIPE_M: + return HIVMPipe::Cube; + case mlir::hivm::PIPE::PIPE_MTE1: + return HIVMPipe::MTE1; + case mlir::hivm::PIPE::PIPE_ALL: + return HIVMPipe::All; + default: + return HIVMPipe::Unknown; + } +} + +static std::string stringifyTypedPipe(mlir::hivm::PIPE pipe) { + return mlir::hivm::stringifyPIPE(pipe).str(); +} + +static std::string +stringifyTypedCore(std::optional core) { + if (!core) + return ""; + return mlir::hivm::stringifyTCoreType(*core).str(); +} + +static std::string canonicalizeStaticEventToken(llvm::StringRef token) { + if (token.consume_front("EVENT_ID")) + return token.str(); + return token.str(); +} + +static bool populateTypedHivmOp(mlir::Operation *op, ParsedOp &parsed) { + auto isTypedCubeOpName = [&](llvm::StringRef opName) { + return opName == "matmul" || opName == "mix_matmul" || + opName == "mix_group_matmul" || opName == "mmadL1"; + }; + parsed.op.opName = getLeafOpName(op).str(); + + if (auto pipeIface = llvm::dyn_cast(op)) { + if (pipeIface.isSinglePipeOp()) { + parsed.op.pipe = convertTypedPipe(pipeIface.getPipe()); + } else if (!isTypedCubeOpName(parsed.op.opName)) { + parsed.op.pipe = convertTypedPipe(pipeIface.getOutPipe()); + } + } + if (auto coreIface = llvm::dyn_cast(op)) + parsed.op.coreType = stringifyTypedCore(coreIface.getCoreType()); + if (parsed.op.coreType.empty()) { + if (auto inferIface = + llvm::dyn_cast(op)) + parsed.op.coreType = stringifyTypedCore(inferIface.inferCoreType()); + } + // Resolve "CUBE_OR_VECTOR" to a concrete core type using the enclosing + // function name. This is critical for disambiguating PIPE_MTE2 into + // PIPE_MTE2_C vs PIPE_MTE2_V for pipe_barrier ops. + parsed.op.coreType = resolveCoreTypeFromFunc(op, parsed.op.coreType).str(); + + if (llvm::isa(op)) + parsed.op.pipe = selectMTE2PipeForSpaces( + getCanonicalTypeAddressSpace(op->getOperand(0).getType()), + op->getNumOperands() > 1 + ? getCanonicalTypeAddressSpace(op->getOperand(1).getType()) + : llvm::StringRef(), + parsed.op.coreType); + else if (llvm::isa(op)) + parsed.op.pipe = HIVMPipe::MTE3; + else if (llvm::isa(op)) + parsed.op.pipe = HIVMPipe::FixPipe; + else if (parsed.op.opName == "nd2nz") + parsed.op.pipe = HIVMPipe::CubeMTE2; + else if (parsed.op.opName == "nz2nd") + parsed.op.pipe = HIVMPipe::MTE3; + else if (auto copyOp = llvm::dyn_cast(op)) { + std::string srcSpace = + getCanonicalTypeAddressSpace(copyOp.getSrc().getType()); + std::string dstSpace = + getCanonicalTypeAddressSpace(copyOp.getDst().getType()); + if (srcSpace == "ub" && dstSpace == "l1") + parsed.op.pipe = HIVMPipe::MTE3; + else if (srcSpace == "gm" && dstSpace == "l1") + parsed.op.pipe = HIVMPipe::CubeMTE2; + else if (srcSpace == "l0c" && dstSpace == "gm") + parsed.op.pipe = HIVMPipe::FixPipe; + else if (srcSpace == "ub" && dstSpace == "ub") + parsed.op.pipe = HIVMPipe::Vector; + } else if (parsed.op.opName == "convert_layout" || + parsed.op.opName == "pointer_cast") + parsed.op.pipe = HIVMPipe::Unknown; + else if (llvm::isa(op)) + parsed.op.pipe = HIVMPipe::Cube; + + if (auto barrier = llvm::dyn_cast(op)) { + parsed.op.opName = "pipe_barrier"; + parsed.op.isSyncOp = true; + parsed.op.isBarrier = true; + HIVMPipe rawPipe = convertTypedPipe(barrier.getPipe().getPipe()); + parsed.op.pipe = + disambiguateMTE2Pipe(rawPipe, HIVMPipe::Unknown, parsed.op.coreType); + parsed.barrierPipes.push_back(parsed.op.pipe); + return true; + } + if (auto setFlag = llvm::dyn_cast(op)) { + parsed.op.opName = "set_flag"; + parsed.op.isSyncOp = true; + parsed.senderEvent = stringifyTypedPipe(setFlag.getSetPipe().getPipe()); + parsed.receiverEvent = stringifyTypedPipe(setFlag.getWaitPipe().getPipe()); + parsed.eventId = stringifyTypedEvent(setFlag.getStaticEventId(), + setFlag.getDynamicEventId()); + parsed.syncIdValue = setFlag.getDynamicEventId(); + parsed.senderPipe = disambiguateMTE2Pipe( + convertTypedPipe(setFlag.getSetPipe().getPipe()), + convertTypedPipe(setFlag.getWaitPipe().getPipe()), parsed.op.coreType); + parsed.receiverPipe = + disambiguateMTE2Pipe(convertTypedPipe(setFlag.getWaitPipe().getPipe()), + parsed.senderPipe, parsed.op.coreType); + parsed.op.pipe = parsed.senderPipe; + return true; + } + if (auto waitFlag = llvm::dyn_cast(op)) { + parsed.op.opName = "wait_flag"; + parsed.op.isSyncOp = true; + parsed.senderEvent = stringifyTypedPipe(waitFlag.getSetPipe().getPipe()); + parsed.receiverEvent = stringifyTypedPipe(waitFlag.getWaitPipe().getPipe()); + parsed.eventId = stringifyTypedEvent(waitFlag.getStaticEventId(), + waitFlag.getDynamicEventId()); + parsed.syncIdValue = waitFlag.getDynamicEventId(); + parsed.senderPipe = disambiguateMTE2Pipe( + convertTypedPipe(waitFlag.getSetPipe().getPipe()), + convertTypedPipe(waitFlag.getWaitPipe().getPipe()), parsed.op.coreType); + parsed.receiverPipe = + disambiguateMTE2Pipe(convertTypedPipe(waitFlag.getWaitPipe().getPipe()), + parsed.senderPipe, parsed.op.coreType); + parsed.op.pipe = parsed.receiverPipe; + return true; + } + if (auto syncSet = llvm::dyn_cast(op)) { + parsed.op.opName = "sync_block_set"; + parsed.op.isSyncOp = true; + parsed.syncCoreType = + mlir::hivm::stringifyTCoreType(syncSet.getTcoreType().getTcoretype()) + .str(); + parsed.op.coreType = parsed.syncCoreType; + parsed.senderEvent = stringifyTypedPipe(syncSet.getTpipe().getPipe()); + parsed.receiverEvent = stringifyTypedPipe(syncSet.getPipe().getPipe()); + parsed.senderPipe = disambiguateMTE2Pipe( + convertTypedPipe(syncSet.getTpipe().getPipe()), + convertTypedPipe(syncSet.getPipe().getPipe()), parsed.op.coreType); + parsed.receiverPipe = + disambiguateMTE2Pipe(convertTypedPipe(syncSet.getPipe().getPipe()), + parsed.senderPipe, parsed.op.coreType); + parsed.eventId = stringifyTypedFlag(syncSet.getStaticFlagId(), + syncSet.getDynamicFlagId()); + parsed.syncIdValue = syncSet.getDynamicFlagId(); + parsed.op.pipe = parsed.senderPipe; + return true; + } + if (auto syncWait = llvm::dyn_cast(op)) { + parsed.op.opName = "sync_block_wait"; + parsed.op.isSyncOp = true; + parsed.op.isBarrier = true; + parsed.syncCoreType = + mlir::hivm::stringifyTCoreType(syncWait.getTcoreType().getTcoretype()) + .str(); + parsed.op.coreType = parsed.syncCoreType; + parsed.senderEvent = stringifyTypedPipe(syncWait.getTpipe().getPipe()); + parsed.receiverEvent = stringifyTypedPipe(syncWait.getPipe().getPipe()); + // The sender pipe lives on the *opposite* core in cross-core sync. + llvm::StringRef senderCoreType = + (parsed.op.coreType == "CUBE" || parsed.op.coreType == "AIC") ? "VECTOR" + : "CUBE"; + parsed.senderPipe = disambiguateMTE2Pipe( + convertTypedPipe(syncWait.getTpipe().getPipe()), + convertTypedPipe(syncWait.getPipe().getPipe()), senderCoreType); + parsed.receiverPipe = + disambiguateMTE2Pipe(convertTypedPipe(syncWait.getPipe().getPipe()), + parsed.senderPipe, parsed.op.coreType); + parsed.eventId = stringifyTypedFlag(syncWait.getStaticFlagId(), + syncWait.getDynamicFlagId()); + parsed.syncIdValue = syncWait.getDynamicFlagId(); + parsed.op.pipe = HIVMPipe::All; + parsed.barrierPipes.push_back(HIVMPipe::All); + return true; + } + if (auto syncBlock = llvm::dyn_cast(op)) { + parsed.op.opName = "sync_block"; + parsed.op.isSyncOp = true; + parsed.op.isBarrier = true; + parsed.op.pipe = HIVMPipe::All; + if (auto inferredCore = syncBlock.inferCoreType()) + parsed.op.coreType = mlir::hivm::stringifyTCoreType(*inferredCore).str(); + return true; + } + + return startsWithHivmOp(op); +} +#endif + +static std::string stringifyAttribute(mlir::Attribute attr) { + if (!attr) + return ""; + std::string storage; + llvm::raw_string_ostream os(storage); + attr.print(os); + os.flush(); + return storage; +} + +static std::pair +parseLoadStoreSpaces(llvm::StringRef line); + +static HIVMPipe parsePipeToken(llvm::StringRef text) { + if (text.contains("PIPE_ALL")) + return HIVMPipe::All; + if (text.contains("PIPE_MTE3")) + return HIVMPipe::MTE3; + if (text.contains("PIPE_MTE2")) + return HIVMPipe::VectorMTE2; + if (text.contains("PIPE_MTE1")) + return HIVMPipe::MTE1; + if (text.contains("PIPE_FIX")) + return HIVMPipe::FixPipe; + if (text.contains("PIPE_M")) + return HIVMPipe::Cube; + if (text.contains("PIPE_V")) + return HIVMPipe::Vector; + if (text.contains("PIPE_S")) + return HIVMPipe::Scalar; + return HIVMPipe::Unknown; +} + +static std::string parseEventToken(llvm::StringRef text) { + size_t pos = text.find("EVENT_ID"); + if (pos == llvm::StringRef::npos) + return ""; + size_t end = pos; + while ( + end < text.size() && + (std::isalnum(static_cast(text[end])) || text[end] == '_')) + ++end; + return text.slice(pos, end).str(); +} + +#ifndef TRITONSIM_HAS_BISHENGIR_HIVM +static std::string inferGenericCoreType(mlir::Operation *op) { + if (auto parentFunc = op->getParentOfType()) { + std::string attr = + stringifyAttribute(parentFunc->getAttr("hivm.func_core_type")); + if (llvm::StringRef(attr).contains("AIC") || + llvm::StringRef(attr).contains("CUBE")) + return "CUBE"; + if (llvm::StringRef(attr).contains("AIV") || + llvm::StringRef(attr).contains("VECTOR")) + return "VECTOR"; + } + return resolveCoreTypeFromFunc(op, "").str(); +} + +static bool populateGenericHivmOp(mlir::Operation *op, ParsedOp &parsed) { + parsed.op.opName = getLeafOpName(op).str(); + parsed.op.coreType = inferGenericCoreType(op); + std::string opText = renderOperation(op); + auto spaces = parseLoadStoreSpaces(opText); + + if (parsed.op.opName == "load") { + parsed.op.pipe = selectMTE2PipeForSpaces(spaces.first, spaces.second, + parsed.op.coreType); + } else if (parsed.op.opName == "store") { + parsed.op.pipe = HIVMPipe::MTE3; + } else if (parsed.op.opName == "fixpipe") { + parsed.op.pipe = HIVMPipe::FixPipe; + } else if (parsed.op.opName == "nd2nz") { + parsed.op.pipe = HIVMPipe::CubeMTE2; + } else if (parsed.op.opName == "nz2nd") { + parsed.op.pipe = HIVMPipe::MTE3; + } else if (parsed.op.opName == "copy") { + if (spaces.second == "gm") + parsed.op.pipe = HIVMPipe::MTE3; + else if (spaces.second == "l1") + parsed.op.pipe = HIVMPipe::CubeMTE2; + else + parsed.op.pipe = HIVMPipe::Vector; + } else if (parsed.op.opName == "pointer_cast" || + parsed.op.opName == "convert_layout") { + parsed.op.pipe = HIVMPipe::Unknown; + } else if (parsed.op.opName == "matmul" || parsed.op.opName == "mix_matmul" || + parsed.op.opName == "mix_group_matmul" || + parsed.op.opName == "mmadL1") { + parsed.op.pipe = HIVMPipe::Cube; + } else { + parsed.op.pipe = HIVMPipe::Vector; + } + + if (parsed.op.opName == "pipe_barrier") { + parsed.op.isSyncOp = true; + parsed.op.isBarrier = true; + parsed.op.pipe = disambiguateMTE2Pipe( + parsePipeToken(stringifyAttribute(op->getAttr("pipe"))), + HIVMPipe::Unknown, parsed.op.coreType); + parsed.barrierPipes.push_back(parsed.op.pipe); + return true; + } + if (parsed.op.opName == "set_flag" || parsed.op.opName == "wait_flag") { + parsed.op.isSyncOp = true; + HIVMPipe setPipe = + parsePipeToken(stringifyAttribute(op->getAttr("set_pipe"))); + HIVMPipe waitPipe = + parsePipeToken(stringifyAttribute(op->getAttr("wait_pipe"))); + parsed.senderPipe = + disambiguateMTE2Pipe(setPipe, waitPipe, parsed.op.coreType); + parsed.receiverPipe = + disambiguateMTE2Pipe(waitPipe, parsed.senderPipe, parsed.op.coreType); + parsed.eventId = + parseEventToken(stringifyAttribute(op->getAttr("static_event_id"))); + parsed.op.pipe = parsed.op.opName == "set_flag" ? parsed.senderPipe + : parsed.receiverPipe; + attachSyncMetadata(parsed); + return true; + } + + return true; +} +#endif + +static std::string renderValueToken(mlir::Value value) { + if (!value) + return ""; + std::string storage; + llvm::raw_string_ostream os(storage); + os << value; + os.flush(); + return storage; +} + +static std::string renderOpaqueValueToken(mlir::Value value) { + if (!value) + return ""; + std::string storage; + llvm::raw_string_ostream os(storage); + os << "ssa@" << value.getAsOpaquePointer(); + os.flush(); + return storage; +} + +static std::optional +evaluateAffineExpr(mlir::AffineExpr expr, mlir::AffineMap map, + llvm::ArrayRef inputs); + +static bool resolveMLIRValueImpl(mlir::Value value, const AnalysisState &state, + int64_t &resolved, + llvm::SmallDenseSet &visited); + +static bool resolveAffineApply(mlir::affine::AffineApplyOp affineApply, + const AnalysisState &state, int64_t &resolved, + llvm::SmallDenseSet &visited) { + llvm::SmallVector inputs; + inputs.reserve(affineApply.getOperands().size()); + for (mlir::Value operand : affineApply.getOperands()) { + int64_t operandValue = 0; + if (!resolveMLIRValueImpl(operand, state, operandValue, visited)) + return false; + inputs.push_back(operandValue); + } + auto result = evaluateAffineExpr(affineApply.getAffineMap().getResult(0), + affineApply.getAffineMap(), inputs); + if (!result) + return false; + resolved = *result; + return true; +} + +static bool resolveMLIRValueImpl(mlir::Value value, const AnalysisState &state, + int64_t &resolved, + llvm::SmallDenseSet &visited) { + if (!visited.insert(value).second) + return false; + auto finish = [&](bool ok) { + visited.erase(value); + return ok; + }; + + auto cstIt = state.constants.find(value); + if (cstIt != state.constants.end()) { + resolved = cstIt->second; + return finish(true); + } + auto boundIt = state.boundValues.find(value); + if (boundIt != state.boundValues.end()) { + resolved = boundIt->second; + return finish(true); + } + mlir::Operation *defOp = value.getDefiningOp(); + if (!defOp || defOp->getNumResults() != 1) + return finish(false); + + if (auto constantOp = llvm::dyn_cast(defOp)) { + if (auto intAttr = + llvm::dyn_cast(constantOp.getValue())) { + resolved = intAttr.getInt(); + return finish(true); + } + } + if (auto castOp = llvm::dyn_cast(defOp)) + return finish( + resolveMLIRValueImpl(castOp.getIn(), state, resolved, visited)); + if (auto castOp = llvm::dyn_cast(defOp)) + return finish( + resolveMLIRValueImpl(castOp.getIn(), state, resolved, visited)); + if (auto truncOp = llvm::dyn_cast(defOp)) + return finish( + resolveMLIRValueImpl(truncOp.getIn(), state, resolved, visited)); + if (auto extOp = llvm::dyn_cast(defOp)) + return finish( + resolveMLIRValueImpl(extOp.getIn(), state, resolved, visited)); + if (auto addOp = llvm::dyn_cast(defOp)) { + int64_t lhs = 0, rhs = 0; + if (!resolveMLIRValueImpl(addOp.getLhs(), state, lhs, visited) || + !resolveMLIRValueImpl(addOp.getRhs(), state, rhs, visited)) + return finish(false); + resolved = lhs + rhs; + return finish(true); + } + if (auto subOp = llvm::dyn_cast(defOp)) { + int64_t lhs = 0, rhs = 0; + if (!resolveMLIRValueImpl(subOp.getLhs(), state, lhs, visited) || + !resolveMLIRValueImpl(subOp.getRhs(), state, rhs, visited)) + return finish(false); + resolved = lhs - rhs; + return finish(true); + } + if (auto mulOp = llvm::dyn_cast(defOp)) { + int64_t lhs = 0, rhs = 0; + bool lhsResolved = + resolveMLIRValueImpl(mulOp.getLhs(), state, lhs, visited); + if (lhsResolved && lhs == 0) { + resolved = 0; + return finish(true); + } + bool rhsResolved = + resolveMLIRValueImpl(mulOp.getRhs(), state, rhs, visited); + if (rhsResolved && rhs == 0) { + resolved = 0; + return finish(true); + } + if (!lhsResolved || !rhsResolved) + return finish(false); + resolved = lhs * rhs; + return finish(true); + } + if (auto divOp = llvm::dyn_cast(defOp)) { + int64_t lhs = 0; + if (!resolveMLIRValueImpl(divOp.getLhs(), state, lhs, visited)) + return finish(false); + if (lhs == 0) { + resolved = 0; + return finish(true); + } + int64_t rhs = 0; + if (!resolveMLIRValueImpl(divOp.getRhs(), state, rhs, visited) || rhs == 0) + return finish(false); + resolved = lhs / rhs; + return finish(true); + } + if (auto remOp = llvm::dyn_cast(defOp)) { + int64_t lhs = 0; + if (!resolveMLIRValueImpl(remOp.getLhs(), state, lhs, visited)) + return finish(false); + if (lhs == 0) { + resolved = 0; + return finish(true); + } + int64_t rhs = 0; + if (!resolveMLIRValueImpl(remOp.getRhs(), state, rhs, visited) || rhs == 0) + return finish(false); + resolved = lhs % rhs; + return finish(true); + } + if (auto minOp = llvm::dyn_cast(defOp)) { + int64_t lhs = 0, rhs = 0; + if (!resolveMLIRValueImpl(minOp.getLhs(), state, lhs, visited) || + !resolveMLIRValueImpl(minOp.getRhs(), state, rhs, visited)) + return finish(false); + resolved = std::min(lhs, rhs); + return finish(true); + } + if (auto cmpOp = llvm::dyn_cast(defOp)) { + int64_t lhs = 0, rhs = 0; + if (!resolveMLIRValueImpl(cmpOp.getLhs(), state, lhs, visited) || + !resolveMLIRValueImpl(cmpOp.getRhs(), state, rhs, visited)) + return finish(false); + switch (cmpOp.getPredicate()) { + case mlir::arith::CmpIPredicate::eq: + resolved = lhs == rhs; + return finish(true); + case mlir::arith::CmpIPredicate::ne: + resolved = lhs != rhs; + return finish(true); + case mlir::arith::CmpIPredicate::slt: + resolved = lhs < rhs; + return finish(true); + case mlir::arith::CmpIPredicate::sle: + resolved = lhs <= rhs; + return finish(true); + case mlir::arith::CmpIPredicate::sgt: + resolved = lhs > rhs; + return finish(true); + case mlir::arith::CmpIPredicate::sge: + resolved = lhs >= rhs; + return finish(true); + case mlir::arith::CmpIPredicate::ult: + resolved = static_cast(lhs) < static_cast(rhs); + return finish(true); + case mlir::arith::CmpIPredicate::ule: + resolved = static_cast(lhs) <= static_cast(rhs); + return finish(true); + case mlir::arith::CmpIPredicate::ugt: + resolved = static_cast(lhs) > static_cast(rhs); + return finish(true); + case mlir::arith::CmpIPredicate::uge: + resolved = static_cast(lhs) >= static_cast(rhs); + return finish(true); + } + } + if (auto selectOp = llvm::dyn_cast(defOp)) { + int64_t cond = 0, trueValue = 0, falseValue = 0; + if (!resolveMLIRValueImpl(selectOp.getCondition(), state, cond, visited) || + !resolveMLIRValueImpl(selectOp.getTrueValue(), state, trueValue, + visited) || + !resolveMLIRValueImpl(selectOp.getFalseValue(), state, falseValue, + visited)) + return finish(false); + resolved = cond != 0 ? trueValue : falseValue; + return finish(true); + } + if (auto affineApply = llvm::dyn_cast(defOp)) + return finish(resolveAffineApply(affineApply, state, resolved, visited)); + + return finish(false); +} + +static bool resolveMLIRValue(mlir::Value value, const AnalysisState &state, + int64_t &resolved) { + llvm::SmallDenseSet visited; + return resolveMLIRValueImpl(value, state, resolved, visited); +} + +static std::string canonicalizeSyncId(mlir::Value value, + const AnalysisState &state) { + if (!value) + return ""; + + int64_t resolved = 0; + if (resolveMLIRValue(value, state, resolved)) + return std::to_string(resolved); + + auto producerIt = state.valueProducers.find(value); + if (producerIt != state.valueProducers.end()) + return ("ssa_producer_" + std::to_string(producerIt->second)); + + return renderOpaqueValueToken(value); +} + +static bool parseForTripCount(mlir::scf::ForOp forOp, + const AnalysisState &state, int64_t &tripCount) { + int64_t lb = 0; + int64_t ub = 0; + int64_t step = 0; + if (!resolveMLIRValue(forOp.getLowerBound(), state, lb) || + !resolveMLIRValue(forOp.getUpperBound(), state, ub) || + !resolveMLIRValue(forOp.getStep(), state, step) || step <= 0 || ub < lb) { + tripCount = 1; + return false; + } + tripCount = std::max(1, ceilDiv(ub - lb, step)); + return true; +} + +static bool captureConstant(mlir::Operation *op, AnalysisState &state) { + auto constantOp = llvm::dyn_cast(op); + if (!constantOp || op->getNumResults() != 1) + return false; + + mlir::Attribute valueAttr = constantOp.getValue(); + if (auto intAttr = llvm::dyn_cast(valueAttr)) { + state.constants[op->getResult(0)] = intAttr.getInt(); + return true; + } + return false; +} + +static std::optional +evaluateAffineExpr(mlir::AffineExpr expr, mlir::AffineMap map, + llvm::ArrayRef inputs); + +static bool resolveMLIRValue(mlir::Value value, const AnalysisState &state, + int64_t &resolved); + +static std::optional +evaluateAffineExpr(mlir::AffineExpr expr, mlir::AffineMap map, + llvm::ArrayRef inputs) { + if (auto constant = llvm::dyn_cast(expr)) + return constant.getValue(); + if (auto dim = llvm::dyn_cast(expr)) { + unsigned pos = dim.getPosition(); + if (pos < inputs.size()) + return inputs[pos]; + return std::nullopt; + } + if (auto symbol = llvm::dyn_cast(expr)) { + unsigned pos = map.getNumDims() + symbol.getPosition(); + if (pos < inputs.size()) + return inputs[pos]; + return std::nullopt; + } + if (auto binary = llvm::dyn_cast(expr)) { + auto lhs = evaluateAffineExpr(binary.getLHS(), map, inputs); + auto rhs = evaluateAffineExpr(binary.getRHS(), map, inputs); + if (!lhs || !rhs) + return std::nullopt; + switch (binary.getKind()) { + case mlir::AffineExprKind::Add: + return *lhs + *rhs; + case mlir::AffineExprKind::Mul: + return *lhs * *rhs; + case mlir::AffineExprKind::Mod: + if (*rhs == 0) + return std::nullopt; + return *lhs % *rhs; + case mlir::AffineExprKind::FloorDiv: + if (*rhs == 0) + return std::nullopt; + return llvm::divideFloorSigned(*lhs, *rhs); + case mlir::AffineExprKind::CeilDiv: + if (*rhs == 0) + return std::nullopt; + return llvm::divideCeilSigned(*lhs, *rhs); + default: + return std::nullopt; + } + } + return std::nullopt; +} + +static bool captureDerivedScalarValue(mlir::Operation *op, + AnalysisState &state) { + if (op->getNumResults() != 1) + return false; + + auto recordValue = [&](int64_t value) { + state.boundValues[op->getResult(0)] = value; + return true; + }; + + if (op->getName().getStringRef() == "hivm.hir.get_block_idx" || + op->getName().getStringRef() == "get_block_idx") { + auto it = state.argBindings.find("pid_x"); + if (it == state.argBindings.end()) + it = state.argBindings.find("program_id_x"); + if (it != state.argBindings.end()) + return recordValue(it->second); + return false; + } + + if (auto castOp = llvm::dyn_cast(op)) { + int64_t resolved = 0; + if (resolveMLIRValue(castOp.getIn(), state, resolved)) + return recordValue(resolved); + return false; + } + + if (auto castOp = llvm::dyn_cast(op)) { + int64_t resolved = 0; + if (resolveMLIRValue(castOp.getIn(), state, resolved)) + return recordValue(resolved); + return false; + } + + if (auto truncOp = llvm::dyn_cast(op)) { + int64_t resolved = 0; + if (resolveMLIRValue(truncOp.getIn(), state, resolved)) + return recordValue(resolved); + return false; + } + + if (auto extOp = llvm::dyn_cast(op)) { + int64_t resolved = 0; + if (resolveMLIRValue(extOp.getIn(), state, resolved)) + return recordValue(resolved); + return false; + } + + if (auto addOp = llvm::dyn_cast(op)) { + int64_t lhs = 0, rhs = 0; + if (resolveMLIRValue(addOp.getLhs(), state, lhs) && + resolveMLIRValue(addOp.getRhs(), state, rhs)) + return recordValue(lhs + rhs); + return false; + } + + if (auto subOp = llvm::dyn_cast(op)) { + int64_t lhs = 0, rhs = 0; + if (resolveMLIRValue(subOp.getLhs(), state, lhs) && + resolveMLIRValue(subOp.getRhs(), state, rhs)) + return recordValue(lhs - rhs); + return false; + } + + if (auto mulOp = llvm::dyn_cast(op)) { + int64_t lhs = 0, rhs = 0; + if (resolveMLIRValue(mulOp.getLhs(), state, lhs) && + resolveMLIRValue(mulOp.getRhs(), state, rhs)) + return recordValue(lhs * rhs); + return false; + } + + if (auto divOp = llvm::dyn_cast(op)) { + int64_t lhs = 0, rhs = 0; + if (resolveMLIRValue(divOp.getLhs(), state, lhs) && + resolveMLIRValue(divOp.getRhs(), state, rhs) && rhs != 0) + return recordValue(lhs / rhs); + return false; + } + + if (auto remOp = llvm::dyn_cast(op)) { + int64_t lhs = 0, rhs = 0; + if (resolveMLIRValue(remOp.getLhs(), state, lhs) && + resolveMLIRValue(remOp.getRhs(), state, rhs) && rhs != 0) + return recordValue(lhs % rhs); + return false; + } + + if (auto minOp = llvm::dyn_cast(op)) { + int64_t lhs = 0, rhs = 0; + if (resolveMLIRValue(minOp.getLhs(), state, lhs) && + resolveMLIRValue(minOp.getRhs(), state, rhs)) + return recordValue(std::min(lhs, rhs)); + return false; + } + + if (auto cmpOp = llvm::dyn_cast(op)) { + int64_t lhs = 0, rhs = 0; + if (!resolveMLIRValue(cmpOp.getLhs(), state, lhs) || + !resolveMLIRValue(cmpOp.getRhs(), state, rhs)) + return false; + bool result = false; + switch (cmpOp.getPredicate()) { + case mlir::arith::CmpIPredicate::eq: + result = lhs == rhs; + break; + case mlir::arith::CmpIPredicate::ne: + result = lhs != rhs; + break; + case mlir::arith::CmpIPredicate::slt: + result = lhs < rhs; + break; + case mlir::arith::CmpIPredicate::sle: + result = lhs <= rhs; + break; + case mlir::arith::CmpIPredicate::sgt: + result = lhs > rhs; + break; + case mlir::arith::CmpIPredicate::sge: + result = lhs >= rhs; + break; + case mlir::arith::CmpIPredicate::ult: + result = static_cast(lhs) < static_cast(rhs); + break; + case mlir::arith::CmpIPredicate::ule: + result = static_cast(lhs) <= static_cast(rhs); + break; + case mlir::arith::CmpIPredicate::ugt: + result = static_cast(lhs) > static_cast(rhs); + break; + case mlir::arith::CmpIPredicate::uge: + result = static_cast(lhs) >= static_cast(rhs); + break; + } + return recordValue(result ? 1 : 0); + } + + if (auto selectOp = llvm::dyn_cast(op)) { + int64_t cond = 0, trueValue = 0, falseValue = 0; + if (resolveMLIRValue(selectOp.getCondition(), state, cond) && + resolveMLIRValue(selectOp.getTrueValue(), state, trueValue) && + resolveMLIRValue(selectOp.getFalseValue(), state, falseValue)) + return recordValue(cond != 0 ? trueValue : falseValue); + return false; + } + + if (auto affineApply = llvm::dyn_cast(op)) { + llvm::SmallVector inputs; + inputs.reserve(affineApply.getOperands().size()); + for (mlir::Value operand : affineApply.getOperands()) { + int64_t resolved = 0; + if (!resolveMLIRValue(operand, state, resolved)) + return false; + inputs.push_back(resolved); + } + auto result = evaluateAffineExpr(affineApply.getAffineMap().getResult(0), + affineApply.getAffineMap(), inputs); + if (result) + return recordValue(*result); + return false; + } + + return false; +} + +static std::string getOrCreateBufferRoot(mlir::Value value, + AnalysisState &state) { + auto it = state.bufferRoots.find(value); + if (it != state.bufferRoots.end()) + return it->second; + std::string root = renderValueToken(value); + state.bufferRoots[value] = root; + return root; +} + +static void captureBufferMetadata(mlir::Operation *op, AnalysisState &state) { +#ifdef TRITONSIM_HAS_BISHENGIR_HIVM + if (auto markOp = llvm::dyn_cast(op)) { + mlir::Value src = markOp.getSrc(); + if (markOp.isAnnotatedByStaticAttr("hivm.multi_buffer")) { + if (auto intAttr = llvm::dyn_cast_or_null( + markOp.getStaticAttrValue("hivm.multi_buffer"))) { + std::string root = getOrCreateBufferRoot(src, state); + state.bufferSlots[root] = std::max(1, intAttr.getInt()); + } + } + return; + } +#endif + + if (auto subviewOp = llvm::dyn_cast(op)) { + auto it = state.bufferRoots.find(subviewOp.getSource()); + if (it != state.bufferRoots.end()) + state.bufferRoots[subviewOp.getResult()] = it->second; + return; + } + if (auto castOp = llvm::dyn_cast(op)) { + auto it = state.bufferRoots.find(castOp.getSource()); + if (it != state.bufferRoots.end()) + state.bufferRoots[castOp.getResult()] = it->second; + return; + } + if (auto castOp = llvm::dyn_cast(op)) { + auto it = state.bufferRoots.find(castOp.getSource()); + if (it != state.bufferRoots.end()) + state.bufferRoots[castOp.getResult()] = it->second; + return; + } +} + +static void dedupeBufferList(std::vector &buffers) { + std::sort(buffers.begin(), buffers.end()); + buffers.erase(std::unique(buffers.begin(), buffers.end()), buffers.end()); +} + +static bool isLikelyWritingOp(llvm::StringRef opName) { + return opName == "load" || opName == "copy" || opName == "vadd" || + opName == "vmul" || opName == "vcast" || opName == "vbrc" || + opName == "vreduce" || opName == "fixpipe" || opName == "nd2nz" || + opName == "nz2nd" || opName == "mmadL1" || opName == "matmul" || + opName == "mix_matmul" || opName == "mix_group_matmul"; +} + +static void attachBufferAccessMetadata(mlir::Operation *op, ParsedOp &parsed, + AnalysisState &state) { + if (parsed.op.opName == "pointer_cast" && op->getNumResults() == 1 && + mlir::isa(op->getResult(0).getType())) { + getOrCreateBufferRoot(op->getResult(0), state); + } + + llvm::SmallVector rootedOperands; + for (mlir::Value operand : op->getOperands()) { + auto it = state.bufferRoots.find(operand); + if (it != state.bufferRoots.end()) + rootedOperands.push_back(it->second); + } + + if (rootedOperands.empty()) + return; + + if (isLikelyWritingOp(parsed.op.opName)) { + parsed.op.writeBuffers.push_back(rootedOperands.back()); + parsed.op.multiBufferSlots = std::max( + parsed.op.multiBufferSlots, state.bufferSlots[rootedOperands.back()]); + rootedOperands.pop_back(); + } + + for (const std::string &root : rootedOperands) + parsed.op.readBuffers.push_back(root); + dedupeBufferList(parsed.op.readBuffers); + dedupeBufferList(parsed.op.writeBuffers); +} + +static std::pair +parseLoadStoreSpaces(llvm::StringRef line) { + llvm::SmallVector spaces; + size_t pos = 0; + while (spaces.size() < 2) { + pos = line.find("#hivm.address_space<", pos); + if (pos == llvm::StringRef::npos) + break; + pos += strlen("#hivm.address_space<"); + size_t end = line.find('>', pos); + if (end == llvm::StringRef::npos) + break; + spaces.push_back(canonicalizeAddressSpace(line.slice(pos, end))); + pos = end + 1; + } + if (spaces.size() < 2) + return {"", ""}; + return {spaces[0], spaces[1]}; +} + +static std::map +parseArgBindings(llvm::StringRef bindings) { + std::map result; + llvm::SmallVector entries; + bindings.split(entries, ',', -1, false); + for (llvm::StringRef entry : entries) { + auto kv = trim(entry).split('='); + if (kv.first.empty() || kv.second.empty()) + continue; + int64_t value = 0; + if (trim(kv.second).getAsInteger(10, value)) + continue; + result[trim(kv.first).str()] = value; + } + return result; +} + +static bool isZeroCostOp(llvm::StringRef opName) { + return opName == "pointer_cast" || opName == "convert_layout"; +} + +static bool isCubeOpName(llvm::StringRef opName) { + return opName == "matmul" || opName == "mix_matmul" || + opName == "mix_group_matmul" || opName == "mmadL1"; +} + +static int64_t estimateDuration(const ParsedOp &parsed, + const HardwareConfig &config) { + llvm::StringRef opName = parsed.op.opName; + llvm::StringRef line = parsed.op.text; + + auto startupForPipe = [&](HIVMPipe pipe) -> int64_t { + switch (pipe) { + case HIVMPipe::Vector: + return config.getVectorStartupLatency(); + case HIVMPipe::VectorMTE2: + case HIVMPipe::CubeMTE2: + return config.getMTE2StartupLatency(); + case HIVMPipe::MTE3: + return config.getMTE3StartupLatency(); + case HIVMPipe::FixPipe: + return config.getFixPipeStartupLatency(); + case HIVMPipe::Cube: + return config.getCubeStartupLatency(); + case HIVMPipe::MTE1: + return std::max(1, config.getMTE2StartupLatency() / 2); + case HIVMPipe::Scalar: + return 1; + case HIVMPipe::All: + return config.getPipeBarrierCyclesPerIter(); + case HIVMPipe::Unknown: + return 1; + } + return 1; + }; + + auto estimateSpaceTransferCycles = + [&](llvm::StringRef srcSpace, llvm::StringRef dstSpace, int64_t bytes, + int64_t startup) -> int64_t { + int64_t clampedBytes = std::max(bytes, 1); + auto chooseBandwidth = [&](llvm::StringRef name) { + double bw = config.getMemoryBandwidthBytesPerCycle(name); + return bw > 0.0 ? bw : 0.0; + }; + double srcBw = chooseBandwidth(srcSpace); + double dstBw = chooseBandwidth(dstSpace); + double bw = 0.0; + if (srcBw > 0.0 && dstBw > 0.0) + bw = std::min(srcBw, dstBw); + else + bw = std::max(srcBw, dstBw); + if (bw <= 0.0) + bw = static_cast(config.getVectorWidthBytes()); + int latency = std::max(config.getMemoryLatencyCycles(srcSpace), + config.getMemoryLatencyCycles(dstSpace)); + int64_t transfer = std::max( + 1, static_cast(std::ceil(clampedBytes / bw))); + return startup + latency + transfer; + }; + + if (isZeroCostOp(opName)) + return 0; + + if (opName == "set_mask_norm" || opName == "get_block_idx") + return 1; + + if (opName == "set_flag") + return 1; + if (opName == "wait_flag") + return 2; + if (opName == "sync_block_set") { + HIVMPipe sender = parsed.senderPipe; + HIVMPipe receiver = parsed.receiverPipe; + int64_t crossPipe = sender != receiver ? 1 : 0; + int64_t crossCore = parsed.op.coreType == "CUBE" ? 2 : 1; + return 1 + crossPipe + crossCore; + } + if (opName == "sync_block_wait") { + HIVMPipe receiver = parsed.receiverPipe; + int64_t base = std::max(2, startupForPipe(receiver) / 8); + if (parsed.op.coreType == "CUBE") + base += 2; + return base; + } + if (opName == "sync_block") + return std::max(config.getPipeBarrierCyclesPerIter(), + config.getVectorStartupLatency()); + + // pipe_barrier drains the in-flight instructions on the target pipe. + // Cost is the pipeline depth, NOT the full startup latency. + if (opName == "pipe_barrier") { + if (parsed.barrierPipes.empty()) + return 8; + if (llvm::is_contained(parsed.barrierPipes, HIVMPipe::All)) + return 64; + HIVMPipe pipe = parsed.barrierPipes.front(); + switch (pipe) { + case HIVMPipe::Vector: + return 4; + case HIVMPipe::VectorMTE2: + case HIVMPipe::CubeMTE2: + return 16; + case HIVMPipe::MTE3: + return 16; + case HIVMPipe::FixPipe: + return 8; + case HIVMPipe::Cube: + return 8; + default: + return 8; + } + } + + if (opName == "load") { + int64_t bytes = parsed.op.bytes; + auto spaces = parseLoadStoreSpaces(line); + if (spaces.first == "gm" && spaces.second == "ub") + return config.getMTE2StartupLatency() + + config.estimateMemoryCycles("vector_mte2", + std::max(bytes, 1)); + if (spaces.first == "gm" && spaces.second == "l1") + return config.getMTE2StartupLatency() + + config.estimateMemoryCycles("cube_mte2", + std::max(bytes, 1)); + return config.getMTE2StartupLatency() + + config.estimateMemoryCyclesWithLatency("hbm", + std::max(bytes, 1)); + } + + if (opName == "store") { + int64_t bytes = parsed.op.bytes; + auto spaces = parseLoadStoreSpaces(line); + if (spaces.first == "ub" && spaces.second == "gm") + return config.getMTE3StartupLatency() + + config.estimateMemoryCycles("mte3", std::max(bytes, 1)); + if (spaces.first == "l0c" && spaces.second == "gm") + return config.getFixPipeStartupLatency() + + config.estimateMemoryCycles("fixpipe", + std::max(bytes, 1)); + return config.getMTE3StartupLatency() + + config.estimateMemoryCyclesWithLatency("hbm", + std::max(bytes, 1)); + } + + if (opName == "copy") { + int64_t bytes = + std::max(parsed.op.bytes, config.getVectorWidthBytes()); + auto spaces = parseLoadStoreSpaces(line); + if (spaces.first == "ub" && spaces.second == "ub") + return estimateSpaceTransferCycles("ub", "ub", bytes, + config.getVectorStartupLatency()); + if (spaces.first == "gm" && spaces.second == "l1") + return config.getMTE2StartupLatency() + + config.estimateMemoryCycles("cube_mte2", bytes); + if (spaces.first == "ub" && spaces.second == "l1") + return estimateSpaceTransferCycles("ub", "l1", bytes, + config.getMTE3StartupLatency()); + if (spaces.first == "l0c" && spaces.second == "gm") + return config.getFixPipeStartupLatency() + + config.estimateMemoryCycles("fixpipe", bytes); + return estimateSpaceTransferCycles("ub", "ub", bytes, + startupForPipe(parsed.op.pipe)); + } + + auto vectorCycles = [&](int opCost) -> int64_t { + int64_t elems = + std::max(parsed.op.elements, config.getVectorWidthElements()); + return config.getVectorStartupLatency() + + ceilDiv(elems, config.getVectorWidthElements()) * opCost; + }; + + auto isVectorALUOp = [&](llvm::StringRef name) { + return name == "vadd" || name == "vmul" || name == "vsub" || + name == "vcast" || name == "vexp" || name == "vdiv" || + name == "vlog" || name == "vsqrt" || name == "vrsqrt" || + name == "vtanh" || name == "vsigmoid" || name == "vreduce"; + }; + if (isVectorALUOp(opName)) + return vectorCycles(config.getVectorOpCyclesPerInstruction(opName)); + if (opName == "vbrc") { + if (parsed.op.pipe == HIVMPipe::VectorMTE2 || + parsed.op.pipe == HIVMPipe::CubeMTE2) { + int64_t bytes = + std::max(parsed.op.bytes, config.getVectorWidthBytes()); + return estimateSpaceTransferCycles( + "ub", parsed.op.pipe == HIVMPipe::CubeMTE2 ? "l1" : "ub", bytes, + config.getMTE2StartupLatency()); + } + return vectorCycles(1); + } + if (opName == "fixpipe") { + int64_t bytes = std::max(parsed.op.bytes, 1); + auto spaces = parseLoadStoreSpaces(line); + if (spaces.second == "ub") + return estimateSpaceTransferCycles("l0c", "ub", bytes, + config.getFixPipeStartupLatency()); + if (spaces.second == "gm") + return config.getFixPipeStartupLatency() + + config.estimateMemoryCycles("fixpipe", bytes); + return config.getFixPipeStartupLatency() + + config.estimateMemoryCycles("fixpipe", bytes); + } + + if (opName == "nd2nz") { + return estimateND2NZCycles(parsed, config); + } + + if (opName == "nz2nd") { + int64_t bytes = std::max(parsed.op.bytes, 1); + return config.getMTE3StartupLatency() + + config.estimateMemoryCycles("mte3", bytes); + } + + if (isCubeOpName(opName)) { + int64_t totalElements = 0; + size_t memrefCount = 0; + size_t searchPos = 0; + while ((searchPos = line.find("memref<", searchPos)) != + llvm::StringRef::npos && + memrefCount < 3) { + totalElements += parseMemRefElementCount(line.drop_front(searchPos)); + ++memrefCount; + searchPos += 7; + } + if (memrefCount >= 3) { + int64_t lhs = std::max(1, totalElements / 3); + int64_t rhs = std::max(1, totalElements / 3); + int64_t out = std::max(1, totalElements - lhs - rhs); + int64_t M = std::max(1, static_cast(std::sqrt(out))); + int64_t N = std::max(1, out / M); + int64_t K = std::max(1, lhs / M); + return config.getCubeStartupLatency() + + config.estimateCubeCycles(M, N, K); + } + return config.getCubeStartupLatency() + 16; + } + + return 1; +} + +static void +addLatestPipeDependency(HIVMPipe pipe, + const std::map &latestPipeProducer, + ParsedOp &parsed) { + auto it = latestPipeProducer.find(pipe); + if (it != latestPipeProducer.end()) + parsed.op.dependsOn.push_back(it->second); +} + +static bool pipeBelongsToCore(HIVMPipe pipe, llvm::StringRef coreType) { + bool isCubeCore = coreType == "CUBE" || coreType == "AIC"; + bool isVectorCore = coreType == "VECTOR" || coreType == "AIV"; + if (isCubeCore) { + return pipe == HIVMPipe::Cube || pipe == HIVMPipe::MTE1 || + pipe == HIVMPipe::CubeMTE2 || pipe == HIVMPipe::FixPipe || + pipe == HIVMPipe::Scalar; + } + if (isVectorCore) { + return pipe == HIVMPipe::Vector || pipe == HIVMPipe::VectorMTE2 || + pipe == HIVMPipe::MTE3 || pipe == HIVMPipe::Scalar; + } + return false; +} + +static llvm::SmallVector +getCoreBarrierPipes(llvm::StringRef coreType) { + if (coreType == "CUBE" || coreType == "AIC") + return {HIVMPipe::Cube, HIVMPipe::MTE1, HIVMPipe::CubeMTE2, + HIVMPipe::FixPipe, HIVMPipe::Scalar}; + if (coreType == "VECTOR" || coreType == "AIV") + return {HIVMPipe::Vector, HIVMPipe::VectorMTE2, HIVMPipe::MTE3, + HIVMPipe::Scalar}; + return {}; +} + +static void ingestParsedOp(const ParsedOp &parsed, AnalysisState &state, + HIVMAnalysisReport &report, + const HardwareConfig &config) { + ParsedOp mutableParsed = parsed; + if (mutableParsed.syncIdValue) { + std::string canonicalSyncId = + canonicalizeSyncId(mutableParsed.syncIdValue, state); + if (!canonicalSyncId.empty()) + mutableParsed.op.eventId = canonicalSyncId; + } + EventKey opEventKey{mutableParsed.senderPipe, mutableParsed.receiverPipe, + mutableParsed.op.eventId}; + + // Enforce program order within each pipe: every op depends on the previous + // op on the same pipe, matching hardware sequential execution semantics. + if (mutableParsed.op.pipe != HIVMPipe::Unknown && + mutableParsed.op.pipe != HIVMPipe::All) + addLatestPipeDependency(mutableParsed.op.pipe, state.latestPipeProducer, + mutableParsed); + + mutableParsed.op.readBufferVersions.reserve( + mutableParsed.op.readBuffers.size()); + for (const std::string &root : mutableParsed.op.readBuffers) { + auto it = state.bufferVersions.find(root); + mutableParsed.op.readBufferVersions.push_back( + it != state.bufferVersions.end() ? it->second : 0); + } + mutableParsed.op.writeBufferVersions.reserve( + mutableParsed.op.writeBuffers.size()); + for (const std::string &root : mutableParsed.op.writeBuffers) { + int64_t nextVersion = state.bufferVersions[root] + 1; + state.bufferVersions[root] = nextVersion; + mutableParsed.op.writeBufferVersions.push_back(nextVersion); + } + + if (mutableParsed.op.isBarrier) { + if (mutableParsed.barrierPipes.empty() || + llvm::is_contained(mutableParsed.barrierPipes, HIVMPipe::All)) { + for (const auto &entry : state.latestPipeProducer) { + if (mutableParsed.op.pipe == HIVMPipe::All && + !mutableParsed.op.coreType.empty() && + !pipeBelongsToCore(entry.first, mutableParsed.op.coreType)) + continue; + mutableParsed.op.dependsOn.push_back(entry.second); + } + } else { + for (HIVMPipe pipe : mutableParsed.barrierPipes) + addLatestPipeDependency(pipe, state.latestPipeProducer, mutableParsed); + } + } + + if (mutableParsed.op.opName == "wait_flag" || + mutableParsed.op.opName == "sync_block_wait") { + auto genIt = state.eventGenerations.find(opEventKey); + if (genIt != state.eventGenerations.end()) + mutableParsed.op.eventGeneration = genIt->second; + auto eventIt = state.eventProducers.find(opEventKey); + if (eventIt != state.eventProducers.end()) + mutableParsed.op.dependsOn.push_back(eventIt->second); + addLatestPipeDependency(mutableParsed.op.pipe, state.latestPipeProducer, + mutableParsed); + } + + std::vector expandedOps = expandMacroOp(mutableParsed, config); + size_t firstExpandedId = report.operations.size(); + size_t previousExpandedId = std::numeric_limits::max(); + for (HIVMOp &expanded : expandedOps) { + expanded.id = report.operations.size(); + if (previousExpandedId != std::numeric_limits::max()) + expanded.dependsOn.push_back(previousExpandedId); + // Expanded sub-ops may land on a different pipe than the parsed op; + // enforce program order on each sub-op's actual pipe. + if (expanded.pipe != mutableParsed.op.pipe && + expanded.pipe != HIVMPipe::Unknown && expanded.pipe != HIVMPipe::All) { + auto it = state.latestPipeProducer.find(expanded.pipe); + if (it != state.latestPipeProducer.end()) + expanded.dependsOn.push_back(it->second); + } + previousExpandedId = expanded.id; + report.operations.push_back(std::move(expanded)); + } + + if (mutableParsed.op.opName == "set_flag" || + mutableParsed.op.opName == "sync_block_set") { + int64_t nextGeneration = state.eventGenerations[opEventKey] + 1; + state.eventGenerations[opEventKey] = nextGeneration; + mutableParsed.op.eventGeneration = nextGeneration; + for (size_t idx = firstExpandedId; + idx <= previousExpandedId && idx < report.operations.size(); ++idx) + report.operations[idx].eventGeneration = nextGeneration; + state.eventProducers[opEventKey] = firstExpandedId; + } + + for (mlir::Value result : mutableParsed.mlirResults) + state.valueProducers[result] = previousExpandedId; + + for (size_t idx = firstExpandedId; + idx <= previousExpandedId && idx < report.operations.size(); ++idx) { + HIVMOp &expanded = report.operations[idx]; + if (expanded.pipe == HIVMPipe::All && expanded.isBarrier) { + // A PIPE_ALL barrier blocks all pipes on its core. Register it as the + // latest producer for every pipe in that core so subsequent ops on any + // of those pipes depend on the barrier completing. + for (HIVMPipe p : getCoreBarrierPipes(expanded.coreType)) + state.latestPipeProducer[p] = expanded.id; + } else if (expanded.pipe != HIVMPipe::All && + expanded.pipe != HIVMPipe::Unknown) { + state.latestPipeProducer[expanded.pipe] = expanded.id; + } + } +} + +static void analyzeParsedOperation(mlir::Operation *op, int64_t loopMultiplier, + AnalysisState &state, + HIVMAnalysisReport &report, + const HardwareConfig &config, + bool replayIterations); + +static void analyzeParsedRegion(mlir::Region ®ion, int64_t loopMultiplier, + AnalysisState &state, + HIVMAnalysisReport &report, + const HardwareConfig &config, + bool replayIterations) { + for (mlir::Block &block : region) { + for (mlir::Operation &op : block) + analyzeParsedOperation(&op, loopMultiplier, state, report, config, + replayIterations); + } +} + +static void seedLoopCarriedState(mlir::scf::ForOp forOp, + const AnalysisState &parentState, + AnalysisState &loopState) { + mlir::Block &body = forOp.getRegion().front(); + mlir::Block::BlockArgListType bodyArgs = body.getArguments(); + unsigned iterArgOffset = 1; + for (auto [idx, initArg] : llvm::enumerate(forOp.getInitArgs())) { + if (iterArgOffset + idx >= bodyArgs.size()) + break; + mlir::BlockArgument regionArg = bodyArgs[iterArgOffset + idx]; + if (auto rootIt = parentState.bufferRoots.find(initArg); + rootIt != parentState.bufferRoots.end()) { + loopState.bufferRoots[regionArg] = rootIt->second; + } + if (auto producerIt = parentState.valueProducers.find(initArg); + producerIt != parentState.valueProducers.end()) { + loopState.valueProducers[regionArg] = producerIt->second; + } + if (auto constantIt = parentState.constants.find(initArg); + constantIt != parentState.constants.end()) { + loopState.constants[regionArg] = constantIt->second; + } + if (auto boundIt = parentState.boundValues.find(initArg); + boundIt != parentState.boundValues.end()) { + loopState.boundValues[regionArg] = boundIt->second; + } + } +} + +static void propagateLoopResults(mlir::scf::ForOp forOp, + const AnalysisState &loopState, + AnalysisState &parentState) { + auto yieldOp = llvm::dyn_cast( + forOp.getRegion().front().getTerminator()); + if (!yieldOp) + return; + + for (auto [idx, yielded] : llvm::enumerate(yieldOp.getOperands())) { + if (idx >= forOp.getNumResults()) + break; + mlir::Value result = forOp.getResult(idx); + if (auto producerIt = loopState.valueProducers.find(yielded); + producerIt != loopState.valueProducers.end()) { + parentState.valueProducers[result] = producerIt->second; + } + if (auto rootIt = loopState.bufferRoots.find(yielded); + rootIt != loopState.bufferRoots.end()) { + parentState.bufferRoots[result] = rootIt->second; + } + if (auto constantIt = loopState.constants.find(yielded); + constantIt != loopState.constants.end()) { + parentState.constants[result] = constantIt->second; + } + if (auto boundIt = loopState.boundValues.find(yielded); + boundIt != loopState.boundValues.end()) { + parentState.boundValues[result] = boundIt->second; + } + } + + parentState.bufferSlots = loopState.bufferSlots; + parentState.bufferVersions = loopState.bufferVersions; + parentState.eventProducers = loopState.eventProducers; + parentState.eventGenerations = loopState.eventGenerations; + parentState.latestPipeProducer = loopState.latestPipeProducer; +} + +static void advanceLoopCarriedState(mlir::scf::ForOp forOp, + AnalysisState &loopState) { + auto yieldOp = llvm::dyn_cast( + forOp.getRegion().front().getTerminator()); + if (!yieldOp) + return; + + mlir::Block &body = forOp.getRegion().front(); + mlir::Block::BlockArgListType bodyArgs = body.getArguments(); + unsigned iterArgOffset = 1; + for (auto [idx, yielded] : llvm::enumerate(yieldOp.getOperands())) { + if (iterArgOffset + idx >= bodyArgs.size()) + break; + mlir::BlockArgument regionArg = bodyArgs[iterArgOffset + idx]; + + if (auto producerIt = loopState.valueProducers.find(yielded); + producerIt != loopState.valueProducers.end()) { + loopState.valueProducers[regionArg] = producerIt->second; + } else { + loopState.valueProducers.erase(regionArg); + } + + if (auto rootIt = loopState.bufferRoots.find(yielded); + rootIt != loopState.bufferRoots.end()) { + loopState.bufferRoots[regionArg] = rootIt->second; + } else { + loopState.bufferRoots.erase(regionArg); + } + + if (auto constantIt = loopState.constants.find(yielded); + constantIt != loopState.constants.end()) { + loopState.constants[regionArg] = constantIt->second; + } else { + loopState.constants.erase(regionArg); + } + + if (auto boundIt = loopState.boundValues.find(yielded); + boundIt != loopState.boundValues.end()) { + loopState.boundValues[regionArg] = boundIt->second; + } else { + loopState.boundValues.erase(regionArg); + } + } +} + +static void analyzeParsedOperation(mlir::Operation *op, int64_t loopMultiplier, + AnalysisState &state, + HIVMAnalysisReport &report, + const HardwareConfig &config, + bool replayIterations) { + if (captureConstant(op, state)) + return; + captureDerivedScalarValue(op, state); + + captureBufferMetadata(op, state); + + if (auto funcOp = llvm::dyn_cast(op)) { + // Each function (AIC / AIV) runs on its own core in parallel. Use a + // fresh per-function state so that pipe-ordering and event tracking do + // not bleed across functions. Only preserve arg-bindings. + AnalysisState funcState; + funcState.argBindings = state.argBindings; + unsigned userArgIndex = 0; + for (auto [idx, arg] : llvm::enumerate(funcOp.getArguments())) { + if (funcOp.getArgAttr(idx, "hacc.arg_type")) + continue; + auto bindIt = + funcState.argBindings.find("arg" + std::to_string(userArgIndex++)); + if (bindIt != funcState.argBindings.end()) + funcState.boundValues[arg] = bindIt->second; + } + analyzeParsedRegion(funcOp.getBody(), loopMultiplier, funcState, report, + config, replayIterations); + return; + } + + if (auto forOp = llvm::dyn_cast(op)) { + int64_t tripCount = 1; + bool hasConcreteTripCount = parseForTripCount(forOp, state, tripCount); + int64_t lowerBound = 0; + int64_t step = 1; + bool hasConcreteInductionValue = + resolveMLIRValue(forOp.getLowerBound(), state, lowerBound) && + resolveMLIRValue(forOp.getStep(), state, step); + int64_t nestedMultiplier = loopMultiplier * std::max(tripCount, 1); + report.maxLoopMultiplier = + std::max(report.maxLoopMultiplier, nestedMultiplier); + AnalysisState loopState = state; + seedLoopCarriedState(forOp, state, loopState); + if (replayIterations && hasConcreteTripCount && tripCount > 1) { + for (int64_t iter = 0; iter < tripCount; ++iter) { + if (hasConcreteInductionValue) + loopState.boundValues[forOp.getInductionVar()] = + lowerBound + iter * step; + analyzeParsedRegion(op->getRegion(0), loopMultiplier, loopState, report, + config, replayIterations); + if (iter + 1 < tripCount) + advanceLoopCarriedState(forOp, loopState); + } + } else { + if (hasConcreteInductionValue) + loopState.boundValues[forOp.getInductionVar()] = lowerBound; + int64_t bodyMultiplier = (replayIterations && hasConcreteTripCount) + ? loopMultiplier + : nestedMultiplier; + analyzeParsedRegion(op->getRegion(0), bodyMultiplier, loopState, report, + config, replayIterations); + } + propagateLoopResults(forOp, loopState, state); + return; + } + + if (startsWithHivmOp(op)) { + std::string opText = renderOperation(op); + ParsedOp parsed; +#ifdef TRITONSIM_HAS_BISHENGIR_HIVM + if (!populateTypedHivmOp(op, parsed)) + return; + parsed.op.lineNumber = getLineNumberFromLocation(op->getLoc()); +#else + if (!populateGenericHivmOp(op, parsed)) + return; + parsed.op.lineNumber = getLineNumberFromLocation(op->getLoc()); +#endif + parsed.op.loopMultiplier = loopMultiplier; + parsed.op.text = opText; + if (parsed.op.opName.empty()) + parsed.op.opName = getLeafOpName(op).str(); + parsed.mlirResults.assign(op->result_begin(), op->result_end()); + + for (mlir::Value operand : op->getOperands()) { + auto it = state.valueProducers.find(operand); + if (it != state.valueProducers.end()) + parsed.op.dependsOn.push_back(it->second); + } + + if (op->getNumResults() > 0) { + parsed.op.bytes = inferValueBytes(op->getResult(0)); + parsed.op.elements = inferValueElements(op->getResult(0)); + } + if (parsed.op.bytes == 0 || parsed.op.elements == 0) { + for (mlir::Value operand : op->getOperands()) { + parsed.op.bytes = std::max(parsed.op.bytes, inferValueBytes(operand)); + parsed.op.elements = + std::max(parsed.op.elements, inferValueElements(operand)); + } + } + if (parsed.op.bytes == 0) + parsed.op.bytes = parseMemRefBytes(parsed.op.text); + if (parsed.op.elements == 0) + parsed.op.elements = parseMemRefElementCount(parsed.op.text); + attachSyncMetadata(parsed); + attachBufferAccessMetadata(op, parsed, state); + parsed.op.duration = estimateDuration(parsed, config); + ingestParsedOp(parsed, state, report, config); + } else if (!llvm::isa(op) && + !llvm::isa(op) && op->getNumResults() > 0) { + // Non-hivm ops with results are scalar operations (arith, memref, etc.) + // that execute on PIPE_S. They produce SSA values consumed by hivm.hir + // ops, creating cross-pipe dependencies via def-use chains. + ParsedOp parsed; + parsed.op.opName = getLeafOpName(op).str(); + parsed.op.pipe = HIVMPipe::Scalar; + parsed.op.loopMultiplier = loopMultiplier; + parsed.op.lineNumber = getLineNumberFromLocation(op->getLoc()); + parsed.op.text = renderOperation(op); + parsed.op.duration = 1; // scalar ops take 1 cycle + parsed.mlirResults.assign(op->result_begin(), op->result_end()); + + // Determine core type from function context + if (auto parentFunc = op->getParentOfType()) { + llvm::StringRef funcName = parentFunc.getName(); + if (funcName.contains("aic") || funcName.contains("AIC") || + funcName.contains("cube")) + parsed.op.coreType = "CUBE"; + else if (funcName.contains("aiv") || funcName.contains("AIV") || + funcName.contains("vector") || funcName.contains("mix")) + parsed.op.coreType = "VECTOR"; + } + + // SSA dependencies: if this op uses a value produced by a previous op + for (mlir::Value operand : op->getOperands()) { + auto it = state.valueProducers.find(operand); + if (it != state.valueProducers.end()) + parsed.op.dependsOn.push_back(it->second); + } + + ingestParsedOp(parsed, state, report, config); + } + + for (mlir::Region ®ion : op->getRegions()) + analyzeParsedRegion(region, loopMultiplier, state, report, config, + replayIterations); +} + +static void finalizeScheduledReport(HIVMAnalysisReport &report, + const HardwareConfig &config) { + std::map pipeAvailableAt; + for (HIVMOp &op : report.operations) { + int64_t earliest = 0; + for (size_t depId : op.dependsOn) { + if (depId < report.operations.size()) + earliest = std::max(earliest, report.operations[depId].endCycle); + } + + if (op.isBarrier) { + int64_t start = earliest; + if (op.pipe == HIVMPipe::All) { + for (const auto &entry : pipeAvailableAt) + start = std::max(start, entry.second); + op.startCycle = start; + op.endCycle = start + op.duration; + for (auto &entry : pipeAvailableAt) + entry.second = op.endCycle; + } else { + start = std::max(start, pipeAvailableAt[op.pipe]); + op.startCycle = start; + op.endCycle = start + op.duration; + pipeAvailableAt[op.pipe] = op.endCycle; + } + } else if (op.pipe == HIVMPipe::Unknown) { + op.startCycle = earliest; + op.endCycle = earliest + op.duration; + } else { + int64_t start = std::max(earliest, pipeAvailableAt[op.pipe]); + op.startCycle = start; + op.endCycle = start + op.duration; + pipeAvailableAt[op.pipe] = op.endCycle; + } + + report.oneIterationCycles = + std::max(report.oneIterationCycles, op.endCycle); + report.totalBusyCycles += op.duration; + report.opCount++; + if (op.pipe == HIVMPipe::Scalar && op.opName != "set_flag" && + op.opName != "wait_flag" && op.opName != "sync_block_set" && + op.opName != "sync_block_wait" && op.opName != "sync_block" && + op.opName != "pipe_barrier" && op.opName != "get_block_idx" && + op.opName != "get_block_num" && op.opName != "get_sub_block_idx" && + op.opName != "get_sub_block_num" && op.opName != "set_mask_norm" && + op.opName != "pointer_cast" && op.opName != "convert_layout") + report.unknownOpCount++; + if (op.isSyncOp) { + report.syncCycles += op.duration; + report.syncOpCount++; + } + if (op.isBarrier) { + report.barrierCycles += op.duration; + report.barrierCount++; + } + if (op.pipe != HIVMPipe::All && op.pipe != HIVMPipe::Unknown) { + report.pipeBusyCycles[op.pipe] += op.duration; + report.weightedPipeCycles[op.pipe] += op.duration * op.loopMultiplier; + } + } + + int64_t globalBarrierWeightedCycles = 0; + for (const HIVMOp &op : report.operations) { + if (op.isBarrier && op.pipe == HIVMPipe::All) + globalBarrierWeightedCycles += op.duration * op.loopMultiplier; + } + for (const auto &entry : report.weightedPipeCycles) + report.weightedCycles = std::max(report.weightedCycles, entry.second); + report.weightedCycles += globalBarrierWeightedCycles; + if (report.weightedCycles == 0) + report.weightedCycles = report.oneIterationCycles; +} + +struct CompletionEvent { + int64_t time = 0; + size_t opId = 0; + + bool operator>(const CompletionEvent &other) const { + return std::tie(time, opId) > std::tie(other.time, other.opId); + } +}; + +struct BufferSlotState { + int64_t writableAt = 0; + int64_t readableAt = 0; + int64_t version = 0; +}; + +struct BufferRootState { + std::vector slots; + int64_t latestReadableAt = 0; + int64_t latestVersion = 0; + std::map versionReadableAt; + std::map versionToSlot; +}; + +static llvm::StringRef getSyncBlockSourceCore(const HIVMOp &op) { + bool isCubeCore = op.coreType == "CUBE" || op.coreType == "AIC"; + if (op.opName == "sync_block_set") + return isCubeCore ? "AIC" : "AIV"; + if (op.opName == "sync_block_wait") + return isCubeCore ? "AIV" : "AIC"; + return ""; +} + +static void normalizeSyncBlockGenerations(HIVMAnalysisReport &report) { + std::map, int64_t> setGeneration; + std::map, int64_t> waitGeneration; + for (HIVMOp &op : report.operations) { + if ((op.opName != "sync_block_set" && op.opName != "sync_block_wait") || + op.eventId.empty()) + continue; + llvm::StringRef sourceCore = getSyncBlockSourceCore(op); + if (sourceCore.empty()) + continue; + auto key = std::make_pair(op.eventId, sourceCore.str()); + if (op.opName == "sync_block_set") + op.eventGeneration = ++setGeneration[key]; + else + op.eventGeneration = ++waitGeneration[key]; + } +} + +/// After generation normalization, wire explicit dependency edges from each +/// sync_block_set to its matching sync_block_wait so the DES respects +/// cross-core ordering. Without this, the wait may be scheduled before the +/// set completes (they live in different func::FuncOps with independent state). +static void wireCrossCoreSyncDependencies(HIVMAnalysisReport &report) { + // Key: (eventId, sourceCore, generation) → set-op id + using SyncKey = std::tuple; + std::map setOpById; + for (HIVMOp &op : report.operations) { + if (op.opName != "sync_block_set" || op.eventId.empty()) + continue; + llvm::StringRef sourceCore = getSyncBlockSourceCore(op); + if (sourceCore.empty()) + continue; + SyncKey key{op.eventId, sourceCore.str(), op.eventGeneration}; + setOpById[key] = op.id; + } + for (HIVMOp &op : report.operations) { + if (op.opName != "sync_block_wait" || op.eventId.empty()) + continue; + llvm::StringRef sourceCore = getSyncBlockSourceCore(op); + if (sourceCore.empty()) + continue; + // sync_block_wait's sourceCore returns the core that *set* the flag + // (the opposite core), which matches the set-op's sourceCore. + SyncKey key{op.eventId, sourceCore.str(), op.eventGeneration}; + auto it = setOpById.find(key); + if (it != setOpById.end()) + op.dependsOn.push_back(it->second); + } +} + +static void finalizeDiscreteEventReport(HIVMAnalysisReport &report, + const HardwareConfig &config) { + normalizeSyncBlockGenerations(report); + wireCrossCoreSyncDependencies(report); + + const size_t numOps = report.operations.size(); + if (numOps == 0) { + report.weightedCycles = 0; + return; + } + + std::vector remainingDeps(numOps, 0); + std::vector readyAt(numOps, 0); + std::vector> successors(numOps); + std::vector queued(numOps, false); + std::vector started(numOps, false); + std::vector completed(numOps, false); + std::deque readyOps; + std::priority_queue, + std::greater> + completions; + std::map pipeAvailableAt; + std::map flagEventVisibleAt; + std::map blockSyncVisibleAt; + std::map bufferStates; + std::map>> + writeSlotAssignments; + size_t completedCount = 0; + + for (const HIVMOp &op : report.operations) { + for (const std::string &root : op.writeBuffers) { + auto &state = bufferStates[root]; + if (state.slots.empty()) { + int64_t count = std::max(1, op.multiBufferSlots); + for (int64_t i = 0; i < count; ++i) + state.slots.push_back(BufferSlotState{}); + } + state.versionReadableAt.emplace(0, 0); + } + for (const std::string &root : op.readBuffers) { + auto [it, inserted] = bufferStates.try_emplace(root, BufferRootState{}); + it->second.versionReadableAt.emplace(0, 0); + } + } + + for (size_t opId = 0; opId < numOps; ++opId) { + HIVMOp &op = report.operations[opId]; + remainingDeps[opId] = op.dependsOn.size(); + for (size_t depId : op.dependsOn) { + if (depId < numOps) + successors[depId].push_back(opId); + } + if (remainingDeps[opId] == 0) { + readyOps.push_back(opId); + queued[opId] = true; + } + } + + auto completeOp = [&](size_t opId, int64_t time) { + if (completed[opId]) + return; + HIVMOp &op = report.operations[opId]; + op.endCycle = time; + completed[opId] = true; + ++completedCount; + report.oneIterationCycles = + std::max(report.oneIterationCycles, op.endCycle); + report.totalBusyCycles += op.duration; + report.opCount++; + if (op.pipe == HIVMPipe::Scalar && op.opName != "set_flag" && + op.opName != "wait_flag" && op.opName != "sync_block_set" && + op.opName != "sync_block_wait" && op.opName != "sync_block" && + op.opName != "pipe_barrier" && op.opName != "get_block_idx" && + op.opName != "get_block_num" && op.opName != "get_sub_block_idx" && + op.opName != "get_sub_block_num" && op.opName != "set_mask_norm" && + op.opName != "pointer_cast" && op.opName != "convert_layout") + report.unknownOpCount++; + if (op.isSyncOp) { + report.syncCycles += op.duration; + report.syncOpCount++; + if ((op.opName == "set_flag" || op.opName == "sync_block_set") && + !op.eventId.empty()) { + EventInstanceKey key{{op.senderPipe, op.receiverPipe, op.eventId}, + op.eventGeneration}; + if (op.opName == "sync_block_set") + blockSyncVisibleAt[key] = time; + else + flagEventVisibleAt[key] = time; + } + } + auto slotIt = writeSlotAssignments.find(opId); + if (slotIt != writeSlotAssignments.end()) { + for (const auto &[root, slotIndex] : slotIt->second) { + auto rootIt = bufferStates.find(root); + if (rootIt == bufferStates.end() || + slotIndex >= rootIt->second.slots.size()) + continue; + BufferRootState &state = rootIt->second; + BufferSlotState &slot = state.slots[slotIndex]; + auto rootVersionIt = llvm::find(op.writeBuffers, root); + if (rootVersionIt == op.writeBuffers.end()) + continue; + size_t bufferIdx = + std::distance(op.writeBuffers.begin(), rootVersionIt); + if (bufferIdx >= op.writeBufferVersions.size()) + continue; + int64_t version = op.writeBufferVersions[bufferIdx]; + slot.readableAt = time; + slot.version = version; + state.latestVersion = std::max(state.latestVersion, version); + state.latestReadableAt = std::max(state.latestReadableAt, time); + state.versionReadableAt[version] = time; + state.versionToSlot[version] = slotIndex; + } + } + if (op.isBarrier) { + report.barrierCycles += op.duration; + report.barrierCount++; + } + if (op.pipe != HIVMPipe::All && op.pipe != HIVMPipe::Unknown) { + report.pipeBusyCycles[op.pipe] += op.duration; + report.weightedPipeCycles[op.pipe] += op.duration * op.loopMultiplier; + } + for (size_t succId : successors[opId]) { + readyAt[succId] = std::max(readyAt[succId], time); + if (remainingDeps[succId] > 0) + --remainingDeps[succId]; + if (remainingDeps[succId] == 0 && !queued[succId]) { + readyOps.push_back(succId); + queued[succId] = true; + } + } + }; + + auto computeStartTime = [&](const HIVMOp &op) -> int64_t { + int64_t start = readyAt[op.id]; + for (size_t idx = 0; idx < op.readBuffers.size(); ++idx) { + const std::string &root = op.readBuffers[idx]; + auto it = bufferStates.find(root); + if (it == bufferStates.end()) + continue; + int64_t version = + idx < op.readBufferVersions.size() ? op.readBufferVersions[idx] : 0; + auto readableIt = it->second.versionReadableAt.find(version); + if (readableIt != it->second.versionReadableAt.end()) + start = std::max(start, readableIt->second); + else if (it->second.latestVersion >= version) + start = std::max(start, it->second.latestReadableAt); + } + for (const std::string &root : op.writeBuffers) { + auto it = bufferStates.find(root); + if (it != bufferStates.end() && !it->second.slots.empty()) { + int64_t slotReady = std::numeric_limits::max(); + for (const BufferSlotState &slot : it->second.slots) + slotReady = std::min(slotReady, slot.writableAt); + start = std::max(start, slotReady); + } + } + if ((op.opName == "wait_flag" || op.opName == "sync_block_wait") && + !op.eventId.empty()) { + EventInstanceKey key{{op.senderPipe, op.receiverPipe, op.eventId}, + op.eventGeneration}; + auto &visibleAt = op.opName == "sync_block_wait" ? blockSyncVisibleAt + : flagEventVisibleAt; + auto it = visibleAt.find(key); + if (it != visibleAt.end()) + start = std::max(start, it->second); + } + if (op.pipe == HIVMPipe::Unknown) + return start; + if (op.isBarrier && op.pipe == HIVMPipe::All) { + if (op.coreType.empty()) { + for (const auto &entry : pipeAvailableAt) + start = std::max(start, entry.second); + } else { + for (const auto &entry : pipeAvailableAt) { + if (pipeBelongsToCore(entry.first, op.coreType)) + start = std::max(start, entry.second); + } + } + return start; + } + return std::max(start, pipeAvailableAt[op.pipe]); + }; + + auto startOp = [&](size_t opId, int64_t startTime) { + HIVMOp &op = report.operations[opId]; + started[opId] = true; + op.startCycle = startTime; + const int64_t endTime = startTime + op.duration; + for (size_t idx = 0; idx < op.readBuffers.size(); ++idx) { + const std::string &root = op.readBuffers[idx]; + auto it = bufferStates.find(root); + if (it == bufferStates.end()) + continue; + int64_t version = + idx < op.readBufferVersions.size() ? op.readBufferVersions[idx] : 0; + if (version <= 0) + continue; + auto slotIt = it->second.versionToSlot.find(version); + if (slotIt == it->second.versionToSlot.end()) + continue; + size_t slotIndex = slotIt->second; + if (slotIndex >= it->second.slots.size()) + continue; + it->second.slots[slotIndex].writableAt = + std::max(it->second.slots[slotIndex].writableAt, endTime); + } + for (const std::string &root : op.writeBuffers) { + auto it = bufferStates.find(root); + if (it == bufferStates.end() || it->second.slots.empty()) + continue; + BufferRootState &state = it->second; + size_t bestSlot = 0; + int64_t bestTime = state.slots.front().writableAt; + for (size_t i = 1; i < state.slots.size(); ++i) { + if (state.slots[i].writableAt < bestTime) { + bestTime = state.slots[i].writableAt; + bestSlot = i; + } + } + state.slots[bestSlot].writableAt = endTime; + writeSlotAssignments[opId].push_back({root, bestSlot}); + } + if (op.pipe != HIVMPipe::Unknown) { + if (op.isBarrier && op.pipe == HIVMPipe::All) { + auto barrierPipes = getCoreBarrierPipes(op.coreType); + if (barrierPipes.empty()) { + for (auto &entry : pipeAvailableAt) + entry.second = endTime; + } else { + for (HIVMPipe barrierPipe : barrierPipes) + pipeAvailableAt[barrierPipe] = endTime; + } + } else { + pipeAvailableAt[op.pipe] = endTime; + } + } + if (op.duration == 0) + completeOp(opId, endTime); + else + completions.push({endTime, opId}); + }; + + int64_t currentTime = 0; + while (completedCount < numOps) { + bool startedAny = false; + size_t readyCount = readyOps.size(); + for (size_t i = 0; i < readyCount; ++i) { + size_t opId = readyOps.front(); + readyOps.pop_front(); + HIVMOp &op = report.operations[opId]; + if (started[opId] || completed[opId]) + continue; + int64_t startTime = computeStartTime(op); + if (startTime <= currentTime) { + startOp(opId, currentTime); + startedAny = true; + } else { + readyOps.push_back(opId); + } + } + + while (!completions.empty() && completions.top().time <= currentTime) { + size_t opId = completions.top().opId; + completions.pop(); + if (!completed[opId]) { + completeOp(opId, currentTime); + } + } + + if (startedAny) + continue; + + int64_t nextTime = std::numeric_limits::max(); + if (!completions.empty()) + nextTime = std::min(nextTime, completions.top().time); + for (size_t opId : readyOps) + nextTime = std::min(nextTime, computeStartTime(report.operations[opId])); + + if (nextTime == std::numeric_limits::max()) + break; + currentTime = std::max(currentTime, nextTime); + + while (!completions.empty() && completions.top().time <= currentTime) { + size_t opId = completions.top().opId; + completions.pop(); + if (!completed[opId]) { + completeOp(opId, currentTime); + } + } + } + + int64_t globalBarrierWeightedCycles = 0; + for (const HIVMOp &op : report.operations) { + if (op.isBarrier && op.pipe == HIVMPipe::All) + globalBarrierWeightedCycles += op.duration * op.loopMultiplier; + } + for (const auto &entry : report.weightedPipeCycles) + report.weightedCycles = std::max(report.weightedCycles, entry.second); + report.weightedCycles += globalBarrierWeightedCycles; + if (report.weightedCycles == 0) + report.weightedCycles = report.oneIterationCycles; +} + +} // namespace + +llvm::StringRef HIVMAnalyzer::stringifySchedulerMode(HIVMSchedulerMode mode) { + switch (mode) { + case HIVMSchedulerMode::Static: + return "static"; + case HIVMSchedulerMode::DES: + return "des"; + } + return "static"; +} + +llvm::StringRef HIVMAnalyzer::stringifyPipe(HIVMPipe pipe) { + switch (pipe) { + case HIVMPipe::Vector: + return "PIPE_V"; + case HIVMPipe::VectorMTE2: + return "PIPE_MTE2_V"; + case HIVMPipe::CubeMTE2: + return "PIPE_MTE2_C"; + case HIVMPipe::MTE3: + return "PIPE_MTE3"; + case HIVMPipe::Scalar: + return "PIPE_S"; + case HIVMPipe::FixPipe: + return "PIPE_FIX"; + case HIVMPipe::Cube: + return "PIPE_M"; + case HIVMPipe::MTE1: + return "PIPE_MTE1"; + case HIVMPipe::All: + return "PIPE_ALL"; + case HIVMPipe::Unknown: + return "PIPE_UNKNOWN"; + } + return "PIPE_UNKNOWN"; +} + +HIVMAnalyzer::HIVMAnalyzer(const HardwareConfig &config, + llvm::StringRef argBindings, + HIVMSchedulerMode schedulerMode) + : config(config), argBindingsStr(argBindings.str()), + schedulerMode(schedulerMode) {} + +bool HIVMAnalyzer::analyzeFile(llvm::StringRef path, HIVMAnalysisReport &report, + std::string &error) const { + auto fileOrErr = llvm::MemoryBuffer::getFile(path); + if (!fileOrErr) { + error = "failed to read HIVM file: " + path.str(); + return false; + } + + llvm::StringRef rawBuffer = fileOrErr.get()->getBuffer(); + std::string sanitized = sanitizeMlirBuffer(rawBuffer); + report = HIVMAnalysisReport(); + report.sourcePath = path.str(); + report.sourceMode = "direct-hivm"; + report.schedulerMode = schedulerMode; + + { + mlir::DialectRegistry registry; + registry.insert(); +#ifdef TRITONSIM_HAS_BISHENGIR_HIVM + registry.insert(); +#endif + mlir::MLIRContext context(registry); + context.allowUnregisteredDialects(); + + std::string parseDiagnostics; + mlir::ScopedDiagnosticHandler diagHandler( + &context, [&](mlir::Diagnostic &diag) { + llvm::raw_string_ostream os(parseDiagnostics); + diag.print(os); + os << "\n"; + return mlir::success(); + }); + + llvm::SmallVector parseCandidates; + parseCandidates.push_back(rawBuffer); + if (sanitized != rawBuffer) + parseCandidates.push_back(sanitized); + for (llvm::StringRef buffer : parseCandidates) { + if (auto module = + mlir::parseSourceString(buffer, &context)) { + if (!analyzeModule(*module, report, error)) + return false; + report.sourcePath = path.str(); + report.sourceMode = "direct-hivm"; + return true; + } + } + error = "failed to parse HIVM MLIR module"; + if (!parseDiagnostics.empty()) + error += ":\n" + parseDiagnostics; + return false; + } +} + +bool HIVMAnalyzer::analyzeModule(mlir::ModuleOp module, + HIVMAnalysisReport &report, + std::string &error) const { + if (!module) { + error = "null module passed to HIVM analysis"; + return false; + } + + report = HIVMAnalysisReport(); + report.sourcePath = ""; + report.sourceMode = "mlir-pass"; + report.schedulerMode = schedulerMode; + + AnalysisState state; + state.argBindings = parseArgBindings(argBindingsStr); + analyzeParsedRegion(module.getBodyRegion(), 1, state, report, config, + schedulerMode == HIVMSchedulerMode::DES); + if (schedulerMode == HIVMSchedulerMode::DES) + finalizeDiscreteEventReport(report, config); + else + finalizeScheduledReport(report, config); + return true; +} + +void HIVMAnalysisReport::print(llvm::raw_ostream &os, + const HardwareConfig &config) const { + os << "=== HIVM Analysis ===\n"; + os << "Source mode: " << sourceMode << "\n"; + os << "Source: " << sourcePath << "\n"; + os << "Scheduler: " << HIVMAnalyzer::stringifySchedulerMode(schedulerMode) + << "\n"; + os << "Hardware: " << config.getName() << " @ " + << llvm::format("%.2f", config.getClockFrequencyGHz()) << " GHz\n\n"; + + os << "Summary:\n"; + os << " Operations: " << opCount << "\n"; + os << " Sync ops: " << syncOpCount << "\n"; + os << " Barriers: " << barrierCount << "\n"; + os << " Scalar/unknown fallback ops: " << unknownOpCount << "\n"; + os << " One-iteration critical path: " << oneIterationCycles << " cycles (" + << llvm::format("%.3f", config.cyclesToMicroseconds(oneIterationCycles)) + << " us)\n"; + os << " Weighted pipe max: " << weightedCycles << " cycles (" + << llvm::format("%.3f", config.cyclesToMicroseconds(weightedCycles)) + << " us)\n"; + os << " Sync cycles: " << syncCycles << "\n"; + os << " Barrier cycles: " << barrierCycles << "\n"; + os << " Max loop multiplier: " << maxLoopMultiplier << "\n\n"; + + os << "Per-pipe utilization (one iteration):\n"; + for (const auto &entry : pipeBusyCycles) { + double util = oneIterationCycles > 0 ? static_cast(entry.second) / + oneIterationCycles * 100.0 + : 0.0; + os << " " << HIVMAnalyzer::stringifyPipe(entry.first) << ": " + << entry.second << " cycles, " << llvm::format("%.1f", util) << "%\n"; + } + + os << "\nPer-pipe weighted cycles:\n"; + for (const auto &entry : weightedPipeCycles) { + os << " " << HIVMAnalyzer::stringifyPipe(entry.first) << ": " + << entry.second << "\n"; + } + + os << "\nTop operations by weighted cost:\n"; + std::vector sorted; + sorted.reserve(operations.size()); + for (const HIVMOp &op : operations) + sorted.push_back(&op); + std::sort(sorted.begin(), sorted.end(), + [](const HIVMOp *lhs, const HIVMOp *rhs) { + return lhs->duration * lhs->loopMultiplier > + rhs->duration * rhs->loopMultiplier; + }); + + size_t limit = std::min(10, sorted.size()); + for (size_t i = 0; i < limit; ++i) { + const HIVMOp *op = sorted[i]; + os << " line " << op->lineNumber << " " << op->opName << " [" + << HIVMAnalyzer::stringifyPipe(op->pipe) + << "]: " << (op->duration * op->loopMultiplier) << " weighted cycles"; + if (!op->coreType.empty()) + os << ", core=" << op->coreType; + if (op->bytes > 0) + os << ", " << op->bytes << " bytes"; + if (op->elements > 0) + os << ", " << op->elements << " elems"; + os << "\n"; + } +} + +void HIVMAnalysisReport::emitPerfettoTrace(llvm::raw_ostream &os, + const HardwareConfig &config) const { + // Assign each pipe a unique tid. Pipes are grouped into AIC (Cube core) + // and AIV (Vector core) processes so that Perfetto renders them separately. + // AIC pid=1 : Cube, MTE1, CubeMTE2, FixPipe, Scalar(AIC) + // AIV pid=2 : Vector, VectorMTE2, MTE3, Scalar(AIV) + // Shared pid=3 : All, Unknown (cross-core barriers / unclassified) + constexpr int kPidAIC = 1; + constexpr int kPidAIV = 2; + constexpr int kPidShared = 3; + + auto pipeTid = [](HIVMPipe pipe) -> int { + switch (pipe) { + case HIVMPipe::Cube: + return 1; + case HIVMPipe::MTE1: + return 2; + case HIVMPipe::CubeMTE2: + return 3; + case HIVMPipe::FixPipe: + return 4; + case HIVMPipe::Scalar: + return 5; + case HIVMPipe::Vector: + return 1; + case HIVMPipe::VectorMTE2: + return 2; + case HIVMPipe::MTE3: + return 3; + case HIVMPipe::All: + return 1; + case HIVMPipe::Unknown: + return 5; // Coalesce with Scalar — metadata/address ops + } + return 5; + }; + + auto pipePid = [&](HIVMPipe pipe, llvm::StringRef coreType) -> int { + switch (pipe) { + case HIVMPipe::Cube: + case HIVMPipe::MTE1: + case HIVMPipe::CubeMTE2: + case HIVMPipe::FixPipe: + return kPidAIC; + case HIVMPipe::Vector: + case HIVMPipe::VectorMTE2: + case HIVMPipe::MTE3: + return kPidAIV; + case HIVMPipe::Scalar: + // Scalar exists on both cores; assign by op's core_type. + if (coreType == "CUBE" || coreType == "AIC") + return kPidAIC; + return kPidAIV; + case HIVMPipe::All: + case HIVMPipe::Unknown: + if (coreType == "CUBE" || coreType == "AIC") + return kPidAIC; + if (coreType == "VECTOR" || coreType == "AIV") + return kPidAIV; + return kPidShared; + } + return kPidShared; + }; + + auto cyclesToTraceUs = [&](int64_t cycles) -> double { + return config.cyclesToMicroseconds(cycles); + }; + auto joinStrings = [](const std::vector &values) { + std::string joined; + llvm::raw_string_ostream ss(joined); + for (size_t i = 0; i < values.size(); ++i) { + if (i) + ss << ";"; + ss << values[i]; + } + ss.flush(); + return joined; + }; + auto joinInts = [](const std::vector &values) { + std::string joined; + llvm::raw_string_ostream ss(joined); + for (size_t i = 0; i < values.size(); ++i) { + if (i) + ss << ";"; + ss << values[i]; + } + ss.flush(); + return joined; + }; + + os << "{\n \"traceEvents\": [\n"; + bool first = true; + auto emitComma = [&]() { + if (!first) + os << ",\n"; + first = false; + }; + + // Process names for AIC, AIV, and Shared groups. + emitComma(); + os << " {\"ph\":\"M\",\"pid\":" << kPidAIC + << ",\"tid\":0,\"name\":\"process_name\",\"args\":{\"name\":\"AIC (Cube " + "Core)\"}}"; + emitComma(); + os << " {\"ph\":\"M\",\"pid\":" << kPidAIV + << ",\"tid\":0,\"name\":\"process_name\",\"args\":{\"name\":\"AIV (Vector " + "Core)\"}}"; + emitComma(); + os << " {\"ph\":\"M\",\"pid\":" << kPidShared + << ",\"tid\":0,\"name\":\"process_name\",\"args\":{\"name\":\"Shared\"}}"; + + // AIC pipes: Cube, MTE1, CubeMTE2, FixPipe, Scalar(AIC) + for (HIVMPipe pipe : {HIVMPipe::Cube, HIVMPipe::MTE1, HIVMPipe::CubeMTE2, + HIVMPipe::FixPipe}) { + emitComma(); + os << " {\"ph\":\"M\",\"pid\":" << kPidAIC + << ",\"tid\":" << pipeTid(pipe) + << ",\"name\":\"thread_name\",\"args\":{\"name\":\"" + << HIVMAnalyzer::stringifyPipe(pipe) << "\"}}"; + } + // Scalar thread under AIC + emitComma(); + os << " {\"ph\":\"M\",\"pid\":" << kPidAIC + << ",\"tid\":" << pipeTid(HIVMPipe::Scalar) + << ",\"name\":\"thread_name\",\"args\":{\"name\":\"Scalar\"}}"; + + // AIV pipes: Vector, VectorMTE2, MTE3, Scalar(AIV) + for (HIVMPipe pipe : + {HIVMPipe::Vector, HIVMPipe::VectorMTE2, HIVMPipe::MTE3}) { + emitComma(); + os << " {\"ph\":\"M\",\"pid\":" << kPidAIV + << ",\"tid\":" << pipeTid(pipe) + << ",\"name\":\"thread_name\",\"args\":{\"name\":\"" + << HIVMAnalyzer::stringifyPipe(pipe) << "\"}}"; + } + // Scalar thread under AIV + emitComma(); + os << " {\"ph\":\"M\",\"pid\":" << kPidAIV + << ",\"tid\":" << pipeTid(HIVMPipe::Scalar) + << ",\"name\":\"thread_name\",\"args\":{\"name\":\"Scalar\"}}"; + + // Shared process: cross-core barrier track + emitComma(); + os << " {\"ph\":\"M\",\"pid\":" << kPidShared + << ",\"tid\":" << pipeTid(HIVMPipe::All) + << ",\"name\":\"thread_name\",\"args\":{\"name\":\"" + << HIVMAnalyzer::stringifyPipe(HIVMPipe::All) << "\"}}"; + + for (const HIVMOp &op : operations) { + // Skip zero-cycle metadata ops that are not real scheduled work. + if (op.opName == "pointer_cast" || op.opName == "convert_layout") + continue; + emitComma(); + os << " {\"ph\":\"X\",\"pid\":" << pipePid(op.pipe, op.coreType) + << ",\"tid\":" << pipeTid(op.pipe) + << ",\"ts\":" << llvm::format("%.3f", cyclesToTraceUs(op.startCycle)) + << ",\"dur\":" << llvm::format("%.3f", cyclesToTraceUs(op.duration)) + << ",\"name\":\"" << op.opName << "\",\"args\":{" + << "\"line\":" << op.lineNumber << ",\"cycles\":" << op.duration + << ",\"loop_multiplier\":" << op.loopMultiplier + << ",\"bytes\":" << op.bytes << ",\"elements\":" << op.elements + << ",\"event_id\":\"" << op.eventId << "\"" + << ",\"event_generation\":" << op.eventGeneration + << ",\"sender_pipe\":\"" << HIVMAnalyzer::stringifyPipe(op.senderPipe) + << "\"" + << ",\"receiver_pipe\":\"" + << HIVMAnalyzer::stringifyPipe(op.receiverPipe) << "\"" + << ",\"read_buffers\":\"" << joinStrings(op.readBuffers) << "\"" + << ",\"write_buffers\":\"" << joinStrings(op.writeBuffers) << "\"" + << ",\"read_versions\":\"" << joinInts(op.readBufferVersions) << "\"" + << ",\"write_versions\":\"" << joinInts(op.writeBufferVersions) << "\"" + << ",\"core_type\":\"" << op.coreType << "\"" + << ",\"sync\":" << (op.isSyncOp ? "true" : "false") + << ",\"barrier\":" << (op.isBarrier ? "true" : "false") << "}}"; + } + + // Emit flow events linking sync_block_set → sync_block_wait across cores. + // Build a map from (eventId, generation) → set-op index, then match waits. + { + // Key: (eventId, generation, sourceCore) + using SyncKey = std::tuple; + std::map> setOps; + std::map> waitOps; + for (size_t i = 0; i < operations.size(); ++i) { + const HIVMOp &op = operations[i]; + if (op.opName == "sync_block_set" && !op.eventId.empty()) { + bool isCube = op.coreType == "CUBE" || op.coreType == "AIC"; + SyncKey key{op.eventId, op.eventGeneration, isCube ? "AIC" : "AIV"}; + setOps[key].push_back(i); + } else if (op.opName == "sync_block_wait" && !op.eventId.empty()) { + // Wait on CUBE core means waiting for AIV→AIC signal. + bool isCube = op.coreType == "CUBE" || op.coreType == "AIC"; + SyncKey key{op.eventId, op.eventGeneration, isCube ? "AIV" : "AIC"}; + waitOps[key].push_back(i); + } + } + int64_t flowId = 0; + for (auto &[key, setIndices] : setOps) { + auto it = waitOps.find(key); + if (it == waitOps.end()) + continue; + auto &waits = it->second; + size_t pairs = std::min(setIndices.size(), waits.size()); + for (size_t p = 0; p < pairs; ++p) { + const HIVMOp &setOp = operations[setIndices[p]]; + const HIVMOp &waitOp = operations[waits[p]]; + // Flow start at set-op end time + emitComma(); + os << " {\"ph\":\"s\",\"id\":" << flowId + << ",\"pid\":" << pipePid(setOp.pipe, setOp.coreType) + << ",\"tid\":" << pipeTid(setOp.pipe) << ",\"ts\":" + << llvm::format("%.3f", + cyclesToTraceUs(setOp.startCycle + setOp.duration)) + << ",\"name\":\"sync\",\"cat\":\"sync\"}"; + // Flow finish at wait-op start time + emitComma(); + os << " {\"ph\":\"f\",\"id\":" << flowId + << ",\"pid\":" << pipePid(waitOp.pipe, waitOp.coreType) + << ",\"tid\":" << pipeTid(waitOp.pipe) << ",\"ts\":" + << llvm::format("%.3f", cyclesToTraceUs(waitOp.startCycle)) + << ",\"name\":\"sync\",\"cat\":\"sync\",\"bp\":\"e\"}"; + ++flowId; + } + } + } + + os << "\n ],\n \"displayTimeUnit\": \"us\"\n}\n"; +} + +void HIVMAnalysisReport::emitDESGraph(llvm::raw_ostream &os, + const HardwareConfig &config) const { + auto joinStrVec = [](const std::vector &v) { + std::string s; + llvm::raw_string_ostream ss(s); + ss << "["; + for (size_t i = 0; i < v.size(); ++i) { + if (i) + ss << ","; + ss << "\"" << v[i] << "\""; + } + ss << "]"; + ss.flush(); + return s; + }; + auto joinIntVec = [](const std::vector &v) { + std::string s; + llvm::raw_string_ostream ss(s); + ss << "["; + for (size_t i = 0; i < v.size(); ++i) { + if (i) + ss << ","; + ss << v[i]; + } + ss << "]"; + ss.flush(); + return s; + }; + auto joinSizeVec = [](const std::vector &v) { + std::string s; + llvm::raw_string_ostream ss(s); + ss << "["; + for (size_t i = 0; i < v.size(); ++i) { + if (i) + ss << ","; + ss << v[i]; + } + ss << "]"; + ss.flush(); + return s; + }; + + os << "{\n"; + os << " \"clock_ghz\": " + << llvm::format("%.3f", config.getClockFrequencyGHz()) << ",\n"; + os << " \"operations\": [\n"; + for (size_t i = 0; i < operations.size(); ++i) { + const HIVMOp &op = operations[i]; + if (i) + os << ",\n"; + os << " {" + << "\"id\":" << op.id << ",\"name\":\"" << op.opName << "\"" + << ",\"pipe\":\"" << HIVMAnalyzer::stringifyPipe(op.pipe) << "\"" + << ",\"duration\":" << op.duration << ",\"line\":" << op.lineNumber + << ",\"depends_on\":" << joinSizeVec(op.dependsOn) + << ",\"is_sync\":" << (op.isSyncOp ? "true" : "false") + << ",\"is_barrier\":" << (op.isBarrier ? "true" : "false") + << ",\"event_id\":\"" << op.eventId << "\"" + << ",\"event_generation\":" << op.eventGeneration + << ",\"sender_pipe\":\"" << HIVMAnalyzer::stringifyPipe(op.senderPipe) + << "\"" + << ",\"receiver_pipe\":\"" + << HIVMAnalyzer::stringifyPipe(op.receiverPipe) << "\"" + << ",\"core_type\":\"" << op.coreType << "\"" + << ",\"bytes\":" << op.bytes << ",\"elements\":" << op.elements + << ",\"loop_multiplier\":" << op.loopMultiplier + << ",\"multi_buffer_slots\":" << op.multiBufferSlots + << ",\"read_buffers\":" << joinStrVec(op.readBuffers) + << ",\"write_buffers\":" << joinStrVec(op.writeBuffers) + << ",\"read_versions\":" << joinIntVec(op.readBufferVersions) + << ",\"write_versions\":" << joinIntVec(op.writeBufferVersions) << "}"; + } + os << "\n ]\n}\n"; +} diff --git a/third_party/ascend/costmodel/lib/AscendModel/Analysis/HardwareConfig.cpp b/third_party/ascend/costmodel/lib/AscendModel/Analysis/HardwareConfig.cpp new file mode 100644 index 0000000000..473ca91f52 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Analysis/HardwareConfig.cpp @@ -0,0 +1,1454 @@ +//===- HardwareConfig.cpp - Hardware Configuration Implementation ---------===// +// +// This file implements the HardwareConfig class for loading and querying +// hardware parameters from JSON configuration files. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/HardwareConfig.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include + +using namespace mlir::ascend; + +//===----------------------------------------------------------------------===// +// Global Config Instance +//===----------------------------------------------------------------------===// + +static std::unique_ptr globalConfig; + +HardwareConfig &mlir::ascend::getHardwareConfig() { + if (!globalConfig) { + globalConfig = HardwareConfig::getDefault910B(); + } + return *globalConfig; +} + +void mlir::ascend::setHardwareConfig(std::unique_ptr config) { + globalConfig = std::move(config); +} + +bool mlir::ascend::loadHardwareConfigFromFile(llvm::StringRef path, + std::string &error) { + auto config = HardwareConfig::loadFromFile(path); + if (!config) { + error = "Failed to load hardware config from: " + path.str(); + return false; + } + if (!config->validate(error)) { + return false; + } + setHardwareConfig(std::move(config)); + return true; +} + +std::shared_ptr +mlir::ascend::loadHardwareConfigForAnalysis(llvm::StringRef path, + std::string &error) { + if (path.empty()) { + static std::once_flag defaultConfigOnce; + static std::shared_ptr defaultConfig; + static std::string defaultConfigError; + std::call_once(defaultConfigOnce, []() { + auto config = HardwareConfig::getDefault910B(); + if (!config) { + defaultConfigError = "Failed to load default hardware config"; + return; + } + if (!config->validate(defaultConfigError)) { + return; + } + defaultConfig = std::move(config); + }); + error = defaultConfigError; + return defaultConfig; + } + + auto config = HardwareConfig::loadFromFile(path); + if (!config) { + error = "Failed to load hardware config from: " + path.str(); + return nullptr; + } + if (!config->validate(error)) { + return nullptr; + } + return std::shared_ptr(std::move(config)); +} + +//===----------------------------------------------------------------------===// +// HardwareConfig Implementation +//===----------------------------------------------------------------------===// + +HardwareConfig::HardwareConfig() : clockFreqGHz(1.0) {} + +HardwareConfig::~HardwareConfig() = default; + +std::unique_ptr +HardwareConfig::loadFromFile(llvm::StringRef path) { + auto bufferOrErr = llvm::MemoryBuffer::getFile(path); + if (!bufferOrErr) { + llvm::errs() << "Error reading file: " << path << "\n"; + return nullptr; + } + + auto json = llvm::json::parse(bufferOrErr.get()->getBuffer()); + if (!json) { + llvm::errs() << "Error parsing JSON: " << llvm::toString(json.takeError()) + << "\n"; + return nullptr; + } + + return loadFromJSON(*json); +} + +std::unique_ptr +HardwareConfig::loadFromJSON(const llvm::json::Value &json) { + auto config = std::make_unique(); + std::string error; + if (!config->parseJSON(json, error)) { + llvm::errs() << "Error parsing hardware config: " << error << "\n"; + return nullptr; + } + return config; +} + +//===----------------------------------------------------------------------===// +// Default 910B Configuration +//===----------------------------------------------------------------------===// + +std::unique_ptr HardwareConfig::getDefault910B() { + // Try to load from standard config locations + std::vector searchPaths = { + "configs/ascend_910b.json", // Current directory + "../configs/ascend_910b.json", // Parent directory + "../../configs/ascend_910b.json", // Two levels up + "../config/ascend_910b.json", // Alternative naming + "config/ascend_910b.json", + }; + + // Also check environment variable for config path + if (const char *envPath = std::getenv("ASCEND_CONFIG_PATH")) { + searchPaths.insert(searchPaths.begin(), + std::string(envPath) + "/ascend_910b.json"); + } + + for (const auto &path : searchPaths) { + if (llvm::sys::fs::exists(path)) { + auto config = loadFromFile(path); + if (config) { + return config; + } + } + } + + // Fallback to defaults if no config file found + llvm::errs() << "Warning: Costmodel config file not found, using default " + "ascend_910b.json\n"; + return createHardcodedDefault910B(); +} + +/// Populate the tilesim-migrated micro-architecture tables with the 910B1 +/// measured values (bandwidth_910B1.csv / vec_cycle_910B1.csv / 910B1.yaml). +/// Used by the hardcoded fallback so the migrated model is available even +/// when the JSON config file cannot be located. The JSON config is the +/// authoritative source in production; this mirrors the same numbers. +void HardwareConfig::populateTilesimDefaults910B() { + // ---- Bandwidth tables (GB/s) ---- + // Core-independent movers. + auto addScalar = [&](llvm::StringRef key, double gbps) { + BandwidthTable t; + t.singleGbps = gbps; + bandwidthTables[key.str()] = std::move(t); + }; + addScalar("hbm:l2", 33.68666667); // GM->SHM(L2) + addScalar("l2:hbm", 34.34291667); // SHM->GM + addScalar("l2:ub", 137.0); // L2->UB + addScalar("ub:l2", 93.0); // UB->L2 + addScalar("hbm:l1", 135.0); // GM->L1 (cube MTE2) + addScalar("l2:l1", 221.0); // L2->L1 + addScalar("l1:l2", 221.0); // L1->L2 + addScalar("l1:l0a", 441.0); // MTE1 A + addScalar("l1:l0b", 220.5); // MTE1 B + addScalar("hbm:l0b", 104.5); + addScalar("l0c:hbm", 70.0); // FixPipe + addScalar("l0c:l2", 70.0); + addScalar("l0c:l1", 70.0); + addScalar("hbm:l2_agg", 1638.4); // aggregate HBM bandwidth + + // GM->UB per-core (vector MTE2): bandwidth degrades as cores contend. + { + static const double gmUb[] = { + 100.9, 64.5, 88.29666667, 77.945, 84.852, + 80.82666667, 73.31571429, 68.93375, 73.22888889, 67.586, + 71.13727273, 68.08833333, 69.14615385, 72.60785714, 74.99866667, + 73.26375, 72.05058824, 70.66833333, 73.27, 68.7705, + 68.72190476, 66.07909091, 66.33826087, 65.08041667, 65.3448, + 62.61884615, 62.15148148, 58.95964286, 58.23, 56.146, + 56.48, 53.3059375, 51.55151515, 49.97264706, 48.51342857, + 46.525, 46.06621622, 43.74315789, 42.97435897, 41.5595, + 40.44609756, 39.31904762, 38.47302326, 37.03886364, 36.33777778, + 35.40717391, 34.83446809, 33.68666667}; + BandwidthTable t; + t.hasPerCore = true; + for (int c = 1; c <= 48; ++c) + t.perCoreGbps[c] = gmUb[c - 1]; + t.singleGbps = gmUb[47]; + bandwidthTables["hbm:ub"] = std::move(t); + } + // UB->GM per-core (MTE3). + { + static const double ubGm[] = { + 188.46, 210.53, 117.2733333, 130.295, 108.964, + 119.54, 117.280446425, 115.02089285, 112.761339275, 110.5017857, + 108.242232125, 105.98267855, 103.723124975, 101.4635714, 100.0593333, + 100.373125, 96.81470588, 98.78555556, 90.44578947, 88.896, + 83.38571429, 83.77727273, 77.92608696, 76.65166667, 71.0792, + 70.58615385, 66.58407407, 66.42357143, 61.56655172, 59.854, + 56.1083871, 55.9315625, 53.0, 51.53, 49.45828571, + 48.11111111, 46.14540541, 45.26947368, 43.51512821, 42.56075, + 40.9797561, 40.19857143, 38.46488372, 38.07454545, 36.684, + 36.17608696, 34.70893617, 34.34291667}; + BandwidthTable t; + t.hasPerCore = true; + for (int c = 1; c <= 48; ++c) + t.perCoreGbps[c] = ubGm[c - 1]; + t.singleGbps = ubGm[47]; + bandwidthTables["ub:hbm"] = std::move(t); + } + + // ---- Vector instruction cycle tables (intrinsic -> dtype -> triplet) ---- + auto addVec = [&](llvm::StringRef inst, int bits, int compute, int head, + int interval) { + llvm::StringRef dt = (bits == 32) ? "fp32" : "fp16"; + vecCycleTables[inst.str()][dt.str()] = + VecCycleEntry{compute, head, interval}; + }; + // (intrinsic, dtype, computing, head, interval) from vec_cycle_910B1.csv. + addVec("VADD", 16, 2, 13, 18); + addVec("VADD", 32, 2, 13, 18); + addVec("VSUB", 16, 2, 13, 18); + addVec("VSUB", 32, 2, 13, 18); + addVec("VMUL", 16, 2, 13, 19); + addVec("VMUL", 32, 2, 13, 19); + addVec("VDIV", 16, 8, 13, 25); + addVec("VDIV", 32, 4, 13, 25); + addVec("VMAX", 16, 2, 14, 16); + addVec("VMAX", 32, 2, 14, 16); + addVec("VMIN", 16, 2, 14, 16); + addVec("VMIN", 32, 2, 14, 16); + addVec("VEXP", 16, 4, 13, 24); + addVec("VEXP", 32, 2, 13, 24); + addVec("VSQRT", 16, 2, 14, 25); + addVec("VSQRT", 32, 2, 14, 25); + addVec("VABS", 16, 1, 13, 16); + addVec("VABS", 32, 1, 14, 16); + addVec("RELU", 16, 1, 14, 16); + addVec("RELU", 32, 1, 14, 16); + addVec("LOG", 16, 2, 26, 14); + addVec("LOG", 32, 2, 26, 14); + addVec("VSEL", 16, 2, 13, 13); + addVec("VSEL", 32, 2, 13, 14); + addVec("VBRCB", 16, 0, 0, 18); + addVec("VBRCB", 32, 0, 0, 18); + addVec("VCMPV_NE", 16, 2, 14, 22); + addVec("VCMPV_NE", 32, 2, 14, 22); + addVec("VCMPV_GE", 16, 2, 14, 22); + addVec("VCMPV_GE", 32, 2, 14, 22); + addVec("CMPV_EQ", 16, 2, 13, 22); + addVec("CMPV_EQ", 32, 2, 13, 22); + addVec("VCGADD", 32, 1, 14, 24); + addVec("VCGMAX", 32, 1, 14, 17); + addVec("VCGMIN", 32, 1, 14, 17); + addVec("VREDUCEV2", 32, 14, 14, 20); + addVec("VCOPY", 16, 1, 11, 13); + addVec("VCOPY", 32, 1, 11, 13); + addVec("CONV_F322F16", 32, 1, 16, 14); + addVec("CONV_F162F32", 16, 2, 13, 15); + addVec("CONV_F322BF16", 32, 1, 14, 16); + addVec("CONV_BF162F32", 16, 2, 14, 18); + + // ---- Cube (GEMM) model ---- + cubeModel.basicM = 16; + cubeModel.basicKNumerator = 32; + cubeModel.basicN = 16; + cubeModel.l0TileLimitKb = 32; + cubeModel.repeatCycles["fp32"] = 2; + cubeModel.repeatCycles["fp16"] = 1; + cubeModel.repeatCycles["bf16"] = 1; + cubeModel.repeatCycles["int8"] = 1; + + activeBandwidthCores = 48; + // tilesim's engineering by-rule-pipeline path (eval_by_rule_pipe.py, the + // "main simulation loop" this costmodel is aligned to) sets + // enable_small_pkt_bw=True; the base config default (False) is not the + // accuracy target. Global pkt_param mirrors tilesim 910B1.yaml. + enableSmallPacketBw = true; + globalSmallPacket.enabled = true; + globalSmallPacket.thresholdBytes = 256; + globalSmallPacket.a64 = 0.295; + globalSmallPacket.b64 = 12.061; + globalSmallPacket.a128 = 0.272; + globalSmallPacket.b128 = 16.820; + globalSmallPacket.a256 = 0.322; + globalSmallPacket.b256 = 24.004; + hasGlobalSmallPacket = true; + // Mutex unit cliques (tilesim MutexComponents / pipe_exclusive_config). + // 910B: AIV MTE2 (vec load) and MTE3 (vec store) share one pipeline. + mutexGroups.clear(); + mutexGroups.push_back({"vec_mte2", "mte3"}); +} + +/// Hardcoded fallback configuration (used when JSON file not found) +std::unique_ptr HardwareConfig::createHardcodedDefault910B() { + auto config = std::make_unique(); + + config->name = "Ascend 910B"; + config->vendor = "Huawei"; + config->version = "1.0"; + config->clockFreqGHz = 1.85; + + // Memory spaces + // HBM: 32GB, 1.6TB/s + { + MemorySpace hbm; + hbm.name = "hbm"; + hbm.type = MemoryType::OffChip; + hbm.sizeBytes = 32ULL * 1024 * 1024 * 1024; + hbm.bandwidthBytesPerCycle = (1.6e12) / (config->clockFreqGHz * 1e9); + hbm.latencyCycles = 200; + config->memorySpaces["hbm"] = std::move(hbm); + } + + // L2: 192MB shared + { + MemorySpace l2; + l2.name = "l2"; + l2.type = MemoryType::OnChipShared; + l2.sizeBytes = 192 * 1024 * 1024; + l2.bandwidthBytesPerCycle = (3200e9) / (config->clockFreqGHz * 1e9); + l2.latencyCycles = 50; + config->memorySpaces["l2"] = std::move(l2); + } + + // L1: 1MB for Cube staging + { + MemorySpace l1; + l1.name = "l1"; + l1.type = MemoryType::OnChipLocal; + l1.sizeBytes = 1024 * 1024; + l1.bandwidthBytesPerCycle = (6400e9) / (config->clockFreqGHz * 1e9); + l1.latencyCycles = 10; + l1.description = "Cube data staging buffer"; + config->memorySpaces["l1"] = std::move(l1); + } + + // L0A: 64KB left matrix input + { + MemorySpace l0a; + l0a.name = "l0a"; + l0a.type = MemoryType::RegisterFile; + l0a.sizeBytes = 64 * 1024; + l0a.bandwidthBytesPerCycle = 0; // Internal + l0a.latencyCycles = 0; + l0a.description = "Left matrix input for Cube"; + config->memorySpaces["l0a"] = std::move(l0a); + } + + // L0B: 64KB right matrix input + { + MemorySpace l0b; + l0b.name = "l0b"; + l0b.type = MemoryType::RegisterFile; + l0b.sizeBytes = 64 * 1024; + l0b.bandwidthBytesPerCycle = 0; + l0b.latencyCycles = 0; + l0b.description = "Right matrix input for Cube"; + config->memorySpaces["l0b"] = std::move(l0b); + } + + // L0C: 256KB Cube output + { + MemorySpace l0c; + l0c.name = "l0c"; + l0c.type = MemoryType::RegisterFile; + l0c.sizeBytes = 256 * 1024; + l0c.bandwidthBytesPerCycle = 0; + l0c.latencyCycles = 0; + l0c.description = "Cube output accumulator"; + config->memorySpaces["l0c"] = std::move(l0c); + } + + // UB: 256KB Unified Buffer for Vector + { + MemorySpace ub; + ub.name = "ub"; + ub.type = MemoryType::OnChipLocal; + ub.sizeBytes = 256 * 1024; + ub.bandwidthBytesPerCycle = (3200e9) / (config->clockFreqGHz * 1e9); + ub.latencyCycles = 5; + ub.description = "Unified Buffer for Vector compute"; + config->memorySpaces["ub"] = std::move(ub); + } + + // Compute units + // Cube: 320 TFLOPS FP16, 16x16x16 tiles + { + ComputeUnit cube; + cube.name = "cube"; + cube.type = ComputeUnitType::MatrixEngine; + cube.tflopsFP16 = 320; + cube.tflopsFP32 = 160; + cube.tflopsINT8 = 640; + cube.tileM = 16; + cube.tileN = 16; + cube.tileK = 16; + // Fractal sizes per data type + // Fractal sizes per data type (m x k x n) + cube.fractalSizes["fp16"] = FractalSize{16, 16, 16}; + cube.fractalSizes["bf16"] = FractalSize{16, 16, 16}; + cube.fractalSizes["fp32"] = FractalSize{16, 8, 16}; + cube.fractalSizes["int8"] = FractalSize{16, 32, 16}; + cube.inputSpaces = {"l0a", "l0b"}; + cube.outputSpace = "l0c"; + cube.supportedDtypes = {"fp16", "bf16", "fp32", "int8"}; + config->computeUnits["cube"] = std::move(cube); + } + + // Vector: 128 FP16 elements per cycle + { + ComputeUnit vector; + vector.name = "vector"; + vector.type = ComputeUnitType::SIMDEngine; + vector.tflopsFP16 = 20; + vector.tflopsFP32 = 10; + vector.tflopsINT8 = 0; + vector.widthElements = 128; + vector.widthBytes = 256; + vector.computeSpace = "ub"; + vector.supportedOps = { + "add", "sub", "mul", "div", "max", "min", + "exp", "log", "sqrt", "rsqrt", "tanh", "sigmoid", + "relu", "reduce_sum", "reduce_max", "cast", "broadcast"}; + vector.supportedDtypes = {"fp32", "fp16", "bf16", "int32", "int8"}; + config->computeUnits["vector"] = std::move(vector); + } + + config->vectorOpCyclesPerInstruction["vadd"] = 1; + config->vectorOpCyclesPerInstruction["vsub"] = 1; + config->vectorOpCyclesPerInstruction["vmul"] = 1; + config->vectorOpCyclesPerInstruction["vcast"] = 1; + config->vectorOpCyclesPerInstruction["vreduce"] = 2; + config->vectorOpCyclesPerInstruction["vexp"] = 4; + config->vectorOpCyclesPerInstruction["vdiv"] = 3; + config->vectorOpCyclesPerInstruction["vlog"] = 3; + config->vectorOpCyclesPerInstruction["vtanh"] = 3; + config->vectorOpCyclesPerInstruction["vsigmoid"] = 3; + config->vectorOpCyclesPerInstruction["vsqrt"] = 2; + config->vectorOpCyclesPerInstruction["vrsqrt"] = 2; + + // Data movers + // MTE2 (Cube): HBM -> L1 + { + DataMover dm; + dm.name = "cube_mte2"; + dm.description = "Cube input: HBM to L1"; + dm.srcSpace = "hbm"; + dm.dstSpaces = {"l1"}; + dm.bandwidthBytesPerCycle = (200e9) / (config->clockFreqGHz * 1e9); + dm.maxBurstBytes = 65536; + dm.alignmentBytes = 32; + config->dataMovers["cube_mte2"] = std::move(dm); + } + + // MTE1: L1 -> L0A/L0B + { + DataMover dm; + dm.name = "mte1"; + dm.description = "Cube input: L1 to L0A/L0B"; + dm.srcSpace = "l1"; + dm.dstSpaces = {"l0a", "l0b"}; + dm.bandwidthBytesPerCycle = (400e9) / (config->clockFreqGHz * 1e9); + dm.maxBurstBytes = 32768; + dm.alignmentBytes = 32; + config->dataMovers["mte1"] = std::move(dm); + } + + // FixPipe: L0C -> HBM + { + DataMover dm; + dm.name = "fixpipe"; + dm.description = "Cube output: L0C to HBM"; + dm.srcSpace = "l0c"; + dm.dstSpaces = {"hbm"}; + dm.bandwidthBytesPerCycle = (200e9) / (config->clockFreqGHz * 1e9); + dm.maxBurstBytes = 65536; + dm.alignmentBytes = 32; + dm.supportsAccumulate = true; + dm.supportsCast = true; + config->dataMovers["fixpipe"] = std::move(dm); + } + + // MTE2 (Vector): HBM -> UB + { + DataMover dm; + dm.name = "vector_mte2"; + dm.description = "Vector input: HBM to UB"; + dm.srcSpace = "hbm"; + dm.dstSpaces = {"ub"}; + dm.bandwidthBytesPerCycle = (200e9) / (config->clockFreqGHz * 1e9); + dm.maxBurstBytes = 65536; + dm.alignmentBytes = 32; + config->dataMovers["vector_mte2"] = std::move(dm); + } + + // MTE3: UB -> HBM + { + DataMover dm; + dm.name = "mte3"; + dm.description = "Vector output: UB to HBM"; + dm.srcSpace = "ub"; + dm.dstSpaces = {"hbm"}; + dm.bandwidthBytesPerCycle = (200e9) / (config->clockFreqGHz * 1e9); + dm.maxBurstBytes = 65536; + dm.alignmentBytes = 32; + config->dataMovers["mte3"] = std::move(dm); + } + + // Pipeline paths + { + PipelinePath cubePath; + cubePath.name = "cube_path"; + cubePath.stages = {"cube_mte2", "mte1", "cube", "fixpipe"}; + cubePath.description = "Matrix multiplication data flow"; + config->pipelinePaths["cube_path"] = std::move(cubePath); + } + { + PipelinePath vectorPath; + vectorPath.name = "vector_path"; + vectorPath.stages = {"vector_mte2", "vector", "mte3"}; + vectorPath.description = "Vector computation data flow"; + config->pipelinePaths["vector_path"] = std::move(vectorPath); + } + + // Parallelism + config->parallelismFlags["cube_and_vector"] = true; + config->parallelismFlags["cube_mte2_and_fixpipe"] = true; + config->parallelismFlags["vector_mte2_and_mte3"] = true; + + // tilesim-migrated micro-architecture tables (bandwidth/vec/cube). + config->populateTilesimDefaults910B(); + + return config; +} + +//===----------------------------------------------------------------------===// +// JSON Parsing +//===----------------------------------------------------------------------===// + +static MemoryType parseMemoryType(llvm::StringRef typeStr) { + if (typeStr == "off_chip") + return MemoryType::OffChip; + if (typeStr == "on_chip_shared") + return MemoryType::OnChipShared; + if (typeStr == "on_chip_local") + return MemoryType::OnChipLocal; + if (typeStr == "register_file") + return MemoryType::RegisterFile; + return MemoryType::OnChipLocal; +} + +static ComputeUnitType parseComputeUnitType(llvm::StringRef typeStr) { + if (typeStr == "matrix_engine") + return ComputeUnitType::MatrixEngine; + if (typeStr == "simd_engine") + return ComputeUnitType::SIMDEngine; + if (typeStr == "scalar_engine") + return ComputeUnitType::ScalarEngine; + return ComputeUnitType::SIMDEngine; +} + +bool HardwareConfig::parseJSON(const llvm::json::Value &json, + std::string &error) { + const auto *root = json.getAsObject(); + if (!root) { + error = "Root must be a JSON object"; + return false; + } + + // Basic info + if (auto n = root->getString("name")) + name = n->str(); + if (auto v = root->getString("vendor")) + vendor = v->str(); + if (auto ver = root->getString("version")) + version = ver->str(); + + // Clock + if (const auto *clock = root->getObject("clock")) { + if (auto freq = clock->getNumber("frequency_ghz")) + clockFreqGHz = *freq; + } + + // Memory spaces + if (const auto *memSpaces = root->getObject("memory_spaces")) { + for (const auto &kv : *memSpaces) { + const auto *obj = kv.second.getAsObject(); + if (!obj) + continue; + + MemorySpace space; + space.name = kv.first.str(); + + if (auto t = obj->getString("type")) + space.type = parseMemoryType(*t); + + // Size (support multiple units) + if (auto gb = obj->getNumber("size_gb")) + space.sizeBytes = static_cast(*gb * 1024 * 1024 * 1024); + else if (auto mb = obj->getNumber("size_mb")) + space.sizeBytes = static_cast(*mb * 1024 * 1024); + else if (auto kb = obj->getNumber("size_kb")) + space.sizeBytes = static_cast(*kb * 1024); + + // Bandwidth + if (auto tbps = obj->getNumber("bandwidth_tbps")) + space.bandwidthBytesPerCycle = (*tbps * 1e12) / (clockFreqGHz * 1e9); + else if (auto gbps = obj->getNumber("bandwidth_gbps")) + space.bandwidthBytesPerCycle = (*gbps * 1e9) / (clockFreqGHz * 1e9); + + if (auto lat = obj->getInteger("latency_cycles")) + space.latencyCycles = *lat; + + if (auto desc = obj->getString("description")) + space.description = desc->str(); + + memorySpaces[space.name] = std::move(space); + } + } + + // Compute units + if (const auto *units = root->getObject("compute_units")) { + for (const auto &kv : *units) { + const auto *obj = kv.second.getAsObject(); + if (!obj) + continue; + + ComputeUnit unit; + unit.name = kv.first.str(); + + if (auto t = obj->getString("type")) + unit.type = parseComputeUnitType(*t); + + if (auto v = obj->getNumber("tflops_fp16")) + unit.tflopsFP16 = *v; + if (auto v = obj->getNumber("tflops_fp32")) + unit.tflopsFP32 = *v; + if (auto v = obj->getNumber("tflops_int8")) + unit.tflopsINT8 = *v; + + if (auto v = obj->getInteger("tile_m")) + unit.tileM = *v; + if (auto v = obj->getInteger("tile_n")) + unit.tileN = *v; + if (auto v = obj->getInteger("tile_k")) + unit.tileK = *v; + + // Parse fractal sizes per dtype + if (const auto *fractalObj = obj->getObject("fractal_sizes")) { + for (const auto &fkv : *fractalObj) { + const auto *sizeObj = fkv.second.getAsObject(); + if (!sizeObj) + continue; + + FractalSize fs; + if (auto m = sizeObj->getInteger("m")) + fs.m = *m; + if (auto n = sizeObj->getInteger("n")) + fs.n = *n; + if (auto k = sizeObj->getInteger("k")) + fs.k = *k; + unit.fractalSizes[fkv.first.str()] = fs; + } + } + + if (auto v = obj->getInteger("width_elements")) + unit.widthElements = *v; + if (auto v = obj->getInteger("width_bytes")) + unit.widthBytes = *v; + + if (auto s = obj->getString("output_space")) + unit.outputSpace = s->str(); + if (auto s = obj->getString("compute_space")) + unit.computeSpace = s->str(); + + if (const auto *arr = obj->getArray("input_spaces")) { + for (const auto &v : *arr) { + if (auto s = v.getAsString()) + unit.inputSpaces.push_back(s->str()); + } + } + + if (const auto *arr = obj->getArray("supported_ops")) { + for (const auto &v : *arr) { + if (auto s = v.getAsString()) + unit.supportedOps.push_back(s->str()); + } + } + + if (const auto *arr = obj->getArray("supported_dtypes")) { + for (const auto &v : *arr) { + if (auto s = v.getAsString()) + unit.supportedDtypes.push_back(s->str()); + } + } + + computeUnits[unit.name] = std::move(unit); + } + } + + // Data movers + if (const auto *movers = root->getObject("data_movers")) { + for (const auto &kv : *movers) { + const auto *obj = kv.second.getAsObject(); + if (!obj) + continue; + + DataMover dm; + dm.name = kv.first.str(); + + if (auto s = obj->getString("description")) + dm.description = s->str(); + if (auto s = obj->getString("src_space")) + dm.srcSpace = s->str(); + if (auto s = obj->getString("dst_space")) + dm.dstSpaces.push_back(s->str()); + + if (const auto *arr = obj->getArray("dst_spaces")) { + dm.dstSpaces.clear(); + for (const auto &v : *arr) { + if (auto s = v.getAsString()) + dm.dstSpaces.push_back(s->str()); + } + } + + if (auto gbps = obj->getNumber("bandwidth_gbps")) + dm.bandwidthBytesPerCycle = (*gbps * 1e9) / (clockFreqGHz * 1e9); + + if (auto v = obj->getInteger("max_burst_bytes")) + dm.maxBurstBytes = *v; + if (auto v = obj->getInteger("alignment_bytes")) + dm.alignmentBytes = *v; + if (auto v = obj->getBoolean("supports_accumulate")) + dm.supportsAccumulate = *v; + if (auto v = obj->getBoolean("supports_cast")) + dm.supportsCast = *v; + + dataMovers[dm.name] = std::move(dm); + } + } + + // Pipeline + if (const auto *pipeline = root->getObject("pipeline")) { + for (const auto &kv : *pipeline) { + if (kv.first == "parallelism") { + if (const auto *par = kv.second.getAsObject()) { + for (const auto &pkv : *par) { + if (auto v = pkv.second.getAsBoolean()) + parallelismFlags[pkv.first.str()] = *v; + } + } + continue; + } + + const auto *obj = kv.second.getAsObject(); + if (!obj) + continue; + + PipelinePath path; + path.name = kv.first.str(); + + if (const auto *arr = obj->getArray("stages")) { + for (const auto &v : *arr) { + if (auto s = v.getAsString()) + path.stages.push_back(s->str()); + } + } + + if (auto s = obj->getString("description")) + path.description = s->str(); + + pipelinePaths[path.name] = std::move(path); + } + } + + if (const auto *calibration = root->getObject("calibration")) { + if (const auto *vecOps = + calibration->getObject("vector_op_cycles_per_vec_instruction")) { + auto readInt = [&](llvm::StringRef key, llvm::StringRef opName) { + if (auto v = vecOps->getInteger(key)) + vectorOpCyclesPerInstruction[opName] = static_cast(*v); + }; + readInt("simple_ops_add_sub_mul_etc", "vadd"); + readInt("simple_ops_add_sub_mul_etc", "vsub"); + readInt("simple_ops_add_sub_mul_etc", "vmul"); + readInt("simple_ops_add_sub_mul_etc", "vcast"); + readInt("exp", "vexp"); + readInt("log", "vlog"); + readInt("tanh", "vtanh"); + readInt("sigmoid", "vsigmoid"); + readInt("sqrt", "vsqrt"); + readInt("rsqrt", "vrsqrt"); + readInt("div", "vdiv"); + } + } + + //===------------------------------------------------------------------===// + // tilesim-migrated micro-architecture tables (root cause ①/②) + // All fields are optional; absence leaves the (less accurate) legacy + // estimation path intact, preserving backward compatibility. + //===------------------------------------------------------------------===// + + // Bandwidth tables keyed by "src:dst". Supports core-independent + // ("bandwidth_gbps") and per-core ("per_core_gbps": {coreNum: gbps}) + // forms, plus optional small-packet fitting coefficients. + if (const auto *bwTables = root->getObject("bandwidth_tables")) { + for (const auto &kv : *bwTables) { + const auto *obj = kv.second.getAsObject(); + if (!obj) + continue; + BandwidthTable tbl; + if (auto gbps = obj->getNumber("bandwidth_gbps")) { + tbl.singleGbps = *gbps; + } else if (const auto *perCore = obj->getObject("per_core_gbps")) { + tbl.hasPerCore = true; + for (const auto &pkv : *perCore) { + if (auto v = pkv.second.getAsNumber()) { + int cores = std::stoi(pkv.first.str()); + tbl.perCoreGbps[cores] = *v; + } + } + // singleGbps fallback = the largest (most-saturated) core entry. + if (!tbl.perCoreGbps.empty()) + tbl.singleGbps = tbl.perCoreGbps.rbegin()->second; + } + if (const auto *sp = obj->getObject("small_packet")) { + tbl.smallPacket.enabled = true; + if (auto t = sp->getInteger("threshold_bytes")) + tbl.smallPacket.thresholdBytes = *t; + auto readPair = [&](llvm::StringRef key, double &a, double &b) { + if (const auto *arr = sp->getArray(key); arr && arr->size() >= 2) { + if (auto va = (*arr)[0].getAsNumber()) + a = *va; + if (auto vb = (*arr)[1].getAsNumber()) + b = *vb; + } + }; + readPair("64B", tbl.smallPacket.a64, tbl.smallPacket.b64); + readPair("128B", tbl.smallPacket.a128, tbl.smallPacket.b128); + readPair("256B", tbl.smallPacket.a256, tbl.smallPacket.b256); + } + bandwidthTables[kv.first.str()] = std::move(tbl); + } + } + + // Vector instruction cycle tables: intrinsic -> dtype -> + // {compute,head,interval}. + if (const auto *vecTables = root->getObject("vec_cycle_tables")) { + for (const auto &kv : *vecTables) { + const auto *byDtype = kv.second.getAsObject(); + if (!byDtype) + continue; + llvm::StringMap inner; + for (const auto &dkv : *byDtype) { + const auto *entry = dkv.second.getAsObject(); + if (!entry) + continue; + VecCycleEntry e; + if (auto v = entry->getInteger("compute")) + e.compute = *v; + if (auto v = entry->getInteger("head")) + e.head = *v; + if (auto v = entry->getInteger("interval")) + e.interval = *v; + inner[dkv.first.str()] = e; + } + vecCycleTables[kv.first.str()] = std::move(inner); + } + } + + // Cube (GEMM) micro-architecture: throughput [m,k,n], repeat cycles, L0 + // limit. + if (const auto *cube = root->getObject("cube_model")) { + if (const auto *arr = cube->getArray("throughput"); + arr && arr->size() >= 3) { + if (auto v = (*arr)[0].getAsInteger()) + cubeModel.basicM = *v; + if (auto v = (*arr)[1].getAsInteger()) + cubeModel.basicKNumerator = *v; + if (auto v = (*arr)[2].getAsInteger()) + cubeModel.basicN = *v; + } + if (const auto *rc = cube->getObject("repeat_cycles")) { + for (const auto &rkv : *rc) { + if (auto v = rkv.second.getAsInteger()) + cubeModel.repeatCycles[rkv.first.str()] = static_cast(*v); + } + } + if (auto v = cube->getInteger("l0_tile_limit_kb")) + cubeModel.l0TileLimitKb = *v; + } + + // tilesim tuning knobs. + if (const auto *ts = root->getObject("tilesim")) { + if (auto v = ts->getInteger("active_bandwidth_cores")) + activeBandwidthCores = *v; + if (auto v = ts->getBoolean("enable_small_packet_bw")) + enableSmallPacketBw = *v; + // Global pkt_param (tilesim 910B1.yaml pkt_param): one (a,b) set per size + // bucket applied to every mover. Sits behind any per-table small_packet + // entry parsed above from bandwidth_tables. + if (const auto *sp = ts->getObject("small_packet")) { + SmallPacketCoeffs c; + c.enabled = true; + if (auto t = sp->getInteger("threshold_bytes")) + c.thresholdBytes = *t; + auto readPair = [&](llvm::StringRef key, double &a, double &b) { + if (const auto *arr = sp->getArray(key); arr && arr->size() >= 2) { + if (auto va = (*arr)[0].getAsNumber()) + a = *va; + if (auto vb = (*arr)[1].getAsNumber()) + b = *vb; + } + }; + readPair("64B", c.a64, c.b64); + readPair("128B", c.a128, c.b128); + readPair("256B", c.a256, c.b256); + globalSmallPacket = c; + hasGlobalSmallPacket = true; + } + // Mutex unit cliques (tilesim MutexComponents): object with a "cliques" + // array of unit-name arrays, e.g. {"cliques":[["vec_mte2","mte3"]]}. + // Units in a clique share a pipeline and cannot run in parallel. + if (const auto *mg = ts->getObject("mutex_groups")) { + mutexGroups.clear(); + if (const auto *cliques = mg->getArray("cliques")) { + for (const auto &clique : *cliques) { + if (const auto *units = clique.getAsArray()) { + std::vector group; + for (const auto &u : *units) + if (auto s = u.getAsString()) + group.push_back(s->str()); + if (group.size() >= 2) + mutexGroups.push_back(std::move(group)); + } + } + } + } + } + + return true; +} + +//===----------------------------------------------------------------------===// +// Query Methods +//===----------------------------------------------------------------------===// + +const MemorySpace *HardwareConfig::getMemorySpace(llvm::StringRef name) const { + auto it = memorySpaces.find(name); + return it != memorySpaces.end() ? &it->second : nullptr; +} + +double HardwareConfig::getMemoryBandwidthTBps(llvm::StringRef name) const { + if (const auto *space = getMemorySpace(name)) { + return space->bandwidthBytesPerCycle * clockFreqGHz * 1e9 / 1e12; + } + return 0; +} + +double +HardwareConfig::getMemoryBandwidthBytesPerCycle(llvm::StringRef name) const { + if (const auto *space = getMemorySpace(name)) { + return space->bandwidthBytesPerCycle; + } + return 0; +} + +int HardwareConfig::getMemoryLatencyCycles(llvm::StringRef name) const { + if (const auto *space = getMemorySpace(name)) { + return space->latencyCycles; + } + return 0; +} + +size_t HardwareConfig::getMemorySizeBytes(llvm::StringRef name) const { + if (const auto *space = getMemorySpace(name)) { + return space->sizeBytes; + } + return 0; +} + +std::vector HardwareConfig::getMemorySpaceNames() const { + std::vector names; + for (const auto &kv : memorySpaces) { + names.push_back(kv.first().str()); + } + return names; +} + +const ComputeUnit *HardwareConfig::getComputeUnit(llvm::StringRef name) const { + auto it = computeUnits.find(name); + return it != computeUnits.end() ? &it->second : nullptr; +} + +double HardwareConfig::getCubeTFlopsFP16() const { + if (const auto *cube = getComputeUnit("cube")) { + return cube->tflopsFP16; + } + return 320; // Default +} + +void HardwareConfig::getCubeTileSize(int &m, int &n, int &k) const { + if (const auto *cube = getComputeUnit("cube")) { + m = cube->tileM; + n = cube->tileN; + k = cube->tileK; + return; + } + m = n = k = 16; // Default +} + +void HardwareConfig::getCubeFractalSize(int elementBits, int &m, int &n, + int &k) const { + if (const auto *cube = getComputeUnit("cube")) { + // Determine dtype key from element bits + llvm::StringRef dtypeKey; + if (elementBits == 8) { + dtypeKey = "int8"; + } else if (elementBits == 32) { + dtypeKey = "fp32"; + } else { + // Default to fp16 for 16-bit types (fp16, bf16) + dtypeKey = "fp16"; + } + + // Look up fractal size for this dtype + auto it = cube->fractalSizes.find(dtypeKey); + if (it != cube->fractalSizes.end()) { + m = it->second.m; + n = it->second.n; + k = it->second.k; + return; + } + + // Fall back to default tile size + m = cube->tileM; + n = cube->tileN; + k = cube->tileK; + return; + } + + // Ultimate fallback: FP16 defaults + m = 16; + n = 16; + k = 16; +} + +llvm::StringRef HardwareConfig::getCubeOutputSpace() const { + if (const auto *cube = getComputeUnit("cube")) { + return cube->outputSpace; + } + return "l0c"; +} + +double HardwareConfig::getVectorTFlopsFP32() const { + if (const auto *vec = getComputeUnit("vector")) { + return vec->tflopsFP32; + } + return 10; // Default +} + +int HardwareConfig::getVectorWidthElements() const { + if (const auto *vec = getComputeUnit("vector")) { + return vec->widthElements; + } + return 128; // Default +} + +int HardwareConfig::getVectorWidthBytes() const { + if (const auto *vec = getComputeUnit("vector")) { + return vec->widthBytes; + } + return 256; // Default +} + +llvm::StringRef HardwareConfig::getVectorComputeSpace() const { + if (const auto *vec = getComputeUnit("vector")) { + return vec->computeSpace; + } + return "ub"; +} + +int HardwareConfig::getVectorOpCyclesPerInstruction( + llvm::StringRef opName) const { + auto it = vectorOpCyclesPerInstruction.find(opName); + if (it != vectorOpCyclesPerInstruction.end()) + return it->second; + if (opName == "vreduce") + return 2; + return 1; +} + +double HardwareConfig::getHBMBandwidthGBs() const { + if (const auto *hbm = getMemorySpace("hbm")) { + // Convert bytes/cycle to GB/s + return hbm->bandwidthBytesPerCycle * clockFreqGHz * 1e9 / 1e9; + } + return 1600.0; // Default: 1.6 TB/s = 1600 GB/s +} + +double HardwareConfig::getHBMBandwidthTBs() const { + return getHBMBandwidthGBs() / 1000.0; +} + +double HardwareConfig::getCubeTFLOPS() const { return getCubeTFlopsFP16(); } + +double HardwareConfig::getVectorTFLOPS() const { return getVectorTFlopsFP32(); } + +int HardwareConfig::getMTE2StartupLatency() const { + // MTE2 DMA pipeline startup: address translation + HBM access setup. + // Previously inflated to 320 to absorb pipe_barrier fill latency, but + // pipe_barrier now has its own cost model. Restored to reflect actual + // MTE2 transfer startup overhead. + return 50; +} + +int HardwareConfig::getMTE3StartupLatency() const { return 40; } + +int HardwareConfig::getFixPipeStartupLatency() const { return 30; } + +int HardwareConfig::getCubeStartupLatency() const { return 20; } + +int HardwareConfig::getVectorStartupLatency() const { + // Calibrated: 35 cycles (was 10). + // Reflects UB read-after-write penalty between dependent vector instructions + // in a serial dependency chain (e.g., sub → exp → reduce in softmax). + return 35; +} + +double HardwareConfig::getAIVScalarOverheadFactor() const { + // Calibrated from _attn_fwd profiling (BM=48/64 steady-state): + // aiv_vec_ratio = 0.211 → scalar+overhead fraction = 1 - 0.211 = 0.789 + // effective factor = (1 - vec_ratio) / vec_ratio = 0.789 / 0.211 ≈ 3.74 + // Applying this to vec_cycles gives total_aiv_cycles that matches the + // observed aiv_time, capturing scalar (loop/barrier) + idle fractions. + return 3.74; +} + +int HardwareConfig::getNumAICCores() const { + // Block Dim = 20 in profiling runs. + return 20; +} + +int HardwareConfig::getNumAIVCores() const { + // Mix Block Dim = 40 → 40 AIV cores (2 per AIC block × 20 blocks). + return 40; +} + +int HardwareConfig::getPipeBarrierCyclesPerIter() const { + // Calibrated from BM=64, 1-wave execution: + // AIV wall time = 31.993 μs = 59 187 cycles + // Active fraction (vec+scalar+mte) = 61.1% → idle = 38.9% = 23 044 cycles + // With N_iter = 3 inner iterations → 23 044 / 3 ≈ 7 500 cycles per barrier. + return 7500; +} + +const DataMover *HardwareConfig::getDataMover(llvm::StringRef name) const { + auto it = dataMovers.find(name); + return it != dataMovers.end() ? &it->second : nullptr; +} + +std::vector HardwareConfig::getDataMoverNames() const { + std::vector names; + for (const auto &kv : dataMovers) { + names.push_back(kv.first().str()); + } + return names; +} + +//===----------------------------------------------------------------------===// +// Performance Estimation +//===----------------------------------------------------------------------===// + +int64_t HardwareConfig::estimateCubeCycles(int64_t M, int64_t N, + int64_t K) const { + int tileM = 16; + int tileN = 16; + int tileK = 16; + getCubeTileSize(tileM, tileN, tileK); + auto ceilDiv = [](int64_t a, int64_t b) -> int64_t { + return b > 0 ? (a + b - 1) / b : 0; + }; + return std::max(1, ceilDiv(M, tileM) * ceilDiv(N, tileN) * + ceilDiv(K, tileK)); +} + +int64_t HardwareConfig::estimateVectorCycles(int64_t numElements) const { + int width = getVectorWidthElements(); + if (width <= 0) + return std::max(1, numElements); + return std::max(1, (numElements + width - 1) / width); +} + +int64_t HardwareConfig::estimateMemoryCycles(llvm::StringRef moverName, + int64_t bytes) const { + const DataMover *mover = getDataMover(moverName); + if (!mover || mover->bandwidthBytesPerCycle <= 0.0) + return std::max(1, bytes); + return std::max(1, static_cast(std::ceil( + bytes / mover->bandwidthBytesPerCycle))); +} + +int64_t HardwareConfig::estimateMemoryCyclesWithLatency(llvm::StringRef space, + int64_t bytes) const { + const MemorySpace *mem = getMemorySpace(space); + if (!mem || mem->bandwidthBytesPerCycle <= 0.0) + return std::max(1, bytes); + return std::max(1, mem->latencyCycles + + static_cast(std::ceil( + bytes / mem->bandwidthBytesPerCycle))); +} + +//===----------------------------------------------------------------------===// +// tilesim-migrated micro-architecture queries (root cause ①/②) +//===----------------------------------------------------------------------===// + +namespace { +/// Map an element bit width to the dtype key used in the migrated tables. +/// 16-bit types (fp16/bf16) share the FP16 bucket, matching tilesim's CSV. +llvm::StringRef elementBitsToDtypeKey(int elementBits) { + if (elementBits == 8) + return "int8"; + if (elementBits == 32) + return "fp32"; + if (elementBits == 64) + return "fp64"; + return "fp16"; // fp16 / bf16 +} + +/// tilesim small-packet bandwidth convention: 1 GB/s == 1073.741824 B/us +/// (it treats "GB" as GiB when converting the measured GB/s figures). +constexpr double kBytesPerUsPerGbs = + (1024.0 * 1024.0 * 1024.0) / 1e6; // 1073.74... +} // namespace + +BandwidthEntry HardwareConfig::lookupBandwidth(llvm::StringRef src, + llvm::StringRef dst, int coreNum, + int64_t pktBytes) const { + BandwidthEntry result; + std::string key = (src + ":" + dst).str(); + auto it = bandwidthTables.find(key); + if (it != bandwidthTables.end()) { + const BandwidthTable &tbl = it->second; + double gbps = tbl.singleGbps; + if (tbl.hasPerCore && !tbl.perCoreGbps.empty()) { + // tilesim selects the exact core-count row; clamp out-of-range counts + // to the nearest tabulated value (no interpolation, matching tilesim). + int cores = coreNum > 0 ? coreNum : activeBandwidthCores; + int minCores = tbl.perCoreGbps.begin()->first; + int maxCores = tbl.perCoreGbps.rbegin()->first; + if (cores <= minCores) { + gbps = tbl.perCoreGbps.begin()->second; + } else if (cores >= maxCores) { + gbps = tbl.perCoreGbps.rbegin()->second; + } else { + auto eit = tbl.perCoreGbps.find(cores); + gbps = (eit != tbl.perCoreGbps.end()) + ? eit->second + : tbl.perCoreGbps.rbegin()->second; + } + } + result.bwGBs = gbps; + + // Small-packet fitting (tilesim pkt_param). tilesim keeps a single global + // pkt_param applied to every mover; a per-table small_packet entry + // overrides it. Opt-in via enableSmallPacketBw. + if (enableSmallPacketBw && pktBytes > 0) { + const SmallPacketCoeffs *sp = nullptr; + if (tbl.smallPacket.enabled) + sp = &tbl.smallPacket; + else if (hasGlobalSmallPacket && globalSmallPacket.enabled) + sp = &globalSmallPacket; + if (sp && pktBytes < sp->thresholdBytes) { + double a = 0, b = 0; + if (pktBytes < 64) { + a = sp->a64; + b = sp->b64; + } else if (pktBytes < 128) { + a = sp->a128; + b = sp->b128; + } else { + a = sp->a256; + b = sp->b256; + } + if (b > 0) { + // tilesim: bw[B/us] = b / (a*b + pkt_GB) * 1024^3/1e6 + double pktGb = + static_cast(pktBytes) / (1024.0 * 1024.0 * 1024.0); + double bwBus = b / (a * b + pktGb) * kBytesPerUsPerGbs; + result.bwGBs = bwBus / kBytesPerUsPerGbs; // back to GB/s + result.isSmallPacket = true; + } + } + } + } else { + // Unknown (src,dst): fall back to aggregate HBM bandwidth so estimates + // degrade gracefully instead of returning zero. + result.bwGBs = getHBMBandwidthGBs(); + } + return result; +} + +VecCycleEntry HardwareConfig::lookupVecCycle(llvm::StringRef intrinsic, + int elementBits) const { + auto it = vecCycleTables.find(intrinsic); + if (it != vecCycleTables.end()) { + const auto &byDtype = it->second; + llvm::StringRef dt = elementBitsToDtypeKey(elementBits); + auto dit = byDtype.find(dt); + if (dit != byDtype.end()) + return dit->second; + // tilesim falls back to FP32 then warns; we silently fall back. + auto fp32 = byDtype.find("fp32"); + if (fp32 != byDtype.end()) + return fp32->second; + } + return VecCycleEntry{1, 0, 0}; +} + +int64_t HardwareConfig::estimateTransferCycles(llvm::StringRef src, + llvm::StringRef dst, + int64_t bytes, + int coreNum) const { + BandwidthEntry bw = lookupBandwidth(src, dst, coreNum, bytes); + if (bw.bwGBs <= 0) + bw.bwGBs = getHBMBandwidthGBs(); + if (bw.bwGBs <= 0) + bw.bwGBs = 1600.0; + // Replicate tilesim: latency[us] = bytes / (bw[B/us]); cycles = latency * + // clock[MHz]. + double bwBus = bw.bwGBs * kBytesPerUsPerGbs; + double latencyUs = static_cast(bytes) / bwBus; + double cycles = latencyUs * (clockFreqGHz * 1000.0); + return static_cast(std::ceil(cycles)); +} + +void HardwareConfig::getCubeModelThroughput(int elementBits, int &basicM, + int &basicK, int &basicN) const { + basicM = cubeModel.basicM; + int elemBytes = (elementBits + 7) / 8; + basicK = elemBytes > 0 ? cubeModel.basicKNumerator / elemBytes + : cubeModel.basicKNumerator; + basicN = cubeModel.basicN; +} + +int HardwareConfig::getCubeModelRepeatCycles(int elementBits) const { + llvm::StringRef dt = elementBitsToDtypeKey(elementBits); + auto it = cubeModel.repeatCycles.find(dt); + if (it != cubeModel.repeatCycles.end()) + return it->second; + // tilesim: fp32 -> 2, fp16/bf16/int8 -> 1. + return (elementBits == 32) ? 2 : 1; +} + +int HardwareConfig::getCubeModelL0TileLimitBytes() const { + return cubeModel.l0TileLimitKb * 1024; +} + +bool HardwareConfig::areMutexUnits(llvm::StringRef a, llvm::StringRef b) const { + if (a.empty() || b.empty() || a == b) + return false; + for (const auto &group : mutexGroups) { + bool hasA = false, hasB = false; + for (const auto &u : group) { + if (u == a) + hasA = true; + if (u == b) + hasB = true; + } + if (hasA && hasB) + return true; + } + return false; +} + +//===----------------------------------------------------------------------===// +// Pipeline +//===----------------------------------------------------------------------===// + +const PipelinePath * +HardwareConfig::getPipelinePath(llvm::StringRef name) const { + auto it = pipelinePaths.find(name); + return it != pipelinePaths.end() ? &it->second : nullptr; +} + +bool HardwareConfig::canRunInParallel(llvm::StringRef path1, + llvm::StringRef path2) const { + // Check explicit parallelism flags + std::string key = path1.str() + "_and_" + path2.str(); + auto it = parallelismFlags.find(key); + if (it != parallelismFlags.end()) + return it->second; + + // Try reverse order + key = path2.str() + "_and_" + path1.str(); + it = parallelismFlags.find(key); + if (it != parallelismFlags.end()) + return it->second; + + return false; +} + +//===----------------------------------------------------------------------===// +// Validation +//===----------------------------------------------------------------------===// + +bool HardwareConfig::validate(std::string &error) const { + if (clockFreqGHz <= 0) { + error = "Clock frequency must be positive"; + return false; + } + + if (memorySpaces.empty()) { + error = "At least one memory space must be defined"; + return false; + } + + if (computeUnits.empty()) { + error = "At least one compute unit must be defined"; + return false; + } + + // Validate data mover references + for (const auto &kv : dataMovers) { + const auto &dm = kv.second; + if (!dm.srcSpace.empty() && !getMemorySpace(dm.srcSpace)) { + error = "Data mover '" + dm.name + "' references unknown source space '" + + dm.srcSpace + "'"; + return false; + } + for (const auto &dst : dm.dstSpaces) { + if (!getMemorySpace(dst)) { + error = "Data mover '" + dm.name + + "' references unknown destination space '" + dst + "'"; + return false; + } + } + } + + return true; +} diff --git a/third_party/ascend/costmodel/lib/AscendModel/Analysis/PipelineAnalysis.cpp b/third_party/ascend/costmodel/lib/AscendModel/Analysis/PipelineAnalysis.cpp new file mode 100644 index 0000000000..cdd94536ac --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Analysis/PipelineAnalysis.cpp @@ -0,0 +1,599 @@ +//===- PipelineAnalysis.cpp - Pipeline scheduling analysis ----------------===// +// +// This file implements the pipeline analysis for Ascend NPU performance +// modeling, using HardwareConfig for configurable hardware parameters. +// +// Key insight: Ascend 910B has a fully pipelined architecture where all +// hardware units can run in parallel. The only scheduling constraints are +// data dependencies between operations. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/Analysis/PipelineAnalysis.h" +#include "AscendModel/HardwareConfig.h" + +#include "llvm/ADT/SetVector.h" +#include "llvm/Support/Format.h" + +#include +#include + +using namespace mlir; +using namespace mlir::ascend; + +//===----------------------------------------------------------------------===// +// HWUnitPipeline Implementation +//===----------------------------------------------------------------------===// + +void HWUnitPipeline::scheduleOp(PipelineOp &op, int64_t earliestStart) { + // In a fully pipelined architecture, the unit can start a new operation + // as soon as it finishes the previous one OR as soon as dependencies are met. + // The start time is the maximum of: + // 1. When the unit becomes free (currentCycle) + // 2. When all dependencies are satisfied (earliestStart) + int64_t startTime = std::max(currentCycle, earliestStart); + + op.startCycle = startTime; + op.endCycle = startTime + op.duration; + + // Update when this unit will be free for the next operation + currentCycle = op.endCycle; + scheduledOps.push_back(&op); +} + +int64_t HWUnitPipeline::getTotalBusyCycles() const { + int64_t total = 0; + for (const auto *op : scheduledOps) { + total += op->duration; + } + return total; +} + +double HWUnitPipeline::getUtilization(int64_t totalCycles) const { + if (totalCycles == 0) + return 0.0; + return static_cast(getTotalBusyCycles()) / totalCycles * 100.0; +} + +//===----------------------------------------------------------------------===// +// DependencyGraph Implementation +//===----------------------------------------------------------------------===// + +void DependencyGraph::addOp(int64_t opId, Operation *op) { + ops[opId] = op; + if (edges.find(opId) == edges.end()) { + edges[opId] = {}; + } + if (reverseEdges.find(opId) == reverseEdges.end()) { + reverseEdges[opId] = {}; + } +} + +void DependencyGraph::addDependency(int64_t fromId, int64_t toId) { + edges[fromId].push_back(toId); + reverseEdges[toId].push_back(fromId); +} + +llvm::SmallVector +DependencyGraph::getDependencies(int64_t opId) const { + auto it = reverseEdges.find(opId); + if (it != reverseEdges.end()) { + return it->second; + } + return {}; +} + +std::vector DependencyGraph::getTopologicalOrder() const { + std::vector order; + llvm::DenseMap inDegree; + + // Initialize in-degrees + for (const auto &[id, _] : ops) { + inDegree[id] = 0; + } + + for (const auto &[id, successors] : edges) { + for (int64_t succ : successors) { + inDegree[succ]++; + } + } + + // Find all nodes with zero in-degree + std::queue queue; + for (const auto &[id, degree] : inDegree) { + if (degree == 0) { + queue.push(id); + } + } + + // Process nodes + while (!queue.empty()) { + int64_t node = queue.front(); + queue.pop(); + order.push_back(node); + + auto it = edges.find(node); + if (it != edges.end()) { + for (int64_t succ : it->second) { + inDegree[succ]--; + if (inDegree[succ] == 0) { + queue.push(succ); + } + } + } + } + + return order; +} + +bool DependencyGraph::hasCycle() const { + return getTopologicalOrder().size() != ops.size(); +} + +//===----------------------------------------------------------------------===// +// PipelineScheduler Implementation +//===----------------------------------------------------------------------===// + +PipelineScheduler::PipelineScheduler(const HardwareConfig *config) + : hwConfig(config), ownsConfig(false), totalCycles(0) { + if (!hwConfig) { + // Use global config or create default + hwConfig = static_cast(&getHardwareConfig()); + } + initPipelines(); +} + +void PipelineScheduler::initPipelines() { + // Initialize pipelines for all hardware units defined in the config + // For now, we use the standard 910B units + pipelines.emplace(HWUnit::Cube, HWUnitPipeline(HWUnit::Cube)); + pipelines.emplace(HWUnit::CubeMTE2, HWUnitPipeline(HWUnit::CubeMTE2)); + pipelines.emplace(HWUnit::FixPipe, HWUnitPipeline(HWUnit::FixPipe)); + pipelines.emplace(HWUnit::Vector, HWUnitPipeline(HWUnit::Vector)); + pipelines.emplace(HWUnit::VecMTE2, HWUnitPipeline(HWUnit::VecMTE2)); + pipelines.emplace(HWUnit::MTE3, HWUnitPipeline(HWUnit::MTE3)); + pipelines.emplace(HWUnit::Scalar, HWUnitPipeline(HWUnit::Scalar)); +} + +void PipelineScheduler::addOperation(PipelineOp op) { + depGraph.addOp(op.opId, op.mlirOp); + operations.push_back(op); +} + +void PipelineScheduler::addDependency(int64_t fromId, int64_t toId) { + depGraph.addDependency(fromId, toId); +} + +int64_t PipelineScheduler::getEarliestStartTime(const PipelineOp &op) { + int64_t earliest = 0; + + // Check all dependencies - must wait for ALL producers to complete + for (int64_t depId : op.dependsOn) { + // Find the dependent operation + for (const auto &depOp : operations) { + if (depOp.opId == depId) { + earliest = std::max(earliest, depOp.endCycle); + break; + } + } + } + + return earliest; +} + +bool PipelineScheduler::schedule() { + if (depGraph.hasCycle()) { + return false; + } + + // Get topological order - ensures we schedule producers before consumers + std::vector order = depGraph.getTopologicalOrder(); + + // Create a map from opId to operation index + llvm::DenseMap opIdToIndex; + for (size_t i = 0; i < operations.size(); ++i) { + opIdToIndex[operations[i].opId] = i; + } + + // ASAP Scheduling: Schedule each operation as early as possible + // Constraints: + // 1. Data dependencies (must wait for all producers) + // 2. Resource availability (each HW unit can only run one op at a time) + // + // NOTE: In Ascend 910B, DIFFERENT hardware units can ALL run in parallel. + // Only the SAME unit has serialization constraints. + + for (int64_t opId : order) { + auto it = opIdToIndex.find(opId); + if (it == opIdToIndex.end()) + continue; + + PipelineOp &op = operations[it->second]; + + // Get earliest start time based on DATA DEPENDENCIES + int64_t earliestStart = getEarliestStartTime(op); + + // Get the pipeline for this hardware unit + auto pipeIt = pipelines.find(op.hwUnit); + if (pipeIt == pipelines.end()) + continue; + + // Schedule on the appropriate pipeline + // The HWUnitPipeline will handle resource availability constraints + pipeIt->second.scheduleOp(op, earliestStart); + + // Update total cycles (the overall completion time) + totalCycles = std::max(totalCycles, op.endCycle); + } + + return true; +} + +int64_t PipelineScheduler::getKernelCycles(int64_t numPrograms, + int64_t numParallelUnits, + int64_t numInnerIters) const { + if (numPrograms <= 0 || numParallelUnits <= 0) + return totalCycles; + + // Step 1: add pipe_barrier sync cost for the inner loop iterations. + int64_t barrierCycles = + numInnerIters * hwConfig->getPipeBarrierCyclesPerIter(); + + // Step 2: apply scalar overhead factor (captures loop control, address + // arithmetic, and AIC↔AIV synchronisation bubbles). + double scalarFactor = hwConfig->getAIVScalarOverheadFactor(); + int64_t perProgramCycles = static_cast( + (totalCycles + barrierCycles) * (1.0 + scalarFactor)); + + // Step 3: multiply by number of waves (wave serialisation). + int64_t numWaves = (numPrograms + numParallelUnits - 1) / numParallelUnits; + return perProgramCycles * numWaves; +} + +const HWUnitPipeline &PipelineScheduler::getPipeline(HWUnit unit) const { + static HWUnitPipeline emptyPipeline(HWUnit::Scalar); + auto it = pipelines.find(unit); + if (it != pipelines.end()) { + return it->second; + } + return emptyPipeline; +} + +void PipelineScheduler::printTimeline(llvm::raw_ostream &os) const { + // Collect all operations sorted by start time + std::vector sortedOps; + for (const auto &op : operations) { + sortedOps.push_back(&op); + } + std::sort(sortedOps.begin(), sortedOps.end(), + [](const PipelineOp *a, const PipelineOp *b) { + return a->startCycle < b->startCycle; + }); + + // Get hardware info from config + double freqGHz = hwConfig->getClockFrequencyGHz(); + double cyclesPerUs = freqGHz * 1000.0; + + // Print header + os << "\n"; + os << "Hardware: " << hwConfig->getName() << " @ " + << llvm::format("%.2f", freqGHz) << " GHz\n"; + os << "Pipeline Model: Fully pipelined (all units can run in parallel)\n\n"; + + os << "┌────────┬──────────────────────┬────────────┬──────────────┬─────────" + "─────┐\n"; + os << "│ Op ID │ Operation │ HW Unit │ Start │ End " + " │\n"; + os << "├────────┼──────────────────────┼────────────┼──────────────┼─────────" + "─────┤\n"; + + // Print each operation + for (const auto *op : sortedOps) { + std::string opName = op->opName; + if (opName.length() > 20) { + opName = opName.substr(0, 17) + "..."; + } + opName.resize(20, ' '); + + std::string hwUnitStr = stringifyHWUnit(op->hwUnit).str(); + hwUnitStr.resize(10, ' '); + + os << "| " << llvm::format("%6ld", op->opId) << " | " << opName << " | " + << hwUnitStr << " | " << llvm::format("%12ld", op->startCycle) << " | " + << llvm::format("%12ld", op->endCycle) << " |\n"; + } + + os << "└────────┴──────────────────────┴────────────┴──────────────┴─────────" + "─────┘\n"; + os << llvm::format("\nTotal execution time: %ld cycles (%.3f μs)\n", + totalCycles, totalCycles / cyclesPerUs); +} + +void PipelineScheduler::printUtilizationReport(llvm::raw_ostream &os) const { + double freqGHz = hwConfig->getClockFrequencyGHz(); + + os << "\n=== Hardware Unit Utilization ===\n"; + os << "All units can execute in parallel (fully pipelined)\n\n"; + + // Group by path for clarity + os << "Cube Path (HBM -> L1 -> L0A/B -> Cube -> L0C -> HBM):\n"; + for (HWUnit unit : {HWUnit::CubeMTE2, HWUnit::Cube, HWUnit::FixPipe}) { + auto it = pipelines.find(unit); + if (it == pipelines.end()) + continue; + + const auto &pipeline = it->second; + int64_t busyCycles = pipeline.getTotalBusyCycles(); + if (busyCycles > 0 || true) { // Always show + double utilization = pipeline.getUtilization(totalCycles); + std::string unitStr = stringifyHWUnit(unit).str(); + unitStr.resize(12, ' '); + + int barWidth = static_cast(utilization / 5); + std::string bar(barWidth, '#'); + bar.resize(20, '-'); + + os << " " << unitStr << " [" << bar << "] " + << llvm::format("%6.2f", utilization) << "% (" << busyCycles + << " cycles)\n"; + } + } + + os << "\nVector Path (HBM -> UB -> Vector -> UB -> HBM):\n"; + for (HWUnit unit : {HWUnit::VecMTE2, HWUnit::Vector, HWUnit::MTE3}) { + auto it = pipelines.find(unit); + if (it == pipelines.end()) + continue; + + const auto &pipeline = it->second; + int64_t busyCycles = pipeline.getTotalBusyCycles(); + double utilization = pipeline.getUtilization(totalCycles); + std::string unitStr = stringifyHWUnit(unit).str(); + unitStr.resize(12, ' '); + + int barWidth = static_cast(utilization / 5); + std::string bar(barWidth, '#'); + bar.resize(20, '-'); + + os << " " << unitStr << " [" << bar << "] " + << llvm::format("%6.2f", utilization) << "% (" << busyCycles + << " cycles)\n"; + } + + // Find bottleneck (unit with highest utilization) + double maxUtil = 0; + HWUnit bottleneck = HWUnit::Scalar; + for (const auto &[unit, pipeline] : pipelines) { + double util = pipeline.getUtilization(totalCycles); + if (util > maxUtil) { + maxUtil = util; + bottleneck = unit; + } + } + + if (maxUtil > 0) { + os << "\nBottleneck: " << stringifyHWUnit(bottleneck).str() + << llvm::format(" (%.2f%% utilization)\n", maxUtil); + } + + os << "\n"; +} + +//===----------------------------------------------------------------------===// +// PerformanceReport Implementation +//===----------------------------------------------------------------------===// + +void PerformanceReport::print(llvm::raw_ostream &os) const { + os << "\n"; + os << "╔══════════════════════════════════════════════════════════════╗\n"; + os << "║ " << hardwareName << " Performance Analysis Report"; + // Pad to align + int padding = 50 - hardwareName.length(); + for (int i = 0; i < padding; ++i) + os << " "; + os << "║\n"; + os << "╠══════════════════════════════════════════════════════════════╣\n"; + + // Timing + os << "║ Timing Summary ║\n"; + os << llvm::format( + "║ Clock Frequency: %8.2f GHz ║\n", + clockFreqGHz); + os << llvm::format( + "║ Total Cycles (compute):%12ld ║\n", + totalCycles); + os << llvm::format( + "║ Estimated Time: %12.3f μs ║\n", + totalTimeUs); + if (kernelTotalCycles > 0) { + double kernelTimeUs = kernelTotalCycles / (clockFreqGHz * 1000.0); + os << llvm::format( + "║ Kernel Cycles (+ovhd): %12ld ║\n", + kernelTotalCycles); + os << llvm::format( + "║ Kernel Time (+ovhd): %12.3f μs ║\n", + kernelTimeUs); + os << llvm::format( + "║ Num Waves: %12ld ║\n", + numWaves); + } + + os << "║ ║\n"; + + // Roofline + os << "║ Roofline Analysis ║\n"; + os << llvm::format( + "║ Arithmetic Intensity: %8.3f FLOP/Byte ║\n", + arithmeticIntensity); + os << llvm::format( + "║ Achieved TFLOPS: %8.3f ║\n", + achievedTFLOPS); + os << llvm::format( + "║ Peak TFLOPS: %8.3f ║\n", + peakTFLOPS); + os << llvm::format( + "║ Achieved Bandwidth: %8.1f GB/s ║\n", + achievedBandwidth); + os << llvm::format( + "║ Peak Bandwidth: %8.1f GB/s ║\n", + peakBandwidth); + os << "║ Bound: " + << (isComputeBound ? "Compute-bound" : "Memory-bound ") + << " ║\n"; + + os << "║ ║\n"; + + // Utilization + os << "║ Hardware Unit Utilization ║\n"; + for (const auto &[unit, util] : unitUtilization) { + if (util > 0.01) { // Only show if > 0.01% + std::string unitStr = stringifyHWUnit(unit).str(); + unitStr.resize(12, ' '); + + int barWidth = static_cast(util / 5); + std::string bar(barWidth, '#'); + bar.resize(20, '-'); + + os << "║ " << unitStr << " [" << bar << "] " + << llvm::format("%5.1f%%", util) << " ║\n"; + } + } + + os << "╚══════════════════════════════════════════════════════════════╝\n"; +} + +std::string PerformanceReport::toJSON() const { + std::string json = "{\n"; + json += " \"hardware\": \"" + hardwareName + "\",\n"; + json += " \"clock_freq_ghz\": " + std::to_string(clockFreqGHz) + ",\n"; + json += " \"total_cycles\": " + std::to_string(totalCycles) + ",\n"; + json += " \"total_time_us\": " + std::to_string(totalTimeUs) + ",\n"; + json += " \"arithmetic_intensity\": " + std::to_string(arithmeticIntensity) + + ",\n"; + json += " \"achieved_tflops\": " + std::to_string(achievedTFLOPS) + ",\n"; + json += " \"peak_tflops\": " + std::to_string(peakTFLOPS) + ",\n"; + json += + " \"achieved_bandwidth_gbps\": " + std::to_string(achievedBandwidth) + + ",\n"; + json += " \"peak_bandwidth_gbps\": " + std::to_string(peakBandwidth) + ",\n"; + json += " \"is_compute_bound\": " + + std::string(isComputeBound ? "true" : "false") + ",\n"; + json += " \"bottleneck_unit\": \"" + stringifyHWUnit(bottleneckUnit).str() + + "\",\n"; + json += + " \"bottleneck_utilization\": " + std::to_string(bottleneckUtilization) + + ",\n"; + + json += " \"unit_utilization\": {\n"; + bool first = true; + for (const auto &[unit, util] : unitUtilization) { + if (!first) + json += ",\n"; + first = false; + json += + " \"" + stringifyHWUnit(unit).str() + "\": " + std::to_string(util); + } + json += "\n }\n"; + json += "}\n"; + + return json; +} + +//===----------------------------------------------------------------------===// +// RooflineAnalyzer Implementation +//===----------------------------------------------------------------------===// + +RooflineAnalyzer::RooflineAnalyzer(const PipelineScheduler &sched) + : scheduler(sched), config(sched.getConfig()), totalFLOPs(0), + totalBytes(0) { + computeMetrics(); +} + +void RooflineAnalyzer::computeMetrics() { + totalFLOPs = 0; + totalBytes = 0; + + for (const auto &op : scheduler.getAllOps()) { + totalFLOPs += op.flops; + totalBytes += op.bytes; + } +} + +double RooflineAnalyzer::getArithmeticIntensity() const { + if (totalBytes == 0) + return 0.0; + return static_cast(totalFLOPs) / totalBytes; +} + +double RooflineAnalyzer::getTheoreticalPeak() const { + // Peak compute: Cube FP16 TFLOPS + return config.getCubeTFlopsFP16(); +} + +double RooflineAnalyzer::getAchievedPerformance() const { + int64_t cycles = scheduler.getTotalCycles(); + if (cycles == 0) + return 0.0; + + double freqGHz = config.getClockFrequencyGHz(); + double timeSeconds = cycles / (freqGHz * 1e9); + + return (totalFLOPs / 1e12) / timeSeconds; // TFLOPS +} + +bool RooflineAnalyzer::isComputeBound() const { + double ai = getArithmeticIntensity(); + double peakTFLOPS = getTheoreticalPeak(); + double peakBWTBps = config.getMemoryBandwidthTBps("hbm"); + + // Ridge point: where compute ceiling meets memory ceiling + // ridge_ai = peak_compute / peak_bandwidth + double ridgePoint = peakTFLOPS / peakBWTBps; // FLOP/Byte + + return ai >= ridgePoint; +} + +PerformanceReport RooflineAnalyzer::analyze() { + PerformanceReport report; + + // Hardware info + report.hardwareName = config.getName().str(); + report.clockFreqGHz = config.getClockFrequencyGHz(); + + // Timing + report.totalCycles = scheduler.getTotalCycles(); + report.totalTimeUs = config.cyclesToMicroseconds(report.totalCycles); + + // Roofline + report.arithmeticIntensity = getArithmeticIntensity(); + report.achievedTFLOPS = getAchievedPerformance(); + report.peakTFLOPS = getTheoreticalPeak(); + report.isComputeBound = isComputeBound(); + + // Bandwidth + report.peakBandwidth = config.getMemoryBandwidthTBps("hbm") * 1000.0; // GB/s + if (report.totalCycles > 0) { + double timeSeconds = report.totalCycles / (report.clockFreqGHz * 1e9); + report.achievedBandwidth = (totalBytes / 1e9) / timeSeconds; // GB/s + } else { + report.achievedBandwidth = 0; + } + + // Utilization + double maxUtil = 0; + for (HWUnit unit : {HWUnit::Cube, HWUnit::CubeMTE2, HWUnit::FixPipe, + HWUnit::Vector, HWUnit::VecMTE2, HWUnit::MTE3}) { + const auto &pipeline = scheduler.getPipeline(unit); + double util = pipeline.getUtilization(report.totalCycles); + report.unitUtilization[unit] = util; + report.unitBusyCycles[unit] = pipeline.getTotalBusyCycles(); + + if (util > maxUtil) { + maxUtil = util; + report.bottleneckUnit = unit; + } + } + report.bottleneckUtilization = maxUtil; + + return report; +} diff --git a/third_party/ascend/costmodel/lib/AscendModel/Analysis/RooflineAnalysis.cpp b/third_party/ascend/costmodel/lib/AscendModel/Analysis/RooflineAnalysis.cpp new file mode 100644 index 0000000000..d20636d977 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Analysis/RooflineAnalysis.cpp @@ -0,0 +1,330 @@ +//===- RooflineAnalysis.cpp - Roofline model analysis ---------------------===// +// +// This file implements the roofline model analysis for Ascend 910B. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/Analysis/PipelineAnalysis.h" +#include "AscendModel/HardwareParams.h" +#include "AscendModel/IR/AscendModelDialect.h" + +#include "llvm/Support/Format.h" +#include "llvm/Support/JSON.h" + +using namespace mlir; +using namespace mlir::ascend; + +//===----------------------------------------------------------------------===// +// Helper functions +//===----------------------------------------------------------------------===// + +static int64_t getNumElementsFromType(Type type) { + if (auto tensorType = dyn_cast(type)) { + int64_t count = 1; + for (int64_t dim : tensorType.getShape()) { + if (dim == ShapedType::kDynamic) + return 1024; + count *= dim; + } + return count; + } + return 1; +} + +//===----------------------------------------------------------------------===// +// RooflineAnalyzer Implementation +//===----------------------------------------------------------------------===// + +RooflineAnalyzer::RooflineAnalyzer(const PipelineScheduler &scheduler) + : scheduler(scheduler), totalFLOPs(0), totalBytes(0) { + computeMetrics(); +} + +void RooflineAnalyzer::computeMetrics() { + for (const auto &op : scheduler.getAllOps()) { + if (!op.mlirOp) + continue; + + Operation *mlirOp = op.mlirOp; + + // Count FLOPs based on operation type + if (auto matmulOp = dyn_cast(mlirOp)) { + int64_t m = matmulOp.getM(); + int64_t n = matmulOp.getN(); + int64_t k = matmulOp.getK(); + totalFLOPs += 2 * m * n * k; + } + // Binary ops - 1 FLOP per element + else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += getNumElementsFromType(mlirOp->getResult(0).getType()); + } + } + // Division - more expensive + else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += + getNumElementsFromType(mlirOp->getResult(0).getType()) * 10; + } + } + // Simple unary ops + else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += getNumElementsFromType(mlirOp->getResult(0).getType()); + } + } + // Transcendental functions — differentiated FLOP costs + // Reflects relative complexity on Ascend 910B Vector unit + else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += + getNumElementsFromType(mlirOp->getResult(0).getType()) * 15; + } + } else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += + getNumElementsFromType(mlirOp->getResult(0).getType()) * 20; + } + } else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += + getNumElementsFromType(mlirOp->getResult(0).getType()) * 30; + } + } else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += + getNumElementsFromType(mlirOp->getResult(0).getType()) * 25; + } + } + // Sqrt operations + else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += + getNumElementsFromType(mlirOp->getResult(0).getType()) * 10; + } + } + // Reduction ops + else if (isa(mlirOp)) { + if (mlirOp->getNumOperands() > 0) { + totalFLOPs += getNumElementsFromType(mlirOp->getOperand(0).getType()); + } + } + // Select/where op — 1 FLOP per element (comparison + select) + else if (isa(mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += getNumElementsFromType(mlirOp->getResult(0).getType()); + } + } + // Comparison ops — 1 FLOP per element + else if (isa( + mlirOp)) { + if (mlirOp->getNumResults() > 0) { + totalFLOPs += getNumElementsFromType(mlirOp->getResult(0).getType()); + } + } + + // Count bytes for memory operations + if (auto loadOp = dyn_cast(mlirOp)) { + totalBytes += loadOp.getBytes(); + } else if (auto storeOp = dyn_cast(mlirOp)) { + totalBytes += storeOp.getBytes(); + } else if (auto loadOp = dyn_cast(mlirOp)) { + totalBytes += loadOp.getBytes(); + } else if (auto storeOp = dyn_cast(mlirOp)) { + totalBytes += storeOp.getBytes(); + } + } +} + +double RooflineAnalyzer::getArithmeticIntensity() const { + if (totalBytes == 0) + return 0.0; + return static_cast(totalFLOPs) / totalBytes; +} + +bool RooflineAnalyzer::isComputeBound() const { + double ai = getArithmeticIntensity(); + double ridgePoint = + ::ascend::hw::CUBE_TFLOPS / ::ascend::hw::HBM_BANDWIDTH_GBS; + return ai > ridgePoint; +} + +double RooflineAnalyzer::getTheoreticalPeak() const { + double ai = getArithmeticIntensity(); + double memoryRoof = ::ascend::hw::HBM_BANDWIDTH_GBS * ai; + return std::min(::ascend::hw::CUBE_TFLOPS, memoryRoof); +} + +double RooflineAnalyzer::getAchievedPerformance() const { + int64_t totalCycles = scheduler.getTotalCycles(); + if (totalCycles == 0) + return 0.0; + + double timeSeconds = totalCycles / (::ascend::hw::CORE_FREQUENCY_GHZ * 1e9); + return (totalFLOPs / 1e12) / timeSeconds; +} + +PerformanceReport RooflineAnalyzer::analyze() { + PerformanceReport report; + + report.totalCycles = scheduler.getTotalCycles(); + report.totalTimeUs = report.totalCycles / ::ascend::hw::CYCLES_PER_US; + + for (int i = 0; i <= static_cast(HWUnit::Scalar); ++i) { + HWUnit unit = static_cast(i); + const auto &pipeline = scheduler.getPipeline(unit); + report.unitBusyCycles[unit] = pipeline.getTotalBusyCycles(); + report.unitUtilization[unit] = pipeline.getUtilization(report.totalCycles); + } + + report.bottleneckUtilization = 0; + report.bottleneckUnit = HWUnit::Scalar; + for (const auto &[unit, util] : report.unitUtilization) { + if (util > report.bottleneckUtilization) { + report.bottleneckUtilization = util; + report.bottleneckUnit = unit; + } + } + + report.arithmeticIntensity = getArithmeticIntensity(); + report.achievedTFLOPS = getAchievedPerformance(); + report.peakTFLOPS = ::ascend::hw::CUBE_TFLOPS; + report.achievedBandwidth = + totalBytes > 0 ? (totalBytes / 1e9) / (report.totalTimeUs / 1e6) : 0; + report.peakBandwidth = ::ascend::hw::HBM_BANDWIDTH_GBS; + report.isComputeBound = isComputeBound(); + + std::map opCounts; + std::map opCycles; + + for (const auto &op : scheduler.getAllOps()) { + opCounts[op.opName]++; + opCycles[op.opName] += op.duration; + } + + for (const auto &[name, count] : opCounts) { + PerformanceReport::OpStat stat; + stat.opType = name; + stat.count = count; + stat.totalCycles = opCycles[name]; + stat.percentage = report.totalCycles > 0 + ? 100.0 * opCycles[name] / report.totalCycles + : 0; + report.opStats.push_back(stat); + } + + std::sort( + report.opStats.begin(), report.opStats.end(), + [](const auto &a, const auto &b) { return a.percentage > b.percentage; }); + + return report; +} + +//===----------------------------------------------------------------------===// +// PerformanceReport Implementation +//===----------------------------------------------------------------------===// + +void PerformanceReport::print(llvm::raw_ostream &os) const { + os << "\n"; + os << "+=====================================================================" + "=+\n"; + os << "| Ascend 910B Roofline Performance Report " + "|\n"; + os << "+=====================================================================" + "=+\n"; + os << "| " + " |\n"; + os << "| Timing " + " |\n"; + os << "| ------ " + " |\n"; + os << llvm::format( + "| Total Cycles: %15ld |\n", + totalCycles); + os << llvm::format( + "| Estimated Time: %15.3f us |\n", + totalTimeUs); + os << "| " + " |\n"; + os << "| Roofline Analysis " + " |\n"; + os << "| ----------------- " + " |\n"; + os << llvm::format( + "| Arithmetic Intensity: %15.3f FLOP/Byte |\n", + arithmeticIntensity); + os << llvm::format( + "| Achieved TFLOPS: %15.3f |\n", + achievedTFLOPS); + os << llvm::format( + "| Peak TFLOPS: %15.1f |\n", + peakTFLOPS); + os << llvm::format( + "| Achieved Bandwidth: %15.3f GB/s |\n", + achievedBandwidth); + os << llvm::format( + "| Peak Bandwidth: %15.1f GB/s |\n", + peakBandwidth); + os << "| Bound: "; + os << (isComputeBound ? " Compute-bound" : " Memory-bound"); + os << " |\n"; + os << "| " + " |\n"; + os << "| Bottleneck " + " |\n"; + os << "| ---------- " + " |\n"; + os << "| Unit: "; + os << llvm::format("%-15s", stringifyHWUnit(bottleneckUnit).str().c_str()); + os << " |\n"; + os << llvm::format( + "| Utilization: %14.2f%% |\n", + bottleneckUtilization); + os << "| " + " |\n"; + os << "+=====================================================================" + "=+\n"; +} + +std::string PerformanceReport::toJSON() const { + llvm::json::Object root; + + root["total_cycles"] = totalCycles; + root["total_time_us"] = totalTimeUs; + + llvm::json::Object utilization; + for (const auto &[unit, util] : unitUtilization) { + utilization[stringifyHWUnit(unit).str()] = util; + } + root["unit_utilization"] = std::move(utilization); + + llvm::json::Object roofline; + roofline["arithmetic_intensity"] = arithmeticIntensity; + roofline["achieved_tflops"] = achievedTFLOPS; + roofline["peak_tflops"] = peakTFLOPS; + roofline["achieved_bandwidth_gbs"] = achievedBandwidth; + roofline["peak_bandwidth_gbs"] = peakBandwidth; + roofline["is_compute_bound"] = isComputeBound; + root["roofline"] = std::move(roofline); + + llvm::json::Object bottleneck; + bottleneck["unit"] = stringifyHWUnit(bottleneckUnit).str(); + bottleneck["utilization"] = bottleneckUtilization; + root["bottleneck"] = std::move(bottleneck); + + llvm::json::Array opStatsArray; + for (const auto &stat : opStats) { + llvm::json::Object statObj; + statObj["op_type"] = stat.opType; + statObj["count"] = stat.count; + statObj["total_cycles"] = stat.totalCycles; + statObj["percentage"] = stat.percentage; + opStatsArray.push_back(std::move(statObj)); + } + root["op_stats"] = std::move(opStatsArray); + + std::string result; + llvm::raw_string_ostream stream(result); + stream << llvm::json::Value(std::move(root)); + return result; +} diff --git a/third_party/ascend/costmodel/lib/AscendModel/CMakeLists.txt b/third_party/ascend/costmodel/lib/AscendModel/CMakeLists.txt new file mode 100644 index 0000000000..535ceb7343 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(IR) +add_subdirectory(Analysis) +add_subdirectory(Transforms) diff --git a/third_party/ascend/costmodel/lib/AscendModel/IR/AscendModelDialect.cpp b/third_party/ascend/costmodel/lib/AscendModel/IR/AscendModelDialect.cpp new file mode 100644 index 0000000000..ee4b0867d5 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/IR/AscendModelDialect.cpp @@ -0,0 +1,48 @@ +//===- AscendModelDialect.cpp - AscendModel dialect implementation --------===// +// +// This file implements the AscendModel dialect. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/IR/AscendModelDialect.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/DialectImplementation.h" +#include "mlir/IR/OpImplementation.h" +#include "llvm/ADT/TypeSwitch.h" + +using namespace mlir; +using namespace mlir::ascend; + +//===----------------------------------------------------------------------===// +// AscendModel Dialect +//===----------------------------------------------------------------------===// + +#include "AscendModel/IR/AscendModelDialect.cpp.inc" + +void AscendModelDialect::initialize() { + addOperations< +#define GET_OP_LIST +#include "AscendModel/IR/AscendModelOps.cpp.inc" + >(); +} + +//===----------------------------------------------------------------------===// +// Enum Definitions +//===----------------------------------------------------------------------===// + +#include "AscendModel/IR/AscendModelOpsEnums.cpp.inc" + +//===----------------------------------------------------------------------===// +// Attribute Definitions +//===----------------------------------------------------------------------===// + +#define GET_ATTRDEF_CLASSES +#include "AscendModel/IR/AscendModelOpsAttrDefs.cpp.inc" + +//===----------------------------------------------------------------------===// +// Type Definitions +//===----------------------------------------------------------------------===// + +#define GET_TYPEDEF_CLASSES +#include "AscendModel/IR/AscendModelOpsTypes.cpp.inc" diff --git a/third_party/ascend/costmodel/lib/AscendModel/IR/AscendModelOps.cpp b/third_party/ascend/costmodel/lib/AscendModel/IR/AscendModelOps.cpp new file mode 100644 index 0000000000..2aaa6f9841 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/IR/AscendModelOps.cpp @@ -0,0 +1,479 @@ +//===- AscendModelOps.cpp - AscendModel operations implementation ---------===// +// +// This file implements the operations in the AscendModel dialect. +// Each operation implements the EstimateCyclesOpInterface to provide +// hardware-aware cycle estimation. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/Analysis/PipelineAnalysis.h" +#include "AscendModel/HardwareConfig.h" +#include "AscendModel/IR/AscendModelDialect.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/OpImplementation.h" +#include "mlir/IR/TypeUtilities.h" +#include "llvm/ADT/STLExtras.h" + +using namespace mlir; +using namespace mlir::ascend; + +namespace { +/// tilesim bandwidth convention: 1 GB/s == (1024^3)/1e6 B/us. Must match +/// HardwareConfig::estimateTransferCycles / lookupBandwidth exactly. +constexpr double kBytesPerUsPerGbs = (1024.0 * 1024.0 * 1024.0) / 1e6; +} // namespace + +//===----------------------------------------------------------------------===// +// Helper functions +//===----------------------------------------------------------------------===// + +/// Get number of elements from a tensor type. +static int64_t getNumElementsFromType(Type type) { + if (auto tensorType = dyn_cast(type)) { + int64_t count = 1; + for (int64_t dim : tensorType.getShape()) { + if (dim == ShapedType::kDynamic) + return 1024; // Default estimate for dynamic shapes + count *= dim; + } + return count; + } + return 1; +} + +/// Get element bit width from a tensor type. +static int getElementBitsFromType(Type type) { + if (auto tensorType = dyn_cast(type)) { + Type elemType = tensorType.getElementType(); + if (elemType.isF16() || elemType.isBF16()) + return 16; + else if (elemType.isF32()) + return 32; + else if (elemType.isF64()) + return 64; + else if (auto intType = dyn_cast(elemType)) + return intType.getWidth(); + } + return 32; // Default to FP32 +} + +/// Estimate vector operation cycles. +/// Vector unit is 2048 bits wide = 128 FP16 = 64 FP32 elements per cycle. +static int64_t estimateVectorCycles(int64_t numElements, int cyclesPerVectorOp, + int elementBits, int startupLatency) { + // Vector width based on element type + int64_t vectorWidth = 2048 / elementBits; + + // Number of vector operations + int64_t numVectorOps = (numElements + vectorWidth - 1) / vectorWidth; + + // Total cycles + return numVectorOps * cyclesPerVectorOp + startupLatency; +} + +/// Estimate memory transfer cycles. +static int64_t estimateMemoryCycles(int64_t bytes, const HardwareConfig &config, + int startupLatency) { + double bandwidth_gbs = config.getHBMBandwidthGBs(); + if (bandwidth_gbs <= 0) + bandwidth_gbs = 1600.0; + + double time_seconds = static_cast(bytes) / (bandwidth_gbs * 1e9); + double cycles = time_seconds * config.getClockFrequencyGHz() * 1e9; + return static_cast(cycles) + startupLatency; +} + +/// tilesim-migrated vector estimate (root cause 1, vector side): +/// repeats = ceil(numElems * elemBytes / 256B) (vec_size_single_repeat) +/// cycles = compute_cycles(intrinsic, dtype) * repeats + startup +/// Mirrors tilesim plain_simple_op_based_on_cce. The calibrated +/// getVectorStartupLatency() is retained as the per-op pipeline startup +/// anchor; intrinsic compute costs come from the migrated 910B1 table. +static int64_t estimateVectorFromTable(Type type, llvm::StringRef intrinsic, + const HardwareConfig &config) { + int64_t n = getNumElementsFromType(type); + int bits = getElementBitsFromType(type); + int elemBytes = (bits + 7) / 8; + // 256 B per repeat == 2048 bits / elementBits elements per repeat. + int64_t repeats = (n * elemBytes + 255) / 256; + VecCycleEntry e = config.lookupVecCycle(intrinsic, bits); + int64_t compute = static_cast(e.compute) * repeats; + return compute + config.getVectorStartupLatency(); +} + +//===----------------------------------------------------------------------===// +// Include generated definitions +//===----------------------------------------------------------------------===// + +#include "AscendModel/IR/AscendModelInterfaces.cpp.inc" + +#define GET_OP_CLASSES +#include "AscendModel/IR/AscendModelOps.cpp.inc" + +//===----------------------------------------------------------------------===// +// EstimateCyclesOpInterface helpers (defined in interface) +//===----------------------------------------------------------------------===// + +int EstimateCyclesOpInterface::getElementBits() { + Operation *op = getOperation(); + if (op->getNumOperands() > 0) + return getElementBitsFromType(op->getOperand(0).getType()); + if (op->getNumResults() > 0) + return getElementBitsFromType(op->getResult(0).getType()); + return 32; +} + +int64_t EstimateCyclesOpInterface::getNumElements() { + Operation *op = getOperation(); + if (op->getNumOperands() > 0) + return getNumElementsFromType(op->getOperand(0).getType()); + if (op->getNumResults() > 0) + return getNumElementsFromType(op->getResult(0).getType()); + return 1; +} + +//===----------------------------------------------------------------------===// +// MatmulOp Interface Implementation +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// MatmulOp Interface Implementation (L1-pipe roofline — root cause 2) +//===----------------------------------------------------------------------===// +// +// Migrated from tilesim plain_gemm_based_on_l1_pipe. Models the Cube core as a +// real pipeline: the steady-state latency is the max of (L1->L0 input copy) and +// (cube compute), then the first-tile warmup (pipeline fill) is added on top. +// Input copy uses per-mover L1->L0A/L0B bandwidth (441/220.5 GB/s) instead of +// aggregate HBM. HBM->L1 copy is accounted by the separate cube_load op, so it +// is NOT double-counted here (design 4.2.3 option b). +// +// k0 = best_k0(m, n) # largest k0 fitting 32KB L0 tiles +// copy_time = m*k*dt/L0A_bw + n*k*dt/L0B_bw +// compute = ceil(m/bm)*ceil(k/bk)*ceil(n/bn) * repeat_cycles[dt] / clock +// first_tile = (m*k0 + k0*n)*dt / bw +// latency = max(copy_time - first_tile, compute) + first_tile + +int64_t MatmulOp::estimateCycles(const HardwareConfig &config) { + int64_t m = getM(); + int64_t n = getN(); + int64_t k = getK(); + + int elemBits = getElementBitsFromType(getLhs().getType()); + double dtBytes = (elemBits + 7) / 8; + double clockMHz = config.getClockFrequencyGHz() * 1000.0; + + // best_k0: tilesim _calculate_best_k0 — largest k0 whose (m,k0) and (k0,n) + // tiles fit the L0 footprint limit (tilesim uses a fixed *2 byte factor). + int64_t tileLimit = config.getCubeModelL0TileLimitBytes(); + auto fits = [&](int64_t k0) { + return m * k0 * 2 <= tileLimit && n * k0 * 2 <= tileLimit; + }; + int64_t k0 = 32; + if (fits(256)) + k0 = 256; + else if (fits(128)) + k0 = 128; + else if (fits(64)) + k0 = 64; + + int64_t sizeA0 = m * k0 * (int64_t)dtBytes; + int64_t sizeB0 = k0 * n * (int64_t)dtBytes; + + // L1 -> L0A / L0B copy bandwidth (MTE1), in B/us (tilesim GiB convention). + // tilesim cube_op passes the k0-tile sizes (size_a0/size_b0) as pkt_size so + // the small-packet fitting can apply to tiny L1->L0 tiles; mirror that here. + auto bwBus = [&](llvm::StringRef src, llvm::StringRef dst, + int64_t pktBytes) -> double { + BandwidthEntry bw = config.lookupBandwidth(src, dst, 1, pktBytes); + double gbps = bw.bwGBs > 0 ? bw.bwGBs : config.getHBMBandwidthGBs(); + return gbps * kBytesPerUsPerGbs; + }; + double l0aBw = bwBus("l1", "l0a", sizeA0); + double l0bBw = bwBus("l1", "l0b", sizeB0); + + double copyTime = (m * k * dtBytes) / l0aBw + (n * k * dtBytes) / l0bBw; + + // Cube compute (L1-pipe roofline). + int basicM = 16, basicK = 16, basicN = 16; + config.getCubeModelThroughput(elemBits, basicM, basicK, basicN); + int repeatCycles = config.getCubeModelRepeatCycles(elemBits); + auto ceilDiv = [](int64_t a, int64_t b) -> int64_t { + return b > 0 ? (a + b - 1) / b : 0; + }; + int64_t repeats = + ceilDiv(m, basicM) * ceilDiv(k, basicK) * ceilDiv(n, basicN); + double computeTime = static_cast(repeats * repeatCycles) / clockMHz; + + // First-tile warmup (pipeline fill). + double firstTile = + static_cast(sizeA0) / l0aBw + static_cast(sizeB0) / l0bBw; + + double latency = std::max(copyTime - firstTile, computeTime) + firstTile; + int64_t cycles = static_cast(latency * clockMHz); + if (cycles <= 0) + cycles = 1; + return cycles + config.getCubeStartupLatency(); +} + +HWUnit MatmulOp::getHWUnit() { return HWUnit::Cube; } + +int64_t MatmulOp::getFlops() { return 2 * getM() * getN() * getK(); } + +//===----------------------------------------------------------------------===// +// Memory Operations Interface Implementation +//===----------------------------------------------------------------------===// +// +// Migrated to tilesim per-(src,dst,corenum) bandwidth lookup (root cause 1). +// Each transfer op carries optional src_space/dst_space attributes (defaulting +// to the op's natural memory path) so estimateTransferCycles can pick the +// correct mover bandwidth instead of the aggregate HBM figure. + +int64_t CubeLoadOp::estimateCycles(const HardwareConfig &config) { + return config.estimateTransferCycles(getSrcMemSpace(), getDstMemSpace(), + getBytes(), + config.getActiveBandwidthCores()) + + config.getMTE2StartupLatency(); +} +llvm::StringRef CubeLoadOp::getSrcMemSpace() { + if (auto a = getSrcSpaceAttr()) + return a.getValue(); + return "hbm"; +} +llvm::StringRef CubeLoadOp::getDstMemSpace() { + if (auto a = getDstSpaceAttr()) + return a.getValue(); + return "l1"; +} +HWUnit CubeLoadOp::getHWUnit() { return HWUnit::CubeMTE2; } +int64_t CubeLoadOp::getTransferBytes() { return getBytes(); } + +int64_t CubeStoreOp::estimateCycles(const HardwareConfig &config) { + return config.estimateTransferCycles(getSrcMemSpace(), getDstMemSpace(), + getBytes(), + config.getActiveBandwidthCores()) + + config.getFixPipeStartupLatency(); +} +llvm::StringRef CubeStoreOp::getSrcMemSpace() { + if (auto a = getSrcSpaceAttr()) + return a.getValue(); + return "l0c"; +} +llvm::StringRef CubeStoreOp::getDstMemSpace() { + if (auto a = getDstSpaceAttr()) + return a.getValue(); + return "hbm"; +} +HWUnit CubeStoreOp::getHWUnit() { return HWUnit::FixPipe; } +int64_t CubeStoreOp::getTransferBytes() { return getBytes(); } + +int64_t VectorLoadOp::estimateCycles(const HardwareConfig &config) { + return config.estimateTransferCycles(getSrcMemSpace(), getDstMemSpace(), + getBytes(), + config.getActiveBandwidthCores()) + + config.getMTE2StartupLatency(); +} +llvm::StringRef VectorLoadOp::getSrcMemSpace() { + if (auto a = getSrcSpaceAttr()) + return a.getValue(); + return "hbm"; +} +llvm::StringRef VectorLoadOp::getDstMemSpace() { + if (auto a = getDstSpaceAttr()) + return a.getValue(); + return "ub"; +} +HWUnit VectorLoadOp::getHWUnit() { return HWUnit::VecMTE2; } +int64_t VectorLoadOp::getTransferBytes() { return getBytes(); } + +int64_t VectorStoreOp::estimateCycles(const HardwareConfig &config) { + return config.estimateTransferCycles(getSrcMemSpace(), getDstMemSpace(), + getBytes(), + config.getActiveBandwidthCores()) + + config.getMTE3StartupLatency(); +} +llvm::StringRef VectorStoreOp::getSrcMemSpace() { + if (auto a = getSrcSpaceAttr()) + return a.getValue(); + return "ub"; +} +llvm::StringRef VectorStoreOp::getDstMemSpace() { + if (auto a = getDstSpaceAttr()) + return a.getValue(); + return "hbm"; +} +HWUnit VectorStoreOp::getHWUnit() { return HWUnit::MTE3; } +int64_t VectorStoreOp::getTransferBytes() { return getBytes(); } + +//===----------------------------------------------------------------------===// +// Simple Vector Binary Operations +//===----------------------------------------------------------------------===// +// Migrated to tilesim per-(intrinsic,dtype) cycle table (root cause 1). + +#define IMPL_VEC_BINARY(OpClass, Intrinsic) \ + int64_t OpClass::estimateCycles(const HardwareConfig &config) { \ + return estimateVectorFromTable(getLhs().getType(), Intrinsic, config); \ + } \ + HWUnit OpClass::getHWUnit() { return HWUnit::Vector; } \ + int64_t OpClass::getFlops() { \ + return getNumElementsFromType(getLhs().getType()); \ + } + +IMPL_VEC_BINARY(AddOp, "VADD") +IMPL_VEC_BINARY(SubOp, "VSUB") +IMPL_VEC_BINARY(MulOp, "VMUL") +IMPL_VEC_BINARY(MaxOp, "VMAX") +IMPL_VEC_BINARY(MinOp, "VMIN") +// Division is latency-limited; tilesim VDIV table value (fp16:8, fp32:4). +IMPL_VEC_BINARY(DivOp, "VDIV") +// DivOp (VectorBinaryComplex) declares getCyclesPerVectorOp; the macro above +// already provides estimateCycles/getHWUnit/getFlops, so only the latency hook +// is defined separately. tilesim VDIV table value (fp16:8, fp32:4). +int DivOp::getCyclesPerVectorOp() { return 4; } + +#undef IMPL_VEC_BINARY + +//===----------------------------------------------------------------------===// +// Vector Comparison Operations +//===----------------------------------------------------------------------===// +// tilesim maps equal->CMPV_EQ, not-equal->VCMPV_NE, comparisons->VCMPV_GE +// (all ~2 compute cycles/repeat). + +#define IMPL_VEC_CMP(OpClass, Intrinsic) \ + int64_t OpClass::estimateCycles(const HardwareConfig &config) { \ + return estimateVectorFromTable(getLhs().getType(), Intrinsic, config); \ + } \ + HWUnit OpClass::getHWUnit() { return HWUnit::Vector; } \ + int64_t OpClass::getFlops() { \ + return getNumElementsFromType(getLhs().getType()); \ + } + +IMPL_VEC_CMP(CmpEqOp, "CMPV_EQ") +IMPL_VEC_CMP(CmpNeOp, "VCMPV_NE") +IMPL_VEC_CMP(CmpLtOp, "VCMPV_NE") +IMPL_VEC_CMP(CmpLeOp, "VCMPV_NE") +IMPL_VEC_CMP(CmpGtOp, "VCMPV_GE") +IMPL_VEC_CMP(CmpGeOp, "VCMPV_GE") + +#undef IMPL_VEC_CMP + +//===----------------------------------------------------------------------===// +// Simple Vector Unary Operations +//===----------------------------------------------------------------------===// + +#define IMPL_VEC_UNARY_TABLE(OpClass, Intrinsic) \ + int64_t OpClass::estimateCycles(const HardwareConfig &config) { \ + return estimateVectorFromTable(getInput().getType(), Intrinsic, config); \ + } \ + HWUnit OpClass::getHWUnit() { return HWUnit::Vector; } \ + int64_t OpClass::getFlops() { \ + return getNumElementsFromType(getInput().getType()); \ + } + +IMPL_VEC_UNARY_TABLE(AbsOp, "VABS") +IMPL_VEC_UNARY_TABLE(ReluOp, "RELU") +IMPL_VEC_UNARY_TABLE(ExpOp, "VEXP") +IMPL_VEC_UNARY_TABLE(LogOp, "LOG") +IMPL_VEC_UNARY_TABLE(SqrtOp, "VSQRT") + +#undef IMPL_VEC_UNARY_TABLE + +// Ops without a direct tilesim intrinsic keep the legacy calibrated estimate +// (Neg, Cast, Rsqrt, Tanh, Sigmoid). These are minor contributors; the +// bandwidth migration (root cause 1) is the dominant accuracy win. The macro +// deliberately omits getCyclesPerVectorOp: Neg/Cast are VectorUnarySimple and +// do not declare it, so defining it would be an error. The Complex unary ops +// that DO declare it provide it standalone below. +#define IMPL_VEC_UNARY_LEGACY(OpClass, CyclesPerOp) \ + int64_t OpClass::estimateCycles(const HardwareConfig &config) { \ + int64_t n = getNumElementsFromType(getInput().getType()); \ + int bits = getElementBitsFromType(getInput().getType()); \ + return estimateVectorCycles(n, CyclesPerOp, bits, \ + config.getVectorStartupLatency()); \ + } \ + HWUnit OpClass::getHWUnit() { return HWUnit::Vector; } \ + int64_t OpClass::getFlops() { \ + return getNumElementsFromType(getInput().getType()); \ + } + +IMPL_VEC_UNARY_LEGACY(NegOp, 1) +IMPL_VEC_UNARY_LEGACY(CastOp, 1) +IMPL_VEC_UNARY_LEGACY(RsqrtOp, 6) +IMPL_VEC_UNARY_LEGACY(TanhOp, 18) +IMPL_VEC_UNARY_LEGACY(SigmoidOp, 15) + +#undef IMPL_VEC_UNARY_LEGACY + +// VectorUnaryComplex ops (Sqrt/Rsqrt/Exp/Log/Tanh/Sigmoid) declare +// getCyclesPerVectorOp via their ODS base, so a definition is required for the +// link to succeed. estimateCycles above is table/legacy-based and does not set +// it; values retained from the pre-migration transcendental calibration. +// (getCyclesPerVectorOp is a separate interface hook not consumed by the +// current pipeline metric.) +int SqrtOp::getCyclesPerVectorOp() { return 6; } +int RsqrtOp::getCyclesPerVectorOp() { return 6; } +int ExpOp::getCyclesPerVectorOp() { return 9; } +int LogOp::getCyclesPerVectorOp() { return 12; } +int TanhOp::getCyclesPerVectorOp() { return 18; } +int SigmoidOp::getCyclesPerVectorOp() { return 15; } + +//===----------------------------------------------------------------------===// +// Reduce Operations +//===----------------------------------------------------------------------===// + +#define IMPL_REDUCE_OP(OpClass) \ + int64_t OpClass::estimateCycles(const HardwareConfig &config) { \ + int64_t numElems = getNumElementsFromType(getInput().getType()); \ + int bits = getElementBitsFromType(getInput().getType()); \ + int64_t vectorWidth = 2048 / bits; \ + int64_t numVectors = (numElems + vectorWidth - 1) / vectorWidth; \ + int vectorReduceCycles = 0; \ + int64_t w = vectorWidth; \ + while (w > 1) { \ + w /= 2; \ + vectorReduceCycles++; \ + } \ + int crossVectorCycles = 0; \ + int64_t v = numVectors; \ + while (v > 1) { \ + v /= 2; \ + crossVectorCycles++; \ + } \ + return numVectors + vectorReduceCycles + crossVectorCycles + \ + config.getVectorStartupLatency(); \ + } \ + HWUnit OpClass::getHWUnit() { return HWUnit::Vector; } \ + int64_t OpClass::getFlops() { \ + return getNumElementsFromType(getInput().getType()); \ + } + +IMPL_REDUCE_OP(ReduceSumOp) +IMPL_REDUCE_OP(ReduceMaxOp) +IMPL_REDUCE_OP(ReduceMinOp) +IMPL_REDUCE_OP(ReduceProdOp) + +#undef IMPL_REDUCE_OP + +//===----------------------------------------------------------------------===// +// BroadcastOp +//===----------------------------------------------------------------------===// + +int64_t BroadcastOp::estimateCycles(const HardwareConfig &config) { + // Broadcast is essentially free, just startup latency + return 1 + config.getVectorStartupLatency(); +} +HWUnit BroadcastOp::getHWUnit() { return HWUnit::Vector; } + +//===----------------------------------------------------------------------===// +// SelectOp +//===----------------------------------------------------------------------===// + +int64_t SelectOp::estimateCycles(const HardwareConfig &config) { + int64_t n = getNumElementsFromType(getResult().getType()); + int bits = getElementBitsFromType(getResult().getType()); + return estimateVectorCycles(n, 1, bits, config.getVectorStartupLatency()); +} +HWUnit SelectOp::getHWUnit() { return HWUnit::Vector; } +int64_t SelectOp::getFlops() { + return getNumElementsFromType(getResult().getType()); +} diff --git a/third_party/ascend/costmodel/lib/AscendModel/IR/CMakeLists.txt b/third_party/ascend/costmodel/lib/AscendModel/IR/CMakeLists.txt new file mode 100644 index 0000000000..6cf27587a8 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/IR/CMakeLists.txt @@ -0,0 +1,20 @@ +# The AscendModelIR library contains the dialect and ops implementation. +# TableGen targets are defined in include/AscendModel/CMakeLists.txt. + +add_mlir_library(AscendModelIR + AscendModelDialect.cpp + AscendModelOps.cpp + + ADDITIONAL_HEADER_DIRS + ${PROJECT_SOURCE_DIR}/include/AscendModel/IR + + DEPENDS + AscendModelOpsIncGen + AscendModelInterfacesIncGen + + LINK_LIBS PUBLIC + MLIRIR + MLIRSupport + MLIRSideEffectInterfaces + MLIRInferTypeOpInterface +) diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/AssignOpIDs.cpp b/third_party/ascend/costmodel/lib/AscendModel/Transforms/AssignOpIDs.cpp new file mode 100644 index 0000000000..dacd3cf18a --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/AssignOpIDs.cpp @@ -0,0 +1,49 @@ +//===- AssignOpIDs.cpp - Assign unique operation IDs ---------------------===// +// +// This file assigns unique IDs to AscendModel operations for tracking +// in the pipeline analysis. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/IR/AscendModelDialect.h" +#include "AscendModel/Transforms/Passes.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace ascend { + +#define GEN_PASS_DEF_ASSIGNOPIDSPASS +#include "AscendModel/Transforms/Passes.h.inc" + +namespace { + +struct AssignOpIDsPass : public impl::AssignOpIDsPassBase { + using AssignOpIDsPassBase::AssignOpIDsPassBase; + + void runOnOperation() override { + ModuleOp module = getOperation(); + int64_t nextId = 0; + + module.walk([&](Operation *op) { + // Check if this is an AscendModel operation + if (op->getDialect() && op->getDialect()->getNamespace() == "ascend") { + + // Set op_id attribute + op->setAttr( + "op_id", + IntegerAttr::get(IntegerType::get(op->getContext(), 64), nextId++)); + } + }); + + // Store total operation count as module attribute + module->setAttr( + "ascend.total_ops", + IntegerAttr::get(IntegerType::get(module.getContext(), 64), nextId)); + } +}; + +} // namespace +} // namespace ascend +} // namespace mlir diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/CMakeLists.txt b/third_party/ascend/costmodel/lib/AscendModel/Transforms/CMakeLists.txt new file mode 100644 index 0000000000..61d63a855f --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/CMakeLists.txt @@ -0,0 +1,32 @@ +add_mlir_library(AscendModelTransforms + ConvertTritonToAscend.cpp + InsertDataTransfers.cpp + AssignOpIDs.cpp + EstimateCycles.cpp + HIVMAnalysisPass.cpp + PipelineAnalysisPass.cpp + PerfReportPass.cpp + PassRegistration.cpp + + DEPENDS + AscendModelOpsIncGen + AscendModelTransformsPassIncGen + + LINK_LIBS PUBLIC + AscendModelIR + AscendModelAnalysis + MLIRIR + MLIRPass + MLIRSupport + MLIRTransformUtils + MLIRSCFDialect + MLIRArithDialect + MLIRMathDialect + MLIRTensorDialect +) + +# Conditionally link Triton dialect library and add dependency +if(TRITONSIM_HAS_TRITON AND TRITON_DIALECT_LIB) + add_dependencies(obj.AscendModelTransforms TritonDialectIncGen) + target_link_libraries(obj.AscendModelTransforms PRIVATE ${TRITON_DIALECT_LIB}) +endif() diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/ConvertTritonToAscend.cpp b/third_party/ascend/costmodel/lib/AscendModel/Transforms/ConvertTritonToAscend.cpp new file mode 100644 index 0000000000..9f43e5666a --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/ConvertTritonToAscend.cpp @@ -0,0 +1,860 @@ +//===- ConvertTritonToAscend.cpp - Convert Triton IR to AscendModel -------===// +// +// This file implements the conversion from Triton IR to AscendModel dialect +// for Ascend 910B performance modeling. +// +// Key design: Triton's pointer arithmetic chain (tt.splat -> tt.make_range -> +// tt.addptr -> tt.load/store) is folded into a single ascend.vector_load or +// ascend.vector_store. The address computation ops have zero hardware cost +// on Ascend -- they are absorbed into the DMA descriptor. +// +// When built with Triton support (TRITONSIM_HAS_TRITON), this pass uses +// typed op matching. Without Triton, string-based matching is used. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/HardwareParams.h" +#include "AscendModel/IR/AscendModelDialect.h" +#include "AscendModel/Transforms/Passes.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" + +// Triton typed op headers (optional) +#ifdef TRITONSIM_HAS_TRITON +#include "triton/Dialect/Triton/IR/Dialect.h" +#endif + +namespace mlir { +namespace ascend { + +#define GEN_PASS_DEF_CONVERTTRITONTOASCENDPASS +#include "AscendModel/Transforms/Passes.h.inc" + +namespace { + +//===----------------------------------------------------------------------===// +// Helper Functions +//===----------------------------------------------------------------------===// + +static int64_t getByteSize(Type type) { + if (auto tensorType = dyn_cast(type)) { + int64_t count = 1; + for (int64_t dim : tensorType.getShape()) { + if (dim == ShapedType::kDynamic) + return -1; + count *= dim; + } + Type elemType = tensorType.getElementType(); + int64_t elemSize = (elemType.getIntOrFloatBitWidth() + 7) / 8; + return count * elemSize; + } + if (type.isIntOrFloat()) { + return (type.getIntOrFloatBitWidth() + 7) / 8; + } + return 0; +} + +/// Check if an op matches a given dialect.op name string. +static bool isOpNamed(Operation *op, StringRef name) { + return op->getName().getStringRef() == name; +} + +/// Walk back through the Triton pointer arithmetic chain: +/// tt.load <- tt.addptr <- tt.splat <- %blockArg +/// Returns the ultimate source Value (typically a function argument). +/// Used for provenance tracking; the exact address is irrelevant for +/// performance modeling. +static Value tracePointerSource(Value ptr) { + Value current = ptr; + while (Operation *def = current.getDefiningOp()) { + StringRef name = def->getName().getStringRef(); + if (name == "tt.addptr" || name == "tt.splat" || name == "tt.bitcast" || + name == "tt.int_to_ptr") { + current = def->getOperand(0); + } else { + break; + } + } + return current; +} + +/// Create a placeholder tensor source for ascend load ops. +/// Triton pointer types (tensor>) are not valid +/// AscendModel tensor inputs, so we create a tensor.empty with the +/// correct shape to represent "loading N bytes from memory". +static Value createPlaceholderSource(Type resultType, Location loc, + PatternRewriter &rewriter) { + if (auto tensorType = dyn_cast(resultType)) { + return rewriter.create(loc, tensorType.getShape(), + tensorType.getElementType()); + } + return Value(); +} + +//===----------------------------------------------------------------------===// +// Triton-Specific Conversion Patterns +//===----------------------------------------------------------------------===// + +/// Helper: check if a value comes from a cube operation (dot/matmul or +/// cube_load) +static bool isFromCubeOp(Value v) { + Operation *def = v.getDefiningOp(); + if (!def) + return false; + StringRef name = def->getName().getStringRef(); + return name == "tt.dot" || name == "ascend.matmul" || + name == "ascend.cube_load"; +} + +/// Helper: check if a value comes from a load (tt.load or ascend.*_load) +static bool isFromLoad(Value v) { + // Trace through tt.trans etc + Value current = v; + while (Operation *def = current.getDefiningOp()) { + StringRef name = def->getName().getStringRef(); + if (name == "tt.load" || name == "ascend.cube_load" || + name == "ascend.vector_load") { + return true; + } + if (name == "tt.trans" || name == "tt.bitcast" || name == "tt.reshape" || + name == "tt.expand_dims") { + current = def->getOperand(0); + } else { + break; + } + } + return false; +} + +/// Convert tt.dot -> ascend.matmul with proper data transfer ops +/// +/// Data flow rules: +/// - If input comes from tt.load: already converted to cube_load by +/// ConvertTritonLoad +/// - If input comes from vector ops: insert vector_store + cube_load +/// - Output always goes to cube_store + vector_load (for subsequent vector ops) +struct ConvertTritonDot : public RewritePattern { + ConvertTritonDot(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/10, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { +#ifdef TRITONSIM_HAS_TRITON + auto dotOp = dyn_cast(op); + if (!dotOp) + return failure(); + Value lhs = dotOp.getA(); + Value rhs = dotOp.getB(); + Value acc = dotOp.getC(); +#else + if (!isOpNamed(op, "tt.dot")) + return failure(); + Value lhs = op->getOperand(0); + Value rhs = op->getOperand(1); + Value acc = op->getNumOperands() > 2 ? op->getOperand(2) : Value(); +#endif + + Location loc = op->getLoc(); + auto lhsType = dyn_cast(lhs.getType()); + auto rhsType = dyn_cast(rhs.getType()); + + if (!lhsType || !rhsType || lhsType.getRank() != 2 || + rhsType.getRank() != 2) + return failure(); + + int64_t M = lhsType.getShape()[0]; + int64_t K = lhsType.getShape()[1]; + int64_t N = rhsType.getShape()[1]; + + auto resultType = op->getResult(0).getType(); + + // Helper lambda: ensure operand is in Cube memory (cube_load output) + // If it comes from vector ops, insert vector_store + cube_load + auto ensureCubeInput = [&](Value operand) -> Value { + // If from load (already handled by ConvertTritonLoad), use as-is + if (isFromLoad(operand)) { + return operand; + } + + // If from vector operations, need to transfer to cube + auto tensorType = dyn_cast(operand.getType()); + if (!tensorType) + return operand; + + int64_t bytes = getByteSize(tensorType); + + // Vector store (write from vector core to HBM via MTE3) + rewriter.create( + loc, operand, bytes, rewriter.getStringAttr("ub"), + rewriter.getStringAttr("hbm"), nullptr, nullptr); + + // Cube load (read into cube core L1 via MTE2) + Value placeholder = createPlaceholderSource(tensorType, loc, rewriter); + auto cubeLoad = rewriter.create( + loc, tensorType, placeholder ? placeholder : operand, bytes, + rewriter.getStringAttr("hbm"), rewriter.getStringAttr("l1"), nullptr, + nullptr); + + return cubeLoad.getResult(); + }; + + // Prepare inputs - ensure they're in cube memory + Value lhsInput = ensureCubeInput(lhs); + Value rhsInput = ensureCubeInput(rhs); + + // Handle accumulator if present and non-zero + Value accInput = acc; + if (acc) { + // Check if acc is a non-zero constant + bool isZeroAcc = false; + if (auto defOp = acc.getDefiningOp()) { + if (defOp->getName().getStringRef() == "arith.constant") { + if (auto attr = defOp->getAttr("value")) { + if (auto denseAttr = dyn_cast(attr)) { + isZeroAcc = denseAttr.isSplat() && + denseAttr.getSplatValue().isZero(); + } + } + } + } + + // If non-zero accumulator from vector ops, transfer to cube + if (!isZeroAcc && !isFromLoad(acc) && !isFromCubeOp(acc)) { + accInput = ensureCubeInput(acc); + } + } + + // Create matmul + auto matmul = rewriter.create(loc, resultType, lhsInput, rhsInput, + M, N, K, nullptr, nullptr); + + // Output: cube_store (write from cube L0C to HBM via FixPipe) + int64_t resultBytes = getByteSize(resultType); + rewriter.create( + loc, matmul.getResult(), resultBytes, rewriter.getStringAttr("l0c"), + rewriter.getStringAttr("hbm"), nullptr, nullptr); + + // Check if result is used by vector operations (or loop yield) + // If so, insert vector_load to bring data into vector core + bool usedByVectorOps = false; + for (Operation *user : op->getResult(0).getUsers()) { + StringRef userName = user->getName().getStringRef(); + // Only skip if used exclusively by another dot/matmul + if (userName != "tt.dot" && userName != "ascend.matmul") { + usedByVectorOps = true; + break; + } + } + + if (usedByVectorOps) { + // Vector load (read from HBM into vector core UB via MTE2) + auto resultTensorType = dyn_cast(resultType); + Value placeholder = createPlaceholderSource(resultType, loc, rewriter); + auto vecLoad = rewriter.create( + loc, resultType, placeholder ? placeholder : matmul.getResult(), + resultBytes, rewriter.getStringAttr("hbm"), + rewriter.getStringAttr("ub"), nullptr, nullptr); + rewriter.replaceOp(op, vecLoad.getResult()); + } else { + rewriter.replaceOp(op, matmul.getResult()); + } + + return success(); + } +}; + +/// Convert tt.load -> ascend.cube_load or ascend.vector_load +/// +/// If the load has "ascend.used_by_dot" attribute (set in analysis phase), +/// it becomes a cube_load. Otherwise, it becomes a vector_load. +struct ConvertTritonLoad : public RewritePattern { + ConvertTritonLoad(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/10, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { +#ifdef TRITONSIM_HAS_TRITON + if (!isa(op)) + return failure(); +#else + if (!isOpNamed(op, "tt.load")) + return failure(); +#endif + + if (op->getNumResults() == 0) + return failure(); + + Location loc = op->getLoc(); + auto resultType = op->getResult(0).getType(); + int64_t bytes = getByteSize(resultType); + + // Create a placeholder tensor source + Value source = createPlaceholderSource(resultType, loc, rewriter); + if (!source) { + source = op->getOperand(0); + } + + // Check if this load is marked as used by dot (from analysis phase) + bool usedByDot = op->hasAttr("ascend.used_by_dot"); + + if (usedByDot) { + auto cubeLoad = rewriter.create( + loc, resultType, source, bytes, rewriter.getStringAttr("hbm"), + rewriter.getStringAttr("l1"), nullptr, nullptr); + rewriter.replaceOp(op, cubeLoad.getResult()); + } else { + auto vecLoad = rewriter.create( + loc, resultType, source, bytes, rewriter.getStringAttr("hbm"), + rewriter.getStringAttr("ub"), nullptr, nullptr); + rewriter.replaceOp(op, vecLoad.getResult()); + } + return success(); + } +}; + +/// Convert tt.trans -> direct replacement (zero cost in perf modeling) +/// +/// Transpose is a logical reshape that doesn't incur hardware cost +/// on Ascend (handled by address calculation in hardware). +/// We replace it directly with its input for modeling purposes. +struct ConvertTritonTrans : public RewritePattern { + ConvertTritonTrans(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/10, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { + if (!isOpNamed(op, "tt.trans")) + return failure(); + + if (op->getNumOperands() == 0 || op->getNumResults() == 0) + return failure(); + + // tt.trans just reorders the tensor - for perf modeling we pass through + // The actual memory access pattern is handled by the load/store + Value input = op->getOperand(0); + rewriter.replaceOp(op, input); + return success(); + } +}; + +/// Convert tt.store -> ascend.vector_store +/// +/// The data operand (operand 1) is already a proper tensor -- used +/// directly. The pointer operand (operand 0, from tt.addptr) is discarded. +struct ConvertTritonStore : public RewritePattern { + ConvertTritonStore(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/10, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { +#ifdef TRITONSIM_HAS_TRITON + if (!isa(op)) + return failure(); +#else + if (!isOpNamed(op, "tt.store")) + return failure(); +#endif + + if (op->getNumOperands() < 2) + return failure(); + + // operand(0) = pointer (from addptr chain, discarded) + // operand(1) = data tensor (used directly) + Value data = op->getOperand(1); + int64_t bytes = getByteSize(data.getType()); + + rewriter.create( + op->getLoc(), data, bytes, rewriter.getStringAttr("ub"), + rewriter.getStringAttr("hbm"), nullptr, nullptr); + rewriter.eraseOp(op); + return success(); + } +}; + +/// Erase Triton address-computation ops that become dead after load/store +/// conversion. These ops have zero hardware cost: +/// tt.addptr, tt.splat, tt.make_range, tt.expand_dims, +/// tt.get_program_id, tt.int_to_ptr, tt.ptr_to_int, tt.bitcast, tt.trans +/// +/// The pattern runs at low benefit so load/store conversions happen first. +/// Greedy rewriting iterates until convergence, so the cascade works: +/// tt.load erased -> tt.addptr dead -> tt.splat dead -> tt.make_range dead +struct EraseDeadTritonAddrOps : public RewritePattern { + EraseDeadTritonAddrOps(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/1, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { + // Only match Triton dialect ops + if (!op->getName().getStringRef().starts_with("tt.")) + return failure(); + + StringRef name = op->getName().getStringRef(); + bool isAddrOrNoCost = + (name == "tt.addptr" || name == "tt.splat" || name == "tt.make_range" || + name == "tt.expand_dims" || name == "tt.get_program_id" || + name == "tt.int_to_ptr" || name == "tt.ptr_to_int" || + name == "tt.bitcast" || name == "tt.trans" || name == "tt.reshape"); + + if (!isAddrOrNoCost) + return failure(); + + // Only erase if all results are unused + if (!op->use_empty()) + return failure(); + + rewriter.eraseOp(op); + return success(); + } +}; + +// NOTE: tt.func / tt.return are intentionally NOT converted. +// They are Triton's function shell. func.return requires func.func as parent, +// but the parent here is tt.func. For perf modeling we only care about the +// compute ops inside the function body; the shell is harmless and all +// downstream passes walk ModuleOp, not func.func specifically. + +struct ConvertTritonReduce : public RewritePattern { + ConvertTritonReduce(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/10, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { +#ifdef TRITONSIM_HAS_TRITON + auto reduceOp = dyn_cast(op); + if (!reduceOp) + return failure(); + int64_t axis = reduceOp.getAxis(); +#else + if (!isOpNamed(op, "tt.reduce")) + return failure(); + auto axisAttr = op->getAttrOfType("axis"); + if (!axisAttr) + return failure(); + int64_t axis = axisAttr.getInt(); +#endif + + Location loc = op->getLoc(); + auto resultType = op->getResult(0).getType(); + + // Determine reduction kind from the body + bool isMax = false, isMin = false, isProd = false; + if (!op->getRegions().empty()) { + Region ®ion = op->getRegion(0); + if (!region.empty()) { + Block &block = region.front(); + for (Operation &innerOp : block) { + StringRef innerName = innerOp.getName().getStringRef(); + if (innerName.contains("max")) + isMax = true; + else if (innerName.contains("min")) + isMin = true; + else if (innerName.contains("mul")) + isProd = true; + } + } + } + + Operation *newReduceOp; + if (isMax) { + newReduceOp = rewriter.create( + loc, resultType, op->getOperand(0), axis, nullptr, nullptr); + } else if (isMin) { + newReduceOp = rewriter.create( + loc, resultType, op->getOperand(0), axis, nullptr, nullptr); + } else if (isProd) { + newReduceOp = rewriter.create( + loc, resultType, op->getOperand(0), axis, nullptr, nullptr); + } else { + newReduceOp = rewriter.create( + loc, resultType, op->getOperand(0), axis, nullptr, nullptr); + } + + rewriter.replaceOp(op, newReduceOp->getResult(0)); + return success(); + } +}; + +struct ConvertTritonBroadcast : public RewritePattern { + ConvertTritonBroadcast(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/10, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { +#ifdef TRITONSIM_HAS_TRITON + if (!isa(op)) + return failure(); +#else + if (!isOpNamed(op, "tt.broadcast")) + return failure(); +#endif + + auto resultType = op->getResult(0).getType(); + auto tensorType = dyn_cast(resultType); + if (!tensorType) + return failure(); + + SmallVector shape(tensorType.getShape()); + auto broadcastOp = rewriter.create( + op->getLoc(), resultType, op->getOperand(0), + rewriter.getDenseI64ArrayAttr(shape), nullptr, nullptr); + + rewriter.replaceOp(op, broadcastOp.getResult()); + return success(); + } +}; + +//===----------------------------------------------------------------------===// +// Arith / Math Conversion Patterns +//===----------------------------------------------------------------------===// + +struct ConvertArithBinaryOp : public RewritePattern { + ConvertArithBinaryOp(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/1, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { + StringRef name = op->getName().getStringRef(); + if (op->getNumResults() == 0) + return failure(); + + auto resultType = op->getResult(0).getType(); + if (!isa(resultType)) + return failure(); + if (op->getNumOperands() < 2) + return failure(); + + Value lhs = op->getOperand(0); + Value rhs = op->getOperand(1); + Location loc = op->getLoc(); + Operation *newOp = nullptr; + + if (name == "arith.addf" || name == "arith.addi") { + newOp = + rewriter.create(loc, resultType, lhs, rhs, nullptr, nullptr); + } else if (name == "arith.subf" || name == "arith.subi") { + newOp = + rewriter.create(loc, resultType, lhs, rhs, nullptr, nullptr); + } else if (name == "arith.mulf" || name == "arith.muli") { + newOp = + rewriter.create(loc, resultType, lhs, rhs, nullptr, nullptr); + } else if (name == "arith.divf" || name == "arith.divsi" || + name == "arith.divui") { + newOp = + rewriter.create(loc, resultType, lhs, rhs, nullptr, nullptr); + } else if (name == "arith.maxnumf" || name == "arith.maximumf" || + name == "arith.maxsi" || name == "arith.maxui") { + newOp = + rewriter.create(loc, resultType, lhs, rhs, nullptr, nullptr); + } else if (name == "arith.minnumf" || name == "arith.minimumf" || + name == "arith.minsi" || name == "arith.minui") { + newOp = + rewriter.create(loc, resultType, lhs, rhs, nullptr, nullptr); + } else { + return failure(); + } + + rewriter.replaceOp(op, newOp->getResult(0)); + return success(); + } +}; + +struct ConvertArithCmpOp : public RewritePattern { + ConvertArithCmpOp(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/1, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { + StringRef name = op->getName().getStringRef(); + + // Match arith.cmpf and arith.cmpi + if (!name.starts_with("arith.cmpf") && !name.starts_with("arith.cmpi")) + return failure(); + + if (op->getNumResults() == 0 || op->getNumOperands() < 2) + return failure(); + + auto resultType = op->getResult(0).getType(); + if (!isa(resultType)) + return failure(); + + Value lhs = op->getOperand(0); + Value rhs = op->getOperand(1); + Location loc = op->getLoc(); + + // Get the predicate attribute + auto predicateAttr = op->getAttrOfType("predicate"); + if (!predicateAttr) + return failure(); + + int64_t predicate = predicateAttr.getInt(); + Operation *newOp = nullptr; + + if (name == "arith.cmpf") { + // Float comparison predicates (arith::CmpFPredicate) + // 1=OEQ, 2=OGT, 3=OGE, 4=OLT, 5=OLE, 6=ONE, 8=UEQ, 9=UGT, 10=UGE, 11=ULT, + // 12=ULE, 13=UNE + switch (predicate) { + case 1: // OEQ - ordered equal + case 8: // UEQ - unordered equal + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 6: // ONE - ordered not equal + case 13: // UNE - unordered not equal + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 4: // OLT - ordered less than + case 11: // ULT - unordered less than + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 5: // OLE - ordered less or equal + case 12: // ULE - unordered less or equal + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 2: // OGT - ordered greater than + case 9: // UGT - unordered greater than + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 3: // OGE - ordered greater or equal + case 10: // UGE - unordered greater or equal + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + default: + return failure(); + } + } else if (name == "arith.cmpi") { + // Integer comparison predicates (arith::CmpIPredicate) + // 0=eq, 1=ne, 2=slt, 3=sle, 4=sgt, 5=sge, 6=ult, 7=ule, 8=ugt, 9=uge + switch (predicate) { + case 0: // eq + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 1: // ne + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 2: // slt + case 6: // ult + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 3: // sle + case 7: // ule + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 4: // sgt + case 8: // ugt + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + case 5: // sge + case 9: // uge + newOp = rewriter.create(loc, resultType, lhs, rhs, nullptr, + nullptr); + break; + default: + return failure(); + } + } else { + return failure(); + } + + rewriter.replaceOp(op, newOp->getResult(0)); + return success(); + } +}; + +struct ConvertMathUnaryOp : public RewritePattern { + ConvertMathUnaryOp(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/1, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { + StringRef name = op->getName().getStringRef(); + if (op->getNumResults() == 0 || op->getNumOperands() == 0) + return failure(); + + auto resultType = op->getResult(0).getType(); + if (!isa(resultType)) + return failure(); + + Value input = op->getOperand(0); + Location loc = op->getLoc(); + Operation *newOp = nullptr; + + if (name == "math.exp" || name == "math.exp2") { + newOp = rewriter.create(loc, resultType, input, nullptr, nullptr); + } else if (name == "math.log" || name == "math.log2") { + newOp = rewriter.create(loc, resultType, input, nullptr, nullptr); + } else if (name == "math.sqrt") { + newOp = rewriter.create(loc, resultType, input, nullptr, nullptr); + } else if (name == "math.rsqrt") { + newOp = + rewriter.create(loc, resultType, input, nullptr, nullptr); + } else if (name == "math.tanh") { + newOp = rewriter.create(loc, resultType, input, nullptr, nullptr); + } else if (name == "arith.negf") { + newOp = rewriter.create(loc, resultType, input, nullptr, nullptr); + } else if (name == "math.absf" || name == "math.absi") { + newOp = rewriter.create(loc, resultType, input, nullptr, nullptr); + } else { + return failure(); + } + + rewriter.replaceOp(op, newOp->getResult(0)); + return success(); + } +}; + +struct ConvertArithSelect : public RewritePattern { + ConvertArithSelect(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/1, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { + if (!isOpNamed(op, "arith.select")) + return failure(); + if (op->getNumOperands() != 3 || op->getNumResults() == 0) + return failure(); + + auto resultType = op->getResult(0).getType(); + if (!isa(resultType)) + return failure(); + + auto selectOp = rewriter.create( + op->getLoc(), resultType, op->getOperand(0), op->getOperand(1), + op->getOperand(2), nullptr, nullptr); + + rewriter.replaceOp(op, selectOp.getResult()); + return success(); + } +}; + +struct ConvertArithCast : public RewritePattern { + ConvertArithCast(MLIRContext *ctx) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/1, ctx) {} + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const override { + StringRef name = op->getName().getStringRef(); + + // Check if it's a type conversion operation + // All cast operations (1 cycle each) are unified to ascend.cast + if (!name.starts_with("arith.ext") && !name.starts_with("arith.trunc") && + !name.starts_with("arith.sitofp") && + !name.starts_with("arith.uitofp") && + !name.starts_with("arith.fptosi") && + !name.starts_with("arith.fptoui") && + !name.starts_with("arith.bitcast") && + !name.starts_with("arith.index_cast")) + return failure(); + + if (op->getNumResults() == 0 || op->getNumOperands() == 0) + return failure(); + + auto resultType = op->getResult(0).getType(); + if (!isa(resultType)) + return failure(); + + // All cast operations map to ascend.cast (1 cycle per vector op) + auto castOp = rewriter.create(op->getLoc(), resultType, + op->getOperand(0), nullptr, nullptr); + + rewriter.replaceOp(op, castOp.getResult()); + return success(); + } +}; + +//===----------------------------------------------------------------------===// +// Pass Definition +//===----------------------------------------------------------------------===// + +struct ConvertTritonToAscendPass + : public impl::ConvertTritonToAscendPassBase { + using ConvertTritonToAscendPassBase::ConvertTritonToAscendPassBase; + + void runOnOperation() override { + ModuleOp module = getOperation(); + MLIRContext *ctx = &getContext(); + + // === Phase 1: Analysis === + // Mark all tt.load operations that are used by tt.dot (directly or through + // tt.trans) This allows ConvertTritonLoad to correctly choose cube_load vs + // vector_load + module.walk([&](Operation *op) { + StringRef name = op->getName().getStringRef(); + if (name != "tt.dot") + return; + + // For each operand of tt.dot, trace back to find source tt.load + for (Value operand : op->getOperands()) { + Value current = operand; + // Trace through tt.trans, tt.bitcast, etc. + while (Operation *def = current.getDefiningOp()) { + StringRef defName = def->getName().getStringRef(); + if (defName == "tt.load") { + // Mark this load as used by dot + def->setAttr("ascend.used_by_dot", UnitAttr::get(ctx)); + break; + } + if (defName == "tt.trans" || defName == "tt.bitcast" || + defName == "tt.reshape" || defName == "tt.expand_dims") { + current = def->getOperand(0); + } else { + break; + } + } + } + }); + + // === Phase 2: Pattern Rewriting === + RewritePatternSet patterns(ctx); + + // === High benefit (10): Triton compute ops -> AscendModel ops === + // These run first and break pointer chain dependencies. + patterns.add(ctx); // tt.dot -> matmul + cube_store + patterns.add( + ctx); // tt.load -> cube_load (for dot) or vector_load + patterns.add( + ctx); // tt.trans -> pass-through (zero cost) + patterns.add(ctx); // tt.store -> ascend.vector_store + patterns.add(ctx); // tt.reduce -> ascend.reduce_* + patterns.add( + ctx); // tt.broadcast -> ascend.broadcast + // NOTE: tt.func/tt.return are left as-is (see comment above) + + // === Low benefit (1): cleanup and arith/math conversion === + // Erase dead address ops (cascade: addptr -> splat -> make_range) + patterns.add(ctx); + + // Arith/Math tensor ops -> AscendModel vector ops + patterns.add(ctx); + patterns.add(ctx); + patterns.add(ctx); + patterns.add(ctx); + patterns.add(ctx); + + if (failed(applyPatternsGreedily(module, std::move(patterns)))) { + signalPassFailure(); + } + } +}; + +} // namespace +} // namespace ascend +} // namespace mlir diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/EstimateCycles.cpp b/third_party/ascend/costmodel/lib/AscendModel/Transforms/EstimateCycles.cpp new file mode 100644 index 0000000000..bb065ec419 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/EstimateCycles.cpp @@ -0,0 +1,316 @@ +//===- EstimateCycles.cpp - Estimate execution cycles --------------------===// +// +// This file estimates execution cycles for AscendModel operations using +// the Roofline model. Compute and memory operations can overlap, so we +// take max(compute_time, memory_time) instead of sum. +// +// For Triton kernels with loops depending on tt.get_program_id, use: +// inproc-costmodel --estimate-cycles="arg-bindings=arg2=100,pid_x=0" +// input.mlir +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/HardwareConfig.h" +#include "AscendModel/IR/AscendModelDialect.h" +#include "AscendModel/IR/AscendModelInterfaces.h" +#include "AscendModel/Transforms/Passes.h" +#include "AscendModel/Utils.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/Support/raw_ostream.h" + +namespace mlir { +namespace ascend { + +#define GEN_PASS_DEF_ESTIMATECYCLESPASS +#include "AscendModel/Transforms/Passes.h.inc" + +namespace { + +using utils::analyzeScfForTripCount; +using utils::getLoopMultiplier; +using utils::getNumElements; +using utils::getScfForTripCount; +using utils::getScfForTripCountWithBindings; +using utils::LoopTripCountResult; +using utils::parseBindings; +using utils::parseLoopTripCounts; + +//===----------------------------------------------------------------------===// +// Roofline Analysis +//===----------------------------------------------------------------------===// + +/// Statistics for roofline analysis, separated by hardware path. +struct RooflineStats { + // Cube path + int64_t cubeFlops = 0; + int64_t cubeLoadBytes = 0; // CubeMTE2 + int64_t cubeStoreBytes = 0; // FixPipe + + // Vector path + int64_t vectorFlops = 0; + int64_t vectorLoadBytes = 0; // VecMTE2 + int64_t vectorStoreBytes = 0; // MTE3 + + // Cycle estimates (per-op, for annotation) + int64_t cubeCycles = 0; + int64_t cubeLoadCycles = 0; + int64_t cubeStoreCycles = 0; + int64_t vectorCycles = 0; + int64_t vectorLoadCycles = 0; + int64_t vectorStoreCycles = 0; + + /// Calculate total cycles using roofline model. + /// For each path (Cube/Vector), take max of compute and memory time. + /// Then consider if paths can overlap or must serialize. + int64_t calculateRooflineCycles(const HardwareConfig &config, + bool cubeVectorOverlap = true) const { + // Cube path: max(compute, load, store) + int64_t cubePathCycles = + std::max({cubeCycles, cubeLoadCycles, cubeStoreCycles}); + + // Vector path: max(compute, load, store) + int64_t vectorPathCycles = + std::max({vectorCycles, vectorLoadCycles, vectorStoreCycles}); + + // If Cube and Vector can overlap, take max; otherwise sum + if (cubeVectorOverlap) { + return std::max(cubePathCycles, vectorPathCycles); + } else { + return cubePathCycles + vectorPathCycles; + } + } +}; + +//===----------------------------------------------------------------------===// +// Pass Implementation +//===----------------------------------------------------------------------===// + +struct EstimateCyclesPass + : public impl::EstimateCyclesPassBase { + using EstimateCyclesPassBase::EstimateCyclesPassBase; + + void runOnOperation() override { + ModuleOp module = getOperation(); + + std::string hardwareConfigError; + auto hardwareConfig = + loadHardwareConfigForAnalysis(hardwareConfigPath, hardwareConfigError); + if (!hardwareConfig) { + emitError(module.getLoc(), hardwareConfigError); + return signalPassFailure(); + } + const HardwareConfig &config = *hardwareConfig; + + // Parse bindings + llvm::DenseMap argBindings; + llvm::StringMap programIdBindings; + if (!argBindingsStr.empty()) { + std::string parseError; + if (!parseBindings(argBindingsStr, argBindings, programIdBindings, + parseError)) { + emitError(module.getLoc(), parseError); + return signalPassFailure(); + } + } + + // Parse loop trip count overrides + SmallVector loopTripCountOverrides; + if (!loopTripCountsStr.empty()) { + std::string parseError; + if (!parseLoopTripCounts(loopTripCountsStr, loopTripCountOverrides, + parseError)) { + emitError(module.getLoc(), parseError); + return signalPassFailure(); + } + } + + bool hasError = false; + + // First pass: collect all loops and assign trip counts + SmallVector allLoops; + module.walk([&](scf::ForOp forOp) { allLoops.push_back(forOp); }); + + for (size_t loopIdx = 0; loopIdx < allLoops.size(); ++loopIdx) { + scf::ForOp forOp = allLoops[loopIdx]; + int64_t tripCount = 1; + std::string source = "unknown"; + + if (loopIdx < loopTripCountOverrides.size()) { + tripCount = loopTripCountOverrides[loopIdx]; + source = "override"; + } else { + auto result = getScfForTripCountWithBindings(forOp, argBindings, + programIdBindings); + + if (result.isStatic) { + tripCount = result.staticTripCount; + source = result.dependentArgs.empty() && + result.lowerBound.dependentProgramIds.empty() && + result.upperBound.dependentProgramIds.empty() + ? "static" + : "evaluated"; + } else if (!result.errorMsg.empty()) { + std::string errorMsg = "Loop " + std::to_string(loopIdx) + + " trip count cannot be determined.\n"; + errorMsg += " Lower bound: " + result.lowerBound.description + "\n"; + errorMsg += " Upper bound: " + result.upperBound.description + "\n"; + errorMsg += " Step: " + result.step.description + "\n"; + errorMsg += result.errorMsg; + + emitError(forOp.getLoc(), errorMsg); + hasError = true; + continue; + } + } + + forOp->setAttr("ascend.trip_count", + IntegerAttr::get(IntegerType::get(forOp.getContext(), 64), + tripCount)); + forOp->setAttr("ascend.trip_count_source", + StringAttr::get(forOp.getContext(), source)); + } + + if (hasError) + return signalPassFailure(); + + // Second pass: estimate cycles for each operation and collect roofline + // stats + RooflineStats stats; + int64_t totalOps = 0; + + module.walk([&](Operation *op) { + if (isa(op)) + return; + + if (auto cyclesOp = dyn_cast(op)) { + int64_t cycles = cyclesOp.estimateCycles(config); + if (cycles <= 0) + return; + + HWUnit hwUnit = cyclesOp.getHWUnit(); + int64_t loopMultiplier = getLoopMultiplier(op); + int64_t totalOpCycles = cycles * loopMultiplier; + + // Set attributes on the operation + op->setAttr( + "estimated_cycles", + IntegerAttr::get(IntegerType::get(op->getContext(), 64), cycles)); + op->setAttr("hw_unit", + StringAttr::get(op->getContext(), stringifyHWUnit(hwUnit))); + + if (int64_t bytes = cyclesOp.getTransferBytes(); bytes > 0) { + op->setAttr( + "bytes", + IntegerAttr::get(IntegerType::get(op->getContext(), 64), bytes)); + } + if (int64_t flops = cyclesOp.getFlops(); flops > 0) { + op->setAttr( + "flops", + IntegerAttr::get(IntegerType::get(op->getContext(), 64), flops)); + } + if (loopMultiplier > 1) { + op->setAttr("loop_multiplier", + IntegerAttr::get(IntegerType::get(op->getContext(), 64), + loopMultiplier)); + } + + totalOps++; + + // Accumulate roofline stats by hardware unit + int64_t bytes = cyclesOp.getTransferBytes() * loopMultiplier; + int64_t flops = cyclesOp.getFlops() * loopMultiplier; + + switch (hwUnit) { + case HWUnit::Cube: + stats.cubeFlops += flops; + stats.cubeCycles += totalOpCycles; + break; + case HWUnit::CubeMTE2: + stats.cubeLoadBytes += bytes; + stats.cubeLoadCycles += totalOpCycles; + break; + case HWUnit::FixPipe: + stats.cubeStoreBytes += bytes; + stats.cubeStoreCycles += totalOpCycles; + break; + case HWUnit::Vector: + stats.vectorFlops += flops; + stats.vectorCycles += totalOpCycles; + break; + case HWUnit::VecMTE2: + stats.vectorLoadBytes += bytes; + stats.vectorLoadCycles += totalOpCycles; + break; + case HWUnit::MTE3: + stats.vectorStoreBytes += bytes; + stats.vectorStoreCycles += totalOpCycles; + break; + default: + break; + } + } + }); + + // Calculate roofline-based total cycles + int64_t rooflineCycles = stats.calculateRooflineCycles(config, true); + + // Also calculate simple sum for comparison + int64_t simpleSumCycles = stats.cubeCycles + stats.cubeLoadCycles + + stats.cubeStoreCycles + stats.vectorCycles + + stats.vectorLoadCycles + stats.vectorStoreCycles; + + // Third pass: annotate loops with body cycles + module.walk([&](scf::ForOp forOp) { + int64_t tripCount = 1; + if (auto attr = forOp->getAttrOfType("ascend.trip_count")) + tripCount = attr.getInt(); + + int64_t bodyCycles = 0; + int64_t bodyOps = 0; + for (Operation &op : forOp.getBody()->getOperations()) { + if (isa(&op)) + continue; + if (auto nestedFor = dyn_cast(&op)) { + if (auto nestedTotal = + nestedFor->getAttrOfType("ascend.total_cycles")) + bodyCycles += nestedTotal.getInt(); + } else if (auto cyclesAttr = + op.getAttrOfType("estimated_cycles")) { + bodyCycles += cyclesAttr.getInt(); + bodyOps++; + } + } + + forOp->setAttr("ascend.body_cycles", + IntegerAttr::get(IntegerType::get(forOp.getContext(), 64), + bodyCycles)); + forOp->setAttr("ascend.total_cycles", + IntegerAttr::get(IntegerType::get(forOp.getContext(), 64), + bodyCycles * tripCount)); + forOp->setAttr( + "ascend.body_ops", + IntegerAttr::get(IntegerType::get(forOp.getContext(), 64), bodyOps)); + }); + + // Set module-level attributes + module->setAttr("ascend.roofline_cycles", + IntegerAttr::get(IntegerType::get(module.getContext(), 64), + rooflineCycles)); + module->setAttr("ascend.simple_sum_cycles", + IntegerAttr::get(IntegerType::get(module.getContext(), 64), + simpleSumCycles)); + module->setAttr( + "ascend.total_ops", + IntegerAttr::get(IntegerType::get(module.getContext(), 64), totalOps)); + module->setAttr("ascend.hardware", + StringAttr::get(module.getContext(), config.getName())); + } +}; + +} // namespace +} // namespace ascend +} // namespace mlir diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/HIVMAnalysisPass.cpp b/third_party/ascend/costmodel/lib/AscendModel/Transforms/HIVMAnalysisPass.cpp new file mode 100644 index 0000000000..7e8ab2f26f --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/HIVMAnalysisPass.cpp @@ -0,0 +1,94 @@ +//===- HIVMAnalysisPass.cpp - Native HIVM scheduling analysis ------------===// +// +// Runs the HIVM-native analysis on a parsed MLIR module and emits a textual +// report plus an optional Perfetto trace. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/Analysis/HIVMAnalysis.h" +#include "AscendModel/HardwareConfig.h" +#include "AscendModel/Transforms/Passes.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/raw_ostream.h" + +namespace mlir { +namespace ascend { + +#define GEN_PASS_DEF_HIVMANALYSISPASS +#include "AscendModel/Transforms/Passes.h.inc" + +namespace { + +static FailureOr parseSchedulerMode(llvm::StringRef mode) { + if (mode.empty() || mode == "static") + return HIVMSchedulerMode::Static; + if (mode == "des") + return HIVMSchedulerMode::DES; + return failure(); +} + +struct HIVMAnalysisPass : public impl::HIVMAnalysisPassBase { + using HIVMAnalysisPassBase::HIVMAnalysisPassBase; + + void runOnOperation() override { + ModuleOp module = getOperation(); + + std::string loadError; + auto hardwareConfig = + loadHardwareConfigForAnalysis(hardwareConfigPath, loadError); + if (!hardwareConfig) { + module.emitError() << "failed to load hardware config: " << loadError; + signalPassFailure(); + return; + } + const HardwareConfig &config = *hardwareConfig; + auto schedulerOr = parseSchedulerMode(schedulerMode); + if (failed(schedulerOr)) { + module.emitError() << "invalid HIVM scheduler mode `" << schedulerMode + << "`; expected `static` or `des`"; + signalPassFailure(); + return; + } + HIVMAnalyzer analyzer(config, argBindingsStr, *schedulerOr); + HIVMAnalysisReport report; + std::string error; + if (!analyzer.analyzeModule(module, report, error)) { + module.emitError() << "HIVM analysis failed: " << error; + signalPassFailure(); + return; + } + + if (!reportFile.empty()) { + std::error_code ec; + llvm::raw_fd_ostream os(reportFile, ec, llvm::sys::fs::OF_Text); + if (ec) { + module.emitError() << "failed to open HIVM report file `" << reportFile + << "`: " << ec.message(); + signalPassFailure(); + return; + } + report.print(os, config); + } else { + report.print(llvm::errs(), config); + } + + if (!perfettoTraceFile.empty()) { + std::error_code ec; + llvm::raw_fd_ostream os(perfettoTraceFile, ec, llvm::sys::fs::OF_Text); + if (ec) { + module.emitError() << "failed to open Perfetto trace file `" + << perfettoTraceFile << "`: " << ec.message(); + signalPassFailure(); + return; + } + report.emitPerfettoTrace(os, config); + } + } +}; + +} // namespace +} // namespace ascend +} // namespace mlir diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/InsertDataTransfers.cpp b/third_party/ascend/costmodel/lib/AscendModel/Transforms/InsertDataTransfers.cpp new file mode 100644 index 0000000000..88f2925636 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/InsertDataTransfers.cpp @@ -0,0 +1,216 @@ +//===- InsertDataTransfers.cpp - Insert Cube/Vector memory transfers ------===// +// +// This pass inserts explicit memory transfer operations when data flows +// between Vector and Cube compute units on Ascend 910B. +// +// Data flow model (910B architecture): +// +// Vector path: HBM ──MTE2──▶ UB ──▶ Vector ──▶ UB ──MTE3──▶ HBM +// Cube path: HBM ──MTE2──▶ L1 ──MTE1──▶ L0A/L0B ──▶ Cube ──▶ L0C +// ──FixPipe──▶ HBM +// +// When Vector result is used by Cube (e.g., arith.mulf -> tt.dot): +// Insert: vector_store (MTE3: UB → HBM) + cube_load (MTE2: HBM → L1) +// +// When Cube result is used by Vector (e.g., tt.dot -> arith.addf): +// Insert: cube_store (FixPipe: L0C → HBM) + vector_load (MTE2: HBM → UB) +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/IR/AscendModelDialect.h" +#include "AscendModel/Transforms/Passes.h" +#include "AscendModel/Utils.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" + +namespace mlir { +namespace ascend { + +#define GEN_PASS_DEF_INSERTDATATRANSFERSPASS +#include "AscendModel/Transforms/Passes.h.inc" + +namespace { + +// Use utility functions from Utils.h +using utils::getByteSize; + +//===----------------------------------------------------------------------===// +// Helper: Classify operations by compute path +//===----------------------------------------------------------------------===// + +enum class ComputePath { + Cube, // Matrix operations: matmul + Vector, // SIMD operations: elementwise, reduce, etc. + Memory, // Memory operations: load/store + Unknown +}; + +/// Classify an operation by its compute path. +static ComputePath getComputePath(Operation *op) { + // Cube operations + if (isa(op)) + return ComputePath::Cube; + + // Vector operations + if (isa(op)) + return ComputePath::Vector; + + // Memory operations + if (isa(op)) + return ComputePath::Memory; + + // Arith/Math tensor ops are Vector (before conversion) + StringRef dialect = op->getDialect()->getNamespace(); + if (dialect == "arith" || dialect == "math") { + // Check if it operates on tensors + for (Type resultType : op->getResultTypes()) { + if (isa(resultType)) + return ComputePath::Vector; + } + } + + return ComputePath::Unknown; +} + +//===----------------------------------------------------------------------===// +// Pass Implementation +//===----------------------------------------------------------------------===// + +struct InsertDataTransfersPass + : public impl::InsertDataTransfersPassBase { + using InsertDataTransfersPassBase::InsertDataTransfersPassBase; + + void runOnOperation() override { + ModuleOp module = getOperation(); + + // Track which values have been transferred to avoid duplicates + llvm::DenseMap vectorToHBM; // Value in UB -> Value in HBM + llvm::DenseMap hbmToL1; // Value in HBM -> Value in L1 + + // Collect all MatmulOps first (to avoid iterator invalidation) + SmallVector matmulOps; + module.walk([&](MatmulOp op) { matmulOps.push_back(op); }); + + // Process each MatmulOp + for (MatmulOp matmulOp : matmulOps) { + OpBuilder builder(matmulOp); + Location loc = matmulOp.getLoc(); + + // Null attrs for optional parameters + ::mlir::IntegerAttr nullAttr; + // Memory-space attrs for transfer ops (tilesim migration, root cause 1). + auto hbm = builder.getStringAttr("hbm"); + auto l1 = builder.getStringAttr("l1"); + auto l0c = builder.getStringAttr("l0c"); + auto ub = builder.getStringAttr("ub"); + + // === Step 1: Handle inputs to Cube === + // MatmulOp has lhs and rhs operands + SmallVector operands = {matmulOp.getLhs(), matmulOp.getRhs()}; + + for (unsigned i = 0; i < operands.size(); ++i) { + Value operand = operands[i]; + Operation *defOp = operand.getDefiningOp(); + + // Skip if no defining op (block argument) + if (!defOp) + continue; + + // Skip if already a memory load + if (isa(defOp)) + continue; + + ComputePath path = getComputePath(defOp); + + // If operand comes from Vector path, insert transfers + if (path == ComputePath::Vector) { + auto tensorType = dyn_cast(operand.getType()); + if (!tensorType) + continue; + + uint64_t bytes = static_cast(getByteSize(tensorType)); + + // Check if we already transferred this value + Value hbmValue; + auto it = vectorToHBM.find(operand); + if (it != vectorToHBM.end()) { + hbmValue = it->second; + } else { + // Insert vector_store: UB → HBM (MTE3) + builder.create(loc, operand, bytes, ub, hbm, + nullAttr, nullAttr); + hbmValue = operand; // After store, conceptually in HBM + vectorToHBM[operand] = hbmValue; + } + + // Check if we already loaded to L1 + Value l1Value; + auto it2 = hbmToL1.find(hbmValue); + if (it2 != hbmToL1.end()) { + l1Value = it2->second; + } else { + // Insert cube_load: HBM → L1 (MTE2) + auto cubeLoad = builder.create( + loc, tensorType, hbmValue, bytes, hbm, l1, nullAttr, nullAttr); + l1Value = cubeLoad.getResult(); + hbmToL1[hbmValue] = l1Value; + } + + // Replace operand in matmul + matmulOp->setOperand(i, l1Value); + } + } + + // === Step 2: Handle output from Cube === + // Check if any user is a Vector operation + + Value matmulResult = matmulOp.getResult(); + auto resultType = dyn_cast(matmulResult.getType()); + if (!resultType) + continue; + + // Collect Vector users + SmallVector vectorUsers; + for (OpOperand &use : matmulResult.getUses()) { + Operation *user = use.getOwner(); + ComputePath userPath = getComputePath(user); + if (userPath == ComputePath::Vector) { + vectorUsers.push_back(&use); + } + } + + if (vectorUsers.empty()) + continue; + + // Insert after matmul + builder.setInsertionPointAfter(matmulOp); + + uint64_t bytes = static_cast(getByteSize(resultType)); + + // Insert cube_store: L0C → HBM (FixPipe) + builder.create(loc, matmulResult, bytes, l0c, hbm, nullAttr, + nullAttr); + + // Insert vector_load: HBM → UB (MTE2) + auto vectorLoad = builder.create( + loc, resultType, matmulResult, bytes, hbm, ub, nullAttr, nullAttr); + + // Replace uses in Vector operations + for (OpOperand *use : vectorUsers) { + use->set(vectorLoad.getResult()); + } + } + } +}; + +} // namespace +} // namespace ascend +} // namespace mlir diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/PassRegistration.cpp b/third_party/ascend/costmodel/lib/AscendModel/Transforms/PassRegistration.cpp new file mode 100644 index 0000000000..e66a21edd4 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/PassRegistration.cpp @@ -0,0 +1,98 @@ +//===- PassRegistration.cpp - Register AscendModel passes ----------------===// +// +// This file registers the AscendModel pipeline with configurable options. +// +// Usage (options are space-separated, NOT comma-separated): +// inproc-costmodel -ascend-perf-model="arg-bindings=arg2=100,pid_x=0" +// input.mlir inproc-costmodel -ascend-perf-model="loop-trip-counts=4,6588" +// input.mlir inproc-costmodel +// -ascend-perf-model="hardware-config=path/to/config.json" input.mlir +// inproc-costmodel -ascend-perf-model="hardware-config=config.json +// arg-bindings=pid_x=0" input.mlir +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/Transforms/Passes.h" + +#include "mlir/Pass/PassManager.h" +#include "mlir/Pass/PassOptions.h" +#include "mlir/Pass/PassRegistry.h" + +namespace mlir { +namespace ascend { + +//===----------------------------------------------------------------------===// +// Pipeline Options +//===----------------------------------------------------------------------===// + +/// Options for the ascend-perf-model pipeline. +struct AscendPerfModelPipelineOptions + : public PassPipelineOptions { + + /// Bindings for function arguments and program_ids. + /// Format: "arg2=100,arg3=200,pid_x=0" + PassOptions::Option argBindings{ + *this, "arg-bindings", + llvm::cl::desc( + "Bindings for args and program_ids (e.g., 'arg2=100,pid_x=0')"), + llvm::cl::init("")}; + + /// Direct loop trip count overrides. + /// Format: "4,6588" (first loop=4, second loop=6588) + PassOptions::Option loopTripCounts{ + *this, "loop-trip-counts", + llvm::cl::desc( + "Direct loop trip count overrides in order (e.g., '4,6588')"), + llvm::cl::init("")}; + + /// Hardware config JSON file path. + PassOptions::Option hardwareConfig{ + *this, "hardware-config", + llvm::cl::desc("Path to hardware configuration JSON file"), + llvm::cl::init("")}; +}; + +//===----------------------------------------------------------------------===// +// Pipeline Registration +//===----------------------------------------------------------------------===// + +void registerAscendModelPipeline() { + PassPipelineRegistration( + "ascend-perf-model", + "Run the full Ascend 910B performance modeling pipeline", + [](OpPassManager &pm, const AscendPerfModelPipelineOptions &options) { + // Step 1: Convert Triton IR to AscendModel IR + pm.addPass(createConvertTritonToAscendPass()); + + // Step 2: Insert data transfers between Cube and Vector paths + pm.addPass(createInsertDataTransfersPass()); + + // Step 3: Assign unique IDs to all operations + pm.addPass(createAssignOpIDsPass()); + + // Step 4: Estimate execution cycles based on hardware config + // Create options and pass to the pass + { + EstimateCyclesPassOptions estimateOpts; + estimateOpts.argBindingsStr = options.argBindings; + estimateOpts.loopTripCountsStr = options.loopTripCounts; + estimateOpts.hardwareConfigPath = options.hardwareConfig; + pm.addPass(createEstimateCyclesPass(estimateOpts)); + } + + // Step 5: Perform pipeline scheduling analysis + { + PipelineAnalysisPassOptions pipelineOpts; + pipelineOpts.argBindingsStr = options.argBindings; + pipelineOpts.loopTripCountsStr = options.loopTripCounts; + pipelineOpts.hardwareConfigPath = options.hardwareConfig; + pm.addPass(createPipelineAnalysisPass(pipelineOpts)); + } + + // Step 6: Generate performance report + pm.addPass(createPerfReportPass()); + }); +} + +} // namespace ascend +} // namespace mlir diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/PerfReportPass.cpp b/third_party/ascend/costmodel/lib/AscendModel/Transforms/PerfReportPass.cpp new file mode 100644 index 0000000000..8d7f6388be --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/PerfReportPass.cpp @@ -0,0 +1,160 @@ +//===- PerfReportPass.cpp - Generate performance report ------------------===// +// +// This file generates a comprehensive performance report. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/Analysis/PipelineAnalysis.h" +#include "AscendModel/HardwareParams.h" +#include "AscendModel/IR/AscendModelDialect.h" +#include "AscendModel/Transforms/Passes.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/Support/Format.h" + +namespace mlir { +namespace ascend { + +#define GEN_PASS_DEF_PERFREPORTPASS +#include "AscendModel/Transforms/Passes.h.inc" + +namespace { + +/// Get the HWUnit for an operation +static HWUnit getOpHWUnit(Operation *op) { + if (isa(op)) + return HWUnit::Cube; + if (isa(op)) + return HWUnit::CubeMTE2; + if (isa(op)) + return HWUnit::FixPipe; + if (isa(op)) + return HWUnit::VecMTE2; + if (isa(op)) + return HWUnit::MTE3; + if (isa(op)) + return HWUnit::Vector; + return HWUnit::Scalar; +} + +static int64_t getNumElementsFromType(Type type) { + if (auto tensorType = dyn_cast(type)) { + int64_t count = 1; + for (int64_t dim : tensorType.getShape()) { + if (dim == ShapedType::kDynamic) + return 1024; + count *= dim; + } + return count; + } + return 1; +} + +struct PerfReportPass : public impl::PerfReportPassBase { + using PerfReportPassBase::PerfReportPassBase; + + void runOnOperation() override { + ModuleOp module = getOperation(); + + int64_t totalCycles = 0; + int64_t totalFLOPs = 0; + int64_t totalBytes = 0; + + std::map unitCycles; + std::map opCounts; + std::map opCycles; + + for (int i = 0; i <= static_cast(HWUnit::Scalar); ++i) { + unitCycles[static_cast(i)] = 0; + } + + module.walk([&](Operation *op) { + auto cyclesAttr = op->getAttrOfType("estimated_cycles"); + if (!cyclesAttr) + return; + + int64_t cycles = cyclesAttr.getInt(); + std::string opName = op->getName().getStringRef().str(); + + opCounts[opName]++; + opCycles[opName] += cycles; + + HWUnit hwUnit = getOpHWUnit(op); + + // Count FLOPs + if (auto matmulOp = dyn_cast(op)) { + int64_t m = matmulOp.getM(); + int64_t n = matmulOp.getN(); + int64_t k = matmulOp.getK(); + totalFLOPs += 2 * m * n * k; + } else if (isa(op)) { + if (op->getNumResults() > 0) { + totalFLOPs += getNumElementsFromType(op->getResult(0).getType()); + } + } else if (isa(op)) { + if (op->getNumResults() > 0) { + totalFLOPs += getNumElementsFromType(op->getResult(0).getType()) * 10; + } + } else if (isa(op)) { + if (op->getNumOperands() > 0) { + totalFLOPs += getNumElementsFromType(op->getOperand(0).getType()); + } + } + + // Count bytes + if (isa(op)) { + if (auto bytesAttr = op->getAttrOfType("bytes")) + totalBytes += bytesAttr.getInt(); + } else if (isa(op)) { + if (auto bytesAttr = op->getAttrOfType("bytes")) + totalBytes += bytesAttr.getInt(); + } else if (isa(op)) { + if (auto bytesAttr = op->getAttrOfType("bytes")) + totalBytes += bytesAttr.getInt(); + } else if (isa(op)) { + if (auto bytesAttr = op->getAttrOfType("bytes")) + totalBytes += bytesAttr.getInt(); + } + + unitCycles[hwUnit] += cycles; + totalCycles += cycles; + }); + + auto scheduledCyclesAttr = + module->getAttrOfType("ascend.scheduled_cycles"); + auto rooflineCyclesAttr = + module->getAttrOfType("ascend.roofline_cycles"); + int64_t scheduledCycles = + scheduledCyclesAttr + ? scheduledCyclesAttr.getInt() + : (rooflineCyclesAttr ? rooflineCyclesAttr.getInt() : totalCycles); + + // Ascend 910B clock: 1.85 GHz = 1850 cycles/us + constexpr double CYCLES_PER_US = 1850.0; + double timeUs = scheduledCycles / CYCLES_PER_US; + double achievedTFLOPS = (totalFLOPs / 1e12) / (timeUs / 1e6); + double achievedBandwidth = (totalBytes / 1e9) / (timeUs / 1e6); + double arithmeticIntensity = + totalBytes > 0 ? static_cast(totalFLOPs) / totalBytes : 0; + + double ridgePoint = CUBE_TFLOPS / HBM_BANDWIDTH_GBS; + bool isComputeBound = arithmeticIntensity > ridgePoint; + + HWUnit bottleneck = HWUnit::Scalar; + int64_t maxCycles = 0; + for (const auto &[unit, cycles] : unitCycles) { + if (cycles > maxCycles) { + maxCycles = cycles; + bottleneck = unit; + } + } + } +}; + +} // namespace +} // namespace ascend +} // namespace mlir diff --git a/third_party/ascend/costmodel/lib/AscendModel/Transforms/PipelineAnalysisPass.cpp b/third_party/ascend/costmodel/lib/AscendModel/Transforms/PipelineAnalysisPass.cpp new file mode 100644 index 0000000000..aba9791607 --- /dev/null +++ b/third_party/ascend/costmodel/lib/AscendModel/Transforms/PipelineAnalysisPass.cpp @@ -0,0 +1,240 @@ +//===- PipelineAnalysisPass.cpp - Pipeline scheduling analysis -----------===// +// +// Uses HardwareConfig for configurable hardware parameters. +// Handles dynamic loop bounds via arg-bindings option (supports program_id). +// Uses Roofline model for cycle estimation with HW unit overlap. +// +//===----------------------------------------------------------------------===// + +#include "AscendModel/Analysis/PipelineAnalysis.h" +#include "AscendModel/HardwareConfig.h" +#include "AscendModel/IR/AscendModelDialect.h" +#include "AscendModel/Transforms/Passes.h" +#include "AscendModel/Utils.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/Support/raw_ostream.h" + +#include + +namespace mlir { +namespace ascend { + +#define GEN_PASS_DEF_PIPELINEANALYSISPASS +#include "AscendModel/Transforms/Passes.h.inc" + +namespace { + +using utils::getLoopMultiplier; +using utils::getScfForTripCount; +using utils::getScfForTripCountWithBindings; +using utils::parseBindings; +using utils::parseLoopTripCounts; + +HWUnit getOpHWUnit(Operation *op) { + if (isa(op)) + return HWUnit::Cube; + if (isa(op)) + return HWUnit::CubeMTE2; + if (isa(op)) + return HWUnit::FixPipe; + if (isa(op)) + return HWUnit::VecMTE2; + if (isa(op)) + return HWUnit::MTE3; + if (isa(op)) + return HWUnit::Vector; + return HWUnit::Scalar; +} + +struct PipelineAnalysisPass + : public impl::PipelineAnalysisPassBase { + using PipelineAnalysisPassBase::PipelineAnalysisPassBase; + + void runOnOperation() override { + ModuleOp module = getOperation(); + + std::string hardwareConfigError; + auto hardwareConfig = + loadHardwareConfigForAnalysis(hardwareConfigPath, hardwareConfigError); + if (!hardwareConfig) { + emitError(module.getLoc(), hardwareConfigError); + return signalPassFailure(); + } + const HardwareConfig &config = *hardwareConfig; + + // Parse bindings + llvm::DenseMap argBindings; + llvm::StringMap programIdBindings; + SmallVector loopTripCountOverrides; + + if (!argBindingsStr.empty()) { + std::string parseError; + if (!parseBindings(argBindingsStr, argBindings, programIdBindings, + parseError)) { + emitError(module.getLoc(), parseError); + return signalPassFailure(); + } + } + + if (!loopTripCountsStr.empty()) { + std::string parseError; + if (!parseLoopTripCounts(loopTripCountsStr, loopTripCountOverrides, + parseError)) { + emitError(module.getLoc(), parseError); + return signalPassFailure(); + } + } + + // Collect loops and ensure trip counts are set + SmallVector allLoops; + module.walk([&](scf::ForOp forOp) { allLoops.push_back(forOp); }); + + bool hasError = false; + for (size_t loopIdx = 0; loopIdx < allLoops.size(); ++loopIdx) { + scf::ForOp forOp = allLoops[loopIdx]; + + if (forOp->hasAttr("ascend.trip_count")) + continue; + + int64_t tripCount = 1; + if (loopIdx < loopTripCountOverrides.size()) { + tripCount = loopTripCountOverrides[loopIdx]; + } else { + auto result = getScfForTripCountWithBindings(forOp, argBindings, + programIdBindings); + if (result.isStatic) { + tripCount = result.staticTripCount; + } else { + emitError(forOp.getLoc(), "Loop " + std::to_string(loopIdx) + + " trip count unknown. " + + result.errorMsg); + hasError = true; + continue; + } + } + + forOp->setAttr("ascend.trip_count", + IntegerAttr::get(IntegerType::get(forOp.getContext(), 64), + tripCount)); + } + + if (hasError) + return signalPassFailure(); + + // Build scheduler + PipelineScheduler scheduler(&config); + llvm::DenseMap valueProducers; + + module.walk([&](Operation *op) { + if (isa(op)) + return; + + auto opIdAttr = op->getAttrOfType("op_id"); + if (!opIdAttr) + return; + + int64_t opId = opIdAttr.getInt(); + auto cyclesAttr = op->getAttrOfType("estimated_cycles"); + int64_t cycles = cyclesAttr ? cyclesAttr.getInt() : 1; + + PipelineOp pipelineOp; + pipelineOp.opId = opId; + pipelineOp.hwUnit = getOpHWUnit(op); + pipelineOp.duration = cycles; + pipelineOp.mlirOp = op; + pipelineOp.opName = op->getName().getStringRef().str(); + pipelineOp.loopMultiplier = getLoopMultiplier(op); + + for (Value operand : op->getOperands()) { + auto it = valueProducers.find(operand); + if (it != valueProducers.end()) { + pipelineOp.dependsOn.push_back(it->second); + scheduler.addDependency(it->second, opId); + } + } + + for (Value result : op->getResults()) + valueProducers[result] = opId; + + scheduler.addOperation(pipelineOp); + }); + + if (!scheduler.schedule()) { + emitError(module.getLoc(), "Failed to schedule pipeline"); + return signalPassFailure(); + } + + // Calculate cycles using roofline model + // oneIterCycles from scheduler already considers HW unit parallelism for + // one iteration + int64_t oneIterCycles = scheduler.getTotalCycles(); + + // For total cycles with loops, we need to consider: + // 1. Each HW unit's total work across all iterations + // 2. Take max (not sum) since they can overlap + + // Collect per-HW-unit cycles + llvm::DenseMap hwUnitCycles; + for (const auto &pipelineOp : scheduler.getAllOps()) { + hwUnitCycles[pipelineOp.hwUnit] += + pipelineOp.duration * pipelineOp.loopMultiplier; + } + + // Group by path and apply roofline model. + // Cube path: max(Cube, CubeMTE2, FixPipe). No cube-side mutex on 910B + // (tilesim pipe_exclusive_config only pairs AIV MTE2<->MTE3). + int64_t cubePathCycles = + std::max({hwUnitCycles[HWUnit::Cube], hwUnitCycles[HWUnit::CubeMTE2], + hwUnitCycles[HWUnit::FixPipe]}); + + // Vector path. Vector compute overlaps with load/store transfers, but on + // 910B AIV the MTE2 (load) and MTE3 (store) units share one physical + // pipeline (tilesim MutexComponents) and must serialize. With the mutex + // the transfer time is VecMTE2 + MTE3; without it (legacy) they are + // assumed to overlap (max). This is root cause 3. + int64_t vecTransfer; + if (config.areMutexUnits("vec_mte2", "mte3")) + vecTransfer = hwUnitCycles[HWUnit::VecMTE2] + hwUnitCycles[HWUnit::MTE3]; + else + vecTransfer = + std::max(hwUnitCycles[HWUnit::VecMTE2], hwUnitCycles[HWUnit::MTE3]); + int64_t vectorPathCycles = + std::max(hwUnitCycles[HWUnit::Vector], vecTransfer); + + // Total: max of paths (Cube and Vector paths overlap) + int64_t rooflineTotalCycles = std::max(cubePathCycles, vectorPathCycles); + + // Also calculate simple sum for comparison + int64_t simpleSumCycles = 0; + for (const auto &pipelineOp : scheduler.getAllOps()) + simpleSumCycles += pipelineOp.duration * pipelineOp.loopMultiplier; + + module->setAttr("ascend.scheduled_cycles_one_iter", + IntegerAttr::get(IntegerType::get(module.getContext(), 64), + oneIterCycles)); + module->setAttr("ascend.roofline_cycles", + IntegerAttr::get(IntegerType::get(module.getContext(), 64), + rooflineTotalCycles)); + // Public summary consumed by the in-process Python costmodel bridge. + // This must include loop multipliers; otherwise configs with many inner + // loop iterations are under-estimated and can incorrectly outrank faster + // configs during costmodel prefiltering. + module->setAttr("ascend.scheduled_cycles", + IntegerAttr::get(IntegerType::get(module.getContext(), 64), + rooflineTotalCycles)); + module->setAttr("ascend.simple_sum_cycles", + IntegerAttr::get(IntegerType::get(module.getContext(), 64), + simpleSumCycles)); + } +}; + +} // namespace +} // namespace ascend +} // namespace mlir diff --git a/third_party/ascend/include/AutoBlockify/AutoBlockify.h b/third_party/ascend/include/AutoBlockify/AutoBlockify.h new file mode 100644 index 0000000000..3ce8159ef9 --- /dev/null +++ b/third_party/ascend/include/AutoBlockify/AutoBlockify.h @@ -0,0 +1,119 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "mlir/Pass/Pass.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/IR/PatternMatch.h" + +#define GEN_PASS_DECL_AUTOBLOCKIFY +#include "ascend/include/AutoBlockify/Passes.h.inc" + +#define GEN_PASS_DEF_AUTOBLOCKIFY +#include "ascend/include/AutoBlockify/Passes.h.inc" + +namespace mlir { +namespace triton { + +std::unique_ptr> +createAutoBlockifyPass(const AutoBlockifyOptions &options = {}); + +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace triton; + +class PropagateUnrealizedCastDown + : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + + explicit PropagateUnrealizedCastDown(MLIRContext *context, + Value logicalBlockId, + Value logicalBlockNum, + int autoBlockifySize); + + LogicalResult matchAndRewrite(UnrealizedConversionCastOp op, + PatternRewriter &rewriter) const override; + +private: + void handleBlockifyLoop(scf::ForOp blockifyLoop, Operation *op, + PatternRewriter &rewriter) const; + void rewriteSplat(UnrealizedConversionCastOp op, triton::SplatOp splatOp, + PatternRewriter &rewriter) const; + void rewriteExpandDims(UnrealizedConversionCastOp op, + triton::ExpandDimsOp expandDimsOp, + PatternRewriter &rewriter) const; + void rewriteReduce(UnrealizedConversionCastOp op, triton::ReduceOp reduceOp, + PatternRewriter &rewriter) const; + void rewriteScan(UnrealizedConversionCastOp op, triton::ScanOp scanOp, + PatternRewriter &rewriter) const; + void rewriteLoad(UnrealizedConversionCastOp op, triton::LoadOp loadOp, + PatternRewriter &rewriter) const; + void rewriteStore(UnrealizedConversionCastOp op, triton::StoreOp storeOp, + PatternRewriter &rewriter) const; + void rewriteAtomicRMW(UnrealizedConversionCastOp op, + triton::AtomicRMWOp atomicRMWOp, + PatternRewriter &rewriter) const; + void rewriteAssert(UnrealizedConversionCastOp op, triton::AssertOp assertOp, + PatternRewriter &rewriter) const; + void rewriteExtractSlice(UnrealizedConversionCastOp op, + tensor::ExtractSliceOp extractSliceOp, + PatternRewriter &rewriter) const; + void rewriteInsertSlice(UnrealizedConversionCastOp op, + tensor::InsertSliceOp insertSliceOp, + PatternRewriter &rewriter) const; + void rewriteWhile(UnrealizedConversionCastOp op, scf::WhileOp whileOp, + PatternRewriter &rewriter) const; + void rewriteLoop(UnrealizedConversionCastOp op, LoopLikeOpInterface loopOp, + PatternRewriter &rewriter) const; + void rewriteIf(UnrealizedConversionCastOp &op, scf::IfOp ifOp, + ArrayRef indices, PatternRewriter &rewriter) const; + void rewriteYield(UnrealizedConversionCastOp &op, scf::YieldOp yieldOp, + PatternRewriter &rewriter) const; + void rewriteCondition(UnrealizedConversionCastOp op, + scf::ConditionOp conditionOp, + PatternRewriter &rewriter) const; + void rewriteGeneraleOp(UnrealizedConversionCastOp op, Operation *generalOp, + PatternRewriter &rewriter) const; + + Value logicalBlockId; + Value logicalBlockNum; + int autoBlockifySize; +}; + +class AutoBlockifyPass : public ::impl::AutoBlockifyBase { +public: + explicit AutoBlockifyPass(const AutoBlockifyOptions &options); + void runOnOperation() override; + +private: + bool checkBlockifiable(Value v); + void preProcess(triton::FuncOp func); + + DenseSet checkedValues; + Value logicalBlockId; + Value logicalBlockNum; +}; diff --git a/third_party/ascend/include/AutoBlockify/CMakeLists.txt b/third_party/ascend/include/AutoBlockify/CMakeLists.txt new file mode 100644 index 0000000000..ca4cf9f552 --- /dev/null +++ b/third_party/ascend/include/AutoBlockify/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name AutoBlockify) +add_public_tablegen_target(AutoBlockifyPassIncGen) diff --git a/third_party/ascend/include/AutoBlockify/Passes.h b/third_party/ascend/include/AutoBlockify/Passes.h new file mode 100644 index 0000000000..7d5147ef92 --- /dev/null +++ b/third_party/ascend/include/AutoBlockify/Passes.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_AUTO_BLOCKIFY_PASSES_H +#define TRITON_ADAPTER_AUTO_BLOCKIFY_PASSES_H + +#include "AutoBlockify.h" + +namespace mlir { +namespace triton { + +#define GEN_PASS_REGISTRATION +#include "ascend/include/AutoBlockify/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_AUTO_BLOCKIFY_PASSES_H diff --git a/third_party/ascend/include/AutoBlockify/Passes.td b/third_party/ascend/include/AutoBlockify/Passes.td new file mode 100644 index 0000000000..627d3ac2dc --- /dev/null +++ b/third_party/ascend/include/AutoBlockify/Passes.td @@ -0,0 +1,21 @@ +#ifndef AUTO_BLOCKIFY_PASSES +#define AUTO_BLOCKIFY_PASSES + +include "mlir/Pass/PassBase.td" + +def AutoBlockify : Pass<"auto-blockify", "mlir::ModuleOp"> { + let summary = "Apply auto blockify v2"; + let constructor = "triton::createAutoBlockifyPass()"; + let dependentDialects = [ + "mlir::arith::ArithDialect", + "mlir::tensor::TensorDialect", + "mlir::triton::TritonDialect" + ]; + let options = [ + Option<"autoBlockifySize", "auto-blockify-size", "int", "1", + "Apply auto blockify v2 with the automatic block-mapping policy." + "Expand highest dimension with blockify size"> + ]; +} + +#endif // AUTO_BLOCKIFY_PASSES diff --git a/third_party/ascend/include/AutoBlockify/Utils.h b/third_party/ascend/include/AutoBlockify/Utils.h new file mode 100644 index 0000000000..385fa51a10 --- /dev/null +++ b/third_party/ascend/include/AutoBlockify/Utils.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +using namespace mlir; +using namespace triton; + +constexpr llvm::StringLiteral autoBlockifySizeAttr = "auto_blockify_size"; +constexpr llvm::StringLiteral logicalBlockIdAttr = "logical_block_id"; +constexpr llvm::StringLiteral autoBlockifyLoopAttr = "auto_blockify_loop"; +constexpr llvm::StringLiteral autoBlockifyRegionOpAttr = + "auto_blockify_region_op"; + +RankedTensorType getExpandedType(Type type, UnrealizedConversionCastOp op); + +Value rewriteValue(Value value, UnrealizedConversionCastOp op, + OpBuilder &builder); + +void replaceValue(Operation *newOp, Operation *oldOp, Value newMask, + RewriterBase &rewriter, + ArrayRef replaceIndices = {}); + +Value createMask(Value mask, Value uccMask, ArrayRef targetShape, + RewriterBase &rewriter); + +void mapRegionIterArg(IRMapping &mapping, ValueRange oldArgs, + ValueRange newArgs, ArrayRef indices, Value mask, + OpBuilder &builder); + +void mapYieldedValue(IRMapping &mapping, scf::YieldOp yieldOp, + ArrayRef indices, UnrealizedConversionCastOp op, + OpBuilder &builder); + +Operation *createBlockifyLoop(Operation *targetOp, + UnrealizedConversionCastOp op, + Value logicalBlockId, Value logicalBlockNum, + int autoBlockifySize, RewriterBase &rewriter); + +std::optional getBlockifyLoop(Operation *op); diff --git a/third_party/ascend/include/CMakeLists.txt b/third_party/ascend/include/CMakeLists.txt new file mode 100644 index 0000000000..a2737f1c11 --- /dev/null +++ b/third_party/ascend/include/CMakeLists.txt @@ -0,0 +1,15 @@ +add_subdirectory(Dialect) +add_subdirectory(TritonToAnnotation) +add_subdirectory(TritonToHFusion) +add_subdirectory(TritonToHIVM) +add_subdirectory(TritonToGraph) +add_subdirectory(TritonToLinalg) +add_subdirectory(TritonControlFlowOpt) +add_subdirectory(Utils) +add_subdirectory(DiscreteMaskAccessConversion) +add_subdirectory(TritonToUnstructure) +add_subdirectory(TritonToLLVM) +add_subdirectory(TritonToStructured) +add_subdirectory(AutoBlockify) +add_subdirectory(DynamicCVPipeline) +add_subdirectory(CommonIRToHIVM) diff --git a/third_party/ascend/include/CommonIRToHIVM/CMakeLists.txt b/third_party/ascend/include/CommonIRToHIVM/CMakeLists.txt new file mode 100644 index 0000000000..1543d859b3 --- /dev/null +++ b/third_party/ascend/include/CommonIRToHIVM/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name CommonIRToHIVM) +add_public_tablegen_target(CommonIRToHIVMConversionPassIncGen) diff --git a/third_party/ascend/include/CommonIRToHIVM/Passes.h b/third_party/ascend/include/CommonIRToHIVM/Passes.h new file mode 100644 index 0000000000..a1e5be1179 --- /dev/null +++ b/third_party/ascend/include/CommonIRToHIVM/Passes.h @@ -0,0 +1,49 @@ +/* + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * Copyright 2025- FlagOS Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TILE_IR_TO_HIVM_CONVERSION_PASSES_H +#define TRITON_ADAPTER_TILE_IR_TO_HIVM_CONVERSION_PASSES_H + +#include "mlir/Pass/Pass.h" + +namespace mlir { +// Forward declarations. +class ModuleOp; + +namespace triton { + +/// Creates a pass to convert CommonIR dialect to HIVM dialect. +std::unique_ptr> createCommonIRToHIVMPass(); + +/// Creates a pass to reduce sum strength for profitable add reductions. +std::unique_ptr> createReduceSumStrengthPass(); + +#define GEN_PASS_REGISTRATION +#include "ascend/include/CommonIRToHIVM/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_TILE_IR_TO_HIVM_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/CommonIRToHIVM/Passes.td b/third_party/ascend/include/CommonIRToHIVM/Passes.td new file mode 100644 index 0000000000..dedfb46258 --- /dev/null +++ b/third_party/ascend/include/CommonIRToHIVM/Passes.td @@ -0,0 +1,66 @@ +/* + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * Copyright 2025- FlagOS Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef TILE_IR_TO_HIVM_CONVERSION_PASSES +#define TILE_IR_TO_HIVM_CONVERSION_PASSES + +include "mlir/Pass/PassBase.td" + +def CommonIRToHIVM : Pass<"commonir-to-hivm", "mlir::ModuleOp"> { + let summary = "Convert CommonIR dialect to HIVM dialect"; + let constructor = "triton::createCommonIRToHIVMPass()"; + let dependentDialects = [ + "hivm::HIVMDialect", + "triton::tile::CommonIRDialect", + "mlir::bufferization::BufferizationDialect", + "mlir::memref::MemRefDialect", + "mlir::tensor::TensorDialect", + "mlir::triton::TritonDialect", + ]; +} + +def ReduceSumStrength : Pass<"reduce-sum-strength", "mlir::ModuleOp"> { + let summary = "Reduce sum strength for profitable tl.sum-style add reductions"; + let description = [{ + This pass rewrites profitable add reductions by pre-adding two equal halves + of the reduced dimension and reducing the half-sized tensor. It is owned by + the Ascend ttir-to-ttadapter pipeline for backends where a full-width reduction + is more expensive than an elementwise add plus a smaller reduction. + }]; + let constructor = "triton::createReduceSumStrengthPass()"; + + let dependentDialects = ["mlir::arith::ArithDialect", + "mlir::tensor::TensorDialect", + "mlir::triton::TritonDialect"]; + + let options = [ + Option<"enable", "enable", "bool", /*default*/"true", + "Enable the rewrite">, + Option<"splitFactor", "split-factor", "int32_t", /*default*/"2", + "Reduction-axis split factor; currently only 2 is supported"> + ]; +} + +#endif // TILE_IR_TO_HIVM_CONVERSION_PASSES diff --git a/third_party/ascend/include/CommonIRToHIVM/ReduceSumStrengthPass.h b/third_party/ascend/include/CommonIRToHIVM/ReduceSumStrengthPass.h new file mode 100644 index 0000000000..67a7bd94f0 --- /dev/null +++ b/third_party/ascend/include/CommonIRToHIVM/ReduceSumStrengthPass.h @@ -0,0 +1,26 @@ +/* + * Copyright 2026- Xcoresigma Technology Co., Ltd + */ + +#ifndef TRITON_ADAPTER_COMMONIR_TO_HIVM_REDUCESUMSTRENGTHPASS_H +#define TRITON_ADAPTER_COMMONIR_TO_HIVM_REDUCESUMSTRENGTHPASS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +#include +#include +#include + +namespace mlir { +namespace triton { + +std::unique_ptr> createReduceSumStrengthPass(); + +std::unique_ptr> +createReduceSumStrengthPass(bool enable, int32_t splitFactor); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_COMMONIR_TO_HIVM_REDUCESUMSTRENGTHPASS_H diff --git a/third_party/ascend/include/Dialect/CMakeLists.txt b/third_party/ascend/include/Dialect/CMakeLists.txt new file mode 100644 index 0000000000..5fedd7ca27 --- /dev/null +++ b/third_party/ascend/include/Dialect/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(TritonStructured) +add_subdirectory(TritonAscend) diff --git a/third_party/ascend/include/Dialect/TritonAscend/CMakeLists.txt b/third_party/ascend/include/Dialect/TritonAscend/CMakeLists.txt new file mode 100644 index 0000000000..f33061b2d8 --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonAscend/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(IR) diff --git a/third_party/ascend/include/Dialect/TritonAscend/IR/CMakeLists.txt b/third_party/ascend/include/Dialect/TritonAscend/IR/CMakeLists.txt new file mode 100644 index 0000000000..488e9132f6 --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonAscend/IR/CMakeLists.txt @@ -0,0 +1,15 @@ +set(MLIR_BINARY_DIR ${CMAKE_BINARY_DIR}) + +set(LLVM_TARGET_DEFINITIONS TritonAscendOps.td) +mlir_tablegen(TritonAscendDialect.h.inc -gen-dialect-decls -dialect=ascend) +mlir_tablegen(TritonAscendDialect.cpp.inc -gen-dialect-defs -dialect=ascend) +mlir_tablegen(TritonAscendOps.h.inc -gen-op-decls) +mlir_tablegen(TritonAscendOps.cpp.inc -gen-op-defs) +add_mlir_doc(TritonAscendDialect TritonAscendDialect dialects/ -gen-dialect-doc) +add_mlir_doc(TritonAscendOps TritonAscendOps dialects/ -gen-op-doc) +add_public_tablegen_target(TritonAscendTableGen) + +set(LLVM_TARGET_DEFINITIONS TritonAscendAttrDefs.td) +mlir_tablegen(TritonAscendOpsAttrDefs.h.inc -gen-attrdef-decls) +mlir_tablegen(TritonAscendOpsAttrDefs.cpp.inc -gen-attrdef-defs) +add_public_tablegen_target(TritonAscendAttrDefsIncGen) diff --git a/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendAttrDefs.td b/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendAttrDefs.td new file mode 100644 index 0000000000..7ce5eefb80 --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendAttrDefs.td @@ -0,0 +1,25 @@ +//===-- TritonAscendAttrDefs.td - dialect attributes def. ----*- tablegen -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef TRITON_ASCEND_ATTRDEFS +#define TRITON_ASCEND_ATTRDEFS + +include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.td" + +include "mlir/IR/AttrTypeBase.td" +include "mlir/IR/EnumAttr.td" + +class TritonAscend_Attr traits = []> + : AttrDef { + let mnemonic = attrMnemonic; + let cppNamespace = "::mlir::triton::ascend"; +} + + + +#endif // TRITON_ASCEND_ATTRDEFS diff --git a/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h b/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h new file mode 100644 index 0000000000..5864110ca9 --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h @@ -0,0 +1,48 @@ +//===- TritonAscendDialect.h - MLIR TritonAscend dialect --------------*- C++ +//-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file defines the TritonAscend dialect in MLIR, containing Ascend +// operations. +// +//===----------------------------------------------------------------------===// + +#ifndef TRITON_DIALECT_ASCEND_DIALECT_H +#define TRITON_DIALECT_ASCEND_DIALECT_H + +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/IR/Dialect.h" +#include "mlir/IR/OpDefinition.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h.inc" + +#define GET_ATTRDEF_CLASSES +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendOpsAttrDefs.h.inc" + +#define GET_OP_CLASSES +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendOps.h.inc" + +#include "mlir/Transforms/InliningUtils.h" + +namespace mlir::triton::ascend { + +struct TritonAscendInlinerInterface : public mlir::DialectInlinerInterface { + using mlir::DialectInlinerInterface::DialectInlinerInterface; + // All operations within the TritonAscend dialect (eg: ascend.sort, + // ascend.flip) can be inlined. + bool isLegalToInline(mlir::Operation *, mlir::Region *, bool, + mlir::IRMapping &) const final { + return true; + } +}; + +} // namespace mlir::triton::ascend + +#endif // TRITON_DIALECT_ASCEND_DIALECT_H diff --git a/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.td b/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.td new file mode 100644 index 0000000000..a7314eff6d --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.td @@ -0,0 +1,31 @@ +//===-- TritonAscendDialect.td - dialect op definitions -------*- tablegen -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef TRITON_ASCEND_DIALECT +#define TRITON_ASCEND_DIALECT + +include "mlir/IR/OpBase.td" + +def TritonAscend_Dialect : Dialect { + let name = "ascend"; + let cppNamespace = "::mlir::triton::ascend"; + let summary = "The TritonAscend dialect in Triton."; + + let description = [{ + TritonAscend is a dialect for representing operations on Ascend NPUs. + }]; + + let dependentDialects = [ + "mlir::LLVM::LLVMDialect", + "triton::TritonDialect", + ]; + + let extraClassDeclaration = [{}]; +} + +#endif // TRITON_ASCEND_DIALECT diff --git a/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendOps.td b/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendOps.td new file mode 100644 index 0000000000..63ccd7be3e --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonAscend/IR/TritonAscendOps.td @@ -0,0 +1,656 @@ +//===-- TritonAscendOps.td - TritonAscend op definitions ---------*- tablegen -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This is the TritonAscend IR operation definition file. +// +//===----------------------------------------------------------------------===// + +#ifndef TRITON_ASCEND_OPS +#define TRITON_ASCEND_OPS + +include "mlir/IR/OpBase.td" +include "mlir/IR/EnumAttr.td" +include "mlir/Dialect/LLVMIR/LLVMTypes.td" +include "mlir/Interfaces/SideEffectInterfaces.td" +include "mlir/IR/OpAsmInterface.td" +include "mlir/Interfaces/InferTypeOpInterface.td" // SameOperandsAndResultType + +include "triton/Dialect/Triton/IR/TritonAttrDefs.td" +include "triton/Dialect/Triton/IR/TritonTypes.td" +include "triton/Dialect/Triton/IR/TritonInterfaces.td" + +include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.td" +include "ascend/include/Dialect/TritonAscend/IR/TritonAscendAttrDefs.td" + +//===----------------------------------------------------------------------===// +// TritonAscend op definitions +//===----------------------------------------------------------------------===// + +class TT_Ascend_Op traits = []> : + Op; + + +// +// Interfaces +// +def GlobalMemory : Resource<"::mlir::triton::GlobalMemory">; + + +// +// Annotation Op +// +def AnnotationOp : TT_Ascend_Op<"annotation", [Pure, MemoryEffects<[MemWrite]>]> { + let summary = "Annotate a tensor with key-value attribute pairs"; + let description = [{ + `ascend.annotation` operation can be used to annotate a tensor with + key-value attribute pairs. + + Example: + ```mlir + ascend.annotation %target {key : val} + ``` + }]; + let arguments = (ins TT_Tensor:$src); + let assemblyFormat = [{ + $src attr-dict `:` type($src) + }]; +} + + +// +// Mod Op +// +def ModOp : TT_Ascend_Op<"mod", [Pure]> { + let summary = "Mod operation (%) of input tensors."; + let description = [{ + Performs element-wise division with remainder of input tensors. + }]; + + let arguments = (ins TT_Tensor:$lhs, TT_Tensor:$rhs); + let results = (outs TT_Tensor:$result); + + let assemblyFormat = "$lhs `,` $rhs attr-dict `:` type($lhs) type($rhs) `->` type($result)"; +} + + +// +// IndexPut Op +// +def IndexPutOp : TT_Ascend_Op<"index_put", [ + MemoryEffects<[MemWrite]>, + SameVariadicOperandSize, +]> { + let summary = "Scatter store to a tensor pointer with embedding semantics"; + + let description = [{ + Index put values from a tensor into a destination tensor. + + The operation takes: + - ptr: pointer type, the destination tensor pointer (in GM) + - index: tensor, a index to scatter (in UB) + - value: tensor, a value to store (in UB) + - dim: int32, the dimension to scatter along + - index_boundary: int64, the upper boundary for index values + - end_offset: tuple of int, the offsets of each dimension for the end of the scatter region + - start_offset: tuple of int, the offsets of each dimension for the start of the scatter region + - dst_stride: tuple of int, the stride of each dimension of destination tensor + + + Constraints: + - `ptr` and `value` must have the same rank. + - `ptr.dtype` only supports `float16`, `bfloat16`, `float32` currently. + - `index` must be an integer tensor. If `index.rank` != 1, it will be reshaped to 1D. + - `index.numel` must equal `value.shape[dim]`. + - `value` support 2~5D tensors. + - `dim` must be valid (0 <= dim < rank(value) - 1). + }]; + + let arguments = ( + ins TT_Ptr:$ptr, + TT_Tensor:$index, + TT_Tensor:$value, + TT_Int:$dim, + TT_Int:$indexBoundary, + Variadic>:$endOffset, + Variadic>:$startOffset, + Variadic>:$dstStride + ); + + let assemblyFormat = [{ + $ptr `:` type($ptr) `,` $index `:` type($index) `,` + $value `:` type($value) `,` $dim `:` type($dim) `,` $indexBoundary `:` type($indexBoundary) `,` + `[` $endOffset `:` type($endOffset) `]` `,` `[` $startOffset `:` type($startOffset) `]` `,` + `[` $dstStride `:` type($dstStride) `]` + attr-dict + }]; +} + +// +// GatherOutToUb Op +// +def GatherOutToUbOp : TT_Ascend_Op<"gather_out_to_ub", [ + DeclareOpInterfaceMethods, + AttrSizedOperandSegments, +]> { + let summary = "Gather load from a tensor pointer with the embedding semantics"; + + let description = [{ + Gather from a source tensor in Global Memory (GM) to Unified Buffer (UB) + along a specified dimension with out-of-bound handling. + + The operation takes: + - src: pointer type, the source tensor pointer (in GM) + - index: tensor, a tensor to gather (in UB) + - index_boundary: int64, the upper boundary for index values + - dim: int32, the dimension to gather along + - src_stride: tuple of int64, the stride of each dimension of src tensor + - end_offset: tuple of int32, the end offsets of each dimension for index tensor + - start_offset: tuple of int32, the start offsets of each dimension for index tensor + - other(Optional): scalar value, the default value when index is out of boundary (in UB) + + Returns: + a tensor, with the same shape as `index.shape` (in UB) + + Constraints: + - `src` and `index` must have the same rank. + - `src.dtype` only supports `float16`, `bfloat16`, `float32` currently. + - `index` must be an integer tensor, with rank between 1 and 5. + - `dim` must be valid (0 <= dim < rank(index)). + - `other` must be a scalar value. + - For every dimension `i` not equal to `dim`, `index.size[i]` <= `src.size[i]`. + - The output shape is the same as `index.shape`. If `index` is None, \ + the output tensor will be an empty tensor with the same shape as `index`. + }]; + + let arguments = ( + ins TT_Ptr:$src, + TT_Tensor:$index, + TT_Int:$indexBoundary, + TT_Int:$dim, + Variadic>:$srcStride, + Variadic>:$endOffset, + Variadic>:$startOffset, + Optional:$other + ); + + let results = (outs TT_Tensor:$result); + + let assemblyFormat = [{ + $src `:` type($src) `,` $index `:` type($index) `,` + $indexBoundary `:` type($indexBoundary) `,` $dim `:` type($dim) `,` + `[` $srcStride `:` type($srcStride) `]` `,` `[` $endOffset `:` type($endOffset) `]` `,` + `[` $startOffset `:` type($startOffset) `]` (`,` $other^ `:` type($other))? + attr-dict `->` type($result) + }]; +} + +// +// ScatterUbToOut Op +// +def ScatterUbToOutOp : TT_Ascend_Op<"scatter_ub_to_out", [ + MemoryEffects<[MemWrite]>, + SameVariadicOperandSize, +]> { + let summary = "scatter store from a tensor pointer with the embedding semantics"; + + let description = [{ + Scatter a tile from Unified Buffer (UB) into a destination tensor in Global Memory (GM) + along a specified dimension, with index-boundary checking. + + The operation takes: + - ptr: pointer type, the destination tensor pointer (in GM) + - value: tensor, a tile value to store (in UB) + - index: tensor, a tile index to scatter (in UB) + - index_boundary: int, the upper boundary for index values + - dim: int, the dimension to scatter along + - dst_stride: tuple of int, the stride of each dimension of destination tensor + - end_offset: tuple of int32, the end offsets of each dimension for index tensor + - start_offset: tuple of int32, the start offsets of each dimension for index tensor + + Constraints: + - `ptr` and `index` must have the same rank. + - `ptr.dtype` only supports `float16`, `bfloat16`, `float32` currently. + - `index` must be an integer tensor, with rank between 1 and 5. + - `dim` must be valid (0 <= dim < rank(index)). + - For every dimension `i` not equal to `dim`, `index.size[i]` <= `ptr.size[i]`. + - The output shape is the same as `index.shape`. If `index` is None, \ + the output tensor will be an empty tensor with the same shape as `index`. + }]; + + let arguments = ( + ins TT_Ptr:$ptr, + TT_Tensor:$value, + TT_Tensor:$index, + TT_Int:$indexBoundary, + TT_Int:$dim, + Variadic>:$dstStride, + Variadic>:$endOffset, + Variadic>:$startOffset + ); + + let assemblyFormat = [{ + $ptr `:` type($ptr) `,` $value `:` type($value) `,` `,` $index `:` type($index) `,` + $indexBoundary `:` type($indexBoundary) `,` $dim `:` type($dim) `,` + `[` $dstStride `:` type($dstStride) `]` `,` `[` $endOffset `:` type($endOffset) `]` `,` + `[` $startOffset `:` type($startOffset) `]` + attr-dict + }]; +} + + +// +// IndexSelectSimd Op +// +def IndexSelectSimdOp : TT_Ascend_Op<"index_select_simd", [ + MemoryEffects<[MemRead]>, + DeclareOpInterfaceMethods, + AttrSizedOperandSegments +]> { + let summary = "Index select SIMD operation from global memory"; + + let description = [{ + Index select operation (SIMD version) that loads data from multiple indices along a + specified dimension. The operation selects data from GM and loads them + as tiles directly to UB with zero-copy semantics. + + The operation takes: + - src: Source pointer (in GM) + - index: 1D tensor of indices to select (already in UB) + - dim: The dimension along which to select + - src_shape: Complete shape of the source tensor + - src_offset: Starting offset for reading + - read_shape: Size to read (tile shape) + + Constraints: + - read_shape[dim] must be -1 + - src_offset[dim] can be -1 (will be ignored) + }]; + + let arguments = ( + ins + TT_PtrLike:$src, + TT_IntTensor:$index, + I32Attr:$dim, + Variadic:$src_shape, + Variadic:$src_offset, + DenseI32ArrayAttr:$read_shape + ); + + let results = (outs TT_Tensor:$result); + + let assemblyFormat = [{ + $src `,` $index `,` $dim `,` + `[` $src_shape `]` `,` + `[` $src_offset `]` `,` + $read_shape + attr-dict `:` type($src) `,` type($index) `->` type($result) + }]; +} + + +// +// Built-in: IndirectLoad Op +// +def IndirectLoadOp : TT_Ascend_Op<"indirect_load", [ + DeclareOpInterfaceMethods, + AttrSizedOperandSegments +]> { + let summary = "Built-in: indirect load from global memory using per-element offsets with optional mask/other"; + + let description = [{ + Built-in operation emitted by the compiler for unstructured (discrete) memory + accesses.These are not written directly in the user IR. + + Load values from global memory based on per-element offsets. If `mask` + is provided, false lanes return `other`. + + The operation takes: + - src: Source pointer + - offsets: Tensor of per-element offsets (relative to `src`) for accessing source memory + - mask (optional): if mask[idx] is false, do not load the data at address pointer[idx] + - other (optional): if mask[idx] is false, return other[idx] + - isVolatile: defaults to true. Set to false only when analysis proves + the source pointer has no prior possible writes. + }]; + + let arguments = ( + ins TT_Ptr:$src, + TT_IntTensor:$offsets, + Optional:$mask, + Optional:$other, + DefaultValuedAttr:$isVolatile + ); + + let results = (outs TT_Tensor:$result); + + let assemblyFormat = [{ + $src `:` type($src) `,` + $offsets `:` type($offsets) + (`,` $mask^ `:` type($mask))? + (`,` $other^ `:` type($other))? + attr-dict `->` type($result) + }]; + +} + +// +// Built-in: StrideLoad Op +// +def StrideLoadOp : TT_Ascend_Op<"stride_load", [ + DeclareOpInterfaceMethods, + SameVariadicOperandSize +]> { + let summary = "Built-in: strided load from global memory"; + + let description = [{ + Built-in operation emitted by the compiler for SIMT stride-load templates. + It loads a tensor from a scalar source pointer with one i32/i64 linear + base offset, per-dimension physical strides and valid element counts. + Out-of-bound elements are filled with the scalar `other` value. + }]; + + let arguments = ( + ins TT_Ptr:$src, + AnyTypeOf<[I32, I64]>:$offset, + AnyType:$other, + Variadic>:$stride, + Variadic>:$numel + ); + + let results = (outs TT_Tensor:$result); + + let assemblyFormat = [{ + $src `:` type($src) `,` + $offset `:` type($offset) `,` + $other `:` type($other) `,` + `[` $stride `:` type($stride) `]` `,` + `[` $numel `:` type($numel) `]` + attr-dict `->` type($result) + }]; +} + +// +// Built-in: StrideStore Op +// +def StrideStoreOp : TT_Ascend_Op<"stride_store", [ + MemoryEffects<[MemWrite]>, + SameVariadicOperandSize +]> { + let summary = "Built-in: strided store to global memory"; + + let description = [{ + Built-in operation emitted by the compiler for SIMT stride-store templates. + It stores a tensor to a scalar destination pointer with one i32/i64 linear + base offset, per-dimension physical strides and valid element counts. + }]; + + let arguments = ( + ins TT_Ptr:$dst, + TT_Type:$src, + AnyTypeOf<[I32, I64]>:$offset, + Variadic>:$stride, + Variadic>:$numel + ); + + let assemblyFormat = [{ + $dst `:` type($dst) `,` + $src `:` type($src) `,` + $offset `:` type($offset) `,` + `[` $stride `:` type($stride) `]` `,` + `[` $numel `:` type($numel) `]` + attr-dict + }]; +} + + +// +// Built-in: IndirectStore Op +// +def IndirectStoreOp : TT_Ascend_Op<"indirect_store", [ + MemoryEffects<[MemWrite]> +]> { + let summary = "Built-in: indirect store from UB using per-element offsets with optional mask/other"; + + let description = [{ + Built-in operation emitted by the compiler for unstructured (discrete) memory + accesses.These are not written directly in the user IR. + + Store values from UB based to GM on per-element offsets. + + The operation takes: + - src: Source pointer + - offsets: Tensor of per-element offsets (relative to `src`) for accessing source memory + - value: The tensor of elements to be stored + - mask (optional): If mask[idx] is false, do not store value[idx] at pointer[idx] + }]; + + let arguments = ( + ins TT_Ptr:$src, + TT_IntTensor:$offsets, + TT_Type:$value, + Optional:$mask + ); + + let assemblyFormat = [{ + $src `:` type($src) `,` + $offsets `:` type($offsets) `,` + $value `:` type($value) + (`,` $mask^ `:` type($mask))? + attr-dict + }]; + +} + +// +// Custom Op +// +def CustomOp : TT_Ascend_Op<"custom", [Pure, MemoryEffects<[MemWrite]>]> { + let summary = "self-defined custom operation"; + let description = [{ + `ascend.custom` triton custom op is designed to pass self-defined custom operation. + + Example: + ```ascend.custom {str_args = ["sync_block_wait", "cube"]} + ``` + }]; + let arguments = (ins StrAttr:$op_name, ArrayAttr:$str_args, Variadic:$args); + + let assemblyFormat = "$op_name attr-dict ($args^ `:` type($args))?"; +} + +def FlipOp : TT_Ascend_Op<"flip", [ + NoMemoryEffect, + DeclareOpInterfaceMethods +]> { + let summary = "Reverse a tensor along a given dimension"; + let description = [{ + Reverses the elements of the input tensor along the specified dimension. + The output tensor has the same shape and element type as the input. + }]; + + let arguments = (ins + TT_Tensor:$src, // Input tensor + I64Attr:$dim // Dimension to flip along + ); + + let results = (outs + TT_Tensor:$flipped // Flipped values + ); + + let assemblyFormat = + "$src `,` $dim attr-dict `:` type($src) `->` type($flipped)"; +} + +def SortOp : TT_Ascend_Op<"sort", [ + NoMemoryEffect, + DeclareOpInterfaceMethods +]> { + let summary = "Sorts a tensor along a given dimension and returns sorted values."; + let description = [{ + Sorts the elements of the input tensor along the specified dimension. + Returns one tensor: + The sorted tensor (same shape and element type as input). + }]; + + let arguments = (ins + TT_Tensor:$src, // Input tensor + I64Attr:$dim, // Dimension to sort along + BoolAttr:$descending // Sort order + ); + + let results = (outs + TT_Tensor:$sorted // Sorted values + ); + + let assemblyFormat = "$src `,` $dim `,` $descending attr-dict `:` type($src) `->` type($sorted)"; +} + +// +// Conv Op +// +def Conv1dOp : TT_Ascend_Op<"conv1d", [ + Pure, + DeclareOpInterfaceMethods +]> { + let summary = "1D convolution operation"; + let description = [{ + Performs a 1D convolution operation on the input tensor. + }]; + + let arguments = (ins + TT_Tensor:$input, + TT_Tensor:$weight, + Optional:$bias, + I64Attr:$stride, + I64Attr:$padding_size, + I64Attr:$dilation, + I64Attr:$groups + ); + + let results = (outs TT_Tensor:$result); + + let assemblyFormat = [{ + `(` operands `)` + attr-dict `:` functional-type(operands, $result) + }]; + + let hasVerifier = 1; +} + +// +// Dot Op +// +def DotOp : TT_Ascend_Op<"dot", [ + Pure, + DeclareOpInterfaceMethods +]> { + let summary = "Matmul D = A * B with per-operand fractal (zN) control"; + let description = [{ + Computes `D = A * B`, declaring per operand whether it is stored in a + fractal (zN) layout: `fractal_a` / `fractal_b` mark input A / B as + fractal (converted to ND before the multiply); `fractal_c` produces D in + fractal layout. The result carries the cube accumulator dtype (f32 for + float inputs, i32 for int8), and its type is inferred from A, B and + these flags. + + zN convention (non-transpose; inner block [16, b], b = 32 / elem_bytes, + i.e. f16/bf16 -> b=16, f32 -> b=8, int8 -> b=32): + A [M,K] <-> [K1, M1, 16, b] (ND = [d1*d2, d0*d3]) + B [K,N] <-> [N1, K1, 16, b] (ND = [d1*d2, d0*d3]) + C [M,N] <-> [N1, M1, 16, 16] (L0C accumulator, block 16x16) + + Example (fractal_a + fractal_c, f16 in -> f32 accumulator out): + ```mlir + %d = ascend.dot %a, %b {fractal_a = true, fractal_c = true} + : tensor<1x1x16x16xf16>, tensor<16x16xf16> -> tensor<1x1x16x16xf32> + ``` + }]; + + let arguments = (ins + TT_Tensor:$a, + TT_Tensor:$b, + DefaultValuedAttr:$fractal_a, + DefaultValuedAttr:$fractal_b, + DefaultValuedAttr:$fractal_c + ); + + let results = (outs TT_Tensor:$d); + + let assemblyFormat = [{ + $a `,` $b attr-dict `:` type($a) `,` type($b) `->` type($d) + }]; + + let hasVerifier = 1; +} + +// +// Conv Op +// +def Conv2dOp : TT_Ascend_Op<"conv2d", [ + Pure, + DeclareOpInterfaceMethods +]> { + let summary = "2D convolution operation"; + let description = [{ + Performs a 2D convolution operation on the input tensor. + + Dimension constraints: + - input: [C_in, H_in, W_in] (unbatched) or [N, C_in, H_in, W_in] + (batched), where N is the batch size, C_in the input channel + count, and H_in/W_in the spatial input dimensions. + - weight: [C_out, C_in/groups, kH, kW], where C_out is the output + channel count and kH/kW the convolution kernel size. + - bias (optional): [C_out], one bias per output channel. Must have + the same element type as input and weight. + - result: [C_out, H_out, W_out] (unbatched) or + [N, C_out, H_out, W_out] (batched). + + Configurable attributes: + - stride: an integer (uniform along H and W) or 2-element integer + array [stride_h, stride_w], the step of the convolution kernel + along H and W. Elements must be > 0. + - padding: an integer (uniform on all sides), a 2-element integer + array [pad_h, pad_w] (symmetric per dimension), or a 4-element + integer array [pad_top, pad_bottom, pad_left, pad_right], the + number of zero-padding elements added to each side of the input. + - dilation: an integer (uniform along H and W) or 2-element integer + array [dil_h, dil_w], the spacing between kernel elements along H + and W. + - groups: number of blocked connections from input channels to + output channels. C_in and C_out must both be divisible by + groups. + + Output shape derivation: + H_out = floor((H_in + pad_top + pad_bottom - dil_h * (kH - 1) - 1) + / stride_h + 1) + W_out = floor((W_in + pad_left + pad_right - dil_w * (kW - 1) - 1) + / stride_w + 1) + }]; + + let arguments = (ins + TT_Tensor:$input, + TT_Tensor:$weight, + Optional:$bias, + // IntegerAttr (uniform scalar) or DenseI32ArrayAttr. + AnyAttr:$stride, + AnyAttr:$padding, + AnyAttr:$dilation, + I64Attr:$groups + ); + + let results = (outs TT_Tensor:$result); + + let assemblyFormat = [{ + `(` operands `)` + attr-dict `:` functional-type(operands, $result) + }]; + + let hasVerifier = 1; +} + +#endif // TRITON_ASCEND_OPS diff --git a/third_party/ascend/include/Dialect/TritonStructured/CMakeLists.txt b/third_party/ascend/include/Dialect/TritonStructured/CMakeLists.txt new file mode 100644 index 0000000000..f33061b2d8 --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonStructured/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(IR) diff --git a/third_party/ascend/include/Dialect/TritonStructured/IR/CMakeLists.txt b/third_party/ascend/include/Dialect/TritonStructured/IR/CMakeLists.txt new file mode 100644 index 0000000000..9c32c97c8c --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonStructured/IR/CMakeLists.txt @@ -0,0 +1,8 @@ +set(LLVM_TARGET_DEFINITIONS TritonStructuredDialect.td) +mlir_tablegen(TritonStructuredDialect.h.inc -gen-dialect-decls -dialect=tts) +mlir_tablegen(TritonStructuredDialect.cpp.inc -gen-dialect-defs -dialect=tts) +mlir_tablegen(TritonStructuredOps.h.inc -gen-op-decls) +mlir_tablegen(TritonStructuredOps.cpp.inc -gen-op-defs) + + +add_public_tablegen_target(TritonStructuredTableGen) diff --git a/third_party/ascend/include/Dialect/TritonStructured/IR/TritonStructuredDialect.h b/third_party/ascend/include/Dialect/TritonStructured/IR/TritonStructuredDialect.h new file mode 100644 index 0000000000..f619fa06db --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonStructured/IR/TritonStructuredDialect.h @@ -0,0 +1,30 @@ +#ifndef MLIR_DIALECT_TRITON_STRUCTURED_IR_TRITON_STRUCTURED_DIALECT_H_ +#define MLIR_DIALECT_TRITON_STRUCTURED_IR_TRITON_STRUCTURED_DIALECT_H_ + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Dialect.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/IR/TypeSupport.h" +#include "mlir/IR/Types.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/IR/Dialect.h" + +using namespace mlir; +using namespace mlir::triton; +//===----------------------------------------------------------------------===// +// TritonStructured Operations +//===----------------------------------------------------------------------===// +#include "Dialect/TritonStructured/IR/TritonStructuredDialect.h.inc" + +// Include the auto-generated header file containing the declarations of the +// TritonStructured operations. +#define GET_OP_CLASSES + +#include "Dialect/TritonStructured/IR/TritonStructuredOps.h.inc" + +#endif diff --git a/third_party/ascend/include/Dialect/TritonStructured/IR/TritonStructuredDialect.td b/third_party/ascend/include/Dialect/TritonStructured/IR/TritonStructuredDialect.td new file mode 100644 index 0000000000..7d205da94f --- /dev/null +++ b/third_party/ascend/include/Dialect/TritonStructured/IR/TritonStructuredDialect.td @@ -0,0 +1,66 @@ +#ifndef TRITON_STRUCTURED_DIALECT +#define TRITON_STRUCTURED_DIALECT + +include "mlir/IR/OpBase.td" +include "triton/Dialect/Triton/IR/TritonDialect.td" +include "triton/Dialect/Triton/IR/TritonTypes.td" +include "triton/Dialect/Triton/IR/TritonAttrDefs.td" +//include "triton/Dialect/Triton/IR/TritonTypes.td" +include "mlir/Interfaces/SideEffectInterfaces.td" + + + +def Triton_Structured_Dialect : Dialect { + let name = "tts"; + + let cppNamespace = "::mlir::tts"; + + let summary = "Structured Triton operations"; + + let description = [{ + Triton Structured Dialect. + }]; + + let dependentDialects = [ + "triton::TritonDialect" + ]; + + let usePropertiesForAttributes = 1; +} + +// +// Op Base +// +class TTS_Op traits = []> : + Op { +} + + +// SameVariadicResultSize +// AttrSizedResultSegments +def TTS_GetStructuredStateOp : TTS_Op<"get_structured_state", [AttrSizedResultSegments, Pure]> { + let summary = "Placeholder for the structured pointer states computed during PtrAnalysis."; + let description = "Used to pass the offsets and strides to scf.for op to simplify IR rewrites."; + + let arguments = (ins AnyTypeOf<[TT_PtrLike, I32Tensor, I64Tensor,I16Tensor,I8Tensor,I1Tensor]>:$input); + let results = (outs AnyTypeOf<[TT_PtrLike, I32Tensor, I64Tensor,I16Tensor,I8Tensor,I1Tensor]>:$structured, Variadic:$offsets, Variadic:$strides); + + let builders = [ + OpBuilder<(ins "Value":$input)>, + ]; + + let extraClassDeclaration = [{ + static std::optional, SmallVector>> + getOffsetAndStrideTypes(MLIRContext *context, Type ptrLikeType); + + static std::optional> + getOffsetAndStrideSegmentSizes(Type ptrLikeType); + }]; + + let hasFolder = 0; + let hasVerifier = 1; +} + + + +#endif // TRITON_STRUCTURED_DIALECT diff --git a/third_party/ascend/include/DiscreteMaskAccessConversion/CMakeLists.txt b/third_party/ascend/include/DiscreteMaskAccessConversion/CMakeLists.txt new file mode 100644 index 0000000000..17d750cf9b --- /dev/null +++ b/third_party/ascend/include/DiscreteMaskAccessConversion/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name DiscreteMaskAccessConversion) +add_public_tablegen_target(DiscreteMaskAccessConversionPassIncGen) diff --git a/third_party/ascend/include/DiscreteMaskAccessConversion/DiscreteMaskAccessConversionPass.h b/third_party/ascend/include/DiscreteMaskAccessConversion/DiscreteMaskAccessConversionPass.h new file mode 100644 index 0000000000..8bd9272fec --- /dev/null +++ b/third_party/ascend/include/DiscreteMaskAccessConversion/DiscreteMaskAccessConversionPass.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DISCRETEMASKACCESSCONVERSION_H +#define TRITON_ADAPTER_DISCRETEMASKACCESSCONVERSION_H + +#include "mlir/Pass/Pass.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "ascend/include/Utils/Utils.h" +#include "mlir/IR/PatternMatch.h" + +#define GEN_PASS_DECL_DISCRETEMASKACCESSCONVERSION +#include "ascend/include/DiscreteMaskAccessConversion/Passes.h.inc" + +#define GEN_PASS_DEF_DISCRETEMASKACCESSCONVERSION +#include "ascend/include/DiscreteMaskAccessConversion/Passes.h.inc" + +namespace mlir { +namespace triton { + +std::unique_ptr> createDiscreteMaskAccessConversionPass( + const DiscreteMaskAccessConversionOptions &options = {}); + +} // namespace triton +} // namespace mlir + +namespace { + +using namespace mlir; +using namespace triton; + +class DiscreteMaskAccessConversionPass + : public ::impl::DiscreteMaskAccessConversionBase< + DiscreteMaskAccessConversionPass> { +public: + explicit DiscreteMaskAccessConversionPass( + const DiscreteMaskAccessConversionOptions &options); + void getDependentDialects(DialectRegistry ®istry) const override; + void runOnOperation() override; +}; + +} // namespace + +#endif // DISCRETE_MASK_ACCESS_CONVERSION_H diff --git a/third_party/ascend/include/DiscreteMaskAccessConversion/Passes.h b/third_party/ascend/include/DiscreteMaskAccessConversion/Passes.h new file mode 100644 index 0000000000..51d9f81703 --- /dev/null +++ b/third_party/ascend/include/DiscreteMaskAccessConversion/Passes.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DISCRETE_MASK_ACCESS_CONVERSION_PASSES_H +#define TRITON_ADAPTER_DISCRETE_MASK_ACCESS_CONVERSION_PASSES_H + +#include "DiscreteMaskAccessConversionPass.h" + +namespace mlir { +namespace triton { + +#define GEN_PASS_REGISTRATION +#include "ascend/include/DiscreteMaskAccessConversion/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DISCRETE_MASK_ACCESS_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/DiscreteMaskAccessConversion/Passes.td b/third_party/ascend/include/DiscreteMaskAccessConversion/Passes.td new file mode 100644 index 0000000000..ffd9060550 --- /dev/null +++ b/third_party/ascend/include/DiscreteMaskAccessConversion/Passes.td @@ -0,0 +1,24 @@ +/* + * Copyright (c) Huawei Technologies Co. + * Licensed under the MIT license. + */ + +#ifndef DISCRETE_MASK_ACCESS_CONVERSION_PASSES +#define DISCRETE_MASK_ACCESS_CONVERSION_PASSES + +include "mlir/Pass/PassBase.td" + +def DiscreteMaskAccessConversion : Pass<"discrete-mask-access-conversion", "mlir::ModuleOp"> { + let summary = "Recognize and convert discrete mask memory access"; + let constructor = "triton::createDiscreteMaskAccessConversionPass()"; + let options = [ + Option<"compileOn91095", "compile-on-910-95", + "bool", /*default*/"false", + "compile on 910_95">, + Option<"compileMode", "compile-mode", + "std::string", /*default*/"\"simd_simt_template\"", + "Compilation mode: simd, simd_simt_template (or unstructured_in_simt), simt_only"> + ]; +} + +#endif // DISCRETE_MASK_ACCESS_CONVERSION_PASSES diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition.h new file mode 100644 index 0000000000..82a4358c7b --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition.h @@ -0,0 +1,138 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_ADD_CONTROLFLOW_CONDITION_PASS_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_ADD_CONTROLFLOW_CONDITION_PASS_H + +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/TransformOps/DialectExtension.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" + +namespace mlir { +namespace triton { +// Buffer count thresholds for flowOpt condition +constexpr int CROSS_CORE_BUFFER_COUNT_THRESHOLD = 1; +constexpr int INTRA_CORE_BUFFER_COUNT_THRESHOLD = 2; + +// Relationship between a tensor iter_arg and ssbuffer.if in the main_loop +struct TensorIterArgIfOpRelation { + Value iterArg; + scf::IfOp producer; + llvm::SmallVector consumers; +}; + +// One consumer op may appear in multiple groups. +// Each inner SmallVector is the producers of one group this consumer belongs +// to. +using ConsumerProducerMap = + llvm::DenseMap>>; + +inline int countProducerGroups(const ConsumerProducerMap &map) { + int numProducerGroups = 0; + for (const auto &entry : map) + numProducerGroups += static_cast(entry.second.size()); + return numProducerGroups; +} + +// Variables to control when an ifOp is both producer and consumer of a tensor +// iter_args +struct TensorIterArgIfOpVars { + // The variables that need to be controlled as a producer + llvm::SmallVector producerVars; + // The variables that need to be controlled as a consumer + llvm::SmallVector consumerVars; +}; + +// Kind of dependency edge between two ifOps in ifBlockDAG. +enum class IfBlockDepKind : uint8_t { CrossCore = 0, IntraCore = 1 }; + +// Per scf.while block-arg map: whileOp -> block_id -> (new_arg_idx -> +// old_arg_idx). +using WhileBlockArgMap = + llvm::DenseMap>>; + +struct ControlFlowConditionInfo { + // Keys: main-loop op (scf.for/scf.while carrying ssbuffer.main_loop) + llvm::DenseMap> blockCounters; + llvm::DenseMap blockCounterNums; + llvm::DenseMap> innerDepConds; + + ConsumerProducerMap crossCoreDependentMap; + llvm::DenseMap>> + intraCoreDependentMap; + // Stores producer/consumer relationship between tensor iter_args in main_loop + // and ssbuffer.if; vector index corresponds to iter arg index in the + // main-loop op + llvm::DenseMap> + tensorIterArgDepsMap; + // Records control condition variable index for newly created iter_args of + // tensor iter_args + llvm::DenseMap>> + tensorIterArgIndicesMap; + + // unique counter value for each ifblock scf.for only. + llvm::DenseMap cntArgs; + + // DAG for if block dependencies. + llvm::DenseMap>> + ifBlockDAG; + llvm::DenseMap flowOptIfOpPairs; + + // Buffer counts for flowOpt condition + int intraCoreBufferCount = 0; + int crossCoreBufferCount = 0; + + // Per scf.while (with main_loop attr): records per-block new iter_args + // mirroring iter_args used in scf.condition. Keys: whileOp -> block_id -> new + // iter_arg index. Value: original iter_arg index. + WhileBlockArgMap whileBlockArgMap; +}; + +class AddControlFlowConditionPass + : public PassWrapper> { +public: + AddControlFlowConditionPass() = default; + + void runOnOperation() override; + + void getDependentDialects(DialectRegistry ®istry) const override { + registry.insert(); + } + + llvm::StringRef getArgument() const override { + return "add-control-flow-condition"; + } +}; + +std::unique_ptr> createAddControlFlowConditionPass(); + +void registerAddControlFlowConditionPasses(); +} // namespace triton +} // namespace mlir +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_ADD_CONTROLFLOW_CONDITION_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/CloneOps.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/CloneOps.h new file mode 100644 index 0000000000..baa187e1a0 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/CloneOps.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_SSBUF_CLONE_OPS_FOR_CONTROL_FLOW_H +#define TRITON_ASCEND_SSBUF_CLONE_OPS_FOR_CONTROL_FLOW_H +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/DialectRegistry.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class CloneOpsPass : public PassWrapper> { +public: + CloneOpsPass() = default; + + void runOnOperation() override; + + LogicalResult validateBlockIdsConsecutive(ModuleOp module); + LogicalResult cloneOpsInMainLoop(Operation *op); + LogicalResult cleanupClonedOpsInMainLoop(Operation *op); + LogicalResult validateClonedOpsInVector(ModuleOp module); + + llvm::StringRef getArgument() const override { return "clone-ops"; } +}; + +std::unique_ptr> createCloneOpsPass(); + +} // namespace triton +} // namespace mlir +#endif // TRITON_ASCEND_SSBUF_CLONE_OPS_FOR_CONTROL_FLOW_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/CreateIfOps.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/CreateIfOps.h new file mode 100644 index 0000000000..54c1892948 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/CreateIfOps.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_SSBUF_CREATE_IF_OPS_FOR_CONTROL_FLOW_H +#define TRITON_ASCEND_SSBUF_CREATE_IF_OPS_FOR_CONTROL_FLOW_H +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" + +namespace mlir { +namespace triton { + +class CreateIfOpsPass + : public PassWrapper> { +public: + CreateIfOpsPass() = default; + + void runOnOperation() override; + + void setConditionInfo(ControlFlowConditionInfo *info) { this->info = info; } + + // `op` is the main-loop op (scf.for or scf.while) carrying + // ssbuffer.main_loop. + LogicalResult computeYieldValues( + Operation *op, + const llvm::DenseMap> &blockOps, + llvm::DenseMap> &thenYieldValues, + llvm::DenseMap> &elseYieldValues); + + LogicalResult createIfInMainLoop( + Operation *op, + const llvm::DenseMap> &blockOps, + const llvm::DenseMap> &thenYieldValues, + const llvm::DenseMap> &elseYieldValues); + + llvm::StringRef getArgument() const override { return "create-if-ops"; } + +private: + ControlFlowConditionInfo *info = nullptr; +}; + +std::unique_ptr> createCreateIfOpsPass(); + +} // namespace triton +} // namespace mlir +#endif // TRITON_ASCEND_SSBUF_CREATE_IF_OPS_FOR_CONTROL_FLOW_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/InitDependentMap.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/InitDependentMap.h new file mode 100644 index 0000000000..bcb3d0b928 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/InitDependentMap.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_SSBUF_INITDEPENDENTMAPFORCONTROLFLOW_H +#define TRITON_ASCEND_SSBUF_INITDEPENDENTMAPFORCONTROLFLOW_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" + +namespace mlir { +namespace triton { + +class InitDependentMapPass + : public PassWrapper> { +public: + InitDependentMapPass() = default; + void runOnOperation() override; + void setConditionInfo(ControlFlowConditionInfo *info) { this->info = info; } + +private: + ControlFlowConditionInfo *info = nullptr; +}; + +std::unique_ptr> createInitDependentMapPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ASCEND_SSBUF_INITDEPENDENTMAPFORCONTROLFLOW_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/ProcessArgs.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/ProcessArgs.h new file mode 100644 index 0000000000..44f072f029 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/ProcessArgs.h @@ -0,0 +1,120 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_SSBUF_PROCESS_ARGS_FOR_CONTROL_FLOW_H +#define TRITON_ASCEND_SSBUF_PROCESS_ARGS_FOR_CONTROL_FLOW_H +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/DialectRegistry.h" +#include "mlir/Pass/Pass.h" + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" + +namespace mlir { +namespace triton { + +struct ControlFlowConditionInfo; + +// For each shared iter_arg tracks: which block_ids use it, who is owner (first +// block_id in order), and what new iter_arg index each non-owner block uses +struct SharedArgInfo { + int argIndex; + Value iterArg; + int ownerBlockId; + int newArgIndex; + int nonOwnerBlockId; + + SharedArgInfo(int arg, int owner, int newIdx, int nonOwner) + : argIndex(arg), iterArg(Value()), ownerBlockId(owner), + newArgIndex(newIdx), nonOwnerBlockId(nonOwner) {} +}; + +// Per-whileOp state for cloning cond-used iter_arg update chains into the new +// scf.while's after body. Populated by planWhileIterArgDescriptors; consumed by +// cloneWhileBlockChains, buildNewWhileYield, recordWhileBlockArgMap. +struct WhileIterArgClonePlan { + // Per-origIdx metadata, keyed on the original iter_arg index. + llvm::DenseMap compOp; + llvm::DenseMap> chainOps; + llvm::DenseMap posInClonedVec; + // (blockId, newArgIdx, origIdx) triples in planning order, one per (blockId, + // cond-used iter_arg) pair + SmallVector> newArgDescriptors; + // Output: cloned compOp results per blockId, indexed by posInClonedVec + llvm::DenseMap> clonedPerBlock; +}; + +class ProcessArgsPass + : public PassWrapper> { +public: + ProcessArgsPass() = default; + + void runOnOperation() override; + + LogicalResult processSharedIterArgs(ModuleOp module); + + // Snapshots whileOp iter_args; clones cond-used update chain per block (same + // ssbuffer.block_id run); records (new_arg_idx, old_arg_idx) in + // ControlFlowConditionInfo. + LogicalResult updateIndependentCondsInWhileBlocks(ModuleOp module); + + // Per-whileOp driver for updateIndependentCondsInWhileBlocks. + LogicalResult processWhileIterArgsInWhileOp(scf::WhileOp whileOp, + ControlFlowConditionInfo *info); + + // Per-op driver for shared-iter_args processing. + LogicalResult processSharedIterArgsInLoop(Operation *op, + ControlFlowConditionInfo *info); + + // Completes the scf.while path: migrate before/after bodies, rebuild + // yield/condition, transfer maps. + LogicalResult processSharedArgsInWhileOp( + scf::WhileOp whileOp, scf::WhileOp newWhileOp, + SmallVector &sharedArgsInfo, + const llvm::DenseMap &sharedArgToCompOp, + const llvm::DenseMap> + &sharedArgToChainOps, + ControlFlowConditionInfo *info); + + void setConditionInfo(ControlFlowConditionInfo *info_) { info = info_; } + + llvm::StringRef getArgument() const override { return "process-args"; } + + ControlFlowConditionInfo *info = nullptr; + + // Original iter_args of every scf.while op with main_loop attr, captured at + // start of ProcessArgs. Used to identify iter_args referenced by + // scf.condition. + llvm::DenseMap> + originalWhileIterArgIndices; + + // Local copy of whileBlockArgMap; also mirrored to info->whileBlockArgMap + // when info is set, so the mapping is observable when --process-args runs + // standalone (info may be null). + WhileBlockArgMap localWhileBlockArgMap; +}; + +std::unique_ptr> createProcessArgsPass(); + +} // namespace triton +} // namespace mlir +#endif // TRITON_ASCEND_SSBUF_PROCESS_ARGS_FOR_CONTROL_FLOW_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateConditionInfo.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateConditionInfo.h new file mode 100644 index 0000000000..72a77f02f8 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateConditionInfo.h @@ -0,0 +1,256 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_UPDATE_CONDITION_INFO_H +#define TRITON_ADAPTER_UPDATE_CONDITION_INFO_H + +#include + +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" + +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/TransformOps/DialectExtension.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/Pass/Pass.h" + +#include "third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" + +namespace mlir { +namespace triton { +enum class VarUpdateType { INC, DEC }; + +struct OutputGroupInfo { + SmallVector outputs; + SmallVector inputVars; +}; + +class UpdateConditionInfoPass + : public PassWrapper> { +public: + UpdateConditionInfoPass() = default; + + void runOnOperation() override; + + void setConditionInfo(ControlFlowConditionInfo *info) { this->info = info; } + +private: + SmallVector> allocSSBuffer(ModuleOp module); + + int updateIfConds(ModuleOp module, + SmallVector> ssbufferPtrs); + + // Collect ssbuffer ifOps: for walks body; while-do walks after-region only. + int collectSSBufferIfOps(Operation *loopOp, SmallVector &ifOps); + + // Validate blockCounters for for; while skips (counters are for-only). + int validateBlockCounters(Operation *loopOp, size_t ifOpCount); + + void updateForIterTimes(ModuleOp module); + + scf::ForOp extendForOpIterationCount(scf::ForOp oldForOp, int ifCount, + int requiredBuffers, int x, + IRMapping &mapper, + SmallVector &ifOpsInThisFor); + + void replaceForOpCounterInIfOps(SmallVector ifOpsInThisFor, + scf::ForOp oldForOp, scf::ForOp newForOp, + IRMapping &mapper); + + Value getVarValue(Operation *loopOp, int varIndex); + + void collectDependencyBuffers( + ModuleOp module, SmallVector &mainLoopOps, + DenseMap>> + &crossCoreBuffers, + DenseMap>>> + &intraCoreBuffersMap); + + int buildIdxToVarMap( + Operation *loopOp, + const DenseMap>> + &intraCoreBuffers, + DenseMap &idxToVar); + + int getInputOutputValues( + scf::IfOp ifOp, + DenseMap>> + crossCoreBuffers, + DenseMap>> + intraCoreBuffers, + SmallVector &crossCoreInputValues, + SmallVector &crossCoreOutputValues, + SmallVector &intraCoreInputValues, + SmallVector &intraCoreOutputValues); + + int buildOutputGroups( + SmallVector &intraCoreOutputValues, + DenseMap>> + &intraCoreBuffers, + DenseMap &idxToVar, + SmallVector &outputGroups); + + void collectIntraCoreInputConditions( + OpBuilder &builder, Location loc, SmallVector &intraCoreInputValues, + DenseMap &idxToVar, SmallVector &conditions, + DenseSet &usedVarsSet, + DenseMap &varUpdateTypes); + + int collectIntraCoreOutputConditions( + OpBuilder &builder, Location loc, + DenseMap>> + &intraCoreBuffers, + SmallVector &intraCoreOutputValues, DenseMap &idxToVar, + SmallVector &conditions, DenseSet &usedVarsSet, + DenseMap &varUpdateTypes); + + // Build the ifOp variable mapping for the tensor iter_args + int buildTensorIterArgIfOpVarMap(Operation *loopOp); + + // Collect the consumption conditions of the tensor iter_args consumer + void collectTensorIterArgInputConditions( + OpBuilder &builder, Location loc, scf::IfOp ifOp, + SmallVector &conditions, DenseSet &usedVarsSet, + DenseMap &varUpdateTypes); + + // Collect tensor iter_args producer conditions + void collectTensorIterArgOutputConditions( + OpBuilder &builder, Location loc, scf::IfOp ifOp, + SmallVector &conditions, DenseSet &usedVarsSet, + DenseMap &varUpdateTypes); + + SmallVector buildNewIfResultTypes(scf::IfOp oldIfOp, bool hasCounter, + Value counter); + + void collectYieldOperands(Block &block, Operation *&yieldOp, + SmallVector &yieldOperands); + + void populateNewThenBlock(scf::IfOp newIfOp, Block &oldThenBlock, + Operation *oldThenYieldOp, + ArrayRef oldYieldOperands, + DenseMap &varUpdateTypes, + bool hasCounter, Value counter, Value step); + + void populateNewElseBlock(scf::IfOp newIfOp, scf::IfOp oldIfOp, + bool oldHasElse, bool hasCounter, Value counter); + + scf::IfOp + createNewIfOpWithBlocks(scf::IfOp oldIfOp, Value combinedCond, + DenseMap &varUpdateTypes, + bool hasCounter, Value counter, Value step); + + int setIntraCoreCondition( + ModuleOp module, scf::IfOp ifOp, + DenseMap>> + &intraCoreBuffers, + SmallVector &intraCoreInputIndices, + SmallVector &intraCoreOutputIndices, DenseMap &idxToVar, + DenseMap &varUpdateTypes, Value &intraCoreCond); + + void updateControlVarToLatestValue(scf::IfOp newIfOp, scf::IfOp oldIfOp, + bool hasCounter, Value counter); + + int updateForOpYield(scf::ForOp forOp); + + // Update after-region yield for while when control vars were rewritten. + int updateWhileOpYield(scf::WhileOp whileOp); + + // Dispatch yield update for scf.for / scf.while main_loop. + int updateLoopYield(Operation *loopOp); + + // loopOp is scf.for or scf.while main_loop. + int combineConditions(ModuleOp module, Value crossCoreCond, + Value intraCoreCond, Value flowOptCond, scf::IfOp ifOp, + Operation *loopOp, size_t &usedCounterNum, + DenseMap &varUpdateTypes); + + int setCrossCoreCondition( + SmallVector crossCoreInputValues, + SmallVector crossCoreOutputValues, + DenseMap>> + &crossCoreBuffers, + scf::IfOp ifOp, SmallVector> ssbufferPtrs, + Value &crossCoreCond); + + // Set the FlowOpt extra condition for the third if block in the DAG. + // Needs lb/ub/step from scf.for; scf.while leaves flowOptCond null. + int setFlowOptCondition(scf::IfOp currentIfOp, Operation *loopOp, + Value &flowOptCond); + + // Update DAG nodes after ifOp replacement + void updateDAGAfterIfOpReplacement(scf::IfOp oldIfOp, scf::IfOp newIfOp); + + // Helper function to get pointer based on core type + Value getSSBufferMemref(bool isAIC, int groupIdx, int ptrSetIdx, + DenseMap &precomputedPtrs, + ArrayRef> ssbufferPtrs); + + // Compute pointers for VECTOR core SSBuffer + std::optional> + computeVectorSSBufferMemrefs(OpBuilder &builder, Location loc, + Operation *scopeOp, + SmallVector crossCoreInputValues, + SmallVector crossCoreOutputValues); + + // Part 2: Add cross-core conditions + Value addCrossCoreConditions( + OpBuilder &builder, Location loc, SmallVector crossCoreInputValues, + SmallVector crossCoreOutputValues, + DenseMap>> + &crossCoreBuffers, + bool isAIC, Value zeroConst, DenseMap &precomputedPtrs, + SmallVector> ssbufferPtrs); + + // Part 3: Update control variables in then block + void updateCrossCoreControlVars(OpBuilder &builder, Location loc, + scf::IfOp ifOp, + SmallVector crossCoreInputValues, + SmallVector crossCoreOutputValues, + bool isAIC, Value oneConst, + DenseMap &precomputedPtrs, + SmallVector> ssbufferPtrs); + + DenseMap controlVarToLatestValue; + SmallVector currentUsedVars; + ControlFlowConditionInfo *info = nullptr; + // Record each ifOp as the variables that need to be controlled when it acts + // as a consumer or a producer + llvm::DenseMap tensorIterArgIfOpVars; + + template + auto createSsbufLoads(OpBuilder &builder, Location loc, bool isAIC, + int groupIdx, + DenseMap &vectorSSBufferMemrefs, + llvm::ArrayRef> ssbufferMemrefs, + FuncTy &&pred); +}; + +std::unique_ptr> createUpdateConditionInfoPass(); +} // namespace triton +} // namespace mlir +#endif // TRITON_ADAPTER_UPDATE_CONDITION_INFO_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopIterTimes.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopIterTimes.h new file mode 100644 index 0000000000..a13bb835f4 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopIterTimes.h @@ -0,0 +1,143 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_UPDATE_LOOP_ITER_TIMES_H +#define TRITON_ADAPTER_UPDATE_LOOP_ITER_TIMES_H + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" + +namespace mlir { +namespace triton { + +// Loop iteration times info +struct IterationTimesInfo { + int ifCount = 0; // Number of if operations + int requiredBuffers = 1; // Number of required buffers + int x = 1; // Number of producer buffers + SmallVector ifOpsInThisFor; // If operations in this loop +}; + +class UpdateLoopIterTimesPass + : public PassWrapper> { +public: + UpdateLoopIterTimesPass() = default; + void runOnOperation() override; + void setConditionInfo(ControlFlowConditionInfo *info) { this->info = info; } + +private: + ControlFlowConditionInfo *info = nullptr; + + int GetMainLoopIdToLoopOpMap(ModuleOp module, + DenseMap> &cmap, + DenseMap> &vmap); + + int ComputeMainLoopTimes(DenseMap> &loopMap, + DenseMap &infoMap); + + int UpdateForLoopIteration( + DenseMap> &cmap, + DenseMap> &vmap, + DenseMap &infoMap); + + int collectForOpsAndUpdateMax( + DenseMap> &map, int id, + SmallVector &allForOps, int &maxIfCount, + int &maxRequiredBuffers, int &maxX, + DenseMap &infoMap); + + int replaceForOpCounterInIfOps(); + + int UpdateWhileLoopCondition( + DenseMap> &mainLoopIdMap); + + // Calculate factor = requiredBuffers / x + std::pair calculateFactor(scf::ForOp forOp); + + // Calculate factor based on intra-core dependencies + // Iterates all dependencies and computes the maximum required buffer count + // Returns {maxRequiredBuffers, maxX} where: + // - maxRequiredBuffers: maximum (m - n + 1) across all dependencies + // - maxX: the x value corresponding to maxRequiredBuffers + // - returns {-1, -1} on error + std::pair calculateIntraDepsFactor( + SmallVector &ifOps, DenseMap &ifOpIndex, + DenseMap> &deps); + + // Calculate factor based on cross-core dependencies + // For cross-core deps: consumer is in current forOp, producer is in another + // mainloop with the same id but in a different scope (cube vs vector) + // Returns {maxRequiredBuffers, maxX} where: + // - maxRequiredBuffers: maximum (m - n + 1) across all dependencies + // - maxX: the x value corresponding to maxRequiredBuffers + // - returns {-1, -1} on error + std::pair + calculateCrossDepsFactor(scf::ForOp forOp, SmallVector &ifOps, + DenseMap &ifOpIndex, + ConsumerProducerMap &crossDeps); + + // Calculate factor based on iteration dependencies (tensor iter_args + // dependencies) For iter deps: consumer and producer are IfOps within the + // same forOp Returns {maxRequiredBuffers, maxX} where: + // - maxRequiredBuffers: maximum (m - n + 1) across all dependencies + // - maxX: the x value corresponding to maxRequiredBuffers + // - returns {-1, -1} on error + std::pair + calculateIterDepsFactor(scf::ForOp forOp, SmallVector &ifOps, + DenseMap &ifOpIndex); + + // Extend for loop iteration count + scf::ForOp extendForOpIterationCount(scf::ForOp oldForOp, int ifCount, + int requiredBuffers, int x, + IRMapping &mapper, + SmallVector &ifOpsInThisFor); + + Value computeNewLoopUpperBound(OpBuilder &builder, Location loc, + scf::ForOp forOp, int ifCount, + int requiredBuffers, int x); + + scf::ForOp cloneForOpWithNewUpperBound(OpBuilder &builder, Location loc, + scf::ForOp oldForOp, + Value newUpperBound, + IRMapping &mapper); + + int updateCntArgsAfterClone(scf::ForOp oldForOp, IRMapping &mapper, + SmallVector &ifOpsInThisFor); + + void updateMainLoopMaps(Operation *oldForOp, Operation *newForOp, + DenseMap> &cmap, + DenseMap> &vmap, + DenseMap &infoMap); +}; + +std::unique_ptr> createUpdateLoopIterTimesPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_UPDATE_LOOP_ITER_TIMES_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopOps.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopOps.h new file mode 100644 index 0000000000..3f87c5f6de --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopOps.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_SSBUF_UPDATE_LOOP_OPS_FOR_CONTROL_FLOW_H +#define TRITON_ASCEND_SSBUF_UPDATE_LOOP_OPS_FOR_CONTROL_FLOW_H +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" + +namespace mlir { +namespace triton { + +class UpdateLoopOpsPass + : public PassWrapper> { +public: + UpdateLoopOpsPass() = default; + + void runOnOperation() override; + + void setConditionInfo(ControlFlowConditionInfo *info) { this->info = info; } + + llvm::StringRef getArgument() const override { return "update-loop-ops"; } + +private: + LogicalResult + addBlockCountersAndInnerDepConds(ModuleOp module, + ControlFlowConditionInfo *info); + + LogicalResult deriveBlockCountersFromIfOps(ModuleOp module, + ControlFlowConditionInfo *info); + + LogicalResult insertInterCorePipeS(ModuleOp module); + + // Analyze tensor type iter_args dependencies in main_loop with ssbuffer.if + // ops + LogicalResult + analyzeTensorIterArgDependencies(ModuleOp module, + ControlFlowConditionInfo *info); + + ControlFlowConditionInfo *info = nullptr; +}; + +std::unique_ptr> createUpdateLoopOpsPass(); + +} // namespace triton +} // namespace mlir +#endif // TRITON_ASCEND_SSBUF_UPDATE_LOOP_OPS_FOR_CONTROL_FLOW_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h new file mode 100644 index 0000000000..076b7fde1d --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h @@ -0,0 +1,127 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_UTILS_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_UTILS_H + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +namespace mlir { + +// Collect all nested ops within an operation's regions +LogicalResult collectAllNestedOps(Operation *op, + llvm::DenseSet ®ionOps); + +// Group body ops of a main-loop op (scf.for or scf.while carrying +// ssbuffer.main_loop) by block_id. +LogicalResult +collectOpsByBlockId(Operation *op, + llvm::DenseMap> &blockOps); + +// Topological sort of operations based on operand dependencies +LogicalResult topologicalSort(llvm::DenseSet &ops, + llvm::DenseMap *opOrder, + SmallVectorImpl &sorted); + +LogicalResult topologicalSort(SmallVector &ops); + +// Get block_ids in order of appearance in the main-loop body (forOp body or +// whileOp after-region body). Returns empty if `op` is neither. +SmallVector getBlockIdsInOrder(Operation *op); + +// Count unique ssbuffer.if values inside a main-loop op (scf.for or scf.while +// carrying ssbuffer.main_loop), walking all nested ops. Returns 0 if none. +int countUniqueIfBlockIds(Operation *loopOp); + +// Get block_id of immediate child of main-loop (scf.for/scf.while carrying +// ssbuffer.main_loop) that contains op. For scf.while, "body" means the +// after-region block. +std::optional getLoopDirectChildBlockId(Operation *op); + +// Find the tcb group id that contains value v +int findTcbGroupId( + Value v, + llvm::DenseMap> &tightlyCoupledBufferGroups); + +// Set isCube/isVector based on the scope's tcore_type attribute +// Returns failure if scopeOp does not have tcore_type attribute +LogicalResult getScopeType(Operation *scopeOp, bool &isCube, bool &isVector); + +// Check if op is scf.if whose body only contains hivm.hir.sync_block_wait, +// hivm.hir.sync_block_set and hivm.fixpipe ops (excluding terminators). Returns +// false if op is not scf.if or contains any other op. +bool isIfOpWithOnlySyncOps(Operation *op); + +// Migrate ops from oldBlock to newBlock; replaceAllUsesWith on oldBlock's +// args to newBlock's args (same index). Used for both branches of scf.while +// (before/after) and for scf.for body replacement. +void migrateBody(Block *oldBlock, Block *newBlock); + +// Migrate both before and after regions of a scf.while op. Does not touch +// terminators — the caller is expected to build a new scf.condition and +// scf.yield in the new regions. +void migrateWhileBodies(scf::WhileOp oldWhileOp, scf::WhileOp newWhileOp); + +// Build new scf.yield at end of `newBlock`: copies oldBlock's yield operands, +// appends `extraYieldValues`, creates new scf::YieldOp, erases old yield. +LogicalResult buildNewYieldOp(Block *oldBlock, Block *newBlock, + Operation *newOp, + ArrayRef extraYieldValues); + +// Replace all uses of `oldOp`'s results with `newOp`'s matching results. +// No-op when `oldOp` is result-less. +void replaceOpResultUses(Operation *oldOp, Operation *newOp); + +// Build new scf.condition in `newWhileOp`'s before region. Condition preserved +// from `whileOp`; forwarded values = new before-block args (incl. extras). +void buildNewWhileCondition(scf::WhileOp whileOp, scf::WhileOp newWhileOp); + +// Creates a new scf.for with `extraInitArgs` appended to the original init +// args. Returns `oldForOp` unchanged when `extraInitArgs` is empty. +scf::ForOp createNewForOpWithExtras(scf::ForOp oldForOp, + ArrayRef extraInitArgs); + +// Creates a new scf.while with `extraInitArgs` appended to the original inits +// and empty before/after blocks. Returns `oldWhileOp` unchanged when empty. +scf::WhileOp createNewWhileOpWithExtras(scf::WhileOp oldWhileOp, + ArrayRef extraInitArgs); + +// Dispatches createNewForOpWithExtras / createNewWhileOpWithExtras by op type. +// Returns nullptr if `oldOp` is neither scf.for nor scf.while. +Operation *createMainLoopOpWithExtras(Operation *oldOp, + ArrayRef extraInitArgs); + +// Prints whileBlockArgMap (whileOp -> block_id -> (new_arg_idx -> old_arg_idx)) +// to the debug stream, gated by LLVM_DEBUG. `header` is logged once before the +// iteration. +void dumpWhileBlockArgMap(const triton::WhileBlockArgMap &map, + llvm::StringRef header); + +} // namespace mlir +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_UTILS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AddDynamicCVPipeline.h b/third_party/ascend/include/DynamicCVPipeline/AddDynamicCVPipeline.h new file mode 100644 index 0000000000..1fa13f41b8 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AddDynamicCVPipeline.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ADD_DYNAMIC_CVPIPELINE_PASSES_H +#define TRITON_ADAPTER_ADD_DYNAMIC_CVPIPELINE_PASSES_H + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/IR/DialectRegistry.h" +#include "mlir/Pass/Pass.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#define GEN_PASS_DECL_ADDDYNAMICCVPIPELINE +#include "ascend/include/DynamicCVPipeline/Passes.h.inc" + +using namespace mlir; + +#define GEN_PASS_DEF_ADDDYNAMICCVPIPELINE +#include "ascend/include/DynamicCVPipeline/Passes.h.inc" + +extern bool compileOn91095Flag; + +namespace mlir { +namespace triton { +std::unique_ptr> +createAddDynamicCVPipelinePass(const AddDynamicCVPipelineOptions &options = {}); +} // namespace triton +} // namespace mlir + +namespace { +using namespace mlir; +using namespace triton; + +class AddDynamicCVPipelinePass + : public ::impl::AddDynamicCVPipelineBase { +public: + using Base = ::impl::AddDynamicCVPipelineBase; + + explicit AddDynamicCVPipelinePass(const AddDynamicCVPipelineOptions &options); + void runOnOperation() override; + void getDependentDialects(DialectRegistry ®istry) const override; +}; + +} // namespace + +#endif // TRITON_ADAPTER_ADD_DYNAMIC_CVPIPELINE_PASSES_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache.h b/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache.h new file mode 100644 index 0000000000..23a06ca13d --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ALLOC_MULTI_CACHE_PASS_H +#define TRITON_ADAPTER_ALLOC_MULTI_CACHE_PASS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +// AllocMultiCachePass for allocating multi-cache +class AllocMultiCachePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AllocMultiCachePass) + + // Constructor + AllocMultiCachePass() = default; + + // Run the pass + void runOnOperation() override; +}; + +// Create the pass +std::unique_ptr> createAllocMultiCachePass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_ALLOC_MULTI_CACHE_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.h b/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.h new file mode 100644 index 0000000000..7bc8b02355 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ALLOC_MULTI_CACHE_ADD_MULTI_BUFFER_INNER_SCOPE_PASS_H +#define TRITON_ADAPTER_ALLOC_MULTI_CACHE_ADD_MULTI_BUFFER_INNER_SCOPE_PASS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +// AddMultiBufferInnerScopePass for adding multi-buffer optimization within +// inner scope +class AddMultiBufferInnerScopePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AddMultiBufferInnerScopePass) + + // Constructor + AddMultiBufferInnerScopePass() = default; + + // Pass argument + StringRef getArgument() const override { + return "add_multi_buffer_inner_scope"; + } + + // Dependent dialects + void getDependentDialects(DialectRegistry ®istry) const override; + + // Run the pass + void runOnOperation() override; +}; + +// Create the pass +std::unique_ptr> createAddMultiBufferInnerScopePass(); + +// Register the pass +void registerAddMultiBufferInnerScopePasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_ALLOC_MULTI_CACHE_ADD_MULTI_BUFFER_INNER_SCOPE_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.h b/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.h new file mode 100644 index 0000000000..3284ed8ac7 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.h @@ -0,0 +1,131 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ALLOC_MULTI_CACHE_ADD_MULTI_BUFFER_OUTER_SCOPE_PASS_H +#define TRITON_ADAPTER_ALLOC_MULTI_CACHE_ADD_MULTI_BUFFER_OUTER_SCOPE_PASS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/DialectRegistry.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallSet.h" + +namespace mlir { +namespace triton { + +// Data structures + +/// Buffer alloc pair: {allocOp, markOp} +struct BufferAllocPair { + Operation *allocOp = nullptr; + Operation *markOp = nullptr; +}; + +/// Transfer chain for one side. Tag+dataflow classification: +/// - writes a group buffer: sender +/// - first value-read of a group buffer: receiver head +struct TransferOpChain { + Operation *waitOp = nullptr; + Operation *transferOp = nullptr; + // Group-buffer operand of transferOp; remapped to the spare buffer on wrap + Value bufferOperand; + Operation *toTensorOp = + nullptr; // memref→tensor boundary op terminating the receiver chain + Operation *setOp = nullptr; +}; + +/// Buffer alloc info: {sender, receiver} +struct BufferAllocInfo { + BufferAllocPair sender; + BufferAllocPair receiver; +}; + +/// Extra sync info for sync ops whose parent does NOT have main_loop attribute +struct ExtraSyncInfo { + Operation *setOp = nullptr; + Operation *waitOp = nullptr; +}; + +/// Transfer chain info: {senderChain, receiverChain} +struct TransferChainInfo { + TransferOpChain sender; + TransferOpChain receiver; +}; + +/// Complete transfer group information +struct TransferGroupInfo { + int tid = -1; + int originalFlag = -1; + int outputFlag = -1; + bool isCtoV = false; // true=C→V, false=V→C + + BufferAllocPair senderBuf; + BufferAllocPair receiverBuf; + + TransferOpChain senderChain; + TransferOpChain receiverChain; + + // Input/output buffer values + Value senderInputBuffer; + Value senderOutputBuffer; + Value receiverInputBuffer; + Value receiverOutputBuffer; + + // TCB ID shared across all 4 buffers in the same transfer group + int tcbId = -1; + + // Extra sync positions for output flag synchronization + Operation *extraSyncSetOp = nullptr; + Operation *extraSyncWaitOp = nullptr; +}; + +/// AddMultiBufferOuterScopePass for adding outer (CV inter-core) multi-buffer +/// optimization +class AddMultiBufferOuterScopePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AddMultiBufferOuterScopePass) + + // Constructor + AddMultiBufferOuterScopePass() = default; + + // Pass argument + StringRef getArgument() const override { + return "add_multi_buffer_outer_scope"; + } + + // Run the pass + void runOnOperation() override; + + // Get dependent dialects + void getDependentDialects(DialectRegistry ®istry) const override; +}; + +std::unique_ptr> createAddMultiBufferOuterScopePass(); + +void registerAddMultiBufferOuterScopePasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_ALLOC_MULTI_CACHE_ADD_MULTI_BUFFER_OUTER_SCOPE_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h b/third_party/ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h new file mode 100644 index 0000000000..5d46c90687 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h @@ -0,0 +1,189 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_ANALYZE_DATAFLOW_H +#define TRITON_ASCEND_ANALYZE_DATAFLOW_H + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +// Pass for analyzing tensor args in main_loop forOps +class AnalyzeArgsPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AnalyzeArgsPass) + + AnalyzeArgsPass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const override { return "analyze-args"; } + llvm::StringRef getDescription() const override { + return "Analyze tensor args in main_loop forOps"; + } +}; + +// Pass for analyzing function names +class AnalyzeNamePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AnalyzeNamePass) + + AnalyzeNamePass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const override { return "analyze-name"; } + llvm::StringRef getDescription() const override { + return "Analyze function names for dynamic CV pipeline"; + } +}; + +// Wrapper pass for AnalyzeDataFlow +class AnalyzeDataFlowPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AnalyzeDataFlowPass) + + AnalyzeDataFlowPass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const override { return "analyze-data-flow"; } + llvm::StringRef getDescription() const override { + return "Analyze data flow and detect tensor args in different block_ids"; + } +}; + +// Pass for analyzing flag operations +class AnalyzeFlagPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AnalyzeFlagPass) + + AnalyzeFlagPass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const override { return "analyze-flag"; } + llvm::StringRef getDescription() const override { + return "Analyze flag operations for control flow optimization"; + } +}; + +// Pass for analyzing scope-level issues +class AnalyzeScopePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AnalyzeScopePass) + + AnalyzeScopePass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const override { return "analyze-scope"; } + llvm::StringRef getDescription() const override { + return "Analyze scope-level issues for control flow"; + } +}; + +// Pass for analyzing whether an scf.while condition depends on non-scalar +// (tensor) data through its loop-carried arg update chain +class AnalyzeWhileConditionArgsPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AnalyzeWhileConditionArgsPass) + + AnalyzeWhileConditionArgsPass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const override { + return "analyze-while-condition-args"; + } + llvm::StringRef getDescription() const override { + return "Analyze whether scf.while condition args are updated with " + "non-scalar data"; + } +}; + +// Pass for analyzing cube control flow input chain +class AnalyzeCubeControlFlowInputChainPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID( + AnalyzeCubeControlFlowInputChainPass) + + AnalyzeCubeControlFlowInputChainPass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const override { + return "analyze-cube-control-flow-input-chain"; + } + llvm::StringRef getDescription() const override { + return "Analyze cube control flow input chain"; + } +}; + +// Pass for analyzing split-if scenarios. Rolls back the dynamic CV pipeline +// when a split-if is the only meaningful compute in both the VECTOR and CUBE +// main loops (i.e. the split-if enables no cube/vector overlap), and the same +// ssbuffer.splitted_if tag set appears on both cores. +class AnalyzeSplitIfPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AnalyzeSplitIfPass) + + AnalyzeSplitIfPass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const override { return "analyze-split-if"; } + llvm::StringRef getDescription() const override { + return "Analyze split-if scenarios and roll back when split-if is the " + "only compute in both VECTOR and CUBE main loops"; + } +}; + +std::unique_ptr> createAnalyzeArgsPass(); +std::unique_ptr> createAnalyzeFlagPass(); +std::unique_ptr> createAnalyzeNamePass(); +std::unique_ptr> +createAnalyzeCubeContolFLowInputChainPass(); +std::unique_ptr> createAnalyzeDataFlowPass(); +std::unique_ptr> createAnalyzeScopePass(); +std::unique_ptr> createAnalyzeWhileConditionArgsPass(); +std::unique_ptr> createAnalyzeSplitIfPass(); + +void registerAnalyzeDataFlowPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ASCEND_ANALYZE_DATAFLOW_H diff --git a/third_party/ascend/include/DynamicCVPipeline/CMakeLists.txt b/third_party/ascend/include/DynamicCVPipeline/CMakeLists.txt new file mode 100644 index 0000000000..c2514cc0a2 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name AddDynamicCVPipeline) +add_public_tablegen_target(AddDynamicCVPipelinePassIncGen) diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/BlockDependencyGraph.h b/third_party/ascend/include/DynamicCVPipeline/Common/BlockDependencyGraph.h new file mode 100644 index 0000000000..85e175730d --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/BlockDependencyGraph.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_BLOCK_DEPENDENCY_GRAPH_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_BLOCK_DEPENDENCY_GRAPH_H + +#include "mlir/IR/Block.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include + +namespace mlir { +namespace CVPipeline { + +// Forward declarations +class MemoryDependenceGraph; +class ComputeBlockIdManager; + +struct BlockNode { + int blockId; + llvm::SmallVector ops; + bool isCube; + int depth; + + // Neighbour sets are stored directly on the node so that traversal and + // edge rewiring can avoid the extra hash lookup a separate map would + // require. Because each BlockNode is heap-allocated behind a unique_ptr, + // pointers stored here stay stable across insertions/erases in + // `BlockDependencyGraph::blockNodes`. + llvm::DenseSet predecessors; + llvm::DenseSet successors; +}; + +class BlockDependencyGraph { +public: + BlockDependencyGraph(Block *block, const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm); + + // Build block-level dependency graph (only includes dependencies within + // current MLIR Block) + llvm::LogicalResult buildGraph(); + + BlockNode *getBlockNode(int blockId); + llvm::SmallVector getPredecessors(BlockNode *node); + llvm::SmallVector getSuccessors(BlockNode *node); + + // computeDepth + void computeDepths(); + int getDepth(int blockId); + + // rebuildgraph + llvm::LogicalResult rebuildAfterMerge(BlockNode *target, BlockNode *source); + + // BlockNode objects are owned by unique_ptr so that the address of a + // BlockNode is stable across insertions/erases in the DenseMap. The + // pointers stored in each BlockNode's predecessor/successor sets + // therefore remain valid for the lifetime of the graph. + llvm::DenseMap> blockNodes; + +private: + Block *block; + const MemoryDependenceGraph &memGraph; + ComputeBlockIdManager &bm; + + void addEdge(BlockNode *from, BlockNode *to); + + bool isInCurrentBlock(Operation *op); + + // Recompute depths starting from `start` and propagating forward through + // its successor chain. Assumes predecessors of `start` already hold + // correct depths. + void recomputeDepthsFrom(BlockNode *start); +}; + +} // namespace CVPipeline +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_BLOCK_DEPENDENCY_GRAPH_H diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/BufferCountManager.h b/third_party/ascend/include/DynamicCVPipeline/Common/BufferCountManager.h new file mode 100644 index 0000000000..55e26350a4 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/BufferCountManager.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_DYNAMIC_CV_PIPELINE_ADDMULTIBUFFERCONTROL_BUFFER_COUNT_MANAGER_H +#define TRITON_DYNAMIC_CV_PIPELINE_ADDMULTIBUFFERCONTROL_BUFFER_COUNT_MANAGER_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/DenseMap.h" +#include + +namespace mlir { +namespace triton { + +class BufferCountManager { +public: + enum class DepType { IntraCore, InterCore, LoadStore }; + + explicit BufferCountManager(Operation *root); + explicit BufferCountManager(ModuleOp module); + + BufferCountManager(const BufferCountManager &) = delete; + BufferCountManager &operator=(const BufferCountManager &) = delete; + + void setBufferCount(DepType type, int count); + + void + buildBufferCountMap(llvm::DenseMap> &depValueMap, + llvm::DenseMap &bufferCountMap, DepType type); + + int getBufferCountByType(DepType type) const; + +private: + void initFromModule(); + ModuleOp module_; +}; + +} // namespace triton +} // namespace mlir + +#endif // TRITON_DYNAMIC_CV_PIPELINE_ADDMULTIBUFFERCONTROL_BUFFER_COUNT_MANAGER_H diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/CycleDetector.h b/third_party/ascend/include/DynamicCVPipeline/Common/CycleDetector.h new file mode 100644 index 0000000000..c9d9b07992 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/CycleDetector.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_DYNAMIC_CV_PIPELINE_COMMON_CYCLEDETECTOR_H +#define TRITON_DYNAMIC_CV_PIPELINE_COMMON_CYCLEDETECTOR_H + +#include "DynamicCVPipeline/Common/DependencyHelper.h" +#include "DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/IR/Operation.h" +#include "mlir/Support/LLVM.h" +namespace mlir::CVPipeline { + +class DependencyCycleDetector { + const DenseSet &group; + llvm::DenseSet visited; + const DependencyHelper depHelper; + ComputeBlockIdManager &bm; + Block *const block; + + bool detectCycleFrom(Operation *cur); + +public: + DependencyCycleDetector(Block *block, const DependencyHelper &depHelper, + llvm::DenseSet &group, + ComputeBlockIdManager &bm) + : block(block), depHelper(depHelper), group(group), bm(bm) {} + + bool detectCycle(); +}; + +} // namespace mlir::CVPipeline + +#endif diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/DependencyHelper.h b/third_party/ascend/include/DynamicCVPipeline/Common/DependencyHelper.h new file mode 100644 index 0000000000..e824a0bdc3 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/DependencyHelper.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_DYNAMIC_CV_PIPELINE_COMMON_DEPENDENCYHELPER_H +#define TRITON_DYNAMIC_CV_PIPELINE_COMMON_DEPENDENCYHELPER_H + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "mlir/IR/Operation.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/WalkResult.h" + +namespace mlir::CVPipeline { + +class DependencyHelper { +public: + enum class SourceMode { Default, AcrossIterArg }; + +private: + using CallbackFn = function_ref; + + mlir::WalkResult forEachUserImpl(mlir::Operation *op, CallbackFn pred) const; + + template + mlir::WalkResult forEachSourceImpl(mlir::Operation *op, + CallbackFn pred) const; + + template static auto makeWalkCallback(Fn &&pred) { + if constexpr (std::is_void_v>) { + return [pred = std::forward(pred)](mlir::Operation *op) mutable { + pred(op); + return mlir::WalkResult::advance(); + }; + } else { + return std::forward(pred); + } + } + + template + static auto mapToAncestorInBlock(mlir::Block *block, Fn &&pred) { + return [block, pred = std::forward(pred)](mlir::Operation *op) mutable { + if (auto *ancestor = block->findAncestorOpInBlock(*op)) { + return pred(ancestor); + } + if constexpr (std::is_same_v, + mlir::WalkResult>) { + return mlir::WalkResult::advance(); + } + }; + } + +public: + const MemoryDependenceGraph &memGraph; + + explicit DependencyHelper(const MemoryDependenceGraph &memGraph) + : memGraph(memGraph) {} + + template + mlir::WalkResult forEachUser(mlir::Operation *op, Fn &&pred) const { + return forEachUserImpl(op, makeWalkCallback(std::forward(pred))); + } + + template + mlir::WalkResult forEachSource(mlir::Operation *op, Fn &&pred) const { + return forEachSourceImpl(op, makeWalkCallback(std::forward(pred))); + } + + template + mlir::WalkResult forEachUserInSameBlock(mlir::Operation *op, + Fn &&pred) const { + return forEachUser( + op, mapToAncestorInBlock(op->getBlock(), std::forward(pred))); + } + + template + mlir::WalkResult forEachSourceInSameBlock(mlir::Operation *op, + Fn &&pred) const { + return forEachSource( + op, mapToAncestorInBlock(op->getBlock(), std::forward(pred))); + } +}; + +} // namespace mlir::CVPipeline + +#endif diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/FallbackHelper.h b/third_party/ascend/include/DynamicCVPipeline/Common/FallbackHelper.h new file mode 100644 index 0000000000..66c3ecc998 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/FallbackHelper.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_DYNAMIC_CV_PIPELINE_COMMON_FALLBACKHELPER_H +#define TRITON_DYNAMIC_CV_PIPELINE_COMMON_FALLBACKHELPER_H + +#include + +#include "llvm/ADT/STLExtras.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" + +namespace mlir::CVPipeline { + +class FallbackHelper { + struct OperationEraser { + void operator()(Operation *op) const { + if (op) { + op->erase(); + } + } + }; + + std::unique_ptr backup; + ModuleOp original; + +public: + FallbackHelper(ModuleOp original) + : backup(original->clone()), original(original) {} + + void restore() { + if (!backup) { + return; + } + original->setLoc(backup->getLoc()); + original->setAttrs(backup->getAttrs()); + if (original->getPropertiesStorageSize() != 0) { + original->copyProperties(backup->getPropertiesStorage()); + } + for (auto [oRegion, bRegion] : + llvm::zip(original->getRegions(), backup->getRegions())) { + oRegion.takeBody(bRegion); + } + } +}; + +} // namespace mlir::CVPipeline + +#endif diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/FlagIdManager.h b/third_party/ascend/include/DynamicCVPipeline/Common/FlagIdManager.h new file mode 100644 index 0000000000..1a5ebdded1 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/FlagIdManager.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_FLAG_ID_MANAGER_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_FLAG_ID_MANAGER_H + +#include "mlir/IR/BuiltinOps.h" +#include + +namespace mlir { +namespace triton { + +// Flag ID Manager for synchronization between cores +// Purpose: Globally scan and allocate unique flag_id for synchronization +class FlagIdManager { +public: + // Maximum flag count (hardware limitation) + static constexpr int MAX_FLAG_ID = 14; + static constexpr int MULTI_MAX_FLAG_ID = 7; + static constexpr int INVALID_FLAG_ID = -1; + + // Constructor: initialize with Module for scanning + FlagIdManager(ModuleOp module); + + // Acquire an available ID: try to reuse existing flags first (conservative + // analysis), if cannot reuse, then increment and allocate. insertionPoint: + // the position where sync operation will be inserted, used for linear + // comparison in reuse analysis. Can be nullptr. + int acquireId(); + + bool checkCurrentId(); + +private: + // Scan existing Flag IDs + // Traverse Module to find all existing flag_id to prevent duplicate + // allocation + void scanExistingFlags(ModuleOp module); + + // Currently allocated maximum ID + int64_t currentMaxId = 0; + + // Save module for reuse analysis + ModuleOp module; +}; + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_FLAG_ID_MANAGER_H diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h b/third_party/ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h new file mode 100644 index 0000000000..47a0de92d2 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h @@ -0,0 +1,122 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_MEMORY_EFFECTS_TRACKER_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_MEMORY_EFFECTS_TRACKER_H + +#include "DynamicCVPipeline/Common/SyncWall.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" + +namespace mlir { +namespace CVPipeline { +constexpr int INIT_SIZE = 4; + +class MemoryDependenceGraph { +public: + MemoryDependenceGraph(Operation *root, AliasAnalysis &aa); + + ArrayRef getMemDefs(Operation *op) const; + ArrayRef getMemUsers(Operation *op) const; + + ArrayRef getExecBefore(Operation *op) const; + ArrayRef getExecAfter(Operation *op) const; + + // Refine a frontOp -> backOp memory edge to the leaf front ops that cause it. + // Returns empty when no dependency is found. + SmallVector getRealDependency(Operation *frontOp, + Operation *backOp); + +private: + struct MemSlot { + Value memref; + Operation *lastWriter = nullptr; + Operation *dataSource = nullptr; + SmallPtrSet pendingReads; + explicit MemSlot(Value v) : memref(v) {} + }; + + struct Snapshot { + SmallVector states; + }; + + void analyzeOp(Operation *op); + void analyzeRegionsOf(Operation *op); + + SmallVector + collectOuterEffects(Operation *op, bool &unknown, bool recursive = true); + + SmallVector findAliasSlots(Value v); + ArrayRef + resolveAliasSlots(Value v, DenseMap> &cache); + MemSlot *getOrCreateSlot(Value v); + + void collectPreds(ArrayRef effects, + bool unknown, SmallVectorImpl &defsOut, + SmallVectorImpl &predsOut); + + void applyEffects(Operation *op, + ArrayRef effects, + bool unknown); + + Snapshot takeSnapshot() const; + void restoreSnapshot(Snapshot &&snap); + + void recordEdges(Operation *op, ArrayRef defs, + ArrayRef preds); + AliasResult queryAlias(Value lhs, Value rhs); + + // True when a synchronization op sits strictly between @p a and @p b in the + // block they share. Memory edges that cross a sync are dropped so the graph + // never spans a barrier. + bool isSyncSeparated(Operation *a, Operation *b); + SyncWall &getWall(Block *block); + + Operation *root; + AliasAnalysis &aa; + + // Data dependency + DenseMap> memDefs; + DenseMap> memUsers; + + // Execute order + DenseMap> execBefore; + DenseMap> execAfter; + + SmallVector> slots; + DenseMap valueToSlot; + + // Per-block sync walls + DenseMap walls; +}; + +} // namespace CVPipeline +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_MEMORY_EFFECTS_TRACKER_H diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/SSBufferManager.h b/third_party/ascend/include/DynamicCVPipeline/Common/SSBufferManager.h new file mode 100644 index 0000000000..a8b04ce0c8 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/SSBufferManager.h @@ -0,0 +1,124 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_SSBUFFER_MANAGER_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_SSBUFFER_MANAGER_H + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Types.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/DenseMap.h" +#include + +namespace mlir { +namespace triton { + +static constexpr llvm::StringRef kMemrefExtVolatile = "memref_ext.volatile"; + +// SSBuffer Manager for managing SSBuffer address allocation and type tracking +// Purpose: Globally manage SSBuffer addresses across the entire pass pipeline +// This class maintains a single mapping table: Value -> address (int64_t) +// Type information is retrieved from the Value itself +class SSBufferManager { +public: + // SSBuffer address space and constants + static constexpr int ADDR_INT_TYPE = 64; + static constexpr int SSBUF_BASE_ADDR = 2048; // Base address for SSBuffer + static constexpr int SSBUF_ADDR_OFFSET = + 8; // Address offset for each allocation + static constexpr int SSBUF_ADDR_MAX = 6072; // Maximum allowed address + + // Constructor + SSBufferManager() = default; + + std::optional allocateAddr(Value value); + + std::optional> findValueByAddr(int64_t addr); + + std::optional + writeToSSBuffer(Value value, OpBuilder &builder, + SmallVectorImpl &createdOps); + + std::optional + readFromSSBuffer(int64_t addr, OpBuilder &builder, + SmallVectorImpl &createdOps); + + // Get the number of allocated addresses + size_t getAllocatedCount() const { return valueToAddrMap.size(); } + + // Clear all mappings (for testing or reset) + void clear() { + valueToAddrMap.clear(); + addrToValueMap.clear(); + } + +private: + static bool isScalarType(Type type); + + // Forward mapping: Value -> address (int64_t) + // Used for address allocation and reuse + llvm::DenseMap valueToAddrMap; + + // Reverse mapping: address (int64_t) -> Value + // Used for fast lookup when reading from SSBuffer + // This avoids O(n) traversal in findValueByAddr + llvm::DenseMap addrToValueMap; +}; + +static constexpr int ADDR_INT_TYPE = 64; +static constexpr int CONST_INT_TYPE = 32; + +inline MemRefType getSsbufMemrefType(Builder &builder) { + auto i32Type = builder.getIntegerType(CONST_INT_TYPE); + auto addressSpaceAttr = + builder.getAttr(hivm::AddressSpace::SSBUF); + return MemRefType::get({}, i32Type, nullptr, addressSpaceAttr); +} + +inline std::pair +getSsbufConstAndPointerCast(OpBuilder &builder, Location loc, uint64_t addr, + Type elemType) { + auto i64Type = builder.getIntegerType(ADDR_INT_TYPE); + auto addrAttr = builder.getIntegerAttr(i64Type, addr); + auto addrConst = builder.create(loc, i64Type, addrAttr); + auto addressSpaceAttr = + builder.getAttr(hivm::AddressSpace::SSBUF); + auto memrefType = MemRefType::get({}, elemType, nullptr, addressSpaceAttr); + + return {addrConst, builder.create( + loc, memrefType, addrConst.getResult())}; +} + +inline hivm::PointerCastOp createPointerCastOp(OpBuilder &builder, Location loc, + uint64_t addr) { + return getSsbufConstAndPointerCast(builder, loc, addr, + builder.getIntegerType(CONST_INT_TYPE)) + .second; +} + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_SSBUFFER_MANAGER_H diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/SyncWall.h b/third_party/ascend/include/DynamicCVPipeline/Common/SyncWall.h new file mode 100644 index 0000000000..4e302009e6 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/SyncWall.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_SYNC_WALL_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_SYNC_WALL_H + +#include "mlir/IR/Block.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" + +namespace mlir { +namespace CVPipeline { + +class SyncWall { +public: + explicit SyncWall(Block *block); + + // Source-order position of a block-level op inside the block (0-based). + // Ops not owned by the block report position 0. + unsigned positionOf(Operation *op) const; + + // True iff a synchronization op is strictly between @p a and @p b in the + // block's linear source order. + bool hasSyncBetween(Operation *a, Operation *b) const; + + // Number of synchronization ops strictly preceding @p op in source order. + // Ops separated by at least one sync belong to different segments. + unsigned segmentOf(Operation *op) const; + + bool sameSegment(Operation *a, Operation *b) const; + +private: + llvm::DenseMap ordinal; + llvm::DenseSet syncPositions; +}; + +} // namespace CVPipeline +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMMON_SYNC_WALL_H diff --git a/third_party/ascend/include/DynamicCVPipeline/Common/Utils.h b/third_party/ascend/include/DynamicCVPipeline/Common/Utils.h new file mode 100644 index 0000000000..6a705f6f70 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Common/Utils.h @@ -0,0 +1,343 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef ADD_AUTO_SCHEDULING_COMMON_UTILS_H +#define ADD_AUTO_SCHEDULING_COMMON_UTILS_H +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Casting.h" +#include +#include +#include + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" + +namespace mlir { +namespace CVPipeline { + +inline constexpr llvm::StringLiteral kCoreType = "ssbuffer.core_type"; +inline constexpr llvm::StringLiteral kBlockId = "ssbuffer.block_id"; +inline constexpr llvm::StringLiteral kExternalSync = "ssbuffer.external_sync"; +inline constexpr llvm::StringLiteral kTransferId = "ssbuffer.transfer_id"; +inline constexpr llvm::StringLiteral kCubeFirst = "ssbuffer.cube_first"; +inline constexpr llvm::StringLiteral kVectorFirst = "ssbuffer.vector_first"; +inline constexpr llvm::StringLiteral kAddFromMatmul = + "ssbuffer.add_from_matmul"; +inline constexpr llvm::StringLiteral kMainLoop = "ssbuffer.main_loop"; +inline constexpr llvm::StringLiteral kTcoreType = "hivm.tcore_type"; +inline constexpr llvm::StringLiteral kIf = "ssbuffer.if"; +inline constexpr llvm::StringLiteral kSplittedIf = "ssbuffer.splitted_if"; +inline constexpr llvm::StringLiteral kIntraBuffer = "ssbuffer.intra_buffer"; +inline constexpr llvm::StringLiteral kIntraBufCount = + "ssbuffer.intra_buf_count"; +inline constexpr llvm::StringLiteral kInterCoreBufCount = + "ssbuffer.inter_core_buf_count"; +inline constexpr llvm::StringLiteral kLoadStoreBufCount = + "ssbuffer.load_store_buf_count"; +inline constexpr llvm::StringLiteral kAnalyzeFlagId = + "ssbuffer.analyze_flag_id"; +inline constexpr llvm::StringLiteral kLoopId = "ssbuffer.loop_id"; +inline constexpr llvm::StringLiteral kLoopCarriedL0C = + "ssbuffer.loop_carried_l0c"; +inline constexpr llvm::StringLiteral kCrossCoreDeps = "ssbuffer.crossCoreDeps"; +inline constexpr llvm::StringLiteral kIntraDeps = "ssbuffer.intraDeps"; +inline constexpr llvm::StringLiteral kMemCrossDeps = "ssbuffer.memCrossDeps"; +inline constexpr llvm::StringLiteral kDepMark = "ssbuffer.dep_mark"; +inline constexpr llvm::StringLiteral kMayNotExec = "ssbuffer.may_not_exec"; +inline constexpr llvm::StringLiteral kMayNotExecNPU = "may_not_exec"; +inline constexpr llvm::StringLiteral kIterCounter = "ssbuffer.iterCounter"; +inline constexpr llvm::StringLiteral kForMayNotExec = + "ssbuffer.for_may_not_exec"; +inline constexpr llvm::StringLiteral kClone = "ssbuffer.clone"; +inline constexpr llvm::StringLiteral kInsertionOptimization = + "ssbuffer.insertionOptimization"; +inline constexpr llvm::StringLiteral kArg = "ssbuffer.arg"; +inline constexpr llvm::StringLiteral kWhileArg = "ssbuffer.while_arg"; +static constexpr llvm::StringLiteral kInlinableQuantScaleAttr = + "enable_fast_tf32_mul"; +inline constexpr llvm::StringLiteral kGMLoadMultiBufferHintAttr = "gm_load"; +inline constexpr llvm::StringLiteral kGMLoadHintAttr = "gm_load_hint"; +inline constexpr llvm::StringLiteral kHIVMMatmulLimitedInCubeAttr = + "hivm.matmul_limited_in_cube"; +inline constexpr llvm::StringLiteral kTightlyCoupledBufferAttr = + "hivm.tightly_coupled_buffer"; +inline constexpr llvm::StringLiteral kCoreTypeCube = "CUBE"; +inline constexpr llvm::StringLiteral kCoreTypeVector = "VECTOR"; +inline constexpr llvm::StringLiteral kFromMakeRange = "tt.from_make_range"; +inline constexpr llvm::StringLiteral kSubBlock = "ssbuffer.subBlock"; +inline constexpr llvm::StringLiteral kMergeComputeBlockApplied = + "ssbuffer.merge_compute_block_applied"; +inline constexpr llvm::StringLiteral kMergeSmallBlockFirstRunDone = + "ssbuffer.merge_small_block_first_run_done"; + +inline constexpr const char *ERRCODE_ATTR = + "triton_ascend.dynamic_cv_pipeline.rc"; +static constexpr const int ERRCODE_FAILED = 1; +static constexpr const int ERRCODE_IGNORED = 2; +static constexpr const int ERRCODE_TUPLE_PRELOAD_FAILED = 3; +constexpr int64_t CACHE_TABLE_BUFFER_SIZE = 4096; +constexpr int64_t BYTE_SIZE = 8; +static constexpr int crossCoreProducerId = 1; +static constexpr int crossCoreConsumerId = 0; + +enum CoreType { + UNDETERMINED = 0, + VECTOR_ONLY = 1 << 0, + CUBE_ONLY = 1 << 1, + CUBE_AND_VECTOR = VECTOR_ONLY | CUBE_ONLY, +}; + +inline constexpr CoreType fromStrCoreType(std::string_view s) { + if (s == "VECTOR") { + return CoreType::VECTOR_ONLY; + } + if (s == "CUBE") { + return CoreType::CUBE_ONLY; + } + + return CoreType::UNDETERMINED; +} + +void setEnableUBRefineOpt(bool enable); +bool isUBRefineOptEnabled(); + +// Functions for managing core types +CoreType getOpCoreType(Operation *op); +std::optional getOpBlockId(Operation *op); +llvm::LogicalResult verifyOpBlockId(Operation *op); +int getAvailableBlockId(ModuleOp module); +void setFallbackAttr(ModuleOp module, int errorCode); +bool hasFallbackAttr(ModuleOp module); +bool isScfOp(Operation *op); +bool isOnlyDirectlyUse(Operation *preOp, Operation *nextOp, + const CVPipeline::MemoryDependenceGraph &memGraph); +bool isSyncOp(Operation *op); +bool isExternalSyncOp(Operation *op); + +void setSubBlockId(Operation *op, int subBlockId); + +// Wrapper around a "main loop" — either scf.for or scf.while carrying the +// ssbuffer.main_loop attribute. Lets downstream code treat both uniformly. +class MainLoop { +public: + Operation *op = nullptr; + Block *body = nullptr; + Value iterCounter; + + Block *getBody() const; + Operation *getOperation() const; + MLIRContext *getContext() const; + Location getLoc() const; + Block *getBlock() const; + Block::iterator getIterator() const; + Operation *operator->() const; + bool isWhile() const; + + // Iter args carried across loop iterations, as BlockArguments. + // forOp: getRegionIterArgs(). + // whileOp: after-body args. + SmallVector getIterArgs() const; + + // Only meaningful for whileOp (before-body args); forOp returns empty. + // Same count/types as getIterArgs() on whileOp, distinct Value identity. + SmallVector getBeforeIterArgs() const; + + explicit MainLoop(Operation *loopOp); + + // Returns the scf.yield terminator of a forOp's body / whileOp's after + // body. Returns {} if `loopOp` is neither. + static scf::YieldOp getLoopYieldOp(Operation *loopOp); +}; + +// True when `op` is a main_loop loop op (forOp / whileOp carrying the tag). +inline bool isMainLoopOp(Operation *op) { + return op && isa(op) && op->hasAttr(kMainLoop); +} + +CoreType getCoreTypeOfSimpleOpOrCf(Operation *op); + +inline bool isVectorSimpleOpOrCf(Operation *op) { + return getCoreTypeOfSimpleOpOrCf(op) == CoreType::VECTOR_ONLY; +} + +// ============================================================================ +// Unified Loop Helpers: abstract ForOp/WhileOp differences +// ============================================================================ +// Get the body block of a loop (ForOp's body or WhileOp's after-body block) +inline Block *getLoopBodyBlock(Operation *loop) { + if (auto forOp = dyn_cast(loop)) + return forOp.getBody(); + if (auto whileOp = dyn_cast(loop)) + return whileOp.getAfterBody()->getNextNode(); + return nullptr; +} + +// Get the init values of a loop (ForOp's initArgs or WhileOp's inits) +inline ValueRange getLoopInitValues(Operation *loop) { + if (auto forOp = dyn_cast(loop)) + return forOp.getInitArgs(); + if (auto whileOp = dyn_cast(loop)) + return whileOp.getInits(); + return {}; +} + +// Get the yield terminator of a loop's body +inline Operation *getLoopYieldOp(Operation *loop) { + if (auto forOp = dyn_cast(loop)) + return forOp.getBody()->getTerminator(); + if (auto whileOp = dyn_cast(loop)) + return whileOp.getAfterBody()->getTerminator(); + return nullptr; +} + +// Check if a block argument is an iter_arg of a loop (ForOp body or WhileOp +// after-body) +inline bool isLoopIterArg(BlockArgument blockArg) { + Operation *parentOp = blockArg.getOwner()->getParentOp(); + if (isa(parentOp)) + return true; + if (auto whileOp = dyn_cast(parentOp)) + return blockArg.getOwner() == whileOp.getAfterBody()->getNextNode(); + return false; +} + +// Helper: Check if a value is a scalar (not a tensor type) +inline bool isScalarType(Value value) { + return !isa(value.getType()); +} + +// Helper: Check if a value is a scalar iter_arg from scf.for or scf.while +inline bool isScalarIterArgOp(Value iterArg) { + auto blockArg = dyn_cast(iterArg); + if (!blockArg) + return false; + if (!isLoopIterArg(blockArg)) + return false; + return isScalarType(iterArg); +} + +bool isVectorOnlyOp(Operation *op); + +bool isScalarLike(Value value); +bool isStoreLike(Operation *op); +bool isViewLike(Operation *op); + +// Returns true iff `v` is the result of a `linalg.fill` initialized with a +// 0 scalar constant. Used to detect the "add 0" operand of VECTOR pseudo-ops +// (`arith.addf` / `arith.addi` carrying `ssbuffer.add_from_matmul`). +bool isZeroFillValue(Value v); +bool isZeroAdd(mlir::Operation *op); + +// Read the `hivm.tightly_coupled_buffer` id attached to a `memref.alloc` +// via its `annotation.mark` user. Returns nullopt when no annotation with +// a concrete id is present, or when `allocVal` is null. +std::optional getTightlyCoupledBufferId(Value allocVal); + +// Walk back through opaque memref casts (`memref.memory_space_cast`, +// `memref.cast`) to recover the underlying `memref.alloc` that backs a +// `bufferization.to_tensor`'s source. Returns the input unchanged when no +// such cast is found. +Value traceBackToMemrefAlloc(Value v); +bool allResultHasOneUser(Operation *op); + +int64_t getBTSizeFromValidBroadcastOp(linalg::BroadcastOp broadcastOp); + +int getLoopCarriedArgIndex(Value operand, Block *block); + +// Returns the index of `v` in `iterArgs` when `v` is a tensor-type iter_arg, +// or -1 otherwise. +int getTensorIterArgIndex(Value v, ArrayRef iterArgs); + +// Helper: convert OpCoreType to string for IR attribute +inline llvm::StringRef coreTypeToString(CoreType ct) { + switch (ct) { + case CUBE_ONLY: + return "CUBE"; + case VECTOR_ONLY: + return "VECTOR"; + case CUBE_AND_VECTOR: + return "CUBE_AND_VECTOR"; + default: + return "UNDETERMINED"; + } +} + +CoreType getValueCoreType(Value value); + +inline OpOperand *getTiedYieldOperand(Value value, Block *block) { + int argIdx = getLoopCarriedArgIndex(value, block); + if (argIdx == -1) { + return nullptr; + } + auto *terminator = block->getTerminator(); + return &terminator->getOpOperand(argIdx); +} + +inline Operation *getLoopCarriedDefOp(Value value, Block *block) { + auto *yieldOperand = getTiedYieldOperand(value, block); + if (yieldOperand && yieldOperand->get()) { + return yieldOperand->get().getDefiningOp(); + } + return nullptr; +} + +inline bool isTensorComputeOp(Operation *op) { + if (auto linalgOp = dyn_cast(op)) { + if (linalg::isaCopyOpInterface(linalgOp)) + return false; + auto genericOp = dyn_cast(op); + if (genericOp && linalg::isaBroadcastOpInterface(genericOp).has_value()) + return false; + if (isa(op)) + return false; + return true; + } + + if (op->hasTrait()) { + return llvm::any_of(op->getResultTypes(), + [](Type t) { return isa(t); }); + } + + return false; +} + +// Determine FixpipePreQuantMode from a trunc op. +// Returns std::nullopt for scalars (non-shaped types) or unrecognized patterns. +// Supported patterns: +// - arith.truncf: f32 -> bf16, f32 -> f16 +// - arith.trunci: i32 -> i8 +std::optional +getFixpipePreQuantMode(Operation *truncOp); + +// Trace an operand's defining op back through C2C intermediate ops to find the +// underlying producing op. Returns null when the operand has no defining op. +Operation *getSourceThroughCIntermediateOps(Value operand); + +} // namespace CVPipeline +} // namespace mlir + +#endif diff --git a/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h b/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h new file mode 100644 index 0000000000..dcdcad0138 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMPUTE_BLOCK_OPT_COMMON_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMPUTE_BLOCK_OPT_COMMON_H + +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/IR/Operation.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "llvm/ADT/ArrayRef.h" + +namespace mlir { +namespace CVPipeline { + +/** + * @brief Detect if unifying a list of operations to target block_id would + * create a cycle + * + * This helper temporarily assigns every op in @p opsToUnify to @p + * targetBlockId, walks the SSA + memory dependency edges, and reports whether + * the resulting block-level dependency graph would contain a cycle. The + * temporary block_id assignments are always rolled back before returning, so + * the function leaves + * @p bm in its original state regardless of the result. + * + * Shared by the ComputeBlockOpt passes (e.g. UnifyAllocBlockPass and + * MergeVectorIfBlockPass) that merge operations into a common block_id. + * + * @param opsToUnify Block-level operations to add to the safe set (okSet) + * @param memGraph Memory dependence graph for RAW/WAW/WAR dependency analysis + * @param targetBlockId Target block_id after unification + * @param bm Block-id manager used to query/temporarily mutate block ids + * @return bool Returns true if unification would create a cycle, false + * otherwise + */ +bool willCreateCycle(llvm::ArrayRef opsToUnify, + const MemoryDependenceGraph &memGraph, int targetBlockId, + ComputeBlockIdManager &bm); + +/** + * @brief Clone scalar-producing ops shared between a pattern and other blocks + * + * Walks the pattern's scalar-producing ops in reverse topological order. If + * such an op's result is used by an op that is outside both the pattern and + * the target's original block (matchedOps[0]), clones the op and redirects + * those external uses to the clone, keeping the original for pattern-internal + * use. This prevents the cross-block dependency cycle that would otherwise + * appear after unifying the pattern ops into matchedOps[0]'s block_id. + * + * @param bmOriginal "Original" block_id view (before any fusion happens in the + * caller); used to decide whether a user is in a different + * block than the target (matchedOps[0]). + * @param matchedOps The op set of one pattern (target op first). + */ +void cloneScalarOpsForCrossBlockUses(ComputeBlockIdManager &bmOriginal, + SetVector &matchedOps, + int targetBlockId); + +/** + * @brief Check if a value originates from global memory (GM) and collect + * viewOps + * + * Traces back through nested view-like operations (subview, reinterpret_cast, + * etc.) to determine if the source is a function argument (block argument in + * the entry block). Only view-like operations in the same block as the input + * viewOp are collected. + * + * @param viewValue The value to check + * @param matchedOps SetVector to collect all same-block view-like operations in + * the trace + * @return bool Returns true if the source is from global memory (function + * argument), false otherwise + */ +bool collectViewOpsAndCheckGlobalMemory(Value viewValue, + SetVector &matchedOps); + +} // namespace CVPipeline +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMPUTE_BLOCK_OPT_COMMON_H diff --git a/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/MergeCubeBlockPass.h b/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/MergeCubeBlockPass.h new file mode 100644 index 0000000000..8b93561071 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/MergeCubeBlockPass.h @@ -0,0 +1,99 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_MERGE_CUBE_BLOCK_PASS_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_MERGE_CUBE_BLOCK_PASS_H + +#include "DynamicCVPipeline/Common/BlockDependencyGraph.h" +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" + +namespace mlir { +namespace CVPipeline { + +class MergeCubeBlockPass + : public PassWrapper> { +public: + MergeCubeBlockPass() = default; + + StringRef getArgument() const override { return "merge-cube-block"; } + StringRef getDescription() const override { + return "Merge cube blocks with same loaded data"; + } + + void runOnOperation() override; + +private: + llvm::LogicalResult processBlock(Block *block, + const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm, bool &mergedAny); + + bool canMergeBlocks(BlockNode *target, BlockNode *source, + BlockDependencyGraph &graph, + const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm); + + // Execute merge + llvm::LogicalResult mergeBlocks(BlockNode *target, BlockNode *source, + ComputeBlockIdManager &bm); + + // Perform iterative merging by repeatedly scanning the live cube blocks. + llvm::LogicalResult performMerging(BlockDependencyGraph &graph, + const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm, + bool &mergedAny); + + // Print graph structure + void printGraph(BlockDependencyGraph &graph); + + bool hasCommonInputOrOutput(BlockNode *node1, BlockNode *node2, + BlockDependencyGraph &graph); + bool hasSameDepth(BlockNode *node1, BlockNode *node2, + BlockDependencyGraph &graph); + bool checkNoCycle(BlockNode *node1, BlockNode *node2, + BlockDependencyGraph &graph, + const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm); + + // Return the subset of `blocks` whose type (cube/vector) differs from + // `currentNode`'s. Returned as a DenseSet so callers can do O(N) set + // intersection directly. + llvm::DenseSet + getBlocksOfDifferentType(BlockNode *currentNode, + llvm::ArrayRef blocks); +}; + +} // namespace CVPipeline +} // namespace mlir + +namespace mlir { +namespace triton { +std::unique_ptr> createMergeCubeBlockPass(); +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_MERGE_CUBE_BLOCK_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h b/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h new file mode 100644 index 0000000000..bf16134a61 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_BLOCK_ID_OPT_PASSES_H +#define TRITON_ADAPTER_BLOCK_ID_OPT_PASSES_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +std::unique_ptr> createUBUsageOptPass(); +std::unique_ptr> createMergeSameSourceAxisPass(); +void registerMergeSameSourceAxisPass(); +std::unique_ptr> createUnifyAllocBlockPass(); +void registerUnifyAllocBlockPass(); +std::unique_ptr> createMergeVectorIfBlockPass(); +void registerMergeVectorIfBlockPass(); +std::unique_ptr> createUnifyStoreBlockPass(); +void registerUnifyStoreBlockPass(); +std::unique_ptr> createFixpipeOptPass(); +std::unique_ptr> createMergeSmallBlockPass(); +void registerMergeSmallBlockPass(); +std::unique_ptr> createSinkI1ProducersIntoUsersPass(); +std::unique_ptr> createBroadcastUBOptPass(); +std::unique_ptr> createMoveLoadIntoUserPass(); +std::unique_ptr> createPosMaskPatternPass(); +std::unique_ptr> createRelocateMemrefDeclPass(); +std::unique_ptr> createMergeComputeBlockPass(); +std::unique_ptr> +createMergeInputInitSharedCubeBlockPass(); + +std::unique_ptr> createSplitIfByBlockIdPass(); +std::unique_ptr> createExpSubfPatternPass(); +std::unique_ptr> createExpLoadPatternPass(); +void registerSplitIfByBlockIdPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_BLOCK_ID_OPT_PASSES_H diff --git a/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOptPass.h b/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOptPass.h new file mode 100644 index 0000000000..f73e36732e --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/ComputeBlockOptPass.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_BLOCK_ID_OPT_PASS_H +#define TRITON_ADAPTER_BLOCK_ID_OPT_PASS_H + +#include "mlir/Dialect/Linalg/TransformOps/DialectExtension.h" +#include "mlir/Dialect/Linalg/Transforms/TilingInterfaceImpl.h" +#include "mlir/Dialect/SCF/TransformOps/SCFTransformOps.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class ComputeBlockOptPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(ComputeBlockOptPass) + + ComputeBlockOptPass() = default; + + void getDependentDialects(DialectRegistry ®istry) const override { + mlir::linalg::registerTransformDialectExtension(registry); + mlir::linalg::registerTilingInterfaceExternalModels(registry); + mlir::scf::registerTransformDialectExtension(registry); + } + + void runOnOperation() override; + + StringRef getArgument() const override { return "compute-block-opt"; } + + StringRef getDescription() const override { + return "Optimize block_id assignment and operation sequencing"; + } + +private: +}; + +std::unique_ptr> createComputeBlockOptPass(); + +void registerComputeBlockOptPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_BLOCK_ID_OPT_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadInternal.h b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadInternal.h new file mode 100644 index 0000000000..35f9ec192d --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadInternal.h @@ -0,0 +1,204 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_INTERNAL_H +#define TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_INTERNAL_H + +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadTypes.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Operation.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; + +// ============================================================================ +// Constants +// ============================================================================ + +static constexpr const char *DEBUG_TYPE = "add-multi-buffer-to-gm-load"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +static constexpr llvm::StringLiteral kBufferableMarker("gm_load_bufferable"); +static constexpr llvm::StringLiteral kBlockIdAttr("ssbuffer.block_id"); +static constexpr llvm::StringLiteral kLoadStoreAttr("ssbuffer.load_store"); +static constexpr llvm::StringLiteral + kGMLoadMultiBufferStage("gm-load multi-buffer"); +static constexpr unsigned kInitialRefCapacity = + 8; // Tuned for typical load chain depth +static constexpr unsigned kForInductionVarArgIndex = 0; +static constexpr unsigned kForBodyIterArgOffset = 1; +// Each load group carries `depth` slot flags plus producer/consumer counters. +// This count is independent of the configured buffer depth. +static constexpr int kLoadGroupCounterIterArgCount = 2; +static constexpr int kLoadGroupProducerCounterOffset = 0; +static constexpr int kLoadGroupConsumerCounterOffset = 1; + +// ============================================================================ +// Data structures (debug/utility only, types are in +// AddMultiBufferToGMLoadTypes.h) +// ============================================================================ + +namespace gmload { + +/// Reuses existing arith.constant ops found in the IR rather than creating +/// duplicates. Pre-populated by scanning a block; get-or-create semantics. +/// Suitable for untagged constants only (no ssbuffer.block_id attribution). +struct ConstantCache { + llvm::DenseMap table; + + void scan(Block *block) { + for (auto &op : *block) + if (auto cst = dyn_cast(op)) + table.try_emplace(cst.getValue(), cst.getResult()); + } + + Value get(OpBuilder &builder, Location loc, mlir::TypedAttr attr) { + auto [cacheIt, inserted] = table.try_emplace(attr, Value{}); + if (!inserted) + return cacheIt->second; + cacheIt->second = builder.create(loc, attr).getResult(); + return cacheIt->second; + } + + Value getFalse(OpBuilder &builder, Location loc) { + return get(builder, loc, builder.getBoolAttr(false)); + } + + Value getIndex(OpBuilder &builder, Location loc, int64_t value) { + return get(builder, loc, + cast(builder.getIndexAttr(value))); + } +}; + +// ============================================================================ +// Function declarations: Dependency analysis +// ============================================================================ + +Operation *getAncestorInBlock(Operation *op, Block *scopeBlock); +void backwardTrace(Operation *op, Block *scopeBlock, + llvm::SetVector &visited, + SmallVectorImpl &worklist); +void backwardTraceFromWorklist(llvm::SetVector &visited, + SmallVectorImpl &worklist, + Block *scopeBlock); +SmallVector +forwardTraceFromAllocs(llvm::SetVector &visited, + Block *scopeBlock); +void captureRegionFreeVars(llvm::SetVector &visited, + SmallVectorImpl &worklist, + Block *scopeBlock); +SmallVector computeLoadChain(Operation *markedOp, + Block *scopeBlock); +memref::AllocOp findBackingAlloc(Operation *markedOp, + ArrayRef chain); +SmallVector collectMarkedOps(ModuleOp module); +std::optional getConstantTripCount(scf::ForOp forOp); +void groupByEnclosingForOp(SmallVector &markedOps, + SmallVectorImpl &contexts); +void collectChainAndMarkedOps(const SmallVector &loads, + llvm::DenseSet &allChainOps, + llvm::DenseSet &markedOps); +bool isResultUsedExternally(Value result, Operation *yieldOp, + const llvm::DenseSet &allChainOps, + Block *forBody); +SmallVector +collectOperandsIncludingRegions(Operation *op); +llvm::DenseSet +computeSkipInConsumer(const SmallVector &loads, Block *forBody); + +// ============================================================================ +// Function declarations: Loop transformation utilities +// ============================================================================ + +bool isLoopInvariant(Value value, scf::ForOp forOp); +bool getLinearIterArgDelta(Value iterArg, Value yieldVal, scf::ForOp forOp, + Value &delta); +Value castIndexTo(OpBuilder &builder, Location loc, Value value, + Type targetType); +Value castToIndex(OpBuilder &builder, Location loc, Value value); +void tagWithBlockId(Value value, IntegerAttr blockId); +void allocateBufferSlots(OpBuilder &builder, Location loc, scf::ForOp forOp, + LoadGroup &group); +ExtendedForInfo buildExtendedFor(OpBuilder &builder, Location loc, + scf::ForOp forOp, ArrayRef groups, + ConstantCache &cache); +bool isDeadIterArg(scf::ForOp forOp, unsigned iterArgIdx); +FailureOr pruneDeadIterArgs(OpBuilder &builder, scf::ForOp forOp, + unsigned candidateCount); +void eraseDeadBodyOps(Block *body); +bool isAvailableForClone(Value value, Block *oldBody, const IRMapping &mapping); +bool areOperandsAvailableForClone(Operation *op, Block *oldBody, + const IRMapping &mapping); +std::optional getBlockId(Operation *op); +bool sameRunBlockId(const BodyRun &run, std::optional bid); +SmallVector collectBodyRuns(Block *body); +int findRunContainingOp(ArrayRef runs, Operation *target); +int findFirstRunWithBlockId(ArrayRef runs, int32_t blockId); +void logRepeatedTopLevelBlockRuns(Block *body, llvm::StringRef stage); +bool isAncestorBlock(Block *ancestor, Block *descendant); +void deduplicateConstants(ModuleOp module); +LogicalResult +applyMultiBufferToForLoop(ForBufferCtx &context, + const llvm::DenseSet &allCtxForOps); + +// ============================================================================ +// Function declarations: Consumer/Producer emission +// ============================================================================ + +Value emitLoadSlotSelection(OpBuilder &builder, Location loc, + const MarkedLoad &load, ArrayRef slotBufs, + Value target, int depth, IRMapping &mapping, + ArrayRef slotConsts); +Value emitSlotFlagClear(OpBuilder &builder, Location loc, Value target, + Value slotConst, Value flagNext, Value falseVal, + IntegerAttr blockId); +FailureOr> emitProducerFillForSlot( + OpBuilder &builder, Location loc, Block *oldBody, Block *newBody, + const LoadGroup &group, int slotIdx, Value flagArg, + Value currentProducerCounter, Value tripCount, Value falseVal, + Value loopLowerBound, Value loopStep, Value trueFlag, Value indexOne, + const SmallVector &iterArgDeltas, scf::ForOp origForOp); +LogicalResult emitMultiBufferLoopBody( + OpBuilder &builder, Location loc, SmallVectorImpl &groups, + ExtendedForInfo &info, const SmallVector &groupIndexOneVals, + const SmallVector> &allGroupSlotConsts, + const SmallVector &allGroupDepthConsts, + const llvm::DenseSet &allCtxForOps, Value tripCount, + Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &iterArgDeltas, scf::ForOp origForOp); + +} // namespace gmload + +#endif // TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_INTERNAL_H diff --git a/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadPass.h b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadPass.h new file mode 100644 index 0000000000..511b6141d9 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadPass.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_PASS_H +#define TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_PASS_H + +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadTypes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/DenseSet.h" + +namespace mlir { +namespace triton { + +// Rewrites GM load operations inside scf.for loops into a multi-buffer +// producer/consumer structure to overlap memory transfers with compute. +class AddMultiBufferToGMLoadPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AddMultiBufferToGMLoadPass) + + StringRef getArgument() const override { return "gm-load-multi-buffer"; } + void runOnOperation() override; + +private: + // Step 1: Collect marked ops and group by enclosing forOp + void collectAndGroupMarkedOps(); + + // Step 2: Sort contexts inner-first + void sortContextsInnerFirst(); + + // Step 3: Transform each for loop with multi-buffer logic + LogicalResult applyMultiBufferToGMLoadLoops(); + + // Step 4: Cleanup transformed IR + void cleanupTransformedIR(); + + // Shared state between steps + llvm::SmallVector markedOps_; + llvm::SmallVector contexts_; + llvm::DenseSet allCtxForOps_; +}; + +// Create the pass +std::unique_ptr> createAddMultiBufferToGMLoadPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadTypes.h b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadTypes.h new file mode 100644 index 0000000000..d55b6ef349 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadTypes.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_TYPES_H +#define TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_TYPES_H + +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/SmallVector.h" + +namespace gmload { + +// One marked op and the topo-sorted dependency chain needed to reproduce it. +struct MarkedLoad { + mlir::Operation *markedOp = nullptr; + llvm::SmallVector chain; + mlir::memref::AllocOp allocOp; +}; + +// Each marked load gets its own independent LoadGroup with its own +// producer/consumer counter pair. +struct LoadGroup { + llvm::SmallVector loads; + int depth = 0; + llvm::SmallVector> + bufSlots; // bufSlots[slot][load] + llvm::SmallVector mergedChain; +}; + +// Transformation context for one scf.for. +struct ForBufferCtx { + mlir::scf::ForOp forOp; + llvm::SmallVector groups; +}; + +// Iter-arg handles for one LoadGroup inside the extended scf.for. +struct GroupIterArgs { + llvm::SmallVector flagArgs; + mlir::Value prodCounter; + mlir::Value consCounter; +}; + +// Output of buildExtendedFor: new loop + per-group iter_arg handles. +struct ExtendedForInfo { + mlir::scf::ForOp newForOp; + mlir::Block *oldBody = nullptr; + mlir::Block *newBody = nullptr; + mlir::IRMapping mapping; + llvm::SmallVector groupArgs; + mlir::Value falseVal; + int numOrig = 0; + int depth = 0; +}; + +// One consecutive top-level run in an old loop body. +struct BodyRun { + bool hasBlockId = false; + int32_t blockId = 0; + llvm::SmallVector ops; +}; + +} // namespace gmload + +#endif // TRITON_ADAPTER_ADD_MULTI_BUFFER_TO_GMLOAD_TYPES_H diff --git a/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoistingPass.h b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoistingPass.h new file mode 100644 index 0000000000..f1a1aa6db1 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoistingPass.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ASYNC_LOAD_HOISTING_PASS_H +#define TRITON_ADAPTER_ASYNC_LOAD_HOISTING_PASS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class AsyncLoadHoistingPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AsyncLoadHoistingPass) + + AsyncLoadHoistingPass() = default; + + StringRef getArgument() const override { return "async-load-hoisting"; } + + void runOnOperation() override; +}; + +std::unique_ptr> createAsyncLoadHoistingPass(); +void registerAsyncLoadHoistingPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_ASYNC_LOAD_HOISTING_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/DecoupleComputeAndMemoryPass.h b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/DecoupleComputeAndMemoryPass.h new file mode 100644 index 0000000000..429f0dd712 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/DecoupleComputeAndMemoryPass.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DECOUPLE_COMPUTE_AND_MEMORY_PASS_H +#define TRITON_ADAPTER_DECOUPLE_COMPUTE_AND_MEMORY_PASS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +/// Sub-pipeline pass that decouples compute and memory operations. +class DecoupleComputeAndMemoryPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(DecoupleComputeAndMemoryPass) + + DecoupleComputeAndMemoryPass() = default; + + void runOnOperation() override; +}; + +std::unique_ptr> createDecoupleComputeAndMemoryPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DECOUPLE_COMPUTE_AND_MEMORY_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/Passes.h b/third_party/ascend/include/DynamicCVPipeline/Passes.h new file mode 100644 index 0000000000..a4a5a67b5d --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Passes.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PASSES_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PASSES_H + +#include "ascend/include/DynamicCVPipeline/PreCheckAvailable.h" +#include "ascend/include/DynamicCVPipeline/RemoveAttributes.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/MarkMainLoop.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/SeparateCVScope.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflowPass.h" +#include "ascend/include/DynamicCVPipeline/StandardizeOp.h" + +#include "AddDynamicCVPipeline.h" +#include "third_party/ascend/include/DynamicCVPipeline/ComputeBlockOptPass.h" +#include "third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/OpClassifier.h" +#include "third_party/ascend/include/DynamicCVPipeline/PlanComputeBlockPass.h" + +namespace mlir { +namespace triton { + +using namespace mlir; +#define GEN_PASS_REGISTRATION +#include "ascend/include/DynamicCVPipeline/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PASSES_H diff --git a/third_party/ascend/include/DynamicCVPipeline/Passes.td b/third_party/ascend/include/DynamicCVPipeline/Passes.td new file mode 100644 index 0000000000..5ba6e699e0 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/Passes.td @@ -0,0 +1,87 @@ +/* + * Copyright (c) Huawei Technologies Co. + * Licensed under the MIT license. + */ + +#ifndef TRITON_DYNAMIC_CV_PIPELINE_PASSES +#define TRITON_DYNAMIC_CV_PIPELINE_PASSES + +include "mlir/Pass/PassBase.td" + +def AddDynamicCVPipeline : Pass<"add_dynamic_cv_pipeline", "mlir::ModuleOp"> { + let summary = "Add dynamic cv pipeline based on ssbufer"; + let constructor = "triton::createAddDynamicCVPipelinePass()"; + let options = [ + Option<"compileOn91095", "compile-on-910-95", + "bool", /*default*/"false", + "compile on 910_95"> + ]; + let dependentDialects = ["hivm::HIVMDialect", "bufferization::BufferizationDialect", "scope::ScopeDialect", "annotation::AnnotationDialect", "LLVM::LLVMDialect"]; +} + +def PreCheckAvailablePass : Pass<"pre-check-dynamic-cv-pipeline-available", "mlir::ModuleOp"> { + let summary = "Check whether dynamic cv pipeline can process the input module"; + let constructor = "triton::createPreCheckAvailablePass()"; + let dependentDialects = ["scope::ScopeDialect"]; +} + +def OpClassifierPass : Pass<"op-classifier", "mlir::ModuleOp"> { + let summary = "Classify operations as CUBE or VECTOR"; + let constructor = "triton::createOpClassifierPass()"; +} + +def PlanComputeBlockPass : Pass<"plan-compute-block", "mlir::ModuleOp"> { + let summary = "Plan compute block"; + let constructor = "triton::createPlanComputeBlockPass()"; +} + +def AddBlockIdForControlOpsPass : Pass<"add-block-id-for-control-ops", "mlir::ModuleOp"> { + let summary = "Add block_id attribute to control flow operations"; + let constructor = "triton::createAddBlockIdForControlOpsPass()"; +} + +def DataDependencyAnalysisPass : Pass<"data-dependency-analysis", "mlir::ModuleOp"> { + let summary = "Analyze data dependencies between Vector and Cube blocks"; + let constructor = "triton::createDataDependencyAnalysisPass()"; +} + +def InterCoreTransferAndSyncPass : Pass<"inter-core-transfer-and-sync", "mlir::ModuleOp"> { + let summary = "Insert inter-core transfer operations and PIPE_S synchronization between Vector and Cube cores"; + let constructor = "triton::createInterCoreTransferAndSyncPass()"; + let dependentDialects = ["hivm::HIVMDialect", "bufferization::BufferizationDialect", "scope::ScopeDialect", "annotation::AnnotationDialect"]; +} + +def MarkMainLoopPass : Pass<"mark-main-loop", "mlir::ModuleOp"> { + let summary = "Mark main computation loops containing FixpipeOp or CopyOp with main_loop attribute"; + let constructor = "triton::createMarkMainLoopPass()"; + let dependentDialects = ["hivm::HIVMDialect", "bufferization::BufferizationDialect", "scope::ScopeDialect", "annotation::AnnotationDialect"]; +} + +def SeparateCVScopePass : Pass<"separate-cv-scope", "mlir::ModuleOp"> { + let summary = "separate ops into cv scope"; + let constructor = "triton::createSeparateCVScopePass()"; + let dependentDialects = ["hivm::HIVMDialect", "bufferization::BufferizationDialect", "scope::ScopeDialect", "annotation::AnnotationDialect"]; +} + +def PreserveControlAttrsCanonicalizePass : Pass<"preserve-control-attrs-canonicalize", "mlir::ModuleOp"> { + let summary = "Canonicalize while preserving selected control-flow attrs"; + let constructor = "triton::createPreserveControlAttrsCanonicalizePass()"; +} + +def ComputeBlockOptPass : Pass<"compute-block-id-opt", "mlir::ModuleOp"> { + let summary = "Block ID Optimization Pass"; + let constructor = "triton::createComputeBlockOptPass()"; +} + +def RemoveSsbufAttrPass : Pass<"remove-ssbuf-attr", "mlir::ModuleOp"> { + let summary = "Remove ssbuf attributes"; + let constructor = "triton::createRemoveSsbufAttrPass()"; +} + +def CheckUnsupportedScenarioPass : Pass<"ssbuf-check-unsupported-scenario", "mlir::ModuleOp"> { + let summary = "Check unsupported scenario pass for ssbuf"; + let constructor = "triton::createCheckUnsupportedScenarioPass()"; + let dependentDialects = ["memref::MemRefDialect"]; +} + +#endif // TRITON_DYNAMIC_CV_PIPELINE_PASSES diff --git a/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h new file mode 100644 index 0000000000..b3758bbf73 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_COMMON_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_COMMON_H + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLFunctionalExtras.h" + +#include "mlir/IR/Block.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" + +#include "ascend/include/DynamicCVPipeline/Common/DependencyHelper.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "llvm/ADT/DenseSet.h" + +namespace mlir { +namespace CVPipeline { + +Operation *getAncestorInBlock(Operation *inner, Block *block); +void initializeIndegreeForBlock(Block *block, + llvm::DenseMap &indegree, + const DependencyHelper &depHelper, + ComputeBlockIdManager &bm); + +} // namespace CVPipeline +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_COMMON_H diff --git a/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h new file mode 100644 index 0000000000..8c87d2fda2 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h @@ -0,0 +1,88 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_COMPUTE_BLOCK_ID_MANAGER_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_COMPUTE_BLOCK_ID_MANAGER_H + +#include + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/IR/Operation.h" + +#include "DynamicCVPipeline/Common/Utils.h" + +namespace mlir { +namespace CVPipeline { + +/** + * the class is to promise CUBEID and VECTORID are unified. + */ +class ComputeBlockIdManager { +public: + ComputeBlockIdManager(Operation *root); + bool isSameBlock(Operation *a, Operation *b); + bool isWholeCubeReady(Operation *seedOp, + llvm::DenseMap &indegree); + + llvm::LogicalResult markOpBlockId(Operation *op); + llvm::LogicalResult markOpsWithNewId(llvm::ArrayRef ops); + void updateBlockIdWithInner(Operation *parentOp, int targetId); + void updateBlockId(Operation *op, int blockId); + + void forgetOp(Operation *op); + + bool shouldInheritFromParent(Block *block, CoreType requiredCoreType) const; + llvm::LogicalResult inheritFromParent(Block *block); + + llvm::SmallVector getOpsByBlockId(int blockId) const; + llvm::ArrayRef getOpsRefByBlockId(int blockId) const; + + // Get operations that share the same block_id AND mlir block of op + llvm::SmallVector getOpsInSameBlock(Operation *op) const; + + std::optional getBlockIdByOpOpt(Operation *op) const; + int getNextId(); + + int getBlockIdByOp(Operation *op); + + ~ComputeBlockIdManager() = default; + ComputeBlockIdManager(const ComputeBlockIdManager &) = delete; + ComputeBlockIdManager &operator=(const ComputeBlockIdManager &) = delete; + ComputeBlockIdManager(ComputeBlockIdManager &&) = default; + ComputeBlockIdManager &operator=(ComputeBlockIdManager &&) = default; + +private: + int cntComputeBlockId = 0; + llvm::DenseMap> blockIdToOps; + llvm::DenseMap opToBlockId; + static constexpr int kBlockIdWidth = 32; + llvm::LogicalResult markAndRecord(Operation *op, int blockId); +}; + +} // namespace CVPipeline +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_COMPUTE_BLOCK_ID_MANAGER_H diff --git a/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/OpClassifier.h b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/OpClassifier.h new file mode 100644 index 0000000000..811e37a596 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/OpClassifier.h @@ -0,0 +1,208 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_OP_CLASSIFIER_H +#define TRITON_ADAPTER_OP_CLASSIFIER_H + +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Value.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassManager.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace mlir { +namespace triton { + +// Core type enumeration for operations +enum OpCoreType { + OP_UNDETERMINED = 0, + OP_CUBE_ONLY = 1, + OP_VECTOR_ONLY = 2, + OP_CUBE_AND_VECTOR = 3 +}; + +// OpClassifierPass for categorizing operations as CUBE or VECTOR +class OpClassifierPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OpClassifierPass) + + // Constructor + OpClassifierPass() = default; + + // Run the pass + void runOnOperation() override; + + // Return the pass argument name + static constexpr ::llvm::StringRef getArgumentName() { + return "op-classifier"; + } + ::llvm::StringRef getArgument() const override { return "op-classifier"; } + ::llvm::StringRef getDescription() const override { + return "Classify operations as CUBE or VECTOR for dynamic CV pipeline"; + } + ::llvm::StringRef getName() const override { return "OpClassifierPass"; } + +private: + llvm::DenseMap CloneOpMap; + + // Map from operation to its core type + llvm::DenseMap opCoreTypes; + + // Cache: value -> whether its defining chain reaches a VECTOR-only op. + // Memoizes hasVectorOnlyProducer during CUBE upstream propagation. + llvm::DenseMap vectorOnlyProducerCache; + + // All operations in the module + llvm::SmallVector allOps; + + // Seed operations for CUBE upstream propagation + llvm::SmallVector cubeSeeds; + // if A*B+C's C from broadcast chain, they need to keep for Normalize. + llvm::DenseSet inBroadcastChain; + + std::shared_ptr aliasAnalysis; + std::shared_ptr memDepGraph; + + // Mark an operation as CUBE + void markCube(Operation *op); + + // Mark synchronization Op + llvm::LogicalResult markSynchronizationOp(); + + // Pattern matching for CUBE operations + int patternMatchCUBE(); + + // Upstream pattern matching helpers + void matchToTensorPattern(Operation *def); + void matchTransposePattern(Operation *def); + void matchFillPattern(Operation *def); + void matchEmptyPattern(Operation *def); + void matchBroadcastPattern(Operation *def); + Value extractMmadBiasFromPotentialUnitDimExpand(Value bias); + + // Downstream pattern matching helpers + void matchStorePattern(Operation *user); + void matchExtractSlicePattern(Operation *user); + void matchMaterializePattern(Operation *user); + + // Propagate CUBE core type upstream + int propagateCubeUpstream(); + + // Propagate CUBE upstream for a specific operation + void propagateCubeUpstreamForOp(Operation *startOp); + + // Shared skip predicates for both CUBE upstream BFS paths. + bool shouldSkipCubeUpstream(Operation *op); + + // Helper: Handle fill op in scf.if - if all ops in scf.if are CUBE, mark + // scf.if and propagate upstream + void handleFillInScfIf(Operation *fillOp); + + // Get upstream operations based on both SSA and memory dependencies + void + getUpstreamOpsWithMemoryDeps(Operation *cur, + llvm::SmallVectorImpl &upstreamOps); + + // Step 3: Mark remaining operations as VECTOR + int markRemainingAsVector(); + + void markUpstreamsOfImplicitTranspose(); + + // Step 4: Propagate VECTOR core type upstream + int propagateVectorUpstream(); + + // Step 4.5: Penetrate CUBE coloring into pure loader scf.for loops whose + // results are consumed exclusively by CUBE ops. + llvm::LogicalResult penetrateCubeIntoForLoops(); + + // Helper: decide whether an scf.for or scf.while is a pure cube-loader loop + bool isCubeLoaderForOp(scf::ForOp forOp); + bool isCubeLoaderForWhileOp(scf::WhileOp whileOp); + + // Initialize the pass + void initializePass(ModuleOp module); + + // Get the core type of an operation + OpCoreType getCoreType(Operation *op) const; + OpCoreType getForInitCoreType(OpOperand *operand) const; + + // Set the core type of an operation + void setCoreType(Operation *op, OpCoreType coreType); + + // Helper: join core types into comma-separated string + std::string joinCoreTypes(const std::vector &coreTypes); + + // Helper: check if propagation should stop at this operation for yield + bool shouldStopPropagationForYield(Operation *op, OpCoreType targetCoreType); + + // Helper: propagate core_type upward for yield operand + void propagateCoreTypeUpwardForYield(Operation *startOp, + OpCoreType targetCoreType); + + // Step 5: Handle else region yield of scf.if by extracting core_type from + // then region yield + bool handleYieldFromElseRegion(std::vector &coreTypes, + unsigned operandIndex, + Operation *thenYieldForElse, Value &operand, + Operation *elseYieldOp); + + // Step 6: Handle CUBE_AND_VECTOR operations + int handleCubeAndVector(); + + // Helper: split CUBE_AND_VECTOR operation into CUBE and VECTOR versions + void splitOperationForCubeAndVector( + Operation *op, llvm::DenseSet &processedOps, + llvm::DenseMap &opToVectorClone); + + // Step 7: Process SCF yield results + int handleSCFYield(); + + // Helper: process a single yield operation + void processYieldOperation(Operation *op, Operation *thenYieldForElse); + + // Helper: Mark fill operations as CUBE when their output buffer is CUBE + void markFillOpsAsCube(); + + // Step 8: Stamp core type info to IR + int stampToIR(); + + // Helper: describe operation for logging + std::string describeOp(Operation *op) const; +}; + +// Create the pass +std::unique_ptr> createOpClassifierPass(); +void registerAddOpClassifierPasses(); +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_OP_CLASSIFIER_H diff --git a/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/Passes.h b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/Passes.h new file mode 100644 index 0000000000..e85d832821 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/Passes.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_PASSES_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_PASSES_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +std::unique_ptr> createPlanVectorBlockPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_PASSES_H diff --git a/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/PlanCubeBlockPass.h b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/PlanCubeBlockPass.h new file mode 100644 index 0000000000..6bd028cddf --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/PlanCubeBlockPass.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITION_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_PLAN_CUBE_BLOCK_PASS_H +#define TRITION_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_PLAN_CUBE_BLOCK_PASS_H + +#include + +#include "mlir/IR/BuiltinOps.h" + +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class PlanCubeBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PlanCubeBlockPass); + + PlanCubeBlockPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "plan-cube-block"; } +}; + +std::unique_ptr> createPlanCubeBlockPass(); +} // namespace triton +} // namespace mlir + +#endif // TRITION_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_PLAN_CUBE_BLOCK_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.h b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.h new file mode 100644 index 0000000000..bd5e191b1b --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITION_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_REORDER_OPS_BY_BLOCKID_H +#define TRITION_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_REORDER_OPS_BY_BLOCKID_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir::triton { + +class ReorderOpsByBlockIdPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(ReorderOpsByBlockIdPass); + + ReorderOpsByBlockIdPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { + return "reorder-ops-by-block-id"; + } +}; + +std::unique_ptr> createReorderOpsByBlockIdPass(); + +} // namespace mlir::triton + +#endif // TRITION_ADAPTER_DYNAMIC_CV_PIPELINE_PLAN_COMPUTE_BLOCK_REORDER_OPS_BY_BLOCKID_H diff --git a/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlockPass.h b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlockPass.h new file mode 100644 index 0000000000..5ef210d5c6 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/PlanComputeBlockPass.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMPUTE_BLOCK_PASS_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMPUTE_BLOCK_PASS_H + +#include "mlir/Dialect/Linalg/TransformOps/DialectExtension.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +// PlanComputeBlockPass for partitioning operations into compute blocks +class PlanComputeBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PlanComputeBlockPass) + + // Constructor + PlanComputeBlockPass() = default; + + // Run the pass + void runOnOperation() override; + + // Return the pass argument name + static constexpr ::llvm::StringRef getArgumentName() { + return "plan-compute-block"; + } + ::llvm::StringRef getArgument() const override { + return "plan-compute-block"; + } + ::llvm::StringRef getDescription() const override { + return "Plan compute blocks for for dynamic CV pipeline"; + } + ::llvm::StringRef getName() const override { return "PlanComputeBlockPass"; } +}; + +// Create the pass +std::unique_ptr> createPlanComputeBlockPass(); + +void registerPlanComputeBlockPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMPUTE_BLOCK_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/PreCheckAvailable.h b/third_party/ascend/include/DynamicCVPipeline/PreCheckAvailable.h new file mode 100644 index 0000000000..93e19a1859 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/PreCheckAvailable.h @@ -0,0 +1,114 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PRE_CHECK_AVAILABLE_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PRE_CHECK_AVAILABLE_H + +#include + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class PreCheckBlacklistPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PreCheckBlacklistPass) + + PreCheckBlacklistPass() = default; + + void runOnOperation() override; + void getDependentDialects(DialectRegistry ®istry) const override; + + ::llvm::StringRef getArgument() const override { + return "pre-check-blacklist"; + } + ::llvm::StringRef getDescription() const override { + return "Check whether the input module contains any blacklist operations"; + } +}; + +class PreCheckMatmul + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PreCheckMatmul) + + PreCheckMatmul() = default; + + void runOnOperation() override; + + ::llvm::StringRef getArgument() const override { return "pre-check-matmul"; } + ::llvm::StringRef getDescription() const override { + return "Check whether the input module contains matmul operations."; + } +}; + +class PreCheckAvailablePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PreCheckAvailablePass) + + PreCheckAvailablePass() = default; + + void runOnOperation() override; + + static constexpr ::llvm::StringRef getArgumentName() { + return "pre-check-dynamic-cv-pipeline-available"; + } + ::llvm::StringRef getArgument() const override { return getArgumentName(); } + ::llvm::StringRef getDescription() const override { + return "Check whether dynamic CV pipeline can process the input module"; + } + ::llvm::StringRef getName() const override { return "PreCheckAvailablePass"; } +}; + +class PreCheckDisablePreload + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PreCheckDisablePreload) + + PreCheckDisablePreload() = default; + + void runOnOperation() override; + + ::llvm::StringRef getArgument() const override { + return "pre-disable-tuple-preload"; + } + ::llvm::StringRef getDescription() const override { + return "Check whether the input module should disable the 3-preload " + "feature"; + } +}; + +std::unique_ptr> createPreCheckBlacklistPass(); +std::unique_ptr> createPreCheckMatmulPass(); +std::unique_ptr> createPreCheckAvailablePass(); +std::unique_ptr> createPreCheckDisablePreloadPass(); +void registerPreCheckAvailablePasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_PRE_CHECK_AVAILABLE_H diff --git a/third_party/ascend/include/DynamicCVPipeline/RemoveAttributes.h b/third_party/ascend/include/DynamicCVPipeline/RemoveAttributes.h new file mode 100644 index 0000000000..48e550ae63 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/RemoveAttributes.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_REMOVE_ATTRIBUTES_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_REMOVE_ATTRIBUTES_H + +#include + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class RemoveSsbufAttrPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(RemoveSsbufAttrPass); + + RemoveSsbufAttrPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "remove-ssbuf-attr"; } +}; + +std::unique_ptr> createRemoveSsbufAttrPass(); +void registerRemoveSsbufAttrPasses(); +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_REMOVE_ATTRIBUTES_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/MarkGMLoadPass.h b/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/MarkGMLoadPass.h new file mode 100644 index 0000000000..a34c0cdeae --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/MarkGMLoadPass.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_MARK_GM_LOAD_PASS_H +#define TRITON_ADAPTER_MARK_GM_LOAD_PASS_H + +#include + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +/// Sub-pass of SeparateMemoryFromComputePass that marks global memory load +/// operations to be decoupled from compute operations. +class MarkGMLoadPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MarkGMLoadPass) + + MarkGMLoadPass() = default; + + StringRef getArgument() const override { return "mark-gm-load"; } + + void getDependentDialects(DialectRegistry ®istry) const override; + + void runOnOperation() override; +}; + +std::unique_ptr> createMarkGMLoadPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_MARK_GM_LOAD_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.h b/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.h new file mode 100644 index 0000000000..3d64be4b89 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_UB_OVERFLOW_CHECKER_H +#define TRITON_ADAPTER_UB_OVERFLOW_CHECKER_H + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Types.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Support/LogicalResult.h" + +#include "llvm/ADT/SmallVector.h" + +#include +#include + +namespace mlir { +namespace triton { + +struct BufferInfo { + enum class Kind { + Annot, + Unannot, + }; + + Kind kind = Kind::Unannot; + memref::AllocOp allocOp = nullptr; + annotation::MarkOp markOp = nullptr; + int64_t originalSize = 0; + int64_t reducedSize = 0; + int64_t alignedSize = 0; + int64_t multiBufferCount = 1; + scf::ForOp forOp = nullptr; + bool fromHint = false; +}; + +struct TensorInfo { + tensor::EmptyOp emptyOp = nullptr; + int64_t originalSize = 0; + int64_t reducedSize = 0; + int64_t alignedSize = 0; +}; + +struct UBEstimateResult { + int64_t totalBits = 0; +}; + +namespace UBConstants { + +/// UB total space in bits (248 KB). +inline constexpr int64_t UB_SPACE_SIZE_BITS = 2031616; + +/// TileAndBindSubBlock halves dim0. +inline constexpr int64_t K_SUB_BLOCK_DIM = 2; + +/// EnableStrideAlign align-to-32-bytes. +inline constexpr int64_t STRIDE_ALIGN_BYTES = 32; + +/// 256-bit align-up unit. +inline constexpr int64_t ALIGN_UNIT_BITS = 256; + +} // namespace UBConstants + +int getAlignUnit(Type elementType); + +SmallVector collectBuffers(ModuleOp module); + +SmallVector collectTensorEmpties(ModuleOp module); + +void computeBufferSize(BufferInfo &buf); + +void computeTensorSize(TensorInfo &tensor); + +UBEstimateResult checkUBOverflow(ArrayRef buffers, + ArrayRef tensors); + +LogicalResult pruneMultiBufferMarks(ModuleOp module); + +class UBOverflowCheckerPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(UBOverflowCheckerPass) + + UBOverflowCheckerPass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "ub-overflow-check"; } + + llvm::StringRef getDescription() const final { + return "Estimate UB usage and prune multi_buffer marks if overflow " + "detected"; + } +}; + +std::unique_ptr> createUBOverflowCheckerPass(); +void registerUBOverflowCheckerPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_UB_OVERFLOW_CHECKER_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromComputePass.h b/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromComputePass.h new file mode 100644 index 0000000000..bb1d984f44 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SeparateMemoryFromComputePass.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_SEPARATE_MEMORY_FROM_COMPUTE_PASS_H +#define TRITON_ADAPTER_SEPARATE_MEMORY_FROM_COMPUTE_PASS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +/// Sub-pipeline pass that separates memory from compute operations. +class SeparateMemoryFromComputePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(SeparateMemoryFromComputePass) + + SeparateMemoryFromComputePass() = default; + + void runOnOperation() override; +}; + +std::unique_ptr> createSeparateMemoryFromComputePass(); + +void registerSeparateMemoryFromComputePasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_SEPARATE_MEMORY_FROM_COMPUTE_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.h new file mode 100644 index 0000000000..6206fb99a2 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ADD_BLOCK_ID_FOR_CONTROL_OPS_H +#define TRITON_ADAPTER_ADD_BLOCK_ID_FOR_CONTROL_OPS_H + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +// Define pass +// Pass for adding block_id attributes to control flow operations +class AddBlockIdForControlOpsPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AddBlockIdForControlOpsPass) + + AddBlockIdForControlOpsPass() = default; + + // Run the pass + void runOnOperation() override; + + static constexpr ::llvm::StringRef getArgumentName() { + return "add-block-id-for-control-ops"; + } + ::llvm::StringRef getArgument() const override { + return "add-block-id-for-control-ops"; + } + ::llvm::StringRef getDescription() const override { + return "Add block_id attribute to control flow operations"; + } + ::llvm::StringRef getName() const override { + return "AddBlockIdForControlOpsPass"; + } + +private: +}; + +std::unique_ptr> createAddBlockIdForControlOpsPass(); + +void registerAddBlockIdForControlOpsPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_ADD_BLOCK_ID_FOR_CONTROL_OPS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h new file mode 100644 index 0000000000..f04fd974e1 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h @@ -0,0 +1,208 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DATA_DEPENDENCY_ANALYSIS_H +#define TRITON_ADAPTER_DATA_DEPENDENCY_ANALYSIS_H + +#include "mlir/Analysis/DataFlowFramework.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace mlir { +namespace triton { +enum class DependencyType { VectorToCube, CubeToVector, CubeToCube }; + +struct BlockInfo { + int blockId; + bool isCube; + bool isControl; + llvm::SetVector inputs; + llvm::SmallVector outputs; + llvm::SmallVector Operations; +}; + +struct DependencyInfo { + DependencyType type; + mlir::Value value; + int producerBlockId; + int consumerBlockId; + int iniProducerBlockId; + int iniConsumerBlockId; + + bool isAllTranspoesd = false; + + // Optional Items for splitted if + bool isSplitedIf = false; + mlir::Value realValue; + + // Optional Items for memDependencies + mlir::Operation *predOp; + mlir::Operation *nextOp; + // Optional Items for iterarg yield dependency + mlir::Operation *consumerYieldOp = nullptr; + + mlir::OpOperand *operand = nullptr; +}; + +class DataDependencyInfo { +public: + explicit DataDependencyInfo(mlir::Operation *op) {} + + bool isValid() const { return valid; } + + // for MLIR Analysis framework + bool isInvalidated(const mlir::AnalysisManager::PreservedAnalyses &pa) { + return false; + } + + static bool classof(const DataDependencyInfo *info) { return true; } + + // get analyze result + llvm::DenseMap &getBlockInfoMap() { return blockInfoMap; } + llvm::SmallVector &getV2CDependencies() { + return v2cDependencies; + } + llvm::SmallVector &getC2VDependencies() { + return c2vDependencies; + } + llvm::SmallVector &getC2CDependencies() { + return c2cDependencies; + } + llvm::SmallVector &getMemoryDependencies() { + return memoryDependencies; + } + llvm::SmallVector &getIntraC2CDependencies() { + return intraC2CDependencies; + } + + void setValid(bool v) { valid = v; } + +private: + bool valid = false; + llvm::DenseMap blockInfoMap; + llvm::SmallVector v2cDependencies; + llvm::SmallVector c2vDependencies; + llvm::SmallVector c2cDependencies; + llvm::SmallVector memoryDependencies; + llvm::SmallVector intraC2CDependencies; +}; + +// Define pass +// Pass for analyzing data dependencies between Vector and Cube blocks +class DataDependencyAnalysisPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(DataDependencyAnalysisPass) + + DataDependencyAnalysisPass() = default; + + // Run the pass + void runOnOperation() override; + + static constexpr ::llvm::StringRef getArgumentName() { + return "data-dependency-analysis"; + } + ::llvm::StringRef getArgument() const override { + return "data-dependency-analysis"; + } + ::llvm::StringRef getDescription() const override { + return "Analyze data dependencies between Vector and Cube blocks"; + } + ::llvm::StringRef getName() const override { + return "DataDependencyAnalysisPass"; + } + +private: + void createBlockInfoMap(DataDependencyInfo &info); + void collectBlockInfo(DataDependencyInfo &info, int blockId, + llvm::SmallVector &ops); + mlir::Operation *createBlockInfoConstOp(OpBuilder &builder, Location loc, + llvm::StringRef coreType, + DataDependencyInfo &info); + + bool collectDepInfo(mlir::Value depvalue, DependencyType dependencyType, + llvm::SmallVector &dependencies, + int iniProdId, int iniConsId, DataDependencyInfo &info, + bool isAllTranspoesd = false); + void collectMemDepInfo(llvm::StringRef predCoreType, int producerBlockId, + int consumerBlockId, int predBlockId, int currBlockId, + llvm::SmallVector &memoryDependencies, + mlir::Operation *predOp, mlir::Operation *nextOp); + void analyzeExternalInputs(DataDependencyInfo &info); + void analyzeExternalOutputs(DataDependencyInfo &info); + + void analyzeInternalDeps(DataDependencyInfo &info); + + void analyzeMemoryEffect(DataDependencyInfo &info); + std::pair findCommonLevelBlockIds(DataDependencyInfo &info, + int producerBlockId, + int consumerBlockId); + + bool isControlFlowOp(mlir::Operation *op); + bool isCubeAndVectorOp(mlir::Operation *op); + bool isValidShapeForDependency(mlir::Value value); + bool isValidValueForDependency(mlir::Value value); + bool isValidScalarDependency(mlir::Value value); + bool isValid1DValueForDependency(mlir::Value value); + std::pair> + isAllTransposedInVector(mlir::Value value); + bool isOuterOpArg(mlir::Value value); + mlir::Value resolveNestedIterArgInitValue(mlir::Value initValue); + void processIterArgDependencies(); + llvm::SmallVector + collectDiffCoreTypeUsers(mlir::BlockArgument iterArg, + llvm::StringRef initCoreType); + void + insertProducerAndRecordDeps(mlir::LoopLikeOpInterface loopOp, + mlir::BlockArgument loopArg, + llvm::StringRef initCoreType, + llvm::SmallVector &diffUsers, + DataDependencyInfo &info); + void insertConsumerAndRecordDeps(mlir::LoopLikeOpInterface loopOp, + mlir::Value yieldedValue, int iterArgIndex, + llvm::StringRef initCoreType, + DataDependencyInfo &info); + void recordInitValueDeps(mlir::LoopLikeOpInterface loopOp, + mlir::Value initValue, llvm::StringRef yieldCoreType, + DataDependencyInfo &info); + void updateCoreTypeAtIndex(Operation *op, int index, + llvm::StringRef newCoreType); + void deduplicateDependencies(llvm::SmallVector &dependencies); + mlir::ModuleOp module; +}; + +std::unique_ptr> createDataDependencyAnalysisPass(); + +void registerDataDependencyAnalysisPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DATA_DEPENDENCY_ANALYSIS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/FlagIdReuse.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/FlagIdReuse.h new file mode 100644 index 0000000000..889050b00d --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/FlagIdReuse.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SPLIT_DATAFLOW_FLAG_ID_REUSE_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SPLIT_DATAFLOW_FLAG_ID_REUSE_H + +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/SmallVector.h" + +namespace mlir { +namespace triton { + +class FlagIdReuseManager { + +public: + FlagIdReuseManager() {} + + void insertRelationBetweenSetAndWait(Operation *before, Operation *after); + DenseMap + reuseInterCoreTransferFlagIds(const llvm::SmallVector &syncOps); + +private: + static int getFlagId(Operation *op); + bool hasPath(llvm::SmallSet &visited, + Operation *from, Operation *to); + void preworkForAnalyze(const llvm::SmallVector &syncOps); + + enum class SyncDir { VectorToCube, CubeToVector }; + SyncDir getFlagDirection(int flagId); + + // Lifecycle boundary of a flag group: earliest set (acquire) / latest wait + // (release). + Operation *getEarliestSet(int flagId); + Operation *getLatestWait(int flagId); + // p is provably ordered before q: same op, same-block program order, or + // reachable through the happens-before relations graph. + bool opPrecedes(Operation *p, Operation *q); + // `before`'s release point precedes `after`'s acquire point. + bool flagReleasedBefore(int before, int after); + // Two flags interfere iff neither is fully released before the other is + // acquired. + bool flagsInterfere(int lhs, int rhs); + // Greedy interference-graph coloring + compact renumber. Returns origFlagId + // -> newFlagId. + DenseMap colorInterferenceGraph(); + + DenseMap> relations; + DenseMap> flagIdToOps; + DenseMap opOrder; +}; + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SPLIT_DATAFLOW_FLAG_ID_REUSE_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.h new file mode 100644 index 0000000000..54059b096f --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.h @@ -0,0 +1,243 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_INTER_CORE_TRANSFER_AND_SYNC_H +#define TRITON_ADAPTER_INTER_CORE_TRANSFER_AND_SYNC_H + +#include "ascend/include/DynamicCVPipeline/Common/FlagIdManager.h" +#include "ascend/include/DynamicCVPipeline/Common/SSBufferManager.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/FlagIdReuse.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassManager.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include + +namespace mlir { +namespace triton { + +struct TransferPipeConfig { + hivm::PipeAttr forReadTPipe; + hivm::PipeAttr forReadPipe; + hivm::PipeAttr forWriteTPipe; + hivm::PipeAttr forWritePipe; + hivm::TCoreTypeAttr srcCoreAttr; + hivm::TCoreTypeAttr dstCoreAttr; + llvm::StringRef srcCoreType; + llvm::StringRef dstCoreType; +}; + +// Information found while matching a VECTOR to_tensor yielded from an SCF op. +struct VectorToTensorInfo { + bufferization::ToTensorOp toTensor; + scf::YieldOp yield; + int tightlyCoupledBufferId; +}; + +// Result of matching a CUBE -> VECTOR direct-store pattern: CUBE data is +// transferred through UB and directly stored by VECTOR, without VECTOR tensor +// computation. +struct CubeToVectorDirectStoreInfo { + std::optional tightlyCoupledBufferId; + Operation *storeOp = nullptr; +}; + +// Define pass +class InterCoreTransferAndSyncPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(InterCoreTransferAndSyncPass) + + InterCoreTransferAndSyncPass() = default; + + // Declare dependent dialects + void getDependentDialects(DialectRegistry ®istry) const override; + + // Run the pass + void runOnOperation() override; + + static constexpr ::llvm::StringRef getArgumentName() { + return "inter-core-transfer-and-sync"; + } + ::llvm::StringRef getArgument() const override { + return "inter-core-transfer-and-sync"; + } + ::llvm::StringRef getDescription() const override { + return "Insert inter-core transfer and synchronization operations between " + "Vector and Cube cores"; + } + ::llvm::StringRef getName() const override { + return "InterCoreTransferAndSyncPass"; + } + +private: + mlir::ModuleOp module; + int transferIndex = 0; + int markAllocIndex = 0; + int intraDepsGroupId = 0; + + llvm::DenseMap> loopInclusions; + llvm::DenseSet singleLoopSet; + + llvm::DenseMap ndnzValueMapping; + SSBufferManager ssbufferManager; + + void analyzeLoopInclusion(); + void moveStartEndSync(mlir::OpBuilder &builder); + + mlir::LogicalResult + processDependencies(FlagIdManager &flagManager, + FlagIdReuseManager &flagIdReuseManager); + mlir::LogicalResult + handleVectorToCube(mlir::OpBuilder &builder, DependencyInfo &dep, + FlagIdManager &flagManager, + FlagIdReuseManager &flagIdReuseManager); + mlir::LogicalResult + handleCubeToVector(mlir::OpBuilder &builder, DependencyInfo &dep, + FlagIdManager &flagManager, + FlagIdReuseManager &flagIdReuseManager); + mlir::LogicalResult handleCubeToCube(mlir::OpBuilder &builder, + DependencyInfo &dep); + mlir::LogicalResult handleMemoryDependency( + mlir::OpBuilder &builder, DependencyInfo &dep, size_t depIndex, + llvm::SmallVector memDependencies, + FlagIdManager &flagManager, FlagIdReuseManager &flagIdReuseManager); + + std::pair + getBlockStartEnd(int blockId, mlir::ModuleOp module); + std::pair + getSubBlockStartEnd(mlir::Operation *defOp); + bool + isOuterLayerDependency(size_t depIndex, mlir::Operation *currProdEnd, + mlir::Operation *currConsStart, + llvm::SmallVector &memDependencies); + + SmallVector computeExpectedShape(mlir::Value depValue, + int NzDimWidth); + bool isExpectedShape(Value value, SmallVector &expectedShape); + mlir::Value alignShapeByInsertSlice(mlir::OpBuilder &builder, + DependencyInfo &dep, mlir::Location loc, + mlir::Value origValue, + llvm::SmallVector expectedShape, + int originBlockId); + void Nd2NzNormalize(mlir::OpBuilder &builder, DependencyInfo &dep, + mlir::Location loc); + + mlir::Operation *annotateTightlyCoupledBuffer(mlir::OpBuilder &builder, + mlir::Operation *allocOp, + mlir::Location loc); + mlir::Operation *findMainLoopforTransfer(mlir::Operation *endOp, + mlir::Operation *startOp); + mlir::Operation * + createC2CSharedL1Buffer(mlir::OpBuilder &builder, mlir::Location loc, + llvm::ArrayRef shape, mlir::Type elemType, + int prodBlockId, mlir::Operation *prodEnd, + mlir::Operation *consStart, bool isIntraC2C); + std::pair + createTransferAllocs(mlir::OpBuilder &builder, mlir::Location loc, + llvm::ArrayRef shape, mlir::Type elemType, + hivm::AddressSpace addrSpace, mlir::Operation *prodEndOp, + mlir::Operation *consStartOp, int prodBlockId, + int consBlockId, llvm::StringRef prodTag, + llvm::StringRef consTag, int transferIndex); + mlir::Operation *analyzeConsumerReadInsertPoint(Value srcValue, + int iniConsumerId); + mlir::Operation *getConsumerWaitPoint(int transferIndex); + mlir::Operation *getCopyPointBeforeStore(Value depValue, + Operation *vectorEndOp, + int iniProducerBlockId); + mlir::Operation *getFixpipePointAfterProducer(Value depValue, + int iniProducerBlockId); + linalg::MatmulOp findYieldMatmulInSplitIf(Value splittedIfResult); + void AnalyzeSplittedIf(DependencyInfo &dep); + mlir::Operation *insertVectorToCubeTransfer( + mlir::OpBuilder &builder, mlir::Value srcValue, + mlir::Value normalizedValue, mlir::Operation *vectorEndOp, + mlir::Operation *cubeStartOp, mlir::Location loc, int transferIndex, + DependencyInfo &dep, bool is1DTensor, + mlir::Operation **consumedDataOp = nullptr); + mlir::Operation * + insertCubeToVectorTransfer(mlir::OpBuilder &builder, mlir::Value srcValue, + mlir::Operation *cubeEndOp, + mlir::Operation *vectorStartOp, mlir::Location loc, + int transferIndex, DependencyInfo &dep, + mlir::Operation **consumedDataOp = nullptr); + TransferPipeConfig getTransferPipeConfig(Operation *transferOp, + bool isStoreDirectly = false); + void insertInterCoreSync(mlir::OpBuilder &builder, + mlir::Operation *transferOp, + mlir::Operation *consumerStartOp, + mlir::Operation *consumerEndOp, int flag, + mlir::Location loc, int transferIndex, + FlagIdReuseManager &flagIdReuseManager, + mlir::Operation *consumedDataOp = nullptr, + bool isStoreDirectly = false); + void insertMemDepSync(mlir::OpBuilder &builder, + mlir::Operation *producerStartOp, + mlir::Operation *producerEndOp, + mlir::Operation *consumerStartOp, + mlir::Operation *consumerEndOp, int flag, + mlir::Location loc, bool isCubeToVector, + FlagIdReuseManager &flagIdReuseManager); + // Match the CUBE -> VECTOR direct-store pattern inside the given SCF op: + // CUBE data reaches VECTOR through UB and is stored without VECTOR tensor + // computation (`scf.for` / `scf.while` / `scf.if`). + static std::optional + matchCubeToVectorDirectStorePattern(Operation *scfOp); + // Detect SCF ops whose yielded data flows out via fixpipe and is then + // stored by a post-loop materialize_in_destination. Insert a + // CUBE.PIPE_FIX -> VECTOR.PIPE_MTE3 sync pair to guard the store. + void + processCubeToVectorDirectStoreSync(mlir::OpBuilder &builder, + FlagIdManager &flagManager, + FlagIdReuseManager &flagIdReuseManager); + // Remove VECTOR add-from-matmul pseudo-ops so the SCF yield points at the + // real data source. + void removeVectorPseudoOps(); + void sortDependencies(llvm::SmallVector &dependencies, + mlir::ModuleOp module); + llvm::SmallVector + insertAnalyzeFlagRelations(mlir::ModuleOp module, + FlagIdReuseManager &flagIdReuseManager); + void remapInterCoreTransferFlagIds(llvm::DenseMap &remapResult); + bool isStoreDirectlyInUserChain(mlir::Value toTensorValue); + std::pair analyzeMatmulOperand(DependencyInfo &dep); + int getNzDimWidth(DependencyInfo &dep, int64_t blk); +}; + +std::unique_ptr> createInterCoreTransferAndSyncPass(); + +void registerInterCoreTransferAndSyncPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_INTER_CORE_TRANSFER_AND_SYNC_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/MarkMainLoop.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/MarkMainLoop.h new file mode 100644 index 0000000000..d5cd817ae3 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/MarkMainLoop.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_MARK_MAIN_LOOP_H +#define TRITON_ADAPTER_MARK_MAIN_LOOP_H + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +// Define pass +// Pass for marking the main computation loop in the module +class MarkMainLoopPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MarkMainLoopPass) + + MarkMainLoopPass() = default; + + // Run the pass + void runOnOperation() override; + + // Return the pass argument name + static constexpr ::llvm::StringRef getArgumentName() { + return "mark-main-loop"; + } + ::llvm::StringRef getArgument() const override { return "mark-main-loop"; } + ::llvm::StringRef getDescription() const override { + return "Mark main computation loops with main_loop attribute"; + } + ::llvm::StringRef getName() const override { return "MarkMainLoopPass"; } + +private: +}; + +std::unique_ptr> createMarkMainLoopPass(); + +void registerMarkMainLoopPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_MARK_MAIN_LOOP_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.h new file mode 100644 index 0000000000..eea6b3ec60 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_PRESERVE_CONTROL_ATTRS_CANONICALIZE_H +#define TRITON_ADAPTER_PRESERVE_CONTROL_ATTRS_CANONICALIZE_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class PreserveControlAttrsCanonicalizePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID( + PreserveControlAttrsCanonicalizePass) + + PreserveControlAttrsCanonicalizePass() = default; + + void runOnOperation() override; + + static constexpr ::llvm::StringRef getArgumentName() { + return "preserve-control-attrs-canonicalize"; + } + + ::llvm::StringRef getArgument() const override { + return "preserve-control-attrs-canonicalize"; + } + + ::llvm::StringRef getDescription() const override { + return "Canonicalize while preserving selected control-flow attrs"; + } + + ::llvm::StringRef getName() const override { + return "PreserveControlAttrsCanonicalizePass"; + } +}; + +std::unique_ptr> +createPreserveControlAttrsCanonicalizePass(); + +void registerPreserveControlAttrsCanonicalizePasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_PRESERVE_CONTROL_ATTRS_CANONICALIZE_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.h new file mode 100644 index 0000000000..f0385bfad0 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_MOVE_UPDATE_OPS_TO_FIRST_USE_H +#define TRITON_ADAPTER_MOVE_UPDATE_OPS_TO_FIRST_USE_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class RefineArgsBlockIdPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(RefineArgsBlockIdPass) + + RefineArgsBlockIdPass() = default; + + void runOnOperation() override; + + static constexpr ::llvm::StringRef getArgumentName() { + return "refine-args-block-id"; + } + ::llvm::StringRef getArgument() const override { + return "refine-args-block-id"; + } + ::llvm::StringRef getDescription() const override { + return "Move update ops for iteration variables to the first block that " + "uses them in main loops"; + } + ::llvm::StringRef getName() const override { return "RefineArgsBlockIdPass"; } + +private: +}; + +std::unique_ptr> createRefineArgsBlockIdPass(); + +void registerRefineArgsBlockIdPasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_MOVE_UPDATE_OPS_TO_FIRST_USE_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/SeparateCVScope.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/SeparateCVScope.h new file mode 100644 index 0000000000..5352af45da --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/SeparateCVScope.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SEPARATE_CV_SCOPE_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SEPARATE_CV_SCOPE_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class SeparateCVScopePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(SeparateCVScopePass) + + SeparateCVScopePass() = default; + + void getDependentDialects(DialectRegistry ®istry) const override; + + void runOnOperation() override; + + // Return the pass argument name + static constexpr ::llvm::StringRef getArgumentName() { + return "separate-cv-scope"; + } + ::llvm::StringRef getArgument() const override { return "separate-cv-scope"; } + ::llvm::StringRef getDescription() const override { + return "separate ops into cv scope"; + } + ::llvm::StringRef getName() const override { return "SeparateCVScopePass"; } +}; + +std::unique_ptr> createSeparateCVScopePass(); + +void registerSeparateCVScopePasses(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SEPARATE_CV_SCOPE_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/Utils.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/Utils.h new file mode 100644 index 0000000000..0c46e40d58 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflow/Utils.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_SPLIT_DATAFLOW_UTILS_H +#define TRITON_ADAPTER_SPLIT_DATAFLOW_UTILS_H + +#include "mlir/IR/Operation.h" +#include "llvm/ADT/StringRef.h" + +namespace mlir { +namespace triton { + +void setOpBlockId(mlir::Operation *op, int blockId); +void setOpCoreType(mlir::Operation *op, llvm::StringRef coreType); +bool isScalarDependency(mlir::Value depValue); +bool is1DTensorDependency(mlir::Value depValue); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_SPLIT_DATAFLOW_UTILS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/SplitDataflowPass.h b/third_party/ascend/include/DynamicCVPipeline/SplitDataflowPass.h new file mode 100644 index 0000000000..d406520172 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/SplitDataflowPass.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SPLIT_DATAFLOW_PASS_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SPLIT_DATAFLOW_PASS_H + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir { +namespace triton { + +class SplitDataflowPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(SplitDataflowPass) + + // Constructor + SplitDataflowPass() = default; + + // Run the pass + void runOnOperation() override; + + /// Return the dialect that must be loaded in the context before this pass. + void getDependentDialects(::mlir::DialectRegistry ®istry) const override { + registry.insert(); + registry.insert(); + registry.insert(); + registry.insert(); + } +}; + +// Create the pass +std::unique_ptr> createSplitDataflowPass(); +std::unique_ptr> createCheckUnsupportedScenarioPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_SPLIT_DATAFLOW_PASS_H diff --git a/third_party/ascend/include/DynamicCVPipeline/StandardizeOp.h b/third_party/ascend/include/DynamicCVPipeline/StandardizeOp.h new file mode 100644 index 0000000000..e72eba8b29 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/StandardizeOp.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CVPIPELINE_STANDARDIZE_OP_H +#define TRITON_ADAPTER_DYNAMIC_CVPIPELINE_STANDARDIZE_OP_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir::triton { + +class StandardizeOpPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(StandardizeOpPass); + + StandardizeOpPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "ssbuf-standardize-op"; } +}; + +std::unique_ptr> createStandardizeOpPass(); +void registerStandardizeOpPasses(); + +} // namespace mlir::triton + +#endif // TRITON_ADAPTER_DYNAMIC_CVPIPELINE_STANDARDIZE_OP_H diff --git a/third_party/ascend/include/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.h b/third_party/ascend/include/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.h new file mode 100644 index 0000000000..75618525f6 --- /dev/null +++ b/third_party/ascend/include/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.h @@ -0,0 +1,76 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CVPIPELINE_PATTERN_MATCH_REWRITES_H +#define TRITON_ADAPTER_DYNAMIC_CVPIPELINE_PATTERN_MATCH_REWRITES_H + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +namespace mlir::triton::CVSplit { + +class SplitMatmulPattern : public mlir::OpRewritePattern { +public: + explicit SplitMatmulPattern(mlir::MLIRContext *context, bool needSplitAll) + : OpRewritePattern(context), + needSplitAll(needSplitAll) {} + llvm::LogicalResult matchAndRewrite(linalg::MatmulOp matmulOp, + PatternRewriter &rewriter) const override; + +private: + bool needSplitAll; +}; + +class FoldExpandExtCollapse + : public mlir::OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + llvm::LogicalResult matchAndRewrite(mlir::tensor::CollapseShapeOp collapseOp, + PatternRewriter &rewriter) const override; +}; + +class PatternMatchRewritePass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PatternMatchRewritePass); + + PatternMatchRewritePass() = default; + void runOnOperation() override; + + void getDependentDialects(mlir::DialectRegistry ®istry) const override { + registry.insert(); + } + + [[nodiscard]] llvm::StringRef getArgument() const final { + return "ssbuf-standardize-op-pattern-match"; + } +}; + +std::unique_ptr> createPatternMatchRewritePass(); + +} // namespace mlir::triton::CVSplit + +#endif // TRITON_ADAPTER_DYNAMIC_CVPIPELINE_PATTERN_MATCH_REWRITES_H diff --git a/third_party/ascend/include/LibEntry/libentry.h b/third_party/ascend/include/LibEntry/libentry.h new file mode 100644 index 0000000000..2a9ba371bd --- /dev/null +++ b/third_party/ascend/include/LibEntry/libentry.h @@ -0,0 +1,77 @@ +/* + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef LIBENTRY_H +#define LIBENTRY_H + +#include +#include +#include +#include + +#include +#include + +namespace py = pybind11; + +using KeyType = py::tuple; + +namespace libentry { + +class ArgProcessor { +public: + ArgProcessor(int div) : divisibility_(div) {}; + + void + classifyArguments(const py::list &args, const py::dict &kwargs, + const py::list &jit_params, + const std::unordered_set &specialize_indices, + const std::unordered_set &do_not_specialize_indices); + + KeyType generateKey(); + + py::list getKArgs(); + +private: + py::list spec_args_; // specialize args + py::list dns_args_; // do not specialize args + py::list const_args_; // constexpr args + py::list k_args_; // kernel args + int divisibility_; // 对齐 +}; + +} // namespace libentry + +PYBIND11_MODULE(libentryC, m) { + py::class_(m, "ArgProcessor") + .def(py::init()) + .def("classify_arguments", &libentry::ArgProcessor::classifyArguments, + py::arg("args"), py::arg("kwargs"), py::arg("jit_params"), + py::arg("specialize_indices"), py::arg("do_not_specialize_indices"), + "classify arguments") + .def("get_k_args", &libentry::ArgProcessor::getKArgs, "get kernel") + .def("generate_key", &libentry::ArgProcessor::generateKey, + "generate kernel cache key"); +} + +#endif diff --git a/third_party/ascend/include/TritonControlFlowOpt/BlockPtrDecompose.h b/third_party/ascend/include/TritonControlFlowOpt/BlockPtrDecompose.h new file mode 100644 index 0000000000..678ccde0db --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/BlockPtrDecompose.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_BLOCK_PTR_DECOMPOSE_H +#define TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_BLOCK_PTR_DECOMPOSE_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Support/LogicalResult.h" + +namespace mlir::triton::controlflow { + +/// Internal decomposition entry point used by TritonControlFlowOpt. +/// Decomposes block-pointer control-flow slots into descriptor components. +LogicalResult runBlockPtrDecompose(ModuleOp module); + +} // namespace mlir::triton::controlflow + +#endif // TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_BLOCK_PTR_DECOMPOSE_H diff --git a/third_party/ascend/include/TritonControlFlowOpt/CFGStructuring.h b/third_party/ascend/include/TritonControlFlowOpt/CFGStructuring.h new file mode 100644 index 0000000000..029ae71c4a --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/CFGStructuring.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CFG_STRUCTURING_H +#define TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CFG_STRUCTURING_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Support/LogicalResult.h" + +namespace mlir::triton::controlflow { + +/// Internal entry point used by the composite TritonControlFlowOpt pass. +/// +/// Converts supported acyclic, tree-like function CFGs in `module` to SCF. It +/// is intentionally not registered as an independent pass because pointer +/// decomposition requires structured control flow. +LogicalResult structureCFG(ModuleOp module); + +} // namespace mlir::triton::controlflow + +#endif // TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CFG_STRUCTURING_H diff --git a/third_party/ascend/include/TritonControlFlowOpt/CMakeLists.txt b/third_party/ascend/include/TritonControlFlowOpt/CMakeLists.txt new file mode 100644 index 0000000000..e0561bd754 --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonControlFlowOpt) +add_public_tablegen_target(TritonControlFlowOptPassIncGen) diff --git a/third_party/ascend/include/TritonControlFlowOpt/ControlFlowAnalysis.h b/third_party/ascend/include/TritonControlFlowOpt/ControlFlowAnalysis.h new file mode 100644 index 0000000000..c8a31cd723 --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/ControlFlowAnalysis.h @@ -0,0 +1,219 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CONTROL_FLOW_ANALYSIS_H +#define TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CONTROL_FLOW_ANALYSIS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/Support/LogicalResult.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" + +namespace mlir::triton::controlflow { + +/// Classification used by the signature planner. Recomputed components are +/// intentionally not modeled yet; they can be added when IV extraction becomes +/// part of the analysis rather than the existing block-pointer rewrite helper. +enum class ComponentTransferKind { Invariant, Transferred }; + +/// Symbolic identity of a component during read-only analysis. +/// +/// A component produced by an ordinary SSA value is identified by that value +/// and its policy-defined component index. Zero represents a canonical zero +/// offsets value without materializing an arith.constant in the source IR. +struct ComponentIdentity { + enum class Kind { Value, Zero } kind = Kind::Value; + Value value; + unsigned componentIndex = 0; + + static ComponentIdentity fromValue(Value value, unsigned componentIndex) { + return {Kind::Value, value, componentIndex}; + } + static ComponentIdentity zero(unsigned componentIndex = 0) { + return {Kind::Zero, {}, componentIndex}; + } + + bool operator==(const ComponentIdentity &other) const { + return kind == other.kind && value == other.value && + componentIndex == other.componentIndex; + } + bool operator!=(const ComponentIdentity &other) const { + return !(*this == other); + } +}; + +struct AnalyzedComponent { + Type type; + ComponentIdentity identity; +}; + +/// Policy-owned abstract pointer state. Unlike DecomposedValue, this structure +/// never contains newly created IR values and is therefore safe to compute +/// before a rewrite starts. +struct AnalyzedValue { + Type originalType; + SmallVector components; + SmallVector attributes; +}; + +/// Signature decision for one original result/iter-argument position. The slot +/// itself means the original value is removed from the SCF signature; +/// componentIndices may be empty when every component is invariant and no +/// replacement operand/result is required. +struct ControlFlowSlotAnalysis { + unsigned oldIndex = 0; + SmallVector componentKinds; + SmallVector componentIndices; + SmallVector componentTypes; + /// Policy metadata after all incoming paths have been joined. + SmallVector resultAttributes; +}; + +/// Cached decision for one structured control-flow operation. +struct ControlFlowOpAnalysis { + SmallVector slots; + bool hasNestedRewrite = false; + + bool rewritesOwnSignature() const { return !slots.empty(); } + bool needsRewrite() const { + return rewritesOwnSignature() || hasNestedRewrite; + } +}; + +/// Immutable operation-level contract consumed after read-only analysis. +/// +/// Temporary Value -> AnalyzedValue state is deliberately excluded: those +/// values may be erased while earlier roots are rewritten, whereas a root and +/// its nested operations remain valid until that root is processed. +struct ControlFlowRewritePlan { + llvm::DenseMap operations; + + const ControlFlowOpAnalysis *lookup(Operation *op) const; +}; + +class ControlFlowAnalysisContext; + +/// Pointer-specific part of the read-only analysis. Implementations describe +/// their component layout and merge rules; the common analyzer owns SCF region +/// traversal, argument/result correspondence, and nested-op caching. +class ControlFlowAnalysisPolicy { +public: + virtual ~ControlFlowAnalysisPolicy() = default; + + virtual bool matches(Type type) const = 0; + + /// Returns whether a value belongs to this decomposition stage. Pointer + /// policies use the type-based default. A future StructuredOffsets policy + /// can override this with the result of a backward address-demand analysis. + virtual bool isDecompositionTarget(Value value) const { + return matches(value.getType()); + } + + virtual FailureOr + analyzeValue(Value value, ControlFlowAnalysisContext &context) const = 0; + + /// Components which may legally be carried by a loop. Other components must + /// remain symbolically identical across the backedge. + virtual FailureOr> + getLoopCandidateComponents(const AnalyzedValue &value) const = 0; + + /// Components which may cross a scope.scope result boundary. Scope has no + /// region arguments or backedge, so the first implementation returns every + /// component that the policy already permits a loop to carry. A policy may + /// override this hook when its Scope schema differs from its loop schema. + virtual FailureOr> + getScopeCandidateComponents(const AnalyzedValue &value) const { + return getLoopCandidateComponents(value); + } + + virtual FailureOr> + getLoopTransferredComponents(const AnalyzedValue &initial, + const AnalyzedValue ®ionArgument, + const AnalyzedValue &next) const = 0; + + virtual FailureOr> + getIfTransferredComponents(const AnalyzedValue &thenValue, + const AnalyzedValue &elseValue) const = 0; + + /// Selects the component type used in the replacement SCF signature. + virtual FailureOr joinComponentTypes(Type lhs, Type rhs) const = 0; + + /// Joins policy-owned metadata across one control-flow boundary. The + /// default requires exact equality, which preserves BlockPtr behavior. + virtual FailureOr> + mergeControlFlowAttributes(const AnalyzedValue &lhs, + const AnalyzedValue &rhs) const { + if (lhs.attributes != rhs.attributes) + return failure(); + return lhs.attributes; + } +}; + +/// Stage-scoped transient cache used while computing a rewrite plan. One +/// context analyzes all outermost roots before any IR mutation; only the +/// operation-level decisions survive in ControlFlowRewritePlan. +class ControlFlowAnalysisContext { +public: + explicit ControlFlowAnalysisContext(const ControlFlowAnalysisPolicy &policy) + : policy(policy) {} + + FailureOr analyzeValue(Value value); + FailureOr analyzeControlFlowOp(Operation *op); + + ControlFlowRewritePlan takeRewritePlan() &&; + + const AnalyzedValue *lookupValue(Value value) const; + const ControlFlowOpAnalysis *lookup(Operation *op) const; + +private: + LogicalResult analyzeNestedOperations(Block *block, bool &hasNestedRewrite); + FailureOr analyzeFor(Operation *op); + FailureOr analyzeWhile(Operation *op); + FailureOr analyzeIf(Operation *op); + FailureOr analyzeScope(Operation *op); + + void bindRegionArgument(Value argument, const AnalyzedValue &initial, + ArrayRef componentIndices); + FailureOr> + getTransferredTypes(const AnalyzedValue &lhs, const AnalyzedValue &rhs, + ArrayRef componentIndices) const; + + const ControlFlowAnalysisPolicy &policy; + llvm::DenseMap analyzedValues; + llvm::DenseMap analyzedOps; + llvm::DenseSet operationsBeingAnalyzed; +}; + +/// Returns SCF roots that are not nested in another supported SCF operation. +SmallVector collectOutermostControlFlowOps(ModuleOp module); + +/// Analyzes every control-flow root for one decomposition stage before any IR +/// mutation and freezes only the operation-level rewrite decisions. +FailureOr +analyzeControlFlow(ModuleOp module, const ControlFlowAnalysisPolicy &policy); + +} // namespace mlir::triton::controlflow + +#endif // TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CONTROL_FLOW_ANALYSIS_H diff --git a/third_party/ascend/include/TritonControlFlowOpt/ControlFlowRewrite.h b/third_party/ascend/include/TritonControlFlowOpt/ControlFlowRewrite.h new file mode 100644 index 0000000000..97a44a2ad8 --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/ControlFlowRewrite.h @@ -0,0 +1,202 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CONTROL_FLOW_REWRITE_H +#define TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CONTROL_FLOW_REWRITE_H + +#include "TritonControlFlowOpt/ControlFlowAnalysis.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/Support/LogicalResult.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace mlir::triton::controlflow { + +/// Handoff marker for SCF loops whose pointer slots have already been expanded +/// into policy-owned descriptor components. TritonToLinalg consumes and +/// removes it instead of applying its legacy pointer-loop decomposition a +/// second time. +inline constexpr llvm::StringLiteral kPointerDescriptorBoundaryAttr = + "PointerDescriptorBoundary"; + +/// Handoff marker for a pointer rebuilt at a CFO control-flow boundary. The +/// defining operation's operands are the complete descriptor roots, including +/// invariant components omitted from the replacement SCF signature. +inline constexpr llvm::StringLiteral kPointerDescriptorRebuildAttr = + "PointerDescriptorRebuild"; + +/// Optional companion on a PointerDescriptorRebuild root. The `strided_1d` +/// form means the tensor-pointer offset operand is a proven rank-1 affine +/// expression rather than an opaque complete-offset carrier. +inline constexpr llvm::StringLiteral kPointerDescriptorOffsetFormAttr = + "PointerDescriptorOffsetForm"; +inline constexpr llvm::StringLiteral kStrided1DOffsetForm = "strided_1d"; + +/// Dense per-axis classification attached to a descriptor rebuild. A value +/// of one means the complete contribution of that logical axis is represented +/// by its scalar stride; zero means the axis is carried by the opaque complete +/// contribution tensor. +inline constexpr llvm::StringLiteral kPointerDescriptorStructuredAxesAttr = + "PointerDescriptorStructuredAxes"; + +/// Policy-owned description of one value crossing a control-flow boundary. +/// +/// `components` contain every runtime value needed to rebuild the original +/// value. A policy may place a selected subset in an expanded SCF signature. +/// `attributes` retain non-SSA metadata. Both layouts are private to the +/// policy; the shared rewrite never interprets pointer-specific fields. +struct DecomposedValue { + Type originalType; + SmallVector components; + SmallVector attributes; +}; + +/// Read-only view of the SSA mapping and decompositions visible at the current +/// rewrite point. Policies use it while recursively analyzing pointer +/// producers; the state exists only for one control-flow rewrite attempt. +class ControlFlowRewriteContext { +public: + ControlFlowRewriteContext( + const IRMapping &valueMapping, + const llvm::DenseMap &decomposedValues) + : valueMapping(valueMapping), decomposedValues(decomposedValues) {} + + Value remap(Value value) const; + const DecomposedValue *lookup(Value value) const; + +private: + const IRMapping &valueMapping; + const llvm::DenseMap &decomposedValues; +}; + +/// Pointer-semantics interface implemented by each decomposition policy. +/// +/// The policy decides how its value is decomposed and rebuilt, which components +/// cross loop/if boundaries, and whether two decompositions share a compatible +/// non-carried schema. It carries no mutable state between policy invocations. +/// A policy that expands pointer descriptors may request a downstream handoff +/// marker; the shared rewrite owns the positional slot metadata. +class ControlFlowRewritePolicy : public ControlFlowAnalysisPolicy { +public: + virtual ~ControlFlowRewritePolicy() = default; + + /// Whether results of this ordinary operation need immediate decomposition + /// after cloning so later operations can reuse their exact component state. + virtual bool shouldDecomposeOperation(Operation *op) const = 0; + + /// Whether a rebuilt value may be used as a direct decomposition-cache key. + /// + /// The default preserves the existing behavior for policies whose rebuilt + /// values are already consumable by downstream conversion. A policy that + /// rebuilds an opaque tensor-of-pointers can reject the cache entry so the + /// next consumer re-analyzes the pointer producer and follows its normal + /// scalar-address lowering path instead of exposing tensor.extract of a + /// scalar Triton pointer to T2L. + virtual bool shouldCacheRebuiltDecomposition(const DecomposedValue &) const { + return true; + } + + /// Whether rewritten loops owned by this policy must expose their descriptor + /// slots to downstream conversion. The shared rewrite records the exact + /// expanded result indices because only it knows both old and new signatures. + virtual bool requiresPointerDescriptorBoundaryMarker() const { return false; } + + /// Whether recomposing a result of this ordinary cloned operation creates a + /// descriptor root that downstream conversion must recognize. This is more + /// selective than the boundary marker request above: most policies need + /// handoff markers only for SCF argument/result reconstructions, while a + /// policy may opt in for a specific pointer-preserving operation. + virtual bool shouldMarkOperationRecomposition(Operation *) const { + return false; + } + + /// Adds policy-private metadata to a descriptor rebuild root. + /// This is called only after the generic rewrite has attached the ownership + /// marker, whether the root was created at an SCF boundary or by an ordinary + /// operation recomposition explicitly selected by the policy. + virtual LogicalResult + annotatePointerDescriptorRebuild(Operation *, const DecomposedValue &) const { + return success(); + } + + /// Normalizes a concrete descriptor to the schema selected by control-flow + /// analysis. The default only replaces policy metadata; tensor-pointer + /// policies override it to fold structured fields into an opaque carrier + /// when an incoming edge was downgraded. + virtual LogicalResult + normalizeControlFlowValue(DecomposedValue &value, + ArrayRef targetAttributes, OpBuilder &, + Location) const { + value.attributes.assign(targetAttributes.begin(), targetAttributes.end()); + return success(); + } + + /// Normalizes a descriptor used at a loop boundary. The default preserves + /// the generic control-flow behavior. Policies may use the exact set of + /// loop-carried components to retain a more compact equivalent schema that + /// would not be valid for an if or scope result. + virtual LogicalResult + normalizeLoopCarriedValue(DecomposedValue &value, + ArrayRef carriedComponents, + ArrayRef targetAttributes, + OpBuilder &builder, Location loc) const { + return normalizeControlFlowValue(value, targetAttributes, builder, loc); + } + + virtual FailureOr + decompose(Value value, const ControlFlowRewriteContext &context, + OpBuilder &builder, Location loc) const = 0; + + virtual Value recompose(const DecomposedValue &value, OpBuilder &builder, + Location loc) const = 0; +}; + +/// Rewrites supported SCF operations from outermost to innermost. +/// +/// Applies a previously frozen plan without running value analysis again. +/// Signature expansion, region cloning, terminator rewriting, nested recursion +/// and result replacement are driven solely by operation-level decisions in +/// `plan`. +/// Set `emitDiagnostics` to false for a speculative rewrite on a detached +/// clone whose failure is intentionally treated as an unsupported case. +LogicalResult applyControlFlowRewritePlan( + ModuleOp module, const ControlFlowRewritePolicy &policy, + const ControlFlowRewritePlan &plan, bool emitDiagnostics = true); + +/// Analyzes the complete decomposition stage before mutating the IR, then +/// applies the frozen plan from outermost to innermost. Pointer semantics +/// remain selected by `policy` so different decompositions share the same SCF +/// plumbing. +/// When `allowUnsupportedFallback` is true, a failed complete rewrite is +/// treated as an unsupported optimization and leaves the input module intact. +LogicalResult rewriteControlFlow(ModuleOp module, + const ControlFlowRewritePolicy &policy, + bool allowUnsupportedFallback = false); + +} // namespace mlir::triton::controlflow + +#endif // TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_CONTROL_FLOW_REWRITE_H diff --git a/third_party/ascend/include/TritonControlFlowOpt/Passes.h b/third_party/ascend/include/TritonControlFlowOpt/Passes.h new file mode 100644 index 0000000000..5339b9beac --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/Passes.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITON_CONTROL_FLOW_OPT_PASSES_H +#define TRITON_ADAPTER_TRITON_CONTROL_FLOW_OPT_PASSES_H + +#include "TritonControlFlowOptPass.h" + +namespace mlir { +namespace triton { + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonControlFlowOpt/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_TRITON_CONTROL_FLOW_OPT_PASSES_H diff --git a/third_party/ascend/include/TritonControlFlowOpt/Passes.td b/third_party/ascend/include/TritonControlFlowOpt/Passes.td new file mode 100644 index 0000000000..c87686a756 --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/Passes.td @@ -0,0 +1,11 @@ +#ifndef TRITON_CONTROL_FLOW_OPT_PASSES +#define TRITON_CONTROL_FLOW_OPT_PASSES + +include "mlir/Pass/PassBase.td" + +def TritonControlFlowOpt : Pass<"triton-control-flow-opt", "mlir::ModuleOp"> { + let summary = "Normalize Triton control flow before pointer analysis"; + let constructor = "triton::createTritonControlFlowOptPass()"; +} + +#endif // TRITON_CONTROL_FLOW_OPT_PASSES diff --git a/third_party/ascend/include/TritonControlFlowOpt/TensorPtrDecompose.h b/third_party/ascend/include/TritonControlFlowOpt/TensorPtrDecompose.h new file mode 100644 index 0000000000..4fd63928d9 --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/TensorPtrDecompose.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_TENSOR_PTR_DECOMPOSE_H +#define TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_TENSOR_PTR_DECOMPOSE_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Support/LogicalResult.h" + +namespace mlir::triton::controlflow { + +/// Internal decomposition entry point used by TritonControlFlowOpt. +/// Replaces common-base tensor-of-pointers control-flow slots with complete +/// offsets. The common base remains outside the rewritten control-flow +/// signature and is used only to rebuild the original pointer value. +LogicalResult runTensorPtrDecompose(ModuleOp module); + +} // namespace mlir::triton::controlflow + +#endif // TRITON_ASCEND_TRITON_CONTROL_FLOW_OPT_TENSOR_PTR_DECOMPOSE_H diff --git a/third_party/ascend/include/TritonControlFlowOpt/TritonControlFlowOptPass.h b/third_party/ascend/include/TritonControlFlowOpt/TritonControlFlowOptPass.h new file mode 100644 index 0000000000..ece341f4ed --- /dev/null +++ b/third_party/ascend/include/TritonControlFlowOpt/TritonControlFlowOptPass.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_CONVERSION_TRITON_CONTROL_FLOW_OPT_H +#define TRITON_ADAPTER_CONVERSION_TRITON_CONTROL_FLOW_OPT_H + +#include "mlir/Pass/Pass.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace mlir { +namespace triton { + +#define GEN_PASS_CLASSES +#include "ascend/include/TritonControlFlowOpt/Passes.h.inc" + +std::unique_ptr> createTritonControlFlowOptPass(); + +class TritonControlFlowOptPass + : public TritonControlFlowOptBase { +public: + TritonControlFlowOptPass() = default; + + void getDependentDialects(DialectRegistry ®istry) const override; + void runOnOperation() override; +}; + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_CONVERSION_TRITON_CONTROL_FLOW_OPT_H diff --git a/third_party/ascend/include/TritonMemoryAccess/LoadStoreMaskAnalysis.h b/third_party/ascend/include/TritonMemoryAccess/LoadStoreMaskAnalysis.h new file mode 100644 index 0000000000..fa53449b52 --- /dev/null +++ b/third_party/ascend/include/TritonMemoryAccess/LoadStoreMaskAnalysis.h @@ -0,0 +1,219 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation, Meta Platforms. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_MEMORY_ACCESS_LOAD_STORE_MASK_ANALYSIS_H +#define TRITON_MEMORY_ACCESS_LOAD_STORE_MASK_ANALYSIS_H + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include +#include + +namespace mlir { + +// this class helps build Operations +class OpBuilder; + +namespace triton { +// use to decode the pattern in a mask used for load and store + +enum class MaskPosition { Head, Tail, Middle, Unknown }; + +class MaskState { +public: + OpFoldResult start; + OpFoldResult end; + SmallVector dims; + SmallVector offsets; + OpFoldResult scalar; + + int64_t getRank() const { + assert(dims.size() == offsets.size() && "dims and offsets rank mismatch!"); + return dims.size(); + } + + MaskPosition getMaskPosition(llvm::ArrayRef &tensorShape) { + if (getRank() != tensorShape.size()) { + return MaskPosition::Unknown; + } + + bool isHead = true; + int dynIndex = -1; + + for (int i = 0; i < getRank(); ++i) { + auto offsetVal = mlir::getConstantIntValue(offsets[i]); + if (!offsetVal.has_value() || offsetVal.value() != 0) { + isHead = false; + if (dynIndex == -1) { + dynIndex = i; + } else { // temporarily support only one dyn dim + return MaskPosition::Unknown; + } + } + } + + if (isHead) { + return MaskPosition::Head; + } + + for (int i = 0; i < getRank(); ++i) { + auto dimVal = mlir::getConstantIntValue(dims[i]); + if (i == dynIndex) { + continue; + } + if (!dimVal.has_value() || dimVal.value() != tensorShape[i]) { + return MaskPosition::Unknown; + } + } + return MaskPosition::Middle; + } + + bool isEmpty() const { return getRank() == 0 && !scalar && !start && !end; } + + bool isMask() const { + return !start && !end && !scalar && dims.size() != 0 && offsets.size() != 0; + } + + bool isMemrefSubviewValid(Value source, OpBuilder &builder) const; + + // parse value recursively + LogicalResult parse(Value operand, const Location &loc, OpBuilder &builder); + + tensor::ExtractSliceOp getExtractSlice(Value source, const Location &loc, + OpBuilder &builder) const; + + tensor::ExtractSliceOp getExtractSlice(Value source, const Location &loc, + OpBuilder &builder, + SmallVector offsets, + SmallVector dims) const; + + tensor::InsertSliceOp getInsertSlice(Value source, Value dest, + const Location &loc, + OpBuilder &builder) const; + + tensor::InsertSliceOp getInsertSlice(Value source, Value dest, + const Location &loc, OpBuilder &builder, + SmallVector offsets, + SmallVector dims) const; + + memref::SubViewOp getSubview(Value source, const Location &loc, + OpBuilder &builder) const; + + void eraseInsertedOps(Operation *rawOp, PatternRewriter &rewriter); + +private: + LogicalResult addStateScalar(const MaskState &state, + const OpFoldResult scalar, const Location &loc, + OpBuilder &builder); + + LogicalResult addStates(const MaskState &lhsState, const MaskState &rhsState, + const Location &loc, OpBuilder &builder); + + LogicalResult subStateScalar(const MaskState &state, + const OpFoldResult scalar, const Location &loc, + OpBuilder &builder); + + // Only supports (range - scalar). + LogicalResult subStates(const MaskState &lhsState, const MaskState &rhsState, + const Location &loc, OpBuilder &builder); + + LogicalResult divStateScalar(const MaskState &state, + const OpFoldResult scalar, const Location &loc, + OpBuilder &builder); + + LogicalResult divStates(const MaskState &lhsState, const MaskState &rhsState, + const Location &loc, OpBuilder &builder); + + // Helper function to handle operator `and` both mask state + LogicalResult minStates(const MaskState &lhsState, const MaskState &rhsState, + const Location &loc, OpBuilder &builder); + + OpFoldResult clampToNonNegativeIndex(const OpFoldResult value, + const Location &loc, OpBuilder &builder, + bool clampDynamic = false) const; + + // Helper functions to parse values to populate MaskState + + LogicalResult parseConstant(arith::ConstantOp constOp, const Location &loc, + OpBuilder &builder); + + // Operand is an integer scalar + LogicalResult parseIntScalar(Value scalar, const Location &loc, + OpBuilder &builder); + + // TODO + LogicalResult parseAdd(arith::AddIOp addOp, const Location &loc, + OpBuilder &builder); + + // Operand is the result of subi (range - scalar). + LogicalResult parseSub(arith::SubIOp subOp, const Location &loc, + OpBuilder &builder); + + // operand is the result of divsi + LogicalResult parseDiv(arith::DivSIOp divOp, const Location &loc, + OpBuilder &builder); + + // Operand is the result of andi + LogicalResult parseAnd(arith::AndIOp andOp, const Location &loc, + OpBuilder &builder); + + // Operand is the result of cmpi, necessary method to fuse scalar, start and + // end into dims and offset + LogicalResult parseCmp(arith::CmpIOp cmpOp, const Location &loc, + OpBuilder &builder); + + // Operand is the result of select + LogicalResult parseSel(arith::SelectOp selOp, const Location &loc, + OpBuilder &builder); + + // Operand is the result of make_range + LogicalResult parseMakeRange(triton::MakeRangeOp rangeOp, const Location &loc, + OpBuilder &builder); + + // Operand is the result of broadcast + LogicalResult parseBroadcast(triton::BroadcastOp broadcastOp, + const Location &loc, OpBuilder &builder); + + // Operand is the result of splat + LogicalResult parseSplat(triton::SplatOp splatOp, const Location &loc, + OpBuilder &builder); + + // Operand is the result of expand_dims + LogicalResult parseExpandDims(triton::ExpandDimsOp expandDimsOp, + const Location &loc, OpBuilder &builder); + + // Operand is the result of insert + LogicalResult parseInsert(tensor::InsertOp insertOp, const Location &loc, + OpBuilder &builder); +}; + +std::optional runMaskAnalysis(Operation *op, OpBuilder &builder); + +} // namespace triton + +} // namespace mlir + +#endif // TRITON_MEMORY_ACCESS_LOAD_STORE_MASK_ANALYSIS_H diff --git a/third_party/ascend/include/TritonMemoryAccess/MemoryAccessTags.h b/third_party/ascend/include/TritonMemoryAccess/MemoryAccessTags.h new file mode 100644 index 0000000000..6e47b1bf22 --- /dev/null +++ b/third_party/ascend/include/TritonMemoryAccess/MemoryAccessTags.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_MEMORY_ACCESS_TAGS_H +#define TRITON_MEMORY_ACCESS_TAGS_H + +namespace mlir::triton::memory_access { + +inline constexpr const char *ImplicitPermuteHandledTAG = + "ImplicitPermuteHandled"; +inline constexpr const char *InspectedByStridedLoadStoreRewriteTAG = + "InspectedByStridedLoadStoreRewrite"; +inline constexpr const char *RewrittenByStridedLoadStoreRewriteTAG = + "RewrittenByStridedLoadStoreRewrite"; +inline constexpr const char *IATRuntimeExtentUnsignedMaskTAG = + "IATRuntimeExtentUnsignedMask"; +inline constexpr const char *PTSMRuntimeExtentUnsignedMaskTAG = + "PTSMRuntimeExtentUnsignedMask"; + +} // namespace mlir::triton::memory_access + +#endif // TRITON_MEMORY_ACCESS_TAGS_H diff --git a/third_party/ascend/include/TritonMemoryAccess/OpFoldResultUtils.h b/third_party/ascend/include/TritonMemoryAccess/OpFoldResultUtils.h new file mode 100644 index 0000000000..3cb16a2128 --- /dev/null +++ b/third_party/ascend/include/TritonMemoryAccess/OpFoldResultUtils.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_MEMORY_ACCESS_OP_FOLD_RESULT_UTILS_H +#define TRITON_MEMORY_ACCESS_OP_FOLD_RESULT_UTILS_H + +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallBitVector.h" +#include "llvm/Support/LogicalResult.h" + +#include + +namespace mlir { + +class OpBuilder; + +enum class IntegerExtensionKind { + Signed, + Unsigned, +}; + +FailureOr +castIntegerLike(OpBuilder &builder, Location loc, Value value, Type targetType, + IntegerExtensionKind extension = IntegerExtensionKind::Signed); + +std::optional getConstantOfAttr(const OpFoldResult &arg); + +/// Without an explicit result type, preserve equal types and widen signless +/// integers. Mixed index/integer operands require an explicit result type. +OpFoldResult addOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &builder, + Type resultType = {}); + +OpFoldResult subOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &builder); + +/// Uses the same result-type rules as addOpFoldResult. +OpFoldResult mulOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &builder, + Type resultType = {}); + +OpFoldResult divOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &builder); + +OpFoldResult remOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &builder); + +OpFoldResult minOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &builder); + +OpFoldResult maxOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &builder); + +std::optional getIntAttr(const OpFoldResult ofr); + +Value materializeValue(OpBuilder &builder, Location loc, OpFoldResult ofr); + +bool isZero(const OpFoldResult ofr); + +bool isOne(const OpFoldResult ofr); + +Value convertToIndexIfNeeded(Value input, const Location &loc, + OpBuilder &builder); + +RankedTensorType getExtractSlicedType(llvm::ArrayRef shape, + const llvm::SmallBitVector &droppedDims, + Type elemType); + +OpFoldResult getOpFoldResultOfLayoutInfo(Value value, OpBuilder &builder); + +} // namespace mlir + +#endif // TRITON_MEMORY_ACCESS_OP_FOLD_RESULT_UTILS_H diff --git a/third_party/ascend/include/TritonToAnnotation/CMakeLists.txt b/third_party/ascend/include/TritonToAnnotation/CMakeLists.txt new file mode 100644 index 0000000000..d8cdc5dcbb --- /dev/null +++ b/third_party/ascend/include/TritonToAnnotation/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonToAnnotation) +add_public_tablegen_target(TritonToAnnotationConversionPassIncGen) diff --git a/third_party/ascend/include/TritonToAnnotation/Passes.h b/third_party/ascend/include/TritonToAnnotation/Passes.h new file mode 100644 index 0000000000..2899e4c008 --- /dev/null +++ b/third_party/ascend/include/TritonToAnnotation/Passes.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITON_TO_ANNOTATION_CONVERSION_PASSES_H +#define TRITON_ADAPTER_TRITON_TO_ANNOTATION_CONVERSION_PASSES_H + +#include "mlir/Pass/Pass.h" + +namespace mlir { +// Forward declarations. +class ModuleOp; + +namespace triton { + +/// Creates a pass to convert Triton dialect to Annotation dialect. +std::unique_ptr> createTritonToAnnotationPass(); + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToAnnotation/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_TRITON_TO_ANNOTATION_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/TritonToAnnotation/Passes.td b/third_party/ascend/include/TritonToAnnotation/Passes.td new file mode 100644 index 0000000000..871de7f498 --- /dev/null +++ b/third_party/ascend/include/TritonToAnnotation/Passes.td @@ -0,0 +1,17 @@ +/* + * Copyright (c) Huawei Technologies Co. + * Licensed under the MIT license. + */ + +#ifndef TRITON_TO_ANNOTATION_CONVERSION_PASSES +#define TRITON_TO_ANNOTATION_CONVERSION_PASSES + +include "mlir/Pass/PassBase.td" + +def TritonToAnnotation : Pass<"triton-to-annotation", "mlir::ModuleOp"> { + let summary = "Convert Triton to Annotation dialect"; + let constructor = "triton::createTritonToAnnotationPass()"; + let dependentDialects = ["annotation::AnnotationDialect"]; +} + +#endif // TRITON_TO_ANNOTATION_CONVERSION_PASSES diff --git a/third_party/ascend/include/TritonToGraph/AliasAnalysis.h b/third_party/ascend/include/TritonToGraph/AliasAnalysis.h new file mode 100644 index 0000000000..ed620ade3b --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/AliasAnalysis.h @@ -0,0 +1,173 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_ALIAS_ANALYSIS_H +#define TRITON_TO_CFG_ALIAS_ANALYSIS_H + +#include "TritonToGraph/tensor.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/TypeUtilities.h" +#include "mlir/IR/Types.h" +#include "mlir/IR/Value.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" + +#include + +namespace mlir { +namespace triton { +namespace cfg { + +class ControlFlowGraph; +class DataFlowGraph; + +// AliasAnalysis - Alias分析和指针跟踪 +// 用于分析pointer的alias关系,跟踪全局内存对象(如gm_obj)的别名链 +class AliasAnalysis { +public: + // 为整个CFG分析pointer别名 + void analyzePointerAliases(ControlFlowGraph &cfg); + + // 获取Value的base pointer(递归查找真实的基础指针) + Value getBasePointer(Value ptr) const; + + // 获取Value对应的TensorObject + TensorObject *getTensorObject(Value value) const { + auto it = baseTensorMap.find(value); + return (it != baseTensorMap.end()) ? it->second : nullptr; + } + + // 判断两个指针是否可能指向同一tensor + bool mayAlias(Value ptr1, Value ptr2) const { + return getBasePointer(ptr1) == getBasePointer(ptr2); + } + + // 添加alias关系 + void addAlias(Value ptr, Value base, TensorObject *tensor) { + aliasMap[ptr] = base; + baseTensorMap[ptr] = tensor; + } + + // 判断是否是指针类型 + static bool isPointerType(Type type) { + if (auto ptrType = + mlir::dyn_cast(getElementTypeOrSelf(type))) { + return true; + } + return false; + } + + // 判断是否是tensor pointer类型(指向tensor的指针) + static bool isTensorPointerType(Type type) { + if (auto ptrType = mlir::dyn_cast(type)) { + Type pointeeType = ptrType.getPointeeType(); + return mlir::isa(pointeeType); + } + return false; + } + + // 判断是否是标量指针类型(指向标量的指针) + static bool isScalarPointerType(Type type) { + if (auto ptrType = mlir::dyn_cast(type)) { + Type pointeeType = ptrType.getPointeeType(); + return !mlir::isa(pointeeType); + } + return false; + } + + // 判断是否是全局内存类型 + static bool isGlobalMemoryType(Type type) { + // 在Triton中,全局内存通常由!tt.ptr类型表示 + // 可以进一步根据地址空间等信息判断 + if (auto ptrType = mlir::dyn_cast(type)) { + // 地址空间1通常是全局内存 + // 需要根据具体硬件进行调整 + return ptrType.getAddressSpace() == 1 || ptrType.getAddressSpace() == 0; + } + return false; + } + + // 获取所有tracked的base pointer + const DenseMap &getAliasMap() const { return aliasMap; } + + // 获取所有tracked的tensor对象 + const DenseMap &getBaseTensorMap() const { + return baseTensorMap; + } + + // 打印所有alias信息 + void print(llvm::raw_ostream &os) const { + os << "=== Alias Analysis Result ===\n"; + os << "Total tracked aliases: " << aliasMap.size() << "\n"; + + for (const auto &entry : aliasMap) { + Value ptr = entry.first; + Value base = entry.second; + TensorObject *tensor = baseTensorMap.lookup(ptr); + + os << " " << ptr << " -> " << base; + if (tensor) { + os << " [" << tensor->getName() << "]"; + } + os << "\n"; + } + } + +private: + friend class DataFlowGraph; + + bool beginDataFlowGraphBorrow(); + void endDataFlowGraphBorrow(); + + // 分析addptr操作 + void analyzeAddPtrOp(mlir::triton::AddPtrOp addptrOp); + + // 分析make_tensor_ptr操作 + void analyzeMakeTensorPtrOp(mlir::triton::MakeTensorPtrOp op); + + // 分析load操作 + void analyzeLoadOp(mlir::triton::LoadOp loadOp); + + // 分析store操作 + void analyzeStoreOp(mlir::triton::StoreOp storeOp); + + // 分析broadcast操作 + void analyzeBroadcastOp(mlir::triton::BroadcastOp broadcastOp); + + // 分析splat操作 + void analyzeSplatOp(mlir::triton::SplatOp splatOp); + + void analyzeExpandDimsOp(mlir::triton::ExpandDimsOp expandDimsOp); + + SmallVector> ownedTensorObjects; + DenseMap aliasMap; // ptr -> base ptr + DenseMap baseTensorMap; // value -> tensor + bool dataFlowGraphBorrowed = false; +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_ALIAS_ANALYSIS_H diff --git a/third_party/ascend/include/TritonToGraph/CMakeLists.txt b/third_party/ascend/include/TritonToGraph/CMakeLists.txt new file mode 100644 index 0000000000..5ffe7ae0cb --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonToGraph) +add_public_tablegen_target(TritonToGraphPassIncGen) diff --git a/third_party/ascend/include/TritonToGraph/ControlFlowGraph.h b/third_party/ascend/include/TritonToGraph/ControlFlowGraph.h new file mode 100644 index 0000000000..53e32ebb11 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/ControlFlowGraph.h @@ -0,0 +1,266 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_CONTROL_FLOW_GRAPH_H +#define TRITON_TO_CFG_CONTROL_FLOW_GRAPH_H + +#include "TritonToGraph/MemorySSA.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Region.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/MapVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// 前向声明 +class BasicBlock; +class ControlFlowGraph; + +// Instruction 结构体:表示一个指令 +class Instruction { +public: + Instruction(size_t id, Operation *op, BasicBlock *parentBlock) + : id(id), operation(op), parentBlock(parentBlock), memorySSAInfo() {} + + // 获取基本信息 + size_t getId() const { return id; } + Operation *getOperation() const { return operation; } + BasicBlock *getParentBlock() const { return parentBlock; } + + // 检查是否有子图(内部区域) + bool hasSubGraph() const { return subGraph != nullptr; } + ControlFlowGraph *getSubGraph() const { return subGraph.get(); } + void setSubGraph(std::unique_ptr graph) { + subGraph = std::move(graph); + } + + // 获取指令的字符串表示 + std::string getAsString() const; + + // 打印 + void print(raw_ostream &os, unsigned indent = 0) const; + void dump() const; + + // Memory SSA信息 + MemorySSAInfo &getMemorySSAInfo() { return memorySSAInfo; } + const MemorySSAInfo &getMemorySSAInfo() const { return memorySSAInfo; } + +private: + size_t id; // 唯一ID + Operation *operation; // 对应的 MLIR Operation + BasicBlock *parentBlock; // 所属的 BasicBlock + std::unique_ptr + subGraph; // 子图(用于 reduce 等有内部区域的操作) + MemorySSAInfo memorySSAInfo; // Memory SSA信息(用于tensor/pointer分析) +}; + +// 基本块类型 +enum class BlockType { + NORMAL, // 普通块(包含多个指令) + ENTRY, // 函数入口块 + EXIT, // 函数出口块 + IF_COND, // if 条件判断块(包含单个 scf.if 指令) + FOR_COND, // for 循环头块(包含单个 scf.for 指令) + WHILE_COND, // while 条件块(包含单个 scf.while 指令) + COND_BR, // cf.cond_br 条件分支块(包含单个 cf.cond_br 指令) + BR, // cf.br 无条件跳转块(包含单个 cf.br 指令) + LOOP_BODY, // 循环体块 + LOOP_EXIT, // 循环出口块 +}; + +// 基本块节点 +class BasicBlock { +public: + BasicBlock(size_t id, BlockType type, BasicBlock *parentStructure = nullptr) + : id(id), type(type), parentStructure(parentStructure) {} + + // 获取基本信息 + size_t getId() const { return id; } + BlockType getType() const { return type; } + void setType(BlockType t) { type = t; } + + // 外层结构(如果是嵌套在 loop/if 中) + BasicBlock *getParentStructure() const { return parentStructure; } + void setParentStructure(BasicBlock *parent) { parentStructure = parent; } + + // 对应控制流结构的出口块(用于 IF_COND/FOR_COND/WHILE_COND) + // 指向 if/for/while 结束后到达的基本块 + BasicBlock *getExitBlock() const { return exitBlock; } + void setExitBlock(BasicBlock *exit) { exitBlock = exit; } + + // Instruction 操作 + void addInstruction(std::unique_ptr inst); + Instruction *getInstruction(size_t idx) const; + size_t getNumInstructions() const { return instructions.size(); } + const SmallVector> &getInstructions() const { + return instructions; + } + + // 检查最后一条指令是否为 ReturnOp + bool endsWithReturnOp() const; + + // 获取后继和前驱 + ArrayRef getSuccessors() const { return successors; } + ArrayRef getPredecessors() const { return predecessors; } + + // 边的操作 + void addSuccessor(BasicBlock *succ); + void addPredecessor(BasicBlock *pred); + + size_t getNumSuccessors() const { return successors.size(); } + size_t getNumPredecessors() const { return predecessors.size(); } + + // 获取名称 + std::string getName() const; + + // 获取类型字符串 + StringRef getTypeString() const; + + // 打印 + void print(raw_ostream &os) const; + void dump() const; + + // 导出为 JSON(用于网页可视化) + void exportToJSON(raw_ostream &os, unsigned indent = 0) const; + +private: + size_t id; // 唯一ID + BlockType type; // 块类型 + BasicBlock *parentStructure; // 外层结构(loop/if)的 basic block + BasicBlock *exitBlock = + nullptr; // 对应控制流结构的出口块(用于 IF_COND/FOR_COND/WHILE_COND) + SmallVector> instructions; // 指令列表 + SmallVector successors; // 后继块指针列表 + SmallVector predecessors; // 前驱块指针列表 +}; + +// 控制流图 +class ControlFlowGraph { +public: + explicit ControlFlowGraph(triton::FuncOp func); + ~ControlFlowGraph(); + + // 获取函数 + triton::FuncOp getFunction() const { return function; } + + // 基本块操作 + BasicBlock *createBasicBlock(BlockType type, + BasicBlock *parentStructure = nullptr); + + BasicBlock *getBasicBlock(size_t id) { + if (id < basicBlocks.size()) + return basicBlocks[id].get(); + return nullptr; + } + const BasicBlock *getBasicBlock(size_t id) const { + if (id < basicBlocks.size()) + return basicBlocks[id].get(); + return nullptr; + } + + size_t getNumBlocks() const { return basicBlocks.size(); } + + // 获取入口和出口块 + BasicBlock *getEntryBlock() { return entryBlock; } + const BasicBlock *getEntryBlock() const { return entryBlock; } + BasicBlock *getExitBlock() { return exitBlock; } + const BasicBlock *getExitBlock() const { return exitBlock; } + + void setEntryBlock(BasicBlock *bb) { entryBlock = bb; } + void setExitBlock(BasicBlock *bb) { exitBlock = bb; } + + // 添加边 + void addEdge(BasicBlock *from, BasicBlock *to); + + // 遍历 + using BlockVisitor = llvm::function_ref; + void traverse(BlockVisitor visitor); + + // Operation 到 Instruction 的查询 + Instruction *getInstruction(Operation *op) const { + auto it = opToInstructionMap.find(op); + return (it != opToInstructionMap.end()) ? it->second : nullptr; + } + + // 添加 Operation 到 Instruction 的映射 + void addOpToInstruction(Operation *op, Instruction *inst) { + opToInstructionMap[op] = inst; + } + + bool isBackEdge(BasicBlock *from, BasicBlock *to) const; + + // 结构化搜索 API + // 从起始块开始,沿着 successor 顺序向下结构化搜索 + // NORMAL 块:遍历每个 Instruction,调用 callback 传入 Operation* + // IF_COND/FOR_COND/WHILE_COND 块:递归访问每个 successor + // 遇到 exitBlock 时停止 + using OperationVisitor = llvm::function_ref; + void searchNormalBlock(BasicBlock *block, OperationVisitor callback) const; + void searchCondBlock(BasicBlock *block, OperationVisitor callback) const; + void searchBlock(BasicBlock *block, OperationVisitor callback) const; + + // 打印 + void print(raw_ostream &os) const; + void dump() const; + + // 导出为 DOT 格式 + void exportDOT(raw_ostream &os) const; + + // 导出到文件 + llvm::Error exportToFile(StringRef filename) const; + + // 导出为 HTML(网页可视化) + llvm::Error exportToHTML(StringRef filename) const; + + // 导出为 JSON + void exportToJSON(raw_ostream &os) const; + +private: + triton::FuncOp function; // 所属函数 + SmallVector> basicBlocks; // 基本块节点 + BasicBlock *entryBlock = nullptr; // 入口块 + BasicBlock *exitBlock = nullptr; // 出口块 + size_t nextBlockId = 0; // 下一个块ID + + // Operation 到 Instruction 的映射(支持快速查询) + std::unordered_map opToInstructionMap; +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_CONTROL_FLOW_GRAPH_H diff --git a/third_party/ascend/include/TritonToGraph/ControlFlowGraphBuilder.h b/third_party/ascend/include/TritonToGraph/ControlFlowGraphBuilder.h new file mode 100644 index 0000000000..c76ddf2072 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/ControlFlowGraphBuilder.h @@ -0,0 +1,242 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_CONTROL_FLOW_GRAPH_BUILDER_H +#define TRITON_TO_CFG_CONTROL_FLOW_GRAPH_BUILDER_H + +#include "TritonToGraph/ControlFlowGraph.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Value.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassRegistry.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// 构建控制流图的 Pass +class BuildCFGPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(BuildCFGPass) + + BuildCFGPass() = default; + + StringRef getArgument() const override { return "build-cfg"; } + StringRef getDescription() const override { + return "Build Control Flow Graph from TTIR"; + } + + void runOnOperation() override; + + // Pass 选项 + std::string outputDir = "."; + + // 获取依赖的方言 + void getDependentDialects(DialectRegistry ®istry) const override { + registry.insert(); + } + +protected: + // 构建单个函数的 CFG + std::unique_ptr buildForFunction(triton::FuncOp func); +}; + +// 添加数据结构定义 +// IF_COND 的 yield value 和 result value 的对应关系 +struct IfYieldResultMapping { + // then 分支的 yield values(来自 scf.yield 操作的操作数) + SmallVector thenYieldValues; + // else 分支的 yield values(如果有 else 分支) + SmallVector elseYieldValues; + // if 操作的 result values + SmallVector resultValues; + // 对应关系: resultValues[i] 对应 thenYieldValues[i] 或 elseYieldValues[i] +}; + +// FOR_COND 的 yield value 和 iter args value 的对应关系 +struct ForYieldIterArgMapping { + // yield 操作的 values(来自循环体末尾的 scf.yield) + SmallVector yieldValues; + // iter_args(循环初始参数,对应 for 操作的 iter_args) + SmallVector iterArgValues; + // for 操作的 result values + SmallVector resultValues; + // 对应关系: iterArgValues[i] 在循环体中使用时被更新,yieldValues[i] 是新的值 + // resultValues[i] 对应最后一次迭代的 yieldValues[i] +}; + +// COND_BR 的 true/false 分支信息 +struct CondBranchMapping { + // true 分支的目标块参数值(来自 cf.cond_br 的 trueOperands) + SmallVector trueOperands; + // false 分支的目标块参数值(来自 cf.cond_br 的 falseOperands) + SmallVector falseOperands; + // 条件值 + Value condition; + // true 分支目标块 + Block *trueDest; + // false 分支目标块 + Block *falseDest; +}; + +// BR 的分支信息 +struct BranchMapping { + // 目标块参数值(来自 cf.br 的 destOperands) + SmallVector destOperands; + // 目标块 + Block *dest; +}; + +// 独立的 CFG 构建器类(用于非 Pass 场景) +class ControlFlowGraphBuilder { +public: + // 为函数构建 CFG + std::unique_ptr build(triton::FuncOp func); + + // 为模块构建所有函数的 CFG + std::vector> + buildForModule(ModuleOp module); + + // 处理一个 region,返回该 region 的入口块和出口块 + struct RegionBlocks { + cfg::BasicBlock *entryBlock; + cfg::BasicBlock *exitBlock; + }; + + RegionBlocks buildForRegion(Region ®ion, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *entryBlock, + cfg::BasicBlock *parentStructure = nullptr); + + // 处理 block 中的操作,返回最后处理的基本块 + cfg::BasicBlock *processBlock(Block &block, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *currentBB, + cfg::BasicBlock *parentStructure = nullptr); + + // 处理 scf.if 操作,返回 if 后面的基本块 + cfg::BasicBlock *handleIfOp(scf::IfOp ifOp, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *currentBB, + cfg::BasicBlock *parentStructure = nullptr); + + // 处理 scf.for 操作,返回 for 后面的基本块 + cfg::BasicBlock *handleForOp(scf::ForOp forOp, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *currentBB, + cfg::BasicBlock *parentStructure = nullptr); + + // 处理 scf.while 操作,返回 while 后面的基本块 + cfg::BasicBlock *handleWhileOp(scf::WhileOp whileOp, + cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *currentBB, + cfg::BasicBlock *parentStructure = nullptr); + + // 处理 cf.cond_br 操作,返回条件分支后面的基本块 + cfg::BasicBlock * + handleCondBranchOp(cf::CondBranchOp condBrOp, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *currentBB, + cfg::BasicBlock *parentStructure = nullptr); + + // 处理 cf.br 操作,返回无条件跳转后面的基本块 + cfg::BasicBlock *handleBranchOp(cf::BranchOp brOp, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *currentBB, + cfg::BasicBlock *parentStructure = nullptr); + + // 创建一个新的指令并添加到 basic block + cfg::Instruction *createInstruction(Operation *op, + cfg::BasicBlock *parentBlock, + cfg::ControlFlowGraph &cfg); + + // 1. 快速收集所有的 IF_COND 基本块 + // 遍历 CFG 中所有基本块,返回类型为 IF_COND 的基本块列表 + SmallVector + collectIfCondBlocks(cfg::ControlFlowGraph &cfg); + + // 2. 快速收集所有的 FOR_COND 基本块 + // 遍历 CFG 中所有基本块,返回类型为 FOR_COND 的基本块列表 + SmallVector + collectForCondBlocks(cfg::ControlFlowGraph &cfg); + + // 3. 获取 IF_COND 对应的 yield value 和 result value 的对应关系 + // 参数: IF_COND 类型的基本块 + // 返回: IfYieldResultMapping 结构体,包含 then/else 的 yield values 和 result + // values + std::optional + getIfYieldResultMapping(cfg::BasicBlock *ifCondBB); + + // 4. 获取 FOR_COND 对应的 yield value 和 iter args value 的对应关系 + // 参数: FOR_COND 类型的基本块 + // 返回: ForYieldIterArgMapping 结构体,包含 yield values、iter_args 和 result + // values + std::optional + getForYieldIterArgMapping(cfg::BasicBlock *forCondBB); + + // 5. 快速收集所有的 COND_BR 基本块 + // 遍历 CFG 中所有基本块,返回类型为 COND_BR 的基本块列表 + SmallVector + collectCondBrBlocks(cfg::ControlFlowGraph &cfg); + + // 6. 获取 COND_BR 对应的条件分支信息 + // 参数: COND_BR 类型的基本块 + // 返回: CondBranchMapping 结构体,包含条件、目标块和参数信息 + std::optional + getCondBranchMapping(cfg::BasicBlock *condBrBB); + + // 7. 快速收集所有的 BR 基本块 + // 遍历 CFG 中所有基本块,返回类型为 BR 的基本块列表 + SmallVector collectBrBlocks(cfg::ControlFlowGraph &cfg); + + // 8. 获取 BR 对应的分支信息 + // 参数: BR 类型的基本块 + // 返回: BranchMapping 结构体,包含目标块和参数信息 + std::optional getBranchMapping(cfg::BasicBlock *brBB); + + // 获取下一个指令 ID + size_t getNextInstructionId() { return nextInstructionId++; } + + // MLIR Block 到 CFG BasicBlock 的映射(用于处理 cf.cond_br 等跳转指令) + DenseMap blockToBasicBlockMap; + + // 获取或创建 Block 对应的 BasicBlock + cfg::BasicBlock * + getOrCreateBasicBlockForBlock(Block *block, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *parentStructure = nullptr); + + // 注册 Block 到 BasicBlock 的映射 + void registerBlockMapping(Block *mlirBlock, cfg::BasicBlock *cfgBlock); + +private: + size_t nextInstructionId = 0; // 下一个指令 ID +}; + +// 创建 Pass 的工厂函数 +std::unique_ptr> createBuildCFGPass(); + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_CONTROL_FLOW_GRAPH_BUILDER_H diff --git a/third_party/ascend/include/TritonToGraph/DataflowGraph.h b/third_party/ascend/include/TritonToGraph/DataflowGraph.h new file mode 100644 index 0000000000..3e58c3765d --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/DataflowGraph.h @@ -0,0 +1,278 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_DATAFLOW_GRAPH_H +#define TRITON_TO_CFG_DATAFLOW_GRAPH_H + +#include "TritonToGraph/AliasAnalysis.h" +#include "TritonToGraph/ControlFlowGraph.h" +#include "TritonToGraph/MemorySSA.h" +#include "TritonToGraph/MemorySsaBuilder.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Casting.h" +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// 前向声明结果类 +class DataFlowResult; +class MemorySSAResult; +class SSAResult; + +// DataFlowInfo - 统一的数据流信息 +class DataFlowInfo { +public: + // 为函数入口创建参数定义 + void createParameterDefinitions(triton::FuncOp func); + + // Memory SSA接口 + MemorySSADef *getMemoryDefinition(Value value) const; + void addMemoryDefinition(Value value, MemorySSADef *def); + + SmallVector getMemoryUses(Value value) const; + void addMemoryUse(Value value, const MemorySSAUse &use); + + void removeMemoryDefinition(Value value); + void clearMemoryUses(Value value); + + // 传统SSA接口(复用MLIR原生功能) + Operation *getSSADefinition(Value value) const { + return value.getDefiningOp(); + } + + SmallVector getSSAUses(Value value) const { + SmallVector result; + for (OpOperand &use : value.getUses()) { + result.push_back(&use); + } + return result; + } + + // 循环Phi接口 + void addPhi(Value value, const PhiInfo &phiInfo) { Phis[value] = phiInfo; } + + PhiInfo &getPhi(Value value) { return Phis[value]; } + + bool hasPhi(Value value) const { return Phis.count(value) > 0; } + + // 统一查询接口 - 返回unique_ptr,使用LLVM RTTI进行类型判断 + std::unique_ptr queryDataFlow(Value value) const; + + // 查询某个定义的所有使用 + SmallVector getUses(MemorySSADef *def) const; + + // 查询某个操作的memory使用 + SmallVector getUsesByUserOp(Operation *userOp) const; + + // 遍历接口 + void + forEachDefinition(llvm::function_ref func) const; + void forEachUse(llvm::function_ref func) const; + + // 获取所有Memory SSA definitions + const DenseMap &getMemoryDefinitions() const { + return memoryDefinitions; + } + + // 获取所有循环phi信息 + const DenseMap &getPhis() const { return Phis; } + + // 构建def-use缓存 + void buildDefUseCache() const; + + // 打印信息(调试用) + void print(llvm::raw_ostream &os) const; + + // 导出到JSON + void exportToJSON(llvm::raw_ostream &os) const; + +private: + // Memory SSA映射 + DenseMap memoryDefinitions; + DenseMap> memoryUses; + + // Loop Phi映射 + DenseMap Phis; + + // Use-Def映射缓存(def -> uses) + mutable DenseMap> defUseCache; + mutable bool defUseCacheValid = false; + + void invalidateDefUseCache() { + defUseCacheValid = false; + defUseCache.clear(); + } +}; + +// DataFlowResult - 数据流查询结果的基类 +// 使用LLVM RTTI系统,支持isa<>和dyn_cast<>进行类型判断 +class DataFlowResult { +public: + enum class Kind { + MemorySSA, // Memory SSA结果(tensor/pointer) + SSA, // 传统SSA结果(标量) + NONE // 无数据流信息 + }; + + DataFlowResult(Kind kind, Operation *originOp) + : kind(kind), originOp(originOp) {} + virtual ~DataFlowResult() = default; + + Kind getKind() const { return kind; } + Operation *getOriginOp() const { return originOp; } + + SmallVector &getUses() { return uses; } + const SmallVector &getUses() const { return uses; } + + std::optional &getPhi() { return Phi; } + const std::optional &getPhi() const { return Phi; } + + // LLVM RTTI支持 + static bool classof(const DataFlowResult *) { return true; } + +protected: + Kind kind; + Operation *originOp; + SmallVector uses; // 所有uses + std::optional Phi; // Phi信息(如果有) +}; + +// MemorySSAResult - Memory SSA的结果 +class MemorySSAResult : public DataFlowResult { +public: + MemorySSAResult(Operation *originOp, MemorySSADef *definition) + : DataFlowResult(Kind::MemorySSA, originOp), definition(definition) {} + + MemorySSADef *getDefinition() const { return definition; } + + // LLVM RTTI支持 + static bool classof(const DataFlowResult *result) { + return result->getKind() == Kind::MemorySSA; + } + +private: + MemorySSADef *definition; // MEMORY_SSA时使用 +}; + +// SSAResult - 传统SSA的结果 +class SSAResult : public DataFlowResult { +public: + SSAResult(Operation *originOp, Operation *ssaDefinition) + : DataFlowResult(Kind::SSA, originOp), ssaDefinition(ssaDefinition) {} + + Operation *getSSADefinition() const { return ssaDefinition; } + + // LLVM RTTI支持 + static bool classof(const DataFlowResult *result) { + return result->getKind() == Kind::SSA; + } + +private: + Operation *ssaDefinition; // SSA时使用 +}; + +// NoneResult - 无数据流信息的结果 +class NoneResult : public DataFlowResult { +public: + NoneResult() : DataFlowResult(Kind::NONE, nullptr) {} + + // LLVM RTTI支持 + static bool classof(const DataFlowResult *result) { + return result->getKind() == Kind::NONE; + } +}; + +// DataFlowGraph - 数据流图 +class DataFlowGraph { +public: + DataFlowGraph(ControlFlowGraph &cfg, AliasAnalysis &aliasAnalysis) + : cfg(cfg), aliasAnalysis(aliasAnalysis) {} + + ~DataFlowGraph(); + + // 构建完整的数据流信息 + void build(); + + // 查询Value的数据流信息(使用LLVM RTTI判断具体类型) + std::unique_ptr queryDataFlow(Value value) const { + return dataFlowInfo.queryDataFlow(value); + } + + // 获取所有Memory SSA definitions + SmallVector getAllDefinitions() const { + SmallVector result; + for (const auto &entry : dataFlowInfo.getMemoryDefinitions()) { + result.push_back(entry.second); + } + return result; + } + + // 获取definition的所有uses + SmallVector getUses(MemorySSADef *def) const { + return dataFlowInfo.getUses(def); + } + + // 获取操作的所有uses + SmallVector getUsesByUserOp(Operation *userOp) const { + return dataFlowInfo.getUsesByUserOp(userOp); + } + + // 获取CFG + ControlFlowGraph &getCFG() { return cfg; } + const ControlFlowGraph &getCFG() const { return cfg; } + + // 获取DataFlowInfo + DataFlowInfo &getDataFlowInfo() { return dataFlowInfo; } + const DataFlowInfo &getDataFlowInfo() const { return dataFlowInfo; } + + // 导出数据流信息到JSON + void exportToJSON(llvm::raw_ostream &os) const; + + // 导出def-use链到DOT格式 + void exportDefUseToDOT(llvm::raw_ostream &os) const; + + // 打印所有数据流信息(调试用) + void print(llvm::raw_ostream &os) const; + void dump() const; + +private: + ControlFlowGraph &cfg; + AliasAnalysis &aliasAnalysis; + + // The builder borrows alias analysis and must be destroyed before it. + std::unique_ptr memorySSABuilder; + DataFlowInfo dataFlowInfo; + + // 构建def-use图 + void buildDefUseGraph(); +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_DATAFLOW_GRAPH_H diff --git a/third_party/ascend/include/TritonToGraph/DotRowCoalescing.h b/third_party/ascend/include/TritonToGraph/DotRowCoalescing.h new file mode 100644 index 0000000000..7517b84762 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/DotRowCoalescing.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_DOT_ROW_COALESCING_H +#define TRITON_TO_GRAPH_DOT_ROW_COALESCING_H + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// Return a privately rewritten and verified plan for independent dot rows. +// Unsupported inputs return nullptr without changing the original IR. +std::unique_ptr createDotRowCoalescingPlan(triton::FuncOp function, + unsigned epoch); + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_GRAPH_DOT_ROW_COALESCING_H diff --git a/third_party/ascend/include/TritonToGraph/EntryArgPointerAliasAnalysis.h b/third_party/ascend/include/TritonToGraph/EntryArgPointerAliasAnalysis.h new file mode 100644 index 0000000000..7fc0e83f7a --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/EntryArgPointerAliasAnalysis.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_ENTRY_ARG_POINTER_ALIAS_ANALYSIS_H +#define TRITON_TO_GRAPH_ENTRY_ARG_POINTER_ALIAS_ANALYSIS_H + +#include "mlir/IR/Value.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/SmallVector.h" + +#include + +namespace mlir { +namespace triton { +namespace cfg { + +class AliasAnalysis; + +// Relationship between pointer values that can be traced to global pointer +// arguments of the current tt.func. Distinct roots use the graph-optimization +// ABI assumption that distinct entry pointers do not share storage. +enum class EntryArgPointerRelation : uint8_t { + SameEntryRoot, + DistinctEntryRoots, + Unknown, +}; + +// Classifies pointer values by their entry-function global pointer root. This +// intentionally builds on AliasAnalysis provenance without changing the +// language-level semantics of AliasAnalysis::mayAlias(). +class EntryArgPointerAliasAnalysis { +public: + EntryArgPointerAliasAnalysis(triton::FuncOp function, + const AliasAnalysis &aliasAnalysis); + + EntryArgPointerRelation classify(Value lhs, Value rhs) const; + +private: + // Returns the current function's global pointer block argument from which + // pointer is derived, or a null Value when provenance is unsupported. + Value getEntryPointerRoot(Value pointer) const; + + const AliasAnalysis &aliasAnalysis; + SmallVector entryPointerRoots; +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_GRAPH_ENTRY_ARG_POINTER_ALIAS_ANALYSIS_H diff --git a/third_party/ascend/include/TritonToGraph/GraphAnalysis.h b/third_party/ascend/include/TritonToGraph/GraphAnalysis.h new file mode 100644 index 0000000000..6652ab1ad3 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/GraphAnalysis.h @@ -0,0 +1,402 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + */ + +#ifndef TRITON_TO_GRAPH_GRAPH_ANALYSIS_H +#define TRITON_TO_GRAPH_GRAPH_ANALYSIS_H + +#include "TritonToGraph/ControlFlowGraph.h" +#include "TritonToGraph/DataflowGraph.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include + +namespace mlir { +namespace triton { +namespace cfg { + +//===----------------------------------------------------------------------===// +// Traversal Context - 遍历时维护上下文 +//===----------------------------------------------------------------------===// + +struct TraversalContext { + // 当前所处的嵌套结构栈(从外到内) + SmallVector structureStack; + + // 当前深度 + int depth = 0; + + void push(BasicBlock *structure) { structureStack.push_back(structure); } + void pop() { + if (!structureStack.empty()) + structureStack.pop_back(); + } + BasicBlock *currentStructure() const { + return structureStack.empty() ? nullptr : structureStack.back(); + } + bool isEmpty() const { return structureStack.empty(); } +}; + +/// CFGTraversalBase - CFG 遍历基类 +/// 用户继承此类,实现 preVisit/postVisit 回调 +/// 可在子类中添加自定义状态字段 +class CFGTraversalBase { +public: + virtual ~CFGTraversalBase() = default; + + // 访问基本块前调用,返回 false 则跳过此块 + virtual bool preVisitBlock(BasicBlock *block, TraversalContext &ctx); + + // 访问基本块后调用 + virtual void postVisitBlock(BasicBlock *block, TraversalContext &ctx) {} + + // 访问指令后调用 + virtual void VisitInstruction(Instruction *inst, TraversalContext &ctx) {} + + // 遇到控制流结构(for/if/while)时调用 + virtual void onEnterStructure(BasicBlock *structure, TraversalContext &ctx) {} + virtual void onExitStructure(BasicBlock *structure, TraversalContext &ctx) {} + + // 遇到回边时调用 + virtual void onBackEdge(BasicBlock *from, BasicBlock *to, + TraversalContext &ctx) {} +}; + +/// CFGTraverser - CFG 遍历器(双向支持) +class CFGTraverser { +public: + explicit CFGTraverser(ControlFlowGraph &cfg) : cfg(cfg) {} + + //===----------------------------------------------------------------------=== + // Forward Traversal (沿后继节点向下遍历) + //===----------------------------------------------------------------------=== + + /// 从入口开始的 DFS + void dfsForward(CFGTraversalBase &visitor); + + /// 从指定块开始的 DFS + void dfsForward(BasicBlock *start, CFGTraversalBase &visitor); + + /// BFS 遍历 + void bfsForward(CFGTraversalBase &visitor); + void bfsForward(BasicBlock *start, CFGTraversalBase &visitor); + + //===----------------------------------------------------------------------=== + // Backward Traversal (沿前驱节点向上遍历) + //===----------------------------------------------------------------------=== + + /// 从指定块反向 DFS(沿前驱遍历) + void dfsBackward(BasicBlock *start, CFGTraversalBase &visitor); + + /// 从指定块反向 BFS + void bfsBackward(BasicBlock *start, CFGTraversalBase &visitor); + +private: + ControlFlowGraph &cfg; + + void dfsForwardImpl(BasicBlock *block, DenseSet &visited, + TraversalContext &ctx, CFGTraversalBase &visitor); + void dfsBackwardImpl(BasicBlock *block, DenseSet &visited, + TraversalContext &ctx, CFGTraversalBase &visitor); +}; + +//===----------------------------------------------------------------------===// +// Curly Recursive Template Pattern for DFG Traversal +//===----------------------------------------------------------------------===// + +/// DFGTraversalBase - DFG 遍历基类 +class DFGTraversalBase { +public: + virtual ~DFGTTraversalBase() = default; + + // 访问定义前调用(反向遍历 value -> def) + virtual bool VisitDef(Value value, Operation *defOp, int depth); + + // 访问使用前调用(正向遍历 value -> use) + virtual bool VisitUse(Value value, OpOperand *use, int depth); + + // 遇到 phi/iter_arg 时调用 + virtual void onPhi(Value phiValue, const PhiInfo &phiInfo, int depth) {} +}; + +/// DFGTraverser - DFG 遍历器(双向 SSA/MemorySSA) +class DFGTraverser { +public: + struct Options { + bool useMemorySSA = false; // false=传统 SSA, true=Memory SSA + bool followPhi = true; // 是否跨越 phi/iter_arg + int maxDepth = -1; // -1 = 无限制 + DenseSet stopOps; // 遇到停止的操作 + }; + + explicit DFGTraverser(DataFlowGraph &dfg) : dfg(dfg) {} + + //===----------------------------------------------------------------------=== + // Backward Traversal (value -> definitions) + //===----------------------------------------------------------------------=== + + /// 从 value 开始反向追踪所有定义 + void dfsBackward(Value seed, DFGTraversalBase &visitor, + const Options &opts = {}); + + /// 多起点反向 DFS + void dfsBackward(ArrayRef seeds, DFGTraversalBase &visitor, + const Options &opts = {}); + + /// BFS 反向追踪 + void bfsBackward(Value seed, DFGTraversalBase &visitor, + const Options &opts = {}); + + //===----------------------------------------------------------------------=== + // Forward Traversal (value -> uses) + //===----------------------------------------------------------------------=== + + /// 从 value 开始正向追踪所有使用 + void dfsForward(Value seed, DFGTraversalBase &visitor, + const Options &opts = {}); + + /// 多起点正向 DFS + void dfsForward(ArrayRef seeds, DFGTraversalBase &visitor, + const Options &opts = {}); + + /// BFS 正向追踪 + void bfsForward(Value seed, DFGTraversalBase &visitor, + const Options &opts = {}); + + //===----------------------------------------------------------------------=== + // Bidirectional + //===----------------------------------------------------------------------=== + + /// 从 value 双向遍历(先 backward 到根,再 forward 到所有 uses) + void traverseBidirectional(Value seed, DFGTraversalBase &visitor, + const Options &opts = {}); + +private: + DataFlowGraph &dfg; + + void dfsBackwardImpl(Value value, DFGTraversalBase &visitor, + DenseSet &visited, const Options &opts, + int depth); + void dfsForwardImpl(Value value, DFGTraversalBase &visitor, + DenseSet &visited, const Options &opts, + int depth); +}; + +//===----------------------------------------------------------------------===// +// Region Abstraction and Analysis +//===----------------------------------------------------------------------===// + +/// Region - 指令集合(替代原始代码中的 SmallVector) +class Region { +public: + explicit Region(StringRef name = "") : name_(name.str()) {} + + void add(Instruction *inst); + void add(Operation *op, ControlFlowGraph &cfg); + void addAll(ArrayRef insts); + + bool contains(Instruction *inst) const; + bool contains(Operation *op) const; + + void remove(Instruction *inst); + void clear(); + + size_t size() const { return instSet_.size(); } + bool empty() const { return instSet_.empty(); } + + // 获取按块内顺序排序的指令列表 + SmallVector orderedInstructions() const; + + // 获取所有操作 + SmallVector operations() const; + + StringRef name() const { return name_; } + void setName(StringRef name) { name_ = name.str(); } + + // 迭代器支持 + auto begin() const { return instSet_.begin(); } + auto end() const { return instSet_.end(); } + +private: + std::string name_; + DenseSet instSet_; +}; + +/// RegionAnalyzer - Region 分析器 +class RegionAnalyzer { +public: + struct Dependency { + enum Type { DATA, CONTROL }; + Type type; + Value value; + Instruction *from; + Instruction *to; + }; + + struct ExternalDeps { + // 外部定义 -> 内部使用 + struct Input { + Value value; + Instruction *externalDef; // null 表示函数参数 + SmallVector internalUses; + }; + // 内部定义 -> 外部使用 + struct Output { + Value value; + Instruction *internalDef; + SmallVector externalUses; + }; + SmallVector inputs; + SmallVector outputs; + }; + + explicit RegionAnalyzer(DataFlowGraph &dfg, ControlFlowGraph &cfg) + : dfg(dfg), cfg(cfg) {} + + // 检查 region 之间是否有依赖 + bool hasDependency(const Region &from, const Region &to) const; + + // 获取两个 region 间的所有依赖 + SmallVector getDependencies(const Region &from, + const Region &to) const; + + // 分析 region 的外部依赖 + ExternalDeps analyzeExternalDeps(const Region ®ion) const; + + // 检查依赖是否为循环依赖(region A 依赖 B,B 又依赖 A) + bool isCyclicDependency(const Region &a, const Region &b) const; + +private: + DataFlowGraph &dfg; + ControlFlowGraph &cfg; +}; + +//===----------------------------------------------------------------------===// +// Program Slicing +//===----------------------------------------------------------------------===// + +/// SliceCriterion - 切片准则 +struct SliceCriterion { + enum Direction { BACKWARD, FORWARD, BIDIRECTIONAL }; + SmallVector seeds; + Direction dir = BACKWARD; + DFGTraverser::Options dfgOpts; +}; + +/// ProgramSlice - 程序切片 +class ProgramSlice { +public: + void add(Instruction *inst) { instructions_.insert(inst); } + void addAll(const Region ®ion); + + bool contains(Instruction *inst) const { + return instructions_.contains(inst); + } + + size_t size() const { return instructions_.size(); } + bool empty() const { return instructions_.empty(); } + + // 获取入口点(没有前驱在切片中) + SmallVector entryPoints(DataFlowGraph &dfg) const; + + // 获取出口点(没有后继在切片中) + SmallVector exitPoints(DataFlowGraph &dfg) const; + + // 集合操作 + void merge(const ProgramSlice &other); + void intersect(const ProgramSlice &other); + void subtract(const ProgramSlice &other); + + // 转换为 Region + Region toRegion(StringRef name = "") const; + + // 迭代器 + auto begin() const { return instructions_.begin(); } + auto end() const { return instructions_.end(); } + +private: + DenseSet instructions_; +}; + +/// ProgramSlicer - 程序切片器 +class ProgramSlicer { +public: + ProgramSlicer(DataFlowGraph &dfg, ControlFlowGraph &cfg) + : dfg(dfg), cfg(cfg) {} + + // 计算切片 + ProgramSlice compute(const SliceCriterion &criterion); + + // 从 yield values 计算切片(常用场景) + ProgramSlice sliceFromYields(ArrayRef yields, + SliceCriterion::Direction dir); + + // 多切片操作 + static ProgramSlice merge(ArrayRef slices); + static ProgramSlice intersect(ArrayRef slices); + + // 检查切片间依赖 + struct SliceDependency { + const ProgramSlice *from; + const ProgramSlice *to; + SmallVector values; + }; + SmallVector + computeDependencies(ArrayRef slices); + +private: + DataFlowGraph &dfg; + ControlFlowGraph &cfg; +}; + +//===----------------------------------------------------------------------===// +// Region Absorption +//===----------------------------------------------------------------------===// + +/// AbsorptionPolicy - 吸收策略 +struct AbsorptionPolicy { + enum Direction { UPSTREAM, DOWNSTREAM, BOTH }; + Direction dir = BOTH; + int maxDepth = -1; + bool crossRegionBoundary = false; + DenseSet stopOps; + std::function shouldStop = nullptr; +}; + +/// RegionAbsorber - Region 吸收器 +class RegionAbsorber { +public: + RegionAbsorber(DataFlowGraph &dfg, ControlFlowGraph &cfg) + : dfg(dfg), cfg(cfg) {} + + // 从种子指令开始吸收 + void absorb(Region ®ion, ArrayRef seeds, + const AbsorptionPolicy &policy); + + // 从 value 的 def/use 链吸收 + void absorbFromValue(Region ®ion, Value value, + const AbsorptionPolicy &policy); + + // 吸收直到遇到边界 + void absorbUntilBoundary(Region ®ion, ArrayRef seeds, + std::function isBoundary); + +private: + DataFlowGraph &dfg; + ControlFlowGraph &cfg; + + void absorbUpstream(Region ®ion, Instruction *inst, + const AbsorptionPolicy &policy, + DenseSet &visited, int depth); + void absorbDownstream(Region ®ion, Instruction *inst, + const AbsorptionPolicy &policy, + DenseSet &visited, int depth); +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_GRAPH_GRAPH_ANALYSIS_H diff --git a/third_party/ascend/include/TritonToGraph/GraphOptimization.h b/third_party/ascend/include/TritonToGraph/GraphOptimization.h new file mode 100644 index 0000000000..1748bc1642 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/GraphOptimization.h @@ -0,0 +1,335 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_H +#define TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +#include +#include +#include +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// The identity of every rule is one bit of the pass rule mask, so the mask +using GraphOptimizationRuleMask = uint32_t; + +enum class GraphOptimizationRuleId : GraphOptimizationRuleMask { + LoadStoreTranspose = 1, + TransposePointwiseReorder = 2, + StoreCoalescing = 4, + // RowCoalescing includes a shared-rhs dot pattern and a pure-SIMT-only + // elementwise pattern. It is intentionally + // scheduled once after the normal per-function phases because its launch + // contract must not be suppressed by their rewrite budget. + RowCoalescing = 8, + // DiagonalMaskRemoval rewrites compute logic rather than memory access, so + // it is architecture and template independent. + DiagonalMaskRemoval = 128, + // ConvertModuloToMask linearizes wrapped tile addresses. It runs before the + // memory-access rules so they see the linear form. + ConvertModuloToMask = 256, + // The following identities are owned by the layout/memory compatibility + // passes. They deliberately are not GraphOptimizationRule candidates and + // are not added to GraphOptimizePass's per-function phase loop. + StridedAxisCoalescing = 16, + ChunkCoalescing = 32, + StridedLoadStoreRewrite = 64, + IndependentAxisTensorize = 512, + PersistentTaskStripMining = 2048, + ResidentLoadForwarding = 4096, + IntermediatePrecisionBoundaryElision = 8192, + StoreCoveragePlanning = 16384, + ContiguousBlockAccessFormation = 32768, +}; + +enum class GraphOptimizationRulePhase : uint8_t { + DiagonalMaskRemoval, + ConvertModuloToMask, + ProgramMapping, + PersistentTaskMapping, + LoadStoreTranspose, + TransposePointwiseReorder, + ResidentLoadForwarding, + IntermediatePrecisionBoundaryElision, + StoreCoveragePlanning, + StoreCoalescing, + ContiguousBlockAccessFormation, + RowCoalescing, + Compatibility, +}; + +constexpr const char * +getGraphOptimizationRuleName(GraphOptimizationRuleId rule) { + switch (rule) { + case GraphOptimizationRuleId::LoadStoreTranspose: + return "LoadStoreTranspose"; + case GraphOptimizationRuleId::TransposePointwiseReorder: + return "TransposePointwiseReorder"; + case GraphOptimizationRuleId::StoreCoalescing: + return "StoreCoalescing"; + case GraphOptimizationRuleId::RowCoalescing: + return "RowCoalescing"; + case GraphOptimizationRuleId::DiagonalMaskRemoval: + return "DiagonalMaskRemoval"; + case GraphOptimizationRuleId::ConvertModuloToMask: + return "ConvertModuloToMask"; + case GraphOptimizationRuleId::StridedAxisCoalescing: + return "StridedAxisCoalescing"; + case GraphOptimizationRuleId::ChunkCoalescing: + return "ChunkCoalescing"; + case GraphOptimizationRuleId::StridedLoadStoreRewrite: + return "StridedLoadStoreRewrite"; + case GraphOptimizationRuleId::IndependentAxisTensorize: + return "IndependentAxisTensorizeRule"; + case GraphOptimizationRuleId::PersistentTaskStripMining: + return "PersistentTaskStripMiningRule"; + case GraphOptimizationRuleId::ResidentLoadForwarding: + return "ResidentLoadForwardingRule"; + case GraphOptimizationRuleId::IntermediatePrecisionBoundaryElision: + return "IntermediatePrecisionBoundaryElisionRule"; + case GraphOptimizationRuleId::StoreCoveragePlanning: + return "StoreCoveragePlanningRule"; + case GraphOptimizationRuleId::ContiguousBlockAccessFormation: + return "ContiguousBlockAccessFormationRule"; + } + return "Unknown"; +} + +constexpr GraphOptimizationRuleMask +getGraphOptimizationRuleMask(GraphOptimizationRuleId rule) { + return static_cast(rule); +} + +constexpr GraphOptimizationRulePhase +getGraphOptimizationRulePhase(GraphOptimizationRuleId rule) { + switch (rule) { + case GraphOptimizationRuleId::DiagonalMaskRemoval: + return GraphOptimizationRulePhase::DiagonalMaskRemoval; + case GraphOptimizationRuleId::ConvertModuloToMask: + return GraphOptimizationRulePhase::ConvertModuloToMask; + case GraphOptimizationRuleId::IndependentAxisTensorize: + return GraphOptimizationRulePhase::ProgramMapping; + case GraphOptimizationRuleId::PersistentTaskStripMining: + return GraphOptimizationRulePhase::PersistentTaskMapping; + case GraphOptimizationRuleId::LoadStoreTranspose: + return GraphOptimizationRulePhase::LoadStoreTranspose; + case GraphOptimizationRuleId::TransposePointwiseReorder: + return GraphOptimizationRulePhase::TransposePointwiseReorder; + case GraphOptimizationRuleId::ResidentLoadForwarding: + return GraphOptimizationRulePhase::ResidentLoadForwarding; + case GraphOptimizationRuleId::IntermediatePrecisionBoundaryElision: + return GraphOptimizationRulePhase::IntermediatePrecisionBoundaryElision; + case GraphOptimizationRuleId::StoreCoveragePlanning: + return GraphOptimizationRulePhase::StoreCoveragePlanning; + case GraphOptimizationRuleId::StoreCoalescing: + return GraphOptimizationRulePhase::StoreCoalescing; + case GraphOptimizationRuleId::ContiguousBlockAccessFormation: + return GraphOptimizationRulePhase::ContiguousBlockAccessFormation; + case GraphOptimizationRuleId::RowCoalescing: + return GraphOptimizationRulePhase::RowCoalescing; + case GraphOptimizationRuleId::StridedAxisCoalescing: + case GraphOptimizationRuleId::ChunkCoalescing: + case GraphOptimizationRuleId::StridedLoadStoreRewrite: + return GraphOptimizationRulePhase::Compatibility; + } + return GraphOptimizationRulePhase::Compatibility; +} + +constexpr std::array + kGraphOptimizationRuleRegistry = { + GraphOptimizationRuleId::LoadStoreTranspose, + GraphOptimizationRuleId::TransposePointwiseReorder, + GraphOptimizationRuleId::StoreCoalescing, + GraphOptimizationRuleId::RowCoalescing, + GraphOptimizationRuleId::DiagonalMaskRemoval, + GraphOptimizationRuleId::ConvertModuloToMask, + GraphOptimizationRuleId::StridedAxisCoalescing, + GraphOptimizationRuleId::ChunkCoalescing, + GraphOptimizationRuleId::StridedLoadStoreRewrite, + GraphOptimizationRuleId::IndependentAxisTensorize, + GraphOptimizationRuleId::PersistentTaskStripMining, + GraphOptimizationRuleId::ResidentLoadForwarding, + GraphOptimizationRuleId::IntermediatePrecisionBoundaryElision, + GraphOptimizationRuleId::StoreCoveragePlanning, + GraphOptimizationRuleId::ContiguousBlockAccessFormation, +}; + +constexpr bool hasUniqueSingleBitGraphOptimizationRuleIds() { + for (std::size_t index = 0; index < kGraphOptimizationRuleRegistry.size(); + ++index) { + const GraphOptimizationRuleMask mask = + getGraphOptimizationRuleMask(kGraphOptimizationRuleRegistry[index]); + if (mask == 0 || (mask & (mask - 1)) != 0) + return false; + for (std::size_t other = index + 1; + other < kGraphOptimizationRuleRegistry.size(); ++other) { + if (mask == + getGraphOptimizationRuleMask(kGraphOptimizationRuleRegistry[other])) + return false; + } + } + return true; +} + +static_assert(hasUniqueSingleBitGraphOptimizationRuleIds(), + "GraphOptimizationRuleId values must be unique single bits"); + +constexpr GraphOptimizationRuleMask kLegacyGraphOptimizationRuleMask = + getGraphOptimizationRuleMask(GraphOptimizationRuleId::LoadStoreTranspose) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::TransposePointwiseReorder) | + getGraphOptimizationRuleMask(GraphOptimizationRuleId::StoreCoalescing) | + getGraphOptimizationRuleMask(GraphOptimizationRuleId::RowCoalescing) | + getGraphOptimizationRuleMask(GraphOptimizationRuleId::DiagonalMaskRemoval) | + getGraphOptimizationRuleMask(GraphOptimizationRuleId::ConvertModuloToMask) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::StridedAxisCoalescing) | + getGraphOptimizationRuleMask(GraphOptimizationRuleId::ChunkCoalescing) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::StridedLoadStoreRewrite); + +constexpr GraphOptimizationRuleMask kKnownGraphOptimizationRuleMask = + kLegacyGraphOptimizationRuleMask | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::IndependentAxisTensorize) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::PersistentTaskStripMining) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::ResidentLoadForwarding) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::IntermediatePrecisionBoundaryElision) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::StoreCoveragePlanning) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::ContiguousBlockAccessFormation); + +constexpr GraphOptimizationRuleMask kDefaultEligibleGraphOptimizationRuleMask = + kLegacyGraphOptimizationRuleMask | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::IndependentAxisTensorize) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::PersistentTaskStripMining); + +constexpr GraphOptimizationRuleMask kFixedDefaultOffGraphOptimizationRuleMask = + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::ResidentLoadForwarding) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::IntermediatePrecisionBoundaryElision) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::StoreCoveragePlanning) | + getGraphOptimizationRuleMask( + GraphOptimizationRuleId::ContiguousBlockAccessFormation); + +constexpr GraphOptimizationRuleMask kDefaultGraphOptimizationRuleMask = + kDefaultEligibleGraphOptimizationRuleMask; + +constexpr GraphOptimizationRuleMask kAllGraphOptimizationRuleMask = + kKnownGraphOptimizationRuleMask; + +static_assert(kLegacyGraphOptimizationRuleMask == 511, + "legacy graph optimization rule mask is an ABI contract"); +static_assert(kDefaultGraphOptimizationRuleMask == + kDefaultEligibleGraphOptimizationRuleMask, + "IAT and PTSM must remain enabled by default"); +static_assert(kKnownGraphOptimizationRuleMask == + (kDefaultEligibleGraphOptimizationRuleMask | + kFixedDefaultOffGraphOptimizationRuleMask), + "known graph rule identities must retain their scope partition"); +static_assert((kDefaultEligibleGraphOptimizationRuleMask & + kFixedDefaultOffGraphOptimizationRuleMask) == 0, + "fixed-default-off rules must not become default candidates"); +static_assert((kDefaultGraphOptimizationRuleMask & + ~kDefaultEligibleGraphOptimizationRuleMask) == 0, + "default graph rules must stay within the approved candidates"); +static_assert(static_cast(getGraphOptimizationRulePhase( + GraphOptimizationRuleId::ResidentLoadForwarding)) < + static_cast(getGraphOptimizationRulePhase( + GraphOptimizationRuleId::StoreCoveragePlanning)), + "resident forwarding must run before store planning"); +static_assert(static_cast(getGraphOptimizationRulePhase( + GraphOptimizationRuleId::StoreCoveragePlanning)) < + static_cast(getGraphOptimizationRulePhase( + GraphOptimizationRuleId::ContiguousBlockAccessFormation)), + "store planning must run before access formation"); + +constexpr bool +isValidGraphOptimizationRuleMask(GraphOptimizationRuleMask ruleMask) { + constexpr GraphOptimizationRuleMask unknownRuleBits = + ~kKnownGraphOptimizationRuleMask; + return (ruleMask & unknownRuleBits) == 0; +} + +struct IndependentAxisTensorizeRuleOptions { + bool enabledForCompileMode = true; + bool iatAndPtsmEnabled = false; +}; +struct PersistentTaskStripMiningRuleOptions { + bool enabledForCompileMode = true; +}; +struct StoreCoalescingRuleOptions { + bool enabledForCompileMode = true; +}; +struct ResidentLoadForwardingRuleOptions {}; +struct IntermediatePrecisionBoundaryElisionRuleOptions {}; +struct StoreCoveragePlanningRuleOptions {}; +struct ContiguousBlockAccessFormationRuleOptions {}; + +struct GraphOptimizationOptions { + // A zero mask intentionally disables every native GraphOptimizationRule. + // Layout/memory compatibility stages retain their original fixed scheduling + // and do not use this option as a new opt-out. + GraphOptimizationRuleMask enabledRuleMask = kDefaultGraphOptimizationRuleMask; + unsigned maxRewritesPerFunction = 64; + unsigned ubCapacityBytes = 0; + unsigned mappingUBCapacityBytes = 0; + unsigned storeCoalescingUBBudgetBytes = 0; + unsigned ubSafetyPercent = 80; + unsigned reservedUBBytes = 0; + // RowCoalescing changes the launch grid and is valid only for + // compile_mode="simt_only". Keep the source selector rather than a + // second derived force flag so every consumer follows one mode contract. + std::string compileMode = "simd_simt_template"; + IndependentAxisTensorizeRuleOptions independentAxisTensorize; + PersistentTaskStripMiningRuleOptions persistentTaskStripMining; + StoreCoalescingRuleOptions storeCoalescing; + ResidentLoadForwardingRuleOptions residentLoadForwarding; + IntermediatePrecisionBoundaryElisionRuleOptions + intermediatePrecisionBoundaryElision; + StoreCoveragePlanningRuleOptions storeCoveragePlanning; + ContiguousBlockAccessFormationRuleOptions contiguousBlockAccessFormation; +}; + +std::unique_ptr> +createGraphOptimizePass(GraphOptimizationOptions options = {}); + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_H diff --git a/third_party/ascend/include/TritonToGraph/GraphOptimizationContext.h b/third_party/ascend/include/TritonToGraph/GraphOptimizationContext.h new file mode 100644 index 0000000000..70f77b9239 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/GraphOptimizationContext.h @@ -0,0 +1,161 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_CONTEXT_H +#define TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_CONTEXT_H + +#include "TritonToGraph/ResourceCostModel.h" +#include "mlir/Support/LogicalResult.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +class AliasAnalysis; +class ControlFlowGraph; +class DataFlowGraph; +class EntryArgPointerAliasAnalysis; +class ProgramAxisDependenceAnalysis; + +enum class AnalysisRequirement : uint8_t { + None = 0, + CFG = 1u << 0, + ControlFlowGraph = CFG, + Alias = 1u << 1, + AliasAnalysis = Alias, + DataFlow = 1u << 2, + DataFlowGraph = DataFlow, + MemorySSA = 1u << 3, + EntryArgPointerAlias = 1u << 4, + ProgramAxisDependence = 1u << 5, + ResourceCost = 1u << 6, +}; + +constexpr AnalysisRequirement operator|(AnalysisRequirement lhs, + AnalysisRequirement rhs) { + return static_cast(static_cast(lhs) | + static_cast(rhs)); +} + +constexpr AnalysisRequirement operator&(AnalysisRequirement lhs, + AnalysisRequirement rhs) { + return static_cast(static_cast(lhs) & + static_cast(rhs)); +} + +constexpr bool hasAnalysisRequirement(AnalysisRequirement requirements, + AnalysisRequirement requirement) { + return static_cast(requirements & requirement) != 0; +} + +// Owns all graph analyses for one immutable function epoch. Any rewrite must +// call invalidate() before requesting analysis results again. +class GraphOptimizationContext { +public: + explicit GraphOptimizationContext(triton::FuncOp function, + ResourceSnapshot resources = {}); + ~GraphOptimizationContext(); + + GraphOptimizationContext(const GraphOptimizationContext &) = delete; + GraphOptimizationContext & + operator=(const GraphOptimizationContext &) = delete; + + // Lazily constructs the requested analysis and all of its dependencies. + LogicalResult ensure(AnalysisRequirement requirements); + + // Drops every analysis that may retain pointers into the old IR epoch. + void invalidate(); + + unsigned getEpoch() const { return epoch; } + triton::FuncOp getFunction() const { + assert(function.operator->() && + "GraphOptimizationContext requires a valid tt.func"); + return function; + } + + ControlFlowGraph &getCFG() { + assert(controlFlowGraph && "call ensure() before accessing the CFG"); + return *controlFlowGraph; + } + + ControlFlowGraph &getControlFlowGraph() { return getCFG(); } + + AliasAnalysis &getAliasAnalysis() { + assert(aliasAnalysis && "call ensure() before accessing alias analysis"); + return *aliasAnalysis; + } + + EntryArgPointerAliasAnalysis &getEntryArgPointerAliasAnalysis() { + assert(entryArgPointerAliasAnalysis && + "call ensure() before accessing entry-argument pointer alias " + "analysis"); + return *entryArgPointerAliasAnalysis; + } + + DataFlowGraph &getDataFlowGraph() { + assert(dataFlowGraph && + "call ensure() before accessing the data-flow graph"); + return *dataFlowGraph; + } + + ProgramAxisDependenceAnalysis &getProgramAxisDependenceAnalysis() { + assert(programAxisDependenceAnalysis && + "call ensure() before accessing program-axis dependence analysis"); + return *programAxisDependenceAnalysis; + } + + ResourceCostAnalysis &getResourceCostAnalysis() { + assert(resourceCostAnalysis && + "call ensure() before accessing resource/cost analysis"); + return *resourceCostAnalysis; + } + +private: + LogicalResult ensureControlFlowGraph(); + LogicalResult ensureAliasAnalysis(); + LogicalResult ensureEntryArgPointerAliasAnalysis(); + LogicalResult ensureDataFlowGraph(); + LogicalResult ensureProgramAxisDependenceAnalysis(); + LogicalResult ensureResourceCostAnalysis(); + + triton::FuncOp function; + ResourceSnapshot resources; + unsigned epoch = 0; + + std::unique_ptr controlFlowGraph; + std::unique_ptr aliasAnalysis; + std::unique_ptr entryArgPointerAliasAnalysis; + std::unique_ptr dataFlowGraph; + std::unique_ptr programAxisDependenceAnalysis; + std::unique_ptr resourceCostAnalysis; +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_CONTEXT_H diff --git a/third_party/ascend/include/TritonToGraph/GraphOptimizationRule.h b/third_party/ascend/include/TritonToGraph/GraphOptimizationRule.h new file mode 100644 index 0000000000..80f4010705 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/GraphOptimizationRule.h @@ -0,0 +1,126 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_RULE_H +#define TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_RULE_H + +#include "TritonToGraph/GraphOptimization.h" +#include "TritonToGraph/GraphOptimizationContext.h" + +#include "mlir/IR/PatternMatch.h" +#include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/SmallVector.h" + +#include + +namespace mlir { +namespace triton { +namespace cfg { + +enum class RewritePlanApplyResult { + Applied, + NotApplicable, + Failed, +}; + +// A plan is created for one immutable GraphOptimizationContext epoch. Its +// apply() implementation must be transactional: on failure it leaves the IR +// unchanged. +class RewritePlan { +public: + virtual ~RewritePlan() = default; + + virtual GraphOptimizationRuleId getRuleId() const = 0; + virtual unsigned getBenefit() const = 0; + virtual Operation *getAnchor() const = 0; + virtual unsigned getCreationEpoch() const = 0; + + // Failure means this plan is no longer applicable to the current epoch. + virtual LogicalResult revalidate(GraphOptimizationContext &context) const = 0; + virtual LogicalResult apply(IRRewriter &rewriter) = 0; + + virtual RewritePlanApplyResult applyWithResult(IRRewriter &rewriter) { + return succeeded(apply(rewriter)) ? RewritePlanApplyResult::Applied + : RewritePlanApplyResult::Failed; + } +}; + +class GraphOptimizationRule { +public: + virtual ~GraphOptimizationRule() = default; + + virtual GraphOptimizationRuleId getId() const = 0; + virtual AnalysisRequirement getAnalysisRequirements() const = 0; + + // Candidate discovery may inspect analyses but must not mutate IR. + virtual LogicalResult + findCandidates(GraphOptimizationContext &context, + SmallVectorImpl> &plans) = 0; +}; + +// Internal extension point for the builtin rule set. Concrete rules receive a +// stable copy of the pass options when they are constructed. +void populateBuiltinGraphOptimizationRules( + const GraphOptimizationOptions &options, + SmallVectorImpl> &rules); + +struct ReservedGraphOptimizationRuleOptions { + GraphOptimizationRuleId id; + const char *optionNamespace; +}; + +std::unique_ptr createReservedGraphOptimizationRule( + ReservedGraphOptimizationRuleOptions options); + +std::unique_ptr createTransposePointwiseReorderRule(); +std::unique_ptr createLoadStoreTransposeRule(); +std::unique_ptr +createStoreCoalescingRule(unsigned ubCapacityBytes, + const StoreCoalescingRuleOptions &options); +std::unique_ptr +createRowCoalescingRule(bool enableLegacy = true); +std::unique_ptr createDiagonalMaskRemovalRule(); +std::unique_ptr createConvertModuloToMaskRule(); +std::unique_ptr createIndependentAxisTensorizeRule( + const IndependentAxisTensorizeRuleOptions &options); +std::unique_ptr createPersistentTaskStripMiningRule( + const PersistentTaskStripMiningRuleOptions &options); + +LogicalResult materializePersistentTaskStripMiningCandidate( + ModuleOp module, triton::FuncOp function, const ResourceSnapshot &resources, + unsigned requestedBlockT, CandidateEvaluation *evaluation = nullptr, + bool deferIntermediateResourceRejection = false); +std::unique_ptr createResidentLoadForwardingRule( + const ResidentLoadForwardingRuleOptions &options); +std::unique_ptr +createIntermediatePrecisionBoundaryElisionRule( + const IntermediatePrecisionBoundaryElisionRuleOptions &options); +std::unique_ptr createStoreCoveragePlanningRule( + const StoreCoveragePlanningRuleOptions &options); +std::unique_ptr createContiguousBlockAccessFormationRule( + const ContiguousBlockAccessFormationRuleOptions &options); + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_GRAPH_GRAPH_OPTIMIZATION_RULE_H diff --git a/third_party/ascend/include/TritonToGraph/IndependentRowReductionAnalysis.h b/third_party/ascend/include/TritonToGraph/IndependentRowReductionAnalysis.h new file mode 100644 index 0000000000..cb29cca850 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/IndependentRowReductionAnalysis.h @@ -0,0 +1,186 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#ifndef TRITON_TO_GRAPH_INDEPENDENT_ROW_REDUCTION_ANALYSIS_H +#define TRITON_TO_GRAPH_INDEPENDENT_ROW_REDUCTION_ANALYSIS_H + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +enum class IndependentRowReductionKind : uint8_t { + Other, + SingleMoment, + TwoMoment, +}; + +inline bool isIndependentRowReduction(ReduceOp reduce) { + if (reduce.getSrcs().size() != 1 || reduce.getResults().size() != 1) + return false; + auto source = dyn_cast(reduce.getSrcs().front().getType()); + if (!source || !source.hasStaticShape()) + return false; + Type result = reduce.getResults().front().getType(); + if (source.getRank() == 1 && reduce.getAxis() == 0) + return !isa(result); + auto rankedResult = dyn_cast(result); + return source.getRank() == 2 && reduce.getAxis() == 1 && rankedResult && + rankedResult.hasStaticShape() && rankedResult.getRank() == 1 && + source.getShape()[0] == rankedResult.getShape()[0]; +} + +inline bool isMergeStyleReduction(ReduceOp reduce) { + if (reduce.getSrcs().size() != 1 || reduce.getResults().size() != 1) + return false; + auto source = dyn_cast(reduce.getSrcs().front().getType()); + auto result = + dyn_cast(reduce.getResults().front().getType()); + return source && result && source.hasStaticShape() && + result.hasStaticShape() && source.getRank() == 2 && + result.getRank() == 1 && reduce.getAxis() == 0 && + source.getShape()[1] == result.getShape()[0]; +} + +inline Value stripIdentityReshapes(Value value) { + while (auto reshape = value.getDefiningOp()) { + Value source = reshape.getOperand(); + if (source.getType() != value.getType()) + break; + value = source; + } + return value; +} + +inline Value getSquareOperand(Value value) { + value = stripIdentityReshapes(value); + if (auto multiply = value.getDefiningOp()) { + Value lhs = stripIdentityReshapes(multiply.getLhs()); + Value rhs = stripIdentityReshapes(multiply.getRhs()); + return lhs == rhs ? lhs : Value(); + } + if (auto multiply = value.getDefiningOp()) { + Value lhs = stripIdentityReshapes(multiply.getLhs()); + Value rhs = stripIdentityReshapes(multiply.getRhs()); + return lhs == rhs ? lhs : Value(); + } + return Value(); +} + +inline bool isMomentDataflowCarrier(Operation *operation) { + return operation->hasTrait() || + isa(operation); +} + +inline void collectMomentDataflow(Value root, llvm::DenseSet &values) { + SmallVector worklist; + values.insert(root); + worklist.push_back(root); + while (!worklist.empty()) { + Value value = worklist.pop_back_val(); + for (Operation *user : value.getUsers()) { + if (!isMomentDataflowCarrier(user)) + continue; + for (Value result : user->getResults()) + if (values.insert(result).second) + worklist.push_back(result); + } + } +} + +inline bool hasMomentDownstreamUse(Value reductionResult) { + llvm::DenseSet flow; + collectMomentDataflow(reductionResult, flow); + return flow.size() > 1; +} + +inline bool hasJointMomentDownstreamUse(triton::FuncOp function, + Value directResult, + Value squareResult) { + llvm::DenseSet directFlow; + llvm::DenseSet squareFlow; + collectMomentDataflow(directResult, directFlow); + collectMomentDataflow(squareResult, squareFlow); + bool joined = false; + function.walk([&](Operation *operation) { + if (joined || !isMomentDataflowCarrier(operation)) + return; + bool consumesDirect = false; + bool consumesSquare = false; + for (Value operand : operation->getOperands()) { + consumesDirect |= directFlow.contains(operand); + consumesSquare |= squareFlow.contains(operand); + } + joined = consumesDirect && consumesSquare; + }); + return joined; +} + +inline IndependentRowReductionKind +classifyIndependentRowReduction(triton::FuncOp function) { + SmallVector reductions; + bool hasMergeStyleReduction = false; + function.walk([&](triton::ReduceOp reduce) { + hasMergeStyleReduction |= isMergeStyleReduction(reduce); + if (isIndependentRowReduction(reduce)) + reductions.push_back(reduce); + }); + if (hasMergeStyleReduction || reductions.empty()) + return IndependentRowReductionKind::Other; + + SmallVector> squareReductions; + SmallVector directReductions; + for (triton::ReduceOp reduce : reductions) { + Value squareOperand = getSquareOperand(reduce.getSrcs().front()); + if (squareOperand) + squareReductions.push_back({reduce, squareOperand}); + else + directReductions.push_back(reduce); + } + if (reductions.size() == 1) + return squareReductions.size() == 1 && + hasMomentDownstreamUse( + squareReductions.front().first.getResults().front()) + ? IndependentRowReductionKind::SingleMoment + : IndependentRowReductionKind::Other; + + if (reductions.size() != 2 || squareReductions.size() != 1 || + directReductions.size() != 1) + return IndependentRowReductionKind::Other; + return stripIdentityReshapes(directReductions.front().getSrcs().front()) == + squareReductions.front().second && + hasMomentDownstreamUse( + directReductions.front().getResults().front()) && + hasMomentDownstreamUse( + squareReductions.front().first.getResults().front()) && + hasJointMomentDownstreamUse( + function, directReductions.front().getResults().front(), + squareReductions.front().first.getResults().front()) + ? IndependentRowReductionKind::TwoMoment + : IndependentRowReductionKind::Other; +} + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif diff --git a/third_party/ascend/include/TritonToGraph/InterProceduralCFG.h b/third_party/ascend/include/TritonToGraph/InterProceduralCFG.h new file mode 100644 index 0000000000..c4c8bf1a25 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/InterProceduralCFG.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_INTER_PROCEDURAL_CFG_H +#define TRITON_TO_CFG_INTER_PROCEDURAL_CFG_H + +#include "TritonToGraph/ControlFlowGraph.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// 过程间控制流图 (ICFG) +class InterProceduralCFG { +public: + explicit InterProceduralCFG(ModuleOp module); + ~InterProceduralCFG(); + + // 为每个函数构建CFG + void build(); + + // 获取函数的CFG + ControlFlowGraph *getFunctionCFG(triton::FuncOp func); + const ControlFlowGraph *getFunctionCFG(triton::FuncOp func) const; + ControlFlowGraph *getFunctionCFG(StringRef funcName); + + // 在调用点连接调用者和被调用者的CFG + struct CallSite { + Operation *callOp; // 调用操作 + triton::FuncOp caller; // 调用者函数 + triton::FuncOp callee; // 被调用者函数 + BasicBlock *callBlock; // 调用点的基本块 + Instruction *callInst; // 调用指令 + }; + + // 获取所有调用点 + const SmallVector &getCallSites() const { return callSites; } + + // 连接ICFG中的调用边 + void connectCallGraph(); + + // 查询函数调用关系 + SmallVector getCallees(triton::FuncOp caller) const; + SmallVector getCallers(triton::FuncOp callee) const; + + // 全局可达性分析 + void computeReachability(); + bool isReachable(triton::FuncOp from, triton::FuncOp to) const; + + // 可视化 + void dumpToDot(const std::string &filename) const; + void print(raw_ostream &os) const; + + // 导出到 HTML(包含所有函数) + llvm::Error exportToHTML(const std::string &filename) const; + +private: + ModuleOp module; + + // 函数到CFG的映射 + DenseMap> functionCFGs; + + // 调用点列表 + SmallVector callSites; + + // 调用图 (函数级别) + DenseMap> callGraph; + DenseMap> reverseCallGraph; + + // 可达性矩阵 + DenseMap> reachability; +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_INTER_PROCEDURAL_CFG_H diff --git a/third_party/ascend/include/TritonToGraph/LayoutMemoryOptimization.h b/third_party/ascend/include/TritonToGraph/LayoutMemoryOptimization.h new file mode 100644 index 0000000000..f13b1689fc --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/LayoutMemoryOptimization.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_LAYOUT_MEMORY_OPTIMIZATION_H +#define TRITON_TO_GRAPH_LAYOUT_MEMORY_OPTIMIZATION_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// These phases deliberately preserve the T2L insertion points around the +// legacy diagonal rewrite. They are not GraphOptimizationRule phases: the +// legacy rewrites are module-wide and some of them intentionally mutate IR +// while doing their analysis. +enum class LayoutMemoryCompatibilityPhase : uint8_t { + BeforeDiagonal, + AfterDiagonal, +}; + +std::unique_ptr> +createLayoutMemoryCompatibilityPass(LayoutMemoryCompatibilityPhase phase); + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_GRAPH_LAYOUT_MEMORY_OPTIMIZATION_H diff --git a/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.h b/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.h new file mode 100644 index 0000000000..232b048974 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_CHUNK_COALESCING_H +#define TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_CHUNK_COALESCING_H + +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace ChunkCoalescing { + +void rewriteChunkCoalesce(mlir::ModuleOp moduleOp); + +} // namespace ChunkCoalescing + +#endif // TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_CHUNK_COALESCING_H diff --git a/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/RowCoalescing.h b/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/RowCoalescing.h new file mode 100644 index 0000000000..a1745cc2b3 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/RowCoalescing.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_ROW_COALESCING_H +#define TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_ROW_COALESCING_H + +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace RowCoalescing { + +void rewriteRowCoalesce(mlir::ModuleOp moduleOp); + +} // namespace RowCoalescing + +#endif // TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_ROW_COALESCING_H diff --git a/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.h b/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.h new file mode 100644 index 0000000000..8e60261d03 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_STRIDED_AXIS_COALESCING_H +#define TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_STRIDED_AXIS_COALESCING_H + +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace StridedAxisCoalescing { + +void rewriteStridedAxisCoalesce(mlir::ModuleOp moduleOp); + +} // namespace StridedAxisCoalescing + +#endif // TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_STRIDED_AXIS_COALESCING_H diff --git a/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.h b/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.h new file mode 100644 index 0000000000..bbbb63cb16 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_STRIDED_LOAD_STORE_REWRITE_H +#define TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_STRIDED_LOAD_STORE_REWRITE_H + +#include "TritonMemoryAccess/MemoryAccessTags.h" +#include "mlir/IR/PatternMatch.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace StridedLoadStoreRewrite { + +using namespace mlir; +using namespace triton; + +inline constexpr const char *RewrittenByStridedLoadStoreRewriteTAG = + mlir::triton::memory_access::RewrittenByStridedLoadStoreRewriteTAG; +inline constexpr const char *InspectedByStridedLoadStoreRewriteTAG = + mlir::triton::memory_access::InspectedByStridedLoadStoreRewriteTAG; + +class LoadConverter : public OpRewritePattern { +public: + explicit LoadConverter(MLIRContext *context) + : OpRewritePattern(context) {} + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::LoadOp op, + PatternRewriter &rewriter) const override; +}; + +class StoreConverter : public OpRewritePattern { +public: + explicit StoreConverter(MLIRContext *context) + : OpRewritePattern(context) {} + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const override; +}; + +} // namespace StridedLoadStoreRewrite + +#endif // TRITON_TO_GRAPH_LEGACY_MEMORY_ACCESS_STRIDED_LOAD_STORE_REWRITE_H diff --git a/third_party/ascend/include/TritonToGraph/MemorySSA.h b/third_party/ascend/include/TritonToGraph/MemorySSA.h new file mode 100644 index 0000000000..26f553b2fa --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/MemorySSA.h @@ -0,0 +1,294 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_MEMORY_SSA_H +#define TRITON_TO_CFG_MEMORY_SSA_H + +#include "TritonToGraph/tensor.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/SmallVector.h" + +namespace mlir { +namespace triton { +namespace cfg { + +// Forward declarations +class MemorySSADef; +class MemorySSAUse; + +class BasicBlock; +class ControlFlowGraph; + +// Memory SSA Definition - 表示tensor/pointer的定义 +class MemorySSADef { +public: + // 构造函数 + MemorySSADef(TensorObject *tensor, Operation *defOp, unsigned version = 0) + : tensor(tensor), defOp(defOp), version(version) {} + + // 获取tensor对象 + TensorObject *getTensor() const { return tensor; } + + // 获取创建该definition的操作 + // - 对于入参,返回nullptr + // - 对于其他操作,返回对应的Operation指针 + Operation *getDefOp() const { return defOp; } + + // 获取版本号(函数内全局唯一序数) + unsigned getVersion() const { return version; } + + // 获取唯一标识: tensor_name,version 或 tensor_name,param + std::string getId() const { + return tensor->getName() + "," + + (defOp ? std::to_string(version) : std::to_string(0)); + } + + // 判断是否是入参(函数参数) + bool isParameter() const { return defOp == nullptr; } + + // 判断是否是phi节点(控制流合并) + bool isPhi() const { + return defOp && (isa(defOp) || isa(defOp) || + isa(defOp)); + } + + // 打印信息 + void print(llvm::raw_ostream &os) const { + os << "Definition[" << getId() << ", tensor=" << tensor->getName(); + if (defOp) { + os << ", op=" << defOp->getName(); + } else { + os << ", param"; + } + os << "]"; + } + +private: + TensorObject *tensor; // 对应的tensor对象 + Operation *defOp; // 创建该definition的操作 + unsigned version; // 版本号(函数内全局递增) +}; + +// Memory SSA Use - 表示tensor/pointer的使用 +class MemorySSAUse { +public: + // 构造函数 + MemorySSAUse(MemorySSADef *definition, Operation *userOp, unsigned operandIdx) + : definition(definition), userOp(userOp), operandIdx(operandIdx) { + // 缓存value以提高查询性能 + operandValue = userOp->getOperand(operandIdx); + } + + // 获取使用的definition + MemorySSADef *getDefinition() const { return definition; } + + // 获取使用该definition的操作 + Operation *getUserOp() const { return userOp; } + + // 获取operand序号 + unsigned getOperandIdx() const { return operandIdx; } + + // 获取Value(从userOp的operand) + Value getValue() const { return operandValue; } + + // 获取用户操作的名称 + std::string getUserOpName() const { + return userOp->getName().getStringRef().str(); + } + + // 打印信息 + void print(llvm::raw_ostream &os) const { + os << "Use["; + if (definition) { + os << definition->getId(); + } else { + os << "null"; + } + os << " in " << userOp->getName() << ", operand #" << operandIdx << "]"; + } + +private: + MemorySSADef *definition; // 使用的definition + Operation *userOp; // 使用该definition的操作 + unsigned operandIdx; // operand序号 + Value operandValue; // 缓存的operand value +}; + +// PhiInfo - 循环Phi信息 +struct PhiInfo { + // Phi类型 + enum Type { + ITER_ARG, // scf.for的iter_arg + IF_RESULT, // scf.if的result + WHILE_ARG // scf.while的arg + }; + + Type type; + BasicBlock *loopHeader; // 循环头基本块 + + // Phi值的来源 + struct { + MemorySSADef *initialValue; // 初始值(初始iteration) + MemorySSADef *yieldValue; // yield的值(后续iteration) + } comingFrom; + + // 是否是第一次迭代 + bool isInitial() const { return comingFrom.yieldValue == nullptr; } + + // 获取当前definition(根据上下文决定) + MemorySSADef *getCurrentDefinition(int iteration) const { + return (iteration == 0) ? comingFrom.initialValue : comingFrom.yieldValue; + } +}; + +// MemorySSAInfo - 指令的Memory SSA信息 +struct MemorySSAInfo { + // 指令的operands使用的definitions + SmallVector uses; + + // 指令的results创建的definitions + SmallVector definitions; + + // Alias信息(仅对pointer相关操作) + struct AliasInfo { + Value aliasee; // 别名的源value + TensorObject *baseTensor; // 对应的tensor对象 + }; + std::optional aliasInfo; + + // 快速查询接口 + bool hasDefinition(Value value) const { + // 通过 getDefiningOp() 获取定义该 value 的操作 + Operation *defOp = value.getDefiningOp(); + if (!defOp) + return false; + + // 获取该操作的所有 results + auto results = defOp->getResults(); + for (size_t i = 0; i < definitions.size() && i < results.size(); ++i) { + if (results[i] == value) { + return definitions[i] != nullptr; + } + } + return false; + } + + MemorySSADef *getDefinition(Value value) const { + // 查找该value在results中的索引 + for (auto result : llvm::enumerate(value.getDefiningOp()->getResults())) { + if (result.value() == value) { + size_t idx = result.index(); + if (idx < definitions.size()) { + return definitions[idx]; + } + break; + } + } + return nullptr; + } + + bool hasUse(Value value) const { + for (const MemorySSAUse &use : uses) { + if (use.getValue() == value) { + return true; + } + } + return false; + } + + SmallVector getUses(Value value) const { + SmallVector result; + for (const MemorySSAUse &use : uses) { + if (use.getValue() == value) { + result.push_back(use); + } + } + return result; + } + + // 判断是否创建了新的definition + bool hasNewDefinitions() const { return !definitions.empty(); } + + // 判断是否是写入操作 + bool isMemoryWriter() const { + for (MemorySSADef *def : definitions) { + if (def && !def->isParameter()) { + return true; + } + } + return false; + } + + // 遍历定义 + void forEachDefinition(llvm::function_ref func) const { + for (MemorySSADef *def : definitions) { + if (def) + func(def); + } + } + + void forEachUse(llvm::function_ref func) const { + for (const MemorySSAUse &use : uses) { + if (use.getDefinition()) + func(use); + } + } + + // 清空所有信息 + void clear() { + uses.clear(); + definitions.clear(); + aliasInfo.reset(); + } + + // 打印信息 + void print(llvm::raw_ostream &os) const { + os << "MemorySSAInfo[\n"; + os << " Uses: " << uses.size() << "\n"; + for (const MemorySSAUse &use : uses) { + os << " "; + use.print(os); + os << "\n"; + } + os << " Definitions: " << definitions.size() << "\n"; + for (MemorySSADef *def : definitions) { + if (def) { + os << " "; + def->print(os); + os << "\n"; + } + } + if (aliasInfo) { + os << " Alias: " << aliasInfo->aliasee << " -> " + << aliasInfo->baseTensor->getName() << "\n"; + } + os << "]"; + } +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_MEMORY_SSA_H diff --git a/third_party/ascend/include/TritonToGraph/MemorySsaBuilder.h b/third_party/ascend/include/TritonToGraph/MemorySsaBuilder.h new file mode 100644 index 0000000000..9a8ba6ced0 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/MemorySsaBuilder.h @@ -0,0 +1,177 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_MEMORY_SSA_BUILDER_H +#define TRITON_TO_CFG_MEMORY_SSA_BUILDER_H + +#include "TritonToGraph/AliasAnalysis.h" +#include "TritonToGraph/MemorySSA.h" +#include "mlir/IR/Types.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" + +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// 前向声明 +class DataFlowInfo; +class ControlFlowGraph; +class Instruction; + +// MemorySSABuilder - Memory SSA构建器 +// 构建整个CFG的Memory SSA信息,包括创建definitions、uses、处理控制流phi节点 +class MemorySSABuilder { +public: + MemorySSABuilder(ControlFlowGraph &cfg, AliasAnalysis &aliasAnalysis, + DataFlowInfo &dataFlowInfo) + : cfg(cfg), aliasAnalysis(aliasAnalysis), dataFlowInfo(dataFlowInfo) {} + + ~MemorySSABuilder(); + + // 构建整个CFG的Memory SSA + void build(); + +private: + // 处理单个BasicBlock + void processBasicBlock(BasicBlock *bb); + + // 处理单个指令 + void processInstruction(Instruction *inst); + + // 处理scf.if的phi节点 + void processIfOp(scf::IfOp ifOp, Instruction *inst, BasicBlock *thenEntryBB, + BasicBlock *elseEntryBB); + + // 处理scf.for的iter_args + void processForOp(scf::ForOp forOp, Instruction *inst, + BasicBlock *loopBodyEntryBB); + + // 处理scf.while的args + void processWhileOp(scf::WhileOp whileOp, Instruction *inst, + BasicBlock *beforeEntryBB, BasicBlock *afterEntryBB); + + // 判断是否是tensor类型 + bool isTensorType(Type type) const { + return mlir::isa(type) || + mlir::isa(type); + } + + // 根据操作创建tensor对象 + TensorObject *createTensorObject(Operation *op); + + // 创建tensor definition + MemorySSADef *createDefinition(TensorObject *tensor, Operation *op); + + // 创建use + MemorySSAUse createUse(MemorySSADef *def, Operation *userOp, + unsigned operandIdx); + + // 判断是否是入参 + bool isParameter(Operation *op) const { + return op == nullptr; // 入参的defOp为nullptr + } + + // 判断是否是返回新Tensor的操作(根据返回值类型判断,排除load) + bool isTensorWriter(Operation *op) const; + + // 判断是否是修改内存的操作(有副作用) + bool isMemoryWriter(Operation *op) const { + // 只有:tt.store(写入内存) + return isa(op); + } + + // 判断是否是修改读取的操作(有副作用) + bool isMemoryReader(Operation *op) const { + // 只有:tt.store(写入内存) + return isa(op); + } + + // 判断是否是创建指针的操作 + bool isPointerOp(Operation *op) const { + // 返回指针类型:addptr(偏移指针)、make_tensor_ptr(创建张量指针) + if (isa(op)) + return true; + + if (isPointerBroadcastOrSplat(op)) + return true; + + return false; + } + + bool isPointerBroadcastOrSplat(mlir::Operation *op) const; + + // 创建函数的参数定义 + void createParameterDefinitions(); + + // 获取或创建tensor对象 + TensorObject *getOrCreateTensorObject(Value value); + + // 获取操作的字符串名称(用于生成tensor名称) + std::string getOpName(Operation *op); + + // 成员变量 + ControlFlowGraph &cfg; + AliasAnalysis &aliasAnalysis; + DataFlowInfo &dataFlowInfo; + // size_t nextVersionId; // 下一个可用的版本号(用于MemorySSADef) + // size_t nextTensorId; // 下一个可用的tensor ID(用于TensorObject命名) + + std::map nextVersion; + std::map nextTensor; + + SmallVector> ownedTensorObjects; + SmallVector> allDefinitions; + + // tensor对象缓存 + DenseMap tensorObjectCache; +}; + +// MemorySSABuilderHelper - 辅助函数 +namespace MemorySSABuilderHelper { +// 获取操作的结果类型 +Type getResultType(Operation *op, unsigned resultIdx); + +// 获取Value的形状信息 +SmallVector getShapeFromValue(Value value); + +// 判断两个shape是否相同 +bool shapesEqual(ArrayRef shape1, ArrayRef shape2); + +// 获取scf.if的region yield操作 +Operation *getYieldOp(Region ®ion); + +// 为tensor创建唯一名称 +std::string createUniqueTensorName(StringRef prefix, size_t id); + +// 判断是否需要为操作创建新版本 +bool shouldCreateNewVersion(Operation *op, MemorySSADef *currentDef); +} // namespace MemorySSABuilderHelper + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_MEMORY_SSA_BUILDER_H diff --git a/third_party/ascend/include/TritonToGraph/Passes.h b/third_party/ascend/include/TritonToGraph/Passes.h new file mode 100644 index 0000000000..32c0142c6d --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/Passes.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_PASSES_H +#define TRITON_TO_CFG_PASSES_H + +#include "GraphOptimization.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" + +#include + +namespace mlir { +namespace triton { +namespace cfg { + +// 创建 BuildCFG pass 的工厂函数 +std::unique_ptr> createBuildCFGPass(); + +// Merge disjoint masked-load UB buffers concatenated by insert_slice. +std::unique_ptr> +createMergeConcatLoadBufferPass(); + +// Declare TableGen-generated pass option structs before individual pass +// implementations include their corresponding pass bases. +#define GEN_PASS_DECL_GRAPHOPTIMIZE +#include "ascend/include/TritonToGraph/Passes.h.inc" + +// 注册所有 CFG 相关的 passes +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToGraph/Passes.h.inc" + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_PASSES_H diff --git a/third_party/ascend/include/TritonToGraph/Passes.td b/third_party/ascend/include/TritonToGraph/Passes.td new file mode 100644 index 0000000000..a930c83006 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/Passes.td @@ -0,0 +1,95 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_PASSES +#define TRITON_TO_CFG_PASSES + +include "mlir/Pass/PassBase.td" + +def BuildCFG : Pass<"build-cfg", "mlir::ModuleOp"> { + let summary = "Build Control Flow Graph from TTIR"; + let description = [{ + This pass analyzes the TTIR and builds a Control Flow Graph (CFG) + representation. It exports the CFG to multiple formats (text, DOT, JSON, HTML) + for visualization and analysis. + + The output files will be named as _cfg. and placed in + the directory specified by the output-dir option. + }]; + let constructor = "mlir::triton::cfg::createBuildCFGPass()"; + let options = [ + Option<"output-dir", "outputDir", "std::string", "\".\"", + "Directory where CFG files will be generated">, + ]; +} + +def MergeConcatLoadBuffer : Pass<"merge-concat-load-buffer", "mlir::ModuleOp"> { + let summary = "Merge disjoint masked-load UB buffers concatenated by insert_slice"; + let description = [{ + After triton-to-linalg, each masked tt.load allocates a full-tile UB buffer, + fills it with the padding value, and copies only the masked subview. A + subsequent cat/join is lowered to extract_slice + insert_slice, so two + allocs that write disjoint regions of the same static shape can be merged + into one memref.alloc. + + If the union of copy destinations is proven to cover every later read of + the concatenated tensor, both linalg.fill ops are deleted. Otherwise a + single fill is kept (when both fills are bare and use the same value), or + the rewrite is abandoned. + + A fill only counts as padding when it runs before the copies that overwrite + it, so a fill placed after them is what the concatenated value observes and + the rewrite is abandoned. The fill that survives a merge must additionally + precede the copies of both buffers, since it initialises the shared one. + + The merged buffer ends up written by two disjoint memref.copy ops. On + 910_9362 the generated code only makes the copy next to the surviving + to_tensor visible and the rest of the tile reads back the padding value, so + the pipeline only enables this pass on 910_95/950. + }]; + let constructor = "mlir::triton::cfg::createMergeConcatLoadBufferPass()"; +} + +def GraphOptimize : Pass<"graph-optimize", "mlir::ModuleOp"> { + let summary = "Optimize Triton IR with graph analyses"; + let constructor = "mlir::triton::cfg::createGraphOptimizePass()"; + let options = [ + Option<"ruleMask", "rule-mask", "uint64_t", "3071", + "Bitmask of known 32-bit graph rule identities; default 3071 preserves the legacy native bundle and enables IAT/PTSM. RLF/IPBE/SCP/CBAF remain explicit opt-in">, + Option<"maxRewritesPerFunction", "max-rewrites-per-function", "uint64_t", + "64", "Maximum graph rewrites per function">, + Option<"ubCapacityBytes", "ub-capacity-bytes", "uint64_t", "0", + "Legacy unified-buffer budget; explicit mapping/store budgets fall back to this value when omitted">, + Option<"mappingUBCapacityBytes", "mapping-ub-capacity-bytes", "uint64_t", "0", + "Physical unified-buffer capacity available to program-mapping resource checks">, + Option<"storeCoalescingUBBudgetBytes", "store-coalescing-ub-budget-bytes", "uint64_t", "0", + "Conservative unified-buffer budget available to StoreCoalescing">, + Option<"ubSafetyPercent", "ub-safety-percent", "uint64_t", "80", + "Percent of UB capacity available to resource candidates">, + Option<"reservedUBBytes", "reserved-ub-bytes", "uint64_t", "0", + "UB bytes reserved for lowering/backend workspace before candidate budgeting">, + Option<"compileMode", "compile-mode", "std::string", "\"simd_simt_template\"", + "Compilation mode; legacy elementwise RowCoalescing is enabled only for simt_only">, + ]; +} + +#endif // TRITON_TO_CFG_PASSES diff --git a/third_party/ascend/include/TritonToGraph/PermutationAnalysis.h b/third_party/ascend/include/TritonToGraph/PermutationAnalysis.h new file mode 100644 index 0000000000..566966166c --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/PermutationAnalysis.h @@ -0,0 +1,269 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_PERMUTATION_ANALYSIS_H +#define TRITON_TO_GRAPH_PERMUTATION_ANALYSIS_H + +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LogicalResult.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +class EntryArgPointerAliasAnalysis; + +// Every analysis decision is explicit. Callers must treat Rejected and +// Unknown identically for authorization: neither permits an IR rewrite. +enum class ProofResult : uint8_t { + Proven, + Rejected, + Unknown, +}; + +// A stable, inspectable explanation for an intentionally fail-closed result. +// `None` is used only with ProofResult::Proven. +enum class ProofReason : uint8_t { + None, + InvalidPermutationRank, + InvalidPermutationAxis, + DuplicatePermutationAxis, + PermutationRankMismatch, + ShapeRankMismatch, + InvalidOldAxis, + NullValue, + UnresolvedPointer, + UnsupportedPointerType, + UnsupportedPointerForm, + UnsupportedRank, + UnsupportedEncoding, + NonSquareShape, + UnsupportedIndexElementType, + DynamicShape, + UnsupportedAffineOffset, + DynamicStride, + NegativeStride, + ZeroStride, + DuplicateStride, + InvalidAxisProvenance, + DuplicateAxisProvenance, + DuplicateRangeSource, + InvalidMakeRange, + InvalidExpandDimsChain, + OffsetOverflow, + OverflowFlags, + NonRowMajorContiguous, + NonInjectiveLanes, + MaskedAccess, + BoundaryCheck, + VolatileLoad, + NullOperation, + DifferentBlocks, + InvalidProtectedInterval, + RegionOperation, + CallOperation, + BarrierOperation, + UnknownMemoryEffect, + InterveningMemoryEffect, + DifferentAccessBase, + OverlappingAccessRange, + UnsupportedInterveningMemoryAccess, +}; + +llvm::StringRef getProofReasonMessage(ProofReason reason); + +struct ProofOutcome { + ProofResult result = ProofResult::Unknown; + ProofReason reason = ProofReason::UnresolvedPointer; + + constexpr bool isProven() const { return result == ProofResult::Proven; } + constexpr bool isRejected() const { return result == ProofResult::Rejected; } + constexpr bool isUnknown() const { return result == ProofResult::Unknown; } + + static constexpr ProofOutcome proven() { + return {ProofResult::Proven, ProofReason::None}; + } + static constexpr ProofOutcome rejected(ProofReason reason) { + return {ProofResult::Rejected, reason}; + } + static constexpr ProofOutcome unknown(ProofReason reason) { + return {ProofResult::Unknown, reason}; + } +}; + +// The single convention used by this class and TTIR's `tt.trans` order: +// +// perm[newAxis] == oldAxis +// +// If P is applied first and Q is applied after P, P.compose(Q) describes the +// combined transform and has combined[newAxis] == P[Q[newAxis]]. For example, +// P=[2, 0, 1] and Q=[1, 2, 0] compose to [0, 1, 2]. +class Permutation { +public: + // Returns a diagnostic result without constructing a value. This is useful + // when a caller needs to report why a tt.trans order cannot be trusted. + static ProofOutcome validate(llvm::ArrayRef perm); + + // Rejects empty, out-of-range, and duplicate-axis permutations. + static FailureOr create(llvm::ArrayRef perm); + + unsigned rank() const { return newToOld.size(); } + llvm::ArrayRef getNewToOld() const { return newToOld; } + + // Valid Permutation objects always have a valid inverse. + Permutation inverse() const; + + // Returns failure when the ranks differ. The operation means "apply this, + // then apply after" using the new-axis-to-old-axis convention above. + FailureOr compose(const Permutation &after) const; + + // Returns -1 for an invalid old axis instead of asserting or wrapping. + int32_t mapOldAxisToNew(int32_t oldAxis) const; + + // Returns failure when shape.rank() differs from this permutation's rank. + // Dynamic dimension values, if present, are simply moved with their axis. + FailureOr> + permuteShape(llvm::ArrayRef shape) const; + +private: + explicit Permutation(llvm::ArrayRef perm) + : newToOld(perm.begin(), perm.end()) {} + + llvm::SmallVector newToOld; +}; + +// One normalized affine lane term. `range` preserves the source operation +// identity so callers can reject a DAG that reuses one logical range for two +// axes even when those axes have equal extents. +struct StaticAccessAxis { + triton::MakeRangeOp range; + int64_t rangeStart = 0; + int64_t rangeEnd = 0; + int64_t stride = 0; + unsigned outputAxis = 0; +}; + +// A statically understood affine pointer access. Rank-1 supports the +// original tt.splat + tt.addptr + tt.make_range form and the narrow dynamic +// form tt.splat(origin + static_i32) + tt.make_range. In the latter case +// `dynamicOrigin` is the shared scalar origin (or null for a fully static +// access), while `firstOffset` and `lastOffset` are the static residual +// interval relative to it. Higher ranks support a deliberately narrow +// row-major normal form: one independent make_range -> expand_dims* -> +// splat(static i32) -> muli -> broadcast term per logical axis, joined by +// addi. The fields preserve source operations and axis provenance rather +// than deriving axes from equal dimension sizes. +struct StaticAccess { + Value pointer; + Value offset; + Value base; + Value dynamicOrigin; + llvm::SmallVector shape; + llvm::SmallVector strides; + llvm::SmallVector axisProvenance; + llvm::SmallVector axes; + int64_t firstOffset = 0; + int64_t lastOffset = 0; + int64_t elementCount = 0; + bool lanesInjective = false; + + bool isRankOneContiguous() const { + return lanesInjective && shape.size() == 1 && strides.size() == 1 && + strides.front() == 1; + } + + // Proves that flattening the logical tensor in row-major order preserves + // its address order. This is intentionally stronger than generic lane + // injectivity and is the admission condition for UB store packing. + bool isLogicalRowMajorContiguous() const; +}; + +struct StaticAccessProof { + ProofOutcome outcome; + std::optional access; + + bool isProven() const { return outcome.isProven() && access.has_value(); } +}; + +class StaticAccessAnalysis { +public: + // Analyze only the normalized static pointer form documented above. A + // block argument, nested addptr, tensor pointer, dynamic stride, or any + // unrecognized producer is rejected or unknown; it is never assumed safe. + StaticAccessProof analyzePointer(Value pointer) const; + + // V1 access proofs accept neither masks nor boundary/padding behavior; + // volatile loads are likewise rejected before pointer analysis. + StaticAccessProof analyzeLoad(triton::LoadOp load) const; + StaticAccessProof analyzeStore(triton::StoreOp store) const; + + // Proves that two statically understood accesses use the same SSA base and + // dynamic origin (both absent for static accesses), and have disjoint + // closed residual offset intervals. Any missing proof is rejected. + ProofOutcome proveSameBaseDisjoint(const StaticAccess &lhs, + const StaticAccess &rhs) const; + + // Proves a sufficient injectivity condition for a fully static affine lane + // map. All dimensions, strides, and provenance entries must have the same + // non-zero rank. Dynamic, negative, zero, and duplicate strides fail + // closed, as do duplicated axis origins. The proof also rejects maps whose + // nonnegative terms or maximum cumulative offset exceed signed i32, because + // the TTIR access offsets being proven are i32 tensors. + static ProofOutcome + proveLaneInjectivity(llvm::ArrayRef shape, + llvm::ArrayRef strides, + llvm::ArrayRef axisProvenance); +}; + +class ProtectedIntervalAnalysis { +public: + // Proves that the open interval (first, last) is in one MLIR block and has + // no regions, calls, barriers, unknown operations, or memory effects. + // This method analyzes only; it never changes the IR. + ProofOutcome proveNoMemoryEffects(Operation *first, Operation *last) const; + + // Proves that the open interval (first, last) has no effects that conflict + // with any protected access. Without entryArgPointerAliases, only statically + // proven tt.load/tt.store operations with the same base and disjoint ranges + // are permitted. With it, accesses rooted at distinct entry pointers are + // additionally permitted under the StoreCoalescing entry-argument ABI + // contract. + ProofOutcome proveNoConflictingLoadStoreEffects( + Operation *first, Operation *last, + llvm::ArrayRef protectedAccesses, + const EntryArgPointerAliasAnalysis *entryArgPointerAliases = + nullptr) const; +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_GRAPH_PERMUTATION_ANALYSIS_H diff --git a/third_party/ascend/include/TritonToGraph/ProgramAxisDependenceAnalysis.h b/third_party/ascend/include/TritonToGraph/ProgramAxisDependenceAnalysis.h new file mode 100644 index 0000000000..52cdece65b --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/ProgramAxisDependenceAnalysis.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#ifndef TRITON_TO_GRAPH_PROGRAM_AXIS_DEPENDENCE_ANALYSIS_H +#define TRITON_TO_GRAPH_PROGRAM_AXIS_DEPENDENCE_ANALYSIS_H + +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +enum class StoreAddressIndependence : uint8_t { + NotProgramDependent, + ProvenDisjoint, + CanonicalDynamicStride, + Unknown, +}; + +struct StoreAddressDependence { + Operation *store = nullptr; + bool pointerDependsOnAxis = false; + StoreAddressIndependence independence = + StoreAddressIndependence::NotProgramDependent; +}; + +struct ProgramAxisDependence { + int32_t axis = 0; + SmallVector programIds; + SmallVector dependenceClosure; + SmallVector stores; + SmallVector reductionAxes; + + bool escapes = false; + bool hasSideEffects = false; + bool hasUnsupportedSideEffects = false; + bool readsNumPrograms = false; + + bool hasProgramId() const { return !programIds.empty(); } + bool hasOnlyDisjointStoreAddresses() const; + bool hasOnlyLiftableStoreAddressesForProgramMapping() const; + bool isIndependentAxisTransformCandidate() const; + bool isProgramMappingTransformCandidate() const; +}; + +class ProgramAxisDependenceAnalysis { +public: + explicit ProgramAxisDependenceAnalysis(triton::FuncOp function); + + const ProgramAxisDependence &get(int32_t axis) const; + +private: + std::array axes; +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif diff --git a/third_party/ascend/include/TritonToGraph/ProgramGridTransform.h b/third_party/ascend/include/TritonToGraph/ProgramGridTransform.h new file mode 100644 index 0000000000..357e87048e --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/ProgramGridTransform.h @@ -0,0 +1,76 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_PROGRAM_GRID_TRANSFORM_H +#define TRITON_TO_GRAPH_PROGRAM_GRID_TRANSFORM_H + +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Support/LLVM.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +#include + +namespace mlir { +namespace triton { +class FuncOp; +namespace cfg { + +inline constexpr llvm::StringLiteral kProgramGridTransformsAttr = + "hacc.program_grid_transforms"; +inline constexpr int64_t kProgramGridTransformsVersion = 2; + +struct ProgramGridTransform { + int32_t order = 0; + int32_t axis = 0; + int64_t factor = 1; + bool persistentCoverage = false; + bool gridStrideAbiVerified = false; +}; + +struct ProgramGridTransformContract { + int64_t version = kProgramGridTransformsVersion; + SmallVector transforms; +}; + +LogicalResult addProgramGridHiddenExtentArguments(triton::FuncOp function); +bool hasProgramGridHiddenExtentArguments(triton::FuncOp function); + +LogicalResult commitProgramGridFunctionFromSandbox(triton::FuncOp destination, + triton::FuncOp source); + +FailureOr +parseProgramGridTransformContract(Attribute attribute); + +DictionaryAttr serializeProgramGridTransformContract( + MLIRContext *context, const ProgramGridTransformContract &contract); + +LogicalResult +setProgramGridTransformContract(ModuleOp module, + const ProgramGridTransformContract &contract); + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif diff --git a/third_party/ascend/include/TritonToGraph/ProgramMappingScalarSpecialization.h b/third_party/ascend/include/TritonToGraph/ProgramMappingScalarSpecialization.h new file mode 100644 index 0000000000..7bca0044bd --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/ProgramMappingScalarSpecialization.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#ifndef TRITON_TO_GRAPH_PROGRAM_MAPPING_SCALAR_SPECIALIZATION_H +#define TRITON_TO_GRAPH_PROGRAM_MAPPING_SCALAR_SPECIALIZATION_H + +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Support/LLVM.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +#include +#include + +namespace mlir { +namespace triton { +namespace cfg { + +inline constexpr llvm::StringLiteral kProgramMappingScalarSpecializationAttr = + "hacc.program_mapping_scalar_specialization"; +inline constexpr int64_t kProgramMappingScalarSpecializationLegacyVersion = 1; +inline constexpr int64_t kProgramMappingScalarSpecializationVersion = 2; + +struct ProgramMappingScalarArgument { + uint32_t index = 0; + std::string name; + int64_t value = 0; +}; + +struct ProgramMappingScalarSpecialization { + int64_t version = kProgramMappingScalarSpecializationVersion; + SmallVector arguments; +}; + +FailureOr +parseProgramMappingScalarSpecialization(Attribute attribute); + +DictionaryAttr serializeProgramMappingScalarSpecialization( + MLIRContext *context, + const ProgramMappingScalarSpecialization &specialization); + +LogicalResult setProgramMappingScalarSpecialization( + ModuleOp module, const ProgramMappingScalarSpecialization &specialization); +void clearProgramMappingScalarSpecialization(ModuleOp module); + +LogicalResult applyProgramMappingScalarSpecialization(ModuleOp module); + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif diff --git a/third_party/ascend/include/TritonToGraph/ResourceCostModel.h b/third_party/ascend/include/TritonToGraph/ResourceCostModel.h new file mode 100644 index 0000000000..779ad4b14f --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/ResourceCostModel.h @@ -0,0 +1,210 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_GRAPH_RESOURCE_COST_MODEL_H +#define TRITON_TO_GRAPH_RESOURCE_COST_MODEL_H + +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +#include +#include +#include + +namespace mlir { +namespace ascend { +class HardwareConfig; +} + +namespace triton { +namespace cfg { + +enum class ResourceCostRejectReason : uint8_t { + None, + UnknownResource, + DynamicShape, + UnknownElementType, + Overflow, + UBOverflow, + InvalidCandidate, +}; + +const char *getResourceCostRejectReasonName(ResourceCostRejectReason reason); + +struct ResourceSnapshot { + uint64_t ubCapacityBytes = 0; + uint64_t reservedUBBytes = 0; + unsigned ubSafetyPercent = 80; + + bool isKnown() const; + std::optional getSafeUBBudget() const; + + static ResourceSnapshot fromExplicit(uint64_t ubCapacityBytes, + unsigned ubSafetyPercent = 80, + uint64_t reservedUBBytes = 0); + + static ResourceSnapshot + fromHardwareConfig(const ascend::HardwareConfig &hardware, + unsigned ubSafetyPercent = 80, + uint64_t reservedUBBytes = 0); +}; + +struct LiveTensorInterval { + Value value; + uint64_t begin = 0; + uint64_t end = 0; + uint64_t bytes = 0; +}; + +struct LiveByteEstimate { + bool known = false; + bool overflow = false; + ResourceCostRejectReason reason = ResourceCostRejectReason::UnknownResource; + uint64_t peakLiveBytes = 0; + llvm::SmallVector intervals; +}; + +std::optional getStaticTensorBytes(Type type); + +LiveByteEstimate estimatePeakLiveBytes(Operation *root); + +struct CandidatePlan { + uint64_t tensorizeFactor = 1; + uint64_t blockT = 1; + uint64_t staticAxisFusionFactor = 1; + std::string stableId; + uint64_t sourceOrdinal = 0; +}; + +struct CandidateCost { + CandidatePlan plan; + uint64_t logicalTasksBefore = 0; + uint64_t logicalTasksAfter = 0; + uint64_t actualProgramsBefore = 0; + uint64_t actualProgramsAfter = 0; + uint64_t physicalWavesBefore = 0; + uint64_t physicalWavesAfter = 0; + uint64_t launchesBefore = 0; + uint64_t launchesAfter = 0; + uint64_t gmReadBytesBefore = 0; + uint64_t gmReadBytesAfter = 0; + uint64_t gmWriteBytesBefore = 0; + uint64_t gmWriteBytesAfter = 0; + uint64_t storeCountBefore = 0; + uint64_t storeCountAfter = 0; + uint64_t addressCalculationsBefore = 0; + uint64_t addressCalculationsAfter = 0; + uint64_t baselinePeakLiveBytes = 0; + uint64_t estimatedPeakLiveBytes = 0; + uint64_t persistentLoopTripsBefore = 0; + uint64_t persistentLoopTripsAfter = 0; + uint64_t tokenOnlyRepeatedBytesBefore = 0; + uint64_t tokenOnlyRepeatedBytesAfter = 0; + uint64_t workPerProgramBefore = 0; + uint64_t workPerProgramAfter = 0; + bool hasPeakLiveBytes = false; + bool hasDynamicShape = false; + bool hasUnknownResource = false; + bool legacyAutoMapBefore = false; + bool legacyAutoMapAfter = false; + bool persistent = false; +}; + +struct CostModelWeights { + uint64_t logicalProgramReduction = 64; + uint64_t programReduction = 64; + uint64_t launchReduction = 256; + uint64_t gmByte = 1; + uint64_t store = 16; + uint64_t addressCalculation = 1; + uint64_t liveByte = 1; + uint64_t persistentLoopTrip = 512; + uint64_t tokenOnlyRepeatedByte = 1; +}; + +struct CandidateEvaluation { + CandidateCost candidate; + bool accepted = false; + ResourceCostRejectReason reason = ResourceCostRejectReason::UnknownResource; + int64_t benefitScore = 0; + uint64_t safeUBBudgetBytes = 0; + uint64_t requiredParallelPrograms = 0; + uint64_t effectiveWorkPerProgram = 0; + uint64_t physicalWavesBefore = 0; + uint64_t physicalWavesAfter = 0; + uint64_t persistentLoopTripsBefore = 0; + uint64_t persistentLoopTripsAfter = 0; + uint64_t tokenOnlyRepeatedBytesBefore = 0; + uint64_t tokenOnlyRepeatedBytesAfter = 0; + std::string remark; +}; + +CandidateEvaluation evaluateCandidateCost(const ResourceSnapshot &resources, + const CandidateCost &candidate, + const CostModelWeights &weights = {}); + +llvm::SmallVector +enumerateCandidatePlans(llvm::ArrayRef tensorizeFactors, + llvm::ArrayRef blockTFactors, + llvm::ArrayRef staticAxisFusionFactors, + llvm::StringRef stableIdPrefix = "resource"); + +void sortCandidateEvaluations( + llvm::MutableArrayRef values); + +std::string formatCandidateRemark(const CandidateEvaluation &evaluation); +void emitCandidateRemark(Operation *anchor, + const CandidateEvaluation &evaluation); + +struct ProfilerCalibrationRecord { + std::string kernelName; + std::string profilerArtifact; + uint64_t sampleCount = 0; + uint64_t medianNanoseconds = 0; + uint64_t p90Nanoseconds = 0; + CandidateEvaluation evaluation; +}; + +std::string +formatProfilerCalibrationRecord(const ProfilerCalibrationRecord &record); + +class ResourceCostAnalysis { +public: + ResourceCostAnalysis(Operation *root, ResourceSnapshot resources); + + const ResourceSnapshot &getResourceSnapshot() const { return resources; } + const LiveByteEstimate &getLiveByteEstimate() const { return liveBytes; } + CandidateEvaluation evaluate(const CandidateCost &candidate) const; + +private: + ResourceSnapshot resources; + LiveByteEstimate liveBytes; +}; + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif diff --git a/third_party/ascend/include/TritonToGraph/tensor.h b/third_party/ascend/include/TritonToGraph/tensor.h new file mode 100644 index 0000000000..49a9e58df8 --- /dev/null +++ b/third_party/ascend/include/TritonToGraph/tensor.h @@ -0,0 +1,166 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_TO_CFG_TENSOR_H +#define TRITON_TO_CFG_TENSOR_H + +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Types.h" +#include "triton/Dialect/Triton/IR/Types.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace mlir { +namespace triton { +namespace cfg { + +// Compute类型(用于指令分类) +enum class ComputeType { + CUBE, // shape为2维 + VECTOR, // shape为1维 + SCALAR // 其他标量 +}; + +// TensorObject - Tensor对象定义 +class TensorObject { +public: + // Tensor类型分类 + enum class TensorKind { + GLOBAL_MEMORY, // 全局内存tensor(如gm_obj) + L2, // L2 Cache + L1, // L1 Cache + UB // Unified Buffer + }; + + // 构造函数 + TensorObject(StringRef name, ArrayRef shape, Type type, + Type elementType, TensorKind kind = TensorKind::GLOBAL_MEMORY) + : name(name.str()), shape(shape.begin(), shape.end()), type(type), + elementType(elementType), kind(kind) {} + + // 获取tensor名称 + const std::string &getName() const { return name; } + + // 设置tensor名称 + void setName(StringRef newName) { name = newName.str(); } + + // 获取shape + ArrayRef getShape() const { return shape; } + + // 获取MLIR类型 + Type getType() const { return type; } + + // 获取tensor种类 + TensorKind getKind() const { return kind; } + + // 设置tensor种类 + void setKind(TensorKind newKind) { kind = newKind; } + + // 获取元素数据类型 + Type getElementType() const { return elementType; } + + // 设置元素数据类型 + void setElementType(Type newElementType) { elementType = newElementType; } + + // 获取维度数 + size_t getRank() const { return shape.size(); } + + // 获取tensor大小(元素总数) + int64_t getSize() const { + int64_t size = 1; + for (int64_t dim : shape) { + size *= dim; + } + return size; + } + + // 判断是否是一维tensor + bool isVector() const { return shape.size() == 1; } + + // 判断是否是二维tensor + bool isMatrix() const { return shape.size() == 2; } + + // 判断是否是标量 + bool isScalar() const { return shape.empty(); } + + // 打印信息 + void print(llvm::raw_ostream &os) const { + os << "Tensor[" << name << ", shape=["; + for (size_t i = 0; i < shape.size(); ++i) { + if (i > 0) + os << ", "; + os << shape[i]; + } + os << "], element=" << elementType << ", kind=" << getKindString() << "]"; + } + + // 获取kind的字符串表示 + std::string getKindString() const { + switch (kind) { + case TensorKind::GLOBAL_MEMORY: + return "GLOBAL_MEMORY"; + case TensorKind::L2: + return "L2"; + case TensorKind::L1: + return "L1"; + case TensorKind::UB: + return "UB"; + } + return "UNKNOWN"; + } + +private: + std::string name; // Tensor名称,如"gm_obj_0" + SmallVector shape; + Type type; // 完整类型(如tensor<64x64xf32>) + Type elementType; // 元素数据类型(如f32, i8, f16等) + TensorKind kind; +}; + +// 从类型中提取shape和element type +inline void extractShapeAndElementType(Type type, + SmallVectorImpl &shape, + Type &elementType) { + if (auto rankedType = mlir::dyn_cast(type)) { + shape.append(rankedType.getShape().begin(), rankedType.getShape().end()); + elementType = rankedType.getElementType(); + } else if (auto ptrType = mlir::dyn_cast(type)) { + Type pointeeType = ptrType.getPointeeType(); + if (auto rankedType = mlir::dyn_cast(pointeeType)) { + shape.append(rankedType.getShape().begin(), rankedType.getShape().end()); + elementType = rankedType.getElementType(); + } else { + // 标量指针 + elementType = pointeeType; + } + } else { + // 默认值 + elementType = type; + } +} + +} // namespace cfg +} // namespace triton +} // namespace mlir + +#endif // TRITON_TO_CFG_TENSOR_H diff --git a/third_party/ascend/include/TritonToHFusion/CMakeLists.txt b/third_party/ascend/include/TritonToHFusion/CMakeLists.txt new file mode 100644 index 0000000000..dc6dfc3906 --- /dev/null +++ b/third_party/ascend/include/TritonToHFusion/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonToHFusion) +add_public_tablegen_target(TritonToHFusionConversionPassIncGen) diff --git a/third_party/ascend/include/TritonToHFusion/Passes.h b/third_party/ascend/include/TritonToHFusion/Passes.h new file mode 100644 index 0000000000..ff5701c0ee --- /dev/null +++ b/third_party/ascend/include/TritonToHFusion/Passes.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITON_TO_HFUSION_CONVERSION_PASSES_H +#define TRITON_ADAPTER_TRITON_TO_HFUSION_CONVERSION_PASSES_H + +#include "mlir/Pass/Pass.h" + +namespace mlir { +// Forward declarations. +class ModuleOp; + +namespace triton { + +/// Creates a pass to convert Triton dialect to HFusion dialect. +/// When compileOn91095 is true (950 target), histogram is handled by +/// TritonToLinalg pass instead of this pass. +std::unique_ptr> +createTritonToHFusionPass(bool compileOn91095 = false); + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToHFusion/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_TRITON_TO_HFUSION_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/TritonToHFusion/Passes.td b/third_party/ascend/include/TritonToHFusion/Passes.td new file mode 100644 index 0000000000..9903d97a12 --- /dev/null +++ b/third_party/ascend/include/TritonToHFusion/Passes.td @@ -0,0 +1,17 @@ +/* + * Copyright (c) Huawei Technologies Co. + * Licensed under the MIT license. + */ + +#ifndef TRITON_TO_HFUSION_CONVERSION_PASSES +#define TRITON_TO_HFUSION_CONVERSION_PASSES + +include "mlir/Pass/PassBase.td" + +def TritonToHFusion : Pass<"triton-to-hfusion", "mlir::ModuleOp"> { + let summary = "Convert Triton to HFusion dialect"; + let constructor = "triton::createTritonToHFusionPass()"; + let dependentDialects = ["hfusion::HFusionDialect"]; +} + +#endif // TRITON_TO_HFUSION_CONVERSION_PASSES diff --git a/third_party/ascend/include/TritonToHIVM/CMakeLists.txt b/third_party/ascend/include/TritonToHIVM/CMakeLists.txt new file mode 100644 index 0000000000..112189a75f --- /dev/null +++ b/third_party/ascend/include/TritonToHIVM/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonToHIVM) +add_public_tablegen_target(TritonToHIVMConversionPassIncGen) diff --git a/third_party/ascend/include/TritonToHIVM/Passes.h b/third_party/ascend/include/TritonToHIVM/Passes.h new file mode 100644 index 0000000000..6d55efdb73 --- /dev/null +++ b/third_party/ascend/include/TritonToHIVM/Passes.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITON_TO_HIVM_CONVERSION_PASSES_H +#define TRITON_ADAPTER_TRITON_TO_HIVM_CONVERSION_PASSES_H + +#include "mlir/Pass/Pass.h" + +namespace mlir { +// Forward declarations. +class ModuleOp; + +namespace triton { + +/// Creates a pass to convert Triton dialect to HIVM dialect. +std::unique_ptr> createTritonToHIVMPass(); + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToHIVM/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_TRITON_TO_HIVM_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/TritonToHIVM/Passes.td b/third_party/ascend/include/TritonToHIVM/Passes.td new file mode 100644 index 0000000000..7aa37c5136 --- /dev/null +++ b/third_party/ascend/include/TritonToHIVM/Passes.td @@ -0,0 +1,17 @@ +/* + * Copyright (c) Huawei Technologies Co. + * Licensed under the MIT license. + */ + +#ifndef TRITON_TO_HIVM_CONVERSION_PASSES +#define TRITON_TO_HIVM_CONVERSION_PASSES + +include "mlir/Pass/PassBase.td" + +def TritonToHIVM : Pass<"triton-to-hivm", "mlir::ModuleOp"> { + let summary = "Convert Triton to HIVM dialect"; + let constructor = "triton::createTritonToHIVMPass()"; + let dependentDialects = ["hivm::HIVMDialect"]; +} + +#endif // TRITON_TO_HIVM_CONVERSION_PASSES diff --git a/third_party/ascend/include/TritonToLLVM/CMakeLists.txt b/third_party/ascend/include/TritonToLLVM/CMakeLists.txt new file mode 100644 index 0000000000..fb5a9b2952 --- /dev/null +++ b/third_party/ascend/include/TritonToLLVM/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonToLLVM) +add_public_tablegen_target(TritonToLLVMConversionPassIncGen) diff --git a/third_party/ascend/include/TritonToLLVM/Passes.h b/third_party/ascend/include/TritonToLLVM/Passes.h new file mode 100644 index 0000000000..3dfb3ec1cf --- /dev/null +++ b/third_party/ascend/include/TritonToLLVM/Passes.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) Huawei Technologies Co. + * Licensed under the MIT license. + */ + +#ifndef TRITON_ADAPTER_TRITON_TO_LLVM_CONVERSION_PASSES_H +#define TRITON_ADAPTER_TRITON_TO_LLVM_CONVERSION_PASSES_H + +#include "mlir/Pass/Pass.h" + +namespace mlir { +// Forward declarations. +class ModuleOp; + +namespace triton { + +/// Creates a pass to convert Triton dialect to LLVM dialect. +std::unique_ptr> createTritonToLLVMPass(); + +/// Creates a pass to normalize locations for debug line table generation. +std::unique_ptr> +createNormalizeDebugLineLocationsPass(); + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToLLVM/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_TRITON_TO_LLVM_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/TritonToLLVM/Passes.td b/third_party/ascend/include/TritonToLLVM/Passes.td new file mode 100644 index 0000000000..5e1a730bdf --- /dev/null +++ b/third_party/ascend/include/TritonToLLVM/Passes.td @@ -0,0 +1,32 @@ +/* + * Copyright (c) Huawei Technologies Co. + * Licensed under the MIT license. + */ + +#ifndef TRITON_TO_LLVM_CONVERSION_PASSES +#define TRITON_TO_LLVM_CONVERSION_PASSES + +include "mlir/Pass/PassBase.td" + +def TritonToLLVM : Pass<"triton-to-llvm", "mlir::ModuleOp"> { + let summary = "Convert Triton to LLVM dialect"; + let constructor = "triton::createTritonToLLVMPass()"; + let dependentDialects = ["LLVM::LLVMDialect", "tensor::TensorDialect", "arith::ArithDialect"]; +} + +def NormalizeDebugLineLocations : Pass<"normalize-debug-line-locations", "mlir::ModuleOp"> { + let summary = "Normalize locations for source-level debug line tables."; + let description = [{ + This pass classifies operations as semantic, control, or synthetic from the + perspective of source-level debug line table generation. It preserves or + canonicalizes source locations for semantic/control operations, records the + selected class in attributes, and moves synthetic lowering-only operations to + non-user line anchors while preserving original provenance in attributes. + Some synthetic address/value preparation operations may be retargeted to the + associated user-visible store location when that better reflects the source + line. The pass must not change IR semantics or codegen. + }]; + let constructor = "triton::createNormalizeDebugLineLocationsPass()"; +} + +#endif // TRITON_TO_LLVM_CONVERSION_PASSES diff --git a/third_party/ascend/include/TritonToLinalg/ArgMinMaxConverter.h b/third_party/ascend/include/TritonToLinalg/ArgMinMaxConverter.h new file mode 100644 index 0000000000..f09b23f360 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/ArgMinMaxConverter.h @@ -0,0 +1,356 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_ARGMINMAXCONVERTER_H +#define TRITON_ADAPTER_ARGMINMAXCONVERTER_H + +#include "ConversionPatterns.h" +#include "ascend/include/Utils/Utils.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/Interfaces/FunctionInterfaces.h" +#include "mlir/Transforms/DialectConversion.h" + +#define DEBUG_TYPE "triton-to-linalg" + +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" + +#include + +namespace TTOpConverters { +using namespace mlir; +using namespace triton; + +template +class ArgMinMaxBaseConverter : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult matchTieBreakResult(Value currValue, Value currIndex, + Value reduceValue, Value reduceIndex, + mlir::Block::iterator &it, + Value &tileBreakValue) const { + LLVM_DEBUG(llvm::dbgs() << "Matching: " << *it << "\n"); + auto eqCmpOp = dyn_cast(*it); + if (eqCmpOp) { + if (eqCmpOp.getPredicate() != arith::CmpFPredicate::OEQ || + currValue != eqCmpOp.getLhs() || reduceValue != eqCmpOp.getRhs()) { + return failure(); + } + } + + auto eqCmpIOp = dyn_cast(*it++); + if (eqCmpIOp) { + if (eqCmpIOp.getPredicate() != arith::CmpIPredicate::eq || + currValue != eqCmpIOp.getLhs() || reduceValue != eqCmpIOp.getRhs()) { + return failure(); + } + } + + LLVM_DEBUG(llvm::dbgs() << "Matching: " << *it << "\n"); + auto sltCmpOp = dyn_cast(*it++); + if (!sltCmpOp || sltCmpOp.getPredicate() != arith::CmpIPredicate::slt || + currIndex != sltCmpOp.getLhs() || reduceIndex != sltCmpOp.getRhs()) { + return failure(); + } + + // matching: %13 = arith.andi %11, %12 : i1 + LLVM_DEBUG(llvm::dbgs() << "Matching: " << *it << "\n"); + auto andOp = dyn_cast(*it++); + + Value cmpOp; + if (eqCmpOp) + cmpOp = eqCmpOp; + else + cmpOp = eqCmpIOp; + + if (!andOp || andOp.getLhs() != cmpOp || andOp.getRhs() != sltCmpOp) { + return failure(); + } + + tileBreakValue = andOp; + return success(); + } + + LogicalResult matchShouldUpdateValue(Value currValue, Value currIndex, + Value reduceValue, Value reduceIndex, + mlir::Block::iterator &it, + Value &shouldUpdate) const { + Value tieResult; + if (failed(matchTieBreakResult(currValue, currIndex, reduceValue, + reduceIndex, it, tieResult))) { + LLVM_DEBUG(llvm::dbgs() << "Tie break result match failed\n"); + return failure(); + } + + Value comparisonResult; + if (failed(T::matchComparisonResult(currValue, currIndex, reduceValue, + reduceIndex, it, comparisonResult))) { + LLVM_DEBUG(llvm::dbgs() << "Comparison result match failed\n"); + return failure(); + } + + LLVM_DEBUG(llvm::dbgs() << "Matching: " << *it << "\n"); + auto orOp = dyn_cast(*it++); + if (!orOp || orOp.getLhs() != comparisonResult || + orOp.getRhs() != tieResult) { + return failure(); + } + + shouldUpdate = orOp; + return success(); + } + + Value getInitTensor(ConversionPatternRewriter &rewriter, + ArrayRef shape, Value fillValue, + Location loc) const { + Value initTensor = + rewriter.create(loc, shape, fillValue.getType()); + return rewriter + .create(loc, ValueRange{fillValue}, + ValueRange{initTensor}) + .result(); + } + +public: + ArgMinMaxBaseConverter(MLIRContext *context) : OpConversionPattern(context) {} + + LogicalResult + matchAndRewrite(triton::ReduceOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override { + if (op.getBody()->getNumArguments() != 4) { + return failure(); + } + + auto block = op.getBody(); + auto ops = block->without_terminator(); + + Value currValue = block->getArgument(0); + Value currIndex = block->getArgument(1); + Value reduceValue = block->getArgument(2); + Value reduceIndex = block->getArgument(3); + + auto opsIt = ops.begin(); + Value shouldUpdate; + if (failed(matchShouldUpdateValue(currValue, currIndex, reduceValue, + reduceIndex, opsIt, shouldUpdate))) { + return failure(); + } + + LLVM_DEBUG(llvm::dbgs() << "Matching: " << *opsIt << "\n"); + auto valueSelectOp = dyn_cast(*opsIt++); + if (!valueSelectOp || valueSelectOp.getCondition() != shouldUpdate || + currValue != valueSelectOp.getTrueValue() || + reduceValue != valueSelectOp.getFalseValue()) { + return failure(); + } + + LLVM_DEBUG(llvm::dbgs() << "Matching: " << *opsIt << "\n"); + auto indexSelectOp = dyn_cast(*opsIt++); + if (!indexSelectOp || indexSelectOp.getCondition() != shouldUpdate || + currIndex != indexSelectOp.getTrueValue() || + reduceIndex != indexSelectOp.getFalseValue()) { + return failure(); + } + + LLVM_DEBUG(llvm::dbgs() << "Matching: " << *opsIt << "\n"); + auto termOp = dyn_cast(*opsIt++); + if (!(termOp && termOp == block->getTerminator() && + termOp.getOperands() == + ArrayRef{valueSelectOp, indexSelectOp})) { + return failure(); + } + + // Rewrite phase: perform the actual conversion + auto loc = op.getLoc(); + auto elemTypes = op.getElementTypes(); + + auto valueType = elemTypes[0]; + // tl.argmin reorder + bool isUnsigned = false; + if (isa(valueType)) { + arith::CmpFOp cmpFOp; + block->walk([&](arith::CmpFOp cmpOp) { + auto pred = cmpOp.getPredicate(); + if (pred == arith::CmpFPredicate::OEQ || + pred == arith::CmpFPredicate::ONE || + pred == arith::CmpFPredicate::UEQ || + pred == arith::CmpFPredicate::UNE) { + return WalkResult::advance(); + } else if (pred == arith::CmpFPredicate::OGT || + pred == arith::CmpFPredicate::OLT || + pred == arith::CmpFPredicate::UGT || + pred == arith::CmpFPredicate::ULT) { + cmpFOp = cmpOp; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + cmpFOp->moveBefore(block, block->getOperations().begin()); + } else if (isa(valueType)) { + arith::CmpIOp cmpIOp; + block->walk([&](arith::CmpIOp cmpOp) { + auto pred = cmpOp.getPredicate(); + if (pred == arith::CmpIPredicate::ugt || + pred == arith::CmpIPredicate::ult) { + isUnsigned = true; + } + if (pred == arith::CmpIPredicate::eq || + pred == arith::CmpIPredicate::ne) { + return WalkResult::advance(); + } else if (pred == arith::CmpIPredicate::sgt || + pred == arith::CmpIPredicate::slt || + pred == arith::CmpIPredicate::ugt || + pred == arith::CmpIPredicate::ult) { + if (cmpOp.getLhs() == block->getArgument(0) && + cmpOp.getRhs() == block->getArgument(2)) { + cmpIOp = cmpOp; + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + cmpIOp->moveBefore(block, block->getOperations().begin()); + } + + TypedAttr valueAttr; + if (isa(valueType)) { + valueAttr = rewriter.getFloatAttr(valueType, T::getBaseReductionValue()); + } else if (isa(valueType)) { + if (isUnsigned) { + valueAttr = + rewriter.getIntegerAttr(valueType, T::getBaseReductionUIntValue()); + } else { + valueAttr = + rewriter.getIntegerAttr(valueType, T::getBaseReductionIntValue()); + } + } + + auto reduceWithIndexParams = getReduceWithIndexParams(op); + auto valuesAccBaseVal = + rewriter.create(loc, valueType, valueAttr); + int indicesInitValue = + (llvm::succeeded(reduceWithIndexParams) && + reduceWithIndexParams->tieBreakType == TieBreakType::RIGHT) + ? -1 + : std::numeric_limits::max(); + + auto indexType = elemTypes[1]; + auto indicesAccBaseVal = rewriter.create( + loc, indexType, rewriter.getIntegerAttr(indexType, indicesInitValue)); + + auto valueResultType = dyn_cast(op.getType(0)); + const auto isScalarReduce = valueResultType == nullptr; + SmallVector reductionResultShape{ + isScalarReduce ? SmallVector{} + : SmallVector(valueResultType.getShape())}; + + SmallVector outputs{ + getInitTensor(rewriter, reductionResultShape, valuesAccBaseVal, loc), + getInitTensor(rewriter, reductionResultShape, indicesAccBaseVal, loc)}; + + auto linalgOp = rewriter.create( + loc, adaptor.getOperands(), outputs, + SmallVector{adaptor.getAxis()}, + [&](OpBuilder &b, Location loc, ValueRange inputs) { + assert(inputs.size() == 4); + + auto tritonReduceBlock = op.getBody(); + IRMapping mapping; + mapping.map(tritonReduceBlock->getArguments(), inputs); + + for (auto &op : tritonReduceBlock->without_terminator()) { + b.clone(op, mapping); + } + + auto tritonYield = tritonReduceBlock->getTerminator(); + auto results = + llvm::map_to_vector(tritonYield->getOperands(), [&](Value val) { + return mapping.lookup(val); + }); + b.create(loc, results); + }); + + // before we rewrite the argmax reduce op, we know it has return value + // so addReduceWithIndexAttrIfNeeded won't fail + // but ignoring it will lead to compiling failure + if (llvm::succeeded(reduceWithIndexParams) && + reduceWithIndexParams->tieBreakType != TieBreakType::None) { + addReduceWithIndexAttr(*reduceWithIndexParams, rewriter, linalgOp); + } + + if (isScalarReduce) { + SmallVector reduceResults{ + rewriter.create( + loc, valueType, linalgOp.getResults()[0], ValueRange{}), + rewriter.create( + loc, indexType, linalgOp.getResults()[1], ValueRange{})}; + rewriter.replaceOp(op, reduceResults); + } else { + rewriter.replaceOp(op, linalgOp); + } + + return success(); + } +}; + +class ArgMinConverter : public ArgMinMaxBaseConverter { +public: + static LogicalResult matchComparisonResult(Value currValue, Value currIndex, + Value reduceValue, + Value reduceIndex, + mlir::Block::iterator &it, + Value &comparisonResult); + + static float getBaseReductionValue(); + + static int8_t getBaseReductionIntValue(); + static uint8_t getBaseReductionUIntValue(); + + ArgMinConverter(MLIRContext *context) : ArgMinMaxBaseConverter(context) {} +}; + +class ArgMaxConverter : public ArgMinMaxBaseConverter { +public: + static LogicalResult matchComparisonResult(Value currValue, Value currIndex, + Value reduceValue, + Value reduceIndex, + mlir::Block::iterator &it, + Value &comparisonResult); + + static float getBaseReductionValue(); + + static int8_t getBaseReductionIntValue(); + static uint8_t getBaseReductionUIntValue(); + + ArgMaxConverter(MLIRContext *context) : ArgMinMaxBaseConverter(context) {} +}; + +void populatePostConversionCanonicalizationPatterns( + RewritePatternSet &patterns); + +} // namespace TTOpConverters + +#endif diff --git a/third_party/ascend/include/TritonToLinalg/BlockPtrAnalysis.h b/third_party/ascend/include/TritonToLinalg/BlockPtrAnalysis.h new file mode 100644 index 0000000000..c1d799a1bc --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/BlockPtrAnalysis.h @@ -0,0 +1,378 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ANALYSIS_BLOCKPTRANALYSIS_H +#define TRITON_ANALYSIS_BLOCKPTRANALYSIS_H +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +#include +namespace mlir { + +class ConversionPatternRewriter; + +namespace triton { + +/// Temporary provenance for memref carriers materialized from complete scalar +/// pointer addresses. Address round-trip folding is valid only for casts with +/// this marker; ordinary HIVM PointerCast operations retain their baseline +/// descriptor semantics. TritonToLinalg removes the marker after conversion. +inline constexpr llvm::StringLiteral kScalarPointerCarrierAttr = + "ScalarPointerCarrier"; + +/// Returns true when a loop still carries Triton pointer state, or when a +/// loop-carried integer tensor participates in legacy address computation. +/// Ordinary memrefs, including fixed-layout memrefs created by +/// memref.reinterpret_cast, are complete SSA values and do not require the +/// legacy BlockData loop expansion solely because of their producer. +bool needsLegacyBlockDataLoopRewrite(LoopLikeOpInterface loopOp); + +enum class MemAccVal { Undefined = 0, StrucMemAcc = 1, UnstrucMemAcc = 2 }; + +/// Creates a verifier-valid HIVM pointer cast for a scalar Triton pointer +/// represented by an integer address. Triton scalar pointers do not carry an +/// extent, while their downstream carrier is normally `memref` and every +/// dynamic memref dimension requires a size operand. Use one element as the +/// conservative carrier extent; reinterpret-cast lowering replaces it with a +/// precise access range when a larger descriptor is materialized. +hivm::PointerCastOp createScalarPointerCast(OpBuilder &builder, Location loc, + MemRefType resultType, + Value address); + +struct MemAccType { + + MemAccVal value; + + explicit constexpr MemAccType(MemAccVal v = MemAccVal::Undefined) + : value(v) {} + + constexpr operator MemAccVal() const { return value; } + explicit operator bool() = delete; + + constexpr bool isUndefined() const { return value == MemAccVal::Undefined; } + constexpr bool isStructured() const { + return value == MemAccVal::StrucMemAcc; + } + constexpr bool isUnstructured() const { + return value == MemAccVal::UnstrucMemAcc; + } + + void merge(MemAccType &other) { + this->value = (this->value > other.value) ? this->value : other.value; + } + + std::string_view toString() const { + static constexpr std::string_view names[] = {"Undefined", "StrucMemAcc", + "UnstrucMemAcc"}; + return names[static_cast(value)]; + } +}; + +class BlockData { +public: + SmallVector &getOffsetsRef(); + SmallVector &getSizesRef(); + SmallVector &getStridesRef(); + Value &getSourceRef(); + OpFoldResult &getScalarRef(); + Type &getResElemTyRef(); + MemAccType &getMemAccTypeRef(); + + SmallVector getOffsets() const; + SmallVector getSizes() const; + SmallVector getStrides() const; + Type getResElemTy() const; + OpFoldResult getOffset(int) const; + OpFoldResult getSize(int) const; + OpFoldResult getStride(int) const; + OpFoldResult getScalar() const; + Value getSource() const; + MemAccType getMemAccType() const; + + bool isScalar() const; + bool isEmpty() const; + bool hasSource() const; + bool hasResElemTy() const; + void removeSource(); + + int64_t getRank() const; + FailureOr + getResultMemrefType(int64_t offset, ArrayRef resultShape) const; + + void addBlock(BlockData &lBlock, BlockData &rBlock, Location loc, + ConversionPatternRewriter &rewriter); + void subBlock(BlockData &lBlock, BlockData &rBlock, Location loc, + ConversionPatternRewriter &rewriter); + void mulBlock(BlockData &lBlock, BlockData &rBlock, Location loc, + ConversionPatternRewriter &rewriter); + void divBlock(BlockData &lBlock, BlockData &rBlock, Location loc, + ConversionPatternRewriter &rewriter); + + FailureOr + createCastOp(ArrayRef resultShape, const Location &loc, + OpBuilder &builder) const; + + void setResElemTy(const Type &); + void setSource(const Value &); + void setScalar(const OpFoldResult &); + void setOffsets(const SmallVector &); + void setStrides(const SmallVector &); + void setSizes(const SmallVector &); + void setMemAccTy(const MemAccType &); + void setMemAccVal(const MemAccVal); + + void dump() const; + +private: + SmallVector offsets; + SmallVector sizes; + SmallVector strides; + Value source; + // `Scalar` is a shortcut used when the entire blockdata describes a single + // scalar value + OpFoldResult scalar; + Type resElemTy; + MemAccType memAccTy; + + // Accumulate offsets of each dimension in BlockData to get a total offset + // from source ptr, which is used in memref::ReinterpretCastOp + OpFoldResult inferBlockOffset(const Location &loc, OpBuilder &builder) const; +}; + +class BlockDataParser { +public: + /// Materialize the rank-one memref used by a scalar load only when the + /// pointer producer has already been converted to a compatible memref. + /// Returning failure lets a consumer reject an unconverted producer without + /// asserting or creating partial IR. + static FailureOr getScalarMemRef(Value ptr, Value memref, + const Location &loc, + ConversionPatternRewriter &rewriter); + + static LogicalResult + parse(Value operand, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseAdd(arith::AddIOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseSub(arith::SubIOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseMul(arith::MulIOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseDiv(arith::DivSIOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseRem(arith::RemSIOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static void + parseUnrealizedCast(UnrealizedConversionCastOp op, BlockData &data, + const Location &loc, ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static void + parseMakeRange(triton::MakeRangeOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static void parseLinalgGenericFromMakeRange( + linalg::GenericOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseExpandDims(triton::ExpandDimsOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseBitcast(triton::BitcastOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseExtSI(arith::ExtSIOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseBroadcast(triton::BroadcastOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseSplat(triton::SplatOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static void + parseConstSplat(arith::ConstantOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + template + static std::enable_if_t || + std::is_same_v, + LogicalResult> + parseTensorPtr(T op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseAddPtr(triton::AddPtrOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseExtractSlice(tensor::ExtractSliceOp op, BlockData &data, + const Location &loc, ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static void + parseReinterpretCast(memref::ReinterpretCastOp op, BlockData &data, + const Location &loc, ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseReduce(triton::ReduceOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static void + parseAtomicRmw(triton::AtomicRMWOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseFill(linalg::FillOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static LogicalResult + parseSelect(arith::SelectOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + static void parseCustomOp(hivm::CustomOp op, BlockData &data, + const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known, + unsigned resultIdx); + + static LogicalResult + parseStructuredCustomOp(Operation *op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known, + unsigned resultIdx); + + static void rewriteStructuredCustomOp(hivm::CustomOp op, + hivm::CustomOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter); + + static void rewriteStructuredCustomOp(hivm::CustomMacroOp op, + hivm::CustomMacroOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter); + + static void rewriteStructuredCustomOp(Operation *op, + ConversionPatternRewriter &rewriter); + + static LogicalResult + rewriteAddPtr(triton::AddPtrOp op, triton::AddPtrOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known); + + static FailureOr + materializePointer(Value ptr, ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known); + + static LogicalResult + rewriteMakeTensorPtrOp(triton::MakeTensorPtrOp op, Value convertedBase, + ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known); + + static LogicalResult + rewriteAdvanceOp(triton::AdvanceOp op, ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known); + + static LogicalResult + rewriteCustomOp(hivm::CustomOp op, hivm::CustomOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known); + + template + static std::enable_if_t || + std::is_same_v, + LogicalResult> + rewriteTerminator(T op, ConversionPatternRewriter &rewriter, + const llvm::SmallDenseSet &blockArgIdxSet, + ArrayRef iterArgIdxMap, + const llvm::SmallDenseMap &known); + + /// @param known is mainly designed for `rewriteLoop`, and is just non-const + /// in `rewriteLoop`, `rewriteAddPtr` and `rewriteAdvance` + static LogicalResult + rewriteLoopOp(LoopLikeOpInterface op, ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known, + ArrayRef onlyIndexTensorSlots = {}); + + static LogicalResult rewriteAddPtrToUnstrucMemAcc( + triton::AddPtrOp op, triton::AddPtrOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter, BlockData &data); +}; + +template +void parseIndirectLoad(OpTy op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known, + unsigned resultIdx = 0); + +} // namespace triton + +} // namespace mlir + +#endif diff --git a/third_party/ascend/include/TritonToLinalg/CMakeLists.txt b/third_party/ascend/include/TritonToLinalg/CMakeLists.txt new file mode 100644 index 0000000000..d62f670bbe --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonToLinalg) +add_public_tablegen_target(TritonToLinalgConversionPassIncGen) diff --git a/third_party/ascend/include/TritonToLinalg/CanonicalizeDebugLocationsPass.h b/third_party/ascend/include/TritonToLinalg/CanonicalizeDebugLocationsPass.h new file mode 100644 index 0000000000..854ad875ee --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/CanonicalizeDebugLocationsPass.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +//===----------------------------------------------------------------------===// +// CanonicalizeDebugLocationsPass +// +// Collapses call-site locations whose callee is an inlined Triton stdlib +// helper (under site-packages) down to their caller (user-file) frame, so the +// emitted DWARF references the user's source line instead of the library. +// +// Run BEFORE DeduplicateDebugNopsPass. Gated by +// LLVM_EXTRACT_DI_LOCAL_VARIABLES=1. +//===----------------------------------------------------------------------===// + +#ifndef TRITON_ASCEND_TRITONTOLINALG_CANONICALIZEDEBUGLOCATIONSPASS_H +#define TRITON_ASCEND_TRITONTOLINALG_CANONICALIZEDEBUGLOCATIONSPASS_H + +// This pass is added programmatically to the TritonToLinalg pipeline (see +// compiler.py) rather than registered through TableGen, so it has no +// Passes.td entry and no GEN_PASS_DECL include. +#include "mlir/Pass/Pass.h" +#include + +namespace mlir { +namespace triton { + +std::unique_ptr createCanonicalizeDebugLocationsPass(); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ASCEND_TRITONTOLINALG_CANONICALIZEDEBUGLOCATIONSPASS_H diff --git a/third_party/ascend/include/TritonToLinalg/ConversionPatterns.h b/third_party/ascend/include/TritonToLinalg/ConversionPatterns.h new file mode 100644 index 0000000000..4199810dcf --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/ConversionPatterns.h @@ -0,0 +1,133 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef CONVERSIONPATTERNS_H +#define CONVERSIONPATTERNS_H + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Passes.h" + +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/MathExtras.h" + +#include +#include +#include + +using namespace mlir; +using namespace triton; + +//===----------------------------------------------------------------------===// +// Utilities +//===----------------------------------------------------------------------===// + +static Value getScalarValue(Value operand, Location loc, + ConversionPatternRewriter &rewriter) { + SmallVector ops; + + auto reconstructScalarValue = [&](Value src) { + for (auto op = ops.rbegin(); op != ops.rend(); ++op) { + src = TypeSwitch(*op) + .Case([&](Operation *op) { + auto resType = op->getResults()[0].getType(); + if (auto shapedType = dyn_cast(resType)) { + resType = shapedType.getElementType(); + } + return rewriter.create(loc, resType, src); + }) + .Case([&](Operation *op) { + auto resType = op->getResults()[0].getType(); + if (auto shapedType = dyn_cast(resType)) { + resType = shapedType.getElementType(); + } + return rewriter.create(loc, resType, src); + }) + .Default([](Operation *op) { + llvm_unreachable("unsupported op in generating "); + return nullptr; + }); + } + return src; + }; + + while (true) { + if (!dyn_cast(operand.getType())) { + return reconstructScalarValue(operand); + } else if (auto op = operand.getDefiningOp()) { + if (auto attr = dyn_cast(op.getValue())) { + if (!attr.isSplat()) { + InFlightDiagnostic diag = emitError(loc) + << "other value used in masked load " + "produced by unsupported instruction"; + return nullptr; + } + auto elemValue = attr.getSplatValue(); + auto constOp = arith::ConstantOp::materialize( + rewriter, elemValue, attr.getElementType(), op.getLoc()); + return reconstructScalarValue(constOp.getResult()); + } + } else if (auto op = operand.getDefiningOp()) { + operand = op.getSrc(); + } else if (auto op = operand.getDefiningOp()) { + ops.push_back(op.getOperation()); + operand = op.getIn(); + } else if (auto op = operand.getDefiningOp()) { + ops.push_back(op.getOperation()); + operand = op.getIn(); + } else { + InFlightDiagnostic diag = emitError(loc) + << "other value used in masked load produced " + "by unsupported instruction"; + return nullptr; + } + } + return nullptr; +} + +static SmallVector getNParallelLoopsAttrs(unsigned n) { + return SmallVector(n, utils::IteratorType::parallel); +} + +// for IntLike and FloatLike types +static std::optional getBitWidth(Type a) { + if (auto type = dyn_cast(a)) { + auto elementType = type.getElementType(); + if (elementType.isIntOrFloat()) { + return type.getElementType().getIntOrFloatBitWidth(); + } + return std::nullopt; + } + + if (a.isIntOrFloat()) { + return a.getIntOrFloatBitWidth(); + } + return std::nullopt; +} +#endif // CONVERSIONPATTERNS_H diff --git a/third_party/ascend/include/TritonToLinalg/DeduplicateDebugNopsPass.h b/third_party/ascend/include/TritonToLinalg/DeduplicateDebugNopsPass.h new file mode 100644 index 0000000000..0cc7762358 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/DeduplicateDebugNopsPass.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_TRITONTOLINALG_DEDUPLICATE_DEBUG_NOPS_H +#define TRITON_ASCEND_TRITONTOLINALG_DEDUPLICATE_DEBUG_NOPS_H + +#include "mlir/Pass/Pass.h" +#include + +namespace mlir { +namespace triton { + +/// Creates a pass that removes duplicate `llvm.inline_asm "nop"` debug anchors, +/// keeping the first per source line so DWARF line tables have one PC per line. +/// Opt-in via `LLVM_EXTRACT_DI_LOCAL_VARIABLES=1`. +std::unique_ptr createDeduplicateDebugNopsPass(); + +#define GEN_PASS_DECL_DEDUPLICATEDEBUGNOPS +#include "ascend/include/TritonToLinalg/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ASCEND_TRITONTOLINALG_DEDUPLICATE_DEBUG_NOPS_H diff --git a/third_party/ascend/include/TritonToLinalg/DescriptorConverter.h b/third_party/ascend/include/TritonToLinalg/DescriptorConverter.h new file mode 100644 index 0000000000..dd2aaeac3d --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/DescriptorConverter.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DESCRIPTORCONVERTER_H +#define TRITON_ADAPTER_DESCRIPTORCONVERTER_H + +#include "TritonToLinalg/BlockPtrAnalysis.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/Interfaces/FunctionInterfaces.h" +#include "mlir/Transforms/DialectConversion.h" + +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Debug.h" + +namespace DescriptorConverter { +using namespace mlir; +using namespace triton; + +struct Descriptor { + Value base; + SmallVector shape; + SmallVector strides; + triton::PaddingOptionAttr padding; +}; + +bool hasATensorDescriptorType(mlir::TypeRange types); + +class DescriptorLoadConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::DescriptorLoadOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class DescriptorStoreConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::DescriptorStoreOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class DescriptorGatherConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::DescriptorGatherOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class DescriptorScatterConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::DescriptorScatterOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class DescriptorReduceConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::DescriptorReduceOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +} // end of namespace DescriptorConverter + +#endif // TRITON_ADAPTER_DESCRIPTORCONVERTER_H diff --git a/third_party/ascend/include/TritonToLinalg/DevicePrintOffsetRewrite.h b/third_party/ascend/include/TritonToLinalg/DevicePrintOffsetRewrite.h new file mode 100644 index 0000000000..9d2f00fee3 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/DevicePrintOffsetRewrite.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_TRITONTOLINALG_DEVICEPRINTOFFSETREWRITE_H +#define TRITON_ASCEND_TRITONTOLINALG_DEVICEPRINTOFFSETREWRITE_H + +#include "mlir/IR/BuiltinOps.h" + +namespace mlir { +namespace triton { + +void rewriteDevicePrintOffsets(ModuleOp moduleOp); + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ASCEND_TRITONTOLINALG_DEVICEPRINTOFFSETREWRITE_H diff --git a/third_party/ascend/include/TritonToLinalg/FunctionConverter.h b/third_party/ascend/include/TritonToLinalg/FunctionConverter.h new file mode 100644 index 0000000000..c0f10e620d --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/FunctionConverter.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_FUNCTIONCONVERTER_H +#define TRITON_ADAPTER_FUNCTIONCONVERTER_H + +#include "mlir/Interfaces/FunctionInterfaces.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace FunctionConverter { +using namespace mlir; +using namespace triton; + +class GetProgramIDConverter + : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + static uint32_t constexpr LAUNCH_GRID_RANK = + getMaxEnumValForProgramIDDim() + 1; + +public: + LogicalResult + matchAndRewrite(triton::GetProgramIdOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class GetNumProgramsConverter + : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + static uint32_t constexpr LAUNCH_GRID_RANK = + getMaxEnumValForProgramIDDim() + 1; + +public: + LogicalResult + matchAndRewrite(triton::GetNumProgramsOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; +} // namespace FunctionConverter +#endif diff --git a/third_party/ascend/include/TritonToLinalg/HoistBroadcast.h b/third_party/ascend/include/TritonToLinalg/HoistBroadcast.h new file mode 100644 index 0000000000..1ddf20f2fb --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/HoistBroadcast.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITONTOLINALG_HOISTBROADCAST_H +#define TRITON_ADAPTER_TRITONTOLINALG_HOISTBROADCAST_H + +#include "ascend/include/TritonToLinalg/BlockPtrAnalysis.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/Interfaces/FunctionInterfaces.h" +#include "mlir/Transforms/DialectConversion.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "triton-to-linalg" + +namespace HoistBroadcast { +using namespace mlir; +using namespace triton; + +class BroadcastConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::BroadcastOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class BroadcastHoister { +public: + BroadcastHoister(triton::BroadcastOp op); + LogicalResult parse(Value operand, const Location &loc, + ConversionPatternRewriter &rewriter); + LogicalResult parseAddptr(triton::AddPtrOp op, const Location &loc, + ConversionPatternRewriter &rewriter); + LogicalResult parseBroadcast(triton::BroadcastOp op, const Location &loc, + ConversionPatternRewriter &rewriter); + LogicalResult parseSplat(triton::SplatOp op, const Location &loc, + ConversionPatternRewriter &rewriter); + LogicalResult findSrc(Value operand); + LogicalResult replaceBroadcastOp(triton::BroadcastOp op, + ConversionPatternRewriter &rewriter); + bool canBroadcast(); + +private: + Value source; + triton::BroadcastOp opToHoist; + SmallVector tensorSizes; + llvm::SmallDenseMap broadcastMap; +}; +} // namespace HoistBroadcast + +#endif // TRITON_ADAPTER_TRITONTOLINALG_HOISTBROADCAST_H diff --git a/third_party/ascend/include/TritonToLinalg/ImplicitPermute.h b/third_party/ascend/include/TritonToLinalg/ImplicitPermute.h new file mode 100644 index 0000000000..b048fa4bd6 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/ImplicitPermute.h @@ -0,0 +1,129 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonMemoryAccess/MemoryAccessTags.h" +#include "TritonToStructured/MaskAnalysis.h" +#include "TritonToStructured/PtrAnalysis.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/AffineMap.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace ImplicitPermute { + +using namespace mlir; +using namespace triton; + +// Tag attached to load/store/atomic ops that this pass rewrote, so downstream +// sub-steps (e.g. StridedLoadStoreRewrite) can detect "already handled by +// ImplicitPermute" and avoid double-processing. +inline constexpr const char *ImplicitPermuteHandledTAG = + mlir::triton::memory_access::ImplicitPermuteHandledTAG; + +class LoadConverter : public OpRewritePattern { +public: + explicit LoadConverter(MLIRContext *context) + : OpRewritePattern(context) {}; + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::LoadOp op, + PatternRewriter &rewriter) const override; +}; + +class StoreConverter : public OpRewritePattern { +public: + explicit StoreConverter(MLIRContext *context) + : OpRewritePattern(context) {}; + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const override; +}; + +class AtomicRMWConverter : public OpRewritePattern { +public: + explicit AtomicRMWConverter(MLIRContext *context) + : OpRewritePattern(context) {}; + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::AtomicRMWOp op, + PatternRewriter &rewriter) const override; +}; + +class AtomicCASConverter : public OpRewritePattern { +public: + explicit AtomicCASConverter(MLIRContext *context) + : OpRewritePattern(context) {}; + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::AtomicCASOp op, + PatternRewriter &rewriter) const override; +}; + +class MemOpTransformer { +public: + TritonToStructured::PtrState ptrState; + TritonToStructured::MaskState maskState; + + enum class MemType { load, store, deafaultType }; + + MemType currentType = MemType::deafaultType; + + MemOpTransformer(MemType memType) : currentType(memType) {} + + Value materializeImplicitPermute(Value srcTensor, const Location loc, + PatternRewriter &rewriter); + + Value createNewAddPtr(Value oldPtr, const Location loc, + PatternRewriter &rewriter); + + Value createNewAdvancePtr(Value oldPtr, const Location loc, + PatternRewriter &rewriter); + + Value createNewTensorPtr(Value oldPtr, const Location loc, + PatternRewriter &rewriter); + + Value createNewMask(Value oldPtr, const Location loc, + PatternRewriter &rewriter); + + Value createNewOther(Value oldOther, const Location loc, + PatternRewriter &rewriter); + + SmallVector + getBoundaryCheck(ArrayRef oldBoundaryCheck) const; + + bool applyPermuteOnMask(); +}; + +} // namespace ImplicitPermute diff --git a/third_party/ascend/include/TritonToLinalg/LoadStoreConverter.h b/third_party/ascend/include/TritonToLinalg/LoadStoreConverter.h new file mode 100644 index 0000000000..e60c03dcf1 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/LoadStoreConverter.h @@ -0,0 +1,315 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_LOADSTORECONVERTER_H +#define TRITON_ADAPTER_LOADSTORECONVERTER_H + +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/AffineMap.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" + +#include "ascend/include/TritonToLinalg/BlockPtrAnalysis.h" + +namespace LoadStoreConverter { + +using namespace mlir; +using namespace triton; + +class AddPtrConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::AddPtrOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +/// Materialize pointer tensor expressions directly instead of routing them +/// through a synthetic AddPtrOp with a zero offset. A higher benefit than the +/// generic value converters ensures pointer layout is handled by +/// BlockDataParser. +template +class MemoryPointerConverter : public OpConversionPattern { +public: + explicit MemoryPointerConverter(MLIRContext *context) + : OpConversionPattern(context, PatternBenefit(2)) {} + + LogicalResult + matchAndRewrite(OpTy op, typename OpTy::Adaptor, + ConversionPatternRewriter &rewriter) const override { + auto resultTy = dyn_cast(op->getResult(0).getType()); + if (!resultTy || !isa(resultTy.getElementType())) + return failure(); + + llvm::SmallDenseMap known; + FailureOr memref = + BlockDataParser::materializePointer(op->getResult(0), rewriter, known); + if (failed(memref)) + return rewriter.notifyMatchFailure( + op, "unsupported pointer expression for direct materialization"); + + rewriter.replaceOp(op, *memref); + return success(); + } +}; + +class LoadConverter : public OpConversionPattern { +private: + void propagateWasBoolToInt8Attr(Operation *srcLoadOp, Operation *dstOp, + PatternRewriter &rewriter) const; + + LogicalResult toTensorAndReplace(triton::LoadOp &op, + RankedTensorType &tensorType, Value localMem, + bool mayImplicitTransposeWithLastAxis, + const Location &loc, + ConversionPatternRewriter &rewriter) const; + + LogicalResult checkModifiedByAddPtrConverter(triton::LoadOp &op) const; + + LogicalResult + continueModifyFromAddPtrConverter(triton::LoadOp &op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const; + + void + fillTensorWithOtherForMaskScenario(Value other, Value localMem, + ArrayRef maskDim, + ConversionPatternRewriter &rewriter) const; + + LogicalResult + replaceMaskedLoadWithTensorOther(triton::LoadOp op, Value alloc, + bool mayImplicitTransposeWithLastAxis, + ConversionPatternRewriter &rewriter) const; + +public: + explicit LoadConverter(MLIRContext *context); + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::LoadOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +// tempate class's impl must in header file +template +class LoadStoreCanonicalizer : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(OpTy op, + PatternRewriter &rewriter) const override { + Value ptrVal = op.getPtr(); + + // A uniform scalar pointer can be broadcast and immediately extracted at + // a memory boundary. Fold that round-trip here so scalar pointer + // conversion does not require a tensor-of-pointer materialization. + if (auto extractOp = ptrVal.getDefiningOp()) { + if (auto splatOp = + extractOp.getTensor().getDefiningOp()) { + Value scalarPointer = splatOp.getSrc(); + auto pointerType = + dyn_cast(scalarPointer.getType()); + if (pointerType && !isa(pointerType.getPointeeType()) && + scalarPointer.getType() == ptrVal.getType()) { + rewriter.modifyOpInPlace( + op, [&]() { op->replaceUsesOfWith(ptrVal, scalarPointer); }); + return success(); + } + } + } + + auto ptrTy = dyn_cast(ptrVal.getType()); + if (!ptrTy || !isa(ptrTy.getElementType())) + return failure(); + + // ReorderBroadcast may turn + // addptr(splat(base), splat(offset)) + // into + // splat(addptr(base, offset)). + // Recover the former shape at the memory boundary so AddPtrConverter can + // keep using the real (non-synthetic) offset as its lowering anchor. + auto splatOp = ptrVal.getDefiningOp(); + if (!splatOp) + return failure(); + + auto scalarAddPtr = splatOp.getSrc().getDefiningOp(); + if (!scalarAddPtr) + return failure(); + + Value ptrSplat = rewriter.create(op.getLoc(), ptrTy, + scalarAddPtr.getPtr()); + auto offsetTy = + ptrTy.cloneWith(std::nullopt, scalarAddPtr.getOffset().getType()); + Value offsetSplat = rewriter.create( + op.getLoc(), offsetTy, scalarAddPtr.getOffset()); + Value addptr = rewriter.create(op.getLoc(), ptrTy, + ptrSplat, offsetSplat); + + rewriter.modifyOpInPlace(op, + [&]() { op->replaceUsesOfWith(ptrVal, addptr); }); + return success(); + } +}; + +class ScalarStoreCanonicalizer : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const override; +}; + +class StoreConverter : public OpConversionPattern { +public: + explicit StoreConverter(MLIRContext *context); + + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::StoreOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class ScalarAtomicRMWCanonicalizer + : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(triton::AtomicRMWOp op, + PatternRewriter &rewriter) const override; +}; + +class ScalarAtomicCASCanonicalizer + : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(triton::AtomicCASOp op, + PatternRewriter &rewriter) const override; +}; + +class AtomicCASConverter : public OpConversionPattern { +public: + explicit AtomicCASConverter(MLIRContext *context) + : OpConversionPattern(context) {} + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::AtomicCASOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class AtomicRMWConverter : public OpConversionPattern { +private: + Value createAtomicBinaryOps(OpBuilder &builder, Location loc, + triton::AtomicRMWOp op, Type elementType, + Value lhs, Value rhs) const { + auto rmwOp = op.getAtomicRmwOp(); + + // it has been confirmed in AtomicRMWConverter::matchAndRewrite + // that the ptr of op is of MemRefType + Value binaryOp; + if (rmwOp == triton::RMWOp::FADD) { + binaryOp = builder.create(loc, lhs, rhs); + } else if (rmwOp == triton::RMWOp::ADD) { + binaryOp = builder.create(loc, lhs, rhs); + } else if (rmwOp == triton::RMWOp::XOR) { + binaryOp = builder.create(loc, lhs, rhs); + } else if (rmwOp == triton::RMWOp::OR) { + binaryOp = builder.create(loc, lhs, rhs); + } else if (rmwOp == triton::RMWOp::AND) { + binaryOp = builder.create(loc, lhs, rhs); + } else if (rmwOp == triton::RMWOp::MAX) { + // Max/Min only support f32/i32 for now + // Other type is not supported because of semantic.py + if (isa(elementType)) { + binaryOp = builder.create(loc, lhs, rhs); + } else { + binaryOp = builder.create(loc, lhs, rhs); + } + } else if (rmwOp == triton::RMWOp::MIN) { + if (isa(elementType)) { + binaryOp = builder.create(loc, lhs, rhs); + } else { + binaryOp = builder.create(loc, lhs, rhs); + } + } else if (rmwOp == triton::RMWOp::XCHG) { + binaryOp = rhs; + } else if (rmwOp == triton::RMWOp::UMAX) { + binaryOp = builder.create(loc, lhs, rhs); + } else if (rmwOp == triton::RMWOp::UMIN) { + binaryOp = builder.create(loc, lhs, rhs); + } else { + op.emitOpError("unsupported atomic RMW operation: "); + llvm_unreachable( + "Not implemented. Support fadd, add, max, min for now !"); + } + return binaryOp; + } + + // used when handling scalar + // to verify whether we need to handle this scalar + bool isConstantMaskTrue(Value mask) const { + if (auto denseAttr = + mask.getDefiningOp()->getAttrOfType("value")) { + auto eleType = denseAttr.getType().getElementType(); + if (isa(eleType) && + cast(eleType).getWidth() == 1) { + auto values = denseAttr.getValues(); + return values[0]; + } + } + return false; + } + + DenseSet softwareAtomicKinds = { + triton::RMWOp::AND, triton::RMWOp::OR, triton::RMWOp::XOR}; + +public: + explicit AtomicRMWConverter(MLIRContext *context); + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::AtomicRMWOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class AtomicMaxMinCanonicalizer : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(triton::AtomicRMWOp op, + PatternRewriter &rewriter) const override; +}; + +class ReinterpretCastStrideCanonicalizer + : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(memref::ReinterpretCastOp op, + PatternRewriter &rewriter) const override; + static bool hasFixableZeroStride(memref::ReinterpretCastOp op); +}; + +} // namespace LoadStoreConverter +#endif diff --git a/third_party/ascend/include/TritonToLinalg/MarkTensorKindPass.h b/third_party/ascend/include/TritonToLinalg/MarkTensorKindPass.h new file mode 100644 index 0000000000..1c46719ea1 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/MarkTensorKindPass.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_CONVERSION_MARKTENSORKINDPASS_H +#define TRITON_ADAPTER_CONVERSION_MARKTENSORKINDPASS_H + +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#define GEN_PASS_DEF_MARKTENSORKIND +#include "ascend/include/TritonToLinalg/Passes.h.inc" + +namespace mlir { +namespace triton { + +std::unique_ptr> createMarkTensorKindPass(); + +enum TensorKind { NONE = -1, INPUT = 0, OUTPUT = 1, INPUT_OUTPUT = 2 }; + +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace triton; + +class MarkTensorKindPass + : public ::impl::MarkTensorKindBase { +public: + MarkTensorKindPass() = default; + + void runOnOperation() override; +}; + +#endif // TRITON_ADAPTER_CONVERSION_MARKTENSORKINDPASS_H diff --git a/third_party/ascend/include/TritonToLinalg/MaskAnalysis.h b/third_party/ascend/include/TritonToLinalg/MaskAnalysis.h new file mode 100644 index 0000000000..cf9ca27450 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/MaskAnalysis.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation, Meta Platforms. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ANALYSIS_MASKANALYSIS_H +#define TRITON_ANALYSIS_MASKANALYSIS_H + +// Compatibility shim: the public MaskState API is owned by the neutral +// TritonMemoryAccess layer so Graph can use it without depending on T2L. +#include "TritonMemoryAccess/LoadStoreMaskAnalysis.h" + +#endif // TRITON_ANALYSIS_MASKANALYSIS_H diff --git a/third_party/ascend/include/TritonToLinalg/Passes.h b/third_party/ascend/include/TritonToLinalg/Passes.h new file mode 100644 index 0000000000..6fcc73a0ac --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/Passes.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITON_TO_LINALG_CONVERSION_PASSES_H +#define TRITON_ADAPTER_TRITON_TO_LINALG_CONVERSION_PASSES_H + +#include "DeduplicateDebugNopsPass.h" +#include "MarkTensorKindPass.h" +#include "TritonToLinalgPass.h" + +namespace mlir::triton { + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToLinalg/Passes.h.inc" + +} // namespace mlir::triton + +#endif // TRITON_ADAPTER_TRITON_TO_LINALG_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/TritonToLinalg/Passes.td b/third_party/ascend/include/TritonToLinalg/Passes.td new file mode 100644 index 0000000000..5d480fd8f2 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/Passes.td @@ -0,0 +1,49 @@ +#ifndef TRITON_TO_LINALG_CONVERSION_PASSES +#define TRITON_TO_LINALG_CONVERSION_PASSES + +include "mlir/Pass/PassBase.td" + +def TritonToLinalg : Pass<"triton-to-linalg", "mlir::ModuleOp"> { + let summary = "Convert Triton to Linalg dialect"; + let constructor = "triton::createTritonToLinalgPass()"; + let options = [ + Option<"globalKernel", "global-kernel", + "bool", /*default*/"true", + "generate a global kernel">, + Option<"namedOps", "named-ops", + "bool", /*default*/"true", + "use linalg named ops instead of linalg.generic">, + Option<"enableNd2nzOnVector", "enable-nd2nz-on-vector", + "bool", /*default*/"false", + "enable nd2nz on vector">, + Option<"enableSelectAnalysis", "enable-select-analysis", + "bool", /*default*/"true", + "enable select analysis">, + Option<"compileOn91095", "compile-on-910-95", + "bool", /*default*/"false", + "compile on 910_95">, + Option<"compileMode", "compile-mode", + "std::string", /*default*/"\"simd_simt_template\"", + "Compilation mode: simd, simd_simt_template (or unstructured_in_simt), simt_only"> + ]; +} + +def MarkTensorKind : Pass<"mark-tensor-kind", "mlir::ModuleOp"> { + let summary = "Mark tensor kind (INPUT/OUTPUT/INPUT_OUTPUT) on Triton function arguments"; + let constructor = "triton::createMarkTensorKindPass()"; +} + +def DeduplicateDebugNops : Pass<"deduplicate-debug-nops", "mlir::ModuleOp"> { + let summary = "Deduplicate llvm.inline_asm 'nop' debug-anchor ops by source line."; + let description = [{ + Removes redundant `llvm.inline_asm "nop"` ops that share a + `(filename, line)` source location within a function. Keeps the + first occurrence in IR walk order; erases the rest. + + Opt-in via env var `LLVM_EXTRACT_DI_LOCAL_VARIABLES=1`. With the + env var unset the pass is a no-op. + }]; + let constructor = "triton::createDeduplicateDebugNopsPass()"; +} + +#endif // TRITON_TO_LINALG_CONVERSION_PASSES diff --git a/third_party/ascend/include/TritonToLinalg/StridedAxisCoalescing.h b/third_party/ascend/include/TritonToLinalg/StridedAxisCoalescing.h new file mode 100644 index 0000000000..07e905d82d --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/StridedAxisCoalescing.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_STRIDED_AXIS_COALESCING_H +#define TRITON_ASCEND_STRIDED_AXIS_COALESCING_H + +// Compatibility shim. The implementation is owned by TritonToGraph and is +// scheduled only through its original T2L compatibility slot. +#include "TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.h" + +#endif // TRITON_ASCEND_STRIDED_AXIS_COALESCING_H diff --git a/third_party/ascend/include/TritonToLinalg/StridedLoadStoreRewrite.h b/third_party/ascend/include/TritonToLinalg/StridedLoadStoreRewrite.h new file mode 100644 index 0000000000..3dbef86850 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/StridedLoadStoreRewrite.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_STRIDED_LOAD_STORE_REWRITE_H +#define TRITON_ASCEND_STRIDED_LOAD_STORE_REWRITE_H + +// Compatibility shim. The greedy pattern stage is owned by TritonToGraph and +// invoked only from the original T2L compatibility slot. +#include "TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.h" + +#endif // TRITON_ASCEND_STRIDED_LOAD_STORE_REWRITE_H diff --git a/third_party/ascend/include/TritonToLinalg/TileChunkCoalescing.h b/third_party/ascend/include/TritonToLinalg/TileChunkCoalescing.h new file mode 100644 index 0000000000..952052ecfa --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/TileChunkCoalescing.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_TILE_CHUNK_COALESCING_H +#define TRITON_ASCEND_TILE_CHUNK_COALESCING_H + +#include "TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.h" + +namespace TileChunkCoalescing { + +// Compatibility shim for clients of the pre-895 API. The implementation is +// the 895 ChunkCoalescing logic owned by TritonToGraph. +inline void rewriteTileChunkCoalesce(mlir::ModuleOp moduleOp) { + ChunkCoalescing::rewriteChunkCoalesce(moduleOp); +} + +} // namespace TileChunkCoalescing + +#endif // TRITON_ASCEND_TILE_CHUNK_COALESCING_H diff --git a/third_party/ascend/include/TritonToLinalg/TritonOpConverter.h b/third_party/ascend/include/TritonToLinalg/TritonOpConverter.h new file mode 100644 index 0000000000..9f0fbf35b2 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/TritonOpConverter.h @@ -0,0 +1,923 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITONOPCONVERTER_H +#define TRITON_ADAPTER_TRITONOPCONVERTER_H + +#include "TritonToLinalg/TritonToLinalgPass.h" +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "ascend/include/TritonToLinalg/BlockPtrAnalysis.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/Interfaces/FunctionInterfaces.h" +#include "mlir/Transforms/DialectConversion.h" + +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "triton-to-linalg" + +namespace TTOpConverters { +using namespace mlir; +using namespace triton; + +static constexpr unsigned kFuncNameCap = 128; + +/* +Convert `tt.precise_div` operation to `arith.divf` operation. +tensor_x / tensor_y + +```ttir + %11 = tt.precise_divf %7, %10 : tensor<100xf32> +``` + +converts to: + +```mlir + %11 = arith.divf %7, %10 : tensor<100xf32> +``` +*/ +struct PreciseDivConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::PreciseDivFOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +/* +Convert `tt.fp_to_fp` operation with RTNE (default) rounding mode to +`arith.truncf` or `arith.extf` operation. + +For default RTNE rounding: +- downcast: tt.fp_to_fp -> arith.truncf +- upcast: tt.fp_to_fp -> arith.extf +- same-bitwidth conversions between distinct floating-point formats: + tt.fp_to_fp -> arith.extf (to f32) -> arith.truncf + +Only an operation whose complete input and result MLIR types are identical can +be eliminated as an identity conversion. + +Note: Non-RTNE rounding modes (e.g., RTZ) are handled by TritonToHFusion pass. +*/ +struct FpToFpCanonicalizer : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(triton::FpToFpOp op, + PatternRewriter &rewriter) const override; +}; + +class SelectCanonicalizer : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(arith::SelectOp op, + PatternRewriter &rewriter) const override; +}; + +/* + * Move tt.bitcast to a previous location if tt.bitcast is not directly applied + * on function arguments + */ +class BitcastCanonicalizer : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(triton::BitcastOp bitcastOp, + PatternRewriter &rewriter) const override; +}; + +template +class ScalarMathCanonicalizer : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(MathOp op, + PatternRewriter &rewriter) const override { + if (op->getNumResults() != 1) { + return rewriter.notifyMatchFailure( + op, "ScalarMathCanonicalizer expects single scalar output."); + } + if (!op->getResult(0).getType().isIntOrIndexOrFloat()) { + return rewriter.notifyMatchFailure( + op, "ScalarMathCanonicalizer handles scalar load scene."); + } + if (auto linalgOp = op->template getParentOfType()) { + return rewriter.notifyMatchFailure( + op, "ScalarMathCanonicalizer handles op not within tt.reduce."); + } + if (auto linalgOp = op->template getParentOfType()) { + return rewriter.notifyMatchFailure( + op, "ScalarMathCanonicalizer handles op not within tt.scan."); + } + if (auto linalgOp = + op->template getParentOfType()) { + return rewriter.notifyMatchFailure( + op, + "ScalarMathCanonicalizer handles op not within tt.map_elementwise."); + } + auto loc = op.getLoc(); + llvm::SmallVector inputs; + for (auto input : op->getOperands()) { + auto blkTy = RankedTensorType::get({(int64_t)1}, input.getType()); + auto inputSplat = rewriter.create(loc, blkTy, input); + inputs.push_back(inputSplat.getResult()); + } + auto blkOp = rewriter.create(loc, inputs); + Value offset = + rewriter.create(loc, rewriter.getIndexAttr(0)); + auto extractOp = + rewriter.create(loc, blkOp.getResult(), offset); + rewriter.replaceOp(op, extractOp); + return success(); + } +}; + +/* + * Rewrite tt.make_tensor_ptr with non-contiguous order to + * tt.make_tensor_ptr + tt.load + tt.trans. + */ +class MakeTensorPtrCanonicalizer + : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(triton::MakeTensorPtrOp op, + PatternRewriter &rewriter) const override; +}; + +class ReduceSingleCanonicalizer : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(triton::ReduceOp reduceOp, + PatternRewriter &rewriter) const override; +}; + +class DenseConstantConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(arith::ConstantOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class MakeRangeConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::MakeRangeOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class SplatConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::SplatOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class UnsplatConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::UnsplatOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class ReshapeConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::ReshapeOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class ExpandDimsConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::ExpandDimsOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class ClampFConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ClampFOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class BroadcastConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::BroadcastOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +template +class ReductionOpBaseConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(OpTy op, typename OpTy::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const final { + auto sourceType = + cast(adaptor.getOperands().front().getType()); + assert(sourceType.hasRank() && "Expected input is ranked"); + + int64_t axis = op.getAxis(); + assert(axis >= 0 && axis < sourceType.getRank() && + "Expected reduction axis is within operand's rank"); + + auto realReductionOps = this->getRealReductionOps(op); + if (realReductionOps.size() == 1) { + return this->convertToTargetOp(op, adaptor, rewriter); + } + return this->convertToTargetOpExtended(op, adaptor, rewriter); + } + +protected: + llvm::SmallVector getReductionOps(OpTy reductionOp) const { + auto reductionBody = reductionOp.getBody(); + return llvm::map_to_vector(reductionBody->without_terminator(), + [](Operation &op) { return &op; }); + } + + llvm::SmallVector getRealReductionOps(OpTy reductionOp) const { + auto *body = reductionOp.getBody(); + auto *terminator = body->getTerminator(); + + // Compute the backward slice from yielded values: only ops that + // actually contribute to the computation of the reduction result + // are considered. + llvm::DenseSet liveOps; + llvm::SmallVector worklist(terminator->getOperands()); + while (!worklist.empty()) { + Value val = worklist.pop_back_val(); + if (auto *defOp = val.getDefiningOp()) { + if (defOp->getBlock() == body && liveOps.insert(defOp).second) { + for (auto operand : defOp->getOperands()) + worklist.push_back(operand); + } + } + } + + // Count only live ops, excluding type conversions that serve as + // precision promotion or demotion (e.g. bf16 -> f32 -> bf16). + llvm::SmallVector realOps; + for (Operation &bodyOp : body->without_terminator()) { + if (!liveOps.contains(&bodyOp)) + continue; + if (isa(&bodyOp)) + continue; + realOps.push_back(&bodyOp); + } + return realOps; + } + + arith::ConstantOp + getMultiOpReductionBaseConstOp(ConversionPatternRewriter &rewriter, OpTy op, + Location loc, Type constantType) const { + // for multiop reduce of 1 element result is defined as exactly this element + // for multiop reduce of tensor with N elements default value + // is not involved in result computation + auto reductionOps = this->getReductionOps(op); + assert(reductionOps.size() == 1); + auto reductionOp = reductionOps.front(); + + assert(constantType.isIntOrFloat()); + + if (constantType.isInteger()) { + return rewriter.create( + loc, constantType, rewriter.getIntegerAttr(constantType, 0)); + } + return rewriter.create( + loc, constantType, rewriter.getFloatAttr(constantType, 0.f)); + } + + arith::ConstantOp getReductionBaseConstOp(ConversionPatternRewriter &rewriter, + Operation *reductionOp, + Type constantType) const { + const int64_t bitWidth = constantType.getIntOrFloatBitWidth(); + + auto attr = + llvm::TypeSwitch(reductionOp) + .Case([&](arith::AddFOp) { + return rewriter.getFloatAttr(constantType, 0.f); + }) + .Case([&](arith::AddIOp) { + return rewriter.getIntegerAttr(constantType, 0); + }) + .Case([&](arith::MulFOp) { + return rewriter.getFloatAttr(constantType, 1.f); + }) + .Case([&](arith::MulIOp) { + return rewriter.getIntegerAttr(constantType, 1); + }) + .template Case([&](auto) { + return rewriter.getFloatAttr( + constantType, -std::numeric_limits::infinity()); + }) + .template Case([&](auto) { + return rewriter.getFloatAttr( + constantType, std::numeric_limits::infinity()); + }) + .Case([&](arith::MinSIOp) { + return rewriter.getIntegerAttr(constantType, + llvm::maxIntN(bitWidth)); + }) + .Case([&](arith::MinUIOp) { + return rewriter.getIntegerAttr(constantType, + llvm::maxUIntN(bitWidth)); + }) + .Case([&](arith::MaxSIOp) { + return rewriter.getIntegerAttr(constantType, + llvm::minIntN(bitWidth)); + }) + .Case([&](arith::MaxUIOp) { + return rewriter.getIntegerAttr(constantType, 0); + }) + .Case([&](arith::OrIOp) { + return rewriter.getIntegerAttr(constantType, 0); + }) + .Case([&](arith::AndIOp) { + return rewriter.getIntegerAttr(constantType, 1); + }) + .Case([&](arith::XOrIOp) { + return rewriter.getIntegerAttr(constantType, 0); + }) + .Default([](Operation *op) { + op->dump(); + llvm_unreachable("Reduction op not supported yet"); + return nullptr; + }); + + return rewriter.create(reductionOp->getLoc(), + constantType, attr); + } + + bool requiresF32Conversion(const Type elemType, + Operation *reductionOp) const { + return isa(elemType) && + elemType.getIntOrFloatBitWidth() < + Float32Type::get(elemType.getContext()) + .getIntOrFloatBitWidth() && + (isa(reductionOp) || isa(reductionOp)); + } + + Value getReductionElement(Value lhs, Value rhs, const Location loc, + Operation *reductionOp, OpBuilder &b, + const bool convertLhsToF32Precision) const { + return llvm::TypeSwitch(reductionOp) + .template Case([&](auto reductionOp) { + if (convertLhsToF32Precision) { + lhs = b.create(loc, Float32Type::get(b.getContext()), + lhs); + } + return b.create(loc, lhs, rhs); + }) + .template Case([&](auto reductionOp) { + return b.create(loc, lhs, rhs); + }) + .Default([](Operation *op) { + op->dump(); + llvm_unreachable("Reduction op not yet supported"); + return nullptr; + }); + } + + virtual bool isReductionOpSupported(Operation *reductionOp) const = 0; + + virtual LogicalResult + convertToTargetOp(OpTy op, typename OpTy::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const = 0; + + virtual LogicalResult + convertToTargetOpExtended(OpTy op, typename OpTy::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const = 0; +}; + +class ReduceConverter : public ReductionOpBaseConverter { +public: + explicit ReduceConverter(MLIRContext *context) + : ReductionOpBaseConverter(context) {} + + using ReductionOpBaseConverter::ReductionOpBaseConverter; + +protected: + bool isReductionOpSupported(Operation *reductionOp) const override; + + static bool isMultiReductionOpSupported(Operation *reductionOp); + + Value cloneReduceOps(OpBuilder &builder, Value in, Value out, Value opIns, + Value opOuts, triton::ReduceOp op) const; + + void + checkIsNotCallOp(const llvm::SmallVector &reductionOps) const; + + bool isSCFOpReduce(const llvm::SmallVector &reductionOps) const; + + bool + isMultiOpReduce(const llvm::SmallVector &reductionOps) const; + + Value computeReduceResultWithCompileFlag( + OpBuilder &opBuilder, Location loc, Value lhs, Value rhs, Value source, + Value initTensor, triton::ReduceOp reductionOp, + bool compileOn91095Flag = false) const; + + LogicalResult + convertToTargetOp(triton::ReduceOp op, + typename triton::ReduceOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + + LogicalResult + convertToTargetOpExtended(triton::ReduceOp op, + typename triton::ReduceOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class ScanConverter : public ReductionOpBaseConverter { +public: + explicit ScanConverter(MLIRContext *context) + : ReductionOpBaseConverter(context) {} + + using ReductionOpBaseConverter::ReductionOpBaseConverter; + +protected: + bool isReductionOpSupported(Operation *reductionOp) const override; + + LogicalResult + convertToTargetOp(triton::ScanOp op, typename triton::ScanOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + + LogicalResult + convertToTargetOpExtended(triton::ScanOp op, + typename triton::ScanOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +/* + * Decompose tt.map_elementwise region body into tensor-level Named Ops. + * Each scalar op (arith, math, scf.if, etc.) is promoted to its tensor + * counterpart, producing a chain of independent Named Ops. + */ +class MapElementwiseDecomposeConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::MapElementwiseOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + +private: + // Promote a single scalar op to tensor-level and record the result in + // valueMap. Returns the tensor result values (one per op result). + SmallVector promoteOp(Operation *op, + llvm::DenseMap &valueMap, + OpBuilder &builder, Location loc, + ArrayRef tensorShape) const; + + // Promote ops in a region body (excluding terminator), return the yielded + // tensor values from the terminator. + SmallVector promoteRegionBody(Region ®ion, + llvm::DenseMap &valueMap, + OpBuilder &builder, Location loc, + ArrayRef tensorShape) const; +}; + +class ExternElementwiseClOpConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ExternElementwiseOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class UnrealizedCastConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(UnrealizedConversionCastOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class JoinConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::JoinOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class SplitConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::SplitOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class CatConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::CatOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class GatherConverter : public OpConversionPattern { +private: + static constexpr llvm::StringRef gatherFuncNameBase = "triton_gather"; + +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::GatherOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +// These predicates select only scalar !tt.ptr transports. A +// tensor<...x!tt.ptr> follows the separate tensor-pointer lowering. +bool hasScalarPointerResult(scf::IfOp op); +bool isScalarPointerSelect(arith::SelectOp op); + +// Marks an scf.if temporarily rebuilt by IfConverter. Its scalar-pointer +// results are represented as complete i64 addresses, so only its own yields +// require the matching pointer-to-address conversion. +inline constexpr llvm::StringLiteral kScalarPointerCarrierBoundaryAttr = + "ScalarPointerCarrierBoundary"; + +// Rebuild an scf.if with scalar-pointer results so the boundary carries +// complete i64 addresses and reconstructs memrefs only after the join. +// The original branch regions are moved into the new operation, preserving +// side effects and allowing the conversion driver to rewrite each scf.yield +// operand in place. +// +// Example: +// %base = scf.if %cond -> !tt.ptr { +// scf.yield %lhs : !tt.ptr +// } else { +// scf.yield %rhs : !tt.ptr +// } +// %ptr = tt.make_tensor_ptr %base, ... +// becomes an scf.if returning i64 plus one hivm.pointer_cast after the if. +class IfConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(scf::IfOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +// Convert a scalar-pointer select into a select over complete integer addresses +// and reconstruct one memref after the selection. This handles both BlockPtr +// bases and ordinary scalar pointers without asking the backend to merge two +// memory objects. +// +// Example: +// %base = arith.select %cond, %lhs, %rhs : !tt.ptr +// %ptr = tt.make_tensor_ptr %base, ... +// becomes: +// %lhs_addr = memref.extract_aligned_pointer_as_index %lhs +// %rhs_addr = memref.extract_aligned_pointer_as_index %rhs +// %selected_addr = arith.select %cond, %lhs_addr, %rhs_addr : i64 +// %base = hivm.pointer_cast %selected_addr : i64 to memref +class PointerSelectConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(arith::SelectOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +// Convert the yields of an IfConverter-created scf.if to its carrier result +// types. In particular, a yielded scalar pointer becomes its complete i64 +// address. Yields belonging to ordinary ifs or loops are intentionally left to +// their owning conversions. +class YieldConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(scf::YieldOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +template || + std::is_same_v>> +class LoopConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(LoopOpTy op, + typename OpConversionPattern::OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override { + // CFO-expanded descriptor loops already carry pointer-free policy values + // and remain structurally unchanged. This legacy BlockData rewrite is only + // valid for explicitly marked legacy loops. + if (!op->hasAttr("UnhandledLoopOp")) + return failure(); + llvm::SmallDenseMap known; + + rewriter.modifyOpInPlace(op, [&]() { op->removeAttr("UnhandledLoopOp"); }); + return BlockDataParser::rewriteLoopOp(op, rewriter, known, {}); + } +}; + +class AdvanceConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::AdvanceOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class MakeTensorPtrConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + explicit MakeTensorPtrConverter(MLIRContext *context) + : OpConversionPattern(context) {} + + LogicalResult + matchAndRewrite(triton::MakeTensorPtrOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class TransposeConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::TransOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class BitcastConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::BitcastOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class TritonMulhiuiConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::MulhiUIOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class TritonPreciseSqrtConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::PreciseSqrtOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class DeviceAssertConverter : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + +private: + static constexpr llvm::StringRef printFuncNameBase = "triton_assert"; + static constexpr llvm::StringRef msgAttrName = "msg"; + +public: + LogicalResult + matchAndRewrite(triton::AssertOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class DevicePrintConverter : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + +private: + static constexpr llvm::StringRef printFuncNameBase = "triton_print"; + static constexpr llvm::StringRef prefixAttrName = "prefix"; + static constexpr llvm::StringRef hexAttrName = "hex"; + +public: + LogicalResult + matchAndRewrite(triton::PrintOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +struct MatmulConverter : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::DotOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +struct DotConverter : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::ascend::DotOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +struct FlipOpConverter : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::ascend::FlipOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + + static constexpr StringRef baseFuncName = "triton_flip"; +}; + +struct SortOpConverter : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::ascend::SortOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +struct DotScaledConverter : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::DotScaledOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class PtrToIntConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::PtrToIntOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class IntToPtrConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::IntToPtrOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class IndexPutConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ascend::IndexPutOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + +private: + static constexpr llvm::StringRef funcNameBase = "triton_index_put"; +}; + +class GatherOutToUbConverter + : public OpConversionPattern { +public: + using OpConversionPattern< + triton::ascend::GatherOutToUbOp>::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ascend::GatherOutToUbOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + +private: + static constexpr llvm::StringRef funcNameBase = "triton__gather_out_to_ub"; +}; + +class ScatterUbToOutConverter + : public OpConversionPattern { +public: + using OpConversionPattern< + triton::ascend::ScatterUbToOutOp>::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ascend::ScatterUbToOutOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + +private: + static constexpr llvm::StringRef funcNameBase = "triton_scatter_ub_to_out"; +}; + +class IndirectLoadConverter + : public OpConversionPattern { +public: + using OpConversionPattern< + triton::ascend::IndirectLoadOp>::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ascend::IndirectLoadOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + +private: + static constexpr llvm::StringRef funcNameBase = "triton_indirect_load"; +}; + +class StrideLoadConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ascend::StrideLoadOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + +private: + static constexpr llvm::StringRef funcNameBase = "triton_stride_load"; +}; + +class StrideStoreConverter + : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ascend::StrideStoreOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + +private: + static constexpr llvm::StringRef funcNameBase = "triton_stride_store"; +}; + +class IndirectStoreConverter + : public OpConversionPattern { +public: + using OpConversionPattern< + triton::ascend::IndirectStoreOp>::OpConversionPattern; + LogicalResult + matchAndRewrite(triton::ascend::IndirectStoreOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; + +private: + static constexpr llvm::StringRef funcNameBase = "triton_indirect_store"; +}; + +class IndexSelectSimdConverter + : public OpConversionPattern { +public: + explicit IndexSelectSimdConverter(MLIRContext *context); + using OpConversionPattern< + triton::ascend::IndexSelectSimdOp>::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::ascend::IndexSelectSimdOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +class HistogramConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(triton::HistogramOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; + +} // end of namespace TTOpConverters + +#endif diff --git a/third_party/ascend/include/TritonToLinalg/TritonToLinalgPass.h b/third_party/ascend/include/TritonToLinalg/TritonToLinalgPass.h new file mode 100644 index 0000000000..c5440e2da2 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/TritonToLinalgPass.h @@ -0,0 +1,121 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_CONVERSION_TRITONTOLINALG_H +#define TRITON_ADAPTER_CONVERSION_TRITONTOLINALG_H + +#include "ascend/include/Utils/Utils.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include + +#define GEN_PASS_CLASSES +#include "ascend/include/TritonToLinalg/Passes.h.inc" + +extern int nd2nzFlag; +extern bool compileOn91095Flag; +extern bool existDotFlag; +extern mlir::triton::ascend::CompileMode compileModeFlag; + +namespace mlir { +namespace triton { + +std::unique_ptr> createTritonToLinalgPass(); + +std::unique_ptr> +createTritonToLinalgPass(bool, bool, bool, bool, bool, + const std::string &compileMode = "simd_simt_template"); + +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace triton; +const std::string globalKernelAttr = "global_kernel"; +const std::string kernelMixModeName = "mix_mode"; +const std::string kernelParallelModeName = "parallel_mode"; + +class TritonTypeConverter : public mlir::TypeConverter { +public: + explicit TritonTypeConverter(); +}; + +class TritonToLinalgPass : public TritonToLinalgBase { + + static auto constexpr LAUNCH_GRID_RANK = getMaxEnumValForProgramIDDim() + 1; + static unsigned int constexpr TRITON_PROGRAM_INFO_ARG_COUNT = + LAUNCH_GRID_RANK * 2; + +private: + // grid构造 num_programs 3维, program_id 3维 + // remember 'xxxOp' is usually a Pointer, so that we can change target memory + // without giving a reference argument + void addProgramInfo(triton::FuncOp func, bool globalKernel); + + void convertTTFunc(triton::FuncOp func, const bool existDot, + const bool existSIMTOp); + + LogicalResult convertMultipleBlockControlFlow(Operation *funcOp, + OpBuilder &builder); + // 处理嵌套的if/else + scf::IfOp transformNestedIfElse(Operation &nestedBranch, OpBuilder &builder); + + void addDynamicLegal(ConversionTarget &target, + TritonTypeConverter &tritonTypeConverter); + + void + populateTritonToLinalgCanonicalizationPatterns(RewritePatternSet &patterns); + + void populateTritonToLinalgConversionPatterns(TypeConverter &typeConverter, + RewritePatternSet &patterns, + unsigned int launchGridRank); + + LogicalResult processDescriptorOperations(ModuleOp moduleOp); + LogicalResult processPtrBroadcastOperations(ModuleOp moduleOp); + LogicalResult processImplicitPermuteOperations(ModuleOp moduleOp); + LogicalResult processStridedLoadStoreRewriteOperations(ModuleOp moduleOp); + LogicalResult processLegalStrideOperations(ModuleOp moduleOp); + +public: + TritonToLinalgPass() = default; + + TritonToLinalgPass(bool globalKernel, bool namedOps, bool enableNd2nzOnVector, + bool enableSelectAnalysis, bool compileOn91095, + const std::string &compileMode = "simd_simt_template") { + this->globalKernel = globalKernel; + this->namedOps = namedOps; + this->enableNd2nzOnVector = enableNd2nzOnVector; + this->enableSelectAnalysis = enableSelectAnalysis; + this->compileOn91095 = compileOn91095; + this->compileMode = compileMode; + }; + + void getDependentDialects(DialectRegistry ®istry) const override; + + void runOnOperation() override; +}; + +#endif // TRITON_ADAPTER_CONVERSION_TRITONTOLINALG_H diff --git a/third_party/ascend/include/TritonToLinalg/UseAnalysis.h b/third_party/ascend/include/TritonToLinalg/UseAnalysis.h new file mode 100644 index 0000000000..c834f539a9 --- /dev/null +++ b/third_party/ascend/include/TritonToLinalg/UseAnalysis.h @@ -0,0 +1,144 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ANALYSIS_USEANALYSIS_H +#define TRITON_ANALYSIS_USEANALYSIS_H + +#include "mlir/Analysis/DataFlow/SparseAnalysis.h" + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace mlir { +namespace triton { + +enum class UseType { + Undefined, // Initial state + DataUse, // value used for tensor computation only + MetaUse, // value used for metadata only + MixUse // value used for both tensor computation and metadata +}; + +struct UseInfo : public dataflow::AbstractSparseLattice { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(UseInfo) + using AbstractSparseLattice::AbstractSparseLattice; + + // Lattice state transfer function + ChangeResult meetUseType(const UseType &other) { + if (other == UseType::Undefined) { + return ChangeResult::NoChange; + } + + switch (type) { + case UseType::Undefined: + type = other; + return ChangeResult::Change; + case UseType::DataUse: + case UseType::MetaUse: + if (type == other) { + return ChangeResult::NoChange; + } else { + type = UseType::MixUse; + return ChangeResult::Change; + } + case UseType::MixUse: + return ChangeResult::NoChange; + default: + llvm_unreachable("bad type"); + } + } + + ChangeResult meet(const AbstractSparseLattice &other) override { + auto rhs = reinterpret_cast(&other); + return meetUseType(rhs->type); + } + + void print(raw_ostream &os) const override { + switch (type) { + case UseType::DataUse: + os << "DataUse"; + break; + case UseType::MetaUse: + os << "MetaUse"; + break; + case UseType::MixUse: + os << "MixUse"; + break; + default: + os << "Undefined"; + } + } + + UseType type = UseType::Undefined; +}; + +class UseAnalysis : public dataflow::SparseBackwardDataFlowAnalysis { +public: + using SparseBackwardDataFlowAnalysis::SparseBackwardDataFlowAnalysis; + +#if LLVM_VERSION_MAJOR >= 20 + LogicalResult visitOperation(Operation *op, ArrayRef operands, + ArrayRef results) override; +#else + void visitOperation(Operation *op, ArrayRef operands, + ArrayRef results) override; +#endif + + void visitBranchOperand(OpOperand &operand) override { return; } + + void visitCallOperand(OpOperand &operand) override { return; } + + void setToExitState(UseInfo *lattice) override { + lattice->type = UseType::Undefined; + } + +private: + void propagateUse(UseInfo *lattice, const UseType &type) { + auto changed = lattice->meetUseType(type); + propagateIfChanged(lattice, changed); + } + + void propagateResults(UseInfo *lattice, ArrayRef results) { + auto changed = ChangeResult::NoChange; + for (auto result : results) { + changed |= lattice->meet(*result); + } + propagateIfChanged(lattice, changed); + } +}; + +class MetaUseEraser : public RewritePattern { +public: + MetaUseEraser(MLIRContext *context); + + LogicalResult matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const final; +}; + +LogicalResult runUseAnalysis(triton::FuncOp &funcOp); + +} // namespace triton + +} // namespace mlir + +#endif // TRITON_CONVERSION_TRITONTOAFFINE_TRITONUSEANALYSIS_H diff --git a/third_party/ascend/include/TritonToStructured/CMakeLists.txt b/third_party/ascend/include/TritonToStructured/CMakeLists.txt new file mode 100644 index 0000000000..5762c1f697 --- /dev/null +++ b/third_party/ascend/include/TritonToStructured/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonToStructured) +add_public_tablegen_target(TritonToStructuredConversionPassIncGen) diff --git a/third_party/ascend/include/TritonToStructured/CannonicalizerConverter.h b/third_party/ascend/include/TritonToStructured/CannonicalizerConverter.h new file mode 100644 index 0000000000..8d429ab300 --- /dev/null +++ b/third_party/ascend/include/TritonToStructured/CannonicalizerConverter.h @@ -0,0 +1,383 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef TRITON_ADAPTER_CANNONICALIZERCONVERTER_H +#define TRITON_ADAPTER_CANNONICALIZERCONVERTER_H + +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/AffineMap.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace CannonicalizerConverter { + +using namespace mlir; +using namespace triton; + +class CmpConverter : public OpRewritePattern { +public: + explicit CmpConverter(MLIRContext *context) + : OpRewritePattern(context) {} + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(arith::CmpIOp cmpOp, + PatternRewriter &rewriter) const override; +}; + +class SplatCmpConverter : public OpRewritePattern { +public: + explicit SplatCmpConverter(MLIRContext *context) + : OpRewritePattern(context) {} + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(arith::CmpIOp op, + PatternRewriter &rewriter) const override; +}; + +class AddPtrSplatConverter : public OpRewritePattern { +public: + explicit AddPtrSplatConverter(MLIRContext *context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(triton::AddPtrOp op, + PatternRewriter &rewriter) const override; +}; + +class LoadBroadcastConverter : public OpRewritePattern { +public: + explicit LoadBroadcastConverter(MLIRContext *context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(triton::LoadOp op, + PatternRewriter &rewriter) const override; +}; + +class StoreBroadcastConverter : public OpRewritePattern { +public: + explicit StoreBroadcastConverter(MLIRContext *context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const override; +}; + +// Rewrites `tt.load %bptr` whose `%bptr = tt.make_tensor_ptr ...` has all +// statically zero strides. The zero strides are the IR-level signature of a +// broadcasted tensor: every element of the loaded block refers to the same +// memory location `base`. Lower the load into a single scalar `tt.load %base` +// followed by `tt.broadcast` back to the original block shape, so downstream +// passes see only standard ops. +class ZeroStrideMakeTensorPtrConverter + : public OpRewritePattern { +public: + explicit ZeroStrideMakeTensorPtrConverter(MLIRContext *context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(triton::MakeTensorPtrOp op, + PatternRewriter &rewriter) const override; +}; + +class IfYieldAddHoistConverter : public OpRewritePattern { +public: + explicit IfYieldAddHoistConverter(MLIRContext *context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(scf::IfOp ifOp, + PatternRewriter &rewriter) const override; + +private: + bool isSupportedTensorResultType(Type type) const; + + bool isDefinedOutsideIf(Value value, scf::IfOp ifOp) const; + + bool extractAddendFromAddExpr(Value maybeAddExpr, Value baseValue, + Value &addendOut) const; + + Value buildZeroTensorLikeType(Type laneType, Location loc, + PatternRewriter &rewriter) const; + + bool tryRewriteSingleLane(unsigned laneIdx, Value baseBranchYield, + Value addExprBranchYield, bool baseInThenBranch, + Type laneType, scf::IfOp ifOp, + PatternRewriter &rewriter, + SmallVectorImpl &updatedThenYieldOperands, + SmallVectorImpl &updatedElseYieldOperands, + SmallVectorImpl &hoistedBasePerLane, + SmallVectorImpl &laneRewrittenFlags) const; +}; + +class PromotePointerIterArgsPattern : public OpRewritePattern { +public: + explicit PromotePointerIterArgsPattern(MLIRContext *context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(scf::ForOp forOp, + PatternRewriter &rewriter) const override; + +private: + // Information about a pointer iteration argument to be promoted + struct PointerArgInfo { + unsigned oldIndex; // Original index in the iteration arguments + Value basePointer; // Base pointer value passed as init arg + Value offsetValue; // Offset value used in addptr operation + Value newIterArg; // New integer iteration argument + Value addPtrValue; // The addptr operation result that updates the pointer + // Offset value used in advancePtr operation (with explicit inlined + // capacity) + SmallVector offsetValues; + SmallVector newInitArgs; + SmallVector newIterArgTypes; + }; + + LogicalResult matchAndRewriteForAddPtr(scf::ForOp forOp, + PatternRewriter &rewriter) const; + + // Check if the loop meets basic transformation conditions + LogicalResult matchLoop(scf::ForOp forOp) const; + + // Collect all pointer iteration arguments that match the promotion pattern + SmallVector collectPointerIterArgs(scf::ForOp forOp) const; + + // Check if a value has pointer tensor type + bool isPointerIterArg(Value iterArg) const; + + // Analyze a pointer iteration argument to determine if it matches the + // promotion pattern + std::optional analyzePointerIterArg(Value iterArg, + Block &loopBody) const; + + // Check if an index corresponds to a pointer argument being promoted + bool isPointerArgIndex(ArrayRef pointerArgs, + unsigned idx) const; + + // Get pointer argument information for a specific index + const PointerArgInfo *getPointerArgInfo(ArrayRef pointerArgs, + unsigned idx) const; + + // Create a new for loop with updated iteration argument types + scf::ForOp createNewForLoop(scf::ForOp forOp, ArrayRef newInitArgs, + ArrayRef newIterArgTypes, + PatternRewriter &rewriter) const; + + // Rewrite the loop body to use integer iteration arguments instead of + // pointers + LogicalResult rewriteLoopBody(scf::ForOp oldForOp, scf::ForOp newForOp, + SmallVector &pointerArgs, + DenseMap &indexMap, + PatternRewriter &rewriter) const; + + // Create new iteration arguments by replacing pointers with integer offsets + std::tuple, SmallVector, + DenseMap> + createNewIterArgs(scf::ForOp forOp, ArrayRef pointerArgs, + PatternRewriter &rewriter) const; + + // Create IR mapping for cloning operations, rebuilding pointers from integer + // offsets + IRMapping createIRMapping(scf::ForOp oldForOp, scf::ForOp newForOp, + SmallVector &pointerArgs, + DenseMap &indexMap, + PatternRewriter &rewriter) const; + + // Reconstruct a pointer value from base pointer and integer offset + Value rebuildPointer(scf::ForOp forOp, ArrayRef pointerArgs, + unsigned idx, PatternRewriter &rewriter) const; + + // Clone instructions from old loop body to new loop body, skipping + // transformed addptr ops + LogicalResult cloneInstructions(Block &oldBody, Block &newBody, + ArrayRef pointerArgs, + DenseMap &indexMap, + IRMapping &mapping, + PatternRewriter &rewriter) const; + + // Clone and transform the yield operation, converting pointer updates to + // integer additions + LogicalResult cloneYieldOp(scf::YieldOp yieldOp, + ArrayRef pointerArgs, + DenseMap &indexMap, + IRMapping &mapping, + PatternRewriter &rewriter) const; + + // Create integer addition for pointer offset updates in the yield operation + Value createIntegerAdd(unsigned idx, ArrayRef pointerArgs, + DenseMap &indexMap, + PatternRewriter &rewriter) const; + + // Extract constant integer value from offset (handles both scalar and tensor + // constants) + std::optional extractConstantOffset(Value offsetValue) const; + + // Replace the original loop results with reconstructed pointers from integer + // results + LogicalResult replaceResults(scf::ForOp oldForOp, scf::ForOp newForOp, + ArrayRef pointerArgs, + DenseMap &indexMap, + PatternRewriter &rewriter) const; + + // Reconstruct final pointer from integer result after the loop + Value reconstructPointer(scf::ForOp forOp, unsigned idx, Value intResult, + ArrayRef pointerArgs, + PatternRewriter &rewriter) const; + + LogicalResult matchAndRewriteAdvancePtr(scf::ForOp forOp, + PatternRewriter &rewriter) const; + + SmallVector + collectPointerIterArgsForAdvancePtr(scf::ForOp forOp) const; + + std::optional + analyzePointerIterArgForAdvancePtr(Value iterArg, Block &loopBody) const; + + std::tuple, SmallVector, + DenseMap> + createNewIterArgsForAdvancePtr(scf::ForOp forOp, + SmallVector &pointerArgs, + PatternRewriter &rewriter) const; + + IRMapping + createIRMappingForAdvancePtr(scf::ForOp oldForOp, scf::ForOp newForOp, + SmallVector &pointerArgs, + DenseMap &indexMap, + PatternRewriter &rewriter) const; + + Value rebuildPointerForAdvancePtr(scf::ForOp forOp, + ArrayRef pointerArgs, + unsigned idx, + PatternRewriter &rewriter) const; + + SmallVector + createOffsetsForAdvancePtr(unsigned idx, ArrayRef pointerArgs, + DenseMap &indexMap, + PatternRewriter &rewriter) const; + + LogicalResult cloneInstructionsForAdvancePtr( + Block &oldBody, Block &newBody, ArrayRef pointerArgs, + DenseMap &indexMap, IRMapping &mapping, + PatternRewriter &rewriter) const; + + LogicalResult + rewriteLoopBodyForAdvancePtr(scf::ForOp oldForOp, scf::ForOp newForOp, + SmallVector &pointerArgs, + DenseMap &indexMap, + PatternRewriter &rewriter) const; + + LogicalResult cloneYieldOpForAdvancePtr( + scf::YieldOp yieldOp, ArrayRef pointerArgs, + DenseMap &indexMap, IRMapping &mapping, + PatternRewriter &rewriter) const; + + SmallVector + reconstructPointerForAdvance(scf::ForOp forOp, unsigned idx, Value intResult, + ArrayRef pointerArgs, + PatternRewriter &rewriter) const; +}; + +class SimplifyTensorIterArgsPattern : public OpRewritePattern { +public: + explicit SimplifyTensorIterArgsPattern(MLIRContext *context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(scf::ForOp forOp, + PatternRewriter &rewriter) const override; + +private: + static constexpr llvm::StringLiteral kSimplifiedAttr = + "tts.simplify_tensor_iter_args.done"; + static constexpr llvm::StringLiteral kFailedAttr = + "tts.simplify_tensor_iter_args.failed"; + static constexpr llvm::StringLiteral kIncompleteAttr = + "tts.simplify_tensor_iter_args.incomplete"; + struct ShapeChainInfo { + Value base; + SmallVector chain; // from base -> iter shape + void dump() const; + }; + + struct RelayMapM1 { + unsigned innerIdx; + unsigned outerInitIdx; // from inner.initArg block-arg mapping + unsigned outerYieldIdx; // from outer.yield operand position of inner result + }; + + struct CandidateInfo { + unsigned idx; + ShapeChainInfo shapeInfo; + SmallVector arithOps; // in execution order + std::optional relayMap; + void dump() const; + }; + + bool isBlockArgumentFromAnotherForLoop(Value v) const; + std::optional getRelayMapM1(scf::ForOp innerFor, + scf::ForOp outerFor, + unsigned innerIdx) const; + void splitCandidatesByRelay(SmallVector all, + SmallVector &locals, + SmallVector &relays) const; + + Value cloneShapeChain(Location loc, Value base, ArrayRef chain, + PatternRewriter &rewriter) const; + Value normalizeInitArgForShapePeel(Value v) const; + std::optional peelShapeChain(Value v) const; + bool isArithWithConst(Operation *op, Value curVal, Value &nextVal, + Value &constVal) const; + Value getNewConstLikeOperand(Value cst, Type targetTy, + PatternRewriter &rewriter) const; + bool canBuildConstLikeOperand(Value cst, Type targetTy) const; + LogicalResult collectReverseLinearYieldPath( + Value yielded, Value iterArg, + SmallVectorImpl &opsInExecOrder) const; + + bool extractBinaryArithOperands(Operation *op, Value &lhs, Value &rhs) const; + Value createSameBinaryArithOp(Operation *oldOp, Location loc, Value lhs, + Value rhs, PatternRewriter &rewriter) const; + bool + isSafeToRewriteLanesByResultUses(scf::ForOp forOp, + ArrayRef candidates) const; + FailureOr rewriteForWithLocalCandidates( + scf::ForOp forOp, ArrayRef candidates, + const IRMapping *outerCaptureMap, PatternRewriter &rewriter) const; + LogicalResult precheckRelayCandidates(scf::ForOp innerFor, + ArrayRef relayCandidates, + scf::ForOp &outerForOut) const; + FailureOr rewriteInnerForWithRelayCandidates( + scf::ForOp innerFor, ArrayRef relayCandidates, + const IRMapping *outerCaptureMap, PatternRewriter &rewriter) const; + FailureOr rewriteOuterForWithRelayCandidates( + scf::ForOp innerFor, scf::ForOp oldInnerFor, scf::ForOp outerFor, + ArrayRef relayCandidates, PatternRewriter &rewriter) const; + LogicalResult + rewriteForWithRelayCandidates(scf::ForOp newfor, scf::ForOp oldFor, + ArrayRef relayCandidates, + PatternRewriter &rewriter) const; +}; +} // namespace CannonicalizerConverter + +#endif diff --git a/third_party/ascend/include/TritonToStructured/MaskAnalysis.h b/third_party/ascend/include/TritonToStructured/MaskAnalysis.h new file mode 100644 index 0000000000..420ebc0a12 --- /dev/null +++ b/third_party/ascend/include/TritonToStructured/MaskAnalysis.h @@ -0,0 +1,141 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef TRITON_TO_STRUCTURED_MASKANALYSIS_H +#define TRITON_TO_STRUCTURED_MASKANALYSIS_H + +#include +#include + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace TritonToStructured { +using namespace mlir; +using namespace triton; + +struct dimInfo { + OpFoldResult offset; + OpFoldResult shape; + OpFoldResult rhs; + size_t dimIndex; + bool hasBroadCast = false; + + enum class CompareType { slt, sge, ult, uge, deafaultType }; + + CompareType currentType = CompareType::deafaultType; + + dimInfo(size_t dimIndex = 0, bool hasBroadCast = false) + : dimIndex(dimIndex), hasBroadCast(hasBroadCast) {} + + dimInfo(OpFoldResult offset, OpFoldResult shape, size_t dimIndex = 0, + bool hasBroadCast = false, + CompareType Type = CompareType::deafaultType, + OpFoldResult rhs = nullptr) + : offset(offset), shape(shape), dimIndex(dimIndex), + hasBroadCast(hasBroadCast), currentType(Type), rhs(rhs) {} + + bool setType(arith::CmpIPredicate Type); + bool compareTypeIsLess() const; + void dump() const; +}; + +struct MaskState { + SmallVector stateInfo; + OpFoldResult scalar; + Value newMask; + + // Recursively parse a Value; call the corresponding function based on the + // defining operation and Value type + LogicalResult parse(Value operand, const Location loc, OpBuilder &builder); + + bool isEmpty() const { return stateInfo.empty() && !scalar; } + void dump() const; + + // Operand is the result of a constant + // Get the value of the constant and assign it to scalar. + LogicalResult parseConstant(arith::ConstantOp constOp, const Location loc, + OpBuilder &builder); + + LogicalResult parseIntScalar(Value scalar, const Location loc, + OpBuilder &builder); + + LogicalResult parseMakeRange(triton::MakeRangeOp rangeOp, const Location loc, + OpBuilder &builder); + + LogicalResult parseExtSI(arith::ExtSIOp op, const Location loc, + OpBuilder &builder); + + LogicalResult parseSplat(triton::SplatOp splatOp, const Location loc, + OpBuilder &builder); + + LogicalResult parseExpandDims(triton::ExpandDimsOp expandDimsOp, + const Location loc, OpBuilder &builder); + + LogicalResult parseAdd(arith::AddIOp addOp, const Location loc, + OpBuilder &builder); + + LogicalResult parseSub(arith::SubIOp subOp, const Location loc, + OpBuilder &builder); + + LogicalResult parseBroadcast(triton::BroadcastOp broadcastOp, + const Location loc, OpBuilder &builder); + + LogicalResult addStates(const MaskState &lhsState, const MaskState &rhsState, + Location loc, OpBuilder &builder); + + LogicalResult addStateScalar(const MaskState &state, + const OpFoldResult scalar, Location loc, + OpBuilder &builder); + + LogicalResult subStates(const MaskState &lhsState, const MaskState &rhsState, + Location loc, OpBuilder &builder); + + LogicalResult subStateScalar(const MaskState &state, + const OpFoldResult scalar, Location loc, + OpBuilder &builder); + + LogicalResult parseCmp(arith::CmpIOp cmpOp, const Location loc, + OpBuilder &builder); + + LogicalResult parseRem(arith::RemSIOp remOp, const Location loc, + OpBuilder &builder); + + LogicalResult parseDiv(arith::DivSIOp divOp, const Location loc, + OpBuilder &builder); + + LogicalResult parseAnd(arith::AndIOp andOp, const Location loc, + OpBuilder &builder); + + LogicalResult analysisMask(Value operand); + + Value createNewMask(const Location loc, OpBuilder &builder); +}; + +} // namespace TritonToStructured + +#endif diff --git a/third_party/ascend/include/TritonToStructured/MemOpConverter.h b/third_party/ascend/include/TritonToStructured/MemOpConverter.h new file mode 100644 index 0000000000..cf5ab5ad5e --- /dev/null +++ b/third_party/ascend/include/TritonToStructured/MemOpConverter.h @@ -0,0 +1,128 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef TRITON_ADAPTER_MEMOPCONVERTER_H +#define TRITON_ADAPTER_MEMOPCONVERTER_H + +#include "TritonToStructured/MaskAnalysis.h" +#include "TritonToStructured/PtrAnalysis.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/AffineMap.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace MemOpConverter { + +using namespace mlir; +using namespace triton; + +class LoadConverter : public OpRewritePattern { +public: + explicit LoadConverter(MLIRContext *context, + bool optimizeDynamicOffset = false, + bool enableMaskFallbackConversion = false) + : OpRewritePattern(context), + optimizeDynamicOffset(optimizeDynamicOffset), + enableMaskFallbackConversion(enableMaskFallbackConversion) {}; + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::LoadOp op, + PatternRewriter &rewriter) const override; + +private: + bool optimizeDynamicOffset; + bool enableMaskFallbackConversion; +}; + +class StoreConverter : public OpRewritePattern { +public: + explicit StoreConverter(MLIRContext *context, + bool optimizeDynamicOffset = false, + bool enableMaskFallbackConversion = false) + : OpRewritePattern(context), + optimizeDynamicOffset(optimizeDynamicOffset), + enableMaskFallbackConversion(enableMaskFallbackConversion) {}; + + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const override; + +private: + bool optimizeDynamicOffset; + bool enableMaskFallbackConversion; +}; + +class MemOpTransformer { +public: + TritonToStructured::PtrState ptrState; + TritonToStructured::MaskState maskState; + + enum class MemType { load, store, deafaultType }; + + bool optimizeDynamicOffset; + + MemType currentType = MemType::deafaultType; + + MemOpTransformer(MemType memType, bool optimizeDynamicOffset = false) + : currentType(memType), optimizeDynamicOffset(optimizeDynamicOffset) {} + + Value materializeImplicitBroadcast(Value srcTensor, const Location loc, + PatternRewriter &rewriter); + + Value materializeImplicitReshape(Value srcTensor, const Location loc, + PatternRewriter &rewriter); + + Value materializeImplicitSelect(Value srcTensor, Value mask, Value other, + const Location loc, + PatternRewriter &rewriter); + + Value materializeImplicitPermute(Value srcTensor, const Location loc, + PatternRewriter &rewriter); + + Value createNewPtr(Value oldPtr, const Location loc, + PatternRewriter &rewriter); + + Value createNewMask(Value oldPtr, const Location loc, + PatternRewriter &rewriter); + + Value createNewOther(Value oldOther, const Location loc, + PatternRewriter &rewriter); + + bool applyPermuteOnMask(); +}; + +// Create local lock var +hivm::CreateSyncBlockLockOp createSyncBlockLockVar(OpBuilder &builder, + Location loc); + +} // namespace MemOpConverter + +#endif diff --git a/third_party/ascend/include/TritonToStructured/Passes.h b/third_party/ascend/include/TritonToStructured/Passes.h new file mode 100644 index 0000000000..b5315b46a8 --- /dev/null +++ b/third_party/ascend/include/TritonToStructured/Passes.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITON_TO_STRUCTURE_CONVERSION_PASSES_H +#define TRITON_ADAPTER_TRITON_TO_STRUCTURE_CONVERSION_PASSES_H + +#include "TritonToStructuredPass.h" + +namespace mlir { +namespace triton { + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToStructured/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_TRITON_TO_UNSTRUCTURE_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/TritonToStructured/Passes.td b/third_party/ascend/include/TritonToStructured/Passes.td new file mode 100644 index 0000000000..01581ac2d3 --- /dev/null +++ b/third_party/ascend/include/TritonToStructured/Passes.td @@ -0,0 +1,19 @@ +#ifndef TRITON_TO_STRUCTURED_PASSES +#define TRITON_TO_STRUCTURED_PASSES + +include "mlir/Pass/PassBase.td" + +def TritonToStructured : Pass<"triton-to-structured", "mlir::ModuleOp"> { + let summary = "remove reminder/divider and reproduce addptr/mask expression "; + let constructor = "triton::createTritonToStructuredPass()"; + let options = [ + Option<"enableMaskFallbackConversion", "enable-mask-fallback-conversion", + "bool", /*default*/"false", + "If enabled, select will perform a fallback conversion when mask matching fails.">, + Option<"optimizeDynamicOffset", "optimize-dynamic-offset", + "bool", /*default*/"false", + "Enable dynamic offset feature"> + ]; +} + +#endif // TRITON_TO_STRUCTURED_PASSES diff --git a/third_party/ascend/include/TritonToStructured/PtrAnalysis.h b/third_party/ascend/include/TritonToStructured/PtrAnalysis.h new file mode 100644 index 0000000000..ec574400cb --- /dev/null +++ b/third_party/ascend/include/TritonToStructured/PtrAnalysis.h @@ -0,0 +1,199 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef TRITON_TO_STRUCTURED_PTRANALYSIS_H +#define TRITON_TO_STRUCTURED_PTRANALYSIS_H + +#include +#include + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace TritonToStructured { +using namespace mlir; +using namespace triton; + +struct StateInfo { + OpFoldResult stride; + OpFoldResult shape; // rem value + size_t dimIndex; + + StateInfo() : dimIndex(0) {} + StateInfo(OpFoldResult stride, OpFoldResult shape, size_t dimIndex = 0) + : stride(stride), shape(shape), dimIndex(dimIndex) {} + void dump() const; +}; + +struct PtrState { + SmallVector + stateInfo; // shape info when load, maintained with visitOps + SmallVector sizes; // original shape, maintained with visitOps + SmallVector permuteIds; + SmallVector + order; // the order of the original data format, only used for block_ptr + SmallVector + dimOffsets; // the offsets per dimension, only used for block_ptr + + Value source; // base address (ptr), maintained with visitOps + OpFoldResult offset; // scalar offset (int), maintained with visitOps + + // whether the record needs to be processed in the current pass, when ignore + // is true, it indicates that this scenario should not be processed within the + // current pass + bool shouldLinearize = false; + bool isPermuted = false; + + void dump() const; + bool isEmpty() const; + bool isScalar() const; + bool hasSource() const; + bool isBlockPtr() const; + bool isSameSizeAs(const PtrState &x) const; + + void generateOriginPermuteIds(); + + // Formula of "contiguous axes" + // - axis i is contiguous if stride[i] == product(shape[0..i-1]). + size_t countContiguousAxes(SmallVector stateInfo) const; + void analyzePermute(); + + void updatePtrState(SmallVector stateInfo, + SmallVector sizes, Value source, + OpFoldResult offset, const Location loc, + OpBuilder &builder, bool shouldLinearize = false); + + void normalizeState(const Location loc, OpBuilder &builder); + + LogicalResult mulState(const PtrState &lhsState, const PtrState &rhsState, + Operation *op, OpBuilder &builder); + + LogicalResult subState(const PtrState &lhsState, const PtrState &rhsState, + Operation *op, OpBuilder &builder); + + LogicalResult addState(PtrState &lhsState, PtrState &rhsState, Operation *op, + OpBuilder &builder); + + triton::AddPtrOp createAddPtrOp(OpBuilder &builder, Location loc); + + triton::MakeTensorPtrOp createMakeTensorPtrOp(OpBuilder &builder, + Location loc); +}; + +class PtrAnalysis { +public: + // AddptrOp result -> PtrState + llvm::SmallDenseMap knownPtrs; + IRMapping ptrMap; + + bool operandIsScalar(Value operand); + + bool optimizeDynamicOffset; + + PtrAnalysis(bool optimizeDynamicOffset = false) + : optimizeDynamicOffset(optimizeDynamicOffset) {} + + LogicalResult initStateByScalar(Value operand, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult initStateByPointer(Value operand, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult visitOperandMul(arith::MulIOp mulOp, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult visitOperandSub(arith::SubIOp subOp, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult visitOperandMakeRange(triton::MakeRangeOp rangeOp, + PtrState &state, Location loc, + OpBuilder &builder); + + LogicalResult visitOperandBroadcast(triton::BroadcastOp broadcastOp, + PtrState &state, const Location loc, + OpBuilder &builder); + + LogicalResult visitOperandSplat(triton::SplatOp splatOp, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult visitOperandExpandDims(triton::ExpandDimsOp expandDimsOp, + PtrState &state, const Location loc, + OpBuilder &builder); + + LogicalResult visitOperandConstSplat(arith::ConstantOp op, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult visitOperandExtSI(arith::ExtSIOp extOp, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult visitOperandRem(arith::RemSIOp remOp, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult visitOperandDiv(arith::DivSIOp divOp, PtrState &state, + const Location loc, OpBuilder &builder); + + LogicalResult visitOperandAdd(arith::AddIOp addOp, PtrState &state, + const Location loc, OpBuilder &builder); + + // Recursively parse a Value; call the corresponding + // function based on the defining operation and argument type. + LogicalResult visitOperand(Value operand, PtrState &state, const Location loc, + OpBuilder &builder); + + // Operand is the result of addptr. + // Main assumptions: + // - The ptr field should populate the source field + // - ptr and offset fields should result in same rank + // Expected result: + // - The resulting state for ptr and offset wil be added + LogicalResult visitOperandAddptr(triton::AddPtrOp addptrOp, PtrState &state, + const Location loc, OpBuilder &builder); + + // Operand is the result of tt.make_tensor_ptr. + // Expected result: + // Parse source pointer and grab results + LogicalResult + visitOperandMakeTensorPtr(triton::MakeTensorPtrOp makeTensorPtrOp, + PtrState &state, const Location loc, + OpBuilder &builder); + + // Parse the state of AddPtrOp, insert any instruction needed to + // calculate strides and offsets, build PtrState for this operand, and record + // PtrState for knownPtrs. + LogicalResult rewriteAddptrOp(triton::AddPtrOp op); +}; + +bool isMultiple(const OpFoldResult ÷nd, const OpFoldResult &divisor); +bool isEqual(const OpFoldResult &ofr1, const OpFoldResult &ofr2); +bool isLess(const OpFoldResult &ofs1, const OpFoldResult &ofs2); +bool isGreater(const OpFoldResult &ofs1, const OpFoldResult &ofs2); +std::optional +extractDivisibilityFromOpFoldResult(mlir::OpFoldResult ofr); + +} // namespace TritonToStructured + +#endif diff --git a/third_party/ascend/include/TritonToStructured/TritonToStructuredPass.h b/third_party/ascend/include/TritonToStructured/TritonToStructuredPass.h new file mode 100644 index 0000000000..ea8f6521c9 --- /dev/null +++ b/third_party/ascend/include/TritonToStructured/TritonToStructuredPass.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef TRITON_ADAPTER_CONVERSION_TRITONTOSTRUCTURED_H +#define TRITON_ADAPTER_CONVERSION_TRITONTOSTRUCTURED_H + +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#define GEN_PASS_CLASSES +#include "ascend/include/TritonToStructured/Passes.h.inc" + +namespace mlir { +namespace triton { + +std::unique_ptr> createTritonToStructuredPass(); + +std::unique_ptr> createTritonToStructuredPass(bool, + bool); + +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace triton; + +class TritonToStructuredPass + : public TritonToStructuredBase { +public: + TritonToStructuredPass() = default; + + TritonToStructuredPass(bool enableMaskFallbackConversion, + bool optimizeDynamicOffset) { + this->enableMaskFallbackConversion = enableMaskFallbackConversion; + this->optimizeDynamicOffset = optimizeDynamicOffset; + }; + void getDependentDialects(DialectRegistry ®istry) const override; + void runOnOperation() override; + +private: + void populateTritonToStructuredCanonicalizationPatterns( + RewritePatternSet &patterns); + + void populateTritonToStructuredPatterns(RewritePatternSet &patterns, + bool optimizeDynamicOffset, + bool enableMaskFallbackConversion); + + LogicalResult processSplatBinaryOperations(ModuleOp moduleOp); +}; + +#endif // TRITON_ADAPTER_CONVERSION_TRITONTOSTRUCTURED_H diff --git a/third_party/ascend/include/TritonToUnstructure/BubbleUpOperation.h b/third_party/ascend/include/TritonToUnstructure/BubbleUpOperation.h new file mode 100644 index 0000000000..fe745a590a --- /dev/null +++ b/third_party/ascend/include/TritonToUnstructure/BubbleUpOperation.h @@ -0,0 +1,117 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "mlir/Pass/Pass.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/IR/PatternMatch.h" + +#define GEN_PASS_DECL_BUBBLEUPOPERATION +#include "ascend/include/TritonToUnstructure/Passes.h.inc" + +#define GEN_PASS_DEF_BUBBLEUPOPERATION +#include "ascend/include/TritonToUnstructure/Passes.h.inc" + +namespace mlir { +namespace triton { + +std::unique_ptr> +createBubbleUpOperationPass(const BubbleUpOperationOptions &options = {}); + +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace triton; + +template +class BubbleUpExtract : public OpRewritePattern { + static_assert(std::is_same_v || + std::is_same_v); + +public: + using OpRewritePattern::OpRewritePattern; + + explicit BubbleUpExtract(MLIRContext *context, bool enableAggressiveMode); + + LogicalResult matchAndRewrite(ExtractOpTy op, + PatternRewriter &rewriter) const override; + +private: + Value createExtractOp(ExtractOpTy op, Value value, Location loc, + PatternRewriter &rewriter) const; + template + void bubbleUpIntBinaryOp(ExtractOpTy op, BinOpTy binOp, Location loc, + PatternRewriter &rewriter) const; + template + void bubbleUpFloatBinaryOp(ExtractOpTy op, BinOpTy binOp, Location loc, + PatternRewriter &rewriter) const; + + void bubbleUpOperation(ExtractOpTy op, arith::ExtSIOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, arith::CmpIOp parentOp, Location loc, + PatternRewriter &rewriter) const; + // Pushes extract(select(condition, lhs, rhs)) through the select. A shaped + // condition is extracted at the same position while a scalar condition is + // reused directly. + void bubbleUpOperation(ExtractOpTy op, arith::SelectOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, arith::TruncFOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, arith::ExtFOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, arith::FPToSIOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, arith::SIToFPOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, triton::ClampFOp parentOp, + Location loc, PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, arith::CmpFOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, triton::BroadcastOp parentOp, + Location loc, PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, triton::ExpandDimsOp parentOp, + Location loc, PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, triton::SplatOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, triton::MakeRangeOp parentOp, + Location loc, PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, triton::AddPtrOp parentOp, + Location loc, PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, math::FloorOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, math::CeilOp parentOp, Location loc, + PatternRewriter &rewriter) const; + void bubbleUpOperation(ExtractOpTy op, tensor::ExtractSliceOp parentOp, + Location loc, PatternRewriter &rewriter) const; + + bool enableAggressiveMode; +}; + +class BubbleUpOperationPass + : public ::impl::BubbleUpOperationBase { +public: + explicit BubbleUpOperationPass(const BubbleUpOperationOptions &options); + void runOnOperation() override; +}; diff --git a/third_party/ascend/include/TritonToUnstructure/CMakeLists.txt b/third_party/ascend/include/TritonToUnstructure/CMakeLists.txt new file mode 100644 index 0000000000..2ac8e6a64f --- /dev/null +++ b/third_party/ascend/include/TritonToUnstructure/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls --name TritonToUnstructure) +add_public_tablegen_target(TritonToUnstructureConversionPassIncGen) diff --git a/third_party/ascend/include/TritonToUnstructure/IndirectAtomicUtils.h b/third_party/ascend/include/TritonToUnstructure/IndirectAtomicUtils.h new file mode 100644 index 0000000000..e3f4bb5eb0 --- /dev/null +++ b/third_party/ascend/include/TritonToUnstructure/IndirectAtomicUtils.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_INDIRECTATOMICUTILS_H +#define TRITON_ADAPTER_INDIRECTATOMICUTILS_H + +#include "mlir/IR/PatternMatch.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace IndirectAtomicUtils { +using namespace mlir; +using namespace triton; + +// The fast path requires a statically shaped result tensor whose element type +// is not int8. When offsetValue is provided it must be a statically shaped +// tensor or a scalar int/index. Additionally, CAS and XCHG do not use the +// fast path for f16/bf16 results. +bool canUseIndirectAtomicFastPath(triton::AtomicRMWOp op, + Value offsetValue = Value()); + +bool canUseIndirectAtomicFastPath(triton::AtomicCASOp op, + Value offsetValue = Value()); + +// Lowers tt.atomic_rmw to the indirect atomic custom op and restores the +// flattened custom result back to the original result tensor type. +FailureOr tryConvertAtomicRmwToIndirectCustom(triton::AtomicRMWOp op, + Value srcPtr, + Value offsetValue, + PatternRewriter &rewriter); + +// Lowers tt.atomic_cas to the indirect atomic custom op and restores the +// flattened custom result back to the original result tensor type. +FailureOr tryConvertAtomicCasToIndirectCustom(triton::AtomicCASOp op, + Value srcPtr, + Value offsetValue, + PatternRewriter &rewriter); + +} // namespace IndirectAtomicUtils + +#endif // TRITON_ADAPTER_INDIRECTATOMICUTILS_H diff --git a/third_party/ascend/include/TritonToUnstructure/OffsetAnalysis.h b/third_party/ascend/include/TritonToUnstructure/OffsetAnalysis.h new file mode 100644 index 0000000000..e985385569 --- /dev/null +++ b/third_party/ascend/include/TritonToUnstructure/OffsetAnalysis.h @@ -0,0 +1,301 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ANALYSIS_OFFSETANALYSIS_H +#define TRITON_ANALYSIS_OFFSETANALYSIS_H +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/IR/Arith.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Value.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace mlir { +namespace triton { + +/// Temporary T2U handoff marker for loop slots rewritten from a scalar +/// pointer to a relative integer offset. OffsetAnalysis uses this marker to +/// keep the region argument's dynamic backedge value instead of replaying the +/// original pointer-init provenance on every iteration. +inline constexpr llvm::StringLiteral kScalarPointerOffsetBoundaryAttr = + "ScalarPointerOffsetBoundary"; + +struct PtrOffsetInfo { + /** + Possible status of the ptr offset: + - ScalarLike: + - Tensor's elements are all the same such as [[2.0,2.0,2.0],[2.0,2.0,2.0]] + - Constant integer or floating-point such as 2, 2.0, and `load + tensor<1xptr>` + - Unstructured: + - Not a `ScalarLike` ptr offset + - Or satisfy any below conditions: + - Incontinuous stride such as + - `muli [0,1,2,3] [0,1,2,3]` => [0,1,4,9] + - `divsi [9,8,7] [3,2,1]` => [3,4,7] + - `minsi [3,4,5] [5,4,3]` => [3,4,3] + - From non-`scalarLike` floating point element type such as + - `fptosi [1.0,2.0,3.0]` => [1,2,3] + - Compilation time unknown value + - `load %ptr, %offset` => %value + - Structured: + - orthongonal to `Unstructured` + - if PtrOffsetInfo isn't `Unstructured`, it is `Structured` + + In short: + ScalarLike ⊆ Structured + Unstructured = {x| x ∉ Structured} + + Example: + ``` + %y = sitofp %x + %z = fptosi %y + ``` + If %x is scalarLike (structured), %z will be scalar (structured) as well. + If %x is non-scalarLike structured, %z will be unstructured. + */ + +public: + enum class AxisInfo { unstructured, structured, scalarlike, scalar }; + + explicit PtrOffsetInfo(); + PtrOffsetInfo(const PtrOffsetInfo &other); + + explicit PtrOffsetInfo(const Value &ptr); + explicit PtrOffsetInfo(ArrayRef structured); + explicit PtrOffsetInfo(const Value &ptr, AxisInfo structured); + explicit PtrOffsetInfo(const Value &ptr, ArrayRef structured); + explicit PtrOffsetInfo(const Value &ptr, const Value &offset, + AxisInfo structured); + explicit PtrOffsetInfo(const Value &ptr, const Value &offset, + ArrayRef structured); + + PtrOffsetInfo &operator=(const PtrOffsetInfo &other); + + Value getPtr() const; + Value getOffset() const; + SmallVector getOffsets() const; + SmallVector &getOffsetsRef(); + bool isScalarLike() const; + bool isPointerDescriptorOwned() const; + SmallVector &getStructuredRef(); + const SmallVector &getStructured() const; + int getRank() const; + + void setPtr(const Value &ptr); + void setOffset(const Value &offset); + void setOffsets(ValueRange offsets); + void setStructured(); + void setStructured(int rank); + void setStructured(int rank, AxisInfo info); + // These setters update only per-axis structure. They intentionally preserve + // the independent scalarLike property; callers that invalidate scalar + // equivalence must clear it explicitly. + void setUnstructured(); + void setUnstructured(int rank); + void setStructured(ArrayRef structured); + void setStructured(const PtrOffsetInfo &other); + void setScalarLike(bool scalarLike); + void setPointerDescriptorOwned(bool pointerDescriptorOwned); + + bool isStructured(int dim) const; + bool isStructured() const; + bool isUnstructured() const; + bool isUnstructuredOrScalarlike() const; + + void setZeroOffset(); + +private: + Value ptr; + Value offset; + SmallVector tptOffsets; + + bool scalarLike = false; + + // True only when the pointer originates from a CFO descriptor rebuild and + // every pointer-preserving operation since that rebuild was modeled by this + // analysis. Consumers may then use the analyzed scalar base without relying + // on the load operand's direct producer. + bool pointerDescriptorOwned = false; + + SmallVector structured; +}; + +PtrOffsetInfo combineInfo(const PtrOffsetInfo &lhs, const PtrOffsetInfo &rhs); + +void parse(Value operand, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseLoopRegionIterArg(LoopLikeOpInterface loopOp, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap, + BlockArgument regionIterArg); + +void parseArithOp(Operation *arithOp, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseTritonOp(Operation *tritonOp, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseTritonOp(Operation *tritonOp, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseAddPtr(triton::AddPtrOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseSplat(triton::SplatOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +template +void parseBinaryOp(BinOpTy op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseAddI(arith::AddIOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseSubI(arith::SubIOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseIndexCast(arith::IndexCastOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +template +void parseConstantOp(ConstOpTy dst, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseMakeRange(triton::MakeRangeOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseExtSI(arith::ExtSIOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseBitcast(triton::BitcastOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseLoad(triton::LoadOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseMulI(arith::MulIOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseBroadcast(triton::BroadcastOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseExpandDims(triton::ExpandDimsOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseReshape(triton::ReshapeOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseClampF(triton::ClampFOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseSelect(arith::SelectOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseFPToSI(arith::FPToSIOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseSIToFP(arith::SIToFPOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +// FIXME:Z|wait triton version upgrade to 3.4 +// void parseMakeTensorDesc(triton::MakeTensorDescOp op, const Location &loc, +// RewriterBase &rewriter, +// llvm::DenseMap &offsetMap); + +void parseMakeTensorPtr(triton::MakeTensorPtrOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseAdvance(triton::AdvanceOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseReduce(triton::ReduceOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseReduceReturn(triton::ReduceReturnOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseIf(scf::IfOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap, Value dst); + +void parseYield(scf::YieldOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseLoopOp(LoopLikeOpInterface op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap, Value dst); + +void parseExtractSlice(tensor::ExtractSliceOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseInsertSlice(tensor::InsertSliceOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseExtract(tensor::ExtractOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseInsert(tensor::InsertOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseIntToPtr(triton::IntToPtrOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap); + +void parseStructuredCustomOp(Operation *op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap, + unsigned resultIdx); +} // namespace triton + +} // namespace mlir + +#endif diff --git a/third_party/ascend/include/TritonToUnstructure/Passes.h b/third_party/ascend/include/TritonToUnstructure/Passes.h new file mode 100644 index 0000000000..3231cf8feb --- /dev/null +++ b/third_party/ascend/include/TritonToUnstructure/Passes.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_TRITON_TO_UNSTRUCTURE_CONVERSION_PASSES_H +#define TRITON_ADAPTER_TRITON_TO_UNSTRUCTURE_CONVERSION_PASSES_H + +#include "BubbleUpOperation.h" +#include "UnstructureConversionPass.h" + +namespace mlir { +namespace triton { + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToUnstructure/Passes.h.inc" + +} // namespace triton +} // namespace mlir + +#endif // TRITON_ADAPTER_TRITON_TO_UNSTRUCTURE_CONVERSION_PASSES_H diff --git a/third_party/ascend/include/TritonToUnstructure/Passes.td b/third_party/ascend/include/TritonToUnstructure/Passes.td new file mode 100644 index 0000000000..d139c140fb --- /dev/null +++ b/third_party/ascend/include/TritonToUnstructure/Passes.td @@ -0,0 +1,30 @@ +#ifndef TRITON_TO_UNSTRUCTURE_CONVERSION_PASSES +#define TRITON_TO_UNSTRUCTURE_CONVERSION_PASSES + +include "mlir/Pass/PassBase.td" + +def TritonToUnstructure : Pass<"triton-to-unstructure", "mlir::ModuleOp"> { + let summary = "Convert Triton for unstructure case"; + let constructor = "triton::createTritonToUnstructurePass()"; + let options = [ + Option<"forceScalarizeMode", "force-scalarize-mode", "bool", "false", + "Scalarize unstructured memory access even if structured dimensions are mixed.">, + Option<"compileOn91095", "compile-on-910-95", + "bool", /*default*/"false", + "compile on 910_95">, + Option<"compileMode", "compile-mode", + "std::string", /*default*/"\"simd_simt_template\"", + "Compilation mode: simd, simd_simt_template (or unstructured_in_simt), simt_only"> + ]; +} + +def BubbleUpOperation : Pass<"bubble-up-operation", "mlir::ModuleOp"> { + let summary = "Apply bubble up operation optimization"; + let constructor = "triton::createBubbleUpOperationPass()"; + let options = [ + Option<"enableAggressiveMode", "enable-aggressive-mode", "bool", "true", + "Enable aggressive bubble up operation.">, + ]; +} + +#endif // TRITON_TO_UNSTRUCTURE_CONVERSION_PASSES diff --git a/third_party/ascend/include/TritonToUnstructure/UnstructureConversionPass.h b/third_party/ascend/include/TritonToUnstructure/UnstructureConversionPass.h new file mode 100644 index 0000000000..6cbb2b5eb0 --- /dev/null +++ b/third_party/ascend/include/TritonToUnstructure/UnstructureConversionPass.h @@ -0,0 +1,153 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_UNSTRUCTURECONVERSION_H +#define TRITON_ADAPTER_UNSTRUCTURECONVERSION_H + +#include "ascend/include/TritonToUnstructure/OffsetAnalysis.h" +#include "mlir/Pass/Pass.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "ascend/include/Utils/Utils.h" +#include "mlir/IR/PatternMatch.h" + +#define GEN_PASS_DECL_TRITONTOUNSTRUCTURE +#include "ascend/include/TritonToUnstructure/Passes.h.inc" + +#define GEN_PASS_DEF_TRITONTOUNSTRUCTURE +#include "ascend/include/TritonToUnstructure/Passes.h.inc" + +extern bool compileOn91095Flag; + +namespace mlir { +namespace triton { + +std::unique_ptr> +createTritonToUnstructurePass(const TritonToUnstructureOptions &options = {}); + +} // namespace triton +} // namespace mlir + +namespace { + +using namespace mlir; +using namespace triton; + +// For example, in unstructured load case +// %0 = tt.load %structured : tensor<128x128x!tt.ptr> +// %ptr_2 = tt.splat %arg1 : !tt.ptr -> tensor<128x128x!tt.ptr> +// %1 = tt.addptr %ptr_2, %0 : tensor<128x128x!tt.ptr>, +// tensor<128x128xi32> %2 = tt.load %1 : tensor<128x128x!tt.ptr> tt.store +// %output %2 : tensor<128x128x!tt.ptr> +// +// +// In this case, this will be converted to +// +// %0 = tt.load %structured : tensor<128x128x!tt.ptr> +// %1 = tensor.empty() : tensor<128x128xf32> +// %2 = scf.for %arg2 = %c0 to %c128 step %c1 iter_args(%arg3 = %1) -> +// (tensor<128x128xf32>) { +// %4 = scf.for %arg4 = %c0 to %c128 step %c1 iter_args(%arg5 = %arg3) -> +// (tensor<128x128xf32>) { +// %extracted = tensor.extract %10[%arg3, %arg5] {DiscreteMemAccess} : +// tensor<128x128xi32> %5 = arith.extsi %extracted : i32 to i64 %6 = +// tt.addptr %arg1, %5 : !tt.ptr, i64 %7 = tt.load %6 +// {DiscreteMemAccess} : tt.ptr %inserted_slice = tensor.insert_slice +// %7 into %arg5[%arg2, %arg4] [1, 1] [128, 1] {DiscreteMemAccess} : +// tensor<1x1xf32> into tensor<128x128xf32> scf.yield %inserted_slice : +// tensor<128x128xf32> +// } +// scf.yield %4 : tensor<128x128xf32> +// } +// tt.store %output %2 : tensor<128x128x!tt.ptr> +template +class UnstructuredMemAccessConverter : public OpRewritePattern { + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v); + +public: + using OpRewritePattern::OpRewritePattern; + + explicit UnstructuredMemAccessConverter( + MLIRContext *context, bool forceScalarizeMode, + const llvm::DenseMap &offsetMap, + const llvm::SmallDenseMap &fromTensorArg); + LogicalResult matchAndRewrite(MemAccOpTy op, + PatternRewriter &rewriter) const override; + +private: + bool checkUnstructureAnnotated(MemAccOpTy op, + PatternRewriter &rewriter) const; + Value createExtractOp(Location loc, Value value, PatternRewriter &rewriter, + ArrayRef iterIdx) const; + Value createExtractOp(Location loc, Value value, PatternRewriter &rewriter, + ArrayRef offsets, + ArrayRef sizes, + ArrayRef strides) const; + template + typename std::enable_if, + LogicalResult>::type + splatAndLoadScenario(MemAccOpTy op, const PtrOffsetInfo &ptrOffsetInfo, + PatternRewriter &rewriter) const; + + template + MemAccOpTy createMemAccOp(MemAccOpTy op, Value ptrToAccess, Location loc, + PatternRewriter &rewriter, Args &&...args) const; + + const llvm::DenseMap &offsetMap; + const llvm::SmallDenseMap &fromTensorArg; + bool forceScalarizeMode; +}; + +class TritonToUnstructurePass + : public ::impl::TritonToUnstructureBase { +public: + explicit TritonToUnstructurePass(const TritonToUnstructureOptions &options); + void getDependentDialects(DialectRegistry ®istry) const override; + + void runOnOperation() override; + +private: + void runPreparse(LoopLikeOpInterface op); + template || + std::is_same_v || + std::is_same_v || + std::is_same_v>> + void runParse(MemAccOpTy op); + llvm::DenseMap offsetMap; + llvm::DenseMap offsetMapForLoopArgs; + llvm::SmallDenseMap fromTensorArg; + + LogicalResult processIfYieldAddHoistOperations(ModuleOp moduleOp); +}; + +} // namespace + +void replacePtrArguments(triton::FuncOp funcOp, + llvm::DenseMap &offsetMap); + +#endif // TRITON_ADAPTER_UNSTRUCTURECONVERSION_H diff --git a/third_party/ascend/include/Utils/CMakeLists.txt b/third_party/ascend/include/Utils/CMakeLists.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third_party/ascend/include/Utils/DebugUtils.h b/third_party/ascend/include/Utils/DebugUtils.h new file mode 100644 index 0000000000..547774a5d3 --- /dev/null +++ b/third_party/ascend/include/Utils/DebugUtils.h @@ -0,0 +1,126 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ASCEND_UTILS_DEBUGUTILS_H +#define TRITON_ASCEND_UTILS_DEBUGUTILS_H + +#include +#include +#include + +namespace mlir { +class Operation; +class PatternRewriter; +} // namespace mlir + +//===----------------------------------------------------------------------===// +// msdebug helpers. All declarations live in mlir::triton::debug; the NOP +// insertion helpers are additionally exposed at global scope via using +// declarations (see the bottom of this header) so existing converter call +// sites remain unqualified. +// +// The NOP insertion helpers are runtime-gated via the +// LLVM_EXTRACT_DI_LOCAL_VARIABLES environment variable (see isDebugNopEnabled); +// they are not compiled out. Definitions in DebugUtils.cpp. +//===----------------------------------------------------------------------===// + +namespace mlir { +namespace triton { +namespace debug { + +//===----------------------------------------------------------------------===// +// NOP insertion helpers. +//===----------------------------------------------------------------------===// + +/// Unwrap CallSiteLoc (caller-preferring) and FusedLoc (last non-unknown) down +/// to a single representative location for tagging a debug NOP. +/// `depth` tracks recursion internally; callers should use the default (0). +/// Recursion is bounded by an implementation-defined limit (currently 16) to +/// guard against pathologically deep or cyclic location trees; on hitting the +/// limit the location is returned as-is. +Location unwrapFusedLocForDebug(Location loc, unsigned depth = 0); + +/// Insert a side-effecting `llvm.inline_asm "nop"` carrying `loc`, so the +/// source line stays anchored in the DWARF line table. No-op unless +/// LLVM_EXTRACT_DI_LOCAL_VARIABLES is enabled. Must be called before the op +/// carrying the location is erased. +void insertDebugNop(Location loc, PatternRewriter &rewriter); + +/// Anchor the source line of a mask operand: inserts a debug NOP at the +/// location of the op defining `mask`, so a masked load/store/atomic keeps a +/// breakable PC for the `mask = ...` line. No-op when `mask` is null or has no +/// defining op. +void insertDebugNopForMask(Value mask, PatternRewriter &rewriter); + +/// Insert one debug NOP per unique FileLineColLoc reachable from `loc`, +/// descending through CallSiteLoc (caller frame), NameLoc, and nested FusedLoc +/// wrappers. Keeps every distinct user source line fused into a single op +/// breakable. Falls back to a single anchor on `loc` when no concrete +/// FileLineColLoc is reachable. +void insertDebugNopForAllLines(Location loc, PatternRewriter &rewriter); + +//===----------------------------------------------------------------------===// +// Shared location analysis / rewrite helpers used by the debug passes +// (CanonicalizeDebugLocationsPass, DeduplicateDebugNopsPass). +// These do NOT self-gate; the passes gate. +//===----------------------------------------------------------------------===// + +/// True if `filename` is an inlined library/stdlib file (lives under a +/// site-packages or dist-packages tree). +bool isForeignFile(llvm::StringRef filename); + +/// True if `op` is one of our debug NOPs: llvm.inline_asm "nop", side +/// effecting, with no results and no operands. +bool isDebugNop(Operation *op); + +/// Resolve `loc` to the FileLineColLoc the user should see -- caller frame for +/// call sites (via unwrapFusedLocForDebug), descending NameLoc. +/// Returns a null FileLineColLoc when no user-facing file location is +/// reachable; callers MUST check `if (!result)` before use. +FileLineColLoc unwrapToUserFileLineCol(Location loc); + +/// Rewrite call-site locations whose callee resolves to a foreign (stdlib) +/// file so they collapse to their caller (user) frame. Recurses through +/// NameLoc / FusedLoc / nested call sites. `depth` tracks recursion +/// internally; callers should use the default (0). Recursion is bounded by an +/// implementation-defined limit (currently 16). +Location collapseForeignCallsites(Location loc, unsigned depth = 0); + +/// Whitelist-free replacement for tools::getBoolEnv -- reads the +/// LLVM_EXTRACT_DI_LOCAL_VARIABLES gate directly so no core-Triton +/// (GetEnv.hpp) change is required. The environment is read once and cached. +bool isDebugNopEnabled(); + +} // namespace debug +} // namespace triton +} // namespace mlir + +//===----------------------------------------------------------------------===// +// Backward-compatible global-scope aliases for the NOP insertion helpers, so +// the TritonToLinalg converters can keep calling them unqualified. +//===----------------------------------------------------------------------===// + +using mlir::triton::debug::insertDebugNop; +using mlir::triton::debug::insertDebugNopForAllLines; +using mlir::triton::debug::insertDebugNopForMask; + +#endif // TRITON_ASCEND_UTILS_DEBUGUTILS_H diff --git a/third_party/ascend/include/Utils/InterleaveOptimization.h b/third_party/ascend/include/Utils/InterleaveOptimization.h new file mode 100644 index 0000000000..63478c1252 --- /dev/null +++ b/third_party/ascend/include/Utils/InterleaveOptimization.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "TritonMemoryAccess/LoadStoreMaskAnalysis.h" +#include "ascend/include/Utils/Utils.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/GPU/IR/GPUDialect.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Passes.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LogicalResult.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/MathExtras.h" + +#include +#include +#include +#include +#include + +namespace mlir { +namespace triton { + +enum class IndexMode : int { EVEN_MODE = 0, ODD_MODE = 1 }; + +LogicalResult +DeinterleaveStatusOptimization(triton::LoadOp op, + triton::LoadOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter); + +LogicalResult DeinterleaveStatusWithMaskOptimization( + triton::LoadOp op, triton::LoadOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter, MaskState &mstate, Value localMem); + +LogicalResult +InterleaveStatusOptimization(SmallVector materializeVec); + +LogicalResult +InterleaveStatusWithMaskOptimization(SmallVector materializeVec); + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/include/Utils/Utils.h b/third_party/ascend/include/Utils/Utils.h new file mode 100644 index 0000000000..c04abb575c --- /dev/null +++ b/third_party/ascend/include/Utils/Utils.h @@ -0,0 +1,292 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITONNPU_UTILS_UTILS_H +#define TRITONNPU_UTILS_UTILS_H + +#include "TritonMemoryAccess/OpFoldResultUtils.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/Dialect/Utils/StructuredOpsUtils.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/TypeRange.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/LogicalResult.h" + +#include +#include +#include + +namespace mlir { +namespace ConverterUtils { + +const std::string GeneratedByMakeTensorPtrTAG = "GeneratedByMakeTensorPtr"; +const std::string discreteMaskAttrName = "DiscreteMask"; +const std::string mixCompileDiscreteMaskAttrName = "MixCompileDiscreteMask"; +const std::string explicitRouteToSIMDAttrName = "ExplicitRouteToSIMD"; +// The access must consume the actual loop mask; full-load/select is unsafe. +const std::string runtimeLoopMaskAttrName = "RuntimeLoopMask"; +const std::string discreteAttrName = "DiscreteMemAccess"; +const std::string continuousAttrName = "ContinuousMemAccess"; +const std::string customSrcPtrIndexAttrName = "SrcPtrIndex"; + +bool isaPermutedMemRefType(MemRefType); + +std::optional +getLastStrideOfReinterpretCastOp(memref::ReinterpretCastOp op); + +Value getTransposedValue(Value source, const Location loc, + ConversionPatternRewriter &rewriter, + llvm::ArrayRef order); + +SmallVector getNParallelLoopsAttrs(unsigned n); + +Value getScalarValue(Value operand, Location loc, + ConversionPatternRewriter &rewriter); + +memref::SubViewOp makeSubViewOp(Value src, + const llvm::SmallVector &offsets, + const llvm::SmallVector &sizes, + const Location &loc, + ConversionPatternRewriter &rewriter); + +tensor::ExtractSliceOp +makeExtractSliceOp(Value src, const llvm::SmallVector &offsets, + const llvm::SmallVector &sizes, + const Location &loc, ConversionPatternRewriter &rewriter); + +std::optional getFullShapeOp(Value val, + ConversionPatternRewriter &rewriter); + +SmallVector +getBoundarySizes(llvm::ArrayRef boundaryCheck, Value ptr, + const Location &loc, ConversionPatternRewriter &rewriter); + +bool requiresVolatileIndirectLoad(Value srcPtr, Operation *loadOp); + +SmallVector getBroadcastDims(RankedTensorType src, + RankedTensorType dst); + +SmallVector getUnbroadcastDims(RankedTensorType src, + RankedTensorType dst); + +} // namespace ConverterUtils + +class ConversionPatternRewriter; + +namespace triton { + +namespace ascend { + +// Keep the C++ pass contract aligned with the public Python selector. The +// Python boundary performs the target validation and canonicalization; pass +// parsing remains strict so direct pass-pipeline users cannot silently turn an +// unknown string into SIMD. +enum class CompileMode { + Simd, + SimtTemplate, + SimtOnly, +}; + +inline std::optional parseCompileMode(llvm::StringRef mode) { + if (mode == "simd") + return CompileMode::Simd; + if (mode == "simd_simt_template" || mode == "unstructured_in_simt") + return CompileMode::SimtTemplate; + if (mode == "simt_only") + return CompileMode::SimtOnly; + return std::nullopt; +} + +inline bool isSimtTemplateMode(CompileMode mode) { + return mode == CompileMode::SimtTemplate; +} + +inline bool isPureSimtMode(CompileMode mode) { + return mode == CompileMode::SimtOnly; +} + +} // namespace ascend + +enum class IndirectLoadInterfaceOpType { Undefined = 0, Load = 1, Calc = 2 }; + +// Traceback from rootOp to find the targetOp with the specified condition +mlir::Operation * +findFirstMatchingOperandDef(mlir::Operation *rootOp, + const std::function &condFn); + +void traverseBackwardUpdateOperandChainIf( + Operation *op, std::function conditionFn, + std::function stopFn, + std::function actionFn, OpBuilder &builder, + DenseSet &handledOperation); + +void traverseBackwardUpdateOperandChainIf( + Operation *rootOp, std::function conditionFn, + std::function stopFn, + std::function actionFn); + +void traverseForwardUpdateUserChainIf( + Operation *op, std::function conditionFn, + std::function stopFn, + std::function actionFn, OpBuilder &builder, + llvm::SmallPtrSet &stopOps); + +void traverseForwardUpdateUserChainIf( + Operation *rootOp, std::function conditionFn, + std::function stopFn, + std::function actionFn, + llvm::SmallPtrSet &stopOps); + +// UseAnalysis will tag operations whose results are used only as meta-data +// with "MetaUse" tag. +bool isMetaUse(Operation *op); + +bool isMixUse(Operation *op); + +IndirectLoadInterfaceOpType getIndirectLoadInterfaceOpType(Operation *op); + +bool opIsIndirectLoad(Operation *op); + +bool opIsIndirectCalc(Operation *op); + +/// Maximum expected rank for loop tiling in tensor operations. +static constexpr int kMaxTiledRank = 4; + +/// This function generates a series of `scf.for` loops for the given dimensions +/// in `loopDims`. Although the loops are created sequentially, nesting is +/// simulated by adjusting the insertion point to the body of the last created +/// loop. This allows the `bodyFunc` to be inserted into the innermost scope. +/// +/// \param rewriter The MLIR OpBuilder used to create operations. +/// \param loc The source location information for debuggability. +/// \param target The memref value whose dimensions are being looped over. +/// \param loopDims An array of dimension indices to create loops for. +/// \param bodyFunc A callable that defines the operations to insert in the +/// innermost loop. +/// It takes a SmallVector of induction variables (one per +/// loop). +/// +template +void createSimpleNestedLoops(OpBuilder &rewriter, Location loc, Value target, + ArrayRef loopDims, Func bodyFunc) { + MemRefType type = cast(target.getType()); + int rank = type.getRank(); + + Value zero = rewriter.create(loc, 0); + Value one = rewriter.create(loc, 1); + + llvm::SmallVector loops; + llvm::SmallVector ivs; + + for (int dim : loopDims) { + Value ub; + if (type.isDynamicDim(dim)) { + ub = rewriter.create(loc, target, dim).getResult(); + } else { + ub = rewriter.create(loc, type.getDimSize(dim)); + } + + auto forOp = rewriter.create(loc, zero, ub, one); + rewriter.setInsertionPointToStart(forOp.getBody()); + loops.push_back(forOp); + ivs.push_back(forOp.getInductionVar()); + } + + bodyFunc(ivs); + + if (!loops.empty()) { + rewriter.setInsertionPointAfter(loops.front()); + } +} + +scf::ForOp createNestedLoops( + OpBuilder &builder, Location loc, unsigned currentDim, unsigned totalDims, + ValueRange LBs, ValueRange UBs, ValueRange steps, SmallVector &ivs, + ValueRange initArgs, + function_ref &, ValueRange)> + bodyBuilder); + +ModuleOp getModuleOpFromOperation(Operation *op); + +bool isTensorPtrType(Type type); + +namespace ascend { + +// Fractal (zN) block geometry shared by ascend.dot's type inference/verifier +// (DotOp::inferReturnTypes) and its lowering (DotConverter). The inner block is +// [kFractalBlock, b] with b = kBytesPerFractalCol / elemBytes, i.e. f16/bf16 -> +// 16, f32 -> 8, int8 -> 32; fractal_c is the L0C accumulator fractal, always +// [kFractalBlock, kFractalBlock]. +inline constexpr int64_t kFractalBlock = 16; +inline constexpr int64_t kBytesPerFractalCol = 32; +inline constexpr unsigned kBitsPerByte = 8; +// Cube accumulator width for integer inputs (int8 -> i32). +inline constexpr unsigned kDotAccIntWidth = 32; + +} // namespace ascend + +} // namespace triton + +class OpBuilder; + +/// Returns true when both ranges have identical ordered type signatures. +bool haveSameTypes(TypeRange lhs, TypeRange rhs); + +enum class ReduceWithIndexType { MAX, MIN, None }; +enum class TieBreakType { LEFT, RIGHT, None }; + +struct ReduceWithIndexParams { + ReduceWithIndexType withIndexType = ReduceWithIndexType::None; + TieBreakType tieBreakType = TieBreakType::None; + bool isUnsignedSrc; +}; + +llvm::FailureOr +getReduceWithIndexParams(triton::ReduceOp op); + +void addReduceWithIndexAttr(ReduceWithIndexParams params, + ConversionPatternRewriter &rewriter, + linalg::ReduceOp reduceOp); + +enum class TypelessValue { Undefined = 0, Zero = 1, Min = 2, Max = 3 }; + +FailureOr specializeTypelessValueToAttr(TypelessValue, Type, + OpBuilder &); + +FailureOr specializeTypelessValueToConstant(TypelessValue, Type, + Location, OpBuilder &); + +bool checkStructureAnnotated(Operation *op, RewriterBase &rewriter); + +bool isDistributedTypeCustomOp(Operation *op); +} // namespace mlir + +#endif // TRITONNPU_UTILS_UTILS_H diff --git a/third_party/ascend/language/cann/__init__.py b/third_party/ascend/language/cann/__init__.py new file mode 100644 index 0000000000..7d6e74d6c2 --- /dev/null +++ b/third_party/ascend/language/cann/__init__.py @@ -0,0 +1,29 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from triton.language import math + +from . import libdevice +from . import extension + +extension.parallel = extension.aux_ops.parallel +math.tanh = libdevice.tanh + +__all__ = ["libdevice", "extension"] diff --git a/third_party/ascend/language/cann/extension/__init__.py b/third_party/ascend/language/cann/extension/__init__.py new file mode 100644 index 0000000000..bab5ec0d87 --- /dev/null +++ b/third_party/ascend/language/cann/extension/__init__.py @@ -0,0 +1,167 @@ +from triton.backends.ascend.utils import is_compile_on_910_95 +from triton._C.libtriton.ascend import ir as _ascend_ir + +# MLIR affine bindings (same objects as triton._C.libtriton.ascend.ir). +affine_expr = _ascend_ir.affine_expr +affine_constant_expr = _ascend_ir.affine_constant_expr +affine_dim_expr = _ascend_ir.affine_dim_expr +affine_symbol_expr = _ascend_ir.affine_symbol_expr +affine_binary_op_expr = _ascend_ir.affine_binary_op_expr +affine_map = _ascend_ir.affine_map + +AffineExpr = affine_expr +AffineConstantExpr = affine_constant_expr +AffineDimExpr = affine_dim_expr +AffineSymbolExpr = affine_symbol_expr +AffineBinaryOpExpr = affine_binary_op_expr +AffineMap = affine_map + +from .core import ( + ascend_address_space, + builtin, + CORE, + copy_from_ub_to_l1, + copy, + debug_barrier, + fixpipe, + FixpipeDMAMode, + FixpipeDualDstMode, + FixpipePreQuantMode, + FixpipePreReluMode, + int64, + is_builtin, + MODE, + PIPE, + SYNC_HINT, + EVENT_ID, + IteratorType, + sub_vec_id, + sub_vec_num, + sync_block_all, + sync_block_set, + sync_block_wait, + SYNC_IN_VF, + conv1d, + dot, + conv2d, +) + +from .scope import scope + +from .custom_op import ( + custom, + custom_semantic, + register_custom_op, + SyncEventSlot, +) + +from . import builtin_custom_ops + +from .math_ops import (atan2, isfinited, finitef) + +from .aux_ops import ( + parallel, + compile_hint, + multibuffer, +) + +from .vec_ops import ( + insert_slice, + extract_slice, + get_element, + sort, + flip, + cast, +) + +from .mem_ops import ( + index_put, + gather_out_to_ub, + scatter_ub_to_out, + index_select_simd, +) + +__all__ = [ + # core + "builtin", + "copy_from_ub_to_l1", + "copy", + "CORE", + "debug_barrier", + "fixpipe", + "FixpipeDMAMode", + "FixpipeDualDstMode", + "FixpipePreQuantMode", + "FixpipePreReluMode", + "int64", + "is_builtin", + "MODE", + "PIPE", + "SYNC_HINT", + "EVENT_ID", + "IteratorType", + "sub_vec_id", + "sub_vec_num", + "sync_block_all", + "SYNC_IN_VF", + "conv1d", + "conv2d", + + # address space + "ascend_address_space", + + # ascend IR affine (MLIR) + "affine_expr", + "affine_constant_expr", + "affine_dim_expr", + "affine_symbol_expr", + "affine_binary_op_expr", + "affine_map", + "AffineExpr", + "AffineConstantExpr", + "AffineDimExpr", + "AffineSymbolExpr", + "AffineBinaryOpExpr", + "AffineMap", + + # scope + "scope", + + # custom op + "custom", + "custom_semantic", + "register_custom_op", + "SyncEventSlot", + + # math ops + "atan2", + "isfinited", + "finitef", + + # aux ops + "sync_block_set", + "sync_block_wait", + "parallel", + "compile_hint", + "multibuffer", + + # vec ops + "insert_slice", + "extract_slice", + "get_element", + "sort", + "flip", + "cast", + + # mem ops + "index_put", + "gather_out_to_ub", + "scatter_ub_to_out", + "index_select_simd", + + # utils + "is_compile_on_910_95", + + # dot ops + "dot", +] diff --git a/third_party/ascend/language/cann/extension/_utils.py b/third_party/ascend/language/cann/extension/_utils.py new file mode 100644 index 0000000000..97824ae9c9 --- /dev/null +++ b/third_party/ascend/language/cann/extension/_utils.py @@ -0,0 +1,54 @@ +import triton.language.core as tl +from triton._C.libtriton import ir + + +def custom_op(builder: ir.builder, op_name: str, **kwargs): + if op_name == "sync_block_all": + return builder.create_custom_op_for_inter_core_sync(op_name, kwargs["mode"], kwargs["event_id"]) + + elif op_name == "sync_block_set": + return builder.create_custom_op_for_inter_core_sync(op_name, kwargs["sender"], kwargs["event_id"]) + + elif op_name == "sync_block_wait": + return builder.create_custom_op_for_inter_core_sync(op_name, kwargs["sender"], kwargs["event_id"]) + + raise ValueError(f"Unsupported custom op: {op_name}") + + +def _is_int_like_elem(x) -> bool: + """Accept int / tl.constexpr(int) / tl.tensor(int*).""" + if isinstance(x, int): + return True + if isinstance(x, tl.constexpr): + # constexpr value should be python int + return isinstance(x.value, int) + if isinstance(x, tl.tensor): + # Offsets/strides must be integer typed (i32/i64 etc.) + return x.dtype.is_int() + return False + + +def _assert_int_like_tuple(name: str, xs): + assert isinstance(xs, (list, tuple, tl.tuple)), f"{name} should be a tuple/list, but got {type(xs)}" + assert all(_is_int_like_elem(x) for x in xs), f"{name} should be integer" + + +def _convert_elem_to_ir_value(builder, elem, require_i64): + if isinstance(elem, int): + elem = tl.constexpr(elem) + if isinstance(elem, tl.constexpr): + if require_i64: + assert -2**63 <= elem.value < 2**63, f"Block pointers only support 64 bit `shape/strides`, " \ + f"got a value {elem.value} which is out of the range" + return builder.get_int64(elem.value) + else: + assert -2**31 <= elem.value < 2**31, f"Block pointers only support 32 bit `offsets/block_shape`, " \ + f"got a value {elem.value} which is out of the range" + return builder.get_int32(elem.value) + elif isinstance(elem, tl.tensor): + if require_i64: + return builder.create_int_cast(elem.handle, builder.get_int64_ty(), elem.dtype.is_int_signed()) + else: + return builder.create_int_cast(elem.handle, builder.get_int32_ty(), elem.dtype.is_int_signed()) + else: + assert False, f"Unsupported element type in shape/strides/offsets: {type(elem)}" diff --git a/third_party/ascend/language/cann/extension/aux_ops.py b/third_party/ascend/language/cann/extension/aux_ops.py new file mode 100644 index 0000000000..7a1f73a4f0 --- /dev/null +++ b/third_party/ascend/language/cann/extension/aux_ops.py @@ -0,0 +1,106 @@ +import triton.language as tl +from triton.language import core +from triton.language.core import (_unwrap_if_constexpr, _unwrap_iterable, builtin, constexpr, dtype, tensor, + check_bit_width, _unwrap_if_constexpr, range) + +from typing import Optional, Tuple, List, overload, Union +from triton._C.libtriton import ir + + +class parallel(range): + """ + Iterator that counts upward forever, with parallel execution semantics. + + This is a special iterator used to implement similar semantics to Python's ``range`` in the context of + ``triton.jit`` functions. In addition, it allows the user to pass extra attributes to the compiler. + + :param arg1: If ``arg2`` is not given, the end of the range; otherwise the start of the range. + :type arg1: int + :param arg2: The end of the range, if given. + :type arg2: int + :param step: The step size of the iteration. Defaults to 1. + :type step: int + :param num_stages: Number of pipeline stages for the loop. + :type num_stages: int + :param loop_unroll_factor: The unroll factor applied to the loop body. + :type loop_unroll_factor: int + :param bind_sub_block: Tells the compiler whether multiple vector cores participate in the loop. + This is used in the mixed cube-vector kernel on 910B. The number of vector cores is determined by the + number of iterations in this loop. Currently on 910B, at most 2 vector cores can be used. + :type bind_sub_block: bool + """ + + def __init__(self, arg1, arg2=None, step=None, num_stages=None, loop_unroll_factor=None, + bind_sub_block: bool = False): + super().__init__(arg1, arg2, step, num_stages, loop_unroll_factor) + self.bind_sub_block = bind_sub_block + + +def compile_hint_impl(ptr: tensor, hint_name: str, hint_val, builder: ir.builder): + # simt mode does not support hint annotations + # FIXME: is_simt_mode + # if builder.is_simt_mode(): + # return + # Check isinstance(hint_val, bool) first to handle False explicitly + if isinstance(hint_val, bool): + hint_val = builder.get_bool_attr(hint_val) + elif not hint_val: + hint_val = builder.get_unit_attr() + elif isinstance(hint_val, int): + hint_val = builder.get_int32_attr(hint_val) + elif isinstance(hint_val, core.constexpr): + hint_val = builder.get_string_attr(hint_val.value) + elif isinstance(hint_val, (list, core.tuple)): + # only support i64 array attr for now + hint_val = builder.get_i64_array_attr(hint_val) + else: + raise ValueError(f"Unsupported hint value type: {type(hint_val)}") + builder.create_annotation_mark(ptr.handle, hint_name, hint_val) + + +@builtin +def compile_hint(ptr, hint_name, hint_val=None, _semantic=None): + """Attaches a compiler hint annotation to a tensor, guiding Ascend code generation. + + Hints influence how the compiler handles memory layout, buffering, and other + optimization decisions. The hint is attached to ``ptr`` and serialized as an + MLIR annotation in the generated code. + + :param ptr: The tensor to annotate with the compiler hint. + :type ptr: tensor + :param hint_name: Hint identifier string (e.g., ``"hivm.multi_buffer"``). + :type hint_name: str + :param hint_val: Hint value. Can be a bool, int, constexpr string, or list of + integers (serialized as an i64 array attribute). + :type hint_val: bool | int | constexpr | list[int] | None + """ + # simt mode does not support hint annotations + if _semantic.builder.is_simt_mode(): + return + + def _unwrap(val): + return _unwrap_if_constexpr(val) if val else val + + hint_name = _unwrap_if_constexpr(hint_name) + assert isinstance(hint_name, str), f"hint name: {hint_name} is not string" + if isinstance(hint_val, (list, core.tuple)): + hint_val = [_unwrap(val) for val in hint_val] + else: + hint_val = _unwrap(hint_val) + hint_val = _unwrap_if_constexpr(hint_val) if hint_val else hint_val + compile_hint_impl(ptr, hint_name, hint_val, _semantic.builder) + + +@builtin +def multibuffer(src: tensor, size, _semantic=None): + """ + Set multi_buffer for an existing tensor. + + :param src: The tensor set to bufferize multiple time. + :type src: tensor + :param size: The number of copies. + :type size: int or constexpr + """ + buffer_size = _unwrap_if_constexpr(size) + assert isinstance(buffer_size, int) and buffer_size == 2, f"only support bufferize equals 2" + compile_hint_impl(src, "hivm.multi_buffer", buffer_size, _semantic.builder) diff --git a/third_party/ascend/language/cann/extension/builder.py b/third_party/ascend/language/cann/extension/builder.py new file mode 100644 index 0000000000..0144098adf --- /dev/null +++ b/third_party/ascend/language/cann/extension/builder.py @@ -0,0 +1,116 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Ascend-specific builder utilities for code generation. +""" + +__all__ = [ + "create_builder_method_wrapper", + "attach_builder_methods", + "setup_unified_builder", +] + + +def create_builder_method_wrapper(main_builder, delegate_builder, method_name): + """ + Create a wrapper that delegates a method call to another builder while + synchronizing insertion points and locations. + """ + delegate_method = getattr(delegate_builder, method_name) + + def wrapper(*args, **kwargs): + saved_ip = main_builder.get_insertion_point() + saved_loc = main_builder.get_loc() + delegate_builder.restore_insertion_point(saved_ip) + if saved_loc: + delegate_builder.set_loc(saved_loc) + result = delegate_method(*args, **kwargs) + main_builder.restore_insertion_point(saved_ip) + if saved_loc: + main_builder.set_loc(saved_loc) + return result + + wrapper.__name__ = method_name + wrapper.__doc__ = getattr(delegate_method, '__doc__', None) + return wrapper + + +def attach_builder_methods(main_builder, delegate_builder, method_names): + """Attach multiple methods from a delegate builder to the main builder.""" + for method_name in method_names: + wrapper = create_builder_method_wrapper(main_builder, delegate_builder, method_name) + setattr(main_builder, method_name, wrapper) + + +def setup_unified_builder(main_builder, ascend_builder): + """Set up a unified builder interface by attaching methods from specialized builders.""" + main_builder._ascend_builder = ascend_builder + ascend_methods = [ + 'create_scope_op', + 'scope_return', + 'get_t_core_type_attr_name', + 'get_t_core_type_cube_attr', + 'get_t_core_type_vector_attr', + 'get_target_attribute', + 'create_get_sub_vec_id', + 'create_copy_buffer', + 'create_copy_tensor', + 'create_fixpipe', + 'create_annotation_mark', + 'create_bind_buffer', + 'create_debug_barrier', + 'is_910_95', + "sync_block_set", + "sync_block_wait", + "create_convert_layout", + 'sync_block_all', + 'create_custom_op', + 'create_custom_macro_op', + 'get_core_type_attr', + 'get_pipe_attr', + 'get_vf_mode_attr', + 'get_iterator_types_attr', + 'get_int_attr', + 'get_type_array_attr', + 'get_affine_map_attr', + 'get_affine_map_array_attr', + 'get_event_attr', + 'get_sync_event_slot_attr', + 'get_array_attr', + 'is_simt_mode', + 'get_i64_array_attr', + 'get_buffer_ty', + 'get_buffer_ty_with_strides', + 'create_extract_scalar', + 'create_extract_slice', + 'create_insert_slice', + 'create_custom_op_for_inter_core_sync', + 'create_index_select_simd', + 'create_index_put', + 'create_gather_out_to_ub', + 'create_scatter_ub_to_out', + 'create_conv1d', + 'create_conv2d', + 'create_sort', + 'create_flip', + 'create_tanh', + 'create_annotation', + ] + attach_builder_methods(main_builder, ascend_builder, ascend_methods) diff --git a/third_party/ascend/language/cann/extension/builtin_custom_ops.py b/third_party/ascend/language/cann/extension/builtin_custom_ops.py new file mode 100644 index 0000000000..e6ebcb95cf --- /dev/null +++ b/third_party/ascend/language/cann/extension/builtin_custom_ops.py @@ -0,0 +1,105 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton.language.core as tl +from .custom_op import register_custom_op +from .core import CORE, PIPE, MODE +from ..utils import _deprecated +from ._utils import _is_int_like_elem, _assert_int_like_tuple + + +@_deprecated(fn_name="triton.language.extra.cann.extension.custom('__builtin_index_select', ...)") +@register_custom_op +class _index_select: + """ + This operation gathers values from the src GM tensor into the out UB tensor + at positions with offsets specified by the index UB tensor along the specified + dimension using a SIMT template. This operation supports 2D–5D. + + Arguments: + - src: pointer type, the source tensor pointer (in GM) + - index: tensor, a tensor to gather (in UB) + - dim: int, the dimension to gather along + - bound: int, the upper boundary for index + - end_offset: tuple of int, the end offsets of each dimension for index tensor + - start_offset: tuple of int, the start offsets of each dimension for src tensor + - src_stride: tuple of int, the stride of each dimension of src tensor + - other(Optional): scalar value, the default value when index is out of boundary (in UB) + - out: the output tensor (in UB) + + Note: + - Supported source ranks: 2D ~ 5D. + - Supported index ranks: 1D or 2D. + - `dim` must be valid (0 <= dim < source ranks). + + Reference formula: + Index select operation for different tensor ranks: + 1. 2D index gather (0 <= dim <= 1) + 1.1 dim = 0, index_rank = 1, src_rank = 2, out_rank = 2 + index_shape = (Ai,) + end_offset = (Ai_end, B_end) + start_offset = (0, B_begin) + out[i][0:B_end-B_begin] = src[index[i]][B_begin:B_end] + 1.2 dim = 0, index_rank = 2, src_rank = 2, out_rank = 3 + index_shape = (Ai, Aj) + end_offset = (Ai_end, Aj_end, B_end) + start_offset = (0, B_begin) + out[i][j][0:B_end-B_begin] = src[index[i][j]][B_begin:B_end] + 2. 3D index gather (0 <= dim <= 2) + 2.1 dim = 0, index_rank = 2, src_rank = 3, out_rank = 4 + index_shape = (Ai, Aj) + end_offset = (Ai_end, Aj_end, B_end, C_end) + start_offset = (0, B_begin, C_begin) + out[i][j][0:B_end-B_begin][0:C_end-C_begin] = src[index[i][j]][B_begin:B_end][C_begin:C_end] + and so on. + """ + name = '__builtin_index_select' + core = CORE.VECTOR + pipe = PIPE.PIPE_V + mode = MODE.SIMT + + def __init__(self, src, index, dim, bound: tl.int64, end_offset, start_offset, src_stride, other=None, out=None): + assert src.type.is_ptr() or src.dtype.is_ptr(), f"src should be a pointer, but got {src.type}" + assert index.dtype.is_int(), "index should be integer tensor" + src_rank = len(src_stride) + idx_rank = len(index.shape) + assert 2 <= src_rank <= 5, f"src rank should in [2, 5], but got {src_rank}" + assert 1 <= idx_rank <= 2, f"index rank should in [1, 2], but got {idx_rank}" + assert _is_int_like_elem(dim), "dim should be an integer" + assert _is_int_like_elem(bound), "bound should be an integer" + assert 0 <= dim < src_rank, f"dim should in [0, {src_rank - 1}], but got {dim}" + assert len(start_offset) == len(src_stride), "start_offset and src_stride should have same size" + assert len(end_offset) == idx_rank + len( + start_offset) - 1, "len(end_offset) should be equal to index rank + len(start_offset) - 1" + + _assert_int_like_tuple("end_offset", end_offset) + _assert_int_like_tuple("start_offset", start_offset) + _assert_int_like_tuple("src_stride", src_stride) + + assert out, "out is required" + assert out.dtype == src.dtype.element_ty, "out should have same dtype as src" + + # use index type for end_offset, start_offset and src_stride. + self.arg_type['end_offset'] = index.dtype + self.arg_type['start_offset'] = index.dtype + self.arg_type['src_stride'] = index.dtype + self.extra_attr = f"src_stride_len={len(src_stride)}" diff --git a/third_party/ascend/language/cann/extension/code_generator.py b/third_party/ascend/language/cann/extension/code_generator.py new file mode 100644 index 0000000000..49a7ef1314 --- /dev/null +++ b/third_party/ascend/language/cann/extension/code_generator.py @@ -0,0 +1,204 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Ascend-specific code generation handlers for 'with' statement context managers. +""" + +__all__ = ["handle_scope_with", "mangle_ty"] +import ast + + +def mangle_ty(ty): + """ + Replacement implementation for triton.compiler.code_generator.mangle_ty. + + This is registered via ASCEND_WITH_DISPATCH["mangle_ty"] and picked up by + triton.compiler.code_generator through its global WITH_DISPATCH table. + """ + # Lazy imports to avoid circular dependencies at module import time. + from triton import language + from triton.extension.buffer.language import core as bl + + # Buffer types are Python-side dtypes; handle them first. + if isinstance(ty, bl.buffer_type): + elt = mangle_ty(ty.element_ty) + shape = "_".join(map(str, ty.shape)) + return f"B{elt}S{shape}S" + + if ty.is_ptr(): + return "P" + mangle_ty(ty.element_ty) + if ty.is_int(): + SIGNED = language.dtype.SIGNEDNESS.SIGNED + prefix = "i" if ty.int_signedness == SIGNED else "u" + return prefix + str(ty.int_bitwidth) + if ty.is_floating(): + return str(ty) + if ty.is_block(): + elt = mangle_ty(ty.scalar) + shape = "_".join(map(str, ty.shape)) + return f"{elt}S{shape}S" + if ty.is_void(): + return "V" + raise TypeError(f"Unsupported type {ty}") + + +def _extract_scope_attributes(context_expr): + """Extract attributes from scope(...) call.""" + scope_attrs = {} + for keyword in context_expr.keywords: + if isinstance(keyword.value, ast.Constant): + scope_attrs[keyword.arg] = keyword.value.value + return scope_attrs + + +def _py_value_to_mlir_attr(builder, value): + """Convert Python value to MLIR attribute.""" + attr_creators = { + str: lambda v: builder.get_string_attr(v), + bool: lambda v: builder.get_bool_attr(v), + int: lambda v: builder.get_int32_attr(v), + list: lambda v: builder.get_i64_array_attr(v), + } + creator = attr_creators.get(type(value)) + return creator(value) if creator else value + + +def _handle_core_mode_attr(builder, core_mode): + """Handle core_mode attribute conversion.""" + if core_mode not in ("cube", "vector"): + return {} + return { + builder.get_t_core_type_attr_name(): + (builder.get_t_core_type_cube_attr() if core_mode == "cube" else builder.get_t_core_type_vector_attr()) + } + + +def _build_mlir_attrs_from_scope_attrs(builder, scope_attrs): + """Convert Python scope attributes to MLIR attributes. + + Args: + builder: The IR builder + scope_attrs: Dict of scope attributes (e.g., {'core_mode': 'vector', 'noinline': True}) + + Returns: + Dict of MLIR attributes + """ + mlir_attrs = {"noinline": builder.get_unit_attr()} + for k, v in scope_attrs.items(): + if k == "core_mode": + mlir_attrs.update(_handle_core_mode_attr(builder, v)) + elif k == "vec_mode": + if v is not None: + mlir_attrs["vec_mode"] = builder.get_string_attr(v) + elif k == "noinline": + if not v: + mlir_attrs.pop("noinline") + elif k == "disable_auto_sync": + if v: + mlir_attrs["hivm.disable_auto_sync"] = _py_value_to_mlir_attr(builder, v) + else: + mlir_attrs[k] = _py_value_to_mlir_attr(builder, v) + return mlir_attrs + + +def _verify_loop_carried_variable(_is_triton_value, _is_triton_tensor, name, loop_val, live_val): + """Verify that loop-carried variable types are consistent.""" + assert _is_triton_value(loop_val), f'cannot reassign constxpr {name} in the loop' + assert _is_triton_value(live_val), f'cannot reasign constexpr {name} in the loop' + assert type(loop_val) == type(live_val), f'Loop carried variable {name} changed type' + assert not _is_triton_tensor(loop_val) or loop_val.type == live_val.type, \ + f'Loop-carried variable {name} has initial type {live_val.type} '\ + f'but is re-assigned to {loop_val.type} in loop! '\ + f'Please make sure that the type stays consistent.' + + +def _reconstruct_value_from_ir(language, entry_block_arg, ret_type): + """Reconstruct a tensor value from IR.""" + return language.core.tensor(entry_block_arg, ret_type) + + +def handle_scope_with(generator, node): + """ + Handle 'with scope(...)' statements by creating a scope.scope operation. + + This creates a scope.scope operation with a region for the scope block. + Uses SSA threading to properly handle variables modified inside the scope. + + Args: + generator: The CodeGenerator instance + node: AST node for the with statement + """ + # Lazy imports to avoid circular dependency + from triton import language + from triton.compiler.code_generator import enter_sub_region, _is_triton_value, _is_triton_tensor + + context_expr = node.items[0].context_expr + scope_attrs = _extract_scope_attributes(context_expr) + + with enter_sub_region(generator) as sr: + liveins, _ = sr + ip, last_loc = generator._get_insertion_point_and_loc() + + # This implementation is similar to visit_while + dummy = generator.builder.create_block() + generator.builder.set_insertion_point_to_start(dummy) + generator.visit_compound_statement(node.body) + scope_defs = generator.local_defs + dummy.erase() + + # Verify and get return type of the scope.scope + # (variables that exist in parent scope AND are modified in scope) + names = [] + ret_types = [] + for name in scope_defs: + scope_val = scope_defs[name] + ret_types.append(scope_val.type) + names.append(name) + if name in liveins: + live_val = liveins[name] + _verify_loop_carried_variable(_is_triton_value, _is_triton_tensor, name, scope_val, live_val) + + # Convert Python primitives to MLIR attributes + mlir_attrs = _build_mlir_attrs_from_scope_attrs(generator.builder, scope_attrs) + + # Create scope operation with operands (values from outside) + generator._set_insertion_point_and_loc(ip, last_loc) + scope_op = generator.builder.create_scope_op(mlir_attrs, [ty.to_ir(generator.builder) for ty in ret_types]) + + # Create the entry block with arguments matching the operands + entry_block = generator.builder.create_block_with_parent(scope_op.get_region(0), []) + generator.builder.set_insertion_point_to_start(entry_block) + + # Initialize the scope's symbol table with liveins + generator.lscope = liveins.copy() + generator.visit_compound_statement(node.body) + generator.builder.set_insertion_point_to_end(entry_block) + reconstructed_values = [] + + for i in range(len(names)): + # generator.lscope[names[i]] is already a tensor, just get its IR handle + reconstructed_values.append(generator.lscope[names[i]].handle) + generator.builder.scope_return(reconstructed_values) + + # After exiting enter_sub_region, update symbol table with results + # Convert IR values back to tensor objects + for i, name in enumerate(names): + generator.set_value(name, _reconstruct_value_from_ir(language, scope_op.get_result(i), ret_types[i])) + return None diff --git a/third_party/ascend/language/cann/extension/core.py b/third_party/ascend/language/cann/extension/core.py new file mode 100644 index 0000000000..eefae51381 --- /dev/null +++ b/third_party/ascend/language/cann/extension/core.py @@ -0,0 +1,835 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +__all__ = [ + "ascend_address_space", "builtin", "CORE", "copy_from_ub_to_l1", "copy", "debug_barrier", "fixpipe", + "FixpipeDMAMode", "FixpipeDualDstMode", "FixpipePreQuantMode", "FixpipePreReluMode", "int64", "is_builtin", "MODE", + "PIPE", "SYNC_HINT", "EVENT_ID", "IteratorType", "sub_vec_id", "sub_vec_num", "sync_block_all", "sync_block_set", + "sync_block_wait", "SYNC_IN_VF", "conv1d", "dot", "conv2d" +] + +import enum +from typing import TypeVar, List, Union +from functools import wraps + +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +import triton.language.core as tl + +import triton.extension.buffer.language as bl +from triton.language.core import _unwrap_if_constexpr +from triton.backends.ascend.driver import NPUUtils + +from . import semantic as semantic + +PIPE = semantic.PIPE +SYNC_HINT = semantic.SYNC_HINT +EVENT_ID = semantic.EVENT_ID + +PIPE = semantic.PIPE + +T = TypeVar("T") + +TRITON_BUILTIN = "__triton_builtin__" +ASCEND_BUILTIN = "__ascend_builtin__" + + +def builtin(fn: T) -> T: + """Mark a function as a buffer language builtin.""" + assert callable(fn) + + @wraps(fn) + def wrapper(*args, **kwargs): + if "_semantic" not in kwargs or kwargs["_semantic"] is None: + raise ValueError("Did you forget to add @triton.jit ? " + "(`_semantic` argument must be provided outside of JIT functions.)") + return fn(*args, **kwargs) + + # also set triton_builtin to true so that CodeGenerator will recognize this function + setattr(wrapper, TRITON_BUILTIN, True) + setattr(wrapper, ASCEND_BUILTIN, True) + + return wrapper + + +def is_builtin(fn) -> bool: + """Is this a registered ascend language builtin function?""" + return getattr(fn, ASCEND_BUILTIN, False) + + +class int64(int): + """ + For custom op, python int argument will be converted to int32 by default, + if a device-side int64 is required, you can pass an al.int64(x) to it. + """ + + def __new__(cls, value): + obj = int.__new__(cls, value) + obj.type = tl.int64 + return obj + + +class CORE(enum.Enum): + VECTOR = ascend_ir.CoreType.VECTOR + CUBE = ascend_ir.CoreType.CUBE + CUBE_OR_VECTOR = ascend_ir.CoreType.CUBE_OR_VECTOR + CUBE_AND_VECTOR = ascend_ir.CoreType.CUBE_AND_VECTOR + + +class PIPE(enum.Enum): + PIPE_S = ascend_ir.PIPE.PIPE_S + PIPE_V = ascend_ir.PIPE.PIPE_V + PIPE_M = ascend_ir.PIPE.PIPE_M + PIPE_MTE1 = ascend_ir.PIPE.PIPE_MTE1 + PIPE_MTE2 = ascend_ir.PIPE.PIPE_MTE2 + PIPE_MTE3 = ascend_ir.PIPE.PIPE_MTE3 + PIPE_ALL = ascend_ir.PIPE.PIPE_ALL + PIPE_FIX = ascend_ir.PIPE.PIPE_FIX + + +class MODE(enum.Enum): + SIMD = ascend_ir.MODE.SIMD + SIMT = ascend_ir.MODE.SIMT + MIX = ascend_ir.MODE.MIX + + +class IteratorType(enum.Enum): + Parallel = ascend_ir.IteratorType.Parallel + Broadcast = ascend_ir.IteratorType.Broadcast + Transpose = ascend_ir.IteratorType.Transpose + Reduction = ascend_ir.IteratorType.Reduction + Interleave = ascend_ir.IteratorType.Interleave + Deinterleave = ascend_ir.IteratorType.Deinterleave + Inverse = ascend_ir.IteratorType.Inverse + Pad = ascend_ir.IteratorType.Pad + Concat = ascend_ir.IteratorType.Concat + Gather = ascend_ir.IteratorType.Gather + Cumulative = ascend_ir.IteratorType.Cumulative + Opaque = ascend_ir.IteratorType.Opaque + + +class ascend_address_space_base(bl.address_space): + + def __init__(self, address_space_value: ascend_ir.AddressSpace) -> None: + super().__init__() + self.real_address_space = address_space_value + + def to_ir(self, builder: ir.builder) -> ir.attribute: + return builder.get_target_attribute(self.real_address_space) + + +class ascend_address_space_group: + """Ascend hardware address space constants for buffer allocation. + + Provides named address space specifiers that map to Ascend NPU memory regions: + + - ``UB`` — Unified Buffer (on-chip shared memory) + - ``L1`` — L1 cache buffer + - ``L0A`` — L0 buffer A (Cube unit input) + - ``L0B`` — L0 buffer B (Cube unit input) + - ``L0C`` — L0 buffer C (Cube unit output) + + Usage with :func:`bl.alloc() `:: + + buf = bl.alloc(dtype, shape, al.ascend_address_space.UB) + """ + + def __init__(self): + for k, v in {k: v + for k, v in ascend_ir.AddressSpace.__dict__.items() + if isinstance(v, ascend_ir.AddressSpace)}.items(): + setattr(self, k, ascend_address_space_base(v)) + + +ascend_address_space = ascend_address_space_group() + + +@builtin +def sub_vec_id(_semantic=None) -> tl.tensor: + """ + Get the Vector Core index on the AI Core. + """ + return semantic.sub_vec_id(_semantic) + + +@builtin +def copy_from_ub_to_l1(src: Union[tl.tensor, bl.buffer], dst: Union[tl.tensor, bl.buffer], _semantic: None) -> None: + """ + Copies data from the Unified Buffer (UB) to the L1 Buffer. + + :param src: The source data located in the Unified Buffer. + :type src: tl.tensor | bl.buffer + :param dst: The destination buffer located in L1 memory. + :type dst: tl.tensor | bl.buffer + """ + from warnings import warn + warn("copy_from_ub_to_l1 is deprecated, please use copy instead.") + return semantic.copy_from_ub_to_l1(src, dst, _semantic) + + +@builtin +def copy(src: Union[tl.tensor, bl.buffer], dst: Union[tl.tensor, bl.buffer], _semantic: None) -> None: + """ + Copies data from the Unified Buffer (UB) to the Unified Buffer (UB) or L1 Buffer. + + :param src: The source data located in the Unified Buffer. + :type src: tl.tensor | bl.buffer + :param dst: The destination buffer located Unified Buffer (UB) or L1 memory. + :type dst: tl.tensor | bl.buffer + """ + return semantic.copy(src, dst, _semantic) + + +def create_sync_block(sender, receiver, event_id, is_set: bool, sender_pipe=None, receiver_pipe=None, _semantic=None): + sender = _unwrap_if_constexpr(sender) + receiver = _unwrap_if_constexpr(receiver) + assert isinstance(sender, str) and (sender == "cube" + or sender == "vector"), f"ERROR: sender = {sender}, only supports cube/vector" + assert isinstance(receiver, str) and (receiver == "cube" or receiver + == "vector"), f"ERROR: receiver = {receiver}, only supports cube/vector" + if isinstance(event_id, int): + assert (event_id >= 0) and (event_id < 16), f"event_id: {event_id} should be 0 ~ 15" + if sender == receiver: + raise ValueError(f'Unexpected pair: {sender} -> {receiver}, only supports cube -> vector or vector -> cube') + if sender_pipe is None and receiver_pipe is None: + if sender == "cube": + sender_pipe = PIPE.PIPE_FIX + receiver_pipe = PIPE.PIPE_MTE2 + if sender == "vector": + sender_pipe = PIPE.PIPE_MTE3 + receiver_pipe = PIPE.PIPE_MTE2 + if not isinstance(sender_pipe, PIPE) or not isinstance(receiver_pipe, PIPE): + raise TypeError("sender_pipe and receiver_pipe must be instances of PIPE enum") + if is_set: + return semantic.create_sync_block_set(sender, receiver, event_id, sender_pipe, receiver_pipe, _semantic) + return semantic.create_sync_block_wait(sender, receiver, event_id, sender_pipe, receiver_pipe, _semantic) + + +@builtin +def sync_block_set(sender, receiver, event_id, sender_pipe=None, receiver_pipe=None, _semantic=None): + """Sets a cross-core synchronization flag for producer-consumer sync between Cube and Vector cores. + + Pairs with :func:`sync_block_wait` to coordinate execution between different core types. + Each call increments a per-event counter that the corresponding ``sync_block_wait`` will + decrement (semaphore-like behavior). + + Must be used within an :func:`scope` context matching the sender's core type. + + :param sender: Sending core type. Must be ``"cube"`` or ``"vector"`` (must differ from ``receiver``). + :type sender: str + :param receiver: Receiving core type. Must be ``"cube"`` or ``"vector"``. + :type receiver: str + :param event_id: Sync flag identifier in range [0, 15]. Each ID maps to an independent counter. + :type event_id: int + :param sender_pipe: Sender-side pipeline type (e.g., ``PIPE_MTE1``, ``PIPE_V``). + Defaults to ``PIPE_FIX`` if sender is cube, ``PIPE_MTE3`` if sender is vector. + :type sender_pipe: PIPE + :param receiver_pipe: Receiver-side pipeline type. Defaults to ``PIPE_MTE2``. + :type receiver_pipe: PIPE + """ + return create_sync_block(sender, receiver, event_id, True, sender_pipe, receiver_pipe, _semantic) + + +@builtin +def sync_block_wait(sender, receiver, event_id, sender_pipe=None, receiver_pipe=None, _semantic=None): + """Waits on a cross-core synchronization flag set by :func:`sync_block_set`. + + Blocks execution until the corresponding event counter is positive (signaling the + producer has completed), then decrements it by 1. Pairs with ``sync_block_set`` + for producer-consumer synchronization between Cube and Vector cores. + + Must be used within an :func:`scope` context matching the receiver's core type. + + :param sender: Sending core type. Must be ``"cube"`` or ``"vector"`` (must differ from ``receiver``). + :type sender: str + :param receiver: Receiving core type. Must be ``"cube"`` or ``"vector"``. + :type receiver: str + :param event_id: Sync flag identifier in range [0, 15]. Must match the ID used by the + corresponding ``sync_block_set``. + :type event_id: int + :param sender_pipe: Sender-side pipeline type. Defaults to ``PIPE_FIX`` if sender is cube, + ``PIPE_MTE3`` if sender is vector. + :type sender_pipe: PIPE + :param receiver_pipe: Receiver-side pipeline type. Defaults to ``PIPE_MTE2``. + :type receiver_pipe: PIPE + """ + return create_sync_block(sender, receiver, event_id, False, sender_pipe, receiver_pipe, _semantic) + + +@builtin +def sync_block_all(mode, event_id, _semantic=None): + """Performs global synchronization across all cores of a specified type. + + Inserts a sync barrier to resolve RAW, WAR, and WAW data hazards on shared global + memory across Cube cores, Vector cores, or both. Also supports sub-vector-level + synchronization within Vector cores. + + :param mode: Synchronization scope. One of: + ``"all_cube"`` — sync all Cube cores; + ``"all_vector"`` — sync all Vector cores; + ``"all"`` — sync all Cube and Vector cores; + ``"all_sub_vector"`` — sync between Vector sub-blocks. + :type mode: str + :param event_id: Event marker ID in range [0, 15]. + :type event_id: int + """ + mode = _unwrap_if_constexpr(mode) + event_id = _unwrap_if_constexpr(event_id) + assert isinstance(mode, str), f"mode: {mode} is not string" + assert isinstance(event_id, int) and (event_id >= 0) and (event_id < 16), f"event_id: {event_id} should be 0 ~ 15" + assert mode in ("all_cube", "all_vector", "all", + "all_sub_vector"), f"ERROR: mode = {mode}, only supports all_cube/all_vector/all/all_sub_vector" + _semantic.builder.sync_block_all(mode, event_id) + + +class FixpipeDMAMode(enum.Enum): + NZ2DN = ascend_ir.FixpipeDMAMode.NZ2DN + NZ2ND = ascend_ir.FixpipeDMAMode.NZ2ND + NZ2NZ = ascend_ir.FixpipeDMAMode.NZ2NZ + + +class FixpipeDualDstMode(enum.Enum): + NO_DUAL = ascend_ir.FixpipeDualDstMode.NO_DUAL + COLUMN_SPLIT = ascend_ir.FixpipeDualDstMode.COLUMN_SPLIT + ROW_SPLIT = ascend_ir.FixpipeDualDstMode.ROW_SPLIT + + +class FixpipePreQuantMode(enum.Enum): + NO_QUANT = ascend_ir.FixpipePreQuantMode.NO_QUANT + F322BF16 = ascend_ir.FixpipePreQuantMode.F322BF16 + F322F16 = ascend_ir.FixpipePreQuantMode.F322F16 + S322I8 = ascend_ir.FixpipePreQuantMode.S322I8 + + +class FixpipePreReluMode(enum.Enum): + LEAKY_RELU = ascend_ir.FixpipePreReluMode.LEAKY_RELU + NO_RELU = ascend_ir.FixpipePreReluMode.NO_RELU + NORMAL_RELU = ascend_ir.FixpipePreReluMode.NORMAL_RELU + P_RELU = ascend_ir.FixpipePreReluMode.P_RELU + + +@builtin +def fixpipe( + src: tl.tensor, + dst: Union[None, bl.buffer] = None, + dma_mode: FixpipeDMAMode = FixpipeDMAMode.NZ2ND, + dual_dst_mode: FixpipeDualDstMode = FixpipeDualDstMode.NO_DUAL, + _semantic=None, +) -> Union[None, tl.tensor]: + """ + Directly store a tensor on L0C to a local buffer via fixpipe. + Fixpipe is pipeline that performing data movement from L0C to other memory hierarchies. + Currently support: + - L0C to UB (for Ascend910_95 sereies) + + :param src: the source tensor, Must be located in the l0C memory region. + :type src: tl.tensor + :param dst: The destination buffer in UB. If None, an empty tensor will be created. + Must be buffer type in UB if provided. + :type dst: bl.buffer | None + :param dma_mode: DMA transfer mode, "nz2nd" enables NZ to ND layout transformation + :type dma_mode: FixpipeDMAMode + :param dual_dst_mode: Dual destination mode for split operations + :type dual_dst_mode: FixpipeDualDstMode + :return: If dst is None, returns the created tensor; otherwise returns None. + :rtype: tl.tensor | None + """ + if not _semantic.builder.is_910_95(): + raise RuntimeError("this feature is only supported on Ascend910_95") + if not isinstance(src, tl.tensor): + raise TypeError("src is not of tensor type") + + if dst is not None: + if not isinstance(dst, bl.buffer): + raise TypeError("dst must be buffer type or None") + if dst.space != ascend_address_space.UB: + raise TypeError("dst's AddressSpace must be UB") + if len(dst.shape) == 2 and (dst.type.element_ty == tl.float32 or dst.type.element_ty == tl.int32): + N = dst.shape[1] + if N % 8 != 0: + raise ValueError("32b Fixpipe last dim must be aligned to 8") + if (dma_mode != FixpipeDMAMode.NZ2ND) and (N % 16 != 0): + raise ValueError("32b non-NZ2ND Fixpipe last dim must be aligned to 16") + if (dual_dst_mode == FixpipeDualDstMode.COLUMN_SPLIT) and (N % 32 != 0): + raise ValueError("32b Column split dual Fixpipe last dim must be aligned to 32") + M = dst.shape[0] + if (dma_mode == FixpipeDMAMode.NZ2DN) and (M % 8 != 0): + raise ValueError("32b NZ2DN Fixpipe first dim must be aligned to 8") + dst16bits = (dst.type.element_ty == tl.float16 or dst.type.element_ty == tl.int16 + or dst.type.element_ty == tl.bfloat16) + if len(dst.shape) == 2 and dst16bits: + N = dst.shape[1] + if N % 16 != 0: + raise ValueError("16b Fixpipe last dim must be aligned to 16") + M = dst.shape[0] + if (dma_mode == FixpipeDMAMode.NZ2DN) and (M % 16 != 0): + raise ValueError("16b NZ2DN Fixpipe first dim must be aligned to 16") + + return semantic.fixpipe( + src, + dst, + dma_mode, + dual_dst_mode, + FixpipePreQuantMode.NO_QUANT, + FixpipePreReluMode.NO_RELU, + _semantic, + ) + + +class SYNC_IN_VF(enum.Enum): + """Synchronization barrier modes for fine-grained vector/scalar instruction ordering. + + Each value specifies which instruction types are blocked until previous + instructions complete. The name follows the pattern ``{blocked}_{awaited}``: + e.g., ``VST_VLD`` blocks vector stores (VST) until vector loads (VLD) complete. + + Intended for use within an :func:`scope` context. + """ + + VV_ALL = enum.auto() + VST_VLD = enum.auto() + VLD_VST = enum.auto() + VST_VST = enum.auto() + VS_ALL = enum.auto() + VST_LD = enum.auto() + VLD_ST = enum.auto() + VST_ST = enum.auto() + SV_ALL = enum.auto() + ST_VLD = enum.auto() + LD_VST = enum.auto() + ST_VST = enum.auto() + + +@builtin +def debug_barrier( + sync_mode: SYNC_IN_VF, + _semantic=None, +) -> None: + """Inserts a synchronization barrier between vector/scalar load/store instructions. + + Provides fine-grained control over which instruction types are blocked until + prior instructions complete. Intended for use within an :func:`scope` context. + + :param sync_mode: Barrier type specifying which instruction classes to synchronize. + :type sync_mode: SYNC_IN_VF + """ + return semantic.debug_barrier(sync_mode.name, _semantic) + + +@builtin +def sub_vec_num(_semantic=None) -> tl.constexpr: + """ + Get the Vector Core Num on one AI Core. + """ + npuUtils = NPUUtils() + cube_num = npuUtils.get_aivector_core_num() + vector_num = npuUtils.get_aicore_num() + const_val = cube_num // vector_num + return tl.constexpr(const_val) + + +@builtin +def conv1d(input: tl.tensor, weight: tl.tensor, bias: tl.tensor = None, stride=None, padding_size=None, dilation=None, + groups=None, _semantic=None) -> tl.tensor: + """ + Applies a 1D convolution over an input signal. + + :param input: Input tensor of shape (N, C_in, L_in) or (C_in, L_in). N is a batch size, C denotes a number of channels, L is a length of signal sequence. + :type input: tensor + :param weight: Weight tensor of shape (C_out, C_in // groups, kernel_size). + :type weight: tensor + :param bias: Bias tensor of shape (C_out) or None. Default: None. + :type bias: tensor or None + :param stride: The stride of the convolution kernel. Can be an int or a 1-element tuple. + :type stride: int or Tuple[int] + :param padding_size: Padding added to both sides of the input. Can be an int, a 1-element tuple, or a string. Can be a string {'valid', 'same'}, single number or a one-element tuple. + ``padding_size='valid'`` is the same as no padding. + ``padding_size='same'`` pads the input so the output has the same shape as the input. However, this mode doesn't support any stride values other than 1. + :type padding_size: int, Tuple[int], or str + :param dilation: The spacing between kernel elements. Can be an int or a 1-element tuple. + :type dilation: int or Tuple[int] + :param groups: Number of blocked connections from input to output channels. + :type groups: int + + **Example:** + + .. code-block:: python + + @triton.jit + def conv_kernel(input_ptr, weight_ptr, bias_ptr, output_ptr, N, C, L, K, BLOCK_SIZE: tl.constexpr): + # Load a tile of input and weight + input_block = tl.load(input_ptr + ...) + weight_block = tl.load(weight_ptr + ...) + + # Perform 1D convolution + # Using default stride=1, padding_size=0, dilation=1, groups=1 + conv_output = al.conv1d( + input_block, + weight_block, + bias=None, + stride=1, + padding_size=0, + dilation=1, + groups=1, + ) + + # Store the result + tl.store(output_ptr + ..., conv_output) + + :return: The output tensor of shape (N, C_out, L_out). + :rtype: tensor + """ + + stride = _unwrap_if_constexpr(stride) + padding_size = _unwrap_if_constexpr(padding_size) + dilation = _unwrap_if_constexpr(dilation) + groups = _unwrap_if_constexpr(groups) + + # Set default value + stride = stride if stride is not None else 1 + padding_size = padding_size if padding_size is not None else 0 + dilation = dilation if dilation is not None else 1 + groups = groups if groups is not None else 1 + + if type(bias).__name__ == 'constexpr': + bias = getattr(bias, 'value', bias) + if bias is not None: + assert len(bias.shape) == 1, f"bias must be a 1D tensor (C_out), got {len(bias.shape)}D" + assert isinstance(groups, int), f"groups must be an integer, got {groups}" + + def _check_and_normalize_1d_param(param, name): + if param is None: + return None + if isinstance(param, (list, tuple)): + assert len(param) == 1, f"{name} must be an integer or a 1-element tuple, got {param}" + return param[0] + assert isinstance(param, int), f"{name} must be an integer or a 1-element tuple, got {type(param)}" + return param + + stride = _check_and_normalize_1d_param(stride, 'stride') + dilation = _check_and_normalize_1d_param(dilation, 'dilation') + + is_batched = len(input.shape) == 3 + L_in = input.shape[-1] + K = weight.shape[2] + + if isinstance(padding_size, str): + assert padding_size in ['same', 'valid'], f"padding_size string must be 'same' or 'valid', got '{padding_size}'" + else: + padding_size = _check_and_normalize_1d_param(padding_size, 'padding_size') + if isinstance(padding_size, str): + if padding_size == 'valid': + padding_size_int = 0 + elif padding_size == 'same': + #The case where padding_needed is an odd number needs to be handled. + if stride != 1: + raise ValueError("padding_size='same' is only supported when stride=1") + padding_needed = (L_in - 1) * stride + dilation * (K - 1) + 1 - L_in + padding_size_int = padding_needed // 2 + else: + padding_size_int = padding_size if padding_size is not None else 0 + + assert len(input.shape) in [2, 3], f"input must be a 2D (C, L) or 3D (N, C, L) tensor, got {len(input.shape)}D" + assert len( + weight.shape) == 3, f"weight must be a 3D tensor (C_out, C_in // groups, kernel_size), got {len(weight.shape)}D" + + # Create output type + C_in = input.shape[-2] if is_batched else input.shape[0] + C_out = weight.shape[0] + + L_in_val = _unwrap_if_constexpr(input.shape[-1]) + K_val = _unwrap_if_constexpr(weight.shape[2]) + + calculation_result = (L_in_val + 2 * padding_size_int - dilation * (K_val - 1) - 1) / stride + 1 + L_out_val = -int(-calculation_result) + if is_batched: + output_shape = [input.shape[0], C_out, L_out_val] + else: + output_shape = [C_out, L_out_val] + + return semantic.conv1d(input, weight, bias, stride, padding_size_int, dilation, groups, output_shape, _semantic) + + +def _dot_to_nd_shape(shape, fractal, is_lhs): + """A fractal operand's ND shape; lhs -> [M,K], rhs -> [K,N]. 2D is already ND. + + Both operands use the zN fractal (non-transpose): A [M,K] <-> [K1,M1,16,b] + and B [K,N] <-> [N1,K1,16,b]. In each the two logical dims are + [d1*d2, d0*d3] (block rows on d2, block cols on d3).""" + if not (fractal and len(shape) == 4): + return [shape[0], shape[1]] + return [shape[1] * shape[2], shape[0] * shape[3]] + + +def _check_dot_fractal_block(name, shape, is_lhs, dtype): + """Validate an nZ operand's trailing block [.., block_row, block_col]. + + block_col must equal 32 / elem_bytes. block_row must be 16, except for int8 + where the cube fractal is per-operand: a non-transpose left operand is 16 + and a non-transpose right operand is 32. + """ + elem_bytes = dtype.primitive_bitwidth // 8 + expected_col = 32 // elem_bytes + block_row = _unwrap_if_constexpr(shape[-2]) + block_col = _unwrap_if_constexpr(shape[-1]) + + if block_col != expected_col: + raise ValueError(f"dot nZ operand `{name}`: block col dim is {block_col}, expected " + f"{expected_col} (= 32 / {elem_bytes} bytes per {dtype} element)") + + if elem_bytes != 1: + if block_row != 16: + raise ValueError(f"dot nZ operand `{name}`: block row dim is {block_row}, expected 16") + return + + if block_row not in (16, 32): + raise ValueError(f"dot int8 nZ operand `{name}`: block row dim is {block_row}, expected 16 or 32") + if not is_lhs and block_row == 16: + raise ValueError(f"dot int8 right operand `{name}`: block row dim is 16, but a non-transpose " + f"`{name}` needs 32 (block [32,32], i.e. zN[N/32, K/32, 32, 32]); a [16,32] " + f"block forces an unsupported layout conversion in the backend") + + +def _dot_operand_is_fractal(name, fmt): + """Resolve operand ``name``'s layout from its ``format_*`` string: + + ``"fractal"`` -> fractal (zN) + ``"nd"`` / ``""`` / unset -> non-fractal (ND) + anything else -> ValueError + """ + fmt = _unwrap_if_constexpr(fmt) + if fmt is None or fmt == "" or fmt == "nd": + return False + if fmt == "fractal": + return True + raise ValueError(f"dot `{name}`: format must be 'fractal', 'nd', or '' " + f"(empty/unset = non-fractal ND); got {fmt!r}") + + +@builtin +def dot(a: tl.tensor, b: tl.tensor, format_a="", format_b="", format_c="", _semantic=None) -> tl.tensor: + """ + Matrix multiply ``D = A * B`` with per-operand layout format. + + ``format_a`` / ``format_b`` / ``format_c`` select each operand's layout: + - ``"fractal"``: zN fractal (4D) + - ``"nd"`` / ``""`` / unset: ND (non-fractal, 2D) + - any other value raises ``ValueError``. + + zN convention (non-transpose, block [16, b], b = 32 / elem_bytes): + A [M,K] <-> [K1, M1, 16, b] (ND = [d1*d2, d0*d3]) + B [K,N] <-> [N1, K1, 16, b] (ND = [d1*d2, d0*d3]) + C [M,N] <-> [N1, M1, 16, 16] (L0C accumulator, block 16x16) + + :param a: left matrix A (2D ND, or 4D fractal when ``format_a="fractal"``). + :param b: right matrix B (2D ND, or 4D fractal when ``format_b="fractal"``). + :param format_a: layout of A: "fractal" | "nd" | "" (default ND). + :param format_b: layout of B: "fractal" | "nd" | "" (default ND). + :param format_c: layout of D: "fractal" | "nd" | "" (default ND). + + :return: D = A * B (fractal 4D if ``format_c="fractal"`` else 2D ND). + :rtype: tensor + """ + fractal_a = _dot_operand_is_fractal("a", format_a) + fractal_b = _dot_operand_is_fractal("b", format_b) + fractal_c = _dot_operand_is_fractal("c", format_c) + + for name, operand, is_frac, is_lhs in (("a", a, fractal_a, True), ("b", b, fractal_b, False)): + if not isinstance(operand, tl.tensor): + raise TypeError(f"dot operand `{name}` must be a tensor, got {type(operand)}") + if len(operand.shape) not in (2, 4): + raise ValueError(f"dot operand `{name}` must be 2D (ND) or 4D (fractal zN), " + f"got rank {len(operand.shape)}") + if is_frac and len(operand.shape) == 4: + _check_dot_fractal_block(name, operand.shape, is_lhs, operand.dtype) + + # cube mmad requires both inputs share the element dtype (the output takes + # the accumulator dtype separately, see semantic.dot). + if a.dtype != b.dtype: + raise TypeError(f"dot operands must have the same dtype, got {a.dtype} and {b.dtype}") + + a_nd = _dot_to_nd_shape([_unwrap_if_constexpr(s) for s in a.shape], fractal_a, True) + b_nd = _dot_to_nd_shape([_unwrap_if_constexpr(s) for s in b.shape], fractal_b, False) + m, n = a_nd[0], b_nd[1] + # The result carries the cube accumulator dtype (f32/i32, see semantic.dot); + # fractal_c is the L0C accumulator fractal, whose block is 16x16. + output_shape = [n // 16, m // 16, 16, 16] if fractal_c else [m, n] + + return semantic.dot(a, b, fractal_a, fractal_b, fractal_c, output_shape, _semantic=_semantic) + + +@builtin +def conv2d(input: tl.tensor, weight: tl.tensor, bias: tl.tensor = None, stride=None, padding=None, dilation=None, + groups=None, _semantic=None) -> tl.tensor: + """ + Applies a 2D convolution over an input signal. + + :param input: Input tensor of shape (N, C_in, H, W) or (C_in, H, W). + :type input: tensor + :param weight: Weight tensor of shape (C_out, C_in // groups, kH, kW). + :type weight: tensor + :param bias: Bias tensor of shape (C_out) or None. Default: None. + :type bias: tensor or None + :param stride: The stride of the convolution kernel. Can be an int or a 2-element tuple. + :type stride: int or Tuple[int, int] + :param padding: Padding added to the input. Can be an int (symmetric on all + sides), a 2-element tuple (pad_h, pad_w) (symmetric per dimension), + a 4-element tuple (pad_top, pad_bottom, pad_left, pad_right) + (asymmetric), or a string. + ``padding='valid'`` is the same as no padding. + ``padding='same'`` pads the input so the output has the same shape as the input. However, this mode doesn't support any stride values other than 1. + :type padding: int, Tuple[int, int], Tuple[int, int, int, int], or str + :param dilation: The spacing between kernel elements. Can be an int or a 2-element tuple. + :type dilation: int or Tuple[int, int] + :param groups: Number of blocked connections from input to output channels. + :type groups: int + + **Example:** + + .. code-block:: python + + @triton.jit + def conv2d_kernel(input_ptr, weight_ptr, output_ptr, C, H, W, C_out, kH, kW): + input_tile = tl.load(input_ptr + ...) # shape (C, H, W) + weight_tile = tl.load(weight_ptr + ...) # shape (C_out, C, kH, kW) + output = al.conv2d(input_tile, weight_tile, + stride=(1, 1), padding=(0, 0), dilation=(1, 1)) + tl.store(output_ptr + ..., output) + + :return: The output tensor of shape (N, C_out, H_out, W_out). + :rtype: tensor + """ + + stride = _unwrap_if_constexpr(stride) + padding = _unwrap_if_constexpr(padding) + dilation = _unwrap_if_constexpr(dilation) + groups = _unwrap_if_constexpr(groups) + + # Set default values + stride = stride if stride is not None else 1 + padding = padding if padding is not None else 0 + dilation = dilation if dilation is not None else 1 + groups = groups if groups is not None else 1 + + if type(bias).__name__ == 'constexpr': + bias = getattr(bias, 'value', bias) + if bias is not None: + assert len(bias.shape) == 1, f"bias must be a 1D tensor (C_out), got {len(bias.shape)}D" + assert isinstance(groups, int), f"groups must be an integer, got {groups}" + + def _check_and_normalize_2d_param(param, name): + """Keep ints as ints and tuples as tuples; lists are accepted and + converted to tuples.""" + if param is None: + return None + if isinstance(param, list): + assert len(param) == 2, f"{name} must be an integer or a 2-element sequence, got {param}" + return tuple(param) + if isinstance(param, (tuple, tl.tuple)): + assert len(param) == 2, f"{name} must be an integer or a 2-element sequence, got {param}" + return param + assert isinstance(param, int), f"{name} must be an integer or a 2-element sequence, got {type(param)}" + return param + + stride = _check_and_normalize_2d_param(stride, 'stride') + dilation = _check_and_normalize_2d_param(dilation, 'dilation') + + # Per-dimension accessors accepting either the int or tuple form. + stride_h, stride_w = (stride, stride) if isinstance(stride, int) else stride + dilation_h, dilation_w = (dilation, dilation) if isinstance(dilation, int) else dilation + + is_batched = len(input.shape) == 4 + H_in = input.shape[-2] + W_in = input.shape[-1] + kH = weight.shape[2] + kW = weight.shape[3] + + def _check_and_normalize_padding(param): + """Keep ints as ints and tuples as tuples; lists are accepted and + converted to tuples. + + Accepts an int (symmetric on all sides), a 2-element sequence + [pad_h, pad_w] (symmetric per dimension), or a 4-element sequence + [pad_top, pad_bottom, pad_left, pad_right] (fully asymmetric). + """ + if param is None: + return 0 + if isinstance(param, list): + assert len(param) in [2, 4], \ + f"padding must be an int, a 2-element or 4-element sequence, got {param}" + return tuple(param) + if isinstance(param, (tuple, tl.tuple)): + assert len(param) in [2, 4], \ + f"padding must be an int, a 2-element or 4-element sequence, got {param}" + return param + assert isinstance(param, int), \ + f"padding must be an int, a 2-element or 4-element sequence, got {type(param)}" + return param + + if isinstance(padding, str): + assert padding in ['same', 'valid'], f"padding string must be 'same' or 'valid', got '{padding}'" + if padding == 'valid': + padding_int = 0 + elif padding == 'same': + if stride_h != 1 or stride_w != 1: + raise ValueError("padding='same' is only supported when stride=1") + # Total padding needed per dimension; extra element goes to bottom/right + pad_h_total = (H_in - 1) * stride_h + dilation_h * (kH - 1) + 1 - H_in + pad_w_total = (W_in - 1) * stride_w + dilation_w * (kW - 1) + 1 - W_in + padding_int = (pad_h_total // 2, pad_h_total - pad_h_total // 2, pad_w_total // 2, + pad_w_total - pad_w_total // 2) + else: + padding_int = _check_and_normalize_padding(padding) + + assert len(input.shape) in [3, 4], f"input must be 3D (C,H,W) or 4D (N,C,H,W), got {len(input.shape)}D" + assert len(weight.shape) == 4, f"weight must be 4D (C_out, C_in/groups, kH, kW), got {len(weight.shape)}D" + + C_in = input.shape[-3] if is_batched else input.shape[0] + C_out = weight.shape[0] + + H_in_val = _unwrap_if_constexpr(input.shape[-2]) + W_in_val = _unwrap_if_constexpr(input.shape[-1]) + kH_val = _unwrap_if_constexpr(weight.shape[2]) + kW_val = _unwrap_if_constexpr(weight.shape[3]) + + def _compute_output(in_size, pad_before, pad_after, dil, k, strd): + return -int(-((in_size + pad_before + pad_after - dil * (k - 1) - 1) / strd + 1)) + + # Expand padding to [pad_top, pad_bottom, pad_left, pad_right]. + if isinstance(padding_int, int): + pad_top = pad_bottom = pad_left = pad_right = padding_int + elif len(padding_int) == 2: + pad_h, pad_w = padding_int + pad_top = pad_bottom = pad_h + pad_left = pad_right = pad_w + else: + pad_top, pad_bottom, pad_left, pad_right = padding_int + + H_out_val = _compute_output(H_in_val, pad_top, pad_bottom, dilation_h, kH_val, stride_h) + W_out_val = _compute_output(W_in_val, pad_left, pad_right, dilation_w, kW_val, stride_w) + + if is_batched: + output_shape = [input.shape[0], C_out, H_out_val, W_out_val] + else: + output_shape = [C_out, H_out_val, W_out_val] + + return semantic.conv2d(input, weight, bias, stride, padding_int, dilation, groups, output_shape, _semantic) diff --git a/third_party/ascend/language/cann/extension/custom_op.py b/third_party/ascend/language/cann/extension/custom_op.py new file mode 100644 index 0000000000..00576d53bb --- /dev/null +++ b/third_party/ascend/language/cann/extension/custom_op.py @@ -0,0 +1,489 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +__all__ = ["custom", "custom_semantic", "register_custom_op", "SyncEventSlot"] + +import inspect +from warnings import warn +import types +import typing +import itertools +import triton.language.core as tl +from . import core + +# Registry for custom op, mapping name to its configuration. +_custom_op_registry = {} + + +class SyncEventSlot: + """One sync_event_slots entry for custom macro ops.""" + + def __init__(self, set_pipe=None, wait_pipe=None, sync=None, event=None): + self.set_pipe = set_pipe + self.wait_pipe = wait_pipe + self.sync = sync if sync is not None else core.SYNC_HINT.WAIT + self.event = event + + +def _validate_pipe(pipe): + if isinstance(pipe, core.PIPE): + return + if isinstance(pipe, (list, tuple)) and len(pipe) == 2: + assert all(isinstance(p, core.PIPE) for p in pipe), \ + "macro custom op pipe sequence must contain PIPE values" + return + assert False, ("Invalid 'pipe' field: single PIPE lowers to hivm.hir.custom; " + "a sequence of two PIPE values lowers to hivm.hir.custom_macro") + + +def _is_macro_pipe(pipe): + return isinstance(pipe, (list, tuple)) and len(pipe) == 2 + + +def _sync_hint_value(sync): + if isinstance(sync, core.SYNC_HINT): + return sync.value + if isinstance(sync, str): + return getattr(core.SYNC_HINT, sync.upper()).value + return sync + + +def _event_id_value(event): + if isinstance(event, core.EVENT_ID): + return event.value + if isinstance(event, str): + return getattr(core.EVENT_ID, event).value + return event + + +def _build_sync_event_slot_attr(slot, builder): + if isinstance(slot, SyncEventSlot): + set_pipe = slot.set_pipe.value if slot.set_pipe is not None else None + wait_pipe = slot.wait_pipe.value if slot.wait_pipe is not None else None + macro_sync = _sync_hint_value(slot.sync) + event = _event_id_value(slot.event) if slot.event is not None else None + elif isinstance(slot, (list, tuple)): + if len(slot) == 1 and slot[0] == 'internal': + set_pipe = wait_pipe = event = None + macro_sync = core.SYNC_HINT.INTERNAL.value + elif len(slot) >= 2: + set_pipe = slot[0].value + wait_pipe = slot[1].value + macro_sync = _sync_hint_value(slot[2]) if len(slot) > 2 else core.SYNC_HINT.WAIT.value + event = _event_id_value(slot[3]) if len(slot) > 3 else None + else: + assert False, "invalid sync_event_slots entry" + else: + assert False, "sync_event_slots entries must be SyncEventSlot or tuple" + + return builder.get_sync_event_slot_attr( + set_pipe, + wait_pipe, + macro_sync, + event, + ) + + +def _get_op_class(name): + # Try to get op class in _custom_op_registry. + op_class = _custom_op_registry.get(name) + if op_class is None: + # Allow bulitin custom ops used without registry. + assert name.startswith('__builtin_'), f"Custom Op '{name}' not registered." + # Return a dummy op class for builtin custom op. + op_class = type( + "_builtin_custom_op", (object, ), { + "name": name, + "core": core.CORE.VECTOR, + "pipe": core.PIPE.PIPE_V, + "mode": core.MODE.SIMT, + "signature": inspect.signature(object), + }) + return op_class + + +def _unwrap_constexpr(arg): + if isinstance(arg, tl.constexpr): + return arg.value + if isinstance(arg, (tuple, tl.tuple)): + return tuple(_unwrap_constexpr(x) for x in arg) + if isinstance(arg, list): + return [_unwrap_constexpr(x) for x in arg] + if isinstance(arg, dict): + return {k: _unwrap_constexpr(v) for k, v in arg.items()} + return arg + + +def _to_value(value, _semantic=None, ty=None): + # Try to use 'type' attribute if ty not set. + ty = getattr(value, 'type', ty) if ty is None else ty + if isinstance(value, tl.tensor): + if not value.type.is_block() and isinstance(ty, tl.dtype) and value.type != ty: + # For a scalar variable, if its type is not the expected one + # that specified by type hint 'ty', insert a cast for it. + return _semantic.cast(value, ty).handle + return value.handle + if isinstance(value, bool): + return _semantic.builder.get_int1(value) + if isinstance(value, int): + if isinstance(ty, tl.dtype): + if ty.is_int64(): + return _semantic.builder.get_int64(value) + if ty.is_uint64(): + return _semantic.builder.get_uint64(value) + if ty.is_int32(): + return _semantic.builder.get_int32(value) + if ty.is_uint32(): + return _semantic.builder.get_uint32(value) + if ty.is_int16(): + return _semantic.builder.get_int16(value) + if ty.is_uint16(): + return _semantic.builder.get_uint16(value) + if ty.is_int8(): + return _semantic.builder.get_int8(value) + if ty.is_uint8(): + return _semantic.builder.get_uint8(value) + # default int32 + return _semantic.builder.get_int32(value) + if isinstance(value, float): + if isinstance(ty, tl.dtype): + if ty.is_fp64(): + return _semantic.builder.get_fp64(value) + if ty.is_fp32(): + return _semantic.builder.get_fp32(value) + if ty.is_fp16(): + return _semantic.builder.get_fp16(value) + if ty.is_bf16(): + return _semantic.builder.get_bf16(value) + # default float32 + return _semantic.builder.get_fp32(value) + if isinstance(value, tl.constexpr): + return _to_value(value.value, _semantic) + raise TypeError(f"Unsupported argument type {value} : {type(value)}") + + +def _to_operands(args, _semantic=None): + operands = [] + for value in args: + if value is None: + continue + if isinstance(value, (list, tuple, tl.tuple)): + for item in value: + operands.append(_to_value(item, _semantic)) + else: + operands.append(_to_value(value, _semantic)) + return operands + + +def _get_element_type(ty): + if isinstance(ty, types.GenericAlias): + return typing.get_args(ty)[0] + return ty + + +def _args_to_operands(op, _semantic, args, kwargs): + if not op.signature.parameters: + # Without parameters in signature, use the actual parameter order. + return _to_operands(itertools.chain(args, kwargs.values()), _semantic) + + # Convert arguments to operands according the signature. + operands = [] + bind = op.signature.bind(*args, **kwargs) + for param in op.signature.parameters.values(): + value = bind.arguments.get(param.name) + if value is None: + continue + ty = op.arg_type.get(param.name, param.annotation) + if isinstance(value, (list, tuple, tl.tuple)): + ty = _get_element_type(ty) + for item in value: + operands.append(_to_value(item, _semantic, ty)) + else: + operands.append(_to_value(value, _semantic, ty)) + return operands + + +def _bind_op_arguments(op, args, kwargs): + if not op.signature.parameters: + return None + return op.signature.bind(*args, **kwargs) + + +def _make_align_dim_attrs(op, builder, arg_attrs): + # Find op argument by name using op.align_dim's key + # We want to return a dict mapping for each align_dim key -> int attribute for the actual bound argument value. + name = 'align_dim' + if not hasattr(op, name): + return + + # To find argument indices matching each align_dim key, check the op.signature parameters + # and map align_dim key (argument name) to its index position. + align_arg_indices = {} + if hasattr(op, "signature"): + param_names = list(op.signature.parameters.keys()) + for arg_name in op.align_dim.keys(): + if arg_name in param_names: + align_arg_indices[arg_name] = param_names.index(arg_name) + + for arg, align_val in op.align_dim.items(): + if isinstance(arg, str) and arg in align_arg_indices: + arg_attrs[align_arg_indices[arg]] = {name: builder.get_int_attr(align_val)} + print(arg_attrs[align_arg_indices[arg]]) + elif isinstance(arg, int): + arg_attrs[arg] = {name: builder.get_int_attr(align_val)} + print(arg_attrs[arg]) + else: + assert False, f"{name}'s keys should be string or int" + + +def _make_arg_attrs(op, builder): + num_args = len(op.signature.parameters) if hasattr(op, "signature") else 0 + arg_attrs = [{} for _ in range(num_args)] + + _make_align_dim_attrs(op, builder, arg_attrs) + return arg_attrs + + +def _add_optional_attr(op, name, builder, attrs): + if hasattr(op, name): + attrs[name] = builder.get_string_attr(getattr(op, name)) + + +def _add_bitcode_attr(op, builder, attrs): + name = 'bitcode' + if not hasattr(op, name): + return + + from pathlib import Path + bitcode = Path(getattr(op, name)) + assert bitcode.exists(), f"Provided bitcode ({name}) not exist" + attrs[name] = builder.get_string_attr(str(bitcode.absolute())) + + +def _add_optional_extra_buffer_attr(op, builder, attrs): + name = 'extra_buffers' + if not hasattr(op, name): + return + + extra_buffers = getattr(op, name) + if isinstance(extra_buffers, tuple): + extra_buffers = [extra_buffers] + + extra_buffer_types, extra_buffer_sizes = zip(*extra_buffers) + attrs[name + "_types"] = builder.get_type_array_attr([ty.to_ir(builder) for ty in extra_buffer_types]) + attrs[name + "_sizes"] = builder.get_i64_array_attr(list(extra_buffer_sizes)) + + +def _add_optional_indexing_map_attr(op, builder, attrs): + # Optional indexing map attribute: + # `indexing_map` should be an iterable of al.affine_map (MLIR AffineMap) objects. + name = 'indexing_map' + if not hasattr(op, name): + return + + indexing_map = getattr(op, name) + attrs[name] = builder.get_affine_map_array_attr(indexing_map) + + +def _add_optional_iterator_types_attr(op, builder, attrs): + name = 'iterator_types' + if not hasattr(op, name): + return + + attrs[name] = builder.get_iterator_types_attr([iterator_type.value for iterator_type in getattr(op, name)]) + + +def _add_sync_event_slots_attr(op, builder, attrs): + if not hasattr(op, 'sync_event_slots'): + return + slots = getattr(op, 'sync_event_slots') + if isinstance(slots, tuple): + slots = list(slots) + attrs['sync_event_slots'] = builder.get_array_attr([_build_sync_event_slot_attr(slot, builder) for slot in slots]) + + +def _make_attrs(op, builder, is_macro): + attrs = { + 'hivm.tcore_type': builder.get_core_type_attr(op.core.value), + } + # VF mode is only required for non-CUBE custom ops (see HIVM requiresVFMode()). + if op.core != core.CORE.CUBE: + attrs['hivm.vf_mode'] = builder.get_vf_mode_attr(op.mode.value) + if is_macro: + pipe_in, pipe_out = op.pipe + attrs['hivm.pipe_in'] = builder.get_pipe_attr(pipe_in.value) + attrs['hivm.pipe_out'] = builder.get_pipe_attr(pipe_out.value) + _add_sync_event_slots_attr(op, builder, attrs) + else: + attrs['hivm.pipe'] = builder.get_pipe_attr(op.pipe.value) + + if not op.name.startswith('__builtin_'): + assert hasattr(op, 'symbol'), f"Non builtin custom op, symbol is required." + assert hasattr(op, 'bitcode'), f"Non builtin custom op, bitcode path is required." + + # Add bit code path attribute, formalize to abosulte path. + _add_bitcode_attr(op, builder, attrs) + + _add_optional_indexing_map_attr(op, builder, attrs) + _add_optional_iterator_types_attr(op, builder, attrs) + + _add_optional_extra_buffer_attr(op, builder, attrs) + + _add_optional_attr(op, 'symbol', builder, attrs) + _add_optional_attr(op, 'source', builder, attrs) + _add_optional_attr(op, 'compile', builder, attrs) + # Extra attributes can be added here, such as op.extra_attr="attr_a=xx" + _add_optional_attr(op, 'extra_attr', builder, attrs) + + return attrs + + +def _to_result(res, res_types): + assert (len(res) == len(res_types)) + n_res = len(res) + if n_res == 0: + return None + if n_res == 1: + return tl.tensor(res[0], res_types[0]) + return tl.tuple(tl.tensor(res[i], res_types[i]) for i in range(n_res)) + + +def _init_op(op_class, *args, **kwargs): + op = op_class.__new__(op_class) + # Add arg_type dict to support dynamic argument type specifying. + setattr(op, 'arg_type', {}) + if op_class.signature.parameters: + # Init with arguments validate. + op_class.__init__(op, *args, **kwargs) + return op + + +def custom_semantic(name: str, *args, _semantic=None, **kwargs): + """Internal semantic handler for custom operations (used by code generator). + + User code should use :func:`custom` instead. + """ + name = _unwrap_constexpr(name) + # Get op class according the name. + op_class = _get_op_class(name) + deprecated_message = getattr(op_class, "_deprecated_message", None) + if deprecated_message: + warn(deprecated_message, FutureWarning, stacklevel=3) + # Convert constexpr to value in arguments. + args = _unwrap_constexpr(args) + kwargs = _unwrap_constexpr(kwargs) + kwargs.pop('sync_related_args', None) + # Create op instance from op class with the arguments. + op = _init_op(op_class, *args, **kwargs) + is_macro = _is_macro_pipe(op.pipe) + if hasattr(op, 'sync_event_slots'): + assert is_macro, "sync_event_slots is only supported for macro custom ops" + # Prepare inputs and outputs operands. + out = kwargs.pop('out', []) + outs = out if isinstance(out, (list, tuple, tl.tuple)) else [out] + outputs = _to_operands(outs, _semantic) + inputs = _args_to_operands(op, _semantic, args, kwargs) + main_builder = _semantic.builder + builder = getattr(main_builder, '_ascend_builder') + # Setup attributes. These only read context-level state (types, attrs), so + # the delegate builder is fine here. + attrs = _make_attrs(op, builder, is_macro) + arg_attrs = _make_arg_attrs(op, builder) + # Emit through the unified-builder wrappers so the delegate uses the main + # builder's current insertion point and location, not stale delegate state. + if is_macro: + res = main_builder.create_custom_macro_op(name, attrs, inputs, outputs, arg_attrs) + else: + res = main_builder.create_custom_op(name, attrs, inputs, outputs, arg_attrs) + # Results with same types as outputs. + res_types = [out.type for out in outs] + return _to_result(res, res_types) + + +@core.builtin +def custom(name: str, *args, _semantic=None, **kwargs): + """Invoke a custom operation with the given name and arguments.""" + return custom_semantic(name, *args, _semantic=_semantic, **kwargs) + + +def register_custom_op(op): + """Register a custom operation so that it can be invoked via :func:`custom`. + + Used as a class decorator. The decorated class must define a ``core`` field + (a :class:`CORE` value), a ``pipe`` field (a :class:`PIPE` value), and for + non-CUBE ops a ``mode`` field (a :class:`MODE` value). + + :param op: the custom op class to register. + """ + assert inspect.isclass(op), "@register_custom_op should decorate on a class." + # Use class name if name not set. + if not hasattr(op, 'name'): + setattr(op, 'name', op.__name__) + # The op name should not be used. + assert op.name not in _custom_op_registry, f"Custom op name '{op.name}' already used." + + # Check required core, pipe fields; mode is required for non-CUBE ops only. + assert hasattr(op, 'core'), "'core' field is required." + assert hasattr(op, 'pipe'), "'pipe' field is required." + assert isinstance(op.core, core.CORE), "Invalid 'core' field, CORE type is required." + _validate_pipe(op.pipe) + if op.core != core.CORE.CUBE: + assert hasattr(op, 'mode'), "'mode' field is required for non-CUBE custom ops." + assert isinstance(op.mode, core.MODE), "Invalid 'mode' field, MODE type is required." + if hasattr(op, 'sync_event_slots'): + assert _is_macro_pipe(op.pipe), "sync_event_slots requires a two-pipe macro custom op" + # Retrieve arguments signature from __init__ method and save it. + signature = inspect.signature(op) + setattr(op, 'signature', signature) + # Register the custom op configuration. + _custom_op_registry[op.name] = op + return op + + +_dtype_cname_dict = { + 'int1': 'bool', + 'int8': 'int8_t', + 'int16': 'int16_t', + 'int32': 'int32_t', + 'int64': 'int64_t', + 'uint8': 'uint8_t', + 'uint16': 'uint16_t', + 'uint32': 'uint32_t', + 'uint64': 'uint64_t', + 'fp16': 'half', + 'bf16': 'bfloat16_t', + 'fp32': 'float', + 'fp64': 'double', + 'fp8e5': 'float8_e5m2_t', + 'fp8e4nv': 'float8_e4m3_t', + # other float8 types are not supported yet, + # such as 'fp8e4b8', 'fp8e4b15', 'fp8e5b16'. +} + + +def _cname(self): + """Return the corresponding C name of the given tl.dtype""" + return _dtype_cname_dict.get(self.name, self.name) + + +# Add 'cname' property to tl.dtype class. +tl.dtype.cname = property(_cname, None) diff --git a/third_party/ascend/language/cann/extension/dispatch.py b/third_party/ascend/language/cann/extension/dispatch.py new file mode 100644 index 0000000000..d91abdee36 --- /dev/null +++ b/third_party/ascend/language/cann/extension/dispatch.py @@ -0,0 +1,33 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Dispatch table for Ascend-specific 'with' statement context managers. +""" + +from .scope import scope +from .code_generator import handle_scope_with, mangle_ty + +__all__ = ["ASCEND_WITH_DISPATCH"] + +# Registry of 'with' statement handlers for Ascend extension +ASCEND_WITH_DISPATCH = { + scope: handle_scope_with, + "mangle_ty": mangle_ty, +} diff --git a/third_party/ascend/language/cann/extension/math_ops.py b/third_party/ascend/language/cann/extension/math_ops.py new file mode 100644 index 0000000000..ebc9bdc22e --- /dev/null +++ b/third_party/ascend/language/cann/extension/math_ops.py @@ -0,0 +1,59 @@ +from math import pi as math_pi +from triton.language import core, math +from triton.language.core import float32, int1 +from ..libdevice import atan, isnan, isinf +from triton.runtime.jit import jit + +from ..utils import _deprecated + +pi = core.constexpr(math_pi) +half_pi = core.constexpr(0.5 * math_pi) + + +@_deprecated(replacement="triton.language.extra.cann.libdevice.atan2") +@core._tensor_member_fn +@jit +@math._add_math_2arg_docstr("atan2") +def atan2(y, x): + _is_int8_type_x: core.constexpr = x.dtype.is_int8() + core.static_assert(not _is_int8_type_x, f"Expected dtype fp16/fp32/bf16, but got int8 or int1") + _is_int8_type_y: core.constexpr = y.dtype.is_int8() + core.static_assert(not _is_int8_type_y, f"Expected dtype fp16/fp32/bf16, but got int8 or int1") + _is_floating_type_x: core.constexpr = x.dtype.is_floating() + core.static_assert(_is_floating_type_x == True, f"Expected dtype fp16/fp32/bf16, but got {core.constexpr(x.dtype)}") + _is_floating_type_y: core.constexpr = y.dtype.is_floating() + core.static_assert(_is_floating_type_y == True, f"Expected dtype fp16/fp32/bf16, but got {core.constexpr(y.dtype)}") + base = core.where(x == 0, 0.0, atan(y.to(core.dtype("fp32")) / x.to(core.dtype("fp32")))) + base = core.where((x == 0) & (y > 0), half_pi, base) + base = core.where((x == 0) & (y < 0), -half_pi, base) + + add_pi = core.where((x < 0) & (y >= 0), pi, 0.0) + sub_pi = core.where((x < 0) & (y < 0), -pi, 0.0) + return (base + add_pi + sub_pi).to(x.dtype) + + +@_deprecated(replacement="triton.language.extra.cann.libdevice.isfinited") +@core._tensor_member_fn +@jit +@math._add_math_1arg_docstr("isfinited") +def isfinited(x): + _is_int8_type: core.constexpr = x.dtype.is_int8() + core.static_assert(not _is_int8_type, f"Expected dtype fp16/fp32/bf16, but got int8 or int1") + _is_floating_type: core.constexpr = x.dtype.is_floating() + core.static_assert(_is_floating_type == True, f"Expected dtype fp16/fp32/bf16, but got {core.constexpr(x.dtype)}") + nan_mask = isnan(x) + inf_mask = isinf(x) + return (~nan_mask & ~inf_mask).to(int1) + + +@_deprecated(replacement="triton.language.extra.cann.libdevice.finitef") +@core._tensor_member_fn +@jit +@math._add_math_1arg_docstr("finitef") +def finitef(x): + _is_int8_type: core.constexpr = x.dtype.is_int8() + core.static_assert(not _is_int8_type, f"finitef only supports float32, but got int8 or int1") + core.static_assert(x.dtype == float32, f"finitef only supports float32, but got {core.constexpr(x.dtype)}") + nan_mask = isnan(x) + inf_mask = isinf(x) + return (~nan_mask & ~inf_mask).to(int1) diff --git a/third_party/ascend/language/cann/extension/mem_ops.py b/third_party/ascend/language/cann/extension/mem_ops.py new file mode 100644 index 0000000000..fe589cba82 --- /dev/null +++ b/third_party/ascend/language/cann/extension/mem_ops.py @@ -0,0 +1,423 @@ +import numbers +import triton.language as tl +from triton.language.core import ( + _unwrap_if_constexpr, + _tensor_member_fn, + _unwrap_iterable, + builtin, + constexpr, + dtype, + tensor, + check_bit_width, + _unwrap_if_constexpr, +) + +from typing import Optional, Tuple, List, overload, Union +from triton._C.libtriton import ir + +from ..utils import _deprecated +from ._utils import _convert_elem_to_ir_value + + +@_deprecated() +@_tensor_member_fn +@builtin +def index_put(ptr: tensor, index: tensor, value: tensor, dim: int, index_boundary: int, end_offset: tuple, + start_offset: tuple, dst_stride: tuple, _semantic=None): + """ + Index put values from a tensor into a destination tensor. + + Index put operation for different tensor ranks: + + 1. 2D index scatter (0 <= dim < 1): + 1.1 dim = 0 + out[index[i]][start_offset[1]:end_offset[1]] = value[i][0:end_offset[1]-start_offset[1]] + 2. 3D index scatter (0 <= dim < 2): + 2.1 dim = 0 + out[index[i]][start_offset[1]:end_offset[1]][start_offset[2]:end_offset[2]] + = value[i][0:end_offset[1]-start_offset[1]][0:end_offset[2]-start_offset[2]] + 2.2 dim = 1 + out[start_offset[0]:end_offset[0]][index[j]][start_offset[2]:end_offset[2]] + = value[0:end_offset[0]-start_offset[0]][j][0:end_offset[2]-start_offset[2]] + + :param ptr: The destination tensor pointer (in GM). + :type ptr: pointer type + :param index: An index to scatter (in UB). + :type index: tensor + :param value: A value to store (in UB). + :type value: tensor + :param dim: The dimension to scatter along. + :type dim: int + :param index_boundary: The upper boundary for index values (must be a compile-time constant). + :type index_boundary: int + :param end_offset: The offsets of each dimension for the end of the scatter region. + :type end_offset: tuple of int + :param start_offset: The offsets of each dimension for the start of the scatter region. + :type start_offset: tuple of int + :param dst_stride: The stride of each dimension of destination tensor. + :type dst_stride: tuple of int + """ + + def index_put_impl(ptr: tl.tensor, index: tl.tensor, value: tl.tensor, dim: int, index_boundary: int, + end_offset: Tuple, start_offset: Tuple, dst_stride: Tuple, _builder: ir.builder): + assert index.dtype.is_int(), "index must be an integer tensor" + if not ptr.dtype.element_ty.is_floating(): + raise ValueError(f"Expected dtype fp16/fp32/bf16, but got {ptr.dtype.element_ty}") + if not isinstance(dim, int): + raise ValueError("dim must be of type tl.constexpr") + + v_rank = len(value.shape) + idx_rank = len(index.shape) + if v_rank < 2 or v_rank > 5: + raise ValueError(f"value rank must be in [2, 5], got value rank={v_rank}") + if dim < 0 or dim >= v_rank - 1: + raise ValueError(f"dim must satisfy 0<=dim 5: + raise ValueError(f"index rank must be in [1, 5], got rank={idx_rank}") + if dim < 0 or dim >= idx_rank: + raise ValueError(f"dim must satisfy 0<=dim 5: + raise ValueError(f"index rank must be in [1, 5], got rank={idx_rank}") + if dim < 0 or dim >= idx_rank: + raise ValueError(f"dim must satisfy 0<=dim 0 + + dim = _unwrap_if_constexpr(dim) + index_boundary = _unwrap_if_constexpr(index_boundary) + value = _unwrap_if_constexpr(value) + + if not _is_ranked_tensor(value) or isinstance(value, constexpr): + element_ty = ptr.type.scalar.element_ty + value = _semantic.full(index.shape, value, element_ty) + return scatter_ub_to_out_impl(ptr, value, index, index_boundary, dim, dst_stride, end_offset, start_offset, + _semantic.builder) + + +@_tensor_member_fn +@builtin +def index_select_simd(src, dim, index, src_shape, src_offset, read_shape, _semantic=None) -> tensor: + """ + Parallel index_select operation from Global Memory to Unified Buffer (SIMD version). + + Selects data from multiple indices along a specified dimension and loads + them as tiles from GM directly to UB with zero-copy semantics. + + :param src: Source tensor pointer (in GM) + :type src: tensor (pointer type) + :param dim: The dimension along which to select indices + :type dim: int or constexpr + :param index: 1D tensor of indices to select (in UB) + :type index: tensor + :param src_shape: Complete shape of the source tensor (can be int or tensor) + :type src_shape: List[Union[int, tensor]] + :param src_offset: Starting offset for reading (can be int or tensor) + :type src_offset: List[Union[int, tensor]] + :param read_shape: Size to read (tile shape, can be int or tensor) + :type read_shape: List[Union[int, tensor]] + + **Constraints:** + + - ``read_shape[dim]`` must be ``-1`` + - ``src_offset[dim]`` can be ``-1`` (will be ignored) + - Boundary handling: ``src_offset + read_shape > src_shape`` automatically + truncates to ``src_shape`` boundary + - Does not check if ``index`` contains out-of-bounds values + + **Example:** + + .. code-block:: python + + @triton.jit + def kernel(src_ptr, output_ptr, indices_ptr, M, N, D, ...): + # Load indices (e.g., [5, 10, 15, 20]) + indices = tl.load(indices_ptr + tl.arange(0, 4)) + + # Example 1: Static shapes (constants) + # Index select from dimension 1 + # src: [8, 100, 256], index_select at dim=1 + # Read: [4, ?, 128] starting from [4, ?, 128] + result = extension.index_select_simd( + src_ptr, + dim=1, + index=indices, + src_shape=[8, 100, 256], + src_offset=[4, -1, 128], + read_shape=[4, -1, 128] + ) + # result shape: [4, 4, 128] + + # Example 2: Dynamic shapes (variables) + result2 = extension.index_select_simd( + src_ptr, + dim=1, + index=indices, + src_shape=[M, N, D], + src_offset=[4, -1, 128], + read_shape=[4, -1, 128] + ) + + tl.store(output_ptr + ..., result) + + :return: Result tensor in UB with shape where ``dim`` is replaced + by the length of ``index`` + :rtype: tensor + """ + + def index_select_simd_impl(src: tl.tensor, dim: int, index: tl.tensor, src_shape: List[Union[int, tl.tensor]], + src_offset: List[Union[int, tl.tensor]], read_shape: List[Union[int, tl.tensor]], + _builder: ir.builder) -> tl.tensor: + # Validate inputs + ndim = len(src_shape) + assert len(src_offset) == ndim, \ + f"src_offset length {len(src_offset)} must match src_shape length {ndim}" + assert len(read_shape) == ndim, \ + f"read_shape length {len(read_shape)} must match src_shape length {ndim}" + assert 0 <= dim < ndim, \ + f"dim={dim} must be in range [0, {ndim})" + assert len(index.shape) == 1, \ + f"index must be 1D tensor, got {len(index.shape)}D" + assert dim < ndim - 1, \ + f"index_select_simd cannot support trailing dimension as dim={dim}, ndim={ndim}" + # Handle both tensor and int offsets (for interpreter mode) + newsrc_shape = [] + for s in src_shape: + if isinstance(s, tensor): + newsrc_shape.append(s.handle) + elif isinstance(s, int): + # For interpreter mode: keep as int + newsrc_shape.append(s) + else: + newsrc_shape.append(s.handle if hasattr(s, 'handle') else s) + newsrc_offset = [] + for s in src_offset: + if isinstance(s, tensor): + newsrc_offset.append(s.handle) + elif isinstance(s, int): + # For interpreter mode: keep as int + newsrc_offset.append(s) + else: + newsrc_offset.append(s.handle if hasattr(s, 'handle') else s) + + # Match tl.load's ABI for bool values: bool is stored as an i8 byte. + element_ty = src.type.element_ty + is_bool = element_ty == tl.int1 + if is_bool: + src = _semantic.cast(src, tl.pointer_type(tl.int8, src.type.address_space)) + element_ty = tl.int8 + + # Create output type + return_shape = [index.shape[0] if i == dim else read_shape[i] for i in range(ndim)] + output_ty = tl.block_type(element_ty, return_shape) + out = _builder.create_index_select_simd(src.handle, index.handle, dim, newsrc_shape, newsrc_offset, read_shape, + return_shape) + if is_bool: + out.set_attr("was_bool_to_int8", _builder.get_bool_attr(True)) + + ret = tl.tensor(out, output_ty) + if is_bool: + ret.was_bool_to_int8 = True + return ret + + dim = _unwrap_if_constexpr(dim) + + # Process shape parameters: convert constexpr to values, keep tensors as-is + def process_param(val): + """Convert constexpr to value, keep tensor or int as-is""" + if isinstance(val, tensor): + return val + else: + return _unwrap_if_constexpr(val) + + newsrc_shape = [_semantic.to_tensor(o) if isinstance(o, constexpr) else o for o in src_shape] + newsrc_offset = [_semantic.to_tensor(o) if isinstance(o, constexpr) else o for o in src_offset] + assert len(index.shape) == 1, "index must be a 1D tensor" + + return index_select_simd_impl(src, dim, index, newsrc_shape, newsrc_offset, read_shape, _semantic.builder) diff --git a/third_party/ascend/language/cann/extension/scope.py b/third_party/ascend/language/cann/extension/scope.py new file mode 100644 index 0000000000..36ab82252f --- /dev/null +++ b/third_party/ascend/language/cann/extension/scope.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +__all__ = ["scope"] + +from triton.language.core import _unwrap_if_constexpr + + +class scope: + """ + Context manager for entering and exiting a scope, where operations within a scope shares some common characteristics. + + :param core_mode: Allows explicitly specifying which core type should be used + for operations within a code block, helping the compiler generate + appropriate code for cube or vector cores. Either ``"cube"`` or + ``"vector"``. + """ + + def __init__(self, core_mode: str, _builder=None, _semantic=None, **kwargs): + """ + :param core_mode: Either "cube" or "vector" to specify the core type + :param _builder: Internal builder object (set by code_generator) + :param _semantic: Internal semantic object (set by code_generator) + :param kwargs: Additional internal parameters + """ + # Convert constexpr to value if not being called from code generator + self.core_mode = _unwrap_if_constexpr(core_mode) if _builder is None else core_mode + self._builder = _builder + self._semantic = _semantic + + # Validate core_mode + if self.core_mode not in ("cube", "vector"): + raise ValueError(f'core_mode must be "cube" or "vector", got {self.core_mode}') + + def __enter__(self): + if self._builder is None: + raise RuntimeError("scope can only be used inside a Triton kernel") + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return False diff --git a/third_party/ascend/language/cann/extension/semantic.py b/third_party/ascend/language/cann/extension/semantic.py new file mode 100644 index 0000000000..6689e0a810 --- /dev/null +++ b/third_party/ascend/language/cann/extension/semantic.py @@ -0,0 +1,229 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +__all__ = [ + "fixpipe", + "create_address_space", +] + +import enum +from typing import (TypeVar, List, Union) + +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +import triton.language.core as tl +import triton.language.extra.cann.extension as al +import triton.extension.buffer.language as bl + +from .builder import create_builder_method_wrapper + +T = TypeVar('T') + + +def create_address_space(address_space: ascend_ir.AddressSpace, + builder: ascend_ir.ascendnpu_ir_builder) -> ir.attribute: + return builder.get_target_attribute(address_space) + + +class PIPE(enum.Enum): + PIPE_S = ascend_ir.PIPE.PIPE_S + PIPE_V = ascend_ir.PIPE.PIPE_V + PIPE_M = ascend_ir.PIPE.PIPE_M + PIPE_MTE1 = ascend_ir.PIPE.PIPE_MTE1 + PIPE_MTE2 = ascend_ir.PIPE.PIPE_MTE2 + PIPE_MTE3 = ascend_ir.PIPE.PIPE_MTE3 + PIPE_ALL = ascend_ir.PIPE.PIPE_ALL + PIPE_FIX = ascend_ir.PIPE.PIPE_FIX + + +class SYNC_HINT(enum.Enum): + WAIT = ascend_ir.SYNC_HINT.wait + SET = ascend_ir.SYNC_HINT.set + INTERNAL = ascend_ir.SYNC_HINT.internal + + +class EVENT_ID(enum.Enum): + EVENT_ID0 = ascend_ir.EVENT.EVENT_ID0 + EVENT_ID1 = ascend_ir.EVENT.EVENT_ID1 + EVENT_ID2 = ascend_ir.EVENT.EVENT_ID2 + EVENT_ID3 = ascend_ir.EVENT.EVENT_ID3 + EVENT_ID4 = ascend_ir.EVENT.EVENT_ID4 + EVENT_ID5 = ascend_ir.EVENT.EVENT_ID5 + EVENT_ID6 = ascend_ir.EVENT.EVENT_ID6 + EVENT_ID7 = ascend_ir.EVENT.EVENT_ID7 + + +def create_sync_block_set(sender, receiver, event_id, sender_pipe: PIPE, receiver_pipe: PIPE, _semantic=None): + if isinstance(event_id, int): + _semantic.builder.sync_block_set(sender, receiver, + _semantic.to_tensor(tl.constexpr(event_id)).handle, sender_pipe.value, + receiver_pipe.value) + elif isinstance(event_id, tl.constexpr): + _semantic.builder.sync_block_set(sender, receiver, + _semantic.to_tensor(event_id).handle, sender_pipe.value, receiver_pipe.value) + else: + _semantic.builder.sync_block_set(sender, receiver, event_id.handle, sender_pipe.value, receiver_pipe.value) + + +def create_sync_block_wait(sender, receiver, event_id, sender_pipe: PIPE, receiver_pipe: PIPE, _semantic=None): + if isinstance(event_id, int): + _semantic.builder.sync_block_wait(sender, receiver, + _semantic.to_tensor(tl.constexpr(event_id)).handle, sender_pipe.value, + receiver_pipe.value) + elif isinstance(event_id, tl.constexpr): + _semantic.builder.sync_block_wait(sender, receiver, + _semantic.to_tensor(event_id).handle, sender_pipe.value, receiver_pipe.value) + else: + _semantic.builder.sync_block_wait(sender, receiver, event_id.handle, sender_pipe.value, receiver_pipe.value) + + +def sub_vec_id(_semantic=None) -> tl.tensor: + return tl.tensor(_semantic.builder.create_get_sub_vec_id(), tl.int64) + + +def copy_from_ub_to_l1(src: Union[tl.tensor, bl.buffer], dst: Union[tl.tensor, bl.buffer], _semantic=None): + if not _semantic.builder.is_910_95(): + raise RuntimeError("this feature is only supported on Ascend910_95") + if isinstance(src, tl.tensor) or isinstance(dst, tl.tensor): + raise TypeError("tensor not support yet") + if src.shape != dst.shape: + raise TypeError("src and dst must have same shape") + if src.dtype != dst.dtype: + raise TypeError("src and dst need to have the same type") + if isinstance(src, bl.buffer) and isinstance(dst, bl.buffer): + if src.space != al.ascend_address_space.UB: + raise TypeError("src's AddressSpace must be UB") + if dst.space != al.ascend_address_space.L1: + raise TypeError("dst's AddressSpace must be L1") + _semantic.builder.create_copy_buffer(src.handle, dst.handle) + else: + raise TypeError("src and dst must be tl.tensor or bl.buffer") + + +def copy(src: Union[tl.tensor, bl.buffer], dst: Union[tl.tensor, bl.buffer], _semantic=None): + if not _semantic.builder.is_910_95(): + raise RuntimeError("this feature is only supported on Ascend910_95") + if isinstance(src, tl.tensor) or isinstance(dst, tl.tensor): + raise TypeError("tensor not support yet") + if src.shape != dst.shape: + raise TypeError("src and dst must have same shape") + if src.dtype != dst.dtype: + raise TypeError("src and dst need to have the same type") + if isinstance(src, bl.buffer) and isinstance(dst, bl.buffer): + if src.space != al.ascend_address_space.UB: + raise TypeError("src's AddressSpace must be UB") + if dst.space not in (al.ascend_address_space.L1, al.ascend_address_space.UB): + raise TypeError("dst's AddressSpace must be UB or L1") + _semantic.builder.create_copy_buffer(src.handle, dst.handle) + else: + raise TypeError("src and dst must be tl.tensor or bl.buffer") + + +def fixpipe( + src: tl.tensor, + dst, + dma_mode, + dual_dst_mode, + pre_quant_mode, + pre_relu_mode, + _semantic=None, +): + if dst is None: + result = _semantic.builder.create_fixpipe( + src.handle, + dst, + dma_mode.value, + dual_dst_mode.value, + pre_quant_mode.value, + pre_relu_mode.value, + ) + if dual_dst_mode == al.FixpipeDualDstMode.ROW_SPLIT: + new_shape = list(src.type.shape) + if len(new_shape) >= 1 and new_shape[0] > 0: + new_shape[0] = new_shape[0] // 2 + new_type = tl.block_type(src.type.element_ty, new_shape) + return tl.tensor(result, new_type) + elif dual_dst_mode == al.FixpipeDualDstMode.COLUMN_SPLIT: + new_shape = list(src.type.shape) + if len(new_shape) >= 2 and new_shape[1] > 0: + new_shape[1] = new_shape[1] // 2 + new_type = tl.block_type(src.type.element_ty, new_shape) + return tl.tensor(result, new_type) + else: + return tl.tensor(result, src.type) + else: + _semantic.builder.create_fixpipe( + src.handle, + dst.handle, + dma_mode.value, + dual_dst_mode.value, + pre_quant_mode.value, + pre_relu_mode.value, + ) + + +def debug_barrier(sync_mode: str, _semantic=None) -> None: + target = tl.tensor(_semantic.builder.get_int64(0), tl.int64) + attr = _semantic.builder.get_string_attr(sync_mode) + _semantic.builder.create_debug_barrier(target.handle, "SYNC_IN_VF", attr) + + +def conv1d(input_tensor: tl.tensor, weight_tensor: tl.tensor, bias: Union[tl.tensor, None], stride: int, + padding_size: int, dilation: int, groups: int, output_shape, _semantic=None) -> tl.tensor: + bias_handle = None if bias is None else bias.handle + + element_ty = input_tensor.type.element_ty + output_ty = tl.block_type(element_ty, output_shape) + out = _semantic.builder.create_conv1d(input_tensor.handle, weight_tensor.handle, bias_handle, stride, padding_size, + dilation, groups, output_ty.to_ir(_semantic.builder)) + return tl.tensor(out, output_ty) + + +def dot(a: tl.tensor, b: tl.tensor, fractal_a: bool, fractal_b: bool, fractal_c: bool, output_shape, + _semantic=None) -> tl.tensor: + main_builder = _semantic.builder + delegate_builder = main_builder._ascend_builder + # Synchronize the Ascend call without replacing ir.builder.create_dot, + # which tl.dot uses. The interpreter aliases the delegate to itself and + # needs no cross-builder synchronization. + if delegate_builder is main_builder: + create_dot = delegate_builder.create_dot + else: + create_dot = create_builder_method_wrapper(main_builder, delegate_builder, "create_dot") + out = create_dot(a.handle, b.handle, bool(fractal_a), bool(fractal_b), bool(fractal_c)) + # Result carries the cube accumulator dtype: f32 for float inputs, i32 for int8. + in_ty = a.type.element_ty + acc_ty = tl.float32 if in_ty.is_floating() else tl.int32 + output_ty = tl.block_type(acc_ty, output_shape) + return tl.tensor(out, output_ty) + + +def conv2d(input_tensor: tl.tensor, weight_tensor: tl.tensor, bias: Union[tl.tensor, None], stride: Union[int, tuple], + padding: Union[int, tuple], dilation: Union[int, + tuple], groups: int, output_shape, _semantic=None) -> tl.tensor: + bias_handle = None if bias is None else bias.handle + + element_ty = input_tensor.type.element_ty + output_ty = tl.block_type(element_ty, output_shape) + out = _semantic.builder.create_conv2d(input_tensor.handle, weight_tensor.handle, bias_handle, stride, padding, + dilation, groups, output_ty.to_ir(_semantic.builder)) + return tl.tensor(out, output_ty) diff --git a/third_party/ascend/language/cann/extension/vec_ops.py b/third_party/ascend/language/cann/extension/vec_ops.py new file mode 100644 index 0000000000..3e530f3b44 --- /dev/null +++ b/third_party/ascend/language/cann/extension/vec_ops.py @@ -0,0 +1,548 @@ +# insert_slice +# extract_slice +# get_element +# sort +# flip +# gather + +import triton.language as tl +from triton.language import semantic, core, standard +from triton.language.core import (_unwrap_if_constexpr, _tensor_member_fn, _unwrap_iterable, builtin, constexpr, dtype, + tensor, check_bit_width, _unwrap_if_constexpr, range) + +from triton.backends.ascend.utils import is_compile_on_910_95 +from .aux_ops import compile_hint_impl + +from typing import Optional, Tuple, List, overload +from triton._C.libtriton import ir + + +@_tensor_member_fn +@builtin +def insert_slice(ful, sub, offsets, sizes, strides, _semantic=None, _generator=None) -> tensor: + """ + Insert a tensor to another tensor as specified by the offsets, sizes and strides arguments. + + :param ful: The tensor to receive tensor. + :type ful: Tensor + :param sub: The tensor to be inserted. + :type sub: Tensor + :param offsets: The starting element indices in `ful` where the slice `sub` should be inserted. + :type offsets: tuple of ints or tuple of tensors + :param sizes: The dimensions of the slice to be inserted. + :type sizes: tuple of ints + :param strides: The element strides for each dimension of the insertion. + :type strides: tuple of ints + """ + + def insert_slice_impl(ful: tensor, sub: tensor, offsets: List[tensor], sizes: List[int], strides: List[int], + builder: ir.builder) -> tensor: + assert (len(ful.shape) == len(offsets)) + assert (len(ful.shape) == len(sizes)) + assert (len(ful.shape) == len(strides)) + assert (all([s >= 1 for s in sizes])) + assert (all([s >= 0 for s in strides])) + # Handle both tensor and int offsets (for interpreter mode) + new_offsets = [] + for o in offsets: + if isinstance(o, tensor): + new_offsets.append(o.handle) + elif isinstance(o, int): + # For interpreter mode: keep as int + new_offsets.append(o) + else: + new_offsets.append(o.handle if hasattr(o, 'handle') else o) + ret_type = tl.block_type(ful.type.scalar, ful.shape) + out = builder.create_insert_slice(ful.handle, sub.handle, new_offsets, sizes, strides) + return tensor(out, ret_type) + + assert len(ful.shape) > 0 + assert len(ful.shape) == len(sub.shape) + new_offsets = [_semantic.to_tensor(o) if isinstance(o, constexpr) else o for o in offsets] + out = insert_slice_impl(ful, sub, new_offsets, sizes, strides, _semantic.builder) + return out + + +@_tensor_member_fn +@builtin +def extract_slice(ful, offsets, sizes, strides, _semantic=None, _generator=None) -> tensor: + """ + Extract a tensor from another tensor as specified by the offsets, sizes and strides arguments. + + :param ful: The tensor to split. + :type ful: Tensor + :param offsets: The starting element indices in `ful` from where the slice should be extracted. + :type offsets: tuple of ints or tuple of tensors + :param sizes: The dimensions of the slice to be extracted. + :type sizes: tuple of ints + :param strides: The element strides for each dimension of the extraction. + :type strides: tuple of ints + """ + + def extract_slice_impl(ful: tensor, offsets: List[tensor], sizes: List[int], strides: List[int], + builder: ir.builder) -> tensor: + assert (len(ful.shape) == len(offsets)) + assert (len(ful.shape) == len(sizes)) + assert (len(ful.shape) == len(strides)) + assert (all([s >= 1 for s in sizes])) + assert (all([s >= 0 for s in strides])) + # Handle both tensor and int offsets (for interpreter mode) + new_offsets = [] + for o in offsets: + if isinstance(o, tensor): + new_offsets.append(o.handle) + elif isinstance(o, int): + # For interpreter mode: keep as int + new_offsets.append(o) + else: + new_offsets.append(o.handle if hasattr(o, 'handle') else o) + # change sizes to list(sizes) for interpreter mode to bypass tl.block_type's assertion + # and has no effect on the compile mode + ret_type = tl.block_type(ful.type.scalar, list(sizes)) + out = builder.create_extract_slice(ful.handle, new_offsets, sizes, strides) + return tensor(out, ret_type) + + assert len(ful.shape) > 0 + new_offsets = [_semantic.to_tensor(o) if isinstance(o, constexpr) else o for o in offsets] + sub = extract_slice_impl(ful, new_offsets, sizes, strides, _semantic.builder) + return sub + + +@_tensor_member_fn +@builtin +def get_element(src, indice, _semantic=None, _generator=None): + """ + get_element op reads a ranked tensor and returns one element as specified by the given indices. + The result of the op is a value with the same type as the elements of the tensor. + The arity of indices must match the rank of the accessed value. + + :param src: The tensor to be accessed. + :type src: Tensor + :param indice: The indices specifying the position of the element to extract. + :type indice: tuple of ints or tuple of tensors + """ + + def get_element_impl(src: tensor, indice: List[tensor], builder: ir.builder): + if len(src.shape) != len(indice): + raise ValueError("Indice's rank must be equal to src tensor's rank") + + # Handle both tensor and int indices (for interpreter mode) + new_indice = [] + for i in indice: + if isinstance(i, tensor): + new_indice.append(i.handle) + elif isinstance(i, int): + # For interpreter mode: convert int to TensorHandle + new_indice.append(i) + else: + # Try to use .handle attribute if available + new_indice.append(i.handle if hasattr(i, 'handle') else i) + + result = builder.create_extract_scalar(src.handle, new_indice) + return _semantic.wrap_tensor(result, src.type.scalar, None) + + assert len(src.shape) > 0 + new_indice = [_semantic.to_tensor(i) if isinstance(i, constexpr) else i for i in indice] + return get_element_impl(src, new_indice, _semantic.builder) + + +@builtin +def flip(ptr, dim=-1, _semantic=None, _generator=None): + """Flips a tensor along the specified dimension. + + Reverses the order of elements along the given axis. This is an Ascend-specific + implementation that supports both SIMD and non-SIMD execution modes. + + :param ptr: The input tensor to flip. + :type ptr: tensor + :param dim: The dimension along which to flip. Defaults to -1 (last dimension). + :type dim: int + """ + + def flip_impl(ptr: tensor, dim: int, builder: ir.builder, generator=None): + """ + Flips a tensor `ptr` along the dimension `dim`. + + :param ptr: the first input tensor + :type ptr: tensor + :param dim: the dimension to flip along + :type dim: int + :param generator: the code generator (required for reduce operations) + :type generator: generator object + """ + + def _get_flip_dim(dim, shape): + dim = _unwrap_if_constexpr(dim) + shape = _unwrap_if_constexpr(shape) + if dim is None: + dim = len(shape) - 1 + if dim < 0: # flip doesn't work if dim < 0 because the xor-swap for loop will start/end at the wrong index + dim += len(shape) + return constexpr(dim) + + def _log2(i: core.constexpr): + log2 = 0 + n = core.constexpr(i).value + while n > 1: + n >>= 1 + log2 += 1 + return core.constexpr(log2) + + def flip_simd(ptr: tensor, dim: int, builder: ir.builder): + """ + Triton flip operation for simd + + Args: + ptr: tensor, input tensor + dim: int, dimension to flip (can be negative, normalized here) + builder: ir.builder, underlying IR builder + Returns: + flipped: tensor, same type and shape as input + """ + + shape = getattr(ptr, "shape", None) + if shape is None or shape == (): + shape = getattr(getattr(ptr, "type", None), "shape", None) + + rank = None + if shape is not None: + try: + rank = len(shape) + except Exception: + rank = len(list(shape)) + + if rank is not None: + if rank < 1: + raise ValueError("ascend.flip requires tensor rank >= 1") + norm_dim = dim if dim >= 0 else dim + rank + if not (0 <= norm_dim < rank): + raise ValueError(f"ascend.flip got invalid dim={dim} for shape {tuple(shape)}") + dim = norm_dim + else: + if dim < 0: + raise ValueError("ascend.flip with unknown rank requires non-negative dim") + + flipped_vals = builder.create_flip(ptr.handle, dim) + flipped = tensor(flipped_vals, type=ptr.type) + return flipped + + # If compile_mode is not simt, use the simd implementation + if not builder.is_simt_mode(): + return flip_simd(ptr, dim, builder) + core.static_assert(-len(ptr.shape) <= dim and dim < len(ptr.shape), _builder=builder) + _dim: core.constexpr = _get_flip_dim(dim, ptr.shape) + core.static_assert(standard._is_power_of_two(ptr.shape[_dim]), _builder=builder) + steps: core.constexpr = _log2(ptr.shape[_dim]) + # If steps is 0, return the original tensor + if steps == 0: + return ptr + # reshape the swap dimension to (2, 2, ..., 2) + idtype = core.get_int_dtype(bitwidth=ptr.dtype.primitive_bitwidth, signed=True) + y = core.reshape( + ptr.to(idtype, bitcast=True, _builder=builder), + ptr.shape.__getitem__(slice(None, _dim)) + [2] * steps + ptr.shape.__getitem__(slice(_dim + 1, None)), + _builder=builder) + for i in static_range(steps): + y = y.__xor__(standard.xor_sum(y, _dim + i, True, _builder=builder, _generator=generator), _builder=builder) + ptr = core.reshape(y, ptr.shape, _builder=builder).to(ptr.dtype, bitcast=True, _builder=builder) + return ptr + + try: + dim = int(dim.value) if hasattr(dim, "value") else int(dim) + except Exception as e: + raise TypeError(f"dim must be an integer (or tl.constexpr int), got {dim!r}") from e + + dim = len(ptr.shape) - 1 if dim == -1 else dim + return flip_impl(ptr, dim, _semantic.builder, _generator) + + +class static_range: + """ + Iterator for non-JIT Python functions that need to iterate over constexpr values. + This is used in functions like flip that are called during compilation. + """ + + def __init__(self, arg1, arg2=None, step=None): + if step is None: + self.step = core.constexpr(1) + else: + self.step = step + if arg2 is None: + self.start = core.constexpr(0) + self.end = arg1 + else: + self.start = arg1 + self.end = arg2 + + def __iter__(self): + # Extract actual values from constexpr objects for iteration + start_val = core._unwrap_if_constexpr(self.start) + end_val = core._unwrap_if_constexpr(self.end) + step_val = core._unwrap_if_constexpr(self.step) + # Store as regular Python integers for iteration + self._current = start_val + self._end = end_val + self._step = step_val + return self + + def __next__(self): + if self._current >= self._end: + raise StopIteration + value = self._current + self._current += self._step + return value + + +@builtin +def sort(ptr, dim=-1, descending=False, _semantic=None): + """ + sort the input tensor along 'dim' + + :param ptr: the tensor to be sorted + :type ptr: tensor + :param dim: dimension to sort + :type dim: int or tl.constexpr[int] + :param descending: the result is descending or not + :type descending: bool or tl.constexpr[bool] + + """ + + def sort_impl(ptr: tensor, dim: int, descending, builder: ir.builder): + allowed_types = { + tl.int8, tl.int16, tl.bfloat16, tl.float16, tl.float32, tl.int32, tl.int64, tl.float8e4nv, tl.float8e5 + } + base_ty = ptr.type.scalar if hasattr(ptr.type, "scalar") else ptr.type + if base_ty not in allowed_types: + raise TypeError( + f"ascend.sort only supports int8, int16, bfloat16, float16, float32, int32, int64, float8e4nv, float8e5" + f"but got {ptr.type}") + + shape = getattr(ptr, "shape", None) + if shape is None or shape == (): + shape = getattr(getattr(ptr, "type", None), "shape", None) + + rank = None + if shape is not None: + try: + rank = len(shape) + except Exception: + rank = len(list(shape)) + + if rank is not None: + if rank < 1: + raise ValueError("ascend.sort requires tensor rank >= 1") + last_dim = rank - 1 + norm_dim = dim if dim >= 0 else dim + rank + if norm_dim != last_dim: + raise ValueError(f"ascend.sort only supports sorting along the last dimension " + f"(dim={last_dim} or -1) for shape {tuple(shape)}, but got dim={dim}") + dim = last_dim + else: + if dim != -1: + raise ValueError("ascend.sort only supports the last dimension; when rank is unknown " + "you must pass dim=-1") + + if hasattr(descending, "value"): + descending = bool(descending.value) + else: + descending = bool(descending) + + sorted_vals = builder.create_sort(ptr.handle, dim, descending) + + values = tensor(sorted_vals, type=ptr.type) + + return values + + try: + dim = int(dim.value) if hasattr(dim, "value") else int(dim) + except Exception as e: + raise TypeError(f"dim must be an integer (or tl.constexpr int), got {dim!r}. Error: {str(e)}") from e + + if hasattr(descending, "value"): + descending = bool(descending.value) + else: + descending = bool(descending) + + ret = sort_impl(ptr, dim, descending, _semantic.builder) + # interpreter mode not support compile_hint overflow_mode, direct return + from triton.runtime.interpreter import InterpreterBuilder + if isinstance(_semantic.builder, InterpreterBuilder): + return ret + base_ty = ptr.type.scalar if hasattr(ptr.type, "scalar") else ptr.type + if base_ty.is_int8() or base_ty.is_int16(): + compile_hint_impl(ret, "overflow_mode", constexpr("saturate"), _semantic.builder) + return ret + + +def ascend_cast_impl(input: tensor, dst_ty: dtype, _semantic=None, fp_downcast_rounding: Optional[str] = None, + overflow_mode: Optional[str] = None) -> tensor: + src_ty = input.type + if isinstance(dst_ty, tl.constexpr): + dst_ty = dst_ty.value + if isinstance(fp_downcast_rounding, tl.constexpr): + fp_downcast_rounding = fp_downcast_rounding.value + if src_ty.is_block(): + dst_ty = tl.block_type(dst_ty.scalar, input.type.get_block_shapes()) + if src_ty == dst_ty: + return input + + src_sca_ty = src_ty.scalar + dst_sca_ty = dst_ty.scalar + if src_sca_ty == dst_sca_ty: + return input + + # For fp downcasting default rounding mode should be RTNE, for all other conversions it should + # not be set + fp_downcast_rounding = _semantic._str_to_rounding_mode(fp_downcast_rounding) + use_custom_rounding = False + if dst_sca_ty.is_floating() and src_sca_ty.is_floating( + ) and dst_sca_ty.primitive_bitwidth < src_sca_ty.primitive_bitwidth: + if fp_downcast_rounding is None: fp_downcast_rounding = ir.ROUNDING_MODE.RTNE + elif fp_downcast_rounding != ir.ROUNDING_MODE.RTNE: use_custom_rounding = True + else: + if fp_downcast_rounding is not None: + raise ValueError("fp_downcast_rounding should be set only for truncating fp conversions. " + "Source scalar type is " + str(src_sca_ty) + " and destination type is " + str(dst_sca_ty)) + if not is_compile_on_910_95(_semantic.builder.options.arch): + if (src_sca_ty.is_fp8() or dst_sca_ty.is_fp8()) or (src_sca_ty.is_fp64() or dst_sca_ty.is_fp64()): + raise ValueError("[fp8, fp64] is unsupported on Ascend for now." + "Source scalar type is " + str(src_sca_ty) + " and destination type is " + str(dst_sca_ty)) + if (src_sca_ty.is_fp8e4b15() or dst_sca_ty.is_fp8e4b15()): + assert _semantic.builder.codegen_fns.get( + "convert_custom_types") is not None, "target doesn't provide conversion for this type." + return _semantic.builder.codegen_fns["convert_custom_types"](input, dst_ty, fp_downcast_rounding, + _semantic=_semantic) + # Casting with customized floating types involved: fp8 <=> bf16, fp16, fp32, fp64 + # and non-default rounding modes for downcasting + if (src_sca_ty.is_fp8() and dst_sca_ty.is_floating()) or \ + (src_sca_ty.is_floating() and dst_sca_ty.is_fp8()) or \ + use_custom_rounding: + return tensor( + _semantic.builder.create_fp_to_fp(input.handle, dst_ty.to_ir(_semantic.builder), fp_downcast_rounding), + dst_ty) + + # bf16 <=> (not fp32) + if (src_sca_ty.is_fp16() and not dst_sca_ty.is_fp32()) or \ + (src_sca_ty.is_bf16() and not dst_sca_ty.is_fp32()): + return ascend_cast_impl(ascend_cast_impl(input, tl.float32, _semantic), dst_sca_ty, _semantic) + + # Standard floating types' casting: truncation + # fp64 => fp32, fp16, bf16 + # fp32 => fp16, bf16 + truncate_fp = src_sca_ty.is_floating() and \ + dst_sca_ty.is_floating() and \ + src_sca_ty.primitive_bitwidth > dst_sca_ty.primitive_bitwidth + if truncate_fp: + return tensor(_semantic.builder.create_fp_trunc(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + + # Standard floating types' casting: extension + # fp32 => fp64 + # fp16 => fp32, fp64 + # bf16 => fp32, fp64 + ext_fp = src_sca_ty.is_floating() and \ + dst_sca_ty.is_floating() and \ + src_sca_ty.primitive_bitwidth < dst_sca_ty.primitive_bitwidth + if ext_fp: + return tensor(_semantic.builder.create_fp_ext(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + + # Casting between integer types + if src_sca_ty.is_int() and dst_sca_ty.is_int() and \ + (src_sca_ty.int_bitwidth != dst_sca_ty.int_bitwidth or src_sca_ty.int_signedness != dst_sca_ty.int_signedness): + sign_extend = src_sca_ty.is_int_signed() and not src_sca_ty.is_bool() + if dst_sca_ty.is_bool(): + ty = input.dtype.to_ir(_semantic.builder) + _0 = tensor(_semantic.builder.get_null_value(ty), input.dtype) + return _semantic.not_equal(input, _0) + elif overflow_mode == "saturate" and \ + (src_sca_ty.is_int_unsigned() or dst_sca_ty.is_int_unsigned()) and \ + src_sca_ty.int_bitwidth >= dst_sca_ty.int_bitwidth: + if is_compile_on_910_95(_semantic.builder.options.arch): + result = tensor( + _semantic.builder.create_int_cast(input.handle, dst_ty.to_ir(_semantic.builder), sign_extend), + dst_ty) + compile_hint_impl(result, "saturate_src_unsigned", src_sca_ty.is_int_unsigned(), _semantic.builder) + compile_hint_impl(result, "saturate_dst_unsigned", dst_sca_ty.is_int_unsigned(), _semantic.builder) + return result + else: + return ascend_cast_impl(ascend_cast_impl(input, tl.float32, _semantic), dst_sca_ty, _semantic) + return tensor(_semantic.builder.create_int_cast(input.handle, dst_ty.to_ir(_semantic.builder), sign_extend), + dst_ty) + + # Casting standard floating types to integer types + if src_sca_ty.is_standard_floating() and dst_sca_ty.is_int(): + if dst_sca_ty.is_bool(): + ty = input.dtype.to_ir(_semantic.builder) + _0 = tensor(_semantic.builder.get_null_value(ty), input.dtype) + return _semantic.not_equal(input, _0) + elif dst_sca_ty.is_int_signed(): + return tensor(_semantic.builder.create_fp_to_si(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + else: + return tensor(_semantic.builder.create_fp_to_ui(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + + # Casting integer types to standard floating types + if src_sca_ty.is_int() and dst_sca_ty.is_standard_floating(): + if src_sca_ty.is_bool() or not src_sca_ty.is_int_signed(): + return tensor(_semantic.builder.create_ui_to_fp(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + else: + return tensor(_semantic.builder.create_si_to_fp(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + + # Casting pointer types to integer types + if src_sca_ty.is_ptr() and dst_sca_ty.is_int(): + bitwidth = dst_sca_ty.int_bitwidth + if bitwidth == 64: + return tensor(_semantic.builder.create_ptr_to_int(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + if bitwidth == 1: + return _semantic.not_equal(ascend_cast_impl(input, tl.int64, _semantic), + tensor(_semantic.builder.get_int64(0), tl.int64)) + + # Casting integer types to pointer types + if src_sca_ty.is_int() and dst_sca_ty.is_ptr(): + return tensor(_semantic.builder.create_int_to_ptr(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + + # Casting pointer types to pointer types + if src_sca_ty.is_ptr() and dst_sca_ty.is_ptr(): + return tensor(_semantic.builder.create_bitcast(input.handle, dst_ty.to_ir(_semantic.builder)), dst_ty) + + assert False, f'cannot cast {input} to {dst_ty}' + + +@_tensor_member_fn +@builtin +def cast(input, dtype: dtype, fp_downcast_rounding: Optional[str] = None, bitcast: bool = False, + overflow_mode: Optional[str] = None, _semantic=None): + """ + Casts a tensor to the given :code:`dtype`. + + :param dtype: The target data type. + :type dtype: dtype + :param fp_downcast_rounding: The rounding mode for downcasting + floating-point values. This parameter is only used when self is a + floating-point tensor and dtype is a floating-point type with a + smaller bitwidth. Supported values are :code:`"rtne"` (round to + nearest, ties to even) and :code:`"rtz"` (round towards zero). + :type fp_downcast_rounding: str, optional + :param bitcast: If true, the tensor is bitcasted to the given + :code:`dtype`, instead of being numerically casted. + :type bitcast: bool, optional + :param overflow_mode: When overflow_mode is not set or is "trunc", + truncation (cut-off) will be used to handle overflow. When + overflow_mode is "sautrate", the maximum value of the data type + will be used to handle overflow. + :type overflow_mode: string, optional + """ + overflow_modes = ["trunc", "saturate"] + input = _semantic.to_tensor(input) + if isinstance(bitcast, constexpr): + bitcast = bitcast.value + if bitcast: + return _semantic.bitcast(input, dtype) + ret = ascend_cast_impl(input, dtype, _semantic, fp_downcast_rounding, overflow_mode) + if overflow_mode is not None: + if overflow_mode in overflow_modes: + from triton.runtime.interpreter import InterpreterBuilder + if isinstance(_semantic.builder, InterpreterBuilder): + overflow_mode = constexpr(overflow_mode) + compile_hint_impl(ret, "overflow_mode", overflow_mode, _semantic.builder) + else: + raise ValueError(f"Unknown overflow_mode:{overflow_mode} is found.") + return ret diff --git a/third_party/ascend/language/cann/libdevice.py b/third_party/ascend/language/cann/libdevice.py new file mode 100644 index 0000000000..93ab52a44a --- /dev/null +++ b/third_party/ascend/language/cann/libdevice.py @@ -0,0 +1,4128 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from math import pi as math_pi +from triton.language import core, math, semantic, standard +from triton._C.libtriton import ir +from triton.runtime.jit import jit +from triton.backends.ascend.utils import is_compile_on_910_95, triton_enable_libdevice_simt + +from .utils import _deprecated + + +def _is_libdevice_simt_enabled(_semantic) -> bool: + return triton_enable_libdevice_simt(_semantic.builder.options.arch) + + +def _is_a5_target(_semantic) -> bool: + return is_compile_on_910_95(_semantic.builder.options.arch) + + +class _FlipStaticRange: + + def __init__(self, arg1, arg2=None, step=None): + self.step = core.constexpr(1) if step is None else step + if arg2 is None: + self.start = core.constexpr(0) + self.end = arg1 + else: + self.start = arg1 + self.end = arg2 + + def __iter__(self): + self._current = core._unwrap_if_constexpr(self.start) + self._end = core._unwrap_if_constexpr(self.end) + self._step = core._unwrap_if_constexpr(self.step) + return self + + def __next__(self): + if self._current >= self._end: + raise StopIteration + value = self._current + self._current += self._step + return value + + +@core.builtin +def flip(ptr, dim=-1, _semantic=None, _generator=None): + """Flips a tensor along the specified dimension.""" + + def flip_impl(ptr: core.tensor, dim: int, builder: ir.builder, generator=None): + + def _get_flip_dim(dim, shape): + dim = core._unwrap_if_constexpr(dim) + shape = core._unwrap_if_constexpr(shape) + if dim is None: + dim = len(shape) - 1 + if dim < 0: + dim += len(shape) + return core.constexpr(dim) + + def _log2(i: core.constexpr): + log2 = 0 + n = core.constexpr(i).value + while n > 1: + n >>= 1 + log2 += 1 + return core.constexpr(log2) + + def flip_simd(ptr: core.tensor, dim: int, builder: ir.builder): + shape = getattr(ptr, "shape", None) + if shape is None or shape == (): + shape = getattr(getattr(ptr, "type", None), "shape", None) + + rank = None + if shape is not None: + try: + rank = len(shape) + except Exception: + rank = len(list(shape)) + + if rank is not None: + if rank < 1: + raise ValueError("ascend.flip requires tensor rank >= 1") + norm_dim = dim if dim >= 0 else dim + rank + if not (0 <= norm_dim < rank): + raise ValueError(f"ascend.flip got invalid dim={dim} for shape {tuple(shape)}") + dim = norm_dim + elif dim < 0: + raise ValueError("ascend.flip with unknown rank requires non-negative dim") + + flipped_vals = builder.create_flip(ptr.handle, dim) + return core.tensor(flipped_vals, type=ptr.type) + + if not builder.is_simt_mode(): + return flip_simd(ptr, dim, builder) + if not (-len(ptr.shape) <= dim < len(ptr.shape)): + raise ValueError(f"invalid dim={dim} for shape {tuple(ptr.shape)}") + flip_dim = core._unwrap_if_constexpr(_get_flip_dim(dim, ptr.shape)) + if not standard._is_power_of_two(ptr.shape[flip_dim]): + raise ValueError("flip in SIMT mode requires the flipped dimension to be a power of two") + steps = core._unwrap_if_constexpr(_log2(ptr.shape[flip_dim])) + if steps == 0: + return ptr + + idtype = core.get_int_dtype(bitwidth=ptr.dtype.primitive_bitwidth, signed=True) + reshaped = core.reshape( + ptr.to(idtype, bitcast=True, _semantic=_semantic), + ptr.shape.__getitem__(slice(None, flip_dim)) + [2] * steps + + ptr.shape.__getitem__(slice(flip_dim + 1, None)), + _semantic=_semantic, + _generator=_generator, + ) + for i in _FlipStaticRange(steps): + reduced = core.reduce( + reshaped, + flip_dim + i, + standard._xor_combine, + keep_dims=True, + _semantic=_semantic, + _generator=generator, + ) + reshaped = reshaped.__xor__(reduced, _semantic=_semantic) + return core.reshape(reshaped, ptr.shape, _semantic=_semantic, _generator=_generator).to( + ptr.dtype, + bitcast=True, + _semantic=_semantic, + ) + + try: + dim = int(dim.value) if hasattr(dim, "value") else int(dim) + except Exception as exc: + raise TypeError(f"dim must be an integer (or tl.constexpr int), got {dim!r}") from exc + + dim = len(ptr.shape) - 1 if dim == -1 else dim + return flip_impl(ptr, dim, _semantic.builder, _generator) + + +@core.extern +def reciprocal(arg0, _semantic=None): + """ + Computes the reciprocal of x (i.e., 1 / x). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: 1 / x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_reciprocal_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_recipf", core.dtype("fp32")), + (core.dtype("fp16"), ): ("__hmf_recipDh", core.dtype("fp16")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def log1p(arg0, _semantic=None): + """ + Computes the value of log(1 + x). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of log(1 + x). + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_log1p_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_log1pf", core.dtype("fp32")), + (core.dtype("fp16"), ): ("__hmf_log1pDh", core.dtype("fp16")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def relu(arg0, _semantic=None): + """ + Rectified linear unit function, returns x when x > 0, otherwise returns 0. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the rectified linear unit. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_relu_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_reluf", core.dtype("fp32")), + (core.dtype("fp16"), ): ("__hmf_reluDh", core.dtype("fp16")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def isinf(arg0, _semantic=None): + """ + Determines whether the input is infinity. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: True if the input is infinity; otherwise, False. + :rtype: ``bool`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_isinf_fp32", core.dtype("int1")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_isinf", core.dtype("int1")), + (core.dtype("fp16"), ): ("__hmf_isinf", core.dtype("int1")), + (core.dtype("bf16"), ): ("__hmf_isinf", core.dtype("int1")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def tan(arg0, _semantic=None): + """ + Computes the tangent of input parameter x (in radians). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The tangent of input x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_tan_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_tanf", core.dtype("fp32")), + (core.dtype("fp16"), ): ("__hmf_tanDh", core.dtype("fp16")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def atan(arg0, _semantic=None): + """ + Computes the inverse tangent (arctan) of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse tangent of the input parameter, in the range [-π/2, π/2] radians. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_atan_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_atanf", core.dtype("fp32")), + (core.dtype("fp16"), ): ("__hmf_atanDh", core.dtype("fp16")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def tanh(arg0, _semantic=None): + """ + Computes the hyperbolic tangent of input parameter x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The hyperbolic tangent of input x. + :rtype: ``float32`` + """ + arg0 = _semantic.to_tensor(arg0) + original_dtype = arg0.dtype + if original_dtype == core.dtype("bf16"): + arg0 = _semantic.cast(arg0, core.float32) + + if _is_libdevice_simt_enabled(_semantic): + dispatch = { + (core.dtype("fp32"), ): ("__hmf_tanh_fp32", core.dtype("fp32")), + } + else: + dispatch = { + (core.dtype("fp32"), ): ("__hmf_tanhf", core.dtype("fp32")), + (core.dtype("fp16"), ): ("__hmf_tanhDh", core.dtype("fp16")), + } + + res = core.extern_elementwise("", "", [arg0], dispatch, is_pure=True, _semantic=_semantic) + if original_dtype == core.dtype("bf16"): + return _semantic.cast(res, core.dtype("bf16")) + return res + + +@core.extern +def ilogb(arg0, _semantic=None): + """ + Extracts the unbiased exponent (base-2 integer logarithm) of a floating-point number. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The unbiased exponent of the input parameter. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_ilogb_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_ilogbf", core.dtype("fp32")), + (core.dtype("fp16"), ): ("__hmf_ilogbDh", core.dtype("fp16")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def logb(arg0, _semantic=None): + """ + Extracts the exponent value of a floating-point number. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The exponent value of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.logb for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_logb_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ldexp(arg0, arg1, _semantic=None): + """ + Computes the value of x × 2^exp. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``exp``. Supported dtype(s): ``int32``. + :type arg1: scalar or tl.tensor + :return: The result of x × 2^exp. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("int32")): ("__hmf_ldexp_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("int32")): ("__hmf_ldexpf", core.dtype("fp32")), + (core.dtype("fp16"), core.dtype("int32")): ("__hmf_ldexpDh", core.dtype("fp16")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def scalbn(arg0, arg1, _semantic=None): + """ + Computes the value of x × 2^n. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``n``. Supported dtype(s): ``int32``. + :type arg1: scalar or tl.tensor + :return: The result of x × 2^n. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.scalbn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("int32")): ("__hmf_scalbn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def pow(arg0, arg1, _semantic=None): + """ + Power function, computes x raised to the power of y. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: x raised to the power of y. + :rtype: ``float32`` + """ + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + if arg1.dtype == core.dtype("int32"): + arg1 = _semantic.cast(arg1, arg0.dtype) + + if arg0.dtype == core.dtype("fp32") and _is_a5_target(_semantic): + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_pow_fp32", core.dtype("fp32")), + (core.dtype("fp32"), core.dtype("int32")): ("__hmf_powi_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_powf", core.dtype("fp32")), + (core.dtype("fp16"), core.dtype("fp16")): ("__hmf_powDh", core.dtype("fp16")), + (core.dtype("bf16"), core.dtype("bf16")): ("__hmf_powDb", core.dtype("bf16")), + }, is_pure=True, _semantic=_semantic) + + +@core._tensor_member_fn +@jit +@math._add_math_1arg_docstr("isfinited") +def isfinited(arg0): + _is_int8_type: core.constexpr = arg0.dtype.is_int8() + core.static_assert( + not _is_int8_type, + "Expected dtype fp16/fp32/bf16, but got int8 or int1", + ) + _is_floating_type: core.constexpr = arg0.dtype.is_floating() + core.static_assert( + _is_floating_type == True, + f"Expected dtype fp16/fp32/bf16, but got {core.constexpr(arg0.dtype)}", + ) + nan_mask = isnan(arg0) + inf_mask = isinf(arg0) + return (~nan_mask & ~inf_mask).to(core.int1) + + +@core.extern +def finitef(arg0, _semantic=None): + """ + Determines whether the input is a finite floating-point number. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: True if the input is finite; otherwise, False. + :rtype: ``bool`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_finite_fp32", core.dtype("int1")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + if arg0.dtype.is_int8(): + raise ValueError("finitef only supports float32, but got int8 or int1") + if arg0.dtype != core.float32: + raise ValueError(f"finitef only supports float32, but got {core.constexpr(arg0.dtype)}") + nan_mask = isnan(arg0, _semantic=_semantic) + inf_mask = isinf(arg0, _semantic=_semantic) + return _semantic.logical_and(_semantic.not_(nan_mask), _semantic.not_(inf_mask)) + + +@core.extern +def isnan(arg0, _semantic=None): + """ + Determines whether the input is NaN (not a number). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: True if the input is NaN; otherwise, False. + :rtype: ``bool`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_isnan_fp32", core.dtype("int1")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_isnan", core.dtype("int1")), + (core.dtype("fp16"), ): ("__hmf_isnan", core.dtype("int1")), + (core.dtype("bf16"), ): ("__hmf_isnan", core.dtype("int1")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def clz(arg0, _semantic=None): + """ + Counts the number of leading zeros in a 32-bit integer. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The number of leading zeros in the input parameter. Range: [0, 32]. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.clz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_clz_i32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def popc(arg0, _semantic=None): + """ + Counts the number of bits set to 1 in x. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The number of bits set to 1 in x. Range: [0, 32]. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.popc for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_popc_i32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def byte_perm(arg0, arg1, arg2, _semantic=None): + """ + Selects bytes from two 32-bit integers x and y according to selector s and combines them into a new integer. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``int32``. + :type arg1: scalar or tl.tensor + :param arg2: ``s``. Supported dtype(s): ``int32``. + :type arg2: scalar or tl.tensor + :return: The integer whose n-th byte is selected from x and y by selector s. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.byte_perm for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1, arg2], { + (core.dtype("int32"), core.dtype("int32"), core.dtype("int32")): ("__hmf_byte_perm_i32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def mulhi(arg0, arg1, _semantic=None): + """ + Computes the high 32 bits of the multiplication result of x and y. + + :param arg0: ``x``. Supported dtype(s): ``int32`` or ``uint32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``int32`` or ``uint32``; must have the same type as x. + :type arg1: scalar or tl.tensor + :return: The high 32 bits of the multiplication result of x and y. + :rtype: Same as the input type (``int32`` or ``uint32``) + """ + if not _is_a5_target(_semantic): + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + if arg0.dtype == core.uint32 and arg1.dtype == arg0.dtype: + return core.tensor(_semantic.builder.create_umulhi(arg0.handle, arg1.handle), arg0.type) + core.static_print("libdevice.mulhi for this dtype in simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("int32"), core.dtype("int32")): ("__hmf_mulhi_i32", core.dtype("int32")), + (core.dtype("uint32"), core.dtype("uint32")): ("__hmf_umulhi_u32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def mul24(arg0, arg1, _semantic=None): + """ + Computes the lower 24-bit multiplication result of x and y. + + :param arg0: ``x``. Supported dtype(s): ``int32`` or ``uint32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``int32`` or ``uint32``; must have the same type as x. + :type arg1: scalar or tl.tensor + :return: The lower 24-bit multiplication result of x and y. + :rtype: Same as the input type (``int32`` or ``uint32``) + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.mul24 for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("int32"), core.dtype("int32")): ("__hmf_mul24_i32", core.dtype("int32")), + (core.dtype("uint32"), core.dtype("uint32")): ("__hmf_umul24_u32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def brev(arg0, _semantic=None): + """ + Bit reversal function, reverses the bit order of a 32-bit integer. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The 32-bit integer with reversed bit order. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.brev for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_brev_i32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def sad(arg0, arg1, arg2, _semantic=None): + """ + Computes abs(x - y) + z for signed or unsigned 32-bit integers. + + :param arg0: ``x``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. + :type arg2: scalar or tl.tensor + + Supported dtype signatures: + + - (``int32``, ``int32``, ``uint32``) -> ``int32`` + - (``uint32``, ``uint32``, ``uint32``) -> ``uint32`` + + :return: The result of abs(x - y) + z. + :rtype: ``int32`` or ``uint32``, as specified by the supported signatures + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sad for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1, arg2], { + (core.dtype("int32"), core.dtype("int32"), core.dtype("uint32")): ("__hmf_sad_i32", core.dtype("int32")), + (core.dtype("uint32"), core.dtype("uint32"), core.dtype("uint32")): + ("__hmf_usad_u32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ffs(arg0, _semantic=None): + """ + Finds the first bit set to 1 and returns the index of the lowest bit set to 1. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The index of the lowest bit set to 1. Range: [0, 32]. + :rtype: ``int32`` + """ + arg0 = _semantic.to_tensor(arg0) + dtype = arg0.dtype + if _is_a5_target(_semantic): + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_ffs_i32", core.dtype("int32")), + (core.dtype("int64"), ): ("__hmf_ffs_i64", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + core.static_print(f"libdevice.ffs for {dtype} is unsupported for now.") + core.static_assert(False) + + +@core.extern +def saturatef(arg0, _semantic=None): + """ + Clamps x to the range [+0.0, 1.0]. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The saturated value of x, in the range [+0.0, 1.0]. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.saturatef for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_saturate_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def hadd(arg0, arg1, _semantic=None): + """ + Computes the average of x and y. + + :param arg0: ``x``. Supported dtype(s): ``int32`` or ``uint32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``int32`` or ``uint32``; must have the same type as x. + :type arg1: scalar or tl.tensor + :return: The average of x and y. + :rtype: Same as the input type (``int32`` or ``uint32``) + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.hadd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("int32"), core.dtype("int32")): ("__hmf_hadd_i32", core.dtype("int32")), + (core.dtype("uint32"), core.dtype("uint32")): ("__hmf_uhadd_u32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rhadd(arg0, arg1, _semantic=None): + """ + Computes the rounded average of x and y. + + :param arg0: ``x``. Supported dtype(s): ``int32`` or ``uint32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``int32`` or ``uint32``; must have the same type as x. + :type arg1: scalar or tl.tensor + :return: The rounded average of x and y. + :rtype: Same as the input type (``int32`` or ``uint32``) + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rhadd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("int32"), core.dtype("int32")): ("__hmf_rhadd_i32", core.dtype("int32")), + (core.dtype("uint32"), core.dtype("uint32")): ("__hmf_urhadd_u32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def fdim(arg0, arg1, _semantic=None): + """ + Computes the positive difference between x and y. When x > y, returns x - y; otherwise returns 0. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The positive difference between x and y. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fdim for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fdim_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def exp10(arg0, _semantic=None): + """ + Base-10 exponential function, computes 10 raised to the power of x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of 10 raised to the power of x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.exp10 for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_exp10_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def add_rn(arg0, arg1, _semantic=None): + """ + Floating-point addition with round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The addition result rounded to the nearest even number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.add_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_add_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def add_rz(arg0, arg1, _semantic=None): + """ + Floating-point addition with round-toward-zero rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The addition result rounded toward zero. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.add_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_add_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def add_rd(arg0, arg1, _semantic=None): + """ + Floating-point addition with round-down (toward negative infinity) rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The addition result rounded down. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.add_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_add_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def add_ru(arg0, arg1, _semantic=None): + """ + Floating-point addition with round-up (toward positive infinity) rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The addition result rounded up. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.add_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_add_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def sub_rn(arg0, arg1, _semantic=None): + """ + Floating-point subtraction with round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The subtraction result rounded to the nearest even number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sub_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_sub_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def sub_rz(arg0, arg1, _semantic=None): + """ + Floating-point subtraction with round-toward-zero rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The subtraction result rounded toward zero. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sub_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_sub_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def sub_rd(arg0, arg1, _semantic=None): + """ + Floating-point subtraction with round-down rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The subtraction result rounded down. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sub_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_sub_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def sub_ru(arg0, arg1, _semantic=None): + """ + Floating-point subtraction with round-up rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The subtraction result rounded up. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sub_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_sub_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def mul_rn(arg0, arg1, _semantic=None): + """ + Floating-point multiplication with round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The floating-point multiplication result. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.mul_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_mul_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def mul_rz(arg0, arg1, _semantic=None): + """ + Floating-point multiplication with round-toward-zero rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The floating-point multiplication result. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.mul_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_mul_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def mul_ru(arg0, arg1, _semantic=None): + """ + Floating-point multiplication with round-up rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The floating-point multiplication result. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.mul_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_mul_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def mul_rd(arg0, arg1, _semantic=None): + """ + Floating-point multiplication with round-down rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The floating-point multiplication result. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.mul_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_mul_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def div_rd(arg0, arg1, _semantic=None): + """ + Floating-point division with round-down (toward negative infinity) rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The division result. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.div_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_div_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def div_ru(arg0, arg1, _semantic=None): + """ + Floating-point division with round-up (toward positive infinity) rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The division result. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.div_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_div_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def div_rz(arg0, arg1, _semantic=None): + """ + Floating-point division with round-toward-zero rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The division result. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + ret = _semantic.fdiv(arg0, arg1, False) + return ret + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_div_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rcp_rn(arg0, _semantic=None): + """ + Floating-point reciprocal with round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: 1 / x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rcp_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_rcp_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rcp_rz(arg0, _semantic=None): + """ + Floating-point reciprocal with round-toward-zero rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: 1 / x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rcp_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_rcp_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rcp_rd(arg0, _semantic=None): + """ + Floating-point reciprocal with round-down rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: 1 / x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rcp_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_rcp_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rcp_ru(arg0, _semantic=None): + """ + Floating-point reciprocal with round-up rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: 1 / x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rcp_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_rcp_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +@math._check_dtype(dtypes=["fp32"]) +def sqrt_rn(arg0, _semantic=None): + """ + Computes the square root of x with round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The square root of x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_sqrt_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_precise_sqrt(arg0.handle), arg0.type) + + +@core.extern +def sqrt_rz(arg0, _semantic=None): + """ + Computes the square root of x with round-toward-zero rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The square root of x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sqrt_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_sqrt_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def sqrt_rd(arg0, _semantic=None): + """ + Computes the square root of x with round-down rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The square root of x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sqrt_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_sqrt_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def sqrt_ru(arg0, _semantic=None): + """ + Computes the square root of x with round-up rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The square root of x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sqrt_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_sqrt_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rsqrt_rn(arg0, _semantic=None): + """ + Computes the reciprocal square root of x using round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The reciprocal square root of x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rsqrt_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_rsqrt_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def fma_rn(arg0, arg1, arg2, _semantic=None): + """ + Fused multiply-add operation with round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :return: The result of fused multiply-add. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fma_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1, arg2], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fma_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def fma_rz(arg0, arg1, arg2, _semantic=None): + """ + Fused multiply-add operation with round-toward-zero rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :return: The result of fused multiply-add. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fma_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1, arg2], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fma_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def fma_rd(arg0, arg1, arg2, _semantic=None): + """ + Fused multiply-add operation with round-down rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :return: The result of fused multiply-add. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fma_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1, arg2], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fma_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def fma_ru(arg0, arg1, arg2, _semantic=None): + """ + Fused multiply-add operation with round-up rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :return: The result of fused multiply-add. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fma_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1, arg2], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fma_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_dividef(arg0, arg1, _semantic=None): + """ + Fast approximate division. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The result of fast approximate division. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fast_divide_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + ret = _semantic.fdiv(arg0, arg1, False) + return ret + + +@core.builtin +def fast_expf(arg0, _semantic=None): + """ + Fast approximate exponential function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the fast approximate exponential function. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_exp_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + ret = core.tensor(_semantic.builder.create_exp(arg0.handle), arg0.type) + return ret + + +@core.builtin +def fast_exp10f(arg0, _semantic=None): + """ + Fast approximate base-10 exponential function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the fast approximate base-10 exponential function. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_exp10f for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_exp10_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_sinf(arg0, _semantic=None): + """ + Fast approximate sine function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the fast approximate sine function. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_sinf for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_sin_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_cosf(arg0, _semantic=None): + """ + Fast approximate cosine function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the fast approximate cosine function. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_cosf for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_cos_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_tanf(arg0, _semantic=None): + """ + Fast approximate tangent function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the fast approximate tangent function. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_tanf for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_tan_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_tanhf(arg0, _semantic=None): + """ + Computes the hyperbolic tangent of x using a fast approximation. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The fast approximate hyperbolic tangent of x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_tanhf for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_tanh_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_log2f(arg0, _semantic=None): + """ + Fast approximate base-2 logarithm function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the fast approximate base-2 logarithm function. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_log2f for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_log2_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_logf(arg0, _semantic=None): + """ + Fast approximate natural logarithm function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the fast approximate natural logarithm function. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_logf for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_log_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_log10f(arg0, _semantic=None): + """ + Fast approximate base-10 logarithm function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of the fast approximate base-10 logarithm function. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_log10f for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_fast_log10_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +def fast_powf(arg0, arg1, _semantic=None): + """ + Fast approximate power function. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The result of fast approximate power function. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.fast_powf for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fast_pow_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def fmod(arg0, arg1, _semantic=None): + """ + Floating-point modulo, computes the remainder of x / y, with the same sign as x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The floating-point modulo result. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + ret = _semantic.mod(arg0, arg1) + return ret + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fmod_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def remainder(arg0, arg1, _semantic=None): + """ + Computes the remainder of x divided by y, where r = x - ny, and n is the nearest integer to x / y. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The remainder of x divided by y. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.remainder for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_remainder_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float_as_int(arg0, _semantic=None): + """ + Reinterprets the bit pattern of a floating-point number as a 32-bit integer. No numeric conversion is performed. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The bit pattern of the floating-point number reinterpreted as a 32-bit integer. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float_as_int for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float_as_int_fp32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def int_as_float(arg0, _semantic=None): + """ + Reinterprets the bit pattern of a 32-bit integer as a floating-point number. No numeric conversion is performed. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The bit pattern of the 32-bit integer reinterpreted as a floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.int_as_float for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_int_as_float_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float_as_uint(arg0, _semantic=None): + """ + Reinterprets the bit pattern of a floating-point number as a 32-bit unsigned integer. No numeric conversion is + performed. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The bit pattern of the floating-point number reinterpreted as a 32-bit unsigned integer. + :rtype: ``uint32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float_as_uint for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float_as_uint_fp32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def uint_as_float(arg0, _semantic=None): + """ + Reinterprets the bit pattern of a 32-bit unsigned integer as a floating-point number. No numeric conversion is + performed. + + :param arg0: ``x``. Supported dtype(s): ``uint32``. + :type arg0: scalar or tl.tensor + :return: The bit pattern of the 32-bit unsigned integer reinterpreted as a floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.uint_as_float for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint32"), ): ("__hmf_uint_as_float_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2int_rn(arg0, _semantic=None): + """ + Converts a floating-point number to a 32-bit integer with round-to-nearest-even mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit integer. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2int_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2int_rn_fp32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2int_rz(arg0, _semantic=None): + """ + Converts a floating-point number to a 32-bit integer with round-toward-zero mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit integer. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2int_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2int_rz_fp32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2int_rd(arg0, _semantic=None): + """ + Converts a floating-point number to a 32-bit integer with round-down mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit integer. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2int_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2int_rd_fp32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2int_ru(arg0, _semantic=None): + """ + Converts a floating-point number to a 32-bit integer with round-up mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit integer. + :rtype: ``int32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2int_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2int_ru_fp32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def int2float_rn(arg0, _semantic=None): + """ + Converts a 32-bit integer to a floating-point number with round-to-nearest-even mode. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.int2float_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_int2float_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def int2float_rz(arg0, _semantic=None): + """ + Converts a 32-bit integer to a floating-point number with round-toward-zero mode. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.int2float_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_int2float_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def int2float_rd(arg0, _semantic=None): + """ + Converts a 32-bit integer to a floating-point number with round-down mode. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.int2float_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_int2float_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def int2float_ru(arg0, _semantic=None): + """ + Converts a 32-bit integer to a floating-point number with round-up mode. + + :param arg0: ``x``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.int2float_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int32"), ): ("__hmf_int2float_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2uint_rn(arg0, _semantic=None): + """ + Converts a floating-point number to a 32-bit unsigned integer with round-to-nearest-even mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit unsigned integer. + :rtype: ``uint32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2uint_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2uint_rn_fp32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2uint_rz(arg0, _semantic=None): + """ + Converts a floating-point number to a 32-bit unsigned integer with round-toward-zero mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit unsigned integer. + :rtype: ``uint32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2uint_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2uint_rz_fp32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2uint_rd(arg0, _semantic=None): + """ + Converts a floating-point number to a 32-bit unsigned integer with round-down mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit unsigned integer. + :rtype: ``uint32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2uint_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2uint_rd_fp32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2uint_ru(arg0, _semantic=None): + """ + Converts a floating-point number to a 32-bit unsigned integer with round-up mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit unsigned integer. + :rtype: ``uint32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2uint_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2uint_ru_fp32", core.dtype("uint32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def uint2float_rn(arg0, _semantic=None): + """ + Converts a 32-bit unsigned integer to a floating-point number with round-to-nearest-even mode. + + :param arg0: ``x``. Supported dtype(s): ``uint32``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.uint2float_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint32"), ): ("__hmf_uint2float_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def uint2float_rz(arg0, _semantic=None): + """ + Converts a 32-bit unsigned integer to a floating-point number with round-toward-zero mode. + + :param arg0: ``x``. Supported dtype(s): ``uint32``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.uint2float_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint32"), ): ("__hmf_uint2float_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def uint2float_rd(arg0, _semantic=None): + """ + Converts a 32-bit unsigned integer to a floating-point number with round-down mode. + + :param arg0: ``x``. Supported dtype(s): ``uint32``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.uint2float_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint32"), ): ("__hmf_uint2float_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def uint2float_ru(arg0, _semantic=None): + """ + Converts a 32-bit unsigned integer to a floating-point number with round-up mode. + + :param arg0: ``x``. Supported dtype(s): ``uint32``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.uint2float_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint32"), ): ("__hmf_uint2float_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2ll_rn(arg0, _semantic=None): + """ + Converts a floating-point number to a 64-bit integer with round-to-nearest-even mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 64-bit integer. + :rtype: ``int64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2ll_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2ll_rn_fp32", core.dtype("int64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2ll_rz(arg0, _semantic=None): + """ + Converts a floating-point number to a 64-bit integer with round-toward-zero mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 64-bit integer. + :rtype: ``int64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2ll_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2ll_rz_fp32", core.dtype("int64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2ll_rd(arg0, _semantic=None): + """ + Converts a floating-point number to a 64-bit integer with round-down mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 64-bit integer. + :rtype: ``int64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2ll_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2ll_rd_fp32", core.dtype("int64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2ll_ru(arg0, _semantic=None): + """ + Converts a floating-point number to a 64-bit integer with round-up mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 64-bit integer. + :rtype: ``int64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2ll_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2ll_ru_fp32", core.dtype("int64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ll2float_rn(arg0, _semantic=None): + """ + Converts a 64-bit integer to a floating-point number with round-to-nearest-even mode. + + :param arg0: ``x``. Supported dtype(s): ``int64``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.ll2float_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int64"), ): ("__hmf_ll2float_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ll2float_rz(arg0, _semantic=None): + """ + Converts a 64-bit integer to a floating-point number with round-toward-zero mode. + + :param arg0: ``x``. Supported dtype(s): ``int64``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.ll2float_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int64"), ): ("__hmf_ll2float_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ll2float_rd(arg0, _semantic=None): + """ + Converts a 64-bit integer to a floating-point number with round-down mode. + + :param arg0: ``x``. Supported dtype(s): ``int64``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.ll2float_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int64"), ): ("__hmf_ll2float_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ll2float_ru(arg0, _semantic=None): + """ + Converts a 64-bit integer to a floating-point number with round-up mode. + + :param arg0: ``x``. Supported dtype(s): ``int64``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.ll2float_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("int64"), ): ("__hmf_ll2float_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2ull_rn(arg0, _semantic=None): + """ + Converts a floating-point number to a 64-bit unsigned integer with round-to-nearest-even mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 64-bit unsigned integer. + :rtype: ``uint64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2ull_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2ull_rn_fp32", core.dtype("uint64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2ull_rz(arg0, _semantic=None): + """ + Converts a floating-point number to a 64-bit unsigned integer with round-toward-zero mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 64-bit unsigned integer. + :rtype: ``uint64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2ull_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2ull_rz_fp32", core.dtype("uint64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2ull_rd(arg0, _semantic=None): + """ + Converts a floating-point number to a 64-bit unsigned integer with round-down mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 64-bit unsigned integer. + :rtype: ``uint64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2ull_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2ull_rd_fp32", core.dtype("uint64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def float2ull_ru(arg0, _semantic=None): + """ + Converts a floating-point number to a 64-bit unsigned integer with round-up mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 64-bit unsigned integer. + :rtype: ``uint64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2ull_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2ull_ru_fp32", core.dtype("uint64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ull2float_rn(arg0, _semantic=None): + """ + Converts a 64-bit unsigned integer to a floating-point number with round-to-nearest-even mode. + + :param arg0: ``x``. Supported dtype(s): ``uint64``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.ull2float_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint64"), ): ("__hmf_ull2float_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ull2float_rz(arg0, _semantic=None): + """ + Converts a 64-bit unsigned integer to a floating-point number with round-toward-zero mode. + + :param arg0: ``x``. Supported dtype(s): ``uint64``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.ull2float_rz for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint64"), ): ("__hmf_ull2float_rz_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ull2float_rd(arg0, _semantic=None): + """ + Converts a 64-bit unsigned integer to a floating-point number with round-down mode. + + :param arg0: ``x``. Supported dtype(s): ``uint64``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.ull2float_rd for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint64"), ): ("__hmf_ull2float_rd_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def ull2float_ru(arg0, _semantic=None): + """ + Converts a 64-bit unsigned integer to a floating-point number with round-up mode. + + :param arg0: ``x``. Supported dtype(s): ``uint64``. + :type arg0: scalar or tl.tensor + :return: The converted floating-point number. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.ull2float_ru for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("uint64"), ): ("__hmf_ull2float_ru_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def atan2(arg0, arg1, _semantic=None): + """ + Two-argument inverse tangent function, computes the arctangent of x / y. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The arctangent of x / y, in the range [-π, π] radians. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("bf16") or arg1.dtype == core.dtype("bf16"): + core.static_print("extern libdevice.atan2 for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("fp16"), core.dtype("fp16")): ("__hmf_atan2_fp16", core.dtype("fp16")), + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_atan2_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + pi = 3.1415926536 + + _is_int8_type_x: core.constexpr = arg1.dtype.is_int8() + core.static_assert(not _is_int8_type_x, "Expected dtype fp16/fp32/bf16, but got int8 or int1", _semantic=_semantic) + + _is_int8_type_y: core.constexpr = arg0.dtype.is_int8() + core.static_assert(not _is_int8_type_y, "Expected dtype fp16/fp32/bf16, but got int8 or int1", _semantic=_semantic) + + _is_floating_type_x: core.constexpr = arg1.dtype.is_floating() + core.static_assert(_is_floating_type_x == True, + f"Expected dtype fp16/fp32/bf16, but got {core.constexpr(arg1.dtype)}", _semantic=_semantic) + + _is_floating_type_y: core.constexpr = arg0.dtype.is_floating() + core.static_assert(_is_floating_type_y == True, + f"Expected dtype fp16/fp32/bf16, but got {core.constexpr(arg0.dtype)}", _semantic=_semantic) + + half_pi: core.constexpr = 0.5 * pi + arg0_fp32 = arg0.to(core.dtype("fp32"), _semantic=_semantic) + arg1_fp32 = arg1.to(core.dtype("fp32"), _semantic=_semantic) + atan_input = _semantic.truediv(arg0_fp32, arg1_fp32) + + # Ordering comparisons cannot tell -0.0 from +0.0, so the sign bit decides + # the quadrant of a zero operand: atan2(-0.0, -1.0) is -pi, not +pi. + y_negative = signbit(arg0_fp32, _semantic=_semantic) + x_is_zero = _semantic.equal(arg1_fp32, 0) + y_is_zero = _semantic.equal(arg0_fp32, 0) + x_is_negative_zero = _semantic.logical_and(x_is_zero, signbit(arg1_fp32, _semantic=_semantic)) + + # y / x is not finite at x == 0, so those quadrants are selected directly: + # +-pi / 2 for a non-zero y, and +-0.0 (or +-pi when x == -0.0) for y == 0. + zero_axis = _semantic.where(y_is_zero, _semantic.where(x_is_negative_zero, pi, 0.0), half_pi) + zero_axis = _semantic.where(y_negative, _semantic.mul(zero_axis, -1.0, True), zero_axis) + base = _semantic.where(x_is_zero, zero_axis, atan(atan_input, _semantic=_semantic)) + + # x < 0 rotates the result by +-pi, with the sign taken from y. + rotated = _semantic.where(y_negative, _semantic.sub(base, pi, True), _semantic.add(base, pi, True)) + ret = _semantic.where(_semantic.less_than(arg1_fp32, 0), rotated, base) + return ret.to(arg1.dtype, _semantic=_semantic) + + +@core.builtin +@math._check_dtype(dtypes=["fp32"]) +def trunc(arg0, _semantic=None): + """ + Truncation operation, rounds toward zero to the nearest integer. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The truncation result. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_trunc_fp16", core.dtype("fp16")), + (core.dtype("fp32"), ): ("__hmf_trunc_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0 = _semantic.to_tensor(arg0) + + zero = _semantic.full(arg0.shape, 0.0, arg0.type.scalar) + condition = _semantic.greater_equal(arg0, zero) + + floor_result = core.tensor(_semantic.builder.create_floor(arg0.handle), arg0.type) + ceil_result = core.tensor(_semantic.builder.create_ceil(arg0.handle), arg0.type) + + return _semantic.where(condition, floor_result, ceil_result) + + +@core.extern +def round(arg0, _semantic=None): + """ + Computes the nearest integer to x using round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The nearest integer to x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_round_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_roundf", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +@math._check_dtype(dtypes=["bf16", "fp16", "fp32"]) +def acos(arg0: core.tensor, _semantic=None): + """ + Computes the inverse cosine (arccos) of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse cosine of the input parameter, in the range [0, π] radians. + :rtype: ``float32`` + """ + if arg0.dtype == core.dtype("fp32") and _is_a5_target(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_acos_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + pi = 3.1415926536 + pi_half = 1.5707963268 + sqrt2 = 1.4142135624 + eps = 1e-8 + + # |x| < 0.5, acos(x) = pi/2 - [x + x*x²*(0.1666667 + x²*(0.075 + x²*(0.0446429 + 0.0303810*x²))] + arg0 = _semantic.to_tensor(arg0) + abs_x = math.abs(arg0, _semantic=_semantic) + dtype = arg0.dtype + arg0_2 = _semantic.mul(arg0, arg0, True) + arg0_4 = _semantic.mul(arg0_2, arg0_2, True) + arg0_6 = _semantic.mul(arg0_4, arg0_2, True) + arg0_8 = _semantic.mul(arg0_6, arg0_2, True) + arg0_10 = _semantic.mul(arg0_8, arg0_2, True) + poly = _semantic.add(1.0, _semantic.mul(0.166667, arg0_2, True), True) + poly = _semantic.add(poly, _semantic.mul(0.075, arg0_4, True), True) + poly = _semantic.add(poly, _semantic.mul(0.044643, arg0_6, True), True) + poly = _semantic.add(poly, _semantic.mul(0.030380, arg0_8, True), True) + poly = _semantic.add(poly, _semantic.mul(0.022372, arg0_10, True), True) + acos_center = _semantic.sub(pi_half, _semantic.mul(arg0, poly, True), True) + + # 0.5<|x|<0.9, acos(x) = 2*arctan(t), t=sqrt((1-abs_x)/(1+abs_x)) + numerator_mid = _semantic.sub(1.0, abs_x, True) + denom_mid = _semantic.add(1.0, abs_x, True) + div_mid = _semantic.truediv(numerator_mid, denom_mid) + t_mid = math.sqrt(div_mid, _semantic=_semantic) + t2_mid = _semantic.mul(t_mid, t_mid, True) + t4_mid = _semantic.mul(t2_mid, t2_mid, True) + t6_mid = _semantic.mul(t4_mid, t2_mid, True) + + poly_mid1 = _semantic.mul(0.1065976, t2_mid, True) + poly_mid2 = _semantic.add(-0.1420890, poly_mid1, True) + poly_mid3 = _semantic.mul(poly_mid2, t2_mid, True) + poly_mid4 = _semantic.add(0.1999341, poly_mid3, True) + poly_mid5 = _semantic.mul(poly_mid4, t2_mid, True) + poly_mid6 = _semantic.add(-0.3333310, poly_mid5, True) + poly_mid = _semantic.add(1.0, _semantic.mul(poly_mid6, t2_mid, True), True) + arctan_t = _semantic.mul(t_mid, poly_mid, True) + acos_mid = _semantic.mul(2.0, arctan_t, True) + is_neg_mid = _semantic.less_than(arg0, 0.0) + acos_mid_signed = _semantic.where(is_neg_mid, _semantic.sub(pi, acos_mid, True), acos_mid) + + is_center = _semantic.less_than(abs_x, 0.6) + res_mid_boundary = _semantic.where(is_center, acos_center, acos_mid_signed) + return res_mid_boundary + + +@core.builtin +@math._check_dtype(dtypes=["bf16", "fp16", "fp32"]) +def sinh(arg0: core.tensor, _semantic=None): + """ + Computes the hyperbolic sine of input parameter x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The hyperbolic sine of input x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("bf16"): + core.static_print("extern libdevice.sinh for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_sinh_fp16", core.dtype("fp16")), + (core.dtype("fp32"), ): ("__hmf_sinh_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0 = _semantic.to_tensor(arg0) + exp0 = core.tensor(_semantic.builder.create_exp(arg0.handle), arg0.type) + exp1 = _semantic.truediv(1.0, exp0) + tmp = _semantic.sub(exp0, exp1, True) + ret = _semantic.truediv(tmp, 2.0) + return ret + + +@core.builtin +@math._check_dtype(dtypes=["bf16", "fp16", "fp32"]) +def cosh(arg0: core.tensor, _semantic=None): + """ + Computes the hyperbolic cosine of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The hyperbolic cosine of the input parameter. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("bf16"): + core.static_print("extern libdevice.cosh for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_cosh_fp16", core.dtype("fp16")), + (core.dtype("fp32"), ): ("__hmf_cosh_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0 = _semantic.to_tensor(arg0) + exp0 = core.tensor(_semantic.builder.create_exp(arg0.handle), arg0.type) + exp1 = _semantic.truediv(1.0, exp0) + tmp = _semantic.add(exp0, exp1, True) + ret = _semantic.truediv(tmp, 2.0) + return ret + + +@core.builtin +@math._check_dtype(dtypes=["bf16", "fp16", "fp32"]) +def acosh(arg0: core.tensor, _semantic=None): + """ + Computes the inverse hyperbolic cosine of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse hyperbolic cosine of the input parameter, in the range [0, +∞]. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("bf16"): + core.static_print("extern libdevice.acosh for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_acosh_fp16", core.dtype("fp16")), + (core.dtype("fp32"), ): ("__hmf_acosh_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0 = _semantic.to_tensor(arg0) + tmp = _semantic.sub(_semantic.mul(arg0, arg0, True), 1.0, True) + sqrt_res = core.tensor(_semantic.builder.create_sqrt(tmp.handle), tmp.type) + sum_res = _semantic.add(arg0, sqrt_res, True) + return core.tensor(_semantic.builder.create_log(sum_res.handle), sum_res.type) + + +@core.builtin +@math._check_dtype(dtypes=["bf16", "fp16", "fp32"]) +def asinh(arg0: core.tensor, _semantic=None): + """ + Computes the inverse hyperbolic sine of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse hyperbolic sine of the input parameter. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("bf16"): + core.static_print("extern libdevice.asinh for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_asinh_fp16", core.dtype("fp16")), + (core.dtype("fp32"), ): ("__hmf_asinh_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0 = _semantic.to_tensor(arg0) + tmp = _semantic.add(_semantic.mul(arg0, arg0, True), 1.0, True) + sqrt_res = core.tensor(_semantic.builder.create_sqrt(tmp.handle), tmp.type) + sum_res = _semantic.add(arg0, sqrt_res, True) + return core.tensor(_semantic.builder.create_log(sum_res.handle), sum_res.type) + + +@core.builtin +@math._check_dtype(dtypes=["bf16", "fp16", "fp32"]) +def atanh(arg0: core.tensor, _semantic=None): + """ + Inverse hyperbolic tangent function, computes the inverse hyperbolic tangent of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse hyperbolic tangent of the input parameter, in the range (-∞, +∞). + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("bf16"): + core.static_print("extern libdevice.atanh for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_atanh_fp16", core.dtype("fp16")), + (core.dtype("fp32"), ): ("__hmf_atanh_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0 = _semantic.to_tensor(arg0) + a = _semantic.add(1.0, arg0, True) + b = _semantic.sub(1.0, arg0, True) + lna = core.tensor(_semantic.builder.create_log(a.handle), a.type) + lnb = core.tensor(_semantic.builder.create_log(b.handle), b.type) + tmp = _semantic.sub(lna, lnb, True) + return _semantic.mul(tmp, 0.5, True) + + +@core.builtin +@math._check_dtype(dtypes=["bf16", "fp16", "fp32"]) +def expm1(arg0: core.tensor, _semantic=None): + """ + Computes e raised to the power of x, minus 1. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of e raised to the power of x, minus 1. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("bf16"): + core.static_print("extern libdevice.expm1 for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_expm1_fp16", core.dtype("fp16")), + (core.dtype("fp32"), ): ("__hmf_expm1_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0 = _semantic.to_tensor(arg0) + tmp = core.tensor(_semantic.builder.create_exp(arg0.handle), arg0.type) + return _semantic.sub(tmp, 1, True) + + +@core.builtin +@math._check_dtype(dtypes=["fp16", "fp32"]) +def nextafter(arg0: core.tensor, arg1: core.tensor, _semantic=None): + """ + Computes the next representable floating-point number from x toward y. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The next representable floating-point number. + :rtype: ``float32`` + """ + if arg0.dtype == core.dtype("fp32") and _is_a5_target(_semantic): + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_nextafter_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + x = _semantic.to_tensor(arg0) + y = _semantic.to_tensor(arg1) + dtype_map = {"bf16": core.int16, "fp16": core.int16, "fp32": core.int32} + min_pos_bit = {"bf16": 0x0001, "fp16": 0x0001, "fp32": 0x00000001} + max_neg_bit = {"bf16": 0x8001, "fp16": 0x8001, "fp32": 0x80000001} + int_type = dtype_map[x.type.scalar.name] + x_eq_y = _semantic.equal(x, y) + x_gt_0 = _semantic.greater_than(x, 0) + y_gt_x = _semantic.greater_than(y, x) + next_neg = _semantic.xor_(x_gt_0, y_gt_x) + next_pos = _semantic.not_(next_neg) + + p1 = _semantic.full(x.shape, 1, int_type) + n1 = _semantic.full(x.shape, -1, int_type) + dir_xy = _semantic.where(next_pos, p1, n1) + x_abs = math.abs(x, _semantic=_semantic) + x_is_0 = _semantic.equal(x_abs, 0) + + min_pos = _semantic.full(x.shape, min_pos_bit[x.type.scalar.name], int_type) + max_neg = _semantic.full(x.shape, max_neg_bit[x.type.scalar.name], int_type) + min_pos = _semantic.bitcast(min_pos, x.dtype) + max_neg = _semantic.bitcast(max_neg, x.dtype) + bits_x = _semantic.bitcast(x, int_type) + bits_next = _semantic.add(bits_x, dir_xy, True) + next_val = _semantic.bitcast(bits_next, x.dtype) + + need_min_pos = _semantic.logical_and(x_is_0, next_pos) + need_max_neg = _semantic.logical_and(x_is_0, next_neg) + next_val = _semantic.where(need_min_pos, min_pos, next_val) + next_val = _semantic.where(need_max_neg, max_neg, next_val) + return _semantic.where(x_eq_y, x, next_val) + + +@core.builtin +@math._check_dtype(dtypes=["bf16", "fp16", "fp32"]) +def hypot(arg0: core.tensor, arg1: core.tensor, _semantic=None): + """ + Computes the Euclidean distance between x and y. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The Euclidean distance between x and y. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("bf16"): + core.static_print("extern libdevice.hypot for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("fp16"), core.dtype("fp16")): ("__hmf_hypot_fp16", core.dtype("fp16")), + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_hypot_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + x2 = _semantic.mul(arg0, arg0, True) + y2 = _semantic.mul(arg1, arg1, True) + sum_res = _semantic.add(x2, y2, True) + return core.tensor(_semantic.builder.create_sqrt(sum_res.handle), sum_res.type) + + +@core.extern +def cbrt(arg0, _semantic=None): + """ + Computes the cube root of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The cube root of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.cbrt for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_cbrt_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rcbrt(arg0, _semantic=None): + """ + Computes the reciprocal cube root of x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The reciprocal cube root of x. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rcbrt for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_rcbrt_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rhypot(arg0, arg1, _semantic=None): + """ + Computes the reciprocal of the Euclidean distance between x and y. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The reciprocal of the Euclidean distance between x and y. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rhypot for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_rhypot_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def norm3d(arg0, arg1, arg2, _semantic=None): + """ + Computes the Euclidean norm of a 3D vector, i.e., sqrt(x² + y² + z²). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :return: The Euclidean norm of the 3D vector. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.norm3d for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1, arg2], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): ("__hmf_norm3d_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rnorm3d(arg0, arg1, arg2, _semantic=None): + """ + Computes the reciprocal of the Euclidean norm of a 3D vector. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :return: The reciprocal of the Euclidean norm of the 3D vector. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rnorm3d for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1, arg2], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): ("__hmf_rnorm3d_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def norm4d(arg0, arg1, arg2, arg3, _semantic=None): + """ + Computes the Euclidean norm of a 4D vector, i.e., sqrt(x² + y² + z² + w²). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :param arg3: ``w``. Supported dtype(s): ``float32``. + :type arg3: scalar or tl.tensor + :return: The Euclidean norm of the 4D vector. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.norm4d for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1, arg2, arg3], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): + ("__hmf_norm4d_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def rnorm4d(arg0, arg1, arg2, arg3, _semantic=None): + """ + Computes the reciprocal of the Euclidean norm of a 4D vector. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :param arg3: ``w``. Supported dtype(s): ``float32``. + :type arg3: scalar or tl.tensor + :return: The reciprocal of the Euclidean norm of the 4D vector. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.rnorm4d for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1, arg2, arg3], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): + ("__hmf_rnorm4d_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def j0(arg0, _semantic=None): + """ + Computes the Bessel function of the first kind of order 0 of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The Bessel function of the first kind of order 0 of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.j0 for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_j0_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def j1(arg0, _semantic=None): + """ + Computes the Bessel function of the first kind of order 1 of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The Bessel function of the first kind of order 1 of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.j1 for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_j1_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def jn(arg0, arg1, _semantic=None): + """ + Computes the Bessel function of the first kind of order n of the input parameter. + + :param arg0: ``n``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :param arg1: ``x``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The Bessel function of the first kind of order n of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.jn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("int32"), core.dtype("fp32")): ("__hmf_jn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def y0(arg0, _semantic=None): + """ + Computes the Bessel function of the second kind of order 0 of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The Bessel function of the second kind of order 0 of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.y0 for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_y0_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def y1(arg0, _semantic=None): + """ + Computes the Bessel function of the second kind of order 1 of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The Bessel function of the second kind of order 1 of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.y1 for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_y1_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def yn(arg0, arg1, _semantic=None): + """ + Computes the Bessel function of the second kind of order n of the input parameter. + + :param arg0: ``n``. Supported dtype(s): ``int32``. + :type arg0: scalar or tl.tensor + :param arg1: ``x``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The Bessel function of the second kind of order n of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.yn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("int32"), core.dtype("fp32")): ("__hmf_yn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +# This function is derived from the Cephes Math Library release 2.8: June, 2000 +# https://netlib.org/cephes/ +# Copyright (c) 1984, 1987, 2000 by Stephen L. Moshier +# All rights reserved. +@core.builtin +@math._check_dtype(dtypes=["fp16", "fp32"]) +def cyl_bessel_i0(arg0: core.tensor, _semantic=None): + """ + Computes the modified Bessel function of the first kind, order 0, of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The modified Bessel function of the first kind, order 0, of the input parameter. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + if arg0.dtype == core.dtype("fp16"): + core.static_print("extern libdevice.cyl_bessel_i0 for dtype bf16 is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_cyl_bessel_i0_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + param1 = [ + -4.41534164647933937950e-18, + +3.33079451882223809783e-17, + -2.43127984654795469359e-16, + +1.71539128555513303061e-15, + -1.16853328779934516808e-14, + +7.67618549860493561688e-14, + -4.85644678311192946090e-13, + +2.95505266312963983461e-12, + -1.72682629144155570723e-11, + +9.67580903537323691224e-11, + -5.18979560163526290666e-10, + +2.65982372468238665035e-09, + -1.30002500998624804212e-08, + +6.04699502254191894932e-08, + -2.67079385394061173391e-07, + +1.11738753912010371815e-06, + -4.41673835845875056359e-06, + +1.64484480707288970893e-05, + -5.75419501008210370398e-05, + +1.88502885095841655729e-04, + -5.76375574538582365885e-04, + +1.63947561694133579842e-03, + -4.32430999505057594430e-03, + +1.05464603945949983183e-02, + -2.37374148058994688156e-02, + +4.93052842396707084878e-02, + -9.49010970480476444210e-02, + +1.71620901522208775349e-01, + -3.04682672343198398683e-01, + +6.76795274409476084995e-01, + ] + param2 = [ + -7.23318048787475395456e-18, + -4.83050448594418207126e-18, + +4.46562142029675999901e-17, + +3.46122286769746109310e-17, + -2.82762398051658348494e-16, + -3.42548561967721913462e-16, + +1.77256013305652638360e-15, + +3.81168066935262242075e-15, + -9.55484669882830764870e-15, + -4.15056934728722208663e-14, + +1.54008621752140982691e-14, + +3.85277838274214270114e-13, + +7.18012445138366623367e-13, + -1.79417853150680611778e-12, + -1.32158118404477131188e-11, + -3.14991652796324136454e-11, + +1.18891471078464383424e-11, + +4.94060238822496958910e-10, + +3.39623202570838634515e-09, + +2.26666899049817806459e-08, + +2.04891858946906374183e-07, + +2.89137052083475648297e-06, + +6.88975834691682398426e-05, + +3.36911647825569408990e-03, + +8.04490411014108831608e-01, + ] + arg0 = _semantic.to_tensor(arg0) + abs_x = core.tensor(_semantic.builder.create_fabs(arg0.handle), arg0.type) + x_a = _semantic.sub(_semantic.mul(abs_x, 0.5, True), 2.0, True) + a_n_2 = 0 + a_n_1 = 0 + a_n = param1[0] + for i in range(1, 30): + a_n_2 = a_n_1 + a_n_1 = a_n + a_n = _semantic.sub(_semantic.mul(x_a, a_n_1, True), a_n_2, True) + a_n = _semantic.add(a_n, param1[i], True) + + f_32 = _semantic.full(abs_x.shape, 32.0, abs_x.type.scalar) + x_b = _semantic.sub(_semantic.fdiv(f_32, abs_x, True), 2.0, True) + b_n_2 = 0 + b_n_1 = 0 + b_n = param2[0] + for i in range(1, 25): + b_n_2 = b_n_1 + b_n_1 = b_n + b_n = _semantic.sub(_semantic.mul(x_b, b_n_1, True), b_n_2, True) + b_n = _semantic.add(b_n, param2[i], True) + + half_exp = _semantic.mul(core.tensor(_semantic.builder.create_exp(abs_x.handle), abs_x.type), 0.5, True) + res_a = _semantic.mul(half_exp, _semantic.sub(a_n, a_n_2, True), True) + res_b = _semantic.fdiv(_semantic.mul(half_exp, _semantic.sub(b_n, b_n_2, True), True), \ + core.tensor(_semantic.builder.create_sqrt(abs_x.handle), abs_x.type), True) + cond = _semantic.less_equal(abs_x, 8.0) + res = _semantic.where(cond, res_a, res_b) + return res + + +@core.extern +def cyl_bessel_i1(arg0, _semantic=None): + """ + Computes the modified Bessel function of the first kind, order 1, of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The modified Bessel function of the first kind, order 1, of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.cyl_bessel_i1 for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_cyl_bessel_i1_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +@math._check_dtype(dtypes=["fp16", "fp32"]) +def signbit(arg0, _semantic=None): + """ + Extracts the sign bit of x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The sign bit of x. + :rtype: ``int32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_signbit_fp16", core.dtype("int32")), + (core.dtype("fp32"), ): ("__hmf_signbit_fp32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0_scalar_ty = arg0.type.scalar + if arg0_scalar_ty == core.float32: + int_ty = core.int32 + else: # arg0 type: float16 / bfloat16 + int_ty = core.int16 + + arg0 = _semantic.to_tensor(arg0) + int_tensor = _semantic.bitcast(arg0, int_ty) + if int_ty == core.int32: + shift = 31 + elif int_ty == core.int16: + shift = 15 + + shift = _semantic.full(arg0.shape, shift, int_ty) + sign_bit_tensor = _semantic.lshr(int_tensor, shift) + sign_bit_tensor = _semantic.and_(sign_bit_tensor, _semantic.full(arg0.shape, 1, int_ty)) + return _semantic.equal(sign_bit_tensor, 1) + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def erf(arg0, _semantic=None): + """ + Computes the error function of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The error function of the input parameter. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_erf_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_erf(arg0.handle), arg0.type) + + +@core.extern +def erfc(arg0, _semantic=None): + """ + Computes the complementary error function of the input parameter, i.e., 1 - erf(x). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The complementary error function of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.erfc for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_erfc_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def erfcx(arg0, _semantic=None): + """ + Computes the scaled complementary error function of the input parameter, i.e., exp(x²) × erfc(x). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The scaled complementary error function of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.erfcx for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_erfcx_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def erfcinv(arg0, _semantic=None): + """ + Inverse complementary error function, finds the value y such that x = erfc(y). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse complementary error function of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.erfcxinv for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_erfcinv_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +# Note: +# For inputs x very close to ±1 (criterion: 1 - |x| < 1.1e-4), erfinv(x) → ±∞ and the +# inverse error function becomes extremely sensitive to tiny changes in x. The asymptotic +# behavior includes terms like sqrt(-ln(1-|x|)), so tiny relative changes in (1-|x|) map +# to large absolute changes in erfinv, leading to numerical instability and loss of precision, +# resulting in deviations from the reference results. +@core.extern +@math._check_dtype(dtypes=["fp32"]) +def erfinv(arg0, _semantic=None): + """ + Inverse error function, finds the value y such that x = erf(y). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse error function of the input parameter. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_erfinv_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0_scalar_ty = arg0.type.scalar + arg0 = _semantic.to_tensor(arg0) + + inv_sqrt_pi_times_2 = _semantic.full(arg0.shape, 1.128379167, arg0_scalar_ty).handle # 2 / sqrt(pi) + coeff_low_numerator = [-0.140543331, 0.914624893, -1.645349621, 0.886226899] + coeff_low_denominator = [0.012229801, -0.329097515, 1.442710462, -2.118377725, 1.0] + coeff_high_numerator = [1.641345311, 3.429567803, -1.624906493, -1.970840454] + coeff_high_denominator = [1.6370678, 3.5438892, 1.0] + + # low cal + arg0_squared = _semantic.builder.create_fmul(arg0.handle, arg0.handle) + numerator_low_range = _semantic.full(arg0.shape, coeff_low_numerator[0], arg0_scalar_ty).handle + for i in range(1, len(coeff_low_numerator)): + numerator_low_range = _semantic.builder.create_fma( + numerator_low_range, arg0_squared, + _semantic.full(arg0.shape, coeff_low_numerator[i], arg0_scalar_ty).handle) + + denominator_low_range = _semantic.full(arg0.shape, coeff_low_denominator[0], arg0_scalar_ty).handle + for i in range(1, len(coeff_low_denominator)): + denominator_low_range = _semantic.builder.create_fma( + denominator_low_range, arg0_squared, + _semantic.full(arg0.shape, coeff_low_denominator[i], arg0_scalar_ty).handle) + + low_res = _semantic.builder.create_fmul( + arg0.handle, _semantic.builder.create_fdiv(numerator_low_range, denominator_low_range)) + + # high cal + arg0_erf_trans = _semantic.builder.create_sqrt( # (log2-log(1-|arg0|))^1/2 + _semantic.builder.create_fmul( + _semantic.full(arg0.shape, -1, arg0_scalar_ty).handle, + _semantic.builder.create_log( + _semantic.builder.create_fdiv( + _semantic.builder.create_fsub( + _semantic.full(arg0.shape, 1, arg0_scalar_ty).handle, + _semantic.builder.create_fabs(arg0.handle)), + _semantic.full(arg0.shape, 2, arg0_scalar_ty).handle)))) + numerator_high_range = _semantic.full(arg0.shape, coeff_high_numerator[0], arg0_scalar_ty).handle + for i in range(1, len(coeff_high_numerator)): + numerator_high_range = _semantic.builder.create_fma( + numerator_high_range, arg0_erf_trans, + _semantic.full(arg0.shape, coeff_high_numerator[i], arg0_scalar_ty).handle) + + denominator_high_range = _semantic.full(arg0.shape, coeff_high_denominator[0], arg0_scalar_ty).handle + for i in range(1, len(coeff_high_denominator)): + denominator_high_range = _semantic.builder.create_fma( + denominator_high_range, arg0_erf_trans, + _semantic.full(arg0.shape, coeff_high_denominator[i], arg0_scalar_ty).handle) + + high_res = _semantic.builder.create_fdiv(numerator_high_range, denominator_high_range) + high_res = _semantic.mul( + _semantic.where( + signbit(arg0, _semantic=_semantic), + _semantic.full(arg0.shape, -1, arg0_scalar_ty), + _semantic.full(arg0.shape, 1, arg0_scalar_ty), + ), core.tensor(high_res, arg0.type), True).handle + + for _ in range(2): + low_res = _semantic.builder.create_fsub( + low_res, + _semantic.builder.create_fdiv( + _semantic.builder.create_fsub(_semantic.builder.create_erf(low_res), arg0.handle), + _semantic.builder.create_fmul( + inv_sqrt_pi_times_2, + _semantic.builder.create_exp( + _semantic.builder.create_fmul( + _semantic.full(arg0.shape, -1, arg0_scalar_ty).handle, + _semantic.builder.create_fmul(low_res, low_res)))))) + + high_res = _semantic.builder.create_fsub( + high_res, + _semantic.builder.create_fdiv( + _semantic.builder.create_fsub(_semantic.builder.create_erf(high_res), arg0.handle), + _semantic.builder.create_fmul( + inv_sqrt_pi_times_2, + _semantic.builder.create_exp( + _semantic.builder.create_fmul( + _semantic.full(arg0.shape, -1, arg0_scalar_ty).handle, + _semantic.builder.create_fmul(high_res, high_res)))))) + + arg0_abs = core.tensor(_semantic.builder.create_fabs(arg0.handle), arg0.type) + # Check if |arg0| > 1 + arg0_over = _semantic.greater_than(arg0_abs, _semantic.full(arg0.shape, 1, arg0_scalar_ty)) + nan_tensor = _semantic.full(arg0.shape, float("nan"), arg0_scalar_ty) + # Check if |arg0| = 1 + arg0_equal1 = _semantic.equal(arg0_abs, _semantic.full(arg0.shape, 1, arg0_scalar_ty)) + pos_inf_tensor = _semantic.full(arg0.shape, float("inf"), arg0_scalar_ty) + neg_inf_tensor = _semantic.full(arg0.shape, float("-inf"), arg0_scalar_ty) + inf_res = _semantic.where(signbit(arg0, _semantic=_semantic), neg_inf_tensor, pos_inf_tensor) + # Check if |arg0| >= 0.7 + arg0_high = _semantic.greater_equal(arg0_abs, _semantic.full(arg0.shape, 0.7, arg0_scalar_ty)) + + return _semantic.where( + arg0_equal1, inf_res, + _semantic.where( + arg0_over, nan_tensor, + _semantic.where(arg0_high, core.tensor(high_res, arg0.type), core.tensor(low_res, arg0.type)))) + + +@core.extern +def normcdf(arg0, _semantic=None): + """ + Computes the cumulative distribution function of the standard normal distribution. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The cumulative distribution function of the standard normal distribution. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.normcdf for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_normcdf_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def normcdfinv(arg0, _semantic=None): + """ + Computes the inverse of the cumulative distribution function of the standard normal distribution. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse of the cumulative distribution function of the standard normal distribution. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.normcdfinv for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_normcdfinv_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +# Note: +# The gamma function is implemented using the reflection formula for negative inputs: +# gamma(x) = pi / (sin(pi * x) * gamma(1 - x)). For inputs x close to a negative integer +# (e.g., -1, -2, ... ), criterion: x = -1 ± 0.66e-3, x = -2 ± 1.30e-3, x = -3 ± 2.30e-3, ... +# The denominator sin(pi * x) approaches zero, leading to numerical instability and loss +# of precision. Resulting in deviations from the reference results; +# Similar issues occur near other negative integers. +@core.extern +@math._check_dtype(dtypes=["fp32"]) +def gamma(arg0, _semantic=None): + """ + Computes the gamma function of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The gamma function of the input parameter. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_tgamma_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0_scalar_ty = arg0.type.scalar + arg0 = _semantic.to_tensor(arg0) + pi_tensor = _semantic.full(arg0.shape, math_pi, arg0_scalar_ty).handle + sqrt_2pi_tensor = _semantic.full(arg0.shape, 2.506628275, arg0_scalar_ty).handle # sqrt(2*pi) + lanczos_coeff = [ + 676.5203681218851, -1259.1392167224028, 771.32342877765313, -176.61502916214059, 12.507343278686905, + -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7 + ] + condition = _semantic.less_than(arg0, 0.5) # 1 - x = x -> x = 0.5 + reflect_arg0 = _semantic.where(condition, _semantic.sub(1, arg0, True), arg0) + + x = _semantic.full(arg0.shape, 0.99999999999980993, arg0_scalar_ty) + for i in range(0, len(lanczos_coeff)): + x = _semantic.add( + x, + _semantic.fdiv(_semantic.full(arg0.shape, lanczos_coeff[i], arg0_scalar_ty), + _semantic.add(reflect_arg0, i, True), True), True) + t = _semantic.add(reflect_arg0, 6.5, True) + + gamma_res = _semantic.builder.create_fmul( + _semantic.builder.create_fmul(sqrt_2pi_tensor, + pow(t, _semantic.sub(reflect_arg0, 0.5, True), _semantic=_semantic).handle), + _semantic.builder.create_fmul( + x.handle, + _semantic.builder.create_exp( + _semantic.builder.create_fmul(t.handle, + _semantic.full(arg0.shape, -1, arg0_scalar_ty).handle)))) + + gamma_res_reflect = _semantic.builder.create_fdiv( + _semantic.builder.create_fdiv(pi_tensor, gamma_res), + _semantic.builder.create_sin(_semantic.builder.create_fmul(pi_tensor, arg0.handle))) + + is_neg_int = _semantic.logical_and(_semantic.equal(math.floor(arg0, _semantic=_semantic), arg0), + _semantic.less_than(arg0, 0)) + pos_inf_tensor = _semantic.full(arg0.shape, float('inf'), arg0_scalar_ty) + neg_inf_tensor = _semantic.full(arg0.shape, float('-inf'), arg0_scalar_ty) + gamma_res_reflect = _semantic.where(is_neg_int, pos_inf_tensor, core.tensor(gamma_res_reflect, arg0.type)) + + res = _semantic.where(condition, gamma_res_reflect, core.tensor(gamma_res, arg0.type)) + is_pos_inf_input = _semantic.equal(arg0, pos_inf_tensor) + is_neg_inf_input = _semantic.equal(arg0, neg_inf_tensor) + + return _semantic.where(is_pos_inf_input, pos_inf_tensor, _semantic.where(is_neg_inf_input, neg_inf_tensor, res)) + + +@core.extern +def tgamma(arg0, _semantic=None): + """ + Computes the gamma function of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The gamma function of the input parameter. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.tgamma for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_tgamma_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +# Note: +# The lgamma function computes the natural logarithm of the absolute value of the gamma function. +# Since it uses gamma(x) internally, it inherits the same numerical instability near negative integers: +# For inputs x close to a negative integer (e.g., -1, -2, ...), criterion: x = -1 ± 5.75e-5, +# x = -2 ± 1.39e-6, ..., the computation involves log(|pi / (sin(pi * x) * gamma(1 - x))|). +# As sin(pi * x) approaches zero near negative integers, this leads to numerical instability and loss +# of precision, resulting in deviations from the reference results. +# Similar issues occur near other negative integers. +@core.extern +@math._check_dtype(dtypes=["fp32"]) +def lgamma(arg0, _semantic=None): + """ + Computes the natural logarithm of the absolute value of the gamma function for input x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The natural logarithm of the absolute value of the gamma function for input x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_lgamma_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + arg0_scalar_ty = arg0.type.scalar + arg0 = _semantic.to_tensor(arg0) + + inf_tensor = _semantic.full(arg0.shape, float('inf'), arg0_scalar_ty) + is_inf = _semantic.equal(core.tensor(_semantic.builder.create_fabs(arg0.handle), arg0.type), inf_tensor) + gamma_res = _semantic.builder.create_fabs(gamma(arg0, _semantic=_semantic).handle) + lgamma_res = _semantic.builder.create_log(gamma_res) + + return _semantic.where(is_inf, inf_tensor, core.tensor(lgamma_res, arg0.type)) + + +@core.builtin +@math._check_dtype(dtypes=[ + "fp32", +]) +def nearbyint(arg0: core.tensor, _semantic=None): + """ + Converts x to the nearest integer. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The nearest integer. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_nearbyint_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + """ + Round argument x to an integer value in floating-point format. + + Uses the current rounding mode (round-to-nearest-even, aka banker's rounding). + """ + arg0 = _semantic.to_tensor(arg0) + + half = _semantic.full(arg0.shape, 0.5, arg0.type.scalar) + + positive_adjust = _semantic.add(arg0, half, True) + negative_adjust = _semantic.sub(arg0, half, True) + + positive_result = core.tensor(_semantic.builder.create_floor(positive_adjust.handle), arg0.type) + negative_result = core.tensor(_semantic.builder.create_ceil(negative_adjust.handle), arg0.type) + + zero = _semantic.full(arg0.shape, 0.0, arg0.type.scalar) + is_positive = _semantic.greater_equal(arg0, zero) + basic_round = _semantic.where(is_positive, positive_result, negative_result) + + # Banker's rounding special treatment: For values exactly in the middle, round to the nearest even number. + fractional = _semantic.sub(arg0, basic_round, True) + abs_fractional = core.tensor(_semantic.builder.create_fabs(fractional.handle), fractional.type) + + is_half = _semantic.equal(abs_fractional, half) + + two = _semantic.full(arg0.shape, 2.0, arg0.type.scalar) + + half_value = math.fdiv(basic_round, two, _semantic=_semantic) + half_floor = core.tensor(_semantic.builder.create_floor(half_value.handle), half_value.type) + double_half = _semantic.mul(half_floor, two, True) + + is_even = _semantic.equal(basic_round, double_half) + + adjustment = _semantic.where(is_positive, _semantic.full(arg0.shape, -1.0, arg0.type.scalar), + _semantic.full(arg0.shape, 1.0, arg0.type.scalar)) + + banker_result = _semantic.where( + is_even, + basic_round, + _semantic.add(basic_round, adjustment, True), + ) + + # Final result: Use banker's rounding for cases exactly at 0.5, otherwise use basic rounding. + return _semantic.where(is_half, banker_result, basic_round) + + +@core.extern +def sinpi(arg0, _semantic=None): + """ + Computes the value of sin(π × x). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The value of sin(π × x). + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.sinpi for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_sinpi_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def cospi(arg0, _semantic=None): + """ + Computes the value of cos(π × x). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The value of cos(π × x). + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.cospi for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_cospi_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.builtin +@math._check_dtype(dtypes=[ + "fp32", +]) +def asin(arg0: core.tensor, _semantic=None): + """ + Computes the inverse sine (arcsin) of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The inverse sine of the input parameter, in the range [-π/2, π/2] radians. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_asin_fp16", core.dtype("fp16")), + (core.dtype("fp32"), ): ("__hmf_asin_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + """ + Calculate the principal value of the arc sine of the input argument x. + + Returns result in radians, in the interval [-π/2, +π/2] for x inside [-1, +1]. + Returns NaN for x outside [-1, +1]. + """ + arg0 = _semantic.to_tensor(arg0) + + # asin(x) = π/2 - acos(x) + half_pi = _semantic.full(arg0.shape, 1.5707963267948966, arg0.type.scalar) # π/2 + acos_val = acos(arg0, _semantic=_semantic) + return _semantic.sub(half_pi, acos_val, True) + + +@core.builtin +@math._check_dtype(dtypes=[ + "fp32", +]) +def log10(arg0: core.tensor, _semantic=None): + """ + Computes the base-10 logarithm of input x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The base-10 logarithm of input x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_log10_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + """ + Calculate the base 10 logarithm of the input argument x. + + Returns NaN for x < 0, -inf for x = 0, and +0 for x = 1. + log10(x) = log(x) / log(10) + """ + arg0 = _semantic.to_tensor(arg0) + + log_val = math.log(arg0, _semantic=_semantic) + log10_const = _semantic.full(arg0.shape, 2.302585092994046, arg0.type.scalar) + + return math.fdiv(log_val, log10_const, _semantic=_semantic) + + +@core.builtin +@math._check_dtype(dtypes=[ + "fp32", +]) +def copysign(arg0: core.tensor, arg1: core.tensor, _semantic=None): + """ + Generates a floating-point number with magnitude equal to the magnitude of x and sign equal to the sign of y. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: A floating-point number with magnitude equal to the magnitude of x and sign equal to the sign of y. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_copysign_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + else: + """ + Create a floating-point value with the magnitude of x and the sign of y. + """ + x = _semantic.to_tensor(arg0) + y = _semantic.to_tensor(arg1) + + magnitude = core.tensor(_semantic.builder.create_fabs(x.handle), x.type) + + zero = _semantic.full(y.shape, 0.0, y.type.scalar) + one = _semantic.full(y.shape, 1.0, y.type.scalar) + + is_zero = _semantic.equal(y, zero) + y_reciprocal = math.fdiv(one, y, _semantic=_semantic) + is_negative_reciprocal = _semantic.less_than(y_reciprocal, zero) + is_negative_zero = _semantic.and_(is_zero, is_negative_reciprocal) + + is_negative_nonzero = _semantic.less_than(y, zero) + is_negative = _semantic.or_(is_negative_zero, is_negative_nonzero) + + neg_magnitude = _semantic.mul(magnitude, _semantic.full(magnitude.shape, -1.0, magnitude.type.scalar), True) + + return _semantic.where(is_negative, neg_magnitude, magnitude) + + +@core.builtin +@math._check_dtype(dtypes=["fp16", "fp32", "bf16"]) +def rint(arg0: core.tensor, _semantic=None): + """ + Computes the nearest integer to x using round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The nearest integer to x. + :rtype: ``float32`` + """ + arg0 = _semantic.to_tensor(arg0) + if _is_a5_target(_semantic): + if arg0.dtype != core.dtype("fp32"): + arg0 = _semantic.cast(arg0, core.dtype("fp32")) + return core.extern_elementwise("", "", [ + arg0, + ], { + (core.dtype("fp32"), ): ("__hmf_rint_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + floor_x = math.floor(arg0, _semantic=_semantic) + fractional = _semantic.sub(arg0, floor_x, True) + + half = _semantic.full(arg0.shape, 0.5, arg0.type.scalar) + eps = _semantic.full(arg0.shape, 1e-8, arg0.type.scalar) + is_half = _semantic.less_than(math.abs(_semantic.sub(fractional, half, True), _semantic=_semantic), eps) + + floor_int = floor_x.to(core.int32, _semantic=_semantic) if hasattr(floor_x, "to") else _semantic.cast( + floor_x, core.int32) + two_i32 = _semantic.full(arg0.shape, 2, core.int32) + is_even = _semantic.equal(_semantic.mod(floor_int, two_i32), _semantic.full(arg0.shape, 0, core.int32)) + + zero = _semantic.full(arg0.shape, 0.0, arg0.type.scalar) + is_pos = _semantic.greater_equal(arg0, zero) + + round_pos = math.floor(_semantic.add(arg0, half, True), _semantic=_semantic) + round_neg = math.ceil(_semantic.sub(arg0, half, True), _semantic=_semantic) + normal_round = _semantic.where(is_pos, round_pos, round_neg) + + half_round = _semantic.where(is_even, floor_x, _semantic.add(floor_x, 1.0, True)) + + return _semantic.where(is_half, half_round, normal_round) + + +@core.extern +def llrint(arg0, _semantic=None): + """ + Rounds a floating-point number to the nearest 64-bit integer value. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The rounded 64-bit integer. + :rtype: ``int64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.llrint for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_llrint_fp32", core.dtype("int64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def llround(arg0, _semantic=None): + """ + Rounds a floating-point number to the nearest 64-bit integer value. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The rounded 64-bit integer. + :rtype: ``int64`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.llround for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_llround_fp32", core.dtype("int64")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def abs(arg0, _semantic=None): + """ + Computes the absolute value of the input parameter. + + :param arg0: ``x``. Supported dtype(s): ``int32``, ``float32``. + :type arg0: scalar or tl.tensor + :return: The absolute value of the input parameter. + :rtype: ``int32``, ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise( + "", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_abs_fp32", core.dtype("fp32")), + (core.dtype("int32"), ): ("__hmf_abs_i32", core.dtype("int32")), + }, is_pure=True, _semantic=_semantic) + + arg0 = _semantic.to_tensor(arg0) + dtype = arg0.dtype + if dtype.is_fp8e4b15(): + mask = core.full(arg0.shape, 0x7F, core.int8, _semantic=_semantic) + return core.tensor(_semantic.builder.create_and(arg0.handle, mask.handle), arg0.type) + if dtype.is_floating(): + return core.tensor(_semantic.builder.create_fabs(arg0.handle), arg0.type) + if dtype.is_int_signed(): + return core.tensor(_semantic.builder.create_iabs(arg0.handle), arg0.type) + if dtype.is_int_unsigned(): + return arg0 + assert False, f"Unexpected dtype {dtype}" + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def ceil(arg0, _semantic=None): + """ + Ceiling operation, returns the smallest integer greater than or equal to x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The ceiling result. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_ceil_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_ceil(arg0.handle), arg0.type) + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def cos(arg0, _semantic=None): + """ + Computes the cosine of the input parameter (in radians). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The cosine of the input parameter. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_cos_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_cos(arg0.handle), arg0.type) + + +@core.extern +@math._check_dtype(dtypes=["fp32"]) +def div_rn(arg0, arg1, _semantic=None): + """ + Floating-point division with round-to-nearest-even rounding mode. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :return: The division result. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0, arg1], { + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_div_rn_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + arg0, arg1 = core.binary_op_type_legalization(arg0, arg1, _semantic) + return core.tensor(_semantic.builder.create_precise_divf(arg0.handle, arg1.handle), arg0.type) + + +@core.builtin +@math._add_math_2arg_docstr("division") +def fdiv(arg0, arg1, ieee_rounding=False, _semantic=None): + ieee_rounding = core._unwrap_if_constexpr(ieee_rounding) + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + return _semantic.fdiv(arg0, arg1, ieee_rounding) + + +@core.extern +@math._check_dtype(dtypes=["fp16", "fp32", "fp64"]) +def exp(arg0, _semantic=None): + """ + Exponential function, computes e raised to the power of x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of e raised to the power of x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_exp_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_exp(arg0.handle), arg0.type) + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def exp2(arg0, _semantic=None): + """ + Base-2 exponential function, computes 2 raised to the power of x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The result of 2 raised to the power of x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_exp2_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_exp2(arg0.handle), arg0.type) + + +@core.extern +def float2half_rn(arg0, _semantic=None): + """ + Converts x from a 32-bit floating-point value to a 16-bit floating-point value using round-to-nearest-even. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The converted 16-bit floating-point value. + :rtype: ``float16`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.float2half_rn for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_float2half_rn_fp32", core.dtype("fp16")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def floor(arg0, _semantic=None): + """ + Floor operation, returns the largest integer less than or equal to x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The floor result. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_floor_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_floor(arg0.handle), arg0.type) + + +@core.extern +def fma(arg0, arg1, arg2, _semantic=None): + """ + Fused multiply-add, computes x × y + z. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``float32``. + :type arg1: scalar or tl.tensor + :param arg2: ``z``. Supported dtype(s): ``float32``. + :type arg2: scalar or tl.tensor + :return: The result of fused multiply-add. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0, arg1, arg2], { + (core.dtype("fp32"), core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fma_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + arg1 = _semantic.to_tensor(arg1) + arg2 = _semantic.to_tensor(arg2) + arg0, arg1 = core.binary_op_type_legalization(arg0, arg1, _semantic) + arg2, arg0 = core.binary_op_type_legalization(arg2, arg0, _semantic) + arg2, arg1 = core.binary_op_type_legalization(arg2, arg1, _semantic) + return core.tensor(_semantic.builder.create_fma(arg0.handle, arg1.handle, arg2.handle), arg0.type) + + +@core.extern +def max(arg0, arg1, _semantic=None): + """ + Computes the element-wise maximum of x and y. + + :param arg0: ``x``. Supported dtype(s): ``int32``, ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``int32``, ``float32``; must have the same type as x. + :type arg1: scalar or tl.tensor + :return: The element-wise maximum of x and y. + :rtype: Same as the input type (``int32`` or ``float32``) + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.max for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("int32"), core.dtype("int32")): ("__hmf_max_i32", core.dtype("int32")), + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fmax_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def min(arg0, arg1, _semantic=None): + """ + Computes the element-wise minimum of x and y. + + :param arg0: ``x``. Supported dtype(s): ``int32``, ``float32``. + :type arg0: scalar or tl.tensor + :param arg1: ``y``. Supported dtype(s): ``int32``, ``float32``; must have the same type as x. + :type arg1: scalar or tl.tensor + :return: The element-wise minimum of x and y. + :rtype: Same as the input type (``int32`` or ``float32``) + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.min for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise( + "", "", [arg0, arg1], { + (core.dtype("int32"), core.dtype("int32")): ("__hmf_min_i32", core.dtype("int32")), + (core.dtype("fp32"), core.dtype("fp32")): ("__hmf_fmin_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +def half2float(arg0, _semantic=None): + """ + Converts x from a 16-bit floating-point value to a 32-bit floating-point value. + + :param arg0: ``x``. Supported dtype(s): ``float16``. + :type arg0: scalar or tl.tensor + :return: The converted 32-bit floating-point value. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.half2float for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp16"), ): ("__hmf_half2float_fp16", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def log(arg0, _semantic=None): + """ + Computes the natural (base-e) logarithm of input x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The natural logarithm of input x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_log_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_log(arg0.handle), arg0.type) + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def log2(arg0, _semantic=None): + """ + Computes the base-2 logarithm of input x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The base-2 logarithm of input x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_log2_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_log2(arg0.handle), arg0.type) + + +@core.extern +def nan(arg0, _semantic=None): + """ + Generates a NaN value from x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The generated NaN value. + :rtype: ``float32`` + """ + if not _is_a5_target(_semantic): + core.static_print("libdevice.nan for simd is unsupported for now.") + core.static_assert(False) + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_nan_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def rsqrt(arg0, _semantic=None): + """ + Computes the reciprocal square root of x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The reciprocal square root of x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_rsqrt_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_rsqrt(arg0.handle), arg0.type) + + +@core.extern +def sin(arg0, _semantic=None): + """ + Computes the sine of the input parameter x (in radians). + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The sine of input x. + :rtype: ``float32`` + """ + arg0 = _semantic.to_tensor(arg0) + if arg0.dtype == core.dtype("fp32") and _is_a5_target(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_sin_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + return core.tensor(_semantic.builder.create_sin(arg0.handle), arg0.type) + + +@core.extern +@math._check_dtype(dtypes=["fp32", "fp64"]) +def sqrt(arg0, _semantic=None): + """ + Computes the square root of x. + + :param arg0: ``x``. Supported dtype(s): ``float32``. + :type arg0: scalar or tl.tensor + :return: The square root of x. + :rtype: ``float32`` + """ + if _is_libdevice_simt_enabled(_semantic): + return core.extern_elementwise("", "", [arg0], { + (core.dtype("fp32"), ): ("__hmf_sqrt_fp32", core.dtype("fp32")), + }, is_pure=True, _semantic=_semantic) + arg0 = _semantic.to_tensor(arg0) + return core.tensor(_semantic.builder.create_sqrt(arg0.handle), arg0.type) + + +@core.extern +@_deprecated(fn_name="triton.language.extra.cann.libdevice.uhadd", + replacement="triton.language.extra.cann.libdevice.hadd") +def uhadd(arg0, arg1, _semantic=None): + return hadd(arg0, arg1, _semantic=_semantic) + + +@core.extern +@_deprecated(fn_name="triton.language.extra.cann.libdevice.umul24", + replacement="triton.language.extra.cann.libdevice.mul24") +def umul24(arg0, arg1, _semantic=None): + return mul24(arg0, arg1, _semantic=_semantic) + + +@core.extern +@_deprecated(fn_name="triton.language.extra.cann.libdevice.umulhi", + replacement="triton.language.extra.cann.libdevice.mulhi") +def umulhi(arg0, arg1, _semantic=None): + return mulhi(arg0, arg1, _semantic=_semantic) + + +@core.extern +@_deprecated(fn_name="triton.language.extra.cann.libdevice.urhadd", + replacement="triton.language.extra.cann.libdevice.rhadd") +def urhadd(arg0, arg1, _semantic=None): + return rhadd(arg0, arg1, _semantic=_semantic) + + +@core.extern +@_deprecated(fn_name="triton.language.extra.cann.libdevice.usad", + replacement="triton.language.extra.cann.libdevice.sad") +def usad(arg0, arg1, arg2, _semantic=None): + return sad(arg0, arg1, arg2, _semantic=_semantic) diff --git a/third_party/ascend/language/cann/utils.py b/third_party/ascend/language/cann/utils.py new file mode 100644 index 0000000000..3e4c3f74cc --- /dev/null +++ b/third_party/ascend/language/cann/utils.py @@ -0,0 +1,38 @@ +import inspect +from functools import wraps +from warnings import warn +from triton.runtime.jit import JITFunction + +_DEPRECATED_MESSAGE_ATTR = "_deprecated_message" + + +def _deprecated(fn_name=None, replacement=None): + + def decorator(fn): + name = fn_name or f"triton.language.extra.cann.extension.{fn.__name__}" + message = (f"{name} is deprecated and will be removed in the next release" + f"{f'; use {replacement} instead.' if replacement else '.'}") + + if inspect.isclass(fn): + setattr(fn, _DEPRECATED_MESSAGE_ATTR, message) + fn.__doc__ = f"{fn.__doc__ or ''}\n\n.. warning::\n {message}" + return fn + + if isinstance(fn, JITFunction): + # Wrapping a JITFunction in a plain function breaks both the host + # launch (`fn[grid](...)`) and the in-kernel inline path, since the + # code generator would no longer see a JITFunction. Warn at import + # time and pass the function through untouched. + warn(message, FutureWarning, stacklevel=2) + fn.__doc__ = f"{fn.__doc__ or ''}\n\n.. warning::\n {message}" + return fn + + @wraps(fn) + def wrapper(*args, **kwargs): + warn(message, FutureWarning, stacklevel=2) + return fn(*args, **kwargs) + + wrapper.__doc__ = f"{fn.__doc__ or ''}\n\n.. warning::\n {message}" + return wrapper + + return decorator diff --git a/third_party/ascend/language/kernels/__init__.py b/third_party/ascend/language/kernels/__init__.py new file mode 100644 index 0000000000..bd828caca2 --- /dev/null +++ b/third_party/ascend/language/kernels/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Optimized Triton kernels for Ascend NPU. + +This module provides high-level, optimized kernels written in Triton for common operations. +These are different from the low-level builtin extensions in `cann.extension`. +""" +__all__ = ["gather_2d_simd"] + +from .gather import gather_2d_simd diff --git a/third_party/ascend/language/kernels/gather.py b/third_party/ascend/language/kernels/gather.py new file mode 100644 index 0000000000..ad22f8f637 --- /dev/null +++ b/third_party/ascend/language/kernels/gather.py @@ -0,0 +1,92 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Gather kernel optimized for Ascend NPU. +""" +__all__ = ["gather_2d_simd"] + +import triton +import triton.language as tl +from triton.language.core import constexpr + + +@triton.jit +def gather_2d_simd(src_ptr, idx_ptr, out_ptr, M: constexpr, N: constexpr, K: constexpr, XBLOCK: constexpr, + XBLOCK_SUB: constexpr): + """ + 2D gather kernel for axis=1 (tail axis) with SIMD-style vectorization. + + This kernel is optimized for Ascend NPU architecture with focus on: + - Vectorized memory access using XBLOCK_SUB + - Efficient global memory (GM) access patterns + - Suitable for cases where N and K are not extremely large + + Args: + src_ptr: [M, N] source tensor in GM (Global Memory) + idx_ptr: [M, K] indices tensor in GM + out_ptr: [M, K] output tensor in GM + M: batch dimension size + N: source dimension size (gather from this dimension) + K: output dimension size (number of indices per batch) + XBLOCK: outer block size for M dimension (for program distribution) + XBLOCK_SUB: inner block size for M dimension (for SIMD vectorization) + + Example: + import torch + import triton + from third_party.ascend.language.kernels import gather_2d_simd + + M, N, K = 128, 256, 64 + src = torch.randn(M, N, device='npu') + indices = torch.randint(0, N, (M, K), dtype=torch.int32, device='npu') + output = torch.empty((M, K), dtype=src.dtype, device='npu') + + grid = (triton.cdiv(M, 32),) + gather_2d_simd[grid](src, indices, output, M, N, K, + XBLOCK=32, XBLOCK_SUB=4) + """ + tl.static_print("[UserWarning: `gather_2d_simd` kernel is deprecated and will be removed " + "from `third_party/ascend/language/kernels/gather.py`. " + "It has been moved to `third_party/ascend/tutorials/` as an example kernel. " + "Please update your imports accordingly.]") + pid = tl.program_id(0) + m_start = pid * XBLOCK + m_end = min(m_start + XBLOCK, M) + m_base = tl.arange(0, XBLOCK_SUB) + + # Process multiple rows at once using XBLOCK_SUB for vectorization + for m_tile_start in range(m_start, m_end, XBLOCK_SUB): + # M dimension offsets: [XBLOCK_SUB] + m_offs = m_tile_start + m_base + m_mask = m_offs < M + + # Load indices: [XBLOCK_SUB, K] + k_offs = tl.arange(0, K) + idx_tile = tl.load(idx_ptr + m_offs[:, None] * K + k_offs[None, :]) + + # Load source data: [XBLOCK_SUB, N] + n_offs = tl.arange(0, N) + src_tile = tl.load(src_ptr + m_offs[:, None] * N + n_offs[None, :]) + + # Gather operation along axis=1 + gathered_values = tl.gather(src_tile, idx_tile, axis=1) + + # Store results + tl.store(out_ptr + m_offs[:, None] * K + k_offs[None, :], gathered_values, mask=m_mask[:, None]) diff --git a/third_party/ascend/lib/AutoBlockify/AutoBlockify.cpp b/third_party/ascend/lib/AutoBlockify/AutoBlockify.cpp new file mode 100644 index 0000000000..009b34dee9 --- /dev/null +++ b/third_party/ascend/lib/AutoBlockify/AutoBlockify.cpp @@ -0,0 +1,363 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "AutoBlockify/AutoBlockify.h" +#include "AutoBlockify/Utils.h" +#include "Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "Utils/Utils.h" + +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" + +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "auto-blockify" + +using namespace mlir; +using namespace triton; + +PropagateUnrealizedCastDown::PropagateUnrealizedCastDown(MLIRContext *context, + Value logicalBlockId, + Value logicalBlockNum, + int autoBlockifySize) + : OpRewritePattern(context), + logicalBlockId(logicalBlockId), logicalBlockNum(logicalBlockNum), + autoBlockifySize(autoBlockifySize) {} + +LogicalResult +PropagateUnrealizedCastDown::matchAndRewrite(UnrealizedConversionCastOp op, + PatternRewriter &rewriter) const { + if (op.getInputs().size() != 2) + return failure(); + auto funcOp = op->getParentOfType(); + auto input = op.getInputs()[0]; + auto res = op->getResult(0); + SmallPtrSet users(op->user_begin(), op->user_end()); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Handling UnrealizedConversionCastOp:\n" << op << "\n"; + os << "Users:\n"; + for (auto *user : users) + os << *user << "\n"; + }); + for (auto *user : users) { + PatternRewriter::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(user); + if (auto uccOp = dyn_cast(user)) { + if (uccOp->getResultTypes()[0] != input.getType()) { + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << *user << "\n"; + }); + return op.emitError("UnrealizedConversionCastOp cannot be resolved\n"); + } + rewriter.replaceOp(user, input); + } else if (auto blockifyLoop = getBlockifyLoop(user)) { + handleBlockifyLoop(blockifyLoop.value(), user, rewriter); + } else if (auto splatOp = dyn_cast(user)) { + rewriteSplat(op, splatOp, rewriter); + } else if (auto expandDimsOp = dyn_cast(user)) { + rewriteExpandDims(op, expandDimsOp, rewriter); + } else if (auto reduceOp = dyn_cast(user)) { + rewriteReduce(op, reduceOp, rewriter); + } else if (auto scanOp = dyn_cast(user)) { + rewriteScan(op, scanOp, rewriter); + } else if (auto loadOp = dyn_cast(user)) { + rewriteLoad(op, loadOp, rewriter); + } else if (auto storeOp = dyn_cast(user)) { + rewriteStore(op, storeOp, rewriter); + } else if (auto atomicRMWOp = dyn_cast(user)) { + rewriteAtomicRMW(op, atomicRMWOp, rewriter); + } else if (auto assertOp = dyn_cast(user)) { + rewriteAssert(op, assertOp, rewriter); + } else if (auto extractSliceOp = dyn_cast(user)) { + rewriteExtractSlice(op, extractSliceOp, rewriter); + } else if (auto insertSliceOp = dyn_cast(user)) { + rewriteInsertSlice(op, insertSliceOp, rewriter); + } else if (auto whileOp = dyn_cast(user)) { + rewriteWhile(op, whileOp, rewriter); + } else if (auto loopOp = dyn_cast(user)) { + rewriteLoop(op, loopOp, rewriter); + } else if (auto yieldOp = dyn_cast(user)) { + rewriteYield(op, yieldOp, rewriter); + } else if (auto conditionOp = dyn_cast(user)) { + rewriteCondition(op, conditionOp, rewriter); + } else if (user->hasTrait() || + isa(user)) { + rewriteGeneraleOp(op, user, rewriter); + } else if (isa(user)) { + auto *newOp = + createBlockifyLoop(user, op, logicalBlockId, logicalBlockNum, + autoBlockifySize, rewriter); + rewriter.setInsertionPoint(newOp); + handleBlockifyLoop(*getBlockifyLoop(newOp), newOp, rewriter); + } else { + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Unhandled Op\n" << *user << "\n"; + }); + llvm_unreachable("Unhandled operation"); + } + } + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "After successful conversion\n"; + os << funcOp << "\n"; + }); + rewriter.eraseOp(op); + return success(); +} + +AutoBlockifyPass::AutoBlockifyPass(const AutoBlockifyOptions &options) + : AutoBlockifyBase(options) {} + +bool AutoBlockifyPass::checkBlockifiable(Value v) { + if (!checkedValues.insert(v).second) + return true; + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Checking blockifiable:\n" << v << "\n"; + }); + for (auto &use : v.getUses()) { + auto *user = use.getOwner(); + auto opNum = use.getOperandNumber(); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "User:\n" << *user << "\n"; + }); + if (isa( + user) || + llvm::any_of(user->getOperandTypes(), isTensorPtrType)) + return false; + if (auto ifOp = dyn_cast(user)) { + user->setAttr(autoBlockifyRegionOpAttr, UnitAttr::get(v.getContext())); + return true; + } else if (auto whileOp = dyn_cast(user)) { + if (!checkBlockifiable(whileOp.getBeforeArguments()[opNum])) + return false; + } else if (auto loopOp = dyn_cast(user)) { + auto regionIterArg = loopOp.getTiedLoopRegionIterArg(&use); + auto loopResult = loopOp.getTiedLoopResult(&use); + if (!regionIterArg || !loopResult) { + user->setAttr(autoBlockifyRegionOpAttr, UnitAttr::get(v.getContext())); + return true; + } + if (!checkBlockifiable(regionIterArg) || !checkBlockifiable(loopResult)) + return false; + } else if (auto conditionOp = dyn_cast(user)) { + auto whileOp = cast(user->getParentOp()); + if (opNum == 0) { + whileOp->setAttr(autoBlockifyRegionOpAttr, + UnitAttr::get(v.getContext())); + return true; + } + if (!checkBlockifiable(whileOp.getAfterArguments()[opNum - 1]) || + !checkBlockifiable(whileOp->getResult(opNum - 1))) + return false; + } else if (auto conditionOp = dyn_cast(user)) { + if (auto loopOp = dyn_cast(user->getParentOp()); + loopOp && !checkBlockifiable(loopOp.getInits()[opNum])) + return false; + } else { + for (auto res : user->getResults()) { + if (!checkBlockifiable(res)) + return false; + } + } + } + return true; +} + +void AutoBlockifyPass::preProcess(triton::FuncOp func) { + IRRewriter rewriter(func.getContext()); + rewriter.setInsertionPointToStart(&func.getBody().front()); + auto loc = rewriter.getUnknownLoc(); + // Get logical block num + auto xNum = + rewriter.create(loc, triton::ProgramIDDim::X); + auto yNum = + rewriter.create(loc, triton::ProgramIDDim::Y); + auto zNum = + rewriter.create(loc, triton::ProgramIDDim::Z); + auto yzNum = rewriter.create(loc, yNum, zNum); + logicalBlockNum = rewriter.create(loc, yzNum, xNum); + + // Get logical block id + auto xDim = + rewriter.create(loc, triton::ProgramIDDim::X); + auto yDim = + rewriter.create(loc, triton::ProgramIDDim::Y); + auto zDim = + rewriter.create(loc, triton::ProgramIDDim::Z); + xDim->setAttr(logicalBlockIdAttr, rewriter.getUnitAttr()); + yDim->setAttr(logicalBlockIdAttr, rewriter.getUnitAttr()); + zDim->setAttr(logicalBlockIdAttr, rewriter.getUnitAttr()); + auto xFlatten = rewriter.create(loc, xDim, yzNum); + auto yFlatten = rewriter.create(loc, yDim, zNum); + logicalBlockId = rewriter.create(loc, xFlatten, yFlatten); + logicalBlockId = rewriter.create(loc, logicalBlockId, zDim); + + // get blockified block id + auto blockifyTensorType = + RankedTensorType::get({autoBlockifySize}, rewriter.getI32Type()); + auto blockfyRange = rewriter.create( + loc, blockifyTensorType, 0, autoBlockifySize); + auto splatedLogicalBlockId = rewriter.create( + loc, blockfyRange.getType(), logicalBlockId); + Value blockifiedId = + rewriter.create(loc, splatedLogicalBlockId, blockfyRange); + + // get mask + auto splatedBlockNum = rewriter.create( + loc, blockfyRange.getType(), logicalBlockNum); + auto upperboundMask = rewriter.create( + loc, arith::CmpIPredicate::slt, blockifiedId, splatedBlockNum); + auto splatedZero = rewriter.create( + loc, DenseElementsAttr::get(blockifyTensorType, + rewriter.getI32IntegerAttr(0))); + auto lowerboundMask = rewriter.create( + loc, arith::CmpIPredicate::sge, blockifiedId, splatedZero); + Value blockifiedIdMask = + rewriter.create(loc, upperboundMask, lowerboundMask); + + blockifiedId = rewriter + .create( + loc, logicalBlockId.getType(), + ValueRange({blockifiedId, blockifiedIdMask})) + ->getResult(0); + + // replace program id to be computed from blockified id + SmallVector toReplace; + func.walk([&](triton::GetProgramIdOp id) { + if (id->hasAttr(logicalBlockIdAttr)) + return; + toReplace.push_back(id); + }); + for (auto id : toReplace) { + rewriter.setInsertionPoint(id); + Value newId; + if (id.getAxis() == triton::ProgramIDDim::X) { + newId = rewriter.create(id.getLoc(), blockifiedId, yzNum); + newId = rewriter.create(id.getLoc(), newId, xNum); + } else if (id.getAxis() == triton::ProgramIDDim::Y) { + newId = rewriter.create(id.getLoc(), blockifiedId, zNum); + newId = rewriter.create(id.getLoc(), newId, yNum); + } else { + newId = rewriter.create(id.getLoc(), blockifiedId, zNum); + } + rewriter.replaceOp(id, newId); + } + + // Create for loop for region ops + func.walk([&](Operation *op) { + if (op->hasAttr(autoBlockifyRegionOpAttr)) { + auto *newOp = createBlockifyLoop( + op, blockifiedId.getDefiningOp(), + logicalBlockId, logicalBlockNum, autoBlockifySize, rewriter); + newOp->removeAttr(autoBlockifyRegionOpAttr); + return WalkResult::skip(); + } + return WalkResult::advance(); + }); +} + +void AutoBlockifyPass::runOnOperation() { + if (autoBlockifySize == 1) + return; + ModuleOp moduleOp = getOperation(); + if (autoBlockifySize <= 0) { + moduleOp->emitWarning("[AutoBlockify V2] AutoBlockifySize cannot be " + "negative integer, skipping."); + return signalPassFailure(); + } + + MLIRContext *ctx = &getContext(); + + moduleOp.walk([&](triton::FuncOp func) { + LogicalResult result = success(); + func.walk([&](triton::GetProgramIdOp id) { + if (!checkBlockifiable(id.getResult())) { + result = failure(); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (failed(result)) { + func->emitWarning("Cannot apply auto blockify"); + return WalkResult::skip(); + } + preProcess(func); + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "After preprocess:\n" << func << "\n"; + }); + + RewritePatternSet patterns(ctx); + patterns.add( + ctx, logicalBlockId, logicalBlockNum, autoBlockifySize); + + if (failed(applyPatternsGreedily(func, std::move(patterns)))) { + moduleOp->emitError("failed to apply Patterns"); + signalPassFailure(); + return WalkResult::interrupt(); + } + + IRRewriter rewriter(ctx); + func->walk([&](UnrealizedConversionCastOp op) { + rewriter.setInsertionPoint(op); + auto input = op.getInputs()[0]; + auto resType = cast(op->getResultTypes()[0]); + if (auto constantOp = input.getDefiningOp()) { + Attribute val = constantOp.getValue(); + if (auto denseAttr = dyn_cast(val)) + val = denseAttr.getSplatValue(); + rewriter.replaceOpWithNewOp( + op, DenseElementsAttr::get(resType, val)); + } else if (auto tensorType = + dyn_cast(input.getType())) { + input = rewriter.create(input.getLoc(), input, 0); + rewriter.replaceOpWithNewOp(op, resType, input); + } else { + rewriter.replaceOpWithNewOp(op, resType, input); + } + }); + func->setAttr(autoBlockifySizeAttr, + rewriter.getI32IntegerAttr(autoBlockifySize)); + return WalkResult::skip(); + }); + + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + if (failed(runPipeline(pm, moduleOp))) { + signalPassFailure(); + } +} + +std::unique_ptr> +triton::createAutoBlockifyPass(const AutoBlockifyOptions &options) { + return std::make_unique(options); +} diff --git a/third_party/ascend/lib/AutoBlockify/CMakeLists.txt b/third_party/ascend/lib/AutoBlockify/CMakeLists.txt new file mode 100644 index 0000000000..20ffce4753 --- /dev/null +++ b/third_party/ascend/lib/AutoBlockify/CMakeLists.txt @@ -0,0 +1,22 @@ +add_triton_library(AutoBlockify + AutoBlockify.cpp + RewriteOperation.cpp + Utils.cpp + + DEPENDS + AutoBlockifyPassIncGen + + LINK_LIBS PUBLIC + MLIRArithDialect + MLIRDialectUtils + MLIRIR + MLIRMathDialect + MLIRPass + MLIRTensorDialect + TritonIR + TritonTransforms + TritonAnalysis + MLIRTransforms + MLIRSupport + MLIRSCFTransforms +) diff --git a/third_party/ascend/lib/AutoBlockify/RewriteOperation.cpp b/third_party/ascend/lib/AutoBlockify/RewriteOperation.cpp new file mode 100644 index 0000000000..2a1847149b --- /dev/null +++ b/third_party/ascend/lib/AutoBlockify/RewriteOperation.cpp @@ -0,0 +1,512 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "AutoBlockify/AutoBlockify.h" +#include "AutoBlockify/Utils.h" +#include "Utils/Utils.h" + +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "auto-blockify-rewrite-operation" + +using namespace mlir; +using namespace triton; + +void PropagateUnrealizedCastDown::handleBlockifyLoop( + scf::ForOp blockifyLoop, Operation *op, PatternRewriter &rewriter) const { + SmallVector newOperands; + for (auto opr : op->getOperands()) { + auto uccOp = opr.getDefiningOp(); + if (!uccOp) { + newOperands.push_back(opr); + continue; + } + auto input = uccOp.getInputs()[0]; + auto tensorType = cast(input.getType()); + Value newOperand; + if (tensorType.getRank() > 1) { + SmallVector offsets(tensorType.getRank(), + rewriter.getIndexAttr(0)); + SmallVector sizes(1, rewriter.getIndexAttr(1)); + SmallVector strides(tensorType.getRank(), + rewriter.getIndexAttr(1)); + offsets[0] = blockifyLoop.getInductionVar(); + for (auto dim : llvm::drop_begin(tensorType.getShape())) + sizes.push_back(rewriter.getIndexAttr(dim)); + newOperand = rewriter.create( + input.getLoc(), cast(opr.getType()), input, offsets, + sizes, strides); + } else { + newOperand = rewriter.create( + input.getLoc(), input, ValueRange{blockifyLoop.getInductionVar()}); + if (isa(opr.getType())) { + newOperand = rewriter.create( + input.getLoc(), rewriter.getIndexType(), newOperand); + } + } + newOperands.push_back(newOperand); + } + rewriter.modifyOpInPlace(op, [&]() { op->setOperands(newOperands); }); +} + +void PropagateUnrealizedCastDown::rewriteGeneraleOp( + UnrealizedConversionCastOp op, Operation *generalOp, + PatternRewriter &rewriter) const { + auto input = op.getInputs()[0]; + auto mask = op.getInputs()[1]; + auto res = op->getResult(0); + auto inputType = cast(input.getType()); + SmallVector newOperands; + SmallVector newResults; + SmallVector newResultTypes; + + for (auto operand : generalOp->getOperands()) + newOperands.push_back(rewriteValue(operand, op, rewriter)); + for (auto resType : generalOp->getResultTypes()) { + newResultTypes.push_back(getExpandedType(resType, op)); + } + auto *newOp = + rewriter.create(generalOp->getLoc(), generalOp->getName().getIdentifier(), + newOperands, newResultTypes, generalOp->getAttrs()); + replaceValue(newOp, generalOp, mask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteSplat( + UnrealizedConversionCastOp op, triton::SplatOp splatOp, + PatternRewriter &rewriter) const { + auto input = op.getInputs()[0]; + auto mask = op.getInputs()[1]; + auto resType = cast(splatOp.getResult().getType()); + auto curShape = + llvm::to_vector(cast(input.getType()).getShape()); + auto splatedShape = resType.getShape(); + for (auto dim : splatedShape) { + input = rewriter.create(input.getLoc(), input, + curShape.size()); + curShape.push_back(dim); + input = rewriter.create( + input.getLoc(), + RankedTensorType::get(curShape, getElementTypeOrSelf(input)), input); + } + replaceValue(input.getDefiningOp(), splatOp, mask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteExpandDims( + UnrealizedConversionCastOp op, triton::ExpandDimsOp expandDimsOp, + PatternRewriter &rewriter) const { + auto input = op.getInputs()[0]; + auto mask = op.getInputs()[1]; + auto newOp = rewriter.create( + expandDimsOp.getLoc(), input, expandDimsOp.getAxis() + 1); + for (auto attr : expandDimsOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + replaceValue(newOp, expandDimsOp, mask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteReduce( + UnrealizedConversionCastOp op, triton::ReduceOp reduceOp, + PatternRewriter &rewriter) const { + auto mask = op.getInputs()[1]; + auto srcs = llvm::map_to_vector(reduceOp.getSrcs(), [&](Value src) { + return rewriteValue(src, op, rewriter); + }); + auto newOp = rewriter.create(reduceOp.getLoc(), srcs, + reduceOp.getAxis() + 1); + auto &newCombineOp = newOp.getCombineOp(); + rewriter.cloneRegionBefore(reduceOp.getCombineOp(), newCombineOp, + newCombineOp.end()); + for (auto attr : reduceOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + replaceValue(newOp, reduceOp, mask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteScan(UnrealizedConversionCastOp op, + triton::ScanOp scanOp, + PatternRewriter &rewriter) const { + auto mask = op.getInputs()[1]; + auto srcs = llvm::map_to_vector(scanOp.getSrcs(), [&](Value src) { + return rewriteValue(src, op, rewriter); + }); + auto newOp = rewriter.create( + scanOp.getLoc(), srcs, scanOp.getAxis() + 1, scanOp.getReverse()); + auto &newCombineOp = newOp.getCombineOp(); + rewriter.cloneRegionBefore(scanOp.getCombineOp(), newCombineOp, + newCombineOp.end()); + for (auto attr : scanOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + replaceValue(newOp, scanOp, mask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteLoad(UnrealizedConversionCastOp op, + triton::LoadOp loadOp, + PatternRewriter &rewriter) const { + auto uccMask = op.getInputs()[1]; + auto ptr = rewriteValue(loadOp.getPtr(), op, rewriter); + auto other = rewriteValue(loadOp.getOther(), op, rewriter); + auto mask = rewriteValue(loadOp.getMask(), op, rewriter); + auto res = loadOp.getResult(); + auto resType = getExpandedType(res.getType(), op); + if (!other) { + other = rewriter.create( + rewriter.getUnknownLoc(), + DenseElementsAttr::get( + resType, rewriter.getZeroAttr(getElementTypeOrSelf(res)))); + } + mask = createMask(mask, uccMask, resType.getShape(), rewriter); + auto boundaryCheck = llvm::map_to_vector(loadOp.getBoundaryCheck(), + [](int32_t idx) { return idx + 1; }); + auto newOp = rewriter.create( + loadOp.getLoc(), ptr, mask, other, boundaryCheck, loadOp.getPadding(), + loadOp.getCache(), loadOp.getEvict(), loadOp.getIsVolatile()); + for (auto attr : loadOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + replaceValue(newOp, loadOp, uccMask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteStore( + UnrealizedConversionCastOp op, triton::StoreOp storeOp, + PatternRewriter &rewriter) const { + auto uccMask = op.getInputs()[1]; + auto ptr = rewriteValue(storeOp.getPtr(), op, rewriter); + auto value = rewriteValue(storeOp.getValue(), op, rewriter); + auto mask = rewriteValue(storeOp.getMask(), op, rewriter); + auto ptrShape = cast(ptr.getType()).getShape(); + mask = createMask(mask, uccMask, ptrShape, rewriter); + auto boundaryCheck = llvm::map_to_vector(storeOp.getBoundaryCheck(), + [](int32_t idx) { return idx + 1; }); + auto newOp = rewriter.create( + storeOp.getLoc(), ptr, value, mask, boundaryCheck, storeOp.getCache(), + storeOp.getEvict()); + for (auto attr : storeOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + rewriter.replaceOp(storeOp, newOp); +} + +void PropagateUnrealizedCastDown::rewriteAtomicRMW( + UnrealizedConversionCastOp op, triton::AtomicRMWOp atomicRMWOp, + PatternRewriter &rewriter) const { + auto uccMask = op.getInputs()[1]; + auto ptr = rewriteValue(atomicRMWOp.getPtr(), op, rewriter); + auto val = rewriteValue(atomicRMWOp.getVal(), op, rewriter); + auto mask = rewriteValue(atomicRMWOp.getMask(), op, rewriter); + auto resType = getExpandedType(atomicRMWOp.getResult().getType(), op); + mask = createMask(mask, uccMask, resType.getShape(), rewriter); + auto newOp = rewriter.create( + atomicRMWOp.getLoc(), resType, atomicRMWOp.getAtomicRmwOp(), ptr, val, + mask, atomicRMWOp.getSem(), atomicRMWOp.getScope()); + for (auto attr : atomicRMWOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + replaceValue(newOp, atomicRMWOp, uccMask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteAssert( + UnrealizedConversionCastOp op, triton::AssertOp assertOp, + PatternRewriter &rewriter) const { + auto input = op.getInputs()[0]; + auto mask = op.getInputs()[1]; + auto inputShape = cast(input.getType()).getShape(); + auto conditionType = cast(mask.getType()); + auto oneAttr = rewriter.getIntegerAttr(getElementTypeOrSelf(mask), 1); + auto one = rewriter.create( + mask.getLoc(), DenseElementsAttr::get(conditionType, oneAttr)); + Value condition = rewriter.create(input.getLoc(), mask, one); + condition = createMask(nullptr, condition, inputShape, rewriter); + condition = + rewriter.create(condition.getLoc(), condition, input); + auto newOp = rewriter.create(assertOp.getLoc(), condition, + assertOp.getMessage()); + for (auto attr : assertOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + rewriter.replaceOp(assertOp, newOp); +} + +void PropagateUnrealizedCastDown::rewriteExtractSlice( + UnrealizedConversionCastOp op, tensor::ExtractSliceOp extractSliceOp, + PatternRewriter &rewriter) const { + auto mask = op.getInputs()[1]; + auto src = rewriteValue(extractSliceOp.getSource(), op, rewriter); + auto offsets = llvm::to_vector(extractSliceOp.getMixedOffsets()); + auto sizes = llvm::to_vector(extractSliceOp.getMixedSizes()); + auto strides = llvm::to_vector(extractSliceOp.getMixedStrides()); + auto srcType = cast(src.getType()); + offsets.insert(offsets.begin(), rewriter.getIndexAttr(0)); + sizes.insert(sizes.begin(), rewriter.getIndexAttr(srcType.getShape()[0])); + strides.insert(strides.begin(), rewriter.getIndexAttr(1)); + auto newOp = rewriter.create( + extractSliceOp.getLoc(), src, offsets, sizes, strides); + auto newMask = rewriter.create( + mask.getLoc(), mask, offsets, sizes, strides); + for (auto attr : extractSliceOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + replaceValue(newOp, extractSliceOp, newMask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteInsertSlice( + UnrealizedConversionCastOp op, tensor::InsertSliceOp insertSliceOp, + PatternRewriter &rewriter) const { + auto mask = op.getInputs()[1]; + auto src = rewriteValue(insertSliceOp.getSource(), op, rewriter); + auto dst = rewriteValue(insertSliceOp.getDest(), op, rewriter); + auto offsets = llvm::to_vector(insertSliceOp.getMixedOffsets()); + auto sizes = llvm::to_vector(insertSliceOp.getMixedSizes()); + auto strides = llvm::to_vector(insertSliceOp.getMixedStrides()); + auto srcType = cast(src.getType()); + offsets.insert(offsets.begin(), rewriter.getIndexAttr(0)); + sizes.insert(sizes.begin(), rewriter.getIndexAttr(srcType.getShape()[0])); + strides.insert(strides.begin(), rewriter.getIndexAttr(1)); + auto newOp = rewriter.create( + insertSliceOp.getLoc(), src, dst, offsets, sizes, strides); + for (auto attr : insertSliceOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + replaceValue(newOp, insertSliceOp, mask, rewriter); +} + +void PropagateUnrealizedCastDown::rewriteWhile( + UnrealizedConversionCastOp op, scf::WhileOp whileOp, + PatternRewriter &rewriter) const { + auto input = op.getInputs()[0]; + auto mask = op.getInputs()[1]; + auto res = op->getResult(0); + SmallVector indices; + SmallVector newInits; + IRMapping mapping; + for (auto [idx, init] : llvm::enumerate(whileOp.getInits())) { + if (init == res) { + indices.push_back(idx); + newInits.push_back(input); + } else { + newInits.push_back(init); + } + } + auto newOp = rewriter.create( + whileOp.getLoc(), whileOp->getResultTypes(), newInits, + [&](OpBuilder &b, Location loc, ValueRange args) { + mapRegionIterArg(mapping, whileOp.getBeforeArguments(), args, indices, + mask, b); + for (auto &bodyOp : *whileOp.getBeforeBody()) + b.clone(bodyOp, mapping); + }, + [&](OpBuilder &b, Location loc, ValueRange args) { + mapRegionIterArg(mapping, whileOp.getAfterArguments(), args, {}, mask, + b); + for (auto &bodyOp : whileOp.getAfterBody()->without_terminator()) + b.clone(bodyOp, mapping); + auto yieldOp = + cast(whileOp.getAfterBody()->getTerminator()); + mapYieldedValue(mapping, yieldOp, indices, op, b); + }); + for (auto attr : whileOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + rewriter.replaceOp(whileOp, newOp); +} + +void PropagateUnrealizedCastDown::rewriteLoop(UnrealizedConversionCastOp op, + LoopLikeOpInterface loopOp, + PatternRewriter &rewriter) const { + auto input = op.getInputs()[0]; + auto mask = op.getInputs()[1]; + auto res = op->getResult(0); + SmallVector indices; + SmallVector newInits; + IRMapping mapping; + for (auto [idx, init] : llvm::enumerate(loopOp.getInits())) { + if (init == res) { + indices.push_back(idx); + newInits.push_back(input); + } else { + newInits.push_back(init); + } + } + LoopLikeOpInterface newOp; + if (auto forOp = dyn_cast(loopOp.getOperation())) { + newOp = rewriter.create( + forOp.getLoc(), forOp.getLowerBound(), forOp.getUpperBound(), + forOp.getStep(), newInits, + [&](OpBuilder &b, Location loc, Value iv, ValueRange args) { + mapping.map(forOp.getInductionVar(), iv); + mapRegionIterArg(mapping, forOp.getRegionIterArgs(), args, indices, + mask, b); + for (auto &bodyOp : forOp.getBody()->without_terminator()) + b.clone(bodyOp, mapping); + auto yieldOp = cast(forOp.getBody()->getTerminator()); + mapYieldedValue(mapping, yieldOp, indices, op, b); + }); + for (auto attr : forOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + } else { + llvm_unreachable("Unhandled loopOp"); + } + replaceValue(newOp, loopOp, mask, rewriter, indices); +} + +void PropagateUnrealizedCastDown::rewriteIf(UnrealizedConversionCastOp &op, + scf::IfOp ifOp, + ArrayRef indices, + PatternRewriter &rewriter) const { + IRMapping mapping; + auto mask = op.getInputs()[1]; + auto thenBlockBuilder = [&](OpBuilder &b, Location loc) { + for (auto &bodyOp : *ifOp.thenBlock()) + b.clone(bodyOp, mapping); + }; + auto elseBlockBuilder = [&](OpBuilder &b, Location loc) { + for (auto &bodyOp : *ifOp.elseBlock()) + b.clone(bodyOp, mapping); + }; + scf::IfOp newOp; + if (ifOp.elseBlock()) { + newOp = rewriter.create(ifOp.getLoc(), ifOp.getCondition(), + thenBlockBuilder, elseBlockBuilder); + } else { + newOp = rewriter.create(ifOp.getLoc(), ifOp.getCondition(), + thenBlockBuilder, nullptr); + } + for (auto attr : ifOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + if (mapping.contains(op)) + op = cast(mapping.lookup(op)); + replaceValue(newOp, ifOp, mask, rewriter, indices); +} + +void PropagateUnrealizedCastDown::rewriteYield( + UnrealizedConversionCastOp &op, scf::YieldOp yieldOp, + PatternRewriter &rewriter) const { + auto input = op.getInputs()[0]; + auto mask = op.getInputs()[1]; + auto res = op->getResult(0); + SmallVector indices; + auto newOperands = llvm::to_vector(yieldOp.getOperands()); + for (auto [idx, opr] : llvm::enumerate(newOperands)) { + if (opr == res) + indices.push_back(idx); + } + if (auto loopOp = dyn_cast(yieldOp->getParentOp())) { + auto uccOp = rewriter.create( + op.getLoc(), res.getType(), ValueRange({input})); + for (auto curIdx : indices) + newOperands[curIdx] = uccOp->getResult(0); + auto newOp = rewriter.create(yieldOp.getLoc(), newOperands); + for (auto attr : yieldOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + rewriter.replaceOp(yieldOp, newOp); + rewriter.setInsertionPoint(loopOp); + for (auto curIdx : indices) { + auto &initArg = loopOp.getInitsMutable()[curIdx]; + auto initVal = initArg.get(); + uccOp = rewriter.create( + initVal.getLoc(), input.getType(), ValueRange({initVal})); + uccOp = rewriter.create( + initVal.getLoc(), initVal.getType(), + ValueRange({uccOp->getResult(0), mask})); + rewriter.modifyOpInPlace(loopOp, + [&]() { initArg.set(uccOp->getResult(0)); }); + } + } else if (auto ifOp = dyn_cast(yieldOp->getParentOp())) { + for (auto curIdx : indices) + newOperands[curIdx] = input; + auto newOp = rewriter.create(yieldOp.getLoc(), newOperands); + for (auto attr : yieldOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + rewriter.replaceOp(yieldOp, newOp); + yieldOp = ifOp.thenYield() == yieldOp ? ifOp.elseYield() : ifOp.thenYield(); + if (yieldOp) { + rewriter.setInsertionPoint(yieldOp); + newOperands = llvm::to_vector(yieldOp.getOperands()); + for (auto curIdx : indices) { + auto uccOp = rewriter.create( + op.getLoc(), input.getType(), ValueRange({newOperands[curIdx]})); + newOperands[curIdx] = uccOp->getResult(0); + } + rewriter.replaceOpWithNewOp(yieldOp, newOperands); + } + rewriter.setInsertionPoint(ifOp); + rewriteIf(op, ifOp, indices, rewriter); + } +} + +void PropagateUnrealizedCastDown::rewriteCondition( + UnrealizedConversionCastOp op, scf::ConditionOp conditionOp, + PatternRewriter &rewriter) const { + auto whileOp = cast(conditionOp->getParentOp()); + auto input = op.getInputs()[0]; + auto mask = op.getInputs()[1]; + auto res = op->getResult(0); + int64_t curIdx = -1; + auto args = llvm::to_vector(conditionOp.getArgs()); + for (auto [idx, opr] : llvm::enumerate(args)) { + if (opr == res) + curIdx = idx; + } + args[curIdx] = input; + auto newOp = rewriter.create( + conditionOp.getLoc(), conditionOp.getCondition(), args); + for (auto attr : conditionOp->getAttrs()) { + if (!newOp->hasAttr(attr.getName())) + newOp->setAttr(attr.getName(), attr.getValue()); + } + rewriter.replaceOp(conditionOp, newOp); + + res = whileOp->getResult(curIdx); + auto oldResType = res.getType(); + auto newResType = getExpandedType(oldResType, op); + rewriter.modifyOpInPlace(whileOp, [&]() { res.setType(newResType); }); + rewriter.setInsertionPointAfter(whileOp); + auto newUccOp = rewriter.create( + res.getLoc(), oldResType, ValueRange({res, mask})); + rewriter.replaceAllUsesExcept(res, newUccOp->getResult(0), newUccOp); + auto arg = whileOp.getAfterArguments()[curIdx]; + auto oldArgType = arg.getType(); + auto newArgType = getExpandedType(oldArgType, op); + rewriter.modifyOpInPlace(whileOp, [&]() { arg.setType(newArgType); }); + rewriter.setInsertionPointToStart(whileOp.getAfterBody()); + newUccOp = rewriter.create( + arg.getLoc(), oldArgType, ValueRange({arg, mask})); + rewriter.replaceAllUsesExcept(arg, newUccOp->getResult(0), newUccOp); +} diff --git a/third_party/ascend/lib/AutoBlockify/Utils.cpp b/third_party/ascend/lib/AutoBlockify/Utils.cpp new file mode 100644 index 0000000000..ee3b81badd --- /dev/null +++ b/third_party/ascend/lib/AutoBlockify/Utils.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "AutoBlockify/Utils.h" +#include "Utils/Utils.h" + +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "auto-blockify-utils" + +using namespace mlir; +using namespace triton; + +RankedTensorType getExpandedType(Type type, UnrealizedConversionCastOp op) { + auto target = op.getInputs()[0]; + auto targetType = cast(target.getType()); + SmallVector targetShape{targetType.getShape()[0]}; + if (auto valueType = dyn_cast(type)) { + targetShape.append(valueType.getShape().begin(), + valueType.getShape().end()); + } + return RankedTensorType::get(targetShape, getElementTypeOrSelf(type)); +} + +Value rewriteValue(Value value, UnrealizedConversionCastOp op, + OpBuilder &builder) { + if (value == nullptr) + return nullptr; + if (value == op->getResult(0)) + return op.getInputs()[0]; + return builder + .create( + value.getLoc(), getExpandedType(value.getType(), op), value) + ->getResult(0); +} + +void replaceValue(Operation *newOp, Operation *oldOp, Value newMask, + RewriterBase &rewriter, ArrayRef replaceIndices) { + int64_t idx = 0; + for (auto [res, oldRes] : + llvm::zip_equal(newOp->getResults(), oldOp->getResults())) { + if (replaceIndices.empty() || + llvm::find(replaceIndices, idx) != replaceIndices.end()) { + auto resType = res.getType(); + auto newUccOp = rewriter.create( + newOp->getLoc(), oldRes.getType(), ValueRange({res, newMask})); + rewriter.replaceAllUsesExcept(oldRes, newUccOp->getResult(0), newUccOp); + } else { + rewriter.replaceAllUsesWith(oldRes, res); + } + idx++; + } + rewriter.eraseOp(oldOp); +} + +Value createMask(Value mask, Value uccMask, ArrayRef targetShape, + RewriterBase &rewriter) { + SmallVector curShape{targetShape[0]}; + for (auto [idx, dim] : llvm::drop_begin(llvm::enumerate(targetShape))) { + curShape.push_back(dim); + uccMask = + rewriter.create(uccMask.getLoc(), uccMask, idx); + uccMask = rewriter.create( + uccMask.getLoc(), + RankedTensorType::get(curShape, getElementTypeOrSelf(uccMask)), + uccMask); + } + if (mask) { + mask = rewriter.create(mask.getLoc(), mask, uccMask); + } else { + mask = uccMask; + } + return mask; +} + +void mapRegionIterArg(IRMapping &mapping, ValueRange oldArgs, + ValueRange newArgs, ArrayRef indices, Value mask, + OpBuilder &builder) { + auto newArgIter = newArgs.begin(); + for (auto [idx, oldArg] : llvm::enumerate(oldArgs)) { + if (llvm::find(indices, idx) != indices.end()) { + auto newUccOp = builder.create( + oldArg.getLoc(), oldArg.getType(), ValueRange({*newArgIter, mask})); + mapping.map(oldArg, newUccOp->getResult(0)); + } else { + mapping.map(oldArg, *newArgIter); + } + ++newArgIter; + } +} + +void mapYieldedValue(IRMapping &mapping, scf::YieldOp yieldOp, + ArrayRef indices, UnrealizedConversionCastOp op, + OpBuilder &builder) { + SmallVector newOperands; + for (auto [idx, operand] : llvm::enumerate(yieldOp.getOperands())) { + operand = mapping.lookup(operand); + if (llvm::find(indices, idx) != indices.end()) + newOperands.push_back(rewriteValue(operand, op, builder)); + else + newOperands.push_back(operand); + } + builder.create(yieldOp.getLoc(), newOperands); +} + +Operation *createBlockifyLoop(Operation *targetOp, + UnrealizedConversionCastOp op, + Value logicalBlockId, Value logicalBlockNum, + int autoBlockifySize, RewriterBase &rewriter) { + auto loc = targetOp->getLoc(); + rewriter.setInsertionPoint(targetOp); + auto initVal = + rewriter.create(loc, rewriter.getIndexAttr(0)); + auto stepVal = + rewriter.create(loc, rewriter.getIndexAttr(1)); + auto blockifySizeVal = rewriter.create( + loc, rewriter.getIndexAttr(autoBlockifySize)); + Value upperBound = + rewriter.create(loc, logicalBlockNum, logicalBlockId); + auto i32Zero = + rewriter.create(loc, rewriter.getI32IntegerAttr(0)); + upperBound = rewriter.create(loc, upperBound, i32Zero); + upperBound = rewriter.create(loc, rewriter.getIndexType(), + upperBound); + upperBound = + rewriter.create(loc, upperBound, blockifySizeVal); + SmallVector inits; + if (auto loopOp = dyn_cast(targetOp)) { + inits = llvm::map_to_vector(loopOp.getInits(), + [&rewriter, &op](Value v) -> Value { + return rewriteValue(v, op, rewriter); + }); + } else { + auto resultTypes = + llvm::map_to_vector(targetOp->getResultTypes(), [&op](Type type) { + return getExpandedType(type, op); + }); + inits = + llvm::map_to_vector(resultTypes, [&rewriter, &loc](Type type) -> Value { + auto tensorType = cast(type); + return rewriter.create(loc, tensorType.getShape(), + tensorType.getElementType()); + }); + } + auto mask = op.getInputs()[1]; + Operation *newOp; + auto blockifyLoop = rewriter.create( + loc, initVal, upperBound, stepVal, inits, + [&](OpBuilder &b, Location loc, Value iv, ValueRange args) { + newOp = b.clone(*targetOp); + + SmallVector newResults; + for (auto [arg, res] : llvm::zip_equal(args, newOp->getResults())) { + auto tensorType = cast(arg.getType()); + auto rank = tensorType.getRank(); + Value newRes; + if (rank > 1) { + SmallVector offsets(tensorType.getRank(), + b.getIndexAttr(0)); + SmallVector sizes(1, b.getIndexAttr(1)); + SmallVector strides(tensorType.getRank(), + b.getIndexAttr(1)); + offsets[0] = iv; + for (auto dim : llvm::drop_begin(tensorType.getShape())) + sizes.push_back(b.getIndexAttr(dim)); + newRes = b.create(loc, res, arg, offsets, + sizes, strides); + } else { + newRes = b.create(loc, res, arg, ValueRange{iv}); + } + newResults.push_back(newRes); + } + b.create(loc, newResults); + }); + + replaceValue(blockifyLoop, targetOp, mask, rewriter); + blockifyLoop->setAttr(autoBlockifyLoopAttr, rewriter.getUnitAttr()); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "After creating blockify loop:\n" << blockifyLoop << "\n"; + }); + return newOp; +} + +std::optional getBlockifyLoop(Operation *op) { + while (auto forOp = op->getParentOfType()) { + if (forOp->hasAttr(autoBlockifyLoopAttr)) + return forOp; + op = forOp; + } + return std::nullopt; +} diff --git a/third_party/ascend/lib/CMakeLists.txt b/third_party/ascend/lib/CMakeLists.txt new file mode 100644 index 0000000000..63c3e2183f --- /dev/null +++ b/third_party/ascend/lib/CMakeLists.txt @@ -0,0 +1,49 @@ +add_subdirectory(AutoBlockify) +add_subdirectory(Dialect) +add_subdirectory(TritonMemoryAccess) +add_subdirectory(Utils) +add_subdirectory(TritonToStructured) +add_subdirectory(TritonToGraph) +add_subdirectory(TritonToLinalg) +add_subdirectory(TritonToAnnotation) +add_subdirectory(TritonToHFusion) +add_subdirectory(TritonToHIVM) +add_subdirectory(TritonControlFlowOpt) +add_subdirectory(DiscreteMaskAccessConversion) +add_subdirectory(TritonToUnstructure) +add_subdirectory(TritonToLLVM) +add_subdirectory(DynamicCVPipeline) +add_subdirectory(LibEntry) +add_subdirectory(CommonIRToHIVM) + +if(TRITON_ENABLE_COVERAGE_HITEST) + set(_instrument_targets + DiscreteMaskAccessConversion + TritonToAnnotation + TritonToHFusion + TritonToHIVM + TritonToLinalg + TritonControlFlowOpt + TritonToLLVM + TritonToStructured + TritonToUnstructure + MLIRTritonNPUUtils # from Utils + TritonMemoryAccessCore + TritonMemoryAccessAnalysis + TritonAscendIR # from Dialect/TritonAscend/IR + TritonStructuredIR # from Dialect/TritonStructured/IR + AutoBlockify + ) + + foreach(_target ${_instrument_targets}) + if(TARGET ${_target}) + set_target_properties(${_target} PROPERTIES + RULE_LAUNCH_COMPILE "hitestwrapper" + RULE_LAUNCH_LINK "hitestwrapper" + ) + message(STATUS "Enabled hitestwrapper for target: ${_target}") + else() + message(WARNING "Target ${_target} not found, please check the actual target name") + endif() + endforeach() +endif() diff --git a/third_party/ascend/lib/CommonIRToHIVM/CMakeLists.txt b/third_party/ascend/lib/CommonIRToHIVM/CMakeLists.txt new file mode 100644 index 0000000000..333e9797ab --- /dev/null +++ b/third_party/ascend/lib/CommonIRToHIVM/CMakeLists.txt @@ -0,0 +1,20 @@ +add_triton_library(CommonIRToHIVM + CommonIRToHIVM.cpp + ReduceSumStrengthPass.cpp + + DEPENDS + CommonIRToHIVMConversionPassIncGen + + LINK_LIBS + CommonIRIR + BiShengIRHIVMDialect + MLIRIR + MLIRPass + MLIRTransforms + MLIRSupport + TritonIR + MLIRMemRefDialect + MLIRTensorDialect + MLIRBufferizationDialect + MLIRArithDialect +) diff --git a/third_party/ascend/lib/CommonIRToHIVM/CommonIRToHIVM.cpp b/third_party/ascend/lib/CommonIRToHIVM/CommonIRToHIVM.cpp new file mode 100644 index 0000000000..bb6985d8bf --- /dev/null +++ b/third_party/ascend/lib/CommonIRToHIVM/CommonIRToHIVM.cpp @@ -0,0 +1,873 @@ +/* + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * Copyright 2025- FlagOS Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +//===----------------------------------------------------------------------===// +// CommonIRToHIVM — Lowers CommonIR dialect ops to HIVM (Ascend NPU) dialect +// ops. +// +// Strategy: greedy rewrite runs patterns iteratively to fixed point. +// Step 1: tile.alloc → memref.alloc (produces memref with +// #hivm.address_space) Step 2: tile.to_tensor → RAUW: replace all uses of +// result with src operand, +// then erase. The src is now memref (from Step 1), consumers get +// memref instead of tensor, bridged by UnrealizedConversionCast. +// Step 3: tile.copy → hivm.copy (tile.* src, on-chip DMA) +// or memref.copy (!tt.ptr src, GM↔local DMA; the ptr is +// bridged to memref via UnrealizedConversionCast) +// tile.store_tensor → hivm.copy. +// Step 4: tile.load/store → hivm.load/store +// Step 5: sync ops → hivm sync ops +//===----------------------------------------------------------------------===// + +#include "ascend/include/CommonIRToHIVM/Passes.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir-ext/Dialect/CommonIR/IR/CommonIRDialect.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinDialect.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/DialectConversion.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" +#include "llvm/Support/LogicalResult.h" + +using namespace mlir; +namespace tile = mlir::triton::tile; +namespace hivm = mlir::hivm; + +namespace mlir { +namespace triton { +#define GEN_PASS_DEF_COMMONIRTOHIVM +#include "ascend/include/CommonIRToHIVM/Passes.h.inc" +} // namespace triton +} // namespace mlir + +// ============================================================================= +// Common Helpers +// ============================================================================= + +static bool isOneToOne(UnrealizedConversionCastOp op) { + return op.getInputs().size() == 1 && op->getNumResults() == 1; +} + +/// Return true if \p op is an annotation.mark operation. +static bool isAnnotationMark(Operation *op) { + return op->getName().getStringRef() == "annotation.mark"; +} + +static Value castDefaultMemrefToGM(Value value, Location loc, + PatternRewriter &rewriter) { + auto memrefTy = dyn_cast(value.getType()); + if (!memrefTy || memrefTy.getMemorySpace()) + return value; + + auto gmSpace = hivm::AddressSpaceAttr::get(rewriter.getContext(), + hivm::AddressSpace::GM); + auto gmTy = MemRefType::get(memrefTy.getShape(), memrefTy.getElementType(), + memrefTy.getLayout(), gmSpace); + return rewriter.create(loc, gmTy, value); +} + +static hivm::AddressSpace mapMemSpaceToHIVM(tile::MemorySpace tileSpace) { + switch (tileSpace) { + case tile::MemorySpace::GM: + return hivm::AddressSpace::GM; + case tile::MemorySpace::L1: + return hivm::AddressSpace::L1; + case tile::MemorySpace::L0A: + return hivm::AddressSpace::L0A; + case tile::MemorySpace::L0B: + return hivm::AddressSpace::L0B; + case tile::MemorySpace::L0C: + return hivm::AddressSpace::L0C; + case tile::MemorySpace::UB: + return hivm::AddressSpace::UB; + // Generic GPU memory spaces (Global/Shared/Local/Register) have no HIVM + // counterpart and should never reach this lowering pass. + case tile::MemorySpace::Global: + case tile::MemorySpace::Shared: + case tile::MemorySpace::Local: + case tile::MemorySpace::Register: + llvm_unreachable( + "unsupported generic memory space in CommonIR-to-HIVM lowering"); + } + llvm_unreachable("unknown CommonIR memory space"); +} + +static MemRefType convertBufToMemRef(tile::BufType bufTy) { + auto shape = bufTy.getShape(); + auto elemTy = bufTy.getElementType(); + auto space = bufTy.getMemorySpace(); + auto *ctx = elemTy.getContext(); + return MemRefType::get( + llvm::SmallVector(shape), elemTy, MemRefLayoutAttrInterface{}, + hivm::AddressSpaceAttr::get(ctx, mapMemSpaceToHIVM(space))); +} + +static MemRefType convertTensorToMemRef(tile::TensorType tensorTy) { + auto shape = tensorTy.getShape(); + auto elemTy = tensorTy.getElementType(); + auto space = tensorTy.getMemorySpace(); + auto *ctx = elemTy.getContext(); + return MemRefType::get( + llvm::SmallVector(shape), elemTy, MemRefLayoutAttrInterface{}, + hivm::AddressSpaceAttr::get(ctx, mapMemSpaceToHIVM(space))); +} + +/// Get the underlying memref value, converting tile/builtin tensor types to +/// memref. +static Value getAsMemRef(Value val, PatternRewriter &rewriter) { + if (isa(val.getType())) + return val; + // Look through UnrealizedConversionCast + if (auto cast = val.getDefiningOp()) { + if (cast->getNumResults() == 1 && cast->getNumOperands() == 1) { + auto inner = cast->getOperand(0); + if (isa(inner.getType())) + return inner; + } + } + // Convert tile/builtin types to memref + Type targetTy; + if (auto bufTy = dyn_cast(val.getType())) + targetTy = convertBufToMemRef(bufTy); + else if (auto tensorTy = dyn_cast(val.getType())) + targetTy = convertTensorToMemRef(tensorTy); + else if (auto rankedTy = dyn_cast(val.getType())) { + auto elemTy = rankedTy.getElementType(); + if (auto ptrElemTy = dyn_cast(elemTy)) { + auto gmSpace = hivm::AddressSpaceAttr::get(val.getType().getContext(), + hivm::AddressSpace::GM); + targetTy = + MemRefType::get(rankedTy.getShape(), ptrElemTy.getPointeeType(), + MemRefLayoutAttrInterface{}, gmSpace); + } else { + targetTy = MemRefType::get(rankedTy.getShape(), elemTy); + } + } else if (auto ptrTy = dyn_cast(val.getType())) { + // tt.ptr> → memref<...> + auto pointeeTy = ptrTy.getPointeeType(); + auto gmSpace = hivm::AddressSpaceAttr::get(val.getType().getContext(), + hivm::AddressSpace::GM); + if (auto rankedTy = dyn_cast(pointeeTy)) + targetTy = MemRefType::get(rankedTy.getShape(), rankedTy.getElementType(), + MemRefLayoutAttrInterface{}, gmSpace); + else + targetTy = MemRefType::get({ShapedType::kDynamic}, pointeeTy, + MemRefLayoutAttrInterface{}, gmSpace); + } else + return val; + return rewriter + .create(val.getLoc(), targetTy, val) + ->getResult(0); +} + +static bool isTensorOfPointer(Type ty) { + auto rankedTy = dyn_cast(ty); + return rankedTy && isa(rankedTy.getElementType()); +} + +static void lowerScatteredLoad(tile::CopyOp op, Value ptr, Value buf, + PatternRewriter &rewriter) { + Location loc = op.getLoc(); + Value loaded = rewriter.create( + loc, ptr, Value(), Value(), ArrayRef{}, + std::optional(), triton::CacheModifier::NONE, + triton::EvictionPolicy::NORMAL, false); + + auto ty = cast(loaded.getType()); + Value mem = rewriter.create( + loc, MemRefType::get(ty.getShape(), ty.getElementType()), loaded); + + rewriter.create(loc, TypeRange{}, mem, + getAsMemRef(buf, rewriter)); + rewriter.eraseOp(op); +} + +static void lowerScatteredStore(tile::CopyOp op, Value buf, Value ptr, + PatternRewriter &rewriter) { + Location loc = op.getLoc(); + Value mem = getAsMemRef(buf, rewriter); + auto m = cast(mem.getType()); + + Value t = rewriter.create( + loc, RankedTensorType::get(m.getShape(), m.getElementType()), mem, true, + false); + + rewriter.replaceOpWithNewOp( + op, ptr, t, Value(), ArrayRef{}, triton::CacheModifier::NONE, + triton::EvictionPolicy::NORMAL); +} + +static bool isTritonPointerLike(Type ty) { + if (isa(ty)) + return true; + if (auto rankedTy = dyn_cast(ty)) + return isa(rankedTy.getElementType()); + return false; +} + +static hivm::PIPE mapPipe(int64_t tilePipe) { + switch (static_cast(tilePipe)) { + case tile::Pipe::PIPE_M: + return hivm::PIPE::PIPE_M; + case tile::Pipe::PIPE_V: + return hivm::PIPE::PIPE_V; + case tile::Pipe::PIPE_MTE1: + return hivm::PIPE::PIPE_MTE1; + case tile::Pipe::PIPE_MTE2: + return hivm::PIPE::PIPE_MTE2; + case tile::Pipe::PIPE_MTE3: + return hivm::PIPE::PIPE_MTE3; + case tile::Pipe::PIPE_FIX: + return hivm::PIPE::PIPE_FIX; + case tile::Pipe::PIPE_S: + return hivm::PIPE::PIPE_S; + } + llvm_unreachable("unknown CommonIR pipe"); +} + +static hivm::EVENT mapEvent(int64_t tileEvent) { + return static_cast(tileEvent); +} + +// ============================================================================= +// Step 1: tile.alloc → memref.alloc + #hivm.address_space +// ============================================================================= +struct TileAllocToMemRef : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(tile::AllocOp op, + PatternRewriter &rewriter) const final { + auto bufTy = op.getResult().getType(); + rewriter.replaceOpWithNewOp(op, convertBufToMemRef(bufTy)); + return success(); + } +}; + +// ============================================================================= +// step 1.5 +// ============================================================================= + +struct TileSubviewToMemrefSubview : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(tile::SubViewOp op, + PatternRewriter &rewriter) const override { + // 1. Convert the source buffer and deduce the result type + Value sourceMemRef = getAsMemRef(op.getOperand(0), rewriter); + auto sourceMemRefType = mlir::cast(sourceMemRef.getType()); + + // 2. Map dynamic offsets directly to OpFoldResult + SmallVector mixedOffsets = llvm::to_vector<4>(llvm::map_range( + op.getOffsets(), [](Value v) { return OpFoldResult(v); })); + + // 3. Unpack static sizes and strides into OpFoldResult arrays + SmallVector mixedSizes = getAsOpFoldResult(op.getSizesAttr()); + SmallVector mixedStrides = + getAsOpFoldResult(op.getStridesAttr()); + + auto targetShape = op.getType().getShape(); + auto inferredType = + mlir::cast(memref::SubViewOp::inferRankReducedResultType( + targetShape, sourceMemRefType, mixedOffsets, mixedSizes, + mixedStrides)); + + // 4. Replace the old op with the standard memref.subview + auto memrefSubview = rewriter.create( + op.getLoc(), inferredType, sourceMemRef, mixedOffsets, mixedSizes, + mixedStrides); + + rewriter.replaceOp(op, memrefSubview.getResult()); + + return success(); + } +}; + +// ============================================================================= +// Step 2: tile.to_tensor → UnrealizedConversionCast (memref → tensor) +// ============================================================================= +struct TileToTensorEliminate : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(tile::ToTensorOp op, + PatternRewriter &rewriter) const final { + // The src is now memref (from Step 1). The result type is tensor<>. + // Bridge the gap with UnrealizedConversionCast: memref → tensor. + Value src = op.getOperand(); + auto resultTy = op.getResult().getType(); + if (op->getAttr("writable")) { + // The caller (tle.dsa.to_tensor defaults to writable=True) expects an + // in-place view: custom ops use such a tensor as a DPS out and write + // the results back into the source buffer. Lower it to a writable + // bufferization.to_tensor; a plain cast would make One-Shot + // Bufferization copy the out into a fresh UB allocation that + // hivm-plan-memory cannot plan, which ends up as a `call @malloc` in + // the device code (AICore link failure) and a broken dataflow. + rewriter.replaceOpWithNewOp( + op, cast(resultTy), src, + /*restrict=*/true, /*writable=*/true); + return success(); + } + if (src.getType() != resultTy) { + auto cast = rewriter.create(op.getLoc(), + resultTy, src); + rewriter.replaceOp(op, cast->getResult(0)); + } else { + rewriter.replaceOp(op, src); + } + return success(); + } +}; + +// ============================================================================= +// Step 3: tile.copy → hivm.copy / memref.copy +// - tile.* source → hivm.copy (on-chip buffer DMA) +// - !tt.ptr source → memref.copy (GM→local DMA; later passes lower it) +// ============================================================================= +struct TileCopyToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(tile::CopyOp op, + PatternRewriter &rewriter) const final { + Value src = op.getOperand(0); + Value dst = op.getOperand(1); + + if (isTensorOfPointer(src.getType())) { + lowerScatteredLoad(op, src, dst, rewriter); + return success(); + } + if (isTensorOfPointer(dst.getType())) { + lowerScatteredStore(op, src, dst, rewriter); + return success(); + } + + Value srcMem = getAsMemRef(src, rewriter); + Value dstMem = getAsMemRef(dst, rewriter); + if (isTritonPointerLike(src.getType())) { + rewriter.replaceOpWithNewOp(op, srcMem, dstMem); + return success(); + } + rewriter.create(op.getLoc(), TypeRange{}, srcMem, dstMem); + rewriter.eraseOp(op); + return success(); + } +}; + +struct TileStoreTensorToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(tile::StoreTensorOp op, + PatternRewriter &rewriter) const final { + Value dstMem = getAsMemRef(op.getOperand(1), rewriter); + Value srcMem = op.getOperand(0); + if (!isa(srcMem.getType())) { + auto tensorTy = dyn_cast(srcMem.getType()); + if (!tensorTy) + return failure(); + auto memrefTy = + MemRefType::get(tensorTy.getShape(), tensorTy.getElementType()); + srcMem = rewriter.create(op.getLoc(), memrefTy, + srcMem); + } + + rewriter.create(op.getLoc(), TypeRange{}, srcMem, dstMem); + rewriter.eraseOp(op); + return success(); + } +}; + +// ============================================================================= +// Step 4: tile.load → hivm.load +// ============================================================================= +struct TileLoadToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(tile::LoadOp op, + PatternRewriter &rewriter) const final { + auto resultTy = op.getResult().getType(); + if (auto t = dyn_cast(resultTy)) { + auto memrefTy = convertTensorToMemRef(t); + rewriter.replaceOpWithNewOp(op, memrefTy, op.getSrc()); + return success(); + } + return failure(); + } +}; + +// ============================================================================= +// Step 4: tile.store → hivm.store +// ============================================================================= +struct TileStoreToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(tile::StoreOp op, + PatternRewriter &rewriter) const final { + Value src = getAsMemRef(op.getOperand(0), rewriter); + rewriter.replaceOpWithNewOp(op, Type(), src, + op.getOperand(1)); + return success(); + } +}; + +// ============================================================================= +// Step 5: Sync ops +// ============================================================================= +struct TileSetFlagToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(tile::SetFlagOp op, + PatternRewriter &rewriter) const final { + auto *ctx = op->getContext(); + rewriter.replaceOpWithNewOp( + op, + hivm::PipeAttr::get(ctx, + mapPipe(static_cast(op.getProducer()))), + hivm::PipeAttr::get(ctx, + mapPipe(static_cast(op.getConsumer()))), + hivm::EventAttr::get(ctx, + mapEvent(static_cast(op.getEvent()))), + Value()); + return success(); + } +}; + +struct TileWaitFlagToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(tile::WaitFlagOp op, + PatternRewriter &rewriter) const final { + auto *ctx = op->getContext(); + rewriter.replaceOpWithNewOp( + op, + hivm::PipeAttr::get(ctx, + mapPipe(static_cast(op.getProducer()))), + hivm::PipeAttr::get(ctx, + mapPipe(static_cast(op.getConsumer()))), + hivm::EventAttr::get(ctx, + mapEvent(static_cast(op.getEvent()))), + Value()); + return success(); + } +}; + +struct TilePipeBarrierToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(tile::PipeBarrierOp op, + PatternRewriter &rewriter) const final { + auto *ctx = op->getContext(); + rewriter.replaceOpWithNewOp( + op, + hivm::PipeAttr::get(ctx, mapPipe(static_cast(op.getPipe())))); + return success(); + } +}; + +struct TileCubeWaitToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(tile::CubeWaitOp op, + PatternRewriter &rewriter) const final { + auto *ctx = op->getContext(); + rewriter.replaceOpWithNewOp( + op, hivm::TCoreTypeAttr::get(ctx, hivm::TCoreType::VECTOR), + hivm::PipeAttr::get(ctx, hivm::PIPE::PIPE_FIX), + hivm::PipeAttr::get(ctx, hivm::PIPE::PIPE_MTE3), + OpFoldResult(rewriter.getIndexAttr(0))); + return success(); + } +}; + +struct TileGmOffsetToHIVM : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(tile::GmOffsetOp op, + PatternRewriter &rewriter) const final { + auto loc = op.getLoc(); + Value result = op.getBase(); + auto indices = op.getIndices(); + auto strides = op.getStrides(); + for (size_t i = 0; i < indices.size(); ++i) { + Value off = rewriter.create(loc, indices[i], strides[i]); + result = rewriter.create(loc, result, off); + } + rewriter.replaceOp(op, result); + return success(); + } +}; + +//===----------------------------------------------------------------------===// +// Pattern A: !tt.ptr> -> memref<> +//===----------------------------------------------------------------------===// +struct LowerPtrTensorCastToMemref + : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(UnrealizedConversionCastOp op, + PatternRewriter &rewriter) const override { + if (!isOneToOne(op)) + return failure(); + Value in = op.getInputs().front(); + auto ptrTy = dyn_cast(in.getType()); + if (!ptrTy) + return failure(); + auto tensorPointee = dyn_cast(ptrTy.getPointeeType()); + if (!tensorPointee) + return failure(); + auto outMemref = dyn_cast(op->getResult(0).getType()); + if (!outMemref) + return failure(); + if (outMemref.getShape() != tensorPointee.getShape() || + outMemref.getElementType() != tensorPointee.getElementType()) + return failure(); + + Location loc = op.getLoc(); + // %t = tt.load %ptr : !tt.ptr> (tensor-pointer overload) + Value loaded = rewriter.create( + loc, in, /*boundaryCheck=*/ArrayRef{}, + /*padding=*/std::optional(), + triton::CacheModifier::NONE, triton::EvictionPolicy::NORMAL, + /*isVolatile=*/false); + // %m = bufferization.to_buffer %t + Value asMemref = + rewriter.create(loc, outMemref, loaded); + rewriter.replaceOp(op, asMemref); + return success(); + } +}; + +//===----------------------------------------------------------------------===// +// Pattern B: memref<..., #space> -> tensor<...> +//===----------------------------------------------------------------------===// +struct LowerMemrefCastToTensor + : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(UnrealizedConversionCastOp op, + PatternRewriter &rewriter) const override { + if (!isOneToOne(op)) + return failure(); + Value in = op.getInputs().front(); + auto memrefTy = dyn_cast(in.getType()); + if (!memrefTy) + return failure(); + auto tensorOut = dyn_cast(op->getResult(0).getType()); + if (!tensorOut) + return failure(); + if (memrefTy.getShape() != tensorOut.getShape() || + memrefTy.getElementType() != tensorOut.getElementType()) + return failure(); + + Location loc = op.getLoc(); + // %t = bufferization.to_tensor %m restrict + Value asTensor = rewriter.create( + loc, tensorOut, in, /*restrict=*/true, /*writable=*/false); + rewriter.replaceOp(op, asTensor); + return success(); + } +}; + +//===----------------------------------------------------------------------===// +// Pattern C (utility): simplify chains of one-to-one unrealized casts whose +// outer source type equals the outer result type. This catches cases like +// %a = unrealized_conversion_cast %x : memref -> tensor +// %b = unrealized_conversion_cast %a : tensor -> memref +// which arise after the linalg pass partially converts surrounding ops. +//===----------------------------------------------------------------------===// +struct FoldRoundtripCast : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(UnrealizedConversionCastOp op, + PatternRewriter &rewriter) const override { + if (!isOneToOne(op)) + return failure(); + Value in = op.getInputs().front(); + auto prev = in.getDefiningOp(); + if (!prev || !isOneToOne(prev)) + return failure(); + Value origin = prev.getInputs().front(); + if (origin.getType() != op->getResult(0).getType()) + return failure(); + rewriter.replaceOp(op, origin); + return success(); + } +}; + +//===----------------------------------------------------------------------===// +// Rewrite pattern: fold staging alloc + copy1 + to_tensor + copy2 +//===----------------------------------------------------------------------===// +struct FoldStagingCopyPattern : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(memref::CopyOp copyOp, + PatternRewriter &rewriter) const override { + Value stage = copyOp.getSource(); + auto stageCast = stage.getDefiningOp(); + if (stageCast) + stage = stageCast.getSource(); + Value dst = copyOp.getTarget(); + + // ① Destination must have an explicit memory space (on-chip buffer). + auto dstType = dyn_cast(dst.getType()); + if (!dstType || !dstType.getMemorySpace()) + return failure(); + + // ② Source must be a staging memref.alloc in default/GM address space. + auto stageAlloc = stage.getDefiningOp(); + if (!stageAlloc) + return failure(); + auto stageType = stageAlloc.getType(); + if (auto stageSpace = stageType.getMemorySpace()) { + auto stageAddr = dyn_cast(stageSpace); + if (!stageAddr || stageAddr.getAddressSpace() != hivm::AddressSpace::GM) + return failure(); + } + + // ③ Enumerate the source copy: memref.copy %src, %stage. + // Collect all users of %stage to verify the expected pattern. + memref::CopyOp srcCopy; + Operation *stageMark = nullptr; + bufferization::ToTensorOp toTensor; + Operation *tensorMark = nullptr; + SmallVector otherUses; + + for (Operation *user : stage.getUsers()) { + if (auto c = dyn_cast(user)) { + if (c.getTarget() == stage) { + if (srcCopy) + return failure(); // multiple incoming copies — ambiguous + srcCopy = c; + } else if (c == copyOp.getOperation()) { + continue; // the outgoing copy we are folding + } else { + otherUses.push_back(user); + } + } else if (isAnnotationMark(user)) { + if (stageMark) + otherUses.push_back(user); + else + stageMark = user; + } else if (stageCast && user == stageCast.getOperation()) { + continue; + } else if (auto tt = dyn_cast(user)) { + if (toTensor) + otherUses.push_back(user); + else + toTensor = tt; + } else { + otherUses.push_back(user); + } + } + + // srcCopy is mandatory; toTensor is optional (Flavour A has it, B doesn't). + if (!srcCopy || !otherUses.empty()) + return failure(); + + // Collect annotation.mark users of the tensor value (only if toTensor + // exists — Flavour A path). + if (toTensor) { + Value tensorVal = toTensor.getResult(); + for (Operation *user : tensorVal.getUsers()) { + if (isAnnotationMark(user) && !tensorMark) + tensorMark = user; + } + } + + Location loc = copyOp.getLoc(); + Value src = castDefaultMemrefToGM(srcCopy.getSource(), loc, rewriter); + Value dstCbuf = dst; + + // ④ Create the merged copy: memref.copy %src, %dst(#space) + rewriter.create(loc, src, dstCbuf); + + // ⑤ If there was an annotation.mark on %stage, move it to %dst. + if (stageMark) { + OperationState state(loc, "annotation.mark"); + state.addOperands(dstCbuf); + for (auto &namedAttr : stageMark->getAttrs()) + state.addAttribute(namedAttr.getName(), namedAttr.getValue()); + rewriter.create(state); + } + + // ⑥ For Flavour A: drop the address space via memory_space_cast and + // re-create the to_tensor pointing at the cbuf alloc. + // For Flavour B: nothing to do — the tensor path never existed. + if (toTensor) { + auto genericType = + MemRefType::get(dstType.getShape(), dstType.getElementType()); + Value cast = + rewriter.create(loc, genericType, dstCbuf); + Value tensor = rewriter.create( + toTensor.getLoc(), toTensor.getType(), cast, toTensor.getRestrict(), + toTensor.getWritable()); + + // ⑦ If there was an annotation.mark on the tensor value, re-create it. + if (tensorMark) { + OperationState state(loc, "annotation.mark"); + state.addOperands(tensor); + for (auto &namedAttr : tensorMark->getAttrs()) + state.addAttribute(namedAttr.getName(), namedAttr.getValue()); + rewriter.create(state); + } + + rewriter.replaceOp(toTensor, tensor); + } + + // ⑧ Erase the dead ops. + rewriter.eraseOp(copyOp); + rewriter.eraseOp(srcCopy); + if (stageCast) + rewriter.eraseOp(stageCast); + if (stageMark) + rewriter.eraseOp(stageMark); + if (tensorMark) + rewriter.eraseOp(tensorMark); + + return success(); + } +}; + +// ============================================================================= +// Pass +// ============================================================================= +namespace { +struct CommonIRToHIVMPass + : public mlir::triton::impl::CommonIRToHIVMBase { + void runOnOperation() override; +}; +} // namespace + +void CommonIRToHIVMPass::runOnOperation() { + auto module = getOperation(); + + // Use greedy rewrite to iteratively apply patterns to fixed point. + // This avoids the type-conversion complexity of the dialect conversion + // framework: tile.alloc → memref.alloc replaces !tile.buf with memref, + // then tile.copy (now seeing memref operands) → hivm.copy, etc. + +// apply pattern separately to ensure their relative order +// template lambda is a C++ 20 feature, so we'll have to use MACRO +#define APPLY_REWRITE_PATTERN(...) \ + { \ + RewritePatternSet patterns(&getContext()); \ + patterns.add<__VA_ARGS__>(&getContext()); \ + if (failed(applyPatternsAndFoldGreedily(module, std::move(patterns)))) { \ + return signalPassFailure(); \ + } \ + } + + APPLY_REWRITE_PATTERN(TileAllocToMemRef); + APPLY_REWRITE_PATTERN(TileSubviewToMemrefSubview); + APPLY_REWRITE_PATTERN(TileToTensorEliminate); + APPLY_REWRITE_PATTERN(TileCopyToHIVM); + APPLY_REWRITE_PATTERN(TileStoreTensorToHIVM); + APPLY_REWRITE_PATTERN(TileLoadToHIVM); + APPLY_REWRITE_PATTERN(TileStoreToHIVM); + APPLY_REWRITE_PATTERN(TileSetFlagToHIVM); + APPLY_REWRITE_PATTERN(TileWaitFlagToHIVM); + APPLY_REWRITE_PATTERN(TilePipeBarrierToHIVM); + APPLY_REWRITE_PATTERN(TileCubeWaitToHIVM); + APPLY_REWRITE_PATTERN(TileGmOffsetToHIVM); + + // Step N: After all tile ops are lowered, convert !tile.buf types remaining + // in tt.func signatures and tt.call ops (these arise when tile.alloc results + // are passed across function boundaries). + module.walk([](triton::FuncOp funcOp) { + auto funcType = funcOp.getFunctionType(); + bool changed = false; + + // Convert input types + SmallVector newInputs; + for (auto ty : funcType.getInputs()) { + if (auto bufTy = dyn_cast(ty)) { + newInputs.push_back(convertBufToMemRef(bufTy)); + changed = true; + } else { + newInputs.push_back(ty); + } + } + + // Convert result types + SmallVector newResults; + for (auto ty : funcType.getResults()) { + if (auto bufTy = dyn_cast(ty)) { + newResults.push_back(convertBufToMemRef(bufTy)); + changed = true; + } else { + newResults.push_back(ty); + } + } + + if (!changed) + return; + + // Update function type + auto newFuncType = + FunctionType::get(funcOp.getContext(), newInputs, newResults); + funcOp.setFunctionType(newFuncType); + + // Update block argument types + if (!funcOp.empty()) { + Block &entry = funcOp.front(); + for (unsigned i = 0; i < entry.getNumArguments(); ++i) { + if (auto bufTy = + dyn_cast(entry.getArgument(i).getType())) { + entry.getArgument(i).setType(convertBufToMemRef(bufTy)); + } + } + } + }); + + // Update tt.call result types to match new function signatures. + module.walk([](triton::CallOp callOp) { + for (unsigned i = 0; i < callOp->getNumResults(); ++i) { + if (auto bufTy = + dyn_cast(callOp->getResult(i).getType())) { + callOp->getResult(i).setType(convertBufToMemRef(bufTy)); + } + } + }); + + APPLY_REWRITE_PATTERN(LowerPtrTensorCastToMemref); + APPLY_REWRITE_PATTERN(LowerMemrefCastToTensor); + APPLY_REWRITE_PATTERN(FoldRoundtripCast); + + // Finally, fold any residual A<->B unrealized cast chains. + SmallVector casts; + module->walk([&](UnrealizedConversionCastOp c) { casts.push_back(c); }); + reconcileUnrealizedCasts(casts); + + APPLY_REWRITE_PATTERN(FoldStagingCopyPattern); +} + +std::unique_ptr> +mlir::triton::createCommonIRToHIVMPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/CommonIRToHIVM/ReduceSumStrengthPass.cpp b/third_party/ascend/lib/CommonIRToHIVM/ReduceSumStrengthPass.cpp new file mode 100644 index 0000000000..378c34ad5f --- /dev/null +++ b/third_party/ascend/lib/CommonIRToHIVM/ReduceSumStrengthPass.cpp @@ -0,0 +1,210 @@ +/* + * Copyright 2026- Xcoresigma Technology Co., Ltd + */ + +#include "ascend/include/CommonIRToHIVM/ReduceSumStrengthPass.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" + +namespace mlir { +namespace triton { +#define GEN_PASS_DECL_REDUCESUMSTRENGTH +#define GEN_PASS_DEF_REDUCESUMSTRENGTH +#include "ascend/include/CommonIRToHIVM/Passes.h.inc" +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace mlir::triton; + +namespace { + +enum class StrategyDType { F32, F16, BF16, I32, Unsupported }; + +StrategyDType getStrategyDType(Type elementType) { + if (isa(elementType)) + return StrategyDType::F32; + if (isa(elementType)) + return StrategyDType::F16; + if (isa(elementType)) + return StrategyDType::BF16; + if (auto intType = dyn_cast(elementType)) { + if (intType.getWidth() == 32) + return StrategyDType::I32; + } + return StrategyDType::Unsupported; +} + +bool isAddCombiner(ReduceOp reduceOp, Type elementType) { + Region &combineOp = reduceOp.getCombineOp(); + if (!combineOp.hasOneBlock() || combineOp.front().getOperations().size() != 2) + return false; + + Operation *combiner = reduceOp.getSingleCombiner(); + if (!combiner) + return false; + + if (isa(elementType)) + return isa(combiner); + if (isa(elementType)) + return isa(combiner); + return false; +} + +struct ReductionStrategy { + StrategyDType dtype; + int64_t minProfitableReduceDim; + int64_t maxProfitableReduceDim; +}; + +const ReductionStrategy reductionStrategies[] = { + {StrategyDType::F32, 256, 16384}, + {StrategyDType::F16, 512, 8192}, + {StrategyDType::BF16, 512, 512}, + {StrategyDType::I32, 0, -1}, +}; + +bool isProfitable(Type elementType, int64_t reduceDim) { + StrategyDType dtype = getStrategyDType(elementType); + for (const ReductionStrategy &entry : reductionStrategies) { + if (entry.dtype != dtype) + continue; + return reduceDim >= entry.minProfitableReduceDim && + reduceDim <= entry.maxProfitableReduceDim && + llvm::isPowerOf2_64(reduceDim); + } + return false; +} + +class ReduceSumStrengthPattern : public OpRewritePattern { +public: + explicit ReduceSumStrengthPattern(MLIRContext *context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(ReduceOp reduceOp, + PatternRewriter &rewriter) const override { + if (reduceOp.getNumOperands() != 1 || reduceOp->getNumResults() != 1) + return failure(); + + auto inputType = + dyn_cast(reduceOp.getOperand(0).getType()); + if (!inputType || !inputType.hasStaticShape()) + return failure(); + + Type elementType = inputType.getElementType(); + if (!isAddCombiner(reduceOp, elementType)) + return failure(); + + int64_t axis = reduceOp.getAxis(); + if (axis < 0 || axis >= inputType.getRank()) + return failure(); + + ArrayRef shape = inputType.getShape(); + int64_t reduceDim = shape[axis]; + if (reduceDim <= 1 || reduceDim % 2 != 0) + return failure(); + + if (!isProfitable(elementType, reduceDim)) + return failure(); + + SmallVector halfShape(shape.begin(), shape.end()); + halfShape[axis] = reduceDim / 2; + auto halfType = + RankedTensorType::get(halfShape, elementType, inputType.getEncoding()); + + SmallVector loOffsets(inputType.getRank(), + rewriter.getIndexAttr(0)); + SmallVector hiOffsets(inputType.getRank(), + rewriter.getIndexAttr(0)); + SmallVector sizes; + SmallVector strides(inputType.getRank(), + rewriter.getIndexAttr(1)); + sizes.reserve(inputType.getRank()); + for (int64_t dim = 0; dim < inputType.getRank(); ++dim) + sizes.push_back(rewriter.getIndexAttr(halfShape[dim])); + hiOffsets[axis] = rewriter.getIndexAttr(reduceDim / 2); + + Location loc = reduceOp.getLoc(); + Value input = reduceOp.getOperand(0); + auto lo = rewriter.create( + loc, halfType, input, loOffsets, sizes, strides); + auto hi = rewriter.create( + loc, halfType, input, hiOffsets, sizes, strides); + + Value preAdded; + if (isa(elementType)) + preAdded = rewriter.create(loc, lo, hi); + else if (isa(elementType)) + preAdded = rewriter.create(loc, lo, hi); + else + return failure(); + + auto newReduce = rewriter.create(loc, ValueRange{preAdded}, axis); + for (auto attr : reduceOp->getAttrs()) { + if (!newReduce->hasAttr(attr.getName())) + newReduce->setAttr(attr.getName(), attr.getValue()); + } + + Region &newCombineOp = newReduce.getCombineOp(); + rewriter.cloneRegionBefore(reduceOp.getCombineOp(), newCombineOp, + newCombineOp.end()); + rewriter.replaceOp(reduceOp, newReduce.getResults()); + return success(); + } +}; + +class ReduceSumStrengthPass + : public mlir::triton::impl::ReduceSumStrengthBase { +public: + using mlir::triton::impl::ReduceSumStrengthBase< + ReduceSumStrengthPass>::ReduceSumStrengthBase; + + void runOnOperation() override { + if (!enable) + return; + if (splitFactor != 2) { + getOperation().emitError( + "reduce-sum-strength only supports split-factor=2"); + signalPassFailure(); + return; + } + + MLIRContext *context = &getContext(); + RewritePatternSet patterns(context); + patterns.add(context); + + if (applyPatternsGreedily(getOperation(), std::move(patterns)).failed()) + signalPassFailure(); + } +}; + +} // namespace + +namespace mlir { +namespace triton { + +std::unique_ptr> createReduceSumStrengthPass() { + return std::make_unique(); +} + +std::unique_ptr> +createReduceSumStrengthPass(bool enable, int32_t splitFactor) { + ReduceSumStrengthOptions opts; + opts.enable = enable; + opts.splitFactor = splitFactor; + return std::make_unique(opts); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/Dialect/CMakeLists.txt b/third_party/ascend/lib/Dialect/CMakeLists.txt new file mode 100644 index 0000000000..8ef42a0e82 --- /dev/null +++ b/third_party/ascend/lib/Dialect/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(TritonAscend) +add_subdirectory(TritonStructured) diff --git a/third_party/ascend/lib/Dialect/TritonAscend/CMakeLists.txt b/third_party/ascend/lib/Dialect/TritonAscend/CMakeLists.txt new file mode 100644 index 0000000000..f33061b2d8 --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonAscend/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(IR) diff --git a/third_party/ascend/lib/Dialect/TritonAscend/IR/CMakeLists.txt b/third_party/ascend/lib/Dialect/TritonAscend/IR/CMakeLists.txt new file mode 100644 index 0000000000..07ad62ddb1 --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonAscend/IR/CMakeLists.txt @@ -0,0 +1,14 @@ +add_triton_library(TritonAscendIR + TritonAscendAttrs.cpp + TritonAscendDialect.cpp + TritonAscendOps.cpp + + DEPENDS + TritonAscendTableGen + TritonAscendAttrDefsIncGen + + LINK_LIBS PUBLIC + MLIRIR + MLIRLLVMDialect + TritonIR +) diff --git a/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendAttrs.cpp b/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendAttrs.cpp new file mode 100644 index 0000000000..cabe2897df --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendAttrs.cpp @@ -0,0 +1,14 @@ +//===- TritonAscendAttrs.cpp - TritonAscend Attributes Definition ---------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" + +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/TypeSwitch.h" + +namespace mlir::triton::ascend {} // namespace mlir::triton::ascend diff --git a/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendDialect.cpp b/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendDialect.cpp new file mode 100644 index 0000000000..952f94ddd2 --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendDialect.cpp @@ -0,0 +1,44 @@ +//===- TritonAscendDialect.cpp - TritonAscend Dialect registration --------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" + +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/SPIRV/IR/TargetAndABI.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/DialectImplementation.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/Operation.h" + +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/AsmParser/Parser.h" +#include "llvm/IR/Function.h" +#include "llvm/Support/SourceMgr.h" + +using namespace mlir; +using namespace mlir::triton::ascend; + +void TritonAscendDialect::initialize() { + addOperations< +#define GET_OP_LIST +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendOps.cpp.inc" + >(); + addAttributes< +#define GET_ATTRDEF_LIST +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendOpsAttrDefs.cpp.inc" + >(); + addInterfaces(); +} + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.cpp.inc" +#define GET_ATTRDEF_CLASSES +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendOpsAttrDefs.cpp.inc" +#define GET_OP_CLASSES +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendOps.cpp.inc" diff --git a/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendOps.cpp b/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendOps.cpp new file mode 100644 index 0000000000..86e512c9db --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonAscend/IR/TritonAscendOps.cpp @@ -0,0 +1,476 @@ +//===- TritonAscendOps.cpp - TritonAscend dialect operations --------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "ascend/include/Utils/Utils.h" +#include "mlir/Dialect/SPIRV/IR/TargetAndABI.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "triton/Tools/Sys/GetEnv.hpp" +#include "llvm/ADT/STLExtras.h" +#include +#include + +using namespace mlir; +using namespace mlir::triton; + +namespace mlir::triton::ascend { + +void GatherOutToUbOp::getEffects( + SmallVectorImpl> + &effects) { + effects.emplace_back(MemoryEffects::Read::get(), &getSrcMutable(), + triton::GlobalMemory::get()); +} + +void IndirectLoadOp::getEffects( + SmallVectorImpl> + &effects) { + effects.emplace_back(MemoryEffects::Read::get(), &getSrcMutable(), + triton::GlobalMemory::get()); +} + +void StrideLoadOp::getEffects( + SmallVectorImpl> + &effects) { + effects.emplace_back(MemoryEffects::Read::get(), &getSrcMutable(), + triton::GlobalMemory::get()); +} + +//-- DotOp -- +// Fractal (zN) block geometry (kFractalBlock / kDotAccIntWidth) comes from +// Utils.h, shared with DotConverter's lowering. +LogicalResult +DotOp::inferReturnTypes(MLIRContext *context, std::optional location, + ValueRange operands, DictionaryAttr attributes, + OpaqueProperties properties, RegionRange regions, + SmallVectorImpl &inferredReturnTypes) { + DotOpAdaptor adaptor(operands, attributes, properties, regions); + auto aTy = dyn_cast(adaptor.getA().getType()); + auto bTy = dyn_cast(adaptor.getB().getType()); + if (!aTy || !bTy) + return failure(); + + // A fractal operand's ND shape. Both operands are zN (non-transpose): + // lhs [K1,M1,16,b] -> [M,K], rhs [N1,K1,16,b] -> [K,N], each [d1*d2, d0*d3]. + auto toND = [](ArrayRef shape, bool fractal, + bool isLhs) -> SmallVector { + if (!fractal || shape.size() != 4) + return {shape[0], shape[1]}; + return {shape[1] * shape[2], shape[0] * shape[3]}; + }; + int64_t m = toND(aTy.getShape(), adaptor.getFractalA(), /*isLhs=*/true)[0]; + int64_t n = toND(bTy.getShape(), adaptor.getFractalB(), /*isLhs=*/false)[1]; + + // The result carries the cube accumulator dtype (f32 for float inputs, i32 + // for int8), not the input dtype. fractal_c produces the L0C accumulator + // fractal, whose block is 16x16 regardless of dtype. + Type inElemTy = aTy.getElementType(); + Type accTy = + isa(inElemTy) + ? static_cast(IntegerType::get(context, kDotAccIntWidth)) + : static_cast(Float32Type::get(context)); + SmallVector resShape{m, n}; + if (adaptor.getFractalC()) + // ND [M,N] -> zN accumulator [N1, M1, 16, 16]. + resShape = {n / kFractalBlock, m / kFractalBlock, kFractalBlock, + kFractalBlock}; + inferredReturnTypes.push_back(RankedTensorType::get(resShape, accTy)); + return success(); +} + +// Verify operand ranks match their fractal flags: a fractal operand must be 4D +// (zN), a non-fractal operand must be 2D (ND). Anything else (e.g. rank > 4) is +// a compile error. +LogicalResult DotOp::verify() { + auto aTy = dyn_cast(getA().getType()); + auto bTy = dyn_cast(getB().getType()); + if (!aTy || !bTy) + return emitOpError("operands must be ranked tensors"); + auto checkRank = [&](RankedTensorType ty, bool fractal, + StringRef name) -> LogicalResult { + int64_t rank = ty.getRank(); + if (fractal && rank != 4) + return emitOpError() << "fractal operand '" << name + << "' must be 4D (zN), got rank " << rank; + if (!fractal && rank != 2) + return emitOpError() << "non-fractal operand '" << name + << "' must be 2D (ND), got rank " << rank; + return success(); + }; + if (failed(checkRank(aTy, getFractalA(), "a")) || + failed(checkRank(bTy, getFractalB(), "b"))) + return failure(); + return success(); +} + +//-- IndexSelectSimdOp -- +LogicalResult IndexSelectSimdOp::inferReturnTypes( + MLIRContext *context, std::optional location, ValueRange operands, + DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions, + SmallVectorImpl &inferredReturnTypes) { + + // Get operands using adaptor + IndexSelectSimdOpAdaptor adaptor(operands, attributes, properties, regions); + + // Get element type from src pointer + Type elemType; + if (auto ptrType = + dyn_cast(adaptor.getSrc().getType())) { + elemType = ptrType.getPointeeType(); + } else { + return failure(); + } + + // Get index shape to determine the size of dim + auto indicesType = dyn_cast(adaptor.getIndex().getType()); + if (!indicesType) + return failure(); + int64_t numIndices = indicesType.getShape()[0]; + + // Use adaptor to get attributes - this is the compatible way + int32_t dim = adaptor.getDim(); + auto readShapeAttr = adaptor.getReadShape(); + + // Build result shape: read_shape but with dim replaced by numIndices + SmallVector resultShape; + for (size_t i = 0; i < readShapeAttr.size(); ++i) { + if (i == static_cast(dim)) { + resultShape.push_back(numIndices); + } else { + resultShape.push_back(readShapeAttr[i]); + } + } + + // Create result tensor type + inferredReturnTypes.push_back(RankedTensorType::get(resultShape, elemType)); + + return success(); +} + +// FlipOp +LogicalResult +FlipOp::inferReturnTypes(MLIRContext *context, std::optional location, + ValueRange operands, DictionaryAttr attributes, + OpaqueProperties properties, RegionRange regions, + SmallVectorImpl &inferredReturnTypes) { + auto inputTy = dyn_cast(operands[0].getType()); + if (!inputTy) { + if (location) + return emitOptionalError(location, + "expected ranked tensor for flip input"); + return failure(); + } + inferredReturnTypes.push_back(inputTy); + return success(); +} + +//-- SortOp -- +LogicalResult +SortOp::inferReturnTypes(MLIRContext *context, std::optional location, + ValueRange operands, DictionaryAttr attributes, + OpaqueProperties properties, RegionRange regions, + SmallVectorImpl &inferredReturnTypes) { + if (operands.size() != 1) { + return emitOptionalError(location, + "expected exactly one operand for SortOp"); + } + + if (!isa(operands[0].getType())) { + return emitOptionalError(location, + "operand must be a ranked tensor type for SortOp"); + } + + Value src = operands[0]; + auto srcTy = cast(src.getType()); + auto srcShape = srcTy.getShape(); + auto srcEnc = srcTy.getEncoding(); + + if (srcShape.empty()) { + return emitOptionalError(location, "input tensor must have rank >= 1"); + } + + Type sortedTy = + RankedTensorType::get(srcShape, srcTy.getElementType(), srcEnc); + + inferredReturnTypes.push_back(sortedTy); + + return success(); +} + +//-- Conv1dOp -- +LogicalResult Conv1dOp::verify() { + auto inputType = dyn_cast(getInput().getType()); + auto weightType = dyn_cast(getWeight().getType()); + + constexpr int64_t dim2 = 2; + constexpr int64_t dim3 = 3; + auto inputRank = inputType.getShape().size(); + if (inputRank != dim2 && inputRank != dim3) { + return emitOpError("input tensor must be 2D or 3D, but got rank ") + << inputRank; + } + if (weightType.getShape().size() != dim3) { + return emitOpError("weight tensor must be 3D, but got rank ") + << weightType.getShape().size(); + } + + Value biasValue = getBias(); + if (biasValue) { + auto biasType = dyn_cast(biasValue.getType()); + if (!biasType || biasType.getRank() != 1) { + return emitOpError("bias must be a 1D ranked tensor"); + } + if (biasType.getElementType() != inputType.getElementType()) { + return emitOpError( + "bias must have the same element type as input and weight"); + } + if (biasType.getShape()[0] != weightType.getShape()[0]) { + return emitOpError( + "bias size must match weight's output channel dimension"); + } + } + + int64_t C_in = inputType.getShape()[inputRank - 2]; + int64_t C_out = weightType.getShape()[0]; + int64_t weight_C_in = weightType.getShape()[1]; + + if (C_in % getGroups() != 0) { + return emitOpError("input channels must be divisible by groups"); + } + if (C_out % getGroups() != 0) { + return emitOpError("output channels must be divisible by groups"); + } + if (weight_C_in != C_in / getGroups()) { + return emitOpError( + "weight's input channel dimension must equal input channels / groups"); + } + + return success(); +} + +LogicalResult Conv1dOp::inferReturnTypes( + MLIRContext *context, std::optional location, ValueRange operands, + DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions, + SmallVectorImpl &inferredReturnTypes) { + Conv1dOpAdaptor adaptor(operands, attributes, properties, regions); + + auto inputType = dyn_cast(adaptor.getInput().getType()); + auto weightType = dyn_cast(adaptor.getWeight().getType()); + if (!inputType || !weightType) { + return failure(); + } + + ArrayRef inputShape = inputType.getShape(); + bool isBatched = inputShape.size() == 3; + if (isBatched) { + int64_t N = inputShape[0]; + } + int64_t C_in = inputShape[isBatched ? 1 : 0]; + int64_t L_in = inputShape[isBatched ? 2 : 1]; + + ArrayRef weightShape = weightType.getShape(); + int64_t C_out = weightShape[0]; + int64_t K = weightShape[2]; + + int64_t stride = adaptor.getStride(); + int64_t padding_size = adaptor.getPaddingSize(); + int64_t dilation = adaptor.getDilation(); + + if (stride == 0) { + return failure(); + } + double l_out_double = + static_cast(L_in + 2 * padding_size - dilation * (K - 1) - 1) / + stride + + 1; + int64_t L_out = static_cast(std::floor(l_out_double)); + + constexpr int64_t dim3 = 3; + SmallVector outputShape; + if (isBatched) { + outputShape.push_back(inputShape[0]); + } + outputShape.push_back(weightShape[0]); + outputShape.push_back(L_out); + + Type elementType = inputType.getElementType(); + auto returnType = RankedTensorType::get(outputShape, elementType); + inferredReturnTypes.push_back(returnType); + return success(); +} + +//-- Conv2dOp -- + +// Parse a conv2d parameter attribute: an integer (uniform scalar) or an +// integer array. Returns the raw values, or std::nullopt for an invalid +// attribute. +static std::optional> +parseConv2dParamAttr(Attribute attr) { + if (auto intAttr = dyn_cast(attr)) + return SmallVector{intAttr.getInt()}; + if (auto arrayAttr = dyn_cast(attr)) { + SmallVector values; + for (int32_t v : arrayAttr.asArrayRef()) + values.push_back(static_cast(v)); + return values; + } + return std::nullopt; +} + +LogicalResult Conv2dOp::verify() { + auto inputType = dyn_cast(getInput().getType()); + auto weightType = dyn_cast(getWeight().getType()); + + constexpr int64_t dim3 = 3; + constexpr int64_t dim4 = 4; + auto inputRank = inputType.getShape().size(); + if (inputRank != dim3 && inputRank != dim4) { + return emitOpError( + "input tensor must be 3D (C,H,W) or 4D (N,C,H,W), but got rank ") + << inputRank; + } + if (weightType.getShape().size() != dim4) { + return emitOpError("weight tensor must be 4D (C_out, C_in/groups, kH, kW), " + "but got rank ") + << weightType.getShape().size(); + } + + Value biasValue = getBias(); + if (biasValue) { + auto biasType = dyn_cast(biasValue.getType()); + if (!biasType || biasType.getRank() != 1) { + return emitOpError("bias must be a 1D ranked tensor"); + } + if (biasType.getElementType() != inputType.getElementType()) { + return emitOpError( + "bias must have the same element type as input and weight"); + } + if (biasType.getShape()[0] != weightType.getShape()[0]) { + return emitOpError( + "bias size must match weight's output channel dimension"); + } + } + + auto stride = parseConv2dParamAttr(getStride()); + auto padding = parseConv2dParamAttr(getPadding()); + auto dilation = parseConv2dParamAttr(getDilation()); + if (!stride || stride->empty() || stride->size() > 2) { + return emitOpError( + "stride must be an integer or a 2-element array [stride_h, stride_w]"); + } + if (!padding || padding->empty() || padding->size() > 4) { + return emitOpError( + "padding must be an integer, a 2-element array [pad_h, pad_w], or a " + "4-element array [pad_top, pad_bottom, pad_left, pad_right]"); + } + if (!dilation || dilation->empty() || dilation->size() > 2) { + return emitOpError( + "dilation must be an integer or a 2-element array [dil_h, dil_w]"); + } + // Uniform scalar or pair: front/back covers both forms. + if (stride->front() == 0 || stride->back() == 0) { + return emitOpError("stride elements must be > 0"); + } + + int64_t C_in = inputType.getShape()[inputRank - 3]; + int64_t C_out = weightType.getShape()[0]; + int64_t weight_C_in = weightType.getShape()[1]; + + if (C_in % getGroups() != 0) { + return emitOpError("input channels must be divisible by groups"); + } + if (C_out % getGroups() != 0) { + return emitOpError("output channels must be divisible by groups"); + } + if (weight_C_in != C_in / getGroups()) { + return emitOpError( + "weight's input channel dimension must equal input channels / groups"); + } + + return success(); +} + +LogicalResult Conv2dOp::inferReturnTypes( + MLIRContext *context, std::optional location, ValueRange operands, + DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions, + SmallVectorImpl &inferredReturnTypes) { + Conv2dOpAdaptor adaptor(operands, attributes, properties, regions); + + auto inputType = dyn_cast(adaptor.getInput().getType()); + auto weightType = dyn_cast(adaptor.getWeight().getType()); + if (!inputType || !weightType) { + return failure(); + } + + ArrayRef inputShape = inputType.getShape(); + bool isBatched = inputShape.size() == 4; + int64_t C_in = inputShape[isBatched ? 1 : 0]; + int64_t H_in = inputShape[isBatched ? 2 : 1]; + int64_t W_in = inputShape[isBatched ? 3 : 2]; + + ArrayRef weightShape = weightType.getShape(); + int64_t C_out = weightShape[0]; + int64_t kH = weightShape[2]; + int64_t kW = weightShape[3]; + + auto strideAttr = parseConv2dParamAttr(adaptor.getStride()); + auto paddingAttr = parseConv2dParamAttr(adaptor.getPadding()); + auto dilationAttr = parseConv2dParamAttr(adaptor.getDilation()); + if (!strideAttr || strideAttr->empty() || strideAttr->size() > 2 || + !paddingAttr || paddingAttr->empty() || paddingAttr->size() > 4 || + !dilationAttr || dilationAttr->empty() || dilationAttr->size() > 2) { + return failure(); + } + auto &stride = *strideAttr; + auto &padding = *paddingAttr; + auto &dilation = *dilationAttr; + + auto computeOutDim = [](int64_t in_size, int64_t pad_before, + int64_t pad_after, int64_t dil, int64_t k, + int64_t str) -> int64_t { + double val = static_cast(in_size + pad_before + pad_after - + dil * (k - 1) - 1) / + str + + 1; + return static_cast(std::floor(val)); + }; + + // Expand padding to [pad_top, pad_bottom, pad_left, pad_right]. + SmallVector pads; + if (padding.size() == 1) { + pads.assign(4, padding[0]); + } else if (padding.size() == 2) { + pads = {padding[0], padding[0], padding[1], padding[1]}; + } else { + pads.assign(padding.begin(), padding.end()); + } + // Uniform scalar or pair: front/back covers both forms. + int64_t strideH = stride.front(); + int64_t strideW = stride.back(); + int64_t dilH = dilation.front(); + int64_t dilW = dilation.back(); + + // pads = [pad_top, pad_bottom, pad_left, pad_right] + int64_t H_out = computeOutDim(H_in, pads[0], pads[1], dilH, kH, strideH); + int64_t W_out = computeOutDim(W_in, pads[2], pads[3], dilW, kW, strideW); + + SmallVector outputShape; + if (isBatched) { + outputShape.push_back(inputShape[0]); + } + outputShape.push_back(weightShape[0]); + outputShape.push_back(H_out); + outputShape.push_back(W_out); + + Type elementType = inputType.getElementType(); + auto returnType = RankedTensorType::get(outputShape, elementType); + inferredReturnTypes.push_back(returnType); + return success(); +} + +} // namespace mlir::triton::ascend diff --git a/third_party/ascend/lib/Dialect/TritonStructured/CMakeLists.txt b/third_party/ascend/lib/Dialect/TritonStructured/CMakeLists.txt new file mode 100644 index 0000000000..f33061b2d8 --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonStructured/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(IR) diff --git a/third_party/ascend/lib/Dialect/TritonStructured/IR/CMakeLists.txt b/third_party/ascend/lib/Dialect/TritonStructured/IR/CMakeLists.txt new file mode 100644 index 0000000000..27aac38fa0 --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonStructured/IR/CMakeLists.txt @@ -0,0 +1,11 @@ +add_triton_library(TritonStructuredIR + TritonStructuredOps.cpp + TritonStructuredDialect.cpp + + DEPENDS + TritonStructuredTableGen + + LINK_LIBS PUBLIC + TritonIR + MLIRIR + ) diff --git a/third_party/ascend/lib/Dialect/TritonStructured/IR/TritonStructuredDialect.cpp b/third_party/ascend/lib/Dialect/TritonStructured/IR/TritonStructuredDialect.cpp new file mode 100644 index 0000000000..28a996017b --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonStructured/IR/TritonStructuredDialect.cpp @@ -0,0 +1,22 @@ +#include "Dialect/TritonStructured/IR/TritonStructuredDialect.h" + +using namespace mlir; +using namespace mlir::tts; + +/// Dialect creation, the instance will be owned by the context. This is the +/// point of registration of custom types and operations for the dialect. +void TritonStructuredDialect::initialize() { + addOperations< +#define GET_OP_LIST +#include "Dialect/TritonStructured/IR/TritonStructuredOps.cpp.inc" + >(); +} + +//===----------------------------------------------------------------------===// +// TableGen'd op method definitions +//===----------------------------------------------------------------------===// + +#define GET_OP_CLASSES +#include "Dialect/TritonStructured/IR/TritonStructuredOps.cpp.inc" + +#include "Dialect/TritonStructured/IR/TritonStructuredDialect.cpp.inc" diff --git a/third_party/ascend/lib/Dialect/TritonStructured/IR/TritonStructuredOps.cpp b/third_party/ascend/lib/Dialect/TritonStructured/IR/TritonStructuredOps.cpp new file mode 100644 index 0000000000..c9f71470f1 --- /dev/null +++ b/third_party/ascend/lib/Dialect/TritonStructured/IR/TritonStructuredOps.cpp @@ -0,0 +1,109 @@ +#include "mlir/Bytecode/BytecodeOpInterface.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Support/LogicalResult.h" +#include "triton/Dialect/Triton/IR/Types.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/LogicalResult.h" +#include +#include +#include + +#define GET_OP_CLASSES +#include "Dialect/TritonStructured/IR/TritonStructuredDialect.h" +using namespace mlir; +using namespace mlir::tts; + +namespace mlir { +namespace tts { + +LogicalResult GetStructuredStateOp::verify() { + auto expectedOffsetAndStrideTypes = + getOffsetAndStrideTypes(getContext(), getInput().getType()); + + if (!expectedOffsetAndStrideTypes.has_value()) { + return failure(); + } + + auto [expectedOffsetTypes, expectedStrideTypes] = + *expectedOffsetAndStrideTypes; + + return success(expectedOffsetTypes.size() == getOffsets().size() && + llvm::equal(expectedOffsetTypes, getOffsets().getTypes()) && + expectedStrideTypes.size() == getStrides().size() && + llvm::equal(expectedStrideTypes, getStrides().getTypes())); +} + +void GetStructuredStateOp::build(OpBuilder &b, OperationState &state, + Value val) { + auto type = val.getType(); + + // Builder cannot fail, so we default to empty offset and stride types. + // The invalid op will be rejected by the verifier later. + auto [offsetTypes, strideTypes] = + getOffsetAndStrideTypes(b.getContext(), type) + .value_or(std::make_pair(SmallVector{}, SmallVector{})); + + build(b, state, val.getType(), offsetTypes, strideTypes, val); +} + +std::optional, SmallVector>> +GetStructuredStateOp::getOffsetAndStrideTypes(MLIRContext *context, Type type) { + auto sizes = getOffsetAndStrideSegmentSizes(type); + if (!sizes.has_value()) { + return std::nullopt; + } + return std::make_pair( + SmallVector(sizes->first, IndexType::get(context)), + SmallVector(sizes->second, IndexType::get(context))); +} + +std::optional> +GetStructuredStateOp::getOffsetAndStrideSegmentSizes(Type type) { + int32_t offsetSegmentSize = 0; + int32_t strideSegmentSize = 0; + + if (auto tensorType = llvm::dyn_cast(type)) { + if (tensorType.getElementType().isIntOrIndex()) { + // Tensors of offsets + // Important note: + // We only care about tensor of index / int (in addition to pointer type) + // because only values of int and index type can potentially be part of a + // pointer arithmetic sequence. + offsetSegmentSize = strideSegmentSize = tensorType.getRank(); + } else if (auto ptrType = + dyn_cast(tensorType.getElementType())) { + // Unstructured pointers (tensor>) + // Each tensor of rank k gets k values for its offsets and k values for + // its strides, all of which has Index type. + offsetSegmentSize = strideSegmentSize = tensorType.getRank(); + } + } + // Block pointers (!tt.ptr> or !tt.ptr) + else if (auto ptrType = llvm::dyn_cast(type)) { + if (auto tensorType = + llvm::dyn_cast(ptrType.getPointeeType())) { + // Each tensor of rank k gets k values for its offsets and k values for + // its strides, all of which has Index type. + offsetSegmentSize = strideSegmentSize = tensorType.getRank(); + } else { + // The only relevant state that can be updated in loops for scalar + // pointers are offset. No need to include stride here. + offsetSegmentSize = 1; + } + } else { + return std::nullopt; + } + + return std::make_pair(offsetSegmentSize, strideSegmentSize); +} + +} // namespace tts +} // namespace mlir diff --git a/third_party/ascend/lib/DiscreteMaskAccessConversion/CMakeLists.txt b/third_party/ascend/lib/DiscreteMaskAccessConversion/CMakeLists.txt new file mode 100644 index 0000000000..3452c257bd --- /dev/null +++ b/third_party/ascend/lib/DiscreteMaskAccessConversion/CMakeLists.txt @@ -0,0 +1,18 @@ +add_triton_library(DiscreteMaskAccessConversion + DiscreteMaskAccessConversionPass.cpp + + DEPENDS + DiscreteMaskAccessConversionPassIncGen + + LINK_LIBS + BiShengIRHIVMDialect + MLIRIR + MLIRPass + MLIRSCFDialect + MLIRTransforms + MLIRSupport + TritonIR + TritonToLinalg + TritonMemoryAccessAnalysis + TritonToLinalg +) diff --git a/third_party/ascend/lib/DiscreteMaskAccessConversion/DiscreteMaskAccessConversionPass.cpp b/third_party/ascend/lib/DiscreteMaskAccessConversion/DiscreteMaskAccessConversionPass.cpp new file mode 100644 index 0000000000..156b6e90cc --- /dev/null +++ b/third_party/ascend/lib/DiscreteMaskAccessConversion/DiscreteMaskAccessConversionPass.cpp @@ -0,0 +1,808 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "Utils/Utils.h" +#include "ascend/include/DiscreteMaskAccessConversion/Passes.h" + +#include "TritonControlFlowOpt/ControlFlowRewrite.h" +#include "ascend/include/TritonToLinalg/LoadStoreConverter.h" +#include "ascend/include/TritonToLinalg/MaskAnalysis.h" +#include "ascend/include/TritonToStructured/MemOpConverter.h" +#include "ascend/include/TritonToUnstructure/OffsetAnalysis.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/DialectConversion.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/LogicalResult.h" + +namespace mlir { +namespace triton { +#define GEN_PASS_DEF_DISCRETEMASKACCESSCONVERSION +#include "ascend/include/DiscreteMaskAccessConversion/Passes.h.inc" +} // namespace triton +} // namespace mlir + +#define DEBUG_TYPE "discrete-mask-access-conversion" + +using namespace mlir; +using namespace hivm; + +// File-scope flags set by DiscreteMaskAccessConversionPass::runOnOperation() +// before pattern application, so that OpRewritePattern subclasses can read +// them. +static bool compileOn91095Flag = false; +static triton::ascend::CompileMode compileModeFlag = + triton::ascend::CompileMode::Simd; +static bool useSyncBlockLockFlag = true; + +// Only signed comparisons against the same fixed, zero-based range are +// accepted. Preserve the original bound's integer semantics, including +// negative values; MaskState clamps the eventual access extent. +static OpFoldResult getPrefixBound(Value mask, Value range) { + auto cmp = mask.getDefiningOp(); + if (!cmp || cmp.getPredicate() != arith::CmpIPredicate::slt || + cmp.getLhs() != range) + return {}; + if (auto splat = cmp.getRhs().getDefiningOp()) + return splat.getSrc(); + if (auto constant = cmp.getRhs().getDefiningOp()) { + auto dense = dyn_cast(constant.getValue()); + if (dense && dense.isSplat()) + return IntegerAttr::get(dense.getElementType(), + dense.getSplatValue()); + } + return {}; +} + +// (lane < bound) & (lane < limit) == lane < min(bound, limit). +// Carry the exact scalar boundary instead of a tensor. This retains the +// history even when limits grow again, and lets the existing range lowering +// keep using subview/copy. Do not change CFO-owned descriptor signatures. +struct CanonicalizeLoopPrefixMask : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(scf::ForOp loop, + PatternRewriter &rewriter) const override { + if (loop->hasAttr(triton::controlflow::kPointerDescriptorBoundaryAttr)) + return failure(); + + std::optional maskSlot; + for (auto [slot, init] : llvm::enumerate(loop.getInitArgs())) { + auto type = dyn_cast(init.getType()); + if (!type || !type.getElementType().isInteger(1)) + continue; + if (maskSlot || type.getRank() != 1 || !type.hasStaticShape()) + return failure(); + maskSlot = slot; + } + if (!maskSlot) + return failure(); + + unsigned slot = *maskSlot; + auto iterMask = loop.getRegionIterArgs()[slot]; + auto yield = cast(loop.getBody()->getTerminator()); + auto update = yield.getOperand(slot).getDefiningOp(); + if (!update || update->getBlock() != loop.getBody()) + return failure(); + Value condition; + if (update.getLhs() == iterMask) + condition = update.getRhs(); + else if (update.getRhs() == iterMask) + condition = update.getLhs(); + else + return failure(); + + auto cmp = condition.getDefiningOp(); + if (!cmp) + return failure(); + auto range = cmp.getLhs().getDefiningOp(); + auto maskType = cast(iterMask.getType()); + if (!range || range.getStart() != 0 || range.getEnd() <= 0 || + range.getEnd() != maskType.getDimSize(0) || + !loop.isDefinedOutsideOfLoop(range.getResult())) + return failure(); + OpFoldResult limit = getPrefixBound(condition, range); + if (!limit) + return failure(); + + Value init = loop.getInitArgs()[slot]; + OpFoldResult initialBound = getPrefixBound(init, range); + if (!initialBound) { + auto constant = init.getDefiningOp(); + auto dense = constant + ? dyn_cast(constant.getValue()) + : DenseIntElementsAttr(); + if (!dense || !dense.isSplat()) + return failure(); + initialBound = rewriter.getI32IntegerAttr( + dense.getSplatValue().isZero() ? 0 : range.getEnd()); + } + + IRMapping mapping; + auto materializeBound = [&](OpFoldResult bound) -> Value { + if (auto value = dyn_cast(bound)) + return mapping.lookupOrDefault(value); + return rewriter.create( + loop.getLoc(), cast(cast(bound))); + }; + auto makeMask = [&](Value bound) -> Value { + auto splat = rewriter.create(loop.getLoc(), + range.getType(), bound); + return rewriter.create( + loop.getLoc(), arith::CmpIPredicate::slt, range.getResult(), splat); + }; + + SmallVector inits(loop.getInitArgs()); + inits[slot] = materializeBound(initialBound); + auto newLoop = rewriter.create( + loop.getLoc(), loop.getLowerBound(), loop.getUpperBound(), + loop.getStep(), inits); + newLoop->setAttrs(loop->getAttrs()); + if (!newLoop.getBody()->empty()) + rewriter.eraseOp(newLoop.getBody()->getTerminator()); + rewriter.setInsertionPointToStart(newLoop.getBody()); + mapping.map(loop.getInductionVar(), newLoop.getInductionVar()); + for (auto [index, argument] : llvm::enumerate(loop.getRegionIterArgs())) { + Value replacement = newLoop.getRegionIterArgs()[index]; + if (index == slot) + replacement = makeMask(replacement); + mapping.map(argument, replacement); + } + + Value nextBound; + for (Operation &op : loop.getBody()->without_terminator()) { + if (&op == update.getOperation()) { + nextBound = rewriter.create( + update.getLoc(), newLoop.getRegionIterArgs()[slot], + materializeBound(limit)); + mapping.map(update.getResult(), makeMask(nextBound)); + } else { + rewriter.clone(op, mapping); + } + } + SmallVector yielded; + for (auto [index, value] : llvm::enumerate(yield.getOperands())) + yielded.push_back(index == slot ? nextBound + : mapping.lookupOrDefault(value)); + rewriter.create(yield.getLoc(), yielded); + + rewriter.setInsertionPointAfter(newLoop); + SmallVector results(newLoop.getResults()); + // Reconstruct the initial mask too when the loop executes zero times. + results[slot] = makeMask(results[slot]); + rewriter.replaceOp(loop, results); + return success(); + } +}; + +static bool dependsOnLoopMask(Value mask) { + if (!mask) + return false; + SmallVector worklist{mask}; + llvm::SmallPtrSet visited; + while (!worklist.empty()) { + Value value = worklist.pop_back_val(); + if (!visited.insert(value).second) + continue; + auto type = dyn_cast(value.getType()); + bool isMask = type && type.getElementType().isInteger(1); + if (auto argument = dyn_cast(value)) { + if (isMask && + isa(argument.getOwner()->getParentOp())) + return true; + continue; + } + Operation *producer = value.getDefiningOp(); + if (!producer) + continue; + if (isMask && isa(producer)) + return true; + llvm::append_range(worklist, producer->getOperands()); + // Follow captured masks through region results, e.g. scf.if yields. + for (Region ®ion : producer->getRegions()) + for (Block &block : region) + llvm::append_range(worklist, block.getTerminator()->getOperands()); + } + return false; +} + +static void markRuntimeLoopMasks(ModuleOp module) { + module.walk([](Operation *op) { + Value mask; + if (auto load = dyn_cast(op)) + mask = load.getMask(); + else if (auto store = dyn_cast(op)) + mask = store.getMask(); + else if (auto atomic = dyn_cast(op)) + mask = atomic.getMask(); + if (!dependsOnLoopMask(mask)) + return; + OpBuilder builder(op); + MaskState state; + if (succeeded(state.parse(mask, op->getLoc(), builder))) + return; + // Unknown loop masks must reach a lowering that consumes the actual mask. + // In particular, do not use the SIMD full-load-and-select fallback. + op->setAttr(ConverterUtils::runtimeLoopMaskAttrName, builder.getUnitAttr()); + op->setAttr(ConverterUtils::mixCompileDiscreteMaskAttrName, + builder.getUnitAttr()); + }); +} + +static void markSyncBlockLockUnordered(Operation *op) { + op->setAttr(hivm::SyncBlockLockUnorderedAttr::name, + UnitAttr::get(op->getContext())); +} + +static bool traceUserToTargetOp(Value val) { + llvm::SmallVector worklist; + llvm::SmallPtrSet visited; + worklist.push_back(val); + + while (!worklist.empty()) { + Value currVal = worklist.pop_back_val(); + if (!visited.insert(currVal).second) + continue; + + for (Operation *user : currVal.getUsers()) { + if (auto mulOp = dyn_cast(user)) { + Value lhs = mulOp.getLhs(); + Value rhs = mulOp.getRhs(); + Value constVal; + if (lhs.getDefiningOp()) { + constVal = lhs; + } else if (rhs.getDefiningOp()) { + constVal = rhs; + } else { + continue; + } + + auto constDef = constVal.getDefiningOp(); + int64_t blockSize = 0; + if (auto intAttr = mlir::dyn_cast(constDef.getValue())) { + blockSize = intAttr.getInt(); + } + + llvm::SmallVector searchQueue; + llvm::SmallPtrSet searchVis; + for (Value mulRes : mulOp->getResults()) { + searchQueue.push_back(mulRes); + searchVis.insert(mulRes); + } + + bool findMatch = false; + while (!searchQueue.empty()) { + Value checkVal = searchQueue.pop_back_val(); + for (Operation *subUser : checkVal.getUsers()) { + if (auto addOp = dyn_cast(subUser)) { + Value otherOperand = (addOp.getLhs() == checkVal) + ? addOp.getRhs() + : addOp.getLhs(); + + Value curSrc = otherOperand; + bool hitRange = false; + int depth = 0; + while (curSrc.getDefiningOp() && depth < 5) { + Operation *defOp = curSrc.getDefiningOp(); + if (auto rangeOp = dyn_cast(defOp)) { + if (rangeOp.getEnd() == blockSize) { + hitRange = true; + break; + } + } + + if (isa(defOp)) { + curSrc = defOp->getOperand(0); + depth++; + continue; + } + break; + } + if (hitRange) { + findMatch = true; + break; + } + } + if (isa(subUser)) { + for (Value subRes : subUser->getResults()) { + if (!searchVis.count(subRes)) { + searchVis.insert(subRes); + searchQueue.push_back(subRes); + } + } + } + } + if (findMatch) + break; + } + if (findMatch) { + return true; + } + + for (Value mulRes : mulOp->getResults()) { + worklist.push_back(mulRes); + } + } + + if (isa(user)) { + for (Value res : user->getResults()) { + worklist.push_back(res); + } + } + } + } + return false; +} + +static bool checkAllProgramIdNonOverlap(ModuleOp module) { + bool allNonOverlap = true; + module.walk([&](triton::GetProgramIdOp pidOp) { + if (!traceUserToTargetOp(pidOp.getResult())) { + allNonOverlap = false; + } + }); + return allNonOverlap; +} + +LogicalResult isDiscreteMask(Operation *op, Value mask, + PatternRewriter &rewriter) { + if (!mask || op->hasAttr(ConverterUtils::mixCompileDiscreteMaskAttrName)) { + return failure(); + } + + MaskState mstate; + auto isContMask = mstate.parse(mask, op->getLoc(), rewriter); + if (!isContMask.failed()) { + mstate.eraseInsertedOps(op, rewriter); + return failure(); + } + return success(); +} + +// Recursively collect all leaf operands of a nested arith::AndIOp tree. +// This function also normalizes masks by distributing broadcast over andi +// broadcast(andi(a, b)) = andi(broadcast(a), broadcast(b)) +// so that inner AND operands nested inside a broadcast are still reachable. +static void collectAndLeaves(Value mask, SmallVectorImpl &leaves, + Location loc, PatternRewriter &rewriter) { + if (auto andOp = mask.getDefiningOp()) { + collectAndLeaves(andOp.getLhs(), leaves, loc, rewriter); + collectAndLeaves(andOp.getRhs(), leaves, loc, rewriter); + } else if (auto broadcastOp = mask.getDefiningOp()) { + // Distribute broadcast over andi so we can inspect each factor separately. + if (auto innerAnd = broadcastOp.getSrc().getDefiningOp()) { + Type dstType = mask.getType(); + Value broadcastA = + rewriter.create(loc, dstType, innerAnd.getLhs()) + .getResult(); + Value broadcastB = + rewriter.create(loc, dstType, innerAnd.getRhs()) + .getResult(); + collectAndLeaves(broadcastA, leaves, loc, rewriter); + collectAndLeaves(broadcastB, leaves, loc, rewriter); + } else { + leaves.push_back(mask); + } + } else { + leaves.push_back(mask); + } +} + +struct MaskDecomposition { + // AND of all leaves that MaskState::parse() can analyze as a rectangle mask. + // nullptr when no such leaves exist. + Value contMask; + // AND of all leaves that MaskState::parse() cannot analyze + // (discrete/runtime). nullptr when no such leaves exist. + Value discMask; +}; + +// Decompose an AND-tree mask into its continuous and discrete leaf components +// so that we can use contMask to bound GM accesses while discMask still drives +// the per-element selection. +static MaskDecomposition decomposeAndMask(Operation *op, Value mask, + const Location &loc, + PatternRewriter &rewriter) { + SmallVector leaves; + collectAndLeaves(mask, leaves, loc, rewriter); + + SmallVector contLeaves; + SmallVector discLeaves; + + for (Value leaf : leaves) { + MaskState st; + if (st.parse(leaf, loc, rewriter).succeeded()) { + if (st.isMask()) + contLeaves.push_back(leaf); + else + discLeaves.push_back(leaf); + } else { + discLeaves.push_back(leaf); + } + } + + Value contMask = nullptr; + for (Value v : contLeaves) + contMask = + contMask ? rewriter.create(loc, contMask, v).getResult() + : v; + + Value discMask = nullptr; + for (Value v : discLeaves) + discMask = + discMask ? rewriter.create(loc, discMask, v).getResult() + : v; + + return {contMask, discMask}; +} + +struct DiscreteMaskStoreConversion : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const final { + if (op->hasAttr(ConverterUtils::mixCompileDiscreteMaskAttrName)) + return failure(); + + auto mask = op.getMask(); + auto loc = op.getLoc(); + auto dst = op.getPtr(); + auto src = op.getValue(); + + if (failed(isDiscreteMask(op, mask, rewriter))) + return failure(); + + auto ptr = op.getPtr(); + auto ptrType = dyn_cast(ptr.getType()); + bool rankWithinIndirectFastPathLimit = + ptrType && ptrType.getShape().size() <= 5; + if (compileOn91095Flag && + triton::ascend::isSimtTemplateMode(compileModeFlag) && + rankWithinIndirectFastPathLimit) { + op->setAttr(ConverterUtils::mixCompileDiscreteMaskAttrName, + rewriter.getUnitAttr()); + return failure(); + } + + // When mask = contMask & discMask, use contMask to bound GM accesses and + // discMask to select the final per-element value. This prevents the + // unguarded full-load from reading past the tail-block boundary. + auto [contMask, discMask] = decomposeAndMask(op, mask, loc, rewriter); + if (contMask && discMask) { + // insert sync_block_lock (unordered: see markSyncBlockLockUnordered) + auto lockVar = MemOpConverter::createSyncBlockLockVar(rewriter, loc); + markSyncBlockLockUnordered(lockVar.getOperation()); + if (useSyncBlockLockFlag) { + rewriter.create( + loc, + hivm::PipeAttr::get(rewriter.getContext(), hivm::PIPE::PIPE_ALL)); + auto lockOp = rewriter.create(loc, lockVar); + markSyncBlockLockUnordered(lockOp.getOperation()); + } + auto safeLoad = rewriter.create( + loc, dst, contMask, op.getCache(), op.getEvict(), false); + auto selOp = rewriter.create(loc, discMask, src, + safeLoad.getResult()); + auto newStore = rewriter.create( + loc, dst, selOp, contMask, op.getCache(), op.getEvict()); + newStore->setAttr(ConverterUtils::discreteMaskAttrName, + UnitAttr::get(rewriter.getContext())); + if (useSyncBlockLockFlag) { + auto unlockOp = rewriter.create(loc, lockVar); + markSyncBlockLockUnordered(unlockOp.getOperation()); + } + rewriter.replaceOp(op, newStore); + return success(); + } + + // SIMD fallback: original full load + select (contMask absent, pure + // discrete). Has DDR OOB risk but no better option in pure simd mode. + // insert sync_block_lock to serialize the read-modify-write window. + auto lockVar = MemOpConverter::createSyncBlockLockVar(rewriter, loc); + markSyncBlockLockUnordered(lockVar.getOperation()); + if (useSyncBlockLockFlag) { + rewriter.create( + loc, + hivm::PipeAttr::get(rewriter.getContext(), hivm::PIPE::PIPE_ALL)); + auto lockOp = rewriter.create(loc, lockVar); + markSyncBlockLockUnordered(lockOp.getOperation()); + } + auto loadFromDstOp = rewriter.create( + loc, dst, op.getCache(), op.getEvict(), false); + auto selOp = rewriter.create(loc, mask, src, + loadFromDstOp.getResult()); + auto newStore = rewriter.create( + loc, dst, selOp, op.getCache(), op.getEvict()); + newStore->setAttr(ConverterUtils::discreteMaskAttrName, + UnitAttr::get(rewriter.getContext())); + if (useSyncBlockLockFlag) { + auto unlockOp = rewriter.create(loc, lockVar); + markSyncBlockLockUnordered(unlockOp.getOperation()); + } + rewriter.replaceOp(op, newStore); + return success(); + } +}; + +struct DiscreteMaskLoadConversion : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::LoadOp op, + PatternRewriter &rewriter) const final { + if (op->hasAttr(ConverterUtils::mixCompileDiscreteMaskAttrName)) + return failure(); + + auto loc = op.getLoc(); + auto other = op.getOther(); + auto mask = op.getMask(); + auto ptr = op.getPtr(); + + if (failed(isDiscreteMask(op, mask, rewriter))) + return failure(); + + auto ptrType = dyn_cast(ptr.getType()); + bool rankWithinIndirectFastPathLimit = + ptrType && ptrType.getShape().size() <= 5; + if (compileOn91095Flag && + triton::ascend::isSimtTemplateMode(compileModeFlag) && + rankWithinIndirectFastPathLimit) { + op->setAttr(ConverterUtils::mixCompileDiscreteMaskAttrName, + rewriter.getUnitAttr()); + return failure(); + } + + // SIMD path: when mask = contMask & discMask, load only the safe range + // defined by contMask and use discMask for the per-element select, + // avoiding OOB reads. + auto [contMask, discMask] = decomposeAndMask(op, mask, loc, rewriter); + if (contMask && discMask) { + if (!other) { + FailureOr constant = specializeTypelessValueToConstant( + TypelessValue::Zero, ptr.getType(), loc, rewriter); + if (failed(constant)) { + llvm_unreachable("Unsupported type for constant creation"); + } + other = *constant; + } + auto safeLoad = rewriter.create( + loc, ptr, contMask, op.getCache(), op.getEvict(), op.getIsVolatile()); + // Use combined mask to select the result, avoid the uninitialized memory + // access. + auto combinedMask = + rewriter.create(loc, contMask, discMask); + auto discreteMaskOp = rewriter.create( + loc, combinedMask, safeLoad.getResult(), other); + rewriter.replaceOp(op, discreteMaskOp); + return success(); + } + + // Fallback: original full load + select (contMask absent, pure discrete). + if (!other) { + FailureOr constant = specializeTypelessValueToConstant( + TypelessValue::Zero, ptr.getType(), loc, rewriter); + if (failed(constant)) + llvm_unreachable("Unsupported type for constant creation"); + other = *constant; + } + + auto newLoadOp = rewriter.create( + loc, ptr, op.getCache(), op.getEvict(), op.getIsVolatile()); + auto discreteMaskOp = + rewriter.create(loc, mask, newLoadOp, other); + rewriter.replaceOp(op, discreteMaskOp); + return success(); + } +}; + +struct DiscreteMaskAtomicConversion + : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + bool routeToSIMD(mlir::triton::AtomicRMWOp op, + PatternRewriter &rewriter) const { + if (op->hasAttr(ConverterUtils::explicitRouteToSIMDAttrName)) { + return op + ->getAttrOfType(ConverterUtils::explicitRouteToSIMDAttrName) + .getValue(); + } + + auto loc = op.getLoc(); + auto ptr = op.getPtr(); + llvm::DenseMap offsetMap; + triton::parse(ptr, loc, rewriter, offsetMap); + auto it = offsetMap.find(ptr); + auto toSimd = it != offsetMap.end() && + llvm::all_of(it->second.getStructuredRef(), [](auto dim) { + return dim == PtrOffsetInfo::AxisInfo::structured; + }); + op->setAttr(ConverterUtils::explicitRouteToSIMDAttrName, + rewriter.getBoolAttr(toSimd)); + return toSimd; + } + + LogicalResult matchAndRewrite(mlir::triton::AtomicRMWOp op, + PatternRewriter &rewriter) const final { + auto loc = op.getLoc(); + auto ptr = op.getPtr(); + auto src = op.getVal(); + auto mask = op.getMask(); + RMWOp rmwOp = op.getAtomicRmwOp(); + + if (failed(isDiscreteMask(op, mask, rewriter))) + return failure(); + + // Keep the original mask for the SIMT atomic template when the pointer + // needs the indirect atomic ABI. The unstructure pass flattens it together + // with offsets and values; replacing it with a select here would lose that + // lane mask. Structured pointers do not need the indirect ABI, so fall + // through to the SIMD select+atomic rewrite below instead. + if (compileOn91095Flag && + triton::ascend::isSimtTemplateMode(compileModeFlag) && + !routeToSIMD(op, rewriter)) { + op->setAttr(ConverterUtils::mixCompileDiscreteMaskAttrName, + rewriter.getUnitAttr()); + return failure(); + } + + const std::map initMap = { + {RMWOp::FADD, TypelessValue::Zero}, + {RMWOp::ADD, TypelessValue::Zero}, + {RMWOp::UMAX, TypelessValue::Zero}, + {RMWOp::OR, TypelessValue::Zero}, + {RMWOp::MIN, TypelessValue::Max}, + {RMWOp::UMIN, TypelessValue::Max}, + {RMWOp::AND, TypelessValue::Max}, + {RMWOp::MAX, TypelessValue::Min}, + {RMWOp::XOR, TypelessValue::Zero}, + {RMWOp::XCHG, TypelessValue::Undefined}, + }; + assert(initMap.find(rmwOp) != initMap.end()); + auto typelessVal = initMap.at(rmwOp); + if (typelessVal == TypelessValue::Undefined) { + // Undefined default value atomic op will be decomposed in AscendNPU-IR + op->setAttr(ConverterUtils::discreteMaskAttrName, + UnitAttr::get(rewriter.getContext())); + return failure(); + } + + auto [contMask, discMask] = decomposeAndMask(op, mask, loc, rewriter); + FailureOr fill = specializeTypelessValueToConstant( + typelessVal, src.getType(), loc, rewriter); + if (failed(fill)) { + LLVM_DEBUG({ + llvm::dbgs() << "Unsupported type for constant creation: " + << src.getType() << "\n"; + }); + op->emitError("Unsupported atomic operation."); + return failure(); + } + + // For mask = contMask & discMask, retain contMask as the memory-access + // guard and replace only the discrete part with the RMW identity value. + // The continuous mask can then be lowered to a bounded subview without + // accessing the masked-off tail. + Value valueMask = mask; + Value accessMask = nullptr; + if (contMask && discMask) { + valueMask = discMask; + accessMask = contMask; + } + + auto maskedValue = + rewriter.create(loc, valueMask, src, *fill); + auto newAtomicOp = rewriter.create( + loc, src.getType(), rmwOp, ptr, maskedValue, accessMask, op.getSem(), + op.getScope()); + rewriter.replaceOp(op, newAtomicOp); + return success(); + } +}; + +DiscreteMaskAccessConversionPass::DiscreteMaskAccessConversionPass( + const DiscreteMaskAccessConversionOptions &options) + : DiscreteMaskAccessConversionBase(options) {} + +void DiscreteMaskAccessConversionPass::runOnOperation() { + compileOn91095Flag = this->compileOn91095; + auto compileMode = triton::ascend::parseCompileMode(this->compileMode); + if (!compileMode) { + getOperation().emitError() + << "discrete-mask-access-conversion compile-mode is invalid: " + << this->compileMode; + signalPassFailure(); + return; + } + compileModeFlag = *compileMode; + auto moduleOp = getOperation(); + RewritePatternSet loopMaskPatterns(&getContext()); + loopMaskPatterns.add(&getContext()); + if (failed(applyPatternsGreedily(moduleOp, std::move(loopMaskPatterns)))) { + moduleOp.emitError("failed to canonicalize loop prefix masks"); + signalPassFailure(); + return; + } + markRuntimeLoopMasks(moduleOp); + + bool tileNonOverlap = checkAllProgramIdNonOverlap(moduleOp); + useSyncBlockLockFlag = !tileNonOverlap; + + // Restore floating-point atomic max/min expanded by semantic.py before + // discrete-mask rewriting changes the atomic value into arith.select. + // Run this in a separate greedy-rewrite phase so that + // DiscreteMaskAtomicConversion cannot consume the expanded form first. + RewritePatternSet atomicMaxMinPatterns(&getContext()); + atomicMaxMinPatterns.add( + atomicMaxMinPatterns.getContext()); + if (failed( + applyPatternsGreedily(moduleOp, std::move(atomicMaxMinPatterns)))) { + moduleOp->emitError("failed to canonicalize floating-point atomic max/min"); + signalPassFailure(); + return; + } + + RewritePatternSet patterns(&getContext()); + patterns.add(patterns.getContext()); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + moduleOp->emitError("failed to apply discrete mask access patterns"); + signalPassFailure(); + } + + // Clean up dead analysis ops left behind by MaskState::parse(). + // These are trivially-dead auxiliary ops (constants, arithmetic) with no + // users that parse() creates as side effects of mask analysis. + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + if (failed(runPipeline(pm, getOperation()))) { + moduleOp->emitWarning( + "DiscreteMaskAccessConversion: dead-code cleanup failed"); + } + + LLVM_DEBUG({ + llvm::dbgs() << "==============================================\n"; + llvm::dbgs() << "After DiscreteMaskAccessConversionPass:\n" << moduleOp; + llvm::dbgs() << "\n==============================================\n"; + }); +} + +void DiscreteMaskAccessConversionPass::getDependentDialects( + DialectRegistry ®istry) const { + registry.insert(); +} + +std::unique_ptr> +mlir::triton::createDiscreteMaskAccessConversionPass( + const DiscreteMaskAccessConversionOptions &options) { + return std::make_unique(options); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition.cpp new file mode 100644 index 0000000000..0ac691b7b1 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/CloneOps.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/CreateIfOps.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/InitDependentMap.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/ProcessArgs.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateConditionInfo.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopIterTimes.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopOps.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Pass/PassRegistry.h" +#include "llvm/Support/Debug.h" +#include + +static constexpr const char *DEBUG_TYPE = "AddControlFlowCondition"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(X) LLVM_DEBUG(DBGS() << (X) << "\n") + +using namespace mlir; +using namespace triton; + +// Check if the module should be skipped for control flow condition processing +static LogicalResult verifyControlFlowPrerequisites(ModuleOp module) { + // Check if scopeOp has ssbuffer.skip + bool hasSkipAttr = false; + module.walk([&](Operation *op) { + auto scopeOp = dyn_cast(op); + if (!scopeOp) { + return; + } + if (scopeOp->hasAttr("ssbuffer.skip")) { + hasSkipAttr = true; + } + }); + if (hasSkipAttr) { + LDBG("scopeOp has ssbuffer.skip, skip processing."); + return failure(); + } + + return success(); +} + +void AddControlFlowConditionPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Enter add controlflow condition pass.\n"); + LDBG("before AddControlFlowCondition:"); + LLVM_DEBUG(module.dump()); + + if (failed(verifyControlFlowPrerequisites(module))) { + return; + } + + PassManager pm(&getContext(), module.getOperationName()); + ControlFlowConditionInfo info; + + // Step0: Clone ops in vector/cube to ensure that each block_id has its own + // ops without sharing + pm.addPass(createCloneOpsPass()); + + // Step1: Process shared iter_args in for ops to eliminate arg sharing across + // block_ids + std::unique_ptr processArgsPass(new ProcessArgsPass()); + processArgsPass->setConditionInfo(&info); + pm.addPass(std::move(processArgsPass)); + + // Step2: Create if ops based on block_id + std::unique_ptr createIfOpsPass(new CreateIfOpsPass()); + createIfOpsPass->setConditionInfo(&info); + pm.addPass(std::move(createIfOpsPass)); + + // Step3: Initialize crossCoreDependentMap, intraCoreDependentMap, and build + // if block DAG + std::unique_ptr initDependentMapPass( + new InitDependentMapPass()); + initDependentMapPass->setConditionInfo(&info); + pm.addPass(std::move(initDependentMapPass)); + + // Step4: Update for ops with block counters and inner dependency conditions, + // and insert PIPE_S inter-core synchronization + std::unique_ptr updateLoopOpsPass(new UpdateLoopOpsPass()); + updateLoopOpsPass->setConditionInfo(&info); + pm.addPass(std::move(updateLoopOpsPass)); + + // Step5:Update the conditions of ifOp based on the intraCoreDependentMap and + // crossCoreDependentMap + + constexpr llvm::StringLiteral legacyKernel[] = { + "chunk_gated_delta_rule_fwd_kernel_h_blockdim128"}; + bool useLegacyConditions = false; + for (auto &op : module.getOps()) { + auto funcOp = llvm::dyn_cast(&op); + if (funcOp && llvm::is_contained(legacyKernel, funcOp.getSymName())) { + useLegacyConditions = true; + break; + } + } + + auto updatePass = std::make_unique(); + updatePass->setConditionInfo(&info); + pm.addPass(std::move(updatePass)); + + // Step6: Update for loop iteration times based on intraCoreDependentMap + std::unique_ptr updateLoopIterTimesPass( + new UpdateLoopIterTimesPass()); + updateLoopIterTimesPass->setConditionInfo(&info); + pm.addPass(std::move(updateLoopIterTimesPass)); + + if (failed(runPipeline(pm, module))) { + LDBG("Pass failed!"); + if (!CVPipeline::hasFallbackAttr(module)) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + } + + LDBG("Exit add controlflow condition pass."); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAddControlFlowConditionPass() { + return std::make_unique(); +} + +void registerAddControlFlowConditionPasses() { + registerPass(createCloneOpsPass); + registerPass(createCreateIfOpsPass); + registerPass(createProcessArgsPass); + registerPass(createUpdateLoopOpsPass); + registerPass(createAddControlFlowConditionPass); +} +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/CloneOps.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/CloneOps.cpp new file mode 100644 index 0000000000..e29935f5a6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/CloneOps.cpp @@ -0,0 +1,625 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/CloneOps.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/IRMapping.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "CloneOps"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace mlir; +using namespace triton; +using namespace CVPipeline; +using namespace hivm; + +using MemDepGraph = std::unique_ptr; +using MemDepGraphT = CVPipeline::MemoryDependenceGraph; + +// Updates op operands via value mapping; skips main-loop body yield values +// (yieldValues collected once per main-loop from the body's terminator). +static LogicalResult +updateCloneMapping(Operation *op, llvm::DenseMap &valueMap, + const llvm::DenseSet &yieldValues) { + if (!op) { + return failure(); + } + + for (OpOperand &operand : op->getOpOperands()) { + // Only skip if this operand is a yield value from the main_loop body. + // Nested yield ops (in scf.if/scf.for) should have their operands updated. + Value v = operand.get(); + if (yieldValues.contains(v)) { + continue; + } + + auto it = valueMap.find(v); + if (it != valueMap.end()) { + if (it->second.getType() != v.getType()) { + LDBG("[Error]: type mismatch in value mapping: " + << v.getType() << " vs " << it->second.getType()); + return failure(); + } + operand.set(it->second); + } + } + + for (Region ®ion : op->getRegions()) { + for (Block &block : region) { + for (Operation &nestedOp : block) { + if (failed(updateCloneMapping(&nestedOp, valueMap, yieldValues))) { + return failure(); + } + } + } + } + + return success(); +} + +// Clone a single op with IRMapping +static Operation *cloneOpWithMapping(Operation *op, OpBuilder &builder, + llvm::DenseMap &valueMap) { + IRMapping mapper; + // Populate mapper with ALL previously cloned values (not just the current + // op's results). + for (const auto &entry : valueMap) { + mapper.map(entry.first, entry.second); + } + + Operation *cloned = builder.clone(*op, mapper); + for (auto it : llvm::zip(op->getResults(), cloned->getResults())) { + valueMap[std::get<0>(it)] = std::get<1>(it); + } + + return cloned; +} + +// Clones ops for a single block in vector/cube mode. `bodyBlock` (forOp body +// or whileOp after-region) only supplies iter-arg yields not to be remapped. +static LogicalResult +cloneOpsForBlock(int curId, SmallVector &curOps, + const SmallVector &earlierIds, + const llvm::DenseMap> &blockOps, + Block *bodyBlock) { + if (curOps.empty() || earlierIds.empty()) { + return success(); + } + + // Collect all ops from earlier blocks to clone + SmallVector toClone; + for (int eid : earlierIds) { + llvm::append_range(toClone, blockOps.lookup(eid)); + } + if (toClone.empty()) { + return success(); + } + + llvm::DenseMap valueMap; + SmallVector clonedOps; + OpBuilder builder(curOps.front()); + + for (Operation *op : toClone) { + Operation *cloned = cloneOpWithMapping(op, builder, valueMap); + cloned->setAttr(CVPipeline::kBlockId, builder.getI32IntegerAttr(curId)); + if (auto origBlockIdOpt = CVPipeline::getOpBlockId(op)) { + cloned->setAttr( + CVPipeline::kClone, + builder.getI32IntegerAttr(static_cast(*origBlockIdOpt))); + } + clonedOps.push_back(cloned); + } + + curOps.insert(curOps.begin(), clonedOps.begin(), clonedOps.end()); + + llvm::DenseSet yieldValues; + if (bodyBlock) { + if (auto yieldOp = dyn_cast(bodyBlock->getTerminator())) { + for (Value operand : yieldOp.getOperands()) { + yieldValues.insert(operand); + } + } + } + + for (Operation *op : curOps) { + if (failed(updateCloneMapping(op, valueMap, yieldValues))) { + return failure(); + } + } + + if (failed(topologicalSort(curOps))) { + return failure(); + } + + return success(); +} + +// Clones ops for all blocks in the main loop op (scf.for or scf.while). +LogicalResult CloneOpsPass::cloneOpsInMainLoop(Operation *op) { + Block *bodyBlock = MainLoop(op).getBody(); + if (!bodyBlock) { + LDBG("[Error]: op with ssbuffer.main_loop is not a scf::ForOp or " + "scf::WhileOp"); + return failure(); + } + + llvm::DenseMap> blockOps; + if (failed(collectOpsByBlockId(op, blockOps))) { + return failure(); + } + SmallVector idsInOrder = getBlockIdsInOrder(op); + + for (int i = idsInOrder.size() - 1; i >= 0; --i) { + int curId = idsInOrder[i]; + SmallVector earlierIds(idsInOrder.begin(), idsInOrder.begin() + i); + + if (failed(cloneOpsForBlock(curId, blockOps[curId], earlierIds, blockOps, + bodyBlock))) { + return failure(); + } + } + + return success(); +} + +// Should an op be erased during cube cleanup? sameBlockIdExecAfter: per-block +// same-block-id exec-after ops (sync filtered); erasedOps no longer pin ops. +static bool shouldEraseOpForCube( + Operation *op, + const llvm::DenseMap> + &sameBlockIdExecAfter, + const llvm::DenseSet &erasedOps) { + // Rule 1: SyncBlockWaitOp, SyncBlockSetOp, FixpipeOp -> directly erase, + // including when inter-core multi-buffering has already wrapped them in an + // scf.if picking the flag for this iteration's buffer half. Only scf.ifs + // that yield nothing qualify: the wrapper around a transfer op yields its + // result and is data, not a handshake. + if (isa(op) || isa(op) || + isa(op) || + (op->getNumResults() == 0 && isIfOpWithOnlySyncOps(op))) { + return true; + } + + auto opBlockId = getLoopDirectChildBlockId(op); + + // Rule 2: if op has results, check via SSA whether any result is used by a + // later op with the same loop-direct-child block_id (main-loop's own child). + if (op->getNumResults() > 0) { + for (auto result : op->getResults()) { + if (result.use_empty()) { + // Result not used by anyone, can erase + continue; + } + if (!opBlockId) { + // No block_id but result is used, be conservative and keep + return false; + } + // Check if any user is in the same loop-direct-child block_id + bool usedInSameBlockId = + llvm::any_of(result.getUsers(), [&](Operation *user) { + auto userBlockId = getLoopDirectChildBlockId(user); + return userBlockId && *userBlockId == *opBlockId; + }); + if (usedInSameBlockId) { + // Result used in same loop-direct-child block, cannot erase + return false; + } + } + // All results are either unused or not used in same loop-direct-child + // block, can erase + return true; + } + + // Rule 3: no results -> consult precomputed same-block-id exec-after set; ops + // already erased in this pass are not live (mirrors per-iteration rebuild). + auto it = sameBlockIdExecAfter.find(op); + if (it != sameBlockIdExecAfter.end()) { + for (Operation *execOp : it->second) { + // Already erased in this pass → no longer pins the current op. + if (erasedOps.contains(execOp)) { + continue; + } + // scf.if bodies holding only sync_block_wait/set are cleaned up in their + // own turn; skip so they don't block this op's erasure. + if (isIfOpWithOnlySyncOps(execOp)) { + continue; + } + auto execBlockId = CVPipeline::getOpBlockId(execOp); + if (execBlockId && opBlockId && *execBlockId == *opBlockId) { + return false; + } + } + } + + // Can erase if no results and no live exec-after dependencies in same block + return true; +} + +// Check if an op should be erased (for vector) +static bool shouldEraseOpForVector(Operation *op) { + return llvm::none_of(op->getResults(), + [](auto result) { return !result.use_empty(); }); +} + +// Verify cloned sync/fixpipe ops were erased after cleanup; nullptr bodyBlock +// means nothing to check. +static LogicalResult validateClonedSyncOpsErased(Block *bodyBlock) { + if (!bodyBlock) { + return success(); + } + for (Operation &op : bodyBlock->without_terminator()) { + if (!op.hasAttr(CVPipeline::kClone)) { + continue; + } + if (isa(&op) || isa(&op) || + isa(&op)) { + LDBG("[ERROR]: Cloned sync/fixpipe op should have been erased: " + << op.getName()); + return failure(); + } + // Look inside as well + WalkResult nested = op.walk([](Operation *inner) { + return isa(inner) || isa(inner) + ? WalkResult::interrupt() + : WalkResult::advance(); + }); + if (nested.wasInterrupted()) { + LDBG("[ERROR]: Cloned op still holds a sync op that should have been " + "erased: " + << op.getName()); + return failure(); + } + } + + return success(); +} + +// Cleans up cloned ops in a main-loop op. `bodyBlock` is only scanned after +// cleanup; `mainLoopOp` goes to `memGraphFactory` to rebuild the mem graph. +static LogicalResult +cleanupClonedOps(Operation *mainLoopOp, Block *bodyBlock, + llvm::DenseMap> &blockOps, + const SmallVector &idsInOrder, bool isCube, + std::function memGraphFactory) { + for (int i = idsInOrder.size() - 1; i >= 0; --i) { + auto &curOps = blockOps[idsInOrder[i]]; + if (curOps.empty()) { + continue; + } + + // Find last index of cloned ops + int startIdx = -1; + for (int j = curOps.size() - 1; j >= 0; --j) { + if (curOps[j]->hasAttr(CVPipeline::kClone)) { + startIdx = j; + break; + } + } + if (startIdx < 0) { + continue; + } + + // Locate the start of the contiguous cloned-op suffix; the original + // cleanup broke at the first non-cloned op, so keep that same set. + int firstClonedIdx = 0; + for (int j = startIdx - 1; j >= 0; --j) { + if (!curOps[j]->hasAttr(CVPipeline::kClone)) { + firstClonedIdx = j + 1; + break; + } + } + + // Collect the cloned ops in the contiguous suffix (in execution order). + SmallVector clonedOps; + clonedOps.reserve(startIdx - firstClonedIdx + 1); + for (int j = firstClonedIdx; j <= startIdx; ++j) { + clonedOps.push_back(curOps[j]); + } + + // Build the MemoryDependenceGraph at most once per block, only if a cloned + // op has no results (only Rule 3 needs it; Rule 2 SSA, Rule 1 type-based). + MemDepGraph memGraph; + if (isCube) { + bool needsMemGraph = llvm::any_of( + clonedOps, [](Operation *o) { return o->getNumResults() == 0; }); + if (needsMemGraph) { + memGraph = memGraphFactory(mainLoopOp); + } + } + + // Precompute each cloned op's exec-after ops sharing its block_id once and + // reuse it; erased ops are filtered at check time (as if rebuilt). + llvm::DenseMap> + sameBlockIdExecAfter; + if (memGraph) { + for (Operation *op : clonedOps) { + if (op->getNumResults() > 0) { + // Rule 2 path: memgraph is not consulted. + continue; + } + auto opBlockId = getLoopDirectChildBlockId(op); + if (!opBlockId) { + continue; + } + for (Operation *execOp : memGraph->getExecAfter(op)) { + // sync_block_wait/sync_block_set are not memory side effects for + // cleanup analysis, so skip them. + if (isa(execOp) || isa(execOp)) { + continue; + } + auto execBlockId = CVPipeline::getOpBlockId(execOp); + if (execBlockId && *execBlockId == *opBlockId) { + sameBlockIdExecAfter[op].insert(execOp); + } + } + } + } + + // Erase cloned ops bottom-to-top (original ordering); erasedOps makes an + // already-erased op count as absent from the precomputed exec-after sets. + llvm::DenseSet erasedOps; + for (int j = startIdx; j >= firstClonedIdx; --j) { + Operation *op = curOps[j]; + bool shouldErase = + isCube ? shouldEraseOpForCube(op, sameBlockIdExecAfter, erasedOps) + : shouldEraseOpForVector(op); + if (shouldErase) { + op->erase(); + erasedOps.insert(op); + } + } + } + + return validateClonedSyncOpsErased(bodyBlock); +} + +// Cleans up cloned ops for one main loop (scf.for or scf.while); body block +// comes from MainLoop::getBody, the op feeds collect/order helpers+factory. +LogicalResult CloneOpsPass::cleanupClonedOpsInMainLoop(Operation *op) { + Block *bodyBlock = MainLoop(op).getBody(); + if (!bodyBlock) { + LDBG("[Error]: op with ssbuffer.main_loop is not a scf::ForOp or " + "scf::WhileOp"); + return failure(); + } + + ModuleOp module = getOperation(); + scope::ScopeOp scopeOp = op->getParentOfType(); + if (!scopeOp) { + return success(); + } + + auto attr = + scopeOp->getAttrOfType(CVPipeline::kTcoreType); + if (!attr) { + return success(); + } + + bool isCube = (attr == hivm::TCoreTypeAttr::get(module.getContext(), + hivm::TCoreType::CUBE)); + + llvm::DenseMap> blockOps; + if (failed(collectOpsByBlockId(op, blockOps))) { + return failure(); + } + SmallVector idsInOrder = getBlockIdsInOrder(op); + + if (failed(cleanupClonedOps(op, bodyBlock, blockOps, idsInOrder, isCube, + [&](Operation *loopOp) -> MemDepGraph { + if (!isCube) { + return nullptr; + } + auto &aliasAnalysis = + getAnalysis(); + return std::make_unique( + loopOp, aliasAnalysis); + }))) { + return failure(); + } + + return success(); +} + +// Validate each block_id's ops form contiguous ranges, not interleaved (e.g. +// [1,1,2,2] valid, [1,2,1,2] invalid), on a main-loop body block. +static bool areBlockIdsConsecutive(Block *bodyBlock) { + SmallVector idsInOrder; + for (Operation &op : bodyBlock->without_terminator()) { + auto blockIdOpt = CVPipeline::getOpBlockId(&op); + if (!blockIdOpt) { + LDBG("[ERROR]: Op missing ssbuffer.block_id: " << op.getName()); + return false; + } + + idsInOrder.push_back(static_cast(*blockIdOpt)); + } + + // Check that each block_id forms a contiguous range + for (size_t i = 0; i < idsInOrder.size();) { + int currentId = idsInOrder[i]; + size_t j = i; + + while (j < idsInOrder.size() && idsInOrder[j] == currentId) { + ++j; + } + + for (size_t k = j; k < idsInOrder.size(); ++k) { + if (idsInOrder[k] == currentId) { + LDBG("[ERROR]: block_id: " << currentId << " is interleaved"); + return false; + } + } + + i = j; + } + + return true; +} + +LogicalResult CloneOpsPass::validateBlockIdsConsecutive(ModuleOp module) { + WalkResult result = module.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + Block *bodyBlock = MainLoop(op).getBody(); + if (!bodyBlock) { + LDBG("[Error]: op with ssbuffer.main_loop is not a scf::ForOp or " + "scf::WhileOp"); + return WalkResult::interrupt(); + } + if (!areBlockIdsConsecutive(bodyBlock)) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (result.wasInterrupted()) + return failure(); + + return success(); +} + +// Checks that no op in a VECTOR scope's main_loop op (scf.for or scf.while) +// has a tensor result carrying ssbuffer.clone. +LogicalResult CloneOpsPass::validateClonedOpsInVector(ModuleOp module) { + WalkResult result = module.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + Block *bodyBlock = MainLoop(op).getBody(); + if (!bodyBlock) { + LDBG("[Error]: op with ssbuffer.main_loop is not a scf::ForOp or " + "scf::WhileOp"); + return WalkResult::interrupt(); + } + + scope::ScopeOp scopeOp = op->getParentOfType(); + if (!scopeOp) { + return WalkResult::advance(); + } + + auto attr = + scopeOp->getAttrOfType(CVPipeline::kTcoreType); + if (!attr) { + return WalkResult::advance(); + } + + if (attr != hivm::TCoreTypeAttr::get(module.getContext(), + hivm::TCoreType::VECTOR)) { + return WalkResult::advance(); + } + + for (Operation &bodyOp : bodyBlock->without_terminator()) { + if (!bodyOp.hasAttr(CVPipeline::kClone)) { + continue; + } + if (isa(&bodyOp)) { + continue; + } + bool hasTensorDep = llvm::any_of(bodyOp.getResults(), [](Value result) { + return isa(result.getType()); + }); + if (hasTensorDep) { + LDBG("[Error]: VECTOR main_loop contains cloned op with tensor type: " + << bodyOp.getName()); + return WalkResult::interrupt(); + } + } + + return WalkResult::advance(); + }); + + if (result.wasInterrupted()) + return failure(); + + return success(); +} + +void CloneOpsPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("before cloneOps:\n" << module); + + // Validate block_ids are consecutive before cloning + if (failed(validateBlockIdsConsecutive(module))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Clone ops in vector/cube so each block_id owns its ops (no sharing); entry + // points take any main-loop op (scf.for/scf.while) and dispatch internally. + auto walkResult = module.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + + if (failed(cloneOpsInMainLoop(op))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return WalkResult::interrupt(); + } + + if (failed(cleanupClonedOpsInMainLoop(op))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (walkResult.wasInterrupted()) { + return; + } + + LDBG("after cloneOps:\n" << module); + + // Validate no cloned tensor ops remaining in VECTOR main_loop op + if (failed(validateClonedOpsInVector(module))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createCloneOpsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/CreateIfOps.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/CreateIfOps.cpp new file mode 100644 index 0000000000..d027ab7dcc --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/CreateIfOps.cpp @@ -0,0 +1,386 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/Support/Debug.h" + +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/IRMapping.h" + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/CreateIfOps.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" + +static constexpr const char *DEBUG_TYPE = "CreateIfOps"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +// Check if a value is used outside the given region ops +static bool isUsedOutsideRegion(Value v, + const llvm::DenseSet ®ionOps) { + for (OpOperand &use : v.getUses()) { + if (!regionOps.contains(use.getOwner())) { + return true; + } + } + + return false; +} + +// Find iter_arg in main loop for `v` (yield operand idx = body arg position). +static Value findIterArgInMainLoop(Value v, mlir::Type t) { + for (Operation *user : v.getUsers()) { + auto yieldOp = dyn_cast(user); + if (!yieldOp) { + continue; + } + + SmallVector iterArgs = + MainLoop(yieldOp->getParentOp()).getIterArgs(); + if (iterArgs.empty()) { + continue; + } + + for (auto [idx, operand] : llvm::enumerate(yieldOp.getOperands())) { + if (operand.getAsOpaquePointer() == v.getAsOpaquePointer()) { + if (idx >= iterArgs.size()) { + LDBG("[Error]: yield operand index " + << idx << " out of range for iter_args size " + << iterArgs.size()); + continue; + } + Value iterArg = iterArgs[idx]; + if (iterArg.getType() == t) { + return iterArg; + } + } + } + } + + LDBG("[Error]: else yield value not found in forOp/whileOp iter_args: " << v); + return nullptr; +} + +// Replace uses of old values that are outside the ifOp with the new values from +// ifOp +static LogicalResult replaceExternalIfOpUses(scf::IfOp ifOp, + ArrayRef oldYieldValues) { + for (size_t i = 0; i < oldYieldValues.size(); ++i) { + Value oldVal = oldYieldValues[i]; + + if (!oldVal) { + LDBG("[Error]: oldVal is null at index " << i); + return failure(); + } + + if (i >= ifOp.getNumResults()) { + LDBG("[Error]: index " << i << " exceeds ifOp results count " + << ifOp.getNumResults()); + return failure(); + } + + Value newVal = ifOp.getResult(i); + if (oldVal.getType() != newVal.getType()) { + LDBG("[Error]: type mismatch at index " << i << ": " << oldVal.getType() + << " vs " << newVal.getType()); + return failure(); + } + + SmallVector usesToReplace; + + for (OpOperand &use : llvm::make_early_inc_range(oldVal.getUses())) { + Operation *user = use.getOwner(); + + // Skip uses inside the ifOp + if (ifOp->isAncestor(user)) { + continue; + } + + // Skip uses after the ifOp in the same block + if (user->getBlock() == ifOp->getBlock() && + !ifOp->isBeforeInBlock(user)) { + continue; + } + + usesToReplace.push_back(&use); + } + + for (OpOperand *use : usesToReplace) { + use->set(newVal); + } + } + + return success(); +} + +// Computes yield values per block (values yielded from the if); op-agnostic as +// findIterArgInMainLoop dispatches on the yield's parent op (forOp/whileOp). +LogicalResult CreateIfOpsPass::computeYieldValues( + Operation *loopOp, + const llvm::DenseMap> &blockOps, + llvm::DenseMap> &thenYieldValues, + llvm::DenseMap> &elseYieldValues) { + (void)loopOp; // unused; findIterArgInMainLoop walks the yield's parent. + for (auto &p : blockOps) { + int id = p.first; + const SmallVector &ops = p.second; + + // Collect all ops in this block including nested ones + llvm::DenseSet regionOps; + for (Operation *op : ops) { + if (failed(collectAllNestedOps(op, regionOps))) { + return failure(); + } + } + + // Find values used outside the region + SmallVector yieldValues; + for (Operation *op : ops) { + for (Value res : op->getResults()) { + if (isUsedOutsideRegion(res, regionOps)) { + yieldValues.push_back(res); + } + } + } + + thenYieldValues[id] = yieldValues; + + // For else branch, use iteration args from main loop + elseYieldValues[id].clear(); + elseYieldValues[id].reserve(yieldValues.size()); + for (Value v : yieldValues) { + Value foundArg = findIterArgInMainLoop(v, v.getType()); + if (!foundArg) { + return failure(); + } else { + elseYieldValues[id].push_back(foundArg); + } + } + } + + return success(); +} + +// Create result types from yield values +static SmallVector +getResultTypes(const SmallVector &values) { + SmallVector types; + + for (Value v : values) { + types.push_back(v.getType()); + } + + return types; +} + +// Create ifOp for a single block +static scf::IfOp createIfOpForBlock(OpBuilder &builder, Location loc, + int blockId, + const SmallVector &thenValues, + const SmallVector &elseValues) { + bool needsYield = !thenValues.empty(); + // Check size consistency + if (needsYield && thenValues.size() != elseValues.size()) { + LDBG("[Error]: then/else yield count mismatch: " + << thenValues.size() << " vs " << elseValues.size()); + return scf::IfOp(); + } + + // Check type consistency + if (needsYield) { + for (size_t i = 0; i < thenValues.size(); ++i) { + if (thenValues[i].getType() != elseValues[i].getType()) { + LDBG("[Error]: then/else yield type mismatch at index " + << i << ": " << thenValues[i].getType() << " vs " + << elseValues[i].getType()); + return scf::IfOp(); + } + } + } + + SmallVector resultTypes = getResultTypes(thenValues); + + Value trueVal = builder.create(loc, builder.getI1Type(), + builder.getBoolAttr(true)); + + scf::IfOp ifOp; + if (needsYield) { + ifOp = builder.create(loc, resultTypes, trueVal, true); + } else { + ifOp = builder.create(loc, TypeRange{}, trueVal, false); + } + + ifOp->setAttr(CVPipeline::kIf, builder.getI32IntegerAttr(blockId)); + + // notify npuir that of the scenario + ifOp->setAttr(CVPipeline::kHIVMMatmulLimitedInCubeAttr, + builder.getUnitAttr()); + + return ifOp; +} + +// Move ops to then branch and create yield +static LogicalResult moveOpsToThenBranch(scf::IfOp ifOp, + SmallVector &ops, + const SmallVector &thenValues, + const SmallVector &elseValues, + Location loc) { + if (ops.empty() && !thenValues.empty()) { + LDBG("[Error]: moving empty ops but thenValues not empty"); + return failure(); + } + + Block &thenBlock = ifOp.getThenRegion().front(); + + for (Operation *op : llvm::reverse(ops)) { + op->moveBefore(&thenBlock, thenBlock.begin()); + } + + if (!thenValues.empty()) { + OpBuilder thenBuilder(&thenBlock, thenBlock.end()); + thenBuilder.create(loc, thenValues); + + Block &elseBlock = ifOp.getElseRegion().front(); + OpBuilder elseBuilder(&elseBlock, elseBlock.end()); + elseBuilder.create(loc, elseValues); + } + + return success(); +} + +// Create if ops (scf.if %true) for each block_id in the main loop. +// `op` is the main-loop op (scf.for or scf.while). +LogicalResult CreateIfOpsPass::createIfInMainLoop( + Operation *op, + const llvm::DenseMap> &blockOps, + const llvm::DenseMap> &thenYieldValues, + const llvm::DenseMap> &elseYieldValues) { + SmallVector ids = getBlockIdsInOrder(op); + if (ids.empty() && !MainLoop(op).getBody()) { + LDBG("[Error]: op with ssbuffer.main_loop is neither scf::ForOp nor " + "scf::WhileOp"); + return failure(); + } + + for (int id : ids) { + const SmallVector &ops = blockOps.lookup(id); + if (ops.empty()) { + continue; + } + + OpBuilder builder(ops.front()); + Location loc = ops.front()->getLoc(); + + scf::IfOp ifOp = + createIfOpForBlock(builder, loc, id, thenYieldValues.lookup(id), + elseYieldValues.lookup(id)); + if (!ifOp) { + return failure(); + } + + // Clone ops list since we'll modify it + SmallVector opsToMove = ops; + if (failed(moveOpsToThenBranch(ifOp, opsToMove, thenYieldValues.lookup(id), + elseYieldValues.lookup(id), loc))) { + return failure(); + } + + if (failed(replaceExternalIfOpUses(ifOp, thenYieldValues.lookup(id)))) { + return failure(); + } + } + + return success(); +} + +void CreateIfOpsPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("before createIfOps:\n" << module); + + auto walkResult = module.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + + llvm::DenseMap> blockOps; + if (failed(collectOpsByBlockId(op, blockOps))) { + LDBG("[Error]: op with ssbuffer.main_loop is neither scf::ForOp nor " + "scf::WhileOp, or a body op is missing ssbuffer.block_id\n"); + return WalkResult::interrupt(); + } + + // blockCounterNums is keyed on the main-loop op (scf.for or scf.while). + // Record it for both forOp and whileOp. + if (info) { + info->blockCounterNums[op] = blockOps.size(); + } + + llvm::DenseMap> thenYieldValues; + llvm::DenseMap> elseYieldValues; + + if (failed(computeYieldValues(op, blockOps, thenYieldValues, + elseYieldValues))) { + return WalkResult::interrupt(); + } + + if (failed(createIfInMainLoop(op, blockOps, thenYieldValues, + elseYieldValues))) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (walkResult.wasInterrupted()) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + LDBG("after createIfOps:\n" << module); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createCreateIfOpsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/InitDependentMap.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/InitDependentMap.cpp new file mode 100644 index 0000000000..f2087491e9 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/InitDependentMap.cpp @@ -0,0 +1,771 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/InitDependentMap.h" +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" +#include "third_party/ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinAttributes.h" + +static constexpr const char *DEBUG_TYPE = "InitDependentMap"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) LLVM_DEBUG(DBGS() << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace triton; +using namespace CVPipeline; +using namespace hivm; + +// Returns 0 if `consumer` is inside `mainLoop` (and pushes it to `consumers`) +// or inside a nested mainloop (skip), or -1 on error. +static int isConsumerInMainLoop(Operation *consumer, Operation *mainLoop, + SmallVector &consumers) { + Operation *current = consumer->getParentOp(); + + // Traverse up the parent chain until we reach the top (nullptr) + while (current != nullptr) { + if (isMainLoopOp(current) && current != mainLoop) { + // consumer Op not in the current mainloop + return 0; + } + // If we reach the target mainLoop, consumer is inside it + if (current == mainLoop) { + consumers.push_back(consumer); + return 0; + } + current = current->getParentOp(); + } + + LDBG("Can not find the consumer's mainloop!"); + return -1; +} + +// Collect ops with dependency attr `attrName` into depsByGroup (group -> +// [(op, role)]). Attr is a flat list of pairs: [group, role, group, role, ...]. +// role: 1=producer, 0=consumer. One op may contribute multiple pairs (dual role +// or several groups). 0 ok, -1 fail. +static int +collectDepsByGroup(Operation *rootOp, const char *attrName, + llvm::DenseMap>> + &depsByGroup) { + int ret = 0; + // One record is [group, role]; the attr concatenates N such records. + int depSize = 2; + int groupOffset = 0; + int roleOffset = 1; + + rootOp->walk([&](Operation *op) { + auto depsAttr = op->getAttrOfType(attrName); + if (!depsAttr) + return; + + if (depsAttr.empty() || depsAttr.size() % depSize != 0) { + LDBG("format of dependency attribute error, expect even-length pairs!"); + ret = -1; + return; + } + + for (size_t i = 0; i < depsAttr.size(); i += depSize) { + Attribute groupAttr = depsAttr[i + groupOffset]; + Attribute roleAttr = depsAttr[i + roleOffset]; + if (!isa(groupAttr) || !isa(roleAttr)) { + LDBG("type of dependency attritbute is not Int! error op:" << *op); + ret = -1; + return; + } + + int group = cast(groupAttr).getInt(); + int role = cast(roleAttr).getInt(); + depsByGroup[group].push_back({op, role}); + } + }); + + return ret; +} + +// Cross-core: consumer -> one producer list per group it consumes. An op may +// be both roles and/or in several groups; each group keeps its own list. +static int buildCrossCoreProducerConsumerMapping( + llvm::DenseMap>> &depsByGroup, + ConsumerProducerMap &result) { + for (auto &groupEntry : depsByGroup) { + auto &ops = groupEntry.second; + + SmallVector producers; + SmallVector consumers; + + for (auto &opRole : ops) { + Operation *op = opRole.first; + int role = opRole.second; + if (role == CVPipeline::crossCoreProducerId) { + if (!llvm::is_contained(producers, op)) + producers.push_back(op); + } else if (role == CVPipeline::crossCoreConsumerId) { + if (!llvm::is_contained(consumers, op)) + consumers.push_back(op); + } else { + LDBG("Get error role id in dependency attribute: OP: " + << *op << ", role: " << role); + return -1; + } + } + + for (Operation *consumer : consumers) { + result[consumer].push_back(producers); + } + } + + return 0; +} + +// Build consumer -> producers mapping from depsByGroup (role 1=producer, +// 0=consumer); if mainLoop != nullptr only consumers inside it. 0 ok, -1 fail. +static int buildIntraCoreProducerConsumerMapping( + llvm::DenseMap>> &depsByGroup, + llvm::DenseMap> &result, + Operation *mainLoop = nullptr) { + for (auto &groupEntry : depsByGroup) { + auto &ops = groupEntry.second; + + // Collect all producers and consumers in this group + SmallVector producers; + SmallVector consumers; + + for (auto &opRole : ops) { + Operation *op = opRole.first; + int role = opRole.second; + if (role == CVPipeline::crossCoreProducerId) { + producers.push_back(op); + } else if (role == CVPipeline::crossCoreConsumerId) { + // For intra-core mapping, only include consumers inside mainLoop + if (mainLoop != nullptr) { + if (isConsumerInMainLoop(op, mainLoop, consumers) != 0) { + LDBG("isConsumerInMainLoop failed"); + return -1; + } + } else { + consumers.push_back(op); + } + } else { + LDBG("Get error role id in dependency attribute: OP: " + << *op << ", role: " << role); + return -1; + } + } + + // Skip if no consumers (for intra-core mapping with mainLoop filter) + if (mainLoop != nullptr && consumers.empty()) + continue; + + // For each consumer, build mapping to all producers + for (Operation *consumer : consumers) { + result[consumer] = producers; + } + } + + return 0; +} + +// Collects every scf.for/scf.while op tagged CVPipeline::kMainLoop, stored +// uniformly as Operation* so downstream lookups ignore the op kind. +static int collectMainLoopById(ModuleOp module, + llvm::DenseMap &mainLoopById) { + int ret = 0; + module.walk([&](Operation *op) { + if (!op->hasAttr(CVPipeline::kMainLoop)) + return; + if (!isMainLoopOp(op)) { + LDBG("Do not support mainloop op other than scf.for or scf.while: " + << op->getName()); + ret = -1; + return; + } + auto mainLoopIdAttr = op->getAttrOfType(CVPipeline::kMainLoop); + if (mainLoopIdAttr) { + mainLoopById[op] = mainLoopIdAttr.getInt(); + } + }); + return ret; +} + +static int +findMainLoopIdContainingOp(Operation *op, + llvm::DenseMap &mainLoopById) { + for (auto &entry : mainLoopById) { + if (entry.first->isAncestor(op)) { + return entry.second; + } + } + return -1; +} + +static int +filterMemCrossCoreDepsByMainLoop(ModuleOp module, + ConsumerProducerMap &initialDepsMap, + ConsumerProducerMap &filteredDepsMap) { + LDBG("memCrossCore dependencies before filter: " + << countProducerGroups(initialDepsMap)); + + // Step 1: Collect all main_loop ops (scf.for or scf.while) and their ids + llvm::DenseMap mainLoopById; + if (collectMainLoopById(module, mainLoopById) != 0) { + LDBG("collectMainLoopById Failed!"); + return -1; + } + + // Step 2: Filter mapping - only keep producer/consumer pairs in the same + // main_loop. Each inner producer list is one group and is filtered on its + // own so a multi-group consumer can keep some groups and drop others. + for (auto &entry : initialDepsMap) { + Operation *consumer = entry.first; + + // Find the main_loop id containing the consumer + int consumerMainLoopId = findMainLoopIdContainingOp(consumer, mainLoopById); + if (consumerMainLoopId == -1) { + LDBG("Consumer op is not in any main_loop, skip: " << *consumer); + continue; + } + + for (SmallVector &producers : entry.second) { + if (producers.empty()) { + LDBG("Producers list is empty!"); + return -1; + } + + // Find the main_loop id containing the producer + int producerMainLoopId = + findMainLoopIdContainingOp(producers[0], mainLoopById); + if (producerMainLoopId == -1) { + LDBG("producer op is not in any main_loop: " << *producers[0]); + continue; + } + + // Check all producers in the same mainloop + for (size_t i = 1; i < producers.size(); i++) { + int otherProducerMainLoopId = + findMainLoopIdContainingOp(producers[i], mainLoopById); + if (otherProducerMainLoopId != producerMainLoopId) { + LDBG("Producers are not in the same main_loop. " + << "First producer main_loop id: " << producerMainLoopId + << ", Producer[" << i + << "] main_loop id: " << otherProducerMainLoopId); + return -1; + } + } + + // Check if consumer and producers are in the same main_loop + if (consumerMainLoopId != producerMainLoopId) { + LDBG("Consumer and producers are in different main_loop, skip. " + << "Consumer main_loop id: " << consumerMainLoopId + << ", Producer main_loop id: " << producerMainLoopId); + continue; + } + + filteredDepsMap[consumer].push_back(producers); + } + } + + LDBG("memCrossCore dependencies after filter: " + << countProducerGroups(filteredDepsMap)); + + return 0; +} + +// Init crossCoreDependentMap from ssbuffer.crossCoreDeps pairs +// [group, role, ...]; 1=producer 0=consumer. Consumer -> one producer list per +// group, same main_loop. 0 ok, -1 fail. +int initCrossCoreDependentMap(ModuleOp module, ControlFlowConditionInfo *info) { + // Step 1: Collect all crossDeps by group (including memCrossDeps) + llvm::DenseMap>> + crossDepsByGroup; + if (collectDepsByGroup(module, CVPipeline::kCrossCoreDeps.data(), + crossDepsByGroup) != 0) { + LDBG("collectDepsByGroup on crossDeps Failed!"); + return -1; + } + + // Step 2: Build initial mapping (all producers for each consumer) + ConsumerProducerMap initialCrossDepsMap; + if (buildCrossCoreProducerConsumerMapping(crossDepsByGroup, + initialCrossDepsMap) != 0) { + LDBG("buildCrossCoreProducerConsumerMapping on crossDeps Failed!"); + return -1; + } + + // Step 3: Filter by main_loop constraint + ConsumerProducerMap filteredCrossDepsMap; + if (filterMemCrossCoreDepsByMainLoop(module, initialCrossDepsMap, + filteredCrossDepsMap) != 0) { + LDBG("filterCrossCoreDepsByMainLoop Failed!"); + return -1; + } + + info->crossCoreDependentMap = filteredCrossDepsMap; + return 0; +} + +// Initializes intraCoreDependentMap: per main_loop op (scf.for/scf.while) keeps +// consumers inside it but not in nested mainloops. 0 on success, -1 on error. +int initIntraCoreDependentMap(ModuleOp module, ControlFlowConditionInfo *info) { + // Collect all intra-core deps from the entire module + llvm::DenseMap>> + allIntraDepsByGroup; + if (collectDepsByGroup(module, CVPipeline::kIntraDeps.data(), + allIntraDepsByGroup) != 0) { + LDBG("collectDepsByGroup on intraDeps Failed!"); + return -1; + } + + // For each mainLoop, build the mapping with consumers inside it; both scf.for + // and scf.while can carry ssbuffer.main_loop (map keyed on Operation*). + int ret = 0; + module.walk([&](Operation *op) { + if (!op->hasAttr(CVPipeline::kMainLoop)) + return; + if (!isMainLoopOp(op)) { + LDBG("Do not support mainloop op other than scf.for or scf.while: " + << op->getName()); + ret = -1; + return; + } + + llvm::DenseMap> depMap; + if (buildIntraCoreProducerConsumerMapping(allIntraDepsByGroup, depMap, + op) != 0) { + LDBG("buildIntraCoreProducerConsumerMapping on intraDeps Failed!"); + ret = -1; + return; + } + + // Only insert if there are dependencies for this mainLoop + if (!depMap.empty()) { + info->intraCoreDependentMap[op] = depMap; + } + }); + return ret; +} + +// Print all dependent maps for verification +static void printDependentMaps(ControlFlowConditionInfo *info) { + // Print crossCoreDependentMap + LDBG("crossCoreDependentMap size: " + << countProducerGroups(info->crossCoreDependentMap)); + LDBG("crossCoreDependentMap contents:"); + for (auto &entry : info->crossCoreDependentMap) { + Operation *consumer = entry.first; + LDBG(" Consumer: " << *consumer << " (groups: " << entry.second.size() + << ")"); + for (SmallVector &producers : entry.second) { + LDBG(" group producers count: " << producers.size()); + for (Operation *producer : producers) { + LDBG(" Producer: " << *producer); + } + } + } + + // Print intraCoreDependentMap + LDBG("intraCoreDependentMap size: " << info->intraCoreDependentMap.size()); + LDBG("intraCoreDependentMap contents:"); + for (auto &forEntry : info->intraCoreDependentMap) { + Operation *loopOp = forEntry.first; + auto &depMap = forEntry.second; + LDBG(" MainLoopOp (depMap size: " << depMap.size() << "):"); + LDBG(" " << OpWithFlags(loopOp, OpPrintingFlags().skipRegions())); + + for (auto &entry : depMap) { + Operation *consumer = entry.first; + SmallVector &producers = entry.second; + LDBG(" Consumer: " << *consumer << " (producers count: " + << producers.size() << ")"); + for (Operation *producer : producers) { + LDBG(" Producer: " << *producer); + } + } + } +} + +// Find the IfOp that contains a given operation +static scf::IfOp findIfOpContainingOp(Operation *op) { + if (!op) { + return nullptr; + } + + constexpr int maxDepth = 100; + int depth = 0; + + Operation *current = op; + while (current && depth < maxDepth) { + if (auto ifOp = dyn_cast(current)) { + if (ifOp->hasAttr(CVPipeline::kIf)) { + LDBG("Found ssbuffer.if at depth " << depth); + return ifOp; + } + } + current = current->getParentOp(); + depth++; + } + + if (depth >= maxDepth) { + LDBG("Warning: Max depth " << maxDepth + << " exceeded in findIfOpContainingOp"); + } + + return nullptr; +} + +// Compute producer buffer counts (max map size) from cross/intra-core maps; +// falls back to BufferCountManager IntraCore when the intra-core map is empty. +static void computeProducerBufferCount(ControlFlowConditionInfo *info, + ModuleOp module) { + // Get cross-core buffer count (max size in the map) + info->crossCoreBufferCount = 0; + for (auto &entry : info->crossCoreDependentMap) { + for (SmallVector &producers : entry.second) { + info->crossCoreBufferCount = + std::max(info->crossCoreBufferCount, (int)producers.size()); + } + } + LDBG("Cross-core buffer count (max): " << info->crossCoreBufferCount); + + // Get intra-core buffer count (max size across all main loops) + info->intraCoreBufferCount = 0; + for (auto &loopEntry : info->intraCoreDependentMap) { + auto &intraDepMap = loopEntry.second; + for (auto &entry : intraDepMap) { + info->intraCoreBufferCount = + std::max(info->intraCoreBufferCount, (int)entry.second.size()); + } + } + LDBG("Intra-core buffer count (max across all main loops): " + << info->intraCoreBufferCount); + + // If intra-core map is empty, use BufferCountManager's IntraCore value + if (info->intraCoreBufferCount == 0) { + BufferCountManager bufferCountMgr(module); + info->intraCoreBufferCount = bufferCountMgr.getBufferCountByType( + BufferCountManager::DepType::IntraCore); + LDBG("Intra-core map is empty, using BufferCountManager IntraCore value: " + << info->intraCoreBufferCount); + } +} + +// Add an edge to ifBlockDAG if it is not already present +static void addIfBlockEdge(ControlFlowConditionInfo *info, scf::IfOp producerIf, + scf::IfOp consumerIf, IfBlockDepKind kind) { + auto &edges = info->ifBlockDAG[producerIf]; + bool seen = + llvm::any_of(edges, [&](const std::pair &e) { + return e.first == consumerIf; + }); + if (!seen) { + edges.push_back({consumerIf, kind}); + } +} + +// Build if block DAG from crossCoreDependentMap and intraCoreDependentMap +static int buildIfBlockDAG(ModuleOp module, ControlFlowConditionInfo *info) { + // Step 1: Traverse crossCoreDependentMap to add cross-core edges. + for (auto &entry : info->crossCoreDependentMap) { + Operation *consumerOp = entry.first; + + // Consumer op is inside an if block + scf::IfOp consumerIf = findIfOpContainingOp(consumerOp); + if (!consumerIf) { + LDBG("Consumer op not in any ssbuffer.if block: " << *consumerOp); + return -1; + } + + // Each inner list is one dependency group + for (SmallVector &producers : entry.second) { + for (Operation *producerOp : producers) { + scf::IfOp producerIf = findIfOpContainingOp(producerOp); + if (!producerIf) { + LDBG("Producer op not in any ssbuffer.if block: " << *producerOp); + return -1; + } + + if (producerIf == consumerIf) { + LDBG("Producer and consumer are in the same if block, this is " + "invalid: " + << *producerIf); + return -1; + } + + addIfBlockEdge(info, producerIf, consumerIf, IfBlockDepKind::CrossCore); + } + } + } + + // Step 2: Traverse intraCoreDependentMap to add intra-core edges. + for (auto &loopEntry : info->intraCoreDependentMap) { + for (auto &consumerProducers : loopEntry.second) { + Operation *consumerOp = consumerProducers.first; + scf::IfOp consumerIf = findIfOpContainingOp(consumerOp); + if (!consumerIf) { + LDBG("Intra consumer op not in any ssbuffer.if block: " << *consumerOp); + return -1; + } + + for (Operation *producerOp : consumerProducers.second) { + scf::IfOp producerIf = findIfOpContainingOp(producerOp); + if (!producerIf) { + LDBG("Intra producer op not in any ssbuffer.if block: " + << *producerOp); + return -1; + } + + if (producerIf == consumerIf) { + LDBG("Intra producer and consumer are in the same if block, this " + "is invalid: " + << *producerIf); + return -1; + } + + addIfBlockEdge(info, producerIf, consumerIf, IfBlockDepKind::IntraCore); + } + } + } + + return 0; +} + +// Detect cycle in the if-block DAG via DFS over all edges. +enum class DfsState : uint8_t { Unvisited, Visiting, Done }; + +static bool +dfsCycle(scf::IfOp node, + llvm::DenseMap>> + &dag, + llvm::DenseMap &state) { + state[node] = DfsState::Visiting; + auto it = dag.find(node); + if (it != dag.end()) { + for (auto &edge : it->second) { + scf::IfOp neighbor = edge.first; + auto s = state.lookup(neighbor); + if (s == DfsState::Visiting) + return true; + if (s == DfsState::Unvisited && dfsCycle(neighbor, dag, state)) + return true; + } + } + state[node] = DfsState::Done; + return false; +} + +static int detectCycle(ControlFlowConditionInfo *info) { + // Collect all nodes in the DAG (both producers and consumers) + llvm::DenseMap state; + for (auto &entry : info->ifBlockDAG) { + state.try_emplace(entry.first, DfsState::Unvisited); + for (auto &edge : entry.second) { + state.try_emplace(edge.first, DfsState::Unvisited); + } + } + + for (auto &entry : state) { + if (entry.second == DfsState::Unvisited) { + if (dfsCycle(entry.first, info->ifBlockDAG, state)) { + LDBG("Cycle detected in DAG"); + return -1; + } + } + } + + return 0; +} + +// DFS helper function to find nodes at target distance from start node +static int collectFlowOptIfOpPairs(ModuleOp module, + ControlFlowConditionInfo *info) { + // Step 1: Collect every node appearing in the DAG (key or value). + llvm::DenseSet allNodes; + for (auto &entry : info->ifBlockDAG) { + allNodes.insert(entry.first); + for (auto &edge : entry.second) { + allNodes.insert(edge.first); + } + } + + // Step 2: Compute in-degree (every edge, cross or intra, contributes 1). + llvm::DenseMap inDegree; + for (auto &entry : info->ifBlockDAG) { + for (auto &edge : entry.second) { + inDegree[edge.first]++; + } + } + + // Step 3: Identify start nodes (in-degree 0). + llvm::SmallVector startNodes; + for (auto node : allNodes) { + if (inDegree.lookup(node) == 0) { + startNodes.push_back(node); + } + } + + LDBG("Number of start nodes (depth=1): " << startNodes.size()); + + // Step 4: For each start node, run a per-source DFS to compute each + // reachable node's depth (max over all paths from this start). Every node + // with depth = 3 becomes a flowOpt pair keyed on this start. + constexpr int targetDepth = 3; + for (scf::IfOp start : startNodes) { + llvm::DenseMap depth; + llvm::SmallVector> worklist; + worklist.push_back({start, 1}); + depth[start] = 1; + + while (!worklist.empty()) { + auto [node, nodeDepth] = worklist.pop_back_val(); + auto it = info->ifBlockDAG.find(node); + if (it == info->ifBlockDAG.end()) + continue; + for (auto &edge : it->second) { + scf::IfOp neighbor = edge.first; + int step = (edge.second == IfBlockDepKind::CrossCore) ? 1 : 0; + int candidate = nodeDepth + step; + auto inserted = depth.try_emplace(neighbor, candidate); + if (inserted.second) { + worklist.push_back({neighbor, candidate}); + } else if (candidate > inserted.first->second) { + inserted.first->second = candidate; + worklist.push_back({neighbor, candidate}); + } + } + } + + for (auto &entry : depth) { + if (entry.second == targetDepth) { + info->flowOptIfOpPairs[entry.first] = start; + } + } + } + + LDBG("flowOptIfOpPairs size: " << info->flowOptIfOpPairs.size()); + + return 0; +} + +// Print DAG and flowOpt pairs for verification +static void printDAGInfo(ControlFlowConditionInfo *info) { + LDBG("ifBlockDAG contents:"); + for (auto &entry : info->ifBlockDAG) { + scf::IfOp producer = entry.first; + LDBG(" Producer IfOp has " << entry.second.size() << " consumers"); + for (auto &edge : entry.second) { + LDBG(" -> Consumer IfOp (kind: " + << (edge.second == IfBlockDepKind::CrossCore ? "CrossCore" + : "IntraCore") + << ")"); + } + } + + LDBG("flowOptIfOpPairs contents:"); + for (auto &entry : info->flowOptIfOpPairs) { + scf::IfOp target = entry.first; + scf::IfOp source = entry.second; + LDBG(" Target IfOp (third node) -> Source IfOp (start node)"); + } +} + +void InitDependentMapPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Enter InitDependentMap pass."); + + // Step 1: Initialize crossCoreDependentMap + if (initCrossCoreDependentMap(module, info) != 0) { + LDBG("initCrossCoreDependentMap failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 2: Initialize intraCoreDependentMap + if (initIntraCoreDependentMap(module, info) != 0) { + LDBG("initIntraCoreDependentMap failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Print all dependent maps for verification + LLVM_DEBUG(printDependentMaps(info)); + + // Step 4: Compute producer buffer count for flowOpt condition + computeProducerBufferCount(info, module); + + // Step 4: Build if block DAG from crossCoreDependentMap (always) + if (buildIfBlockDAG(module, info) != 0) { + LDBG("buildIfBlockDAG failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 5: Detect cycle in DAG + if (detectCycle(info) != 0) { + LDBG("Cycle detected!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return; + } + + // Step 6: Collect flowOpt if block pairs from DAG (only when buffer counts + // exceed threshold) + if (info->crossCoreBufferCount > CROSS_CORE_BUFFER_COUNT_THRESHOLD && + info->intraCoreBufferCount > INTRA_CORE_BUFFER_COUNT_THRESHOLD) { + LDBG("Buffer counts meet requirements, collecting flowOpt pairs."); + + if (collectFlowOptIfOpPairs(module, info) != 0) { + LDBG("collectFlowOptIfOpPairs failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + LLVM_DEBUG(printDAGInfo(info)); + } + + LDBG("Exit InitDependentMap pass."); +} + +namespace mlir { +namespace triton { +std::unique_ptr> createInitDependentMapPass() { + return std::make_unique(); +} +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/ProcessArgs.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/ProcessArgs.cpp new file mode 100644 index 0000000000..8d29a23034 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/ProcessArgs.cpp @@ -0,0 +1,872 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/ProcessArgs.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/TypeUtilities.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "ProcessArgs"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace mlir; +using namespace CVPipeline; +using namespace triton; + +// Collects iter_arg index -> block_ids that use it. `ivOffset` is 1 for +// scf.for (IV at arg 0) or 0 for scf.while. Uses body args directly +// (block-local). +static LogicalResult collectArgIndexToBlockIds( + Block *body, unsigned ivOffset, + llvm::DenseMap> &argIndexToBlockIds) { + if (!body || !body->mightHaveTerminator()) { + LDBG("[Error]: loop body is invalid or has no terminator"); + return failure(); + } + + for (auto iterArg : body->getArguments()) { + int argIdx = iterArg.getArgNumber(); + if (argIdx < (int)ivOffset) { + // scf.for's IV at block arg 0 — never an iter_arg. + continue; + } + // Skip tensor-type iter_args, only process scalar and index types + if (mlir::isa(iterArg.getType())) { + continue; + } + for (auto &use : iterArg.getUses()) { + Operation *owner = use.getOwner(); + if (auto blockId = getLoopDirectChildBlockId(owner)) { + argIndexToBlockIds[argIdx - (int)ivOffset].insert(*blockId); + } + } + } + return success(); +} + +// Finds iter_args used by multiple block_ids. Owner = first block in order; +// each non-owner gets its own extra iter_arg via SharedArgInfo. +static LogicalResult findSharedArgs( + const llvm::DenseMap> &argIndexToBlockIds, + const SmallVector &idsInOrder, + SmallVector &sharedArgsInfo) { + int extraArgCount = 0; + for (auto &p : argIndexToBlockIds) { + int argIndex = p.first; + const llvm::DenseSet &blockIds = p.second; + + if (blockIds.size() <= 1) + continue; + + int ownerBlockId = -1; + for (int id : idsInOrder) { + if (blockIds.contains(id)) { + ownerBlockId = id; + break; + } + } + if (ownerBlockId == -1) + continue; + + // Each non-owner block for this argIndex gets its own extra iter_arg + for (int bid : blockIds) { + if (bid == ownerBlockId) + continue; + sharedArgsInfo.push_back( + SharedArgInfo(argIndex, ownerBlockId, extraArgCount, bid)); + extraArgCount++; + } + } + return success(); +} + +// Returns the op defining iter_arg at `argIndex` in `body`'s scf.yield (top +// of update chain), or nullptr if out of bounds. Shared by both paths. +static Operation *findYieldDefiningOp(Block *body, unsigned argIndex) { + auto yieldOp = cast(body->getTerminator()); + if (argIndex >= yieldOp.getNumOperands()) { + return nullptr; + } + return yieldOp.getOperand(argIndex).getDefiningOp(); +} + +// Collects all operations in the computation chain by backward traversal +// from compOp, scoped to ops inside `loopOp`'s body. +static void collectChainOps(Operation *loopOp, Operation *compOp, + llvm::DenseSet &chainOps) { + SmallVector worklist; + worklist.push_back(compOp); + + while (!worklist.empty()) { + Operation *op = worklist.pop_back_val(); + if (chainOps.contains(op)) + continue; + chainOps.insert(op); + + op->walk([&](Operation *nestOp) { + for (Value operand : nestOp->getOperands()) { + if (auto *defOp = operand.getDefiningOp()) { + if (defOp->getParentOp() == loopOp && !chainOps.contains(defOp)) { + worklist.push_back(defOp); + } + } + } + }); + } +} + +// Builds compOp + chainOps for each shared arg. `body` is the loop body +// (forOp body or whileOp after-body) used to locate scf.yield. +static LogicalResult buildCompInfoForSharedArgs( + Operation *loopOp, Block *body, SmallVector &sharedArgsInfo, + llvm::DenseMap &sharedArgToCompOp, + llvm::DenseMap> &sharedArgToChainOps) { + for (auto &info : sharedArgsInfo) { + int argIndex = info.argIndex; + if (sharedArgToCompOp.contains(argIndex)) + continue; + + Operation *compOp = findYieldDefiningOp(body, argIndex); + if (!compOp) { + continue; + } + + sharedArgToCompOp[argIndex] = compOp; + + llvm::DenseSet chainOps; + collectChainOps(loopOp, compOp, chainOps); + sharedArgToChainOps[argIndex] = chainOps; + } + return success(); +} + +// Clones the computation chain for a non-owner block (topo-sorted, operands +// remapped). argRemapping: iter_arg -> new extra. clonedArgIdx: this block's +// arg. +static LogicalResult +cloneChainForBlock(SharedArgInfo &info, Operation *compOp, + const llvm::DenseSet &chainOps, Block *newBlock, + IRMapping &argRemapping, OpBuilder &cloneBuilder, + IRMapping &resultMapper, int clonedArgIdx) { + if (!compOp || chainOps.empty()) { + return failure(); + } + + SmallVector sortedChain(chainOps.begin(), chainOps.end()); + if (failed(topologicalSort(sortedChain))) { + return failure(); + } + + for (Operation *op : sortedChain) { + IRMapping opMapper; + for (OpOperand &operand : op->getOpOperands()) { + Value oldVal = operand.get(); + Value newVal = oldVal; + if (argRemapping.contains(oldVal)) { + newVal = argRemapping.lookup(oldVal); + } else if (resultMapper.contains(oldVal)) { + // Operand is a result from earlier in the owner chain, use cloned + // result + newVal = resultMapper.lookup(oldVal); + } + opMapper.map(oldVal, newVal); + } + + if (resultMapper.contains(op->getResult(0))) + continue; + + Operation *cloned = cloneBuilder.clone(*op, opMapper); + if (!cloned) { + LDBG("Clone chain failed when cloning op " << *op); + return failure(); + } + cloned->setAttr(CVPipeline::kBlockId, + cloneBuilder.getI32IntegerAttr(info.nonOwnerBlockId)); + cloned->setAttr(CVPipeline::kArg, + cloneBuilder.getI32IntegerAttr(info.argIndex)); + + resultMapper.map(op->getResult(0), cloned->getResult(0)); + cloneBuilder.setInsertionPointAfter(cloned); + } + return success(); +} + +// Replaces iter_arg uses in non-owner block with the cloned iter_arg. +// argRemapping maps migrated iter_arg Value -> new extra iter_arg Value. +static LogicalResult replaceIterArgsInBlock(SharedArgInfo &info, + Block *newBlock, + IRMapping &argRemapping, + OpBuilder &cloneBuilder) { + for (auto arg : newBlock->getArguments()) { + if (!argRemapping.contains(arg)) { + continue; + } + Value newVal = argRemapping.lookup(arg); + + SmallVector usesToReplace; + for (OpOperand &use : arg.getUses()) { + auto blockId = getLoopDirectChildBlockId(use.getOwner()); + if (blockId && *blockId == info.nonOwnerBlockId) { + usesToReplace.push_back(&use); + } + } + for (OpOperand *use : usesToReplace) { + use->set(newVal); + use->getOwner()->setAttr(CVPipeline::kArg, + cloneBuilder.getI32IntegerAttr(info.argIndex)); + } + } + + return success(); +} + +// Processes each shared arg: finds insertion point, clones chain, replaces +// iter_args. `ivOffset` is 1 for scf.for (IV at arg 0), 0 for scf.while. +static LogicalResult processSharedArgsIteration( + Block *newBlock, SmallVector &sharedArgsInfo, + const llvm::DenseMap &sharedArgToCompOp, + const llvm::DenseMap> &sharedArgToChainOps, + ValueRange iterArgs, unsigned ivOffset, SmallVector &clonedResults) { + unsigned numOriginalIterArgs = iterArgs.size(); + unsigned extraIterArgsBase = ivOffset + numOriginalIterArgs; + + int clonedArgIdx = clonedResults.size(); + for (auto &info : sharedArgsInfo) { + int argIndex = info.argIndex; + info.iterArg = iterArgs[argIndex]; + + // The migrated iter_arg (original iter_arg moved to new block) + Value migratedIterArg = newBlock->getArgument(argIndex + ivOffset); + // The new extra iter_arg added for this shared arg + unsigned newExtraBlockArgIdx = extraIterArgsBase + info.newArgIndex; + Value newExtraIterArg = newBlock->getArgument(newExtraBlockArgIdx); + + // Build argRemapping: migratedIterArg -> newExtraIterArg + IRMapping argRemapping; + argRemapping.map(migratedIterArg, newExtraIterArg); + + Operation *lastOpInBlock = nullptr; + for (Operation &op : newBlock->without_terminator()) { + auto blockIdAttr = op.getAttrOfType(CVPipeline::kBlockId); + if (blockIdAttr && blockIdAttr.getInt() == info.nonOwnerBlockId) { + lastOpInBlock = &op; + } + } + + OpBuilder cloneBuilder(newBlock, newBlock->end()); + if (lastOpInBlock) { + cloneBuilder.setInsertionPointAfter(lastOpInBlock); + } + + IRMapping resultMapper; + if (failed(cloneChainForBlock(info, sharedArgToCompOp.lookup(argIndex), + sharedArgToChainOps.lookup(argIndex), + newBlock, argRemapping, cloneBuilder, + resultMapper, clonedArgIdx))) { + continue; + } + + if (failed(replaceIterArgsInBlock(info, newBlock, argRemapping, + cloneBuilder))) { + continue; + } + + Value clonedResult = + resultMapper.lookup(sharedArgToCompOp.lookup(argIndex)->getResult(0)); + clonedResults.push_back(clonedResult); + clonedArgIdx++; + } + return success(); +} + +// Prepares all shared args data: collects arg->blockId mapping, finds shared +// args, and builds computation info for each shared arg. +static LogicalResult prepareSharedArgsData( + Operation *loopOp, Block *body, SmallVector &sharedArgsInfo, + llvm::DenseMap &sharedArgToCompOp, + llvm::DenseMap> &sharedArgToChainOps) { + if (!body || !body->mightHaveTerminator()) { + LDBG("[Error]: loop body is invalid or has no terminator"); + return failure(); + } + + // ivOffset: 1 for scf.for (IV at block arg 0), 0 for scf.while (no IV). + unsigned ivOffset = isa(loopOp) ? 1 : 0; + + llvm::DenseMap> argIndexToBlockIds; + if (failed(collectArgIndexToBlockIds(body, ivOffset, argIndexToBlockIds))) { + return failure(); + } + + SmallVector idsInOrder = getBlockIdsInOrder(loopOp); + if (idsInOrder.empty() && !MainLoop(loopOp).getBody()) { + LDBG("[Error]: loopOp is neither scf::ForOp nor scf::WhileOp"); + return failure(); + } + if (failed(findSharedArgs(argIndexToBlockIds, idsInOrder, sharedArgsInfo))) { + return failure(); + } + + if (sharedArgsInfo.empty()) { + return success(); + } + + LDBG("[INFO]: Found " << sharedArgsInfo.size() + << " shared iter_args to process"); + + if (failed(buildCompInfoForSharedArgs(loopOp, body, sharedArgsInfo, + sharedArgToCompOp, + sharedArgToChainOps))) { + return failure(); + } + + return success(); +} + +// Replaces all uses of the old main-loop op with the new op's results, erases +// the old op, and transfers its intraCoreDependentMap entry to the new op. +static LogicalResult replaceMainLoopOpAndErase(Operation *oldOp, + Operation *newOp, + ControlFlowConditionInfo *info) { + replaceOpResultUses(oldOp, newOp); + + // Transfer intraCoreDependentMap entry from oldOp to newOp. + if (info && info->intraCoreDependentMap.count(oldOp)) { + info->intraCoreDependentMap[newOp] = info->intraCoreDependentMap[oldOp]; + info->intraCoreDependentMap.erase(oldOp); + } + + oldOp->erase(); + return success(); +} + +// Derives the body block to inspect for shared-iter_arg analysis and the IV +// offset (1 for scf.for, 0 for scf.while). Returns false if `op` is neither. +static bool getOpIterParams(Operation *op, Block *&inspectBody, + unsigned &ivOffset) { + MainLoop ml = MainLoop(op); + if (ml.getBody()) { + inspectBody = ml.getBody(); + ivOffset = ml.isWhile() ? 0 : 1; + return true; + } + return false; +} + +// Completes the scf.for path: migrate body, clone per-block chains, rebuild +// scf.yield, swap old op out. +static LogicalResult processSharedArgsInForOp( + scf::ForOp forOp, scf::ForOp newForOp, + SmallVector &sharedArgsInfo, + const llvm::DenseMap &sharedArgToCompOp, + const llvm::DenseMap> &sharedArgToChainOps, + ControlFlowConditionInfo *info) { + Block *oldBlock = forOp.getBody(); + Block *newBlock = newForOp.getBody(); + migrateBody(oldBlock, newBlock); + + SmallVector clonedResults; + if (failed(processSharedArgsIteration( + newBlock, sharedArgsInfo, sharedArgToCompOp, sharedArgToChainOps, + MainLoop(forOp).getIterArgs(), 1, clonedResults))) { + return failure(); + } + if (failed(buildNewYieldOp(oldBlock, newBlock, newForOp, clonedResults))) { + return failure(); + } + return replaceMainLoopOpAndErase(forOp, newForOp, info); +} + +// Completes the scf.while path: migrate before/after bodies, clone per-block +// chains, rebuild scf.yield/condition, transfer maps, swap old op out. +LogicalResult ProcessArgsPass::processSharedArgsInWhileOp( + scf::WhileOp whileOp, scf::WhileOp newWhileOp, + SmallVector &sharedArgsInfo, + const llvm::DenseMap &sharedArgToCompOp, + const llvm::DenseMap> &sharedArgToChainOps, + ControlFlowConditionInfo *info) { + migrateWhileBodies(whileOp, newWhileOp); + + SmallVector clonedResults; + if (failed(processSharedArgsIteration( + newWhileOp.getAfterBody(), sharedArgsInfo, sharedArgToCompOp, + sharedArgToChainOps, MainLoop(whileOp).getIterArgs(), 0, + clonedResults))) { + return failure(); + } + buildNewWhileCondition(whileOp, newWhileOp); + if (failed(buildNewYieldOp(whileOp.getAfterBody(), newWhileOp.getAfterBody(), + newWhileOp, clonedResults))) { + return failure(); + } + // Transfer originalWhileIterArgIndices so downstream still resolves + // newWhileOp->old arg indices. + if (originalWhileIterArgIndices.count(whileOp)) { + originalWhileIterArgIndices[newWhileOp] = + originalWhileIterArgIndices[whileOp]; + originalWhileIterArgIndices.erase(whileOp); + } + // Transfer whileBlockArgMap entries (local + info mirror) so the (block_id, + // new_arg_idx) -> old_arg_idx mapping survives the old-op erase. + if (localWhileBlockArgMap.count(whileOp)) { + localWhileBlockArgMap[newWhileOp] = + std::move(localWhileBlockArgMap[whileOp]); + localWhileBlockArgMap.erase(whileOp); + } + if (info && info->whileBlockArgMap.count(whileOp)) { + info->whileBlockArgMap[newWhileOp] = + std::move(info->whileBlockArgMap[whileOp]); + info->whileBlockArgMap.erase(whileOp); + } + return replaceMainLoopOpAndErase(whileOp, newWhileOp, info); +} + +// Single entry point for processing shared iter_args in a main-loop op +// (scf.for or scf.while). whileOp builds new scf.condition, transfers indices. +LogicalResult +ProcessArgsPass::processSharedIterArgsInLoop(Operation *op, + ControlFlowConditionInfo *info) { + Block *inspectBody = nullptr; + unsigned ivOffset = 0; + if (!getOpIterParams(op, inspectBody, ivOffset)) { + LDBG("[Error]: op with ssbuffer.main_loop is neither scf::ForOp nor " + "scf::WhileOp"); + return failure(); + } + + SmallVector sharedArgsInfo; + llvm::DenseMap sharedArgToCompOp; + llvm::DenseMap> sharedArgToChainOps; + if (failed(prepareSharedArgsData(op, inspectBody, sharedArgsInfo, + sharedArgToCompOp, sharedArgToChainOps))) { + return failure(); + } + if (sharedArgsInfo.empty()) { + return success(); + } + + // Build extra init values from original arg values at shared indices. Each + // new iter_arg shadows the existing one at the same index. + SmallVector extraInitArgs; + extraInitArgs.reserve(sharedArgsInfo.size()); + ValueRange origInits; + if (auto forOp = dyn_cast(op)) { + origInits = forOp.getInitArgs(); + } else if (auto whileOp = dyn_cast(op)) { + origInits = whileOp.getInits(); + } + for (const auto &info : sharedArgsInfo) { + extraInitArgs.push_back(origInits[info.argIndex]); + } + + Operation *newOp = createMainLoopOpWithExtras(op, extraInitArgs); + if (!newOp) { + return failure(); + } + + if (auto forOp = dyn_cast(op)) { + return processSharedArgsInForOp(forOp, cast(newOp), + sharedArgsInfo, sharedArgToCompOp, + sharedArgToChainOps, info); + } + return processSharedArgsInWhileOp( + cast(op), cast(newOp), sharedArgsInfo, + sharedArgToCompOp, sharedArgToChainOps, info); +} + +// Walks module to find for/while ops with ssbuffer.main_loop attribute and +// dispatches each into processSharedIterArgsInLoop. +LogicalResult ProcessArgsPass::processSharedIterArgs(ModuleOp module) { + WalkResult result = module.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + if (failed(processSharedIterArgsInLoop(op, info))) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (result.wasInterrupted()) { + return failure(); + } + return success(); +} + +// ----- Per-block update chain for scf.while iter_args used in scf.condition +// For each scf.while op with main_loop, every block_id's run keeps its own +// iter_arg value; clones update chain into each block, extends scf.while with +// one extra iter_arg per (block_id, original iter_arg), and records +// (new_arg_idx -> old_arg_idx) in info->whileBlockArgMap. + +// Snapshots original iter_args of every scf.while with main_loop; per whileOp +// clones cond-used update chains per block_id. Recorded in +// info->whileBlockArgMap. +LogicalResult +ProcessArgsPass::updateIndependentCondsInWhileBlocks(ModuleOp module) { + // Collect whileOps in a worklist (must NOT mutate IR during the walk — + // processWhileIterArgsInWhileOp replaces the old whileOp). Snapshot + // original iter_args in the same pass so we can later identify which + // iter_args were referenced by scf.condition in the input. + SmallVector worklist; + module.walk([&](scf::WhileOp whileOp) { + if (!isMainLoopOp(whileOp)) + return; + SmallVector indices; + for (unsigned i = 0; i < whileOp.getNumOperands(); ++i) + indices.push_back(i); + originalWhileIterArgIndices[whileOp] = indices; + worklist.push_back(whileOp); + }); + for (scf::WhileOp whileOp : worklist) { + if (failed(processWhileIterArgsInWhileOp(whileOp, info))) + return failure(); + } + + // Dump whileBlockArgMap (new_whileop -> block_id -> (new_arg_idx -> + // old_arg_idx)); pass-local map so observable when --process-args runs + // standalone. + dumpWhileBlockArgMap(localWhileBlockArgMap, + "whileBlockArgMap contents (new_whileop -> block_id -> " + "(new_arg_idx -> old_arg_idx))"); + return success(); +} + +// Returns original iter_arg indices contributing to cond value of scf.condition +// for `whileOp`. Walks def-chain to before-block BlockArguments (cond-used). +static llvm::DenseSet collectConditionUsedIterArgIndices( + scf::WhileOp whileOp, const SmallVector &originalIndices) { + llvm::DenseSet used; + auto cond = whileOp.getConditionOp(); + Value condValue = cond.getCondition(); + Block *beforeBlock = whileOp.getBeforeBody(); + + // BFS over def-chain of condValue; stops at before-block BlockArgument + // (collected) or no defining op in region (parent BlockArgument — ignored). + llvm::SmallPtrSet visited; + SmallVector worklist; + worklist.push_back(condValue); + while (!worklist.empty()) { + Value v = worklist.pop_back_val(); + if (!visited.insert(v).second) + continue; + + if (auto blockArg = dyn_cast(v)) { + if (blockArg.getOwner() == beforeBlock) { + unsigned idx = blockArg.getArgNumber(); + if (idx < originalIndices.size()) { + used.insert(idx); + } + } + continue; + } + + Operation *defOp = v.getDefiningOp(); + if (!defOp) + continue; + for (Value operand : defOp->getOperands()) { + worklist.push_back(operand); + } + } + return used; +} + +// (findYieldCompOpForWhile folded into findYieldDefiningOp — see top of +// file. Callers now pass `whileOp.getAfterBody()` directly.) + +// Returns the last op in `body` whose `ssbuffer.block_id` matches `blockId` +// (i.e., end of the block's run of consecutive ops). +static Operation *findLastOpWithBlockId(Block *body, int blockId) { + Operation *last = nullptr; + for (Operation &op : body->without_terminator()) { + auto attr = op.getAttrOfType(CVPipeline::kBlockId); + if (attr && attr.getInt() == blockId) { + last = &op; + } + } + return last; +} + +// Builds a clone of the update chain after the last op with `blockId` in +// `body`. Annotates with ssbuffer.while_arg/block_id; remaps operands. +static LogicalResult cloneUpdateChainForWhileBlock( + scf::WhileOp whileOp, Block *body, Operation *compOp, int blockId, + unsigned originalArgIndex, unsigned newArgIndex, + const llvm::DenseSet &chainOps, Value &cloned) { + SmallVector sortedChain(chainOps.begin(), chainOps.end()); + if (failed(topologicalSort(sortedChain))) { + cloned = Value(); + return failure(); + } + + // Find insertion point: after the last op with this block_id. + Operation *lastOp = findLastOpWithBlockId(body, blockId); + OpBuilder builder(body, body->end()); + if (lastOp) { + builder.setInsertionPointAfter(lastOp); + } + // If no op has this block_id, the builder is at body->end() (set by ctor); + // avoid setInsertionPoint(terminator) since it may not exist yet. + + IRMapping resultMapper; + for (Operation *op : sortedChain) { + IRMapping opMapper; + for (OpOperand &operand : op->getOpOperands()) { + Value oldVal = operand.get(); + Value newVal = oldVal; + // Remap the original iter_arg to the new iter_arg for this block; + // chain-internal operands remap to the corresponding cloned value. + auto blockArg = dyn_cast(oldVal); + if (blockArg && blockArg.getOwner() == body && + (unsigned)blockArg.getArgNumber() == originalArgIndex) { + newVal = body->getArgument(newArgIndex); + } else if (resultMapper.contains(oldVal)) { + newVal = resultMapper.lookup(oldVal); + } + opMapper.map(oldVal, newVal); + } + + if (resultMapper.contains(op->getResult(0))) + continue; + + Operation *cloned = builder.clone(*op, opMapper); + cloned->setAttr(CVPipeline::kBlockId, builder.getI32IntegerAttr(blockId)); + cloned->setAttr(CVPipeline::kWhileArg, + builder.getI32IntegerAttr(originalArgIndex)); + resultMapper.map(op->getResult(0), cloned->getResult(0)); + builder.setInsertionPointAfter(cloned); + } + + cloned = resultMapper.lookup(compOp->getResult(0)); + return success(); +} + +// For each cond-used iter_arg, plans one new iter_arg per (block_id, origIdx). +// compOp + def-chain must be computed BEFORE migrateBody (caused lookup crash). +static WhileIterArgClonePlan +planWhileIterArgDescriptors(scf::WhileOp whileOp, + const SmallVector &originalIndices, + const llvm::DenseSet &condUsed) { + WhileIterArgClonePlan plan; + SmallVector blockIdsInOrder = getBlockIdsInOrder(whileOp); + if (blockIdsInOrder.empty()) + return plan; + unsigned nextNewArgIdx = whileOp.getNumOperands(); + unsigned descIdx = 0; + for (unsigned origIdx : originalIndices) { + if (!condUsed.contains(origIdx)) + continue; + plan.posInClonedVec[origIdx] = descIdx++; + + Operation *compOp = findYieldDefiningOp(whileOp.getAfterBody(), origIdx); + if (!compOp) { + LDBG("[WARN]: no compOp for while iter_arg idx=" << origIdx); + continue; + } + + llvm::DenseSet chainOps; + collectChainOps(whileOp, compOp, chainOps); + plan.compOp[origIdx] = compOp; + plan.chainOps[origIdx] = chainOps; + + for (int blockId : blockIdsInOrder) { + plan.newArgDescriptors.push_back({blockId, nextNewArgIdx++, origIdx}); + } + } + // Pre-size clonedPerBlock so cloneWhileBlockChains can assign by index + // without a placeholder. Slots for cond-used origIdx with no compOp stay + // null and are never read (newArgDescriptors skips them). + unsigned numOrigIds = descIdx; + for (int blockId : blockIdsInOrder) { + plan.clonedPerBlock[blockId].resize(numOrigIds); + } + return plan; +} + +// Clones update chains into newAfter per (block_id, origIdx); reuses compOp/ +// chainOps from before migrateBody; records into plan.clonedPerBlock. +static LogicalResult cloneWhileBlockChains(scf::WhileOp newWhileOp, + WhileIterArgClonePlan &plan) { + Block *newAfter = newWhileOp.getAfterBody(); + // Derive (blockId, origIdx) -> newArgIdx and the ordered blockId list from + // newArgDescriptors (first-appearance order matches getBlockIdsInOrder). + llvm::DenseMap, unsigned> blockOrigToNewArg; + llvm::DenseSet seenBlocks; + SmallVector blockIdsInOrder; + for (auto &desc : plan.newArgDescriptors) { + int blockId; + unsigned newArgIdx, origIdx; + std::tie(blockId, newArgIdx, origIdx) = desc; + blockOrigToNewArg[{blockId, origIdx}] = newArgIdx; + if (seenBlocks.insert(blockId).second) + blockIdsInOrder.push_back(blockId); + } + + // Iterate unique origIdx in first-appearance order; + // planWhileIterArgDescriptors pushes (blockId, origIdx) in originalIndices x + // blockIdsInOrder order. + llvm::DenseSet seenOrig; + for (auto &desc : plan.newArgDescriptors) { + unsigned origIdx; + std::tie(std::ignore, std::ignore, origIdx) = desc; + if (!seenOrig.insert(origIdx).second) + continue; + Operation *compOp = plan.compOp.lookup(origIdx); + if (!compOp) + continue; + const llvm::DenseSet &chainOps = plan.chainOps.lookup(origIdx); + + for (int blockId : blockIdsInOrder) { + unsigned newArgIdx = blockOrigToNewArg.lookup({blockId, origIdx}); + Value cloned; + if (failed(cloneUpdateChainForWhileBlock(newWhileOp, newAfter, compOp, + blockId, origIdx, newArgIdx, + chainOps, cloned))) { + return failure(); + } + plan.clonedPerBlock[blockId][plan.posInClonedVec.lookup(origIdx)] = + cloned; + } + } + return success(); +} + +// Per (block_id, cond-used iter_arg) pair: clone the update chain into the +// new after body, extend the scf.while, and record the new arg mapping. +LogicalResult +ProcessArgsPass::processWhileIterArgsInWhileOp(scf::WhileOp whileOp, + ControlFlowConditionInfo *info) { + auto it = originalWhileIterArgIndices.find(whileOp); + if (it == originalWhileIterArgIndices.end()) + return success(); + const SmallVector &originalIndices = it->second; + llvm::DenseSet condUsed = + collectConditionUsedIterArgIndices(whileOp, originalIndices); + if (condUsed.empty()) + return success(); + + WhileIterArgClonePlan plan = + planWhileIterArgDescriptors(whileOp, originalIndices, condUsed); + if (plan.newArgDescriptors.empty()) { + return success(); + } + + // Gather origIdx from plan.newArgDescriptors in order — same shadowing rule + // as shared-iter-args (one new init per orig to shadow), routes through + // createMainLoopOpWithExtras. + SmallVector extraInitArgs; + extraInitArgs.reserve(plan.newArgDescriptors.size()); + ValueRange origInits = whileOp.getInits(); + for (auto &desc : plan.newArgDescriptors) { + unsigned origIdx; + std::tie(std::ignore, std::ignore, origIdx) = desc; + extraInitArgs.push_back(origInits[origIdx]); + } + auto newWhileOp = + cast(createMainLoopOpWithExtras(whileOp, extraInitArgs)); + migrateWhileBodies(whileOp, newWhileOp); + if (failed(cloneWhileBlockChains(newWhileOp, plan))) { + return failure(); + } + buildNewWhileCondition(whileOp, newWhileOp); + // Pre-compute extra yield values from plan.clonedPerBlock in + // newArgDescriptors order; appended to new scf.yield operands by + // buildNewYieldOp. + SmallVector extraYieldValues; + extraYieldValues.reserve(plan.newArgDescriptors.size()); + for (auto &desc : plan.newArgDescriptors) { + int blockId; + unsigned newArgIdx, origIdx; + std::tie(blockId, newArgIdx, origIdx) = desc; + extraYieldValues.push_back(plan.clonedPerBlock.lookup( + blockId)[plan.posInClonedVec.lookup(origIdx)]); + } + if (failed(buildNewYieldOp(whileOp.getAfterBody(), newWhileOp.getAfterBody(), + newWhileOp, extraYieldValues))) { + return failure(); + } + // Mirror (block_id, new_arg_idx) -> orig_idx into localWhileBlockArgMap and + // info->whileBlockArgMap so downstream passes can resolve newWhileOp's args. + for (auto &desc : plan.newArgDescriptors) { + int blockId; + unsigned newArgIdx, origIdx; + std::tie(blockId, newArgIdx, origIdx) = desc; + localWhileBlockArgMap[newWhileOp][blockId][newArgIdx] = (int)origIdx; + if (info) { + info->whileBlockArgMap[newWhileOp][blockId][newArgIdx] = (int)origIdx; + } + } + if (failed(replaceMainLoopOpAndErase(whileOp, newWhileOp, info))) { + return failure(); + } + return success(); +} + +void ProcessArgsPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("before processArgs:\n" << module); + + // 1. While-specific decoupling: snapshot original iter_args; per scf.while, + // clone cond-used iter_arg update chains per block_id; record in + // info->whileBlockArgMap. + if (failed(updateIndependentCondsInWhileBlocks(module))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // 2. Process shared iter_args (adds per-block clones for args shared + // across block_ids). Uses originalWhileIterArgIndices captured above. + if (failed(processSharedIterArgs(module))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + LDBG("after processArgs:\n" << module); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createProcessArgsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateConditionInfo.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateConditionInfo.cpp new file mode 100644 index 0000000000..b52839c389 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateConditionInfo.cpp @@ -0,0 +1,1948 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include + +#include "llvm/ADT/APFloat.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/CodeGen/MachinePassManager.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/raw_ostream.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/ValueRange.h" + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" +#include "ascend/include/DynamicCVPipeline/Common/SSBufferManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/HIVM/IR/HIVMInterfaces.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Pass/PassManager.h" +#include "third_party/ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateConditionInfo.h" + +static constexpr const char *DEBUG_TYPE = "UpdateConditionInfoPass"; +static constexpr int VECTOR_SSBUF_OFFSET = 1024; +static constexpr int VALUE_SSBUF_OFFSET = 4; +static constexpr int UPDATE_CONDITION_INFO_SUCCESS = 0; +static constexpr int UPDATE_CONDITION_INFO_FAILED = -1; +static constexpr size_t kVectorCount = 2; + +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + }) +using namespace mlir; +using namespace triton; +using namespace hivm; +using namespace CVPipeline; + +static void logConditionGroupIndices(llvm::StringRef label, + llvm::ArrayRef groupIndices) { + std::string message; + llvm::raw_string_ostream os(message); + os << label; + for (int groupIndex : groupIndices) { + os << groupIndex << " "; + } + os << "\n"; + LDBG(os.str()); +} + +static void logOutputGroupValues(llvm::StringRef label, + llvm::ArrayRef values) { + std::string message; + llvm::raw_string_ostream os(message); + os << label; + for (Value value : values) { + os << value << " "; + } + os << "\n"; + LDBG(os.str()); +} + +// Read block id from ssbuffer.if on ifOp. Missing attr is unexpected. +static int getIfBlockId(scf::IfOp ifOp, int &outBlockId) { + auto attr = ifOp->getAttrOfType(kIf); + if (!attr) { + LDBG("ssbuffer.if missing block id on ifOp: " << ifOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + outBlockId = static_cast(attr.getInt()); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// for: body region iter args; while-do: after-region args. +static int getLoopRegionIterArgs(Operation *loopOp, + MutableArrayRef &outArgs) { + if (auto forOp = dyn_cast(loopOp)) { + outArgs = forOp.getRegionIterArgs(); + return UPDATE_CONDITION_INFO_SUCCESS; + } + if (auto whileOp = dyn_cast(loopOp)) { + outArgs = whileOp.getAfterArguments(); + return UPDATE_CONDITION_INFO_SUCCESS; + } + LDBG("getLoopRegionIterArgs expects scf.for or scf.while, got " << *loopOp + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; +} + +static int getLoopRegionIterArg(Operation *loopOp, int argIdx, Value &outArg) { + MutableArrayRef args; + if (getLoopRegionIterArgs(loopOp, args) == UPDATE_CONDITION_INFO_FAILED) + return UPDATE_CONDITION_INFO_FAILED; + if (argIdx < 0 || argIdx >= static_cast(args.size())) { + LDBG("Invalid loop region iter arg index: " + << argIdx << ", iter args " << args.size() << ", loopOp=" << *loopOp + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + outArg = args[argIdx]; + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Clone only the SSA def-chain that produces `condition` when it lives in +// beforeRegion. +static int cloneConditionDefChain(Value value, Region &beforeRegion, + IRMapping &mapping, OpBuilder &builder, + Value &outValue) { + if (Value mapped = mapping.lookupOrNull(value)) { + outValue = mapped; + return UPDATE_CONDITION_INFO_SUCCESS; + } + + if (auto blockArg = dyn_cast(value)) { + if (blockArg.getParentRegion() == &beforeRegion) { + LDBG("Before-region arg is not in whileBlockArgMap mapping: " << value + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + outValue = value; + return UPDATE_CONDITION_INFO_SUCCESS; + } + + Operation *defOp = value.getDefiningOp(); + if (!defOp) { + outValue = value; + return UPDATE_CONDITION_INFO_SUCCESS; + } + // Defined outside before-region: reuse as-is (constants, outer values). + if (defOp->getParentRegion() != &beforeRegion) { + outValue = value; + return UPDATE_CONDITION_INFO_SUCCESS; + } + + for (Value operand : defOp->getOperands()) { + Value remappedOperand; + if (cloneConditionDefChain(operand, beforeRegion, mapping, builder, + remappedOperand) == UPDATE_CONDITION_INFO_FAILED) + return UPDATE_CONDITION_INFO_FAILED; + if (!mapping.lookupOrNull(operand)) + mapping.map(operand, remappedOperand); + } + + Operation *cloned = builder.clone(*defOp, mapping); + outValue = cloned->getResult(cast(value).getResultNumber()); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Remap scf.condition(x): clone only ops that produce x into the after region, +// remapping before args via whileBlockArgMap[while][blockId]: +// {new_arg_idx(after) -> old_arg_idx(before)}. +static int buildWhileCounterCondition( + scf::WhileOp whileOp, scf::IfOp ifOp, ControlFlowConditionInfo *info, + OpBuilder &builder, const DenseMap &controlVarToLatestValue, + Value &outCond) { + int blockId; + if (getIfBlockId(ifOp, blockId) == UPDATE_CONDITION_INFO_FAILED) + return UPDATE_CONDITION_INFO_FAILED; + + auto whileIt = info->whileBlockArgMap.find(whileOp); + if (whileIt == info->whileBlockArgMap.end()) { + LDBG("whileBlockArgMap has no entry for whileOp=" << whileOp << ", ifOp=" + << ifOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + auto blockIt = whileIt->second.find(blockId); + if (blockIt == whileIt->second.end()) { + LDBG("whileBlockArgMap has no entry for blockId " + << blockId << ", whileOp=" << whileOp << ", ifOp=" << ifOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + const DenseMap &argIdxMap = blockIt->second; + auto beforeArgs = whileOp.getBeforeArguments(); + auto afterArgs = whileOp.getAfterArguments(); + + IRMapping mapping; + for (auto [newArgIdx, oldArgIdx] : argIdxMap) { + if (oldArgIdx < 0 || oldArgIdx >= static_cast(beforeArgs.size()) || + newArgIdx < 0 || newArgIdx >= static_cast(afterArgs.size())) { + LDBG("Invalid whileBlockArgMap entry (new=" + << newArgIdx << ", old=" << oldArgIdx << "), whileOp=" << whileOp + << ", ifOp=" << ifOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + Value afterArg = afterArgs[newArgIdx]; + auto latestIt = controlVarToLatestValue.find(afterArg); + if (latestIt != controlVarToLatestValue.end()) + afterArg = latestIt->second; + mapping.map(beforeArgs[oldArgIdx], afterArg); + } + + // Only take condition value x from scf.condition(x); + scf::ConditionOp condOp = whileOp.getConditionOp(); + Value beforeCond = condOp.getCondition(); + if (cloneConditionDefChain(beforeCond, whileOp.getBefore(), mapping, builder, + outCond) == UPDATE_CONDITION_INFO_FAILED) { + LDBG("Failed to remap while before-region condition expression, whileOp=" + << whileOp << ", ifOp=" << ifOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Allocate the SSBuffer pointer +SmallVector> +UpdateConditionInfoPass::allocSSBuffer(ModuleOp module) { + OpBuilder builder(module.getContext()); + auto i64Type = builder.getIntegerType(ADDR_INT_TYPE); + auto i32Type = builder.getIntegerType(CONST_INT_TYPE); + auto memrefType = getSsbufMemrefType(builder); + + // alloc 2 group of ssbuffer pointers: + // Core Vector 0: allocate ssbuffer address: 0, 4, 8, ... + // Core Vector 1: allocate ssbuffer address: 1024, 1028, 1032, ... + SmallVector> ssbufferMemrefs; + SmallVector ssbufferVec0Memrefs; + SmallVector ssbufferVec1Memrefs; + int numBuffers = countProducerGroups(info->crossCoreDependentMap); + if (numBuffers == 0) { + LDBG("crossCoreDependentMap is empty!" << "\n"); + return ssbufferMemrefs; + } + + module->walk([&](Operation *op) { + if (auto scopeOp = dyn_cast(op)) { + builder.setInsertionPoint(scopeOp); + auto loc = scopeOp.getLoc(); + auto zeroConst = builder.create( + scopeOp->getLoc(), i32Type, builder.getIntegerAttr(i32Type, 0)); + + for (int i = 0; i < numBuffers; i++) { + auto memref0 = + createPointerCastOp(builder, loc, i * VALUE_SSBUF_OFFSET); + auto memref1 = createPointerCastOp( + builder, loc, VECTOR_SSBUF_OFFSET + i * VALUE_SSBUF_OFFSET); + + builder.create(scopeOp->getLoc(), zeroConst, memref0); + builder.create(scopeOp->getLoc(), zeroConst, memref1); + + ssbufferVec0Memrefs.push_back(memref0.getResult()); + ssbufferVec1Memrefs.push_back(memref1.getResult()); + } + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + + ssbufferMemrefs.push_back(ssbufferVec0Memrefs); + ssbufferMemrefs.push_back(ssbufferVec1Memrefs); + return ssbufferMemrefs; +} + +// Collect dependency buffer +void UpdateConditionInfoPass::collectDependencyBuffers( + ModuleOp module, SmallVector &mainLoopOps, + DenseMap>> + &crossCoreBuffers, + DenseMap>>> + &intraCoreBuffersMap) { + // Collect crossCoreBuffers by traversing module in deterministic order + int crossCoreIdx = 0; + module.walk([&](Operation *op) { + // Collect crossCoreBuffers for this op + auto it = info->crossCoreDependentMap.find(op); + if (it != info->crossCoreDependentMap.end()) { + for (SmallVector &producers : it->second) { + crossCoreBuffers[crossCoreIdx][op] = producers; + crossCoreIdx++; + } + } + + return WalkResult::advance(); + }); + + // Collect intraCoreBuffers for all main_loop for/while ops + for (Operation *loopOp : mainLoopOps) { + if (info->intraCoreDependentMap.count(loopOp)) { + auto &loopDeps = info->intraCoreDependentMap[loopOp]; + DenseMap>> + intraCoreBuffers; + int intraCoreIdx = 0; + for (auto &entry : loopDeps) { + intraCoreBuffers[intraCoreIdx][entry.first] = entry.second; + intraCoreIdx++; + } + intraCoreBuffersMap[loopOp] = intraCoreBuffers; + } + } +} + +// Helper: Find the tcb group id that contains op +// Returns the group id if found, -1 otherwise +static int findTcbGroupId( + Operation *op, + DenseMap> &tightlyCoupledBufferGroups) { + for (auto &tcbEntry : tightlyCoupledBufferGroups) { + if (llvm::is_contained(tcbEntry.second, op)) { + return tcbEntry.first; + } + } + return UPDATE_CONDITION_INFO_FAILED; +} + +// Helper: Add all equivalent ops from tcbOps to ops (excluding op itself) +int addEquivalentOps(Operation *op, SmallVector &tcbOps, + SmallVector &ops) { + int ret = -1; + for (Operation *equivOp : tcbOps) { + if (equivOp != op && !llvm::is_contained(ops, equivOp)) { + ret = 0; + ops.push_back(equivOp); + } + } + return ret; +} + +int UpdateConditionInfoPass::buildIdxToVarMap( + Operation *loopOp, + const DenseMap>> + &intraCoreBuffers, + DenseMap &idxToVar) { + int varIdx = 0; + MutableArrayRef regionIterArgs; + if (getLoopRegionIterArgs(loopOp, regionIterArgs) == + UPDATE_CONDITION_INFO_FAILED) + return UPDATE_CONDITION_INFO_FAILED; + int iterArgNum = static_cast(regionIterArgs.size()); + + const auto &innerDepIndices = info->innerDepConds[loopOp]; + if (innerDepIndices.size() < intraCoreBuffers.size()) { + LDBG("Not enough inner dependency condition indices: assigned " + << innerDepIndices.size() << ", expected " << intraCoreBuffers.size() + << ", loopOp=" << *loopOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + for (const auto &entry : intraCoreBuffers) { + int idx = entry.first; + + int argIdx = innerDepIndices[varIdx]; + if (argIdx < 0 || argIdx >= iterArgNum) { + LDBG("Invalid inner dependency arg index: " + << argIdx << ", iter args " << iterArgNum << ", loopOp=" << *loopOp + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + idxToVar[idx] = regionIterArgs[argIdx]; + LDBG("Assign intraCore buffer group " << idx << " to iter arg index " + << argIdx << "\n"); + varIdx++; + } + + LDBG("Assigned " << idxToVar.size() << " intraCore condition variables." + << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Helper function to build buffer dependency mappings (fully Operation* based) +// Outputs two reverse lookup tables for O(1) lookup during IR walk: +// - consumerToGroups: consumer Op -> [groupIdx] (one op may consume several +// groups) +// - producerToGroups: producer Op -> [groupIdx] +static int buildBufferDependencyMappings( + DenseMap>> &buffers, + DenseMap> &consumerToGroups, + DenseMap> &outputToGroups) { + for (auto &[groupIdx, deps] : buffers) { + for (auto &[consumer, producers] : deps) { + consumerToGroups[consumer].push_back(groupIdx); + + for (Operation *producer : producers) { + outputToGroups[producer].push_back(groupIdx); + } + } + } + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// getInputOutputValues - Analyze the input/output buffer groups used in a +// single ifOp This function traverses all operations within ifOp, identifies +// FixpipeOp, CopyOp and MaterializeInDestinationOp, and extracts cross-core and +// intra-core buffer group indices from their operands and yield values. + +// Data structure description +// - crossCoreBuffers: {groupIdx -> (consumer -> [producers])} +// - intraCoreBuffers: {groupIdx -> (consumer -> [producers])} +// - crossCoreInputValues: list of cross-core group indices read by this ifOp +// - crossCoreOutputValues: list of cross-core group indices written by this +// ifOp +// - intraCoreInputValues: list of intra-core group indices read by this ifOp +// - intraCoreOutputValues: list of intra-core group indices written by this +// ifOp + +// Processing flow +// 1. Build reverse mapping table from Value to groupIdx +// 2. Traverse Fixpipe/Copy/MaterializeInDestination ops in ifOp, collect +// input/output groups +// 3. Traverse ifOp.thenYield() operands, collect yield output groups +// 4. Deduplicate and output four groups of index values +int UpdateConditionInfoPass::getInputOutputValues( + scf::IfOp ifOp, + DenseMap>> + crossCoreBuffers, + DenseMap>> + intraCoreBuffers, + SmallVector &crossCoreInputValues, + SmallVector &crossCoreOutputValues, + SmallVector &intraCoreInputValues, + SmallVector &intraCoreOutputValues) { + DenseSet crossCoreInputSet; + DenseSet crossCoreOutputSet; + DenseSet intraCoreInputSet; + DenseSet intraCoreOutputSet; + + // Build output mappings for cross-core and intra-core + // Same producer/output can be used by multiple consumers/inputs, so we need + // to track all related groups + DenseMap> crossCoreOutputToGroups; + DenseMap> intraCoreOutputToGroups; + + // Add consumer mappings for input dependency identification + DenseMap> crossCoreConsumerToGroups; + DenseMap> intraCoreConsumerToGroups; + + // Build cross-core mappings + if (buildBufferDependencyMappings(crossCoreBuffers, crossCoreConsumerToGroups, + crossCoreOutputToGroups) == + UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + + // Build intra-core mappings + if (buildBufferDependencyMappings(intraCoreBuffers, intraCoreConsumerToGroups, + intraCoreOutputToGroups) == + UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + + // collect input/output groups + ifOp.walk([&](Operation *op) { + if (op == ifOp) + return WalkResult::advance(); + + // Check if this op is a consumer (for input dependency) + if (crossCoreConsumerToGroups.count(op)) { + for (int idx : crossCoreConsumerToGroups[op]) + crossCoreInputSet.insert(idx); + } + if (intraCoreConsumerToGroups.count(op)) { + for (int idx : intraCoreConsumerToGroups[op]) + intraCoreInputSet.insert(idx); + } + + // Check if this op is a producer (for output dependency) + if (crossCoreOutputToGroups.count(op)) { + for (int idx : crossCoreOutputToGroups[op]) { + crossCoreOutputSet.insert(idx); + } + } + if (intraCoreOutputToGroups.count(op)) { + for (int idx : intraCoreOutputToGroups[op]) { + intraCoreOutputSet.insert(idx); + } + } + return WalkResult::advance(); + }); + + crossCoreInputValues.assign(crossCoreInputSet.begin(), + crossCoreInputSet.end()); + crossCoreOutputValues.assign(crossCoreOutputSet.begin(), + crossCoreOutputSet.end()); + intraCoreInputValues.assign(intraCoreInputSet.begin(), + intraCoreInputSet.end()); + intraCoreOutputValues.assign(intraCoreOutputSet.begin(), + intraCoreOutputSet.end()); + + LDBG("==== Cross Core & Intra Core Values ====" << "\n"); + LLVM_DEBUG( + logConditionGroupIndices("crossCoreInputValues: ", crossCoreInputValues)); + LLVM_DEBUG(logConditionGroupIndices("crossCoreOutputValues: ", + crossCoreOutputValues)); + LLVM_DEBUG( + logConditionGroupIndices("intraCoreInputValues: ", intraCoreInputValues)); + LLVM_DEBUG(logConditionGroupIndices("intraCoreOutputValues: ", + intraCoreOutputValues)); + + return UPDATE_CONDITION_INFO_SUCCESS; +} + +Value UpdateConditionInfoPass::getVarValue(Operation *loopOp, int varIndex) { + if (!info->innerDepConds.count(loopOp)) + return Value(); + SmallVector &innerDepIndices = info->innerDepConds[loopOp]; + if (varIndex < (int)innerDepIndices.size()) { + int argIdx = innerDepIndices[varIndex]; + Value arg; + if (getLoopRegionIterArg(loopOp, argIdx, arg) == + UPDATE_CONDITION_INFO_FAILED) + return Value(); + return arg; + } + return Value(); +} + +// Build the information of the producer group. +int UpdateConditionInfoPass::buildOutputGroups( + SmallVector &intraCoreOutputValues, + DenseMap>> + &intraCoreBuffers, + DenseMap &idxToVar, + SmallVector &outputGroups) { + outputGroups.clear(); + + for (int idx : intraCoreOutputValues) { + auto bufferIt = intraCoreBuffers.find(idx); + if (bufferIt == intraCoreBuffers.end()) { + LDBG("Failed to build output groups: no buffer entry for intraCore " + "output group " + << idx << "." << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + auto varIt = idxToVar.find(idx); + if (varIt == idxToVar.end()) { + LDBG("Failed to build output groups: no control variable for intraCore " + "output group " + << idx << "." << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + Value var = varIt->second; + + for (auto &entry : bufferIt->second) { + SmallVector outputOps; + for (Operation *producer : entry.second) { + outputOps.push_back(producer); + } + if (outputOps.empty()) + continue; + + bool flag = true; + for (auto &outputGroup : outputGroups) { + if (outputGroup.outputs == outputOps) { + outputGroup.inputVars.push_back(var); + flag = false; + break; + } + } + if (flag) { + OutputGroupInfo groupInfo; + groupInfo.outputs = outputOps; + groupInfo.inputVars.push_back(var); + outputGroups.push_back(groupInfo); + } + } + } + + LDBG("Built " << outputGroups.size() << " intraCore output groups." << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Select corresponding SSBuffer ptr based on ifblock running on vector/cube +// core +Value UpdateConditionInfoPass::getSSBufferMemref( + bool isAIC, int groupIdx, int ptrSetIdx, + DenseMap &VectorSSBufferPtrs, + ArrayRef> ssbufferPtrs) { + if (isAIC) { + return ssbufferPtrs[ptrSetIdx][groupIdx]; + } else { + return VectorSSBufferPtrs[groupIdx]; + } +} + +// Compute pointers for VECTOR core SSBuffer +std::optional> +UpdateConditionInfoPass::computeVectorSSBufferMemrefs( + OpBuilder &builder, Location loc, Operation *scopeOp, + SmallVector crossCoreInputValues, + SmallVector crossCoreOutputValues) { + if (!scopeOp) { + LDBG("Scope Op is null pointer!"); + return std::nullopt; + } + + // Collect all unique group indices + SmallVector allGroupIndices; + DenseSet uniqueIndices; + for (int idx : crossCoreInputValues) { + if (uniqueIndices.insert(idx).second) { + allGroupIndices.push_back(idx); + } + } + for (int idx : crossCoreOutputValues) { + if (uniqueIndices.insert(idx).second) { + allGroupIndices.push_back(idx); + } + } + + DenseMap vectorSSBufferMemrefs; + + builder.setInsertionPointToStart(&scopeOp->getRegion(0).front()); + int vec1Offset = 1024; + Value vec1OffsetValue = builder.create( + loc, VECTOR_SSBUF_OFFSET, ADDR_INT_TYPE); + auto subIdOp = builder.create( + loc, builder.getIntegerType(ADDR_INT_TYPE)); + Value ssbAddrOffset = + builder.create(loc, subIdOp, vec1OffsetValue); + + for (int groupIdx : allGroupIndices) { + auto ssbBaseAddr = builder.create( + loc, groupIdx * VALUE_SSBUF_OFFSET, ADDR_INT_TYPE); + auto ssbAddr = + builder.create(loc, ssbBaseAddr, ssbAddrOffset); + Value memref = builder.create( + loc, getSsbufMemrefType(builder), ssbAddr.getResult()); + vectorSSBufferMemrefs[groupIdx] = memref; + } + + return vectorSSBufferMemrefs; +} + +template +auto UpdateConditionInfoPass::createSsbufLoads( + OpBuilder &builder, Location loc, bool isAIC, int groupIdx, + DenseMap &vectorSSBufferMemrefs, + llvm::ArrayRef> ssbufferMemrefs, FuncTy &&pred) { + using ResTy = std::invoke_result_t; + SmallVector res; + size_t resNum = isAIC ? kVectorCount : 1; + for (int ptrSetIdx : llvm::seq(resNum)) { + auto loadOp = builder.create( + loc, + getSSBufferMemref(isAIC, groupIdx, ptrSetIdx, vectorSSBufferMemrefs, + ssbufferMemrefs), + ValueRange{}); + auto markOp = builder.create(loc, loadOp); + markOp->setAttr(kMemrefExtVolatile, builder.getUnitAttr()); + res.push_back(std::invoke(pred, loadOp, ptrSetIdx)); + } + return res; +} + +// Part 2: Add cross-core conditions +Value UpdateConditionInfoPass::addCrossCoreConditions( + OpBuilder &builder, Location loc, SmallVector crossCoreInputValues, + SmallVector crossCoreOutputValues, + DenseMap>> + &crossCoreBuffers, + bool isAIC, Value zeroConst, DenseMap &VectorSSBufferPtrs, + SmallVector> ssbufferPtrs) { + Value conditions = nullptr; + + auto combineCondition = [&](Value newCond) { + if (conditions) { + conditions = builder.create(loc, conditions, newCond); + } else { + conditions = newCond; + } + }; + + for (int inputGroupIdx : crossCoreInputValues) { + auto conds = createSsbufLoads( + builder, loc, isAIC, inputGroupIdx, VectorSSBufferPtrs, ssbufferPtrs, + [&](memref::LoadOp loadOp, auto) { + return builder.create(loc, arith::CmpIPredicate::sgt, + loadOp, zeroConst); + }); + Value cond = conds[0]; + if (isAIC) { + cond = builder.create(loc, conds[0], conds[1]); + } + combineCondition(cond); + } + + for (int outputGroupIdx : crossCoreOutputValues) { + int outputCount = 0; + if (crossCoreBuffers.count(outputGroupIdx)) { + for (auto &entry : crossCoreBuffers[outputGroupIdx]) { + outputCount += entry.second.size(); + } + } else { + LDBG("outputGroupIdx " << outputGroupIdx + << " not found in any buffers map!" << "\n"); + return nullptr; + } + Value bufferNum = + builder.create(loc, outputCount, CONST_INT_TYPE); + auto conds = createSsbufLoads( + builder, loc, isAIC, outputGroupIdx, VectorSSBufferPtrs, ssbufferPtrs, + [&](Value cond, auto) { + return builder.create(loc, arith::CmpIPredicate::slt, + cond, bufferNum); + }); + Value cond = conds[0]; + if (isAIC) { + cond = builder.create(loc, conds[0], conds[1]); + } + combineCondition(cond); + } + + return conditions; +} + +// Part 3: Update control variables in then block +void UpdateConditionInfoPass::updateCrossCoreControlVars( + OpBuilder &builder, Location loc, scf::IfOp ifOp, + SmallVector crossCoreInputValues, + SmallVector crossCoreOutputValues, bool isAIC, Value oneConst, + DenseMap &VectorSSBufferPtrs, + SmallVector> ssbufferPtrs) { + Block *thenBlock = &ifOp.getThenRegion().front(); + auto yieldOp = cast(thenBlock->getTerminator()); + builder.setInsertionPoint(yieldOp); + + for (int inputGroupIdx : crossCoreInputValues) { + createSsbufLoads(builder, loc, isAIC, inputGroupIdx, VectorSSBufferPtrs, + ssbufferPtrs, [&](Value loadRes, int ptrSetIdx) { + Value newValue = builder.create( + loc, loadRes, oneConst); + return builder.create( + loc, newValue, + getSSBufferMemref(isAIC, inputGroupIdx, ptrSetIdx, + VectorSSBufferPtrs, ssbufferPtrs), + ValueRange{}); + }); + } + + for (int outputGroupIdx : crossCoreOutputValues) { + createSsbufLoads(builder, loc, isAIC, outputGroupIdx, VectorSSBufferPtrs, + ssbufferPtrs, [&](Value loadRes, int ptrSetIdx) { + Value newValue = builder.create( + loc, loadRes, oneConst); + return builder.create( + loc, newValue, + getSSBufferMemref(isAIC, outputGroupIdx, ptrSetIdx, + VectorSSBufferPtrs, ssbufferPtrs), + ValueRange{}); + }); + } +} + +// Set the crossCore condition +int UpdateConditionInfoPass::setCrossCoreCondition( + SmallVector crossCoreInputValues, + SmallVector crossCoreOutputValues, + DenseMap>> + &crossCoreBuffers, + scf::IfOp ifOp, SmallVector> ssbufferPtrs, + Value &crossCoreCond) { + OpBuilder builder(ifOp); + Location loc = ifOp.getLoc(); + + // ========== Part 1: Preparation ========== + // Determine whether the current ifblock is on cube or vector core + auto aiCAttr = + hivm::TCoreTypeAttr::get(builder.getContext(), hivm::TCoreType::CUBE); + auto aivAttr = + hivm::TCoreTypeAttr::get(builder.getContext(), hivm::TCoreType::VECTOR); + bool isAIC = false; + bool isAIV = false; + mlir::Operation *parentOp = ifOp->getParentOp(); + mlir::Operation *scopeOp = nullptr; + while (parentOp) { + if (dyn_cast(parentOp)) { + scopeOp = parentOp; + break; + } + parentOp = parentOp->getParentOp(); + } + if (scopeOp && scopeOp->hasAttr("hivm.tcore_type")) { + auto attr = scopeOp->getAttr("hivm.tcore_type"); + if (attr == aiCAttr) { + isAIC = true; + } else if (attr == aivAttr) { + isAIV = true; + } else { + LDBG("scope block has invalid tcore_type attribute" << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + } else { + LDBG("ifblock not in a correct scope block" << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + Value zeroConst = + builder.create(loc, 0, CONST_INT_TYPE); + Value oneConst = builder.create(loc, 1, CONST_INT_TYPE); + + // If ifblock is on vector core, compute the required SSBuffer ptrs for vector + // side + DenseMap VectorSSBufferPtrs; + if (!isAIC) { + auto result = computeVectorSSBufferMemrefs( + builder, loc, scopeOp, crossCoreInputValues, crossCoreOutputValues); + if (!result) { + LDBG("computeVectorSSBufferPtrs failed!"); + return UPDATE_CONDITION_INFO_FAILED; + } + VectorSSBufferPtrs = std::move(*result); + } + + builder.setInsertionPoint(ifOp); + + // ========== Part 2: Add cross-core conditions ========== + crossCoreCond = addCrossCoreConditions( + builder, loc, crossCoreInputValues, crossCoreOutputValues, + crossCoreBuffers, isAIC, zeroConst, VectorSSBufferPtrs, ssbufferPtrs); + + // ========== Part 3: Update control variables ========== + updateCrossCoreControlVars(builder, loc, ifOp, crossCoreInputValues, + crossCoreOutputValues, isAIC, oneConst, + VectorSSBufferPtrs, ssbufferPtrs); + + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Collect the conditions for intra-core consumer values. +void UpdateConditionInfoPass::collectIntraCoreInputConditions( + OpBuilder &builder, Location loc, SmallVector &intraCoreInputValues, + DenseMap &idxToVar, SmallVector &conditions, + DenseSet &usedVarsSet, + DenseMap &varUpdateTypes) { + if (intraCoreInputValues.empty()) { + LDBG("No intraCore input conditions to collect." << "\n"); + return; + } + + size_t beforeConditionNum = conditions.size(); + Value zeroConst = + builder.create(loc, 0, CONST_INT_TYPE); + for (int idx : intraCoreInputValues) { + auto varIt = idxToVar.find(idx); + if (varIt == idxToVar.end()) { + LDBG("Skip intraCore input group " << idx << ": no control variable." + << "\n"); + continue; + } + + Value var = varIt->second; + Value varToUse = var; + auto latestIt = controlVarToLatestValue.find(var); + if (latestIt != controlVarToLatestValue.end()) { + varToUse = latestIt->second; + } + + Value cond = builder.create(loc, arith::CmpIPredicate::sgt, + varToUse, zeroConst); + conditions.push_back(cond); + usedVarsSet.insert(var); + varUpdateTypes[var] = VarUpdateType::DEC; + LDBG("Add intraCore input condition for group " << idx << "." << "\n"); + } + LDBG("Collected " << (conditions.size() - beforeConditionNum) + << " intraCore input conditions." << "\n"); +} + +// Collect the conditions for intra-core producer values. +int UpdateConditionInfoPass::collectIntraCoreOutputConditions( + OpBuilder &builder, Location loc, + DenseMap>> + &intraCoreBuffers, + SmallVector &intraCoreOutputValues, DenseMap &idxToVar, + SmallVector &conditions, DenseSet &usedVarsSet, + DenseMap &varUpdateTypes) { + if (intraCoreOutputValues.empty()) { + LDBG("No intraCore output conditions to collect." << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; + } + + size_t beforeConditionNum = conditions.size(); + SmallVector outputGroups; + if (buildOutputGroups(intraCoreOutputValues, intraCoreBuffers, idxToVar, + outputGroups) == UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + for (auto &group : outputGroups) { + int size = group.outputs.size(); + Value limitVal = + builder.create(loc, size, CONST_INT_TYPE); + for (Value var : group.inputVars) { + Value varToUse = var; + auto latestIt = controlVarToLatestValue.find(var); + if (latestIt != controlVarToLatestValue.end()) { + varToUse = latestIt->second; + } + + Value cond = builder.create(loc, arith::CmpIPredicate::slt, + varToUse, limitVal); + conditions.push_back(cond); + usedVarsSet.insert(var); + varUpdateTypes[var] = VarUpdateType::INC; + LDBG("Add intraCore output condition with producer limit " << size << "." + << "\n"); + } + } + LDBG("Collected " << (conditions.size() - beforeConditionNum) + << " intraCore output conditions." << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Build the ifOp variable mapping for the tensor iter_args +int UpdateConditionInfoPass::buildTensorIterArgIfOpVarMap(Operation *loopOp) { + // Clear any previous data + tensorIterArgIfOpVars.clear(); + + if (!info->tensorIterArgDepsMap.count(loopOp) || + !info->tensorIterArgIndicesMap.count(loopOp)) { + LDBG("Skip buildTensorIterArgIfOpVarMap: no tensor iter_args info for " + << *loopOp << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; + } + + auto &depsVec = info->tensorIterArgDepsMap[loopOp]; + auto &indicesMap = info->tensorIterArgIndicesMap[loopOp]; + + llvm::DenseMap> producerVars; + llvm::DenseMap> consumerVars; + + for (auto &depEntry : depsVec) { + Value origIterArg = depEntry.iterArg; + TensorIterArgIfOpRelation &relation = depEntry; + + if (!indicesMap.count(origIterArg)) { + LDBG("[Error]: origIterArg not found in indicesMap, loopOp=" << *loopOp + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + SmallVector &argIndices = indicesMap[origIterArg]; + + if (relation.consumers.size() != argIndices.size()) { + LDBG("[Error]: consumers size mismatch: " + << relation.consumers.size() << " vs " << argIndices.size() + << ", loopOp=" << *loopOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + // Establish a mapping (one-to-one) from consumers to variables + // for: region iter args; while-do: after-region args. + llvm::DenseMap consumerToVar; + for (size_t i = 0; i < relation.consumers.size(); ++i) { + scf::IfOp consumer = relation.consumers[i]; + Value var; + if (getLoopRegionIterArg(loopOp, argIndices[i], var) == + UPDATE_CONDITION_INFO_FAILED) { + LDBG("[Error]: invalid tensor iter_arg index " + << argIndices[i] << ", loopOp=" << *loopOp + << ", consumer=" << consumer << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + consumerToVar[consumer] = var; + } + + // Add all the variables of the consumers that depend on the producer + if (relation.producer) { + for (auto &[consumer, var] : consumerToVar) { + producerVars[relation.producer].insert(var); + } + } + + // Add all the variables of the consumers that depend on each producer + for (auto &[consumer, var] : consumerToVar) { + consumerVars[consumer].insert(var); + } + } + + // Convert the temporary data structure to tensorIfOpVarMap + for (auto &[producer, vars] : producerVars) { + auto &ifOpVars = tensorIterArgIfOpVars[producer]; + for (Value var : vars) { + ifOpVars.producerVars.push_back(var); + } + } + + for (auto &[consumer, vars] : consumerVars) { + auto &ifOpVars = tensorIterArgIfOpVars[consumer]; + for (Value var : vars) { + ifOpVars.consumerVars.push_back(var); + } + } + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Collect the conditions for tensor iter arg consumer values. +void UpdateConditionInfoPass::collectTensorIterArgInputConditions( + OpBuilder &builder, Location loc, scf::IfOp ifOp, + SmallVector &conditions, DenseSet &usedVarsSet, + DenseMap &varUpdateTypes) { + if (!tensorIterArgIfOpVars.count(ifOp)) { + return; + } + + auto &ifOpVars = tensorIterArgIfOpVars[ifOp]; + for (Value var : ifOpVars.consumerVars) { + Value varToUse = var; + auto latestIt = controlVarToLatestValue.find(var); + if (latestIt != controlVarToLatestValue.end()) { + varToUse = latestIt->second; + } + + Value oneConst = + builder.create(loc, 1, CONST_INT_TYPE); + Value cond = builder.create(loc, arith::CmpIPredicate::eq, + varToUse, oneConst); + conditions.push_back(cond); + usedVarsSet.insert(var); + varUpdateTypes[var] = VarUpdateType::DEC; + LDBG("Add tensor iter arg consumer condition for var.\n"); + } +} + +// Collect the conditions for tensor iter arg producer values. +void UpdateConditionInfoPass::collectTensorIterArgOutputConditions( + OpBuilder &builder, Location loc, scf::IfOp ifOp, + SmallVector &conditions, DenseSet &usedVarsSet, + DenseMap &varUpdateTypes) { + if (!tensorIterArgIfOpVars.count(ifOp)) { + return; + } + + auto &ifOpVars = tensorIterArgIfOpVars[ifOp]; + for (Value var : ifOpVars.producerVars) { + Value varToUse = var; + auto latestIt = controlVarToLatestValue.find(var); + if (latestIt != controlVarToLatestValue.end()) { + varToUse = latestIt->second; + } + + Value zeroConst = + builder.create(loc, 0, CONST_INT_TYPE); + Value cond = builder.create(loc, arith::CmpIPredicate::eq, + varToUse, zeroConst); + conditions.push_back(cond); + usedVarsSet.insert(var); + varUpdateTypes[var] = VarUpdateType::INC; + LDBG("Add tensor iter arg producer condition (var == 0) and +1 update for " + "var.\n"); + } +} + +// Set the intraCore condition. +int UpdateConditionInfoPass::setIntraCoreCondition( + ModuleOp module, scf::IfOp ifOp, + DenseMap>> + &intraCoreBuffers, + SmallVector &intraCoreInputValues, + SmallVector &intraCoreOutputValues, DenseMap &idxToVar, + DenseMap &varUpdateTypes, Value &intraCoreCond) { + LDBG("Enter set intraCore condition." << "\n"); + intraCoreCond = Value(); + OpBuilder builder(ifOp.getContext()); + builder.setInsertionPoint(ifOp); + Location loc = ifOp.getLoc(); + + SmallVector conditions; + DenseSet usedVarsSet; + LDBG("Collect intraCore conditions: inputs " + << intraCoreInputValues.size() << ", outputs " + << intraCoreOutputValues.size() << "\n"); + // Collect the conditions for intra-core consumer values. + collectIntraCoreInputConditions(builder, loc, intraCoreInputValues, idxToVar, + conditions, usedVarsSet, varUpdateTypes); + // Collect the conditions for intra-core producer values. + if (collectIntraCoreOutputConditions( + builder, loc, intraCoreBuffers, intraCoreOutputValues, idxToVar, + conditions, usedVarsSet, + varUpdateTypes) == UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + // Collect tensor iter_args conditions + collectTensorIterArgInputConditions(builder, loc, ifOp, conditions, + usedVarsSet, varUpdateTypes); + collectTensorIterArgOutputConditions(builder, loc, ifOp, conditions, + usedVarsSet, varUpdateTypes); + + if (!conditions.empty()) { + intraCoreCond = conditions[0]; + for (size_t i = 1; i < conditions.size(); ++i) { + intraCoreCond = + builder.create(loc, intraCoreCond, conditions[i]); + } + } + + currentUsedVars.clear(); + for (Value var : usedVarsSet) { + currentUsedVars.push_back(var); + } + LDBG("Built " << conditions.size() << " intraCore conditions using " + << currentUsedVars.size() << " control variables." << "\n"); + + LDBG("Exit set intraCore condition." << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Set the FlowOpt extra condition for the third if block in the DAG +int UpdateConditionInfoPass::setFlowOptCondition(scf::IfOp currentIfOp, + Operation *loopOp, + Value &flowOptCond) { + auto forOp = dyn_cast(loopOp); + if (!forOp) { + flowOptCond = nullptr; + return UPDATE_CONDITION_INFO_SUCCESS; + } + + // Check if current ifOp is a target node (third node) in flowOptIfOpPairs + if (!info->flowOptIfOpPairs.count(currentIfOp)) { + LDBG("Current ifOp is not a flowOpt target node, skip."); + flowOptCond = nullptr; + return UPDATE_CONDITION_INFO_SUCCESS; + } + + // Check if flowOpt condition is needed based on buffer counts + // - Cross-core buffer count > CROSS_CORE_BUFFER_COUNT_THRESHOLD + // - Intra-core buffer count > INTRA_CORE_BUFFER_COUNT_THRESHOLD + if (info->crossCoreBufferCount <= CROSS_CORE_BUFFER_COUNT_THRESHOLD || + info->intraCoreBufferCount <= INTRA_CORE_BUFFER_COUNT_THRESHOLD) { + flowOptCond = nullptr; + return UPDATE_CONDITION_INFO_SUCCESS; + } + + // Get the start node + scf::IfOp sourceIfOp = info->flowOptIfOpPairs[currentIfOp]; + if (!info->cntArgs.count(sourceIfOp)) { + LDBG("[Error] Start node has no counter in cntArgs, cannot build flowOpt " + "condition. currentIfOp=" + << currentIfOp << ", sourceIfOp=" << sourceIfOp << ", forOp=" << forOp + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + // Create builder and location + OpBuilder builder(currentIfOp); + Location loc = currentIfOp.getLoc(); + + Value counter = info->cntArgs[sourceIfOp]; + Value lowerBound = forOp.getLowerBound(); + Value upperBound = forOp.getUpperBound(); + Value step = forOp.getStep(); + + // Build condition: counter >= upperBound + Value cond1 = builder.create(loc, arith::CmpIPredicate::sge, + counter, upperBound); + + // Build condition: counter >= lowerBound + step * opt_num + // opt_num = min(intraCoreBufferCount - 1, crossCoreBufferCount) + int optInt = + std::min(info->intraCoreBufferCount - 1, info->crossCoreBufferCount); + + auto stepIntType = dyn_cast(step.getType()); + if (!stepIntType) { + LDBG("[Error] forOp step is expected to be an integer type, got " + << step.getType() << ", forOp=" << forOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + Value optNum = + builder.create(loc, optInt, stepIntType.getWidth()); + Value optOffset = builder.create(loc, step, optNum); + Value lowerPlusOffset = + builder.create(loc, lowerBound, optOffset); + Value cond2 = builder.create(loc, arith::CmpIPredicate::sge, + counter, lowerPlusOffset); + + // Combine: cond1 OR cond2 + flowOptCond = builder.create(loc, cond1, cond2); + + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Update DAG nodes after ifOp replacement +void UpdateConditionInfoPass::updateDAGAfterIfOpReplacement(scf::IfOp oldIfOp, + scf::IfOp newIfOp) { + // 1. Update ifBlockDAG + if (info->ifBlockDAG.count(oldIfOp)) { + auto consumers = info->ifBlockDAG[oldIfOp]; + info->ifBlockDAG.erase(oldIfOp); + info->ifBlockDAG[newIfOp] = consumers; + } + + for (auto &entry : info->ifBlockDAG) { + for (auto &edge : entry.second) { + if (edge.first == oldIfOp) { + edge.first = newIfOp; + } + } + } + + // 2. Update flowOptIfOpPairs + if (info->flowOptIfOpPairs.count(oldIfOp)) { + auto source = info->flowOptIfOpPairs[oldIfOp]; + info->flowOptIfOpPairs.erase(oldIfOp); + info->flowOptIfOpPairs[newIfOp] = source; + } + + for (auto &entry : info->flowOptIfOpPairs) { + if (entry.second == oldIfOp) { + entry.second = newIfOp; + } + } +} + +// Update the mapping of control variables to their latest values +void UpdateConditionInfoPass::updateControlVarToLatestValue(scf::IfOp newIfOp, + scf::IfOp oldIfOp, + bool hasCounter, + Value counter) { + if (currentUsedVars.empty() && !hasCounter) { + LDBG("No control variable latest values to update." << "\n"); + return; + } + + size_t origResultCount = oldIfOp.getNumResults(); + + for (size_t i = 0; i < currentUsedVars.size(); ++i) { + Value var = currentUsedVars[i]; + Value newValue = newIfOp.getResult(origResultCount + i); + controlVarToLatestValue[var] = newValue; + LDBG("Record latest intraCore control value at result index " + << (origResultCount + i) << "." << "\n"); + } + + if (hasCounter) { + size_t counterResultIdx = origResultCount + currentUsedVars.size(); + Value newCounterValue = newIfOp.getResult(counterResultIdx); + controlVarToLatestValue[counter] = newCounterValue; + LDBG("Record latest counter value at result index " << counterResultIdx + << "." << "\n"); + } + LDBG("[DEBUG] controlVarToLatestValue size: " + << controlVarToLatestValue.size() << "\n"); + + for (auto &entry : controlVarToLatestValue) { + LDBG("[DEBUG] key = " << entry.first + << " --> new value = " << entry.second << "\n"); + } +} + +// Update the yield in the forOp +int UpdateConditionInfoPass::updateForOpYield(scf::ForOp forOp) { + if (controlVarToLatestValue.empty()) { + LDBG("Failed to update forOp yield: no latest control variable values, " + << "forOp=" << forOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + Location loc = forOp.getLoc(); + Block *forBody = forOp.getBody(); + auto yieldOp = dyn_cast(forBody->getTerminator()); + if (!yieldOp) { + LDBG("Failed to update forOp yield: terminator is not scf.yield, forOp=" + << forOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + SmallVector newYieldOperands(yieldOp.getOperands().begin(), + yieldOp.getOperands().end()); + if (newYieldOperands.size() != forOp.getNumRegionIterArgs()) { + LDBG("Failed to update forOp yield: yield operands " + << newYieldOperands.size() << ", iter args " + << forOp.getNumRegionIterArgs() << ", forOp=" << forOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + DenseMap iterArgToIndex; + for (unsigned j = 0; j < forOp.getNumRegionIterArgs(); ++j) { + iterArgToIndex[forOp.getRegionIterArgs()[j]] = j; + } + + for (auto &entry : controlVarToLatestValue) { + Value origVar = entry.first; + Value latestValue = entry.second; + auto it = iterArgToIndex.find(origVar); + if (it == iterArgToIndex.end()) { + LDBG("Failed to update forOp yield: control variable is not a region " + "iter arg, forOp=" + << forOp << ", var=" << origVar << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + newYieldOperands[it->second] = latestValue; + LDBG("Update forOp yield operand index " << it->second << "\n"); + } + + OpBuilder yieldBuilder(yieldOp); + yieldBuilder.create(loc, newYieldOperands); + yieldOp.erase(); + LDBG("Updated forOp yield with " << controlVarToLatestValue.size() + << " latest control values." << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +int UpdateConditionInfoPass::updateWhileOpYield(scf::WhileOp whileOp) { + LDBG("Enter update whileOp yield " << "\n"); + if (controlVarToLatestValue.empty()) { + LDBG("Skip update whileOp yield: no latest control variable values, " + << "whileOp=" << whileOp << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; + } + + Location loc = whileOp.getLoc(); + Block *afterBody = whileOp.getAfterBody(); + auto yieldOp = dyn_cast(afterBody->getTerminator()); + if (!yieldOp) { + LDBG("Failed to update whileOp yield: terminator is not scf.yield, " + << "whileOp=" << whileOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + SmallVector newYieldOperands(yieldOp.getOperands().begin(), + yieldOp.getOperands().end()); + auto afterArgs = whileOp.getAfterArguments(); + if (newYieldOperands.size() != afterArgs.size()) { + LDBG("Failed to update whileOp yield: yield operands " + << newYieldOperands.size() << ", after args " << afterArgs.size() + << ", whileOp=" << whileOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + DenseMap iterArgToIndex; + for (auto [idx, arg] : llvm::enumerate(afterArgs)) + iterArgToIndex[arg] = idx; + + for (auto &entry : controlVarToLatestValue) { + Value origVar = entry.first; + Value latestValue = entry.second; + auto it = iterArgToIndex.find(origVar); + if (it == iterArgToIndex.end()) { + LDBG("Failed to update whileOp yield: control variable is not an after " + "region iter arg, whileOp=" + << whileOp << ", var=" << origVar << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + newYieldOperands[it->second] = latestValue; + LDBG("Update whileOp yield operand index " << it->second << "\n"); + } + + OpBuilder yieldBuilder(yieldOp); + yieldBuilder.create(loc, newYieldOperands); + yieldOp.erase(); + LDBG("Updated whileOp yield with " << controlVarToLatestValue.size() + << " latest control values." << "\n"); + LDBG("Exit update whileOp yield " << "\n"); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +int UpdateConditionInfoPass::updateLoopYield(Operation *loopOp) { + if (auto forOp = dyn_cast(loopOp)) + return updateForOpYield(forOp); + if (auto whileOp = dyn_cast(loopOp)) + return updateWhileOpYield(whileOp); + LDBG("updateLoopYield expects scf.for or scf.while, got " << *loopOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; +} + +int UpdateConditionInfoPass::collectSSBufferIfOps( + Operation *loopOp, SmallVector &ifOps) { + auto collectIfOps = [&](Operation *op) -> WalkResult { + if (!op->hasAttr(kIf)) { + return WalkResult::advance(); + } + + auto ifOp = dyn_cast(op); + if (!ifOp) { + LDBG("Found unsupported ssbuffer if op: " + << op->getName() << " under loopOp=" << *loopOp << "\n"); + return WalkResult::interrupt(); + } + + ifOps.push_back(ifOp); + return WalkResult::advance(); + }; + + WalkResult ifWalkResult; + if (auto forOp = dyn_cast(loopOp)) { + ifWalkResult = forOp.walk(collectIfOps); + } else if (auto whileOp = dyn_cast(loopOp)) { + // while-do: only after-region. + ifWalkResult = whileOp.getAfterBody()->walk(collectIfOps); + } else { + LDBG("collectSSBufferIfOps expects scf.for or scf.while, got " << *loopOp + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + if (ifWalkResult.wasInterrupted()) { + return UPDATE_CONDITION_INFO_FAILED; + } + return UPDATE_CONDITION_INFO_SUCCESS; +} + +int UpdateConditionInfoPass::validateBlockCounters(Operation *loopOp, + size_t ifOpCount) { + auto forOp = dyn_cast(loopOp); + if (!forOp) { + // blockCounters / cntArgs are for-only. + return UPDATE_CONDITION_INFO_SUCCESS; + } + + auto counterIt = info->blockCounters.find(forOp); + if (counterIt == info->blockCounters.end()) { + LDBG("Failed to assign counters for ssbuffer if ops: no counters for " + << "forOp=" << forOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + size_t counterNum = counterIt->second.size(); + if (ifOpCount > counterNum) { + LDBG("Failed to assign counters for all ssbuffer if ops: if ops " + << ifOpCount << ", counters " << counterNum << ", forOp=" << forOp + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + return UPDATE_CONDITION_INFO_SUCCESS; +} + +SmallVector +UpdateConditionInfoPass::buildNewIfResultTypes(scf::IfOp oldIfOp, + bool hasCounter, Value counter) { + SmallVector resultTypes; + for (Value result : oldIfOp.getResults()) { + resultTypes.push_back(result.getType()); + } + for (Value var : currentUsedVars) { + resultTypes.push_back(var.getType()); + } + if (hasCounter) { + resultTypes.push_back(counter.getType()); + } + LDBG("Build new if result types: old results " + << oldIfOp.getNumResults() << ", control vars " << currentUsedVars.size() + << ", has counter " << hasCounter << "." << "\n"); + return resultTypes; +} + +void UpdateConditionInfoPass::collectYieldOperands( + Block &block, Operation *&yieldOp, SmallVector &yieldOperands) { + yieldOp = nullptr; + yieldOperands.clear(); + if (block.empty()) { + LDBG("Collect yield operands: block is empty." << "\n"); + return; + } + + Operation *lastOp = &block.back(); + if (!isa(lastOp)) { + LDBG("Collect yield operands: block terminator is not scf.yield." << "\n"); + return; + } + + yieldOp = lastOp; + auto scfYieldOp = cast(lastOp); + yieldOperands.assign(scfYieldOp.getOperands().begin(), + scfYieldOp.getOperands().end()); + LDBG("Collected " << yieldOperands.size() << " yield operands." << "\n"); +} + +void UpdateConditionInfoPass::populateNewThenBlock( + scf::IfOp newIfOp, Block &oldThenBlock, Operation *oldThenYieldOp, + ArrayRef oldYieldOperands, + DenseMap &varUpdateTypes, bool hasCounter, + Value counter, Value step) { + Location loc = newIfOp.getLoc(); + Block &newThenBlock = newIfOp.getThenRegion().front(); + for (Operation &op : llvm::make_early_inc_range(oldThenBlock)) { + if (&op != oldThenYieldOp) { + op.moveBefore(&newThenBlock, newThenBlock.end()); + } + } + LDBG("Populate new then block with " << oldYieldOperands.size() + << " original yield operands." << "\n"); + + OpBuilder thenBuilder(&newThenBlock, newThenBlock.end()); + SmallVector thenYieldOperands(oldYieldOperands.begin(), + oldYieldOperands.end()); + if (!currentUsedVars.empty()) { + Value one = + thenBuilder.create(loc, 1, CONST_INT_TYPE); + for (Value var : currentUsedVars) { + Value varToUse = var; + auto latestIt = controlVarToLatestValue.find(var); + if (latestIt != controlVarToLatestValue.end()) { + varToUse = latestIt->second; + } + + Value yieldVal = varToUse; + auto it = varUpdateTypes.find(var); + if (it != varUpdateTypes.end()) { + if (it->second == VarUpdateType::DEC) { + yieldVal = thenBuilder.create(loc, varToUse, one); + } else if (it->second == VarUpdateType::INC) { + yieldVal = thenBuilder.create(loc, varToUse, one); + } + } + thenYieldOperands.push_back(yieldVal); + } + } + + if (hasCounter) { + Value newCounter = thenBuilder.create(loc, counter, step); + thenYieldOperands.push_back(newCounter); + LDBG("Append updated counter to then yield." << "\n"); + } + + LDBG("Create then yield with " << thenYieldOperands.size() << " operands." + << "\n"); + thenBuilder.create(loc, thenYieldOperands); +} + +void UpdateConditionInfoPass::populateNewElseBlock(scf::IfOp newIfOp, + scf::IfOp oldIfOp, + bool oldHasElse, + bool hasCounter, + Value counter) { + Location loc = newIfOp.getLoc(); + Block &newElseBlock = newIfOp.getElseRegion().front(); + SmallVector oldElseYieldOperands; + Operation *oldElseYieldOp = nullptr; + + if (oldHasElse) { + Block &oldElseBlock = oldIfOp.getElseRegion().front(); + collectYieldOperands(oldElseBlock, oldElseYieldOp, oldElseYieldOperands); + for (Operation &op : llvm::make_early_inc_range(oldElseBlock)) { + if (&op != oldElseYieldOp) { + op.moveBefore(&newElseBlock, newElseBlock.end()); + } + } + LDBG("Moved old else block ops and collected " + << oldElseYieldOperands.size() << " old else yield operands." << "\n"); + } + + OpBuilder elseBuilder(&newElseBlock, newElseBlock.end()); + SmallVector elseYieldOperands; + for (Value operand : oldElseYieldOperands) { + Value newOperand = operand; + auto it = controlVarToLatestValue.find(operand); + if (it != controlVarToLatestValue.end()) { + newOperand = it->second; + } + elseYieldOperands.push_back(newOperand); + } + + for (Value var : currentUsedVars) { + Value varToUse = var; + auto it = controlVarToLatestValue.find(var); + if (it != controlVarToLatestValue.end()) { + varToUse = it->second; + } + elseYieldOperands.push_back(varToUse); + } + + if (hasCounter) { + Value counterToUse = counter; + auto it = controlVarToLatestValue.find(counter); + if (it != controlVarToLatestValue.end()) { + counterToUse = it->second; + } + elseYieldOperands.push_back(counterToUse); + } + + LDBG("Create else yield with " << elseYieldOperands.size() << " operands." + << "\n"); + elseBuilder.create(loc, elseYieldOperands); + if (oldElseYieldOp) { + oldElseYieldOp->erase(); + } +} + +// Create new IfOp with new then and else blocks. +scf::IfOp UpdateConditionInfoPass::createNewIfOpWithBlocks( + scf::IfOp oldIfOp, Value combinedCond, + DenseMap &varUpdateTypes, bool hasCounter, + Value counter, Value step) { + Location loc = oldIfOp.getLoc(); + OpBuilder builder(oldIfOp); + + bool needsYield = !currentUsedVars.empty() || hasCounter; + bool oldHasElse = oldIfOp.getElseRegion().hasOneBlock(); + // Only create else when we will populate it; + bool withElse = needsYield || oldHasElse; + LDBG("Create replacement if op: needs yield " + << needsYield << ", old has else " << oldHasElse << ", with else " + << withElse << ", current used vars " << currentUsedVars.size() << "." + << "\n"); + + Block &oldThenBlock = oldIfOp.getThenRegion().front(); + Operation *oldThenYieldOp = nullptr; + SmallVector oldYieldOperands; + collectYieldOperands(oldThenBlock, oldThenYieldOp, oldYieldOperands); + SmallVector resultTypes = + buildNewIfResultTypes(oldIfOp, hasCounter, counter); + scf::IfOp newIfOp = + builder.create(loc, resultTypes, combinedCond, withElse); + LDBG("Created replacement if op with " << resultTypes.size() << " results." + << "\n"); + + for (auto &attr : oldIfOp->getAttrs()) { + newIfOp->setAttr(attr.getName(), attr.getValue()); + } + + populateNewThenBlock(newIfOp, oldThenBlock, oldThenYieldOp, oldYieldOperands, + varUpdateTypes, hasCounter, counter, step); + if (withElse) { + populateNewElseBlock(newIfOp, oldIfOp, oldHasElse, hasCounter, counter); + } + + for (size_t i = 0; i < oldIfOp.getNumResults(); ++i) { + oldIfOp.getResult(i).replaceAllUsesWith(newIfOp.getResult(i)); + } + LDBG("Replaced " << oldIfOp.getNumResults() << " old if results." << "\n"); + + return newIfOp; +} + +// Combine the conditions: crossCore condition + intraCore condition + counter +// condition + flowOpt condition +int UpdateConditionInfoPass::combineConditions( + ModuleOp module, Value crossCoreCond, Value intraCoreCond, + Value flowOptCond, scf::IfOp ifOp, Operation *loopOp, + size_t &usedCounterNum, DenseMap &varUpdateTypes) { + Location loc = ifOp.getLoc(); + SmallVector validConditions; + Value counter; + // Only for updates counter args inside if then (+step). + bool updateCounterArg = false; + + if (crossCoreCond) { + validConditions.push_back(crossCoreCond); + } + if (intraCoreCond) { + validConditions.push_back(intraCoreCond); + } + if (flowOptCond) { + validConditions.push_back(flowOptCond); + } + + auto forOp = dyn_cast(loopOp); + auto whileOp = dyn_cast(loopOp); + OpBuilder condBuilder(ifOp); + + if (forOp) { + if (!info->blockCounters.count(forOp)) { + LDBG("Missing block counters for forOp=" << forOp << ", ifOp=" << ifOp + << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + SmallVector &counterIndices = info->blockCounters[forOp]; + + if (info->cntArgs.count(ifOp)) { + counter = info->cntArgs[ifOp]; + updateCounterArg = true; + } else { + if (usedCounterNum >= counterIndices.size()) { + LDBG("Not enough counters for ssbuffer if ops: used " + << usedCounterNum << ", counters " << counterIndices.size() + << ", forOp=" << forOp << ", ifOp=" << ifOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + int argIdx = counterIndices[usedCounterNum]; + int iterArgNum = static_cast(forOp.getNumRegionIterArgs()); + if (argIdx < 0 || argIdx >= iterArgNum) { + LDBG("Invalid counter arg index: " << argIdx << ", iter args " + << iterArgNum << ", forOp=" << forOp + << ", ifOp=" << ifOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + counter = forOp.getRegionIterArgs()[argIdx]; + updateCounterArg = true; + info->cntArgs[ifOp] = counter; + usedCounterNum++; + LDBG("Assign counter iter arg index " << argIdx << " to ssbuffer if op." + << "\n"); + } + + LDBG("this ifop used counter is: " << counter << "\n"); + Value upperBound = forOp.getUpperBound(); + Value counterToUse = counter; + auto latestIt = controlVarToLatestValue.find(counter); + if (latestIt != controlVarToLatestValue.end()) { + counterToUse = latestIt->second; + } + Value counterCond = condBuilder.create( + loc, arith::CmpIPredicate::slt, counterToUse, upperBound); + validConditions.push_back(counterCond); + } else if (whileOp) { + // Only add remapped scf.condition(x) expression; + Value counterCond; + if (buildWhileCounterCondition(whileOp, ifOp, info, condBuilder, + controlVarToLatestValue, + counterCond) == UPDATE_CONDITION_INFO_FAILED) + return UPDATE_CONDITION_INFO_FAILED; + validConditions.push_back(counterCond); + } else { + LDBG("Unsupported loop op for counter condition, loopOp=" + << *loopOp << ", ifOp=" << ifOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + if (validConditions.empty()) { + LDBG("Failed to build any condition for ssbuffer if op, ifOp=" + << ifOp << ", loopOp=" << *loopOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + LDBG("Combine " << validConditions.size() << " conditions for ssbuffer if op." + << "\n"); + OpBuilder builder(ifOp); + Value combinedCond = validConditions[0]; + for (size_t i = 1; i < validConditions.size(); ++i) { + combinedCond = + builder.create(loc, combinedCond, validConditions[i]); + } + + Value step = updateCounterArg ? forOp.getStep() : Value(); + scf::IfOp newIfOp = createNewIfOpWithBlocks( + ifOp, combinedCond, varUpdateTypes, updateCounterArg, counter, step); + + // Update DAG nodes + updateDAGAfterIfOpReplacement(ifOp, newIfOp); + + if (updateCounterArg) { + info->cntArgs.erase(ifOp); + info->cntArgs[newIfOp] = counter; + } + + // Update mappings that refer to the old ifOp BEFORE erasing it + // Update the tensorIterArgIfOpVars mapping + if (tensorIterArgIfOpVars.count(ifOp)) { + auto ifOpVars = tensorIterArgIfOpVars[ifOp]; + tensorIterArgIfOpVars.erase(ifOp); + tensorIterArgIfOpVars[newIfOp] = ifOpVars; + } + + // Update tensorIterArgDepsMap with new ifOp + if (info->tensorIterArgDepsMap.count(loopOp)) { + auto &depsVec = info->tensorIterArgDepsMap[loopOp]; + for (auto &relation : depsVec) { + // Update producer ifOp - use pointer comparison + if (relation.producer.getOperation() == ifOp.getOperation()) { + relation.producer = newIfOp; + } + // Update consumer ifOps + for (auto &consumerIfOp : relation.consumers) { + if (consumerIfOp.getOperation() == ifOp.getOperation()) { + consumerIfOp = newIfOp; + } + } + } + } + + updateControlVarToLatestValue(newIfOp, ifOp, updateCounterArg, counter); + + ifOp.erase(); + return UPDATE_CONDITION_INFO_SUCCESS; +} + +// Update the conditions of ifOp. +int UpdateConditionInfoPass::updateIfConds( + ModuleOp module, SmallVector> ssbufferPtrs) { + // Walk main_loop for/while ops to update the conditions of ifOp + SmallVector mainLoopOps; + WalkResult walkResult = module.walk([&](Operation *op) -> WalkResult { + if (CVPipeline::isMainLoopOp(op)) { + mainLoopOps.push_back(op); + return WalkResult::advance(); + } + + if (op->hasAttr(kMainLoop)) { + LDBG("Found unsupported main loop op: " << *op << "\n"); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (walkResult.wasInterrupted()) { + return UPDATE_CONDITION_INFO_FAILED; + } + + // Step0: Collect dependency buffers once outside the loop + DenseMap>> + crossCoreBuffers; + DenseMap>>> + intraCoreBuffersMap; + collectDependencyBuffers(module, mainLoopOps, crossCoreBuffers, + intraCoreBuffersMap); + + for (Operation *loopOp : mainLoopOps) { + controlVarToLatestValue.clear(); + + // Step1: Get intraCoreBuffers from pre-collected map + DenseMap>> + intraCoreBuffers; + if (intraCoreBuffersMap.count(loopOp)) { + intraCoreBuffers = intraCoreBuffersMap[loopOp]; + } + + // for/while requires at least one of cross/intra deps; + if (crossCoreBuffers.empty() && intraCoreBuffers.empty()) { + LDBG("crossCoreBuffers and intraCoreBuffers are all empty! loopOp=" + << *loopOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + // Step2: Assign control variables (intraCore inputs / tensor iter_args) + DenseMap idxToVar; + if (buildIdxToVarMap(loopOp, intraCoreBuffers, idxToVar) == + UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + if (buildTensorIterArgIfOpVarMap(loopOp) == UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + + size_t usedCounterNum = 0; + SmallVector ifOps; + if (collectSSBufferIfOps(loopOp, ifOps) == UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + if (validateBlockCounters(loopOp, ifOps.size()) == + UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + + // Update the conditions of ifOp in this loop op. + for (scf::IfOp ifOp : ifOps) { + SmallVector crossCoreInputValues; + SmallVector crossCoreOutputValues; + SmallVector intraCoreInputValues; + SmallVector intraCoreOutputValues; + + if (getInputOutputValues(ifOp, crossCoreBuffers, intraCoreBuffers, + crossCoreInputValues, crossCoreOutputValues, + intraCoreInputValues, + intraCoreOutputValues) != 0) { + LDBG("getInputOutputValues failed! ifOp=" << ifOp << ", loopOp=" + << *loopOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + + // Step3:Set the crossCore condition + Value crossCoreCond; + if (setCrossCoreCondition(crossCoreInputValues, crossCoreOutputValues, + crossCoreBuffers, ifOp, ssbufferPtrs, + crossCoreCond) != 0) { + LDBG("setCrossCoreCondition failed! ifOp=" << ifOp << ", loopOp=" + << *loopOp << "\n"); + return UPDATE_CONDITION_INFO_FAILED; + } + // Step4:Set the intraCore condition + DenseMap varUpdateTypes; + Value intraCoreCond; + if (setIntraCoreCondition(module, ifOp, intraCoreBuffers, + intraCoreInputValues, intraCoreOutputValues, + idxToVar, varUpdateTypes, intraCoreCond) == + UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + // Step5:Set the flowOpt condition (for only; needs lb/ub/step) + Value flowOptCond; + if (setFlowOptCondition(ifOp, loopOp, flowOptCond) == + UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + // Step6:Combine the conditions: crossCore + intraCore + counter + + // flowOpt + if (combineConditions(module, crossCoreCond, intraCoreCond, flowOptCond, + ifOp, loopOp, usedCounterNum, + varUpdateTypes) == UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + } + // Step7: Update loop yield with latest control values + if (updateLoopYield(loopOp) == UPDATE_CONDITION_INFO_FAILED) { + return UPDATE_CONDITION_INFO_FAILED; + } + } + return UPDATE_CONDITION_INFO_SUCCESS; +} + +void UpdateConditionInfoPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Enter UpdateConditionInfo pass." << "\n"); + // Step1:Init the ssbufferPtrs + SmallVector> ssbufferPtrs = allocSSBuffer(module); + + // Step2:Update the conditions of ifOp based on the intraCoreDependentMap and + // crossCoreDependentMap + int updateResult = updateIfConds(module, ssbufferPtrs); + + if (updateResult != UPDATE_CONDITION_INFO_SUCCESS) { + LDBG("updateIfConds failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + + LDBG("Exit UpdateConditionInfo pass." << "\n"); +} + +namespace mlir { +namespace triton { +std::unique_ptr> createUpdateConditionInfoPass() { + return std::make_unique(); +} +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopIterTimes.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopIterTimes.cpp new file mode 100644 index 0000000000..d401bda879 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopIterTimes.cpp @@ -0,0 +1,1341 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopIterTimes.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "UpdateLoopIterTimes"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) LLVM_DEBUG(DBGS() << __VA_ARGS__ << "\n") + +using llvm::DenseMap; +using llvm::DenseSet; +using llvm::SmallVector; +using namespace mlir; +using namespace triton; +using namespace hivm; + +// Find the IfOp index that an operation belongs to +// Uses isAncestor to check if the operation is within the IfOp's region +static int findIfOpIndexInList(Operation *op, SmallVector &ifOps, + llvm::DenseMap &ifOpIndex) { + for (scf::IfOp ifOp : ifOps) { + if (ifOp->isAncestor(op)) { + return ifOpIndex[ifOp.getOperation()]; + } + } + return -1; +} + +// Filter out entries where consumer op is not inside the specified forOp +static ConsumerProducerMap +filterCrossCoreMapByForOp(scf::ForOp forOp, ConsumerProducerMap &crossCoreMap) { + ConsumerProducerMap filteredMap; + for (auto &entry : crossCoreMap) { + Operation *consumerOp = entry.first; + if (!forOp->isAncestor(consumerOp)) { + continue; + } + filteredMap[consumerOp] = entry.second; + } + return filteredMap; +} + +// Find the other side's mainloop with the same mainloop id in the module +// currentIsCube/currentIsVector indicates which side is the current scope +// Returns the forOp of the other side's mainloop, or nullptr if not found +static scf::ForOp getOtherScopeMainloop(ModuleOp module, bool currentIsCube, + bool currentIsVector, int mainLoopId) { + scf::ForOp otherSideForOp = nullptr; + int ret = 1; + + module.walk([&](scope::ScopeOp scopeOp) { + // Determine this scope's type + bool scopeIsCube = false; + bool scopeIsVector = false; + if (failed(getScopeType(scopeOp, scopeIsCube, scopeIsVector))) { + ret = -1; + LDBG("failed to get ScopeOp core type!"); + return mlir::WalkResult::interrupt(); + } + + // Only proceed when current is cube and scope is vector, or current is + // vector and scope is cube + bool isOtherSide = + (currentIsCube && scopeIsVector) || (currentIsVector && scopeIsCube); + if (!isOtherSide) { + return WalkResult::advance(); + } + + // Walk for loops inside this scope to find the one with matching mainloop + // id + scopeOp.walk([&](Operation *op) { + if (op->hasAttr(CVPipeline::kMainLoop)) { + // Support both ForOp and WhileOp for mainloop + bool isValidLoop = isa(op) || isa(op); + if (!isValidLoop) { + LDBG("mainloop must be ForOp or WhileOp!"); + return WalkResult::advance(); + } + + // Only cast to ForOp for compatibility with current interface + // WhileOp will be skipped in later steps + auto targetForOp = dyn_cast(op); + if (!targetForOp) { + // WhileOp case: skip for now, will be handled separately + return WalkResult::advance(); + } + + auto targetMainLoopId = + targetForOp->getAttrOfType(CVPipeline::kMainLoop); + if (targetMainLoopId && targetMainLoopId.getInt() == mainLoopId) { + otherSideForOp = targetForOp; + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + + if (otherSideForOp) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (ret == -1) { + return nullptr; + } else { + return otherSideForOp; + } +} + +// Check if the current compute block runs first by checking if the first ifOp +// has any consumer defOp Returns true if the current compute block runs first +// (no consumer in first ifOp) Returns false if the current compute block runs +// later (has consumer in first ifOp) +static bool isRunFirst(SmallVector &ifOps, + ConsumerProducerMap &crossDeps) { + if (ifOps.empty()) { + return true; + } + scf::IfOp firstIfOp = ifOps[0]; + for (auto &entry : crossDeps) { + Operation *consumerOp = entry.first; + if (consumerOp && firstIfOp->isAncestor(consumerOp)) { + return false; + } + } + return true; +} + +// Type definition for IfOp filter function +// Returns true if the IfOp should be collected, false otherwise +using IfOpFilter = std::function; + +// Default filter that accepts all IfOps +static bool defaultIfOpFilter(scf::IfOp ifOp) { return true; } + +// Filter that checks if IfOp contains sync_block_wait or sync_block_set op +static bool syncBlockFilter(scf::IfOp ifOp) { + bool containsSyncBlock = false; + ifOp.walk([&](Operation *innerOp) { + if (isa(innerOp) || + isa(innerOp)) { + containsSyncBlock = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return containsSyncBlock; +} + +// Collect ifOps from forOp based on the provided filter +// Fills ifOps and ifOpIndexMap with the collected ifOps +// Returns true on success, false on error or if no ifOps found +bool collectIfOps(scf::ForOp forOp, SmallVector &ifOps, + llvm::DenseMap &ifOpIndexMap, + IfOpFilter filter = defaultIfOpFilter) { + ifOps.clear(); + ifOpIndexMap.clear(); + int index = 1; + int ret = 0; + + forOp.walk([&](Operation *op) { + if (op->hasAttr(CVPipeline::kIf)) { + auto ifOp = dyn_cast(op); + if (!ifOp) { + ret = -1; + LDBG("ssbuffer.if attribute is not allocated on ifOp!"); + return WalkResult::interrupt(); + } + // Apply the filter to determine if this IfOp should be collected + if (filter(ifOp)) { + ifOps.push_back(ifOp); + ifOpIndexMap[ifOp.getOperation()] = index++; + } + } + return WalkResult::advance(); + }); + + if (ret == -1) { + LDBG("Failed to collect ifOps!"); + return false; + } + + if (ifOps.empty()) { + LDBG("mainloop does not contain any ifblocks!"); + return false; + } + + return true; +} + +// Helper function to find the other side's mainloop and collect its ifOps +// Returns the other side's forOp and fills otherSideIfOps with ifOps from that +// mainloop Returns nullptr if not found +static scf::ForOp findOtherSideMainloopAndIfOps( + scf::ForOp currentForOp, bool currentIsCube, bool currentIsVector, + SmallVector &otherSideIfOps, + llvm::DenseMap &otherSideIfOpIndexMap) { + otherSideIfOps.clear(); + otherSideIfOpIndexMap.clear(); + + // Get current forOp's mainloop id + auto currentMainLoopId = + currentForOp->getAttrOfType(CVPipeline::kMainLoop); + if (!currentMainLoopId) { + LDBG("Current forOp does not have main_loop id!"); + return nullptr; + } + int mainLoopId = currentMainLoopId.getInt(); + + // Find the other side's mainloop with the same mainloop id + // Traverse up the parent chain to find ModuleOp + Operation *currentOp = currentForOp->getParentOp(); + ModuleOp module = nullptr; + while (currentOp) { + if (auto mod = dyn_cast(currentOp)) { + module = mod; + break; + } + currentOp = currentOp->getParentOp(); + } + if (!module) { + LDBG("Can not find ModuleOp in parent chain!"); + return nullptr; + } + + // Find other side mainloop with the same mainloop id + scf::ForOp otherSideForOp = + getOtherScopeMainloop(module, currentIsCube, currentIsVector, mainLoopId); + + if (!otherSideForOp) { + LDBG("Can not find the other side's mainloop with id: " << mainLoopId); + return nullptr; + } + + // Collect ifOps from the other side's mainloop + // Only collect ifOps that contain hivm.hir.sync_block_wait op + if (!collectIfOps(otherSideForOp, otherSideIfOps, otherSideIfOpIndexMap, + syncBlockFilter)) { + LDBG("Failed to collect ifOps from other side mainloop!"); + return nullptr; + } + + return otherSideForOp; +} + +// calculateFactor computes the buffer factor needed for loop iteration +// extension Core idea: if producer ifOp (n) and consumer ifOp (m) have +// dependency, we need (m - n + 1) buffers to support the loop extension This +// function handles both intra-core and cross-core dependencies Returns +// {maxRequiredBuffers, x} for computing: ceil(original_iter_count * +// requiredBuffers / x) + ifCount +std::pair UpdateLoopIterTimesPass::calculateFactor(scf::ForOp forOp) { + int maxRequiredBuffers = 1; + int maxX = 1; + + // Step1: Collect all IfOps with ssbuffer.if attribute in this for loop + SmallVector ifOps; + DenseMap ifOpIndex; + if (!collectIfOps(forOp, ifOps, ifOpIndex)) { + LDBG("Failed to collect ifOps!"); + return {-1, -1}; + } + + // Step2: Calculate factor based on intra-core dependencies + bool hasIntraDeps = info->intraCoreDependentMap.count(forOp) && + !info->intraCoreDependentMap[forOp].empty(); + bool hasCrossDeps = !info->crossCoreDependentMap.empty(); + + // If no dependencies at all, return factor = 1 + if (!hasIntraDeps && !hasCrossDeps) { + return {1, 1}; + } + + // Calculate intra-core factor + if (hasIntraDeps) { + auto &intraDeps = info->intraCoreDependentMap[forOp]; + auto [intraRequiredBuffers, intraX] = + calculateIntraDepsFactor(ifOps, ifOpIndex, intraDeps); + if (intraRequiredBuffers == -1 || intraX == -1) { + LDBG("calculateIntraDepsFactor failed!"); + return {-1, -1}; + } + maxRequiredBuffers = intraRequiredBuffers; + maxX = intraX; + } + + // Calculate cross-core factor and merge with intra-core factor + if (hasCrossDeps) { + // Filter out entries where consumer op is not inside current forOp + ConsumerProducerMap filteredCrossCoreMap = + filterCrossCoreMapByForOp(forOp, info->crossCoreDependentMap); + + // for caculating the crossdeps, need to filter ifblocks without + // sync_wait/sync_set op + SmallVector filterIfOps; + DenseMap filterIfOpIndex; + if (!collectIfOps(forOp, filterIfOps, filterIfOpIndex, syncBlockFilter)) { + LDBG("Failed to collect filtered ifOps!"); + return {-1, -1}; + } + + auto [crossRequiredBuffers, crossX] = calculateCrossDepsFactor( + forOp, filterIfOps, filterIfOpIndex, filteredCrossCoreMap); + if (crossRequiredBuffers == -1 || crossX == -1) { + LDBG("calculateCrossDepsFactor failed!"); + return {-1, -1}; + } + + // Update maxRequiredBuffers/maxX + if (crossRequiredBuffers * maxX > maxRequiredBuffers * crossX) { + maxRequiredBuffers = crossRequiredBuffers; + maxX = crossX; + } + } + + // Step3: Calculate factor based on iteration dependencies (tensor iter_args + // dependencies) + bool hasIterDeps = info->tensorIterArgDepsMap.count(forOp) && + !info->tensorIterArgDepsMap[forOp].empty(); + + if (hasIterDeps) { + LDBG("find cross-iteration dependency, size: " + << info->tensorIterArgDepsMap[forOp].size()); + auto [iterRequiredBuffers, iterX] = + calculateIterDepsFactor(forOp, ifOps, ifOpIndex); + if (iterRequiredBuffers == -1 || iterX == -1) { + LDBG("calculateIterDepsFactor failed!"); + return {-1, -1}; + } + // Compare iterRequiredBuffers/iterX vs maxRequiredBuffers/maxX + // Take the larger fraction + if (iterRequiredBuffers * maxX > maxRequiredBuffers * iterX) { + maxRequiredBuffers = iterRequiredBuffers; + maxX = iterX; + } + } + + return {maxRequiredBuffers, maxX}; +} + +static int getConsumerIfOpIndex(Operation *consumerOp, + SmallVector &ifOps, + llvm::DenseMap &ifOpIndex) { + if (!consumerOp) { + LDBG("consumerOp is null!"); + return -1; + } + int idx = findIfOpIndexInList(consumerOp, ifOps, ifOpIndex); + if (idx == -1) { + LDBG("Can not find the consumerOp in any ifOps!"); + return -1; + } + return idx; +} + +static int getProducerIfOpIndex(SmallVector &producerOps, + SmallVector &ifOps, + llvm::DenseMap &ifOpIndex) { + if (producerOps.empty()) { + LDBG("consumer do not have the producerOps!"); + return -1; + } + + int producerIfOpIndex = -1; + for (Operation *producerOp : producerOps) { + int currentIndex = findIfOpIndexInList(producerOp, ifOps, ifOpIndex); + + if (currentIndex == -1) { + LDBG("ProducerOp not found in any ifOps: " << *producerOp); + return -1; + } + + // First producer found + if (producerIfOpIndex == -1) { + producerIfOpIndex = currentIndex; + } else { + // Check if all producers are in the same ifOp + if (currentIndex != producerIfOpIndex) { + LDBG("ProducerOps are not in the same ifOp! " + << "First producer ifOp index: " << producerIfOpIndex + << ", Current producer ifOp index: " << currentIndex); + return -1; + } + } + } + + if (producerIfOpIndex == -1) { + LDBG("All producerOps are not found in any ifOps!"); + return -1; + } + + return producerIfOpIndex; +} + +std::pair UpdateLoopIterTimesPass::calculateIntraDepsFactor( + SmallVector &ifOps, DenseMap &ifOpIndex, + llvm::DenseMap> &deps) { + int maxRequiredBuffers = 1; + int maxX = 1; + + // Iterate all dependencies and calculate required buffer count + for (auto &entry : deps) { + Operation *consumerOp = entry.first; // Consumer operation + SmallVector producerOps = entry.second; // Producer op list + int x = producerOps.size(); // Producer op count + + // Find the IfOp index that consumer belongs to (ConsumerIdx) + int ConsumerIdx = getConsumerIfOpIndex(consumerOp, ifOps, ifOpIndex); + if (ConsumerIdx == -1) { + return {-1, -1}; + } + + // Find the IfOp index that producer belongs to (producerIdx) + int producerIdx = getProducerIfOpIndex(producerOps, ifOps, ifOpIndex); + if (producerIdx == -1) { + return {-1, -1}; + } + + // If consumer is after producer (ConsumerIdx > producerIdx), calculate + // required buffer count ConsumerIdx - producerIdx + 1 represents the buffer + // count needed to cover this distance + if (ConsumerIdx <= producerIdx) { + LDBG("producer is after the consumer!"); + return {-1, -1}; + } + int requiredBuffers = ConsumerIdx - producerIdx + 1; + + // Update maxRequiredBuffers/maxX + if (requiredBuffers * maxX > maxRequiredBuffers * x) { + maxRequiredBuffers = requiredBuffers; + maxX = x; + } + } + + return {maxRequiredBuffers, maxX}; +} + +// calculateCrossDepsFactor computes the buffer factor based on cross-core +// dependencies For cross-core deps consumer is in current forOp (one side: cube +// or vector) producer is in another mainloop with the same id but in the other +// scope +std::pair UpdateLoopIterTimesPass::calculateCrossDepsFactor( + scf::ForOp forOp, SmallVector &ifOps, + DenseMap &ifOpIndex, ConsumerProducerMap &crossDeps) { + int maxRequiredBuffers = 1; + int maxX = 1; + + // Determine current forOp's scope type (cube or vector) + bool currentIsCube = false; + bool currentIsVector = false; + Operation *currentScope = forOp->getParentOp(); + while (currentScope) { + if (isa(currentScope)) { + break; + } + currentScope = currentScope->getParentOp(); + } + if (failed(getScopeType(currentScope, currentIsCube, currentIsVector))) { + LDBG("Current forOp is not in a valid cube or vector scope!"); + return {-1, -1}; + } + + // Find the other side scope's mainloop and collect its ifOps + SmallVector otherSideIfOps; + DenseMap otherSideIfOpIndexMap; + scf::ForOp otherSideForOp = + findOtherSideMainloopAndIfOps(forOp, currentIsCube, currentIsVector, + otherSideIfOps, otherSideIfOpIndexMap); + + if (!otherSideForOp) { + LDBG("Failed to find other side mainloop or its ifOps!"); + return {-1, -1}; + } + + // Check if the first ifOp has any consumer op in crossDeps + // If not, it means the current compute block executes first, need to subtract + // 1 from requiredBuffers + bool runFirst = isRunFirst(ifOps, crossDeps); + + // Iterate all cross-core dependencies + for (auto &entry : crossDeps) { + Operation *consumerOp = entry.first; // Consumer operation + for (SmallVector &producerOps : entry.second) { + int x = producerOps.size(); // Producer op count (one buffer has + // two ops in different scope) + // some special buffer is not Symmetrical + if (producerOps.size() == 1) { + x = 1; + } + + // Find the IfOp index that consumer belongs to (comsumerIdx) + int comsumerIdx = getConsumerIfOpIndex(consumerOp, ifOps, ifOpIndex); + if (comsumerIdx == -1) { + return {-1, -1}; + } + + // Find producer's position in the other side's ifOps (producerIdx) + int producerIdx = getProducerIfOpIndex(producerOps, otherSideIfOps, + otherSideIfOpIndexMap); + if (producerIdx == -1) { + return {-1, -1}; + } + + // If consumer is after producer (comsumerIdx >= producerIdx), calculate + // required buffer count comsumerIdx - producerIdx + 1 represents the + // buffer count needed to cover this distance If the current compute + // block executes first (firstIfOp has no consumer), subtract 1 + if (comsumerIdx < producerIdx) { + // case : C1 -> V1V2V3 -> C2 + // in this case comsumerIdx < producerIdx, crossDeps hard to process, do + // not change the loop iteration times + LDBG("there is complex case!"); + return {1, 1}; + } + int requiredBuffers = comsumerIdx - producerIdx + 1; + if (runFirst) { + requiredBuffers = requiredBuffers - 1; + } + LDBG("consumer : " << *consumerOp); + LDBG("consumer comsumerIdx: " << comsumerIdx + << ", producerIdx: " << producerIdx); + LDBG("requiredBuffers: " << requiredBuffers); + LDBG("buffer: " << x); + LDBG("runFirst: " << runFirst); + + // Update max value using fraction comparison to avoid precision issues + // Comparing requiredBuffers/maxX vs maxRequiredBuffers/x is equivalent to + // comparing requiredBuffers * maxX vs maxRequiredBuffers * x + if (requiredBuffers * maxX > maxRequiredBuffers * x) { + maxRequiredBuffers = requiredBuffers; + maxX = x; + } + } + } + + return {maxRequiredBuffers, maxX}; +} + +// calculateIterDepsFactor computes the buffer factor based on iteration +// dependencies For iter deps: consumer and producer are IfOps within the same +// forOp Core idea: iteration dependencies are special - consume first, then +// produce So if consumer ifOp (comsumerIdx) and producer ifOp (producerIdx) +// have dependency, we need (producerIdx - comsumerIdx) buffers to support the +// loop extension This function iterates all dependencies from +// tensorIterArgDepsMap and finds the maximum required buffer count +std::pair UpdateLoopIterTimesPass::calculateIterDepsFactor( + scf::ForOp forOp, SmallVector &ifOps, + DenseMap &ifOpIndex) { + int maxRequiredBuffers = 1; + int maxX = 1; + + // Check if this forOp has iteration dependencies + if (!info->tensorIterArgDepsMap.count(forOp)) { + return {maxRequiredBuffers, maxX}; + } + + auto &iterArgDepsVec = info->tensorIterArgDepsMap[forOp]; + + // Iterate all tensor iter_args dependencies in this forOp + // tensorIterArgDepsMap now stores a SmallVector of TensorIterArgIfOpRelation + for (TensorIterArgIfOpRelation &relation : iterArgDepsVec) { + scf::IfOp producerIfOp = relation.producer; + llvm::SmallVector &consumerIfOps = relation.consumers; + + // Skip if no producer or consumers + if (!producerIfOp || consumerIfOps.empty()) { + continue; + } + + // x is the number of producer buffers (1 for iteration dependencies) + int x = 1; + + // Get producer IfOp index (producerIdx) + auto producerIt = ifOpIndex.find(producerIfOp.getOperation()); + if (producerIt == ifOpIndex.end()) { + LDBG("Producer IfOp not found in ifOps list!"); + return {-1, -1}; + } + int producerIdx = producerIt->second; + + // For each consumer IfOp, find its index (comsumerIdx) + // Calculate requiredBuffers = producerIdx - comsumerIdx (consume first, + // then produce) + for (scf::IfOp consumerIfOp : consumerIfOps) { + // Find consumer IfOp index in ifOps list + auto consumerIt = ifOpIndex.find(consumerIfOp.getOperation()); + if (consumerIt == ifOpIndex.end()) { + LDBG("Consumer IfOp not found in ifOps list!"); + return {-1, -1}; + } + int comsumerIdx = consumerIt->second; + + if (producerIdx <= comsumerIdx) { + LDBG("Producer IfOp index (producerIdx) is not greater than consumer " + "IfOp index " + "(comsumerIdx)!"); + LDBG("arg value: " << relation.iterArg); + LDBG("Producer IfOp index producerIdx: " << producerIdx); + LDBG("consumer IfOp index comsumerIdx: " << comsumerIdx); + return {-1, -1}; + } + int requiredBuffers = producerIdx - comsumerIdx + 1; + LDBG("consumerIfOp : " << consumerIfOp); + LDBG("consumer comsumerIdx: " << comsumerIdx + << ", producerIdx: " << producerIdx); + LDBG("requiredBuffers: " << requiredBuffers); + + if (requiredBuffers * maxX > maxRequiredBuffers * x) { + maxRequiredBuffers = requiredBuffers; + maxX = x; + } + } + } + + return {maxRequiredBuffers, maxX}; +} + +// Part 1: Compute new loop upper bound and create arithmetic ops +Value UpdateLoopIterTimesPass::computeNewLoopUpperBound( + OpBuilder &builder, Location loc, scf::ForOp forOp, int ifCount, + int requiredBuffers, int x) { + Value originalLowerBound = forOp.getLowerBound(); + Value originalUpperBound = forOp.getUpperBound(); + Value originalStep = forOp.getStep(); + Type ubType = originalStep.getType(); + + // Helper lambda to create constant value based on type + auto createConstant = [&](int val) -> Value { + if (ubType.isIndex()) { + return builder.create(loc, val); + } else if (auto intType = dyn_cast(ubType)) { + return builder.create(loc, intType, val); + } else { + auto indexVal = builder.create(loc, val); + return builder.create(loc, ubType, indexVal); + } + }; + + // Helper lambda to create ceil division op based on type + auto createCeilDiv = [&](Value lhs, Value rhs) -> Value { + if (ubType.isIndex()) { + return builder.create(loc, lhs, rhs); + } else if (auto intType = dyn_cast(ubType)) { + if (intType.isSigned()) { + return builder.create(loc, lhs, rhs); + } else { + return builder.create(loc, lhs, rhs); + } + } else { + return builder.create(loc, lhs, rhs); + } + }; + + // Create constant values + Value ifCountValue = createConstant(ifCount); + Value requiredBuffersValue = createConstant(requiredBuffers); + Value xValue = createConstant(x); + + // Calculate: rangeDiff = upperBound - lowerBound + Value rangeDiff = builder.create(loc, originalUpperBound, + originalLowerBound); + + // Calculate: iterCount = ceil(rangeDiff / step) + Value iterCount = createCeilDiv(rangeDiff, originalStep); + + // Calculate: scaledIterCount = iterCount * requiredBuffers + Value scaledIterCount = + builder.create(loc, iterCount, requiredBuffersValue); + + // Calculate: ceiledScaledIterCount = ceil(scaledIterCount / x) + Value ceiledScaledIterCount = createCeilDiv(scaledIterCount, xValue); + + // Calculate: newIterCount = ceiledScaledIterCount + ifCount + Value newIterCount = + builder.create(loc, ceiledScaledIterCount, ifCountValue); + + // Calculate: totalSteps = step * newIterCount + Value totalSteps = + builder.create(loc, originalStep, newIterCount); + + // Calculate: newUpperBound = lowerBound + totalSteps + Value newUpperBound = + builder.create(loc, originalLowerBound, totalSteps); + + return newUpperBound; +} + +// Part 2: Clone for loop with new upper bound +scf::ForOp UpdateLoopIterTimesPass::cloneForOpWithNewUpperBound( + OpBuilder &builder, Location loc, scf::ForOp oldForOp, Value newUpperBound, + IRMapping &mapper) { + Value originalLowerBound = oldForOp.getLowerBound(); + Value originalStep = oldForOp.getStep(); + + SmallVector newInitArgs(oldForOp.getInitArgs().begin(), + oldForOp.getInitArgs().end()); + + auto newForOp = builder.create( + loc, originalLowerBound, newUpperBound, originalStep, newInitArgs); + + // Copy attributes from old forOp + for (auto &attr : oldForOp->getAttrs()) { + newForOp->setAttr(attr.getName(), attr.getValue()); + } + + // Map induction variable and iter args + mapper.map(oldForOp.getInductionVar(), newForOp.getInductionVar()); + + for (auto [oldArg, newArg] : + llvm::zip(oldForOp.getRegionIterArgs(), newForOp.getRegionIterArgs())) { + mapper.map(oldArg, newArg); + } + + Block *oldBlock = oldForOp.getBody(); + Block *newBlock = newForOp.getBody(); + + // Replace block arguments + unsigned totalArgs = oldBlock->getNumArguments(); + for (unsigned i = 0; i < totalArgs; ++i) { + BlockArgument oldArg = oldBlock->getArgument(i); + BlockArgument newArg = newBlock->getArgument(i); + oldArg.replaceAllUsesWith(newArg); + } + + Operation *oldTerminator = oldBlock->getTerminator(); + + // Clone operations in the loop body + builder.setInsertionPointToStart(newBlock); + for (Operation &op : + llvm::make_early_inc_range(oldBlock->without_terminator())) { + builder.clone(op, mapper); + } + + // Create new yield operation + auto oldYield = cast(oldTerminator); + SmallVector newYieldOperands; + for (unsigned i = 0; i < oldYield.getNumOperands(); ++i) { + newYieldOperands.push_back(mapper.lookupOrDefault(oldYield.getOperand(i))); + } + + builder.setInsertionPointToEnd(newBlock); + builder.create(loc, newYieldOperands); + + // Handle results replacement + unsigned numOriginalResults = oldForOp.getNumResults(); + if (numOriginalResults > 0) { + SmallVector originalResults; + for (unsigned i = 0; i < numOriginalResults; ++i) { + originalResults.push_back(newForOp.getResult(i)); + } + oldForOp.replaceAllUsesWith(originalResults); + } + + return newForOp; +} + +// Part 3: Update cntArgs mapping after cloning +int UpdateLoopIterTimesPass::updateCntArgsAfterClone( + scf::ForOp oldForOp, IRMapping &mapper, + SmallVector &ifOpsInThisFor) { + // Update cntArgs for each old ifOp + for (scf::IfOp oldIfOp : ifOpsInThisFor) { + scf::IfOp newIfOp = dyn_cast(mapper.lookupOrDefault(oldIfOp)); + if (!newIfOp) + continue; + + Value oldCntVal = info->cntArgs[oldIfOp]; + Value newCntVal = mapper.lookupOrDefault(oldCntVal); + LDBG("updating CntArgs After Clone..."); + LDBG("oldCntVal: " << oldCntVal); + LDBG("newCntVal: " << newCntVal); + + // Remove old entry and add new entry + info->cntArgs.erase(oldIfOp); + info->cntArgs[newIfOp] = newCntVal; + } + return 0; +} + +// Extend for loop iteration count +scf::ForOp UpdateLoopIterTimesPass::extendForOpIterationCount( + scf::ForOp oldForOp, int ifCount, int requiredBuffers, int x, + IRMapping &mapper, SmallVector &ifOpsInThisFor) { + OpBuilder builder(oldForOp); + Location loc = oldForOp.getLoc(); + + // Part 1: Compute new loop upper bound + Value newUpperBound = computeNewLoopUpperBound(builder, loc, oldForOp, + ifCount, requiredBuffers, x); + if (!newUpperBound) { + LDBG("computeNewLoopUpperBound failed!"); + return nullptr; + } + + // Part 2: Clone for loop with new upper bound + scf::ForOp newForOp = cloneForOpWithNewUpperBound(builder, loc, oldForOp, + newUpperBound, mapper); + if (!newForOp) { + LDBG("cloneForOpWithNewUpperBound failed!"); + return nullptr; + } else { + LDBG("cloneForOpWithNewUpperBound Success!"); + LDBG("new ForOp: " << newForOp); + LDBG("old ForOp: " << oldForOp); + } + + // Part 3: Update cntArgs mapping + if (updateCntArgsAfterClone(oldForOp, mapper, ifOpsInThisFor) != 0) { + LDBG("updateCntArgsAfterClone failed!"); + return nullptr; + } + + return newForOp; +} + +// Update WhileOp condition based on ifblock conditions +// Uses info->whileBlockArgMap: {WhileOp: {block_id: {new_arg_idx: +// old_arg_idx}}} For each IfOp in WhileOp, get its block_id Get new args and +// mapping from info->whileBlockArgMap Copy entire beforeRegion and replace old +// args with new args Combine all ifblock conditions with OR operation +int UpdateLoopIterTimesPass::UpdateWhileLoopCondition( + DenseMap> &mainLoopIdMap) { + int ret = 0; + + for (auto &entry : mainLoopIdMap) { + for (Operation *loopOp : entry.second) { + if (!isa(loopOp)) { + continue; + } + + auto whileOp = dyn_cast(loopOp); + if (!info->whileBlockArgMap.count(whileOp)) { + LDBG("WhileOp not found in whileBlockArgMap!"); + return -1; + } + auto &blockArgMap = info->whileBlockArgMap[whileOp]; + if (blockArgMap.empty()) { + LDBG("blockArgMap is empty for WhileOp!"); + return -1; + } + + // Get the 'before' region which contains the condition + Region &beforeRegion = whileOp.getBefore(); + Block &beforeBlock = beforeRegion.front(); + Operation *terminator = beforeBlock.getTerminator(); + if (!terminator || !isa(terminator)) { + LDBG("Before block has no valid ConditionOp terminator!"); + return -1; + } + auto conditionOp = dyn_cast(terminator); + Value originalCondition = conditionOp.getCondition(); + OpBuilder builder(conditionOp); + Location loc = whileOp.getLoc(); + + // Collect all operations to clone + SmallVector opsToClone; + for (Operation &op : beforeBlock.without_terminator()) { + opsToClone.push_back(&op); + } + + // Traverse all IfOps with ssbuffer.if and + // build condition for each IfOp directly + Value combinedCondition; + whileOp.walk([&](scf::IfOp ifOp) { + if (!ifOp->hasAttr(CVPipeline::kIf)) { + return WalkResult::advance(); + } + + auto blockIdAttr = ifOp->getAttrOfType(CVPipeline::kIf); + if (!blockIdAttr) { + ret = -1; + return WalkResult::interrupt(); + } + int blockId = blockIdAttr.getInt(); + if (!blockArgMap.count(blockId)) { + ret = -1; + LDBG("blockId " << blockId << " not found in blockArgMap!"); + return WalkResult::interrupt(); + } + + auto &argMap = blockArgMap[blockId]; // {new_arg_idx: old_arg_idx} + + // Step 1: Build arg replacement mapping + IRMapping mapper; + for (auto &argEntry : argMap) { + int newArgIdx = argEntry.first; + int oldArgIdx = argEntry.second; + + BlockArgument oldArg = beforeBlock.getArgument(oldArgIdx); + BlockArgument newArg = beforeBlock.getArgument(newArgIdx); + + mapper.map(oldArg, newArg); + } + + // Step 2: Clone entire beforeRegion + // Set insertion point before conditionOp + builder.setInsertionPoint(conditionOp); + + // Clone all operations + Value newCondition; + for (Operation *op : opsToClone) { + Operation *clonedOp = builder.clone(*op, mapper); + + // If this op defines the original condition, get the corresponding + // result + if (op->getResultTypes().size() > 0) { + for (unsigned i = 0; i < op->getNumResults(); ++i) { + if (op->getResult(i) == originalCondition) { + newCondition = clonedOp->getResult(i); + break; + } + } + } + } + + if (!newCondition) { + ret = -1; + LDBG("Failed to create new condition for blockId: " << blockId); + return WalkResult::interrupt(); + } + + // Step 3: Combine with existing conditions using OR + if (!combinedCondition) { + combinedCondition = newCondition; + } else { + combinedCondition = builder.create( + loc, combinedCondition, newCondition); + } + + return WalkResult::advance(); + }); + + // Step 4: Update the condition in the before block + if (combinedCondition) { + conditionOp.getConditionMutable().assign(combinedCondition); + LDBG("Updated WhileOp condition for: " << whileOp); + } else { + LDBG("Failed to generate valid condition!"); + return -1; + } + } + } + + return ret; +} + +// Traverse each mainloop, find ifOp with ssbuffer.if attribute inside, +// and replace the mainloop's induction variable with the counter in cntArgs +int UpdateLoopIterTimesPass::replaceForOpCounterInIfOps() { + int ret = 0; + // Traverse all mainloops in the module + getOperation().walk([&](Operation *op) { + if (op->hasAttr(CVPipeline::kMainLoop)) { + // Skip WhileOp, only process ForOp in this step + if (isa(op)) { + LDBG("Skip WhileOp in replaceForOpCounterInIfOps."); + return WalkResult::advance(); + } + + auto forOp = dyn_cast(op); + if (!forOp) { + ret = -1; + LDBG("Do not support other mainloop except forOp!"); + return WalkResult::interrupt(); + } + + Value indVar = forOp.getInductionVar(); + + // Find all ifOps with ssbuffer.if attribute inside this mainloop + forOp.walk([&](scf::IfOp ifOp) { + if (ifOp->hasAttr(CVPipeline::kIf)) { + if (!info->cntArgs.count(ifOp)) { + LDBG("ifblock has no counter in cntArgs"); + ret = -1; + return WalkResult::interrupt(); + } + + Value cntVal = info->cntArgs[ifOp]; + + // Replace all uses of induction variable in the ifOp with cntVal + ifOp.walk([&](Operation *op) { + for (OpOperand &operand : op->getOpOperands()) { + if (operand.get() == indVar) { + operand.set(cntVal); + } + } + }); + } + return mlir::WalkResult::advance(); + }); + } + return mlir::WalkResult::advance(); + }); + return ret; +} + +// step1: Get mainloop id to loop operation mapping, separated into cube and +// vector +int UpdateLoopIterTimesPass::GetMainLoopIdToLoopOpMap( + ModuleOp module, DenseMap> &cmap, + DenseMap> &vmap) { + cmap.clear(); + vmap.clear(); + int ret = 0; + + module.walk([&](scope::ScopeOp scopeOp) { + // Determine if it's CUBE or VECTOR + bool isCube = false; + bool isVector = false; + if (failed(getScopeType(scopeOp, isCube, isVector))) { + ret = -1; + LDBG("mlir do not processed by split mix kernel!"); + return mlir::WalkResult::interrupt(); + } + + // Walk for loops inside the scope + scopeOp.walk([&](Operation *op) { + if (op->hasAttr(CVPipeline::kMainLoop)) { + // Support both ForOp and WhileOp for mainloop + bool isValidLoop = isa(op) || isa(op); + if (!isValidLoop) { + ret = -1; + LDBG("mainloop must be ForOp or WhileOp!"); + return mlir::WalkResult::interrupt(); + } + + auto mainLoopId = op->getAttrOfType(CVPipeline::kMainLoop); + if (mainLoopId) { + int id = mainLoopId.getInt(); + if (isCube) { + cmap[id].push_back(op); + } else if (isVector) { + vmap[id].push_back(op); + } + } + } + return mlir::WalkResult::advance(); + }); + return mlir::WalkResult::advance(); + }); + + return ret; +} + +// Input: loopMap: {mainloop_id: [loopOp1, loopOp2], ...} +// Output: infoMap: {loopOp: extraIterationTimesInfo, ...} +int UpdateLoopIterTimesPass::ComputeMainLoopTimes( + DenseMap> &loopMap, + DenseMap &infoMap) { + for (auto &entry : loopMap) { + for (Operation *loopOp : entry.second) { + if (isa(loopOp)) { + continue; + } + + scf::ForOp forOp = dyn_cast(loopOp); + if (!forOp) { + LDBG("currently only support forOp!"); + return -1; + } + + // Collect if operations in this loop + IterationTimesInfo iterInfo; + if (info->cntArgs.empty()) { + LDBG("cntArgs is empty, no ifblock is contained!"); + return -1; + } + for (auto &[ifOp, cntVal] : info->cntArgs) { + if (ifOp->hasAttr(CVPipeline::kIf)) { + auto parentOp = ifOp->getParentOp(); + if (parentOp->hasAttr(CVPipeline::kMainLoop) && + isa(parentOp)) { + if (parentOp == forOp.getOperation()) { + iterInfo.ifCount++; + iterInfo.ifOpsInThisFor.push_back(ifOp); + } + } else { + LDBG("Get wrong ifblock structure: ifblock's parentOp is not " + "mainloop!"); + LDBG("ifblock Op: " << ifOp); + LDBG("Parent Op: " << *parentOp); + return -1; + } + } else { + LDBG("ifOp in cntArgs does not contain ssbuffer.if Attribute\n ifOp: " + << ifOp); + return -1; + } + } + + // Calculate factor + auto [requiredBuffers, x] = calculateFactor(forOp); + if (requiredBuffers == -1 || x == -1) { + LDBG("calculateFactor failed!"); + return -1; + } + iterInfo.requiredBuffers = requiredBuffers; + iterInfo.x = x; + infoMap[loopOp] = iterInfo; + } + } + return 0; +} + +int UpdateLoopIterTimesPass::collectForOpsAndUpdateMax( + DenseMap> &map, int id, + SmallVector &allForOps, int &maxIfCount, + int &maxRequiredBuffers, int &maxX, + DenseMap &infoMap) { + if (map.count(id)) { + for (Operation *loopOp : map[id]) { + if (!isa(loopOp)) { + continue; + } + + allForOps.push_back(loopOp); + if (infoMap.count(loopOp)) { + IterationTimesInfo &iterInfo = infoMap[loopOp]; + if (iterInfo.ifCount > maxIfCount) { + maxIfCount = iterInfo.ifCount; + } + // requiredBuffers/x > maxRequiredBuffers/maxX = + // requiredBuffers * maxX > maxRequiredBuffers * x + if (iterInfo.requiredBuffers * maxX > maxRequiredBuffers * iterInfo.x) { + maxRequiredBuffers = iterInfo.requiredBuffers; + maxX = iterInfo.x; + } + } else { + LDBG("mainloop Op: " << *loopOp << " do not include in infoMap!"); + return -1; + } + } + } + return 0; +} + +void UpdateLoopIterTimesPass::updateMainLoopMaps( + Operation *oldForOp, Operation *newForOp, + DenseMap> &cmap, + DenseMap> &vmap, + DenseMap &infoMap) { + // Update cmap + for (auto &entry : cmap) { + for (Operation *&op : entry.second) { + if (op == oldForOp) { + op = newForOp; + } + } + } + // Update vmap + for (auto &entry : vmap) { + for (Operation *&op : entry.second) { + if (op == oldForOp) { + op = newForOp; + } + } + } + // Update infoMap + auto it = infoMap.find(oldForOp); + if (it != infoMap.end()) { + IterationTimesInfo iterInfo = std::move(it->second); + infoMap.erase(it); + infoMap[newForOp] = std::move(iterInfo); + } +} + +int UpdateLoopIterTimesPass::UpdateForLoopIteration( + DenseMap> &cmap, + DenseMap> &vmap, + DenseMap &infoMap) { + int ret = 0; + // Collect all mainloop ids + DenseSet allIds; + for (auto &entry : cmap) { + allIds.insert(entry.first); + } + for (auto &entry : vmap) { + allIds.insert(entry.first); + } + + // For each mainloop id, calculate max values and update all related loops + SmallVector allForOps; + for (int id : allIds) { + int maxIfCount = 0; + int maxRequiredBuffers = 1; + int maxX = 1; + + // Collect all loops with same id from cmap and vmap + SmallVector sameIdForOps; + ret = collectForOpsAndUpdateMax(cmap, id, sameIdForOps, maxIfCount, + maxRequiredBuffers, maxX, infoMap); + if (ret != 0) + return -1; + ret = collectForOpsAndUpdateMax(vmap, id, sameIdForOps, maxIfCount, + maxRequiredBuffers, maxX, infoMap); + if (ret != 0) + return -1; + + // Update all loops with this id using the same max values + for (Operation *loopOp : sameIdForOps) { + if (maxIfCount == 0) { + LDBG("no ifblock in mainloop!"); + return -1; + } + + scf::ForOp oldForOp = dyn_cast(loopOp); + if (!oldForOp) { + LDBG("do not surpport other loop op except forOp!"); + return -1; + } + + IterationTimesInfo &iterInfo = infoMap[loopOp]; + IRMapping mapper; + scf::ForOp newForOp = + extendForOpIterationCount(oldForOp, maxIfCount, maxRequiredBuffers, + maxX, mapper, iterInfo.ifOpsInThisFor); + if (!newForOp) { + LDBG("extendForOpIterationCount failed!"); + return -1; + } + updateMainLoopMaps(loopOp, newForOp.getOperation(), cmap, vmap, infoMap); + allForOps.push_back(loopOp); + } + } + + // Delete old forOp after cntArgs and maps have been updated + for (Operation *loopOp : allForOps) { + if (!loopOp) { + LDBG("erasing error: loopOp is nullptr, there are nested mainloop!"); + return -1; + } + loopOp->erase(); + } + return 0; +} + +void UpdateLoopIterTimesPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("before updateloopitertimes:\n" << module); + LDBG("\nEnter UpdateLoopIterTimesPass!"); + + int ret = 0; + // step1: Get mainloop id to loop operation mapping, separated into cube and + // vector + DenseMap> cmap; + DenseMap> vmap; + ret = GetMainLoopIdToLoopOpMap(module, cmap, vmap); + if (ret != 0) { + LDBG("GetMainLoopIdToLoopOpMap Failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + + // step2: Calculate info for each loop operation, store into the same + // iterationTimesinfoMap + DenseMap infoMap; + ret = ComputeMainLoopTimes(cmap, infoMap); + if (ret != 0) { + LDBG("ComputeMainLoopTimes from cube Failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + ret = ComputeMainLoopTimes(vmap, infoMap); + if (ret != 0) { + LDBG("ComputeMainLoopTimes from vector Failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + + // step3: Update loop iteration count (process loop operations with same id + // from both cmap and vmap) + ret = UpdateForLoopIteration(cmap, vmap, infoMap); + if (ret != 0) { + LDBG("Update ForLoop Iteration Failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + LDBG("after UpdateForLoopIteration:\n" << module); + + // step4: Replace loop counter by if blocks' counter + ret = replaceForOpCounterInIfOps(); + if (ret != 0) { + LDBG("replaceForOpCounterInIfOps Failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + + // step5: Update WhileOp condition based on ifblock conditions + ret = UpdateWhileLoopCondition(cmap); + if (ret != 0) { + LDBG("UpdateWhileLoopCondition from cube Failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + ret = UpdateWhileLoopCondition(vmap); + if (ret != 0) { + LDBG("UpdateWhileLoopCondition from vector Failed!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + + LDBG("after updateloopitertimes:\n" << module); + LDBG("\nExit UpdateLoopIterTimes pass."); +} + +namespace mlir { +namespace triton { +std::unique_ptr> createUpdateLoopIterTimesPass() { + return std::make_unique(); +} +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopOps.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopOps.cpp new file mode 100644 index 0000000000..0f91812028 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopOps.cpp @@ -0,0 +1,738 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/UpdateLoopOps.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "UpdateLoopOps"; +static constexpr int kPipeSFlagId = 15; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace mlir; +using namespace triton; +using namespace hivm; +using namespace CVPipeline; + +// Derive block counters from ssbuffer.if for the main-loop op (scf.for or +// scf.while with ssbuffer.main_loop). blockCounterNums keyed on Operation*. +LogicalResult UpdateLoopOpsPass::deriveBlockCountersFromIfOps( + ModuleOp module, ControlFlowConditionInfo *info) { + if (!info) { + LDBG("[Error]: info is null"); + return failure(); + } + + module.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + + int numIfBlockIds = countUniqueIfBlockIds(op); + if (numIfBlockIds > 0) { + info->blockCounterNums[op] = numIfBlockIds; + } + return WalkResult::advance(); + }); + + return success(); +} + +// Creates new scf.for with extras and migrates old body. Yields each new +// iter_arg forward (counts on themselves, deps/tensors via buildNewYieldOp). +static scf::ForOp +createForOpAndMigrateBody(scf::ForOp oldForOp, + const llvm::SmallVector &extraInitArgs) { + scf::ForOp newForOp = createNewForOpWithExtras(oldForOp, extraInitArgs); + if (newForOp == oldForOp) { + return oldForOp; + } + + Block *oldBlock = oldForOp.getBody(); + Block *newBlock = newForOp.getBody(); + migrateBody(oldBlock, newBlock); + + // Append extra iter_arg yields (new block args at [1+oldNumArgs, +numExtra)). + unsigned oldNumArgs = oldForOp.getNumRegionIterArgs(); + llvm::SmallVector extras; + extras.reserve(extraInitArgs.size()); + for (size_t i = 0; i < extraInitArgs.size(); ++i) { + extras.push_back(newBlock->getArgument(1 + oldNumArgs + i)); + } + if (failed(buildNewYieldOp(oldBlock, newBlock, newForOp, extras))) { + newForOp.erase(); + return scf::ForOp(); + } + + return newForOp; +} + +// Splices the new main-loop op in place of the old one: type-checks results, +// redirects external uses to new results, erases old op. +static LogicalResult replaceMainLoopOpUsesAndErase(Operation *oldOp, + Operation *newOp) { + if (oldOp->getNumResults() > 0) { + for (unsigned i = 0; i < oldOp->getNumResults(); ++i) { + if (oldOp->getResult(i).getType() != newOp->getResult(i).getType()) { + LDBG("[Error]: main_loop op result type mismatch at index " << i); + return failure(); + } + } + } + + replaceOpResultUses(oldOp, newOp); + oldOp->erase(); + return success(); +} + +// Appends the new iter_args (new before-block args at +// [numOriginal..numOriginal+numExtra-1]) to the scf.condition's forwarded +// values. +static void extendWhileCondition(scf::ConditionOp oldCond, Block *newBefore, + unsigned numOriginalIterArgs, + unsigned numExtraArgs) { + llvm::SmallVector newCondValues(oldCond.getArgs().begin(), + oldCond.getArgs().end()); + for (unsigned i = 0; i < numExtraArgs; ++i) { + newCondValues.push_back(newBefore->getArgument(numOriginalIterArgs + i)); + } + oldCond.getArgsMutable().assign(newCondValues); +} + +// Appends new iter_args (new after-block args at +// [numOriginal..numOriginal+numExtra-1]) to scf.yield. Counters forward +// themselves. +static void extendWhileYield(scf::YieldOp oldYield, Block *newAfter, + unsigned numOriginalIterArgs, + unsigned numExtraArgs) { + llvm::SmallVector newYieldOperands(oldYield.getOperands().begin(), + oldYield.getOperands().end()); + for (unsigned i = 0; i < numExtraArgs; ++i) { + newYieldOperands.push_back(newAfter->getArgument(numOriginalIterArgs + i)); + } + oldYield.getResultsMutable().assign(newYieldOperands); +} + +// Migrates both before/after regions: migrateBody args+ops, move terminator, +// extend with new iter_args. +static LogicalResult migrateWhileRegions(scf::WhileOp oldWhileOp, + scf::WhileOp newWhileOp, + unsigned numOriginalIterArgs, + unsigned numExtraArgs) { + Block *oldBefore = oldWhileOp.getBeforeBody(); + Block *newBefore = newWhileOp.getBeforeBody(); + migrateBody(oldBefore, newBefore); + auto oldCond = cast(oldBefore->getTerminator()); + oldCond->moveBefore(newBefore, newBefore->end()); + extendWhileCondition(oldCond, newBefore, numOriginalIterArgs, numExtraArgs); + + Block *oldAfter = oldWhileOp.getAfterBody(); + Block *newAfter = newWhileOp.getAfterBody(); + migrateBody(oldAfter, newAfter); + auto oldYield = cast(oldAfter->getTerminator()); + oldYield->moveBefore(newAfter, newAfter->end()); + extendWhileYield(oldYield, newAfter, numOriginalIterArgs, numExtraArgs); + + return success(); +} + +// Creates new scf.while with extras, then migrates old before/after regions. +// condition/yield forward new iter_args (mirroring forOp layout). +static scf::WhileOp +createWhileOpAndMigrateBody(scf::WhileOp oldWhileOp, + const llvm::SmallVector &extraInitArgs) { + scf::WhileOp newWhileOp = + createNewWhileOpWithExtras(oldWhileOp, extraInitArgs); + if (newWhileOp == oldWhileOp) { + return oldWhileOp; + } + + unsigned numOriginalIterArgs = oldWhileOp.getInits().size(); + if (failed(migrateWhileRegions(oldWhileOp, newWhileOp, numOriginalIterArgs, + extraInitArgs.size()))) { + newWhileOp.erase(); + return scf::WhileOp(); + } + + return newWhileOp; +} + +// Dispatches createForOpAndMigrateBody / createWhileOpAndMigrateBody by op +// type. Returns nullptr if op is neither scf.for nor scf.while. +static Operation * +createMainLoopOpAndMigrateBody(Operation *oldLoopOp, + const llvm::SmallVector &extraInitArgs) { + if (auto forOp = dyn_cast(oldLoopOp)) + return createForOpAndMigrateBody(forOp, extraInitArgs); + if (auto whileOp = dyn_cast(oldLoopOp)) + return createWhileOpAndMigrateBody(whileOp, extraInitArgs); + return nullptr; +} + +// Computes extra-arg counts (block counters / inner dep conds / tensor +// iter_args) for `oldLoopOp`. Returns depsVecCopy so the caller can move it +// into info later. +static void computeMainLoopExtraArgs( + Operation *oldLoopOp, ControlFlowConditionInfo *info, int &numBlockCounters, + int &numInnerDepConds, int &numTensorIterArgs, + llvm::SmallVector &depsVecCopy) { + numBlockCounters = info->blockCounterNums.lookup(oldLoopOp); + numInnerDepConds = info->intraCoreDependentMap.count(oldLoopOp) + ? (int)info->intraCoreDependentMap[oldLoopOp].size() + : 0; + + numTensorIterArgs = 0; + auto tensorIterArgDepsIt = info->tensorIterArgDepsMap.find(oldLoopOp); + if (tensorIterArgDepsIt != info->tensorIterArgDepsMap.end()) { + depsVecCopy = tensorIterArgDepsIt->second; + for (auto &entry : depsVecCopy) { + numTensorIterArgs += entry.consumers.size(); + } + } +} + +// Returns the index of the first extra iter_arg in the new op. ForOp excludes +// IV; WhileOp shares arg layout between before/after. +static unsigned getMainLoopBaseIdx(Operation *oldLoopOp, bool isWhile) { + return isWhile ? (unsigned)cast(oldLoopOp).getInits().size() + : cast(oldLoopOp).getNumRegionIterArgs(); +} + +// Appends initial values for extra iter_args: +// - block counters: forOp reuses lowerBound; whileOp creates a fresh i32(0) +// per counter (distinct SSA values) +// - inner dep conds: i32(0) +// - tensor iter_args: i32(1) +static void buildMainLoopExtraInitArgs( + OpBuilder &builder, Location loc, Value forOpLowerBound, bool isWhile, + int numBlockCounters, int numInnerDepConds, int numTensorIterArgs, + llvm::SmallVector &extraInitArgs) { + for (int i = 0; i < numBlockCounters; ++i) { + if (isWhile) { + extraInitArgs.push_back(builder.create( + loc, builder.getI32Type(), builder.getI32IntegerAttr(0))); + } else { + extraInitArgs.push_back(forOpLowerBound); + } + } + for (int i = 0; i < numInnerDepConds; ++i) { + extraInitArgs.push_back(builder.create( + loc, builder.getI32Type(), builder.getI32IntegerAttr(0))); + } + for (int i = 0; i < numTensorIterArgs; ++i) { + extraInitArgs.push_back(builder.create( + loc, builder.getI32Type(), builder.getI32IntegerAttr(1))); + } +} + +// Migrates block-counter / inner-dep-cond index ranges from oldLoopOp to newOp. +static void recordMainLoopBlockCountersAndConds( + Operation *oldLoopOp, Operation *newOp, ControlFlowConditionInfo *info, + unsigned baseIdx, int numBlockCounters, int numInnerDepConds) { + if (numBlockCounters > 0) { + llvm::SmallVector indices; + for (int j = 0; j < numBlockCounters; ++j) + indices.push_back(baseIdx + j); + info->blockCounters.erase(oldLoopOp); + info->blockCounters[newOp] = indices; + } + + if (numInnerDepConds > 0) { + llvm::SmallVector indices; + for (int j = 0; j < numInnerDepConds; ++j) + indices.push_back(baseIdx + numBlockCounters + j); + info->innerDepConds.erase(oldLoopOp); + info->innerDepConds[newOp] = indices; + } +} + +// Migrates tensor iter_arg index ranges and moves depsVecCopy into +// info->tensorIterArgDepsMap. +static void recordMainLoopTensorIterArgs( + Operation *oldLoopOp, Operation *newOp, ControlFlowConditionInfo *info, + unsigned baseIdx, int numBlockCounters, int numInnerDepConds, + int numTensorIterArgs, + llvm::SmallVector &depsVecCopy) { + if (numTensorIterArgs == 0) { + return; + } + unsigned tensorBaseIdx = baseIdx + numBlockCounters + numInnerDepConds; + auto &newIndicesMap = info->tensorIterArgIndicesMap[newOp]; + + unsigned currentIdx = tensorBaseIdx; + for (auto &entry : depsVecCopy) { + llvm::SmallVector indices; + for (int j = 0; j < (int)entry.consumers.size(); ++j) { + indices.push_back(currentIdx++); + } + newIndicesMap[entry.iterArg] = indices; + } + + info->tensorIterArgIndicesMap.erase(oldLoopOp); + info->tensorIterArgDepsMap[newOp] = std::move(depsVecCopy); + info->tensorIterArgDepsMap.erase(oldLoopOp); +} + +// Transfers intraCoreDependentMap (and WhileOp-only whileBlockArgMap) from +// oldLoopOp to newOp so the maps survive the old op being erased. +static void transferMainLoopInfoMaps(Operation *oldLoopOp, Operation *newOp, + ControlFlowConditionInfo *info, + bool isWhile) { + if (info->intraCoreDependentMap.count(oldLoopOp)) { + info->intraCoreDependentMap[newOp] = info->intraCoreDependentMap[oldLoopOp]; + info->intraCoreDependentMap.erase(oldLoopOp); + } + if (isWhile) { + auto oldWhileOp = cast(oldLoopOp); + auto newWhileOp = cast(newOp); + if (info->whileBlockArgMap.count(oldWhileOp)) { + info->whileBlockArgMap[newWhileOp] = + std::move(info->whileBlockArgMap[oldWhileOp]); + info->whileBlockArgMap.erase(oldWhileOp); + } + } +} + +// Extends scf.for / scf.while with iter_args for block counters, inner dep +// conds, and tensor iter_args (seeded from maps). +static LogicalResult +extendMainLoopOpWithExtraArgs(Operation *oldLoopOp, + ControlFlowConditionInfo *info) { + int numBlockCounters, numInnerDepConds, numTensorIterArgs; + llvm::SmallVector depsVecCopy; + computeMainLoopExtraArgs(oldLoopOp, info, numBlockCounters, numInnerDepConds, + numTensorIterArgs, depsVecCopy); + + int totalExtraArgs = numBlockCounters + numInnerDepConds + numTensorIterArgs; + if (totalExtraArgs == 0) { + return success(); + } + + // ForOp block-counter init uses lowerBound (single Value reused for every + // counter); WhileOp creates a fresh arith.constant per counter so each new + // iter_arg has a distinct SSA value (expected by tests and downstream + // passes). + OpBuilder builder(oldLoopOp); + Value forOpLowerBound; + bool isWhile = false; + if (auto forOp = dyn_cast(oldLoopOp)) { + forOpLowerBound = forOp.getLowerBound(); + } else if (isa(oldLoopOp)) { + isWhile = true; + } else { + LDBG("[Error]: main_loop op is neither scf::ForOp nor scf::WhileOp"); + return failure(); + } + + llvm::SmallVector extraInitArgs; + buildMainLoopExtraInitArgs(builder, oldLoopOp->getLoc(), forOpLowerBound, + isWhile, numBlockCounters, numInnerDepConds, + numTensorIterArgs, extraInitArgs); + + Operation *newOp = createMainLoopOpAndMigrateBody(oldLoopOp, extraInitArgs); + if (!newOp) { + return failure(); + } + + unsigned baseIdx = getMainLoopBaseIdx(oldLoopOp, isWhile); + recordMainLoopBlockCountersAndConds(oldLoopOp, newOp, info, baseIdx, + numBlockCounters, numInnerDepConds); + recordMainLoopTensorIterArgs(oldLoopOp, newOp, info, baseIdx, + numBlockCounters, numInnerDepConds, + numTensorIterArgs, depsVecCopy); + transferMainLoopInfoMaps(oldLoopOp, newOp, info, isWhile); + + return replaceMainLoopOpUsesAndErase(oldLoopOp, newOp); +} + +// Add block counter and inner dep cond iter args to for ops (and whileOps). +// Processed from info-driven `mainLoopOpsToProcess` set (keyed on Operation*). +LogicalResult UpdateLoopOpsPass::addBlockCountersAndInnerDepConds( + ModuleOp module, ControlFlowConditionInfo *info) { + llvm::DenseSet mainLoopOpsToProcess; + + for (auto &p : info->blockCounterNums) { + if (p.second < 0) { + LDBG("[Error]: invalid blockCounterNum " << p.second); + return failure(); + } + mainLoopOpsToProcess.insert(p.first); + } + for (auto &p : info->tensorIterArgDepsMap) { + mainLoopOpsToProcess.insert(p.first); + } + + for (Operation *loopOp : mainLoopOpsToProcess) { + if (failed(extendMainLoopOpWithExtraArgs(loopOp, info))) + return failure(); + } + + return success(); +} + +// Insert sync ops inside a main-loop body (scf.for or scf.while after-region): +// wait at start, set before yield. Body block is resolved via getMainLoopBody. +static LogicalResult +insertSyncOpsInsideMainLoop(Block *loopBody, Location loc, + hivm::TCoreTypeAttr coreType, PipeAttr setPipe, + PipeAttr waitPipe, int waitFlagId, int setFlagId) { + Operation *forTerminator = loopBody->getTerminator(); + if (!forTerminator) { + return failure(); + } + + // Insert wait at loop body start + OpBuilder insertionBuilder(&loopBody->front()); + auto waitFlagAttr = insertionBuilder.getIntegerAttr( + insertionBuilder.getI64Type(), waitFlagId); + insertionBuilder.create(loc, coreType, setPipe, waitPipe, + waitFlagAttr); + + // Insert set before yield + OpBuilder setBuilder(forTerminator); + auto setFlagAttr = + setBuilder.getIntegerAttr(setBuilder.getI64Type(), setFlagId); + setBuilder.setInsertionPoint(forTerminator); + setBuilder.create(loc, coreType, setPipe, waitPipe, + setFlagAttr); + + return success(); +} + +// Insert a sync op (SET before, or WAIT after) outside a main-loop op. +// op-agnostic. `isBefore`: true -> SET, false -> WAIT. +static LogicalResult +insertSyncOpsOutsideMainLoop(Operation *loopOp, Location loc, + hivm::TCoreTypeAttr coreType, PipeAttr setPipe, + PipeAttr waitPipe, int flagId, bool isBefore) { + OpBuilder builder(loopOp); + auto flagAttr = builder.getIntegerAttr(builder.getI64Type(), flagId); + if (isBefore) { + builder.create(loc, coreType, setPipe, waitPipe, flagAttr); + } else { + builder.setInsertionPointAfter(loopOp); + builder.create(loc, coreType, setPipe, waitPipe, flagAttr); + } + return success(); +} + +// Returns the loop body block for a main-loop op. scf.for has the single +// region; scf.while uses the after region (where the actual loop body lives). +static Block *getMainLoopBody(Operation *loopOp) { + if (auto whileOp = dyn_cast(loopOp)) { + return &whileOp.getAfter().front(); + } + return &loopOp->getRegion(0).front(); +} + +// Insert PIPE_S for a main_loop op (scf.for or scf.while) by loop/scope type. +// Body resolved via getMainLoopBody — scf.while uses the after region block. +static LogicalResult +insertPipeSForMainLoopOp(Operation *loopOp, scope::ScopeOp scopeOp, + bool isScopeCube, bool isScopeVector, PipeAttr setPipe, + PipeAttr waitPipe, int flagId) { + Block *loopBody = getMainLoopBody(loopOp); + Location loc = loopOp->getLoc(); + bool isVectorFirst = loopOp->hasAttr(CVPipeline::kVectorFirst); + auto cubeType = + hivm::TCoreTypeAttr::get(loopOp->getContext(), hivm::TCoreType::CUBE); + auto vectorType = + hivm::TCoreTypeAttr::get(loopOp->getContext(), hivm::TCoreType::VECTOR); + + if (isVectorFirst) { + if (isScopeCube) { + // vector_first + CUBE: before loop op (SET), inside (WAIT/SET) + if (failed(insertSyncOpsOutsideMainLoop(loopOp, loc, cubeType, setPipe, + waitPipe, flagId, true))) { + return failure(); + } + if (failed(insertSyncOpsInsideMainLoop(loopBody, loc, cubeType, setPipe, + waitPipe, flagId, flagId))) { + return failure(); + } + } else if (isScopeVector) { + // vector_first + VECTOR: inside (WAIT/SET), after loop op (WAIT) + if (failed(insertSyncOpsInsideMainLoop(loopBody, loc, vectorType, setPipe, + waitPipe, flagId, flagId))) { + return failure(); + } + if (failed(insertSyncOpsOutsideMainLoop(loopOp, loc, vectorType, setPipe, + waitPipe, flagId, false))) { + return failure(); + } + } + } else { + // cube_first (including default when neither attribute is present) + if (isScopeCube) { + // cube_first + CUBE: inside (WAIT/SET), after loop op (WAIT) + if (failed(insertSyncOpsInsideMainLoop(loopBody, loc, cubeType, setPipe, + waitPipe, flagId, flagId))) { + return failure(); + } + if (failed(insertSyncOpsOutsideMainLoop(loopOp, loc, cubeType, setPipe, + waitPipe, flagId, false))) { + return failure(); + } + } else if (isScopeVector) { + // cube_first + VECTOR: before loop op (SET), inside (WAIT/SET) + if (failed(insertSyncOpsOutsideMainLoop(loopOp, loc, vectorType, setPipe, + waitPipe, flagId, true))) { + return failure(); + } + if (failed(insertSyncOpsInsideMainLoop(loopBody, loc, vectorType, setPipe, + waitPipe, flagId, flagId))) { + return failure(); + } + } + } + return success(); +} + +LogicalResult UpdateLoopOpsPass::insertInterCorePipeS(ModuleOp module) { + auto cubeCoreType = + hivm::TCoreTypeAttr::get(module.getContext(), hivm::TCoreType::CUBE); + auto vectorCoreType = + hivm::TCoreTypeAttr::get(module.getContext(), hivm::TCoreType::VECTOR); + auto setPipeType = PipeAttr::get(module.getContext(), hivm::PIPE::PIPE_S); + auto waitPipeType = PipeAttr::get(module.getContext(), hivm::PIPE::PIPE_S); + + WalkResult result = module.walk([&](scope::ScopeOp scopeOp) -> WalkResult { + auto scopeTypeAttr = + scopeOp->getAttrOfType("hivm.tcore_type"); + if (!scopeTypeAttr) { + return WalkResult::advance(); + } + + bool isScopeCube = (scopeTypeAttr == cubeCoreType); + bool isScopeVector = (scopeTypeAttr == vectorCoreType); + + // Walk both scf.for and scf.while with ssbuffer.main_loop; both need + // same PIPE_S sync. insertPipeSForMainLoopOp dispatches via + // getMainLoopBody. + WalkResult innerResult = scopeOp.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + if (failed(insertPipeSForMainLoopOp(op, scopeOp, isScopeCube, + isScopeVector, setPipeType, + waitPipeType, kPipeSFlagId))) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (innerResult.wasInterrupted()) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + return result.wasInterrupted() ? failure() : success(); +} + +// Analyzes producer/consumer relationship between tensor iter_args in +// main_loop and ssbuffer.if. Downstream only acts on forOp (whileOp future +// use). +LogicalResult UpdateLoopOpsPass::analyzeTensorIterArgDependencies( + ModuleOp module, ControlFlowConditionInfo *info) { + bool failed = false; + module.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + + LDBG("Analyzing main_loop op: " << op); + + // Get iter_args to analyze. forOp: getRegionIterArgs() (body args - IV). + // whileOp: getAfterArguments() (before-block args aren't visible in body). + llvm::SmallVector iterArgsVec = MainLoop(op).getIterArgs(); + + for (auto iterArg : iterArgsVec) { + if (!mlir::isa(iterArg.getType())) { + continue; + } + + LDBG("Found tensor type iter_arg: " << iterArg); + scf::IfOp producerIfOp = nullptr; + llvm::SmallVector consumerIfOps; + + for (auto &use : iterArg.getUses()) { + Operation *user = use.getOwner(); + scf::IfOp ifOp = nullptr; + Operation *curr = user; + while (curr && curr != op) { + if (auto currIf = dyn_cast(curr)) { + if (currIf->hasAttr(CVPipeline::kIf)) { + ifOp = currIf; + break; + } + } + curr = curr->getParentOp(); + } + + if (!ifOp) { + LDBG("Use of tensor iter_arg " + << iterArg << " is not inside any ssbuffer.if op."); + continue; + } + + // Only the direct terminator of this ssbuffer.if is a producer. + // Nested if/for/while yields that forward iter_arg are consumers. + bool isProducer = isa(user) && + user->getParentOp() == ifOp.getOperation(); + + // Check and update status of ifOp + if (isProducer) { + if (producerIfOp && producerIfOp != ifOp) { + // Found a different producer ifOp! This is an error. + LDBG("[Error]: tensor iter_arg " + << iterArg << " has multiple different producers!"); + LDBG("Existing producer: " << producerIfOp); + LDBG("New producer: " << ifOp); + failed = true; + return WalkResult::interrupt(); + } + if (!producerIfOp) { + // First producer, or upgrade from consumer + auto it = llvm::find(consumerIfOps, ifOp); + if (it != consumerIfOps.end()) { + consumerIfOps.erase(it); + LDBG("This ifOp was consumer, now updated to producer: " << ifOp); + } else { + LDBG("Found producer ifOp (first time): " << ifOp); + } + producerIfOp = ifOp; + } + // Else: already is this producer, do nothing + } else { + // isConsumer + if (producerIfOp == ifOp) { + // Already a producer, even if current use is consumer, do nothing + continue; + } + if (!llvm::is_contained(consumerIfOps, ifOp)) { + consumerIfOps.push_back(ifOp); + LDBG("Found consumer ifOp (first time): " << ifOp << "\n"); + } + // Else: already a consumer, do nothing + } + } + // Check: must have both producers AND consumers + if (!producerIfOp || consumerIfOps.empty()) { + LDBG("tensor iter_arg " << iterArg << " has only " + << (!producerIfOp ? "consumers" : "producers") + << ", skipped"); + continue; + } + TensorIterArgIfOpRelation relation; + relation.iterArg = iterArg; + relation.producer = producerIfOp; + relation.consumers = consumerIfOps; + + // Record into the map (keyed on Operation*; both scf.for and scf.while + // participate). + info->tensorIterArgDepsMap[op].push_back(relation); + LDBG("Recorded tensor iter_arg dependency: " + << iterArg << " has 1 producer, " << relation.consumers.size() + << " consumers"); + } + + return WalkResult::advance(); + }); + + return failed ? failure() : success(); +} + +void UpdateLoopOpsPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("before updateLoopOps:\n" << module); + + // Use provided info, or create a local one if not available + ControlFlowConditionInfo localInfo; + ControlFlowConditionInfo *infoToUse = info ? info : &localInfo; + + // Analyze the dependencies of the tensor type iter_args in the main_loop with + // the ssbuffer.if ops + if (failed(analyzeTensorIterArgDependencies(module, infoToUse))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Derive block counters from ssbuffer.if if blockCounterNums is empty + if (infoToUse->blockCounterNums.empty()) { + if (failed(deriveBlockCountersFromIfOps(module, infoToUse))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + } + + // Update for/while ops iter_args for block counters and inner dep conds. + // forOp from info-driven sets (no-ops when empty); whileOp unconditionally. + if (infoToUse && + (failed(addBlockCountersAndInnerDepConds(module, infoToUse)))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Insert PIPE_S inter-core synchronization + if (failed(insertInterCorePipeS(module))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Dump whileBlockArgMap (whileOp -> block_id -> (new_arg_idx -> old_arg_idx)) + // after all whileOp replacements; verify it survived + // replaceMainLoopOpUsesAndErase. + dumpWhileBlockArgMap(infoToUse->whileBlockArgMap, + "whileBlockArgMap contents after updateLoopOps " + "(whileOp -> block_id -> (new_arg_idx -> old_arg_idx))"); + + LDBG("after updateLoopOps:\n" << module); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createUpdateLoopOpsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/Utils.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/Utils.cpp new file mode 100644 index 0000000000..64109efdc5 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddControlFlowCondition/Utils.cpp @@ -0,0 +1,455 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "llvm/Support/Debug.h" +#include + +static constexpr const char *DEBUG_TYPE = "AddControlFlowConditionUtils"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +namespace mlir { +using namespace llvm; +using namespace CVPipeline; + +// Collect all nested ops within an operation's regions +LogicalResult collectAllNestedOps(Operation *op, + llvm::DenseSet ®ionOps) { + if (!op) { + return failure(); + } + + if (regionOps.contains(op)) { + return success(); + } + + regionOps.insert(op); + for (Region ®ion : op->getRegions()) { + for (Block &block : region) { + for (Operation &nestedOp : block) { + if (failed(collectAllNestedOps(&nestedOp, regionOps))) { + return failure(); + } + } + } + } + + return success(); +} + +// Group operations by their block_id attribute. `op` must be scf.for or +// scf.while (see MainLoop::getBody). +LogicalResult +collectOpsByBlockId(Operation *op, + llvm::DenseMap> &blockOps) { + Block *bodyBlock = MainLoop(op).getBody(); + if (!bodyBlock) { + return failure(); + } + + for (Operation &op : bodyBlock->without_terminator()) { + if (auto attr = op.getAttrOfType(CVPipeline::kBlockId)) { + blockOps[attr.getInt()].push_back(&op); + } else { + return failure(); + } + } + + return success(); +} + +// DFS for topological sort - returns failure if cycle detected +static LogicalResult +dfsTopologicalSort(Operation *op, llvm::DenseSet &visited, + llvm::DenseSet &inStack, + const llvm::DenseSet &ops, + llvm::DenseMap *opOrder, + SmallVectorImpl &sorted) { + if (!op) { + return success(); + } + if (inStack.contains(op)) { + return failure(); + } + if (visited.contains(op)) { + return success(); + } + + visited.insert(op); + inStack.insert(op); + + SmallVector deps; + for (Value operand : op->getOperands()) { + if (Operation *def = operand.getDefiningOp()) { + if (ops.contains(def)) { + deps.push_back(def); + } + } + } + + if (opOrder && !opOrder->empty()) { + llvm::sort(deps, [&](Operation *a, Operation *b) { + auto itA = opOrder->find(a); + auto itB = opOrder->find(b); + if (itA == opOrder->end() || itB == opOrder->end()) { + return false; + } + return itA->second < itB->second; + }); + } + + for (Operation *dep : deps) { + if (failed( + dfsTopologicalSort(dep, visited, inStack, ops, opOrder, sorted))) { + return failure(); + } + } + + inStack.erase(op); + sorted.push_back(op); + return success(); +} + +// Topological sort of operations based on operand dependencies +LogicalResult topologicalSort(llvm::DenseSet &ops, + llvm::DenseMap *opOrder, + SmallVectorImpl &sorted) { + llvm::DenseSet visited; + llvm::DenseSet inStack; + + SmallVector opList(ops.begin(), ops.end()); + if (opOrder && !opOrder->empty()) { + llvm::sort(opList, [&](Operation *a, Operation *b) { + return (*opOrder)[a] < (*opOrder)[b]; + }); + } + + for (Operation *op : opList) { + if (failed( + dfsTopologicalSort(op, visited, inStack, ops, opOrder, sorted))) { + return failure(); + } + } + return success(); +} + +LogicalResult topologicalSort(SmallVector &ops) { + llvm::DenseSet opSet(ops.begin(), ops.end()); + SmallVector sorted; + + if (succeeded(topologicalSort(opSet, nullptr, sorted))) { + ops.assign(sorted.begin(), sorted.end()); + return success(); + } + return failure(); +} + +// Get block_ids in order of appearance in the main-loop body (forOp body +// or whileOp after-region body). Returns empty if `op` is neither. +SmallVector getBlockIdsInOrder(Operation *op) { + Block *bodyBlock = MainLoop(op).getBody(); + if (!bodyBlock) { + return {}; + } + + SmallVector idsInOrder; + llvm::DenseSet seenIds; + + for (Operation &op : bodyBlock->without_terminator()) { + if (auto blockIdAttr = + op.getAttrOfType(CVPipeline::kBlockId)) { + int id = blockIdAttr.getInt(); + if (seenIds.insert(id).second) { + idsInOrder.push_back(id); + } + } + } + return idsInOrder; +} + +// Get block_id of the immediate child of main-loop op (scf.for or scf.while +// carrying ssbuffer.main_loop) containing op. For scf.while "body" is the +// after-region block. +std::optional getLoopDirectChildBlockId(Operation *op) { + if (!op) { + return std::nullopt; + } + Operation *parent = op->getParentOp(); + while (parent) { + if (CVPipeline::isMainLoopOp(parent)) { + return CVPipeline::getOpBlockId(op); + } + op = parent; + parent = parent->getParentOp(); + } + return std::nullopt; +} + +// Counts unique ssbuffer.if values inside a main-loop op (scf.for or +// scf.while), walking all nested ops. Returns 0 if none. +int countUniqueIfBlockIds(Operation *loopOp) { + llvm::DenseSet ifBlockIds; + loopOp->walk([&](Operation *innerOp) { + if (auto ifAttr = innerOp->getAttrOfType(CVPipeline::kIf)) { + ifBlockIds.insert(ifAttr.getInt()); + } + }); + return static_cast(ifBlockIds.size()); +} + +// Find the tcb group id that contains value v +int findTcbGroupId( + Value v, + llvm::DenseMap> &tightlyCoupledBufferGroups) { + for (auto &tcbEntry : tightlyCoupledBufferGroups) { + if (llvm::is_contained(tcbEntry.second, v)) { + return tcbEntry.first; + } + } + return -1; +} + +// Get isCube/isVector from scopeOp's tcore_type attribute. +// Returns failure if attribute is missing or not CUBE/VECTOR. +LogicalResult getScopeType(Operation *scopeOp, bool &isCube, bool &isVector) { + isCube = false; + isVector = false; + + if (!scopeOp->hasAttr(CVPipeline::kTcoreType)) { + return failure(); + } + + auto attr = scopeOp->getAttr(CVPipeline::kTcoreType); + auto aiCAttr = + hivm::TCoreTypeAttr::get(scopeOp->getContext(), hivm::TCoreType::CUBE); + auto aiVAttr = + hivm::TCoreTypeAttr::get(scopeOp->getContext(), hivm::TCoreType::VECTOR); + if (attr == aiCAttr) { + isCube = true; + } else if (attr == aiVAttr) { + isVector = true; + } + + if (!isCube && !isVector) { + return failure(); + } + + return success(); +} + +// Check if op is a scf.if whose body only contains hivm.hir.sync_block_wait, +// hivm.hir.sync_block_set and hivm.fixpipe ops (excluding terminators). +bool isIfOpWithOnlySyncOps(Operation *op) { + auto ifOp = dyn_cast(op); + if (!ifOp) { + return false; + } + + WalkResult result = ifOp->walk([&](Operation *innerOp) -> WalkResult { + if (innerOp == op) { + return WalkResult::advance(); + } + if (innerOp->hasTrait()) { + return WalkResult::advance(); + } + if (!isa(innerOp) && + !isa(innerOp) && !isa(innerOp)) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + return !result.wasInterrupted(); +} + +// Migrate ops from oldBlock to newBlock; replaceAllUsesWith on oldBlock's args +// to newBlock's args (same index). +void migrateBody(Block *oldBlock, Block *newBlock) { + for (unsigned i = 0; i < oldBlock->getNumArguments(); ++i) { + oldBlock->getArgument(i).replaceAllUsesWith(newBlock->getArgument(i)); + } + + for (Operation &op : + llvm::make_early_inc_range(oldBlock->without_terminator())) { + op.moveBefore(newBlock, newBlock->end()); + } +} + +// Migrate both before and after regions of a scf.while op. Does not touch +// terminators (the caller builds new condition/yield in the new regions). +void migrateWhileBodies(scf::WhileOp oldWhileOp, scf::WhileOp newWhileOp) { + migrateBody(oldWhileOp.getBeforeBody(), newWhileOp.getBeforeBody()); + migrateBody(oldWhileOp.getAfterBody(), newWhileOp.getAfterBody()); +} + +// Build new scf.yield at end of `newBlock`: copies oldBlock's yield operands, +// appends `extraYieldValues`, creates new scf::YieldOp, erases old yield. +LogicalResult buildNewYieldOp(Block *oldBlock, Block *newBlock, + Operation *newOp, + ArrayRef extraYieldValues) { + auto oldYield = cast(oldBlock->getTerminator()); + SmallVector yieldOperands; + for (unsigned i = 0; i < oldYield.getNumOperands(); ++i) { + yieldOperands.push_back(oldYield.getOperand(i)); + } + for (Value v : extraYieldValues) { + yieldOperands.push_back(v); + } + OpBuilder builder = OpBuilder::atBlockEnd(newBlock); + builder.create(newOp->getLoc(), yieldOperands); + oldYield.erase(); + return success(); +} + +// Replace all uses of `oldOp`'s results with `newOp`'s matching results. +// No-op when `oldOp` is result-less. +void replaceOpResultUses(Operation *oldOp, Operation *newOp) { + if (oldOp->getNumResults() == 0) + return; + + SmallVector newResults; + for (unsigned i = 0; i < oldOp->getNumResults(); ++i) { + newResults.push_back(newOp->getResult(i)); + } + oldOp->replaceAllUsesWith(newResults); +} + +// Build new scf.condition in `newWhileOp`'s before region. Condition preserved +// from `whileOp`; forwarded values = new before-block args (incl. extras). +void buildNewWhileCondition(scf::WhileOp whileOp, scf::WhileOp newWhileOp) { + auto oldCond = whileOp.getConditionOp(); + Value origCond = oldCond.getCondition(); + + OpBuilder beforeBuilder(newWhileOp.getBeforeBody(), + newWhileOp.getBeforeBody()->end()); + SmallVector forwardedValues; + for (BlockArgument arg : newWhileOp.getBeforeArguments()) { + forwardedValues.push_back(arg); + } + beforeBuilder.create(whileOp.getLoc(), origCond, + forwardedValues); + oldCond.erase(); +} + +// Creates a new scf.for with `extraInitArgs` appended to the original init +// args. Returns `oldForOp` unchanged when `extraInitArgs` is empty. +scf::ForOp createNewForOpWithExtras(scf::ForOp oldForOp, + ArrayRef extraInitArgs) { + if (extraInitArgs.empty()) { + return oldForOp; + } + + OpBuilder builder(oldForOp); + SmallVector newInitArgs(oldForOp.getInitArgs().begin(), + oldForOp.getInitArgs().end()); + llvm::append_range(newInitArgs, extraInitArgs); + + scf::ForOp newForOp = builder.create( + oldForOp.getLoc(), oldForOp.getLowerBound(), oldForOp.getUpperBound(), + oldForOp.getStep(), newInitArgs); + + for (auto &attr : oldForOp->getAttrs()) { + newForOp->setAttr(attr.getName(), attr.getValue()); + } + return newForOp; +} + +// Creates a new scf.while with `extraInitArgs` appended to the original inits +// and empty before/after blocks. Returns `oldWhileOp` unchanged when empty. +scf::WhileOp createNewWhileOpWithExtras(scf::WhileOp oldWhileOp, + ArrayRef extraInitArgs) { + if (extraInitArgs.empty()) { + return oldWhileOp; + } + + OpBuilder builder(oldWhileOp); + + SmallVector newInits(oldWhileOp.getInits().begin(), + oldWhileOp.getInits().end()); + llvm::append_range(newInits, extraInitArgs); + + SmallVector newResultTypes(oldWhileOp->getResultTypes().begin(), + oldWhileOp->getResultTypes().end()); + for (Value v : extraInitArgs) { + newResultTypes.push_back(v.getType()); + } + + scf::WhileOp newWhileOp = builder.create( + oldWhileOp.getLoc(), newResultTypes, newInits); + + for (auto &attr : oldWhileOp->getAttrs()) { + newWhileOp->setAttr(attr.getName(), attr.getValue()); + } + + SmallVector argTypes; + argTypes.reserve(newInits.size()); + for (Value v : newInits) { + argTypes.push_back(v.getType()); + } + SmallVector argLocs(newInits.size(), oldWhileOp.getLoc()); + + builder.createBlock(&newWhileOp.getBefore(), {}, argTypes, argLocs); + builder.createBlock(&newWhileOp.getAfter(), {}, argTypes, argLocs); + + return newWhileOp; +} + +// Dispatches createNewForOpWithExtras / createNewWhileOpWithExtras by op type. +// Returns nullptr if `oldOp` is neither scf.for nor scf.while. +Operation *createMainLoopOpWithExtras(Operation *oldOp, + ArrayRef extraInitArgs) { + if (auto forOp = dyn_cast(oldOp)) { + return createNewForOpWithExtras(forOp, extraInitArgs); + } + if (auto whileOp = dyn_cast(oldOp)) { + return createNewWhileOpWithExtras(whileOp, extraInitArgs); + } + return nullptr; +} + +// Prints whileBlockArgMap (whileOp -> block_id -> (new_arg_idx -> +// old_arg_idx)) to the debug stream, gated by LLVM_DEBUG. +void dumpWhileBlockArgMap(const triton::WhileBlockArgMap &map, + llvm::StringRef header) { + LLVM_DEBUG({ + LDBG("[INFO]: " << header); + for (auto &[whileOp, blockArgMap] : map) { + LDBG(" whileOp @" << whileOp->getLoc()); + for (auto &[blockId, argIdxMap] : blockArgMap) { + for (auto &[newArgIdx, oldArgIdx] : argIdxMap) { + LDBG(" block_id=" << blockId << " new_arg_idx=" << newArgIdx + << " -> old_arg_idx=" << oldArgIdx); + } + } + } + }); +} + +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AddDynamicCVPipeline.cpp b/third_party/ascend/lib/DynamicCVPipeline/AddDynamicCVPipeline.cpp new file mode 100644 index 0000000000..cc0075a245 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AddDynamicCVPipeline.cpp @@ -0,0 +1,199 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Debug.h" + +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/DialectRegistry.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Support/WalkResult.h" +#include "llvm/Support/Debug.h" +#include +#include + +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" +#include "ascend/include/DynamicCVPipeline/AllocMultiCache.h" +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlockPass.h" +#include "ascend/include/DynamicCVPipeline/PreCheckAvailable.h" +#include "ascend/include/DynamicCVPipeline/RemoveAttributes.h" +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromComputePass.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflowPass.h" +#include "ascend/include/DynamicCVPipeline/StandardizeOp.h" + +#include "DynamicCVPipeline/Common/FallbackHelper.h" + +static constexpr const char *DEBUG_TYPE = "add-dynamic-cv-pipeline"; +static constexpr unsigned MAX_RETRY_TIMES = 2; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(X) LLVM_DEBUG(DBGS() << (X) << "\n") + +namespace mlir { +namespace triton { +#define GEN_PASS_DEF_ADDDYNAMICCVPIPELINE +#include "ascend/include/DynamicCVPipeline/Passes.h.inc" +} // namespace triton +} // namespace mlir + +static std::optional getErrorCode(ModuleOp moduleOp) { + auto errCodeAttr = + moduleOp->getAttrOfType(CVPipeline::ERRCODE_ATTR); + return errCodeAttr ? std::optional(errCodeAttr.getInt()) + : std::nullopt; +} + +static inline void addPasses(OpPassManager &pm) { + pm.addPass(createPreCheckAvailablePass()); + pm.addPass(createStandardizeOpPass()); + pm.addPass(createPlanComputeBlockPass()); + pm.addPass(createComputeBlockOptPass()); + pm.addPass(createSplitDataflowPass()); + pm.addPass(createAnalyzeDataFlowPass()); + pm.addPass(createAllocMultiCachePass()); + pm.addPass(createAddControlFlowConditionPass()); + pm.addPass(createSeparateMemoryFromComputePass()); + pm.addPass(createRemoveSsbufAttrPass()); +} + +// must collect all sub-passes since they now are not added to pipeline +void AddDynamicCVPipelinePass::getDependentDialects( + DialectRegistry ®istry) const { + Base::getDependentDialects(registry); + OpPassManager tempPM(ModuleOp::getOperationName()); + addPasses(tempPM); + tempPM.getDependentDialects(registry); +} + +AddDynamicCVPipelinePass::AddDynamicCVPipelinePass( + const AddDynamicCVPipelineOptions &options) + : AddDynamicCVPipelineBase(options) {} + +void AddDynamicCVPipelinePass::runOnOperation() { + auto moduleOp = getOperation(); + OpBuilder builder(moduleOp.getContext()); + compileOn91095Flag = this->compileOn91095; + + LDBG("Enter pass"); + moduleOp->removeAttr(CVPipeline::ERRCODE_ATTR); + + if (!compileOn91095Flag) { + llvm::errs() << "Add-dynamic-cv-pipeline is only supported on 91095 now.\n"; + return; + } + + ScopedDiagnosticHandler handler(&getContext(), [&](Diagnostic &diag) { + LLVM_DEBUG({ + // In debug mode, continue to other handlers, i.e. print to stderr + return llvm::failure(); + }); + + // otherwise prohibit any other handler + return llvm::success(); + }); + + bool tuplePreloadFailed = false; + for (unsigned attempt = 0; attempt < MAX_RETRY_TIMES; ++attempt) { + // restore() consumes the saved region bodies. Each attempt needs its own + // snapshot, taken before changing buffer counts for the retry. + CVPipeline::FallbackHelper fallback(moduleOp); + if (attempt > 0) { + BufferCountManager bufferCountManager(moduleOp); + bufferCountManager.setBufferCount(BufferCountManager::DepType::IntraCore, + 2); + bufferCountManager.setBufferCount(BufferCountManager::DepType::InterCore, + 1); + } + + // Do not reuse pass instances or partially transformed IR on retry. + PassManager pm(&getContext(), moduleOp.getOperationName()); + addPasses(pm); + + // run passes in separate pm, instead of the pipeline to suppress reproducer + auto result = pm.run(moduleOp); + auto errCode = getErrorCode(moduleOp); + if (succeeded(result) && !errCode.has_value()) { + if (tuplePreloadFailed) { + CVPipeline::setFallbackAttr(moduleOp, + CVPipeline::ERRCODE_TUPLE_PRELOAD_FAILED); + } + LDBG("Process successfully"); + return; + } + + if (errCode == CVPipeline::ERRCODE_TUPLE_PRELOAD_FAILED) { + if (attempt + 1 < MAX_RETRY_TIMES) { + LDBG("Tuple-buffer failed; Retrying with tuple preload disabled."); + tuplePreloadFailed = true; + fallback.restore(); + moduleOp->removeAttr(CVPipeline::ERRCODE_ATTR); + continue; + } + // Consume repeated retry requests instead of leaking code 3 to callers. + errCode = CVPipeline::ERRCODE_FAILED; + } + + if (!errCode.has_value()) { + moduleOp->emitWarning() << "[" << DEBUG_TYPE << "] " + << "Unexpected pass failure (no fallback attr " + "set); fallback to compilation without " + "dynamic CV pipeline."; + } else if (errCode == CVPipeline::ERRCODE_IGNORED) { + // This is an expected fallback: do not attach the full module IR. + mlir::emitWarning(moduleOp->getLoc()) + << "[" << DEBUG_TYPE << "] " + << "Kernel not applicable for dynamic CV pipeline " + "(no matmul / already scope-optimized / unsupported " + "pattern); falling back to standard compilation."; + } else { + moduleOp->emitWarning() + << "[" << DEBUG_TYPE << "] " << "Pass failed (errcode=" << *errCode + << "); " + "falling back to compilation without " + "dynamic CV pipeline."; + } + + fallback.restore(); + moduleOp->setAttr(CVPipeline::ERRCODE_ATTR, + builder.getI32IntegerAttr( + errCode.value_or(CVPipeline::ERRCODE_FAILED))); + return; + } + + LDBG("Process successfully"); +} + +std::unique_ptr> +mlir::triton::createAddDynamicCVPipelinePass( + const AddDynamicCVPipelineOptions &options) { + return std::make_unique(options); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache.cpp b/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache.cpp new file mode 100644 index 0000000000..a39b05f21a --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AllocMultiCache.h" +#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.h" +#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +#include "mlir/Pass/PassManager.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "AllocMultiCache"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::outs() << __VA_ARGS__; \ + llvm::outs() << "\n"; \ + }) + +using namespace mlir; +using namespace triton; + +// Run the pass +void AllocMultiCachePass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + OpPassManager pm(module.getOperationName()); + LDBG("Enter pass."); + LDBG("before innerscope:\n" << module << "\n"); + // Step 1:Inner multibuffer + pm.addPass(createAddMultiBufferInnerScopePass()); + + // Step 2: Outer multibuffer + pm.addPass(createAddMultiBufferOuterScopePass()); + + if (failed(runPipeline(pm, module))) { + module->emitError() << "[" << DEBUG_TYPE << "] Pass failed!"; + if (!CVPipeline::hasFallbackAttr(module)) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + return; + } + + LDBG("Process successfully"); + LDBG(llvm::StringRef("after innerscope:\n") << module << "\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAllocMultiCachePass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.cpp b/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.cpp new file mode 100644 index 0000000000..d8f75a406f --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.cpp @@ -0,0 +1,2323 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.h" +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/RegionUtils.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/Support/Debug.h" +#include + +static constexpr const char *DEBUG_TYPE = "AddMultiBufferInnerScope"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(X) LLVM_DEBUG(DBGS() << (X) << "\n") + +using namespace mlir; +using namespace hivm; +using namespace annotation; +using namespace triton; +using namespace CVPipeline; + +using BufferPair = std::pair; +using BufferMap = DenseMap>; + +// Buffer count constants +constexpr int kBufferCountOne = 1; + +namespace mlir { +namespace triton { + +// Collect main_loop loops (forOp / whileOp) in a single block +static int collectMainLoopsInBlock(Block &block, + SmallVector &mainLoops) { + int count = 0; + for (Operation &op : block) { + if (isMainLoopOp(&op)) { + mainLoops.push_back(&op); + count++; + } + } + return count; +} + +// Recursively collect main_loop loops, returns count of collected items +static int collectMainLoopsRecursively(Region ®ion, + SmallVector &mainLoops) { + int totalCount = 0; + for (Block &block : region) { + totalCount += collectMainLoopsInBlock(block, mainLoops); + for (Operation &op : block) { + for (auto &nestedRegion : op.getRegions()) + totalCount += collectMainLoopsRecursively(nestedRegion, mainLoops); + } + } + return totalCount; +} + +struct InnerBlockInfo { + Value blockId; + SmallVector ops; +}; + +static std::optional getOutermostSsbufferId(Operation *op) { + std::optional result; + for (Operation *current = op; current; current = current->getParentOp()) { + if (current->hasAttr(kMainLoop)) + return result.has_value() ? result : -1; + + if (current->getNumRegions() >= 2) + return getOpBlockId(current); + + // Otherwise remember the deepest id seen; the parent walk will + // overwrite it if a closer-to-boundary op carries one. + if (auto curId = getOpBlockId(current); curId.has_value()) + result = curId; + } + return result; +} + +void collectNestedOps(Block *block, SmallVector &ops) { + for (auto &op : *block) { + ops.push_back(&op); + for (auto ®ion : op.getRegions()) { + for (auto &innerBlock : region) { + collectNestedOps(&innerBlock, ops); + } + } + } +} + +// Get priority of forOp (lower value means higher priority) +// Priority order: main_loop (1) > block_id (2) > iter_args (3) > none (0) +// This is used to select the most relevant main loop when multiple candidates +// exist +static int getForOpPriority(scf::ForOp f) { + constexpr int priorityMainLoop = 1; + constexpr int priorityBlockId = 2; + constexpr int priorityIterArgs = 3; + + // Check if forOp itself has main_loop attribute + bool hasMainloop = f->hasAttr(kMainLoop); + bool bodyHasMainloop = false; + bool bodyHasBlockId = false; + + // Check terminator for main_loop and block_id attributes + if (auto *term = f.getBody()->getTerminator()) { + bodyHasMainloop = term->hasAttr(kMainLoop); + bodyHasBlockId = term->getAttrOfType(kBlockId) != nullptr; + } + + bool opHasBlockId = f->getAttrOfType(kBlockId) != nullptr; + bool hasIterArgs = f.getNumResults() > 0 || !f.getInitArgs().empty(); + + if (hasMainloop || bodyHasMainloop) { + return priorityMainLoop; + } + if (opHasBlockId || bodyHasBlockId) { + return priorityIterArgs; + } + if (hasIterArgs) { + return priorityIterArgs; + } + return 0; +} + +// Collect a single dependency value to depValueMap. Same-block check uses +// outermost id so inner ops of a multi-region op (e.g. subview at block 3 +// inside ifOp at block 4) are not treated as cross-block consumers of a +// same-block producer. +// +// i1Found is set to true when the operand is a tensor with element type i1, +// signaling the caller to fall back (set ERRCODE_IGNORED + signalPassFailure) +// rather than process the dep through the multi-buffer pipeline. The operand +// is intentionally NOT added to depValueMap in that case. +// i1 return is done temporarily. + +static void collectDepValue(Value operand, Block *body, Operation *currentOp, + DenseMap &outputToBlockId, + DenseMap> &depValueMap, + Value groupKey, bool &i1Found) { + if (auto barg = dyn_cast(operand)) { + if (barg.getOwner() == body && + !llvm::is_contained(depValueMap[groupKey], barg)) + depValueMap[groupKey].push_back(barg); + return; + } + + if (!outputToBlockId.count(operand)) + return; + + auto currentOutermost = getOutermostSsbufferId(currentOp); + auto operandOutermost = getOutermostSsbufferId(operand.getDefiningOp()); + + if (currentOutermost.has_value() && currentOutermost == operandOutermost) + return; + + // i1 tensor deps: trigger fallback only for cross-block deps that are + // actually about to be multi-buffered. Same-block i1 tensors (e.g. a + // condition operand of an arith.select inside the same block) are + // filtered out by the same-block check above and never enter the + // multi-buffer pipeline, so they do not need the fallback. + if (auto shapedType = dyn_cast(operand.getType())) { + if (shapedType.getElementType().isInteger(1)) { + i1Found = true; + return; + } + } + + if (!llvm::is_contained(depValueMap[groupKey], operand)) + depValueMap[groupKey].push_back(operand); +} + +// Recursively find a nested main_loop (forOp / whileOp) inside `loop`'s body +static Operation *findNestedMainloop(const MainLoop &loop) { + SmallVector allOps; + collectNestedOps(loop.getBody(), allOps); + + for (Operation *op : allOps) { + if (isa(op) && op->hasAttr(kMainLoop)) + return op; + } + return {}; +} + +bool isInsideMainLoopForOp(Operation *op) { + return isMainLoopOp(op->getParentOp()); +} + +bool isInsideMainLoopForOpTraverse(Operation *op) { + for (Operation *parent = op->getParentOp(); parent; + parent = parent->getParentOp()) { + if (isMainLoopOp(parent)) + return true; + } + return false; +} + +// Collect all ops with ssbuffer.id from allOps, grouped by id +// Returns 0=success, -1=invalid negative block ID from upstream pass +static int +groupOpsBySsbufferId(SmallVector &allOps, + llvm::MapVector> &opsById) { + llvm::MapVector opsByValue; + for (Operation *op : allOps) { + auto id = getOpBlockId(op); + if (!id.has_value()) { + continue; + } + for (auto res : op->getResults()) { + opsByValue[res] = op; + } + } + // Deduplicate: a multi-result op (e.g. scf.if) is inserted N times in + // opsByValue and would produce duplicated dep_marks like [1, 1] otherwise. + DenseSet seen; + for (auto &p : opsByValue) { + Operation *op = p.second; + if (!seen.insert(op).second) + continue; + auto id = getOpBlockId(op); + if (!id.has_value()) { + continue; + } + opsById[*id].push_back(op); + } + // Also register ops that carry ssbuffer.block_id but have no results + for (Operation *op : allOps) { + auto id = getOpBlockId(op); + if (!id.has_value() || !op->getResults().empty()) + continue; + if (seen.insert(op).second) + opsById[*id].push_back(op); + } + return 0; +} + +// True when operand is produced by an op with a block_id and lives in a +// different logical block from the consumer (mirrors the same-block check +// in collectDepValue). +static bool +isCrossBlockDepOperand(Operation *consumerOp, Value operand, + const DenseMap &outputToBlockId) { + if (!outputToBlockId.count(operand)) + return false; + auto consumerOutermost = getOutermostSsbufferId(consumerOp); + auto operandOutermost = getOutermostSsbufferId(operand.getDefiningOp()); + return !(consumerOutermost.has_value() && + consumerOutermost == operandOutermost); +} + +// Invoke callback for each cross-block dep operand yielded by a multi-region +// op (e.g. scf.if, scf.while). Skips ops with fewer than 2 regions, empty +// regions, and regions whose terminator is not scf.yield. +static void +forEachYieldedCrossBlockDep(Operation *op, + const DenseMap &outputToBlockId, + llvm::function_ref callback) { + if (op->getNumRegions() < 2) + return; + for (Region ®ion : op->getRegions()) { + if (region.empty()) + continue; + auto yieldOp = dyn_cast(region.back().getTerminator()); + if (!yieldOp) + continue; + for (Value operand : yieldOp->getOperands()) { + if (isCrossBlockDepOperand(op, operand, outputToBlockId)) + callback(operand); + } + } +} + +// Returns 0=success (including normal skip when blocks empty), -1=invalid +// negative block ID Returns 0=success (including normal skip when blocks +// empty), -1=invalid negative block ID. i1Found is set to true when any tensor +// dep collected here has element type i1; the caller is expected to abort and +// trigger fallback in that case. +static int +collectInnerBlockInfo(const MainLoop &loop, + DenseMap &blocks, + DenseMap> &depValueMap, + SmallVector &allOps, bool &i1Found) { + depValueMap.clear(); + Block *body = loop.getBody(); + if (!body) + return 0; + + collectNestedOps(body, allOps); + + llvm::MapVector> opsById; + if (groupOpsBySsbufferId(allOps, opsById) != 0) + return -1; + if (opsById.empty()) + return 0; + + // Build mapping from output to block id + DenseMap outputToBlockId; + for (auto &p : opsById) + for (Operation *op : p.second) + for (auto res : op->getResults()) + outputToBlockId[res] = p.first; + + // Collect dependency values for each block. Inner ops of multi-region ops + // (e.g. scf.if) are included so their scalar deps get tracked; cross-block + // judgment still attributes them to the ifOp via getOutermostSsbufferId. + for (auto &p : opsById) { + Operation *keyOp = nullptr; + for (Operation *op : p.second) { + if (!op->getResults().empty()) { + keyOp = op; + break; + } + } + if (!keyOp) + continue; + Value groupKey = keyOp->getResult(0); + InnerBlockInfo bi; + bi.blockId = groupKey; + bi.ops = p.second; + blocks[groupKey] = bi; + + for (Operation *op : bi.ops) + for (Value operand : op->getOperands()) + collectDepValue(operand, body, op, outputToBlockId, depValueMap, + groupKey, i1Found); + } + + // Additional pass: collect deps from yield ops of multi-region consumers + // (e.g. scf.if), treating the multi-region op as the dep consumer. + for (auto &blockPair : blocks) { + Value blockKey = blockPair.first; + for (Operation *op : blockPair.second.ops) { + forEachYieldedCrossBlockDep(op, outputToBlockId, [&](Value operand) { + if (!llvm::is_contained(depValueMap[blockKey], operand)) + depValueMap[blockKey].push_back(operand); + }); + } + } + + return 0; +} + +// Check if a yieldOp is already processed in blocks +static bool isYieldAlreadyProcessed(scf::YieldOp yieldOp, + DenseMap &blocks) { + for (auto &p : blocks) { + if (llvm::is_contained(p.second.ops, yieldOp.getOperation())) { + return true; + } + } + return false; +} + +// Process yield op that is not in blocks: add parent multi-region op as +// consumer Generic version: supports any op with >= 2 regions (scf.if, +// scf.while, etc.) +static void +processYieldNotInBlocks(scf::YieldOp yieldOp, + DenseMap &blocks, + DenseMap> &depUserMap) { + if (isYieldAlreadyProcessed(yieldOp, blocks)) + return; + + Operation *parentOp = yieldOp->getParentOp(); + // Generic: check if parent op has >= 2 regions + if (!parentOp || parentOp->getNumRegions() < 2) + return; + + for (Value operand : yieldOp->getOperands()) { + // Add parent multi-region op as consumer for yield operands + // This handles cases where depVal is only used in yield (not as direct + // operand) + depUserMap[operand].push_back(parentOp); + } +} + +DenseMap> +buildDepUserMap(DenseMap &blocks, + SmallVector &allOps, + DenseMap> &depValueMap) { + DenseMap> depUserMap; + + // First pass: process operations in blocks + for (auto &p : blocks) + for (Operation *op : p.second.ops) + for (Value operand : op->getOperands()) + depUserMap[operand].push_back(op); + + // Second pass: process yield operations that are not in blocks (e.g., INT_MIN + // block_id) Generic version: supports any multi-region op's yield + for (Operation *op : allOps) { + if (auto yieldOp = dyn_cast(op)) { + processYieldNotInBlocks(yieldOp, blocks, depUserMap); + } + } + + return depUserMap; +} + +// Check if depVal matches the special pattern with linalg::FillOp +static bool isEmptyFillPattern(Value depVal) { + Operation *defOp = depVal.getDefiningOp(); + auto fillOp = dyn_cast(defOp); + if (!fillOp) + return false; + + if (fillOp.getOutputs().empty()) + return false; + + Value outs = fillOp.getOutputs()[0]; + if (!outs) + return false; + Operation *outsDef = outs.getDefiningOp(); + if (!outsDef) + return false; + return isa(outsDef) || + isa(outsDef); +} + +// Check if depVal is the result of a bufferization.alloc_tensor +static bool isAllocTensorPattern(Value depVal) { + return isa_and_nonnull(depVal.getDefiningOp()); +} + +// Check if depVal is the result of a bufferization.to_tensor wrapping a +// freshly-allocated memref that has no data copy landing on it before the +// to_tensor +static bool isAllocToTensorPattern(Value depVal) { + auto toTensorOp = + dyn_cast_or_null(depVal.getDefiningOp()); + if (!toTensorOp) + return false; + Value memref = toTensorOp.getOperand(); + auto allocOp = dyn_cast_or_null(memref.getDefiningOp()); + if (!allocOp) + return false; + + // Scan from allocOp down to (but not including) toTensorOp + bool seenAlloc = false; + for (Operation &op : *allocOp->getBlock()) { + if (&op == allocOp) { + seenAlloc = true; + continue; + } + if (&op == toTensorOp) + break; + if (!seenAlloc) + continue; + for (OpOperand &use : memref.getUses()) { + if (use.getOwner() == &op) + return false; + } + } + return true; +} + +SmallVector +collectBufferValues(DenseMap> &depValueMap, + const DenseSet &clonedDepVals) { + SmallVector valueList; + llvm::DenseSet seenVals; + + for (auto &p : depValueMap) { + for (Value depVal : p.second) { + Operation *op = depVal.getDefiningOp(); + if (!op) + continue; + if (!seenVals.insert(depVal).second) + continue; + + auto shapedType = dyn_cast(depVal.getType()); + if (!shapedType) + continue; + + // Skip tensor::EmptyOp - it should only get dep_mark, not buffer + // allocation + if (isa(op)) + continue; + + // Skip depVals that were already cloned by cloneEmptyFillsInBlocks + if (clonedDepVals.contains(depVal)) + continue; + + // Skip bufferization.alloc_tensor + if (isa(op)) + continue; + + // Skip to_tensor whose operand is a memref.alloc — handled by + // cloneAllocToTensorsInBlocks in Phase 2. + if (isAllocToTensorPattern(depVal)) + continue; + + valueList.push_back(depVal); + } + } + + return valueList; +} + +SmallVector +collectScalarDeps(DenseMap> &depValueMap, + DenseMap> &depUserMap) { + SmallVector scalarValueList; + + for (auto &p : depValueMap) { + for (Value depVal : p.second) { + if (isa(depVal)) + continue; + + Operation *depDefinedOp = depVal.getDefiningOp(); + if (!depDefinedOp) + continue; + + if (isa(depVal.getType())) { + // tensor::EmptyOp should be treated like scalar, add dep_mark + if (!isa(depDefinedOp)) + continue; + // Check if definingOp's parentOp is a main_loop forOp + auto *parentOp = depDefinedOp->getParentOp(); + if (!parentOp || !parentOp->hasAttr(kMainLoop)) + continue; + } + + auto userIt = depUserMap.find(depVal); + if (userIt == depUserMap.end()) + continue; + + auto producerId = getOpBlockId(depDefinedOp); + if (!producerId.has_value()) { + continue; + } + + SmallVector depUsers = userIt->second; + bool hasCrossBlockUser = false; + for (Operation *depUser : depUsers) { + auto userId = getOutermostSsbufferId(depUser); + if (!userId.has_value() || *userId != *producerId) { + hasCrossBlockUser = true; + break; + } + } + + if (hasCrossBlockUser) + scalarValueList.push_back(depVal); + } + } + + return scalarValueList; +} + +// True if op is nested strictly inside the main loop. +static bool isOpInMainLoop(Operation *op, const MainLoop &mainLoop) { + return op && mainLoop.getOperation()->isProperAncestor(op); +} + +// Collect the values an op depends on: its direct operands plus the values its +// nested regions capture from above. +static void collectOpDependencies(Operation *op, SmallVector &deps) { + for (Value v : op->getOperands()) { + deps.push_back(v); + } + if (op->getNumRegions() > 0) { + llvm::SetVector above; + mlir::getUsedValuesDefinedAbove(op->getRegions(), above); + for (Value v : above) { + deps.push_back(v); + } + } +} + +// Depth-first build of the scalar op slice feeding `root`. Recursion stops at +// tensor operands +static void buildScalarSlice(Value root, const MainLoop &mainLoop, + SmallVector &sliceInOrder, + DenseSet &visited, + llvm::SetVector &boundaryTensors) { + Operation *def = root.getDefiningOp(); + if (!def || !isOpInMainLoop(def, mainLoop)) { + return; + } + if (!visited.insert(def).second) { + return; + } + + SmallVector deps; + collectOpDependencies(def, deps); + for (Value dep : deps) { + if (isa(dep.getType())) { + // Tensor boundary: let it travel through the normal tensor path. + boundaryTensors.insert(dep); + continue; + } + Operation *depDef = dep.getDefiningOp(); + if (!depDef || !isOpInMainLoop(depDef, mainLoop)) { + continue; // block arg or loop-invariant value: reference it directly + } + buildScalarSlice(dep, mainLoop, sliceInOrder, visited, boundaryTensors); + } + sliceInOrder.push_back(def); +} + +// Find the ancestor of `op` that is a direct child of `block`. +static Operation *getAncestorInBlock(Operation *op, Block *block) { + while (op && op->getBlock() != block) { + op = op->getParentOp(); + } + return op; +} + +// Rematerialize the scalar slice of `root` into each of its cross-block +// consumer blocks and rewire those consumers to the local copy. Returns true on +// rewrite. +static bool +rematerializeScalarDep(Value root, int producerId, const MainLoop &mainLoop, + const SmallVector &sliceInOrder) { + Block *body = mainLoop.getBody(); + + // Group cross-block users by their block id. + llvm::MapVector> usersByBlock; + for (Operation *user : root.getUsers()) { + Operation *bodyAnc = getAncestorInBlock(user, body); + if (!bodyAnc) { + continue; + } + auto userId = getOpBlockId(user); + if (!userId.has_value()) { + userId = getOpBlockId(bodyAnc); + } + if (!userId.has_value() || *userId == producerId) { + continue; + } + usersByBlock[*userId].push_back(user); + } + if (usersByBlock.empty()) { + return false; + } + + bool changed = false; + for (auto &entry : usersByBlock) { + int userBlockId = entry.first; + SmallVector &users = entry.second; + + // Insert the rematerialized slice before the earliest consumer. + Operation *insertPt = nullptr; + for (Operation *user : users) { + Operation *anc = getAncestorInBlock(user, body); + if (!anc) { + continue; + } + if (!insertPt || anc->isBeforeInBlock(insertPt)) { + insertPt = anc; + } + } + if (!insertPt) { + continue; + } + + OpBuilder builder(insertPt); + IRMapping map; + for (Operation *op : sliceInOrder) { + Operation *cloned = builder.clone(*op, map); + cloned->walk([&](Operation *o) { + o->setAttr(kBlockId, builder.getI32IntegerAttr(userBlockId)); + }); + } + + Value clonedRoot = map.lookupOrDefault(root); + if (clonedRoot == root) { + continue; + } + for (Operation *user : users) { + user->replaceUsesOfWith(root, clonedRoot); + } + changed = true; + } + return changed; +} + +// Scan the main loop for cross-block scalar dependencies whose data originates +// from a tensor, and rematerialize the scalar portion into each consumer block +// so the tensor part can use the normal tensor-dependency buffering. +static void rematerializeTensorRootedScalarDeps(const MainLoop &mainLoop) { + Block *body = mainLoop.getBody(); + if (!body) { + return; + } + + SmallVector allOps; + collectNestedOps(body, allOps); + + // Collect candidate roots (deduplicated) before mutating the IR. + llvm::SetVector roots; + for (Operation *op : allOps) { + auto userId = getOpBlockId(op); + if (!userId.has_value()) { + continue; + } + for (Value operand : op->getOperands()) { + if (isa(operand.getType())) { + continue; // only scalar operands can be cross-block scalar deps + } + Operation *defOp = operand.getDefiningOp(); + if (!defOp || !isOpInMainLoop(defOp, mainLoop)) { + continue; + } + auto producerId = getOpBlockId(defOp); + if (!producerId.has_value() || *producerId == *userId) { + continue; + } + roots.insert(operand); + } + } + + for (Value root : roots) { + // If this .value() failed, it must be a bug in above codes. + auto producerId = getOpBlockId(root.getDefiningOp()).value(); + + SmallVector sliceInOrder; + DenseSet visited; + llvm::SetVector boundaryTensors; + buildScalarSlice(root, mainLoop, sliceInOrder, visited, boundaryTensors); + + // Pure scalar/memref chains keep the existing dep_mark handling. + if (boundaryTensors.empty()) { + continue; + } + + rematerializeScalarDep(root, producerId, mainLoop, sliceInOrder); + } +} + +static Value getIterCount(OpBuilder &builder, const MainLoop &loop, + Location loc, SmallVector *newOps, + int blockId = -1) { + auto i32Type = builder.getI32Type(); + + if (loop.isWhile()) { + assert(loop.iterCounter && + "whileOp main_loop requires a global iteration counter"); + return loop.iterCounter; + } + + auto forOp = cast(loop.getOperation()); + Value iv = forOp.getInductionVar(); + Value lb = forOp.getLowerBound(); + Value step = forOp.getStep(); + Type ivType = iv.getType(); + + // Check if lower bound is a constant zero + bool lbIsZero = false; + if (auto constOp = lb.getDefiningOp()) { + if (auto intAttr = dyn_cast(constOp.getValue())) + lbIsZero = (intAttr.getInt() == 0); + } + + Value iterIdx; + if (lbIsZero) { + // Optimization: if lb is 0, use iv directly (or iv/step if step != 1) + bool stepIsOne = false; + if (auto constOp = step.getDefiningOp()) + if (auto intAttr = dyn_cast(constOp.getValue())) + stepIsOne = intAttr.getInt() == 1; + if (stepIsOne) { + iterIdx = iv; + } else { + iterIdx = builder.create(loc, iv, step); + if (newOps) + newOps->push_back(iterIdx.getDefiningOp()); + if (blockId >= 0) { + iterIdx.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + } + } + } else { + // General case: (iv - lb) / step + Value diff = builder.create(loc, iv, lb); + iterIdx = builder.create(loc, diff, step); + if (newOps) { + newOps->push_back(diff.getDefiningOp()); + newOps->push_back(iterIdx.getDefiningOp()); + } + if (blockId >= 0) { + diff.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + iterIdx.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + } + } + + // Cast to i32 if necessary + if (ivType == i32Type) + return iterIdx; + + Value result; + constexpr int bits32 = 32; + if (ivType.isIndex()) { + result = builder.create(loc, i32Type, iterIdx); + } else if (auto intType = dyn_cast(ivType)) { + // Extend or truncate integer types to i32 + if (intType.getWidth() < bits32) + result = builder.create(loc, i32Type, iterIdx); + else if (intType.getWidth() > bits32) + result = builder.create(loc, i32Type, iterIdx); + else + return iterIdx; + } else { + result = builder.create(loc, i32Type, iterIdx); + } + if (newOps) + newOps->push_back(result.getDefiningOp()); + if (blockId >= 0) { + result.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + } + return result; +} + +// yieldFn is used for normal op case, getNestedResults is used for nested if +// case +static void createConditionalYield( + OpBuilder &builder, Location loc, bool hasResults, + function_ref yieldFn, + Operation *op, + std::function()> getNestedResults = nullptr) { + if (hasResults && getNestedResults) { + builder.create(loc, getNestedResults()); + } else if (hasResults && yieldFn && op) { + builder.create(loc, yieldFn(builder, loc, op)); + } else { + builder.create(loc); + } +} + +// Build if-else chain for N==2 (simple nested structure) +static int buildIfChainTwoBuffers( + OpBuilder &builder, Location loc, Value indexVal, + SmallVector &buffers, SmallVector &newOps, + SmallVector &outIfOps, + function_ref createOpFn, + function_ref yieldFn, + mlir::TypeRange types, bool hasResults, int blockId) { + // Create condition: index == 0 + Value zero = builder.create(loc, 0, 32); + Value firstCond = builder.create( + loc, mlir::arith::CmpIPredicate::eq, indexVal, zero); + auto firstIf = + builder.create(loc, types, firstCond, true, true); + + newOps.push_back(zero.getDefiningOp()); + newOps.push_back(firstCond.getDefiningOp()); + newOps.push_back(firstIf); + outIfOps.push_back(firstIf); + + // Tag counter operations with block_id + if (blockId >= 0) { + zero.getDefiningOp()->setAttr(kBlockId, builder.getI32IntegerAttr(blockId)); + firstCond.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + } + + // Then branch: use buffer[0] + builder.setInsertionPointToStart(&firstIf.getThenRegion().front()); + Operation *op0 = createOpFn(builder, loc, buffers[0].second); + if (!op0) + return -1; + newOps.push_back(op0); + createConditionalYield(builder, loc, hasResults, yieldFn, op0, nullptr); + + // Else branch: use buffer[1] + builder.setInsertionPointToStart(&firstIf.getElseRegion().front()); + Operation *op1 = createOpFn(builder, loc, buffers[1].second); + if (!op1) + return -1; + newOps.push_back(op1); + createConditionalYield(builder, loc, hasResults, yieldFn, op1, nullptr); + + builder.setInsertionPointAfter(firstIf); + return 0; +} + +// Build if-else chain for N>2 (if-else-if-else chain with proper yield +// passthrough) +static int buildIfChainMultiBuffers( + OpBuilder &builder, Location loc, Value indexVal, + SmallVector &buffers, SmallVector &newOps, + SmallVector &outIfOps, + function_ref createOpFn, + function_ref yieldFn, + mlir::TypeRange types, bool hasResults, int blockId) { + int N = buffers.size(); + + // Create rootIf (idx == 0) + Value zeroVal = builder.create(loc, 0, 32); + Value firstCond = builder.create( + loc, mlir::arith::CmpIPredicate::eq, indexVal, zeroVal); + if (blockId >= 0) { + zeroVal.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + firstCond.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + } + newOps.push_back(zeroVal.getDefiningOp()); + newOps.push_back(firstCond.getDefiningOp()); + + auto rootIf = + builder.create(loc, types, firstCond, true, true); + if (!rootIf) + return -1; + newOps.push_back(rootIf); + outIfOps.push_back(rootIf); + + // Then branch of rootIf: use buffer[0] + builder.setInsertionPointToStart(&rootIf.getThenRegion().front()); + Operation *op0 = createOpFn(builder, loc, buffers[0].second); + if (!op0) + return -1; + newOps.push_back(op0); + createConditionalYield(builder, loc, hasResults, yieldFn, op0, nullptr); + + // Build the nested if chain in rootIf's else region + Block *currentElseBlock = &rootIf.getElseRegion().front(); + + for (int i = 1; i < N - 1; ++i) { + // Set insertion to current else block + builder.setInsertionPointToStart(currentElseBlock); + + // Create condition for idx == i + Value iVal = builder.create(loc, i, 32); + Value cond = builder.create( + loc, mlir::arith::CmpIPredicate::eq, indexVal, iVal); + if (blockId >= 0) { + iVal.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + cond.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + } + newOps.push_back(iVal.getDefiningOp()); + newOps.push_back(cond.getDefiningOp()); + + // Create nested if for this level + auto nestedIf = + builder.create(loc, types, cond, true, true); + if (!nestedIf) + return -1; + newOps.push_back(nestedIf); + + // Then branch: use buffer[i] + builder.setInsertionPointToStart(&nestedIf.getThenRegion().front()); + Operation *op = createOpFn(builder, loc, buffers[i].second); + if (!op) + return -1; + newOps.push_back(op); + createConditionalYield(builder, loc, hasResults, yieldFn, op, nullptr); + + // Set insertion to end of currentElseBlock and add yield + builder.setInsertionPointToEnd(currentElseBlock); + createConditionalYield(builder, loc, hasResults, yieldFn, nullptr, + [&nestedIf]() { return nestedIf.getResults(); }); + + // Move to nestedIf's else block for next iteration + currentElseBlock = &nestedIf.getElseRegion().front(); + } + + // Final else: use buffer[N-1] + builder.setInsertionPointToStart(currentElseBlock); + Operation *opLast = createOpFn(builder, loc, buffers[N - 1].second); + if (!opLast) + return -1; + newOps.push_back(opLast); + createConditionalYield(builder, loc, hasResults, yieldFn, opLast, nullptr); + + builder.setInsertionPointAfter(rootIf); + return 0; +} + +// Build if-else chain for buffer selection: if (idx==0) -> buf[0] else ... else +// -> buf[N-1] +static int +buildIfChain(OpBuilder &builder, Location loc, Value indexVal, + SmallVector &buffers, SmallVector &newOps, + SmallVector &outIfOps, + function_ref createOpFn, + function_ref yieldFn, + std::optional resultTypes = std::nullopt, + int blockId = -1) { + int N = buffers.size(); + auto types = resultTypes.value_or(mlir::TypeRange{}); + bool hasResults = !types.empty(); + + if (N == 2) { + return buildIfChainTwoBuffers(builder, loc, indexVal, buffers, newOps, + outIfOps, createOpFn, yieldFn, types, + hasResults, blockId); + } + return buildIfChainMultiBuffers(builder, loc, indexVal, buffers, newOps, + outIfOps, createOpFn, yieldFn, types, + hasResults, blockId); +} + +// Compute buffer index: iterCount % N +static Value computeBufferIndex(OpBuilder &builder, const MainLoop &loop, + Location loc, int N, + SmallVector *newOps, + int blockId = -1) { + Value iterCount = getIterCount(builder, loop, loc, newOps, blockId); + Value Nval = builder.create(loc, N, 32); + Value bufIdx = builder.create(loc, iterCount, Nval); + if (newOps) { + newOps->push_back(Nval.getDefiningOp()); + newOps->push_back(bufIdx.getDefiningOp()); + } + // Tag counter operations with block_id + if (blockId >= 0) { + MLIRContext *ctx = builder.getContext(); + Nval.getDefiningOp()->setAttr(kBlockId, builder.getI32IntegerAttr(blockId)); + bufIdx.getDefiningOp()->setAttr(kBlockId, + builder.getI32IntegerAttr(blockId)); + } + return bufIdx; +} + +static SmallVector +insertProducerLogic(OpBuilder &builder, Value depVal, + SmallVector &buffers, const MainLoop &loop, + int groupId = -1) { + SmallVector newOps; + int N = buffers.size(); + Location loc = depVal.getLoc(); + // Single buffer producer logic + if (N == kBufferCountOne) { + Operation *producerOp = builder.create( + loc, mlir::TypeRange{}, depVal, buffers[0].second); + if (!producerOp) + return newOps; + if (groupId >= 0) { + producerOp->setAttr( + kIntraDeps, builder.getI32ArrayAttr({groupId, crossCoreProducerId})); + } + newOps.push_back(producerOp); + return newOps; + } + + Value bufIdx = computeBufferIndex(builder, loop, loc, N, &newOps); + SmallVector outIfOps; + if (buildIfChain( + builder, loc, bufIdx, buffers, newOps, outIfOps, + [&](OpBuilder &b, Location l, Value buffer) -> Operation * { + return b.create(l, mlir::TypeRange{}, depVal, buffer); + }, + nullptr) != 0) { + return {}; + } + // Multi-buffer: tag each inner-branch hivm.copy with [gid, 1]. The scf.if + // wrapper itself is just `remsi % N` dispatch and is not a write op. + if (groupId >= 0) { + for (Operation *op : newOps) { + if (isa(op)) { + op->setAttr(kIntraDeps, + builder.getI32ArrayAttr({groupId, crossCoreProducerId})); + } + } + } + return newOps; +} + +// Handle consumer when N==1 (directly return buffer) +static Operation *handleSingleBufferConsumer(OpBuilder &builder, Location loc, + SmallVector &buffers) { + auto memrefType = mlir::cast(buffers[0].second.getType()); + auto tensorType = mlir::RankedTensorType::get(memrefType.getShape(), + memrefType.getElementType()); + return builder.create( + loc, tensorType, buffers[0].second, + mlir::UnitAttr::get(builder.getContext()), + mlir::UnitAttr::get(builder.getContext())); +} + +// Helper function to create ToTensorOp +static mlir::bufferization::ToTensorOp createToTensorOp(OpBuilder &builder, + Location loc, + mlir::Type tensorType, + Value buffer) { + return builder.create( + loc, tensorType, buffer, mlir::UnitAttr::get(builder.getContext()), + mlir::UnitAttr::get(builder.getContext())); +} + +static int insertConsumerLogic(OpBuilder &builder, Value depVal, + SmallVector &buffers, + const MainLoop &loop, + SmallVector &outIfOps, + int groupId = -1, int blockId = -1) { + SmallVector newOps; + int N = buffers.size(); + Location loc = builder.getInsertionPoint()->getLoc(); + + if (N == kBufferCountOne) { + Operation *consumerOp = handleSingleBufferConsumer(builder, loc, buffers); + outIfOps.push_back(consumerOp); + if (groupId >= 0) { + consumerOp->setAttr(kIntraDeps, builder.getI32ArrayAttr({groupId, 0})); + } + return 0; + } + + Value readIdx = computeBufferIndex(builder, loop, loc, N, &newOps, blockId); + auto memrefType = mlir::cast(buffers[0].second.getType()); + auto tensorType = mlir::RankedTensorType::get(memrefType.getShape(), + memrefType.getElementType()); + mlir::TypeRange resultTypes(tensorType); + int ret = buildIfChain( + builder, loc, readIdx, buffers, newOps, outIfOps, + [&](OpBuilder &b, Location l, Value buffer) -> Operation * { + return createToTensorOp(b, l, tensorType, buffer); + }, + [&](OpBuilder &b, Location l, Operation *op) -> Value { + return cast(op).getResult(); + }, + resultTypes, blockId); + if (ret != 0) { + return ret; + } + if (groupId >= 0 && !outIfOps.empty()) { + outIfOps.front()->setAttr(kIntraDeps, + builder.getI32ArrayAttr({groupId, 0})); + } + return 0; +} + +static void addBlockAttrForOps(SmallVector &newOps, int blockId, + OpBuilder &builder) { + auto attr = builder.getI32IntegerAttr(blockId); + for (auto *op : newOps) + op->setAttr(kBlockId, attr); +} + +// Add dep_mark attribute to operation +static void addDepMarkAttr(Operation *op, int depMark, OpBuilder &builder) { + if (auto existingAttr = op->getAttrOfType(kDepMark)) { + SmallVector marks; + for (auto attr : existingAttr) + marks.push_back(cast(attr).getInt()); + marks.push_back(depMark); + op->setAttr(kDepMark, builder.getI32ArrayAttr(marks)); + } else { + op->setAttr(kDepMark, builder.getI32ArrayAttr({depMark})); + } +} + +static void addIntraBufferAttr(SmallVector &ops, + OpBuilder &builder) { + for (auto *op : ops) { + if (isa(op) || isa(op) || + isa(op)) { + op->setAttr(kIntraBuffer, builder.getUnitAttr()); + } + } +} + +// Collect cross-block user operations +static SmallVector +collectCrossBlockUsers(Value depVal, int producerId, + DenseMap> &depUserMap) { + SmallVector crossBlockUsers; + + auto userIt = depUserMap.find(depVal); + if (userIt == depUserMap.end()) + return crossBlockUsers; + + for (Operation *depUser : userIt->second) { + auto userId = getOutermostSsbufferId(depUser); + if ((!userId.has_value() || *userId != producerId) && + isInsideMainLoopForOpTraverse(depUser)) + crossBlockUsers.push_back(depUser); + } + return crossBlockUsers; +} + +// Insert buffer selection logic (scf.if + to_tensor) at the start of the given +// region. Returns the scf::IfOp that performs the buffer selection, or nullptr +// on failure. +static Operation * +insertBufferSelectionInRegion(OpBuilder &builder, Region ®ion, Location loc, + Value depVal, SmallVector &buffers, + const MainLoop &loop, int blockId) { + auto memrefType = mlir::cast(buffers[0].second.getType()); + auto tensorType = mlir::RankedTensorType::get(memrefType.getShape(), + memrefType.getElementType()); + + // Insert at the start of the region + builder.setInsertionPointToStart(®ion.front()); + + // Compute buffer index + Value readIdx = + computeBufferIndex(builder, loop, loc, buffers.size(), nullptr, blockId); + + // Build buffer selection if-else chain + SmallVector newIfOps; + SmallVector outIfOps; + int ret = buildIfChain( + builder, loc, readIdx, buffers, newIfOps, outIfOps, + [&](OpBuilder &b, Location l, Value buffer) -> Operation * { + return createToTensorOp(b, l, tensorType, buffer); + }, + [&](OpBuilder &b, Location l, Operation *op) -> Value { + return cast(op).getResult(); + }, + tensorType, blockId); + if (ret != 0) + return nullptr; + + if (newIfOps.empty()) + return nullptr; + + // Tag all operations in newIfOps with block_id + for (auto *op : newIfOps) { + op->setAttr(kBlockId, builder.getI32IntegerAttr(blockId)); + op->setAttr(kIntraBuffer, builder.getUnitAttr()); + } + + // The main scf.if is the first one in outIfOps + return outIfOps.front(); +} + +static void +markScalarDeps(SmallVector &scalarValueList, + DenseMap> &depUserMap, + OpBuilder &builder, int startDepMark) { + int nextDepMark = startDepMark; + + for (Value depVal : scalarValueList) { + Operation *depDefinedOp = depVal.getDefiningOp(); + if (!depDefinedOp) + continue; + + if (!isInsideMainLoopForOp(depDefinedOp)) + continue; + + auto producerId = getOpBlockId(depDefinedOp); + if (!producerId.has_value()) + continue; + auto crossBlockUsers = + collectCrossBlockUsers(depVal, *producerId, depUserMap); + if (crossBlockUsers.empty()) + continue; + + int depMark = nextDepMark++; + // Add depmark to producer + addDepMarkAttr(depDefinedOp, depMark, builder); + // Add depmark to consumer + for (Operation *depUser : crossBlockUsers) { + addDepMarkAttr(depUser, depMark, builder); + } + } +} + +// Check if depUser is a multi-region op and depVal is not a direct operand +// Returns true when op was added as consumer due to yield using depVal +// Generic version: supports any op with >= 2 regions (scf.if, scf.while, etc.) +static bool isMultiRegionConsumerFromYield(Operation *depUser, Value depVal) { + // Check if op has >= 2 regions (generic for any multi-region op) + if (depUser->getNumRegions() < 2) + return false; + + for (OpOperand &operand : depUser->getOpOperands()) { + if (operand.get() == depVal) + return false; // depVal is a direct operand + } + return true; // depVal comes from yield +} + +// Process normal consumer for a block: generate one buffer selection and share +// among all ops in the block +static int processNormalConsumerBlock(OpBuilder &consumedBuilder, Value depVal, + SmallVector &buffers, + const MainLoop &loop, + SmallVector &opsInBlock, + int userBlockId, int groupId, + OpBuilder &globalBuilder) { + SmallVector resultIfOps; + int ret = insertConsumerLogic(consumedBuilder, depVal, buffers, loop, + resultIfOps, groupId, userBlockId); + if (ret != 0) + return -1; + + if (resultIfOps.empty()) + return 0; + + addBlockAttrForOps(resultIfOps, userBlockId, globalBuilder); + if (buffers.size() > kBufferCountOne) { + for (auto *op : resultIfOps) { + if (isa(op)) { + op->setAttr(kIntraBuffer, globalBuilder.getUnitAttr()); + } + } + } else { + addIntraBufferAttr(resultIfOps, globalBuilder); + } + + Operation *resultIf = resultIfOps.back(); + Value selectedBuffer = resultIf->getResult(0); + + // Replace operands of all ops in this block + for (Operation *opInBlock : opsInBlock) { + for (OpOperand &use : opInBlock->getOpOperands()) { + if (use.get() == depVal) + use.set(selectedBuffer); + } + } + return 0; +} + +// Handle all regions (except region 0) when depVal is used in that region's +// yield Generic version: supports any op with >= 2 regions (scf.if, scf.while, +// etc.) For scf.if: handles else region (region index 1) For scf.while: handles +// after region (region index 1) For any op with 3+ regions: handles all regions +// from index 1 onwards +static int processMultiRegionAllYields(OpBuilder &consumedBuilder, Value depVal, + SmallVector &buffers, + const MainLoop &loop, Operation *depUser, + int userBlockId, int groupId) { + // Generic: check if op has >= 2 regions + if (depUser->getNumRegions() < 2) + return 0; + + // Iterate through all regions (from region 1 onwards, excluding region 0) + for (size_t i = 1; i < depUser->getNumRegions(); ++i) { + Region ®ion = depUser->getRegion(i); + if (region.empty()) + continue; + + auto yieldOp = dyn_cast(region.back().getTerminator()); + if (!yieldOp) + continue; + + for (OpOperand &operand : yieldOp->getOpOperands()) { + if (operand.get() != depVal) + continue; + + Operation *selectIf = insertBufferSelectionInRegion( + consumedBuilder, region, yieldOp.getLoc(), depVal, buffers, loop, + userBlockId); + if (!selectIf) + return -1; + + if (groupId >= 0) { + selectIf->setAttr(kIntraDeps, + consumedBuilder.getI32ArrayAttr({groupId, 0})); + } + + operand.set(selectIf->getResult(0)); + return 0; // Only handle one + } + } + return 0; +} + +// Find the last op in `anchorOp->getBlock()` whose `ssbuffer.block_id` +// attribute matches `blockId`. Returns nullptr if anchorOp is null or has no +// block, or if no such op is found. +static Operation *findLastOpWithBlockIdInBlock(Operation *anchorOp, + int blockId) { + if (!anchorOp) + return nullptr; + Block *block = anchorOp->getBlock(); + if (!block) + return nullptr; + Operation *result = nullptr; + for (Operation &op : *block) { + if (auto id = getOpBlockId(&op); id.has_value() && *id == blockId) + result = &op; + } + return result; +} + +// Find the first op in `anchorOp->getBlock()` whose `ssbuffer.block_id` +// attribute matches `blockId`. Returns nullptr if anchorOp is null or has no +// block, or if no such op is found. +static Operation *findFirstOpWithBlockIdInBlock(Operation *anchorOp, + int blockId) { + if (!anchorOp) + return nullptr; + Block *block = anchorOp->getBlock(); + if (!block) + return nullptr; + for (Operation &op : *block) { + if (auto id = getOpBlockId(&op); id.has_value() && *id == blockId) + return &op; + } + return nullptr; +} + +// Process producer and consumer for a single dependency value +static int processDepVal(Value depVal, const MainLoop &loop, + BufferMap &bufferMap, + DenseMap> &depUserMap, + OpBuilder &globalBuilder, int producerId, + int groupId) { + Operation *depDefinedOp = depVal.getDefiningOp(); + if (!depDefinedOp) + return 0; + + SmallVector &buffers = bufferMap[depVal]; + + auto userIt = depUserMap.find(depVal); + if (userIt == depUserMap.end()) + return 0; + SmallVector depUsers = userIt->second; + + // Read the module-level `ssbuffer.insertionOptimization` attribute inline so + // processDepVal can be called multiple times in the same pass run and stay + // in sync with whatever the Python caller last wrote onto the ModuleOp. + bool enableOpt = true; + if (mlir::ModuleOp mod = loop->getParentOfType()) + enableOpt = mod->hasAttr(CVPipeline::kInsertionOptimization); + + // Create producer + OpBuilder producedBuffers(loop.getContext()); + // When enable_buffer_insert_optimization is on, place the producer chain at + // the end of depDefinedOp's block_id=X region (after the last op with that + // block_id). Otherwise keep the original "right after depDefinedOp" anchor. + Operation *producerAnchor = depDefinedOp; + if (enableOpt) { + if (auto prodId = getOpBlockId(depDefinedOp); prodId.has_value()) { + if (Operation *lastInRegion = + findLastOpWithBlockIdInBlock(depDefinedOp, *prodId)) + producerAnchor = lastInRegion; + } + } + producedBuffers.setInsertionPointAfter(producerAnchor); + SmallVector producerNewOps = + insertProducerLogic(producedBuffers, depVal, buffers, loop, groupId); + addBlockAttrForOps(producerNewOps, producerId, globalBuilder); + if (buffers.size() > kBufferCountOne) { + for (auto *op : producerNewOps) { + if (isa(op)) { + op->setAttr(kIntraBuffer, globalBuilder.getUnitAttr()); + } + } + } else { + addIntraBufferAttr(producerNewOps, globalBuilder); + } + + // Single pass: process both normal ops and multi-region ops + // For normal ops: generate one buffer selection per block_id and share among + // all ops in that block For multi-region ops: process independently + DenseMap> opsByBlockId; + DenseMap processedBlockSelections; + + for (Operation *depUser : depUsers) { + auto userBlockId = getOutermostSsbufferId(depUser); + if (!userBlockId.has_value() || *userBlockId == producerId) + continue; + + if (isMultiRegionConsumerFromYield(depUser, depVal)) { + // Multi-region op: process independently + OpBuilder consumedBuilder(loop.getContext()); + // When enable_buffer_insert_optimization is on, place the consumer chain + // at the start of depUser's block_id=X region (before the first op with + // that block_id). Otherwise keep "right before depUser". + Operation *consumerAnchor = depUser; + if (enableOpt) { + if (auto userId = getOpBlockId(depUser); userId.has_value()) { + if (Operation *firstInRegion = + findFirstOpWithBlockIdInBlock(depUser, *userId)) + consumerAnchor = firstInRegion; + } + } + consumedBuilder.setInsertionPoint(consumerAnchor); + + if (int ret = + processMultiRegionAllYields(consumedBuilder, depVal, buffers, + loop, depUser, *userBlockId, groupId)) + return ret; + } else { + // Normal op: collect by block_id for batch processing + opsByBlockId[*userBlockId].push_back(depUser); + } + } + + // Re-group by Block* before processing: getOutermostSsbufferId may merge + // ops from different Regions (e.g. then/else of an scf.if) into the same + // block_id, but a single buffer selection's result is only visible in its + // own Region, so sharing across Regions would break SSA dominance. + for (auto &blockPair : opsByBlockId) { + int userBlockId = blockPair.first; + SmallVector &opsInBlock = blockPair.second; + if (opsInBlock.empty()) + continue; + + DenseMap> opsByBlock; + for (Operation *op : opsInBlock) { + Block *blk = op->getBlock(); + if (!blk) + continue; + opsByBlock[blk].push_back(op); + } + + for (auto ®ionPair : opsByBlock) { + SmallVector &opsInRegion = regionPair.second; + if (opsInRegion.empty()) + continue; + + Operation *firstOp = opsInRegion.front(); + OpBuilder consumedBuilder(loop.getContext()); + // When enable_buffer_insert_optimization is on, place the consumer chain + // at the start of the dep user's block_id=X region (before the first op + // with that block_id). Otherwise keep "right before firstOp". + Operation *consumerAnchor = firstOp; + if (enableOpt) { + if (auto userId = getOpBlockId(firstOp); userId.has_value()) { + if (Operation *firstInRegion = + findFirstOpWithBlockIdInBlock(firstOp, *userId)) + consumerAnchor = firstInRegion; + } + } + consumedBuilder.setInsertionPoint(consumerAnchor); + + if (int ret = processNormalConsumerBlock(consumedBuilder, depVal, buffers, + loop, opsInRegion, userBlockId, + groupId, globalBuilder)) + return ret; + } + } + + return 0; +} + +// If `clonedDepVals` is non-null, depVals that were actually cloned +static int cloneDepsToConsumers( + const MainLoop &loop, DenseMap &blocks, + DenseMap> &depValueMap, + DenseMap> &depUserMap, OpBuilder &builder, + llvm::function_ref patternCheck, + llvm::function_ref users)> + cloneFn, + DenseSet *clonedDepVals = nullptr) { + llvm::DenseSet seenVals; + + for (auto &blockPair : blocks) { + auto depIt = depValueMap.find(blockPair.first); + if (depIt == depValueMap.end()) + continue; + + for (Value depVal : depIt->second) { + Operation *defOp = depVal.getDefiningOp(); + if (!defOp) + continue; + if (!seenVals.insert(depVal).second) + continue; + if (!patternCheck(depVal)) + continue; + if (defOp->getParentOp() != loop.getOperation()) + continue; + + auto producerId = getOpBlockId(defOp); + if (!producerId.has_value()) + continue; + + auto userIt = depUserMap.find(depVal); + if (userIt == depUserMap.end()) + continue; + + // Group users by their consumer block_id, skipping users in the + // producer's own block and users that no longer reference depVal. + DenseMap> opsByBlockId; + for (Operation *user : userIt->second) { + auto userBlockId = getOpBlockId(user); + if (!userBlockId.has_value() || *userBlockId == producerId) + continue; + bool stillUses = false; + for (OpOperand &opnd : user->getOpOperands()) { + if (opnd.get() == depVal) { + stillUses = true; + break; + } + } + if (!stillUses) + continue; + opsByBlockId[*userBlockId].push_back(user); + } + + for (auto &p : opsByBlockId) { + int userBlockId = p.first; + auto &users = p.second; + if (users.empty()) + continue; + + Operation *firstUser = users.front(); + builder.setInsertionPoint(firstUser); + IRMapping mapper; + Value newVal = cloneFn(mapper, builder, depVal, userBlockId, users); + if (!newVal) + continue; + if (clonedDepVals) + clonedDepVals->insert(depVal); + for (Operation *user : users) { + user->replaceUsesOfWith(depVal, newVal); + } + } + } + } + return 0; +} + +// Phase 1: clone empty+fill (and any `ins` defining ops sharing the empty's +// parentOp) to consumer blocks; runs before dep collection because the cloned +// fill's `ins` chain may reach a producer-side tensor that Phase 2 must see. +static int cloneEmptyFillsInBlocks( + const MainLoop &loop, DenseMap &blocks, + DenseMap> &depValueMap, + DenseMap> &depUserMap, + OpBuilder &globalBuilder, DenseSet *clonedDepVals = nullptr) { + return cloneDepsToConsumers( + loop, blocks, depValueMap, depUserMap, globalBuilder, isEmptyFillPattern, + [](IRMapping &mapper, OpBuilder &builder, Value depVal, int userBlockId, + ArrayRef users) -> Value { + auto fillOp = cast(depVal.getDefiningOp()); + // outs may come from either tensor::EmptyOp (the original case) or + // bufferization.alloc_tensor (the new case). Both are "fresh tensor" + // sources and treated identically below. + Operation *origAllocLike = fillOp.getOutputs()[0].getDefiningOp(); + + // Collect the `ins` operands whose defining op shares the parentOp with + // the empty/alloc_tensor. + SmallVector insToClone; + Operation *allocParent = origAllocLike->getParentOp(); + for (Value insVal : fillOp.getInputs()) { + Operation *insDef = insVal.getDefiningOp(); + if (!insDef || insDef->getParentOp() != allocParent) + continue; + insToClone.push_back(insVal); + } + + Operation *newAllocLike = builder.clone(*origAllocLike, mapper); + newAllocLike->setAttr(kBlockId, builder.getI32IntegerAttr(userBlockId)); + mapper.map(origAllocLike->getResult(0), newAllocLike->getResult(0)); + + for (Value insVal : insToClone) { + Operation *insDef = insVal.getDefiningOp(); + Operation *newIns = builder.clone(*insDef, mapper); + newIns->setAttr(kBlockId, builder.getI32IntegerAttr(userBlockId)); + mapper.map(insVal, newIns->getResult(0)); + } + + Operation *newFill = builder.clone(*fillOp, mapper); + newFill->setAttr(kBlockId, builder.getI32IntegerAttr(userBlockId)); + return newFill->getResult(0); + }, + clonedDepVals); +} + +// Clone bufferization.alloc_tensor to each consumer block +static int +cloneAllocTensorsInBlocks(const MainLoop &loop, + DenseMap &blocks, + DenseMap> &depValueMap, + DenseMap> &depUserMap, + OpBuilder &globalBuilder) { + return cloneDepsToConsumers( + loop, blocks, depValueMap, depUserMap, globalBuilder, + isAllocTensorPattern, + [](IRMapping &mapper, OpBuilder &builder, Value depVal, int userBlockId, + ArrayRef users) -> Value { + auto origAlloc = + cast(depVal.getDefiningOp()); + Operation *newAlloc = builder.clone(*origAlloc, mapper); + newAlloc->setAttr(kBlockId, builder.getI32IntegerAttr(userBlockId)); + return newAlloc->getResult(0); + }); +} + +// Clone a memref.alloc + bufferization.to_tensor chain to each consumer block. +static int cloneAllocToTensorsInBlocks( + const MainLoop &loop, DenseMap &blocks, + DenseMap> &depValueMap, + DenseMap> &depUserMap, + OpBuilder &globalBuilder) { + return cloneDepsToConsumers( + loop, blocks, depValueMap, depUserMap, globalBuilder, + isAllocToTensorPattern, + [](IRMapping &mapper, OpBuilder &builder, Value depVal, int userBlockId, + ArrayRef users) -> Value { + auto toTensor = cast(depVal.getDefiningOp()); + Operation *origAlloc = toTensor.getOperand().getDefiningOp(); + + Operation *newAlloc = builder.clone(*origAlloc, mapper); + newAlloc->setAttr(kBlockId, builder.getI32IntegerAttr(userBlockId)); + mapper.map(origAlloc->getResult(0), newAlloc->getResult(0)); + + Operation *newToTensor = builder.clone(*toTensor, mapper); + newToTensor->setAttr(kBlockId, builder.getI32IntegerAttr(userBlockId)); + return newToTensor->getResult(0); + }); +} + +// Process cross-block tensor dependencies for double buffering +static int +processTensorDependencies(const MainLoop &loop, + DenseMap &blocks, + DenseMap> &depValueMap, + DenseMap> &depUserMap, + BufferMap &bufferMap, OpBuilder &globalBuilder, + int &groupId, const DenseSet &clonedDepVals) { + // Dedupe by depVal instead of op + llvm::DenseSet seenVals; + + for (auto &blockPair : blocks) { + Value blockKey = blockPair.first; + auto depIt = depValueMap.find(blockKey); + if (depIt == depValueMap.end()) + continue; + + SmallVector &depValues = depIt->second; + + for (Value depVal : depValues) { + // Skip if already processed + if (!seenVals.insert(depVal).second) + continue; + + // Validate dependency value (skip BlockArgument, null definingOp, + // non-ShapedType) + if (isa(depVal) || !depVal.getDefiningOp() || + !isa(depVal.getType())) + continue; + + // Skip tensor::EmptyOp - it should only get dep_mark, not buffer + // allocation + if (isa(depVal.getDefiningOp())) + continue; + + // Skip bufferization.alloc_tensor + if (isa(depVal.getDefiningOp())) + continue; + + // Skip to_tensor whose operand is a memref.alloc — cloned to consumer + // blocks by cloneAllocToTensorsInBlocks in Phase 2. + if (isAllocToTensorPattern(depVal)) + continue; + + auto *parentOp = depVal.getDefiningOp()->getParentOp(); + if (parentOp != loop.getOperation()) + continue; + + // Skip depVals that were already cloned by cloneEmptyFillsInBlocks. + if (clonedDepVals.contains(depVal)) + continue; + + auto userIt = depUserMap.find(depVal); + if (userIt == depUserMap.end()) + continue; + SmallVector depUsers = userIt->second; + + auto producerId = getOpBlockId(depVal.getDefiningOp()); + if (!producerId.has_value()) { + continue; + } + + // Check if all users are in the same block + bool allUsersSameBlock = true; + for (Operation *depUser : depUsers) { + auto userId = getOutermostSsbufferId(depUser); + if (!userId.has_value() || *userId != *producerId) { + allUsersSameBlock = false; + break; + } + } + if (allUsersSameBlock) + continue; + + // Process cross-block dependency with double buffering + if (processDepVal(depVal, loop, bufferMap, depUserMap, globalBuilder, + *producerId, groupId) != 0) + return -1; + groupId++; + } + } + return 0; +} + +static BufferMap insertBuffersBeforeLoop(const MainLoop &loop, + SmallVector &valueList, + OpBuilder &builder, int groupId) { + BufferMap bufferMap; + Block *parentBlock = loop.getBlock(); + OpBuilder insertedBuffers(builder.getContext()); + insertedBuffers.setInsertionPoint(parentBlock, loop.getIterator()); + + BufferCountManager bufferCountMgr(loop.getOperation()); + int bufNum = bufferCountMgr.getBufferCountByType( + BufferCountManager::DepType::IntraCore); + + for (Value depVal : valueList) { + ShapedType shapedType = cast(depVal.getType()); + Type elemType = shapedType.getElementType(); + AddressSpace addrSpace = AddressSpace::UB; + + SmallVector buffers; + for (int i = 0; i < bufNum; ++i) { + MemRefType memrefType = MemRefType::get( + shapedType.getShape(), elemType, MemRefLayoutAttrInterface{}, + AddressSpaceAttr::get(insertedBuffers.getContext(), addrSpace)); + + auto allocOp = + insertedBuffers.create(loop.getLoc(), memrefType); + + auto genericType = MemRefType::get(shapedType.getShape(), elemType, + MemRefLayoutAttrInterface{}, 0u); + + auto casted = insertedBuffers.create( + loop.getLoc(), genericType, allocOp.getResult()); + + buffers.push_back({casted.getResult(), casted.getResult()}); + } + + bufferMap[depVal] = buffers; + groupId++; + } + + return bufferMap; +} + +static bool +hasMemrefDepValue(DenseMap> &depValueMap) { + for (auto &p : depValueMap) { + Value groupKey = p.first; + Operation *producer = groupKey.getDefiningOp(); + for (Value depVal : p.second) { + if (isa(depVal.getType())) { + Operation *depOp = depVal.getDefiningOp(); + LDBG("MEMREF DEP: producer=<" + + std::string(producer ? producer->getName().getStringRef().str() + : "(block-arg)") + + ">, depVal=<" + + std::string(depOp ? depOp->getName().getStringRef().str() + : "(block-arg)") + + ">"); + return true; + } + } + } + return false; +} + +// Build the before-region of the new whileOp +static void buildBeforeRegion(scf::WhileOp oldWhile, OpBuilder &bb, Location bl, + ValueRange iterArgs) { + Block *oldBefore = oldWhile.getBeforeBody(); + IRMapping mapper; + unsigned numOrig = oldBefore->getNumArguments(); + for (unsigned i = 0; i < numOrig; ++i) + mapper.map(oldBefore->getArgument(i), iterArgs[i]); + + Operation *oldCond = nullptr; + for (Operation &op : *oldBefore) { + if (isa(&op)) { + oldCond = &op; + continue; + } + bb.clone(op, mapper); + } + if (!oldCond) + return; + + SmallVector newCondOps; + for (Value operand : oldCond->getOperands()) { + Value mapped = mapper.lookupOrNull(operand); + newCondOps.push_back(mapped ? mapped : operand); + } + newCondOps.push_back(iterArgs[numOrig]); + Value condValue = newCondOps.front(); + ArrayRef carriedValues = ArrayRef(newCondOps).drop_front(); + bb.create(bl, condValue, carriedValues); +} + +// Build the after-region of the new whileOp. The counter add-1 is not +// created here; it is inserted later by insertWhileCounterOps at the right +// position. counterIterArgOut is yielded as a placeholder and gets replaced. +static void buildAfterRegion(scf::WhileOp oldWhile, OpBuilder &ab, Location al, + ValueRange iterArgs, Value &counterIterArgOut) { + Block *oldAfter = oldWhile.getAfterBody(); + unsigned numOrig = oldAfter->getNumArguments(); + counterIterArgOut = iterArgs[numOrig]; + + IRMapping mapper; + for (unsigned i = 0; i < numOrig; ++i) + mapper.map(oldAfter->getArgument(i), iterArgs[i]); + + Operation *oldYield = nullptr; + for (Operation &op : *oldAfter) { + if (isa(&op)) { + oldYield = &op; + continue; + } + ab.clone(op, mapper); + } + if (!oldYield) + return; + + SmallVector newYieldOps; + for (Value operand : oldYield->getOperands()) { + Value mapped = mapper.lookupOrNull(operand); + newYieldOps.push_back(mapped ? mapped : operand); + } + newYieldOps.push_back(counterIterArgOut); + ab.create(al, newYieldOps); +} + +// Create a pass-managed global iteration counter for a whileOp main_loop +static std::pair +setupWhileIterArgCounter(const MainLoop &loop, OpBuilder &builder) { + auto oldWhile = cast(loop.getOperation()); + Location loc = loop.getLoc(); + MLIRContext *ctx = loop.getContext(); + Type i32Type = builder.getI32Type(); + + // Init 0 for the new counter iter_arg, inserted before oldWhile so the + // new whileOp can replace it in-place. + OpBuilder preBuilder(ctx); + preBuilder.setInsertionPoint(oldWhile); + Value zero = preBuilder.create(loc, 0, 32); + + // Old inits/result-types + i32 counter appended at the end. + SmallVector newInits(oldWhile.getInits().begin(), + oldWhile.getInits().end()); + newInits.push_back(zero); + SmallVector newResultTypes(oldWhile.getResultTypes().begin(), + oldWhile.getResultTypes().end()); + newResultTypes.push_back(i32Type); + + // Captured by the after-builder when the do-region is constructed; + // returned to the caller as the live iteration count. + Value counterIterArg; + + OpBuilder cb(ctx); + cb.setInsertionPoint(oldWhile); + auto newWhile = cb.create( + loc, newResultTypes, newInits, + [&](OpBuilder &bb, Location bl, ValueRange iterArgs) { + buildBeforeRegion(oldWhile, bb, bl, iterArgs); + }, + [&](OpBuilder &ab, Location al, ValueRange iterArgs) { + buildAfterRegion(oldWhile, ab, al, iterArgs, counterIterArg); + }); + + // Move attrs + for (auto attr : oldWhile->getAttrs()) + newWhile->setAttr(attr.getName(), attr.getValue()); + newWhile->setAttr(kIterCounter, cb.getUnitAttr()); + + for (unsigned i = 0, e = oldWhile.getNumResults(); i < e; ++i) + oldWhile.getResult(i).replaceAllUsesWith(newWhile.getResult(i)); + oldWhile.erase(); + + return {counterIterArg, newWhile}; +} + +// Insert the iterCounter add-1 (and its constant 1) right after the last op +// sharing the block_id of the first op that consumes mainLoop.iterCounter. +// Must run after processTensorDependencies (dispatch ops land there later). +// Fallback when no counter user exists: last op's block_id, mirroring the +// legacy buildAfterRegion placement. +static void insertWhileCounterOps(const MainLoop &mainLoop) { + if (!mainLoop.isWhile() || !mainLoop.iterCounter) + return; + + auto whileOp = cast(mainLoop.getOperation()); + Block *doBlock = whileOp.getAfterBody(); + if (!doBlock) + return; + + std::optional counterBlockId; + for (Operation &op : *doBlock) { + if (llvm::is_contained(op.getOperands(), mainLoop.iterCounter)) { + counterBlockId = getOpBlockId(&op); + break; + } + } + if (!counterBlockId) { + for (Operation &op : llvm::reverse(*doBlock)) { + if (auto id = getOpBlockId(&op); id.has_value()) { + counterBlockId = id; + break; + } + } + if (!counterBlockId) + return; + } + + Operation *lastWithBlockId = nullptr; + for (Operation &op : *doBlock) { + auto id = getOpBlockId(&op); + if (id.has_value() && *id == *counterBlockId) + lastWithBlockId = &op; + } + + Location loc = lastWithBlockId->getLoc(); + OpBuilder builder(mainLoop.getContext()); + builder.setInsertionPointAfter(lastWithBlockId); + IntegerAttr blockIdAttr = builder.getI32IntegerAttr(*counterBlockId); + + Value one = builder.create( + loc, 1, mainLoop.iterCounter.getType().getIntOrFloatBitWidth()); + one.getDefiningOp()->setAttr(kBlockId, blockIdAttr); + + Value nextCounter = + builder.create(loc, mainLoop.iterCounter, one); + Operation *iterAddOp = nextCounter.getDefiningOp(); + iterAddOp->setAttr(kBlockId, blockIdAttr); + iterAddOp->setAttr(kIterCounter, builder.getUnitAttr()); + + // Replace the placeholder counterIterArgOut operand in the yield with + // nextCounter so the iter_arg increments each iteration. + Operation *yieldOp = doBlock->getTerminator(); + SmallVector newOperands(yieldOp->getOperands().begin(), + yieldOp->getOperands().end()); + bool replaced = false; + for (Value &operand : newOperands) { + if (operand == mainLoop.iterCounter) { + operand = nextCounter; + replaced = true; + break; + } + } + if (replaced) + yieldOp->setOperands(newOperands); +} + +static int addInnerMultiBuffer(MainLoop mainLoop, OpBuilder &builder, + scope::ScopeOp vectorScope, int &groupId, + bool &i1Found, bool &memrefFound) { + OpBuilder globalBuilder(mainLoop.getContext()); + + // Two-phase dep collection for empty+fill cloning: + // Phase 1 (initial): collect deps, build user map, then clone the + // tensor::EmptyOp + linalg::FillOp pattern into each + // consumer's block. The cloned fill's `ins` chain can + // introduce fresh cross-block references (e.g. a + // producer-side tensor referenced by the cloned + // tensor.extract / arith.extf). + // Scalar rematerialize: trace each cloned fill's `ins` chain. If the + // chain reaches a producer-side tensor (via + // tensor.extract / arith.extf), build a fresh + // block-local chain that reads from a new + // multi-buffer added in Phase 2. + // Phase 2 (re-collect): re-run collectInnerBlockInfo / buildDepUserMap + // so the new cross-block refs (from clone + scalar + // rematerialize) are visible to the multi-buffer + // pipeline. The multi-buffer transfer structure + // (allocs, scf.if, intraDeps, intra_buffer) is + // created here and is not affected by the reorder. + DenseMap blocks; + DenseMap> depValueMap; + SmallVector allOps; + + // whileOp: bufNum>1 needs pre-created counter (no implicit iter count); + // bufNum==1 skips to avoid dead iter_arg. + if (mainLoop.isWhile()) { + BufferCountManager bufferCountMgr(mainLoop.getOperation()); + int bufNum = bufferCountMgr.getBufferCountByType( + BufferCountManager::DepType::IntraCore); + if (bufNum > kBufferCountOne) { + auto [counter, newWhile] = + setupWhileIterArgCounter(mainLoop, globalBuilder); + // Update mainLoop so subsequent collections target the new whileOp + // (the old one was erased inside setupWhileIterArgCounter). + mainLoop.op = newWhile; + mainLoop.body = newWhile.getAfterBody(); + mainLoop.iterCounter = counter; + } + } + + if (collectInnerBlockInfo(mainLoop, blocks, depValueMap, allOps, i1Found) != + 0) + return -1; + + if (blocks.empty()) + return -1; + + // Memref-type dep values are not supported here. + if (hasMemrefDepValue(depValueMap)) { + LDBG("ERROR: Memref type dependent values found in user IR, fallback"); + memrefFound = true; + return -1; + } + + // Phase 1: build initial depUserMap and clone empty+fill patterns. We use + // a fresh user map built from the initial allOps so the clone can find + // consumer-block users; the cloned fills will rewrite those users' uses. + DenseMap> initialDepUserMap = + buildDepUserMap(blocks, allOps, depValueMap); + DenseSet phase1ClonedDepVals; + if (cloneEmptyFillsInBlocks(mainLoop, blocks, depValueMap, initialDepUserMap, + globalBuilder, &phase1ClonedDepVals) != 0) + return -1; + + rematerializeTensorRootedScalarDeps(mainLoop); + + // Phase 2: re-collect deps now that cloned ops (and rematerialized scalar + // chains) have created new cross-block references. depValueMap and allOps + // are cleared inside collectInnerBlockInfo, so we re-discover everything + // from scratch. + blocks.clear(); + depValueMap.clear(); + allOps.clear(); + if (collectInnerBlockInfo(mainLoop, blocks, depValueMap, allOps, i1Found) != + 0) + return -1; + + // Phase 2 may surface i1 tensor deps that the clone introduced (e.g. a + // cloned scalar chain reaching a producer-side i1 tensor). Abort here too. + if (i1Found) { + LDBG("i1 tensor dep found in Phase 2, falling back"); + return -1; + } + + if (blocks.empty()) + return -1; + + // Drop memref-typed deps from Phase 2's collection + int droppedMemrefDeps = 0; + for (auto &p : depValueMap) { + llvm::erase_if(p.second, [&droppedMemrefDeps](Value v) { + if (isa(v.getType())) { + ++droppedMemrefDeps; + return true; + } + return false; + }); + } + if (droppedMemrefDeps > 0) + LDBG("Dropped " + std::to_string(droppedMemrefDeps) + + " clone-induced memref deps from Phase 2 depValueMap"); + + auto depUserMap = buildDepUserMap(blocks, allOps, depValueMap); + + // Clone bufferization.alloc_tensor deps to each consumer's block. + if (cloneAllocTensorsInBlocks(mainLoop, blocks, depValueMap, depUserMap, + globalBuilder) != 0) + return -1; + + // Clone memref.alloc + bufferization.to_tensor deps to each consumer's block + if (cloneAllocToTensorsInBlocks(mainLoop, blocks, depValueMap, depUserMap, + globalBuilder) != 0) + return -1; + auto valueList = collectBufferValues(depValueMap, phase1ClonedDepVals); + LLVM_DEBUG( + llvm::dbgs() + << "[addInnerMultiBuffer] before insertBuffersBeforeLoop, valueList.size=" + << valueList.size() << "\n"); + + auto bufferMap = + insertBuffersBeforeLoop(mainLoop, valueList, builder, groupId); + + LLVM_DEBUG( + llvm::dbgs() << "[addInnerMultiBuffer] before collectScalarDeps\n"); + auto scalarValueList = collectScalarDeps(depValueMap, depUserMap); + + LLVM_DEBUG(llvm::dbgs() << "[addInnerMultiBuffer] before markScalarDeps\n"); + markScalarDeps(scalarValueList, depUserMap, globalBuilder, 1); + + LLVM_DEBUG(llvm::dbgs() + << "[addInnerMultiBuffer] before processTensorDependencies\n"); + if (processTensorDependencies(mainLoop, blocks, depValueMap, depUserMap, + bufferMap, globalBuilder, groupId, + phase1ClonedDepVals) != 0) { + return -1; + } + + // WhileOp only: insert the counter add-1 now that the dispatch ops that + // use mainLoop.iterCounter have been emitted. + insertWhileCounterOps(mainLoop); + + LLVM_DEBUG(llvm::dbgs() << "[addInnerMultiBuffer] DONE\n"); + return 0; +} + +void AddMultiBufferInnerScopePass::getDependentDialects( + DialectRegistry ®istry) const { + registry + .insert(); +} + +void AddMultiBufferInnerScopePass::runOnOperation() { + auto module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + OpBuilder builder(module.getContext()); + + // Scan existing kIntraDeps to start groupId after the max already used + // (e.g. by InterCoreTransferAndSyncPass for C2C fixpipe transfers). + int groupId = 0; + module.walk([&](Operation *op) { + if (auto attr = op->getAttrOfType(CVPipeline::kIntraDeps)) { + if (!attr.empty()) { + if (auto intAttr = dyn_cast(attr[0])) { + groupId = std::max(groupId, static_cast(intAttr.getInt()) + 1); + } + } + } + }); + + LDBG("Enter pass."); + + auto walkResult = module.walk([&](scope::ScopeOp scope) -> WalkResult { + // Step 1: Check if scope has coreType attribute + auto coreTypeAttr = + scope->getAttrOfType(hivm::TCoreTypeAttr::name); + if (!coreTypeAttr) + return WalkResult::advance(); + + // Step 2: Check if core type is VECTOR + hivm::TCoreType coreType = coreTypeAttr.getTcoretype(); + if (coreType != hivm::TCoreType::VECTOR) { + LDBG("Not vector scope"); + return WalkResult::advance(); + } + + // Step 3: Collect all main_loop loops (forOp / whileOp) in the scope + SmallVector mainLoops; + int foundCount = + collectMainLoopsRecursively(scope.getBodyRegion(), mainLoops); + if (foundCount < 0) { + LDBG("collectMainLoopsRecursively failed"); + return WalkResult::interrupt(); + } + if (foundCount == 0) + return WalkResult::advance(); + + // Step 4: Process each main_loop + for (Operation *loopOp : mainLoops) { + MainLoop mainLoop(loopOp); + if (findNestedMainloop(mainLoop)) { + LDBG("Nested main_loop found, this is not allowed"); + return WalkResult::interrupt(); + } + // i1Found / memrefFound are reset per main_loop so they only trigger + // fallback for the current scope's deps. + bool i1Found = false; + bool memrefFound = false; + int ret = addInnerMultiBuffer(mainLoop, builder, scope, groupId, i1Found, + memrefFound); + if (i1Found) { + LDBG("i1 tensor dep found, setting fallback attribute"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return WalkResult::interrupt(); + } + if (memrefFound) { + LDBG("memref dep found, setting fallback attribute to IGNORED"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return WalkResult::interrupt(); + } + if (ret != 0) { + LDBG( + "addInnerMultiBuffer failed for main_loop; signaling pass failure"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return WalkResult::interrupt(); + } + } + + return WalkResult::advance(); + }); + if (walkResult.wasInterrupted()) { + if (!CVPipeline::hasFallbackAttr(module)) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + return; + } + + LDBG("Process successfully"); +} + +std::unique_ptr> createAddMultiBufferInnerScopePass() { + return std::make_unique(); +} + +void registerAddMultiBufferInnerScopePasses() { + registerPass([]() -> std::unique_ptr { + return createAddMultiBufferInnerScopePass(); + }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.cpp b/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.cpp new file mode 100644 index 0000000000..6a415b5c63 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.cpp @@ -0,0 +1,1669 @@ +#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.h" + +#include + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/Support/Debug.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" + +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/Common/FlagIdManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +static constexpr const char *DEBUG_TYPE = "AddMultiBufferOuterScope"; +#define LDBG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace triton; +using namespace hivm; + +namespace mlir { +namespace triton { + +// Maximum number of flag allocation attempts per transfer group +static constexpr int kMaxFlagAttempts = 16; +// Flag ID 15 is reserved for pipe synchronization (e.g. PIPE_S) and must not +// be allocated to cross-core transfers. Usable IDs are 0..MAX_FLAG_ID (14). +static constexpr int kReservedPipeFlagId = 15; + +// --- Attribute helpers --- + +static int getFlagFromSyncOp(Operation *op) { + if (auto attr = op->getAttrOfType("flag_id")) { + return attr.getInt(); + } + if (auto attr = op->getAttrOfType("static_flag_id")) { + return attr.getInt(); + } + if (auto attr = op->getAttrOfType("flag")) { + return attr.getInt(); + } + return -1; +} + +static int getBlockId(Operation *op) { + if (auto attr = op->getAttrOfType(mlir::CVPipeline::kBlockId)) + return attr.getInt(); + return -1; +} + +static int getTransferId(Operation *op) { + if (auto attr = op->getAttrOfType(mlir::CVPipeline::kTransferId)) + return attr.getInt(); + return -1; +} + +// --- main_loop attribute helpers --- + +/// Walk up from `op` (exclusive) and return the first ancestor satisfying +/// `pred`; nullptr if none. +static Operation *findAncestorOp(Operation *op, + llvm::function_ref pred) { + for (Operation *cur = op->getParentOp(); cur; cur = cur->getParentOp()) + if (pred(cur)) + return cur; + return nullptr; +} + +/// Nearest ancestor carrying the main_loop attribute, if any. +static Operation *findMainLoopOp(Operation *op) { + return findAncestorOp( + op, [](Operation *cur) { return CVPipeline::isMainLoopOp(cur); }); +} + +/// True if the sync op is nested inside a main_loop op (ForOp/WhileOp). +static bool parentOpHasMainLoopAttr(Operation *syncOp) { + if (!syncOp) { + return false; + } + return findMainLoopOp(syncOp) != nullptr; +} + +// --- Tag-driven transfer op classification helpers --- + +/// True if op is inside a VECTOR scope; for direction derivation only +static bool isInsideVectorScope(Operation *op) { + auto scopeOp = op->getParentOfType(); + if (!scopeOp) { + return false; + } + if (auto tcoreAttr = scopeOp->getAttrOfType("hivm.tcore_type")) + return tcoreAttr.getTcoretype() == TCoreType::VECTOR; + return false; +} + +/// True if op declares MemoryEffects Write on buffer +static bool hasWriteEffectOn(Operation *op, Value buffer) { + auto iface = dyn_cast(op); + if (!iface) { + return false; + } + SmallVector> effects; + iface.getEffects(effects); + return llvm::any_of(effects, [&](const auto &effect) { + return isa(effect.getEffect()) && + effect.getValue() == buffer; + }); +} + +/// Walk single-use memref chain from receiverOp to the first op producing a +/// tensor. Returns nullptr when: +/// - head result is not a memref +/// - chain forks (multi-use), leaves the block, or hits an op that does not +/// produce exactly one result +static Operation *findChainTensorTerminal(Operation *receiverOp) { + if (receiverOp->getNumResults() != 1) { + return nullptr; + } + Value cur = receiverOp->getResult(0); + Block *block = receiverOp->getBlock(); + // Terminates: SSA def-use is acyclic; same-block users appear strictly + // after their def, so the walk never revisits a value. + while (cur && isa(cur.getType())) { + if (!cur.hasOneUse()) { + return nullptr; // forked chain: ambiguous + } + Operation *user = *cur.getUsers().begin(); + if (user->getBlock() != block || user->getNumResults() != 1) { + return nullptr; + } + Value result = user->getResult(0); + if (isa(result.getType())) { + return user; + } + cur = result; + } + return nullptr; +} + +/// First block_id found in a block's ops, if any +static std::optional getFirstBlockId(Block *block) { + for (Operation &op : *block) + if (auto id = CVPipeline::getOpBlockId(&op)) + return id; + return std::nullopt; +} + +// --- Operation search helpers --- + +/// Find sync op with a specific flag, searching forward or backward in a block +static Operation *findSyncOpWithFlag(Block *block, Operation *start, int flag, + bool forward, bool wantWait) { + if (!block) { + return nullptr; + } + auto it = start->getIterator(); + if (forward) { + for (auto e = block->end(); it != e; ++it) { + Operation *op = &*it; + if (!(isa(op) || isa(op))) { + continue; + } + if (getFlagFromSyncOp(op) != flag) { + continue; + } + if (wantWait && isa(op)) { + return op; + } + if (!wantWait && isa(op)) { + return op; + } + } + } else { + if (it == block->begin()) { + return nullptr; + } + do { + --it; + Operation *op = &*it; + if (!(isa(op) || isa(op))) { + continue; + } + if (getFlagFromSyncOp(op) != flag) { + continue; + } + if (wantWait && isa(op)) { + return op; + } + if (!wantWait && isa(op)) { + return op; + } + } while (it != block->begin()); + } + return nullptr; +} + +// ============================================================================ +// Step 1: Collect transfer group info +// ============================================================================ + +/// Collect all ops with ssbuffer.transfer_id, grouped by transfer_id +static int +collectOpsByTransferId(ModuleOp module, + DenseMap> &opsByTid) { + module.walk([&](Operation *op) { + if (!op->hasAttr(mlir::CVPipeline::kTransferId)) { + return; + } + int tid = getTransferId(op); + if (tid >= 0) { + opsByTid[tid].push_back(op); + } + }); + LDBG("Collected " << opsByTid.size() << " transfer groups."); + + for (auto &p : opsByTid) { + LDBG(" tid=" << p.first << " has " << p.second.size() << " ops."); + DenseMap blockIdCount; + for (auto *op : p.second) { + int bid = getBlockId(op); + blockIdCount[bid]++; + } + for (auto &bp : blockIdCount) { + LDBG(" block_id=" << bp.first << ": " << bp.second << " ops."); + } + } + return 0; +} + +/// Collect alloc/mark pairs from transfer ops in the group. +/// Identifies the correct cross-core buffer (ub/cbuf) used by each transfer op, +/// ignoring local buffers (cc on CUBE side) that are not part of the data +/// transfer. +static int collectBufferAllocs(const SmallVector &ops, + TransferGroupInfo &info) { + // Helper: find the annotation.mark for a given alloc op + auto findMarkForAlloc = [](Operation *allocOp) -> Operation * { + Value allocResult = allocOp->getResult(0); + for (auto *user : allocResult.getUsers()) { + if (isa(user)) + return user; + } + return nullptr; + }; + + // Cross-core buffer = alloc defining the recorded buffer operand + if (info.senderChain.transferOp && info.senderChain.bufferOperand) { + if (auto *defOp = info.senderChain.bufferOperand.getDefiningOp()) { + if (isa(defOp)) { + info.senderBuf.allocOp = defOp; + info.senderBuf.markOp = findMarkForAlloc(defOp); + LDBG("Sender cross-core buffer: alloc from transferOp buffer operand."); + } + } + } + + if (info.receiverChain.transferOp && info.receiverChain.bufferOperand) { + if (auto *defOp = info.receiverChain.bufferOperand.getDefiningOp()) { + if (isa(defOp)) { + info.receiverBuf.allocOp = defOp; + info.receiverBuf.markOp = findMarkForAlloc(defOp); + LDBG("Receiver cross-core buffer: alloc from transferOp buffer " + "operand."); + } + } + } + + // Collect alloc/mark for the OTHER side if not yet found. + // Some transfer ops (e.g. fixpipe) have both a local input (cc) and a + // cross-core output (ub). The receiver side's buffer is the cross-core one. + // Walk all allocs in the group to find any remaining unassigned buffer. + SmallVector allocs; + for (Operation *op : ops) { + if (isa(op)) + allocs.push_back(op); + } + + // Fill missing side from remaining allocs (prefer allocs with marks) + for (auto *allocOp : allocs) { + if (allocOp == info.senderBuf.allocOp || + allocOp == info.receiverBuf.allocOp) + continue; + Operation *mark = findMarkForAlloc(allocOp); + if (!info.senderBuf.allocOp) { + info.senderBuf.allocOp = allocOp; + info.senderBuf.markOp = mark; + } else if (!info.receiverBuf.allocOp) { + info.receiverBuf.allocOp = allocOp; + info.receiverBuf.markOp = mark; + } + } + + LDBG("Sender buffer: " << (info.senderBuf.allocOp ? "alloc" : "none") << " + " + << (info.senderBuf.markOp ? "mark" : "none") << "."); + LDBG("Receiver buffer: " << (info.receiverBuf.allocOp ? "alloc" : "none") + << " + " + << (info.receiverBuf.markOp ? "mark" : "none") + << "."); + return 0; +} + +/// Collect llvm.load volatile and llvm.store volatile ops by transfer_id +static int collectLoadStoreOpsByTransferId( + ModuleOp module, DenseMap> &loadStoreByTid) { + module.walk([&](Operation *op) { + if (!op->hasAttr(mlir::CVPipeline::kTransferId)) { + return; + } + int tid = getTransferId(op); + if (tid < 0) { + return; + } + if (isa(op) || isa(op)) { + loadStoreByTid[tid].push_back(op); + } + }); + LDBG("Collected load/store ops for " << loadStoreByTid.size() + << " transfer groups."); + return 0; +} + +/// Tag load/store ops with crossDeps (producer=store, consumer=load) +static int tagLoadStoreOpsWithCrossDeps( + DenseMap> &loadStoreByTid) { + for (auto &p : loadStoreByTid) { + int tid = p.first; + for (auto *op : p.second) { + MLIRContext *ctx = op->getContext(); + OpBuilder builder(ctx); + if (auto storeOp = dyn_cast(op)) { + // producer: crossDeps = {tid, 1} + // Tag the defining op of the store's second operand (ptr), not the + // store itself + Value ptr = storeOp.getOperand(1); + if (auto *ptrDefOp = ptr.getDefiningOp()) { + ptrDefOp->setAttr( + mlir::CVPipeline::kCrossCoreDeps, + builder.getArrayAttr({builder.getI32IntegerAttr(tid), + builder.getI32IntegerAttr(1)})); + LDBG("Tagged ptr-defining-op with crossDeps={tid=" << tid << ", 1}."); + } + } else if (auto loadOp = dyn_cast(op)) { + // consumer: crossDeps = {tid, 0} + // Tag the load op itself + op->setAttr(mlir::CVPipeline::kCrossCoreDeps, + builder.getArrayAttr({builder.getI32IntegerAttr(tid), + builder.getI32IntegerAttr(0)})); + LDBG("Tagged llvm.load volatile with crossDeps={tid=" << tid + << ", 0}."); + } + } + } + return 0; +} + +/// Collect extra sync ops (parent has no main_loop), paired by flag +static int collectExtraSync(const SmallVector &ops, + int originalFlag, ExtraSyncInfo &info) { + SmallVector extraSets; + SmallVector extraWaits; + + for (Operation *op : ops) { + if (!(isa(op) || isa(op))) { + continue; + } + + bool hasMainLoop = parentOpHasMainLoopAttr(op); + LDBG("sync op: flag=" << getFlagFromSyncOp(op) + << ", block_id=" << getBlockId(op) + << ", parentHasMainLoop=" << hasMainLoop << "."); + + if (!hasMainLoop) { + if (isa(op)) { + extraSets.push_back(op); + } else if (isa(op)) { + extraWaits.push_back(op); + } + } + } + + // Match by flag + for (auto *setOp : extraSets) { + if (getFlagFromSyncOp(setOp) != originalFlag) { + continue; + } + for (auto *waitOp : extraWaits) { + if (getFlagFromSyncOp(waitOp) != originalFlag) { + continue; + } + info.setOp = setOp; + info.waitOp = waitOp; + LDBG("Extra sync pair: set(flag=" + << originalFlag << ", block_id=" << getBlockId(setOp) + << "), wait(flag=" << originalFlag + << ", block_id=" << getBlockId(waitOp) << "."); + return 0; + } + } + + // Fallback: use first available pair if exact match not found + if (!extraSets.empty() && !extraWaits.empty()) { + info.setOp = extraSets.front(); + info.waitOp = extraWaits.front(); + } + + return 0; +} + +/// Collect in-loop sender/receiver chains. Tag+dataflow rules: +/// - group buffer: alloc carrying the transfer_id +/// - sender: writes a group buffer (Write effect or result-less) +/// - receiver head: first op reading a group buffer as a value +/// - no op-type matching; receiver trailing ops found at wrap time +static int collectTransferChains(const SmallVector &ops, + int originalFlag, TransferChainInfo &info) { + // Group buffers: allocs carrying this transfer_id, one per core (sender and + // receiver sides each own one physical buffer) + SmallVector groupBuffers; + for (Operation *op : ops) { + if (auto allocOp = dyn_cast(op)) { + groupBuffers.push_back(allocOp.getResult()); + } + } + auto isGroupBuffer = [&](Value v) { + return llvm::is_contained(groupBuffers, v); + }; + + for (Operation *op : ops) { + if ((isa(op) || isa(op)) || + !op->getBlock()) { + continue; + } + // Buffers and annotations are data, not transfer behavior + if (isa(op) || isa(op)) { + continue; + } + if (!parentOpHasMainLoopAttr(op)) { + continue; + } + + // First operand backed by a group buffer + Value bufferOperand; + for (Value operand : op->getOperands()) { + if (isGroupBuffer(operand)) { + bufferOperand = operand; + break; + } + } + if (!bufferOperand) { + continue; + } + + Block *block = op->getBlock(); + + if (op->getNumResults() == 0 || hasWriteEffectOn(op, bufferOperand)) { + // Writes the cross-core buffer → sender + if (info.sender.transferOp) { + continue; + } + info.sender.transferOp = op; + info.sender.bufferOperand = bufferOperand; + info.sender.waitOp = + findSyncOpWithFlag(block, op, originalFlag, false, true); + info.sender.setOp = + findSyncOpWithFlag(block, op, originalFlag, true, false); + LDBG("Sender chain (tag-driven): " << op->getName() + << ", flag=" << originalFlag << "."); + } else if (!info.receiver.transferOp && op->getNumResults() > 0) { + // Reads the cross-core buffer as a value → receiver head + info.receiver.transferOp = op; + info.receiver.bufferOperand = bufferOperand; + info.receiver.waitOp = + findSyncOpWithFlag(block, op, originalFlag, false, true); + info.receiver.setOp = + findSyncOpWithFlag(block, op, originalFlag, true, false); + info.receiver.toTensorOp = findChainTensorTerminal(op); + LDBG("Receiver chain (tag-driven): " + << op->getName() << ", flag=" << originalFlag << ", toTensorOp=" + << (info.receiver.toTensorOp ? "found" : "none") << "."); + } + } + + return 0; +} + +/// Build TransferGroupInfo for a single transfer_id +static int buildTransferGroupData(int tid, const SmallVector &ops, + TransferGroupInfo &info) { + info.tid = tid; + + LDBG("Building group tid=" << tid << ", ops=" << ops.size() << "."); + + // 1. Determine original flag + for (Operation *op : ops) { + if ((isa(op) || isa(op))) { + int f = getFlagFromSyncOp(op); + if (f >= 0) { + info.originalFlag = f; + break; + } + } + } + + // 2. Collect extra sync (parent has no main_loop) + ExtraSyncInfo extraInfo; + if (collectExtraSync(ops, info.originalFlag, extraInfo)) { + return -1; + } + info.extraSyncSetOp = extraInfo.setOp; + info.extraSyncWaitOp = extraInfo.waitOp; + if (extraInfo.setOp && extraInfo.waitOp) { + LDBG("Extra sync: set(block_id=" << getBlockId(extraInfo.setOp) + << "), wait(block_id=" + << getBlockId(extraInfo.waitOp) << "."); + } else { + LDBG("Extra sync: not found."); + } + + // 3. Collect transfer chain (parent has main_loop) + TransferChainInfo chainInfo; + if (collectTransferChains(ops, info.originalFlag, chainInfo)) { + return -1; + } + info.senderChain = chainInfo.sender; + info.receiverChain = chainInfo.receiver; + + // 3.5 Chain completeness: each side needs both its wait and set syncs. + // Wrapping half a handshake (producer alternates flags while the consumer + // does not) deadlocks; drop the group's chains entirely so the transfer + // stays single-buffered instead. + auto chainComplete = [](const TransferOpChain &chain) { + return !chain.transferOp || (chain.waitOp && chain.setOp); + }; + if (!chainComplete(info.senderChain) || !chainComplete(info.receiverChain)) { + LDBG("Group tid=" << tid + << " has an incomplete chain (wait/set missing), keeping" + << " it single-buffered."); + info.senderChain = TransferOpChain(); + info.receiverChain = TransferOpChain(); + } + + // 4. Direction from the executing core's scope: + // - sender in VECTOR scope: V→C; in CUBE scope: C→V + // - no sender: derive from receiver scope instead + if (info.senderChain.transferOp) { + info.isCtoV = !isInsideVectorScope(info.senderChain.transferOp); + } else if (info.receiverChain.transferOp) { + info.isCtoV = isInsideVectorScope(info.receiverChain.transferOp); + } + + // 5. Collect buffer alloc/mark pairs from transfer ops + // Must run after transfer chain collection to identify the correct + // cross-core buffer (ub/cbuf) from each transfer op's operands, + // ignoring local buffers (e.g. cc on CUBE side). + if (collectBufferAllocs(ops, info)) { + return -1; + } + + // 6. Output flag is assigned in collectTransferGroupData, shared by groups + // with the same (originalFlag, direction). + + if (info.senderChain.transferOp || info.receiverChain.transferOp) { + LDBG("Direction: " << (info.isCtoV ? "C→V" : "V→C") + << ", flag=" << info.originalFlag << "."); + } + + return 0; +} + +/// Collect TransferGroupInfo for all transfer groups +static int collectTransferGroupData( + ModuleOp module, DenseMap> &opsByTid, + FlagIdManager &flagIdMgr, DenseMap &groups) { + for (auto &p : opsByTid) { + TransferGroupInfo info; + if (buildTransferGroupData(p.first, p.second, info)) { + continue; + } + if (info.senderChain.transferOp || info.receiverChain.transferOp) { + groups[p.first] = info; + } + } + + // Output flag reuse: groups with same (originalFlag, direction) share an + // output flag. Assign one new flag per distinct key; std::map iterates in + // sorted key order for determinism. + std::map, int> outputFlagByKey; + for (auto &p : groups) { + outputFlagByKey.try_emplace( + std::make_pair(p.second.originalFlag, p.second.isCtoV), + FlagIdManager::INVALID_FLAG_ID); + } + for (auto &entry : outputFlagByKey) { + for (int attempt = 0; attempt < kMaxFlagAttempts; ++attempt) { + int64_t pf = flagIdMgr.acquireId(); + if (pf == FlagIdManager::INVALID_FLAG_ID || pf != entry.first.first) { + entry.second = static_cast(pf); + break; + } + } + } + for (auto &p : groups) { + auto &g = p.second; + g.outputFlag = outputFlagByKey[std::make_pair(g.originalFlag, g.isCtoV)]; + LDBG("Group tid=" << g.tid << " gets outputFlag=" << g.outputFlag + << " (shared originalFlag=" << g.originalFlag + << ", dir=" << (g.isCtoV ? "C→V" : "V→C") << ")."); + } + + return 0; +} + +// ============================================================================ +// Step 2: Create output buffers +// ============================================================================ + +static constexpr int kMaxTcbSearch = 100; + +static int allocateNewTcbId(int startFrom, std::set &usedTcbIds) { + for (int id = startFrom; id < kMaxTcbSearch; ++id) { + if (!usedTcbIds.count(id)) { + usedTcbIds.insert(id); + return id; + } + } + return -1; +} + +/// Create an output buffer for an input/output buffer pair +static int createOutputBufferPair(Operation *inputAllocOp, int tid, int tcbId, + Value &inputBuffer, Value &outputBuffer, + OpBuilder &builder, bool isSender) { + if (!inputAllocOp) { + return -1; + } + + Location loc = builder.getUnknownLoc(); + + inputBuffer = inputAllocOp->getResult(0); + auto memRefType = dyn_cast(inputBuffer.getType()); + if (!memRefType) { + return -1; + } + + int origBlockId = getBlockId(inputAllocOp); + int outputBlockId = origBlockId; + + builder.setInsertionPointAfter(inputAllocOp); + auto outputAlloc = builder.create(loc, memRefType); + outputAlloc->setAttr(mlir::CVPipeline::kBlockId, + builder.getI32IntegerAttr(outputBlockId)); + outputAlloc->setAttr(mlir::CVPipeline::kTransferId, + builder.getI32IntegerAttr(tid)); + outputBuffer = outputAlloc.getResult(); + + // NOTE: output alloc carries no ssbuffer.crossCoreDeps — alloc is a + // buffer-creation op, not a behavior op. Producer tag lives on the + // fixpipe/copy clone inside scf.if (set in wrapTransferOpWithScfIf*), + // and consumer tag lives on the scf.if wrapper itself (set in + // wrapReceiverChainWithScfIf). Do NOT re-introduce crossDeps here. + + auto outputMark = builder.create(loc, outputBuffer); + outputMark->setAttr("effects", builder.getStrArrayAttr({"write", "read"})); + outputMark->setAttr(mlir::CVPipeline::kBlockId, + builder.getI32IntegerAttr(outputBlockId)); + outputMark->setAttr(mlir::CVPipeline::kTransferId, + builder.getI32IntegerAttr(tid)); + outputMark->setAttr( + "hivm.tightly_coupled_buffer", + hivm::HIVMTightlyCoupledBufferAttr::get(builder.getContext(), tcbId)); + LDBG("Created " << (isSender ? "sender" : "receiver") + << " output buffer: block_id=" << outputBlockId + << ", tcb_id=" << tcbId << "."); + return 0; +} + +static constexpr unsigned kBits32 = 32; + +static int attachSsbufferTags(Operation *op, int blockId, int transferId) { + MLIRContext *ctx = op->getContext(); + op->setAttr(mlir::CVPipeline::kBlockId, + IntegerAttr::get(IntegerType::get(ctx, kBits32), blockId)); + op->setAttr(mlir::CVPipeline::kTransferId, + IntegerAttr::get(IntegerType::get(ctx, kBits32), transferId)); + op->setAttr("ssbuffer.analyze_flag_id", UnitAttr::get(ctx)); + return 0; +} + +static hivm::SyncBlockSetOp createOutputSyncSetOp(Operation *origSetOp, + int outputFlag, int tid, + OpBuilder &builder) { + auto setOp = cast(origSetOp); + builder.setInsertionPointAfter(origSetOp); + auto newSetOp = builder.create( + setOp.getLoc(), setOp.getTcoreType(), setOp.getTpipe(), setOp.getPipe(), + builder.getI64IntegerAttr(outputFlag)); + attachSsbufferTags(newSetOp.getOperation(), getBlockId(setOp), tid); + return newSetOp; +} + +static hivm::SyncBlockWaitOp createOutputSyncWaitOp(Operation *origWaitOp, + int outputFlag, int tid, + OpBuilder &builder) { + auto waitOp = cast(origWaitOp); + builder.setInsertionPointAfter(origWaitOp); + auto newWaitOp = builder.create( + waitOp.getLoc(), waitOp.getTcoreType(), waitOp.getTpipe(), + waitOp.getPipe(), builder.getI64IntegerAttr(outputFlag)); + attachSsbufferTags(newWaitOp.getOperation(), getBlockId(waitOp), tid); + return newWaitOp; +} + +/// Create output buffer for a single transfer group, with output flag sync ops +static int createOutputBufferForGroup(TransferGroupInfo &g, + OpBuilder &builder) { + if (createOutputBufferPair(g.senderBuf.allocOp, g.tid, g.tcbId, + g.senderInputBuffer, g.senderOutputBuffer, builder, + true)) { + return -1; + } + if (createOutputBufferPair(g.receiverBuf.allocOp, g.tid, g.tcbId, + g.receiverInputBuffer, g.receiverOutputBuffer, + builder, false)) { + return -1; + } + // Insert output sync set at extra_sync position + if (g.extraSyncSetOp) { + createOutputSyncSetOp(g.extraSyncSetOp, g.outputFlag, g.tid, builder); + LDBG("Created output sync set with flag=" << g.outputFlag << " at block_id=" + << getBlockId(g.extraSyncSetOp) + << " (sender scope)."); + } + + // Insert output sync wait at extra_sync position + Operation *outputWaitInsertOp = + g.extraSyncWaitOp ? g.extraSyncWaitOp : g.receiverChain.waitOp; + if (outputWaitInsertOp) { + createOutputSyncWaitOp(outputWaitInsertOp, g.outputFlag, g.tid, builder); + LDBG("Created output sync wait with flag=" + << g.outputFlag << " at block_id=" << getBlockId(outputWaitInsertOp) + << " (receiver scope)."); + } + return 0; +} + +/// Create output buffers for all transfer groups +static int createOutputBuffers(DenseMap &groups, + ModuleOp module) { + OpBuilder builder(module.getContext()); + std::set usedTcbIds; + + // Collect existing tcb ids + module.walk([&](Operation *op) { + if (auto tcbAttr = op->getAttrOfType( + "hivm.tightly_coupled_buffer")) { + auto id = tcbAttr.getId(); + if (id.has_value()) { + LDBG("Found mark op with tcb_id=" << id.value() << "."); + usedTcbIds.insert(id.value()); + } + } + }); + + LDBG("=== Step 2: Creating output buffers ===."); + { + std::string ids; + llvm::raw_string_ostream os(ids); + for (int id : usedTcbIds) + os << id << " "; + LDBG("Collected existing tcb_ids: " << ids << "."); + } + + int maxExistingTcbId = usedTcbIds.empty() ? 0 : *usedTcbIds.rbegin(); + LDBG("Max existing tcb_id: " << maxExistingTcbId << "."); + + int nextTcbId = maxExistingTcbId + 1; + + for (auto &p : groups) { + TransferGroupInfo &g = p.second; + LDBG("Group tid=" << g.tid << " (" << (g.isCtoV ? "C→V" : "V→C") << ")."); + + g.tcbId = allocateNewTcbId(nextTcbId, usedTcbIds); + LDBG("Allocated tcb_id=" << g.tcbId << "."); + + nextTcbId = g.tcbId + 1; + + createOutputBufferForGroup(g, builder); + } + return 0; +} + +/// Tag consumer-side alloc and transferOp with crossDeps marks +static int addConsumerCrossDepsTags(TransferGroupInfo &g, ModuleOp module) { + auto &consumerBuf = g.receiverBuf; + auto &consumerChain = g.receiverChain; + + OpBuilder builder(module.getContext()); + + if (consumerBuf.allocOp) { + consumerBuf.allocOp->setAttr( + mlir::CVPipeline::kCrossCoreDeps, + builder.getArrayAttr( + {builder.getI32IntegerAttr(g.tid), builder.getI32IntegerAttr(1)})); + } + if (consumerChain.transferOp) { + consumerChain.transferOp->setAttr( + mlir::CVPipeline::kCrossCoreDeps, + builder.getArrayAttr( + {builder.getI32IntegerAttr(g.tid), builder.getI32IntegerAttr(0)})); + } + return 0; +} + +// ============================================================================ +// Step 3: Add polling control flow +// ============================================================================ + +/// Set ssbuffer tags on an op +static int setSsbufferTags(Operation *op, OpBuilder &builder, int blockId, + int tid) { + op->setAttr(mlir::CVPipeline::kBlockId, builder.getI32IntegerAttr(blockId)); + op->setAttr(mlir::CVPipeline::kTransferId, builder.getI32IntegerAttr(tid)); + return 0; +} + +/// Ensure a WhileOp has an i32 iteration counter (polling: counter % 2 == 0). +/// - reuse an existing one (InnerScope-injected, kIterCounter on the while) +/// - normalize its body-end update: move to body head, re-tag first block id +static Value ensureWhileOpHasCounter(scf::WhileOp whileOp) { + if (whileOp->hasAttr(CVPipeline::kIterCounter)) { + Block &after = whileOp.getAfter().front(); + SmallVector updates; + for (Operation &op : after) + if (op.hasAttr(CVPipeline::kIterCounter)) + updates.push_back(&op); + for (Operation *addi : updates) { + Operation *firstOp = &after.front(); + if (addi == firstOp || addi == firstOp->getNextNode()) + continue; // already at head + std::optional firstId = getFirstBlockId(&after); + if (!firstId) + continue; + addi->moveBefore(&after, after.begin()); + for (Value operand : addi->getOperands()) + if (Operation *defOp = operand.getDefiningOp()) + if (defOp->getBlock() == &after) + defOp->moveBefore(addi); + OpBuilder tagBuilder(whileOp.getContext()); + IntegerAttr blockIdAttr = tagBuilder.getI32IntegerAttr(*firstId); + addi->setAttr(CVPipeline::kBlockId, blockIdAttr); + for (Value operand : addi->getOperands()) + if (Operation *defOp = operand.getDefiningOp()) + defOp->setAttr(CVPipeline::kBlockId, blockIdAttr); + } + return after.getArgument(after.getNumArguments() - 1); + } + + OpBuilder builder(whileOp); + Location loc = whileOp.getLoc(); + auto oldWhile = whileOp; + Type i32Type = builder.getI32Type(); + + // Init counter = 0 + Value zero = builder.create(loc, 0, 32); + + SmallVector newInits(oldWhile.getInits()); + newInits.push_back(zero); + SmallVector newResultTypes(oldWhile.getResultTypes()); + newResultTypes.push_back(i32Type); + + Value counterIterArg; + Operation *counterOne = nullptr; + Operation *counterAdd = nullptr; + + // Rebuild via the Builder callback API (matching InnerScope's + // setupWhileIterArgCounter) + auto newWhile = builder.create( + loc, newResultTypes, newInits, + [&](OpBuilder &bb, Location bl, ValueRange iterArgs) { + Block *oldBefore = oldWhile.getBeforeBody(); + unsigned n = oldBefore->getNumArguments(); + IRMapping map; + for (unsigned i = 0; i < n; ++i) + map.map(oldBefore->getArgument(i), iterArgs[i]); + + for (Operation &op : oldBefore->without_terminator()) + bb.clone(op, map); + + auto oldCond = cast(oldBefore->getTerminator()); + SmallVector condArgs; + for (Value a : oldCond.getArgs()) + condArgs.push_back(map.lookupOrDefault(a)); + condArgs.push_back(iterArgs[n]); // counter + bb.create( + bl, map.lookupOrDefault(oldCond.getCondition()), condArgs); + }, + [&](OpBuilder &ab, Location al, ValueRange iterArgs) { + Block *oldAfter = oldWhile.getAfterBody(); + unsigned n = oldAfter->getNumArguments(); + counterIterArg = iterArgs[n]; + // Counter update at body start; prepareLoopPolling builds the + // polling chain around it (c2+remsi before, c0+cmpi after). + counterOne = ab.create(al, 1, kBits32); + counterAdd = ab.create(al, counterIterArg, + counterOne->getResult(0)); + Value nextCounter = counterAdd->getResult(0); + IRMapping map; + for (unsigned i = 0; i < n; ++i) + map.map(oldAfter->getArgument(i), iterArgs[i]); + + for (Operation &op : oldAfter->without_terminator()) + ab.clone(op, map); + + auto oldYield = cast(oldAfter->getTerminator()); + SmallVector yOps; + for (Value v : oldYield.getOperands()) + yOps.push_back(map.lookupOrDefault(v)); + yOps.push_back(nextCounter); + ab.create(al, yOps); + }); + + // Copy attrs (must include ssbuffer.main_loop) and mark as processed + for (auto attr : oldWhile->getAttrs()) + newWhile->setAttr(attr.getName(), attr.getValue()); + newWhile->setAttr(CVPipeline::kIterCounter, builder.getUnitAttr()); + + // Tag counter-update ops with the first body block_id (CloneOps + // contiguity, mirrors InnerScope); they carry no id yet, so the scan + // picks the first old-body op's id. + Block &afterBody = newWhile.getAfter().front(); + if (std::optional firstBlockId = getFirstBlockId(&afterBody); + firstBlockId && counterOne && counterAdd) { + IntegerAttr blockIdAttr = builder.getI32IntegerAttr(*firstBlockId); + counterOne->setAttr(CVPipeline::kBlockId, blockIdAttr); + counterAdd->setAttr(CVPipeline::kBlockId, blockIdAttr); + counterAdd->setAttr(CVPipeline::kIterCounter, builder.getUnitAttr()); + } + + // Replace results (exclude counter result) + for (unsigned i = 0, e = oldWhile.getNumResults(); i < e; ++i) + oldWhile.getResult(i).replaceAllUsesWith(newWhile.getResult(i)); + oldWhile.erase(); + + return counterIterArg; +} + +/// Create polling condition: (iter / step) % 2 == 0 (true=input, false=output) +static Value createPollingCondition(scf::ForOp forOp, OpBuilder &builder, + int blockId, int tid) { + Location loc = forOp.getLoc(); + Value iterVar = forOp.getInductionVar(); + Value step = forOp.getStep(); + + auto divOp = builder.create(loc, iterVar, step); + setSsbufferTags(divOp.getOperation(), builder, blockId, tid); + + Type counterType = divOp.getResult().getType(); + int bitWidth = counterType.getIntOrFloatBitWidth(); + auto c2Val = builder.create(loc, 2, bitWidth); + setSsbufferTags(c2Val.getOperation(), builder, blockId, tid); + auto remOp = + builder.create(loc, divOp.getResult(), c2Val.getResult()); + setSsbufferTags(remOp.getOperation(), builder, blockId, tid); + + auto c0Val = builder.create(loc, 0, bitWidth); + auto cmpOp = builder.create( + loc, arith::CmpIPredicate::eq, remOp.getResult(), c0Val.getResult()); + setSsbufferTags(cmpOp.getOperation(), builder, blockId, tid); + setSsbufferTags(c0Val.getOperation(), builder, blockId, tid); + + return cmpOp.getResult(); +} + +/// Wrap a sync op (wait/set) in scf.if: then=clone original, else=create +/// alternative +template +static Operation *wrapSyncOpWithScfIf( + Operation *op, Value cond, int outputFlag, OpBuilder &builder, + std::function createAltFn) { + static_assert(std::is_same::value || + std::is_same::value, + "OpTy must be SyncBlockWaitOp or SyncBlockSetOp"); + + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(op); + + Location loc = op->getLoc(); + auto ifOp = builder.create(loc, TypeRange{}, cond, + true /* withElseRegion */); + ifOp->setAttr(mlir::CVPipeline::kBlockId, + builder.getI32IntegerAttr(getBlockId(op))); + ifOp->setAttr("ssbuffer.cross_buffer", builder.getI32IntegerAttr(1)); + + // then branch: clone original op + auto thenBuilder = ifOp.getThenBodyBuilder(); + Operation *cloned = thenBuilder.clone(*op); + + // else branch: create alternative op + auto elseBuilder = ifOp.getElseBodyBuilder(); + Operation *altOp = createAltFn(elseBuilder, loc); + + // Copy ssbuffer tags + int bid = getBlockId(op); + int tid = getTransferId(op); + if (bid >= 0) { + cloned->setAttr(mlir::CVPipeline::kBlockId, builder.getI32IntegerAttr(bid)); + altOp->setAttr(mlir::CVPipeline::kBlockId, builder.getI32IntegerAttr(bid)); + } + if (tid >= 0) { + cloned->setAttr(mlir::CVPipeline::kTransferId, + builder.getI32IntegerAttr(tid)); + altOp->setAttr(mlir::CVPipeline::kTransferId, + builder.getI32IntegerAttr(tid)); + } + if (op->hasAttr("ssbuffer.analyze_flag_id")) { + cloned->setAttr("ssbuffer.analyze_flag_id", builder.getUnitAttr()); + altOp->setAttr("ssbuffer.analyze_flag_id", builder.getUnitAttr()); + } + + op->replaceAllUsesWith(ifOp.getOperation()); + op->erase(); + return ifOp.getOperation(); +} + +/// Wrap transfer op (with external uses) in scf.if with yield: +/// - then: clone with the original buffer operand +/// - else: clone with bufferOperand remapped to the spare buffer +static Operation *wrapTransferOpWithScfIfYield(Operation *transferOp, + Value cond, Value bufferOperand, + Value outputBuffer, int bid, + int tid, bool isProducer, + OpBuilder &builder) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(transferOp); + + Location loc = transferOp->getLoc(); + auto ifOp = builder.create(loc, transferOp->getResultTypes(), cond, + true /* withElseRegion */); + + // then branch: keep the original buffer operand + Operation *thenCloned = nullptr; + { + auto thenBuilder = ifOp.getThenBodyBuilder(); + thenCloned = thenBuilder.clone(*transferOp); + thenBuilder.create(loc, thenCloned->getResults()); + } + + // else branch: use outputBuffer + Operation *elseCloned = nullptr; + { + auto elseBuilder = ifOp.getElseBodyBuilder(); + IRMapping outputMap; + if (bufferOperand) { + outputMap.map(bufferOperand, outputBuffer); + } + elseCloned = elseBuilder.clone(*transferOp, outputMap); + elseBuilder.create(loc, elseCloned->getResults()); + } + + // Producer: tag the cloned transferOps (the actual behavior ops) with + // [tid, 1]. The ifOp wrapper itself does NOT carry crossDeps because + // scf.if is the polling-flow control structure, not the data-movement + // behavior op — the producer role follows the inner fixpipe/copy clones. + if (isProducer) { + auto crossDeps = builder.getArrayAttr( + {builder.getI32IntegerAttr(tid), builder.getI32IntegerAttr(1)}); + thenCloned->setAttr(mlir::CVPipeline::kCrossCoreDeps, crossDeps); + elseCloned->setAttr(mlir::CVPipeline::kCrossCoreDeps, crossDeps); + } + + // Tag the ifOp + ifOp->setAttr(mlir::CVPipeline::kBlockId, builder.getI32IntegerAttr(bid)); + ifOp->setAttr(mlir::CVPipeline::kTransferId, builder.getI32IntegerAttr(tid)); + ifOp->setAttr("ssbuffer.cross_buffer", builder.getI32IntegerAttr(1)); + + // Replace all uses of the original transferOp + for (auto [oldResult, newResult] : + llvm::zip_equal(transferOp->getResults(), ifOp->getResults())) { + oldResult.replaceAllUsesWith(newResult); + } + transferOp->erase(); + return ifOp.getOperation(); +} + +/// Wrap a transfer op (no external uses) in scf.if without yield +static Operation *wrapTransferOpWithScfIfSimple(Operation *transferOp, + Value cond, Value bufferOperand, + Value outputBuffer, int bid, + int tid, bool isProducer, + OpBuilder &builder) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(transferOp); + + Location loc = transferOp->getLoc(); + auto ifOp = builder.create(loc, TypeRange{}, cond, + true /* withElseRegion */); + + // then branch: clone directly (keeps the original buffer operand) + Operation *thenCloned = nullptr; + { + auto thenBuilder = ifOp.getThenBodyBuilder(); + thenCloned = thenBuilder.clone(*transferOp); + } + + // else branch: use outputBuffer + Operation *elseCloned = nullptr; + { + auto elseBuilder = ifOp.getElseBodyBuilder(); + IRMapping outputMap; + if (bufferOperand) { + outputMap.map(bufferOperand, outputBuffer); + } + elseCloned = elseBuilder.clone(*transferOp, outputMap); + } + + // Producer: tag the cloned transferOps (the actual behavior ops) with + // [tid, 1]. Mirror of wrapTransferOpWithScfIfYield: behavior op, not + // the ifOp wrapper, carries the producer role. + if (isProducer) { + auto crossDeps = builder.getArrayAttr( + {builder.getI32IntegerAttr(tid), builder.getI32IntegerAttr(1)}); + thenCloned->setAttr(mlir::CVPipeline::kCrossCoreDeps, crossDeps); + elseCloned->setAttr(mlir::CVPipeline::kCrossCoreDeps, crossDeps); + } + + // Tag the ifOp + ifOp->setAttr(mlir::CVPipeline::kBlockId, builder.getI32IntegerAttr(bid)); + ifOp->setAttr(mlir::CVPipeline::kTransferId, builder.getI32IntegerAttr(tid)); + ifOp->setAttr("ssbuffer.cross_buffer", builder.getI32IntegerAttr(1)); + + transferOp->erase(); + return ifOp.getOperation(); +} + +/// Wrap receiver chain (head + trailing ops + tensor boundary) in scf.if +/// returning tensor +static Operation *wrapReceiverChainWithScfIf(Operation *transferOp, + Operation *toTensorOp, Value cond, + Value bufferOperand, + Value outputBuffer, int bid, + int tid, OpBuilder &builder) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(transferOp); + Location loc = transferOp->getLoc(); + + // Collect the chain from transferOp to toTensorOp: ops whose result flows + // into toTensorOp (e.g. memref.memory_space_cast between convert_layout and + // bufferization.to_tensor for V→C transfers). + SmallVector trailingOps; + Value curVal = transferOp->getResult(0); + while (curVal != toTensorOp->getOperand(0)) { + bool found = false; + for (auto &use : curVal.getUses()) { + Operation *user = use.getOwner(); + if (user->isBeforeInBlock(toTensorOp) || user == toTensorOp) { + curVal = user->getResult(0); + if (user != toTensorOp) + trailingOps.push_back(user); + found = true; + break; + } + } + if (!found) + break; + } + + auto tensorType = toTensorOp->getResult(0).getType(); + auto ifOp = builder.create(loc, tensorType, cond, + true /* withElseRegion */); + + // then branch: keep the original buffer operand → clone chain + boundary + { + auto thenBuilder = ifOp.getThenBodyBuilder(); + IRMapping thenMapper; + Operation *clonedTransfer = thenBuilder.clone(*transferOp, thenMapper); + // Strip crossDeps from the cloned transferOp: clone() inherits attrs + // from the original (which may carry [tid, 0] from upstream tagging), + // but consumer role is owned by the ifOp wrapper below. Inner clone + // must stay clean. + clonedTransfer->removeAttr(mlir::CVPipeline::kCrossCoreDeps); + Value chainResult = clonedTransfer->getResult(0); + thenMapper.map(transferOp->getResult(0), chainResult); + for (Operation *op : trailingOps) { + Operation *cloned = thenBuilder.clone(*op, thenMapper); + cloned->removeAttr(mlir::CVPipeline::kCrossCoreDeps); + thenMapper.map(op->getResult(0), cloned->getResult(0)); + } + Operation *clonedToTensor = thenBuilder.clone(*toTensorOp, thenMapper); + clonedToTensor->removeAttr(mlir::CVPipeline::kCrossCoreDeps); + thenBuilder.create(loc, clonedToTensor->getResult(0)); + } + + // else branch: use outputBuffer → clone chain + boundary + { + auto elseBuilder = ifOp.getElseBodyBuilder(); + IRMapping outputMap; + if (bufferOperand) { + outputMap.map(bufferOperand, outputBuffer); + } + Operation *clonedTransfer = elseBuilder.clone(*transferOp, outputMap); + clonedTransfer->removeAttr(mlir::CVPipeline::kCrossCoreDeps); + Value chainResult = clonedTransfer->getResult(0); + auto elseMapper = outputMap; + elseMapper.map(transferOp->getResult(0), chainResult); + for (Operation *op : trailingOps) { + Operation *cloned = elseBuilder.clone(*op, elseMapper); + cloned->removeAttr(mlir::CVPipeline::kCrossCoreDeps); + elseMapper.map(op->getResult(0), cloned->getResult(0)); + } + Operation *clonedToTensor = elseBuilder.clone(*toTensorOp, elseMapper); + clonedToTensor->removeAttr(mlir::CVPipeline::kCrossCoreDeps); + elseBuilder.create(loc, clonedToTensor->getResult(0)); + } + + // Tag the wrapper — single source of truth for consumer role + ifOp->setAttr(mlir::CVPipeline::kBlockId, builder.getI32IntegerAttr(bid)); + ifOp->setAttr(mlir::CVPipeline::kTransferId, builder.getI32IntegerAttr(tid)); + ifOp->setAttr("ssbuffer.cross_buffer", builder.getI32IntegerAttr(1)); + ifOp->setAttr(mlir::CVPipeline::kCrossCoreDeps, + builder.getArrayAttr({builder.getI32IntegerAttr(tid), + builder.getI32IntegerAttr(0)})); + + // Replace and erase from outermost to innermost to avoid use-after-free + toTensorOp->getResult(0).replaceAllUsesWith(ifOp.getResult(0)); + toTensorOp->erase(); + for (Operation *op : llvm::reverse(trailingOps)) + op->erase(); + transferOp->erase(); + + return ifOp.getOperation(); +} + +/// Process polling for a sender or receiver transfer chain +static LogicalResult processTransferChain(TransferOpChain &chain, Value cond, + Value outputBuffer, int outputFlag, + bool isProducer, OpBuilder &builder) { + if (!chain.waitOp) { + return failure(); + } + + Location loc = chain.waitOp->getLoc(); + + // 1. Wrap waitOp in polling if + chain.waitOp = wrapSyncOpWithScfIf( + chain.waitOp, cond, outputFlag, builder, + [&](OpBuilder &b, Location l) -> Operation * { + auto waitOp = cast(chain.waitOp); + return b + .create(l, waitOp.getTcoreType(), + waitOp.getTpipe(), waitOp.getPipe(), + b.getI64IntegerAttr(outputFlag)) + .getOperation(); + }); + + // 2. Wrap transferOp in polling if (then=original buffer, else=outputBuffer) + if (chain.transferOp) { + int bid = getBlockId(chain.transferOp); + int tid = getTransferId(chain.transferOp); + + // Receiver with boundary op: wrap the whole chain so scf.if yields tensor + if (!isProducer && chain.toTensorOp) { + LDBG("transferOp: " << chain.transferOp->getName() + << " (receiver, wrapping to_tensor)."); + chain.transferOp = wrapReceiverChainWithScfIf( + chain.transferOp, chain.toTensorOp, cond, chain.bufferOperand, + outputBuffer, bid, tid, builder); + chain.toTensorOp = nullptr; + } else { + bool hasExternalUses = !chain.transferOp->getResults().empty() && + !chain.transferOp->getResult(0).getUses().empty(); + + LDBG("transferOp: " << chain.transferOp->getName() + << ", hasExternalUses=" << hasExternalUses << "."); + + chain.transferOp = + hasExternalUses + ? wrapTransferOpWithScfIfYield(chain.transferOp, cond, + chain.bufferOperand, outputBuffer, + bid, tid, isProducer, builder) + : wrapTransferOpWithScfIfSimple(chain.transferOp, cond, + chain.bufferOperand, outputBuffer, + bid, tid, isProducer, builder); + } + } + + // 3. Wrap setOp in polling if + if (chain.setOp) { + chain.setOp = wrapSyncOpWithScfIf( + chain.setOp, cond, outputFlag, builder, + [&](OpBuilder &b, Location l) -> Operation * { + auto setOp = cast(chain.setOp); + return b + .create(l, setOp.getTcoreType(), + setOp.getTpipe(), setOp.getPipe(), + b.getI64IntegerAttr(outputFlag)) + .getOperation(); + }); + } + return success(); +} + +/// Create polling condition and builder for a loop op (ForOp or WhileOp). +/// Returns the condition Value; `builderOut` is set to the insertion point +/// for subsequent wrapping ops (before the loop terminator). +static Value prepareLoopPolling(Operation *loopOp, Operation *waitOp, + OpBuilder &builderOut) { + int bid = getBlockId(waitOp); + int tid = getTransferId(waitOp); + + if (auto forOp = dyn_cast(loopOp)) { + // Build the polling cond at body start so it dominates every group's + // scf.if wrapper (mirrors the while branch). tid comes from the anchor + // wait; the block id from the first body op (CloneOps contiguity). + builderOut.setInsertionPointToStart(forOp.getBody()); + std::optional pollBlockId = getFirstBlockId(forOp.getBody()); + if (!pollBlockId) + pollBlockId = bid; + OpBuilder condBuilder(builderOut); + Value cond = createPollingCondition(forOp, condBuilder, *pollBlockId, tid); + return cond; + } + + if (auto whileOp = dyn_cast(loopOp)) { + // Guard: only main_loop whiles carry an injected counter; a plain + // while here has none to poll on — fall back. + if (!whileOp->hasAttr(CVPipeline::kIterCounter)) + return Value(); + // Polling cond: (counter % 2) == 0. The counter update sits at the + // body head (created / normalized by ensure), so the chain is built + // around it: c2+remsi before, c0+cmpi after. + Block &after = whileOp.getAfter().front(); + Value counter = after.getArgument(after.getNumArguments() - 1); + // Locate the counter update (top-level only; nested loops own theirs). + Operation *lastCounterUpdate = nullptr; + for (Operation &op : after) + if (op.hasAttr(CVPipeline::kIterCounter)) + lastCounterUpdate = &op; + bool counterAtHead = + lastCounterUpdate && (lastCounterUpdate == &after.front() || + lastCounterUpdate == after.front().getNextNode()); + // Insert at body start to dominate the wrapping scf.ifs. + builderOut.setInsertionPointToStart(&after); + // Tag the cond ops with the first body block_id (CloneOps contiguity). + std::optional pollBlockId = getFirstBlockId(&after); + if (!pollBlockId) + pollBlockId = bid; + OpBuilder condBuilder(builderOut); + Operation *c2Op = + condBuilder.create(whileOp.getLoc(), 2, kBits32); + setSsbufferTags(c2Op, condBuilder, *pollBlockId, tid); + Operation *remOp = condBuilder.create( + whileOp.getLoc(), counter, c2Op->getResult(0)); + setSsbufferTags(remOp, condBuilder, *pollBlockId, tid); + // Put c0+cmpi right after the counter update: + // - +1 lands between remsi and cmpi (validated backend ordering) + // - otherwise chain stays contiguous at body start: the cond must + // dominate the wrapping scf.ifs + if (counterAtHead) + condBuilder.setInsertionPointAfter(lastCounterUpdate); + Operation *c0Op = + condBuilder.create(whileOp.getLoc(), 0, kBits32); + setSsbufferTags(c0Op, condBuilder, *pollBlockId, tid); + Operation *condOp = condBuilder.create( + whileOp.getLoc(), arith::CmpIPredicate::eq, remOp->getResult(0), + c0Op->getResult(0)); + setSsbufferTags(condOp, condBuilder, *pollBlockId, tid); + return condOp->getResult(0); + } + + // Unexpected loop type: caller falls back with ERRCODE_IGNORED. + return Value(); +} + +/// Add polling control flow for all transfer groups +/// Get or create the polling condition shared by all groups rotating buffers +/// on one loop. The parity value ((iter%2)==0) is identical for every group, +/// so one scalar condition chain per loop suffices; anchorWait (the earliest +/// wait in the loop) guarantees the cond dominates every group's scf.if. +static Value getOrCreateLoopCond(Operation *loopOp, Operation *anchorWait, + DenseMap &condCache) { + auto it = condCache.find(loopOp); + if (it != condCache.end()) { + return it->second; + } + OpBuilder builder(loopOp->getContext()); + Value cond = prepareLoopPolling(loopOp, anchorWait, builder); + if (cond) { + condCache[loopOp] = cond; + LDBG("Shared polling cond created for loop, tagged by anchor wait."); + } + return cond; +} + +/// Nearest ForOp/WhileOp ancestor of a sync op (sync may sit in scf.if). +/// A While nested in a For resolves to the While, not the For. +static Operation *resolveLoopOp(Operation *op) { + return findAncestorOp(op, [](Operation *cur) { + return isa(cur) || isa(cur); + }); +} + +static LogicalResult +addPollingControlFlow(DenseMap &groups, int &errCode) { + // Anchor per loop: the earliest wait across all groups — the shared cond + // must dominate every group's scf.if wrappers. + DenseMap loopAnchor; + for (auto &[tid, group] : groups) { + auto track = [&](Operation *waitOp) { + if (!waitOp) { + return; + } + Operation *loop = resolveLoopOp(waitOp); + Operation *anchor = loopAnchor.lookup(loop); + if (!anchor || (waitOp->getBlock() == anchor->getBlock() && + waitOp->isBeforeInBlock(anchor))) { + loopAnchor[loop] = waitOp; + } + }; + track(group.senderChain.waitOp); + track(group.receiverChain.waitOp); + } + + DenseMap condCache; + for (auto &[tid, group] : groups) { + // Get sender's loop op (ForOp or WhileOp) + Operation *senderWaitParent = resolveLoopOp(group.senderChain.waitOp); + + // Shared polling condition for the sender loop + OpBuilder senderBuilder(senderWaitParent->getContext()); + Value senderCond = getOrCreateLoopCond( + senderWaitParent, loopAnchor[senderWaitParent], condCache); + if (!senderCond) { + LDBG("FALLBACK: unexpected sender loop op, tid=" + << tid << ", op=" << *senderWaitParent + << ", rc=" << CVPipeline::ERRCODE_IGNORED << "."); + errCode = CVPipeline::ERRCODE_IGNORED; + return failure(); + } + + // Process sender chain (isProducer=true) + if (failed(processTransferChain(group.senderChain, senderCond, + group.senderOutputBuffer, group.outputFlag, + true, senderBuilder))) { + errCode = CVPipeline::ERRCODE_FAILED; + return failure(); + } + + // Process receiver chain (may use different loop op) (isProducer=false) + if (group.receiverChain.waitOp) { + Operation *receiverWaitParent = resolveLoopOp(group.receiverChain.waitOp); + + if (receiverWaitParent == senderWaitParent) { + // Use the same cond and builder + if (failed(processTransferChain( + group.receiverChain, senderCond, group.receiverOutputBuffer, + group.outputFlag, false, senderBuilder))) { + errCode = CVPipeline::ERRCODE_FAILED; + return failure(); + } + } else { + // Receiver uses a different loop op, share its cond too + OpBuilder receiverBuilder(receiverWaitParent->getContext()); + Value receiverCond = getOrCreateLoopCond( + receiverWaitParent, loopAnchor[receiverWaitParent], condCache); + if (!receiverCond) { + LDBG("FALLBACK: unexpected receiver loop op, tid=" + << tid << ", op=" << *receiverWaitParent + << ", rc=" << CVPipeline::ERRCODE_IGNORED << "."); + errCode = CVPipeline::ERRCODE_IGNORED; + return failure(); + } + if (failed(processTransferChain( + group.receiverChain, receiverCond, group.receiverOutputBuffer, + group.outputFlag, false, receiverBuilder))) { + errCode = CVPipeline::ERRCODE_FAILED; + return failure(); + } + } + } + } + return success(); +} + +// ============================================================================ +// Preprocessing: inject iteration counter into WhileOps with main_loop +// ============================================================================ + +/// Inject an i32 iteration counter loop-carried variable into every WhileOp +/// that has main_loop and contains transfer_id ops. Must run BEFORE Step 1 so +/// subsequent data collection sees the already-modified IR. +static void preInjectWhileOpToggles(ModuleOp module) { + SmallVector whileOps; + module.walk([&](scf::WhileOp whileOp) { + if (!CVPipeline::isMainLoopOp(whileOp)) + return; + bool hasTransferOps = false; + whileOp.walk([&](Operation *op) { + if (op->hasAttr(mlir::CVPipeline::kTransferId)) { + hasTransferOps = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (hasTransferOps) + whileOps.push_back(whileOp); + }); + + for (auto whileOp : whileOps) + ensureWhileOpHasCounter(whileOp); + + LDBG("Preprocessed " << whileOps.size() + << " WhileOps with toggle injection."); +} + +// ============================================================================ +// Pass entry point +// ============================================================================ + +void AddMultiBufferOuterScopePass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("============================================================"); + LDBG("Enter AddMultiBufferOuterScope pass."); + LDBG("============================================================"); + + // Determine buffer mode early; only inject toggle for double-buffer + int interCoreBufNum = BufferCountManager(module).getBufferCountByType( + BufferCountManager::DepType::InterCore); + bool isDoubleBuf = (interCoreBufNum > 1); + LDBG("[BufferCount] interCoreBufNum=" << interCoreBufNum + << " doubleBuf=" << isDoubleBuf << "."); + + // Preprocessing: inject iteration counter into WhileOps before data + // collection (only needed for double-buffer polling) + if (isDoubleBuf) { + preInjectWhileOpToggles(module); + } + + // Step 1: Collect transfer group information + LDBG("[Step 1/3] Start: transfer group collection."); + FlagIdManager flagIdMgr(module); + DenseMap> opsByTid; + collectOpsByTransferId(module, opsByTid); + DenseMap groups; + if (collectTransferGroupData(module, opsByTid, flagIdMgr, groups)) { + LDBG("FALLBACK: Step 1/3 failed, no valid transfer groups found, rc=" + << CVPipeline::ERRCODE_FAILED << "."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + LDBG("[Step 1/3] Done: " << groups.size() << " transfer groups."); + + // Flag ID budget: + // 1. Usable ids 0..MAX_FLAG_ID (14); kReservedPipeFlagId (15) is reserved. + // 2. Final max id = largest output flag acquired in Step 1. + // 3. Exceed budget -> keep single-buffer mode. + // 4. Input flag > kReservedPipeFlagId -> fallback rc=2. + std::set usedFlags; + module.walk([&](Operation *op) { + if (isa(op) || isa(op)) { + int f = getFlagFromSyncOp(op); + if (f >= 0) + usedFlags.insert(f); + } + }); + // Input flags: + // 1. Flag id > kReservedPipeFlagId not producible by this pass. + // 2. kReservedPipeFlagId (pipe) itself is allowed. + bool inputOverBudget = false; + for (int f : usedFlags) { + if (f > kReservedPipeFlagId) + inputOverBudget = true; + } + if (inputOverBudget) { + LDBG("FALLBACK: FlagBudget, input flag id > " + << kReservedPipeFlagId << ", rc=" << CVPipeline::ERRCODE_IGNORED + << "."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return; + } + if (isDoubleBuf) { + int maxOutputFlag = -1; + for (auto &p : groups) { + if (p.second.outputFlag > maxOutputFlag) + maxOutputFlag = p.second.outputFlag; + } + LDBG("[FlagBudget] maxOutputFlag=" << maxOutputFlag + << " (usable flag ids 0.." + << FlagIdManager::MAX_FLAG_ID << ")."); + if (maxOutputFlag > FlagIdManager::MAX_FLAG_ID) { + LDBG("FALLBACK: FlagBudget, estimated flag id " + << maxOutputFlag << " exceeds usable range (0.." + << FlagIdManager::MAX_FLAG_ID + << "), fallback to single-buffer mode."); + isDoubleBuf = false; + } + } + + if (isDoubleBuf) { + // Tag llvm.load/store volatile ops with crossDeps + DenseMap> loadStoreByTid; + collectLoadStoreOpsByTransferId(module, loadStoreByTid); + tagLoadStoreOpsWithCrossDeps(loadStoreByTid); + } + + if (isDoubleBuf) { + LDBG("[Step 2/3] Start: output buffer creation."); + if (createOutputBuffers(groups, module)) { + LDBG("FALLBACK: Step 2/3 failed, output buffer creation failed, rc=" + << CVPipeline::ERRCODE_FAILED << "."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + LDBG("[Step 2/3] Done."); + + LDBG("[Step 3/3] Start: polling control flow."); + int pollingRc = CVPipeline::ERRCODE_FAILED; + if (failed(addPollingControlFlow(groups, pollingRc))) { + LDBG("FALLBACK: Step 3/3 failed, polling control flow failed, rc=" + << pollingRc << "."); + CVPipeline::setFallbackAttr(module, pollingRc); + return; + } + LDBG("[Step 3/3] Done."); + } else { + LDBG("[Step 2-3] Skipped (single-buffer mode)."); + } + + LDBG("============================================================"); + LDBG("Exit AddMultiBufferOuterScope pass."); + LDBG("============================================================"); +} + +std::unique_ptr> createAddMultiBufferOuterScopePass() { + return std::make_unique(); +} + +void AddMultiBufferOuterScopePass::getDependentDialects( + DialectRegistry ®istry) const { + registry + .insert(); +} + +void registerAddMultiBufferOuterScopePasses() { + registerPass([]() -> std::unique_ptr { + return createAddMultiBufferOuterScopePass(); + }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow.cpp b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow.cpp new file mode 100644 index 0000000000..63b4d4fe23 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "mlir/Pass/PassManager.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "analyze-data-flow"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(X) LLVM_DEBUG(DBGS() << (X) << "\n") + +using namespace mlir; +using namespace triton; + +void AnalyzeDataFlowPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Enter AnalyzeDataFlow pass."); + + PassManager pm(&getContext(), module.getOperationName()); + + pm.addPass(createAnalyzeNamePass()); + + pm.addPass(createAnalyzeScopePass()); + + pm.addPass(createAnalyzeArgsPass()); + + pm.addPass(createAnalyzeFlagPass()); + + pm.addPass(createAnalyzeCubeContolFLowInputChainPass()); + + pm.addPass(createAnalyzeWhileConditionArgsPass()); + + pm.addPass(createAnalyzeSplitIfPass()); + + if (failed(runPipeline(pm, module))) { + if (!CVPipeline::hasFallbackAttr(module)) { + LDBG("Pass failed; fallback to compilation without dynamic CV pipeline."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + } + + LDBG("Exit AnalyzeDataFlow pass."); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAnalyzeDataFlowPass() { + return std::make_unique(); +} + +void registerAnalyzeDataFlowPasses() { + registerPass(createAnalyzeNamePass); + registerPass(createAnalyzeArgsPass); + registerPass(createAnalyzeFlagPass); + registerPass(createAnalyzeScopePass); + registerPass(createAnalyzeDataFlowPass); + registerPass(createAnalyzeWhileConditionArgsPass); + registerPass(createAnalyzeCubeContolFLowInputChainPass); + registerPass(createAnalyzeSplitIfPass); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeArgs.cpp b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeArgs.cpp new file mode 100644 index 0000000000..779223ece2 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeArgs.cpp @@ -0,0 +1,240 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "analyze-args-in-forOps"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace llvm; +using namespace mlir; +using namespace triton; + +namespace { + +// Data collected for each tensor iter_arg: first block_id that uses it, and set +// of all block_ids +struct TensorArgBlockInfo { + int firstBlockId = -1; + llvm::DenseSet blockIds; +}; + +// Collect block info for all tensor-type iter_args in the given body. +static llvm::DenseMap +collectTensorArgBlockInfo(ArrayRef iterArgs, Block *body) { + llvm::DenseMap result; + + if (!body) { + return result; + } + + for (Operation &op : body->without_terminator()) { + auto blockIdOpt = CVPipeline::getOpBlockId(&op); + if (!blockIdOpt) + continue; + int blockId = *blockIdOpt; + + for (OpOperand &operand : op.getOpOperands()) { + int argIdx = CVPipeline::getTensorIterArgIndex(operand.get(), iterArgs); + if (argIdx >= 0) { + auto &info = result[argIdx]; + info.blockIds.insert(blockId); + if (info.firstBlockId < 0) + info.firstBlockId = blockId; + } + } + } + + return result; +} + +// Check if any tensor-type iter_arg appears in different block_ids. +static bool checkMultiBlockUse( + const llvm::DenseMap &argBlockInfo) { + for (auto &p : argBlockInfo) { + if (p.second.blockIds.size() > 1) + return true; + } + return false; +} + +// Check if the first block_id differs from the block_id of yield's defining op. +static bool checkUseUpdateMismatch( + ArrayRef iterArgs, Block *body, + const llvm::DenseMap &argBlockInfo) { + if (!body) + return false; + + for (unsigned i = 0; i < iterArgs.size(); ++i) { + if (!isa(iterArgs[i].getType())) + continue; + + auto it = argBlockInfo.find(i); + if (it == argBlockInfo.end()) + continue; + + Operation *defOp = CVPipeline::getLoopCarriedDefOp(iterArgs[i], body); + if (!defOp) + continue; + + if (CVPipeline::getOpBlockId(defOp) != it->second.firstBlockId) + return true; + } + return false; +} + +// Main check function that combines both conditions +static bool hasTensorArgInDifferentBlockIds(ArrayRef iterArgs, + Block *body) { + auto argBlockInfo = collectTensorArgBlockInfo(iterArgs, body); + return checkMultiBlockUse(argBlockInfo) || + checkUseUpdateMismatch(iterArgs, body, argBlockInfo); +} + +// Detect "update-before-use": a tensor iter_arg's update-defining op +// appears in code order BEFORE another direct-child use of that iter_arg +// whose ssbuffer.block_id differs from the update's block_id. +static bool checkUpdateBeforeUse(ArrayRef iterArgs, Block *body) { + if (!body) + return false; + auto yieldOp = dyn_cast(body->getTerminator()); + if (!yieldOp) + return false; + + for (Value iterArg : iterArgs) { + if (!isa(iterArg.getType())) + continue; + + Operation *defOp = CVPipeline::getLoopCarriedDefOp(iterArg, body); + if (!defOp) + continue; + + auto defBlockId = CVPipeline::getOpBlockId(defOp); + if (!defBlockId) + continue; + + for (Operation *user : iterArg.getUsers()) { + Operation *directChild = body->findAncestorOpInBlock(*user); + if (!directChild) + continue; + // must come AFTER the update-defining op in code order + if (!defOp->isBeforeInBlock(directChild)) + continue; + + if (CVPipeline::getOpBlockId(directChild) != defBlockId) + return true; + } + } + return false; +} + +} // namespace + +bool checkTensorArgsInMainLoop(ModuleOp module) { + return module + .walk([&](Operation *op) -> WalkResult { + if (!CVPipeline::isMainLoopOp(op)) + return WalkResult::advance(); + + CVPipeline::MainLoop mainLoop(op); + return hasTensorArgInDifferentBlockIds(mainLoop.getIterArgs(), + mainLoop.getBody()) + ? WalkResult::interrupt() + : WalkResult::advance(); + }) + .wasInterrupted(); +} + +bool checkUpdateBeforeUseInMainLoop(ModuleOp module) { + return module + .walk([&](Operation *op) -> WalkResult { + if (!CVPipeline::isMainLoopOp(op)) + return WalkResult::advance(); + + CVPipeline::MainLoop mainLoop(op); + return checkUpdateBeforeUse(mainLoop.getIterArgs(), mainLoop.getBody()) + ? WalkResult::interrupt() + : WalkResult::advance(); + }) + .wasInterrupted(); +} + +void AnalyzeArgsPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Before AnalyzeArgs:\n" << module << "\n"); + + BufferCountManager bufferCountMgr(module); + int intraBufNum = bufferCountMgr.getBufferCountByType( + BufferCountManager::DepType::IntraCore); + int interBufNum = bufferCountMgr.getBufferCountByType( + BufferCountManager::DepType::InterCore); + + if (checkUpdateBeforeUseInMainLoop(module)) { + LDBG("Found tensor iter_args update-before-use in mainloop!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return; + } + + if (intraBufNum == 3 && interBufNum == 2 && + checkTensorArgsInMainLoop(module)) { + LDBG("Found tensor iter_args dependency in mainloop!"); + CVPipeline::setFallbackAttr(module, + CVPipeline::ERRCODE_TUPLE_PRELOAD_FAILED); + return; + } + + LDBG("After AnalyzeArgs:\n" << module << "\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAnalyzeArgsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeCubeContolFLowInputChain.cpp b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeCubeContolFLowInputChain.cpp new file mode 100644 index 0000000000..0d8a60adc6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeCubeContolFLowInputChain.cpp @@ -0,0 +1,187 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Debug.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/Support/LLVM.h" + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" + +static constexpr const char *DEBUG_TYPE = + "analyze-cube-control-flow-input-chain"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace llvm; +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +namespace { + +static bool isCubeScope(scope::ScopeOp scopeOp) { + auto coreTypeAttr = + scopeOp->getAttrOfType(hivm::TCoreTypeAttr::name); + if (!coreTypeAttr) { + return false; + } + return coreTypeAttr.getTcoretype() == hivm::TCoreType::CUBE; +} + +static bool isControlFlowOp(Operation *op) { + return llvm::isa(op->getDialect()); +} + +static bool hasIncompatibleOpForCondition(Value val, + llvm::DenseSet &visited); + +static inline bool hasIncompatibleUpstream(ValueRange operands, + llvm::DenseSet &visited) { + return llvm::any_of(operands, [&](Value operand) { + return hasIncompatibleOpForCondition(operand, visited); + }); +} + +static bool hasIncompatibleOpForCondition(Value val, + llvm::DenseSet &visited) { + if (visited.contains(val)) { + return false; + } + visited.insert(val); + + Operation *defOp = val.getDefiningOp(); + if (!defOp) { + return false; + } + if (isVectorOnlyOp(defOp)) { + LDBG("Fallback reason: incompatible upstream op for control flow: " + << *defOp); + return true; + } + return hasIncompatibleUpstream(defOp->getOperands(), visited); +} + +static bool checkControlFlowOpInputs(Operation *cfOp) { + llvm::SmallVector scalarOperands; + llvm::TypeSwitch(cfOp) + .Case([&](scf::IfOp ifOp) { + scalarOperands.push_back(ifOp.getCondition()); + }) + .Case([&](scf::ForOp forOp) { + scalarOperands.append( + {forOp.getLowerBound(), forOp.getUpperBound(), forOp.getStep()}); + }) + .Case([&](scf::WhileOp whileOp) { + // while op is very complicated, all loop-carried vars may influence + // conditions, conservatively take all args + auto operands = whileOp->getOperands(); + scalarOperands.append(operands.begin(), operands.end()); + }) + .Default([&](Operation *op) { + // user passed in unknown op, conservatively take all operands + auto operands = op->getOperands(); + scalarOperands.append(operands.begin(), operands.end()); + }); + + llvm::DenseSet visited; + return hasIncompatibleUpstream(scalarOperands, visited); +} + +bool checkCubeControlFlowInputChain(ModuleOp module) { + bool shouldReturn = false; + + module.walk([&](scope::ScopeOp scopeOp) -> WalkResult { + if (!isCubeScope(scopeOp)) { + return WalkResult::advance(); + } + LDBG("Found CUBE scope"); + + scopeOp.walk([&](Operation *op) -> WalkResult { + if (!isControlFlowOp(op)) { + return WalkResult::advance(); + } + LDBG("Found control flow op in CUBE scope"); + + if (checkControlFlowOpInputs(op)) { + shouldReturn = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (shouldReturn) { + return WalkResult::interrupt(); + } + + return WalkResult::advance(); + }); + + return shouldReturn; +} + +} // namespace + +void AnalyzeCubeControlFlowInputChainPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Enter AnalyzeCubeControlFlowInputChainPass."); + + if (checkCubeControlFlowInputChain(module)) { + setFallbackAttr(module, ERRCODE_IGNORED); + return; + } + + LDBG("Exit AnalyzeCubeControlFlowInputChainPass."); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> +createAnalyzeCubeContolFLowInputChainPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeFlag.cpp b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeFlag.cpp new file mode 100644 index 0000000000..b5a9fa5bf6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeFlag.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "analyze-flag"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace llvm; +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +namespace { + +static constexpr const char *syncFlagIdAttr = "static_flag_id"; + +static bool checkFlagIdValidity(ModuleOp module) { + bool shouldReturn = false; + int invalidFlagNum = 0; + + module.walk([&](Operation *op) -> WalkResult { + if (!isa(op) && !isa(op)) { + return WalkResult::advance(); + } + + if (auto intAttr = op->getAttrOfType(syncFlagIdAttr)) { + int flag = static_cast(intAttr.getInt()); + if (flag < 0 || flag > 14) { + shouldReturn = true; + invalidFlagNum++; + } + } + + return WalkResult::advance(); + }); + + if (shouldReturn) { + LDBG("[warning]: flag_id is not enough for transfer, invalidFlagNum: " + << invalidFlagNum << "\n"); + } + return shouldReturn; +} + +} // namespace + +void AnalyzeFlagPass::runOnOperation() { + ModuleOp module = getOperation(); + + LDBG("Before AnalyzeFlag:\n" << module << "\n"); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + if (checkFlagIdValidity(module)) { + setFallbackAttr(module, ERRCODE_IGNORED); + return; + } + + LDBG("After AnalyzeFlag:\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAnalyzeFlagPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeName.cpp b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeName.cpp new file mode 100644 index 0000000000..ab6a414de5 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeName.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "analyze-name"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace mlir; +using namespace triton; + +namespace { + +static constexpr llvm::StringLiteral interceptrFunc[]{ + "kernel_sdpa_bwd_kv", + "_sparse_decode_kernel", + "_sparse_decode_model1_kernel", +}; + +static LogicalResult verifyFuncNames(ModuleOp module) { + bool intercepted = false; + + module.walk([&](func::FuncOp funcOp) -> WalkResult { + if (!llvm::is_contained(interceptrFunc, funcOp.getSymName())) { + return WalkResult::advance(); + } + + LDBG("[INFO]: DynamicCVPipeline is interrupted by function name: " + << funcOp.getSymName()); + intercepted = true; + return WalkResult::interrupt(); + }); + + if (!intercepted) { + return success(); + } + + return failure(); +} + +} // namespace + +void AnalyzeNamePass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Before AnalyzeName:\n" << module << "\n"); + + if (failed(verifyFuncNames(module))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return; + } + + LDBG("After AnalyzeName:\n" << module << "\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAnalyzeNamePass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeScope.cpp b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeScope.cpp new file mode 100644 index 0000000000..68fe726ab0 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeScope.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "analyze-scope"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace llvm; +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +namespace { +static bool isVectorScope(scope::ScopeOp scopeOp) { + auto coreTypeAttr = + scopeOp->getAttrOfType(hivm::TCoreTypeAttr::name); + if (!coreTypeAttr) { + return false; + } + return coreTypeAttr.getTcoretype() == hivm::TCoreType::VECTOR; +} + +static bool checkTransferInteraction(mlir::Operation *op) { + bool hasCVInteraction = false; + // check v->c data interaction + if (isa(op)) { + hasCVInteraction = true; + } + // check c->v data interaction + // user with "ssbuffer.add_from_matmul" is not a real c->v data interaction + if (isa(op)) { + for (Operation *user : op->getUsers()) { + if (!user->hasAttr(CVPipeline::kAddFromMatmul)) { + hasCVInteraction = true; + break; + } + for (Operation *userUser : user->getUsers()) { + if (!isa(userUser)) { + hasCVInteraction = true; + break; + } + } + } + } + + return hasCVInteraction; +} + +static bool checkVecScopeMainLoop(ModuleOp module) { + bool hasMainLoop = false; + bool allMainLoopsSatisfy = true; + + module.walk([&](scope::ScopeOp scopeOp) -> WalkResult { + if (!isVectorScope(scopeOp)) { + return WalkResult::advance(); + } + + scopeOp.walk([&](Operation *op) -> WalkResult { + if (!isMainLoopOp(op)) { + return WalkResult::advance(); + } + + hasMainLoop = true; + bool hasCVInteraction = false; + + op->walk([&](mlir::Operation *innerOp) -> WalkResult { + if (innerOp == op) { + return WalkResult::advance(); + } + + // ops with "ssbuffer.transfer_id" are injected in SplitDataflowPass for + // data transfer + if (innerOp->hasAttr(CVPipeline::kTransferId)) { + hasCVInteraction = checkTransferInteraction(innerOp); + if (hasCVInteraction) { + return WalkResult::interrupt(); + } + } + + return WalkResult::advance(); + }); + + // As long as there is a for/while with "ssbuffer.main_loop" that is not a + // real mainloop, the processing conditions are not met, need to skip. + if (!hasCVInteraction) { + allMainLoopsSatisfy = false; + return WalkResult::interrupt(); + } + + return WalkResult::advance(); + }); + + if (!allMainLoopsSatisfy) { + return WalkResult::interrupt(); + } + + return WalkResult::advance(); + }); + + return hasMainLoop && allMainLoopsSatisfy; +} + +static bool isFixpipeToUB(Operation *op) { + auto fixpipeOp = dyn_cast(op); + if (!fixpipeOp) + return false; + auto dstType = dyn_cast(fixpipeOp.getDst().getType()); + if (!dstType) + return false; + auto addrSpaceAttr = + dyn_cast_or_null(dstType.getMemorySpace()); + if (!addrSpaceAttr) + return false; + + return addrSpaceAttr.getAddressSpace() == hivm::AddressSpace::UB; +} + +// For every main_loop id, gather all for/while ops sharing that id and count +// the hivm.hir.copy ops and the hivm.hir.fixpipe ops writing to UB within +// them. Only when ALL main_loop ids have either count equal to zero (every id +// has only copy or only fixpipe, none has both), the dynamic CV pipeline +// cannot be applied and we fall back to the original workflow. +// - hivm::CopyOp typically appears in VECTOR scope main_loops +// - hivm::FixpipeOp typically appears in CUBE scope main_loops +// Nested regions inside the main_loop op are also walked, and scf.yield +// terminators are skipped. +static bool isMainLoopOnlyCopyOrFixpipe(ModuleOp module) { + // main_loop id -> (countCopy, countFixpipe) + llvm::DenseMap> idToCounts; + + module.walk([&](Operation *op) -> WalkResult { + if (!isa(op)) { + return WalkResult::advance(); + } + auto mainLoopAttr = op->getAttrOfType(CVPipeline::kMainLoop); + if (!mainLoopAttr) { + return WalkResult::advance(); + } + + int id = mainLoopAttr.getInt(); + auto &counts = idToCounts[id]; + + op->walk([&](mlir::Operation *innerOp) -> WalkResult { + // Skip the loop op itself (the walk visits it first) + if (innerOp == op) { + return WalkResult::advance(); + } + // Skip yield terminators (they are not real ops) + if (isa(innerOp)) { + return WalkResult::advance(); + } + if (isa(innerOp)) { + ++counts.first; + } else if (isFixpipeToUB(innerOp)) { + ++counts.second; + } + return WalkResult::advance(); + }); + + return WalkResult::advance(); + }); + + // Only trigger fallback when EVERY main_loop id has only copy or only + // fixpipe (one count is zero). If at least one id has both copy and + // fixpipe ops, the dynamic CV pipeline still be applicable. + if (idToCounts.empty()) { + return false; + } + + for (const auto &entry : idToCounts) { + if (entry.second.first != 0 && entry.second.second != 0) { + return false; + } + } + + return true; +} + +static LogicalResult verifyMainLoop(ModuleOp module) { + bool hasMainLoopOp = false; + module.walk([&](Operation *op) { + if (isMainLoopOp(op)) { + hasMainLoopOp = true; + } + }); + + if (!hasMainLoopOp) { + LDBG("[INFO]: No cycle of multiple iterations, the DynamicCVPipeline pass " + "will be interrupted, and resumed to the original workflow."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return failure(); + } + + if (!checkVecScopeMainLoop(module)) { + LDBG("[INFO]: No op beside matmul add in vector main loop."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return failure(); + }; + + if (isMainLoopOnlyCopyOrFixpipe(module)) { + LDBG("[INFO]: One-way CV interaction for fallback."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return failure(); + } + + return success(); +} + +} // namespace + +void AnalyzeScopePass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Before AnalyzeScope:\n" << module << "\n"); + + if (failed(verifyMainLoop(module))) { + return; + } + + LDBG("After AnalyzeScope:\n" << module << "\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAnalyzeScopePass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeSplitIf.cpp b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeSplitIf.cpp new file mode 100644 index 0000000000..0e6161b653 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeSplitIf.cpp @@ -0,0 +1,224 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/Support/WalkResult.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLFunctionalExtras.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "analyze-split-if"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace llvm; +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +namespace { + +static bool isVectorScope(scope::ScopeOp scopeOp) { + auto coreTypeAttr = + scopeOp->getAttrOfType(hivm::TCoreTypeAttr::name); + return coreTypeAttr && coreTypeAttr.getTcoretype() == hivm::TCoreType::VECTOR; +} + +static bool isCubeScope(scope::ScopeOp scopeOp) { + auto coreTypeAttr = + scopeOp->getAttrOfType(hivm::TCoreTypeAttr::name); + return coreTypeAttr && coreTypeAttr.getTcoretype() == hivm::TCoreType::CUBE; +} + +// A main-loop restricted to scf.for (not scf.while): the op must both carry +// ssbuffer.main_loop and be an scf.for. +static bool isForMainLoopOp(Operation *op) { + return isa(op) && op->hasAttr(kMainLoop); +} + +struct MainLoopScan { + bool hasComputeOutsideSplittedIf = false; + llvm::DenseSet splittedIfTags; +}; + +// Walk `mainLoopOp` and: +// 1. Collect every ssbuffer.splitted_if tag attached to a nested scf.if. +// 2. Detect any "interesting" op (per `isInteresting`) that lives outside a +// splitted_if. Such an op means the split-if is NOT the only compute, so +// the rollback scenario does not apply. +static MainLoopScan +scanMainLoop(Operation *mainLoopOp, + llvm::function_ref isInteresting) { + MainLoopScan scan; + + mainLoopOp->walk([&](Operation *op) -> WalkResult { + if (op == mainLoopOp) { + return WalkResult::advance(); + } + if (op->getParentOp() != mainLoopOp) { + return WalkResult::advance(); + } + if (isa(op)) { + return WalkResult::advance(); + } + + if (auto ifOp = dyn_cast(op)) { + if (auto tag = ifOp->getAttrOfType(kSplittedIf)) { + scan.splittedIfTags.insert(static_cast(tag.getInt())); + } else { + scan.hasComputeOutsideSplittedIf = true; + } + return WalkResult::skip(); + } + + if (isInteresting(op)) { + scan.hasComputeOutsideSplittedIf = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return scan; +} + +static bool isMatmulOp(Operation *op) { return isa(op); } + +// "Real" tensor compute: arithmetic/reduction compute on tensors, excluding +// pure data-movement / control-flow-glue ops that are not overlap-able +// compute. isTensorComputeOp already drops copy/broadcast/fill; we further +// drop linalg.transpose (a data permutation) and arith.select (control-flow +// glue selecting between a splitted_if result and the carried value), which +// the split-if itself emits around the splitted_ifs. +static bool isRealTensorComputeOp(Operation *op) { + if (!CVPipeline::isTensorComputeOp(op)) { + return false; + } + if (isa(op)) { + return false; + } + return true; +} + +// Roll back when ALL of the following hold: +// - VECTOR scf.for main loops contain tensor compute ops ONLY inside +// splitted_ifs. +// - CUBE scf.for main loops contain matmuls ONLY inside splitted_ifs. +// - All splitted_if ops on VECTOR share a single tag value, all on CUBE +// share a single tag value, and the two values are the same (i.e. there +// is exactly one split-if pair, appearing on both cores). +static bool shouldRollbackSplitIf(ModuleOp module) { + llvm::DenseSet allTags; + bool vectorHasExtra = false; + bool cubeHasExtra = false; + + module.walk([&](scope::ScopeOp scopeOp) -> WalkResult { + bool isVector = isVectorScope(scopeOp); + bool isCube = isCubeScope(scopeOp); + if (!isVector && !isCube) { + return WalkResult::advance(); + } + + scopeOp.walk([&](Operation *op) -> WalkResult { + if (!isForMainLoopOp(op)) { + return WalkResult::advance(); + } + // Only consider main loops that live directly in this scope, so a main + // loop nested in a child scope is not misattributed to this one. + if (op->getParentOfType() != scopeOp) { + return WalkResult::advance(); + } + + if (isVector) { + auto scan = scanMainLoop(op, isRealTensorComputeOp); + if (scan.hasComputeOutsideSplittedIf) { + vectorHasExtra = true; + return WalkResult::interrupt(); + } + allTags.insert(scan.splittedIfTags.begin(), scan.splittedIfTags.end()); + } else { + auto scan = scanMainLoop(op, isMatmulOp); + if (scan.hasComputeOutsideSplittedIf) { + cubeHasExtra = true; + return WalkResult::interrupt(); + } + allTags.insert(scan.splittedIfTags.begin(), scan.splittedIfTags.end()); + } + return WalkResult::advance(); + }); + + if (vectorHasExtra || cubeHasExtra) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (vectorHasExtra || cubeHasExtra) { + return false; + } + // Each core must carry exactly one splitted_if value, shared by both. This + // also rejects the no-split-if case (sizes 0) and any multi-split-if case. + if (allTags.size() != 1) { + return false; + } + return true; +} + +} // namespace + +void AnalyzeSplitIfPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Before AnalyzeSplitIf:\n" << module << "\n"); + + if (shouldRollbackSplitIf(module)) { + LDBG("split-if is the only compute in both VECTOR and CUBE " + "main loops; rollback to original workflow."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return; + } + + LDBG("After AnalyzeSplitIf:\n" << module << "\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAnalyzeSplitIfPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeWhileConditionArgs.cpp b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeWhileConditionArgs.cpp new file mode 100644 index 0000000000..a0624856f0 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/AnalyzeDataFlow/AnalyzeWhileConditionArgs.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "analyze-while-condition-args"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__; \ + llvm::dbgs() << "\n"; \ + }) + +using namespace llvm; +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +// Expand a block argument reached by the backward trace into the values that +// update it per iteration: +// - a before-region arg is updated by the after-region scf.yield operand of +// the same index (the init value enters once from outside and is not +// traced); +// - an after-region arg is the value forwarded by scf.condition at the same +// index. +// Args of enclosing ops (outer loops, function) are outside the while's +// update process and are not expanded. +static void expandBlockArgument(scf::WhileOp whileOp, BlockArgument blockArg, + SmallVectorImpl &worklist) { + Block *owner = blockArg.getOwner(); + unsigned argIdx = blockArg.getArgNumber(); + + if (owner == whileOp.getBeforeBody()) { + worklist.push_back(whileOp.getYieldOp()->getOperand(argIdx)); + return; + } + + if (owner == whileOp.getAfterBody()) { + worklist.push_back(whileOp.getConditionOp().getArgs()[argIdx]); + return; + } +} + +// Expand an op result reached by the backward trace into the op's operands. +// Computation outside the while op is not traced through. For region-holding +// ops (scf.if/for/while nested in the loop), the results are produced by the +// region terminators, so their operands are conservatively traced too. +static void expandOpResult(scf::WhileOp whileOp, OpResult result, + SmallVectorImpl &worklist) { + Operation *op = result.getOwner(); + + if (!whileOp->isProperAncestor(op)) { + return; + } + + worklist.append(op->operand_begin(), op->operand_end()); + + for (Region ®ion : op->getRegions()) { + for (Block &block : region) { + if (Operation *terminator = block.getTerminator()) { + worklist.append(terminator->operand_begin(), terminator->operand_end()); + } + } + } +} + +// Returns true when `value` is a non-scalar (tensor) value produced inside +// `whileOp`, either by an op nested in one of its regions or as a loop-carried +// arg of the while itself or of a nested loop. +// +// A tensor defined outside the loop is not a problem: its computation stays +// out of the scope that has to be cloned, and only the scalar extracted from +// it takes part in the condition update chain. +static bool isNonScalarProducedInside(scf::WhileOp whileOp, Value value) { + if (!isa(value.getType())) { + return false; + } + + if (auto blockArg = dyn_cast(value)) { + Operation *parentOp = blockArg.getOwner()->getParentOp(); + return parentOp && (parentOp == whileOp.getOperation() || + whileOp->isProperAncestor(parentOp)); + } + + return whileOp->isProperAncestor(value.getDefiningOp()); +} + +// Returns true when the condition of `whileOp` transitively depends on a +// non-scalar (tensor) value produced inside the loop. +// +// The trace starts from the condition operand of scf.condition and walks the +// use-def chain backwards, following loop-carried args across both regions. +static bool conditionDependsOnTensor(scf::WhileOp whileOp) { + SmallVector worklist{whileOp.getConditionOp().getCondition()}; + llvm::DenseSet visited; + + while (!worklist.empty()) { + Value value = worklist.pop_back_val(); + if (!visited.insert(value).second) { + continue; + } + + if (isNonScalarProducedInside(whileOp, value)) { + LDBG("condition of " << *whileOp + << " depends on tensor value: " << value); + return true; + } + + if (auto blockArg = dyn_cast(value)) { + expandBlockArgument(whileOp, blockArg, worklist); + } else { + expandOpResult(whileOp, cast(value), worklist); + } + } + return false; +} + +void AnalyzeWhileConditionArgsPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + WalkResult walkResult = module.walk([&](scf::WhileOp whileOp) -> WalkResult { + if (CVPipeline::isMainLoopOp(whileOp) && + conditionDependsOnTensor(whileOp)) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (walkResult.wasInterrupted()) { + LDBG("scf.while condition depends on non-scalar data, the " + "DynamicCVPipeline pass will be interrupted."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + } +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAnalyzeWhileConditionArgsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/CMakeLists.txt b/third_party/ascend/lib/DynamicCVPipeline/CMakeLists.txt new file mode 100644 index 0000000000..b4e83777c9 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/CMakeLists.txt @@ -0,0 +1,78 @@ +file(GLOB STANDARDIZE_OP_PASS_SRC CONFIGURE_DEPENDS + "StandardizeOp/*.cpp" +) +file(GLOB PLAN_COMPUTE_BLOCK_PASS_SRC CONFIGURE_DEPENDS + "PlanComputeBlock/*.cpp" +) +file(GLOB COMMON_SRC CONFIGURE_DEPENDS + "Common/*.cpp" +) +file(GLOB SPLIT_DATAFLOW_SRC_FILES CONFIGURE_DEPENDS + "SplitDataflow/*.cpp" +) +file(GLOB SEPARATE_MEMORY_FROM_COMPUTE_SRC_FILES CONFIGURE_DEPENDS + "SeparateMemoryFromCompute/*.cpp" +) + +file(GLOB ALLOC_MULTI_CACHE_SRCS_FILES CONFIGURE_DEPENDS + "AllocMultiCache/*.cpp" +) + +file(GLOB ADD_CONTROLFLOW_CONDITION_SRC_FILES CONFIGURE_DEPENDS + "AddControlFlowCondition/*.cpp" +) + +file(GLOB Compute_BLOCK_OPT_SRC_FILES CONFIGURE_DEPENDS + "ComputeBlockOpt/*.cpp" + "ComputeBlockOpt/SplitIfByBlockId/*.cpp" +) + +file(GLOB ANALYZE_DATAFLOW_SRC_FILES CONFIGURE_DEPENDS + "AnalyzeDataFlow/*.cpp" +) + +file(GLOB PRE_CHECK_AVAILABLE_SRC_FILES CONFIGURE_DEPENDS + "PreCheckAvailable/*.cpp" +) + +add_triton_library(DynamicCVPipeline + AddDynamicCVPipeline.cpp + PreCheckAvailable.cpp + + StandardizeOp.cpp + ${STANDARDIZE_OP_PASS_SRC} + + PlanComputeBlock.cpp + ComputeBlockOptPass.cpp + AnalyzeDataFlow.cpp + ${PLAN_COMPUTE_BLOCK_PASS_SRC} + ${COMMON_SRC} + ${Compute_BLOCK_OPT_SRC_FILES} + ${ANALYZE_DATAFLOW_SRC_FILES} + ${PRE_CHECK_AVAILABLE_SRC_FILES} + SplitDataflow.cpp + SeparateMemoryFromComputePass.cpp + SeparateMemoryFromCompute/MarkGMLoadPass.cpp + AllocMultiCache.cpp + AddControlFlowCondition.cpp + ${PLAN_COMPUTE_BLOCK_SRC_FILES} + ${SPLIT_DATAFLOW_SRC_FILES} + ${SEPARATE_MEMORY_FROM_COMPUTE_SRC_FILES} + ${ALLOC_MULTI_CACHE_SRCS_FILES} + ${ADD_CONTROLFLOW_CONDITION_SRC_FILES} + RemoveAttributes.cpp + + DEPENDS + AddDynamicCVPipelinePassIncGen + + LINK_LIBS + BiShengIRHIVMDialect + BiShengIRScopeDialect + BiShengIRAnnotationDialect + MLIRLinalgTransformOps + MLIRLinalgTransforms + MLIRSCFTransformOps + MLIRTransformDialect + MLIRTransformDialectTransforms + MLIRTransforms +) diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/BlockDependencyGraph.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/BlockDependencyGraph.cpp new file mode 100644 index 0000000000..177ff4c7da --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/BlockDependencyGraph.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/BlockDependencyGraph.h" +#include "DynamicCVPipeline/Common/DependencyHelper.h" +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "llvm/Support/Debug.h" +#include + +using namespace mlir; +using namespace CVPipeline; + +static constexpr const char *DEBUG_TYPE = "block-dependency-graph"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) LLVM_DEBUG(DBGS() << __VA_ARGS__ << "\n") + +static bool isCubeSimpleOpOrCf(Operation *op) { + return !isSyncOp(op) && getCoreTypeOfSimpleOpOrCf(op) == CoreType::CUBE_ONLY; +} + +BlockDependencyGraph::BlockDependencyGraph( + Block *block, const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) + : block(block), memGraph(memGraph), bm(bm) {} + +llvm::LogicalResult BlockDependencyGraph::buildGraph() { + // Step 1: Collect all compute blocks (including cube and vector) in current + // MLIR Block + for (Operation &op : *block) { + int blockId = bm.getBlockIdByOp(&op); + if (blockId == -1) + continue; + + if (!blockNodes.contains(blockId)) { + auto node = std::make_unique(); + node->blockId = blockId; + node->isCube = isCubeSimpleOpOrCf(&op); + node->depth = 0; + blockNodes[blockId] = std::move(node); + } + + blockNodes[blockId]->ops.push_back(&op); + } + + LDBG("Collected " << blockNodes.size() << " blocks"); + + // Step 2: Build dependency relationships between blocks (only in current MLIR + // Block) + DependencyHelper depHelper(memGraph); + for (auto &entry : blockNodes) { + BlockNode *consumerNode = entry.second.get(); + + for (Operation *op : consumerNode->ops) { + // Use DependencyHelper to iterate all source ops (SSA defs + memory + // exec-before) in one pass. + depHelper.forEachSource( + op, [&](Operation *source) { + if (!isInCurrentBlock(source)) + return; + + int sourceBlockId = bm.getBlockIdByOp(source); + if (sourceBlockId == -1 || sourceBlockId == consumerNode->blockId) + return; + + BlockNode *producerNode = getBlockNode(sourceBlockId); + if (producerNode) + addEdge(producerNode, consumerNode); + }); + } + } + + // Step 3: Compute depths (for all blocks) + computeDepths(); + + return llvm::success(); +} + +bool BlockDependencyGraph::isInCurrentBlock(Operation *op) { + if (!op) + return false; + + // Get the MLIR Block where op is located + Block *opBlock = op->getBlock(); + + // Check if it's in current MLIR Block + return opBlock == block; +} + +void BlockDependencyGraph::addEdge(BlockNode *from, BlockNode *to) { + to->predecessors.insert(from); + from->successors.insert(to); +} + +void BlockDependencyGraph::computeDepths() { + // Use topological sort to compute depth for each node + // Depth definition: maximum steps from nodes with zero in-degree + // Special rule: depth only increases when cube and vector type conversion + // occurs + + llvm::DenseMap indegree; + std::queue queue; + + // Initialize in-degree directly from each node's predecessor set. + for (auto &entry : blockNodes) { + BlockNode *node = entry.second.get(); + indegree[node] = node->predecessors.size(); + if (indegree[node] == 0) { + queue.push(node); + node->depth = 0; + } + } + + // BFS to compute depths + while (!queue.empty()) { + BlockNode *current = queue.front(); + queue.pop(); + + for (BlockNode *succ : current->successors) { + // Compute depth from current to succ + // If types are different (cube->vector or vector->cube), depth+1 + // If types are the same, depth remains unchanged + int newDepth = current->depth; + if (current->isCube != succ->isCube) { + newDepth++; + } + + // Update successor's depth (take maximum) + succ->depth = std::max(succ->depth, newDepth); + + indegree[succ]--; + if (indegree[succ] == 0) { + queue.push(succ); + } + } + } + + LDBG("Computed depths for all blocks"); +} + +int BlockDependencyGraph::getDepth(int blockId) { + auto it = blockNodes.find(blockId); + if (it != blockNodes.end()) { + return it->second->depth; + } + return -1; +} + +BlockNode *BlockDependencyGraph::getBlockNode(int blockId) { + auto it = blockNodes.find(blockId); + if (it != blockNodes.end()) { + return it->second.get(); + } + return nullptr; +} + +llvm::SmallVector +BlockDependencyGraph::getPredecessors(BlockNode *node) { + if (!node) + return {}; + return llvm::SmallVector(node->predecessors.begin(), + node->predecessors.end()); +} + +llvm::SmallVector +BlockDependencyGraph::getSuccessors(BlockNode *node) { + if (!node) + return {}; + return llvm::SmallVector(node->successors.begin(), + node->successors.end()); +} + +void BlockDependencyGraph::recomputeDepthsFrom(BlockNode *start) { + // Walk forward through the successor chain starting at `start`. For each + // visited node, the new depth is propagated to its successors; a successor + // is enqueued again only when its depth actually grows. Since depth is + // monotonically non-decreasing, this terminates. + std::queue queue; + queue.push(start); + while (!queue.empty()) { + BlockNode *current = queue.front(); + queue.pop(); + + for (BlockNode *succ : current->successors) { + int newDepth = current->depth; + if (current->isCube != succ->isCube) + newDepth++; + if (succ->depth < newDepth) { + succ->depth = newDepth; + queue.push(succ); + } + } + } + + LDBG("Recomputed depths from block " << start->blockId); +} + +llvm::LogicalResult BlockDependencyGraph::rebuildAfterMerge(BlockNode *target, + BlockNode *source) { + // Merge node information: append source ops into target. + target->ops.append(source->ops.begin(), source->ops.end()); + + // Sanity check: source must be connected to the graph (either has at + // least one predecessor or one successor). Merging an isolated node is + // meaningless and indicates an unsupported scenario. + if (source->predecessors.empty() && source->successors.empty()) { + LDBG("Cannot merge block " << source->blockId + << ": no predecessors or successors\n"); + return llvm::failure(); + } + + // Re-direct every edge touching source so that it points to target. + // DenseSet::insert de-duplicates, so any edge target already owns from + // before the merge is preserved naturally. + // + // Note: the iteration loops read `source->predecessors` / `source-> + // successors` while we mutate them in place. To avoid invalidating the + // current iterator we drain them into temporary vectors first. Using + // range insert on the destination keeps this O(N) overall. + llvm::SmallVector sourcePreds(source->predecessors.begin(), + source->predecessors.end()); + llvm::SmallVector sourceSuccs(source->successors.begin(), + source->successors.end()); + + for (BlockNode *pred : sourcePreds) { + pred->successors.erase(source); + pred->successors.insert(target); + } + target->predecessors.insert(sourcePreds.begin(), sourcePreds.end()); + + for (BlockNode *succ : sourceSuccs) { + succ->predecessors.erase(source); + succ->predecessors.insert(target); + } + target->successors.insert(sourceSuccs.begin(), sourceSuccs.end()); + + // Drop the source node from blockNodes. The BlockNode object itself is + // destroyed here (unique_ptr is reset); only `source`'s local pointer + // value is invalidated, which we are done using. + int sourceId = source->blockId; + blockNodes.erase(source->blockId); + + // Only the successor chain reachable from target is affected by the + // merge; recompute depth locally rather than walking the entire graph. + recomputeDepthsFrom(target); + + LDBG("Rebuilt graph after merging block " << sourceId << " into " + << target->blockId); + return llvm::success(); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/BufferCountManager.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/BufferCountManager.cpp new file mode 100644 index 0000000000..decaa7af64 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/BufferCountManager.cpp @@ -0,0 +1,164 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/raw_ostream.h" + +static constexpr const char *DEBUG_TYPE = "BufferCountManager"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +namespace mlir { +namespace triton { + +namespace { + +constexpr int kDefaultIntraBufferCount = 3; +constexpr int kDefaultInterBufferCount = 2; +constexpr int kDefaultLoadBufferCount = 1; +constexpr int kBufferCountWarningThreshold = 3; + +inline llvm::StringLiteral getAttrName(BufferCountManager::DepType type) { + switch (type) { + case BufferCountManager::DepType::IntraCore: + return CVPipeline::kIntraBufCount; + case BufferCountManager::DepType::InterCore: + return CVPipeline::kInterCoreBufCount; + case BufferCountManager::DepType::LoadStore: + return CVPipeline::kLoadStoreBufCount; + } + llvm_unreachable("unknown BufferCountManager::DepType"); +} + +inline int getDefaultCount(BufferCountManager::DepType type) { + switch (type) { + case BufferCountManager::DepType::IntraCore: + return kDefaultIntraBufferCount; + case BufferCountManager::DepType::InterCore: + return kDefaultInterBufferCount; + case BufferCountManager::DepType::LoadStore: + return kDefaultLoadBufferCount; + } + llvm_unreachable("unknown BufferCountManager::DepType"); +} + +} // namespace + +BufferCountManager::BufferCountManager(Operation *root) + : module_(root ? root->getParentOfType() : ModuleOp()) { + initFromModule(); +} + +BufferCountManager::BufferCountManager(ModuleOp module) : module_(module) { + initFromModule(); +} + +void BufferCountManager::initFromModule() { + if (!module_) { + LOG_DEBUG("initFromModule: module_ is null, skip"); + return; + } + OpBuilder builder(module_.getContext()); + LOG_DEBUG("initFromModule: getContext OK, about to loop"); + for (auto type : + {DepType::IntraCore, DepType::InterCore, DepType::LoadStore}) { + if (module_->getAttrOfType(getAttrName(type))) { + LOG_DEBUG("initFromModule: type=" << (int)type << " already set, skip"); + continue; + } + module_->setAttr(getAttrName(type), + builder.getI32IntegerAttr(getDefaultCount(type))); + LOG_DEBUG("initFromModule: type=" << (int)type << " set to " + << getDefaultCount(type)); + } +} + +void BufferCountManager::setBufferCount(DepType type, int count) { + if (count <= 0) { + LOG_DEBUG("Invalid buffer count: " << count << " (must be > 0)"); + return; + } + if (count > kBufferCountWarningThreshold) { + LOG_DEBUG("Warning: buffer count " << count << " > " + << kBufferCountWarningThreshold + << " is not recommended"); + } + OpBuilder builder(module_.getContext()); + switch (type) { + case DepType::IntraCore: + module_->setAttr(CVPipeline::kIntraBufCount, + builder.getI32IntegerAttr(count)); + LOG_DEBUG("IntraBufferCount set to " << count); + break; + case DepType::InterCore: + module_->setAttr(CVPipeline::kInterCoreBufCount, + builder.getI32IntegerAttr(count)); + LOG_DEBUG("InterBufferCount set to " << count); + break; + case DepType::LoadStore: + module_->setAttr(CVPipeline::kLoadStoreBufCount, + builder.getI32IntegerAttr(count)); + LOG_DEBUG("LoadBufferCount set to " << count); + break; + default: + LOG_DEBUG("Unknown DepType: " << static_cast(type)); + break; + } +} + +void BufferCountManager::buildBufferCountMap( + llvm::DenseMap> &depValueMap, + llvm::DenseMap &bufferCountMap, DepType type) { + int bufCount = getBufferCountByType(type); + + for (auto &p : depValueMap) { + for (Value depVal : p.second) { + if (isa(depVal) || !depVal.getDefiningOp()) + continue; + bufferCountMap[depVal] = bufCount; + } + } +} + +int BufferCountManager::getBufferCountByType(DepType type) const { + auto attr = module_->getAttrOfType(getAttrName(type)); + if (!attr) { + int defaultCount = getBufferCountByType(type); + LOG_DEBUG("getBufferCountByType(" << static_cast(type) + << ") returns default buffer count " + << defaultCount); + return defaultCount; + } + int count = static_cast(attr.getInt()); + LOG_DEBUG("getBufferCountByType(" << static_cast(type) + << ") = " << count); + return count; +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/CycleDetector.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/CycleDetector.cpp new file mode 100644 index 0000000000..641266d0b8 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/CycleDetector.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/CycleDetector.h" +#include "mlir/IR/Operation.h" + +namespace mlir::CVPipeline { + +bool DependencyCycleDetector::detectCycleFrom(Operation *cur) { + if (group.contains(cur)) { + return true; + } + if (!visited.insert(cur).second) { + return false; + } + + bool createsCycle = false; + + depHelper.forEachUserInSameBlock(cur, [&](Operation *user) { + if (isExternalSyncOp(user)) { + return; + } + if (createsCycle) + return; + createsCycle = + llvm::any_of(bm.getOpsInSameBlock(user), + [this](Operation *user) { return detectCycleFrom(user); }); + return; + }); + + return createsCycle; +} + +bool DependencyCycleDetector::detectCycle() { + llvm::DenseSet externalUsers; + for (auto *op : group) { + depHelper.forEachUserInSameBlock(op, [&](Operation *user) { + if (!group.contains(user)) { + externalUsers.insert(user); + } + }); + } + return llvm::any_of(externalUsers, [this](Operation *op) { + return this->detectCycleFrom(op); + }); +} + +} // namespace mlir::CVPipeline diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/DependencyHelper.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/DependencyHelper.cpp new file mode 100644 index 0000000000..683ef7efe5 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/DependencyHelper.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/DependencyHelper.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "mlir/Support/WalkResult.h" + +namespace mlir::CVPipeline { + +namespace { + +using SourceMode = DependencyHelper::SourceMode; + +} // namespace + +mlir::WalkResult DependencyHelper::forEachUserImpl(Operation *op, + CallbackFn pred) const { + if (!op) + return WalkResult::advance(); + for (auto *user : op->getUsers()) { + if (pred(user).wasInterrupted()) { + return WalkResult::interrupt(); + } + } + for (auto *user : memGraph.getExecAfter(op)) { + if (pred(user).wasInterrupted()) { + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); +} + +template +mlir::WalkResult DependencyHelper::forEachSourceImpl(Operation *op, + CallbackFn pred) const { + return op->walk( + [this, op, pred = std::forward(pred)](Operation *subOp) { + for (auto operand : subOp->getOperands()) { + if (auto *defOp = operand.getDefiningOp(); defOp) { + if (!op->isAncestor(defOp)) { + if (pred(defOp).wasInterrupted()) { + return WalkResult::interrupt(); + } + } + continue; + } + + if constexpr (SM == SourceMode::AcrossIterArg) { + if (auto *defOp = getLoopCarriedDefOp(operand, op->getBlock())) { + if (pred(defOp).wasInterrupted()) { + return WalkResult::interrupt(); + } + } + } + } + for (auto *source : memGraph.getExecBefore(subOp)) { + if (!op->isAncestor( + source)) { // this filters only the outer mem dependencies + if (pred(source).wasInterrupted()) { + return WalkResult::interrupt(); + } + } + } + return WalkResult::advance(); + }); +} + +// Instantiate concrete functions for linking +template mlir::WalkResult +DependencyHelper::forEachSourceImpl(mlir::Operation *op, + CallbackFn pred) const; + +template mlir::WalkResult +DependencyHelper::forEachSourceImpl( + mlir::Operation *op, CallbackFn pred) const; +} // namespace mlir::CVPipeline diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/FlagIdManager.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/FlagIdManager.cpp new file mode 100644 index 0000000000..ddb9bda3b4 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/FlagIdManager.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/FlagIdManager.h" +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/IR/Operation.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; +static constexpr const char *DEBUG_TYPE = "flag-id-manager"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir::triton; +using namespace hivm; + +static constexpr int kMaxOps = 1024; + +FlagIdManager::FlagIdManager(ModuleOp module) { + this->module = module; + scanExistingFlags(module); + LOG_DEBUG("FlagIdManager: Initialized with max_id = " << currentMaxId + << "\n"); +} + +void FlagIdManager::scanExistingFlags(ModuleOp module) { + module.walk([&](Operation *op) { + if (auto syncBlock = dyn_cast(op)) { + if (auto flagIdOpt = syncBlock.getFlagId()) { + int64_t id = flagIdOpt->getInt(); + if (id >= 0 && id <= MAX_FLAG_ID) { + currentMaxId = std::max(currentMaxId, id); + } + } + } + if (isa(op) || isa(op)) { + int flag = -1; + if (auto intAttr = op->getAttrOfType("static_flag_id")) { + flag = (int)intAttr.getInt(); + } else if (auto intAttr = op->getAttrOfType("flag")) { + flag = (int)intAttr.getInt(); + } + if (flag >= 0) { + currentMaxId = std::max(currentMaxId, (int64_t)flag); + } + } + }); +} + +int FlagIdManager::acquireId() { return ++currentMaxId; } + +bool FlagIdManager::checkCurrentId() { + BufferCountManager::DepType depType = BufferCountManager::DepType::InterCore; + BufferCountManager bufferCountMgr(module); + int outerBufferCount = bufferCountMgr.getBufferCountByType(depType); + if (outerBufferCount > 1) { + return currentMaxId <= MULTI_MAX_FLAG_ID; + } + return currentMaxId <= MAX_FLAG_ID; +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/MemoryEffectsTracker.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/MemoryEffectsTracker.cpp new file mode 100644 index 0000000000..3a6b7f334f --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/MemoryEffectsTracker.cpp @@ -0,0 +1,654 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// Memory side-effect analysis: tracks per-location read/write effects across +// ops to identify implicit ordering constraints (RAW, WAR, WAW) and expose +// them as scheduling edges (memDefs/memUsers, execBefore/execAfter). +// +// Each memory location is a MemSlot recording its last writer and pending +// readers. Two-level alias resolution determines which slots an effect touches: +// 1. Exact: value V found directly in valueToSlot. +// 2. AA query: AliasAnalysis asked whether a slot’s memref may-alias V. +// Root BlockArguments reject MayAlias (rejectMayAlias=true) to avoid +// false positives from conservative AA on function parameters. +// +// Unknown ops (no SideEffect interface) act as full barriers: they depend on +// all prior writers/readers and become the sole writer for every slot. + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Region.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Debug.h" + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/Common/SyncWall.h" +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" + +using namespace mlir; +static constexpr const char *DEBUG_TYPE = "memory-effects-tracker"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir::CVPipeline; + +namespace { + +bool isDefinedInside(Value v, Operation *op) { + if (!v || !op) { + return false; + } + + if (auto arg = dyn_cast(v)) { + Operation *parent = arg.getOwner()->getParentOp(); + return parent && op->isAncestor(parent); + } + + Operation *defOp = v.getDefiningOp(); + if (!defOp) { + return false; + } + + return op->isProperAncestor(defOp); +} + +static Value getAliasSource(Value val) { + auto *op = val.getDefiningOp(); + if (!op) { + return nullptr; + } + return llvm::TypeSwitch(op) + .Case([](ViewLikeOpInterface viewOp) { return viewOp.getViewSource(); }) + .Case([](bufferization::ToTensorOp totensorOp) { + return totensorOp.getBuffer(); + }) + .Default([](auto) { return nullptr; }); +} + +Value getViewSource(Value val) { + while (auto source = getAliasSource(val)) { + val = source; + } + return val; +} + +MemoryEffects::EffectInstance +remapEffectValue(const MemoryEffects::EffectInstance &effect, Value value) { + if (auto result = dyn_cast(value)) { + return MemoryEffects::EffectInstance( + effect.getEffect(), result, effect.getParameters(), effect.getStage(), + effect.getEffectOnFullRegion(), effect.getResource()); + } + + return MemoryEffects::EffectInstance( + effect.getEffect(), cast(value), effect.getParameters(), + effect.getStage(), effect.getEffectOnFullRegion(), effect.getResource()); +} + +bool isKnownNoMemoryEffectCall(Operation *op) { + auto callOp = dyn_cast(op); + return callOp && (callOp.getCallee().starts_with("triton_indirect_load") || + callOp.getCallee().starts_with("triton_stride_load")); +} + +bool shouldAnalyzeAsLeaf(Operation *op) { + return op->getNumRegions() == 0 || isa(op); +} + +void collectLeafOps(Operation *op, SmallVectorImpl &leafOps) { + if (shouldAnalyzeAsLeaf(op)) { + leafOps.push_back(op); + return; + } + + for (Region ®ion : op->getRegions()) { + for (Block &block : region) { + for (Operation &inner : block) { + collectLeafOps(&inner, leafOps); + } + } + } +} + +} // namespace + +MemoryDependenceGraph::MemoryDependenceGraph(Operation *root, AliasAnalysis &aa) + : root(root), aa(aa) { + if (!root) { + return; + } + analyzeOp(root); + slots.clear(); + valueToSlot.clear(); +} + +ArrayRef MemoryDependenceGraph::getMemDefs(Operation *op) const { + auto it = memDefs.find(op); + if (it == memDefs.end()) { + return {}; + } + return it->second; +} + +ArrayRef MemoryDependenceGraph::getMemUsers(Operation *op) const { + auto it = memUsers.find(op); + if (it == memUsers.end()) + return {}; + return it->second; +} + +ArrayRef +MemoryDependenceGraph::getExecBefore(Operation *op) const { + auto it = execBefore.find(op); + if (it == execBefore.end()) + return {}; + return it->second; +} + +ArrayRef MemoryDependenceGraph::getExecAfter(Operation *op) const { + auto it = execAfter.find(op); + if (it == execAfter.end()) + return {}; + return it->second; +} + +SmallVector +MemoryDependenceGraph::getRealDependency(Operation *frontOp, + Operation *backOp) { + if (!frontOp || !backOp) { + return {}; + } + + SmallVector leafOps; + collectLeafOps(frontOp, leafOps); + + bool unknown = false; + SmallVector backEffects = + collectOuterEffects(backOp, unknown, false); + bool backUnknown = unknown; + + // Create slots for frontOps, using existing effects logic to process + slots.clear(); + valueToSlot.clear(); + llvm::SmallSetVector dependencyOps; + for (Operation *leafOp : leafOps) { + auto effects = collectOuterEffects(leafOp, unknown, false); + if (unknown) { + if (!isKnownNoMemoryEffectCall(leafOp)) { + dependencyOps.insert(leafOp); + } + continue; + } + for (const auto &effect : effects) { + if (Value v = effect.getValue()) { + getOrCreateSlot(getViewSource(v)); + } + } + applyEffects(leafOp, effects, unknown); + } + + // Analyze denpendence from frontOps to backOp + SmallVector defs; + SmallVector preds; + collectPreds(backEffects, backUnknown, defs, preds); + dependencyOps.insert(preds.begin(), preds.end()); + return {dependencyOps.begin(), dependencyOps.end()}; +} + +void MemoryDependenceGraph::analyzeOp(Operation *op) { + if (!op) { + return; + } + + // The graph root is treated as a pure container: we don't query its own + // effects against itself, only recurse into its regions. + if (op == root) { + analyzeRegionsOf(op); + return; + } + + LOG_DEBUG("Analyzing op " << *op << "\n"); + // Step 1: collect outside memory effects + bool unknown = false; + SmallVector effects = + collectOuterEffects(op, unknown); + + // Step 2: collect defs(WAR) and preds(WAR, WAW, RAW) dependency. + SmallVector defs; + SmallVector preds; + collectPreds(effects, unknown, defs, preds); + LOG_DEBUG("Defs: \n"); + for (auto def : defs) { + LOG_DEBUG(*def << "\n"); + } + LOG_DEBUG("Preds: \n"); + for (auto pred : preds) { + LOG_DEBUG(*pred << "\n"); + } + + // Step 3: extract edges from defs and preds to the graph. + recordEdges(op, defs, preds); + + // Step 4: recursively search regions. + if (op->getNumRegions() > 0) { + analyzeRegionsOf(op); + } + + // Step 5: store effects into slots. + applyEffects(op, effects, unknown); +} + +void MemoryDependenceGraph::analyzeRegionsOf(Operation *op) { + // Whether to isolated slots. + // For example: scf.for should keeps slots; funOp should use new slots. + const bool isolated = + op != root && op->hasTrait(); + + for (Region ®ion : op->getRegions()) { + // Snapshot is used for quickly restoration. + Snapshot snap = takeSnapshot(); + if (isolated) { + slots.clear(); + valueToSlot.clear(); + } + + for (Block &block : region) { + for (BlockArgument arg : block.getArguments()) { + if (isa(arg.getType())) { + auto *slot = getOrCreateSlot(arg); + } + } + for (Operation &inner : block) { + analyzeOp(&inner); + } + } + + restoreSnapshot(std::move(snap)); + } +} + +SmallVector +MemoryDependenceGraph::collectOuterEffects(Operation *op, bool &unknown, + bool recursive) { + unknown = false; + + if (auto markOp = dyn_cast(op)) { + if (markOp->hasAttr(CVPipeline::kInlinableQuantScaleAttr)) { + return {}; + } else { + MemoryEffects::EffectInstance scopedWrite(MemoryEffects::Write::get()); + return {remapEffectValue(scopedWrite, markOp.getSrc())}; + } + } + + if (auto allocTensorOp = dyn_cast(op)) { + return {}; + } + + std::optional> raw; + if (recursive) { + raw = getEffectsRecursively(op); + } else if (auto effectInterface = dyn_cast(op)) { + raw.emplace(); + effectInterface.getEffects(*raw); + } + if (!raw) { + if (!isKnownNoMemoryEffectCall(op)) { + unknown = true; + } + return {}; + } + + SmallVector filtered; + filtered.reserve(raw->size()); + for (const auto &e : *raw) { + Value value = e.getValue(); + if (!value) { + filtered.push_back(e); + continue; + } + + Value source = getViewSource(value); + if (isDefinedInside(source, op)) { + continue; + } + filtered.push_back(source == value ? e : remapEffectValue(e, source)); + } + return filtered; +} + +AliasResult MemoryDependenceGraph::queryAlias(Value lhs, Value rhs) { + auto lhsSource = getViewSource(lhs); + auto rhsSource = getViewSource(rhs); + if (!lhsSource) { + lhsSource = lhs; + } + if (!rhsSource) { + rhsSource = rhs; + } + + auto isFuncEntryArg = [](const Value &val) -> bool { + auto arg = llvm::dyn_cast(val); + if (!arg) { + return false; + } + auto *block = arg.getOwner(); + return block->isEntryBlock() && + llvm::isa(block->getParentOp()); + }; + if (isFuncEntryArg(lhsSource) && isFuncEntryArg(rhsSource)) { + return lhs == rhs ? AliasResult::MustAlias : AliasResult::NoAlias; + } + return aa.alias(lhsSource, rhsSource); +} + +SmallVector +MemoryDependenceGraph::findAliasSlots(Value v) { + SmallVector result; + + if (!v) { // unknown location: conservatively alias with every slot + result.reserve(slots.size()); + for (auto &slot : slots) { + result.push_back(slot.get()); + } + return result; + } + + SmallPtrSet seen; + + auto it = valueToSlot.find(v); + if (it != valueToSlot.end()) { + result.push_back(it->second); + seen.insert(it->second); + } + + for (auto &slot : slots) { + MemSlot *raw = slot.get(); + if (seen.contains(raw) || !raw->memref) { + continue; + } + auto aliasResult = queryAlias(raw->memref, v); + if (aliasResult != AliasResult::NoAlias) { + result.push_back(raw); + seen.insert(raw); + } + } + + return result; +} + +MemoryDependenceGraph::MemSlot * +MemoryDependenceGraph::getOrCreateSlot(Value v) { + if (!v) { + return nullptr; + } + auto it = valueToSlot.find(v); + if (it != valueToSlot.end()) { + return it->second; + } + auto slot = std::make_unique(v); + MemSlot *raw = slot.get(); + slots.push_back(std::move(slot)); + valueToSlot[v] = raw; + return raw; +} + +ArrayRef +MemoryDependenceGraph::resolveAliasSlots( + Value v, DenseMap> &cache) { + auto it = cache.find(v); + if (it != cache.end()) { + return it->second; + } + auto [ins, _] = cache.try_emplace(v, findAliasSlots(v)); + return ins->second; +} + +void MemoryDependenceGraph::collectPreds( + ArrayRef effects, bool unknown, + SmallVectorImpl &defsOut, + SmallVectorImpl &predsOut) { + llvm::SmallSetVector defs; + llvm::SmallSetVector preds; + + // defs collects lastWriter only on reads (RAW); preds collects all ordering + // deps (RAW/WAR/WAW). + auto addFromSlot = [&](MemSlot *slot, bool isWriteLike) { + if (slot->dataSource && !isWriteLike) { + defs.insert(slot->dataSource); + } + if (slot->lastWriter) { + preds.insert( + slot->lastWriter); // XAW: we write/read what lastWriter wrote + } + if (isWriteLike) { + for (Operation *r : slot->pendingReads) { + preds.insert(r); // WAR: we will overwrite data still needed by readers + } + } + }; + + // The unknown op is considered as a barrier. + if (unknown) { + for (auto &slot : slots) { + addFromSlot(slot.get(), true); + } + predsOut.assign(preds.begin(), preds.end()); + return; + } + + DenseMap> cache; + for (const auto &e : effects) { + Value v = e.getValue(); + if (isa(e.getEffect())) { + for (MemSlot *s : resolveAliasSlots(v, cache)) { + addFromSlot(s, false); + } + } else if (isa(e.getEffect()) || + isa(e.getEffect())) { + for (MemSlot *s : resolveAliasSlots(v, cache)) { + addFromSlot(s, true); + } + } + } + + defsOut.assign(defs.begin(), defs.end()); + predsOut.assign(preds.begin(), preds.end()); +} + +void MemoryDependenceGraph::applyEffects( + Operation *op, ArrayRef effects, + bool unknown) { + // Conservative strategy. Unknown op is to treat as barrier. + if (unknown) { + for (auto &slot : slots) { + slot->lastWriter = op; + slot->pendingReads.clear(); + } + return; + } + + for (auto result : op->getOpResults()) { + if (isa(result.getType())) { + if (MemSlot *s = getOrCreateSlot(result)) { + s->dataSource = op; + s->lastWriter = op; + s->pendingReads.clear(); + } + } + } + + DenseMap> cache; + + for (const auto &e : effects) { + // Reads first: prevents a self-aliasing op from clearing its own pending + // read. + if (isa(e.getEffect())) { + for (MemSlot *s : resolveAliasSlots(e.getValue(), cache)) { + s->pendingReads.insert(op); + } + } + } + + for (const auto &e : effects) { + Value v = e.getValue(); + if (isa(e.getEffect())) { + if (MemSlot *s = getOrCreateSlot(v)) { + s->dataSource = op; + s->lastWriter = op; + s->pendingReads.clear(); + } + } else if (isa(e.getEffect())) { + for (MemSlot *s : resolveAliasSlots(v, cache)) { + // annotation::MarkOp, no data produced, is marked with Mem::Write + if (!isa(op)) { + s->dataSource = op; + } + s->lastWriter = op; + s->pendingReads.clear(); + } + } else if (isa(e.getEffect())) { + SmallPtrSet toRemove; + if (!v) { + LOG_DEBUG("Free of unknown value: conservatively drop all slots."); + for (auto &slot : slots) { + toRemove.insert(slot.get()); + } + } else { + auto aliasSlots = findAliasSlots(v); + for (auto *slot : aliasSlots) { + toRemove.insert(slot); + } + } + llvm::erase_if(slots, [&](std::unique_ptr &sp) { + if (toRemove.contains(sp.get())) { + if (sp->memref) { + valueToSlot.erase(sp->memref); + } + return true; + } + return false; + }); + cache.clear(); + } + } +} + +MemoryDependenceGraph::Snapshot MemoryDependenceGraph::takeSnapshot() const { + Snapshot snap; + snap.states.reserve(slots.size()); + for (const auto &slot : slots) { + snap.states.push_back(*slot); + } + return snap; +} + +void MemoryDependenceGraph::restoreSnapshot(Snapshot &&snap) { + slots.clear(); + valueToSlot.clear(); + slots.reserve(snap.states.size()); + for (auto &s : snap.states) { + auto slot = std::make_unique(std::move(s)); + if (slot->memref) { + valueToSlot[slot->memref] = slot.get(); + } + slots.push_back(std::move(slot)); + } +} + +SyncWall &MemoryDependenceGraph::getWall(Block *block) { + auto it = walls.find(block); + if (it == walls.end()) { + it = walls.try_emplace(block, block).first; + } + return it->second; +} + +bool MemoryDependenceGraph::isSyncSeparated(Operation *a, Operation *b) { + if (!a || !b) { + return false; + } + + if (Block *block = a->getBlock()) { + if (Operation *bAnc = CVPipeline::getAncestorInBlock(b, block)) { + return getWall(block).hasSyncBetween(a, bAnc); + } + } + + if (Block *block = b->getBlock()) { + if (Operation *aAnc = CVPipeline::getAncestorInBlock(a, block)) { + return getWall(block).hasSyncBetween(b, aAnc); + } + } + return false; +} + +void MemoryDependenceGraph::recordEdges(Operation *op, + ArrayRef defs, + ArrayRef preds) { + // Drop memory edges that cross a synchronization op + SmallVector syncFreeDefs; + for (Operation *p : defs) { + if (!isSyncSeparated(op, p)) { + syncFreeDefs.push_back(p); + } + } + SmallVector syncFreePreds; + for (Operation *p : preds) { + if (!isSyncSeparated(op, p)) { + syncFreePreds.push_back(p); + } + } + + if (!syncFreeDefs.empty()) { + auto &defList = memDefs[op]; + defList.assign(syncFreeDefs.begin(), syncFreeDefs.end()); + for (Operation *p : syncFreeDefs) { + memUsers[p].push_back(op); + } + } + + if (!syncFreePreds.empty()) { + auto &execBeforeList = execBefore[op]; + execBeforeList.assign(syncFreePreds.begin(), syncFreePreds.end()); + for (Operation *p : syncFreePreds) { + execAfter[p].push_back(op); + } + } +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/SSBufferManager.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/SSBufferManager.cpp new file mode 100644 index 0000000000..490aeeb871 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/SSBufferManager.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/SSBufferManager.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "mlir/IR/BuiltinTypes.h" + +using namespace mlir; +using namespace triton; + +// Helper function to check if a type is a scalar type +// Scalar types include: IntegerType (i1, i8, i16, i32, i64, etc.) +// and FloatType (f16, f32, f64, bf16, f8, etc.) +bool SSBufferManager::isScalarType(Type type) { + // Check if it's a scalar type + if (isa(type)) { + return true; + } else if (isa(type)) { + return true; + } else { + // Not a scalar type + return false; + } +} + +// Allocate SSBuffer address for a value +std::optional SSBufferManager::allocateAddr(Value value) { + // Check if the value's type is a scalar type + Type valueType = value.getType(); + if (!isScalarType(valueType)) { + // Not a scalar type, return error + return std::nullopt; + } + + // Check if this value already has an allocated SSBuffer address + auto it = valueToAddrMap.find(value); + if (it != valueToAddrMap.end()) { + // Reuse the existing SSBuffer address + return it->second; + } + + // Allocate a new SSBuffer address based on current map size + // Address = base_addr + map_size * offset + int64_t addrValue = + SSBUF_BASE_ADDR + valueToAddrMap.size() * SSBUF_ADDR_OFFSET; + + // Check if address exceeds maximum limit + if (addrValue > SSBUF_ADDR_MAX) { + // Address out of range, return error + return std::nullopt; + } + + valueToAddrMap[value] = addrValue; + addrToValueMap[addrValue] = value; + return addrValue; +} + +// Find the Value and its type for a given address +std::optional> +SSBufferManager::findValueByAddr(int64_t addr) { + // Use reverse mapping table for fast lookup + auto it = addrToValueMap.find(addr); + if (it == addrToValueMap.end()) { + // Address not found in mapping table + return std::nullopt; + } + + // Found the address in the reverse mapping table + Value foundValue = it->second; + Type dataType = foundValue.getType(); + return std::make_pair(foundValue, dataType); +} + +// Write a value to SSBuffer and return the SSBuffer address (int64_t) +std::optional +SSBufferManager::writeToSSBuffer(Value value, OpBuilder &builder, + SmallVectorImpl &createdOps) { + // Allocate or get existing SSBuffer address + auto addrResult = allocateAddr(value); + if (!addrResult) { + // Address allocation failed (either not scalar type or out of range) + return std::nullopt; + } + + int64_t addrValue = addrResult.value(); + Location loc = builder.getUnknownLoc(); + auto [constOp, pointerCastOp] = + getSsbufConstAndPointerCast(builder, loc, addrValue, value.getType()); + createdOps.push_back(constOp); + createdOps.push_back(pointerCastOp); + + // Store the value to SSBuffer + auto storeOp = + builder.create(loc, value, pointerCastOp.getResult()); + createdOps.push_back(storeOp); // Record the created operation + + // Return the address value (int64_t), not the pointer + return addrValue; +} + +// Read a value from SSBuffer based on the given address (int64_t) +std::optional +SSBufferManager::readFromSSBuffer(int64_t addr, OpBuilder &builder, + SmallVectorImpl &createdOps) { + // Find the Value and its type for the given address + auto findResult = findValueByAddr(addr); + if (!findResult) { + // Address not found in manager, return error + return std::nullopt; + } + + // Load the value back with the original scalar type, not a hardcoded i32. + Type dataType = findResult.value().second; + Location loc = builder.getUnknownLoc(); + auto [constOp, pointerCastOp] = + getSsbufConstAndPointerCast(builder, loc, addr, dataType); + createdOps.push_back(constOp); + createdOps.push_back(pointerCastOp); + + // Load the value from SSBuffer address with the retrieved data type + auto loadOp = builder.create(loc, pointerCastOp.getResult(), + ValueRange{}); + createdOps.push_back(loadOp); // Record the created operation + + auto markOp = builder.create(loc, loadOp.getResult()); + markOp->setAttr(kMemrefExtVolatile, builder.getUnitAttr()); + createdOps.push_back(markOp); + + return loadOp.getResult(); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/SyncWall.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/SyncWall.cpp new file mode 100644 index 0000000000..f152c8c7a6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/SyncWall.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" + +#include "DynamicCVPipeline/Common/SyncWall.h" + +using namespace mlir; +using namespace mlir::CVPipeline; + +SyncWall::SyncWall(Block *block) { + unsigned idx = 0; + block->walk([&](Operation *op) { + Operation *owner = getAncestorInBlock(op, block); + if (owner == nullptr) { + return; + } + if (!ordinal.contains(owner)) { + ordinal[owner] = idx++; + } + if (isExternalSyncOp(op)) { + syncPositions.insert(ordinal[owner]); + } + }); +} + +unsigned SyncWall::positionOf(Operation *op) const { + auto it = ordinal.find(op); + if (it != ordinal.end()) { + return it->second; + } + return 0; +} + +bool SyncWall::hasSyncBetween(Operation *a, Operation *b) const { + unsigned lo = positionOf(a); + unsigned hi = positionOf(b); + if (lo > hi) { + unsigned tmp = lo; + lo = hi; + hi = tmp; + } + for (unsigned pos = lo + 1; pos < hi; ++pos) { + if (syncPositions.contains(pos)) { + return true; + } + } + return false; +} + +unsigned SyncWall::segmentOf(Operation *op) const { + unsigned pos = positionOf(op); + unsigned seg = 0; + for (unsigned p = 0; p < pos; ++p) { + if (syncPositions.contains(p)) { + ++seg; + } + } + return seg; +} + +bool SyncWall::sameSegment(Operation *a, Operation *b) const { + return segmentOf(a) == segmentOf(b); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/Common/Utils.cpp b/third_party/ascend/lib/DynamicCVPipeline/Common/Utils.cpp new file mode 100644 index 0000000000..eb1e315252 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/Common/Utils.cpp @@ -0,0 +1,549 @@ +#include +#include + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/GPU/IR/GPUDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/IR/Visitors.h" +#include "mlir/Interfaces/ControlFlowInterfaces.h" +#include "mlir/Interfaces/ViewLikeInterface.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" + +static constexpr const char *DEBUG_TYPE = "dynamic-cv-pipeline-utils"; +#define DBGS(...) LLVM_DEBUG(llvm::dbgs() << __VA_ARGS__) +#define LOG_DEBUG(...) DBGS("[" << DEBUG_TYPE << "] " << __VA_ARGS__) + +namespace mlir { +namespace CVPipeline { + +static bool g_enableUBRefineOpt = false; + +CoreType getOpCoreType(Operation *op) { + if (!op) { + return CoreType::UNDETERMINED; + } + if (auto a = op->getAttrOfType(kCoreType)) { + return fromStrCoreType(a.getValue()); + } + return CoreType::UNDETERMINED; +} + +llvm::LogicalResult verifyOpBlockId(Operation *op) { + if (!op) { + return llvm::failure(); + } + + auto blockId = op->getAttrOfType(kBlockId); + if (blockId && blockId.getInt() < 0) { + std::string_view errorPass = "previous passes"; + auto diag = op->emitError() + << "block id should not be negative! Please report to "; + switch (getOpCoreType(op)) { + case CoreType::CUBE_ONLY: + diag << "PlanCubePass"; + break; + case CoreType::VECTOR_ONLY: + diag << "PlanVectorPass"; + break; + default: + diag << "previous passes"; + } + return llvm::failure(); + } + + return llvm::success(); +} + +std::optional getOpBlockId(Operation *op) { + if (!op) { + return std::nullopt; + } + auto blockIdAttr = op->getAttrOfType(kBlockId); + if (!blockIdAttr) { + return std::nullopt; + } + + return blockIdAttr.getInt(); +} + +int getAvailableBlockId(ModuleOp module) { + int maxBlockId = -1; + module.walk([&](Operation *op) { + auto blockIdOpt = getOpBlockId(op); + if (blockIdOpt) { + int currentId = *blockIdOpt; + if (currentId > maxBlockId) { + maxBlockId = currentId; + } + } + }); + return maxBlockId + 1; +} + +void setFallbackAttr(ModuleOp module, int errorCode) { + OpBuilder builder(module.getContext()); + module->setAttr(CVPipeline::ERRCODE_ATTR, + builder.getI32IntegerAttr(errorCode)); +} + +bool hasFallbackAttr(ModuleOp module) { + return module->hasAttr(CVPipeline::ERRCODE_ATTR); +} + +bool isVectorOnlyOp(Operation *op) { + if (!op) { + return false; + } + + return llvm::TypeSwitch(op) + .Case([](linalg::ReduceOp) { return true; }) + .Case([](Operation *op) { + return isa(op->getResult(0).getType()); + }) + .Default([](auto) { return false; }); +} + +bool isSyncOp(Operation *op) { + return isa(op); +} + +bool isExternalSyncOp(Operation *op) { + return isSyncOp(op) && + op->getAttrOfType(CVPipeline::kExternalSync); +} + +void setSubBlockId(Operation *op, int subBlockId) { + OpBuilder builder(op->getContext()); + op->setAttr(CVPipeline::kSubBlock, builder.getI32IntegerAttr(subBlockId)); +} + +bool isScfOp(Operation *op) { + return llvm::isa(op->getDialect()); +} + +// Check nextOp is only user of preOp +bool isOnlyDirectlyUse(Operation *preOp, Operation *nextOp, + const CVPipeline::MemoryDependenceGraph &memGraph) { + if (!preOp || !nextOp) { + return false; + } + SmallVector allusers; + allusers.append(preOp->getUsers().begin(), preOp->getUsers().end()); + for (auto memUser : memGraph.getExecAfter(preOp)) { + allusers.push_back(memUser); + } + if (allusers.size() != 1) { + return false; + } + return (*allusers.begin()) == nextOp; +} + +static CoreType getCoreTypeOfSimpleOpOrCfImpl(Operation *op) { + if (!llvm::isa(op)) { + return getOpCoreType(op); + } + + CoreType coreType = CoreType::UNDETERMINED; + Operation *failingOp = nullptr; + + // we need to skip sub-op of non-cf ops with regions, hence preorder here + op->walk([&](Operation *subOp) -> WalkResult { + if (llvm::isa(subOp) || + subOp->hasTrait()) { + return WalkResult::advance(); + } + + CoreType currCoreType = getOpCoreType(subOp); + // we have met a simple op without core type + if (currCoreType == CoreType::UNDETERMINED) { + coreType = CoreType::UNDETERMINED; + failingOp = subOp; + return WalkResult::interrupt(); + } + + if (coreType == CoreType::UNDETERMINED) { + coreType = currCoreType; + } else if (currCoreType != coreType) { + // some ops have different core type + coreType = CoreType::CUBE_AND_VECTOR; + return WalkResult::interrupt(); + } + + // skip sub-op + return WalkResult::skip(); + }); + + (void)failingOp; + LOG_DEBUG("CoreType of RegionBranchOp is " << coreType << ": " << *op + << "\n"); + LLVM_DEBUG({ + if (coreType == CoreType::UNDETERMINED && failingOp != nullptr) { + llvm::dbgs() << "\nCoreType is UNDETERMINED due to " << *failingOp; + } + }); + return coreType; +} + +CoreType getCoreTypeOfSimpleOpOrCf(Operation *op) { + if (op == nullptr) { + return CoreType::UNDETERMINED; + } + if (!llvm::isa(op)) { + return getOpCoreType(op); + } + auto funcOp = op->getParentOfType(); + if (funcOp) { + constexpr llvm::StringLiteral regionalDisabledOps[1]{""}; + if (llvm::is_contained(regionalDisabledOps, funcOp.getSymName())) { + return CoreType::UNDETERMINED; + } + } + return getCoreTypeOfSimpleOpOrCfImpl(op); +} + +/** Determines if a value is "scalar-like" based on the following criteria: + 1. True scalar types (integer, index, or float) + 2. Tensor types with empty shape (e.g., tensor) + 3. Constant tensors where all elements have the same value (splat constants) + 4. Tensors with shape where all dimensions equal 1 (single-element tensors) + */ +bool isScalarLike(Value value) { + Type type = value.getType(); + auto shapedType = dyn_cast(type); + + // 1. true scalar (int / index / float) + if (!shapedType) { + return type.isIntOrIndexOrFloat(); + } + + // 2. tensor with empty shape (e.g. tensor) + ArrayRef shape = shapedType.getShape(); + if (shape.empty()) { + return true; + } + + // 3. splat constant tensor (all elements identical) + Attribute attr; + if (matchPattern(value, m_Constant(&attr))) { + auto denseAttr = dyn_cast(attr); + return denseAttr && denseAttr.isSplat() && + denseAttr.getElementType().isIntOrIndexOrFloat(); + } + + // 4. single-element tensor (all dims == 1) + return llvm::all_of(shape, [](int64_t dim) { return dim == 1; }); +} + +bool isStoreLike(mlir::Operation *op) { + if (isa(op)) { + return true; + } + return false; +} + +bool isViewLike(mlir::Operation *op) { + if (isa(op)) { + return true; + } + return false; +} + +bool isZeroFillValue(mlir::Value v) { + auto fill = v.getDefiningOp(); + if (!fill) { + return false; + } + if (fill.getInputs().empty()) { + return false; + } + Value insVal = fill.getInputs()[0]; + auto constOp = insVal.getDefiningOp(); + if (!constOp) { + return false; + } + Attribute value = constOp.getValue(); + if (auto intAttr = dyn_cast(value)) { + return intAttr.getValue().isZero(); + } + if (auto fpAttr = dyn_cast(value)) { + return fpAttr.getValue().isZero(); + } + return false; +} + +bool isZeroAdd(mlir::Operation *op) { + if (isa(op)) { + Value lhs = op->getOperand(0); + Value rhs = op->getOperand(1); + if (isZeroFillValue(lhs) || isZeroFillValue(rhs)) { + return true; + } + return false; + } + return false; +} + +// Read the `hivm.tightly_coupled_buffer` id attached to a `memref.alloc` +// via its `annotation.mark` user. Returns nullopt when no annotation with +// a concrete id is present, or when `allocVal` is null. +std::optional getTightlyCoupledBufferId(Value allocVal) { + if (!allocVal) { + return std::nullopt; + } + for (Operation *user : allocVal.getUsers()) { + if (auto tcbAttr = user->getAttrOfType( + CVPipeline::kTightlyCoupledBufferAttr)) { + auto id = tcbAttr.getId(); + if (id.has_value()) { + return id; + } + } + } + return std::nullopt; +} + +// Walk back through opaque memref casts to recover the underlying +// `memref.alloc` that backs a `bufferization.to_tensor`'s source. +Value traceBackToMemrefAlloc(Value v) { + while (true) { + if (auto cast = v.getDefiningOp()) { + v = cast.getSource(); + continue; + } + if (auto cast = v.getDefiningOp()) { + v = cast.getSource(); + continue; + } + break; + } + return v; +} + +bool allResultHasOneUser(Operation *op) { + bool ret = true; + for (Value result : op->getResults()) { + if (!result.hasOneUse()) { + ret = false; + break; + } + } + return ret; +} + +int64_t getBTSizeFromValidBroadcastOp(linalg::BroadcastOp broadcastOp) { + auto insType = + dyn_cast(broadcastOp.getDpsInputs()[0].getType()); + auto outsType = + dyn_cast(broadcastOp.getDpsInits()[0].getType()); + if (!insType || !outsType) { + return -1; + } + // Only match 1D -> 2D broadcast + if (insType.getRank() != 1 || outsType.getRank() != 2) { + return -1; + } + // Must be static shape to compute size + if (!insType.hasStaticShape()) { + return -1; + } + // Only match broadcast along dimension 0 (dimensions = [0]) + // This means output dimension 0 is broadcast, so input dimension 0 maps to + // output dimension 1, creating a [N] -> [M, N] broadcast (typical matmul bias + // usage where each row has the same bias) + auto dimensions = broadcastOp.getDimensions(); + if (dimensions.size() != 1 || dimensions[0] != 0) { + return -1; + } + // Verify output shape[1] == input shape[0] for correct broadcast semantics + auto outShape = outsType.getShape(); + auto inShape = insType.getShape(); + if (outShape[1] != inShape[0]) { + return -1; + } + // Check if the source data fits within the cache table buffer (4KB) + constexpr int64_t CACHE_TABLE_BUFFER_SIZE = 4096; + int64_t numElements = 1; + for (int64_t dim : inShape) { + numElements *= dim; + } + int64_t sizeBytes = + numElements * (insType.getElementTypeBitWidth() / BYTE_SIZE); + return sizeBytes; +} + +Block *MainLoop::getBody() const { return body; } + +Operation *MainLoop::getOperation() const { return op; } + +MLIRContext *MainLoop::getContext() const { return op->getContext(); } + +Location MainLoop::getLoc() const { return op->getLoc(); } + +Block *MainLoop::getBlock() const { return op->getBlock(); } + +Block::iterator MainLoop::getIterator() const { return op->getIterator(); } + +Operation *MainLoop::operator->() const { return op; } + +bool MainLoop::isWhile() const { return isa(op); } + +SmallVector MainLoop::getIterArgs() const { + SmallVector result; + if (auto f = dyn_cast(op)) { + result.append(f.getRegionIterArgs().begin(), f.getRegionIterArgs().end()); + } else if (auto w = dyn_cast(op)) { + Block::BlockArgListType args = w.getAfterBody()->getArguments(); + result.append(args.begin(), args.end()); + } + return result; +} + +SmallVector MainLoop::getBeforeIterArgs() const { + SmallVector result; + if (auto w = dyn_cast(op)) { + Block::BlockArgListType args = w.getBeforeBody()->getArguments(); + result.append(args.begin(), args.end()); + } + return result; +} + +MainLoop::MainLoop(Operation *loopOp) { + op = loopOp; + if (auto f = dyn_cast(loopOp)) + body = f.getBody(); + else if (auto w = dyn_cast(loopOp)) + body = w.getAfterBody(); +} + +scf::YieldOp MainLoop::getLoopYieldOp(Operation *loopOp) { + if (auto forOp = dyn_cast(loopOp)) + return dyn_cast(forOp.getBody()->getTerminator()); + if (auto whileOp = dyn_cast(loopOp)) + return dyn_cast(whileOp.getAfter().front().getTerminator()); + return {}; +} + +int getLoopCarriedArgIndex(Value operand, Block *block) { + if (!block || !block->mightHaveTerminator()) { + return -1; + } + + auto barg = dyn_cast_if_present(operand); + if (!barg || barg.getOwner() != block) { + return -1; + } + + auto *parentOp = block->getParentOp(); + if (!isa(parentOp)) { + return -1; + } + + auto *terminator = block->getTerminator(); + if (!llvm::isa_and_present(terminator)) { + return -1; + } + + int numArgs = block->getNumArguments(); + int numYieldOperands = terminator->getNumOperands(); + int offset = numArgs - numYieldOperands; + int argIdx = barg.getArgNumber() - offset; + + if (argIdx < 0 || argIdx >= numYieldOperands) { + return -1; + } + + return argIdx; +} + +int getTensorIterArgIndex(Value v, ArrayRef iterArgs) { + for (unsigned i = 0; i < iterArgs.size(); ++i) { + if (v == iterArgs[i] && isa(iterArgs[i].getType())) { + return i; + } + } + return -1; +} + +std::optional getFixpipePreQuantMode(Operation *op) { + if (!isa(op)) + return std::nullopt; + + // Exclude scalars: only shaped types are valid for fixpipe pre_quant. + auto resultType = dyn_cast(op->getResult(0).getType()); + if (!resultType) + return std::nullopt; + + Type inElemType = + cast(op->getOperand(0).getType()).getElementType(); + Type outElemType = resultType.getElementType(); + + if (isa(inElemType) && isa(outElemType)) + return hivm::FixpipePreQuantMode::F322BF16; + if (isa(inElemType) && isa(outElemType)) + return hivm::FixpipePreQuantMode::F322F16; + if (inElemType.isInteger(32) && outElemType.isInteger(8)) + return hivm::FixpipePreQuantMode::S322I8; + return std::nullopt; +} + +Operation *getSourceThroughCIntermediateOps(Value operand) { + auto isIntermediateOp = [](Operation *op) { + return getFixpipePreQuantMode(op).has_value(); + }; + Operation *defOp = operand.getDefiningOp(); + while (defOp && isIntermediateOp(defOp)) { + defOp = defOp->getOperand(0).getDefiningOp(); + } + return defOp; +} + +CoreType getValueCoreType(Value value) { + auto result = llvm::dyn_cast_if_present(value); + if (!result) { + return UNDETERMINED; + } + Operation *defOp = result.getOwner(); + if (defOp->getNumResults() == 1) { + return getOpCoreType(defOp); + } + auto attr = defOp->getAttrOfType(kCoreType); + if (!attr) { + return UNDETERMINED; + } + llvm::SmallVector coreTypeStrs; + attr.getValue().split(coreTypeStrs, ", "); + auto resultIdx = result.getResultNumber(); + if (coreTypeStrs.size() <= resultIdx) { + return UNDETERMINED; + } + return fromStrCoreType(coreTypeStrs[resultIdx]); +} + +} // namespace CVPipeline +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/BroadcastUBOptPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/BroadcastUBOptPass.cpp new file mode 100644 index 0000000000..a9456fbd7f --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/BroadcastUBOptPass.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "broadcast-ub-opt"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir; + +namespace mlir { +namespace triton { + +class BroadcastUBOptPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(BroadcastUBOptPass) + + BroadcastUBOptPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "broadcast-ub-opt"; } + llvm::StringRef getDescription() const final { + return "Optimize broadcast by moving it to the block of its users when " + "safe"; + } +}; + +} // namespace triton +} // namespace mlir + +namespace mlir { +namespace triton { + +void BroadcastUBOptPass::runOnOperation() { + ModuleOp moduleOp = getOperation(); + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(moduleOp, aa); + CVPipeline::ComputeBlockIdManager bm(moduleOp); + LOG_DEBUG(moduleOp); + moduleOp.walk([&](linalg::BroadcastOp op) { + if (CVPipeline::getOpCoreType(op.getOperation()) != + CVPipeline::CoreType::VECTOR_ONLY) + return; + if (op->getUsers().empty()) + return; + + // Just Simple scenario: + // brc's user all in same Block and in same block id. + Operation *oneUser = *op->getUsers().begin(); + if (oneUser->getBlock() != op->getBlock()) { + return; + } + int firstUserBlockId = bm.getBlockIdByOp(oneUser); + if (firstUserBlockId == -1 || CVPipeline::getOpCoreType(oneUser) != + CVPipeline::CoreType::VECTOR_ONLY) { + // No blcok Id (means control flow) && not vector: skip; + // vector only control flow should skip too. + return; + } + bool allUsersSameBlock = llvm::all_of(op->getUsers(), [&](Operation *user) { + return bm.getBlockIdByOp(user) == firstUserBlockId; + }); + if (!allUsersSameBlock) + return; + LOG_DEBUG("broadcast " << *op << " \n all users in same block " + << firstUserBlockId << "\n"); + + int broadcastBlockId = bm.getBlockIdByOp(op.getOperation()); + if (broadcastBlockId == firstUserBlockId) { + LOG_DEBUG("broadcast already in same block, skip\n"); + return; + } + + SmallVector opsToCheck = {op}; + if (CVPipeline::willCreateCycle(opsToCheck, memGraph, firstUserBlockId, + bm)) { + LOG_DEBUG("would create cycle, skip\n"); + return; + } + LOG_DEBUG("moving broadcast to block " << firstUserBlockId << "\n"); + bm.updateBlockId(op, firstUserBlockId); + }); +} + +std::unique_ptr> createBroadcastUBOptPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/Common.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/Common.cpp new file mode 100644 index 0000000000..b70a72eb16 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/Common.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "DynamicCVPipeline/Common/CycleDetector.h" +#include "DynamicCVPipeline/Common/DependencyHelper.h" +#include "ascend/include/DynamicCVPipeline/Common/SyncWall.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "mlir/Analysis/TopologicalSortUtils.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/Operation.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "triton/Analysis/Utility.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "compute-block-opt-common"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; + +namespace mlir { +namespace CVPipeline { + +static bool groupWouldStraddleSync(llvm::ArrayRef opsToUnify, + int targetBlockId, + ComputeBlockIdManager &bm) { + llvm::DenseMap> opsByBlock; + for (auto *op : opsToUnify) { + if (op && op->getBlock()) { + opsByBlock[op->getBlock()].push_back(op); + } + } + for (auto *op : bm.getOpsByBlockId(targetBlockId)) { + if (op && op->getBlock()) { + opsByBlock[op->getBlock()].push_back(op); + } + } + for (auto &blockOps : opsByBlock) { + Block *blk = blockOps.first; + SyncWall wall(blk); + unsigned refSeg = 0; + bool refSet = false; + for (auto *op : blockOps.second) { + if (getAncestorInBlock(op, blk) == nullptr) { + continue; + } + unsigned seg = wall.segmentOf(op); + if (!refSet) { + refSeg = seg; + refSet = true; + continue; + } + if (seg != refSeg) { + LOG_DEBUG("Reject merge: group would straddle a sync in block " + << blk << "\n"); + return true; + } + } + } + return false; +} + +bool willCreateCycle(llvm::ArrayRef opsToUnify, + const MemoryDependenceGraph &memGraph, int targetBlockId, + ComputeBlockIdManager &bm) { + if (opsToUnify.empty()) { + return false; + } + + auto *block = opsToUnify.front()->getBlock(); + + // A block_id group must never straddle a synchronization op + if (groupWouldStraddleSync(opsToUnify, targetBlockId, bm)) { + return true; + } + + llvm::DenseSet okSet; + for (auto *op : bm.getOpsByBlockId(targetBlockId)) { + okSet.insert(op); + } + for (auto *op : opsToUnify) { + okSet.insert(op); + } + + DenseMap origBlockIdMap; + for (auto *op : opsToUnify) { + auto optBlockId = getOpBlockId(op); + origBlockIdMap[op] = optBlockId ? *optBlockId : -1; + bm.updateBlockId(op, targetBlockId); + } + + // Initialize DFS detector + DependencyHelper depHelper{memGraph}; + DependencyCycleDetector dfs(block, depHelper, okSet, bm); + auto hasCycle = dfs.detectCycle(); + + for (auto &[op, origBlockId] : origBlockIdMap) { + bm.updateBlockId(op, origBlockId); + } + + return hasCycle; +} + +void cloneScalarOpsForCrossBlockUses(ComputeBlockIdManager &bm, + SetVector &matchedOps, + int targetBlockId) { + + // This means move matchedOps into targetBlockId + auto sorted = mlir::topologicalSort(matchedOps); + for (Operation *op : llvm::reverse(sorted)) { + if (op->getNumResults() == 1 && + CVPipeline::isScalarLike(op->getResult(0))) { + // replace op not in matchedOps with cloned op, and keep original op for + // other pattern. + SmallVector otherUses; + for (auto &use : op->getResult(0).getUses()) { + Operation *userOp = use.getOwner(); + auto userInBlock = + CVPipeline::getAncestorInBlock(userOp, op->getBlock()); + if (!userInBlock) + continue; + if (llvm::find(matchedOps, userInBlock) == matchedOps.end() && + bm.getBlockIdByOp(userInBlock) != targetBlockId) { + otherUses.push_back(&use); + } + } + if (otherUses.size() > 0) { + LOG_DEBUG("now cloned: " << *op); + OpBuilder builder(op); + auto clonedOp = builder.clone(*op); + bm.updateBlockId(clonedOp, bm.getBlockIdByOp(op)); + for (auto use : otherUses) { + (*use).set(clonedOp->getResult(0)); + } + } + } + } +} + +bool collectViewOpsAndCheckGlobalMemory(Value viewValue, + SetVector &matchedOps) { + // Subview ops may be nested many layers deep through reinterpretation or + // other subviews. like, subview (subview (reinterpret_cast (subview + // (reinterpret_cast (arg0))))) so we need Search and only keep same block + // view-like op. + auto isFuncArg = [&](Value v) { + if (auto blockArg = dyn_cast(v)) { + Operation *parentOp = blockArg.getOwner()->getParentOp(); + if (isa(parentOp)) { + return true; + } else { + LOG_DEBUG( + "Subview source block argument is not from func entry block."); + return false; + } + } + return false; + }; + if (isFuncArg(viewValue)) { + return true; + } + + auto viewOp = viewValue.getDefiningOp(); + if (!viewOp) { + return false; + } + if (!viewOp->hasOneUse()) { + return false; + } + auto block = viewOp->getBlock(); + LOG_DEBUG("Check view source: " << viewValue); + while (true) { + + if (isFuncArg(viewValue)) { + return true; + } + if (!viewValue.getDefiningOp()) { + return false; + } + // From other view-like op + if (auto viewLike = + dyn_cast(viewValue.getDefiningOp())) { + if (viewLike->getBlock() == block) { + matchedOps.insert(viewLike.getOperation()); + } + if (!viewLike->hasOneUse()) { + return false; + } + viewValue = viewLike.getViewSource(); + continue; + } + LOG_DEBUG( + "Subview source defining op is not ViewLikeOpInterface: " << viewValue); + return false; + } + return false; +} + +} // namespace CVPipeline +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/ExpLoadPatternPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/ExpLoadPatternPass.cpp new file mode 100644 index 0000000000..5c0c2700a6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/ExpLoadPatternPass.cpp @@ -0,0 +1,231 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "ComputeBlockOpt/SplitIfByBlockId/Common.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/TopologicalSortUtils.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "extract-exp-load-pattern"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace triton; + +namespace { + +struct ExpLoadPattern { + linalg::BroadcastOp broadcastOp; + Operation *expOp = nullptr; + bufferization::ToTensorOp toTensorOp; + memref::CopyOp copyOp; + memref::AllocOp allocOp; + arith::MulFOp mulfOp; + int blockId = -1; + int newBlockId = -1; +}; + +static bool isExpLike(Operation *op) { + return isa_and_present(op); +} + +// Match pattern: +// mulf <- broadcast <- exp/exp2 <- load +// ↖ +// %arg +static bool matchExpLoadPattern(linalg::BroadcastOp broadcastOp, + CVPipeline::ComputeBlockIdManager &bm, + ExpLoadPattern &info) { + if (CVPipeline::getOpCoreType(broadcastOp) != + CVPipeline::CoreType::VECTOR_ONLY) { + return false; + } + int blockId = bm.getBlockIdByOp(broadcastOp); + if (blockId == -1) { + return false; + } + + if (broadcastOp.getDpsInputs().empty()) { + return false; + } + Operation *expOp = broadcastOp.getDpsInputs()[0].getDefiningOp(); + if (!isExpLike(expOp) || bm.getBlockIdByOp(expOp) != blockId || + !expOp->hasOneUse()) { + return false; + } + + auto toTensorOp = + expOp->getOperand(0).getDefiningOp(); + if (!toTensorOp || bm.getBlockIdByOp(toTensorOp) != blockId || + !toTensorOp->hasOneUse()) { + return false; + } + + auto allocOp = toTensorOp.getBuffer().getDefiningOp(); + if (!allocOp || bm.getBlockIdByOp(allocOp) != blockId) { + return false; + } + + memref::CopyOp copyOp; + for (Operation *user : allocOp->getUsers()) { + auto c = dyn_cast(user); + if (c && c.getTarget() == allocOp.getMemref()) { + copyOp = c; + break; + } + } + if (!copyOp || bm.getBlockIdByOp(copyOp) != blockId || + copyOp->getBlock() != broadcastOp->getBlock()) { + return false; + } + + if (!broadcastOp->hasOneUse()) { + return false; + } + auto mulfOp = dyn_cast(*broadcastOp->getUsers().begin()); + if (!mulfOp) { + return false; + } + + int newBlockId = bm.getBlockIdByOp(mulfOp); + if (newBlockId == blockId) { + return false; + } + if (CVPipeline::getOpCoreType(mulfOp) != CVPipeline::CoreType::VECTOR_ONLY) { + return false; + } + + Value broadcastResult = broadcastOp->getResult(0); + Value otherOperand = + (mulfOp.getLhs() == broadcastResult) ? mulfOp.getRhs() : mulfOp.getLhs(); + if (!isa(otherOperand)) { + return false; + } + + info.broadcastOp = broadcastOp; + info.expOp = expOp; + info.toTensorOp = toTensorOp; + info.copyOp = copyOp; + info.allocOp = allocOp; + info.mulfOp = mulfOp; + info.blockId = blockId; + info.newBlockId = newBlockId; + return true; +} + +} // namespace + +class ExpLoadPatternPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(ExpLoadPatternPass) + + ExpLoadPatternPass() = default; + + StringRef getArgument() const override { return "exp-load-pattern"; } + + StringRef getDescription() const override { + return "Match load->exp->broadcast from a VECTOR block into a fresh " + "VECTOR block"; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aa); + CVPipeline::ComputeBlockIdManager bm(module); + + SmallVector patterns; + module.walk([&](linalg::BroadcastOp broadcastOp) { + ExpLoadPattern info; + if (matchExpLoadPattern(broadcastOp, bm, info)) { + LOG_DEBUG("Matched exp-load pattern in block " << info.blockId); + patterns.push_back(info); + } + }); + + for (auto &info : patterns) { + if (bm.getBlockIdByOp(info.broadcastOp) != info.blockId) { + LOG_DEBUG("Stale pattern (block id changed), skip"); + continue; + } + + SmallVector coreOps = {info.broadcastOp, info.expOp, + info.toTensorOp, info.copyOp, + info.allocOp}; + + CVPipeline::SplitIf::ScalarClosure closure{info.broadcastOp->getBlock(), + coreOps, false}; + closure.collect(); + + llvm::SetVector matchedOps; + for (auto *op : coreOps) { + matchedOps.insert(op); + } + for (Operation *op : closure.scalarOps) { + matchedOps.insert(op); + } + CVPipeline::cloneScalarOpsForCrossBlockUses(bm, matchedOps, + info.newBlockId); + + if (!CVPipeline::willCreateCycle(matchedOps.getArrayRef(), memGraph, + info.newBlockId, bm)) { + for (Operation *op : matchedOps) { + bm.updateBlockIdWithInner(op, info.newBlockId); + } + + LOG_DEBUG("Process exp-load pattern: block " << info.blockId << " -> " + << info.newBlockId); + } + } + } +}; + +namespace mlir { +namespace triton { + +std::unique_ptr> createExpLoadPatternPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/ExpSubfPatternPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/ExpSubfPatternPass.cpp new file mode 100644 index 0000000000..0820d3f4e8 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/ExpSubfPatternPass.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "exp-subf-pattern"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace arith; +using namespace math; + +namespace { + +static math::ExpOp matchExpFromSubf(arith::SubFOp subfOp) { + auto subfResult = subfOp.getResult(); + if (!subfOp->hasOneUse()) { + LOG_DEBUG("SubF has multiple users, skipping"); + return nullptr; + } + + Operation *user = *subfResult.getUsers().begin(); + auto expOp = dyn_cast(user); + if (!expOp) { + LOG_DEBUG("SubF user is not ExpOp, skipping"); + return nullptr; + } + if (expOp->getBlock() != subfOp->getBlock()) { + LOG_DEBUG("SubF and ExpOp not in the same Block, skipping"); + return nullptr; + } + return expOp; +} + +static bool isValidExt(arith::SubFOp subfOp, arith::ExtFOp extOp) { + if (extOp->getBlock() != subfOp->getBlock()) { + LOG_DEBUG("SubF and ExtfOp not in the same Block, skipping"); + return false; + } + auto inType = extOp.getIn().getType(); + if (!inType.isF16()) { + LOG_DEBUG("ExtF input types are not both f16, skipping extended pattern"); + return false; + } + auto outType = extOp.getOut().getType(); + if (!outType.isF32()) { + LOG_DEBUG("ExtF output types are not both f32, skipping extended pattern"); + return false; + } + + auto extRes = extOp.getResult(); + if (!extRes.hasOneUse() || *extRes.getUsers().begin() != subfOp) { + LOG_DEBUG("ExtF has multiple users or not used by subf, skipping"); + return false; + } + + return true; +} + +static std::optional> +matchExtfFromSubf(arith::SubFOp subfOp) { + auto lhs = subfOp.getLhs(); + auto rhs = subfOp.getRhs(); + + auto lhsDef = lhs.getDefiningOp(); + auto rhsDef = rhs.getDefiningOp(); + + if (!lhsDef || !rhsDef) { + LOG_DEBUG( + "SubF operands are not both from ExtFOp, skipping extended pattern"); + return std::nullopt; + } + + if (!isValidExt(subfOp, lhsDef) || !isValidExt(subfOp, rhsDef)) { + return std::nullopt; + } + + SmallVector extfOps; + extfOps.push_back(lhsDef); + extfOps.push_back(rhsDef); + return extfOps; +} + +static void processSubfOp(arith::SubFOp subfOp, + CVPipeline::ComputeBlockIdManager &bm) { + int subfBlockId = bm.getBlockIdByOp(subfOp); + + auto expOp = matchExpFromSubf(subfOp); + if (!expOp) { + return; + } + + int expBlockId = bm.getBlockIdByOp(expOp); + if (expBlockId != subfBlockId) { + LOG_DEBUG("Updating exp blockId from " << expBlockId << " to " + << subfBlockId); + bm.updateBlockId(expOp, subfBlockId); + } else { + LOG_DEBUG("Exp blockId already matches subf, skipping update"); + } + + auto extfOps = matchExtfFromSubf(subfOp); + if (!extfOps) { + return; + } + + for (auto extfOp : *extfOps) { + int extfBlockId = bm.getBlockIdByOp(extfOp); + if (extfBlockId != subfBlockId) { + LOG_DEBUG("Updating extf blockId from " << extfBlockId << " to " + << subfBlockId); + bm.updateBlockId(extfOp, subfBlockId); + } else { + LOG_DEBUG("ExtF blockId already matches subf, skipping update"); + } + } +} + +} // anonymous namespace + +class ExpSubfPatternPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(ExpSubfPatternPass) + + ExpSubfPatternPass() = default; + + StringRef getArgument() const override { return "exp-subf-pattern"; } + + StringRef getDescription() const override { + return "Match exp-subf pattern and unify blockId for exp and optional extf " + "operations"; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + LOG_DEBUG("Input mlir:\n " << module); + CVPipeline::ComputeBlockIdManager bm(module); + + module.walk([&](arith::SubFOp subfOp) { + LOG_DEBUG("Process subf: " << subfOp << "\n"); + processSubfOp(subfOp, bm); + }); + } +}; + +namespace mlir { +namespace triton { + +std::unique_ptr> createExpSubfPatternPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/FixpipeOptPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/FixpipeOptPass.cpp new file mode 100644 index 0000000000..9bc69b0883 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/FixpipeOptPass.cpp @@ -0,0 +1,558 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" +#include +#include + +#include "ComputeBlockOpt/SplitIfByBlockId/Common.h" +#include "mlir/Analysis/TopologicalSortUtils.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "mlir/Support/LLVM.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "triton/Analysis/Utility.h" + +#define DEBUG_TYPE "fixpipe-opt" +#define LOG_DEBUG(msg) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << msg << "\n") + +using namespace mlir; +using namespace triton; + +namespace mlir { +namespace triton { + +class FixpipeOptPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(FixpipeOptPass) + + FixpipeOptPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "fixpipe-opt"; } + void getDependentDialects(DialectRegistry ®istry) const override; + + llvm::StringRef getDescription() const final { + return "Optimize matmul-cast-store pattern for fixpipe by setting " + "core_type to CUBE"; + } + +private: + bool matchFixpipePattern(linalg::MatmulOp matmulOp, + SetVector &toMergeWithMatmul, + CVPipeline::ComputeBlockIdManager &bm, + int &targetBlockId); + bool isFixpipeCastPattern(Operation *truncOp, + SetVector &matchedOps); + bool isFixpipeMulPattern(Operation *mulOp, + SetVector &matchedOps); + bool isStoreToGM(Operation *materializeOp, + SetVector &matchedOps); + bool applyFixpipeOpt(SetVector &matchedOps, int targetBlockId, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm); + bool isSubviewFromGlobalMemory(ViewLikeOpInterface viewOp, + SetVector &matchedOps); + bool isValidMul(Operation *op, Value matmulValues, + SetVector &matchedOps); +}; + +void transSource(Value value, SetVector &matchedOps, + Block *block) { + if (!CVPipeline::isScalarLike(value)) { + return; + } + + if (auto defOp = value.getDefiningOp()) { + // We need to find one point, value changes from tensor to scalar. + // So use specific op condition to stop searching. + if (isa(defOp)) { + return; + } + if (llvm::find(matchedOps, defOp) != matchedOps.end()) { + return; + } + if (defOp->getBlock() != block) { + return; + } + matchedOps.insert(defOp); + defOp->walk([&](Operation *nestedOp) { + for (Value operand : nestedOp->getOperands()) { + transSource(operand, matchedOps, block); + } + }); + } +} + +bool hasQuantScaleCompileHint(Operation *op, + SetVector &matchedOps) { + return any_of(op->getUsers(), [&](Operation *userOp) { + auto markOp = dyn_cast(userOp); + if (!markOp) { + return false; + } + matchedOps.insert(markOp); + return markOp->hasAttr(CVPipeline::kInlinableQuantScaleAttr); + }); +} + +bool FixpipeOptPass::isValidMul(Operation *op, Value matmulValue, + SetVector &matchedOps) { + // Just filter: arith.mulf/muli(scalar) + if (!isa(op) && !isa(op)) { + return false; + } + if (!hasQuantScaleCompileHint(op, matchedOps)) { + return false; + } + auto quantScalarValue = + op->getOperand(0) == matmulValue ? op->getOperand(1) : op->getOperand(0); + if (CVPipeline::isScalarLike(quantScalarValue)) { + transSource(quantScalarValue, matchedOps, op->getBlock()); + return true; + } + + // From one fill op with constant value or args.... + if (auto defOp = quantScalarValue.getDefiningOp()) { + if (auto fillOp = dyn_cast(defOp)) { + auto operands = fillOp->getOperands(); + if (!operands.empty()) { + Value fillValue = operands[0]; + if (CVPipeline::isScalarLike(fillValue)) { + if (llvm::find(matchedOps, fillOp) == matchedOps.end() && + fillOp.getBlock() == op->getBlock()) { + matchedOps.insert(fillOp); + } + transSource(fillValue, matchedOps, op->getBlock()); + return true; + } else { + LOG_DEBUG( + "Fill operand is not scalar-like, NOT match. fill=" << *fillOp); + return false; + } + } + } + } + return false; +} + +bool FixpipeOptPass::isStoreToGM(Operation *storeOp, + SetVector &matchedOps) { + Value viewValue = nullptr; + if (auto materializeOp = + dyn_cast(storeOp)) { + viewValue = materializeOp.getDest(); + } else if (auto hivmStore = dyn_cast(storeOp)) { + viewValue = hivmStore.getDst(); + } else { + LOG_DEBUG("Cannot find store op, NOT match"); + return false; + } + + matchedOps.insert(storeOp); + if (!CVPipeline::collectViewOpsAndCheckGlobalMemory(viewValue, matchedOps)) { + LOG_DEBUG("Subview is not from global memory (GM), NOT match."); + return false; + } + return true; +} + +/** Fixpipe supports cast, the pattern should be like below: + linalg.matmul + ↓ + arith.truncf(f32->bf16, f32->f16, i32->i8) + ↓ + tensor.extract_slice + ↓ + bufferization.materialize_in_destination memref.subview(gm) + After optimization, all these ops will be in same block with matmul and set + core_type to CUBE. + */ +bool FixpipeOptPass::isFixpipeCastPattern(Operation *truncOp, + SetVector &matchedOps) { + Value truncResult = truncOp->getResult(0); + if (!truncResult.hasOneUse()) { + LOG_DEBUG("Trunc not only one user, NOT match."); + return false; + } + auto maybeExtract = *truncResult.getUsers().begin(); + tensor::ExtractSliceOp extractSliceOp = nullptr; + if (auto extract = dyn_cast(maybeExtract)) { + extractSliceOp = extract; + } else if (auto consumerMatmul = dyn_cast(maybeExtract)) { + // matmul -> trunc -> matmul pattern + for (Value input : consumerMatmul.getDpsInputs()) { + if (input == truncResult) { + matchedOps.insert(truncOp); + return true; + } + } + LOG_DEBUG("Trunc result is not a DPS input of consumer matmul, NOT match."); + return false; + } else { + LOG_DEBUG("Cannot find extract slice op or matmul, NOT match"); + return false; + } + + Value extractResult = extractSliceOp.getResult(); + if (!extractResult.hasOneUse()) { + LOG_DEBUG("Extract Slice not only one user, NOT match."); + return false; + } + auto maybeMaterialize = *extractResult.getUsers().begin(); + bufferization::MaterializeInDestinationOp materializeOp = nullptr; + + if (auto materialize = dyn_cast( + maybeMaterialize)) { + materializeOp = materialize; + } else { + LOG_DEBUG("Cannot find materialize op, NOT match"); + return false; + } + + matchedOps.insert(truncOp); + matchedOps.insert(extractSliceOp); + if (!isStoreToGM(materializeOp, matchedOps)) { + LOG_DEBUG("Not store to GM pattern, NOT match."); + return false; + } + return true; +} + +std::optional getOneUserExceptMarkOp(Operation *op) { + int count = 0; + Operation *onlyUser = nullptr; + for (auto user : op->getUsers()) { + if (!isa(user)) { + count += 1; + onlyUser = user; + } + } + if (count != 1) { + return std::nullopt; + } else { + return onlyUser; + } +} + +/** Fixpipe supports scaling, the pattern should be like below: + linalg.matmul + ↓ + arith.mulf/muli (mul one scalar-like value for quantization) + ↓ + tensor.extract_slice (optional) + ↓ + bufferization.materialize_in_destination memref.subview(gm) + After optimization, all these ops will be in same block with matmul and set + core_type to CUBE. + */ +bool FixpipeOptPass::isFixpipeMulPattern(Operation *mulOp, + SetVector &matchedOps) { + Value mulResult = mulOp->getResult(0); + auto maybeExtract = getOneUserExceptMarkOp(mulOp).value_or(nullptr); + if (!maybeExtract) { + LOG_DEBUG("Mul not only one user, NOT match."); + return false; + } + tensor::ExtractSliceOp extractSliceOp = nullptr; + if (auto extract = dyn_cast(maybeExtract)) { + extractSliceOp = extract; + matchedOps.insert(extractSliceOp); + } + + if (extractSliceOp && !getOneUserExceptMarkOp(extractSliceOp).has_value()) { + LOG_DEBUG("Extract Slice not only one user, NOT match."); + return false; + } + + auto storeOp = extractSliceOp ? getOneUserExceptMarkOp(extractSliceOp).value() + : maybeExtract; + matchedOps.insert(mulOp); + if (!isStoreToGM(storeOp, matchedOps)) { + LOG_DEBUG("Not store to GM pattern, NOT match."); + return false; + } + return true; +} + +/** + * Resolve the first value that escapes a chain of nested `scf.for` loops + * from a value defined inside the loop body. + * + * When a loop-carried value is updated by exactly one operation per + * iteration (the incoming block argument has a single use, which is the + * defining op of `nowV`, and `nowV` in turn is yielded as that same + * loop-carried operand), this function walks outward to the result of the + * outermost such loop. + * + * Example: + * scf.for (%a = %init) { + * %a_i = some_op(%a) // %a has a single use -> some_op + * scf.yield %a_i // %a_i has a single use -> yield + * } + * %b = user(%a_n, %k) // %a_n is the loop result + * + * Given `nowV = %a_i` and `outerInValue = %a` (the block argument), this + * returns `%a_n` — the first value visible to users outside the loop. + * + * \param nowV Result of the op defined inside the loop body. + * \param outerInValue The loop-carried block argument (or, at the outermost + * level, the loop init value) that feeds the op defining + * `nowV`. + * \return The value that escapes the outermost qualifying loop; + * if any precondition fails, `nowV` is returned unchanged. + */ +static Value getFirstResultAfterLoop(Value nowV, Value outerInValue) { + + if (outerInValue.getDefiningOp()) { + // we find the outer loop, + return nowV; + } + + auto op = nowV.getDefiningOp(); + auto parentOp = op->getParentOp(); + auto nextSearchValue = nowV; + + if (auto forOp = dyn_cast(parentOp)) { + auto blockArg = dyn_cast_if_present(outerInValue); + if (!blockArg || blockArg.getOwner() != forOp.getBody() || + !blockArg.hasOneUse()) { + return nowV; + } + auto argUser = blockArg.getUses().begin()->getOwner(); + if (argUser != op) { + return nowV; + } + + if (!nowV.hasOneUse()) { + return nowV; + } + auto nowVUser = nowV.getUses().begin()->getOwner(); + int argIdx = + CVPipeline::getLoopCarriedArgIndex(blockArg, blockArg.getOwner()); + auto yieldOp = dyn_cast(nowVUser); + if (!yieldOp || yieldOp->getBlock() != forOp.getBody() || + nowV.getUses().begin()->getOperandNumber() != argIdx) { + return nowV; + } + + outerInValue = forOp.getInitArgs()[argIdx]; + nextSearchValue = forOp->getResult(argIdx); + } else { + LOG_DEBUG("WARN: not limited in one for"); + return nowV; + } + + return getFirstResultAfterLoop(nextSearchValue, outerInValue); +} + +/** Match fixpipe optimization patterns starting from a matmul operation. + Pattern 1 (Cast Pattern): + linalg.matmul -> arith.truncf/i -> tensor.extract_slice -> + bufferization.materialize_in_destination(memref.subview(gm)) + + Pattern 2 (Quantization Pattern): + linalg.matmul -> arith.mulf -> tensor.extract_slice -> + bufferization.materialize_in_destination(memref.subview(gm)) + + NOTE: This function can assure target block id is not -1. + */ +bool FixpipeOptPass::matchFixpipePattern( + linalg::MatmulOp matmulOp, SetVector &toMergeWithMatmul, + CVPipeline::ComputeBlockIdManager &bm, int &targetBlockId) { + LOG_DEBUG("Check matmul op: " << *matmulOp); + Value matmulResult = matmulOp.getResult(0); + if (!matmulResult.hasOneUse()) { + LOG_DEBUG("Matmul not only one user, NOT match."); + return false; + } + Value outerOutValue = + getFirstResultAfterLoop(matmulResult, *matmulOp.getDpsInits().begin()); + + auto outerOutOp = outerOutValue.getDefiningOp(); + LOG_DEBUG("outerOutOp = " << *outerOutOp); + if (bm.getBlockIdByOp(outerOutOp) == -1) { + if (llvm::failed(bm.markOpBlockId(outerOutOp))) { + LOG_DEBUG("Matmul have no blockID, NOT match."); + return false; + } + } + targetBlockId = bm.getBlockIdByOp(outerOutOp); + + if (!outerOutValue.hasOneUse()) { + LOG_DEBUG("Matmul(outerOutValue) not only one user, NOT match."); + return false; + } + if (isa(outerOutOp)) { + toMergeWithMatmul.insert(outerOutOp); + } + + auto matmulUser = *outerOutValue.getUsers().begin(); + + if (CVPipeline::getFixpipePreQuantMode(matmulUser).has_value()) { + if (isFixpipeCastPattern(matmulUser, toMergeWithMatmul)) { + return true; + } + } else if (isValidMul(matmulUser, matmulResult, toMergeWithMatmul)) { + if (isFixpipeMulPattern(matmulUser, toMergeWithMatmul)) { + return true; + } + } else { + LOG_DEBUG("Cannot find valid consumer op (trunc or mulf), NOT match."); + return false; + } + + return false; +} + +bool FixpipeOptPass::applyFixpipeOpt( + SetVector &matchedOps, int targetBlockId, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + // If there are no cycle in Compute Block level, we apply: + // 1. Change block_id to the matmul's block id + // 2. Change core_type to CUBE. + Operation *matmulOp = matchedOps[0]; + for (auto op : matchedOps) { + if (isa(op)) { + matmulOp = op; + break; + } + } + auto block = matmulOp->getBlock(); + + if (CVPipeline::willCreateCycle(matchedOps.getArrayRef(), memGraph, + targetBlockId, bm)) { + return false; + } + for (Operation *op : matchedOps) { + if (isa(op->getDialect())) { + op->walk([&](Operation *nestedOp) { + // Never fold a sync into a compute block: it must keep its own unique + // block id so the fence between before/after ops survives. + if (CVPipeline::isSyncOp(nestedOp)) { + return WalkResult::advance(); + } + bm.updateBlockId(nestedOp, targetBlockId); + nestedOp->setAttr(CVPipeline::kCoreType, + StringAttr::get(op->getContext(), "CUBE")); + return WalkResult::advance(); + }); + } else { + bm.updateBlockId(op, targetBlockId); + op->setAttr(CVPipeline::kCoreType, + StringAttr::get(op->getContext(), "CUBE")); + } + } + return true; +} + +void FixpipeOptPass::getDependentDialects(DialectRegistry ®istry) const { + registry.insert(); +} + +void FixpipeOptPass::runOnOperation() { + LOG_DEBUG("== FixpipeOpt Pass Start ==\n"); + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LOG_DEBUG(module); + auto &aliasAnalysis = getAnalysis(); + CVPipeline::MemoryDependenceGraph memDepGraph(module, aliasAnalysis); + LOG_DEBUG(module); + + SmallVector, int>> allMatchedPatterns; + + CVPipeline::ComputeBlockIdManager bm(module); + module.walk([&](linalg::MatmulOp matmulOp) { + SetVector matchedOps; + int targetBlockId = -1; + if (matchFixpipePattern(matmulOp, matchedOps, bm, targetBlockId)) { + allMatchedPatterns.push_back({matchedOps, targetBlockId}); + } + }); + LOG_DEBUG("== Found " << allMatchedPatterns.size() + << " fixpipe patterns ==\n"); + + /** Inorder to avoid cycle, clone scalar-like ops. + A-> B -> C + ↘ ↗ + D + Now we want to fuse A/B/C, so clone A' for D to avoid cycle. + */ + for (auto &[matchedOps, targetBlockId] : allMatchedPatterns) { + if (matchedOps.empty()) { + continue; + } + CVPipeline::SplitIf::ScalarClosure closure{matchedOps.front()->getBlock(), + matchedOps.getArrayRef(), false}; + closure.collect(); + for (auto op : closure.scalarOps) { + matchedOps.insert(op); + } + CVPipeline::cloneScalarOpsForCrossBlockUses(bm, matchedOps, targetBlockId); + if (!applyFixpipeOpt(matchedOps, targetBlockId, memDepGraph, bm)) { + for (Operation *op : matchedOps) { + LOG_DEBUG("Cannot set block id for op: " << *op); + } + LOG_DEBUG("Cannot set one Block Id, may be because cycle"); + } + } + + LOG_DEBUG("== FixpipeOpt Pass Complete ==\n"); +} + +std::unique_ptr> createFixpipeOptPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeComputeBlockPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeComputeBlockPass.cpp new file mode 100644 index 0000000000..ab8729fbd3 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeComputeBlockPass.cpp @@ -0,0 +1,578 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ComputeBlockOpt/SplitIfByBlockId/Common.h" +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/Common/DependencyHelper.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" + +#include "DynamicCVPipeline/Common/Utils.h" + +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/Interfaces/ViewLikeInterface.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include + +static constexpr const char *DEBUG_TYPE = "merge-compute-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +static constexpr llvm::StringLiteral kEnableMergeComputeBlockKernels[] = { + "flex_attention_backward_dkdv_kernel", + "flex_attention_backward_dkdv_kernel_tasklist", "_swa_bwd_dkdv_kernel", + "kernel_sdpa_bwd_kv", "kernel_da_bwd_kv_ul"}; + +using namespace mlir; +using namespace triton; + +/// Represents a ComputeBlock: a group of ops sharing the same block_id +struct ComputeBlock { + int id; // block_id value + CVPipeline::CoreType coreType; // CUBE_ONLY / VECTOR_ONLY + SmallVector ops; // all ops in the group (in IR order) +}; + +static bool hasTensorResult(const ComputeBlock &blk) { + for (Operation *op : blk.ops) { + for (Value result : op->getResults()) { + if (isa(result.getType())) { + return true; + } + } + } + return false; +} + +/// Step 1: Group ops and build block-level dependency graph (enhanced). +/// computeBlocks: output, block_id → ComputeBlock +/// succs/preds: output, block_id → successor/predecessor block_id list +/// blockEdges: output, srcId → dstId → source ops (cross-block SSA/memory +/// dependency sources) +static void groupAndBuildGraph( + Block *block, const CVPipeline::MemoryDependenceGraph &memGraph, + DenseMap &computeBlocks, + DenseMap> &succs, + DenseMap> &preds, + DenseMap>> &blockEdges) { + // Walk all ops in the body block and its nested regions, group by + // block_id + block->walk([&](Operation *op) { + if (op->hasTrait()) { + return; + } + auto optId = CVPipeline::getOpBlockId(op); + if (!optId.has_value()) { + return; + } + int bid = *optId; + if (!computeBlocks.contains(bid)) { + // find new block_id, create new ComputeBlock + computeBlocks[bid] = {bid, CVPipeline::getOpCoreType(op), {}}; + } + computeBlocks[bid].ops.push_back(op); + }); + + if (computeBlocks.empty()) { + return; + } + + // Build dependency graph between ComputeBlocks via DependencyHelper, + // covering both SSA operands and memory execution dependencies. + CVPipeline::DependencyHelper helper(memGraph); + DenseSet> seenEdges; + DenseSet> seenDeps; + for (auto &kv : computeBlocks) { + int curId = kv.first; + for (Operation *op : kv.second.ops) { + helper.forEachSource(op, [&](Operation *src) { + Operation *ancestor = CVPipeline::getAncestorInBlock(src, block); + if (!ancestor) { + return WalkResult::advance(); // not in this block + } + auto ancIdOpt = CVPipeline::getOpBlockId(ancestor); + if (!ancIdOpt.has_value()) { + return WalkResult::advance(); + } + int ancId = *ancIdOpt; + if (ancId == curId) { + return WalkResult::advance(); // same ComputeBlock internal edge + } + + // Record the source op that triggers this cross-block edge + // (dedup: outer op and its nested subOps report the same source) + if (seenDeps.insert({ancId, curId, src}).second) { + blockEdges[ancId][curId].push_back(src); + } + + if (!seenEdges.insert({ancId, curId}).second) { + return WalkResult::advance(); + } + succs[ancId].push_back(curId); + preds[curId].push_back(ancId); + return WalkResult::advance(); + }); + } + } +} + +/// Find the first CUBE predecessor of a given block. +static std::optional +findCubePred(int blockId, const DenseMap &computeBlocks, + const DenseMap> &preds) { + auto it = preds.find(blockId); + if (it == preds.end()) { + return std::nullopt; + } + for (int p : it->second) { + if (computeBlocks.lookup(p).coreType == CVPipeline::CoreType::CUBE_ONLY) { + return p; + } + } + return std::nullopt; +} + +/// Collect all transitive dependencies of \p startOp via SSA use-def chains, +/// memory execution predecessors, and ops nested in regions. Results are +/// accumulated into \p collected (including \p startOp itself). +static void collectAllDeps(Operation *startOp, + const CVPipeline::MemoryDependenceGraph &memGraph, + SmallPtrSetImpl &collected) { + collected.insert(startOp); + CVPipeline::DependencyHelper helper(memGraph); + SmallVector worklist = {startOp}; + while (!worklist.empty()) { + Operation *cur = worklist.pop_back_val(); + helper.forEachSource(cur, [&](Operation *src) { + if (collected.insert(src).second) + worklist.push_back(src); + return WalkResult::advance(); + }); + } +} + +/// Check if Cube depends on CubePre via bufferization::ToTensorOp defined in +/// CubePre. \p edgeSrcOps are the dependency source ops on the CubePre → Cube +/// edges. Returns the to_tensor ops in CubePre that Cube depends on. +static SmallVector +findToTensorDeps(const ComputeBlock *cubePre, + ArrayRef edgeSrcOps) { + SmallVector result; + if (!cubePre) { + return result; + } + DenseSet cubePreOpsSet(cubePre->ops.begin(), cubePre->ops.end()); + + for (Operation *srcOp : edgeSrcOps) { + if (isa(srcOp) && + cubePreOpsSet.contains(srcOp)) { + result.push_back(srcOp); + } + } + return result; +} + +/// Clone ops from CubePre into Cube at the front of Cube's ops. +/// Selected ops must be in CubePre and are in `toClone`. +static void +cloneOpCrossCubeDep(int cubePreId, int cubeId, + const SmallPtrSet &toClone, + const DenseMap &computeBlocks, + CVPipeline::ComputeBlockIdManager &bm) { + const auto &cubePreBlock = computeBlocks.at(cubePreId); + const auto &cubeBlock = computeBlocks.at(cubeId); + + if (cubeBlock.ops.empty()) { + return; + } + + Operation *insertBefore = cubeBlock.ops.front(); + OpBuilder builder(insertBefore); + IRMapping mapper; + for (Operation *op : cubePreBlock.ops) { + if (!toClone.contains(op)) { + continue; + } + Operation *cloned = builder.clone(*op, mapper); + cloned->walk( + [&](Operation *innerOp) { bm.updateBlockId(innerOp, cubeId); }); + } + + // Remap Cube's original ops' operands: replace references to old CubePre + // values with the corresponding cloned values now in Cube. + for (Operation *op : cubeBlock.ops) { + if (toClone.contains(op)) { + continue; + } + for (auto &operand : op->getOpOperands()) { + if (Value mapped = mapper.lookupOrNull(operand.get())) { + operand.set(mapped); + } + } + } +} + +/// Step 2: Collect VECTOR_ONLY candidates that have tensor results +/// and have at least one edge (incoming or outgoing) to a CUBE block. +static SmallVector +collectVectorCandidates(const DenseMap &computeBlocks, + const DenseMap> &succs, + const DenseMap> &preds) { + auto hasCubeNeighbor = [&](const DenseMap> &neighbors, + int blockId) { + auto it = neighbors.find(blockId); + return it != neighbors.end() && llvm::any_of(it->second, [&](int id) { + return computeBlocks.lookup(id).coreType == + CVPipeline::CoreType::CUBE_ONLY; + }); + }; + + SmallVector vecCandidates; + for (auto &kv : computeBlocks) { + if (kv.second.coreType != CVPipeline::CoreType::VECTOR_ONLY) + continue; + if (!hasTensorResult(kv.second)) + continue; + + // Must have a successor or predecessor that is CUBE + bool hasCubeEdge = + hasCubeNeighbor(succs, kv.first) || hasCubeNeighbor(preds, kv.first); + if (!hasCubeEdge) + continue; + + if (!llvm::is_contained(vecCandidates, kv.first)) + vecCandidates.push_back(kv.first); + } + return vecCandidates; +} + +/// Step 3: Find a pair of adjacent VECTOR blocks (predV → succV). +/// Returns {predVId, succVId} if a pair is found, otherwise nullopt. +static std::optional> +findAdjacentVectorPair(ArrayRef vecCandidates, + const DenseMap> &succs) { + for (int candId : vecCandidates) { + auto it = succs.find(candId); + if (it == succs.end()) + continue; + for (int succId : it->second) { + if (llvm::is_contained(vecCandidates, succId)) { + return std::make_pair(candId, succId); + } + } + } + return std::nullopt; +} + +static void markSubBlock(const DenseMap &computeBlocks, + int predVId, int succVId) { + for (int id : {predVId, succVId}) { + auto it = computeBlocks.find(id); + if (it == computeBlocks.end()) + continue; + for (Operation *op : it->second.ops) { + int curId = CVPipeline::getOpBlockId(op).value_or(id); + CVPipeline::setSubBlockId(op, curId); + } + } +} + +/// Step 4: Try to directly merge succV into predV. +/// Returns true if merge succeeded. +static bool tryDirectMerge(ArrayRef opsToMerge, + const CVPipeline::MemoryDependenceGraph &memGraph, + int predVId, int succVId, + const DenseMap &computeBlocks, + CVPipeline::ComputeBlockIdManager &bm) { + if (willCreateCycle(opsToMerge, memGraph, predVId, bm)) + return false; + LOG_DEBUG("Successfully direct merge: " << succVId << " -> " << predVId); + + markSubBlock(computeBlocks, predVId, succVId); + for (Operation *op : opsToMerge) + bm.updateBlockId(op, predVId); + return true; +} + +/// Step 5e: Collect ops in CubePre that need to be cloned to Cube to break +/// the cycle. Returns the set of ops to clone (empty if none). +static SmallPtrSet +collectOpsToBreakCycle(int cubePreId, + const DenseMap &computeBlocks, + const SmallPtrSet &allPredecessors) { + auto cbIt = computeBlocks.find(cubePreId); + + SmallPtrSet opsToClone; + for (Operation *op : allPredecessors) { + if (llvm::is_contained(cbIt->second.ops, op)) + opsToClone.insert(op); + } + return opsToClone; +} + +/// Step 5: Try cross-Cube clone to break the cycle, then merge. +/// Returns true if merge succeeded after clone. +static bool tryCrossCubeCloneMerge( + Block *block, const DenseMap &computeBlocks, + const DenseMap> &preds, + const DenseMap>> &blockEdges, + int predVId, int succVId, ArrayRef opsToMerge, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + // 5a. Find succV's CUBE predecessor (Cube) + std::optional cubeId = findCubePred(succVId, computeBlocks, preds); + if (!cubeId) { + LOG_DEBUG("MergeComputeBlock: succV " + << succVId + << " has no CUBE predecessor, skipping cross-Cube clone"); + return false; + } + + // 5b. Find Cube's CUBE predecessor (CubePre) + std::optional cubePreId = findCubePred(*cubeId, computeBlocks, preds); + if (!cubePreId) { + LOG_DEBUG("MergeComputeBlock: Cube " + << *cubeId + << " has no CUBE predecessor, skipping cross-Cube clone"); + return false; + } + + // 5c. Check if Cube depends on CubePre via to_tensor + SmallVector toTensorOps; + auto edgeIt = blockEdges.find(*cubePreId); + if (edgeIt != blockEdges.end()) { + auto dstIt = edgeIt->second.find(*cubeId); + if (dstIt != edgeIt->second.end()) { + toTensorOps = + findToTensorDeps(&computeBlocks.at(*cubePreId), dstIt->second); + } + } + if (toTensorOps.empty()) { + LOG_DEBUG("MergeComputeBlock: Cube(" + << *cubeId << ") depends on CubePre(" << *cubePreId + << ") but not via to_tensor, skipping"); + return false; + } + + // 5d. Trace back all transitive dependencies + SmallPtrSet allPredecessors; + for (Operation *toTensor : toTensorOps) { + collectAllDeps(toTensor, memGraph, allPredecessors); + } + + // 5e. Filter to ops in CubePre and clone to Cube + SmallPtrSet opsToClone = + collectOpsToBreakCycle(*cubePreId, computeBlocks, allPredecessors); + + for (auto op : opsToClone) + LOG_DEBUG("Cloning op: " << *op); + cloneOpCrossCubeDep(*cubePreId, *cubeId, opsToClone, computeBlocks, bm); + + // 5f. Re-check cycle after cloning + if (willCreateCycle(opsToMerge, memGraph, predVId, bm)) { + LOG_DEBUG("MergeComputeBlock: still creates cycle after cross-Cube clone " + "for VECTOR " + << predVId << " -> " << succVId); + return false; + } + + LOG_DEBUG("MergeComputeBlock: Successfully merge after cross-Cube clone: " + << succVId << " -> " << predVId); + markSubBlock(computeBlocks, predVId, succVId); + for (Operation *op : opsToMerge) + bm.updateBlockId(op, predVId); + return true; +} + +/// Core merge logic for one scf::ForOp body Block. +static bool tryMergeInBlock(Block *block, CVPipeline::ComputeBlockIdManager &bm, + const CVPipeline::MemoryDependenceGraph &memGraph) { + bool mergedAny = false; + while (true) { + // Step 1: Group and build enhanced dependency graph + /// computeBlocks: block_id → ComputeBlock + DenseMap computeBlocks; + /// succs/preds: block_id → successor/predecessor block‘s blockid list + DenseMap> succs; + DenseMap> preds; + /// blockEdges: srcId → dstId → source ops (cross-block SSA/memory deps) + DenseMap>> blockEdges; + groupAndBuildGraph(block, memGraph, computeBlocks, succs, preds, + blockEdges); + if (computeBlocks.empty()) + return mergedAny; + + // Step 2: Collect VECTOR candidates + SmallVector vecCandidates = + collectVectorCandidates(computeBlocks, succs, preds); + if (vecCandidates.size() < 2) { + LOG_DEBUG("MergeComputeBlock: vecCandidates.size() < 2, skipping"); + return mergedAny; + } + + // Step 3: Find a pair of adjacent VECTOR blocks + auto pairOpt = findAdjacentVectorPair(vecCandidates, succs); + if (!pairOpt) { + LOG_DEBUG("MergeComputeBlock: No adjacent VECTOR pair found, skipping"); + return mergedAny; + } + int predVId = pairOpt->first; + int succVId = pairOpt->second; + + LOG_DEBUG("Found VECTOR pair: predV=" << predVId << ", succV=" << succVId); + + SmallVector opsToMerge = computeBlocks.at(succVId).ops; + + // Step 4: Try direct merge + if (tryDirectMerge(opsToMerge, memGraph, predVId, succVId, computeBlocks, + bm)) { + mergedAny = true; + continue; + } + + // Step 5: Try cross-Cube clone merge + if (!tryCrossCubeCloneMerge(block, computeBlocks, preds, blockEdges, + predVId, succVId, opsToMerge, memGraph, bm)) + return mergedAny; + // continue to try next pair + mergedAny = true; + } +} + +namespace { + +class MergeComputeBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MergeComputeBlockPass) + + MergeComputeBlockPass() = default; + + StringRef getArgument() const override { return "merge-compute-block"; } + + StringRef getDescription() const override { + return "Merge adjacent vector compute blocks between/around CUBE blocks"; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + // Record that this pass ran. Stays false unless a merge actually + // succeeds; the following ReorderOpsByBlockIdPass reads this marker. + module->setAttr(CVPipeline::kMergeComputeBlockApplied, + BoolAttr::get(&getContext(), false)); + + bool shouldRun = false; + for (auto funcOp : module.getOps()) { + if (llvm::is_contained(kEnableMergeComputeBlockKernels, + funcOp.getSymName())) { + LOG_DEBUG( + "Enable MergeComputeBlock for kernel: " << funcOp.getSymName()); + shouldRun = true; + break; + } + } + if (!shouldRun) { + return; + } + + BufferCountManager bufMgr(module); + int intraBufCount = + bufMgr.getBufferCountByType(BufferCountManager::DepType::IntraCore); + int interCoreBufCount = + bufMgr.getBufferCountByType(BufferCountManager::DepType::InterCore); + if (intraBufCount < 3 || interCoreBufCount < 2) { + LOG_DEBUG("MergeComputeBlock disabled: intraBufCount < 3 or " + "interCoreBufCount < 2"); + return; + } + + LOG_DEBUG("Before MergeComputeBlockPass: " << *module); + + SmallVector blocksToProcess; + llvm::LogicalResult walkResult = + CVPipeline::SplitIf::walkMainLoop(module, [&](Operation *loop) { + blocksToProcess.push_back(&loop->getRegion(0).front()); + return success(); + }); + if (failed(walkResult)) { + return; + } + + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aa); + CVPipeline::ComputeBlockIdManager bm(module); + bool mergedAny = false; + for (Block *block : blocksToProcess) { + LOG_DEBUG("try merge in LoopBlock"); + mergedAny = tryMergeInBlock(block, bm, memGraph) || mergedAny; + } + if (mergedAny) { + // A merge actually happened; let the following + // ReorderOpsByBlockIdPass know it should run. + module->setAttr(CVPipeline::kMergeComputeBlockApplied, + BoolAttr::get(&getContext(), true)); + } + + LOG_DEBUG("After MergeComputeBlockPass: " << *module); + } +}; + +} // namespace + +// ============================================================================ +// Pass Registration +// ============================================================================ + +namespace mlir { +namespace triton { + +std::unique_ptr> createMergeComputeBlockPass() { + return std::make_unique(); +} + +void registerMergeComputeBlockPass() { + PassRegistration reg; +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeCubeBlock.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeCubeBlock.cpp new file mode 100644 index 0000000000..999bc400ad --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeCubeBlock.cpp @@ -0,0 +1,412 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ComputeBlockOpt/SplitIfByBlockId/Common.h" +#include "DynamicCVPipeline/Common/BlockDependencyGraph.h" +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/MergeCubeBlockPass.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinOps.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; +using namespace CVPipeline; + +static constexpr const char *DEBUG_TYPE = "merge-cube-block"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) LLVM_DEBUG(DBGS() << __VA_ARGS__ << "\n") + +static const llvm::DenseSet kDisableMergeCubeKernel = { + "_attn_bwd", + "kernel_sdpa_bwd_q", + "_sdpa_infer_kernel", + "pcb06_tc02_c2v2v2c_chain", + "flex_attention_backward_dq_kernel", + "parallel_deltaformer_bwd_kernel_qk", + "chunk_kda_bwd_kernel_intra", +}; + +void MergeCubeBlockPass::runOnOperation() { + auto moduleOp = getOperation(); + + if (hasFallbackAttr(moduleOp)) { + return; + } + + for (auto funcOp : moduleOp.getOps()) { + if (kDisableMergeCubeKernel.contains(funcOp.getSymName())) { + LDBG("Found unsupport kernel: " << funcOp.getSymName()); + moduleOp->setAttr(CVPipeline::kMergeComputeBlockApplied, + BoolAttr::get(&getContext(), false)); + return; + } + } + + auto &aa = getAnalysis(); + auto memGraph = MemoryDependenceGraph(moduleOp, aa); + auto bm = ComputeBlockIdManager(moduleOp); + + LDBG("Starting MergeCubeBlockPass\n" << moduleOp); + + // Collect main loop body blocks via the shared walkMainLoop helper. + llvm::SmallVector mainLoopBlocks; + if (failed(CVPipeline::SplitIf::walkMainLoop( + moduleOp, [&](Operation *loop) -> llvm::LogicalResult { + mainLoopBlocks.push_back(&loop->getRegion(0).front()); + return llvm::success(); + }))) { + CVPipeline::setFallbackAttr(moduleOp, CVPipeline::ERRCODE_FAILED); + return; + } + + LDBG("Found " << mainLoopBlocks.size() << " main loop blocks\n"); + + // Process each main loop block + bool mergedAny = false; + for (Block *block : mainLoopBlocks) { + if (failed(processBlock(block, memGraph, bm, mergedAny))) { + CVPipeline::setFallbackAttr(moduleOp, CVPipeline::ERRCODE_FAILED); + return; + } + } + + if (!mergedAny) { + moduleOp->setAttr(CVPipeline::kMergeComputeBlockApplied, + BoolAttr::get(&getContext(), false)); + } + + LDBG("MergeCubeBlockPass completed\n" << moduleOp); +} + +llvm::LogicalResult +MergeCubeBlockPass::processBlock(Block *block, + const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm, bool &mergedAny) { + LDBG("Processing Block: " << *block); + if (Operation *parentOp = block->getParentOp()) { + LDBG(" Parent operation: " << parentOp->getName().getStringRef()); + } + + // Step 1: Build Block dependency graph + BlockDependencyGraph graph(block, memGraph, bm); + if (failed(graph.buildGraph())) { + LDBG("Failed to build dependency graph"); + return llvm::failure(); + } + + LDBG("Built dependency graph with " << graph.blockNodes.size() + << " blocks\n"); + + // If there are at least two cube blocks, print the graph for debugging. + LLVM_DEBUG({ + unsigned cubeCount = 0; + for (auto &entry : graph.blockNodes) + if (entry.second->isCube) + ++cubeCount; + if (cubeCount >= 2) { + LDBG("=== Printing dependency graph for current region ==="); + printGraph(graph); + } + }); + + // Step 2: Perform iterative merging. The nested loop inside performs the + // candidate selection itself by re-scanning the live cube blocks every + // round, so there is no separate findMergeCandidates phase. + if (failed(performMerging(graph, memGraph, bm, mergedAny))) { + LDBG("Failed to perform merging"); + return llvm::failure(); + } + + return llvm::success(); +} + +llvm::LogicalResult +MergeCubeBlockPass::performMerging(BlockDependencyGraph &graph, + const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm, bool &mergedAny) { + + // Collect the live cube blocks once. After every merge we simply drop + // the merged-out source from this list, so we never need to re-scan + // graph.blockNodes during the merging rounds. + llvm::SmallVector cubeBlocks; + for (auto &entry : graph.blockNodes) { + if (entry.second->isCube) + cubeBlocks.push_back(entry.second.get()); + } + + bool merged = true; + int mergeCount = 0; + + while (merged) { + merged = false; + + // Nested scan: outer index plays the role of target, inner index the + // role of source. As soon as one (target, source) pair is mergeable we + // commit, drop `source` from the list, and restart from the top of the + // round. + for (size_t i = 0; i < cubeBlocks.size() && !merged; ++i) { + for (size_t j = 0; j < cubeBlocks.size(); ++j) { + if (i == j) + continue; + BlockNode *target = cubeBlocks[i]; + BlockNode *source = cubeBlocks[j]; + if (!canMergeBlocks(target, source, graph, memGraph, bm)) + continue; + + LDBG("Merging block " << source->blockId << " into " + << target->blockId); + + // Execute merge + if (failed(mergeBlocks(target, source, bm))) { + LDBG("Failed to merge blocks"); + return llvm::failure(); + } + merged = true; + mergeCount++; + + // Update graph structure. After this call, the BlockNode pointed to + // by `source` has been destroyed; the pointer value itself remains + // usable for equality checks, but any dereference is unsafe. The + // pointers for `target` and unrelated nodes stay valid because + // blockNodes.erase() does not rehash or relocate other entries. + // + // Note: mergeBlocks above has already re-tagged source's ops to + // target via the ComputeBlockIdManager, which is not transactional. + // A rebuildAfterMerge failure therefore leaves the graph and the + // blockIdManager in an inconsistent state, so we abort the whole + // merging pass and surface the error to processBlock. + if (failed(graph.rebuildAfterMerge(target, source))) { + LDBG("Failed to rebuild graph after merging " + << source->blockId << " into " << target->blockId); + return llvm::failure(); + } + + // Drop the merged-out source from the candidate list in place; we + // already know it lives at index j. The pointer itself is dangling + // after the rebuild but we only use its value for erase, which + // shifts elements down to fill the gap. + cubeBlocks.erase(cubeBlocks.begin() + j); + break; + } + } + } + + mergedAny = mergeCount > 0; + + LDBG("Merged " << mergeCount << " blocks\n"); + + return llvm::success(); +} + +bool MergeCubeBlockPass::canMergeBlocks(BlockNode *target, BlockNode *source, + BlockDependencyGraph &graph, + const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + + // Precondition: both blocks must be cube + if (!target || !source || !target->isCube || !source->isCube) { + return false; + } + + // Step 1: Check if they have common input or output nodes + if (!hasCommonInputOrOutput(target, source, graph)) { + LDBG("Blocks " << target->blockId << " and " << source->blockId + << " cannot merge: no common input or output nodes\n"); + return false; + } + + // Step 2: Check if merging would create a cycle + if (!checkNoCycle(target, source, graph, memGraph, bm)) { + LDBG("Blocks " << target->blockId << " and " << source->blockId + << " cannot merge: would create cycle\n"); + return false; + } + + // Step 3: cube blocks at the same depth (with matching successor depths). + if (hasSameDepth(target, source, graph)) { + LDBG("Blocks " << target->blockId << " and " << source->blockId + << " can merge: cube blocks have same depth\n"); + return true; + } + + LDBG("Blocks " << target->blockId << " and " << source->blockId + << " cannot merge: unsupport scenario\n"); + return false; +} + +bool MergeCubeBlockPass::hasSameDepth(BlockNode *node1, BlockNode *node2, + BlockDependencyGraph &graph) { + + if (!node1 || !node2) { + return false; + } + + if (node1->depth != node2->depth) { + return false; + } + + // Check that the max depth among successors of both blocks is the same + auto getMaxSuccDepth = [&](BlockNode *node) { + int maxDepth = -1; + for (BlockNode *succNode : graph.getSuccessors(node)) { + if (succNode && succNode->isCube != node->isCube && + succNode->depth > maxDepth) { + maxDepth = succNode->depth; + } + } + return maxDepth; + }; + + int maxSuccDepth1 = getMaxSuccDepth(node1); + int maxSuccDepth2 = getMaxSuccDepth(node2); + + // If either block has no successors, still mergeable + if (maxSuccDepth1 == -1 || maxSuccDepth2 == -1) { + return true; + } + + return maxSuccDepth1 == maxSuccDepth2; +} + +llvm::DenseSet MergeCubeBlockPass::getBlocksOfDifferentType( + BlockNode *currentNode, llvm::ArrayRef blocks) { + + llvm::DenseSet result; + if (!currentNode) + return result; + + // Filter blocks: only keep blocks with a different type than currentNode. + for (BlockNode *blockNode : blocks) { + if (blockNode && blockNode->isCube != currentNode->isCube) + result.insert(blockNode); + } + return result; +} + +bool MergeCubeBlockPass::checkNoCycle(BlockNode *node1, BlockNode *node2, + BlockDependencyGraph &graph, + const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + + // Get all operations from node2 to be merged into node1 + auto opsToUnify = bm.getOpsByBlockId(node2->blockId); + + // Use willCreateCycle to check if merging would create a cycle + return !willCreateCycle(opsToUnify, memGraph, node1->blockId, bm); +} + +bool MergeCubeBlockPass::hasCommonInputOrOutput(BlockNode *node1, + BlockNode *node2, + BlockDependencyGraph &graph) { + + // Get predecessors and successors, then filter to opposite-type blocks. + // The filtered sets are already DenseSets, so set intersection is O(N). + auto filteredPreds1 = + getBlocksOfDifferentType(node1, graph.getPredecessors(node1)); + auto filteredPreds2 = + getBlocksOfDifferentType(node2, graph.getPredecessors(node2)); + auto filteredSuccs1 = + getBlocksOfDifferentType(node1, graph.getSuccessors(node1)); + auto filteredSuccs2 = + getBlocksOfDifferentType(node2, graph.getSuccessors(node2)); + + // Check if there are common input nodes (predecessors). + bool hasCommonPred = llvm::any_of( + filteredPreds2, [&](BlockNode *p) { return filteredPreds1.count(p); }); + + // Check if there are common output nodes (successors). + bool hasCommonSucc = llvm::any_of( + filteredSuccs2, [&](BlockNode *s) { return filteredSuccs1.count(s); }); + + // Return true if they have common input OR output nodes. + return hasCommonPred || hasCommonSucc; +} + +llvm::LogicalResult MergeCubeBlockPass::mergeBlocks(BlockNode *target, + BlockNode *source, + ComputeBlockIdManager &bm) { + + // Merge all ops of source into target + auto sourceOps = bm.getOpsByBlockId(source->blockId); + for (Operation *op : sourceOps) { + bm.updateBlockId(op, target->blockId); + } + + return llvm::success(); +} + +void MergeCubeBlockPass::printGraph(BlockDependencyGraph &graph) { + + LDBG("Total blocks in graph: " << graph.blockNodes.size()); + + // Print all block nodes + for (auto &entry : graph.blockNodes) { + int blockId = entry.first; + BlockNode *node = entry.second.get(); + + LDBG("Block " << blockId << ":"); + LDBG(" Type: " << (node->isCube ? "CUBE" : "VECTOR")); + LDBG(" Depth: " << node->depth); + LDBG(" Num ops: " << node->ops.size()); + + // Print predecessors + auto preds = graph.getPredecessors(node); + if (!preds.empty()) { + llvm::SmallVector predStrs; + for (BlockNode *p : preds) { + predStrs.push_back(std::to_string(p->blockId)); + } + LDBG(" Predecessors: [" << llvm::join(predStrs, ", ") << "]"); + } else { + LDBG(" Predecessors: []"); + } + + // Print successors + auto succs = graph.getSuccessors(node); + if (!succs.empty()) { + llvm::SmallVector succStrs; + for (BlockNode *s : succs) { + succStrs.push_back(std::to_string(s->blockId)); + } + LDBG(" Successors: [" << llvm::join(succStrs, ", ") << "]"); + } else { + LDBG(" Successors: []"); + } + } + + LDBG("=== End of graph ==="); +} + +std::unique_ptr> +mlir::triton::createMergeCubeBlockPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeInputInitSharedCubeBlockPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeInputInitSharedCubeBlockPass.cpp new file mode 100644 index 0000000000..aad1ebebd1 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeInputInitSharedCubeBlockPass.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" + +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; +using namespace triton; + +static constexpr const char *DEBUG_TYPE = "merge-input-init-shared-cube-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +namespace { + +// Return the matmul whose result feeds both the consumer's init (directly) and +// one of its inputs (possibly through a trunc), or null if no such matmul. +static linalg::MatmulOp getSharedInputInitProducer(linalg::MatmulOp consumer) { + auto inits = consumer.getDpsInits(); + if (inits.empty()) { + return {}; + } + auto initProducer = + dyn_cast_if_present(inits.front().getDefiningOp()); + if (!initProducer) { + return {}; + } + for (Value input : consumer.getDpsInputs()) { + auto inputProducer = dyn_cast_if_present( + CVPipeline::getSourceThroughCIntermediateOps(input)); + if (inputProducer == initProducer) { + return initProducer; + } + } + return {}; +} + +// Merge the ops of consumerBlockId into producerBlockId when it does not +// create a cycle. +static LogicalResult +tryMergeBlocks(int consumerBlockId, int producerBlockId, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + llvm::SmallVector consumerOps = + bm.getOpsByBlockId(consumerBlockId); + if (consumerOps.empty() || + CVPipeline::willCreateCycle(consumerOps, memGraph, producerBlockId, bm)) { + return failure(); + } + LOG_DEBUG("Merging block " << consumerBlockId << " into block " + << producerBlockId); + for (Operation *op : consumerOps) { + bm.updateBlockId(op, producerBlockId); + } + return success(); +} + +class MergeInputInitSharedCubeBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID( + MergeInputInitSharedCubeBlockPass) + + MergeInputInitSharedCubeBlockPass() = default; + + StringRef getArgument() const override { + return "merge-input-init-shared-cube-block"; + } + + StringRef getDescription() const override { + return "Merge cube blocks where a matmul result is used as both input and " + "init of the next matmul"; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + LOG_DEBUG("Before: MergeInputInitSharedCubeBlockPass" << *module); + + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aa); + CVPipeline::ComputeBlockIdManager bm(module); + + // Collect matmuls first; merging rewrites block_id in place. + llvm::SmallVector matmuls; + module.walk([&](linalg::MatmulOp op) { matmuls.push_back(op); }); + + for (linalg::MatmulOp consumer : matmuls) { + auto producer = getSharedInputInitProducer(consumer); + if (!producer) { + continue; + } + + auto producerBlockId = CVPipeline::getOpBlockId(producer); + auto consumerBlockId = CVPipeline::getOpBlockId(consumer); + if (!producerBlockId || !consumerBlockId || + *producerBlockId == *consumerBlockId) { + continue; + } + + if (failed(tryMergeBlocks(*consumerBlockId, *producerBlockId, memGraph, + bm))) { + LOG_DEBUG("[error] matmul result in block_id = " + << *producerBlockId + << " is used by other ops and create cycle after merge"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + } + } +}; + +} // namespace + +namespace mlir { +namespace triton { + +std::unique_ptr> +createMergeInputInitSharedCubeBlockPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeSameSourceAxisPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeSameSourceAxisPass.cpp new file mode 100644 index 0000000000..880265259d --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeSameSourceAxisPass.cpp @@ -0,0 +1,299 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" + +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "merge-same-source-axis"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; + +namespace mlir { +namespace triton { + +namespace { + +static bool isInSameRegion(Operation *op, Operation *source) { + return op->getParentRegion() == source->getParentRegion(); +} + +// Walk parent chain from start to target (inclusive), deduped via inChain. +// Caller ensures target is on start's parent chain. +static void appendPath(Operation *start, Operation *target, + const llvm::DenseMap &parent, + llvm::DenseSet &inChain, + SmallVectorImpl &chainOps) { + for (Operation *pathOp = start; pathOp; pathOp = parent.lookup(pathOp)) { + if (inChain.insert(pathOp).second) { + chainOps.push_back(pathOp); + } + if (pathOp == target) { + break; + } + } +} + +// Append convergenceOp's same-block VECTOR_ONLY direct downstream so the moved +// op doesn't leave its tail behind. +static void +extendChainWithConvergenceTail(Operation *convergenceOp, Operation *source, + llvm::DenseSet &inChain, + SmallVectorImpl &chainOps) { + auto convBlockIdOpt = CVPipeline::getOpBlockId(convergenceOp); + if (!convBlockIdOpt || *convBlockIdOpt < 0) { + return; + } + int convBlockId = *convBlockIdOpt; + for (Operation *user : convergenceOp->getUsers()) { + if (CVPipeline::getOpCoreType(user) != CVPipeline::CoreType::VECTOR_ONLY) { + continue; + } + if (!isInSameRegion(user, source)) { + continue; + } + auto userBidOpt = CVPipeline::getOpBlockId(user); + if (!userBidOpt || *userBidOpt != convBlockId) { + continue; + } + if (inChain.insert(user).second) { + chainOps.push_back(user); + } + } +} + +static bool findNearestConvergence(Operation *source, + ArrayRef consumers, + SmallVectorImpl &chainOps) { + if (consumers.size() < 2) { + return false; + } + + llvm::DenseMap firstIdx; + llvm::DenseMap parent; + SmallVector> bfsQueue; + + for (size_t consumerIndex = 0; consumerIndex < consumers.size(); + ++consumerIndex) { + Operation *consumer = consumers[consumerIndex]; + auto ins = firstIdx.insert({consumer, (int)consumerIndex}); + if (ins.second) { + parent[consumer] = nullptr; + bfsQueue.push_back({consumer, (int)consumerIndex}); + } + } + + for (size_t queueIndex = 0; queueIndex < bfsQueue.size(); ++queueIndex) { + Operation *cur = bfsQueue[queueIndex].first; + int myIdx = bfsQueue[queueIndex].second; + for (Operation *user : cur->getUsers()) { + if (CVPipeline::getOpCoreType(user) != + CVPipeline::CoreType::VECTOR_ONLY) { + continue; + } + if (!isInSameRegion(user, source)) { + continue; + } + auto it = firstIdx.find(user); + if (it == firstIdx.end()) { + firstIdx[user] = myIdx; + parent[user] = cur; + bfsQueue.push_back({user, myIdx}); + } else if (it->second != myIdx) { + if (llvm::is_contained(consumers, user)) { + continue; + } + Operation *cons1 = consumers[it->second]; + Operation *cons2 = consumers[myIdx]; + Operation *convergenceOp = user; + + chainOps.clear(); + llvm::DenseSet inChain; + inChain.clear(); + appendPath(convergenceOp, cons1, parent, inChain, chainOps); + appendPath(cur, cons2, parent, inChain, chainOps); + extendChainWithConvergenceTail(convergenceOp, source, inChain, + chainOps); + return true; + } + } + } + return false; +} + +static void tryMergeSource(Operation *source, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + auto srcBlockIdOpt = CVPipeline::getOpBlockId(source); + if (!srcBlockIdOpt || *srcBlockIdOpt < 0) { + return; + } + int srcBlockId = *srcBlockIdOpt; + + if (mlir::isa(source)) { + return; + } + + // Source must be tensor-shaped; scalars have no axis semantics. + if (source->getNumResults() != 1 || + !mlir::isa(source->getResult(0).getType())) { + return; + } + + SmallVector consumers; + for (Operation *user : source->getUsers()) { + if (CVPipeline::getOpCoreType(user) != CVPipeline::CoreType::VECTOR_ONLY) { + continue; + } + if (!isInSameRegion(user, source)) { + continue; + } + auto bidOpt = CVPipeline::getOpBlockId(user); + if (!bidOpt || *bidOpt == srcBlockId) { + continue; + } + consumers.push_back(user); + } + if (consumers.size() < 2) { + return; + } + + SmallVector chainOps; + if (!findNearestConvergence(source, consumers, chainOps)) { + return; + } + + // Guard: convergenceOp's upstream operands must all live in blocks that this + // merge will cover + Operation *convergenceOp = chainOps.front(); + for (Value operand : convergenceOp->getOperands()) { + Operation *defOp = operand.getDefiningOp(); + if (!defOp) { + continue; + } + if (!isInSameRegion(defOp, source)) { + continue; + } + if (defOp == source) { + continue; + } + if (llvm::is_contained(chainOps, defOp)) { + continue; + } + auto defBidOpt = CVPipeline::getOpBlockId(defOp); + if (defBidOpt && *defBidOpt == srcBlockId) { + continue; + } + LOG_DEBUG("[tryMergeSource] convergenceOp=" + << *convergenceOp << " has unaligned upstream defOp=" << *defOp + << ", skip"); + return; + } + + LOG_DEBUG("[tryMergeSource] candidate source=" << *source << " chainSize=" + << chainOps.size()); + + if (CVPipeline::willCreateCycle(chainOps, memGraph, srcBlockId, bm)) { + LOG_DEBUG("[tryMergeSource] willCreateCycle, skip"); + return; + } + + for (Operation *op : chainOps) { + bm.updateBlockId(op, srcBlockId); + } + LOG_DEBUG("[tryMergeSource] merged " << chainOps.size() + << " ops into block_id=" << srcBlockId); +} + +} // anonymous namespace + +class MergeSameSourceAxisPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MergeSameSourceAxisPass) + + MergeSameSourceAxisPass() = default; + + StringRef getArgument() const override { return "merge-same-source-axis"; } + + StringRef getDescription() const override { + return "Rewrite ssbuffer.block_id so that >=2 vector consumers of a " + "source op whose downstream converges at a common op end up in " + "the same block as the source. Only VECTOR core ops are considered."; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LOG_DEBUG("Before: " << *module); + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aa); + auto bm = CVPipeline::ComputeBlockIdManager(module); + + SmallVector candidates; + module.walk([&](Operation *op) { + if (CVPipeline::getOpCoreType(op) != CVPipeline::CoreType::VECTOR_ONLY || + !CVPipeline::getOpBlockId(op) || op->getUsers().empty()) { + return; + } + if (op->getNumResults() != 1 || + !mlir::isa(op->getResult(0).getType())) { + return; + } + candidates.push_back(op); + }); + + for (Operation *source : candidates) { + tryMergeSource(source, memGraph, bm); + } + + LOG_DEBUG("After: " << *module); + } +}; + +std::unique_ptr> createMergeSameSourceAxisPass() { + return std::make_unique(); +} + +void registerMergeSameSourceAxisPass() { + PassRegistration reg; +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeSmallBlockPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeSmallBlockPass.cpp new file mode 100644 index 0000000000..8333afbd2c --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeSmallBlockPass.cpp @@ -0,0 +1,859 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Debug.h" +#include +#include + +static constexpr const char *DEBUG_TYPE = "merge-small-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace triton; + +namespace mlir { +namespace triton { + +class MergeSmallBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MergeSmallBlockPass) + + MergeSmallBlockPass() = default; + + StringRef getArgument() const override { return "merge-small-block"; } + + StringRef getDescription() const override { + return "Merge small compute blocks (<= 3 ops) into their operand or user " + "block"; + } + void runOnOperation() override; + +private: + const int MIN_VF_SIZE = 3; +}; + +inline bool isTensorComputeOpLegacy(Operation *op) { + if (llvm::isa(op)) { + return false; + } + bool allOperandsTensor = llvm::all_of(op->getOperands(), [](Value operand) { + return llvm::isa(operand.getType()); + }); + bool allResultsTensor = llvm::all_of(op->getResults(), [](Value result) { + return llvm::isa(result.getType()); + }); + return allOperandsTensor && allResultsTensor; +} + +inline bool isTensorComputeOp(Operation *op) { + auto funcOp = op->getParentOfType(); + if (funcOp) { + constexpr llvm::StringLiteral legacyFuncNames[]{"pcb10_tc01_kernel"}; + if (llvm::is_contained(legacyFuncNames, funcOp.getSymName())) { + return isTensorComputeOpLegacy(op); + } + } + if (auto linalgOp = dyn_cast(op)) { + if (linalg::isaCopyOpInterface(linalgOp)) + return false; + auto genericOp = dyn_cast(op); + if (genericOp && linalg::isaBroadcastOpInterface(genericOp).has_value()) + return false; + if (auto fillOp = dyn_cast(op)) { + auto input = fillOp.getInputs()[0]; + auto constantOp = input.getDefiningOp(); + if (constantOp) { + return false; + } + } + return true; + } + + if (op->hasTrait()) { + return llvm::any_of(op->getResultTypes(), + [](Type t) { return isa(t); }); + } + return false; +} + +static int cntComputeOps(const llvm::ArrayRef ops) { + int count = 0; + for (Operation *op : ops) { + if (isTensorComputeOp(op)) { + count++; + } + } + return count; +} + +class MergeStrategy { +public: + struct Context { + Block *block; + CVPipeline::ComputeBlockIdManager &bm; + CVPipeline::MemoryDependenceGraph &memGraph; + const DenseMap &id2order; + const SmallVector &smallBlockOps; + int smallBlockId; + }; + + struct UpDownClassification { + SmallVector upBlockIds; + SmallVector downBlockIds; + }; + + virtual ~MergeStrategy() = default; + virtual void filter(SmallVector &candidates, const Context &ctx) = 0; + + static UpDownClassification + classifierUpDown(const SmallVector &candidates, const Context &ctx) { + UpDownClassification result; + + for (int blockId : candidates) { + if (ctx.id2order.contains(blockId) && + ctx.id2order.lookup(blockId) < + ctx.id2order.lookup(ctx.smallBlockId)) { + result.upBlockIds.push_back(blockId); + } else { + result.downBlockIds.push_back(blockId); + } + } + + return result; + } +}; + +class IROrderStrategy : public MergeStrategy { +public: + void filter(SmallVector &candidates, const Context &ctx) override { + auto classification = classifierUpDown(candidates, ctx); + + if (classification.downBlockIds.empty()) { + return; + } + + int selectedDownBlock = -1; + for (int id : classification.downBlockIds) { + if (selectedDownBlock == -1 || + (ctx.id2order.contains(id) && + ctx.id2order.lookup(id) < ctx.id2order.lookup(selectedDownBlock))) { + selectedDownBlock = id; + } + } + + candidates.clear(); + candidates.append(classification.upBlockIds.begin(), + classification.upBlockIds.end()); + if (selectedDownBlock != -1) { + candidates.push_back(selectedDownBlock); + } + } +}; + +class UBOccupationStrategy : public MergeStrategy { +private: + static constexpr int64_t INF = (1 << 30); + + static int64_t getValueSizeInBytes(Value value) { + if (CVPipeline::isScalarLike(value)) { + return 0; + } + Type type = value.getType(); + auto getElemBytes = [](Type elemType) -> int64_t { + if (elemType.isIntOrFloat()) { + unsigned bits = elemType.getIntOrFloatBitWidth(); + return std::max(1, bits / 8); + } + return 1; + }; + if (auto rankedTensorType = dyn_cast(type)) { + if (!rankedTensorType.hasStaticShape()) { + return INF; + } + int64_t numElements = 1; + for (int64_t dim : rankedTensorType.getShape()) { + if (dim < 0) { + return 1; + } + numElements *= dim; + } + return std::max( + 1, numElements * getElemBytes(rankedTensorType.getElementType())); + } + return INF; + } + + static int64_t getOperandUB(const llvm::ArrayRef smallBlockOps, + int blockId, + CVPipeline::ComputeBlockIdManager &bm) { + // Merge into blockId, how many UB will be saved. + int64_t total = 0; + for (Operation *op : smallBlockOps) { + for (Value operand : op->getOperands()) { + Operation *defOp = operand.getDefiningOp(); + if (!defOp) { + continue; + } + + if (bm.getBlockIdByOp(defOp) == blockId) { + if (!isa(defOp)) { + int64_t valueSize = getValueSizeInBytes(operand); + if (valueSize == INF) { + return INF; + } + total += valueSize; + } + } + } + } + return total; + } + + static int64_t getUserUB(const llvm::ArrayRef smallBlockOps, + int blockId, CVPipeline::ComputeBlockIdManager &bm) { + + // Merge into blockId, how many UB will be saved. + int64_t total = 0; + for (Operation *op : smallBlockOps) { + for (Value result : op->getResults()) { + // If result have multi users and users' block ids is not the same, + // merging cannot save UB. + if (llvm::all_of(result.getUsers(), [&](Operation *user) { + return bm.getBlockIdByOp(user) == blockId; + })) { + int64_t valueSize = getValueSizeInBytes(result); + if (valueSize == INF) { + return INF; + } + total += valueSize; + } + } + } + return total; + } + +public: + void filter(SmallVector &candidates, const Context &ctx) override { + auto classification = classifierUpDown(candidates, ctx); + + DenseMap blockIdToUB; + + for (int upBlockId : classification.upBlockIds) { + int64_t ub = getOperandUB(ctx.smallBlockOps, upBlockId, ctx.bm); + blockIdToUB[upBlockId] = ub; + LOG_DEBUG("Merge into up block " << upBlockId << " will save UB: " << ub + << "B."); + } + + for (int downBlockId : classification.downBlockIds) { + int64_t ub = getUserUB(ctx.smallBlockOps, downBlockId, ctx.bm); + blockIdToUB[downBlockId] = ub; + LOG_DEBUG("Merge into down block " << downBlockId + << " will save UB: " << ub << "B."); + } + + if (blockIdToUB.empty()) { + return; + } + + int64_t maxUB = -1; + for (auto &entry : blockIdToUB) { + if (entry.second > maxUB && entry.second != INF) { + maxUB = entry.second; + } + } + + SmallVector filtered; + for (int blockId : candidates) { + if (blockIdToUB.contains(blockId) && blockIdToUB[blockId] == maxUB) { + filtered.push_back(blockId); + } + } + + candidates = std::move(filtered); + } +}; + +class DefaultUpStrategy : public MergeStrategy { +public: + void filter(SmallVector &candidates, const Context &ctx) override { + auto classification = classifierUpDown(candidates, ctx); + + if (!classification.upBlockIds.empty()) { + candidates.clear(); + candidates.push_back(classification.upBlockIds[0]); + return; + } + + if (!classification.downBlockIds.empty()) { + candidates.clear(); + candidates.push_back(classification.downBlockIds[0]); + return; + } + } +}; + +// This strategy matches a specific Flash Attention Forward (FaFWD) pattern in +// small compute blocks. The pattern consists of 3 operations: exp, mulf, and +// addf, forming a softmax-like computation. +// +// Pattern structure: +// - exp: computes exp(subf), where subf is from an upstream block +// (upBlockIds[0]) +// - mulf: computes exp_result * arg, where arg is a loop iteration argument +// - addf: computes mulf_result + reduce, where reduce is from the same +// upstream block +// +// Data flow: +// 1. exp takes subf as input, its result is used by: +// - mulf (in the current small block) +// - broadcast (in downstream block, downBlockIds[0]) +// +// 2. mulf takes exp_result and arg (BlockArgument) as inputs, its result is +// used by: +// - addf (in the current small block, no other users) +// +// 3. addf takes mulf_result and reduce as inputs, its result is used by: +// - yield (updates the same arg that mulf uses) +// +// This pattern typically appears in Flash Attention forward pass where: +// - exp computes the exponential of (QK^T - max) +// - mulf scales by a running sum +// - addf accumulates the result +// +// When matched, the strategy recommends merging into the downstream block +// (downBlockIds[0]). +class FaFWDPatternStrategy : public MergeStrategy { +private: + bool findTargetOps(math::ExpOp &expOp, arith::MulFOp &mulfOp, + arith::AddFOp &addfOp, const Context &ctx) { + for (Operation *op : ctx.smallBlockOps) { + if (auto exp = dyn_cast(op)) { + expOp = exp; + } else if (auto mulf = dyn_cast(op)) { + mulfOp = mulf; + } else if (auto addf = dyn_cast(op)) { + addfOp = addf; + } else { + return false; + } + } + if (!expOp || !mulfOp || !addfOp) { + return false; + } + return true; + } + + bool isValidExpOp(math::ExpOp &expOp, arith::MulFOp &mulfOp, int upBlockId, + int downBlockId, const Context &ctx) { + Operation *subfDef = expOp.getOperand().getDefiningOp(); + if (!subfDef || !isa(subfDef)) { + return false; + } + int subfBlockId = ctx.bm.getBlockIdByOp(subfDef); + if (subfBlockId != upBlockId) { + return false; + } + + auto expResult = expOp.getResult(); + SmallVector expUsers; + for (Operation *user : expResult.getUsers()) { + Operation *userInBlock = CVPipeline::getAncestorInBlock(user, ctx.block); + if (userInBlock && userInBlock != ctx.block->getTerminator()) { + expUsers.push_back(user); + } + } + if (expUsers.size() != 2) { + return false; + } + bool hasMulfUser = false; + bool hasBroadcastUser = false; + for (Operation *user : expUsers) { + if (user == mulfOp) { + hasMulfUser = true; + } else if (isa(user) || + isa(user)) { + int broadcastBlockId = ctx.bm.getBlockIdByOp(user); + if (broadcastBlockId == downBlockId) { + hasBroadcastUser = true; + } + } + } + if (!hasMulfUser || !hasBroadcastUser) { + return false; + } + return true; + } + + bool isValidMulfOp(math::ExpOp &expOp, arith::MulFOp &mulfOp, + arith::AddFOp &addfOp, const Context &ctx) { + Value expResult = expOp->getResults()[0]; + auto mulfLhs = mulfOp.getLhs(); + auto mulfRhs = mulfOp.getRhs(); + + bool expIsOperand = (mulfLhs == expResult || mulfRhs == expResult); + if (!expIsOperand) { + return false; + } + + Value mulfArgOperand = (mulfLhs == expResult) ? mulfRhs : mulfLhs; + BlockArgument mulfArg = dyn_cast(mulfArgOperand); + if (!mulfArg) { + return false; + } + + for (Operation *user : mulfOp.getResult().getUsers()) { + if (user != addfOp) { + return false; + } + } + return true; + } + + bool isValidAddfOp(arith::MulFOp &mulfOp, arith::AddFOp &addfOp, + int upBlockId, const Context &ctx) { + auto addfLhs = addfOp.getLhs(); + auto addfRhs = addfOp.getRhs(); + + bool mulfIsOperand = + (addfLhs == mulfOp.getResult() || addfRhs == mulfOp.getResult()); + if (!mulfIsOperand) { + return false; + } + + Value reduceOperand = (addfLhs == mulfOp.getResult()) ? addfRhs : addfLhs; + Operation *reduceDef = reduceOperand.getDefiningOp(); + + if (!reduceDef || !isa(reduceDef)) { + return false; + } + + int reduceBlockId = ctx.bm.getBlockIdByOp(reduceDef); + if (reduceBlockId != upBlockId) { + return false; + } + + auto addfResult = addfOp.getResult(); + SmallVector addfUsers; + for (Operation *user : addfResult.getUsers()) { + addfUsers.push_back(user); + } + + // Use for updating mulfArg + if (addfUsers.size() != 1) { + return false; + } + BlockArgument mulfArg; + for (auto v : mulfOp.getOperands()) { + if (!v.getDefiningOp()) { + mulfArg = dyn_cast(v); + } + } + scf::YieldOp yieldOp = dyn_cast(addfUsers[0]); + if (!yieldOp) { + return false; + } + Block *yieldBlock = yieldOp->getBlock(); + scf::ForOp forOp = dyn_cast(yieldBlock->getParentOp()); + if (!forOp) { + return false; + } + int yieldOpIndex = -1; + auto yieldOperands = yieldOp.getOperands(); + for (int i = 0; i < yieldOperands.size(); ++i) { + if (yieldOperands[i] == addfResult) { + yieldOpIndex = i; + break; + } + } + if (yieldOpIndex == -1) { + return false; + } + BlockArgument correspondingIterArg = forOp.getRegionIterArg(yieldOpIndex); + if (correspondingIterArg != mulfArg) { + return false; + } + return true; + } + +public: + void filter(SmallVector &candidates, const Context &ctx) override { + if (ctx.smallBlockOps.size() != 3) { + return; + } + auto classification = classifierUpDown(candidates, ctx); + if (classification.upBlockIds.size() != 1 || + classification.downBlockIds.size() != 1) { + return; + } + + math::ExpOp expOp = nullptr; + arith::MulFOp mulfOp = nullptr; + arith::AddFOp addfOp = nullptr; + if (!findTargetOps(expOp, mulfOp, addfOp, ctx)) { + return; + } + + if (!isValidExpOp(expOp, mulfOp, classification.upBlockIds[0], + classification.downBlockIds[0], ctx)) { + return; + } + + if (!isValidMulfOp(expOp, mulfOp, addfOp, ctx)) { + return; + } + + if (!isValidAddfOp(mulfOp, addfOp, classification.upBlockIds[0], ctx)) { + return; + } + candidates.clear(); + candidates.push_back(classification.downBlockIds[0]); + } +}; + +static void collectUpstream(int smallBlockId, Block *block, + CVPipeline::ComputeBlockIdManager &bm, + SmallVector &candidates, + llvm::ArrayRef ops, + const CVPipeline::MemoryDependenceGraph &memGraph) { + bool haveCubeLink = false; + llvm::SmallVector allDepValues; + llvm::SmallDenseSet upBlockIds; + for (Operation *op : ops) { + for (Value operand : op->getOperands()) { + Operation *defOp = operand.getDefiningOp(); + if (!defOp) { + continue; + } + Operation *defInBlock = CVPipeline::getAncestorInBlock(defOp, block); + if (!defInBlock) { + continue; + } + if (bm.getBlockIdByOp(defInBlock) == -1 || + CVPipeline::getOpCoreType(defInBlock) != + CVPipeline::CoreType::VECTOR_ONLY) { + haveCubeLink = true; + break; + } + int bid = bm.getBlockIdByOp(defInBlock); + if (bid != -1 && bid != smallBlockId) { + upBlockIds.insert(bid); + allDepValues.push_back(operand); + } + } + if (haveCubeLink) { + break; + } + } + if (haveCubeLink) { + LOG_DEBUG("Block " << smallBlockId + << " has operand defined by CUBE, no up candidates."); + upBlockIds.clear(); + } + + if (llvm::all_of(allDepValues, + [&](Value v) { return CVPipeline::isScalarLike(v); })) { + LOG_DEBUG("Block " << smallBlockId << " only has scalar dependency."); + upBlockIds.clear(); + } + + if (upBlockIds.size() == 1) { + int upBlockId = *upBlockIds.begin(); + if (!CVPipeline::willCreateCycle(ops, memGraph, upBlockId, bm)) { + candidates.push_back(upBlockId); + } + } +} + +static void +collectDownstream(int smallBlockId, Block *block, + CVPipeline::ComputeBlockIdManager &bm, + SmallVector &candidates, llvm::ArrayRef ops, + const CVPipeline::MemoryDependenceGraph &memGraph) { + bool haveCubeLink = false; + llvm::SmallDenseSet downBlockIds; + for (Operation *op : ops) { + for (Value result : op->getResults()) { + for (Operation *user : result.getUsers()) { + Operation *userInBlock = CVPipeline::getAncestorInBlock(user, block); + if (!userInBlock || (block->mightHaveTerminator() && + userInBlock == block->getTerminator())) { + continue; + } + int bid = bm.getBlockIdByOp(userInBlock); + if (bid == smallBlockId) { + continue; + } + + if (bid == -1 || CVPipeline::getOpCoreType(userInBlock) != + CVPipeline::CoreType::VECTOR_ONLY) { + // FIXME: wait for multi-region + haveCubeLink = true; + break; + } + if (bid != -1 && bid != smallBlockId) { + downBlockIds.insert(bid); + } + } + if (haveCubeLink) { + break; + } + } + if (haveCubeLink) { + break; + } + } + if (haveCubeLink) { + LOG_DEBUG("Block " << smallBlockId + << " has result used by CUBE, no down candidates."); + downBlockIds.clear(); + } + for (int downBlockId : downBlockIds) { + if (!CVPipeline::willCreateCycle(ops, memGraph, downBlockId, bm)) { + candidates.push_back(downBlockId); + } else { + LOG_DEBUG("Block " << smallBlockId << " merge to " << downBlockId + << " will create cycle!\n"); + } + } +} + +static SmallVector +collectMergeCandidates(int smallBlockId, Block *block, + CVPipeline::ComputeBlockIdManager &bm, + const CVPipeline::MemoryDependenceGraph &memGraph) { + + SmallVector candidates; + auto ops = bm.getOpsByBlockId(smallBlockId); + + collectUpstream(smallBlockId, block, bm, candidates, ops, memGraph); + collectDownstream(smallBlockId, block, bm, candidates, ops, memGraph); + + return candidates; +} + +static std::optional +selectMergeTarget(SmallVector &candidates, + const SmallVector &ops, Block *block, + CVPipeline::ComputeBlockIdManager &bm, + CVPipeline::MemoryDependenceGraph &memGraph, + const DenseMap &id2order, int smallBlockId) { + if (candidates.size() == 1) { + return candidates[0]; + } + SmallVector> strategies; + strategies.push_back(std::make_unique()); + strategies.push_back(std::make_unique()); + strategies.push_back(std::make_unique()); + strategies.push_back(std::make_unique()); + + MergeStrategy::Context ctx{block, bm, memGraph, id2order, ops, smallBlockId}; + + for (auto &strategy : strategies) { + strategy->filter(candidates, ctx); + if (candidates.size() == 1) { + return candidates[0]; + } + } + + return std::nullopt; +} + +static void getBlockIdsInProgramOrder(Block *block, + CVPipeline::ComputeBlockIdManager &bm, + SmallVector &ordered, + DenseMap &id2order) { + ordered.clear(); + id2order.clear(); + llvm::SmallDenseSet seen; + for (Operation &op : *block) { + int bid = bm.getBlockIdByOp(&op); + if (bid != -1 && seen.insert(bid).second) { + id2order[bid] = ordered.size(); + ordered.push_back(bid); + } + } +} + +/// Match the pattern inside a small VECTOR block: +/// %m = arith.sitofp %mask +/// %b = arith.subf %_, %m +/// %r = arith.mulf %b, %_ +static bool matchSIToFPSubMulPattern(llvm::ArrayRef ops) { + arith::SIToFPOp sitofpOp; + arith::SubFOp subfOp; + arith::MulFOp mulfOp; + for (Operation *op : ops) { + if (auto sitofp = dyn_cast(op)) { + sitofpOp = sitofp; + } else if (auto subf = dyn_cast(op)) { + subfOp = subf; + } else if (auto mulf = dyn_cast(op)) { + mulfOp = mulf; + } + } + if (!sitofpOp || !subfOp || !mulfOp) { + return false; + } + Value sitofpResult = sitofpOp.getResult(); + if (subfOp.getLhs() != sitofpResult && subfOp.getRhs() != sitofpResult) { + return false; + } + Value subfResult = subfOp.getResult(); + if (mulfOp.getLhs() != subfResult && mulfOp.getRhs() != subfResult) { + return false; + } + return true; +} + +/// Tag every op in the source block and the target block with its original +/// block id via kSubBlock, before updateBlockId rewrites the block_id +/// attribute. +static void markSubBlockOps(int nowBlockId, int targetBlockId, + CVPipeline::ComputeBlockIdManager &bm) { + auto mark = [&](int blockId) { + for (Operation *op : bm.getOpsByBlockId(blockId)) { + CVPipeline::setSubBlockId(op, blockId); + } + }; + mark(nowBlockId); + mark(targetBlockId); +} + +void MergeSmallBlockPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + // The pass is scheduled twice in the pipeline. The sitofp-subf-mulf pattern + // merge should only be applied on the second run. + bool firstRun = !module->getAttrOfType( + CVPipeline::kMergeSmallBlockFirstRunDone); + module->setAttr(CVPipeline::kMergeSmallBlockFirstRunDone, + BoolAttr::get(module.getContext(), true)); + + LOG_DEBUG("Before: " << *module); + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aa); + auto bm = CVPipeline::ComputeBlockIdManager(module); + + llvm::SmallVector blocks; + SmallVector orderedBlockIds; + DenseMap id2order; + module.walk([&](Block *block) { + getBlockIdsInProgramOrder(block, bm, orderedBlockIds, id2order); + if (orderedBlockIds.size() < 2) { + return; + } + + for (int nowBlockId : orderedBlockIds) { + LOG_DEBUG("Processing block " << nowBlockId); + auto ops = bm.getOpsByBlockId(nowBlockId); + if (ops.empty() || cntComputeOps(ops) > MIN_VF_SIZE) { + continue; + } + if (CVPipeline::getOpCoreType(*ops.begin()) != + CVPipeline::CoreType::VECTOR_ONLY) { + continue; + } + + LOG_DEBUG("Processing small block " << nowBlockId); + for (auto op : ops) { + LOG_DEBUG("op:" << *op); + } + + SmallVector candidates = + collectMergeCandidates(nowBlockId, block, bm, memGraph); + + if (candidates.empty()) { + LOG_DEBUG("No candidates for block " << nowBlockId); + continue; + } + + LLVM_DEBUG({ + LOG_DEBUG("ready to merge block " << nowBlockId + << " with candidates: "); + for (int candidateId : candidates) { + LOG_DEBUG("candidate block " << candidateId); + } + }); + + auto targetBlockId = selectMergeTarget(candidates, ops, block, bm, + memGraph, id2order, nowBlockId); + + if (targetBlockId.has_value()) { + if (matchSIToFPSubMulPattern(ops)) { + if (firstRun) { + LOG_DEBUG("Defer sitofp-subf-mulf pattern merge for block " + << nowBlockId << " to second run"); + continue; + } + markSubBlockOps(nowBlockId, targetBlockId.value(), bm); + } + LOG_DEBUG("Merging block " << nowBlockId << " into block " + << targetBlockId.value()); + for (Operation *op : ops) { + bm.updateBlockId(op, targetBlockId.value()); + } + } + } + }); +} + +std::unique_ptr> createMergeSmallBlockPass() { + return std::make_unique(); +} + +void registerMergeSmallBlockPass() { + PassRegistration reg; +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeVectorIfBlockPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeVectorIfBlockPass.cpp new file mode 100644 index 0000000000..cfbe80dd54 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MergeVectorIfBlockPass.cpp @@ -0,0 +1,320 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "merge-vector-if-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace triton; + +namespace { + +/** + * @brief Check whether an scf.if only contains Vector-tagged operations + * + * A pure-vector if has every (non-terminator) operation inside its regions + * tagged as a VECTOR core op. Any operation that holds nested regions + * (i.e. nested control flow such as scf.if / scf.for) is rejected because + * such control ops carry no block tag and therefore cannot be merged. + */ +static bool isPureVectorIf(scf::IfOp ifOp) { + for (Region ®ion : ifOp->getRegions()) { + for (Block &block : region) { + for (Operation &op : block) { + if (op.hasTrait()) { + continue; + } + // Nested control flow has no Vector tag -> illegal to merge. + if (op.getNumRegions() > 0) { + return false; + } + if (CVPipeline::getOpCoreType(&op) != + CVPipeline::CoreType::VECTOR_ONLY) { + return false; + } + } + } + } + return true; +} + +/** + * @brief Check whether a group of ops (an existing block_id's ops) is safe to + * fold into a pure-VECTOR target block_id. + * + * block_id is consumed downstream both as a scheduling/reorder key + * (ReorderOpsByBlockIdPass) and as a CUBE/VECTOR compute-block boundary + * (DataDependencyAnalysis's collectBlockInfo). Merging a block that contains + * any CUBE-tagged op into a VECTOR-only block_id would silently produce a + * single block_id spanning both core types, which downstream passes do not + * expect. Sync ops are assigned unique block ids and must not be merged here. + * A group op may itself be a control-flow op (e.g. scf.if / scf.for) that + * carries no direct core_type tag, so getCoreTypeOfSimpleOpOrCf is used to + * recurse into its body instead of getOpCoreType. + */ +static bool isPureVectorOpGroup(ArrayRef ops) { + for (Operation *op : ops) { + if (CVPipeline::isSyncOp(op)) { + return false; + } + if (CVPipeline::getCoreTypeOfSimpleOpOrCf(op) != + CVPipeline::CoreType::VECTOR_ONLY) { + return false; + } + } + return true; +} + +/** + * @brief Determine the upstream block_id to merge the scf.if into + * + * The upstream is the data source of the if: its condition plus every value + * consumed inside the regions that is produced outside the if. Constant-like + * producers and values produced inside the if are ignored. When all remaining + * data sources share a single block_id, that id is the merge target. + */ +static LogicalResult getUpstreamBlockId(scf::IfOp ifOp, + CVPipeline::ComputeBlockIdManager &bm, + int &target) { + Block *parent = ifOp->getBlock(); + llvm::SmallDenseSet ids; + + auto addSource = [&](Value v) { + Operation *def = v.getDefiningOp(); + if (!def) { + return; // block argument: not a tagged data source + } + if (def->hasTrait()) { + return; // constants are ignored + } + if (ifOp->isAncestor(def)) { + return; // produced inside the if itself, not external source + } + Operation *anc = CVPipeline::getAncestorInBlock(def, parent); + if (!anc) { + return; // produced in an outer scope, not a sibling block op + } + int bid = bm.getBlockIdByOp(anc); + if (bid != -1) { + ids.insert(bid); + } + }; + + addSource(ifOp.getCondition()); + ifOp->walk([&](Operation *op) { + if (op == ifOp.getOperation()) { + return; + } + for (Value v : op->getOperands()) { + addSource(v); + } + }); + + if (ids.size() != 1) { + LOG_DEBUG("[getUpstreamBlockId] data sources are not consistent, count=" + << ids.size()); + return failure(); + } + target = *ids.begin(); + return success(); +} + +static void applyMerge(scf::IfOp ifOp, ArrayRef downstreamOps, + int target, CVPipeline::ComputeBlockIdManager &bm) { + bm.updateBlockId(ifOp.getOperation(), target); + // Rewrite the inner ops so the whole if body shares the upstream block_id. + for (Region ®ion : ifOp->getRegions()) { + for (Block &block : region) { + for (Operation &op : block) { + if (op.hasTrait()) { + continue; + } + // Never fold a sync into a compute block: it must keep its own unique + // block id so the fence between before/after ops survives. + if (CVPipeline::isSyncOp(&op)) { + continue; + } + if (CVPipeline::getOpBlockId(&op)) { + bm.updateBlockId(&op, target); + } + } + } + } + for (Operation *op : downstreamOps) { + bm.updateBlockId(op, target); + } +} + +/** + * @brief Collect the downstream block_ids that consume the if results, + * ordered by their first appearance in the parent block. + */ +static SmallVector +collectDownstreamBlockIds(scf::IfOp ifOp, int target, + CVPipeline::ComputeBlockIdManager &bm) { + Block *parent = ifOp->getBlock(); + llvm::DenseSet userAnchors; + for (Value res : ifOp->getResults()) { + for (Operation *user : res.getUsers()) { + if (auto *anc = CVPipeline::getAncestorInBlock(user, parent)) { + userAnchors.insert(anc); + } + } + } + + // Collect the users with the program order. + SmallVector ordered; + llvm::SmallDenseSet seen; + for (Operation &op : *parent) { + if (!userAnchors.contains(&op)) { + continue; + } + int bid = bm.getBlockIdByOp(&op); + if (bid == -1 || bid == target) { + continue; + } + if (seen.insert(bid).second) { + ordered.push_back(bid); + } + } + return ordered; +} + +/** + * @brief Try to merge one pure-vector scf.if with its upstream and a downstream + * block + */ +static void tryMergeIf(scf::IfOp ifOp, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + int target = -1; + if (failed(getUpstreamBlockId(ifOp, bm, target))) { + return; + } + LOG_DEBUG("[tryMergeIf] upstream target block_id = " << target << " for " + << *ifOp); + + SmallVector downstream = collectDownstreamBlockIds(ifOp, target, bm); + + // Prefer merging the if together with the first downstream block that keeps + // the dependency graph acyclic, forming one large block. + for (int bid : downstream) { + SmallVector downstreamOps = bm.getOpsByBlockId(bid); + if (!isPureVectorOpGroup(downstreamOps)) { + LOG_DEBUG("[tryMergeIf] downstream block_id " + << bid << " contains non-VECTOR ops, skip to avoid mixed " + << "core_type block_id"); + continue; + } + SmallVector opsToUnify; + opsToUnify.push_back(ifOp.getOperation()); + opsToUnify.append(downstreamOps.begin(), downstreamOps.end()); + if (!CVPipeline::willCreateCycle(opsToUnify, memGraph, target, bm)) { + LOG_DEBUG("[tryMergeIf] merging if with downstream block_id " + << bid << " into " << target); + applyMerge(ifOp, downstreamOps, target, bm); + return; + } + LOG_DEBUG("[tryMergeIf] downstream block_id " + << bid << " would create a cycle, skip"); + } + + // Fallback: at least fold the if into the upstream block if that is safe. + SmallVector ifOnly = {ifOp.getOperation()}; + if (!CVPipeline::willCreateCycle(ifOnly, memGraph, target, bm)) { + LOG_DEBUG("[tryMergeIf] merging if into upstream " + << target << " without downstream"); + applyMerge(ifOp, {}, target, bm); + } +} + +} // anonymous namespace + +class MergeVectorIfBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MergeVectorIfBlockPass) + + MergeVectorIfBlockPass() = default; + + StringRef getArgument() const override { return "merge-vector-if-block"; } + + StringRef getDescription() const override { + return "Merge pure-Vector scf.if blocks with their upstream data-source " + "block and a downstream consumer block"; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LOG_DEBUG("Before: " << *module); + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aa); + auto bm = CVPipeline::ComputeBlockIdManager(module); + + llvm::SmallVector PureVectorIfOps; + module.walk([&](scf::IfOp ifOp) { + if (isPureVectorIf(ifOp)) { + PureVectorIfOps.push_back(ifOp); + } + }); + + for (scf::IfOp ifOp : PureVectorIfOps) { + tryMergeIf(ifOp, memGraph, bm); + } + + LOG_DEBUG("After: " << *module); + } +}; + +namespace mlir { +namespace triton { + +std::unique_ptr> createMergeVectorIfBlockPass() { + return std::make_unique(); +} + +void registerMergeVectorIfBlockPass() { + PassRegistration reg; +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MoveLoadIntoUserPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MoveLoadIntoUserPass.cpp new file mode 100644 index 0000000000..9844789967 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/MoveLoadIntoUserPass.cpp @@ -0,0 +1,317 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Operation.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/raw_ostream.h" +#include + +static constexpr const char *DEBUG_TYPE = "move-load-into-user"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace triton; + +namespace mlir { +namespace triton { + +class MoveLoadIntoUserPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MoveLoadIntoUserPass) + + MoveLoadIntoUserPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "move-load-into-user"; } + +private: + struct LoadPatternInfo { + SmallVector matchedOps; + int commonBlockId; + bufferization::ToTensorOp toTensorOp; + memref::CopyOp copyOp; + memref::AllocOp allocOp; // destination + }; + + bool matchLoadPattern(LoadPatternInfo &info); + + bool checkAllOpsInSameBlock(LoadPatternInfo &info, + CVPipeline::ComputeBlockIdManager &bm); +}; + +static Operation *traceCalculateUser(Operation *originUser, + CVPipeline::ComputeBlockIdManager &bm, + int totensorId, + SmallVector &userChain) { + // only find single chian + // pattern -> collapse1(totensorId)->collapse2(totensorId)...->user + // (targetblockId), collapse should move together. + // \-> other userChain -> user (targetblockId) + auto retUser = originUser; + while (bm.getBlockIdByOp(retUser) == totensorId && + isa(retUser)) { + if (retUser->getResult(0).getNumUses() != 1) { + break; + } + userChain.push_back(retUser); + retUser = *retUser->getUsers().begin(); + } + return retUser; +} + +static std::optional +getFirstUser(bufferization::ToTensorOp toTensorOp, + CVPipeline::ComputeBlockIdManager &bm, + SmallVector &matchedOps) { + Operation *firstUser = nullptr; + SmallVector fistUserChain; + + for (auto *user : toTensorOp->getUsers()) { + auto *userInBlock = + CVPipeline::getAncestorInBlock(user, toTensorOp->getBlock()); + if (userInBlock) { + SmallVector userChain; + auto calUser = traceCalculateUser( + userInBlock, bm, bm.getBlockIdByOp(toTensorOp), userChain); + if (!firstUser || calUser->isBeforeInBlock(firstUser)) { + // First in ordered IR, it's cblock is first too. + firstUser = calUser; + fistUserChain = userChain; + } + } + } + + if (!firstUser) { + LOG_DEBUG("No users of to_tensor found"); + return std::nullopt; + } + LOG_DEBUG("Find first user:\n" << *firstUser); + + if (CVPipeline::getOpCoreType(firstUser) != CVPipeline::VECTOR_ONLY) { + LOG_DEBUG("First User is not VECTOR"); + return std::nullopt; + } + + int blockId = bm.getBlockIdByOp(firstUser); + if (blockId == -1) { + LOG_DEBUG("First user has no block_id"); + return std::nullopt; + } + + if (blockId == bm.getBlockIdByOp(toTensorOp)) { + LOG_DEBUG("First user in same block"); + return std::nullopt; + } + matchedOps.append(fistUserChain.begin(), fistUserChain.end()); + return firstUser; +} + +bool MoveLoadIntoUserPass::checkAllOpsInSameBlock( + LoadPatternInfo &info, CVPipeline::ComputeBlockIdManager &bm) { + if (info.matchedOps.empty()) { + return false; + } + + int commonBlockId = -1; + for (auto *op : info.matchedOps) { + int blockId = bm.getBlockIdByOp(op); + if (blockId == -1) { + LOG_DEBUG("Op has no block_id"); + return false; + } + if (commonBlockId == -1) { + commonBlockId = blockId; + } else if (blockId != commonBlockId) { + LOG_DEBUG("Not all ops in the same block"); + return false; + } + } + info.commonBlockId = commonBlockId; + return true; +} + +memref::AllocOp getOriginAlloc(Value dest) { + if (auto allocOp = dest.getDefiningOp()) { + return allocOp; + } else if (auto viewLike = + dyn_cast(dest.getDefiningOp())) { + auto source = viewLike.getViewSource(); + if (source) { + return getOriginAlloc(source); + } + } + return nullptr; +} + +bool MoveLoadIntoUserPass::matchLoadPattern(LoadPatternInfo &info) { + // Step 1: Check if source is from global memory + SetVector sourceViewOps; + if (!CVPipeline::collectViewOpsAndCheckGlobalMemory(info.copyOp.getSource(), + sourceViewOps)) { + LOG_DEBUG("Copy source is not from global memory"); + return false; + } + + // Step 2: Find the pattern: reinterpret_cast/alloc -> memref.copy -> + // to_tensor Get the reinterpret_cast (or alloc) + Value dest = info.copyOp.getTarget(); + + // Check if dest is alloc/from one alloc + if (auto allocOp = getOriginAlloc(dest)) { + info.allocOp = allocOp; + } else { + LOG_DEBUG("Copy dest is not from alloc"); + return false; + } + + // Step 3: Find the to_tensor op + for (auto *user : info.allocOp->getUsers()) { + auto toTensor = dyn_cast(user); + if (toTensor) { + info.toTensorOp = toTensor; + break; + } + } + + if (!info.toTensorOp) { + LOG_DEBUG("No to_tensor op found after copy"); + return false; + } + + // Collect matched ops: destOp, copyOp, toTensor + info.matchedOps.push_back(info.allocOp); + info.matchedOps.push_back(info.copyOp); + info.matchedOps.push_back(info.toTensorOp); + + return true; +} + +static void +collectAllDependencies(Operation *op, SetVector &deps, + int commonBlockId, CVPipeline::ComputeBlockIdManager &bm, + CVPipeline::MemoryDependenceGraph &memGraph) { + if (deps.contains(op)) { + return; + } + + int blockId = bm.getBlockIdByOp(op); + if (blockId == -1 || blockId != commonBlockId) { + return; + } + + deps.insert(op); + + CVPipeline::DependencyHelper depHelper(memGraph); + depHelper.forEachSource(op, [&](Operation *source) { + collectAllDependencies(source, deps, commonBlockId, bm, memGraph); + return WalkResult::advance(); + }); +} + +void MoveLoadIntoUserPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + auto &aliasAnalysis = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aliasAnalysis); + auto bm = CVPipeline::ComputeBlockIdManager(module); + + SmallVector validPatterns; + + LOG_DEBUG("before MogeLoadIntoUserPass ....\n" << *module); + + module.walk([&](memref::CopyOp copyOp) { + LOG_DEBUG("\nNow check memref.copy = " << copyOp << "\n"); + // Step 1: Match the load pattern and collect 4 ops + LoadPatternInfo info; + info.copyOp = copyOp; + if (!matchLoadPattern(info)) { + return; + } + + // Step 2: Check if all 3 ops are in the same block + if (!checkAllOpsInSameBlock(info, bm)) { + return; + } + // Store the valid pattern + validPatterns.push_back(info); + }); + + // Process each valid pattern + for (auto &pattern : validPatterns) { + auto &matchedOps = pattern.matchedOps; + int commonBlockId = pattern.commonBlockId; + + // Find first user of to_tensor + auto firstUserOpt = getFirstUser(pattern.toTensorOp, bm, matchedOps); + if (!firstUserOpt) { + continue; + } + + Operation *firstUser = firstUserOpt.value(); + int targetBlockId = bm.getBlockIdByOp(firstUser); + + SetVector opsToMove; + for (auto *op : matchedOps) { + collectAllDependencies(op, opsToMove, commonBlockId, bm, memGraph); + } + CVPipeline::cloneScalarOpsForCrossBlockUses(bm, opsToMove, + bm.getBlockIdByOp(firstUser)); + // Check for cycles before moving + if (CVPipeline::willCreateCycle(opsToMove.getArrayRef(), memGraph, + targetBlockId, bm)) { + LOG_DEBUG("Moving would create a cycle, skip(" << commonBlockId << ")"); + continue; + } + + // Update block_id for all ops + for (auto *op : opsToMove) { + bm.updateBlockIdWithInner(op, targetBlockId); + } + + LOG_DEBUG("Successfully moved ops to block " << targetBlockId); + } +} + +std::unique_ptr> createMoveLoadIntoUserPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/PosMaskPatternPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/PosMaskPatternPass.cpp new file mode 100644 index 0000000000..4a98d4f9fa --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/PosMaskPatternPass.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" +#include + +static constexpr const char *DEBUG_TYPE = "pos-mask-pattern-opt"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir; + +namespace mlir { +namespace triton { + +class PosMaskPatternPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PosMaskPatternPass) + + PosMaskPatternPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "pos-mask-pattern-opt"; } + llvm::StringRef getDescription() const final { + return "Optimize pos mask pattern (broadcast+cmpi+extui) by moving ops " + "to user's block when safe"; + } +}; + +} // namespace triton +} // namespace mlir + +namespace { + +struct PosPattern { + linalg::BroadcastOp broadcastOp; + arith::CmpIOp eqcmpOp; + arith::CmpIOp slecmpOp; + arith::ExtUIOp eqextOp; + arith::ExtUIOp sleextOp; + + llvm::SmallVector getAllOps() const { + return {broadcastOp, eqcmpOp, slecmpOp, eqextOp, sleextOp}; + } +}; + +static bool isPosBroadcast(linalg::BroadcastOp broadcastOp) { + if (CVPipeline::getOpCoreType(broadcastOp.getOperation()) != + CVPipeline::CoreType::VECTOR_ONLY) { + return false; + } + return true; +} + +static std::optional +collectPosPattern(linalg::BroadcastOp broadcastOp, + CVPipeline::ComputeBlockIdManager &bm) { + PosPattern pattern; + if (llvm::range_size(broadcastOp.getResult().getUsers()) != 2) { + return std::nullopt; + } + if (any_of(broadcastOp->getUsers(), + [&](Operation *user) { return !isa(user); })) { + return std::nullopt; + } + + arith::CmpIOp eqcmpOp = nullptr; + arith::CmpIOp slecmpOp = nullptr; + + for (Operation *user : broadcastOp->getUsers()) { + auto cmpOp = dyn_cast(user); + auto predicate = cmpOp.getPredicate(); + if (predicate == arith::CmpIPredicate::eq) { + if (eqcmpOp) { + return std::nullopt; + } + eqcmpOp = cmpOp; + } else if (predicate == arith::CmpIPredicate::sle) { + if (slecmpOp) { + return std::nullopt; + } + slecmpOp = cmpOp; + } else { + return std::nullopt; + } + } + + if (!eqcmpOp || !slecmpOp) { + return std::nullopt; + } + if (!eqcmpOp.getResult().hasOneUse() || !slecmpOp.getResult().hasOneUse()) { + return std::nullopt; + } + + arith::ExtUIOp eqextOp = + dyn_cast(*eqcmpOp->getUsers().begin()); + arith::ExtUIOp sleextOp = + dyn_cast(*slecmpOp->getUsers().begin()); + if (!eqextOp || !sleextOp) { + return std::nullopt; + } + + pattern.eqextOp = eqextOp; + pattern.sleextOp = sleextOp; + pattern.eqcmpOp = eqcmpOp; + pattern.slecmpOp = slecmpOp; + pattern.broadcastOp = broadcastOp; + + auto broadcastBlockId = bm.getBlockIdByOp(broadcastOp); + auto broadcastBlock = broadcastOp->getBlock(); + if (!all_of(pattern.getAllOps(), [&](Operation *op) { + return bm.getBlockIdByOp(op) == broadcastBlockId; + })) { + return std::nullopt; + } + if (!all_of(pattern.getAllOps(), [&](Operation *op) { + return op->getBlock() == broadcastBlock; + })) { + return std::nullopt; + } + + return pattern; +} + +static std::optional +findTargetBlock(const PosPattern &pattern, + CVPipeline::ComputeBlockIdManager &bm) { + llvm::SmallVector users; + + for (Operation *user : pattern.eqextOp->getUsers()) { + if (user->getBlock() != pattern.broadcastOp->getBlock()) { + continue; + } + users.push_back(user); + } + + for (Operation *user : pattern.sleextOp->getUsers()) { + if (user->getBlock() != pattern.broadcastOp->getBlock()) { + continue; + } + users.push_back(user); + } + if (users.size() == 0) { + return std::nullopt; + } + + int targetBlockId = bm.getBlockIdByOp(users[0]); + if (targetBlockId == -1) { + return std::nullopt; + } + for (Operation *user : users) { + if (bm.getBlockIdByOp(user) != targetBlockId) { + return std::nullopt; + } + } + + return targetBlockId; +} + +} // namespace + +namespace mlir { +namespace triton { + +void PosMaskPatternPass::runOnOperation() { + ModuleOp moduleOp = getOperation(); + LOG_DEBUG("Input mlir:" << moduleOp); + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(moduleOp, aa); + CVPipeline::ComputeBlockIdManager bm(moduleOp); + + llvm::SmallVector broadcasts; + moduleOp.walk([&](linalg::BroadcastOp op) { + if (isPosBroadcast(op)) { + broadcasts.push_back(op); + } + }); + + for (linalg::BroadcastOp broadcastOp : broadcasts) { + auto patternOpt = collectPosPattern(broadcastOp, bm); + if (!patternOpt.has_value()) { + continue; + } + auto pattern = patternOpt.value(); + std::optional targetBlockId = findTargetBlock(pattern, bm); + if (!targetBlockId) { + LOG_DEBUG("No common target block_id, skip\n"); + continue; + } + + int broadcastBlockId = bm.getBlockIdByOp(pattern.broadcastOp); + if (broadcastBlockId == targetBlockId) { + LOG_DEBUG("Already in same block, skip\n"); + continue; + } + + llvm::SmallVector allOps = pattern.getAllOps(); + if (CVPipeline::willCreateCycle(allOps, memGraph, *targetBlockId, bm)) { + LOG_DEBUG("Would create cycle, skip\n"); + continue; + } + + LOG_DEBUG("Moving pos pattern ops to block " << *targetBlockId << "\n"); + for (Operation *op : allOps) { + bm.updateBlockId(op, *targetBlockId); + } + } +} + +std::unique_ptr> createPosMaskPatternPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/RelocateMemrefDeclPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/RelocateMemrefDeclPass.cpp new file mode 100644 index 0000000000..486086ecd6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/RelocateMemrefDeclPass.cpp @@ -0,0 +1,307 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/SyncWall.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Dominance.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/Support/Debug.h" +#include + +static constexpr const char *DEBUG_TYPE = "relocate-memref-decl"; +#define LDBG(X) \ + LLVM_DEBUG(llvm::dbgs() << '[' << DEBUG_TYPE << "] " << X << "\n") + +namespace mlir { +namespace triton { +namespace { + +// Every operand of every member must either be relocated together with the +// cluster or already dominate the insertion point (the anchor). +static bool allOperandsDominate(const llvm::SetVector &cluster, + Operation *anchor, DominanceInfo &domInfo) { + for (Operation *member : cluster) { + for (Value operand : member->getOperands()) { + Operation *def = operand.getDefiningOp(); + if (!def || cluster.contains(def)) { + continue; // block argument, or relocated together with the cluster + } + if (!domInfo.dominates(def, anchor)) { + return false; + } + } + } + return true; +} + +// True when `def`'s block strictly contains `targetBlock`, so `def` (block +// level in that block) dominates everything inside the target block. +static bool isAboveTargetBlock(Operation *def, Block *targetBlock, + DominanceInfo &domInfo) { + return domInfo.properlyDominates(def->getBlock(), targetBlock); +} + +// Recursively collect the closure of `producer` that has to be sunk into +// `targetBlock` together with the producer, grown to a fixpoint +static SmallVector +buildForwardClosure(Operation *producer, Block *targetBlock, Operation *&anchor, + const DenseMap &pos, + CVPipeline::SyncWall &wall, DominanceInfo &domInfo) { + llvm::SetVector cluster; + cluster.insert(producer); + + bool changed = true; + while (changed) { + changed = false; + for (Operation *cur : llvm::to_vector(cluster)) { + // Forward: uses of the current op. + for (Operation *user : cur->getUsers()) { + Operation *userAnc = CVPipeline::getAncestorInBlock(user, targetBlock); + if (userAnc) { + // Use already lives in the target block; track the earliest one. + if ((!anchor || userAnc->isBeforeInBlock(anchor)) && + wall.hasSyncBetween(cur, userAnc)) { + anchor = userAnc; + } + if (wall.hasSyncBetween(cur, anchor)) { + changed |= cluster.insert(user); + } + continue; + } + // A use outside the target block must be pulled down recursively. + if (user->hasTrait()) { + return {}; // cannot sink past a terminator (e.g. loop-carried yield) + } + } + + // Backward: producers of the current op's (effective) operands. + for (Value operand : cur->getOperands()) { + if (!isa(operand.getType())) { + LDBG("Not a memref tyoe for operand: " << operand); + continue; + } + Operation *def = operand.getDefiningOp(); + if (!def || cluster.contains(def)) { + continue; // block argument or already relocated + } + LDBG("collect def op: " << *def); + if (domInfo.properlyDominates(def->getBlock(), targetBlock)) { + LDBG("Defining op already dominates the target block"); + continue; + } + if (def->hasTrait()) { + return {}; + } + // Pull the operand producer only when every use of it travels with the + // cluster, otherwise moving it would break its outside users. + if (cluster.insert(def)) { + changed = true; + for (Operation *user : def->getUsers()) { + if (!cluster.contains(user)) { + cluster.insert(def); + } + } + } + } + } + } + + if (!anchor) { + return {}; + } + + SmallVector ordered(cluster.begin(), cluster.end()); + llvm::stable_sort(ordered, [&](Operation *a, Operation *b) { + return pos.lookup(a) < pos.lookup(b); + }); + return ordered; +} + +// A memref producer whose use crosses a sync op, together with the whole +// forward closure that has to be sunk into the consumer's block. +struct Relocation { + Operation *anchor; // earliest in-targetBlock use of the closure + SmallVector cluster; // closure members in original pre-order + int targetId; + Attribute coreType; +}; + +// Relocate memref producers whose use crosses a sync op into the consumer's +// block. Every op is scanned for memref-typed operands: when the operand's +// producer op and the consumer are separated by a sync, the producer and its +// whole forward closure (uses, and the uses of those uses' results, ...) are +// re-partitioned to the consumer's group and moved to just before the earliest +// cross-sync use in the consumer's block. The closure is sunk only when every +// use ends up in the consumer's block on the far side of the sync, so the move +// preserves SSA dominance and the re-partition cannot straddle the barrier. +static void relocateMemrefDecls(ModuleOp module) { + CVPipeline::ComputeBlockIdManager bm(module); + DenseMap walls; + DenseMap pos; + unsigned idx = 0; + module.walk([&](Operation *op) { pos[op] = idx++; }); + + DominanceInfo domInfo(module); + SmallVector worklist; + + module.walk([&](Operation *op) { + for (Value operand : op->getOperands()) { + if (!isa(operand.getType())) { + continue; + } + Operation *producer = operand.getDefiningOp(); + if (!producer) { + continue; // block argument or already scheduled for relocation + } + + // Determine the block the two ops share and check that a sync op sits + // strictly between the producer and the consumer. + Block *commonBlock = producer->getBlock(); + Operation *pAnchor = producer; + Operation *cAnchor = nullptr; + if (producer->getBlock() == op->getBlock()) { + cAnchor = op; + } else if (Operation *cAnc = + CVPipeline::getAncestorInBlock(op, producer->getBlock())) { + // Consumer lives inside a region nested under the producer's block. + cAnchor = cAnc; + } else { + continue; // producer is not in the consumer's block hierarchy + } + + CVPipeline::SyncWall &wall = + walls.try_emplace(commonBlock, commonBlock).first->second; + if (!wall.hasSyncBetween(pAnchor, cAnchor)) { + continue; // no sync in between -> nothing to relocate across + } + + Block *targetBlock = op->getBlock(); + int targetId = bm.getBlockIdByOp(op); + if (targetId == -1) { + continue; // consumer not assigned to a group yet + } + + // Pull the producer and every use that does not live in the target block + // (recursively, including the uses of those uses' results) into the + // target block. + Operation *anchor = cAnchor; + SmallVector ordered = buildForwardClosure( + producer, targetBlock, anchor, pos, wall, domInfo); + if (ordered.empty()) { + LDBG("Failed to build closure for op: " << *op); + continue; + } + // The earliest far-side use must sit past the sync, otherwise a use + // above the barrier keeps the producer above it. + if (commonBlock == targetBlock && + !wall.hasSyncBetween(producer, anchor)) { + continue; + } + // Every member's operands must still dominate the insertion point. + llvm::SetVector cluster(ordered.begin(), ordered.end()); + if (!allOperandsDominate(cluster, anchor, domInfo)) { + continue; + } + + worklist.push_back( + {anchor, ordered, targetId, op->getAttr(CVPipeline::kCoreType)}); + } + }); + + for (const Relocation &reloc : worklist) { + auto insertPt = reloc.anchor; + for (auto it = reloc.cluster.rbegin(); it != reloc.cluster.rend(); ++it) { + auto member = *it; + // Move the member to just before the earliest cross-sync use (aka the + // anchor) then re-partition it to the consumer's group (block id and core + // type). + member->moveBefore(insertPt); + insertPt = member; + bm.updateBlockId(member, reloc.targetId); + + LDBG("moved memref producer " << *member << " to block_id " + << reloc.targetId); + } + } +} + +} // namespace + +class RelocateMemrefDeclPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(RelocateMemrefDeclPass) + + RelocateMemrefDeclPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "relocate-memref-decl"; } + llvm::StringRef getDescription() const final { + return "Relocate memref declarations whose uses cross a sync op into the " + "consumer block and pull the dependency cluster with them"; + } +}; + +void RelocateMemrefDeclPass::runOnOperation() { + ModuleOp module = getOperation(); + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + // Without any sync op there is nothing a memref dependency could cross, so + // the pass has no work to do. + bool hasSync = false; + module.walk([&](Operation *op) { + if (CVPipeline::isSyncOp(op)) { + hasSync = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (!hasSync) { + return; + } + + LDBG("Before :\n" << *module); + + // relocate memref declarations whose uses cross a sync op into the consumer + // block and pull their dependency cluster with them, so every declaration + // ends up on the same side of each sync as its uses before SplitDataflow + // lowers the barriers. + relocateMemrefDecls(module); + + LDBG("After :\n" << *module); +} + +std::unique_ptr> createRelocateMemrefDeclPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SinkI1ProducersIntoUsersPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SinkI1ProducersIntoUsersPass.cpp new file mode 100644 index 0000000000..d013f02035 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SinkI1ProducersIntoUsersPass.cpp @@ -0,0 +1,200 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/TopologicalSortUtils.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LLVM.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/raw_ostream.h" + +static constexpr const char *DEBUG_TYPE = "sink-i1-producers-into-users"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir; + +namespace mlir { +namespace triton { + +class SinkI1ProducersIntoUsersPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(SinkI1ProducersIntoUsersPass) + + SinkI1ProducersIntoUsersPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { + return "sink-i1-producers-into-users"; + } + llvm::StringRef getDescription() const final { + return "Sink i1-producing ops next to each of their i1 uses"; + } +}; + +} // namespace triton +} // namespace mlir + +namespace { + +static bool isValidI1Producer(Operation *op) { + if (isa(op->getDialect())) { + return false; + } + if (op->getNumResults() > 1) { + return false; + } + for (auto result : op->getResults()) { + if (auto tensorType = dyn_cast(result.getType())) { + mlir::Type elemType = tensorType.getElementType(); + if (elemType.isInteger(1)) { + return true; + } + } + } + return false; +} + +static bool isPureAndRegionless(Operation *op) { + if (op->hasTrait()) + return false; + if (auto iface = dyn_cast(op)) { + SmallVector effects; + iface.getEffects(effects); + if (!effects.empty()) + return false; + } + return true; +} + +} // namespace + +namespace mlir { +namespace triton { + +void SinkI1ProducersIntoUsersPass::runOnOperation() { + ModuleOp moduleOp = getOperation(); + CVPipeline::ComputeBlockIdManager bm(moduleOp); + LOG_DEBUG(moduleOp); + + SmallVector producers; + // find single-result and regionless op with i1 tensor result-type + moduleOp.walk([&](Operation *op) { + if (isValidI1Producer(op) && isPureAndRegionless(op)) { + LOG_DEBUG("found i1 producer: " << *op << "\n"); + if (bm.getBlockIdByOp(op) != -1) { + producers.push_back(op); + } + } + }); + + for (Operation *p : llvm::reverse(producers)) { + SetVector consumers; + for (OpOperand &use : p->getUses()) { + Operation *consumer = use.getOwner(); + auto consumerInblock = + CVPipeline::getAncestorInBlock(consumer, p->getBlock()); + if (!consumerInblock) { + continue; + } + consumers.insert(consumerInblock); + } + if (consumers.empty()) { + continue; + } + LOG_DEBUG("Producer: " << *p << "have " << consumers.size() + << " consumers.\n"); + LLVM_DEBUG( + for (auto c : consumers) { LOG_DEBUG("consumer: " << *c << "\n"); }); + DenseSet seenBlockIds; + DenseMap blockId2Producer; + auto orderedConsumuers = mlir::topologicalSort(consumers); + // 1. Move into first consumer blockId + int consumerBlockId = bm.getBlockIdByOp(orderedConsumuers[0]); + if (consumerBlockId == -1) { + // if i1 break by one control op, couldn't change. + LOG_DEBUG("First consumer's blockid is -1.\n"); + seenBlockIds.insert(bm.getBlockIdByOp(p)); + blockId2Producer.insert({bm.getBlockIdByOp(p), p}); + } else if (bm.isSameBlock(p, orderedConsumuers[0])) { + LOG_DEBUG("producer and consumer has same blockId.\n"); + seenBlockIds.insert(consumerBlockId); + blockId2Producer.insert({consumerBlockId, p}); + } else { + p->moveBefore(orderedConsumuers[0]); + LOG_DEBUG("move producer " << *p << " to " << consumerBlockId << "\n"); + bm.updateBlockId(p, consumerBlockId); + blockId2Producer.insert({consumerBlockId, p}); + } + + // 2. if there are other consumer, then clone producer. + for (auto consumerInblock : orderedConsumuers) { + int consumerBlockId = bm.getBlockIdByOp(consumerInblock); + if (consumerBlockId == -1) { + // if i1 break by one control op (scf.for/yield), couldn't change. + continue; + } + if (!seenBlockIds.insert(consumerBlockId).second) { + auto producer = blockId2Producer[consumerBlockId]; + for (auto info : llvm::enumerate(p->getResults())) { + auto id = info.index(); + info.value().replaceUsesWithIf( + producer->getResult(id), [&](OpOperand &use) { + return use.getOwner() == consumerInblock; + }); + } + continue; + } + // Create one producer for now blockid. + LOG_DEBUG("clone producer " << *p << " to " << consumerBlockId << "\n"); + auto cloned = OpBuilder(consumerInblock).clone(*p); + blockId2Producer.insert({consumerBlockId, cloned}); + bm.updateBlockId(cloned, consumerBlockId); + for (auto info : llvm::enumerate(p->getResults())) { + auto id = info.index(); + info.value().replaceUsesWithIf( + cloned->getResult(id), + [&](OpOperand &use) { return use.getOwner() == consumerInblock; }); + } + } + } +} + +std::unique_ptr> createSinkI1ProducersIntoUsersPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/Common.h b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/Common.h new file mode 100644 index 0000000000..bff6d458e6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/Common.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMPUTE_BLOCK_COMPUTE_OPT_SPLIT_IF_BY_BLOCK_ID_COMMON_H +#define TRITON_ADAPTER_DYNAMIC_CV_PIPELINE_COMPUTE_BLOCK_COMPUTE_OPT_SPLIT_IF_BY_BLOCK_ID_COMMON_H + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLFunctionalExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Operation.h" + +#include "DynamicCVPipeline/Common/Utils.h" + +namespace mlir::CVPipeline::SplitIf { + +/// One group of ops sharing the same block_id inside an scf.if. +/// N=0: all storage is heap-allocated, keeping sizeof small. +struct BlockGroup { + int blockId; + SmallVector ops; // ops in this group (original order) + SmallVector nestedIfs; // nested ifs that belong to this group +}; + +// Logic: find until the first tensor dependency +class ScalarClosure { +private: + mlir::Block *block = nullptr; + mlir::Block *parentBlock = nullptr; + bool includeParent = true; + llvm::ArrayRef ops; + + void collectScalarClosure(Value val); + void collectOuterDependency(Operation *op); + +public: + static constexpr size_t kExpectedMaxScalarOps = 4; + llvm::SmallPtrSet scalarOps; + ScalarClosure(BlockGroup &group, ArrayRef ops); + ScalarClosure(Block *block, ArrayRef ops, bool includeParent); + void collect(); + + // Safety: both operations must be in scalarOps + bool isBefore(Operation *a, Operation *b); + + std::pair, IRMapping> capture(int blockId); +}; + +struct WalkMainLoopResult { + CoreType coreType = UNDETERMINED; + bool containsMainLoop = false; +}; + +llvm::FailureOr +walkMainLoop(Operation *op, + llvm::function_ref pred); + +} // namespace mlir::CVPipeline::SplitIf + +#endif diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/ScalarClosure.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/ScalarClosure.cpp new file mode 100644 index 0000000000..d5b467203a --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/ScalarClosure.cpp @@ -0,0 +1,141 @@ +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" + +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/IRMapping.h" + +#include "Common.h" + +static constexpr const char *DEBUG_TYPE = "SplitIfByBlockIdScalarClosure"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__ << "\n"; \ + }) + +using namespace mlir; +using namespace CVPipeline; +using namespace SplitIf; + +void ScalarClosure::collectOuterDependency(Operation *op) { + op->walk([this](Operation *nestedOp) { + for (auto operand : nestedOp->getOperands()) { + collectScalarClosure(operand); + } + }); +}; + +void ScalarClosure::collectScalarClosure(Value val) { + if (!val || isa(val.getType())) { + return; + } + Operation *defOp = val.getDefiningOp(); + if (!defOp || llvm::isa(defOp)) { + return; + } + + auto *defBlock = defOp->getBlock(); + if (!defBlock || (defBlock != block && + // to prevent memref dependencies in main loop + (!includeParent || defBlock != parentBlock))) { + return; + } + + // hard to determine - if scf ops have scalar/memref result, should we clone + // the op as well? Perhaps walk into the subregions and clone the + // corresponding ops would be better. But for now let's just clone them + collectOuterDependency(defOp); + scalarOps.insert(defOp); +} + +void ScalarClosure::collect() { + if (block == nullptr) { + return; + } + for (Operation *op : ops) { + collectOuterDependency(op); + } +} + +ScalarClosure::ScalarClosure(BlockGroup &group, ArrayRef ops) + : ops(ops) { + if (group.ops.empty()) { + return; + } + block = group.ops.front()->getBlock(); + Operation *parentOp = block->getParentOp(); + if (!parentOp) { + return; + } + parentBlock = parentOp->getBlock(); +} + +ScalarClosure::ScalarClosure(Block *block, ArrayRef ops, + bool includeParent) + : block(block), ops(ops), includeParent(includeParent) { + if (!block) + return; + Operation *parentOp = block->getParentOp(); + if (!parentOp) { + return; + } + parentBlock = parentOp->getBlock(); +} + +bool ScalarClosure::isBefore(Operation *a, Operation *b) { + if (a->getBlock() == parentBlock || b->getBlock() == parentBlock) { + a = parentBlock->findAncestorOpInBlock(*a); + b = parentBlock->findAncestorOpInBlock(*b); + } + return a->isBeforeInBlock(b); +} + +std::pair, IRMapping> +ScalarClosure::capture(int blockId) { + if (scalarOps.empty()) { + return {}; + } + + IRMapping mapper; + SmallVector opsToClone(scalarOps.begin(), scalarOps.end()); + + // sort the collected ops so that they are in the same dominance order as in + // mlir + llvm::sort(opsToClone, + [this](Operation *a, Operation *b) { return isBefore(a, b); }); + + Operation *firstOp = nullptr; + for (auto *op : ops) { + if (op->getBlock() != block) { + continue; + } + if (firstOp == nullptr || op->isBeforeInBlock(firstOp)) { + firstOp = op; + } + } + OpBuilder builder(firstOp); + builder.setInsertionPoint(firstOp); + + SmallVector newOps; + + LDBG("Collecting scalar closures for blockId: " << blockId); + for (Operation *scalarOp : opsToClone) { + if (llvm::is_contained(ops, scalarOp)) { + // the current operation have the same blockId, but will not be cloned + // hence we need to move insertion point + builder.setInsertionPointAfter(scalarOp); + continue; + } + + LDBG("Clone: " << *scalarOp); + + Operation *cloned = builder.clone(*scalarOp, mapper); + cloned->setAttr(CVPipeline::kBlockId, + Builder(scalarOp->getContext()).getI32IntegerAttr(blockId)); + newOps.push_back(cloned); + } + + return {newOps, mapper}; +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/SplitIfByBlockId.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/SplitIfByBlockId.cpp new file mode 100644 index 0000000000..4e8aeb0e0a --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/SplitIfByBlockId.cpp @@ -0,0 +1,1776 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Analysis/ScalarEvolution.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/WalkResult.h" + +#include "ascend/include/DynamicCVPipeline/Common/DependencyHelper.h" +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" + +#include "ComputeBlockOpt/SplitIfByBlockId/Common.h" +#include "DynamicCVPipeline/Common/FallbackHelper.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/HIVM/IR/HIVMImpl.h" + +static constexpr const char *DEBUG_TYPE = "SplitIfByBlockId"; +static constexpr llvm::StringLiteral kSkippedKernels[1] = {""}; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__ << "\n"; \ + }) + +using namespace mlir; +using namespace triton; +using namespace CVPipeline; +using namespace SplitIf; + +static void addCrossGroupDependency( + Value val, Operation *consumer, unsigned consumerGroup, + llvm::SmallDenseMap &opToGroup, + llvm::SmallDenseMap>> + &crossValueMap) { + auto *defOp = val.getDefiningOp(); + if (!defOp) { + return; + } + auto it = opToGroup.find(defOp); + if (it == opToGroup.end() || it->second == consumerGroup) { + return; + } + + auto &entry = crossValueMap[val]; + entry.first = it->second; + entry.second.insert(consumer); +} + +namespace { + +struct CrossGroupValue { + Value value; // the SSA value crossing groups + unsigned fromGroupIdx; // producer group index + SmallVector toGroupIndices; // consumer group indices +}; + +struct GroupOutputInfo { + + /// Values this group yields (cross-group values it produces; + /// for non-last groups in Case B, also includes original yield values). + SmallVector outputValues; + + /// Case B: original else-side yield value for each output slot. + /// Set for original yield slots; empty for Case A or cross-group-only values. + SmallVector origElseValues; + + SmallVector outputTypes; + + bool isVoid() const { return outputValues.empty(); } +}; + +/// Each split-if gets its own result signature based on what +/// it actually produces. No uniform slot plan. +struct YieldAugmentation { + /// Cross-group values (deduplicated, in producer-group order). + SmallVector crossValues; + + /// Per-group output info for non-last groups only. + /// Last group is handled separately in materialization. + SmallVector groupOutputs; + + // Case B only: info for building the last-if (carries original result types) + unsigned numOriginalSlots = 0; + SmallVector origYieldValues; + SmallVector origYieldProducerGroup; + + bool empty() const { return crossValues.empty(); } +}; + +struct CandidateIf { + scf::IfOp ifOp; + int64_t selfBlockId; + bool hasYield; + SmallVector thenGroups; + SmallVector elseGroups; + + // value-level cross-group + yield plan + YieldAugmentation yieldAug; + + // Pre-computed split decisions: bidirectional cross-core data deps + // (mixed CUBE/VECTOR groups with data consumption in both directions) + bool shouldSplitThen = false; + bool shouldSplitElse = false; + + bool needsSplit() const { return shouldSplitThen || shouldSplitElse; } +}; + +} // namespace + +static bool hasNestedIfs(const CandidateIf &cand) { + for (const auto &group : cand.thenGroups) { + if (!group.nestedIfs.empty()) { + return true; + } + } + for (const auto &group : cand.elseGroups) { + if (!group.nestedIfs.empty()) { + return true; + } + } + return false; +} + +static CoreType groupIsCube(const BlockGroup &group) { + bool seenCube = false; + bool seenVector = false; + auto classifyOp = [&](Operation *op) { + switch (CVPipeline::getCoreTypeOfSimpleOpOrCf(op)) { + case CVPipeline::CoreType::CUBE_AND_VECTOR: + return true; + case CVPipeline::CoreType::CUBE_ONLY: + seenCube = true; + break; + case CVPipeline::CoreType::VECTOR_ONLY: + seenVector = true; + break; + default: + break; + } + return false; + }; + for (auto *op : group.ops) { + if (classifyOp(op) || (seenCube && seenVector)) { + return CoreType::CUBE_AND_VECTOR; + } + } + for (auto nestedIf : group.nestedIfs) { + if (classifyOp(nestedIf) || (seenCube && seenVector)) { + return CoreType::CUBE_AND_VECTOR; + } + } + if (seenCube) { + return CoreType::CUBE_ONLY; + } + if (seenVector) { + return CoreType::VECTOR_ONLY; + } + return CoreType::UNDETERMINED; +} + +static bool groupContainsOp(const BlockGroup &group, Operation *op) { + if (llvm::is_contained(group.ops, op)) { + return true; + } + for (auto nestedIf : group.nestedIfs) { + if (nestedIf == op || nestedIf->isAncestor(op)) { + return true; + } + } + return false; +} + +static bool groupFeedsGroup(const BlockGroup &producer, + const BlockGroup &consumer, + const CVPipeline::MemoryDependenceGraph &memGraph) { + DependencyHelper depHelper(memGraph); + auto checkOp = [&](Operation *op) { + auto result = depHelper.forEachUser(op, [&](Operation *user) { + return groupContainsOp(consumer, user) ? WalkResult::interrupt() + : WalkResult::advance(); + }); + return result.wasInterrupted(); + }; + for (auto *op : producer.ops) { + if (checkOp(op)) { + return true; + } + } + for (auto nestedIf : producer.nestedIfs) { + if (checkOp(nestedIf)) { + return true; + } + } + return false; +} + +/// Returns true when groups mix CUBE and VECTOR core types AND have +/// bidirectional data flow (CUBE↔VECTOR in both directions). +static bool hasBidirectionalCrossCoreDeps( + ArrayRef groups, + const CVPipeline::MemoryDependenceGraph &memGraph) { + SmallVector isCube; + isCube.reserve(groups.size()); + bool hasCubeGroup = false; + bool hasVectorGroup = false; + for (size_t i = 0; i < groups.size(); ++i) { + auto cubeStatus = groupIsCube(groups[i]); + isCube.push_back(cubeStatus); + if (cubeStatus == CoreType::UNDETERMINED || + cubeStatus == CoreType::CUBE_AND_VECTOR) { + continue; + } + hasCubeGroup = hasCubeGroup || (cubeStatus == CoreType::CUBE_ONLY); + hasVectorGroup = hasVectorGroup || (cubeStatus == CoreType::VECTOR_ONLY); + } + if (!hasCubeGroup || !hasVectorGroup) { + return false; + } + + // Cross-core data dependency: CUBE data consumed by VECTOR and VECTOR + // data consumed by CUBE. Ordering alone is not enough to split. + bool cubeFeedsVector = false; + bool vectorFeedsCube = false; + for (size_t i = 0; i < groups.size(); ++i) { + for (size_t j = 0; j < groups.size(); ++j) { + if (i == j || isCube[i] == CoreType::UNDETERMINED || + isCube[i] == CoreType::CUBE_AND_VECTOR || + isCube[j] == CoreType::UNDETERMINED || + isCube[j] == CoreType::CUBE_AND_VECTOR || isCube[i] == isCube[j]) { + continue; + } + if (!groupFeedsGroup(groups[i], groups[j], memGraph)) { + continue; + } + if (isCube[i] == CoreType::CUBE_ONLY) { + cubeFeedsVector = true; + } else { + vectorFeedsCube = true; + } + } + } + return cubeFeedsVector && vectorFeedsCube; +} + +static inline void dumpCandidate(CandidateIf &candidate) { + LDBG("Processing: " << candidate.ifOp); + LDBG(" selfBlockId=" << candidate.selfBlockId); + LDBG(" groups(then=" << candidate.thenGroups.size() + << " else=" << candidate.elseGroups.size() + << ") yield=" << candidate.hasYield + << " nested=" << hasNestedIfs(candidate)); + for (const auto &group : candidate.thenGroups) { + LDBG(" then block_id=" << group.blockId << " ops=" << group.ops.size() + << " nestedIfs=" << group.nestedIfs.size()); + } + for (const auto &group : candidate.elseGroups) { + LDBG(" else block_id=" << group.blockId << " ops=" << group.ops.size() + << " nestedIfs=" << group.nestedIfs.size()); + } +} + +/// Walk a single block, grouping ops by block_id. +/// Nested ifs attach to the nearest preceding group. +static SmallVector groupOpsInBlock(Block &block) { + auto *parentOp = block.getParentOp(); + if (!parentOp) { + return {}; + } + Builder builder(parentOp->getContext()); + SmallVector groups; + llvm::SmallDenseMap idToIdx; // block_id -> index in groups + + auto getOrCreateGroup = [&](int bid) -> BlockGroup & { + auto it = idToIdx.find(bid); + if (it != idToIdx.end()) { + return groups[it->second]; + } + idToIdx[bid] = groups.size(); + groups.push_back({bid, {}, {}}); + return groups.back(); + }; + + int currentId = -1; // which group nested / ambient ops attach to + SmallVector + pendingAmbient; // ambient ops before first real group + + auto flushPending = [&](int targetBid) { + if (pendingAmbient.empty()) { + return; + } + auto &g = getOrCreateGroup(targetBid); + for (auto *op : pendingAmbient) { + g.ops.push_back(op); + } + pendingAmbient.clear(); + }; + + for (auto &op : block) { + if (isa(op)) { + continue; + } + + if (auto nestedIf = dyn_cast(op)) { + // Nested ifs form their own group by block_id so the parent if can + // be split when inner ifs of different block_ids coexist in the same + // region. + auto bid = CVPipeline::getOpBlockId(nestedIf); + if (bid.has_value() && *bid != -1) { + flushPending(*bid); + getOrCreateGroup(*bid).nestedIfs.push_back(nestedIf); + currentId = *bid; + } else if (currentId != -1) { + getOrCreateGroup(currentId).nestedIfs.push_back(nestedIf); + } else { + pendingAmbient.push_back(&op); + } + continue; + } + + auto bid = CVPipeline::getOpBlockId(&op); + if (bid.has_value() && *bid != -1) { + flushPending(*bid); + getOrCreateGroup(*bid).ops.push_back(&op); + currentId = *bid; + } else if (currentId != -1) { + // ambient op (id == -1 or 16): keep in the current group + op.setAttr(CVPipeline::kBlockId, builder.getI32IntegerAttr(currentId)); + getOrCreateGroup(currentId).ops.push_back(&op); + } else { + pendingAmbient.push_back(&op); + } + } + + // Flush remaining pending ambient ops into the first real group (if any), + // otherwise they're the only content and don't need splitting. + if (!pendingAmbient.empty() && !groups.empty()) { + int firstBlockId = groups[0].blockId; + for (auto *op : pendingAmbient) { + op->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(firstBlockId)); + groups[0].ops.push_back(op); + } + } + + return groups; +} + +static CandidateIf +getCandidate(scf::IfOp ifOp, + const CVPipeline::MemoryDependenceGraph &memGraph) { + CandidateIf cand; + cand.ifOp = ifOp; + cand.hasYield = (ifOp->getNumResults() > 0); + + // self block_id + auto selfBlockId = CVPipeline::getOpBlockId(ifOp); + cand.selfBlockId = selfBlockId.value_or(-1); + + // Split decision runs on raw per-blockId groups before merging, + // so producer/consumer roles reflect original block structure. + auto rawThenGroups = groupOpsInBlock(*ifOp.thenBlock()); + SmallVector rawElseGroups; + Block *elseBlk = ifOp.elseBlock(); + if (elseBlk) { + rawElseGroups = groupOpsInBlock(*elseBlk); + } + + // Pre-compute split decisions on raw per-blockId groups. + // Only split when both CUBE and VECTOR groups exchange data bidirectionally. + cand.shouldSplitThen = hasBidirectionalCrossCoreDeps(rawThenGroups, memGraph); + cand.shouldSplitElse = hasBidirectionalCrossCoreDeps(rawElseGroups, memGraph); + if (!cand.needsSplit()) { + return cand; + } + + cand.thenGroups = std::move(rawThenGroups); + cand.elseGroups = std::move(rawElseGroups); + + return cand; +} + +// Clone the outer scalar dependencies into the if ops +static void preprocessScalarDependencies(CandidateIf &cand) { + auto processGroup = [](BlockGroup &group) { + auto allOpsIter = llvm::concat(group.ops, group.nestedIfs); + SmallVector allOps{allOpsIter.begin(), allOpsIter.end()}; + ScalarClosure closure{group, allOps}; + closure.collect(); + + auto [newOps, mapper] = closure.capture(group.blockId); + if (newOps.empty()) { + return; + } + + // insert them to the start of group, so that their inner operands can also + // be rewritten + allOps.append(newOps); + group.nestedIfs.clear(); + group.ops.clear(); + + llvm::sort(allOps, [&closure](Operation *a, Operation *b) { + return closure.isBefore(a, b); + }); + + for (Operation *op : allOps) { + op->walk([&, &mapper = mapper](Operation *innerOp) { + for (OpOperand &operand : innerOp->getOpOperands()) { + Value target = mapper.lookupOrNull(operand.get()); + if (target) { + operand.set(target); + } + } + }); + if (auto ifOp = llvm::dyn_cast(op)) { + group.nestedIfs.push_back(ifOp); + } else { + group.ops.push_back(op); + } + } + }; + + llvm::for_each(llvm::concat(cand.thenGroups, cand.elseGroups), + processGroup); +} + +// ============================================================================ +// Part2: Dependency Analysis & Yield Planning +// ============================================================================ +// Scan cross-group SSA deps, then plan yield augmentation (Case A/B). +static void dumpYieldAugmentation(const CandidateIf &cand) { + auto &ya = cand.yieldAug; + bool splitThen = cand.shouldSplitThen; + auto &groups = splitThen ? cand.thenGroups : cand.elseGroups; + const char *region = splitThen ? "then" : "else"; + + // --- value-level cross-group deps --- + if (ya.crossValues.empty()) { + LDBG("[Part2] no cross-group values (region=" << region << ")"); + } else { + LDBG("[Part2] value-level cross-group deps (" + << region << ", " << ya.crossValues.size() << " values):"); + for (auto &cv : ya.crossValues) { + std::string consumerStr; + for (unsigned toG : cv.toGroupIndices) { + if (!consumerStr.empty()) { + consumerStr += ", "; + } + consumerStr += std::to_string(groups[toG].blockId); + } + LDBG(" val from block_id=" << groups[cv.fromGroupIdx].blockId + << " -> consumed by block_id(s): [" + << consumerStr << "]" + << " type=" << cv.value.getType()); + } + } + + // --- per-group output info --- + for (unsigned gi = 0; gi < ya.groupOutputs.size(); ++gi) { + auto &output = ya.groupOutputs[gi]; + if (output.isVoid()) { + LDBG("[Part2] group[" << gi << "] (block_id=" << groups[gi].blockId + << "): void if"); + } else { + LDBG("[Part2] group[" << gi << "] (block_id=" << groups[gi].blockId + << "): " << output.outputValues.size() + << " output(s)"); + for (unsigned idx = 0; idx < output.outputValues.size(); ++idx) { + LDBG(" output[" << idx << "] = " << output.outputValues[idx] + << " type=" << output.outputTypes[idx]); + } + } + } + + // --- last-if info (Case B) --- + if (cand.hasYield) { + LDBG("[Part2] Case B: last-if original yield (" << ya.numOriginalSlots + << " slot(s))"); + for (unsigned slot = 0; slot < ya.numOriginalSlots; ++slot) { + LDBG(" slot[" << slot << "] = " << ya.origYieldValues[slot] + << " producer=group " << ya.origYieldProducerGroup[slot]); + } + } else { + LDBG(" [Part2] Case A: no original yield"); + } +} + +/// Single-pass scan building value-level crossValueMap. +/// Group order is implicit (block_ids in sequential basic block order). +static void scanRegion( + SmallVector &groups, + llvm::SmallDenseMap &opToGroup, + llvm::SmallDenseMap>> + &crossValueMap) { + unsigned n = groups.size(); + if (n < 2) { + return; + } + + for (unsigned gi = 0; gi < n; ++gi) { + for (auto *op : groups[gi].ops) { + for (auto &operand : op->getOpOperands()) { + addCrossGroupDependency(operand.get(), op, gi, opToGroup, + crossValueMap); + } + for (auto ®ion : op->getRegions()) { + region.walk([&](Operation *nestedOp) { + for (auto &nestedOperand : nestedOp->getOpOperands()) { + addCrossGroupDependency(nestedOperand.get(), op, gi, opToGroup, + crossValueMap); + } + }); + } + } + for (auto nestedIf : groups[gi].nestedIfs) { + for (auto &operand : nestedIf->getOpOperands()) { + addCrossGroupDependency(operand.get(), nestedIf.getOperation(), gi, + opToGroup, crossValueMap); + } + } + for (auto nestedIf : groups[gi].nestedIfs) { + nestedIf->walk([&](Operation *innerOp) { + for (auto &innerOperand : innerOp->getOpOperands()) { + addCrossGroupDependency(innerOperand.get(), nestedIf.getOperation(), + gi, opToGroup, crossValueMap); + } + }); + } + // Scan nested if results for cross-group consumers. + // Walk up from each user to find the nearest ancestor in opToGroup. + for (auto nestedIf : groups[gi].nestedIfs) { + for (auto result : nestedIf->getResults()) { + for (auto *user : result.getUsers()) { + Operation *trackedOp = user; + while (trackedOp) { + auto it = opToGroup.find(trackedOp); + if (it != opToGroup.end()) { + if (it->second != gi) { + addCrossGroupDependency(result, trackedOp, it->second, + opToGroup, crossValueMap); + } + break; + } + trackedOp = trackedOp->getParentOp(); + } + } + } + } + } +} + +/// Compute per-group output info for Case B (original yield exists). +/// Tracks else-side yield values for non-last groups' original slots. +static void +planYieldCaseB(CandidateIf &c, bool splitThen, ArrayRef groups, + llvm::SmallDenseMap &opToGroup) { + unsigned nGroups = groups.size(); + auto &ya = c.yieldAug; + + auto splitYieldOp = + cast(splitThen ? c.ifOp.thenBlock()->getTerminator() + : c.ifOp.elseBlock()->getTerminator()); + + ya.numOriginalSlots = splitYieldOp->getNumOperands(); + ya.origYieldValues.clear(); + ya.origYieldProducerGroup.clear(); + for (unsigned slot = 0; slot < ya.numOriginalSlots; ++slot) { + Value val = splitYieldOp->getOperand(slot); + ya.origYieldValues.push_back(val); + auto *defOp = val.getDefiningOp(); + if (defOp) { + auto it = opToGroup.find(defOp); + ya.origYieldProducerGroup.push_back( + it != opToGroup.end() ? static_cast(it->second) : -1); + } else { + ya.origYieldProducerGroup.push_back(-1); // block argument + } + } + + auto otherYieldOp = + cast(splitThen ? c.ifOp.elseBlock()->getTerminator() + : c.ifOp.thenBlock()->getTerminator()); + SmallVector otherYieldValues; + for (auto v : otherYieldOp->getOperands()) { + otherYieldValues.push_back(v); + } + + llvm::SmallDenseMap valueToSlot; + for (unsigned si = 0; si < ya.crossValues.size(); ++si) { + valueToSlot[ya.crossValues[si].value] = si; + } + + ya.groupOutputs.resize(nGroups > 0 ? nGroups - 1 : 0); + for (unsigned gi = 0; gi < nGroups - 1; ++gi) { + auto &output = ya.groupOutputs[gi]; + + // Build map: original yield value produced by this group → slot number. + llvm::SmallDenseMap origValToSlot; + for (unsigned slot = 0; slot < ya.numOriginalSlots; ++slot) { + if (ya.origYieldProducerGroup[slot] == static_cast(gi)) { + origValToSlot[ya.origYieldValues[slot]] = slot; + } + } + + SmallPtrSet addedValues; + + // Step 1: Cross-group values produced by this group. + // Also check if each value is an original yield slot for else-side + // tracking. + for (auto *op : groups[gi].ops) { + for (auto result : op->getResults()) { + auto it = valueToSlot.find(result); + if (it != valueToSlot.end()) { + output.outputValues.push_back(result); + output.outputTypes.push_back(result.getType()); + addedValues.insert(result); + + auto origIt = origValToSlot.find(result); + if (origIt != origValToSlot.end()) { + output.origElseValues.push_back(otherYieldValues[origIt->second]); + } else { + output.origElseValues.push_back(Value()); + } + } + } + } + for (auto nestedIf : groups[gi].nestedIfs) { + for (auto result : nestedIf->getResults()) { + auto it = valueToSlot.find(result); + if (it != valueToSlot.end()) { + output.outputValues.push_back(result); + output.outputTypes.push_back(result.getType()); + addedValues.insert(result); + + auto origIt = origValToSlot.find(result); + if (origIt != origValToSlot.end()) { + output.origElseValues.push_back(otherYieldValues[origIt->second]); + } else { + output.origElseValues.push_back(Value()); + } + } + } + } + + // Step 2: Original yield values produced by this group that were NOT + // already added as cross-group values + for (unsigned slot = 0; slot < ya.numOriginalSlots; ++slot) { + if (ya.origYieldProducerGroup[slot] == static_cast(gi)) { + Value origVal = ya.origYieldValues[slot]; + if (!addedValues.contains(origVal)) { + output.outputValues.push_back(origVal); + output.outputTypes.push_back(origVal.getType()); + output.origElseValues.push_back(otherYieldValues[slot]); + } + } + } + } +} + +/// Compute per-group output info for Case A (no original yield). +/// Only cross-group values are yielded; last group is void. +static void planYieldCaseA(CandidateIf &c, ArrayRef groups) { + unsigned nGroups = groups.size(); + auto &ya = c.yieldAug; + + llvm::SmallDenseMap valueToSlot; + for (unsigned si = 0; si < ya.crossValues.size(); ++si) { + valueToSlot[ya.crossValues[si].value] = si; + } + + // Per-group output for non-last groups only. + ya.groupOutputs.resize(nGroups > 0 ? nGroups - 1 : 0); + for (unsigned gi = 0; gi < nGroups - 1; ++gi) { + auto &output = ya.groupOutputs[gi]; + for (auto *op : groups[gi].ops) { + for (auto result : op->getResults()) { + auto it = valueToSlot.find(result); + if (it != valueToSlot.end()) { + output.outputValues.push_back(result); + output.outputTypes.push_back(result.getType()); + } + } + } + for (auto nestedIf : groups[gi].nestedIfs) { + for (auto result : nestedIf->getResults()) { + auto it = valueToSlot.find(result); + if (it != valueToSlot.end()) { + output.outputValues.push_back(result); + output.outputTypes.push_back(result.getType()); + } + } + } + } +} + +static void planYield( + CandidateIf &c, bool splitThen, ArrayRef groups, + llvm::SmallDenseMap &opToGroup, + llvm::SmallDenseMap>> + &crossValueMap) { + unsigned nGroups = groups.size(); + auto &ya = c.yieldAug; + ya.crossValues.clear(); + ya.groupOutputs.clear(); + + for (auto &[val, info] : crossValueMap) { + unsigned fromG = info.first; + SmallVector toGroups; + SmallVector groupHasConsumer(nGroups, false); + for (auto *consumerOp : info.second) { + auto git = opToGroup.find(consumerOp); + if (git != opToGroup.end() && !groupHasConsumer[git->second]) { + groupHasConsumer[git->second] = true; + toGroups.push_back(git->second); + } + } + ya.crossValues.push_back({val, fromG, std::move(toGroups)}); + } + + // Stable sort for deterministic order: by producer group, then value ptr. + llvm::sort( + ya.crossValues, [](const CrossGroupValue &a, const CrossGroupValue &b) { + if (a.fromGroupIdx != b.fromGroupIdx) { + return a.fromGroupIdx < b.fromGroupIdx; + } + return a.value.getAsOpaquePointer() < b.value.getAsOpaquePointer(); + }); + + if (c.hasYield) { + planYieldCaseB(c, splitThen, groups, opToGroup); + } else { + planYieldCaseA(c, groups); + } +} + +static void analyzeDependencies(CandidateIf &candidate) { + // Step 2.1: Build op → group index maps (including nested ifs) + llvm::SmallDenseMap opToThenGroup; + for (unsigned gi = 0; gi < candidate.thenGroups.size(); ++gi) { + for (auto *op : candidate.thenGroups[gi].ops) { + opToThenGroup[op] = gi; + } + for (auto nestedIf : candidate.thenGroups[gi].nestedIfs) { + opToThenGroup[nestedIf.getOperation()] = gi; + } + } + + llvm::SmallDenseMap opToElseGroup; + for (unsigned gi = 0; gi < candidate.elseGroups.size(); ++gi) { + for (auto *op : candidate.elseGroups[gi].ops) { + opToElseGroup[op] = gi; + } + for (auto nestedIf : candidate.elseGroups[gi].nestedIfs) { + opToElseGroup[nestedIf.getOperation()] = gi; + } + } + + // Step 2.2: Single-pass scan — value-level crossValueMap only + llvm::SmallDenseMap>> + thenValueMap, elseValueMap; + + scanRegion(candidate.thenGroups, opToThenGroup, thenValueMap); + scanRegion(candidate.elseGroups, opToElseGroup, elseValueMap); + + // Step 2.3: Plan yield augmentation for the active region. + // Groups are in natural dependency order from sequential basic block. + bool splitThen = candidate.shouldSplitThen; + if (splitThen) { + planYield(candidate, /*splitThen=*/true, ArrayRef(candidate.thenGroups), + opToThenGroup, thenValueMap); + } else if (candidate.shouldSplitElse) { + planYield(candidate, /*splitThen=*/false, ArrayRef(candidate.elseGroups), + opToElseGroup, elseValueMap); + } + + // Step 2.4: Debug yield augmentation + dumpYieldAugmentation(candidate); +} + +// ============================================================================ +// Part3: Materialization +// ============================================================================ +// Build split-if chain with per-group signatures; no passthrough chain. + +/// Walk through reinterpret_cast and subview ops to find the root memref. +/// Returns the ultimate source for downstream provenance analyses. +static Value getRootMemRef(Value v) { + while (v) { + if (auto *op = v.getDefiningOp()) { + if (isa(op)) { + v = op->getOperand(0); + continue; + } + // Trace through split-if chain results: the then branch carries + // the real computation, whose root we want for the placeholder. + if (auto ifOp = dyn_cast(op)) { + unsigned resultIdx = cast(v).getResultNumber(); + auto thenTerm = ifOp.thenBlock()->getTerminator(); + if (auto thenYield = dyn_cast(thenTerm)) { + v = thenYield->getOperand(resultIdx); + continue; + } + } + } + break; + } + return v; +} + +/// Walk through scf.if (then branch) and scf.for (init args) to find +/// the ultimate source tensor for provenance-preserving placeholders. +static Value getRootTensor(Value v) { + while (v) { + if (auto *op = v.getDefiningOp()) { + // Trace through split-if chain: then branch carries the real + // computation whose source we want for the placeholder. + if (auto ifOp = dyn_cast(op)) { + unsigned resultIdx = cast(v).getResultNumber(); + auto thenTerm = ifOp.thenBlock()->getTerminator(); + if (auto thenYield = dyn_cast(thenTerm)) { + v = thenYield->getOperand(resultIdx); + continue; + } + } + // Trace through scf.for: init arg is the source before the loop. + if (auto forOp = dyn_cast(op)) { + unsigned resultIdx = cast(v).getResultNumber(); + v = forOp.getInitArgs()[resultIdx]; + continue; + } + } + break; + } + return v; +} + +static llvm::FailureOr createMatmulPlaceHolderValue( + OpBuilder &builder, scf::IfOp ifOp, linalg::MatmulOp matmulOp, + RankedTensorType tensorType, Location loc, int blockId) { + auto bias = matmulOp.getDpsInitOperand(0)->get(); + auto block = ifOp->getBlock(); + return llvm::TypeSwitch>(bias) + .Case([&](BlockArgument barg) -> llvm::FailureOr { + if (barg.getParentBlock()->findAncestorOpInBlock(*ifOp)) + return bias; + else + return llvm::failure(); + }) + .Case([&](OpResult res) -> llvm::FailureOr { + auto defOp = res.getDefiningOp(); + auto fillOp = llvm::dyn_cast_if_present(defOp); + if (!fillOp) { + return llvm::failure(); + } + + auto sourceOp = fillOp.getDpsInitOperand(0)->get().getDefiningOp(); + auto sourceEmpty = llvm::dyn_cast_if_present(sourceOp); + if (!sourceEmpty) + return llvm::failure(); + + auto emptyOp = builder.create( + loc, tensorType.getShape(), tensorType.getElementType()); + emptyOp->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(blockId)); + return emptyOp.getResult(); + }) + .Default([](auto) { return llvm::failure(); }); +} + +/// Create a zero/default SSA value of the given type for else-block +/// placeholders. Traces referenceValue provenance when possible to preserve +/// downstream analysis. +static llvm::FailureOr +createPlaceholderValue(int blockId, OpBuilder &builder, Location loc, Type type, + Value referenceValue, scf::IfOp ifOp) { + Value result; + bool usedTrace = false; + if (auto tensorType = dyn_cast(type)) { + // When referenceValue traces to a real tensor source, use it directly + // so downstream provenance analyses see the same origin in both branches. + if (referenceValue) { + auto matmulOpt = hivm::traceDefOp(referenceValue); + if (matmulOpt.has_value()) { + auto matmulOp = llvm::dyn_cast(matmulOpt.value()); + if (matmulOp) { + auto resultRes = createMatmulPlaceHolderValue( + builder, ifOp, matmulOp, tensorType, loc, blockId); + if (llvm::failed(resultRes)) { + return llvm::failure(); + } + result = resultRes.value(); + usedTrace = true; + } + } + if (!usedTrace) { + Value root = getRootTensor(referenceValue); + auto *rootRegion = root.getParentRegion(); + auto *builderRegion = builder.getBlock()->getParent(); + bool dominates = (rootRegion == builderRegion) || + rootRegion->isAncestor(builderRegion); + if (dominates) { + if (auto *rootOp = root.getDefiningOp()) { + auto existingId = + rootOp->getAttrOfType(CVPipeline::kBlockId); + if (!existingId || existingId.getInt() != -1) { + result = root; + usedTrace = true; + } + } + } + } + } + if (!usedTrace) { + auto emptyOp = builder.create( + loc, tensorType.getShape(), tensorType.getElementType()); + emptyOp->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(blockId)); + Value fillVal = builder.create( + loc, tensorType.getElementType(), + builder.getZeroAttr(tensorType.getElementType())); + auto fillOp = builder.create( + loc, ValueRange{fillVal}, ValueRange{emptyOp.getResult()}); + fillOp->setAttr(CVPipeline::kBlockId, builder.getI32IntegerAttr(blockId)); + result = fillOp.getResult(0); + } + } else if (auto floatType = dyn_cast(type)) { + result = builder.create( + loc, builder.getFloatAttr(floatType, 0.0)); + } else if (auto intType = dyn_cast(type)) { + result = builder.create( + loc, builder.getIntegerAttr(intType, 0)); + } else if (type.isIndex()) { + result = builder.create(loc, builder.getIndexAttr(0)); + } else if (auto memrefType = dyn_cast(type)) { + // Create a base memref placeholder. For cbuf/ub, use alloca; for plain + // memref, use alloc to avoid misleading downstream traceback. + Value baseMemref; + bool usedReference = false; + // When referenceValue traces to a function argument (GM), use that + // argument so both branches of the split-if chain share the same memory + // space. + if (referenceValue && !memrefType.getMemorySpace() && + isa(memrefType.getLayout())) { + Value root = getRootMemRef(referenceValue); + if (auto blockArg = dyn_cast(root)) { + if (blockArg.getOwner()->isEntryBlock()) { + baseMemref = root; + usedReference = true; + } + } + } + if (!usedReference) { + if (memrefType.getMemorySpace()) { + SmallVector allocaSizes; + for (int64_t i = 0; i < memrefType.getRank(); ++i) { + if (memrefType.isDynamicDim(i)) { + allocaSizes.push_back( + builder.create(loc, builder.getIndexAttr(1)) + .getResult()); + } + } + auto simpleType = + MemRefType::get(memrefType.getShape(), memrefType.getElementType()); + baseMemref = + builder.create(loc, simpleType, allocaSizes) + .getResult(); + } else { + // For plain memrefs without address_space, use alloc for static shapes + // and alloca for dynamic shapes. block_id = -1 marks it as placeholder. + bool hasDynamic = + llvm::any_of(memrefType.getShape(), ShapedType::isDynamic); + if (hasDynamic) { + SmallVector allocaSizes; + for (int64_t i = 0; i < memrefType.getRank(); ++i) { + if (memrefType.isDynamicDim(i)) { + allocaSizes.push_back( + builder + .create(loc, builder.getIndexAttr(1)) + .getResult()); + } + } + auto simpleType = MemRefType::get(memrefType.getShape(), + memrefType.getElementType()); + baseMemref = + builder.create(loc, simpleType, allocaSizes) + .getResult(); + } else { + baseMemref = + builder.create(loc, memrefType).getResult(); + } + } + } + // Apply strided layout via reinterpret_cast if needed. + auto layout = memrefType.getLayout(); + if (auto stridedLayout = dyn_cast(layout)) { + SmallVector staticOffsets, staticSizes, staticStrides; + SmallVector dynOffsets, dynSizes, dynStrides; + + int64_t off = stridedLayout.getOffset(); + staticOffsets.push_back(off); + if (ShapedType::isDynamic(off)) { + dynOffsets.push_back( + builder.create(loc, builder.getIndexAttr(0)) + .getResult()); + } + + for (int64_t sz : memrefType.getShape()) { + staticSizes.push_back(sz); + if (ShapedType::isDynamic(sz)) { + dynSizes.push_back( + builder.create(loc, builder.getIndexAttr(1)) + .getResult()); + } + } + + for (int64_t stride : stridedLayout.getStrides()) { + staticStrides.push_back(stride); + if (ShapedType::isDynamic(stride)) { + dynStrides.push_back( + builder.create(loc, builder.getIndexAttr(1)) + .getResult()); + } + } + + SmallVector mixedOffsets = + getMixedValues(staticOffsets, dynOffsets, builder); + SmallVector mixedSizes = + getMixedValues(staticSizes, dynSizes, builder); + SmallVector mixedStrides = + getMixedValues(staticStrides, dynStrides, builder); + auto castOp = builder.create( + loc, memrefType, baseMemref, mixedOffsets.front(), mixedSizes, + mixedStrides); + result = castOp.getResult(); + } else { + result = baseMemref; + } + } else { + LDBG( + "[Error]: unsupported type for placeholder value in Case A else block"); + return llvm::failure(); + } + + if (!usedTrace) { + result.getDefiningOp()->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(blockId)); + } + return result; +} + +static void +rewireAndMoveOps(BlockGroup &group, + llvm::SmallDenseMap &crossValueReplacement, + Block &targetBlock) { + auto rewireOperand = [&](OpOperand &operand) { + auto it = crossValueReplacement.find(operand.get()); + if (it != crossValueReplacement.end()) { + operand.set(it->second); + } + }; + + for (auto *op : group.ops) { + for (auto &operand : op->getOpOperands()) { + rewireOperand(operand); + } + + if (!isa(op)) { + for (auto ®ion : op->getRegions()) { + // Use region walk (not op->walk) to avoid re-visiting the parent op + // whose operands were already rewired above. + region.walk([&](Operation *nestedOp) { + for (auto &operand : nestedOp->getOpOperands()) { + rewireOperand(operand); + } + }); + } + } + } + for (auto nestedIf : group.nestedIfs) { + for (auto &operand : nestedIf->getOpOperands()) { + rewireOperand(operand); + } + + for (auto ®ion : nestedIf->getRegions()) { + region.walk([&](Operation *nestedOp) { + for (auto &operand : nestedOp->getOpOperands()) { + rewireOperand(operand); + } + }); + } + } + + // Merge ops and nestedIfs, then sort by original position to preserve + // interleaving order before moving into the split-if's then block. + SmallVector allOps; + allOps.reserve(group.ops.size() + group.nestedIfs.size()); + allOps.append(group.ops.begin(), group.ops.end()); + for (auto nestedIf : group.nestedIfs) { + allOps.push_back(nestedIf.getOperation()); + } + if (allOps.empty()) { + return; + } + llvm::sort(allOps, + [](Operation *a, Operation *b) { return a->isBeforeInBlock(b); }); + OpBuilder builder{allOps.front()}; + for (auto *op : allOps) { + op->moveBefore(&targetBlock, targetBlock.end()); + if (!op->hasAttr(CVPipeline::kBlockId) && !isa(op)) { + op->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(group.blockId)); + } + } +} + +/// Ensure a value can be safely yielded from the else block. +/// Clones alias ops and allocs locally; constants pass through unchanged. +static Value ensureLocalValue(int blockId, Value val, Block &elseBlock, + OpBuilder &builder) { + if (isa(val)) { + return val; + } + + auto *defOp = val.getDefiningOp(); + if (!defOp) { + return val; + } + + if (defOp->getBlock() == &elseBlock) { + return val; + } + + if (isa(defOp)) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToEnd(&elseBlock); + auto *cloned = builder.clone(*defOp); + cloned->setAttr(CVPipeline::kBlockId, builder.getI32IntegerAttr(blockId)); + return cloned->getResult(0); + } + + return val; +} + +static void buildThenYieldForGroup(const GroupOutputInfo &output, + Block &thenBlock, Location loc, + OpBuilder &builder) { + builder.setInsertionPointToEnd(&thenBlock); + SmallVector yieldVals; + for (auto val : output.outputValues) { + yieldVals.push_back(val); + } + builder.create(loc, yieldVals); +} + +static bool valueDominates(Value val, Block *block) { + if (isa(val)) { + return true; + } + auto *defOp = val.getDefiningOp(); + if (!defOp) { + return true; + } + Block *defBlock = defOp->getBlock(); + for (Block *cur = block; cur; + cur = cur->getParentOp() ? cur->getParentOp()->getBlock() : nullptr) { + if (cur == defBlock) { + return true; + } + } + return false; +} + +static llvm::LogicalResult +buildElseYieldForGroup(const int blockId, const GroupOutputInfo &output, + Block &elseBlock, Location loc, OpBuilder &builder) { + auto ifOp = llvm::dyn_cast(elseBlock.getParentOp()); + if (!ifOp) + return llvm::failure(); + builder.setInsertionPointToEnd(&elseBlock); + SmallVector yieldVals; + + // Cache placeholders by type to avoid creating duplicate constants + // (e.g., 5 identical "arith.constant 0 : index" for 5 index slots). + llvm::SmallDenseMap> placeholderCache; + auto getOrCreatePlaceholder = [&](Type type, Value ref) -> FailureOr { + auto it = placeholderCache.find(type); + if (it != placeholderCache.end()) { + return it->second; + } + auto ph = createPlaceholderValue(blockId, builder, loc, type, ref, ifOp); + placeholderCache[type] = ph; + return ph; + }; + + for (unsigned idx = 0; idx < output.outputValues.size(); ++idx) { + // Check if this slot has an original else-side yield value (Case B). + // If the value doesn't dominate the new split-if's else block, fall back to + // placeholder. + if (idx < output.origElseValues.size() && output.origElseValues[idx]) { + Value elseVal = output.origElseValues[idx]; + if (valueDominates(elseVal, &elseBlock)) { + yieldVals.push_back( + ensureLocalValue(blockId, elseVal, elseBlock, builder)); + } + continue; + } + auto phRes = getOrCreatePlaceholder(output.outputTypes[idx], + output.outputValues[idx]); + if (llvm::failed(phRes)) { + return failure(); + } + yieldVals.push_back(phRes.value()); + } + builder.create(loc, yieldVals); + return llvm::success(); +} + +struct OtherSideContext { + bool hasOps; + SmallVector ops; + SmallVector yieldValues; +}; + +/// Collect ops and yield values from the side NOT being split. +/// In Scene 3/4 the last split-if's else block absorbs them. +static OtherSideContext collectOtherSideInfo(const CandidateIf &c, + bool splitThen) { + OtherSideContext ctx; + auto &otherGroups = splitThen ? c.elseGroups : c.thenGroups; + ctx.hasOps = !otherGroups.empty(); + + if (ctx.hasOps) { + auto originalIf = c.ifOp; + for (auto &g : otherGroups) { + for (auto *op : g.ops) { + ctx.ops.push_back(op); + } + for (auto nestedIf : g.nestedIfs) { + ctx.ops.push_back(nestedIf.getOperation()); + } + } + llvm::sort(ctx.ops, [](Operation *a, Operation *b) { + return a->isBeforeInBlock(b); + }); + } + + // Always collect yield values for Case B, even when the other side has no + // ops. These dominate all split-ifs and must be preserved. + if (c.hasYield) { + auto originalIf = c.ifOp; + Block *otherBlk = + splitThen ? originalIf.elseBlock() : originalIf.thenBlock(); + auto otherYield = cast(otherBlk->getTerminator()); + for (auto v : otherYield->getOperands()) { + ctx.yieldValues.push_back(v); + } + } + return ctx; +} + +// ============================================================================ +// Part3: Materialization (orchestrator) +// ============================================================================ + +/// Build then yield for the last-if in Case B. +/// Re-yields earlier-group values via jump references (no passthrough chain). +static void buildThenYieldForLastIf( + unsigned lastGi, const YieldAugmentation &ya, + llvm::SmallDenseMap &crossValueReplacement, Block &thenBlock, + Location loc, OpBuilder &builder) { + builder.setInsertionPointToEnd(&thenBlock); + SmallVector yieldVals; + for (unsigned slot = 0; slot < ya.numOriginalSlots; ++slot) { + int prodGroup = ya.origYieldProducerGroup[slot]; + if (prodGroup == static_cast(lastGi) || prodGroup < 0) { + // Produced by last group, or is a block arg / external value: + // yield directly (it dominates the split-if). + yieldVals.push_back(ya.origYieldValues[slot]); + } else { + // Produced by an earlier group: re-yield via jump reference. + Value oldVal = ya.origYieldValues[slot]; + auto it = crossValueReplacement.find(oldVal); + if (it != crossValueReplacement.end()) { + yieldVals.push_back(it->second); + } else { + yieldVals.push_back(oldVal); + } + } + } + builder.create(loc, yieldVals); +} + +/// Build else yield for the last-if in Case B. +/// Absorbs other-side ops (Scene 3/4) or creates placeholders. +static llvm::LogicalResult +buildElseYieldForLastIf(int blockId, const OtherSideContext &otherCtx, + ArrayRef lastIfTypes, Block &elseBlock, + Location loc, OpBuilder &builder) { + auto ifOp = llvm::dyn_cast(elseBlock.getParentOp()); + if (elseBlock.mightHaveTerminator()) { + elseBlock.getTerminator()->erase(); + } + + builder.setInsertionPointToEnd(&elseBlock); + SmallVector yieldVals; + + if (otherCtx.hasOps) { + // Scene 3/4: absorb other side ops into else block. + for (auto *op : otherCtx.ops) { + op->moveBefore(&elseBlock, elseBlock.end()); + } + } + + // Use original else-branch yield values when available (Case B). + // These dominate all split-ifs and preserve original semantics. + for (unsigned slot = 0; slot < lastIfTypes.size(); ++slot) { + if (slot < otherCtx.yieldValues.size()) { + yieldVals.push_back(ensureLocalValue(blockId, otherCtx.yieldValues[slot], + elseBlock, builder)); + } else { + auto phRes = createPlaceholderValue(blockId, builder, loc, + lastIfTypes[slot], Value(), ifOp); + if (llvm::failed(phRes)) { + return failure(); + } + yieldVals.push_back(phRes.value()); + } + } + + builder.create(loc, yieldVals); + return llvm::success(); +} + +static void updateCrossValueReplacementGroup( + const GroupOutputInfo &output, scf::IfOp splitIf, Block &thenBlock, + llvm::SmallDenseMap &crossValueReplacement) { + SmallPtrSet thenBlocks; + thenBlock.walk([&](Block *b) { thenBlocks.insert(b); }); + + for (unsigned idx = 0; idx < output.outputValues.size(); ++idx) { + Value oldVal = output.outputValues[idx]; + Value newVal = splitIf.getResult(idx); + oldVal.replaceUsesWithIf(newVal, [&](OpOperand &operand) { + return !thenBlocks.contains(operand.getOwner()->getBlock()); + }); + crossValueReplacement[oldVal] = newVal; + } +} + +static scf::YieldOp safeGetTerminator(Block *block) { + if (!block) { + return nullptr; + } + return llvm::dyn_cast_if_present(block->getTerminator()); +} + +struct SplittedIfTagGenerator { + int counter = 0; + int next() { return counter++; } +}; + +static void postProcess(scf::IfOp ifOp, scf::IfOp sourceIfOp, int blockId, + int splittedIfTag) { + OpBuilder builder{ifOp}; + + ifOp->setAttrs(sourceIfOp->getAttrs()); + + auto thenYield = safeGetTerminator(ifOp.thenBlock()); + auto elseYield = safeGetTerminator(ifOp.elseBlock()); + if (thenYield && thenYield.getNumOperands() > 0) { + std::string joinedAttr = + llvm::join(llvm::map_range(thenYield.getOperands(), + [](Value value) { + CVPipeline::CoreType ct = + CVPipeline::getValueCoreType(value); + return CVPipeline::coreTypeToString(ct); + }), + ", "); + auto coreTypeAttr = builder.getStringAttr(joinedAttr); + ifOp->setAttr(CVPipeline::kCoreType, coreTypeAttr); + thenYield->setAttr(CVPipeline::kCoreType, coreTypeAttr); + if (elseYield) + elseYield->setAttr(CVPipeline::kCoreType, coreTypeAttr); + } else { + ifOp->removeAttr(CVPipeline::kCoreType); + } + + auto setBlockId = [&](Operation *op) { + op->setAttr(CVPipeline::kBlockId, builder.getI32IntegerAttr(blockId)); + }; + setBlockId(ifOp); + // Don't set block_id on scf.yield terminators + + ifOp->setAttr(kSplittedIf, builder.getI32IntegerAttr(splittedIfTag)); +} + +/// Materialize a split-if chain with per-group signatures. +/// Non-last groups get their own result types; last group carries original +/// results. +static llvm::LogicalResult +materializeCandidate(CandidateIf &c, CVPipeline::ComputeBlockIdManager &bm, + SplittedIfTagGenerator &tagGen) { + OpBuilder builder(c.ifOp); + auto originalIf = c.ifOp; + auto loc = originalIf.getLoc(); + Value condition = originalIf.getCondition(); + auto &ya = c.yieldAug; + + int splittedIfTag = tagGen.next(); + + LDBG("[Part3] enter materializeCandidate hasYield=" << c.hasYield); + + bool splitThen = c.shouldSplitThen; + auto &groups = splitThen ? c.thenGroups : c.elseGroups; + unsigned nGroups = groups.size(); + if (nGroups < 2) { + return llvm::success(); + } + + auto otherCtx = collectOtherSideInfo(c, splitThen); + + LDBG("[Part3] splitThen=" << splitThen << " nGroups=" << nGroups + << " otherSideHasOps=" << otherCtx.hasOps); + + builder.setInsertionPoint(originalIf); + + // Step 3.2: Negate condition when splitting else region. + if (!splitThen) { + auto trueVal = builder.create( + loc, builder.getIntegerAttr(builder.getI1Type(), 1)); + condition = + builder.create(loc, condition, trueVal).getResult(); + } + + llvm::SmallDenseMap crossValueReplacement; + Operation *lastCreatedIf = originalIf.getOperation(); + + // Phase 1: Materialize each non-last group with per-group result types. + // Phase 2 handles the last group separately. + for (unsigned gi = 0; gi < nGroups - 1; ++gi) { + auto &output = ya.groupOutputs[gi]; + scf::IfOp splittedIf; + + if (output.isVoid()) { + LDBG("[Part3] group[" << gi << "] void if"); + + builder.setInsertionPointAfter(lastCreatedIf); + splittedIf = builder.create(loc, condition, /*hasElse=*/false); + splittedIf->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(groups[gi].blockId)); + lastCreatedIf = splittedIf.getOperation(); + if (splittedIf.getThenRegion().empty()) { + splittedIf.getThenRegion().emplaceBlock(); + } + Block &thenBlock = splittedIf.getThenRegion().front(); + if (thenBlock.mightHaveTerminator()) { + thenBlock.getTerminator()->erase(); + } + + rewireAndMoveOps(groups[gi], crossValueReplacement, thenBlock); + + builder.setInsertionPointToEnd(&thenBlock); + builder.create(loc); + + // No crossValueReplacement update needed (void if produces no values). + } else { + LDBG("[Part3] group[" << gi << "] result-bearing if (" + << output.outputValues.size() << " outputs)"); + + builder.setInsertionPointAfter(lastCreatedIf); + splittedIf = builder.create(loc, output.outputTypes, condition, + /*hasElse=*/true); + splittedIf->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(groups[gi].blockId)); + lastCreatedIf = splittedIf.getOperation(); + if (splittedIf.getThenRegion().empty()) { + splittedIf.getThenRegion().emplaceBlock(); + } + if (splittedIf.getElseRegion().empty()) { + splittedIf.getElseRegion().emplaceBlock(); + } + + Block &thenBlock = splittedIf.getThenRegion().front(); + if (thenBlock.mightHaveTerminator()) { + thenBlock.getTerminator()->erase(); + } + rewireAndMoveOps(groups[gi], crossValueReplacement, thenBlock); + buildThenYieldForGroup(output, thenBlock, loc, builder); + + Block &elseBlock = splittedIf.getElseRegion().front(); + if (elseBlock.mightHaveTerminator()) { + elseBlock.getTerminator()->erase(); + } + if (buildElseYieldForGroup(groups[gi].blockId, output, elseBlock, loc, + builder) + .failed()) { + return llvm::failure(); + } + + updateCrossValueReplacementGroup(output, splittedIf, thenBlock, + crossValueReplacement); + } + postProcess(splittedIf, originalIf, groups[gi].blockId, splittedIfTag); + } + + // Phase 2: Materialize the last group. + scf::IfOp lastIf; + unsigned lastGi = nGroups - 1; + if (c.hasYield) { + // Case B: last-if carries ALL original result types. + SmallVector lastIfTypes; + for (unsigned slot = 0; slot < ya.numOriginalSlots; ++slot) { + lastIfTypes.push_back(originalIf.getResult(slot).getType()); + } + + LDBG("[Part3] last-if (Case B, " << ya.numOriginalSlots << " results)"); + + builder.setInsertionPointAfter(lastCreatedIf); + lastIf = builder.create(loc, lastIfTypes, condition, + /*hasElse=*/true); + lastIf->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(groups[lastGi].blockId)); + if (lastIf.getThenRegion().empty()) { + lastIf.getThenRegion().emplaceBlock(); + } + if (lastIf.getElseRegion().empty()) { + lastIf.getElseRegion().emplaceBlock(); + } + + Block &thenBlock = lastIf.getThenRegion().front(); + if (thenBlock.mightHaveTerminator()) { + thenBlock.getTerminator()->erase(); + } + rewireAndMoveOps(groups[lastGi], crossValueReplacement, thenBlock); + buildThenYieldForLastIf(lastGi, ya, crossValueReplacement, thenBlock, loc, + builder); + + Block &elseBlock = lastIf.getElseRegion().front(); + if (buildElseYieldForLastIf(groups[lastGi].blockId, otherCtx, lastIfTypes, + elseBlock, loc, builder) + .failed()) { + return llvm::failure(); + } + + for (unsigned ri = 0; ri < ya.numOriginalSlots; ++ri) { + originalIf.getResult(ri).replaceAllUsesWith(lastIf.getResult(ri)); + } + } else { + // Case A: last group is void (no original yield, no cross-group values). + LDBG("[Part3] last-if (Case A, void if)"); + + builder.setInsertionPointAfter(lastCreatedIf); + bool hasElse = otherCtx.hasOps; + lastIf = builder.create(loc, condition, hasElse); + lastIf->setAttr(CVPipeline::kBlockId, + builder.getI32IntegerAttr(groups[lastGi].blockId)); + if (lastIf.getThenRegion().empty()) { + lastIf.getThenRegion().emplaceBlock(); + } + Block &thenBlock = lastIf.getThenRegion().front(); + if (thenBlock.mightHaveTerminator()) { + thenBlock.getTerminator()->erase(); + } + + rewireAndMoveOps(groups[lastGi], crossValueReplacement, thenBlock); + + builder.setInsertionPointToEnd(&thenBlock); + builder.create(loc); + + // Scene 3/4: else block absorbs other side's ops. + if (hasElse) { + if (lastIf.getElseRegion().empty()) { + lastIf.getElseRegion().emplaceBlock(); + } + Block &elseBlock = lastIf.getElseRegion().front(); + if (elseBlock.mightHaveTerminator()) { + elseBlock.getTerminator()->erase(); + } + + for (auto *op : otherCtx.ops) { + op->moveBefore(&elseBlock, elseBlock.end()); + } + + builder.setInsertionPointToEnd(&elseBlock); + builder.create(loc); + } + } + postProcess(lastIf, originalIf, groups[lastGi].blockId, splittedIfTag); + + originalIf->erase(); + + return llvm::success(); +} + +static void rearrangeIfOp(scf::IfOp ifOp, + CVPipeline::MemoryDependenceGraph &memGraph) { + Operation *lastDependency = nullptr; + Block *block = ifOp->getBlock(); + ifOp->walk([&](Operation *nestedOp) { + for (Value operand : nestedOp->getOperands()) { + auto defOp = operand.getDefiningOp(); + if (!defOp || ifOp->isAncestor(defOp)) { + continue; + } + auto defOpInBlock = block->findAncestorOpInBlock(*defOp); + if (!defOpInBlock) { + continue; + } + if (!lastDependency || lastDependency->isBeforeInBlock(defOpInBlock)) { + lastDependency = defOpInBlock; + } + } + }); + for (auto *memDep : memGraph.getExecBefore(ifOp)) { + auto depInBlock = block->findAncestorOpInBlock(*memDep); + if (!depInBlock) { + continue; + } + if (!lastDependency || lastDependency->isBeforeInBlock(depInBlock)) { + lastDependency = depInBlock; + } + } + if (lastDependency) { + ifOp->moveAfter(lastDependency); + } +} + +namespace { + +class SplitIfByBlockIdPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(SplitIfByBlockIdPass) + + SplitIfByBlockIdPass() = default; + + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "split-if-by-block-id"; } + + llvm::StringRef getDescription() const final { + return "Split scf.if operations so that each if contains only ops of a " + "single block_id"; + } + + void getDependentDialects(DialectRegistry ®istry) const override { + registry.insert(); + } +}; + +} // namespace + +void SplitIfByBlockIdPass::runOnOperation() { + SplittedIfTagGenerator tagGen; + ModuleOp module = getOperation(); + if (hasFallbackAttr(module)) { + return; + } + + FallbackHelper fallback{module}; + + LDBG("Before:\n" << module << "\n----------"); + + CVPipeline::ComputeBlockIdManager bm(module); + auto mainRes = walkMainLoop(module, [&](Operation *op) { + auto funcOp = op->getParentOfType(); + if (funcOp && llvm::is_contained(kSkippedKernels, funcOp.getSymName())) { + LDBG("Skip kernel: " << funcOp.getSymName()); + return llvm::success(); + } + LDBG("Detected main loop: " << *op); + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph{op, aa}; + auto walkRes = op->walk([&](scf::IfOp ifOp) { + auto candidate = getCandidate(ifOp, memGraph); + if (!candidate.needsSplit()) { + return WalkResult::advance(); + } + preprocessScalarDependencies(candidate); + LLVM_DEBUG(dumpCandidate(candidate)); + analyzeDependencies(candidate); + if (materializeCandidate(candidate, bm, tagGen).failed()) { + return WalkResult::interrupt(); + } + + return WalkResult::advance(); + }); + if (walkRes.wasInterrupted()) { + return llvm::failure(); + } + return llvm::success(); + }); + + if (llvm::failed(mainRes)) { + LDBG("Split if failed, fallback to original"); + fallback.restore(); + return; + } + + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph{module, aa}; + module->walk([&](scf::IfOp ifOp) { + if (ifOp->hasAttr(CVPipeline::kSplittedIf)) { + rearrangeIfOp(ifOp, memGraph); + } + }); + + LDBG("After: \n" << module << "\n----------"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createSplitIfByBlockIdPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/WalkMainLoop.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/WalkMainLoop.cpp new file mode 100644 index 0000000000..2f5ad4ff3e --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/WalkMainLoop.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/ADT/STLFunctionalExtras.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" + +#include "Common.h" +#include "DynamicCVPipeline/Common/Utils.h" + +namespace mlir::CVPipeline::SplitIf { + +llvm::FailureOr +walkMainLoop(Operation *op, + llvm::function_ref pred) { + CoreType coreType = CVPipeline::getOpCoreType(op); + bool containsMainLoop = false; + for (auto ®ion : op->getRegions()) { + for (auto &block : region.getBlocks()) { + for (auto &nestedOp : block) { + auto nestedResult = walkMainLoop(&nestedOp, pred); + if (failed(nestedResult)) { + return failure(); + } + auto nested = nestedResult.value(); + coreType = + static_cast(coreType | nested.coreType); + containsMainLoop = containsMainLoop || nested.containsMainLoop; + } + } + } + + if (llvm::isa(op) && coreType == CUBE_AND_VECTOR && + !containsMainLoop) { + if (pred(op).failed()) { + return failure(); + } + containsMainLoop = true; + } + + return WalkMainLoopResult{coreType, containsMainLoop}; +} + +} // namespace mlir::CVPipeline::SplitIf diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UBUsageOptPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UBUsageOptPass.cpp new file mode 100644 index 0000000000..4c9787ae8f --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UBUsageOptPass.cpp @@ -0,0 +1,670 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "DynamicCVPipeline/Common/CycleDetector.h" +#include "DynamicCVPipeline/Common/DependencyHelper.h" +#include "DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include +#include + +#define DEBUG_TYPE "ub-usage-opt" +#define LOG_DEBUG(msg) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << msg) + +using namespace mlir; +using namespace triton; +using namespace mlir::triton; + +namespace mlir { +namespace triton { +class UBUsageOptPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(UBUsageOptPass) + + UBUsageOptPass() = default; + void runOnOperation() override; + + llvm::StringRef getArgument() const final { return "ub-usage-opt"; } + +private: + const int64_t MAX_EDGE_SIZE = (1 << 30); + int64_t getValueSizeInBytes(Value value); + void buildUBUsageGraph(Block *block, DenseMap &op2nodeId, + DenseMap &nodeId2op, + SmallVector> &linkOut, + SmallVector> &linkIn, + SmallVector &linkSize, + SmallVector &linkStart, SmallVector &linkEnd, + SmallVector &nodeBlockId, + SmallVector &nodeCoreType, + SmallVector &nodeArgs, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm); + DenseMap collectRecordChange( + const SmallVector> &needUbOpts, + const SmallVector> &linkOut, + const SmallVector> &linkIn, + const SmallVector &linkSize, const SmallVector &linkStart, + const SmallVector &linkEnd, const SmallVector &nodeBlockId, + const SmallVector &nodeCoreType, + DenseMap nodeId2op); + int64_t sumIncomingLinkSize(int nodeId, + const SmallVector> &linkIn, + const SmallVector &linkSize, + const SmallVector &linkStart, + const SmallVector &linkEnd, + const SmallVector &nodeBlockId); + llvm::LogicalResult + UBUsageOptimization(Block *block, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm); +}; +} // namespace triton +} // namespace mlir + +/** + The UB occupied by the value is used as the edge weight. + The compute block will be partitioned at the location with the smallest edge + weight. Where: + 1. Tensor/Vector: Uses the data size directly. + 2. Memref: Set to the maximum value, indicating that a compute block should + not be partitioned here. + 3. Isndex/Other: Set to 0, indicating that it does not occupy any UB. +*/ +int64_t UBUsageOptPass::getValueSizeInBytes(Value value) { + Type type = value.getType(); + auto getElemBytes = [](Type elemType) -> int64_t { + if (elemType.isIntOrFloat()) { + unsigned bits = elemType.getIntOrFloatBitWidth(); + return std::max(1, bits / 8); + } + return 1; + }; + // Tensor + if (auto rankedTensorType = dyn_cast(type)) { + if (!rankedTensorType.hasStaticShape()) { + return MAX_EDGE_SIZE; + } + int64_t numElements = 1; + for (int64_t dim : rankedTensorType.getShape()) { + if (dim < 0) { + return 1; + } + numElements *= dim; + } + return std::max( + 1, numElements * getElemBytes(rankedTensorType.getElementType())); + } + // Memref + if (auto memRefType = dyn_cast(type)) { + return MAX_EDGE_SIZE; + } + // Vector + if (auto vectorType = dyn_cast(type)) { + return std::max(1, vectorType.getNumElements() * + getElemBytes(vectorType.getElementType())); + } + // Index + if (auto idxTy = dyn_cast(value.getType())) { + return 0; + } + return 0; +} + +void UBUsageOptPass::buildUBUsageGraph( + Block *block, DenseMap &op2nodeId, + DenseMap &nodeId2op, + SmallVector> &linkOut, + SmallVector> &linkIn, SmallVector &linkSize, + SmallVector &linkStart, SmallVector &linkEnd, + SmallVector &nodeBlockId, SmallVector &nodeCoreType, + SmallVector &nodeArgs, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + DenseMap cubeBlockId2nodeId; + const int cubeCoreType = static_cast(CVPipeline::CoreType::CUBE_ONLY); + + auto getOrCreateNodeId = [&](Operation *op) -> int { + if (op2nodeId.find(op) != op2nodeId.end()) { + return op2nodeId.at(op); + } + int coreType = static_cast(CVPipeline::getOpCoreType(op)); + int blockId = bm.getBlockIdByOp(op); + bool canShrink = (coreType == cubeCoreType && blockId != -1); + + if (canShrink) { + auto it = cubeBlockId2nodeId.find(blockId); + if (it != cubeBlockId2nodeId.end()) { + op2nodeId[op] = it->second; + return it->second; + } + } + + int nodeId = static_cast(nodeBlockId.size()); + op2nodeId[op] = nodeId; + nodeId2op[nodeId] = op; + nodeBlockId.push_back(blockId); + nodeCoreType.push_back(coreType); + nodeArgs.push_back(-1); + linkOut.emplace_back(); + linkIn.emplace_back(); + + if (canShrink) { + cubeBlockId2nodeId[blockId] = nodeId; + } + return nodeId; + }; + + DenseMap, bool> visited; + auto addEdge = [&](int src, int dst, int64_t sizeInBytes) { + if (visited.contains(std::make_pair(src, dst))) { + return; + } + if (src == dst) { + // self-cycle can only be args dependency. + LOG_DEBUG("Find one self-cycle: id edge from " + << src << " to " << dst << " size = " << sizeInBytes << "\n"); + LOG_DEBUG("op edge from " << *nodeId2op[src] << " to " << *nodeId2op[dst] + << "\n"); + } + int edgeId = linkSize.size(); + linkSize.push_back(sizeInBytes); + linkStart.push_back(src); + linkEnd.push_back(dst); + linkOut[src].push_back(edgeId); + linkIn[dst].push_back(edgeId); + visited[std::make_pair(src, dst)] = true; + }; + + Operation *terminator = block->getTerminator(); + if (terminator && isa(terminator)) { + unsigned maxArgIdx = std::min(block->getNumArguments(), + terminator->getNumOperands()); + for (unsigned argIdx = 0; argIdx < maxArgIdx; ++argIdx) { + Value yielded = terminator->getOperand(argIdx); + Operation *defOp = yielded.getDefiningOp(); + Operation *defInBlock = CVPipeline::getAncestorInBlock(defOp, block); + if (!defInBlock || defInBlock->getBlock() != block) { + continue; + } + int nodeId = getOrCreateNodeId(defInBlock); + if (nodeArgs[nodeId] == -1) { + nodeArgs[nodeId] = static_cast(argIdx); + } + } + } + + for (Operation &blockOp : *block) { + int dstNode = getOrCreateNodeId(&blockOp); + blockOp.walk([&](Operation *op) { + for (Value operand : op->getOperands()) { + Operation *srcInBlock = nullptr; + bool fromArgEdge = false; + if (Operation *defOp = operand.getDefiningOp()) { + srcInBlock = CVPipeline::getAncestorInBlock(defOp, block); + if (srcInBlock && srcInBlock == &blockOp) { + // To avoid unnesessary self-cycle. + // some ops in block(op) used parent(blockop) args can lead one + // self-cycle. + continue; + } + } else if (auto blockArg = dyn_cast(operand)) { + if (blockArg.getOwner() == block && terminator && + isa(terminator)) { + unsigned numArgs = block->getNumArguments(); + unsigned numYieldOperands = terminator->getNumOperands(); + // for op offset=1, while op offset=0 + int offset = (int)numArgs - (int)numYieldOperands; + int argIdx = (int)blockArg.getArgNumber() - offset; + + if (argIdx >= 0 && argIdx < (int)numYieldOperands) { + Value yielded = terminator->getOperand(argIdx); + if (Operation *yieldDefOp = yielded.getDefiningOp()) { + srcInBlock = CVPipeline::getAncestorInBlock(yieldDefOp, block); + fromArgEdge = true; + } + } + } + } + if (!srcInBlock || srcInBlock->getBlock() != block) { + continue; + } + // To avoid unnesessary self-cycle. + // Two ops in the same cube block lead to self-cycle, so continue it. + int coreType = static_cast(CVPipeline::getOpCoreType(op)); + int srcBlockId = bm.getBlockIdByOp(srcInBlock); + int dstBlockId = bm.getBlockIdByOp(&blockOp); + if (coreType == cubeCoreType && srcBlockId == dstBlockId) { + continue; + } + + int srcNode = getOrCreateNodeId(srcInBlock); + int64_t edgeSize = getValueSizeInBytes(operand); + if (fromArgEdge) { + edgeSize *= 2; + } + if (!visited.contains(std::make_pair(srcNode, dstNode))) { + LOG_DEBUG("Add SSA edage from " << *srcInBlock << " to " << blockOp + << "\nweight = " << edgeSize << "\n"); + } + addEdge(srcNode, dstNode, edgeSize); + } + + for (auto memDef : memGraph.getExecBefore(op)) { + Operation *srcInBlock = nullptr; + srcInBlock = CVPipeline::getAncestorInBlock(memDef, block); + if (!srcInBlock || srcInBlock->getBlock() != block) { + continue; + } + if (srcInBlock && srcInBlock == &blockOp) { + // To avoid unnesessary self-cycle. + // some ops in block(op) used parent(blockop) args can lead one + // self-cycle. + continue; + } + // To avoid unnesessary self-cycle. + // Two ops in the same cube block lead to self-cycle, so continue it. + int coreType = static_cast(CVPipeline::getOpCoreType(op)); + int srcBlockId = bm.getBlockIdByOp(srcInBlock); + int dstBlockId = bm.getBlockIdByOp(&blockOp); + if (coreType == cubeCoreType && srcBlockId == dstBlockId) { + continue; + } + + int srcNode = getOrCreateNodeId(srcInBlock); + if (!visited.contains(std::make_pair(srcNode, dstNode))) { + LOG_DEBUG("Add memory edage from " << *srcInBlock << " to " << blockOp + << "\nweight = " << 0 << "\n"); + } + addEdge(srcNode, dstNode, 0); + } + }); + } +} + +static SmallVector +findDependency(int targetNdoe, int preNode, + const SmallVector> &linkIn, + const SmallVector &linkStart) { + SmallVector dependNodes; + DenseSet visited; + std::queue queue; + queue.push(targetNdoe); + visited.insert(targetNdoe); + while (!queue.empty()) { + int curNode = queue.front(); + queue.pop(); + if (curNode == preNode) { + continue; // we find other linkin of b in A->B, so pass A. + } + + for (int inEdgeId : linkIn[curNode]) { + int inStart = linkStart[inEdgeId]; + if (visited.insert(inStart).second) { + dependNodes.push_back(inStart); + queue.push(inStart); + } + } + } + return dependNodes; +} + +static bool isActiveEndNode(int srcNode, int endNode, + const SmallVector> &linkIn, + const SmallVector &linkStart, + const SmallVector &nodeBlockId, + const SmallVector &nodeCoreType, + DenseMap nodeId2op) { + int nodeNum = static_cast(nodeBlockId.size()); + if (nodeCoreType[endNode] != nodeCoreType[srcNode]) { + return false; + } + if (nodeBlockId[endNode] == -1) { + // meet scf.yield/return.. + return false; + } + if (nodeBlockId[srcNode] == nodeBlockId[endNode]) { + // same cmpute block not leaf + return false; + } + // To avoid cycles: we only collect 2 kind node: + // 1. endNode only from src compute block. A->endnode->... + // 2. endNode's srcs donnot depend any compute block. + // A->endnode;args/const/->src1->endnode->... In fact the second kind includes + // first; BFS/DFS search expanding dependNodes transitively + auto dependNodes = findDependency(endNode, srcNode, linkIn, linkStart); + for (int node : dependNodes) { + if (nodeBlockId[node] != nodeBlockId[endNode] && + nodeBlockId[node] != nodeBlockId[srcNode]) { + if (linkIn[node].size() != 0) { + return false; + } + } + } + return true; +} + +static SmallVector> collectNeedUbOpts( + const SmallVector> &linkOut, + const SmallVector> &linkIn, + const SmallVector &linkStart, const SmallVector &linkEnd, + const SmallVector &nodeBlockId, const SmallVector &nodeCoreType, + DenseMap nodeId2op) { + SmallVector> needUbOpts; + int maxBlockId = -1; // FIXME: We need to count how many compute block in the + // graph, but use max is not accurate. + for (int blockId : nodeBlockId) { + maxBlockId = std::max(maxBlockId, blockId); + } + if (maxBlockId >= 0) { + needUbOpts.resize(static_cast(maxBlockId + 1)); + } + for (int i = 0; i < nodeBlockId.size(); ++i) { + int srcBlockId = nodeBlockId[i]; + int srcCoreType = nodeCoreType[i]; + if (srcCoreType != CVPipeline::CoreType::VECTOR_ONLY) { + // only vector block need ub optimization. + continue; + } + bool canOptimize = false; + for (int outEdgeId : linkOut[i]) { + int dstNode = linkEnd[outEdgeId]; + if (isActiveEndNode(i, dstNode, linkIn, linkStart, nodeBlockId, + nodeCoreType, nodeId2op)) { + canOptimize = true; + break; + } + } + if (canOptimize && srcBlockId >= 0) { + needUbOpts[srcBlockId].push_back(i); + } + } + return needUbOpts; +} + +int64_t UBUsageOptPass::sumIncomingLinkSize( + int nodeId, const SmallVector> &linkIn, + const SmallVector &linkSize, const SmallVector &linkStart, + const SmallVector &linkEnd, const SmallVector &nodeBlockId) { + int64_t totalSize = 0; + for (int edgeId : linkIn[nodeId]) { + if (nodeBlockId[linkStart[edgeId]] != nodeBlockId[linkEnd[edgeId]]) { + if (linkSize[edgeId] == MAX_EDGE_SIZE) { + totalSize = MAX_EDGE_SIZE; + } else { + totalSize += linkSize[edgeId]; + } + } + } + return totalSize; +} + +static bool findUniqueDependentNode( + int curNode, int optBlockId, const SmallVector> &linkOut, + const SmallVector> &linkIn, + const SmallVector &linkStart, const SmallVector &linkEnd, + const SmallVector &nodeBlockId, int &uniqueNextNode) { + // only find A link single chain. + if (linkOut[curNode].size() != 1) + return false; + // sigle chian must be in one compute block. + auto edgeId = linkOut[curNode][0]; + uniqueNextNode = linkEnd[edgeId]; + if (nodeBlockId[uniqueNextNode] != nodeBlockId[curNode]) { + return false; + } + // uniqueNextNode's dependent is all in nodeBlockId[curNode] + bool onlyDependsOnCur = true; + auto dependNodes = findDependency(uniqueNextNode, curNode, linkIn, linkStart); + for (auto node : dependNodes) { + if (nodeBlockId[node] != nodeBlockId[curNode]) { + return false; + } + } + return true; +} + +DenseMap UBUsageOptPass::collectRecordChange( + const SmallVector> &needUbOpts, + const SmallVector> &linkOut, + const SmallVector> &linkIn, + const SmallVector &linkSize, const SmallVector &linkStart, + const SmallVector &linkEnd, const SmallVector &nodeBlockId, + const SmallVector &nodeCoreType, + DenseMap nodeId2op) { + DenseMap recordChange; + int nodeNum = static_cast(nodeBlockId.size()); + for (int optBlockId = 0, blockNum = static_cast(needUbOpts.size()); + optBlockId < blockNum; ++optBlockId) { + for (int optNode : needUbOpts[optBlockId]) { + LOG_DEBUG("optNode: " << *nodeId2op.at(optNode) << "\n"); + SmallVector activateSet; + for (int outEdgeId : linkOut[optNode]) { + int dstNode = linkEnd[outEdgeId]; + if (isActiveEndNode(optNode, dstNode, linkIn, linkStart, nodeBlockId, + nodeCoreType, nodeId2op)) { + if (std::find(activateSet.begin(), activateSet.end(), dstNode) == + activateSet.end()) { + activateSet.push_back(dstNode); + } + } + } + LOG_DEBUG("activateSet Size:" << activateSet.size() << "\n"); + + for (int activateNode : activateSet) { + int64_t originUBSize = sumIncomingLinkSize( + activateNode, linkIn, linkSize, linkStart, linkEnd, nodeBlockId); + LOG_DEBUG("activateNode:" << *nodeId2op.at(activateNode) << "\n"); + LOG_DEBUG("originUBSize:" << originUBSize << "\n"); + int64_t minUBSize = originUBSize; + SmallVector chain; + chain.push_back(activateNode); + int bestCutPointIdx = -1; + + // find the activate chain. + while (true) { + int curNode = chain.back(); + int uniqueNextNode = -1; + if (!findUniqueDependentNode(curNode, optBlockId, linkOut, linkIn, + linkStart, linkEnd, nodeBlockId, + uniqueNextNode)) { + break; + } + + chain.push_back(uniqueNextNode); + } + for (auto i = 0; i < chain.size(); i++) { + auto nowUBSize = 0LL; + LOG_DEBUG("now chain op = " << *nodeId2op.at(chain[i]) << "\n"); + for (auto cutEdgeId : linkOut[chain[i]]) { + if (linkSize[cutEdgeId] == MAX_EDGE_SIZE) { + nowUBSize = MAX_EDGE_SIZE; + break; + } else { + nowUBSize += linkSize[cutEdgeId]; + } + } + if (nowUBSize < minUBSize) { + bestCutPointIdx = i + 1; + minUBSize = nowUBSize; + } + } + + if (bestCutPointIdx > 0) { + // need to change not only chain[i], and chain[i]'s dependency. + for (int i = 0; i < bestCutPointIdx; ++i) { + recordChange[chain[i]] = optBlockId; + auto chainPreNode = i - 1 < 0 ? optNode : chain[i - 1]; + auto dependNodes = + findDependency(chain[i], chainPreNode, linkIn, linkStart); + for (auto node : dependNodes) { + if (nodeBlockId[node] != optBlockId) { + recordChange[node] = optBlockId; + } + } + } + } + } + } + } + return recordChange; +} + +bool applyRecordChange(DenseMap &recordChange, + DenseMap &nodeId2op, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + // Get ever blockid should be add which nodeId in recordChange. + DenseMap> blockWilladd; + for (const auto &it : recordChange) { + int nodeId = it.first; + int optBlockId = it.second; + blockWilladd[optBlockId].push_back(nodeId); + } + bool hasError = false; + for (auto it : blockWilladd) { + int targetBlockId = it.first; + auto willaddNodes = it.second; + llvm::SmallVector willaddOps; + for (int nodeId : willaddNodes) { + willaddOps.push_back(nodeId2op[nodeId]); + } + if (CVPipeline::willCreateCycle(willaddOps, memGraph, targetBlockId, bm)) { + LOG_DEBUG("Find cycle when apply change for blockId: " << targetBlockId + << "\n"); + for (auto nodeId : willaddNodes) { + LOG_DEBUG(" - " << *nodeId2op[nodeId] << "\n"); + } + hasError = true; + continue; + } + + for (auto op : willaddOps) { + bm.updateBlockIdWithInner(op, targetBlockId); + } + } + + return hasError; +} + +llvm::LogicalResult UBUsageOptPass::UBUsageOptimization( + Block *block, const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + if (!(isa(block->getParentOp()) || + isa(block->getParentOp()))) { + return llvm::success(); + } + DenseMap op2nodeId; + DenseMap nodeId2op; + SmallVector> linkOut; + SmallVector> linkIn; + SmallVector linkSize; + SmallVector linkStart; + SmallVector linkEnd; + SmallVector nodeBlockId; + SmallVector nodeCoreType; + SmallVector nodeArgs; + buildUBUsageGraph(block, op2nodeId, nodeId2op, linkOut, linkIn, linkSize, + linkStart, linkEnd, nodeBlockId, nodeCoreType, nodeArgs, + memGraph, bm); + SmallVector> needUbOpts = + collectNeedUbOpts(linkOut, linkIn, linkStart, linkEnd, nodeBlockId, + nodeCoreType, nodeId2op); + int candidateCnt = 0; + for (const auto &nodes : needUbOpts) { + candidateCnt += static_cast(nodes.size()); + for (auto opid : nodes) { + LOG_DEBUG("maybe need opt: " << *nodeId2op[opid] << "\t id = " << opid + << "\n"); + } + } + LOG_DEBUG("Find " << candidateCnt << " op maybe need UB optimization\n"); + llvm::DenseMap recordChange = + collectRecordChange(needUbOpts, linkOut, linkIn, linkSize, linkStart, + linkEnd, nodeBlockId, nodeCoreType, nodeId2op); + LOG_DEBUG("Need change blockId for " << recordChange.size() << " nodes\n"); + for (auto rec : recordChange) { + auto node = nodeId2op[rec.first]; + LOG_DEBUG("Change " << *node << " TO " << rec.second << "\n"); + } + + if (applyRecordChange(recordChange, nodeId2op, memGraph, bm)) { + LOG_DEBUG("Some skiped when apply UB usage optimization changes.\n"); + } + return llvm::success(); +} + +void mlir::triton::UBUsageOptPass::runOnOperation() { + LOG_DEBUG("--- Pass: UBUsageOpt ---\n"); + + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + auto &aliasAnalysis = getAnalysis(); + CVPipeline::MemoryDependenceGraph memDepGraph(module, aliasAnalysis); + auto bm = CVPipeline::ComputeBlockIdManager(module); + + llvm::SmallVector blocks; + module.walk([&](Block *block) { blocks.push_back(block); }); + + for (Block *block : blocks) { + if (UBUsageOptimization(block, memDepGraph, bm).failed()) { + LOG_DEBUG("UB usage optimization failed in block.\n"); + } + } + + LOG_DEBUG("=== Pass UBUsageOpt complete ===\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createUBUsageOptPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UnifyAllocBlockPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UnifyAllocBlockPass.cpp new file mode 100644 index 0000000000..fb7aea081f --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UnifyAllocBlockPass.cpp @@ -0,0 +1,372 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/TypeSize.h" + +static constexpr const char *DEBUG_TYPE = "unify-alloc-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace triton; + +namespace { + +struct FillInfo { + linalg::FillOp fillOp; + scf::IfOp parentIf; +}; + +/** + * @brief Collect direct users of alloc result + * + * This function collects all operations that directly use the alloc result, + * excluding linalg.fill operations because linalg.fill uses BlockArgument + * (i.e., the outs parameter) rather than SSA value dependency. + * + * @param allocResult The result value of memref.alloc + * @return SmallVector List of direct user operations + * + * @note linalg.fill is a DestinationStyleOp where: + * - ins(%v : f16) is the input value + * - outs(%alloc : memref) is the target memory location (BlockArgument) + * Therefore, linalg.fill does not appear in allocResult.getUsers(). + */ +static SmallVector collectDirectUsers(Value allocResult) { + SmallVector directUsers; + for (Operation *user : allocResult.getUsers()) { + if (!isa(user)) { + directUsers.push_back(user); + } + } + return directUsers; +} + +/** + * @brief Get common block_id from a list of operations + * + * Checks whether all operations have the same block_id. + * If they are the same, returns that block_id; otherwise returns std::nullopt. + * + * Special handling for memref.subview: if direct users include memref.subview, + * we look through to memref.copy. If there's exactly one memref.copy reachable, + * return its block_id (not the subview's). If multiple copies exist, return + * error. + * + * @param ops List of operations to check + * @return std::optional Returns common block_id if all are the same, + * otherwise returns std::nullopt + */ +static LogicalResult getCommonBlockId(ArrayRef ops, int &blockId) { + if (ops.empty()) { + return failure(); + } + + llvm::SmallDenseSet copyBlockIds; + for (Operation *op : ops) { + if (isa(op)) { + for (auto *user : op->getUsers()) { + if (auto copyOp = dyn_cast(user)) { + if (auto blockId = CVPipeline::getOpBlockId(copyOp)) { + copyBlockIds.insert(*blockId); + } + } + } + } + } + + if (copyBlockIds.size() > 1) { + LOG_DEBUG("[getCommonBlockId] Multiple block_ids found, ops: "); + for (Operation *op : ops) { + LOG_DEBUG(" " << *op); + } + return failure(); + } + + if (copyBlockIds.empty()) { + LOG_DEBUG("[getCommonBlockId] There are not CopyOp!"); + for (Operation *op : ops) { + LOG_DEBUG(" " << *op); + } + return failure(); + } + + blockId = *copyBlockIds.begin(); + return success(); +} + +/** + * @brief Find linalg.fill operation that uses alloc as outs inside scf.if + * + * This function searches for linalg.fill operations that satisfy: + * 1. Use the given alloc result as its outs parameter + * 2. Located inside an scf.if operation (then branch only) + * 3. The scf.if has no else region (withElseRegion=false) + * + * @param allocResult The alloc result value to search for + * @return FillInfo Structure containing fillOp and parentIf if found + */ +static FillInfo findFillOpInSCFIf(Value allocResult) { + FillInfo info; + for (Operation *user : allocResult.getUsers()) { + auto fillOp = dyn_cast(user); + if (!fillOp) { + continue; + } + + auto parentIf = fillOp->getParentOfType(); + if (!parentIf) { + continue; + } + + if (!parentIf.getElseRegion().empty()) { + continue; + } + + Block *parentBlock = fillOp->getBlock(); + if (parentBlock != &parentIf.getThenRegion().front()) { + continue; + } + + if (fillOp.getDpsInits()[0] == allocResult) { + info.fillOp = fillOp; + info.parentIf = parentIf; + return info; + } + } + return info; +} + +/** + * @brief Check if the scf.if containing linalg.fill has other operations + * + * Determines whether the then region of the scf.if that contains linalg.fill + * also contains other operations besides linalg.fill. If it does, unification + * must be aborted, because the linalg.fill is mixed with unrelated operations + * and unifying the whole scf.if block_id would be incorrect. + * + * @param info FillInfo structure containing fillOp and parentIf + * @return bool Returns true if there are other ops besides linalg.fill in the + * then region, false otherwise + */ +static bool hasOtherOpsInIf(const FillInfo &info) { + if (!info.fillOp || !info.parentIf) { + return false; + } + + Block *fillBlock = info.fillOp->getBlock(); + int opCount = 0; + for (auto &op : fillBlock->without_terminator()) { + (void)op; + opCount++; + } + return opCount > 1; +} + +/// Trace back along the view-like chain from \p copyOp's source and collect +/// all ops. Only ops in the same Block as the copyOp are collected. +static void traceViewChain(memref::CopyOp copyOp, + SmallVectorImpl &result) { + for (Value v = copyOp.getSource(); auto *defOp = v.getDefiningOp();) { + if (defOp->getBlock() != copyOp->getBlock()) + break; + if (auto viewOp = dyn_cast(defOp)) + result.push_back(viewOp), v = viewOp.getViewSource(); + else if (auto sliceOp = dyn_cast(defOp)) + result.push_back(sliceOp), v = sliceOp.getSource(); + else + break; + } +} + +/** + * @brief Collect source view chain ops from memref.copy's source operand + * + * For each memref.copy found by penetrating ViewLike direct users (subview of + * alloc), trace back the copy's source operand along the ViewLikeOpInterface / + * tensor.extract_slice chain (e.g., memref.subview -> memref.reinterpret_cast + * -> tensor.extract_slice), and collect all ops in the chain. + * + * @param directUsers Direct users of alloc result (containing ViewLike ops + * whose users include memref.copy) + * @return SmallVector Collected source view chain ops + * + */ +static SmallVector +collectSourceViewChainOps(ArrayRef directUsers) { + SmallVector chainOps; + for (Operation *op : directUsers) { + if (!isa(op)) { + continue; + } + // Penetrate through view-like ops to find memref.copy users + SmallVector worklist = {op}; + while (!worklist.empty()) { + Operation *cur = worklist.pop_back_val(); + for (auto *user : cur->getUsers()) { + if (auto copyOp = dyn_cast(user)) { + traceViewChain(copyOp, chainOps); + } else if (isa(user)) { + worklist.push_back(user); + } + } + } + } + return chainOps; +} + +/** + * @brief Try to unify block_id for a single alloc operation + * + * @param allocOp The memref.alloc operation to process + * @param memGraph Memory dependence graph for cycle detection + * @return LogicalResult Returns success if unification was performed, failure + * otherwise + */ +static LogicalResult +tryUnifyForAlloc(memref::AllocOp allocOp, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + // Step1: Collect direct users (excluding linalg.fill) + Value allocResult = allocOp.getResult(); + LOG_DEBUG("[tryUnifyForAlloc] start from allocOp: " << *allocOp); + SmallVector directUsers = collectDirectUsers(allocResult); + if (directUsers.empty()) { + return success(); + } + + // Step2: Find linalg.fill inside scf.if that uses this alloc + FillInfo fillInfo = findFillOpInSCFIf(allocResult); + if (!fillInfo.fillOp) { + return success(); + } + LOG_DEBUG( + "[tryUnifyForAlloc] Found fillOp in scf.if: " << *fillInfo.parentIf); + + // Step3: Check if all direct users have the same block_id + int targetBlockId; + if (failed(getCommonBlockId(directUsers, targetBlockId))) { + LOG_DEBUG("allocOp has copyOp from different Block"); + return failure(); + } + LOG_DEBUG("[getSameBlockId] GetSameBlockId: " << targetBlockId); + + // Step4: If the scf.if has other operations besides linalg.fill, abort + if (hasOtherOpsInIf(fillInfo)) { + LOG_DEBUG("[warning] SCF.IF has other ops, failed to unify"); + return success(); + } + + // Step5: collect allocOp, ifOp, fillOp, directUsers and ViewChainOps + SmallVector coreOps = { + allocOp.getOperation(), + fillInfo.fillOp.getOperation(), + fillInfo.parentIf.getOperation(), + }; + coreOps.append(directUsers); + coreOps.append(collectSourceViewChainOps(directUsers)); + + // Step6: Cycle detection and block_id assignment + if (CVPipeline::willCreateCycle(coreOps, memGraph, targetBlockId, bm)) { + LOG_DEBUG("[error] Find cycle, have unsupport IR! Should Check!!"); + return failure(); + } + for (auto *op : coreOps) { + bm.updateBlockId(op, targetBlockId); + } + LOG_DEBUG("[tryUnifyForAlloc] Successfully unify block_id to " + << targetBlockId << " for allocOp: " << *allocOp); + return success(); +} + +} // anonymous namespace + +class UnifyAllocBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(UnifyAllocBlockPass) + + UnifyAllocBlockPass() = default; + + StringRef getArgument() const override { return "unify-alloc-block"; } + + StringRef getDescription() const override { + return "Unify block_id for memref.alloc, scf.if with linalg.fill, and " + "memref.subview operations"; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LOG_DEBUG("Before: " << *module << "\n"); + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aa); + auto bm = CVPipeline::ComputeBlockIdManager(module); + + llvm::SmallVector allocOps; + + module.walk([&](memref::AllocOp allocOp) { allocOps.push_back(allocOp); }); + + for (memref::AllocOp allocOp : allocOps) { + if (failed(tryUnifyForAlloc(allocOp, memGraph, bm))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + } + + LOG_DEBUG("After: " << *module << "\n"); + } +}; + +namespace mlir { +namespace triton { + +std::unique_ptr> createUnifyAllocBlockPass() { + return std::make_unique(); +} + +void registerUnifyAllocBlockPass() { + PassRegistration reg; +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UnifyStoreBlockPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UnifyStoreBlockPass.cpp new file mode 100644 index 0000000000..ff4b4b964b --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOpt/UnifyStoreBlockPass.cpp @@ -0,0 +1,374 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Operation.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "unify-store-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +using namespace mlir; +using namespace triton; + +namespace { + +static bool isStoreOp(const Operation *op) { + return isa(op) || + isa(op); +} + +static Value getStoreSource(Operation *storeOp) { + if (auto materialize = + dyn_cast(storeOp)) { + return materialize.getSource(); + } + if (auto hivmStore = dyn_cast(storeOp)) { + return hivmStore.getSrc(); + } + return Value(); +} + +static Value getStoreDest(Operation *storeOp) { + if (auto materialize = + dyn_cast(storeOp)) { + return materialize.getDest(); + } + if (auto hivmStore = dyn_cast(storeOp)) { + return hivmStore.getDst(); + } + return Value(); +} + +static bool isViewOrExtractSliceOp(Operation *op) { + return isa(op) || isa(op); +} + +static Value getViewSourceValue(Operation *viewOp) { + if (auto viewLike = dyn_cast(viewOp)) { + return viewLike.getViewSource(); + } + if (auto extract = dyn_cast(viewOp)) { + return extract.getSource(); + } + return Value(); +} + +static Operation *traceProducerOp(Operation *storeOp, + SmallVector &dataViewOps) { + Value cur = getStoreSource(storeOp); + while (Operation *defOp = cur.getDefiningOp()) { + // view-like op (incl. tensor.extract_slice): transparent, pierce through + if (isViewOrExtractSliceOp(defOp)) { + dataViewOps.push_back(defOp); + cur = getViewSourceValue(defOp); + continue; + } + // scalar-producing op: skip + if (CVPipeline::isScalarLike(cur)) { + return nullptr; // producer is a scalar-like op + } + // hit: a real compute op; only unifiable if it has the same core type as + // storeOp + if (CVPipeline::getOpCoreType(defOp) != + CVPipeline::getOpCoreType(storeOp)) { + LOG_DEBUG("Producer op is not VECTOR_ONLY: " << *defOp << "\n"); + return nullptr; + } + LOG_DEBUG("Find producer op: " << *defOp); + return defOp; + } + LOG_DEBUG("Could not find producer op!"); + return nullptr; // reached a block argument (iter_arg / gm arg) +} + +/** + * @brief Collect view ops (data chain + dest chain) to unify. + * + * All view ops reachable from @p dataViewOps and the dest chain are appended + * to @p ops. Duplicates are skipped via @p seen. + */ +static void collectViewOpsToUnify(SmallVector &ops, + Operation *storeOp, + const SmallVector &dataViewOps, + SmallPtrSet &seen) { + auto addIfMatch = [&](Operation *op) { + if (seen.insert(op).second && op->getBlock() == storeOp->getBlock()) { + ops.push_back(op); + } + }; + + for (Operation *viewOp : dataViewOps) { + addIfMatch(viewOp); + } + + // dest-chain view ops: walk up the dest memref's view chain. + Value cur = getStoreDest(storeOp); + while (Operation *defOp = cur.getDefiningOp()) { + if (!isViewOrExtractSliceOp(defOp)) { + break; + } + addIfMatch(defOp); + cur = getViewSourceValue(defOp); + } +} + +/** + * @brief Recursively collect scalar-like dependencies within the same big + * block. + * + * For every scalar-like operand of each op in @p ops (and of @p storeOp's + * control-flow ancestor ops, e.g. scf.for lb/ub/step), append its defining op + * only if it lives in the same big block as @p storeOp. Scalars defined in + * outer blocks (e.g. func-level constants) are skipped, because moving them + * across blocks is unsafe and they are typically shared by many blocks. + */ +static void collectScalarDeps(SmallVector &ops, Operation *storeOp, + SmallPtrSet &seen) { + SmallVector worklist(ops.begin(), ops.end()); + + for (Operation *p = storeOp->getParentOp(); p && !isa(p); + p = p->getParentOp()) { + if (isa(p) || isa(p)) { + worklist.push_back(p); + } + } + + while (!worklist.empty()) { + Operation *cur = worklist.pop_back_val(); + for (Value operand : cur->getOperands()) { + // only scalar-like operands are collected; non-scalar operands + // belong to data/dest chains or external blocks + if (!CVPipeline::isScalarLike(operand)) { + continue; + } + Operation *defOp = operand.getDefiningOp(); + if (!defOp) { + continue; // block argument, no block_id to change + } + // only collect scalar ops in the same big block as storeOp; + // outer-block scalars (e.g. func-level constants) are skipped. + if (defOp->getBlock() != storeOp->getBlock()) { + continue; + } + if (!seen.insert(defOp).second) { + continue; + } + ops.push_back(defOp); + worklist.push_back(defOp); // recurse on its operands + } + } +} + +/** + * @brief Assemble the full set of ops whose block_id should be unified. + * + * opsToUnify = {store} + viewOps (data + dest) + + * scalarDeps (recursively collected). + */ +static SmallVector +collectOpsToUnify(Operation *storeOp, + const SmallVector &dataViewOps, + CVPipeline::ComputeBlockIdManager &bm) { + SmallVector opsToUnify; + opsToUnify.push_back(storeOp); + + SmallPtrSet seen; + seen.insert(storeOp); + + collectViewOpsToUnify(opsToUnify, storeOp, dataViewOps, seen); + collectScalarDeps(opsToUnify, storeOp, seen); + + for (Operation *op : opsToUnify) { + LOG_DEBUG("opToUnify: " << *op); + } + return opsToUnify; +} + +/** + * @brief Match a store pattern: trace producer, collect ops to unify. + * + * @return true if the pattern is matched and ready for apply, false to skip. + * + * @note producer is inserted as the first element of @p matchedOps (required + * by cloneScalarOpsForCrossBlockUses to identify the target block), + * followed by the store op, view ops, and scalar dependencies. + */ +static bool matchStorePattern(Operation *storeOp, + CVPipeline::ComputeBlockIdManager &bm, + SetVector &matchedOps) { + LOG_DEBUG("Start from storeOp: " << *storeOp); + int storeBlockId = bm.getBlockIdByOp(storeOp); + if (storeBlockId == -1) { + LOG_DEBUG("storeOp has no block_id, cannot unify! "); + return false; + } + + // Step 1: trace producer op from store.source, skipping views + scalars, + // and collect all viewops in the data chain. + SmallVector dataViewOps; + Operation *producer = traceProducerOp(storeOp, dataViewOps); + if (!producer) { + return false; // block argument / fully-scalar chain / core_type mismatch -> + // skip + } + + if (bm.getBlockIdByOp(producer) == -1 || + producer->getBlock() != storeOp->getBlock()) { + LOG_DEBUG("ProducerOp has no block_id or not in the same block as storeOp, " + "cannot unify! "); + return false; // producer has no block_id, cannot unify + } + + // Step 2: build matchedOps = {producer} + {store} + viewOps + scalarDeps. + // producer first is required by cloneScalarOpsForCrossBlockUses. + matchedOps.insert(producer); + SmallVector opsToUnify = + collectOpsToUnify(storeOp, dataViewOps, bm); + matchedOps.insert(opsToUnify.begin(), opsToUnify.end()); + return true; +} + +/** + * @brief Apply store unify: cycle detection + block_id update. + * + * @return true on success, false if cycle detected (pattern is skipped). + */ +static bool applyStoreUnify(const SetVector &matchedOps, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + Operation *producer = matchedOps[0]; + int targetBlockId = bm.getBlockIdByOp(producer); + + SmallVector opsToUnifyList(matchedOps.begin(), matchedOps.end()); + if (CVPipeline::willCreateCycle(opsToUnifyList, memGraph, targetBlockId, + bm)) { + LOG_DEBUG("Cycle detected, cannot unify storeOp to producer!\n"); + return false; + } + + for (Operation *op : matchedOps) { + bm.updateBlockId(op, targetBlockId); + } + LOG_DEBUG("Successfully unify storeOps\n"); + return true; +} + +} // anonymous namespace + +class UnifyStoreBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(UnifyStoreBlockPass) + + UnifyStoreBlockPass() = default; + + StringRef getArgument() const override { return "unify-store-block"; } + + StringRef getDescription() const override { + return "Merge store-semantic operations into the producer vector compute " + "block"; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + LOG_DEBUG("Before UnifyStoreBlock: " << *module); + + auto &aa = getAnalysis(); + CVPipeline::MemoryDependenceGraph memGraph(module, aa); + auto bm = CVPipeline::ComputeBlockIdManager(module); + + // Phase 1: collect all matched store patterns (read-only, no modification). + // Only VECTOR-core stores are candidates. + SmallVector storeOps; + module.walk([&](Operation *op) { + if (isStoreOp(op) && + CVPipeline::getOpCoreType(op) == CVPipeline::CoreType::VECTOR_ONLY) { + storeOps.push_back(op); + } + }); + + for (auto op : storeOps) { + SetVector matchedOps; + // Phase 1: collect all matched store patterns (read-only, no + // modification). + // Only VECTOR-core stores are candidates. + if (matchStorePattern(op, bm, matchedOps)) { + // Phase 2: clone scalar ops shared between a pattern and other blocks, + // so that moving pattern ops into the producer's block_id does + // not create cross-block dependency cycles. + // In order to avoid cycle, clone scalar-like ops. + // A -> D + // ↘ ↗ + // B -> C + // Now we want to fuse D to A, so clone C' scalarOps for + // D dependencies to avoid cycle. + LOG_DEBUG("Producer op = " << *matchedOps[0]); + CVPipeline::cloneScalarOpsForCrossBlockUses( + bm, matchedOps, bm.getBlockIdByOp(matchedOps[0])); + // Phase 3: for each pattern, cycle detection + block_id update. + // Rebuild bm because cloning may have added new ops. + if (!applyStoreUnify(matchedOps, memGraph, bm)) { + for (Operation *op : matchedOps) { + LOG_DEBUG("Cannot set block id for ops: " << *op); + } + } + } + } + + LOG_DEBUG("After UnifyStoreBlock: " << *module); + } +}; + +namespace mlir { +namespace triton { + +std::unique_ptr> createUnifyStoreBlockPass() { + return std::make_unique(); +} + +void registerUnifyStoreBlockPass() { + PassRegistration reg; +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOptPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOptPass.cpp new file mode 100644 index 0000000000..4bb72e1758 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/ComputeBlockOptPass.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/ComputeBlockOptPass.h" +#include "DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "DynamicCVPipeline/PlanComputeBlock/Passes.h" +#include "DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/MergeCubeBlockPass.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlockPass.h" + +#include "mlir/Pass/PassManager.h" + +using namespace mlir; +using namespace triton; + +void ComputeBlockOptPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + OpPassManager pm(module.getOperationName()); + + /** + First, perform UnifyAllocBlock to merge load semantic operations into a + unified block. Then, use UBUsageOpt to find the smallest UB dependency + location and divide the computation blocks. + */ + pm.addPass(createUnifyAllocBlockPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + + pm.addPass(createMergeVectorIfBlockPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + + pm.addPass(createExpLoadPatternPass()); + pm.addPass(createUBUsageOptPass()); + pm.addPass(createBroadcastUBOptPass()); + pm.addPass(createPosMaskPatternPass()); + pm.addPass(createMergeSameSourceAxisPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + pm.addPass(createMergeSmallBlockPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + + pm.addPass(createSinkI1ProducersIntoUsersPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + + pm.addPass(createFixpipeOptPass()); + pm.addPass(createSplitIfByBlockIdPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + pm.addPass(createMoveLoadIntoUserPass()); + pm.addPass(createUnifyStoreBlockPass()); + pm.addPass(createExpSubfPatternPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + pm.addPass(createMergeSmallBlockPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + + pm.addPass(createMergeComputeBlockPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + + pm.addPass(createMergeCubeBlockPass()); + pm.addPass(createMergeInputInitSharedCubeBlockPass()); + pm.addPass(createReorderOpsByBlockIdPass()); + pm.addPass(createRelocateMemrefDeclPass()); + if (failed(runPipeline(pm, module))) { + if (!CVPipeline::hasFallbackAttr(module)) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + return; + } +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createComputeBlockOptPass() { + return std::make_unique(); +} + +void registerComputeBlockOptPasses() { + registerPass([]() -> std::unique_ptr { + return createComputeBlockOptPass(); + }); + registerPass(createUBUsageOptPass); + registerPass(createMergeSameSourceAxisPass); + registerPass(createUnifyAllocBlockPass); + registerPass(createMergeVectorIfBlockPass); + registerPass(createFixpipeOptPass); + registerPass(createUnifyStoreBlockPass); + registerPass(createExpSubfPatternPass); + registerPass(createExpLoadPatternPass); + registerPass(createSinkI1ProducersIntoUsersPass); + registerPass(createBroadcastUBOptPass); + registerPass(createMoveLoadIntoUserPass); + registerPass(createPosMaskPatternPass); + registerPass(createMergeSmallBlockPass); + registerPass(createSplitIfByBlockIdPass); + registerPass(createMergeComputeBlockPass); + registerPass(createMergeInputInitSharedCubeBlockPass); + registerPass(createMergeCubeBlockPass); + registerPass(createRelocateMemrefDeclPass); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory.cpp b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory.cpp new file mode 100644 index 0000000000..a03d91addd --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadPass.h" +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoistingPass.h" +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/DecoupleComputeAndMemoryPass.h" +#include "mlir/Pass/PassManager.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "decouple-compute-and-memory"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(X) LLVM_DEBUG(DBGS() << (X) << "\n") + +using namespace mlir; +using namespace triton; + +void DecoupleComputeAndMemoryPass::runOnOperation() { + ModuleOp module = getOperation(); + + int depth = BufferCountManager(module).getBufferCountByType( + BufferCountManager::DepType::LoadStore); + + if (depth <= 1) { + LDBG("Buffer depth <= 1, skip multi-buffer transformation"); + return; + } + + OpPassManager pm(module.getOperationName()); + LDBG("Enter DecoupleComputeAndMemory pass"); + + // Step 1: Hoist memory operations out of compute blocks + pm.addPass(createAsyncLoadHoistingPass()); + + // Step 2: Apply multi-buffering to memory operations + pm.addPass(createAddMultiBufferToGMLoadPass()); + + if (failed(runPipeline(pm, module))) { + module->emitError() << "[" << DEBUG_TYPE << "] Pass failed!"; + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + + LDBG("Process successfully"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createDecoupleComputeAndMemoryPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoad.cpp b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoad.cpp new file mode 100644 index 0000000000..0295c49ec4 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoad.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadInternal.h" +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadPass.h" + +using namespace mlir; +using namespace triton; +using namespace gmload; + +// ============================================================================ +// Step functions +// ============================================================================ + +void AddMultiBufferToGMLoadPass::collectAndGroupMarkedOps() { + auto module = getOperation(); + + // Scan all IR for marked ops (generic, no loop dependency). + markedOps_ = collectMarkedOps(module); + if (markedOps_.empty()) { + LOG_DEBUG("No marked loads found, nothing to transform\n"); + return; + } + LOG_DEBUG("Marked loads collected, start transformation\n"); + + // Group by enclosing scf::ForOp. + groupByEnclosingForOp(markedOps_, contexts_); + + // Apply depth policy: skip loops whose compile-time trip count is too small + // to benefit, then record the slot count on each group. + int depth = BufferCountManager(module).getBufferCountByType( + BufferCountManager::DepType::LoadStore); + llvm::erase_if(contexts_, [depth](const ForBufferCtx &context) { + if (auto tripCount = getConstantTripCount(context.forOp)) + return *tripCount <= depth; + return false; + }); + for (auto &context : contexts_) + for (auto &group : context.groups) + group.depth = depth; + + if (contexts_.empty()) + LOG_DEBUG("No bufferable loops found\n"); +} + +void AddMultiBufferToGMLoadPass::sortContextsInnerFirst() { + // Sort contexts inner-first so that inner loops are transformed + // before the outer loops that contain them. + auto getNestingDepth = [](Operation *loopOp) { + unsigned nestingDepth = 0; + for (Operation *parentOp = loopOp->getParentOp(); parentOp; + parentOp = parentOp->getParentOp()) + ++nestingDepth; + return nestingDepth; + }; + + llvm::sort(contexts_, [&getNestingDepth](const ForBufferCtx &leftContext, + const ForBufferCtx &rightContext) { + Operation *leftLoopOp = + const_cast(leftContext.forOp).getOperation(); + Operation *rightLoopOp = + const_cast(rightContext.forOp).getOperation(); + unsigned leftDepth = getNestingDepth(leftLoopOp); + unsigned rightDepth = getNestingDepth(rightLoopOp); + if (leftDepth != rightDepth) + return leftDepth > rightDepth; + if (leftLoopOp->getBlock() == rightLoopOp->getBlock()) + return leftLoopOp->isBeforeInBlock(rightLoopOp); + return false; + }); +} + +LogicalResult AddMultiBufferToGMLoadPass::applyMultiBufferToGMLoadLoops() { + for (auto &context : contexts_) + allCtxForOps_.insert(context.forOp.getOperation()); + + // Process inner loops before outer loops. + for (auto &context : contexts_) + if (failed(applyMultiBufferToForLoop(context, allCtxForOps_))) + return failure(); + + return success(); +} + +void AddMultiBufferToGMLoadPass::cleanupTransformedIR() { + auto module = getOperation(); + + // Deduplicate untagged constants that inner-loop processing created + // before outer-loop processing could provide the dominating equivalents. + deduplicateConstants(module); + + // Erase replaced original for ops. + llvm::DenseSet nestedForOps; + for (auto &context : contexts_) { + Operation *parentOp = context.forOp->getParentOp(); + while (parentOp) { + if (allCtxForOps_.contains(parentOp)) { + nestedForOps.insert(context.forOp.getOperation()); + break; + } + parentOp = parentOp->getParentOp(); + } + } + + for (auto &context : llvm::reverse(contexts_)) { + if (nestedForOps.contains(context.forOp.getOperation())) + continue; + context.forOp.erase(); + } +} + +// ============================================================================ +// Pass entry point +// ============================================================================ + +void AddMultiBufferToGMLoadPass::runOnOperation() { + auto module = getOperation(); + LOG_DEBUG("Enter add-multi-buffer-to-gm-load pass\n"); + LOG_DEBUG("Before add-multi-buffer-to-gm-load:\n" << module << "\n"); + + // Step 1: Collect marked ops and group by enclosing forOp + markedOps_.clear(); + contexts_.clear(); + collectAndGroupMarkedOps(); + if (contexts_.empty()) + return; + + // Step 2: Sort contexts inner-first + sortContextsInnerFirst(); + + // Step 3: Transform each for loop with multi-buffer logic + allCtxForOps_.clear(); + if (failed(applyMultiBufferToGMLoadLoops())) { + module.emitError() << "[" << DEBUG_TYPE + << "] Step 3 applyMultiBufferToGMLoadLoops failed"; + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 4: Cleanup transformed IR + cleanupTransformedIR(); + + LOG_DEBUG("After add-multi-buffer-to-gm-load:\n" << module << "\n"); + LOG_DEBUG("Process successfully\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createAddMultiBufferToGMLoadPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoisting.cpp b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoisting.cpp new file mode 100644 index 0000000000..af8e6028e0 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoisting.cpp @@ -0,0 +1,373 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AsyncLoadHoistingPass.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Pass/Pass.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "async-load-hoisting" + +using namespace mlir; +using namespace triton; + +namespace mlir { +namespace triton { + +// Check if op is a mask processing operation (broadcast, cmpf, select, etc.) +// Mask values are not directly fed into Cube +static bool isMaskProcessingOp(Operation *op) { + return mlir::isa(op); +} + +// Recursively check if all downstream users of op are mask processing +// operations +static bool +isOnlyUsedByMaskProcessingOpsRecursively(Operation *op, + llvm::DenseSet &visited) { + if (visited.count(op)) { + return true; // Already visited + } + visited.insert(op); + + if (op->getNumResults() == 0) { + return false; + } + + bool hasAnyUsers = false; + for (auto result : op->getResults()) { + for (auto *user : result.getUsers()) { + hasAnyUsers = true; + if (!isMaskProcessingOp(user) && + !isOnlyUsedByMaskProcessingOpsRecursively(user, visited)) { + return false; + } + } + } + if (!hasAnyUsers) { + return false; + } + return true; +} + +// Check if all downstream users of op are mask processing operations +static bool isOnlyUsedByMaskProcessingOps(Operation *op) { + llvm::DenseSet visited; + return isOnlyUsedByMaskProcessingOpsRecursively(op, visited); +} + +static bool isLoadOp(Operation *op) { + auto effectInterface = dyn_cast(op); + if (!effectInterface) { + return false; + } + + // Skip memref.copy (has exactly 2 operands: src and dst) + if (mlir::isa(op)) { + return false; + } + + // Skip memref.load with all scalar indices + if (mlir::isa(op)) { + bool allScalarIndices = true; + for (size_t i = 1; i < op->getNumOperands(); ++i) { + auto *defOp = op->getOperand(i).getDefiningOp(); + if (!defOp || !llvm::isa(defOp)) { + allScalarIndices = false; + break; + } + } + if (allScalarIndices && op->getNumOperands() > 1) { + return false; + } + } + + if (isOnlyUsedByMaskProcessingOps(op)) { + return false; + } + + return effectInterface.hasEffect(); +} + +// Check if a Value's computation chain is linear and predictable. +// Returns false if the chain contains remsi (modulo), load (indirect +// addressing), or complex loops. +static bool isLinearAndPredictable(mlir::Value value, + llvm::DenseSet &visited) { + if (value.getDefiningOp()) + return true; + if (mlir::isa(value)) + return true; + if (!visited.insert(value).second) + return true; + + mlir::Operation *defOp = value.getDefiningOp(); + if (!defOp) + return true; + + // Block nonlinear ops: memref.load (indirect addressing), scf.while/for + // (complex control flow) + if (mlir::isa( + defOp)) { + return false; + } + + // Recursively check operands + for (mlir::Value operand : defOp->getOperands()) { + mlir::Operation *operandDefOp = operand.getDefiningOp(); + if (!operandDefOp || + operandDefOp->getParentRegion() != defOp->getParentRegion()) { + return true; // Cross-region boundary, treat as linear + } + if (!isLinearAndPredictable(operand, visited)) { + return false; + } + } + + return true; +} + +// Check if all reinterpret_cast offsets in chain are linearly predictable +static bool +filterNonLinearReinterpretCast(const std::vector &chain) { + for (mlir::Operation *op : chain) { + auto castOp = llvm::dyn_cast(op); + if (!castOp) + continue; + + llvm::DenseSet visited; + + for (auto offset : castOp.getOffsets()) { + if (!isLinearAndPredictable(offset, visited)) { + return false; + } + } + } + return true; +} + +// Get ssbuffer.block_id attribute value, returns -1 if not present +static int64_t getBlockId(Operation *op) { + if (auto blockIdAttr = op->getAttrOfType("ssbuffer.block_id")) { + return blockIdAttr.getInt(); + } + return -1; +} + +static void collectAddressGenerationOps(Operation *op, int64_t expectedBlockId, + llvm::DenseSet &visited, + std::vector &chain) { + for (auto operand : op->getOperands()) { + auto *defOp = operand.getDefiningOp(); + + if (!defOp || visited.count(defOp) || llvm::isa(defOp)) { + continue; + } + + if (isLoadOp(defOp)) + continue; + + if (mlir::isa(operand.getType())) { + for (auto *user : operand.getUsers()) { + if (auto copyOp = llvm::dyn_cast(user); + copyOp && copyOp.getTarget() == operand && + visited.insert(copyOp).second) { + chain.push_back(copyOp); + collectAddressGenerationOps(copyOp, expectedBlockId, visited, chain); + continue; + } + if (mlir::isa(user) && visited.insert(user).second) { + chain.push_back(user); + for (auto result : user->getResults()) { + for (auto *subviewUser : result.getUsers()) { + if (auto copyOp = llvm::dyn_cast(subviewUser); + visited.insert(copyOp).second) { + chain.push_back(copyOp); + collectAddressGenerationOps(copyOp, expectedBlockId, visited, + chain); + } + } + } + collectAddressGenerationOps(user, expectedBlockId, visited, chain); + } + } + } + + visited.insert(defOp); + chain.push_back(defOp); + + collectAddressGenerationOps(defOp, expectedBlockId, visited, chain); + } +} +// Returns full chain and filtered chain (only ops with same block_id) +static std::pair, std::vector> +getAddressGenerationChainWithFilter(Operation *loadOp) { + std::vector chain; + chain.push_back(loadOp); + + llvm::DenseSet visitedOp; + visitedOp.insert(loadOp); + + int64_t expectedBlockId = getBlockId(loadOp); + + collectAddressGenerationOps(loadOp, expectedBlockId, visitedOp, chain); + + if (chain.size() > 1) { + auto *block = loadOp->getBlock(); + auto sortStartIter = chain.begin() + 1; + std::stable_sort(sortStartIter, chain.end(), + [block](Operation *a, Operation *b) { + for (auto &op : *block) { + if (&op == a) + return true; + if (&op == b) + return false; + } + return false; + }); + auto loadOpIter = chain.begin(); + std::rotate(loadOpIter, loadOpIter + 1, chain.end()); + } + + std::vector filteredChain; + for (Operation *op : chain) { + if (getBlockId(op) == expectedBlockId) { + filteredChain.push_back(op); + } + } + + return {chain, filteredChain}; +} + +// Check if chain contains any block argument +static bool chainContainsBlockArg(Operation *loadOp) { + auto [fullChain, filteredChain] = getAddressGenerationChainWithFilter(loadOp); + + for (Operation *op : fullChain) { + for (auto operand : op->getOperands()) { + if (!operand.getDefiningOp()) { + return true; + } + } + } + return false; +} + +// Process all load ops with the same block_id +static void scanAndHoistBlock(llvm::SmallVector &cache) { + for (Operation *opPtr : cache) { + bool isLoad = isLoadOp(opPtr); + bool hasBlockArg = chainContainsBlockArg(opPtr); + if (isLoad) { + auto [fullChain, filteredChain] = + getAddressGenerationChainWithFilter(opPtr); + (void)fullChain; + bool passNonLinear = filterNonLinearReinterpretCast(filteredChain); + if (passNonLinear && hasBlockArg) { + opPtr->setAttr("gm_load_bufferable", + UnitAttr::get(opPtr->getContext())); + } + } + } +} + +// Traverse all ops in region, grouped by ssbuffer.block_id +static void asyncLoadHoistingImpl(Region ®ion) { + llvm::SmallVector cache; + + for (Block &block : llvm::make_early_inc_range(region)) { + for (Operation &op : llvm::make_early_inc_range(block)) { + int64_t opBlockId = getBlockId(&op); + + if (op.getNumRegions() > 0) { + for (Region &childRegion : op.getRegions()) { + if (childRegion.empty()) + continue; + asyncLoadHoistingImpl(childRegion); + } + continue; + } + + if (cache.empty()) { + cache.push_back(&op); + continue; + } + + int64_t cacheBlockId = getBlockId(cache.front()); + if (opBlockId == cacheBlockId && opBlockId != -1) { + cache.push_back(&op); + } else { + scanAndHoistBlock(cache); + cache.clear(); + cache.push_back(&op); + } + } + } + + if (!cache.empty()) { + scanAndHoistBlock(cache); + cache.clear(); + } +} + +} // namespace triton +} // namespace mlir + +void AsyncLoadHoistingPass::runOnOperation() { + auto module = getOperation(); + + LLVM_DEBUG({ + llvm::dbgs() << "[async-load-hoisting] Before AsyncLoadHoistingPass:\n" + << module << "\n"; + }); + + module->walk( + [&](func::FuncOp func) { asyncLoadHoistingImpl(func.getBody()); }); + + LLVM_DEBUG({ + llvm::dbgs() << "[async-load-hoisting] After AsyncLoadHoistingPass:\n" + << module << "\n"; + }); +} + +std::unique_ptr> +mlir::triton::createAsyncLoadHoistingPass() { + return std::make_unique(); +} + +void mlir::triton::registerAsyncLoadHoistingPasses() { + registerPass([]() -> std::unique_ptr { + return createAsyncLoadHoistingPass(); + }); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/DependencyAnalysis.cpp b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/DependencyAnalysis.cpp new file mode 100644 index 0000000000..16aae6f1fe --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/DependencyAnalysis.cpp @@ -0,0 +1,393 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadInternal.h" + +namespace gmload { + +// ============================================================================ +// Analysis: dependency chain computation +// ============================================================================ + +/// Given an op that may be nested inside a region (e.g., inside scf.if), +/// walk up the parent chain and return the ancestor op that lives directly +/// in `scopeBlock`. Returns nullptr if no such ancestor exists. +Operation *getAncestorInBlock(Operation *nestedOp, Block *scopeBlock) { + while (nestedOp) { + if (nestedOp->getBlock() == scopeBlock) + return nestedOp; + nestedOp = nestedOp->getParentOp(); + } + return nullptr; +} + +/// Add `op` and all its transitive SSA operand-producers within `scopeBlock` +/// to `visited`. Newly discovered ops are also appended to `worklist`. +void backwardTrace(Operation *sourceOp, Block *scopeBlock, + llvm::SetVector &visited, + SmallVectorImpl &worklist) { + for (Value operand : sourceOp->getOperands()) { + auto *defOp = operand.getDefiningOp(); + if (!defOp || defOp->getBlock() != scopeBlock) + continue; + if (visited.insert(defOp)) + worklist.push_back(defOp); + } +} + +/// Process worklist with backward SSA trace until empty. +/// Used in Phase 1 and Phase 3 of computeLoadChain. +void backwardTraceFromWorklist(llvm::SetVector &visited, + SmallVectorImpl &worklist, + Block *scopeBlock) { + while (!worklist.empty()) { + Operation *currentOp = worklist.pop_back_val(); + backwardTrace(currentOp, scopeBlock, visited, worklist); + } +} + +/// Forward trace from every memref::AllocOp in visited set. +/// Returns newly discovered ops that use alloc results. +/// Used in Phase 2 of computeLoadChain. +SmallVector +forwardTraceFromAllocs(llvm::SetVector &visited, + Block *scopeBlock) { + SmallVector valueWorklist; + for (Operation *currentOp : visited) { + if (isa(currentOp)) { + for (Value result : currentOp->getResults()) + valueWorklist.push_back(result); + } + } + + SmallVector newlyDiscoveredOps; + while (!valueWorklist.empty()) { + Value currentValue = valueWorklist.pop_back_val(); + for (Operation *user : currentValue.getUsers()) { + Operation *ancestor = getAncestorInBlock(user, scopeBlock); + if (!ancestor) + continue; + if (!visited.insert(ancestor)) + continue; + newlyDiscoveredOps.push_back(ancestor); + for (Value result : ancestor->getResults()) + valueWorklist.push_back(result); + } + } + return newlyDiscoveredOps; +} + +/// Capture free variables used in regions of ops in visited set. +/// Performs backward trace for each discovered free variable definition. +/// Used in Phase 4 of computeLoadChain. +void captureRegionFreeVars(llvm::SetVector &visited, + SmallVectorImpl &worklist, + Block *scopeBlock) { + SmallVector freeVarWorklist; + SmallVector initialChain(visited.begin(), visited.end()); + + // First pass: collect free variables from all region-containing ops + for (Operation *chainOp : initialChain) { + if (chainOp->getNumRegions() == 0) + continue; + chainOp->walk([&](Operation *nested) { + if (nested == chainOp) + return; + for (Value operand : nested->getOperands()) { + auto *defOp = operand.getDefiningOp(); + if (!defOp || defOp->getBlock() != scopeBlock) + continue; + if (visited.insert(defOp)) + freeVarWorklist.push_back(defOp); + } + }); + } + + // Second pass: backward trace and recursively capture nested region free vars + worklist.assign(freeVarWorklist.begin(), freeVarWorklist.end()); + while (!worklist.empty()) { + Operation *currentOp = worklist.pop_back_val(); + backwardTrace(currentOp, scopeBlock, visited, worklist); + if (currentOp->getNumRegions() == 0) + continue; + currentOp->walk([&](Operation *nested) { + if (nested == currentOp) + return; + for (Value operand : nested->getOperands()) { + auto *defOp = operand.getDefiningOp(); + if (!defOp || defOp->getBlock() != scopeBlock) + continue; + if (visited.insert(defOp)) { + freeVarWorklist.push_back(defOp); + worklist.push_back(defOp); + } + } + }); + } +} + +/// Compute the full dependency chain of `markedOp` within `scopeBlock`. +/// Phase 1 Backward SSA trace from markedOp. +/// Phase 2 Forward trace from every memref::AllocOp found in Phase 1. +/// Phase 3 Backward SSA trace from newly discovered ops. +/// Phase 4 Capture free variables of chain-op regions. +SmallVector computeLoadChain(Operation *markedOp, + Block *scopeBlock) { + llvm::SetVector visited; + SmallVector worklist; + + // Phase 1: backward SSA trace from markedOp + visited.insert(markedOp); + worklist.push_back(markedOp); + backwardTraceFromWorklist(visited, worklist, scopeBlock); + + // Phase 2: forward trace from every memref.alloc in the chain + SmallVector newlyDiscoveredOps = + forwardTraceFromAllocs(visited, scopeBlock); + + // Phase 3: backward SSA trace from newly discovered ops + worklist.assign(newlyDiscoveredOps.begin(), newlyDiscoveredOps.end()); + backwardTraceFromWorklist(visited, worklist, scopeBlock); + + // Phase 4: capture free variables of chain-op regions + captureRegionFreeVars(visited, worklist, scopeBlock); + + // Sort by block position (topological order) + SmallVector sorted(visited.begin(), visited.end()); + llvm::sort(sorted, [](Operation *leftOp, Operation *rightOp) { + return leftOp->isBeforeInBlock(rightOp); + }); + return sorted; +} + +/// Find the memref::AllocOp in the chain that backs the marked op. +memref::AllocOp findBackingAlloc(Operation *markedOp, + ArrayRef chain) { + for (Operation *chainOp : chain) { + if (auto alloc = dyn_cast(chainOp)) { + if (markedOp->getNumOperands() > 0 && + markedOp->getOperand(0).getDefiningOp() == alloc) + return alloc; + } + } + for (Operation *chainOp : chain) { + if (auto alloc = dyn_cast(chainOp)) + return alloc; + } + return nullptr; +} + +// ============================================================================ +// Step 1a: Generic marked-op collection (scan all IR) +// ============================================================================ + +/// Scan the entire module IR for ops carrying the `kBufferableMarker` +/// attribute and compute the dependency chain for each. +SmallVector collectMarkedOps(ModuleOp module) { + SmallVector results; + + module->walk([&](Operation *op) { + if (!op->hasAttr(kBufferableMarker)) + return; + if (op->getNumResults() == 0) + return; + + Block *scope = op->getBlock(); + auto chain = computeLoadChain(op, scope); + + auto alloc = findBackingAlloc(op, chain); + if (!alloc) + return; + + results.push_back({op, std::move(chain), alloc}); + }); + + return results; +} + +// ============================================================================ +// Step 1b: Group by enclosing loop construct +// ============================================================================ + +/// Try to determine the loop trip count at compile time. +std::optional getConstantTripCount(scf::ForOp forOp) { + auto lowerBoundConst = + forOp.getLowerBound().getDefiningOp(); + auto upperBoundConst = + forOp.getUpperBound().getDefiningOp(); + auto stepConst = forOp.getStep().getDefiningOp(); + if (!lowerBoundConst || !upperBoundConst || !stepConst) + return std::nullopt; + + auto lowerBoundAttr = dyn_cast(lowerBoundConst.getValue()); + auto upperBoundAttr = dyn_cast(upperBoundConst.getValue()); + auto stepAttr = dyn_cast(stepConst.getValue()); + if (!lowerBoundAttr || !upperBoundAttr || !stepAttr) + return std::nullopt; + + int64_t lowerBoundValue = lowerBoundAttr.getInt(); + int64_t upperBoundValue = upperBoundAttr.getInt(); + int64_t stepValue = stepAttr.getInt(); + if (stepValue < 1) + return std::nullopt; + + // stepValue >= 1 guaranteed by check above + int64_t tripCount = + (upperBoundValue - lowerBoundValue + stepValue - 1) / stepValue; + return tripCount > 0 ? std::optional(tripCount) : std::nullopt; +} + +/// Group marked loads by their nearest enclosing scf::ForOp. +/// Each marked load becomes its own independent LoadGroup so that every GM +/// load has its own producer flags, counters, and slot targets. Shared +/// address-computation ops are handled correctly by the consumer skip analysis +/// without forcing multiple loads into the same multi-buffer state machine. +void groupByEnclosingForOp(SmallVector &markedOps, + SmallVectorImpl &contexts) { + // Bucket loads by the nearest enclosing for loop. + llvm::DenseMap> forBuckets; + SmallVector forOpsInOrder; + for (size_t markedIndex = 0; markedIndex < markedOps.size(); ++markedIndex) { + auto forOp = markedOps[markedIndex].markedOp->getParentOfType(); + if (!forOp) + continue; + auto [bucketIt, inserted] = forBuckets.try_emplace(forOp.getOperation()); + if (inserted) + forOpsInOrder.push_back(forOp); + bucketIt->second.push_back(markedIndex); + } + + // Each marked load becomes its own independent LoadGroup. + for (auto forOp : forOpsInOrder) { + auto &indices = forBuckets[forOp.getOperation()]; + + ForBufferCtx context; + context.forOp = forOp; + for (size_t markedIndex : indices) { + LoadGroup group; + group.loads.push_back(std::move(markedOps[markedIndex])); + if (!group.loads.empty()) + group.mergedChain = group.loads[0].chain; + context.groups.push_back(std::move(group)); + } + contexts.push_back(std::move(context)); + } +} + +// ============================================================================ +// Consumer skip analysis +// ============================================================================ + +/// Collect all chain ops and marked ops from the given loads. +void collectChainAndMarkedOps(const SmallVector &loads, + llvm::DenseSet &allChainOps, + llvm::DenseSet &markedOps) { + for (const auto &markedLoad : loads) { + for (Operation *chainOp : markedLoad.chain) + allChainOps.insert(chainOp); + markedOps.insert(markedLoad.markedOp); + } +} + +/// Check if a result value is used by yield or by non-chain ops in forBody. +bool isResultUsedExternally(Value result, Operation *yieldOp, + const llvm::DenseSet &allChainOps, + Block *forBody) { + // Check if result is yielded + for (Value yieldedValue : yieldOp->getOperands()) { + if (yieldedValue == result) + return true; + } + // Check if result is used by non-chain ops + for (Operation *user : result.getUsers()) { + Operation *ancestor = getAncestorInBlock(user, forBody); + if (ancestor && !allChainOps.contains(ancestor)) + return true; + } + return false; +} + +/// Collect all operands from an op, including those from nested regions. +SmallVector +collectOperandsIncludingRegions(Operation *op) { + SmallVector refs; + for (Value operand : op->getOperands()) + refs.push_back(operand); + if (op->getNumRegions() > 0) { + op->walk([&](Operation *nested) { + if (nested == op) + return; + for (Value operand : nested->getOperands()) + refs.push_back(operand); + }); + } + return refs; +} + +/// Return the set of ops that must be skipped (not cloned) in the consumer +llvm::DenseSet +computeSkipInConsumer(const SmallVector &loads, Block *forBody) { + llvm::DenseSet allChainOps; + llvm::DenseSet markedOps; + collectChainAndMarkedOps(loads, allChainOps, markedOps); + + // Phase 1: chain ops whose results are directly used by non-chain ops or + // yielded. These must be cloned so non-chain consumers have valid SSA defs. + llvm::DenseSet needed; + Operation *yieldOp = forBody->getTerminator(); + for (Operation *chainOp : allChainOps) { + if (markedOps.contains(chainOp)) + continue; + bool isNeeded = llvm::any_of(chainOp->getResults(), [&](Value result) { + return isResultUsedExternally(result, yieldOp, allChainOps, forBody); + }); + if (isNeeded) + needed.insert(chainOp); + } + + // Phase 2: backward propagation through operands and region free variables. + SmallVector worklist(needed.begin(), needed.end()); + while (!worklist.empty()) { + Operation *currentOp = worklist.pop_back_val(); + SmallVector operands = + collectOperandsIncludingRegions(currentOp); + for (Value operand : operands) { + auto *defOp = operand.getDefiningOp(); + if (!defOp || defOp->getBlock() != forBody) + continue; + if (!allChainOps.contains(defOp) || markedOps.contains(defOp)) + continue; + if (needed.insert(defOp).second) + worklist.push_back(defOp); + } + } + + // Compute skip set as difference: allChainOps - needed + llvm::DenseSet skip; + for (Operation *chainOp : allChainOps) { + if (!needed.contains(chainOp)) + skip.insert(chainOp); + } + return skip; +} + +} // namespace gmload diff --git a/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/LoopTransform.cpp b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/LoopTransform.cpp new file mode 100644 index 0000000000..7468d9bcd4 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/LoopTransform.cpp @@ -0,0 +1,811 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadInternal.h" + +namespace gmload { + +// ============================================================================ +// Dead iter_arg elimination +// ============================================================================ + +/// Return true if the iter_arg at `iterArgIdx` in `forOp` is dead +/// (a) its loop result has no uses outside the loop, AND +/// (b) its block argument is only used in a chain of side-effect-free ops +/// whose results ultimately feed only the loop yield. +bool isDeadIterArg(scf::ForOp forOp, unsigned iterArgIdx) { + if (!forOp.getResult(iterArgIdx).use_empty()) { + return false; + } + + Value blockArg = + forOp.getBody()->getArgument(iterArgIdx + kForBodyIterArgOffset); + Block *forBody = forOp.getBody(); + Operation *yieldOp = forBody->getTerminator(); + + llvm::SmallSetVector frontier; + frontier.insert(blockArg); + + while (!frontier.empty()) { + Value currentValue = frontier.pop_back_val(); + for (OpOperand &use : currentValue.getUses()) { + Operation *user = use.getOwner(); + Operation *ancestor = getAncestorInBlock(user, forBody); + if (!ancestor) { + continue; + } + if (ancestor == yieldOp) { + continue; + } + if (!mlir::isMemoryEffectFree(ancestor)) { + return false; + } + for (Value result : ancestor->getResults()) { + frontier.insert(result); + } + } + } + return true; +} + +llvm::DenseSet collectDeadIterArgs(scf::ForOp forOp, + unsigned candidateCount) { + llvm::DenseSet deadSet; + for (unsigned iterArgIdx = 0; iterArgIdx < candidateCount; ++iterArgIdx) { + if (isDeadIterArg(forOp, iterArgIdx)) { + deadSet.insert(iterArgIdx); + } + } + return deadSet; +} + +SmallVector +collectLiveIterArgInits(scf::ForOp forOp, + const llvm::DenseSet &deadSet) { + unsigned numIter = forOp.getNumResults(); + SmallVector liveInits; + liveInits.reserve(numIter - deadSet.size()); + for (unsigned iterArgIdx = 0; iterArgIdx < numIter; ++iterArgIdx) { + if (!deadSet.contains(iterArgIdx)) { + liveInits.push_back(forOp.getInitArgs()[iterArgIdx]); + } + } + return liveInits; +} + +scf::ForOp createForWithLiveIterArgs(OpBuilder &builder, scf::ForOp forOp, + ArrayRef liveInits) { + builder.setInsertionPoint(forOp); + auto newFor = builder.create( + forOp.getLoc(), forOp.getLowerBound(), forOp.getUpperBound(), + forOp.getStep(), liveInits); + newFor->setAttrs(forOp->getAttrs()); + return newFor; +} + +IRMapping buildPrunedIterArgMapping(scf::ForOp oldForOp, scf::ForOp newForOp, + const llvm::DenseSet &deadSet) { + unsigned numIter = oldForOp.getNumResults(); + Block *oldBody = oldForOp.getBody(); + Block *newBody = newForOp.getBody(); + + IRMapping mapping; + mapping.map(oldBody->getArgument(kForInductionVarArgIndex), + newForOp.getInductionVar()); + unsigned liveArgIdx = 0; + for (unsigned iterArgIdx = 0; iterArgIdx < numIter; ++iterArgIdx) { + Value oldArg = oldBody->getArgument(iterArgIdx + kForBodyIterArgOffset); + if (deadSet.contains(iterArgIdx)) { + mapping.map(oldArg, oldForOp.getInitArgs()[iterArgIdx]); + continue; + } + mapping.map(oldArg, + newBody->getArgument(liveArgIdx++ + kForBodyIterArgOffset)); + } + return mapping; +} + +void clonePrunedForBody(OpBuilder &builder, scf::ForOp oldForOp, + scf::ForOp newForOp, IRMapping &mapping) { + builder.setInsertionPointToStart(newForOp.getBody()); + for (auto &op : oldForOp.getBody()->without_terminator()) { + builder.clone(op, mapping); + } +} + +SmallVector +collectLiveYieldOperands(scf::ForOp forOp, + const llvm::DenseSet &deadSet, + IRMapping &mapping) { + unsigned numIter = forOp.getNumResults(); + auto oldYield = cast(forOp.getBody()->getTerminator()); + SmallVector liveYield; + liveYield.reserve(numIter - deadSet.size()); + for (unsigned iterArgIdx = 0; iterArgIdx < numIter; ++iterArgIdx) { + if (!deadSet.contains(iterArgIdx)) { + liveYield.push_back( + mapping.lookupOrDefault(oldYield.getOperand(iterArgIdx))); + } + } + return liveYield; +} + +void replaceLiveForResults(scf::ForOp oldForOp, scf::ForOp newForOp, + const llvm::DenseSet &deadSet) { + unsigned numIter = oldForOp.getNumResults(); + unsigned liveArgIdx = 0; + for (unsigned iterArgIdx = 0; iterArgIdx < numIter; ++iterArgIdx) { + if (!deadSet.contains(iterArgIdx)) { + oldForOp.getResult(iterArgIdx) + .replaceAllUsesWith(newForOp.getResult(liveArgIdx++)); + } + } +} + +/// Rebuild `forOp` dropping all iter_args that satisfy `isDeadIterArg`. +/// Only the first `candidateCount` iter_args are considered for pruning. +/// Returns the pruned ForOp, or `forOp` itself when nothing was pruned. +FailureOr pruneDeadIterArgs(OpBuilder &builder, scf::ForOp forOp, + unsigned candidateCount) { + unsigned numIter = forOp.getNumResults(); + if (candidateCount > numIter) { + forOp.emitError() << "[" << DEBUG_TYPE << "] candidate iter_arg count " + << candidateCount << " exceeds total iter_arg count " + << numIter; + return failure(); + } + + llvm::DenseSet deadSet = collectDeadIterArgs(forOp, candidateCount); + if (deadSet.empty()) { + return forOp; + } + + SmallVector liveInits = collectLiveIterArgInits(forOp, deadSet); + scf::ForOp newFor = createForWithLiveIterArgs(builder, forOp, liveInits); + IRMapping mapping = buildPrunedIterArgMapping(forOp, newFor, deadSet); + + clonePrunedForBody(builder, forOp, newFor, mapping); + SmallVector liveYield = + collectLiveYieldOperands(forOp, deadSet, mapping); + builder.create(forOp.getBody()->getTerminator()->getLoc(), + liveYield); + + replaceLiveForResults(forOp, newFor, deadSet); + forOp.erase(); + return newFor; +} + +/// Erase side-effect-free ops in `body` whose results are entirely unused. +/// Propagates: erasing an op may expose its operand-defining ops as newly dead. +/// Only considers ops directly in `body` (not nested regions). +void eraseDeadBodyOps(Block *body) { + SmallVector worklist; + llvm::SmallDenseSet queued; + auto enqueueIfDead = [&body, &queued, &worklist](Operation *candidateOp) { + if (!candidateOp || candidateOp->getBlock() != body || + candidateOp == body->getTerminator()) { + return; + } + if (!queued.insert(candidateOp).second) { + return; + } + worklist.push_back(candidateOp); + }; + + for (auto &op : *body) { + if (&op == body->getTerminator()) { + continue; + } + if (mlir::isMemoryEffectFree(&op) && + llvm::all_of(op.getResults(), + [](Value result) { return result.use_empty(); })) { + enqueueIfDead(&op); + } + } + + while (!worklist.empty()) { + Operation *currentOp = worklist.pop_back_val(); + queued.erase(currentOp); + if (!mlir::isMemoryEffectFree(currentOp)) { + continue; + } + if (!llvm::all_of(currentOp->getResults(), + [](Value result) { return result.use_empty(); })) { + continue; + } + + SmallVector operandDefs; + for (Value operand : currentOp->getOperands()) { + auto *defOp = operand.getDefiningOp(); + if (defOp && defOp->getBlock() == body) { + operandDefs.push_back(defOp); + } + } + currentOp->erase(); + + for (Operation *defOp : operandDefs) { + if (mlir::isMemoryEffectFree(defOp) && + llvm::all_of(defOp->getResults(), + [](Value result) { return result.use_empty(); })) { + enqueueIfDead(defOp); + } + } + } +} + +// ============================================================================ +// Producer address projection helpers +// ============================================================================ + +/// Return true if `value` is defined outside `forOp` (i.e. loop-invariant). +bool isLoopInvariant(Value value, scf::ForOp forOp) { + if (auto blockArg = dyn_cast(value)) { + return blockArg.getOwner() != forOp.getBody(); + } + Operation *defOp = value.getDefiningOp(); + Operation *parent = defOp->getParentOp(); + while (parent) { + if (parent == forOp.getOperation()) { + return false; + } + parent = parent->getParentOp(); + } + return true; +} + +/// If the yield value for iter-arg `iterArg` is of the form +/// arith.addi(iterArg, delta) where `delta` is loop-invariant, +/// return true and set `delta`. +bool getLinearIterArgDelta(Value iterArg, Value yieldVal, scf::ForOp forOp, + Value &delta) { + auto addOp = yieldVal.getDefiningOp(); + if (!addOp) { + return false; + } + Value candidate; + if (addOp.getLhs() == iterArg) { + candidate = addOp.getRhs(); + } else if (addOp.getRhs() == iterArg) { + candidate = addOp.getLhs(); + } else { + return false; + } + if (!isLoopInvariant(candidate, forOp)) { + return false; + } + delta = candidate; + return true; +} + +Value castIndexTo(OpBuilder &builder, Location loc, Value value, + Type targetType) { + if (value.getType() == targetType) { + return value; + } + return builder.create(loc, targetType, value); +} + +Value castToIndex(OpBuilder &builder, Location loc, Value value) { + if (value.getType().isIndex()) { + return value; + } + return builder.create(loc, builder.getIndexType(), value); +} + +// ============================================================================ +// Block-id tagging helper +// ============================================================================ + +/// Set kBlockIdAttr on `value`'s defining op if both the op and the attribute +/// exist. +void tagWithBlockId(Value value, IntegerAttr blockId) { + if (blockId) { + if (auto *defOp = value.getDefiningOp()) { + defOp->setAttr(kBlockIdAttr, blockId); + } + } +} + +// ============================================================================ +// Core transformation helpers +// ============================================================================ + +/// Allocate one memref slot per (depth × load) pair before the loop. +void allocateBufferSlots(OpBuilder &builder, Location loc, scf::ForOp forOp, + LoadGroup &group) { + builder.setInsertionPoint(forOp); + int depth = group.depth; + int numLoads = static_cast(group.loads.size()); + group.bufSlots.resize(depth); + for (int slotIdx = 0; slotIdx < depth; ++slotIdx) { + group.bufSlots[slotIdx].resize(numLoads); + for (int loadIdx = 0; loadIdx < numLoads; ++loadIdx) { + auto newAlloc = builder.create( + loc, group.loads[loadIdx].allocOp.getType()); + newAlloc->setAttrs( + group.loads[loadIdx].allocOp.getOperation()->getAttrs()); + group.bufSlots[slotIdx][loadIdx] = newAlloc; + } + } +} + +/// Build a new scf.for whose iter_args cover all groups: +/// [original..., (flags_g[depth], prodCounter_g, consCounter_g) for each g]. +/// Returns metadata including per-group iter_arg handles. +ExtendedForInfo buildExtendedFor(OpBuilder &builder, Location loc, + scf::ForOp forOp, ArrayRef groups, + ConstantCache &cache) { + builder.setInsertionPoint(forOp); + size_t numOrig = forOp.getInitArgs().size(); + int depth = groups.empty() ? 0 : groups[0].depth; + + Value falseVal = cache.getFalse(builder, loc); + Value zeroIndex = cache.getIndex(builder, loc, 0); + + SmallVector inits; + inits.reserve(numOrig + + groups.size() * (depth + kLoadGroupCounterIterArgCount)); + for (Value initArg : forOp.getInitArgs()) { + inits.push_back(initArg); + } + for (auto &group : groups) { + for (int slotIdx = 0; slotIdx < group.depth; ++slotIdx) { + inits.push_back(falseVal); + } + inits.push_back(zeroIndex); // prodCounter + inits.push_back(zeroIndex); // consCounter + } + + auto newFor = + builder.create(loc, forOp.getLowerBound(), + forOp.getUpperBound(), forOp.getStep(), inits); + newFor->setAttrs(forOp->getAttrs()); + Block *oldBody = forOp.getBody(); + Block *newBody = newFor.getBody(); + + IRMapping mapping; + mapping.map(oldBody->getArgument(kForInductionVarArgIndex), + newBody->getArgument(kForInductionVarArgIndex)); + for (int initArgIdx = 0; initArgIdx < numOrig; ++initArgIdx) { + mapping.map(oldBody->getArgument(initArgIdx + kForBodyIterArgOffset), + newBody->getArgument(initArgIdx + kForBodyIterArgOffset)); + } + + if (!newBody->empty()) { + newBody->getTerminator()->erase(); + } + builder.setInsertionPointToEnd(newBody); + + SmallVector groupArgs; + size_t argOffset = numOrig; + for (auto &group : groups) { + GroupIterArgs groupIterArgs; + for (int slotIdx = 0; slotIdx < group.depth; ++slotIdx) { + groupIterArgs.flagArgs.push_back( + newBody->getArgument(kForBodyIterArgOffset + argOffset + slotIdx)); + } + groupIterArgs.prodCounter = + newBody->getArgument(kForBodyIterArgOffset + argOffset + group.depth + + kLoadGroupProducerCounterOffset); + groupIterArgs.consCounter = + newBody->getArgument(kForBodyIterArgOffset + argOffset + group.depth + + kLoadGroupConsumerCounterOffset); + argOffset += group.depth + kLoadGroupCounterIterArgCount; + groupArgs.push_back(std::move(groupIterArgs)); + } + + return {newFor, + oldBody, + newBody, + std::move(mapping), + std::move(groupArgs), + falseVal, + static_cast(numOrig), + depth}; +} + +// ============================================================================ +// Clone availability checks +// ============================================================================ + +/// Return true if `value` can be used while cloning into the new body with the +/// current mapping. Values defined outside the old body still dominate; values +/// defined in the old body must already have been cloned and mapped. +bool isAvailableForClone(Value value, Block *oldBody, + const IRMapping &mapping) { + if (auto blockArg = dyn_cast(value)) { + return blockArg.getOwner() != oldBody || + static_cast(mapping.lookupOrNull(value)); + } + + Operation *defOp = value.getDefiningOp(); + if (!defOp || defOp->getBlock() != oldBody) { + return true; + } + return static_cast(mapping.lookupOrNull(value)); +} + +/// Check all direct and region-captured operands before cloning an op out of +/// the old loop body. +bool areOperandsAvailableForClone(Operation *op, Block *oldBody, + const IRMapping &mapping) { + for (Value operand : collectOperandsIncludingRegions(op)) { + if (!isAvailableForClone(operand, oldBody, mapping)) { + return false; + } + } + return true; +} + +// ============================================================================ +// Body-run utilities +// ============================================================================ + +std::optional getBlockId(Operation *op) { + if (auto bid = op->getAttrOfType(kBlockIdAttr)) { + return static_cast(bid.getInt()); + } + return std::nullopt; +} + +bool sameRunBlockId(const BodyRun &run, std::optional bid) { + if (run.hasBlockId != bid.has_value()) { + return false; + } + return !run.hasBlockId || run.blockId == *bid; +} + +SmallVector collectBodyRuns(Block *body) { + SmallVector runs; + for (auto &op : body->without_terminator()) { + std::optional bid = getBlockId(&op); + if (runs.empty() || !sameRunBlockId(runs.back(), bid)) { + BodyRun run; + if (bid) { + run.hasBlockId = true; + run.blockId = *bid; + } + runs.push_back(std::move(run)); + } + runs.back().ops.push_back(&op); + } + return runs; +} + +int findRunContainingOp(ArrayRef runs, Operation *target) { + for (size_t runIdx = 0; runIdx < runs.size(); ++runIdx) { + for (Operation *op : runs[runIdx].ops) { + if (op == target) { + return static_cast(runIdx); + } + } + } + return -1; +} + +int findFirstRunWithBlockId(ArrayRef runs, int32_t blockId) { + for (size_t runIdx = 0; runIdx < runs.size(); ++runIdx) { + if (runs[runIdx].hasBlockId && runs[runIdx].blockId == blockId) { + return static_cast(runIdx); + } + } + return -1; +} + +void logRepeatedTopLevelBlockRuns(Block *body, llvm::StringRef stage) { + llvm::DenseSet closed; + bool hasCurrent = false; + int32_t current = 0; + + for (auto &op : body->without_terminator()) { + std::optional bid = getBlockId(&op); + if (!bid) { + if (hasCurrent) { + closed.insert(current); + } + hasCurrent = false; + continue; + } + + if (hasCurrent && current == *bid) { + continue; + } + + if (hasCurrent) { + closed.insert(current); + } + + if (closed.contains(*bid)) { + LOG_DEBUG("non-consecutive top-level ssbuffer.block_id run after " + << stage << ": block_id=" << *bid << "\n"); + } + current = *bid; + hasCurrent = true; + } +} + +// ============================================================================ +// Core transformation +// ============================================================================ + +struct LoopInvariantValues { + Value lowerBound; + Value step; + Value tripCount; +}; + +struct GroupInvariantValues { + SmallVector trueFlags; + SmallVector indexOnes; + SmallVector> slotConsts; + SmallVector depthConsts; +}; + +void allocateBufferSlotsForGroups(OpBuilder &builder, Location loc, + scf::ForOp forOp, + SmallVectorImpl &groups) { + for (auto &group : groups) { + allocateBufferSlots(builder, loc, forOp, group); + } +} + +LoopInvariantValues emitLoopInvariantValues(OpBuilder &builder, Location loc, + scf::ForOp forOp) { + IntegerAttr forBlockId = forOp->getAttrOfType(kBlockIdAttr); + Value lowerBound = forOp.getLowerBound(); + Value step = forOp.getStep(); + Value upperBound = forOp.getUpperBound(); + + Value lowerBoundIndex = castToIndex(builder, loc, lowerBound); + if (lowerBoundIndex != lowerBound) { + tagWithBlockId(lowerBoundIndex, forBlockId); + } + Value stepIndex = castToIndex(builder, loc, step); + if (stepIndex != step) { + tagWithBlockId(stepIndex, forBlockId); + } + Value upperBoundIndex = castToIndex(builder, loc, upperBound); + if (upperBoundIndex != upperBound) { + tagWithBlockId(upperBoundIndex, forBlockId); + } + + Value range = + builder.create(loc, upperBoundIndex, lowerBoundIndex); + tagWithBlockId(range, forBlockId); + Value tripCount = builder.create(loc, range, stepIndex); + tagWithBlockId(tripCount, forBlockId); + return {lowerBound, step, tripCount}; +} + +GroupInvariantValues emitGroupInvariantValues(OpBuilder &builder, Location loc, + ArrayRef groups) { + int numGroups = static_cast(groups.size()); + GroupInvariantValues values; + values.trueFlags.resize(numGroups); + values.indexOnes.resize(numGroups); + values.slotConsts.resize(numGroups); + values.depthConsts.resize(numGroups); + + for (int groupIdx = 0; groupIdx < numGroups; ++groupIdx) { + const auto &group = groups[groupIdx]; + IntegerAttr blockId = + group.loads.empty() + ? IntegerAttr{} + : group.loads[0].markedOp->getAttrOfType(kBlockIdAttr); + int groupDepth = group.depth; + values.trueFlags[groupIdx] = + builder.create(loc, builder.getBoolAttr(true)); + tagWithBlockId(values.trueFlags[groupIdx], blockId); + values.indexOnes[groupIdx] = builder.create(loc, 1); + tagWithBlockId(values.indexOnes[groupIdx], blockId); + + values.slotConsts[groupIdx].resize(groupDepth); + for (int slot = 0; slot < groupDepth; ++slot) { + values.slotConsts[groupIdx][slot] = + builder.create(loc, slot); + tagWithBlockId(values.slotConsts[groupIdx][slot], blockId); + } + + values.depthConsts[groupIdx] = + builder.create(loc, groupDepth); + tagWithBlockId(values.depthConsts[groupIdx], blockId); + } + + return values; +} + +FailureOr> +collectLinearIterArgDeltas(const ExtendedForInfo &info, scf::ForOp forOp) { + if (info.numOrig < 0) { + forOp.emitError() << "[" << DEBUG_TYPE + << "] original iter_arg count is negative: " + << info.numOrig; + return failure(); + } + auto numOrig = static_cast(info.numOrig); + if (info.oldBody->getNumArguments() < numOrig + kForBodyIterArgOffset) { + forOp.emitError() << "[" << DEBUG_TYPE << "] old loop body has " + << info.oldBody->getNumArguments() + << " arguments, expected at least " + << (numOrig + kForBodyIterArgOffset); + return failure(); + } + + auto oldYieldOp = cast(info.oldBody->getTerminator()); + if (oldYieldOp->getNumOperands() < numOrig) { + forOp.emitError() << "[" << DEBUG_TYPE << "] old loop yield has " + << oldYieldOp->getNumOperands() + << " operands, expected at least " << numOrig; + return failure(); + } + + SmallVector iterArgDeltas(info.numOrig, Value{}); + for (unsigned iterArgIdx = 0; iterArgIdx < numOrig; ++iterArgIdx) { + Value delta; + if (getLinearIterArgDelta( + info.oldBody->getArgument(iterArgIdx + kForBodyIterArgOffset), + oldYieldOp->getOperand(iterArgIdx), forOp, delta)) { + iterArgDeltas[iterArgIdx] = delta; + } + } + return iterArgDeltas; +} + +LogicalResult replaceOriginalForResults(scf::ForOp oldForOp, + scf::ForOp newForOp, int numOrig) { + if (numOrig < 0) { + oldForOp.emitError() << "[" << DEBUG_TYPE + << "] original result count is negative: " << numOrig; + return failure(); + } + + auto numOrigResults = static_cast(numOrig); + if (oldForOp.getNumResults() < numOrigResults || + newForOp.getNumResults() < numOrigResults) { + oldForOp.emitError() << "[" << DEBUG_TYPE << "] cannot replace " + << numOrigResults << " loop results; old loop has " + << oldForOp.getNumResults() << ", new loop has " + << newForOp.getNumResults(); + return failure(); + } + + for (unsigned resultIdx = 0; resultIdx < numOrigResults; ++resultIdx) { + oldForOp.getResult(resultIdx).replaceAllUsesWith( + newForOp.getResult(resultIdx)); + } + return success(); +} + +LogicalResult finalizeMultiBufferLoop(OpBuilder &builder, scf::ForOp newForOp, + int numOrig) { + if (numOrig < 0) { + newForOp.emitError() << "[" << DEBUG_TYPE + << "] original iter_arg count is negative: " + << numOrig; + return failure(); + } + + FailureOr finalFor = + pruneDeadIterArgs(builder, newForOp, static_cast(numOrig)); + if (failed(finalFor)) { + return failure(); + } + + eraseDeadBodyOps(finalFor->getBody()); + + // Debug-only diagnostic: keep this as a log instead of asserting so the + // pass can still dump the transformed IR for investigation. + logRepeatedTopLevelBlockRuns(finalFor->getBody(), kGMLoadMultiBufferStage); + return success(); +} + +/// Apply GM-load multi-buffer rewriting to one scf.for. +/// allCtxForOps carries the set of all forOps being transformed so that inner +/// forOps (already handled) are not cloned again into the consumer body. +LogicalResult +applyMultiBufferToForLoop(ForBufferCtx &context, + const llvm::DenseSet &allCtxForOps) { + scf::ForOp forOp = context.forOp; + Location loc = forOp.getLoc(); + OpBuilder builder(forOp); + + allocateBufferSlotsForGroups(builder, loc, forOp, context.groups); + + // Scan parent block once so buildExtendedFor can reuse existing constants. + ConstantCache cache; + cache.scan(forOp->getBlock()); + + builder.setInsertionPoint(forOp); + LoopInvariantValues loopValues = emitLoopInvariantValues(builder, loc, forOp); + GroupInvariantValues groupValues = + emitGroupInvariantValues(builder, loc, context.groups); + + auto info = buildExtendedFor(builder, loc, forOp, context.groups, cache); + FailureOr> iterArgDeltas = + collectLinearIterArgDeltas(info, forOp); + if (failed(iterArgDeltas)) { + return failure(); + } + + LogicalResult emitBodyStatus = emitMultiBufferLoopBody( + builder, loc, context.groups, info, groupValues.indexOnes, + groupValues.slotConsts, groupValues.depthConsts, allCtxForOps, + loopValues.tripCount, loopValues.lowerBound, loopValues.step, + groupValues.trueFlags, *iterArgDeltas, forOp); + if (failed(emitBodyStatus)) { + return failure(); + } + + if (failed(replaceOriginalForResults(forOp, info.newForOp, info.numOrig))) { + return failure(); + } + if (failed(finalizeMultiBufferLoop(builder, info.newForOp, info.numOrig))) { + return failure(); + } + return success(); +} + +// ============================================================================ +// Cleanup utilities +// ============================================================================ + +bool isAncestorBlock(Block *ancestor, Block *descendant) { + for (Block *currentBlock = descendant; currentBlock;) { + if (currentBlock == ancestor) { + return true; + } + auto *parentOp = currentBlock->getParentOp(); + if (!parentOp) { + break; + } + currentBlock = parentOp->getBlock(); + } + return false; +} + +void deduplicateConstants(ModuleOp module) { + llvm::DenseMap canonical; + SmallVector toErase; + + module->walk([&](arith::ConstantOp cst) { + if (cst->hasAttr(kBlockIdAttr)) { + return mlir::WalkResult::advance(); + } + auto [canonicalIt, inserted] = + canonical.try_emplace(cst.getValue(), cst.getResult()); + if (!inserted && canonicalIt->second != cst.getResult()) { + Block *canonBlock = canonicalIt->second.getParentBlock(); + Block *thisBlock = cst->getBlock(); + if (canonBlock == thisBlock || isAncestorBlock(canonBlock, thisBlock)) { + cst.getResult().replaceAllUsesWith(canonicalIt->second); + toErase.push_back(cst); + } + } + return mlir::WalkResult::advance(); + }); + + for (Operation *op : llvm::reverse(toErase)) { + op->erase(); + } +} + +} // namespace gmload diff --git a/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/MultiBufferLoopBodyEmission.cpp b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/MultiBufferLoopBodyEmission.cpp new file mode 100644 index 0000000000..bd36701b55 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/DecoupleComputeAndMemory/MultiBufferLoopBodyEmission.cpp @@ -0,0 +1,904 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/DecoupleComputeAndMemory/AddMultiBufferToGMLoadInternal.h" + +namespace gmload { + +// ============================================================================ +// Producer emission helpers +// ============================================================================ + +static bool canSkipBecauseAllResultsMapped(Operation *op, + const IRMapping &mapping) { + if (op->getNumResults() == 0) { + return false; + } + return llvm::all_of(op->getResults(), [&](Value result) { + return static_cast(mapping.lookupOrNull(result)); + }); +} + +static LogicalResult +materializeProducerOp(OpBuilder &builder, Operation *op, Block *oldBody, + const llvm::DenseSet &producerSourceOps, + IRMapping &mapping, + llvm::DenseSet &materializedOps, + llvm::DenseSet &activeOps); + +static LogicalResult +materializeProducerValue(OpBuilder &builder, Value value, Block *oldBody, + const llvm::DenseSet &producerSourceOps, + IRMapping &mapping, + llvm::DenseSet &materializedOps, + llvm::DenseSet &activeOps) { + if (mapping.lookupOrNull(value)) { + return success(); + } + + if (auto blockArg = dyn_cast(value)) { + if (blockArg.getOwner() == oldBody) { + return failure(); + } + return success(); + } + + Operation *defOp = value.getDefiningOp(); + if (!defOp) { + return success(); + } + + if (producerSourceOps.contains(defOp) || defOp->getBlock() == oldBody) { + return materializeProducerOp(builder, defOp, oldBody, producerSourceOps, + mapping, materializedOps, activeOps); + } + + return success(); +} + +static LogicalResult +materializeProducerOp(OpBuilder &builder, Operation *op, Block *oldBody, + const llvm::DenseSet &producerSourceOps, + IRMapping &mapping, + llvm::DenseSet &materializedOps, + llvm::DenseSet &activeOps) { + if (materializedOps.contains(op) || + canSkipBecauseAllResultsMapped(op, mapping)) { + return success(); + } + + if (activeOps.contains(op)) { + return failure(); + } + + if (!producerSourceOps.contains(op) && op->getBlock() == oldBody) { + if (!mlir::isMemoryEffectFree(op) || op->getNumRegions() != 0) { + return failure(); + } + } + + activeOps.insert(op); + for (Value operand : collectOperandsIncludingRegions(op)) { + LogicalResult materializeStatus = + materializeProducerValue(builder, operand, oldBody, producerSourceOps, + mapping, materializedOps, activeOps); + if (failed(materializeStatus)) { + activeOps.erase(op); + return failure(); + } + } + activeOps.erase(op); + + builder.clone(*op, mapping); + materializedOps.insert(op); + return success(); +} + +static IntegerAttr getGroupBlockId(const LoadGroup &group) { + return group.loads.empty() + ? IntegerAttr{} + : group.loads[0].markedOp->getAttrOfType( + kBlockIdAttr); +} + +static Value createProducerFillCondition(OpBuilder &builder, Location loc, + Value flagArg, + Value currentProducerCounter, + Value tripCount, Value falseVal, + IntegerAttr blockId) { + Value flagEmpty = builder.create(loc, arith::CmpIPredicate::eq, + flagArg, falseVal); + tagWithBlockId(flagEmpty, blockId); + Value prodLtTripCount = builder.create( + loc, arith::CmpIPredicate::ult, currentProducerCounter, tripCount); + tagWithBlockId(prodLtTripCount, blockId); + Value cond = builder.create(loc, flagEmpty, prodLtTripCount); + tagWithBlockId(cond, blockId); + return cond; +} + +static scf::IfOp createProducerFillIf(OpBuilder &builder, Location loc, + Value cond, IntegerAttr blockId) { + auto ifOp = builder.create( + loc, TypeRange{builder.getI1Type(), builder.getIndexType()}, cond, true); + if (blockId) + ifOp->setAttr(kBlockIdAttr, blockId); + ifOp->setAttr(kLoadStoreAttr, builder.getUnitAttr()); + return ifOp; +} + +struct ProducerLoopPosition { + Type inductionVarType; + Value projectedInductionVar; + Value producerCounterInInductionType; +}; + +static ProducerLoopPosition +mapProducerLoopPosition(OpBuilder &builder, Location loc, Block *oldBody, + Value loopLowerBound, Value loopStep, + Value currentProducerCounter, IntegerAttr blockId, + IRMapping &prodMapping) { + Value oldInductionVar = oldBody->getArgument(kForInductionVarArgIndex); + Type inductionVarType = oldInductionVar.getType(); + + Value lowerBound = + castIndexTo(builder, loc, loopLowerBound, inductionVarType); + if (lowerBound != loopLowerBound) + tagWithBlockId(lowerBound, blockId); + Value step = castIndexTo(builder, loc, loopStep, inductionVarType); + if (step != loopStep) + tagWithBlockId(step, blockId); + Value producerCounterInInductionType = + castIndexTo(builder, loc, currentProducerCounter, inductionVarType); + tagWithBlockId(producerCounterInInductionType, blockId); + Value inductionOffset = + builder.create(loc, producerCounterInInductionType, step); + tagWithBlockId(inductionOffset, blockId); + Value projectedInductionVar = + builder.create(loc, lowerBound, inductionOffset); + tagWithBlockId(projectedInductionVar, blockId); + prodMapping.map(oldInductionVar, projectedInductionVar); + + return {inductionVarType, projectedInductionVar, + producerCounterInInductionType}; +} + +static LogicalResult +validateProducerIterArgInputs(scf::ForOp origForOp, Block *oldBody, + Block *newBody, ArrayRef iterArgDeltas) { + size_t numOrig = origForOp.getInitArgs().size(); + if (iterArgDeltas.size() < numOrig) { + origForOp.emitError() << "[" << DEBUG_TYPE << "] iter_arg delta count " + << iterArgDeltas.size() + << " is smaller than original iter_arg count " + << numOrig; + return failure(); + } + if (oldBody->getNumArguments() < numOrig + kForBodyIterArgOffset || + newBody->getNumArguments() < numOrig + kForBodyIterArgOffset) { + origForOp.emitError() + << "[" << DEBUG_TYPE + << "] loop body argument count is smaller than expected " + << (numOrig + kForBodyIterArgOffset); + return failure(); + } + return success(); +} + +static LogicalResult +mapProducerIterArgs(OpBuilder &builder, Location loc, Block *oldBody, + Block *newBody, scf::ForOp origForOp, Value loopLowerBound, + Value loopStep, Value currentProducerCounter, + const SmallVector &iterArgDeltas, + const ProducerLoopPosition &position, IntegerAttr blockId, + IRMapping &prodMapping) { + if (failed(validateProducerIterArgInputs(origForOp, oldBody, newBody, + iterArgDeltas))) { + return failure(); + } + + // producerCounterInInductionType is already cast to inductionVarType; cache + // casts by type to avoid redundant index_cast ops when multiple iter_args + // share the same type. + llvm::DenseMap producerCounterByType; + producerCounterByType[position.inductionVarType] = + position.producerCounterInInductionType; + + int numOrig = static_cast(origForOp.getInitArgs().size()); + for (int iterArgIdx = 0; iterArgIdx < numOrig; ++iterArgIdx) { + Value oldArg = oldBody->getArgument(iterArgIdx + kForBodyIterArgOffset); + Value delta = iterArgDeltas[iterArgIdx]; + if (!delta) { + prodMapping.map(oldArg, + newBody->getArgument(iterArgIdx + kForBodyIterArgOffset)); + continue; + } + + Type argType = oldArg.getType(); + Value initArg = origForOp.getInitArgs()[iterArgIdx]; + // If initArg == lower bound and delta == step this iter_arg tracks the + // induction variable exactly; reuse the projected IV. + if (initArg == loopLowerBound && delta == loopStep && + argType == position.inductionVarType) { + prodMapping.map(oldArg, position.projectedInductionVar); + continue; + } + + auto [counterIt, inserted] = + producerCounterByType.try_emplace(argType, Value{}); + if (inserted) { + counterIt->second = + castIndexTo(builder, loc, currentProducerCounter, argType); + tagWithBlockId(counterIt->second, blockId); + } + Value argMul = builder.create(loc, counterIt->second, delta); + tagWithBlockId(argMul, blockId); + Value argAdd = builder.create(loc, initArg, argMul); + tagWithBlockId(argAdd, blockId); + prodMapping.map(oldArg, argAdd); + } + return success(); +} + +static void mapProducerSlotBuffers(const LoadGroup &group, int slotIdx, + IRMapping &prodMapping) { + for (int loadIdx = 0; loadIdx < static_cast(group.loads.size()); + ++loadIdx) + prodMapping.map(group.loads[loadIdx].allocOp->getResult(0), + group.bufSlots[slotIdx][loadIdx]); +} + +static LogicalResult materializeProducerChain(OpBuilder &builder, + Block *oldBody, + const LoadGroup &group, + IRMapping &prodMapping) { + llvm::DenseSet skipSet; + for (const MarkedLoad &load : group.loads) { + skipSet.insert(load.allocOp); + skipSet.insert(load.markedOp); + } + + llvm::DenseSet producerSourceOps(group.mergedChain.begin(), + group.mergedChain.end()); + llvm::DenseSet materializedOps; + llvm::DenseSet activeOps; + for (Operation *op : group.mergedChain) { + if (skipSet.contains(op)) { + continue; + } + if (failed(materializeProducerOp(builder, op, oldBody, producerSourceOps, + prodMapping, materializedOps, activeOps))) + return failure(); + } + return success(); +} + +static LogicalResult emitProducerFillThenBlock( + OpBuilder &builder, Location loc, scf::IfOp ifOp, Block *oldBody, + Block *newBody, const LoadGroup &group, int slotIdx, + Value currentProducerCounter, Value loopLowerBound, Value loopStep, + Value trueFlag, Value indexOne, const SmallVector &iterArgDeltas, + scf::ForOp origForOp, IntegerAttr blockId) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(ifOp.thenBlock()); + + IRMapping prodMapping; + ProducerLoopPosition position = + mapProducerLoopPosition(builder, loc, oldBody, loopLowerBound, loopStep, + currentProducerCounter, blockId, prodMapping); + LogicalResult mapIterArgStatus = mapProducerIterArgs( + builder, loc, oldBody, newBody, origForOp, loopLowerBound, loopStep, + currentProducerCounter, iterArgDeltas, position, blockId, prodMapping); + if (failed(mapIterArgStatus)) { + return failure(); + } + mapProducerSlotBuffers(group, slotIdx, prodMapping); + if (failed(materializeProducerChain(builder, oldBody, group, prodMapping))) { + return failure(); + } + + Value prodNext = + builder.create(loc, currentProducerCounter, indexOne); + tagWithBlockId(prodNext, blockId); + builder.create(loc, ValueRange{trueFlag, prodNext}); + return success(); +} + +static void emitProducerFillElseBlock(OpBuilder &builder, Location loc, + scf::IfOp ifOp, Value flagArg, + Value currentProducerCounter) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(ifOp.elseBlock()); + builder.create(loc, + ValueRange{flagArg, currentProducerCounter}); +} + +/// Emit the producer scf.if for one buffer slot. Returns {flagNext, +/// prodCounterNext}. +FailureOr> emitProducerFillForSlot( + OpBuilder &builder, Location loc, Block *oldBody, Block *newBody, + const LoadGroup &group, int slotIdx, Value flagArg, + Value currentProducerCounter, Value tripCount, Value falseVal, + Value loopLowerBound, Value loopStep, Value trueFlag, Value indexOne, + const SmallVector &iterArgDeltas, scf::ForOp origForOp) { + IntegerAttr blockId = getGroupBlockId(group); + Value cond = + createProducerFillCondition(builder, loc, flagArg, currentProducerCounter, + tripCount, falseVal, blockId); + scf::IfOp ifOp = createProducerFillIf(builder, loc, cond, blockId); + LogicalResult thenStatus = emitProducerFillThenBlock( + builder, loc, ifOp, oldBody, newBody, group, slotIdx, + currentProducerCounter, loopLowerBound, loopStep, trueFlag, indexOne, + iterArgDeltas, origForOp, blockId); + if (failed(thenStatus)) { + return failure(); + } + emitProducerFillElseBlock(builder, loc, ifOp, flagArg, + currentProducerCounter); + + return std::make_pair(ifOp.getResult(0), ifOp.getResult(1)); +} + +// ============================================================================ +// Consumer emission helpers +// ============================================================================ + +/// Emit slot-selection for one load: one ToTensorOp per slot, followed by a +/// (depth-1)-level comparison/select chain that picks the slot matching +/// `target`. Sets mapping[markedOp->result] = selected value. +Value emitLoadSlotSelection(OpBuilder &builder, Location loc, + const MarkedLoad &load, ArrayRef slotBufs, + Value target, int depth, IRMapping &mapping, + ArrayRef slotConsts) { + IntegerAttr blockId = load.markedOp->getAttrOfType(kBlockIdAttr); + Type tensorTy = load.markedOp->getResult(0).getType(); + + SmallVector slotVals(depth); + for (int slotIdx = 0; slotIdx < depth; ++slotIdx) { + auto toTensor = builder.create( + loc, tensorTy, slotBufs[slotIdx], true, true); + if (blockId) + toTensor->setAttr(kBlockIdAttr, blockId); + slotVals[slotIdx] = toTensor; + } + + Value result = slotVals[depth - 1]; + for (int slotIdx = depth - 2; slotIdx >= 0; --slotIdx) { + Value eq = builder.create(loc, arith::CmpIPredicate::eq, + target, slotConsts[slotIdx]); + tagWithBlockId(eq, blockId); + result = + builder.create(loc, eq, slotVals[slotIdx], result); + tagWithBlockId(result, blockId); + } + + mapping.map(load.markedOp->getResult(0), result); + return result; +} + +/// Emit the scf.if that clears slot `slotIdx`'s flag when it is the consumed +/// slot (`target == slotIdx`). Returns the updated flag value. +Value emitSlotFlagClear(OpBuilder &builder, Location loc, Value target, + Value slotConst, Value flagNext, Value falseVal, + IntegerAttr blockId) { + Value isConsumed = builder.create( + loc, arith::CmpIPredicate::eq, target, slotConst); + tagWithBlockId(isConsumed, blockId); + + auto ifClear = builder.create(loc, TypeRange{builder.getI1Type()}, + isConsumed, true); + if (blockId) + ifClear->setAttr(kBlockIdAttr, blockId); + ifClear->setAttr(kLoadStoreAttr, builder.getUnitAttr()); + { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(ifClear.thenBlock()); + builder.create(loc, ValueRange{falseVal}); + } + { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(ifClear.elseBlock()); + builder.create(loc, ValueRange{flagNext}); + } + return ifClear.getResult(0); +} + +// ============================================================================ + +namespace { + +struct MultiBufferLoopBodyState { + SmallVector> flagFinals; + SmallVector consCounterNexts; + SmallVector prodCounterFinals; + SmallVector> flagNexts; + SmallVector targets; + SmallVector prefixEmitted; + SmallVector finalEmitted; + + explicit MultiBufferLoopBodyState(int numGroups) + : flagFinals(numGroups), consCounterNexts(numGroups), + prodCounterFinals(numGroups), flagNexts(numGroups), targets(numGroups), + prefixEmitted(numGroups, 0), finalEmitted(numGroups, 0) {} +}; + +static void +computeConsumerCloneSets(ArrayRef groups, Block *oldBody, + llvm::DenseSet &allChainOps, + llvm::DenseSet &retainedChainOpSet) { + // A retained chain op is a chain op that must still be cloned into the + // consumer body because non-chain body code uses it. Retained ops are + // emitted in their original top-level run, not in the load group that + // discovered them. + for (const LoadGroup &group : groups) { + llvm::DenseSet skipSet = + computeSkipInConsumer(group.loads, oldBody); + for (Operation *op : group.mergedChain) { + allChainOps.insert(op); + if (!skipSet.contains(op)) + retainedChainOpSet.insert(op); + } + } +} + +static bool +shouldSkipOldBodyOp(Operation *op, + const llvm::DenseSet &allCtxForOps, + const llvm::DenseSet &allChainOps, + const llvm::DenseSet &retainedChainOpSet) { + if (allCtxForOps.contains(op)) { + return true; + } + return allChainOps.contains(op) && !retainedChainOpSet.contains(op); +} + +struct ProducerFillState { + SmallVector flagNexts; + Value producerCounter; +}; + +static FailureOr +emitProducerFillsForGroup(OpBuilder &builder, Location loc, LoadGroup &group, + int groupIdx, ExtendedForInfo &info, Block *oldBody, + Value tripCount, Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &groupIndexOneVals, + const SmallVector &iterArgDeltas, + scf::ForOp origForOp) { + int groupDepth = group.depth; + Value currentProducerCounter = info.groupArgs[groupIdx].prodCounter; + SmallVector flagNexts(groupDepth); + for (int slotIdx = 0; slotIdx < groupDepth; ++slotIdx) { + FailureOr> producerResult = emitProducerFillForSlot( + builder, loc, oldBody, info.newBody, group, slotIdx, + info.groupArgs[groupIdx].flagArgs[slotIdx], currentProducerCounter, + tripCount, info.falseVal, loopLowerBound, loopStep, + groupTrueFlagVals[groupIdx], groupIndexOneVals[groupIdx], iterArgDeltas, + origForOp); + if (failed(producerResult)) { + return failure(); + } + + auto [flagNext, prodCounterNext] = *producerResult; + flagNexts[slotIdx] = flagNext; + currentProducerCounter = prodCounterNext; + } + + return ProducerFillState{std::move(flagNexts), currentProducerCounter}; +} + +static Value emitConsumerSlotTarget(OpBuilder &builder, Location loc, + ExtendedForInfo &info, int groupIdx, + Value groupDepthConst, + IntegerAttr blockId) { + Value target = builder.create( + loc, info.groupArgs[groupIdx].consCounter, groupDepthConst); + tagWithBlockId(target, blockId); + return target; +} + +static void emitLoadSlotSelectionsForGroup( + OpBuilder &builder, Location loc, LoadGroup &group, int groupIdx, + int groupDepth, Value target, ExtendedForInfo &info, + const SmallVector> &allGroupSlotConsts) { + for (int loadIdx = 0; loadIdx < static_cast(group.loads.size()); + ++loadIdx) { + SmallVector slotBufs(groupDepth); + for (int slotIdx = 0; slotIdx < groupDepth; ++slotIdx) + slotBufs[slotIdx] = group.bufSlots[slotIdx][loadIdx]; + emitLoadSlotSelection(builder, loc, group.loads[loadIdx], slotBufs, target, + groupDepth, info.mapping, + allGroupSlotConsts[groupIdx]); + } +} + +static LogicalResult validateGroupEmissionInputs( + ArrayRef groups, ExtendedForInfo &info, int groupIdx, + ArrayRef groupTrueFlagVals, ArrayRef groupIndexOneVals, + ArrayRef allGroupDepthConsts, + ArrayRef> allGroupSlotConsts, scf::ForOp origForOp) { + size_t numGroups = groups.size(); + if (info.groupArgs.size() != numGroups || + groupTrueFlagVals.size() != numGroups || + groupIndexOneVals.size() != numGroups || + allGroupDepthConsts.size() != numGroups || + allGroupSlotConsts.size() != numGroups) { + origForOp.emitError() + << "[" << DEBUG_TYPE + << "] array size mismatch in multi-buffer emission: groups=" + << numGroups << ", groupArgs=" << info.groupArgs.size() + << ", groupTrueFlagVals=" << groupTrueFlagVals.size() + << ", groupIndexOneVals=" << groupIndexOneVals.size() + << ", allGroupDepthConsts=" << allGroupDepthConsts.size() + << ", allGroupSlotConsts=" << allGroupSlotConsts.size(); + return failure(); + } + if (groupIdx < 0 || static_cast(groupIdx) >= numGroups) { + origForOp.emitError() + << "[" << DEBUG_TYPE << "] load group index " << groupIdx + << " is out of range for multi-buffer emission (numGroups=" << numGroups + << ")"; + return failure(); + } + return success(); +} + +static LogicalResult emitProducerAndSelectSlots( + OpBuilder &builder, Location loc, SmallVectorImpl &groups, + int groupIdx, ExtendedForInfo &info, Block *oldBody, Value tripCount, + Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &groupIndexOneVals, + const SmallVector &iterArgDeltas, scf::ForOp origForOp, + const SmallVector &allGroupDepthConsts, + const SmallVector> &allGroupSlotConsts, + MultiBufferLoopBodyState &state) { + LogicalResult validateStatus = validateGroupEmissionInputs( + groups, info, groupIdx, groupTrueFlagVals, groupIndexOneVals, + allGroupDepthConsts, allGroupSlotConsts, origForOp); + if (failed(validateStatus)) { + return failure(); + } + if (static_cast(groupIdx) >= state.prefixEmitted.size()) { + origForOp.emitError() << "[" << DEBUG_TYPE << "] load group index " + << groupIdx << " exceeds multi-buffer state size " + << state.prefixEmitted.size(); + return failure(); + } + if (state.prefixEmitted[groupIdx]) { + return success(); + } + + LoadGroup &group = groups[groupIdx]; + int groupDepth = group.depth; + IntegerAttr blockId = getGroupBlockId(group); + + FailureOr fillState = emitProducerFillsForGroup( + builder, loc, group, groupIdx, info, oldBody, tripCount, loopLowerBound, + loopStep, groupTrueFlagVals, groupIndexOneVals, iterArgDeltas, origForOp); + if (failed(fillState)) { + return failure(); + } + + state.flagNexts[groupIdx] = std::move(fillState->flagNexts); + state.prodCounterFinals[groupIdx] = fillState->producerCounter; + Value target = emitConsumerSlotTarget(builder, loc, info, groupIdx, + allGroupDepthConsts[groupIdx], blockId); + state.targets[groupIdx] = target; + emitLoadSlotSelectionsForGroup(builder, loc, group, groupIdx, groupDepth, + target, info, allGroupSlotConsts); + + state.prefixEmitted[groupIdx] = 1; + return success(); +} + +static LogicalResult emitConsumerReleaseForGroup( + OpBuilder &builder, Location loc, SmallVectorImpl &groups, + int groupIdx, ExtendedForInfo &info, + const SmallVector &groupIndexOneVals, + const SmallVector> &allGroupSlotConsts, + MultiBufferLoopBodyState &state) { + if (state.finalEmitted[groupIdx]) { + return success(); + } + + LoadGroup &group = groups[groupIdx]; + int groupDepth = group.depth; + IntegerAttr blockId = getGroupBlockId(group); + + state.flagFinals[groupIdx].resize(groupDepth); + for (int slotIdx = 0; slotIdx < groupDepth; ++slotIdx) { + state.flagFinals[groupIdx][slotIdx] = emitSlotFlagClear( + builder, loc, state.targets[groupIdx], + allGroupSlotConsts[groupIdx][slotIdx], + state.flagNexts[groupIdx][slotIdx], info.falseVal, blockId); + } + + state.consCounterNexts[groupIdx] = builder.create( + loc, info.groupArgs[groupIdx].consCounter, groupIndexOneVals[groupIdx]); + tagWithBlockId(state.consCounterNexts[groupIdx], blockId); + state.finalEmitted[groupIdx] = 1; + return success(); +} + +static LogicalResult emitConsumerReleaseAfterPrefix( + OpBuilder &builder, Location loc, SmallVectorImpl &groups, + int groupIdx, ExtendedForInfo &info, Block *oldBody, Value tripCount, + Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &groupIndexOneVals, + const SmallVector &iterArgDeltas, scf::ForOp origForOp, + const SmallVector &allGroupDepthConsts, + const SmallVector> &allGroupSlotConsts, + MultiBufferLoopBodyState &state) { + if (!state.prefixEmitted[groupIdx]) { + LogicalResult producerStatus = emitProducerAndSelectSlots( + builder, loc, groups, groupIdx, info, oldBody, tripCount, + loopLowerBound, loopStep, groupTrueFlagVals, groupIndexOneVals, + iterArgDeltas, origForOp, allGroupDepthConsts, allGroupSlotConsts, + state); + if (failed(producerStatus)) { + return failure(); + } + } + + return emitConsumerReleaseForGroup(builder, loc, groups, groupIdx, info, + groupIndexOneVals, allGroupSlotConsts, + state); +} + +static LogicalResult collectGroupsByOwnerOp( + ArrayRef groups, Block *oldBody, + llvm::DenseMap> &groupsByOwnerOp) { + for (int groupIdx = 0; groupIdx < static_cast(groups.size()); + ++groupIdx) { + if (groups[groupIdx].loads.empty()) { + continue; + } + + Operation *owner = + getAncestorInBlock(groups[groupIdx].loads[0].markedOp, oldBody); + if (!owner) { + return failure(); + } + groupsByOwnerOp[owner].push_back(groupIdx); + } + return success(); +} + +static LogicalResult emitProducerHooksForOwnerGroups( + OpBuilder &builder, Location loc, ArrayRef ownerGroups, + SmallVectorImpl &groups, ExtendedForInfo &info, Block *oldBody, + Value tripCount, Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &groupIndexOneVals, + const SmallVector &iterArgDeltas, scf::ForOp origForOp, + const SmallVector &allGroupDepthConsts, + const SmallVector> &allGroupSlotConsts, + MultiBufferLoopBodyState &state) { + for (int groupIdx : ownerGroups) { + LogicalResult producerStatus = emitProducerAndSelectSlots( + builder, loc, groups, groupIdx, info, oldBody, tripCount, + loopLowerBound, loopStep, groupTrueFlagVals, groupIndexOneVals, + iterArgDeltas, origForOp, allGroupDepthConsts, allGroupSlotConsts, + state); + if (failed(producerStatus)) { + return failure(); + } + } + return success(); +} + +static LogicalResult emitConsumerReleaseHooksForOwnerGroups( + OpBuilder &builder, Location loc, ArrayRef ownerGroups, + SmallVectorImpl &groups, ExtendedForInfo &info, Block *oldBody, + Value tripCount, Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &groupIndexOneVals, + const SmallVector &iterArgDeltas, scf::ForOp origForOp, + const SmallVector &allGroupDepthConsts, + const SmallVector> &allGroupSlotConsts, + MultiBufferLoopBodyState &state) { + for (int groupIdx : ownerGroups) { + LogicalResult releaseStatus = emitConsumerReleaseAfterPrefix( + builder, loc, groups, groupIdx, info, oldBody, tripCount, + loopLowerBound, loopStep, groupTrueFlagVals, groupIndexOneVals, + iterArgDeltas, origForOp, allGroupDepthConsts, allGroupSlotConsts, + state); + if (failed(releaseStatus)) { + return failure(); + } + } + return success(); +} + +static LogicalResult +cloneOldBodyOpIfNeeded(OpBuilder &builder, Operation *op, Block *oldBody, + IRMapping &mapping, + const llvm::DenseSet &allCtxForOps, + const llvm::DenseSet &allChainOps, + const llvm::DenseSet &retainedChainOpSet) { + if (shouldSkipOldBodyOp(op, allCtxForOps, allChainOps, retainedChainOpSet)) { + return success(); + } + if (!areOperandsAvailableForClone(op, oldBody, mapping)) { + return failure(); + } + builder.clone(*op, mapping); + return success(); +} + +static LogicalResult emitOldBodyWithMultiBufferHooks( + OpBuilder &builder, Location loc, SmallVectorImpl &groups, + ExtendedForInfo &info, + const llvm::DenseMap> &groupsByOwnerOp, + const llvm::DenseSet &allCtxForOps, + const llvm::DenseSet &allChainOps, + const llvm::DenseSet &retainedChainOpSet, Value tripCount, + Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &groupIndexOneVals, + const SmallVector &iterArgDeltas, scf::ForOp origForOp, + const SmallVector &allGroupDepthConsts, + const SmallVector> &allGroupSlotConsts, + MultiBufferLoopBodyState &state) { + Block *oldBody = info.oldBody; + for (auto &op : oldBody->without_terminator()) { + auto ownerIt = groupsByOwnerOp.find(&op); + if (ownerIt != groupsByOwnerOp.end()) { + LogicalResult producerHookStatus = emitProducerHooksForOwnerGroups( + builder, loc, ownerIt->second, groups, info, oldBody, tripCount, + loopLowerBound, loopStep, groupTrueFlagVals, groupIndexOneVals, + iterArgDeltas, origForOp, allGroupDepthConsts, allGroupSlotConsts, + state); + if (failed(producerHookStatus)) { + return failure(); + } + } + + LogicalResult cloneStatus = + cloneOldBodyOpIfNeeded(builder, &op, oldBody, info.mapping, + allCtxForOps, allChainOps, retainedChainOpSet); + if (failed(cloneStatus)) { + return failure(); + } + + if (ownerIt != groupsByOwnerOp.end()) { + LogicalResult releaseHookStatus = emitConsumerReleaseHooksForOwnerGroups( + builder, loc, ownerIt->second, groups, info, oldBody, tripCount, + loopLowerBound, loopStep, groupTrueFlagVals, groupIndexOneVals, + iterArgDeltas, origForOp, allGroupDepthConsts, allGroupSlotConsts, + state); + if (failed(releaseHookStatus)) { + return failure(); + } + } + } + return success(); +} + +static LogicalResult emitUnownedGroupsAtBodyEnd( + OpBuilder &builder, Location loc, SmallVectorImpl &groups, + ExtendedForInfo &info, Block *oldBody, Value tripCount, + Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &groupIndexOneVals, + const SmallVector &iterArgDeltas, scf::ForOp origForOp, + const SmallVector &allGroupDepthConsts, + const SmallVector> &allGroupSlotConsts, + MultiBufferLoopBodyState &state) { + for (int groupIdx = 0; groupIdx < static_cast(groups.size()); + ++groupIdx) { + if (state.prefixEmitted[groupIdx]) { + continue; + } + + LogicalResult producerStatus = emitProducerAndSelectSlots( + builder, loc, groups, groupIdx, info, oldBody, tripCount, + loopLowerBound, loopStep, groupTrueFlagVals, groupIndexOneVals, + iterArgDeltas, origForOp, allGroupDepthConsts, allGroupSlotConsts, + state); + if (failed(producerStatus)) { + return failure(); + } + LogicalResult releaseStatus = emitConsumerReleaseAfterPrefix( + builder, loc, groups, groupIdx, info, oldBody, tripCount, + loopLowerBound, loopStep, groupTrueFlagVals, groupIndexOneVals, + iterArgDeltas, origForOp, allGroupDepthConsts, allGroupSlotConsts, + state); + if (failed(releaseStatus)) { + return failure(); + } + } + return success(); +} + +static void emitCombinedYield(OpBuilder &builder, Location loc, + ArrayRef groups, ExtendedForInfo &info, + const MultiBufferLoopBodyState &state) { + auto oldYield = cast(info.oldBody->getTerminator()); + SmallVector yieldVals; + yieldVals.reserve(info.numOrig + + static_cast(groups.size()) * + (info.depth + kLoadGroupCounterIterArgCount)); + for (Value yieldedValue : oldYield.getOperands()) { + yieldVals.push_back(info.mapping.lookupOrDefault(yieldedValue)); + } + for (int groupIdx = 0; groupIdx < static_cast(groups.size()); + ++groupIdx) { + for (int slotIdx = 0; slotIdx < groups[groupIdx].depth; ++slotIdx) { + yieldVals.push_back(state.flagFinals[groupIdx][slotIdx]); + } + yieldVals.push_back(state.prodCounterFinals[groupIdx]); + yieldVals.push_back(state.consCounterNexts[groupIdx]); + } + builder.create(loc, yieldVals); +} + +} // namespace + +/// Emit the full loop body with producer prefetch, consumer slot selection, +/// cloned compute, slot release, and a combined scf.yield. +LogicalResult emitMultiBufferLoopBody( + OpBuilder &builder, Location loc, SmallVectorImpl &groups, + ExtendedForInfo &info, const SmallVector &groupIndexOneVals, + const SmallVector> &allGroupSlotConsts, + const SmallVector &allGroupDepthConsts, + const llvm::DenseSet &allCtxForOps, Value tripCount, + Value loopLowerBound, Value loopStep, + const SmallVector &groupTrueFlagVals, + const SmallVector &iterArgDeltas, scf::ForOp origForOp) { + int numGroups = static_cast(groups.size()); + Block *oldBody = info.oldBody; + + llvm::DenseSet allChainOps; + llvm::DenseSet retainedChainOpSet; + computeConsumerCloneSets(groups, oldBody, allChainOps, retainedChainOpSet); + + MultiBufferLoopBodyState state(numGroups); + llvm::DenseMap> groupsByOwnerOp; + if (failed(collectGroupsByOwnerOp(groups, oldBody, groupsByOwnerOp))) { + return failure(); + } + + LogicalResult bodyStatus = emitOldBodyWithMultiBufferHooks( + builder, loc, groups, info, groupsByOwnerOp, allCtxForOps, allChainOps, + retainedChainOpSet, tripCount, loopLowerBound, loopStep, + groupTrueFlagVals, groupIndexOneVals, iterArgDeltas, origForOp, + allGroupDepthConsts, allGroupSlotConsts, state); + if (failed(bodyStatus)) { + return failure(); + } + + LogicalResult unownedStatus = emitUnownedGroupsAtBodyEnd( + builder, loc, groups, info, oldBody, tripCount, loopLowerBound, loopStep, + groupTrueFlagVals, groupIndexOneVals, iterArgDeltas, origForOp, + allGroupDepthConsts, allGroupSlotConsts, state); + if (failed(unownedStatus)) { + return failure(); + } + + emitCombinedYield(builder, loc, groups, info, state); + return success(); +} + +} // namespace gmload diff --git a/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock.cpp b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock.cpp new file mode 100644 index 0000000000..925fd6b821 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/OpClassifier.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlockPass.h" +#include "mlir/Pass/PassManager.h" + +#include "DynamicCVPipeline/PlanComputeBlock/Passes.h" +#include "DynamicCVPipeline/PlanComputeBlock/PlanCubeBlockPass.h" +#include "DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Passes.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlockPass.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; +using namespace triton; + +static constexpr const char *DEBUG_TYPE = "plan-compute-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +// Run the pass +void PlanComputeBlockPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + OpPassManager pm(module.getOperationName()); + LOG_DEBUG("Enter pass.\n"); + + // Step 1: Run OpClassifierPass to classify operations + pm.addPass(createOpClassifierPass()); + + // Step 2: Partition compute blocks for core_type=cube + pm.addPass(createPlanCubeBlockPass()); + + // Step 3: Partition compute blocks for core_type=vector + pm.addPass(createPlanVectorBlockPass()); + + // Step 4: Reorder + pm.addPass(createReorderOpsByBlockIdPass()); + + if (failed(runPipeline(pm, module))) { + if (!CVPipeline::hasFallbackAttr(module)) { + LOG_DEBUG("Pass failed!\n"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + return; + } + + LOG_DEBUG("Process successfully\n"); +} + +namespace mlir { +namespace triton { +std::unique_ptr> createPlanComputeBlockPass() { + return std::make_unique(); +} + +void registerPlanComputeBlockPasses() { + registerPass([]() -> std::unique_ptr { + return createPlanComputeBlockPass(); + }); + registerPass(createPlanCubeBlockPass); + registerPass(createPlanVectorBlockPass); + registerPass(createReorderOpsByBlockIdPass); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/Common.cpp b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/Common.cpp new file mode 100644 index 0000000000..d043385ce6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/Common.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/ADT/iterator.h" + +#include "mlir/IR/Value.h" + +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" + +#include "DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" + +namespace mlir { +namespace CVPipeline { + +void initializeIndegreeForBlock(Block *block, + llvm::DenseMap &indegree, + const DependencyHelper &depHelper, + ComputeBlockIdManager &bm) { + for (auto *op : llvm::make_pointer_range(block->getOperations())) { + indegree[op] = 0; + depHelper.forEachSource(op, [&](Operation *source) { + if (source->getBlock() == block && !bm.isSameBlock(source, op)) { + indegree[op]++; + } + }); + } +} + +Operation *getAncestorInBlock(Operation *inner, Block *block) { + Operation *cur = inner; + while (cur) { + if (cur->getBlock() == block) { + return cur; + } + cur = cur->getParentOp(); + } + return nullptr; +} + +} // namespace CVPipeline +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.cpp b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.cpp new file mode 100644 index 0000000000..0bea67184a --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.cpp @@ -0,0 +1,293 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Support/WalkResult.h" + +namespace mlir { +namespace CVPipeline { + +ComputeBlockIdManager::ComputeBlockIdManager(Operation *root) { + blockIdToOps.clear(); + opToBlockId.clear(); + root->walk([&](Operation *op) { + if (auto blockIdAttr = op->getAttrOfType(kBlockId)) { + auto blockId = blockIdAttr.getInt(); + if (blockId <= 0) { + return; + } + opToBlockId[op] = blockId; + blockIdToOps[blockId].push_back(op); + cntComputeBlockId = + std::max(cntComputeBlockId, static_cast(blockId)); + } + }); + cntComputeBlockId++; // ensure new id is unique +} + +bool ComputeBlockIdManager::isWholeCubeReady( + Operation *seedOp, llvm::DenseMap &indegree) { + for (auto *op : getOpsInSameBlock(seedOp)) { + if (!indegree.contains(op)) { + continue; + } + if (indegree[op] != 0) { + return false; + } + } + return true; +} + +bool ComputeBlockIdManager::isSameBlock(Operation *a, Operation *b) { + if (getBlockIdByOp(a) == getBlockIdByOp(b) && getBlockIdByOp(a) != -1) { + return true; + } + return false; +} + +int ComputeBlockIdManager::getNextId() { return cntComputeBlockId++; } + +void ComputeBlockIdManager::updateBlockIdWithInner(Operation *parentOp, + int targetId) { + // If the parentOp's blockId is the same as every op's id in each of its + // blocks, we need to change the ops inside its blocks to targetId as well. + int parentBlockId = getBlockIdByOp(parentOp); + if (parentBlockId == -1) { + updateBlockId(parentOp, targetId); + return; + } + // LinalgDialect should have targetId only in prarent, the inner op shouldn't + // have blockId. + if (isa(parentOp->getDialect())) { + updateBlockId(parentOp, targetId); + return; + } + + auto ret = parentOp->walk([&](Operation *op) { + auto innerBlockId = getBlockIdByOp(op); + if (innerBlockId != -1 && innerBlockId != parentBlockId) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (ret != WalkResult::interrupt()) { + parentOp->walk([&](Operation *op) { + // Never fold a sync into a compute block: it must keep its own unique + // block id so the fence between before/after ops survives. + if (CVPipeline::isSyncOp(op)) { + return; + } + updateBlockId(op, targetId); + }); + } + updateBlockId(parentOp, targetId); +} + +void ComputeBlockIdManager::updateBlockId(Operation *op, int blockId) { + if (!op) { + return; + } + // Force Update. + MLIRContext *ctx = op->getContext(); + if (blockId == -1) { + op->removeAttr(kBlockId); + } else { + op->setAttr(kBlockId, IntegerAttr::get(IntegerType::get(ctx, kBlockIdWidth), + blockId)); + } + + auto it = opToBlockId.find(op); + if (it != opToBlockId.end()) { + int preBlockId = it->second; + if (preBlockId != -1) { + auto &vec = blockIdToOps[preBlockId]; + auto vecIt = llvm::find(vec, op); + if (vecIt != vec.end()) { + vec.erase(vecIt); + } + } + } + + opToBlockId[op] = blockId; + blockIdToOps[blockId].push_back(op); +} + +void ComputeBlockIdManager::forgetOp(Operation *op) { + if (!op) { + return; + } + + auto it = opToBlockId.find(op); + if (it == opToBlockId.end()) { + return; // never recorded, nothing to drop + } + + const int blockId = it->second; + if (auto vecIt = blockIdToOps.find(blockId); vecIt != blockIdToOps.end()) { + auto &vec = vecIt->second; + auto found = llvm::find(vec, op); + if (found != vec.end()) { + vec.erase(found); + } + } + opToBlockId.erase(it); +} + +llvm::ArrayRef +ComputeBlockIdManager::getOpsRefByBlockId(int blockId) const { + if (blockId == -1) { + return {}; + } + + auto it = blockIdToOps.find(blockId); + if (it == blockIdToOps.end()) { + return {}; + } + return it->second; +} + +llvm::SmallVector +ComputeBlockIdManager::getOpsByBlockId(int blockId) const { + auto ref = getOpsRefByBlockId(blockId); + return {ref.begin(), ref.end()}; +} + +llvm::SmallVector +ComputeBlockIdManager::getOpsInSameBlock(Operation *op) const { + auto blockIdOpt = getBlockIdByOpOpt(op); + if (!blockIdOpt.has_value()) { + return {op}; + } + auto blockId = blockIdOpt.value(); + auto *block = op->getBlock(); + if (auto it = blockIdToOps.find(blockId); it != blockIdToOps.end()) { + auto filtered = + llvm::make_filter_range(it->second, [block](Operation *opInBlock) { + return opInBlock->getBlock() == block; + }); + return {filtered.begin(), filtered.end()}; + } + return {op}; +} + +std::optional +ComputeBlockIdManager::getBlockIdByOpOpt(Operation *op) const { + auto it = opToBlockId.find(op); + if (it != opToBlockId.end()) { + return it->second; + } + return std::nullopt; +} + +int ComputeBlockIdManager::getBlockIdByOp(Operation *op) { + return getBlockIdByOpOpt(op).value_or(-1); +} + +llvm::LogicalResult ComputeBlockIdManager::markAndRecord(Operation *op, + int blockId) { + // When we call mark, we assume the op have no record in manager. + MLIRContext *ctx = op->getContext(); + op->setAttr(kBlockId, + IntegerAttr::get(IntegerType::get(ctx, kBlockIdWidth), blockId)); + auto itOld = opToBlockId.find(op); + if (itOld != opToBlockId.end() && itOld->second != -1) { + llvm::errs() << "Error: Operation already has a block id. Op: " << *op + << ", old block id: " << itOld->second + << ", new block id: " << blockId << "\n"; + return llvm::failure(); + } + + opToBlockId[op] = blockId; + blockIdToOps[blockId].push_back(op); + return llvm::success(); +} + +llvm::LogicalResult ComputeBlockIdManager::markOpBlockId(Operation *op) { + int blockId = getNextId(); + return markAndRecord(op, blockId); +} + +llvm::LogicalResult +ComputeBlockIdManager::markOpsWithNewId(llvm::ArrayRef ops) { + if (ops.empty()) { + return llvm::success(); + } + int id = getNextId(); + for (Operation *op : ops) { + if (llvm::failed(markAndRecord(op, id))) { + return llvm::failure(); + } + } + return llvm::success(); +} + +bool ComputeBlockIdManager::shouldInheritFromParent( + Block *block, CoreType requiredCoreType) const { + auto *parentOp = block->getParentOp(); + if (!parentOp || !isScfOp(parentOp) || + getCoreTypeOfSimpleOpOrCf(parentOp) != requiredCoreType) { + return false; + } + + auto blockIdOpt = getBlockIdByOpOpt(parentOp); + return blockIdOpt.has_value(); +} + +llvm::LogicalResult ComputeBlockIdManager::inheritFromParent(Block *block) { + auto *parentOp = block->getParentOp(); + if (!parentOp) { + return llvm::failure(); + } + + auto blockIdOpt = getBlockIdByOpOpt(parentOp); + if (!blockIdOpt.has_value()) { + return llvm::failure(); + } + + // no need to and should not walk inside nested blocks: + // 1. the caller is from walk already + // 2. if we have marked nested ops with this block id, it could be correct, + // since they will be re-marked with the same id, so no failures will be + // returned, but this is less robust + auto blockId = blockIdOpt.value(); + for (auto &op : *block) { + if (llvm::failed(markAndRecord(&op, blockId))) { + return llvm::failure(); + } + } + return llvm::success(); +} + +} // namespace CVPipeline +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/OpClassifier.cpp b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/OpClassifier.cpp new file mode 100644 index 0000000000..54fb4497de --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/OpClassifier.cpp @@ -0,0 +1,1970 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/GPU/IR/GPUDialect.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/LoopLikeInterface.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "mlir/Support/LLVM.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/OpClassifier.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/Utils.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/HIVM/IR/HIVMImpl.h" +#include "bishengir/Dialect/HIVM/Utils/Utils.h" +#include "bishengir/Dialect/Utils/Util.h" + +using namespace mlir; +using namespace mlir::CVPipeline; +static constexpr const char *DEBUG_TYPE = "op-classifier"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) +using namespace mlir::triton; + +namespace { + +bool isInsideNestedLinalgRegion(Operation *op) { + for (Operation *parent = op ? op->getParentOp() : nullptr; parent; + parent = parent->getParentOp()) { + if (isa(parent->getDialect())) { + return true; + } + } + return false; +} + +} // namespace + +// Helper: describe operation for logging +std::string OpClassifierPass::describeOp(Operation *op) const { + if (!op) + return ""; + std::string result; + llvm::raw_string_ostream os(result); + os << op->getName().getStringRef(); + return result; +} + +// Helper: convert OpCoreType to string for IR attribute +std::string coreTypeToString(OpCoreType ct) { + switch (ct) { + case OP_CUBE_ONLY: + return "CUBE"; + case OP_VECTOR_ONLY: + return "VECTOR"; + case OP_CUBE_AND_VECTOR: + return "CUBE_AND_VECTOR"; + default: + return "UNDETERMINED"; + } +} + +namespace { + +// Maximum number of core types in a comma-separated core type string (e.g., +// "CUBE,VECTOR") +constexpr size_t kMaxCoreTypeParts = 4; + +// Minimum number of inputs for linalg.matmul (A and B matrices) +constexpr size_t kMinMatmulInputs = 2; + +} // namespace + +// Helper: parse OpCoreType from string (handles both "CUBE" and "CUBE,VECTOR" +// formats) For comma-separated multi-value strings, returns the i-th component +// if index is provided, otherwise returns the first component. +OpCoreType parseCoreTypeFromString(const std::string &coreTypeStr, + size_t index = 0) { + llvm::StringRef ref(coreTypeStr); + llvm::SmallVector parts; + ref.split(parts, ','); + if (parts.size() > 1 && index < parts.size()) { + // Multi-value: pick i-th component + return parseCoreTypeFromString(parts[index].str()); + } + StringRef singleValue = parts[0].trim(); + if (singleValue == "CUBE") { + return OP_CUBE_ONLY; + } else if (singleValue == "VECTOR") { + return OP_VECTOR_ONLY; + } else if (singleValue == "CUBE_AND_VECTOR") { + return OP_CUBE_AND_VECTOR; + } + return OP_UNDETERMINED; +} + +// Initialize the pass +void OpClassifierPass::initializePass(ModuleOp module) { + // Reset data structures + opCoreTypes.clear(); + allOps.clear(); + cubeSeeds.clear(); + vectorOnlyProducerCache.clear(); + inBroadcastChain.clear(); + CloneOpMap.clear(); + + // Collect all operations + module.walk([&](Operation *op) { + if (isa(op)) { + return; // Skip module, function, and return ops + } + allOps.push_back(op); + }); + + LLVM_DEBUG(DBGS() << "Collected: " << allOps.size() << " ops\n"); + + // Initialize all ops to UNDETERMINED + for (Operation *op : allOps) { + opCoreTypes[op] = OP_UNDETERMINED; + } +} + +// Mark an operation as CUBE +void OpClassifierPass::markCube(Operation *op) { + if (op && opCoreTypes.count(op)) { + opCoreTypes[op] = static_cast(opCoreTypes[op] | OP_CUBE_ONLY); + LLVM_DEBUG(DBGS() << "CUBE: " << *op << "\n"); + } +} + +static bool isExtractedLoadStoreRelated(Operation *op) { + if (!op) + return false; + return llvm::TypeSwitch(op) + .Case([](bufferization::ToTensorOp toTensorOp) { + return isExtractedLoadStoreRelated( + toTensorOp.getBuffer().getDefiningOp()); + }) + .Case([](memref::AllocOp allocOp) { + Value memref = allocOp.getMemref(); + for (auto user : memref.getUsers()) { + auto forOp = user->getParentOfType(); + if (forOp && forOp->hasAttr(hivm::ExtractLoadStoreAttr)) + return true; + } + return false; + }) + .Case([](ViewLikeOpInterface viewOp) { + return isExtractedLoadStoreRelated( + viewOp.getViewSource().getDefiningOp()); + }) + .Default([](auto) { return false; }); +} + +// ============================================================================ +// Pattern: to_tensor → matmul (Upstream) +// ============================================================================ +// Matches cases where matmul's input comes from bufferization.to_tensor. +// to_tensor converts a memref to a tensor, typically used for loading matrix +// data from memory. IR Example +// %memref = memref.alloc() : memref<1024x1024xf32> +// %tensor = bufferization.to_tensor %memref : memref<1024x1024xf32> -> +// tensor<1024x1024xf32> %result = linalg.matmul ins(%tensor, %tensor_b) +// outs(%init) +// Matching Logic +// 1. Check if matmul's operand defining op is bufferization.to_tensor +// 2. If matched, mark to_tensor as CUBE and add to cubeSeeds +// 3. Also trace back to_tensor's memref source and mark its defining op as +// CUBE +// Purpose: Ensure matmul's input data loading executes on CUBE for efficient +// matrix data transfer. +// ============================================================================ +void OpClassifierPass::matchToTensorPattern(Operation *def) { + auto toTensorOp = dyn_cast(def); + constexpr llvm::StringLiteral kMayImplicitTransposeWithLastAxis = + "MayImplicitTransposeWithLastAxis"; + + if (!toTensorOp) + return; + + // special case: implicit transpose -> remains vector + if (utils::getAnnotateOpWithAttr(toTensorOp.getResult(), + kMayImplicitTransposeWithLastAxis)) { + return; + } + + // special case: ExtractedLoadOrStore -> remains vector + if (isExtractedLoadStoreRelated(toTensorOp)) { + return; + } + + markCube(toTensorOp); + cubeSeeds.push_back(toTensorOp); + + Value memref = toTensorOp.getBuffer(); + // Also mark the memref allocation as CUBE + if (Operation *memrefDef = memref.getDefiningOp()) { + markCube(memrefDef); + cubeSeeds.push_back(memrefDef); + } +} + +// ============================================================================ +// Pattern: transpose → matmul (Upstream) +// ============================================================================ +// Matches cases where matmul's input comes from linalg.transpose. +// transpose performs matrix transposition, commonly used as preprocessing for +// matrix multiplication inputs. +// IR Example +// %input = tensor.empty() : tensor<1024x1024xf32> +// %transposed = linalg.transpose ins(%input: tensor<1024x1024xf32>) +// outs(%out: tensor<1024x1024xf32>) +// permutation = [1, 0] +// %result = linalg.matmul ins(%transposed, %tensor_b) outs(%init) +// Matching Logic +// 1. Check if matmul's operand defining op is linalg.transpose +// 2. Mark transpose itself as CUBE (transpose operation is suitable for CUBE +// execution) +// 3. Only mark transpose's input as CUBE seed when its operands come from +// - bufferization dialect ops (e.g., to_tensor, clone), excluding +// AllocTensorOp +// - tensor.empty op +// 4. Check both transpose's operands and DpsInits (output initial values), +// either one +// satisfying the condition is sufficient +// Special Handling +// When transpose's input comes from compute ops (e.g., arith.truncf), +// transpose itself remains CUBE, but its upstream chain stays VECTOR to avoid +// unnecessary CUBE expansion. +// Purpose: transpose is a common preprocessing op for matmul; executing on CUBE +// leverages matrix transfer capabilities. +// ============================================================================ +void OpClassifierPass::matchTransposePattern(Operation *def) { + auto transposeOp = dyn_cast(def); + if (!transposeOp) + return; + + markCube(transposeOp); + + // Helper lambda to check if an operand's defining op qualifies for CUBE seed + auto shouldMarkCubeSeed = [](Operation *opDef) -> bool { + if (!opDef || isExtractedLoadStoreRelated(opDef)) + return false; + return (isa(opDef->getDialect()) && + !isa(opDef)) || + isa(opDef); + }; + + // Check input tensor + auto operands = transposeOp->getOperands(); + for (const auto &op : operands) { + auto defOp = op.getDefiningOp(); + if (!shouldMarkCubeSeed(defOp) || + utils::getAnnotateOpWithAttr(op, + hivm::kMayImplicitTransposeWithLastAxis)) { + continue; + } + if (llvm::isa(defOp)) { + matchToTensorPattern(defOp); + continue; + } + markCube(defOp); + cubeSeeds.push_back(defOp); + } +} + +// ============================================================================ +// Pattern: linalg.fill → matmul (Upstream) +// ============================================================================ +// Matches cases where matmul's output initial value comes from linalg.fill. +// fill initializes the output matrix (typically to 0). +// IR Example +// %value = arith.constant 0.0 : f32 +// %out = tensor.empty() : tensor<1024x1024xf32> +// %init = linalg.fill ins(%value: f32) outs(%out: tensor<1024x1024xf32>) +// %result = linalg.matmul ins(%a, %b) outs(%init) +// Matching Logic +// 1. Check if matmul's operand defining op is linalg.fill +// 2. If matched, mark fill as CUBE and add to cubeSeeds +// Purpose: fill operation initializes matmul's output buffer; executing on CUBE +// leverages efficient data filling capabilities. +// ============================================================================ +void OpClassifierPass::matchFillPattern(Operation *def) { + auto fillOp = dyn_cast(def); + if (!fillOp) + return; + + markCube(fillOp); + cubeSeeds.push_back(fillOp); +} + +// ============================================================================ +// Pattern: tensor.empty → matmul (Upstream) +// ============================================================================ +// Matches cases where matmul's output initial value comes from tensor.empty. +// empty initializes the output matrix (typically to 0). +// IR Example +// %value = arith.constant 0.0 : f32 +// %out = tensor.empty() : tensor<1024x1024xf32> +// %result = linalg.matmul ins(%a, %b) outs(%out) +// Matching Logic +// 1. Check if matmul's operand defining op is tensor.empty +// 2. If matched, mark empty as CUBE and add to cubeSeeds +// Purpose: empty operation initializes matmul's output buffer; +// ============================================================================ +void OpClassifierPass::matchEmptyPattern(Operation *def) { + auto emptyOp = dyn_cast(def); + if (!emptyOp) + return; + + markCube(emptyOp); + cubeSeeds.push_back(emptyOp); +} + +// ============================================================================ +// Pattern: tensor.broadcast → matmul (Upstream) +// ============================================================================ +// Uses bias table buffer to optimize A*B+C. +// %value = arith.constant 0.0 : f32 +// %out = linalg.broadcast ins(%value: f32) outs(%out: tensor<1024x1024xf32>) +// %result = linalg.matmul ins(%a, %b) outs(%out) +// Matching Logic +// 1. Check if matmul's operand defining op is tensor.broadcast +// 2. If matched, mark broadcast as CUBE and add to cubeSeeds +// Purpose: broadcast operation initializes matmul's output buffer; +// ============================================================================ +Value OpClassifierPass::extractMmadBiasFromPotentialUnitDimExpand(Value bias) { + // It assumes that there only exists expand op in mmad bias defining chain, + // while other reshape op like collapse op seems unlikely + if (auto expandShapeOp = bias.getDefiningOp()) { + auto reassociation = expandShapeOp.getReassociationIndices(); + auto expandedShape = expandShapeOp.getResultType().getShape(); + if (llvm::all_of(reassociation, [&expandedShape](ReassociationIndices cur) { + uint32_t nonUnitCount = + llvm::count_if(cur, [&expandedShape](int64_t idx) { + return expandedShape[idx] != 1; + }); + + return nonUnitCount <= 1; + })) { + bias = expandShapeOp.getSrc(); + markCube(expandShapeOp); + cubeSeeds.push_back(expandShapeOp); + inBroadcastChain.insert(expandShapeOp); + } + } + return bias; +} +void OpClassifierPass::matchBroadcastPattern(Operation *def) { + auto broadcastOp = dyn_cast(def); + + if (!broadcastOp) + return; + if (!CVPipeline::allResultHasOneUser(def)) { + return; + } + // Only match small broadcast with correct dimensions (1D->2D, dimensions=[1]) + if (auto btUsage = CVPipeline::getBTSizeFromValidBroadcastOp(broadcastOp)) { + if (btUsage == -1 || btUsage > CVPipeline::CACHE_TABLE_BUFFER_SIZE) { + return; + } + } + + // check if the broadcast used by matmul's outs + for (Operation *user : broadcastOp->getUsers()) { + auto matmulOp = dyn_cast(user); + if (!matmulOp) + continue; + + auto outs = matmulOp.getDpsInits(); + for (Value out : outs) { + if (out.getDefiningOp() != broadcastOp) { + return; + } + } + } + + markCube(broadcastOp); + cubeSeeds.push_back(broadcastOp); + inBroadcastChain.insert(broadcastOp); + + // Maybe need add some extractShape && cast + Value src = broadcastOp.getDpsInputs()[0]; + if (auto expandShapeOp = src.getDefiningOp()) { + src = extractMmadBiasFromPotentialUnitDimExpand(src); + } + + if (auto castOp = src.getDefiningOp()) { + if (getElementTypeOrSelf(castOp.getIn().getType()).isF16() && + getElementTypeOrSelf(castOp.getResult().getType()).isF32()) { + src = castOp.getIn(); + markCube(castOp); + cubeSeeds.push_back(castOp); + inBroadcastChain.insert(castOp); + } + } + if (auto expandShapeOp = src.getDefiningOp()) { + src = extractMmadBiasFromPotentialUnitDimExpand(src); + } +} + +// ============================================================================ +// Pattern: matmul → hivm.hir.store (Downstream) +// ============================================================================ +// Matches cases where matmul's output is directly stored to memory. +// hivm.store is the HIVM dialect's store operation, writing tensor to memory. +// IR Example +// %result = linalg.matmul ins(%a, %b) outs(%init) +// hivm.store %result, %memref[%offset] : tensor<1024x1024xf32> to memref +// Matching Logic +// 1. Check if matmul result's user is hivm.store +// 2. If matched, mark store as CUBE and add to cubeSeeds +// Purpose: Ensure matmul result's store operation executes on CUBE for +// efficient matrix data write-back. +// ============================================================================ +void OpClassifierPass::matchStorePattern(Operation *user) { + if (!isa(user)) + return; + + markCube(user); + cubeSeeds.push_back(user); +} + +// ============================================================================ +// Pattern: matmul → tensor.extract_slice → hivm.store (Downstream) +// ============================================================================ +// Matches cases where matmul's output is sliced before being stored. +// extract_slice extracts a sub-region (slice) of a tensor, commonly used after +// tiled matrix computation for result extraction. +// IR Example +// %result = linalg.matmul ins(%a, %b) outs(%init) : tensor<1024x1024xf32> +// %slice = tensor.extract_slice %result[0, 0][256, 256][1, 1] : +// tensor<1024x1024xf32> to tensor<256x256xf32> hivm.store %slice, +// %memref[%offset] : tensor<256x256xf32> to memref +// Matching Logic +// 1. Check if matmul result's user is tensor.extract_slice +// 2. If matched, mark extract_slice as CUBE and add to cubeSeeds +// 3. Further check all users of extract_slice and mark hivm.store as CUBE +// Purpose: After tiled matrix computation, slice and store operations on CUBE +// improve local data transfer efficiency. +// ============================================================================ +void OpClassifierPass::matchExtractSlicePattern(Operation *user) { + auto extractSliceOp = dyn_cast(user); + if (!extractSliceOp) + return; + + markCube(extractSliceOp); + cubeSeeds.push_back(extractSliceOp); + // Also mark downstream hivm.hir.store as CUBE + for (Operation *sliceUser : extractSliceOp->getUsers()) { + if (isa(sliceUser) || + isa(sliceUser)) { + markCube(sliceUser); + cubeSeeds.push_back(sliceUser); + } + } +} + +// ============================================================================ +// Pattern: matmul → materialize_in_destination (Downstream) +// ============================================================================ +// Matches cases where matmul's output is written to a destination buffer via +// materialize_in_destination. This operation materializes tensor results into a +// pre-allocated destination location. +// IR Example +// %result = linalg.matmul ins(%a, %b) outs(%init) : tensor<1024x1024xf32> +// %memref = memref.alloc() : memref<1024x1024xf32> +// bufferization.materialize_in_destination %result in %memref +// : tensor<1024x1024xf32> to memref<1024x1024xf32> +// Matching Logic +// 1. Check if matmul result's user is +// bufferization.materialize_in_destination +// 2. If matched, mark it as CUBE and add to cubeSeeds +// Purpose: matmul result materialization on CUBE leverages efficient matrix +// data transfer capabilities. +// ============================================================================ +void OpClassifierPass::matchMaterializePattern(Operation *user) { + if (!isa(user)) + return; + + markCube(user); + cubeSeeds.push_back(user); +} + +// Pattern matching for CUBE operations +int OpClassifierPass::patternMatchCUBE() { + LOG_DEBUG("--- Step 1: pattern match --->\n"); + + for (Operation *op : allOps) { + if (!isa(op)) + continue; + + // matmul always CUBE + opCoreTypes[op] = OP_CUBE_ONLY; + LLVM_DEBUG(DBGS() << "CUBE (matmul): " << *op << "\n"); + + // ---- Upstream pattern matching ---- + for (Value operand : op->getOperands()) { + Operation *def = operand.getDefiningOp(); + // A null defining op means `operand` is a loop iter_arg block argument. + // Walk back to its init operand (repeating to also cross nested loops). + for (Value cur = operand; !def;) { + auto blockArg = dyn_cast(cur); + auto loopLike = blockArg ? dyn_cast_or_null( + blockArg.getOwner()->getParentOp()) + : nullptr; + OpOperand *init = + loopLike ? loopLike.getTiedLoopInit(blockArg) : nullptr; + if (!init) { + break; + } + cur = init->get(); + def = cur.getDefiningOp(); + } + if (!def) + continue; + + matchToTensorPattern(def); + matchTransposePattern(def); + matchFillPattern(def); + matchEmptyPattern(def); + matchBroadcastPattern(def); + } + + // ---- Downstream pattern matching ---- + for (Value result : op->getResults()) { + if (!result.hasOneUse()) { + continue; + } + for (Operation *user : result.getUsers()) { + // If user is scf.yield, follow the chain to find real users + Operation *curUser = user; + Value prevResult = result; + while (curUser) { + if (!CVPipeline::allResultHasOneUser(curUser)) { + break; + } + if (auto yieldOp = dyn_cast(curUser)) { + if (Operation *scfOp = yieldOp->getParentOp()) { + // Find which operand index the previous result corresponds to in + // the yield + unsigned yieldOperandIdx = 0; + for (unsigned i = 0; i < yieldOp->getNumOperands(); ++i) { + if (yieldOp->getOperand(i) == prevResult) { + yieldOperandIdx = i; + break; + } + } + // Get the corresponding scf result + if (yieldOperandIdx < scfOp->getNumResults()) { + Value scfResult = scfOp->getResult(yieldOperandIdx); + prevResult = scfResult; + // Find the next user (skipping yield) + curUser = nullptr; + for (Operation *nextUser : scfResult.getUsers()) { + if (!isa(nextUser)) { + curUser = nextUser; + break; + } + } + // If no non-yield user found, continue searching from yield + if (!curUser) { + for (Operation *nextUser : scfResult.getUsers()) { + if (isa(nextUser)) { + curUser = nextUser; + break; + } + } + } + continue; + } + } + break; + } + matchStorePattern(curUser); + matchExtractSlicePattern(curUser); + matchMaterializePattern(curUser); + break; + } + } + } + } + + LLVM_DEBUG(DBGS() << "seeds: " << cubeSeeds.size() << " to_tensor(s)\n"); + for (Operation *seed : cubeSeeds) { + LLVM_DEBUG(DBGS() << "CUBE: " << *seed << "\n"); + } + + return 0; +} + +// Helper: Find iter_arg initialization op and yield-assigning op for scf.for +// or scf.while loop-carried scalar. When a def comes from an scf.for/scf.while +// iter_arg and is a scalar compute op, we need to: +// 1. Find the iter_arg's initialization op (the op that provides the initial +// value) +// 2. Find the yieldOp, then trace to the op that provides the yielded value +// All found ops are added to upstreamOps +static void +findIterArgUpstreamOps(Value def, + llvm::SmallVectorImpl &upstreamOps) { + // Check if def is a block argument (iter_arg) + auto blockArg = dyn_cast(def); + if (!blockArg) + return; + + Operation *parentOp = blockArg.getOwner()->getParentOp(); + if (!isa(parentOp)) + return; + + // Get the iter_arg index from block argument + unsigned argIdx = blockArg.getArgNumber(); + auto inits = getLoopInitValues(parentOp); + // For scf.for: argIdx 0 is lb (loop lower bound), not an iter_arg, skip it. + // For scf.while: argIdx 0 can be a valid iter_arg, don't skip. + if (argIdx >= inits.size() || (isa(parentOp) && argIdx == 0)) + return; + + Value initValue = inits[argIdx - (isa(parentOp) ? 1 : 0)]; + if (!isScalarType(initValue)) + return; + + Operation *initDef = initValue.getDefiningOp(); + if (initDef && initDef != parentOp) { + LLVM_DEBUG(DBGS() << "[findIterArgUpstreamOps] init def: " << *initDef + << "\n"); + upstreamOps.push_back(initDef); + } + + // For scf.while, iter args in after region are 0-indexed in both inits and + // yield For scf.for, iter args are 1-indexed in initArgs (arg 0 is lb) and + // 1-indexed in yield (arg 0 is lb) + unsigned yieldOperandIdx = (isa(parentOp)) ? argIdx - 1 : argIdx; + + Operation *yieldOp = getLoopYieldOp(parentOp); + if (!yieldOp || !isa(yieldOp) || + yieldOperandIdx >= yieldOp->getNumOperands()) + return; + + Value yieldedValue = yieldOp->getOperand(yieldOperandIdx); + Operation *yieldedDef = yieldedValue.getDefiningOp(); + if (yieldedDef && yieldedDef != parentOp) { + LLVM_DEBUG(DBGS() << "[findIterArgUpstreamOps] yielded def: " << *yieldedDef + << "\n"); + upstreamOps.push_back(yieldedDef); + } +} + +// Helper function to get upstream operations based on both SSA and memory +// dependencies +void OpClassifierPass::getUpstreamOpsWithMemoryDeps( + Operation *cur, llvm::SmallVectorImpl &upstreamOps) { + // Collect SSA dependencies (direct operands) + for (Value operand : cur->getOperands()) { + Operation *def = operand.getDefiningOp(); + if (def && def != cur) { + // Check if this def is an scf.for iter_arg that is a scalar + LLVM_DEBUG(DBGS() << "push op: " << *def << "\n"); + upstreamOps.push_back(def); + } + if (isScalarIterArgOp(operand)) { + findIterArgUpstreamOps(operand, upstreamOps); + } + } + if (!isa(cur)) { + // Only consider memory dependencies for non-to_tensor ops, + // as to_tensor already has SSA deps to its memref source + return; + } + // Collect memory dependencies + if (memDepGraph) { + // Get operations that define memory used by current op + for (Operation *memDef : memDepGraph->getMemDefs(cur)) { + LLVM_DEBUG(DBGS() << "memDef: cur " << *cur << " -> memDef " << *memDef + << "\n"); + if (isa(memDef)) { + LLVM_DEBUG(DBGS() << "push op: " << *memDef << "\n"); + upstreamOps.push_back(memDef); + } + } + } +} + +// arith/math op with a tensor result is VECTOR-only (not CUBE). +static bool isTensorArithOrMathOp(Operation *op) { + if (!isa(op->getDialect())) { + return false; + } + for (Value result : op->getResults()) { + if (isa(result.getType())) { + return true; + } + } + return false; +} + +// True if ANY op in the defining chain is VECTOR-only. Conservative by +// design: once a chain contains a VECTOR-only op the value (and any extract of +// it) cannot be recomputed on CUBE, so mixed VECTOR-only + CUBE producers still +// classify as VECTOR. Memoized in `cache` so a tensor extracted from multiple +// times is not re-walked. +static bool hasVectorOnlyProducer(Value value, + llvm::DenseMap &cache) { + auto it = cache.find(value); + if (it != cache.end()) { + return it->second; + } + + llvm::SmallVector worklist{value}; + llvm::DenseSet visited; + bool result = false; + while (!worklist.empty()) { + Value cur = worklist.pop_back_val(); + Operation *defOp = cur.getDefiningOp(); + if (!defOp || !visited.insert(defOp).second) { + continue; + } + if (CVPipeline::isVectorOnlyOp(defOp)) { + result = true; + break; + } + for (Value operand : defOp->getOperands()) { + worklist.push_back(operand); + } + } + cache[value] = result; + return result; +} + +// Shared skip predicates for both CUBE upstream BFS paths. +bool OpClassifierPass::shouldSkipCubeUpstream(Operation *op) { + if (isTensorArithOrMathOp(op)) { + LLVM_DEBUG(DBGS() << "skip " << op->getName().getStringRef() + << ": arith/math tensor op\n"); + return true; + } + if (auto extOp = dyn_cast(op)) { + if (hasVectorOnlyProducer(extOp.getTensor(), vectorOnlyProducerCache)) { + LLVM_DEBUG(DBGS() << "skip " << op->getName().getStringRef() + << ": extract of vector-only producer\n"); + return true; + } + } + if (isInsideNestedLinalgRegion(op)) { + LLVM_DEBUG(DBGS() << "skip " << op->getName().getStringRef() + << ": inside linalg block\n"); + return true; + } + if (isExtractedLoadStoreRelated(op)) { + LOG_DEBUG("skip " << *op << ": Extracted Load/Store Related"); + return true; + } + return false; +} + +// Propagate CUBE core type upstream +int OpClassifierPass::propagateCubeUpstream() { + LLVM_DEBUG(DBGS() << "--- Step 2: CUBE upstream BFS --->\n"); + llvm::DenseSet cubeVisited; + std::queue cubeQueue; + + for (Operation *seed : cubeSeeds) { + cubeVisited.insert(seed); + cubeQueue.push(seed); + } + + while (!cubeQueue.empty()) { + Operation *cur = cubeQueue.front(); + cubeQueue.pop(); + LLVM_DEBUG(DBGS() << "cur: " << *cur << "\n"); + if (inBroadcastChain.contains(cur)) { + continue; + } + // Get upstream operations considering both SSA and memory dependencies + llvm::SmallVector upstreamOps; + getUpstreamOpsWithMemoryDeps(cur, upstreamOps); + + for (Operation *def : upstreamOps) { + if (!def || cubeVisited.count(def) || isa(def)) + continue; + + // Shared skip rules (see shouldSkipCubeUpstream). + if (shouldSkipCubeUpstream(def)) + continue; + + cubeVisited.insert(def); + + LLVM_DEBUG(DBGS() << "\tcolor-cube: " << def->getName().getStringRef() + << "\n"); + opCoreTypes[def] = OP_CUBE_ONLY; + cubeQueue.push(def); + } + } + + // Handle fill ops: mark as CUBE only if linalg.fill output buffer is CUBE + markFillOpsAsCube(); + + return 0; +} + +// ============================================================================ +// Step 3: Mark remaining operations as VECTOR +// ============================================================================ +// After Step 2 (CUBE propagation), any operation still marked as UNDETERMINED +// is classified as VECTOR. This establishes the default that non-CUBE +// operations should execute on the VECTOR core. +// ============================================================================ +int OpClassifierPass::markRemainingAsVector() { + LLVM_DEBUG(DBGS() << "--- Step 3: default VECTOR --->\n"); + for (Operation *op : allOps) { + if (isa(op)) { + auto markOp = cast(op); + Value src = markOp.getSrc(); + if (Operation *srcDef = src.getDefiningOp()) { + opCoreTypes[op] = getCoreType(srcDef); + } else { + opCoreTypes[op] = OP_VECTOR_ONLY; + } + continue; + } + + if (opCoreTypes[op] == OP_UNDETERMINED && !isa(op)) { + opCoreTypes[op] = OP_VECTOR_ONLY; + } + } + + return 0; +} + +// ============================================================================ +// Helper: Mark fill operations as CUBE when their output buffer is CUBE +// ============================================================================ +// This function handles a special case where linalg.fill's result feeds into +// a CUBE operation. In such cases, the fill itself should be marked CUBE. +// Case 1: fill's outs operand is defined by a CUBE_ONLY operation (not +// tensor.empty) +// IR example +// %empty = tensor.empty() : tensor<1024x1024xf32> // CUBE +// %filled = linalg.fill ins(%value) outs(%empty) // should be CUBE +// Here %filled's outs comes from a CUBE op, so fill is CUBE +// Case 2: fill's outs is a BlockArgument (scf.for/scf.if iter_arg) that is CUBE +// IR example +// scf.for ... iter_args(%arg = %filled) { // %arg is CUBE +// ... +// } +// Here the iter_arg is CUBE, so fill is CUBE +// ============================================================================ +void OpClassifierPass::markFillOpsAsCube() { + LLVM_DEBUG(DBGS() << "--- Marking fill ops as CUBE when outs is CUBE --->\n"); + + for (Operation *op : allOps) { + // Only process fill ops that are still UNDETERMINED + if (!isa(op) || opCoreTypes[op] != OP_UNDETERMINED) { + continue; + } + + auto fillOp = cast(op); + Value outs = fillOp.getDpsInits()[0]; + Operation *outsDef = outs.getDefiningOp(); + + bool outsIsCube = false; + + // Case 1: outs is defined by a CUBE_ONLY operation (not tensor.empty) + // tensor.empty is a special case - it doesn't determine CUBE/VECTOR + if (outsDef && opCoreTypes[outsDef] == OP_CUBE_ONLY && + !isa(outsDef)) { + outsIsCube = true; + LLVM_DEBUG(DBGS() << "\tfill outs defined by CUBE op: " + << outsDef->getName().getStringRef() << "\n"); + } else if (!outsDef) { // Case 2: outs is a BlockArgument + // (scf.for/scf.if/scf.while iter_arg) + auto blockArg = dyn_cast(outs); + if (blockArg) { + Operation *parentOp = blockArg.getOwner()->getParentOp(); + // Check if it's an scf.for, scf.if, or scf.while iter_arg that is CUBE + if ((isa(parentOp) || isa(parentOp) || + isa(parentOp)) && + opCoreTypes[parentOp] == OP_CUBE_ONLY) { + outsIsCube = true; + LLVM_DEBUG(DBGS() << "\tfill outs is CUBE iter_arg of: " + << parentOp->getName().getStringRef() << "\n"); + } + } + } + + if (outsIsCube) { + LLVM_DEBUG(DBGS() << "\tfill-cube (outs is CUBE): " << *op << "\n"); + opCoreTypes[op] = OP_CUBE_ONLY; + } + + // Case 3: handle fill op in scf.if with all CUBE ops, Vector ops are + // automatically marked as vector. + handleFillInScfIf(op); + } +} + +// Helper: Handle fill op in scf.if - if all ops in scf.if are CUBE, mark scf.if +// and propagate upstream +void OpClassifierPass::handleFillInScfIf(Operation *fillOp) { + Operation *parentOp = fillOp->getParentOp(); + auto ifOp = dyn_cast(parentOp); + if (!ifOp) + return; + + // Only handle scf.if used for conditional branching (no iter_args) + // If scf.if has results (iter_args), it's used as a loop structure, skip it + if (!ifOp.getResults().empty()) + return; + + // Check if all ops in scf.if's blocks are CUBE (except yield terminator) + bool allOpsAreCube = true; + for (Region ®ion : ifOp->getRegions()) { + for (Block &block : region) { + for (Operation &innerOp : block.getOperations()) { + if (isa(innerOp)) + continue; + // Check if this op is marked as CUBE + if (opCoreTypes[&innerOp] != OP_CUBE_ONLY) { + allOpsAreCube = false; + break; + } + } + if (!allOpsAreCube) + break; + } + if (!allOpsAreCube) + break; + } + + if (allOpsAreCube) { + LLVM_DEBUG( + DBGS() << "\tfill in scf.if with all CUBE ops, mark scf.if as CUBE: " + << *ifOp << "\n"); + opCoreTypes[ifOp] = OP_CUBE_ONLY; + // Propagate CUBE upstream for the scf.if + propagateCubeUpstreamForOp(ifOp); + } +} + +// Helper: Propagate CUBE core type upstream for a given operation +void OpClassifierPass::propagateCubeUpstreamForOp(Operation *startOp) { + std::queue cubeQueue; + llvm::DenseSet cubeVisited; + cubeVisited.insert(startOp); + cubeQueue.push(startOp); + + while (!cubeQueue.empty()) { + Operation *cur = cubeQueue.front(); + cubeQueue.pop(); + + // Get upstream operations + llvm::SmallVector upstreamOps; + getUpstreamOpsWithMemoryDeps(cur, upstreamOps); + + for (Operation *upstreamOp : upstreamOps) { + if (!upstreamOp || cubeVisited.count(upstreamOp)) + continue; + if (isa(upstreamOp)) + continue; + // Same skip rules as the main CUBE BFS. + if (shouldSkipCubeUpstream(upstreamOp)) + continue; + + cubeVisited.insert(upstreamOp); + LLVM_DEBUG(DBGS() << "\t\tcube upstream: " + << upstreamOp->getName().getStringRef() << "\n"); + opCoreTypes[upstreamOp] = OP_CUBE_ONLY; + cubeQueue.push(upstreamOp); + } + } +} + +// ============================================================================ +// Step 4: Propagate VECTOR core type upstream +// ============================================================================ +// After Step 3, propagate VECTOR classification to upstream operations. +// BFS traversal from all VECTOR operations, marking their upstream dependencies +// as VECTOR unless they are CUBE-only operations (e.g., matmul). +// Skips builtin.module and func.func operations. +// ============================================================================ +int OpClassifierPass::propagateVectorUpstream() { + LLVM_DEBUG(DBGS() << "--- Step 4: VECTOR upstream BFS --->\n"); + llvm::DenseSet vecVisited; + std::queue vecQueue; + + for (Operation *op : allOps) { + if (opCoreTypes[op] == OP_VECTOR_ONLY) { + vecVisited.insert(op); + vecQueue.push(op); + } + } + + while (!vecQueue.empty()) { + Operation *cur = vecQueue.front(); + vecQueue.pop(); + // Skip builtin.module, func.func, and func.return operations + if (isa(cur) || + (isa(cur) && + opCoreTypes[cur] == OP_CUBE_ONLY)) { + continue; + } + LLVM_DEBUG(DBGS() << "vec-def: " << *cur << "\n"); + + // Get upstream operations considering both SSA and memory dependencies + llvm::SmallVector upstreamOps; + getUpstreamOpsWithMemoryDeps(cur, upstreamOps); + + for (Operation *def : upstreamOps) { + if (!def || vecVisited.count(def)) + continue; + + // Skip operations that should not be marked VECTOR: + // - matmul: never mark matmul as vector (CUBE-only operation) + if (isa(def) || + isa(def->getDialect())) { + LLVM_DEBUG(DBGS() << "skip " << def->getName().getStringRef() + << ": should not be marked VECTOR\n"); + continue; + } + + vecVisited.insert(def); + + LLVM_DEBUG(DBGS() << "\tcolor-vec: " << def->getName().getStringRef() + << "\n"); + opCoreTypes[def] = + static_cast(opCoreTypes[def] | OP_VECTOR_ONLY); + vecQueue.push(def); + } + } + + return 0; +} + +namespace { + +Value getAliasSource(Value val) { + auto *op = val.getDefiningOp(); + if (!op) { + return nullptr; + } + return llvm::TypeSwitch(op) + .Case([](ViewLikeOpInterface viewOp) { return viewOp.getViewSource(); }) + .Case([](bufferization::ToTensorOp totensorOp) { + return totensorOp.getBuffer(); + }) + .Default([](auto) { return nullptr; }); +} + +Value getViewSource(Value val) { + while (auto source = getAliasSource(val)) { + val = source; + } + return val; +} + +// A loader loop may only contain data-movement and (scalar) index computation. +// Any tensor-producing compute op means the loop does real vector work and must +// not be swallowed into the cube pipe. +bool isDisqualifyingLoaderOp(Operation *op) { + if (isa(op)) { + return true; + } + if (isa(op->getDialect())) { + return true; + } + // Elementwise arith on tensors is vector compute; scalar index math is fine. + if (isa(op->getDialect())) { + for (Value result : op->getResults()) { + if (isa(result.getType())) { + return true; + } + } + } + if (auto memInt = llvm::dyn_cast(op)) { + // No matmul - must not be store to gm + llvm::SmallVector effects; + memInt.getEffects(effects); + for (auto effect : effects) { + Value val = effect.getValue(); + if (!val || !isa(effect.getEffect())) { + continue; + } + val = getViewSource(val); + if (auto barg = llvm::dyn_cast(val)) { + if (llvm::isa(barg.getOwner()->getParentOp())) { + return true; + } + } + } + } + return false; +} + +} // namespace + +bool OpClassifierPass::isCubeLoaderForOp(scf::ForOp forOp) { + if (forOp->hasAttr(hivm::ExtractLoadStoreAttr)) + return false; + + // Every result must be live and used only by CUBE consumers + if (forOp.getNumResults() == 0) { + return false; + } + for (Value result : forOp.getResults()) { + for (Operation *user : result.getUsers()) { + if (isa(user) || getCoreType(user) == OP_CUBE_ONLY) + continue; + return false; + } + } + // Body must be pure data movement + bool disqualified = false; + forOp.getBody()->walk([&](Operation *op) { + if (op == forOp.getOperation()) + return WalkResult::advance(); + if (isDisqualifyingLoaderOp(op)) { + disqualified = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return !disqualified; +} + +bool OpClassifierPass::isCubeLoaderForWhileOp(scf::WhileOp whileOp) { + // Every result must be live and used only by CUBE consumers + for (Value result : whileOp.getResults()) { + for (Operation *user : result.getUsers()) { + if (isa(user) || getCoreType(user) == OP_CUBE_ONLY) + continue; + return false; + } + } + // After body must be pure data movement + bool disqualified = false; + whileOp.getAfterBody()->walk([&](Operation *op) { + if (op == whileOp.getOperation()) + return WalkResult::advance(); + if (isDisqualifyingLoaderOp(op)) { + disqualified = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return !disqualified; +} + +// Helper: mark all ops in a loop body as CUBE_ONLY +static void +markLoopBodyAsCube(Operation *loop, Block *body, + llvm::DenseMap &opCoreTypes) { + body->walk([&](Operation *op) { + if (op == loop || isa(op->getDialect())) + return; + auto it = opCoreTypes.find(op); + if (it != opCoreTypes.end()) + it->second = OP_CUBE_ONLY; + }); + opCoreTypes[loop] = OP_CUBE_ONLY; +} + +llvm::LogicalResult OpClassifierPass::penetrateCubeIntoForLoops() { + // Collect first so recoloring earlier loops cannot perturb the scan. + // Use a combined walk to collect both ForOps and WhileOps + llvm::SmallVector loaderLoops; + getOperation().walk([&](scf::ForOp forOp) { + if (isCubeLoaderForOp(forOp)) + loaderLoops.push_back(forOp); + }); + getOperation().walk([&](scf::WhileOp whileOp) { + if (isCubeLoaderForWhileOp(whileOp)) + loaderLoops.push_back(whileOp); + }); + + for (Operation *loop : loaderLoops) { + Block *body = getLoopBodyBlock(loop); + if (body) + markLoopBodyAsCube(loop, body, opCoreTypes); + } + + return llvm::success(); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +OpCoreType OpClassifierPass::getCoreType(Operation *op) const { + auto it = opCoreTypes.find(op); + if (it != opCoreTypes.end()) { + return it->second; + } + return OP_VECTOR_ONLY; +} + +void OpClassifierPass::setCoreType(Operation *op, OpCoreType coreType) { + opCoreTypes[op] = coreType; +} + +std::string +OpClassifierPass::joinCoreTypes(const std::vector &coreTypes) { + std::string coreTypeStr; + for (size_t i = 0; i < coreTypes.size(); ++i) { + if (i > 0) { + coreTypeStr += ", "; + } + coreTypeStr += coreTypeToString(coreTypes[i]); + } + return coreTypeStr; +} + +// ============================================================================ +// Step 5: SCF Yield Results Handling +// ============================================================================ + +bool OpClassifierPass::shouldStopPropagationForYield( + Operation *op, OpCoreType targetCoreType) { + // Stop at linalg.matmul + if (isa(op)) { + return true; + } + + // Stop at hivm.hir.store + if (isa(op)) { + return true; + } + + // Stop at tensor-type operations that are already VECTOR_ONLY when targeting + // CUBE + if (targetCoreType == OP_CUBE_ONLY) { + if (op->getNumResults() > 0) { + for (Value result : op->getResults()) { + if (isa(result.getType())) { + OpCoreType currentType = getCoreType(op); + if (currentType == OP_VECTOR_ONLY) { + return true; + } + } + } + } + } + + // Stop at scf dialect operations + if (isa(op->getDialect())) { + return true; + } + + // Stop at func.func or builtin.module + if (isa(op)) { + return true; + } + + return false; +} + +void OpClassifierPass::propagateCoreTypeUpwardForYield( + Operation *startOp, OpCoreType targetCoreType) { + if (!startOp) + return; + + llvm::DenseSet visited; + std::queue queue; + queue.push(startOp); + visited.insert(startOp); + + while (!queue.empty()) { + Operation *cur = queue.front(); + queue.pop(); + + if (!cur || !cur->getBlock()) + continue; + + if (shouldStopPropagationForYield(cur, targetCoreType)) + continue; + + opCoreTypes[cur] = targetCoreType; + + // Propagate to operands (upstream) + for (Value operand : cur->getOperands()) { + if (Operation *defOp = operand.getDefiningOp()) { + if (!visited.count(defOp) && defOp != cur) { + visited.insert(defOp); + queue.push(defOp); + } + } + } + + // Also propagate to memory dependencies + if (memDepGraph) { + for (Operation *memDef : memDepGraph->getMemDefs(cur)) { + if (!visited.count(memDef) && memDef != cur) { + visited.insert(memDef); + queue.push(memDef); + } + } + } + } +} + +// handleYieldFromElseRegion: Extracts core_type from then region yield for else +// region processing. For scf.if with else region, when processing the else +// region's yield terminator, we need to use the then region's yield's core_type +// attribute as the authoritative source. If the then yield has a +// comma-separated multi-value type string (e.g., "CUBE,VECTOR"), this function +// extracts the i-th component for the operand at index i. +// Parameters +// - coreTypes: Vector to append the extracted core type to +// - operandIndex: Index of the operand in the yield (for extracting +// multi-value types) +// - thenYieldForElse: The then region's yield operation (from scf.if) +// - operand: The current operand value from the else region's yield +// Returns: true if core_type was successfully extracted and propagated, false +// otherwise. +bool OpClassifierPass::handleYieldFromElseRegion( + std::vector &coreTypes, unsigned operandIndex, + Operation *thenYieldForElse, Value &operand, Operation *elseYieldOp) { + // Only handle if thenYieldForElse is provided and is a scf.yield + if (!thenYieldForElse || !isa(thenYieldForElse)) { + return false; + } + + // Get the core_type attribute from then region yield + auto thenCoreTypeAttr = thenYieldForElse->getAttr("ssbuffer.core_type"); + if (!thenCoreTypeAttr) { + return false; + } + + StringAttr thenCoreTypeStrAttr = dyn_cast(thenCoreTypeAttr); + if (!thenCoreTypeStrAttr) { + return false; + } + + // Extract the i-th component from comma-separated core type string + std::string thenCoreTypeStr = thenCoreTypeStrAttr.getValue().str(); + OpCoreType coreTypeToUse = + parseCoreTypeFromString(thenCoreTypeStr, operandIndex); + coreTypes.push_back(coreTypeToUse); + + // Propagate the determined core_type upstream to the defining operation + if (Operation *defOp = operand.getDefiningOp()) { + if (CloneOpMap.count(defOp) && opCoreTypes.count(defOp) && + opCoreTypes[defOp] != coreTypeToUse) { + Operation *cloneOp = CloneOpMap[defOp]; + // Replace the operand in else yield with the cloneOp's result + for (unsigned i = 0; i < defOp->getNumResults(); ++i) { + if (defOp->getResult(i) == operand) { + elseYieldOp->setOperand(operandIndex, cloneOp->getResult(i)); + defOp = cloneOp; + break; + } + } + } + propagateCoreTypeUpwardForYield(defOp, coreTypeToUse); + } + + return true; +} + +// ============================================================================ +// processYieldOperation: Set core_type on scf.yield and its parent scf op +// ============================================================================ +// For each operand of scf.yield: +// - If thenYieldForElse is provided (else region of scf.if): use then yield's +// per-operand core_type as reference to determine this operand's type, and +// propagate that type upstream. +// - Otherwise (then region of scf.if, or scf.for): look at the operand's +// defining op's core_type. If it is OP_CUBE_ONLY -> "CUBE", else -> +// "VECTOR". BlockArguments without a defining op (e.g., iter_arg block +// argument) default to VECTOR. +// After collecting all operand types, set ssbuffer.core_type on both the yield +// op and its parent scf operation (scf.if or scf.for). +void OpClassifierPass::processYieldOperation(Operation *op, + Operation *thenYieldForElse) { + std::vector coreTypes; + + // Get the parent SCF operation (scf.if or scf.for) + Operation *parentOp = op->getParentOp(); + + // Process each yield operand in order + if (op->getNumOperands() > 0) { + for (unsigned i = 0; i < op->getNumOperands(); ++i) { + Value operand = op->getOperand(i); + // --------------------------------------------------------------- + // Case 1: else region yield of scf.if + // --------------------------------------------------------------- + // Use then region yield's core_type attribute to determine type. + // If then yield has a comma-separated multi-value type string (e.g. + // "CUBE,VECTOR"), extract the i-th component for this operand. + if (handleYieldFromElseRegion(coreTypes, i, thenYieldForElse, operand, + op)) { + continue; + } + + if (Operation *defOp = operand.getDefiningOp()) { + if (CloneOpMap.count(defOp) && opCoreTypes.count(defOp) && + opCoreTypes[defOp] == OP_CUBE_ONLY) { + Operation *cloneOp = CloneOpMap[defOp]; + // Replace the operand in else yield with the cloneOp's result + for (unsigned j = 0; j < defOp->getNumResults(); ++j) { + if (defOp->getResult(j) == operand && + j < cloneOp->getNumResults()) { + op->setOperand(i, cloneOp->getResult(j)); + defOp = cloneOp; + operand = op->getOperand(i); + break; + } + } + } + } + + // --------------------------------------------------------------- + // Case 2: then region yield of scf.if, or scf.for yield + // --------------------------------------------------------------- + // Determine type from the operand's defining operation: + // - OP_CUBE_ONLY -> yield type is "CUBE" + // - anything else (OP_UNDETERMINED, OP_VECTOR_ONLY) -> "VECTOR" + // If the operand is a BlockArgument without a defining op (e.g., an + // iter_arg that is passed through without modification), default to + // VECTOR since no compute op is associated with it. + if (Operation *def = operand.getDefiningOp()) { + OpCoreType ct = getCoreType(def); + if (ct == OP_CUBE_ONLY) { + coreTypes.push_back(OP_CUBE_ONLY); + } else { + coreTypes.push_back(OP_VECTOR_ONLY); + } + } else { + // BlockArgument (e.g., iter_arg passed through scf.for) -> VECTOR + coreTypes.push_back(OP_VECTOR_ONLY); + } + } + } + + // Write ssbuffer.core_type attribute onto the yield op and its parent scf op + if (!coreTypes.empty()) { + std::string coreTypeStr = joinCoreTypes(coreTypes); + op->setAttr("ssbuffer.core_type", + StringAttr::get(op->getContext(), coreTypeStr)); + + OpCoreType opCt = (coreTypeStr.find("CUBE") != std::string::npos) + ? OP_CUBE_ONLY + : OP_VECTOR_ONLY; + opCoreTypes[op] = opCt; + + if (parentOp && llvm::isa(parentOp->getDialect())) { + parentOp->setAttr("ssbuffer.core_type", + StringAttr::get(parentOp->getContext(), coreTypeStr)); + opCoreTypes[parentOp] = opCt; + } + } +} + +// ============================================================================ +// Step 5: Process SCF Yield Operations +// ============================================================================ +// Entry point for yield processing. Walks all scf ops and sets core_type on +// their yield terminators and on the ops themselves. +// Processing order +// 1. All scf.if ops: process then-region yield first (no thenYieldForElse), +// then process else-region yield (passing thenYieldForElse for type +// reference). +// 2. Remaining scf.yield ops not inside scf.if (e.g., scf.for terminators). +// After this step, every scf.yield and its parent scf.if/scf.for carry a +// ssbuffer.core_type attribute reflecting the core_type of their result +// values (derived from the defining ops of each yield operand). +int OpClassifierPass::handleSCFYield() { + // Collect all scf.if operations + llvm::SmallVector ifOps; + for (Operation *op : allOps) { + if (auto ifOp = dyn_cast(op)) { + ifOps.push_back(ifOp); + } + } + + // Process scf.if operations: first then region, then else region + for (scf::IfOp ifOp : ifOps) { + Operation *thenYield = nullptr; + if (!ifOp.getThenRegion().empty()) { + thenYield = ifOp.getThenRegion().back().getTerminator(); + } + + if (isa(thenYield)) { + processYieldOperation(thenYield, nullptr); + } + + if (!ifOp.getElseRegion().empty()) { + Operation *elseYield = ifOp.getElseRegion().back().getTerminator(); + if (isa(elseYield)) { + processYieldOperation(elseYield, thenYield); + } + } + } + + // Process remaining scf.yield operations (not inside scf.if, e.g. scf.for) + for (Operation *op : allOps) { + if (!isa(op)) + continue; + + Operation *parentOp = op->getParentOp(); + if (dyn_cast_or_null(parentOp)) + continue; + + processYieldOperation(op, nullptr); + } + + return 0; +} + +OpCoreType OpClassifierPass::getForInitCoreType(OpOperand *operand) const { + // Unified handling for scf.for and scf.while using the tied loop interface + Operation *owner = operand->getOwner(); + + if (auto forOp = dyn_cast(owner)) { + auto iterArg = forOp.getTiedLoopRegionIterArg(operand); + if (!iterArg) + return OP_UNDETERMINED; + auto sourceOperand = forOp.getTiedLoopYieldedValue(iterArg); + if (!sourceOperand) + return OP_UNDETERMINED; + auto defOp = sourceOperand->get().getDefiningOp(); + if (!defOp) + return OP_UNDETERMINED; + return getCoreType(defOp); + } + + if (auto whileOp = dyn_cast(owner)) { + auto iterArg = whileOp.getTiedLoopRegionIterArg(operand); + if (!iterArg) + return OP_UNDETERMINED; + auto sourceOperand = whileOp.getTiedLoopYieldedValue(iterArg); + if (!sourceOperand) + return OP_UNDETERMINED; + auto defOp = sourceOperand->get().getDefiningOp(); + if (!defOp) + return OP_UNDETERMINED; + return getCoreType(defOp); + } + + return OP_UNDETERMINED; +} + +// ============================================================================ +// Step 7: CUBE_AND_VECTOR Operation Handling +// ============================================================================ +// Problem: an operation (e.g., linalg.fill) is used by both CUBE users +// (linalg.matmul) and VECTOR users (arith.addf) simultaneously. +// Both cores cannot share the same op instance. +// Solution: clone the op into two versions. +// - Original op → reclassified as OP_CUBE_ONLY → CUBE users keep using it +// - Cloned op → classified as OP_VECTOR_ONLY → VECTOR users switched to it +// Example +// shared_fill (CUBE_AND_VECTOR) +// ↑ ↑ +// matmul(CUBE) arith.addf(VECTOR) +// shared_fill (CUBE_ONLY) → matmul (CUBE) +// shared_fill_vector_clone → arith.addf (VECTOR) +// (VECTOR_ONLY) +// This step runs after step 4 (VECTOR propagation) and before step 6 +// (SCF yield processing). The do-while loop handles cases where splitting +// an op produces new CUBE_AND_VECTOR ops among its operands. +// ============================================================================ + +void OpClassifierPass::splitOperationForCubeAndVector( + Operation *op, llvm::DenseSet &processedOps, + llvm::DenseMap &opToVectorClone) { + // Guard against double-processing the same op + if (processedOps.count(op)) + return; + processedOps.insert(op); + + // Only split ops explicitly marked CUBE_AND_VECTOR + if (opCoreTypes[op] != OP_CUBE_AND_VECTOR) + return; + + // ------------------------------------------------------------------ + // Phase 1: Recursively handle CUBE_AND_VECTOR operands first + // ------------------------------------------------------------------ + // An operand of this op might itself be CUBE_AND_VECTOR (shared upstream). + // We must split the operand's chain before we can split this op, because + // when we build the operand mapping below, we need the VECTOR clone of + // the operand to be available. Process operands depth-first. + for (Value operand : op->getOperands()) { + if (Operation *def = operand.getDefiningOp()) { + if (opCoreTypes[def] == OP_CUBE_AND_VECTOR) { + splitOperationForCubeAndVector(def, processedOps, opToVectorClone); + } + } + } + + // ------------------------------------------------------------------ + // Phase 2: Build IRMapping — redirect operands to VECTOR clones + // ------------------------------------------------------------------ + // When cloning this op for VECTOR users, any operand that already has + // a VECTOR clone (produced by a recursive call above) must be replaced + // with that clone so the cloned op uses the VECTOR-side values. + IRMapping mapping; + for (Value operand : op->getOperands()) { + if (Operation *def = operand.getDefiningOp()) { + auto it = opToVectorClone.find(def); + if (it != opToVectorClone.end()) { + // Operand has a VECTOR clone — use it in the cloned op + Operation *vectorClone = it->second; + for (unsigned i = 0; i < def->getNumResults(); ++i) { + if (def->getResult(i) == operand) { + mapping.map(operand, vectorClone->getResult(i)); + break; + } + } + } + } + } + + // ------------------------------------------------------------------ + // Phase 3: Clone the op for VECTOR users + // ------------------------------------------------------------------ + OpBuilder builder(op); + Operation *vectorOp = builder.clone(*op, mapping); + + // Reclassify: original → CUBE_ONLY, clone → VECTOR_ONLY + opCoreTypes[op] = OP_CUBE_ONLY; + opCoreTypes[vectorOp] = OP_VECTOR_ONLY; + opToVectorClone[op] = vectorOp; // record for callers' operand mapping + allOps.push_back(vectorOp); // track new op so it gets core_type stamped + CloneOpMap[op] = vectorOp; // record for laterClone map + CloneOpMap[vectorOp] = op; // record for laterClone map + + // ------------------------------------------------------------------ + // Phase 4: Redirect VECTOR-only users to the cloned result + // ------------------------------------------------------------------ + // VECTOR users of the original result must now use the cloned result. + // CUBE users still use the original. We iterate through all result uses + // and switch VECTOR-only users over to vectorOp's corresponding result. + for (unsigned i = 0; i < op->getNumResults(); ++i) { + Value originalResult = op->getResult(i); + Value vectorResult = vectorOp->getResult(i); + + llvm::SmallVector usesToUpdate; + for (OpOperand &use : originalResult.getUses()) { + Operation *user = use.getOwner(); + if (!user || user == vectorOp) { + continue; + } + OpCoreType coreType = getCoreType(user); + if (llvm::isa(user)) { + coreType = getForInitCoreType(&use); + } + if (coreType == OP_VECTOR_ONLY) { + usesToUpdate.push_back(&use); + } + } + + for (OpOperand *use : usesToUpdate) { + use->set(vectorResult); // redirect to VECTOR clone + } + } +} + +// ============================================================================ +// handleCubeAndVector: entry point for CUBE_AND_VECTOR splitting +// ============================================================================ +// Repeatedly scans for CUBE_AND_VECTOR ops and splits them until no more +// remain. The do-while loop is needed because splitOperationForCubeAndVector +// is called recursively on operands; each recursive call may turn a +// CUBE_AND_VECTOR operand into CUBE_ONLY + VECTOR_ONLY (via the same +// splitOperationForCubeAndVector), which can in turn enable more splits at +// the current level. + +int OpClassifierPass::handleCubeAndVector() { + bool changed = true; + do { + changed = false; + + // Collect all ops still marked CUBE_AND_VECTOR + llvm::SmallVector cubeAndVectorOps; + for (Operation *op : allOps) { + if (opCoreTypes[op] == OP_CUBE_AND_VECTOR) { + cubeAndVectorOps.push_back(op); + } + } + + if (cubeAndVectorOps.empty()) + break; + + llvm::DenseMap opToVectorClone; + llvm::DenseSet processedOps; + + for (Operation *op : cubeAndVectorOps) { + if (processedOps.count(op)) + continue; + splitOperationForCubeAndVector(op, processedOps, opToVectorClone); + changed = true; + } + } while (changed); + + return 0; +} + +// ============================================================================ +// Step 9: Stamp Core Type to IR +// ============================================================================ + +int OpClassifierPass::stampToIR() { + for (Operation *op : allOps) { + if (!op || !op->getBlock()) + continue; + + auto it = opCoreTypes.find(op); + if (it == opCoreTypes.end()) + continue; + OpCoreType coreType = it->second; + + // Skip most scf dialect operations except scf.condition (terminator in + // while's before region) + if (llvm::isa(op->getDialect()) && + !llvm::isa(op)) + continue; + + // scf.condition is always VECTOR (it's the condition check in while's + // before region) + if (llvm::isa(op)) { + coreType = OP_VECTOR_ONLY; + } + + // Skip linalg operations' internal block operations + Operation *parent = op->getParentOp(); + bool isInsideLinalgBlock = false; + while (parent) { + if (llvm::isa(parent->getDialect())) { + isInsideLinalgBlock = true; + break; + } + parent = parent->getParentOp(); + } + if (isInsideLinalgBlock) + continue; + + // Skip builtin.module, func.func, and tt.func operations + if (isa(op)) + continue; + + op->setAttr("ssbuffer.core_type", + StringAttr::get(op->getContext(), coreTypeToString(coreType))); + } + + return 0; +} + +// ============================================================================ +// Step 1: Mark Synchronization Op +// ============================================================================ +llvm::LogicalResult OpClassifierPass::markSynchronizationOp() { + ModuleOp module = getOperation(); + CVPipeline::ComputeBlockIdManager bm(module); + + for (Operation *op : allOps) { + if (!isSyncOp(op)) { + continue; + } + if (isa(op)) { + setCoreType(op, OP_CUBE_AND_VECTOR); + LLVM_DEBUG(DBGS() << "Barrier classified as CUBE_AND_VECTOR: " << *op + << "\n"); + } + // Pre-existing hivm.sync_block_set/wait carry an explicit tcore_type + // operand. + else if (isa(op)) { + auto tcoreAttr = op->getAttrOfType("tcore_type"); + if (!tcoreAttr) { + continue; + } + switch (tcoreAttr.getTcoretype()) { + case hivm::TCoreType::CUBE: + setCoreType(op, OP_CUBE_ONLY); + break; + case hivm::TCoreType::VECTOR: + setCoreType(op, OP_VECTOR_ONLY); + break; + default: + // CUBE_OR_VECTOR / CUBE_AND_VECTOR are not produced by the sync + // frontend; let the default-VECTOR sweep decide. + break; + } + LOG_DEBUG("sync block set/wait classified by tcore_type: " << *op + << "\n"); + } else { + // Pre-existing hivm.sync_block_all carry an explicit SyncBlockMode + // attribute Its core type is decided by this attribute + auto syncBlock = dyn_cast(op); + if (!syncBlock) { + continue; + } + switch (syncBlock.getSyncBlockModeAttr().getSyncMode()) { + case hivm::SyncBlockMode::ALL_CUBE: + // stamped as "CUBE" by stampToIR + setCoreType(op, OP_CUBE_ONLY); + break; + case hivm::SyncBlockMode::ALL_VECTOR: + [[fallthrough]]; + case hivm::SyncBlockMode::ALL_SUB_VECTOR: + // stamped as "VECTOR" by stampToIR + setCoreType(op, OP_VECTOR_ONLY); + break; + case hivm::SyncBlockMode::ALL: + // handleCubeAndVector splits it into a CUBE copy + a VECTOR copy that + // keep the same block id; the degrade pass re-pairs them by block id. + setCoreType(op, OP_CUBE_AND_VECTOR); + break; + default: + // BARRIER_CUBE / BARRIER_VECTOR are not planned by this pipeline. + break; + } + } + + LOG_DEBUG("sync_block_all classified by mode: " << *op << "\n"); + + // Give every debug barrier / sync block its own unique block id so the sync + // ops lowered from it inherit a block id that no other op shares. Sync ops + // that OpClassifier already stamped (e.g. hivm.sync_block_all) keep their + // id; markOpBlockId would fail on them because they are already recorded. + if (llvm::failed(bm.markOpBlockId(op))) { + return failure(); + } + LOG_DEBUG("======== Assigned unique block_id for synchronization op " + << *op << "\n"); + op->setAttr(CVPipeline::kExternalSync, + IntegerAttr::get(IntegerType::get(op->getContext(), 32), 1)); + } + + return success(); +} + +// Run the pass +void OpClassifierPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LLVM_DEBUG(DBGS() << "\n--- Before Plan Compute Block --->\n"); + LLVM_DEBUG(DBGS() << module << "\n"); + + LLVM_DEBUG(DBGS() << "\n--- Running OpClassifierPass --->\n"); + + // Initialize memory dependence graph for tracking memory side effects + aliasAnalysis = std::make_shared(module); + memDepGraph = std::make_shared( + module, *aliasAnalysis); + + // Initialize the pass + initializePass(module); + + // Step 1: Mark synchronization ops at the very beginning + if (failed(markSynchronizationOp())) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 2: Pattern match around each linalg.matmul to find CUBE seeds + if (patternMatchCUBE() != 0) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 3: CUBE upstream BFS from seed loads + if (propagateCubeUpstream() != 0) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 4: Penetrate CUBE coloring into pure loader for-loops. + if (penetrateCubeIntoForLoops().failed()) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 5: Mark remaining operations as VECTOR + if (markRemainingAsVector() != 0) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 6: VECTOR upstream BFS + if (propagateVectorUpstream() != 0) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 7: Handle CUBE_AND_VECTOR operations + if (handleCubeAndVector() != 0) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 8: Process SCF yield results + if (handleSCFYield() != 0) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + // Step 9: Stamp to IR + if (stampToIR() != 0) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + LLVM_DEBUG(DBGS() << "\n--- OpClassifierPass: end --->\n"); +} + +// Create the pass +namespace mlir { +namespace triton { + +std::unique_ptr> createOpClassifierPass() { + return std::make_unique(); +} + +void registerAddOpClassifierPasses() { + registerPass( + []() -> std::unique_ptr { return createOpClassifierPass(); }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/PlanCubeBlock.cpp b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/PlanCubeBlock.cpp new file mode 100644 index 0000000000..213f1547c8 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/PlanCubeBlock.cpp @@ -0,0 +1,595 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/iterator.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" + +#include "DynamicCVPipeline/Common/CycleDetector.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Visitors.h" +#include "mlir/Pass/Pass.h" + +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/Common/SyncWall.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/PlanCubeBlockPass.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" + +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +static constexpr const char *DEBUG_TYPE = "plan-cube-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +static bool isMatmulOp(Operation *op) { return isa(op); } + +static bool isCubeSimpleOpOrCf(Operation *op) { + if (op->getBlock()->mightHaveTerminator() && + op == op->getBlock()->getTerminator()) { + return false; + } + return !isSyncOp(op) && getCoreTypeOfSimpleOpOrCf(op) == CoreType::CUBE_ONLY; +} + +namespace { + +class SeedRegionPlanner { + SmallVector seeds; + Block *block; + const DependencyHelper &depHelper; + ComputeBlockIdManager &bm; + llvm::DenseSet &assigned; + llvm::SmallVectorImpl &group; + const SyncWall &wall; + bool willCreateCycle(Operation *op); + bool isEligible(Operation *op); + bool tryAddToGroup(Operation *op, Operation *from); + +public: + SeedRegionPlanner(SmallVector seeds, Block *block, + const DependencyHelper &depHelper, + llvm::DenseSet &assigned, + llvm::SmallVectorImpl &group, + ComputeBlockIdManager &bm, const SyncWall &wall) + : seeds(seeds), block(block), depHelper(depHelper), assigned(assigned), + group(group), bm(bm), wall(wall) { + for (auto sd : seeds) { + group.push_back(sd); + } + } + + void run(); +}; + +} // namespace + +bool SeedRegionPlanner::willCreateCycle(Operation *op) { + auto *block = op->getBlock(); + llvm::DenseSet okSet(group.begin(), group.end()); + okSet.insert(op); + + DependencyCycleDetector dfs = {block, depHelper, okSet, bm}; + return dfs.detectCycle(); +} + +/** + * Checks if an operation is eligible to be added to a Cube group. + * Eligibility depends on it being a cube op, not yet assigned, not a compute + * op, and not creating a cycle in the dependence graph. + */ +bool SeedRegionPlanner::isEligible(Operation *op) { + if (!isCubeSimpleOpOrCf(op) || assigned.contains(op) || isMatmulOp(op)) { + return false; + } + return !willCreateCycle(op); +} + +bool SeedRegionPlanner::tryAddToGroup(Operation *op, Operation *from) { + if (!op || llvm::is_contained(group, op) || op->getBlock() != block || + !isEligible(op)) { + return false; + } + // Never let the group straddle a sync: a dependency edge must not bridge a + // synchronization point, otherwise the group would appear on both sides of + // the barrier and the fence built by ReorderOpsByBlockId would cycle. + if (from && wall.hasSyncBetween(from, op)) { + return false; + } + group.push_back(op); + return true; +} + +void SeedRegionPlanner::run() { + size_t head = 0; + while (head < group.size()) { + Operation *currOp = group[head++]; + depHelper.forEachSource( + currOp, + [&, this](Operation *source) { tryAddToGroup(source, currOp); }); + } +} + +namespace { + +/** + * Processes remaining unassigned cube operations by following the topological + * order. + */ +class TopologicalPartitionPlanner { + Block *block; + unsigned nonAssignedCubeCnt = 0; + llvm::DenseMap indegree; + llvm::DenseSet &assigned; + const DependencyHelper &depHelper; + ComputeBlockIdManager &bm; + const SyncWall &wall; + llvm::DenseSet newassigned; + llvm::DenseSet bypassVisited; + std::queue queue; + + void removeNonCubeOpsRecursively(Operation *op); + llvm::LogicalResult removeReadyNonCubeOps(); + + bool shouldSkip(Operation *op) { + return !isCubeSimpleOpOrCf(op) || assigned.contains(op); + }; + bool canExpandTo(Operation *op); + void dumpQueueAndIndegreeInfo(); + llvm::LogicalResult populateQueueWithReadyOps(); + llvm::SmallVector createNewGroupFromQueue(); + +public: + TopologicalPartitionPlanner(Block *block, + llvm::DenseSet &assigned, + const DependencyHelper &depHelper, + ComputeBlockIdManager &bm, const SyncWall &wall) + : block(block), assigned(assigned), depHelper(depHelper), bm(bm), + wall(wall) { + initializeIndegreeForBlock(block, indegree, depHelper, bm); + + block->walk([&](Operation *op) { + if (op->getBlock() == block && isCubeSimpleOpOrCf(op) && + !assigned.contains(op)) { + nonAssignedCubeCnt++; + } + }); + } + llvm::LogicalResult run(); +}; + +} // namespace + +// Recursively bypass non-cube ops: decrement indegree of users; collect +// newly-exposed cube ops +void TopologicalPartitionPlanner::removeNonCubeOpsRecursively(Operation *op) { + LOG_DEBUG("\tRemoved non-cube:" << *op << "\n"); + bypassVisited.insert(op); + depHelper.forEachUserInSameBlock(op, [&](Operation *user) { + if (!indegree.contains(user) || bm.isSameBlock(user, op)) { + return; + } + LOG_DEBUG("Sub indegree to " << *user << " from " << *op << "new degree = " + << indegree[user] - 1 << "\n"); + indegree[user]--; + if (!bm.isWholeCubeReady(user, indegree) || bypassVisited.contains(user) || + !shouldSkip(user)) { + return; + } + for (auto *passop : bm.getOpsInSameBlock(user)) { + if (!bypassVisited.contains(passop)) { + removeNonCubeOpsRecursively(passop); + } + } + }); +} + +/** + * Logic to bypass non-cube operations that are ready to be executed. + * This unblocks downstream cube operations in the topological sort. + */ +llvm::LogicalResult TopologicalPartitionPlanner::removeReadyNonCubeOps() { + auto indegreeBefore = indegree; + size_t beforeVisitedSize = bypassVisited.size(); + for (auto &p : indegree) { + Operation *op = p.first; + if (!shouldSkip(op) || !bm.isWholeCubeReady(op, indegree) || + bypassVisited.contains(op)) { + continue; + } + for (auto *passOp : bm.getOpsInSameBlock(op)) { + if (!bypassVisited.contains(passOp)) { + removeNonCubeOpsRecursively(passOp); + } + } + } + if (indegreeBefore == indegree && beforeVisitedSize == bypassVisited.size()) { + LLVM_DEBUG({ + if (Operation *parentOp = block->getParentOp()) { + LOG_DEBUG("PlanCubeBlock cannot make progress while scheduling " + "cube operations in: " + << *parentOp); + } + dumpQueueAndIndegreeInfo(); + }); + return llvm::failure(); + } + return llvm::success(); +} + +// Expansion condition: op must be CUBE_ONLY, indegree == 0 and all its +// dependency ops are CUBE_ONLY +bool TopologicalPartitionPlanner::canExpandTo(Operation *op) { + if (!isCubeSimpleOpOrCf(op) || assigned.contains(op)) { + return false; + } + auto it = indegree.find(op); + return it->second == 0; +} + +// Encountered error. Need to print failure reason, so no need for LLVM_DEBUG +void TopologicalPartitionPlanner::dumpQueueAndIndegreeInfo() { + // simply print a debug header in a new line + auto errs = []() -> llvm::raw_ostream & { + return llvm::errs() << "\n[" << DEBUG_TYPE << "] "; + }; + + errs() << "failed to make progress while planning cube blocks."; + errs() << "remaining cube count: " << nonAssignedCubeCnt; + + errs() << "ready queue"; + if (queue.empty()) { + llvm::errs() << " is empty."; + } else { + llvm::errs() << ":\n"; + while (!queue.empty()) { + Operation *op = queue.front(); + queue.pop(); + llvm::errs() << " " << *op << "\n"; + } + } + + errs() << "remaining unassigned cube ops:\n"; + bool foundRemainingCube = false; + for (auto &p : indegree) { + Operation *op = p.first; + if (!op || op->getBlock() != block || !isCubeSimpleOpOrCf(op) || + assigned.contains(op) || newassigned.contains(op)) { + continue; + } + foundRemainingCube = true; + llvm::errs() << " indegree=" << p.second << " op=" << *op << "\n"; + } + if (!foundRemainingCube) { + llvm::errs() << " \n"; + } +} + +llvm::LogicalResult TopologicalPartitionPlanner::populateQueueWithReadyOps() { + for (auto [op, indegree] : indegree) { + if (indegree < 0) { + op->emitError("Indegree cannot be negative"); + return llvm::failure(); + } + if (indegree == 0 && !newassigned.contains(op) && isCubeSimpleOpOrCf(op) && + !assigned.contains(op)) { + queue.push(op); + } + } + return llvm::success(); +} + +llvm::SmallVector +TopologicalPartitionPlanner::createNewGroupFromQueue() { + llvm::SmallVector group; + while (!queue.empty()) { + auto *currOp = queue.front(); + queue.pop(); + + newassigned.insert(currOp); + group.push_back(currOp); + nonAssignedCubeCnt--; + + depHelper.forEachUserInSameBlock(currOp, [&](Operation *user) { + if (!newassigned.contains(user)) { + auto &userInDegree = indegree[user]; + userInDegree--; + LOG_DEBUG("Sub indegree to " << *user << " from " << *currOp + << "new degree = " << userInDegree + << "\n"); + if (canExpandTo(user)) { + queue.push(user); + } + } + }); + } + return group; +} + +llvm::LogicalResult TopologicalPartitionPlanner::run() { + while (nonAssignedCubeCnt > 0) { + if (failed(populateQueueWithReadyOps())) { + return llvm::failure(); + } + + if (queue.empty()) { + if (failed(removeReadyNonCubeOps())) { + return llvm::failure(); + } + continue; + } + + auto group = createNewGroupFromQueue(); + // A wave of mutually-independent ready cube ops may span several segments + // separated by syncs. Split by segment so no block_id group straddles a + // synchronization op; each segment gets its own fresh id. + llvm::DenseMap> segmentGroups; + for (auto *op : group) { + segmentGroups[wall.segmentOf(op)].push_back(op); + } + for (auto &segGroup : segmentGroups) { + if (llvm::failed(bm.markOpsWithNewId(segGroup.second))) { + return llvm::failure(); + } + } + } + + return llvm::success(); +} + +static SmallVector collectMatmulOps(Block *block) { + SmallVector computeOps; + for (Operation &op : *block) { + if (isMatmulOp(&op)) { + computeOps.push_back(&op); + } + } + return computeOps; +} + +static void fuseMarkOpToDef(Block *block, ComputeBlockIdManager &bm, + const DependencyHelper &depHelper, + const SyncWall &wall) { + for (auto *op : llvm::make_pointer_range(block->getOperations())) { + if (getOpCoreType(op) != CUBE_ONLY) { + continue; + } + auto markOp = llvm::dyn_cast(op); + if (!markOp) { + continue; + } + auto *defOp = markOp.getSrc().getDefiningOp(); + if (!defOp) { + continue; + } + // Merging the mark into the def's group would make the group straddle a + // sync if the mark sits on the other side of one; keep them separate then. + if (!wall.sameSegment(markOp, defOp)) { + continue; + } + + auto defBlockId = bm.getBlockIdByOp(defOp); + if (defBlockId == -1) { + continue; + } + + auto currGroup = bm.getOpsInSameBlock(defOp); + llvm::DenseSet newGroup{currGroup.begin(), currGroup.end()}; + + if (newGroup.contains(markOp)) { + continue; + } + newGroup.insert(markOp); + + DependencyCycleDetector dfs{block, depHelper, newGroup, bm}; + if (!dfs.detectCycle()) { + bm.updateBlockId(markOp, defBlockId); + } + } +} + +static bool checkValidInputSeed(Operation *op) { + // keep unify to OpClassifer + return isa(op); +} +static bool checkValidUserSeed(Operation *op, linalg::MatmulOp &preDot) { + // keep unify to OpClassifer + if (isa(op)) { + return true; + } + // add matmul link by L0C + if (auto nextDot = dyn_cast(op)) { + if (nextDot.getDpsInits()[0] == *preDot->getResults().begin()) { + preDot = nextDot; + return true; + } + return false; + } + return false; +} + +static SmallVector +matchSeed(Operation *dotOp, ComputeBlockIdManager &bm, + const MemoryDependenceGraph &memGraph) { + // match inputs + SmallVector ret; + ret.push_back(dotOp); + for (Value operand : dotOp->getOperands()) { + Operation *def = operand.getDefiningOp(); + if (!def) + continue; + if (checkValidInputSeed(def) && isCubeSimpleOpOrCf(def) && + dotOp->getBlock() == def->getBlock() && bm.getBlockIdByOp(def) == -1) { + if (CVPipeline::isOnlyDirectlyUse(def, dotOp, memGraph)) { + ret.push_back(def); + } + } + } + // match outputs + Operation *nowOp = dotOp; + linalg::MatmulOp linkMatmul = llvm::dyn_cast(dotOp); + while (nowOp->hasOneUse()) { + auto user = *nowOp->getUsers().begin(); + if (user->getBlock() != dotOp->getBlock() || !isCubeSimpleOpOrCf(user) || + bm.getBlockIdByOp(user) != -1) { + break; + } + if (checkValidUserSeed(user, linkMatmul)) { + nowOp = user; + ret.push_back(user); + } else { + break; + } + } + return ret; +} + +/** + * Main entry point: Process a single block by grouping operations into + * execution blocks using BFS and topological traversal. + */ +static llvm::LogicalResult +processBlockWithCubeBFS(Block *block, const DependencyHelper &depHelper, + ComputeBlockIdManager &bm) { + llvm::DenseSet assigned; + auto allDots = collectMatmulOps(block); + + // Source-order sync walls of this block; used to keep every block_id group + // on a single side of each synchronization op. + SyncWall wall(block); + + // Phase 1: Add helper ops (transpose, load/store, ptr etc.) to cube block of + // related matmul + for (auto *dot : allDots) { + if (assigned.contains(dot)) { + continue; + } + auto temBlockId = bm.getNextId(); + llvm::SmallVector dotSeeds = + matchSeed(dot, bm, depHelper.memGraph); + // Drop seeds straddling a sync relative to the dot: they belong to their + // own segment and Phase 2 hands them a separate id, so the seed group can + // never appear on both sides of the barrier. + llvm::SmallVector prunedSeeds; + for (auto *seed : dotSeeds) { + if (wall.sameSegment(seed, dot)) { + prunedSeeds.push_back(seed); + } + } + llvm::erase_if(dotSeeds, [&](Operation *seed) { + return !wall.sameSegment(seed, dot); + }); + if (willCreateCycle(dotSeeds, depHelper.memGraph, temBlockId, bm)) { + LOG_DEBUG("Cube Seed already have a cycle!!"); + for (auto seed : dotSeeds) { + LOG_DEBUG("Seed: " << *seed << "\n"); + } + return llvm::failure(); + } + llvm::SmallVector newGroup; + SeedRegionPlanner regionPlanner{dotSeeds, block, depHelper, assigned, + newGroup, bm, wall}; + regionPlanner.run(); + + for (auto *op : newGroup) { + assigned.insert(op); + } + if (llvm::failed(bm.markOpsWithNewId(newGroup))) { + return llvm::failure(); + } + } + + // Phase 2: Handle remaining Cube Ops following Topo order + TopologicalPartitionPlanner topoPlanner{block, assigned, depHelper, bm, wall}; + if (failed(topoPlanner.run())) { + return failure(); + } + fuseMarkOpToDef(block, bm, depHelper, wall); + return llvm::success(); +} + +void mlir::triton::PlanCubeBlockPass::runOnOperation() { + auto moduleOp = getOperation(); + + if (CVPipeline::hasFallbackAttr(moduleOp)) { + return; + } + + LOG_DEBUG("Input mlir:\n" << moduleOp << "\n==========\n"); + + auto &aa = getAnalysis(); + MemoryDependenceGraph memGraph{moduleOp, aa}; + DependencyHelper depHelper{memGraph}; + auto bm = ComputeBlockIdManager(moduleOp); + + // We do not need to skip linalg blocks since they do not have core types and + // do not contain matmul + auto result = moduleOp.walk([&](Block *block) { + if (bm.shouldInheritFromParent(block, CoreType::CUBE_ONLY)) { + if (llvm::failed(bm.inheritFromParent(block))) { + block->getParentOp()->emitError() + << "[" << DEBUG_TYPE + << "] Sub-blocks failed to inherit block id from parent op"; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + } + + if (llvm::failed(processBlockWithCubeBFS(block, depHelper, bm))) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (result.wasInterrupted()) { + CVPipeline::setFallbackAttr(moduleOp, CVPipeline::ERRCODE_FAILED); + } + LOG_DEBUG("Output mlir:\n" << moduleOp << "\n==========\n"); +} + +std::unique_ptr> +mlir::triton::createPlanCubeBlockPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/PlanVectorBlockPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/PlanVectorBlockPass.cpp new file mode 100644 index 0000000000..781cb6d31c --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/PlanVectorBlockPass.cpp @@ -0,0 +1,669 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "ascend/include/DynamicCVPipeline/Common/SyncWall.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Passes.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/Operation.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/WalkResult.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" +#include +#include + +static constexpr const char *DEBUG_TYPE = "plan-vector-block"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +static bool isFusableOp(Operation *op) { + if (CVPipeline::isSyncOp(op)) { + return false; + } + if (isVectorSimpleOpOrCf(op)) { + // skip terminators + if (op->getBlock()->mightHaveTerminator() && + op == op->getBlock()->getTerminator()) { + return false; + } + return true; + } + return false; +} + +static void +passAndCollectCandidates(Operation *nowOp, DenseMap &indegree, + SmallVector &candidates, + DenseMap &visited, + const CVPipeline::MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + LOG_DEBUG("Bypassing non-fusable op " << *nowOp << "\nnow candidates size: " + << candidates.size() << "\n"); + DependencyHelper depHelper{memGraph}; + depHelper.forEachUserInSameBlock(nowOp, [&](Operation *user) { + if (!bm.isSameBlock(user, nowOp)) { + indegree[user]--; + } + + if (!bm.isWholeCubeReady(user, indegree)) { + return; + } + + if (visited[user]) { + return; + } + + if (isFusableOp(user)) { + visited[user] = true; + candidates.push_back(user); + return; + } + + for (auto *cubeop : bm.getOpsInSameBlock(user)) { + if (!visited[cubeop]) { + visited[cubeop] = true; + passAndCollectCandidates(cubeop, indegree, candidates, visited, + memGraph, bm); + } + } + }); + + LOG_DEBUG("After bypassing, candidates size: " << candidates.size() << "\n"); +} + +static void byPassNonFusable(DenseMap &indegree, + SmallVector &candidates, + DenseMap &visited, + const CVPipeline::MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + // for every non-fusable candidates, bypass it. + for (auto [op, _] : indegree) { + if (!bm.isWholeCubeReady(op, indegree) || isFusableOp(op) || visited[op]) { + continue; + } + for (auto cubeop : bm.getOpsInSameBlock(op)) { + if (!visited[cubeop]) { + visited[cubeop] = true; + passAndCollectCandidates(cubeop, indegree, candidates, visited, + memGraph, bm); + } + } + } +} + +static void +updateCandidates(Operation *nextFused, SmallVector &candidates, + DenseMap &indegree, + DenseMap &visited, + const CVPipeline::MemoryDependenceGraph &memGraph) { + // 1. Already fuse with nextFused, so remove it from candidates + for (auto it = candidates.begin(); it != candidates.end(); it++) { + if (*it == nextFused) { + it = candidates.erase(it); + break; + } + } + + // 2. Add new candidates whose indegree becomes 0 after fusing nextFused. + DependencyHelper depHelper{memGraph}; + depHelper.forEachUserInSameBlock(nextFused, [&](Operation *user) { + if (!visited[user]) { + indegree[user]--; + if (indegree[user] == 0 && isFusableOp(user)) { + visited[user] = true; + candidates.push_back(user); + } + } + }); +} + +void findCandidates(DenseMap &indegree, + SmallVector &candidates, + DenseMap &visited, + const CVPipeline::MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + // 1. if no candidate, try to bypass non-fusable + if (candidates.empty()) { + byPassNonFusable(indegree, candidates, visited, memGraph, bm); + } + // 2. find candidates whose indegree is 0 and not visited, add them to + // candidates and mark visited + for (auto &[op, degree] : indegree) { + if (degree == 0 && isFusableOp(op) && !visited[op]) { + visited[op] = true; + candidates.push_back(op); + } + } +} + +static SmallVector +findOpsAdjacentToCube(Block *block, const SmallVector &fuseGroup, + DenseMap &visited, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + SmallVector toProcess; + DependencyHelper depHelper{memGraph}; + // If the nonFusable is contorl, we default first considering. + // Otheriwe, the smaller blockId, means the matmul is more front in the IR + // order. + std::optional minBlockId = std::nullopt; + + for (Operation *op : fuseGroup) { + depHelper.forEachUserInSameBlock(op, [&](Operation *userInBlock) { + if (block->mightHaveTerminator() && + userInBlock == block->getTerminator()) { + return WalkResult::advance(); + } + if (!isFusableOp(userInBlock) && !visited[userInBlock]) { + auto newBlockId = bm.getBlockIdByOp(userInBlock); + if (!minBlockId.has_value() || newBlockId < minBlockId) { + minBlockId = newBlockId; + } + } + return WalkResult::advance(); + }); + } + + if (!minBlockId.has_value()) { + return {}; + } + + for (Operation *op : fuseGroup) { + + depHelper.forEachUserInSameBlock(op, [&](Operation *userInBlock) { + if (block->mightHaveTerminator() && + userInBlock == block->getTerminator()) { + return WalkResult::advance(); + } + if (!isFusableOp(userInBlock) && !visited[userInBlock]) { + auto newBlockId = bm.getBlockIdByOp(userInBlock); + if (newBlockId == minBlockId) { + toProcess.push_back(op); + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + } + return toProcess; +} + +static SetVector +collectKeepOpsToCube(Block *block, SmallVector toProcess, + const SmallVector &fuseGroup, + const CVPipeline::MemoryDependenceGraph &memGraph) { + SetVector keepOps; + DependencyHelper depHelper{memGraph}; + while (!toProcess.empty()) { + Operation *op = toProcess.front(); + toProcess.erase(toProcess.begin()); + if (keepOps.contains(op)) { + continue; + } + keepOps.insert(op); + + depHelper.forEachSource( + op, [&](Operation *source) { + if (!keepOps.contains(source) && + llvm::is_contained(fuseGroup, source)) { + toProcess.push_back(source); + } + }); + } + + // special case: annotation.mark always follows the defining op + for (auto op : fuseGroup) { + auto markOp = llvm::dyn_cast(op); + if (!markOp) { + continue; + } + + auto src = markOp.getSrc(); + auto definingOp = src.getDefiningOp(); + if (definingOp && keepOps.contains(definingOp)) { + keepOps.insert(markOp); + } + } + + return keepOps; +} + +static bool isTensorOperation(Operation *op) { + for (auto result : op->getResults()) { + if (isa(result.getType())) { + return true; + } + } + for (auto operand : op->getOperands()) { + if (isa(operand.getType())) { + return true; + } + } + return false; +} + +static bool hasTensorOperation(const SmallVector &fuseGroup) { + for (auto op : fuseGroup) { + if (isTensorOperation(op)) { + return true; + } + } + return false; +} + +static void +collectAllUsersInFuseGroup(Operation *op, + const SmallVector &fuseGroup, + SetVector &toRemove) { + if (toRemove.contains(op) || !llvm::is_contained(fuseGroup, op)) { + return; + } + toRemove.insert(op); + for (auto user : op->getUsers()) { + collectAllUsersInFuseGroup(user, fuseGroup, toRemove); + } +} + +static void +collectAllDependenciesInFuseGroup(Operation *op, + const SmallVector &fuseGroup, + SetVector &toRemove) { + if (toRemove.contains(op) || !llvm::is_contained(fuseGroup, op)) { + return; + } + toRemove.insert(op); + op->walk([&](Operation *nestedOp) { + for (auto operand : nestedOp->getOperands()) { + if (auto definingOp = operand.getDefiningOp()) { + collectAllDependenciesInFuseGroup(definingOp, fuseGroup, toRemove); + } + } + }); +} + +static SmallVector +extractWholeFuseGroup(Block *block, + const SmallVector &nowFuseGroup, + ComputeBlockIdManager &bm) { + SmallVector toProcess(nowFuseGroup.begin(), nowFuseGroup.end()); + + if (!hasTensorOperation(nowFuseGroup)) { + LOG_DEBUG("no tensor operation in nowFuseGroup.....\n"); + return toProcess; + } + + SetVector toRemove; + auto *terminator = block->getTerminator(); + if (llvm::isa_and_present(terminator) && + isa(block->getParentOp())) { + for (auto *op : nowFuseGroup) { + auto walkResult = op->walk( + [block, terminator, &bm, &nowFuseGroup](Operation *nestedOp) { + for (auto operand : nestedOp->getOperands()) { + auto *defOp = getLoopCarriedDefOp(operand, block); + if (defOp && bm.getBlockIdByOp(defOp) == -1 && + !llvm::is_contained(nowFuseGroup, defOp)) { + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + if (walkResult.wasInterrupted()) { + collectAllUsersInFuseGroup(op, nowFuseGroup, toRemove); + } + } + } + + for (auto op : toRemove) { + LOG_DEBUG("Removing op when refining: " << *op << "\n"); + toProcess.erase(std::remove(toProcess.begin(), toProcess.end(), op), + toProcess.end()); + } + + if (!toProcess.empty() && !hasTensorOperation(toProcess)) { + LOG_DEBUG("no tensor operation in toProcess.....\n"); + return {}; + } + return toProcess; +} + +static void evictAndRestoreState( + Block *block, const SetVector &keepOps, + SmallVector &fuseGroup, DenseMap &visited, + SmallVector &candidates, DenseMap &indegree, + const CVPipeline::MemoryDependenceGraph &memGraph) { + // 1. Collect ops to remove + SmallVector toRemove; + for (Operation *op : fuseGroup) { + if (!keepOps.contains(op)) { + toRemove.push_back(op); + } + } + fuseGroup.assign(keepOps.begin(), keepOps.end()); + + DependencyHelper depHelper{memGraph}; + // 2. Restore indegree for successors and reset visited for removed ops + for (Operation *op : toRemove) { + depHelper.forEachUserInSameBlock( + op, [&](Operation *user) { indegree[user]++; }); + visited[op] = false; + } + + // 3. Reset visited for current candidates so they can re-enter + for (auto cand : candidates) { + visited[cand] = false; + } + + // 4. Rebuild candidates from (old candidates + removed ops), zero indegree + // only + SmallVector pool(candidates.begin(), candidates.end()); + pool.append(toRemove.begin(), toRemove.end()); + candidates.clear(); + // Need to find source again + for (Operation *op : pool) { + if (indegree[op] == 0 && !visited[op]) { + visited[op] = true; + candidates.push_back(op); + } + } +} + +void refineFuseGroup(Block *block, SmallVector &nowFuseGroup, + DenseMap &visited, + SmallVector &candidates, + DenseMap &indegree, + const CVPipeline::MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + // 1.Find ops in fuse group whose next node is a non-fusable (CUBE-only) op + auto toProcess = + findOpsAdjacentToCube(block, nowFuseGroup, visited, memGraph, bm); + + // 2. If no cube adjacent op, extract toProcess from fuseGroup using fallback + // rules + if (toProcess.empty()) { + LOG_DEBUG("No Cube adjacent op, extracting toProcess from fuseGroup.\n"); + toProcess = extractWholeFuseGroup(block, nowFuseGroup, bm); + } + + // 3. If still empty after extraction, no op will be cut + if (toProcess.empty()) { + LOG_DEBUG("No op will be cut after extraction.\n"); + findCandidates(indegree, candidates, visited, memGraph, bm); + if (candidates.empty()) { + // Even if cut these ops, and add them into next search, they will be cut + // again and lead to dead cycle. + // the scenario like this: + // v1->v2->yield. + // So after findCandidates, no more new ops, need to fuse nowFuseGroup. + return; + } + } + + // 4. Collect keepOps transitively (data + memory + loop-carried deps) + auto keepOps = collectKeepOpsToCube(block, toProcess, nowFuseGroup, memGraph); + + // 5. Remove non-kept ops from fuseGroup and restore BFS state + evictAndRestoreState(block, keepOps, nowFuseGroup, visited, candidates, + indegree, memGraph); + LOG_DEBUG("After cutting, kept " << keepOps.size() << "\n"); +} + +static SmallVector +findOpsAdjacentFromCube(Block *block, const SmallVector &fuseGroup, + DenseMap &visited, + const CVPipeline::MemoryDependenceGraph &memGraph, + CVPipeline::ComputeBlockIdManager &bm) { + SmallVector toProcess; + DependencyHelper depHelper{memGraph}; + std::optional minBlockId = std::nullopt; + + for (Operation *op : fuseGroup) { + depHelper.forEachSourceInSameBlock(op, [&](Operation *defInBlock) { + if (!isFusableOp(defInBlock) && !visited[defInBlock]) { + auto newBlockId = bm.getBlockIdByOp(defInBlock); + if (!minBlockId.has_value() || newBlockId < minBlockId) { + minBlockId = newBlockId; + } + } + }); + } + + if (!minBlockId.has_value()) { + return {}; + } + + for (Operation *op : fuseGroup) { + depHelper.forEachSourceInSameBlock(op, [&](Operation *defInBlock) { + if (!isFusableOp(defInBlock) && !visited[defInBlock]) { + auto newBlockId = bm.getBlockIdByOp(defInBlock); + if (newBlockId == minBlockId) { + toProcess.push_back(op); + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + } + return toProcess; +} + +SetVector +collectKeepOpsFromCube(Block *block, SmallVector toProcess, + const SmallVector &fuseGroup, + const CVPipeline::MemoryDependenceGraph &memGraph) { + SetVector keepOps; + DependencyHelper depHelper{memGraph}; + while (!toProcess.empty()) { + Operation *op = toProcess.front(); + toProcess.erase(toProcess.begin()); + if (keepOps.contains(op)) { + continue; + } + keepOps.insert(op); + + // Add all users to process + depHelper.forEachUserInSameBlock(op, [&](Operation *userInblock) { + if (!keepOps.contains(userInblock) && + llvm::is_contained(fuseGroup, userInblock)) { + toProcess.push_back(userInblock); + } + }); + } + return keepOps; +} + +void reverseRefineFuseGroup(Block *block, + SmallVector &nowFuseGroup, + DenseMap &visited, + SmallVector &candidates, + DenseMap &indegree, + const CVPipeline::MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + // 1. Find ops in fuse group whose pre node is a non-fusable op + auto toProcess = + findOpsAdjacentFromCube(block, nowFuseGroup, visited, memGraph, bm); + // 2. If no op from cube, extract toProcess from fuseGroup using fallback + // rules + if (toProcess.empty()) { + LOG_DEBUG("No op from cube, extracting toProcess from fuseGroup.\n"); + return; + } + // 3. Collect keepOps transitively (data + memory + loop-carried deps) + auto keepOps = + collectKeepOpsFromCube(block, toProcess, nowFuseGroup, memGraph); + // 4. Remove non-kept ops from fuseGroup and restore BFS state + evictAndRestoreState(block, keepOps, nowFuseGroup, visited, candidates, + indegree, memGraph); + LOG_DEBUG("After reverse cutting, kept " << keepOps.size() << "\n"); +} + +// Main function to plan vector block id for one block +llvm::LogicalResult +planVectorBlockId(Block *block, + const CVPipeline::MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + // 1. topo initialize + llvm::DenseMap indegree; + llvm::SmallVector queue; + llvm::DenseMap visited; // has been visited in search + initializeIndegreeForBlock(block, indegree, DependencyHelper{memGraph}, bm); + + // Source-order sync walls of this block; used to keep every vector group on + // a single side of each sync, otherwise the ReorderOpsByBlockId fence adds + // both (group -> sync) and (sync -> group) edges and scheduling fails. + SyncWall wall(block); + + // 2. initialize visited and find initial candidates + block->walk([&](Operation *op) { + if (op->getBlock() == block) { + visited[op] = false; + if (isFusableOp(op) && indegree[op] == 0) { + visited[op] = true; + queue.push_back(op); + } + } + }); + findCandidates(indegree, queue, visited, memGraph, bm); + + // 3. find fuse group follow topo order + llvm::SmallVector nowFuseGroup; + while (!queue.empty()) { + auto nextFused = queue.front(); + if (nextFused) { + // fused one && update candidates + nowFuseGroup.push_back(nextFused); + updateCandidates(nextFused, queue, indegree, visited, memGraph); + } + if (queue.empty() || nextFused == nullptr) { + LLVM_DEBUG({ + LOG_DEBUG("Prepare to check this group: \n"); + for (auto op : nowFuseGroup) { + LOG_DEBUG("prepare: " << *op << "\n"); + } + }); + // finish one group, assign block id and start next iteration + // Cut error operations before assigning block id + refineFuseGroup(block, nowFuseGroup, visited, queue, indegree, memGraph, + bm); + + reverseRefineFuseGroup(block, nowFuseGroup, visited, queue, indegree, + memGraph, bm); + LOG_DEBUG("Group after cutting: \n"); + for (auto op : nowFuseGroup) { + LOG_DEBUG("fuseing: " << *op << "\n"); + } + // Assign one fresh block id per sync-free segment. A topo wave of + // mutually independent ready ops may span several segments separated by + // syncs; splitting here guarantees no block_id group straddles a sync. + llvm::DenseMap> segmentGroups; + for (auto *op : nowFuseGroup) { + segmentGroups[wall.segmentOf(op)].push_back(op); + } + for (auto &segGroup : segmentGroups) { + if (llvm::failed(bm.markOpsWithNewId(segGroup.second))) { + return llvm::failure(); + } + } + nowFuseGroup.clear(); + // reset queue + findCandidates(indegree, queue, visited, memGraph, bm); + } + } + return llvm::success(); +} + +namespace { + +class PlanVectorBlockPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PlanVectorBlockPass) + + PlanVectorBlockPass() = default; + void runOnOperation() override; + + [[nodiscard]] llvm::StringRef getArgument() const final { + return "plan-vector-block"; + } +}; + +void PlanVectorBlockPass::runOnOperation() { + // 1. Build memory dependence graph + auto moduleOp = getOperation(); + + if (CVPipeline::hasFallbackAttr(moduleOp)) { + return; + } + + auto &aa = getAnalysis(); + auto memDepGraph = MemoryDependenceGraph(moduleOp, aa); + auto bm = ComputeBlockIdManager(moduleOp); + + // 2. search blocks in topo order and assign block id for each block + auto result = + moduleOp.walk([&](Block *block) -> WalkResult { + if (bm.shouldInheritFromParent(block, CoreType::VECTOR_ONLY)) { + LOG_DEBUG("Inherit from parentOp: " << block->getParentOp() << "\n"); + if (llvm::failed(bm.inheritFromParent(block))) { + block->getParentOp()->emitError() + << "[" << DEBUG_TYPE + << "] Sub-blocks failed to inherit block id from parent op"; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + } + if (llvm::failed(planVectorBlockId(block, memDepGraph, bm))) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (result.wasInterrupted()) { + LOG_DEBUG("Failed to plan vector block id for block\n"); + CVPipeline::setFallbackAttr(moduleOp, CVPipeline::ERRCODE_FAILED); + } +} + +} // namespace + +namespace mlir::triton { + +std::unique_ptr> createPlanVectorBlockPass() { + return std::make_unique(); +} +} // namespace mlir::triton diff --git a/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.cpp b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.cpp new file mode 100644 index 0000000000..adafffade2 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.cpp @@ -0,0 +1,587 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/iterator.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" + +#include "DynamicCVPipeline/Common/DependencyHelper.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/GPU/IR/GPUDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Visitors.h" +#include "mlir/Pass/Pass.h" + +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.h" +#include "bishengir/Dialect/HIVM/IR/HIVMImpl.h" +#include "bishengir/Dialect/HIVM/Utils/Utils.h" + +using namespace mlir; +static constexpr const char *DEBUG_TYPE = "ReorderOpsByBlockIdPass"; + +#define DBGS(...) LLVM_DEBUG(llvm::dbgs() << __VA_ARGS__) +#define LOG_DEBUG(...) DBGS("[" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace triton; +using namespace CVPipeline; + +namespace { + +// A dependency DAG of both SSA and memory of the ops +struct BlockOpGraph { + Block *block; + ArrayRef ops; + DenseMap opIndex; // op → position in ops + DenseMap> preds; // op → its defs + DenseMap> succs; // op → its uses + BlockOpGraph(ArrayRef allOps, Block *block, + const MemoryDependenceGraph &memGraph); +}; + +// Helper class to manage edges in OpGraph, mainly to reduce congitive +// complexity of the build function +struct EdgeHelper { + BlockOpGraph &graph; + DenseSet> seen; + Block *block; + + // find the ancestor directly in the block, and in opIndex; return nullptr if + // either fails + Operation *resolveToBlockOp(Operation *op); + + void addEdge(Operation *pred, Operation *succ); + + void addEdgeToUser(Operation *op, Operation *user) { + if (graph.opIndex.contains(user)) { + return; // same-level use, already covered by the def-side loop + } + Operation *ancestor = resolveToBlockOp(user); + addEdge(op, ancestor); + }; + + EdgeHelper(BlockOpGraph &g, Block *block) : graph(g), block(block) {}; +}; + +} // namespace + +Operation *EdgeHelper::resolveToBlockOp(Operation *op) { + if (graph.opIndex.contains(op)) { + return op; + } + Operation *ancestor = getAncestorInBlock(op, block); + if (!ancestor || !graph.opIndex.contains(ancestor)) { + return nullptr; + } + return ancestor; +} + +void EdgeHelper::addEdge(Operation *pred, Operation *succ) { + if (!pred || !succ || pred == succ) { + return; + } + if (seen.insert({pred, succ}).second) { + LOG_DEBUG("Adding edge from " << *pred << " to " << *succ); + graph.succs[pred].push_back(succ); + graph.preds[succ].push_back(pred); + } +}; + +BlockOpGraph::BlockOpGraph(ArrayRef allOps, Block *block, + const MemoryDependenceGraph &memGraph) + : block(block), ops(allOps) { + for (unsigned i = 0; i < allOps.size(); ++i) { + opIndex[allOps[i]] = i; + preds[allOps[i]]; // ensure every node has an entry + succs[allOps[i]]; + } + + EdgeHelper edges(*this, block); + DependencyHelper depHelper{memGraph}; + + for (Operation *op : allOps) { + LOG_DEBUG("Processing op: " << *op); + depHelper.forEachSource(op, [&](Operation *source) { + Operation *def = edges.resolveToBlockOp(source); + edges.addEdge(def, op); + }); + depHelper.forEachUser( + op, [&](Operation *user) { edges.addEdgeToUser(op, user); }); + } +} + +static llvm::FailureOr> +collectBlockIds(ArrayRef allOps, ComputeBlockIdManager &bm) { + DenseMap opBlockId; + for (Operation *op : allOps) { + if (llvm::failed(verifyOpBlockId(op))) { + return llvm::failure(); + } + auto blockIdOpt = getOpBlockId(op); + if (blockIdOpt.has_value()) { + opBlockId[op] = blockIdOpt.value(); + continue; + } + + auto result = op->walk([&](Operation *nestedOp) { + if (nestedOp != op && + !llvm::isa(nestedOp)) { + return WalkResult::interrupt(); + } + auto currBlockIdOpt = getOpBlockId(nestedOp); + if (!blockIdOpt.has_value()) { + blockIdOpt = getOpBlockId(nestedOp); + } + if (currBlockIdOpt.has_value() && currBlockIdOpt != blockIdOpt) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (result.wasInterrupted() || !blockIdOpt.has_value()) { + blockIdOpt = bm.getNextId(); + } else { + bm.updateBlockId(op, blockIdOpt.value()); + } + opBlockId[op] = blockIdOpt.value(); + } + return opBlockId; +} + +namespace { + +// Helper structure to hold the group-level graph data. +struct GroupAdjacencyGraph { + Block *block; + SmallVector groupIds; + SmallVector> succs; + SmallVector inDeg; + ComputeBlockIdManager &bm; + GroupAdjacencyGraph(const BlockOpGraph &g, + const DenseMap &opBlockId, + ComputeBlockIdManager &bm); + llvm::FailureOr> computeTopologicalOrder(); +}; + +} // namespace + +/** + * Step 1: Build the group-level dependency graph from operator-level edges. + * Maps individual operations to their respective groups and identifies + * dependencies between those groups. + */ +GroupAdjacencyGraph::GroupAdjacencyGraph( + const BlockOpGraph &g, const DenseMap &opBlockId, + ComputeBlockIdManager &bm) + : block(g.block), bm(bm) { + // 1. Collect distinct group IDs while preserving the first-appearance order. + DenseSet seenIds; + for (Operation *op : g.ops) { + int id = opBlockId.at(op); + if (seenIds.insert(id).second) { + groupIds.push_back(id); + } + } + + unsigned n = groupIds.size(); + succs.resize(n); + inDeg.assign(n, 0); + + // Map group ID to its index in the groupIds vector for fast lookup. + DenseMap groupPos; + for (unsigned i = 0; i < n; ++i) { + groupPos[groupIds[i]] = i; + } + + // 2. Build group-level edges. Use a set to avoid duplicate edges between + // groups. + DenseSet> addedEdges; + for (Operation *op : g.ops) { + unsigned fromIdx = groupPos[opBlockId.at(op)]; + + for (Operation *succ : g.succs.at(op)) { + unsigned toIdx = groupPos[opBlockId.at(succ)]; + // Ignore intra-group dependencies and duplicate inter-group edges. + if (fromIdx != toIdx && addedEdges.insert({fromIdx, toIdx}).second) { + succs[fromIdx].push_back(toIdx); + inDeg[toIdx]++; + } + } + } + + // Logging the constructed group graph. + LOG_DEBUG("Group-level edges:\n"); + for (unsigned i = 0; i < n; ++i) { + DBGS(" Group " << groupIds[i] << " -> "); + for (unsigned succIdx : succs[i]) { + DBGS(groupIds[succIdx] << " "); + } + DBGS("\n"); + } +} + +/** + * Step 2: Perform a topological sort (Kahn's Algorithm) on the group graph. + * Returns the group IDs in an order that satisfies all dependencies. + */ +llvm::FailureOr> +GroupAdjacencyGraph::computeTopologicalOrder() { + SmallVector result; + SmallVector ready; // Nodes with in-degree 0. + unsigned n = groupIds.size(); + + SmallVector startingVectorBlocks; + for (auto [i, groupId] : llvm::enumerate(groupIds)) { + if (inDeg[i] != 0) { + continue; + } + auto ops = bm.getOpsRefByBlockId(groupId); + if (ops.empty() || + getCoreTypeOfSimpleOpOrCf(ops.front()) == mlir::CVPipeline::CUBE_ONLY) { + ready.push_back(i); + } else { + startingVectorBlocks.push_back(i); + } + } + constexpr size_t kPriviledgedMaxComputeOpCnt = 1; + std::stable_partition(startingVectorBlocks.begin(), + startingVectorBlocks.end(), [this](unsigned idx) { + const auto blockId = groupIds[idx]; + const auto ops = bm.getOpsRefByBlockId(blockId); + auto computeOpCnt = 0; + for (auto op : ops) { + if (isTensorComputeOp(op)) { + computeOpCnt++; + LOG_DEBUG("Tensor compute op: " << *op); + } else { + LOG_DEBUG("Not tensor compute op: " << *op); + } + } + LOG_DEBUG("Summary: group id " + << blockId + << " compute ops: " << computeOpCnt); + return computeOpCnt > kPriviledgedMaxComputeOpCnt; + }); + ready.append(startingVectorBlocks); + + while (!ready.empty()) { + auto cur = ready.pop_back_val(); + + result.push_back(groupIds[cur]); + + for (unsigned succIdx : succs[cur]) { + if (--inDeg[succIdx] == 0) { + ready.push_back(succIdx); + } + } + } + + LLVM_DEBUG({ + LOG_DEBUG("Group order: "); + for (int id : result) { + LOG_DEBUG(id << " "); + } + LOG_DEBUG("\n"); + }); + + if (result.size() == n) { + return result; + } + Operation *op = block->getParentOp(); + constexpr std::string_view kErrorPrefix = + "Failed to compute topological order for "; + if (!op) { + llvm::errs() << kErrorPrefix + << "an unknown block that is not contained in an op"; + return llvm::failure(); + } + size_t regionIdx = 0; + bool found = false; + for (auto [i, region] : llvm::enumerate(op->getRegions())) { + for (auto &possibleBlock : region.getBlocks()) { + if (&possibleBlock == block) { + regionIdx = i; + } + } + } + op->emitError(kErrorPrefix) << "block in region " << regionIdx; + return llvm::failure(); +} + +static bool isStoreLikeWithRegion(Operation *op) { + auto ret = op->walk([&](Operation *subOp) { + if (isa(subOp)) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return ret.wasInterrupted(); +} + +static SmallVector +orderInOneCBlock(ArrayRef opsInSameBlock, + const MemoryDependenceGraph &memGraph) { + // reorder in one compute block following rules: + // 1. vecoter block should sink storeLike Op. (only Vector) + // 2. Other + SmallVector originOrder(opsInSameBlock.begin(), + opsInSameBlock.end()); + if (llvm::any_of(opsInSameBlock, [&](Operation *op) { + return CVPipeline::getCoreTypeOfSimpleOpOrCf(op) != + CVPipeline::VECTOR_ONLY; + })) { + // If this is one CUBE block, storeLike Ops no need to sink down. + // Considering + // 1. CUBE block's store is always use FIXPIPE + // 2. C->V always just next to matmul. + // So there are no conflict between store and inter transfer + return originOrder; + } + + if (llvm::all_of(opsInSameBlock, + [&](Operation *op) { return !isStoreLikeWithRegion(op); })) { + // If there are no store-like op, early return. + return originOrder; + } + Block *block = opsInSameBlock.front()->getBlock(); + BlockOpGraph graph{opsInSameBlock, block, memGraph}; + + // Kahn's topological sort. Track in-degree per op and seed the ready set + // with all ops that have no predecessors. + DenseMap inDeg; + SmallVector ready; + for (Operation *op : opsInSameBlock) { + inDeg[op] = graph.preds.at(op).size(); + if (inDeg[op] == 0) { + ready.push_back(op); + } + } + + // Tie-breaker among ready (in-degree 0) ops: + // 1. Non-store-like ops come first (sink store-like ops to the end). + // 2. The op with a smaller opIndex wins (preserve original program order). + auto comesBefore = [&](Operation *a, Operation *b) { + bool aStore = isStoreLikeWithRegion(a); + bool bStore = isStoreLikeWithRegion(b); + if (aStore != bStore) { + return !aStore; + } + return graph.opIndex.at(a) < graph.opIndex.at(b); + }; + + SmallVector ordered; + ordered.reserve(opsInSameBlock.size()); + while (!ready.empty()) { + // Pick the best candidate under the tie-breaking rules. + auto bestIt = std::min_element(ready.begin(), ready.end(), comesBefore); + Operation *cur = *bestIt; + ready.erase(bestIt); + + ordered.push_back(cur); + + // Release successors; any that drop to in-degree 0 become ready. + for (Operation *succ : graph.succs.at(cur)) { + if (--inDeg[succ] == 0) { + ready.push_back(succ); + } + } + } + + return ordered; +} + +// Stable sort ops based on their group orders +static llvm::FailureOr> buildReorderedOps( + const BlockOpGraph &graph, const DenseMap &opBlockId, + ComputeBlockIdManager &bm, const MemoryDependenceGraph &memGraph) { + SmallVector reordered; + GroupAdjacencyGraph adjacencyGraph{graph, opBlockId, bm}; + auto groupOrderResult = adjacencyGraph.computeTopologicalOrder(); + if (llvm::failed(groupOrderResult)) { + return llvm::failure(); + } + + for (int const blockId : groupOrderResult.value()) { + SmallVector + originOrderOp; // collect ops following program order. + for (Operation *op : graph.ops) { + if (opBlockId.at(op) == blockId) { + originOrderOp.push_back(op); + } + } + SmallVector orderedInOneCBlock = + orderInOneCBlock(originOrderOp, memGraph); + reordered.append(orderedInOneCBlock); + } + return reordered; +} + +// Reorder the ops in the mlir representation +static void applyReorder(Block &block, ArrayRef reordered) { + Operation *terminator = + block.mightHaveTerminator() ? block.getTerminator() : nullptr; + for (Operation *op : reordered) { + op->moveBefore(&block, block.end()); + } + + if (terminator) { + terminator->moveBefore(&block, block.end()); + } +} + +static llvm::LogicalResult +reorderOpsInBlock(Block &block, const MemoryDependenceGraph &memGraph, + ComputeBlockIdManager &bm) { + const auto allOps = + llvm::to_vector(llvm::make_pointer_range(block.without_terminator())); + + const BlockOpGraph graph{allOps, &block, memGraph}; + llvm::FailureOr> opBlockIdOpt = + collectBlockIds(allOps, bm); + if (failed(opBlockIdOpt)) { + return failure(); + } + + auto &opBlockId = *opBlockIdOpt; + LOG_DEBUG("Initial opBlockIds:\n"); + for (Operation *op : allOps) { + LOG_DEBUG(" Op: " << *op << ", opBlockId = " << opBlockId[op] << "\n"); + } + + const auto reorderedRes = buildReorderedOps(graph, opBlockId, bm, memGraph); + if (failed(reorderedRes)) { + return failure(); + } + + applyReorder(block, reorderedRes.value()); + + // Verify the sync fence invariant: every op that preceded (followed) a + // gpu.barrier / hivm.sync_block_all in the original source order must still + // precede (follow) it. + LLVM_DEBUG({ + DenseMap sourceIdx; + for (unsigned i = 0; i < allOps.size(); ++i) { + sourceIdx[allOps[i]] = i; + } + for (Operation &op : block) { + if (!CVPipeline::isSyncOp(&op)) { + continue; + } + bool seenBarrier = false; + unsigned barrierIdx = sourceIdx[&op]; + for (Operation &it : block) { + if (&it == &op) { + seenBarrier = true; + continue; + } + if (!sourceIdx.contains(&it)) { + continue; + } + unsigned idx = sourceIdx.at(&it); + if (seenBarrier && idx < barrierIdx) { + LOG_DEBUG("Barrier fence violated: op after barrier in source moved " + << "before it: " << it << "\n"); + } + if (!seenBarrier && idx > barrierIdx) { + LOG_DEBUG("Barrier fence violated: op before barrier in source moved " + << "after it: " << it << "\n"); + } + } + } + }); + + return llvm::success(); +} + +void ReorderOpsByBlockIdPass::runOnOperation() { + OpBuilder const builder(&getContext()); + + auto moduleOp = getOperation(); + + if (CVPipeline::hasFallbackAttr(moduleOp)) { + return; + } + + // MergeComputeBlockPass sets kMergeComputeBlockApplied to record whether it + // actually merged blocks. Skip reorder only when it ran but merged nothing; + // consume the marker either way so it does not leak into the output IR. + if (auto applied = moduleOp->getAttrOfType( + CVPipeline::kMergeComputeBlockApplied)) { + moduleOp->removeAttr(CVPipeline::kMergeComputeBlockApplied); + if (!applied.getValue()) { + LOG_DEBUG("Skip reorder: MergeComputeBlock ran but merged nothing"); + return; + } + } + + LOG_DEBUG("Input mlir:\n" << moduleOp << "\n"); + llvm::dbgs().flush(); + + auto &aa = getAnalysis(); + auto memGraph = MemoryDependenceGraph(moduleOp, aa); + auto bm = ComputeBlockIdManager(moduleOp); + auto result = moduleOp.walk([&](Block *block) { + auto *parentOp = block->getParentOp(); + if (!parentOp || + // whitelist ops to reorder + !(isa(parentOp) || + isa(parentOp->getDialect()))) { + return WalkResult::skip(); + } + if (llvm::failed(reorderOpsInBlock(*block, memGraph, bm))) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (result.wasInterrupted()) { + CVPipeline::setFallbackAttr(moduleOp, CVPipeline::ERRCODE_FAILED); + return; + } + + LOG_DEBUG("Output mlir:\n" << moduleOp << "\n"); + LOG_DEBUG("=== Pass TuningOpSeq complete ===\n"); +} + +std::unique_ptr> +mlir::triton::createReorderOpsByBlockIdPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable.cpp b/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable.cpp new file mode 100644 index 0000000000..2765c260c6 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/Support/Debug.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Pass/PassRegistry.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PreCheckAvailable.h" + +using namespace mlir; +using namespace triton; + +static constexpr const char *DEBUG_TYPE = + "pre-check-dynamic-cv-pipeline-available"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__ << "\n"; \ + }) + +void PreCheckAvailablePass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LDBG("Enter PreCheckAvailable pass."); + PassManager pm(&getContext(), module.getOperationName()); + + LDBG("Before PreCheck:\n" << module); + pm.addPass(createPreCheckBlacklistPass()); + pm.addPass(createPreCheckMatmulPass()); + pm.addPass(createPreCheckDisablePreloadPass()); + + if (failed(runPipeline(pm, module))) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); + return; + } + + LDBG("Exit PreCheckAvailable pass."); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createPreCheckAvailablePass() { + return std::make_unique(); +} + +void registerPreCheckAvailablePasses() { + registerPass(createPreCheckBlacklistPass); + registerPass(createPreCheckMatmulPass); + registerPass(createPreCheckDisablePreloadPass); + registerPass(createPreCheckAvailablePass); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckBlacklist.cpp b/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckBlacklist.cpp new file mode 100644 index 0000000000..5c45979576 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckBlacklist.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/Support/Debug.h" + +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/IR/BuiltinOps.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PreCheckAvailable.h" + +using namespace mlir; +using namespace triton; + +// The blacklist operations that should skip SSBUFFER +static const llvm::SmallVector kBlacklistOpNames = { + "scope.scope", +}; + +static constexpr const char *DEBUG_TYPE = "pre-check-blacklist"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__ << "\n"; \ + }) + +void PreCheckBlacklistPass::getDependentDialects( + DialectRegistry ®istry) const { + registry.insert(); +} + +void PreCheckBlacklistPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + Operation *foundBlacklistOp = nullptr; + llvm::StringRef foundOpName; + + // Check for all blacklist operations + module.walk([&](Operation *op) -> WalkResult { + llvm::StringRef opName = op->getName().getStringRef(); + if (llvm::is_contained(kBlacklistOpNames, opName)) { + foundBlacklistOp = op; + foundOpName = opName; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (!foundBlacklistOp) { + LDBG("No blacklist operations found, passed."); + return; + } + + LDBG("SSBUFFER will be skipped because " + << foundOpName + << " operation was found, which indicates that it has been optimized " + "for the Ascend."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createPreCheckBlacklistPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckDisablePreload.cpp b/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckDisablePreload.cpp new file mode 100644 index 0000000000..b0a02b7798 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckDisablePreload.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Debug.h" + +#include "DynamicCVPipeline/Common/BufferCountManager.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/BuiltinOps.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PreCheckAvailable.h" + +using namespace mlir; +using namespace triton; + +// Functions that should use at most double buffering. +static const llvm::SmallVector kBlacklistFuncNames = { + "_attn_bwd", "kernel_sdpa_fwd", "_swa_paged_decode_kernel", + "paged_decode_fd_kernel"}; + +static constexpr const char *DEBUG_TYPE = "pre-check-disable-preload"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(...) \ + LLVM_DEBUG({ \ + DBGS(); \ + llvm::dbgs() << __VA_ARGS__ << "\n"; \ + }) + +void PreCheckDisablePreload::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + func::FuncOp foundBlacklistFunc = nullptr; + + // Check for all blacklist operations + module.walk([&](func::FuncOp func) -> WalkResult { + llvm::StringRef funcName = func.getSymName(); + if (llvm::is_contained(kBlacklistFuncNames, funcName)) { + foundBlacklistFunc = func; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + if (!foundBlacklistFunc) { + LDBG("No preload blacklist func found, passed."); + return; + } + + // Keep the explicit 3-preload setting only for kernel_sdpa_fwd. + if (foundBlacklistFunc.getSymName() == "kernel_sdpa_fwd") { + auto intraBufferCount = + module->getAttrOfType(CVPipeline::kIntraBufCount); + auto interBufferCount = + module->getAttrOfType(CVPipeline::kInterCoreBufCount); + if (intraBufferCount && interBufferCount && + intraBufferCount.getInt() == 3 && interBufferCount.getInt() == 2) { + return; + } + } + + LDBG("3-preload will be disabled because " + << foundBlacklistFunc.getSymName() + << " function was found, which is not supported by 3-preload now."); + + // This setting will disable the 3-preload feature. + BufferCountManager bufferCountManager(module); + bufferCountManager.setBufferCount(BufferCountManager::DepType::IntraCore, 2); + bufferCountManager.setBufferCount(BufferCountManager::DepType::InterCore, 1); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createPreCheckDisablePreloadPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckMatmul.cpp b/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckMatmul.cpp new file mode 100644 index 0000000000..6f5ace12a0 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/PreCheckAvailable/PreCheckMatmul.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/Support/Debug.h" + +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/IR/BuiltinOps.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/PreCheckAvailable.h" + +using namespace mlir; +using namespace triton; + +static constexpr const char *DEBUG_TYPE = "pre-check-matmul"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(X) LLVM_DEBUG(DBGS() << (X) << "\n") + +void PreCheckMatmul::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + linalg::MatmulOp firstMatmulOp = nullptr; + + module.walk([&](linalg::MatmulOp matmulOp) -> WalkResult { + firstMatmulOp = matmulOp; + return WalkResult::interrupt(); + }); + + if (firstMatmulOp) { + LDBG("The linalg.matmul operation is found, passed."); + return; + } + + LDBG("SSBUFFER will be skipped because no linalg.matmul operation was found, " + "which indicating that this op is a pure vector computation."); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_IGNORED); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createPreCheckMatmulPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/RemoveAttributes.cpp b/third_party/ascend/lib/DynamicCVPipeline/RemoveAttributes.cpp new file mode 100644 index 0000000000..a39dc13f2e --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/RemoveAttributes.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Debug.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassRegistry.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/RemoveAttributes.h" + +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +static constexpr const char *DEBUG_TYPE = "RemoveAttributes"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +// if extra attr is needed, add to ut @ +// third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/test-remove-attrs.mlir +static constexpr llvm::StringLiteral kAttrsToRemove[]{ + kBlockId, kCoreType, + kTransferId, kMainLoop, + kCubeFirst, kVectorFirst, + kAddFromMatmul, kIntraDeps, + kIntraBuffer, kAnalyzeFlagId, + kLoopCarriedL0C, kCrossCoreDeps, + kMemCrossDeps, kClone, + kIntraBufCount, kInterCoreBufCount, + kLoadStoreBufCount, kInsertionOptimization, + kDepMark, kIntraDeps, + kSubBlock, kMergeSmallBlockFirstRunDone, + kSplittedIf, kLoopId}; + +void RemoveSsbufAttrPass::runOnOperation() { + auto module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + module->walk([](Operation *op) { + LOG_DEBUG("Removing ssbuf attrs of " << *op); + for (auto attrName : kAttrsToRemove) { + op->removeAttr(attrName); + } + }); +} + +namespace mlir::triton { + +std::unique_ptr> createRemoveSsbufAttrPass() { + return std::make_unique(); +} + +void registerRemoveSsbufAttrPasses() { + registerPass(createRemoveSsbufAttrPass); +} + +} // namespace mlir::triton diff --git a/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromCompute/MarkGMLoadPass.cpp b/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromCompute/MarkGMLoadPass.cpp new file mode 100644 index 0000000000..9c7761c036 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromCompute/MarkGMLoadPass.cpp @@ -0,0 +1,358 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/MarkGMLoadPass.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromComputePass.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" + +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "mlir/Pass/Pass.h" + +#include "llvm/Support/Debug.h" + +using namespace mlir; +using namespace triton; + +static constexpr const char *DEBUG_TYPE = "MarkGMLoad"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +namespace { + +static constexpr int kDefaultVBufferCount = 2; +static constexpr int kDefaultCBufferCount = 1; + +struct MarkCandidate { + memref::CopyOp copyOp; + memref::AllocOp destAlloc; // dest backing alloc after view-like piercing + scope::ScopeOp scopeOp; // nearest enclosing scope, should not be null + int bufferCount; // filled in Phase 2 + bool fromHint = false; // true if bufferCount came from gm_load hint +}; + +// Resolve whether a BlockArgument of func::FuncOp traces to a GM pointer. +// Returns: +// - true if the arg belongs to the entry function (=> GM load source) +// - false + non-null nextValue: trace through the caller's corresponding +// operand, continue tracing +// - false + null nextValue: tracing failed (no module / no caller) +static bool resolveFuncBlockArg(BlockArgument blockArg, func::FuncOp funcOp, + Value &nextValue) { + auto moduleOp = funcOp->getParentOfType(); + if (!moduleOp) { + LOG_DEBUG("[warning] moduleOp is null"); + return false; + } + if (SymbolTable::symbolKnownUseEmpty(funcOp.getNameAttr(), moduleOp)) { + return true; // entry func argument => GM load + } + // Non-entry func: trace through callers. + auto symbolUses = SymbolTable::getSymbolUses(funcOp.getNameAttr(), moduleOp); + if (symbolUses && !symbolUses->empty()) { + // get funcOp's caller, continue tracing + auto callOp = cast((*symbolUses->begin()).getUser()); + nextValue = callOp.getOperands()[blockArg.getArgNumber()]; + return false; + } + LOG_DEBUG("[warning] Non-entry func: no caller uses " + << funcOp.getNameAttr()); + return false; +} + +// Rule 1: Pierce view-like ops and trace scf.for / scf.while iter_args back to +// their init values. Returns true only when the terminal is a BlockArgument +// owned by the entry func::FuncOp (i.e. a GM pointer function argument). +static bool traceSourceToFuncArg(Value v) { + while (true) { + // 1. Pierce view-like ops and tensor.extract_slice. + while (auto *defOp = v.getDefiningOp()) { + if (auto viewLike = dyn_cast(defOp)) { + v = viewLike.getViewSource(); + continue; + } + if (auto extractSlice = dyn_cast(defOp)) { + v = extractSlice.getSource(); + continue; + } + break; + } + // 2. Check terminal: must be a BlockArgument. + auto blockArg = dyn_cast(v); + if (!blockArg) { + return false; // ends at a defining op, not a GM arg + } + Operation *parentOp = blockArg.getOwner()->getParentOp(); + + if (auto funcOp = dyn_cast(parentOp)) { + Value nextV; + if (resolveFuncBlockArg(blockArg, funcOp, nextV)) { + return true; // find GM load source in funcOp arg + } + if (nextV) { + v = nextV; + continue; + } + return false; + } + if (auto forOp = dyn_cast(parentOp)) { + if (blockArg.getArgNumber() == 0) + return false; // induction variable, cannot be a GM load source + // iter_arg: trace its init value (skip induction var at index 0). + v = forOp.getInitArgs()[blockArg.getArgNumber() - 1]; + continue; + } + if (auto whileOp = dyn_cast(parentOp)) { + // before-block arg: trace to the corresponding init operand. + if (blockArg.getOwner() == whileOp.getBeforeBody()) { + v = whileOp.getInits()[blockArg.getArgNumber()]; + continue; + } + // after-block arg: trace to the corresponding condition operand. + if (blockArg.getOwner() == whileOp.getAfterBody()) { + auto condOp = + cast(whileOp.getBeforeBody()->getTerminator()); + v = condOp.getArgs()[blockArg.getArgNumber()]; + continue; + } + LOG_DEBUG("[warning] unsupport while op "); + return false; + } + LOG_DEBUG("[warning] unsupport iterarg or control flow"); + return false; // other BlockArgument kinds + } +} + +// Rule 2: Pierce view-like ops and tensor.extract_slice on the dest chain +// and return the backing memref::AllocOp, or null if the chain does not +// terminate at one. +static memref::AllocOp traceDestToAlloc(Value v) { + while (auto *defOp = v.getDefiningOp()) { + if (auto viewLikeOp = dyn_cast(defOp)) { + v = viewLikeOp.getViewSource(); + continue; + } + if (auto extractSliceOp = dyn_cast(defOp)) { + v = extractSliceOp.getSource(); + continue; + } + break; + } + return dyn_cast_or_null(v.getDefiningOp()); +} + +// Rule 3: Resolve multi-buffer count N from the enclosing scope. +// Returns -1 when scopeOp is null or has an unexpected tcore_type, +// which should normally never happen. +static int resolveBufferCount(scope::ScopeOp scopeOp) { + int buffer_num = -1; + if (!scopeOp) { + return buffer_num; + } + bool isCube = false; + bool isVector = false; + if (failed(getScopeType(scopeOp, isCube, isVector))) { + return buffer_num; + } + if (isVector) { + buffer_num = kDefaultVBufferCount; + } else if (isCube) { + buffer_num = kDefaultCBufferCount; + } + LOG_DEBUG("return buffer num = " << buffer_num); + return buffer_num; +} + +// Check whether the dest alloc has a user-specified gm_load compile +// hint (annotation.mark with gm_load attr). Returns the hint value +// (0/1 = force-off, >=2 = force-on with specified depth), or -1 if no hint. +// +// The hint may be attached either directly to the alloc result +// (post-bufferization) or to a tensor produced by bufferization.to_tensor +// (pre-bufferization). In the latter case we pierce through the to_tensor op +// to find the annotation. +static int resolveHintBufferCount(memref::AllocOp destAlloc) { + Value allocResult = destAlloc.getResult(); + std::optional foundHint; + auto hasConflictingHint = [&](annotation::MarkOp markOp) -> bool { + auto attr = markOp->getAttrOfType( + CVPipeline::kGMLoadMultiBufferHintAttr); + if (!attr) + return false; + int val = static_cast(attr.getInt()); + markOp->removeAttr(CVPipeline::kGMLoadMultiBufferHintAttr); + if (foundHint && *foundHint != val) { + LOG_DEBUG("conflicting gm_load hints: " << *foundHint << " vs " << val); + return true; // signal conflict + } + foundHint = val; + return false; + }; + for (auto *user : allocResult.getUsers()) { + // Pre-bufferization: hint is on the tensor produced by to_tensor. + if (auto toTensor = dyn_cast(user)) { + for (auto *tensorUser : toTensor.getResult().getUsers()) { + if (auto markOp = dyn_cast(tensorUser)) { + if (hasConflictingHint(markOp)) + return -1; + } + } + } + } + return foundHint.value_or(-1); +} + +// Apply Rules 1 & 2 to a memref::CopyOp and build a MarkCandidate if eligible. +// Returns std::nullopt when any rule fails. +static std::optional collectCandidate(memref::CopyOp copyOp) { + // Rule 1: source must trace back to a func argument (GM load). + if (!traceSourceToFuncArg(copyOp.getSource())) { + return std::nullopt; + } + // Rule 2: dest must be created by a memref::AllocOp. + auto allocOp = traceDestToAlloc(copyOp.getTarget()); + if (!allocOp) { + LOG_DEBUG("dest not created by memref::AllocOp, skip"); + return std::nullopt; + } + auto scopeOp = copyOp->getParentOfType(); + return MarkCandidate{copyOp, allocOp, scopeOp}; +} + +// Phase 3: insert or update annotation::MarkOp with multi_buffer attr on the +// dest alloc. Returns false when bufferCount <= 1 (skip marking), true +// otherwise. +static bool markGMLoadCandidate(MarkCandidate &c) { + if (c.bufferCount <= 1) { + LOG_DEBUG("bufferCount <= 1, skip marking"); + return false; + } + // Check if an annotation::MarkOp already exists for this alloc. + annotation::MarkOp existingMarkOp = nullptr; + for (auto *user : c.destAlloc->getUsers()) { + if (auto markOp = dyn_cast(user)) { + existingMarkOp = markOp; + break; + } + } + OpBuilder builder(c.destAlloc); + if (existingMarkOp) { + existingMarkOp->setAttr(hivm::MultiBufferAttr::name, + builder.getI32IntegerAttr(c.bufferCount)); + if (c.fromHint) + existingMarkOp->setAttr(CVPipeline::kGMLoadHintAttr, + builder.getUnitAttr()); + } else { + builder.setInsertionPointAfter(c.destAlloc); + auto markOp = builder.create(c.destAlloc->getLoc(), + c.destAlloc.getResult()); + markOp->setAttr(hivm::MultiBufferAttr::name, + builder.getI32IntegerAttr(c.bufferCount)); + if (c.fromHint) + markOp->setAttr(CVPipeline::kGMLoadHintAttr, builder.getUnitAttr()); + } + LOG_DEBUG("marked multi_buffer = " << c.bufferCount << " on " << c.destAlloc); + return true; +} + +} // namespace + +void MarkGMLoadPass::getDependentDialects(DialectRegistry ®istry) const { + registry.insert(); +} + +void MarkGMLoadPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LOG_DEBUG("Enter MarkGMLoad pass"); + + // Phase 1: collect candidates (read-only). + SmallVector candidates; + module.walk([&](memref::CopyOp copyOp) { + if (auto candidate = collectCandidate(copyOp)) { + candidates.push_back(*candidate); + } + }); + + if (candidates.empty()) { + LOG_DEBUG("no GM load candidate found"); + return; + } + + // Phase 2 & 3: resolve buffer count N per candidate and insert + // annotation::MarkOp (mutation). + for (auto &c : candidates) { + int hintVal = resolveHintBufferCount(c.destAlloc); + if (hintVal >= 0) { + // User specified a compile hint. + if (hintVal <= 1) { + // Force-off: skip marking entirely. + LOG_DEBUG("hint force-off (val=" << hintVal << "), skip marking"); + continue; + } + c.bufferCount = hintVal; + c.fromHint = true; + LOG_DEBUG("hint force-on, bufferCount = " << hintVal); + } else { + // No hint: automatic resolution. + c.bufferCount = resolveBufferCount(c.scopeOp); + if (c.bufferCount < 0) { + LOG_DEBUG("resolveBufferCount failed for " << c.copyOp << ", fallback"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + } + markGMLoadCandidate(c); + } + + LOG_DEBUG("after MarkGMLoad: " << module); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createMarkGMLoadPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.cpp b/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.cpp new file mode 100644 index 0000000000..b3d9397932 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.h" +#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinTypes.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/MathExtras.h" + +#include + +using namespace mlir; +using namespace triton; + +#define DEBUG_TYPE "UBOverflow" +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +static annotation::MarkOp findMarkOp(memref::AllocOp allocOp) { + for (auto *user : allocOp.getResult().getUsers()) { + if (auto markOp = dyn_cast(user)) + return markOp; + } + return nullptr; +} + +int triton::getAlignUnit(Type elementType) { + unsigned width = elementType.getIntOrFloatBitWidth(); + if (width == 0 || width > UBConstants::ALIGN_UNIT_BITS) + return 1; + return static_cast(UBConstants::ALIGN_UNIT_BITS / width); +} + +SmallVector triton::collectBuffers(ModuleOp module) { + SmallVector buffers; + + module.walk([&](scope::ScopeOp scopeOp) { + bool isCube = false; + bool isVector = false; + if (failed(getScopeType(scopeOp, isCube, isVector))) + return WalkResult::advance(); + if (!isVector) + return WalkResult::advance(); + + scopeOp.walk([&](memref::AllocOp allocOp) { + BufferInfo buf; + buf.allocOp = allocOp; + buf.forOp = allocOp->getParentOfType(); + + annotation::MarkOp markOp = findMarkOp(allocOp); + if (markOp) { + if (auto attr = markOp->getAttrOfType( + hivm::MultiBufferAttr::name)) { + buf.kind = BufferInfo::Kind::Annot; + buf.markOp = markOp; + buf.multiBufferCount = std::max(attr.getInt(), 1); + buf.fromHint = markOp->hasAttr(CVPipeline::kGMLoadHintAttr); + if (buf.fromHint) + markOp->removeAttr(CVPipeline::kGMLoadHintAttr); + } + } + + buffers.push_back(buf); + }); + + return WalkResult::advance(); + }); + + LOG_DEBUG("collected " << buffers.size() << " buffers"); + return buffers; +} + +SmallVector triton::collectTensorEmpties(ModuleOp module) { + SmallVector tensors; + + module.walk([&](scope::ScopeOp scopeOp) { + bool isCube = false; + bool isVector = false; + if (failed(getScopeType(scopeOp, isCube, isVector)) || !isVector) + return WalkResult::advance(); + + scopeOp.walk([&](tensor::EmptyOp emptyOp) { + if (emptyOp->getParentOfType() != scopeOp) + return; + + auto tensorType = emptyOp.getType(); + if (!tensorType.hasStaticShape() || tensorType.getRank() == 0 || + llvm::any_of(tensorType.getShape(), + [](int64_t dim) { return dim == 0; })) { + LOG_DEBUG("unsupported tensor.empty shape, skipping " << emptyOp); + return; + } + + TensorInfo tensor; + tensor.emptyOp = emptyOp; + LOG_DEBUG("tensor.empty: " << emptyOp); + tensors.push_back(tensor); + }); + + return WalkResult::advance(); + }); + + LOG_DEBUG("collected " << tensors.size() << " tensor.empty ops"); + return tensors; +} + +static void computeShapedSize(ShapedType shapedType, int64_t &originalSize, + int64_t &reducedSize, int64_t &alignedSize) { + ArrayRef shape = shapedType.getShape(); + Type elementType = shapedType.getElementType(); + unsigned bitWidth = elementType.getIntOrFloatBitWidth(); + + if (!shapedType.hasStaticShape() || shape.empty() || bitWidth == 0 || + llvm::any_of(shape, [](int64_t dim) { return dim == 0; })) { + LOG_DEBUG("unsupported shape, skipping size computation"); + return; + } + + int64_t numElements = 1; + for (auto dim : shape) { + numElements *= dim; + } + originalSize = numElements * bitWidth; + + // TileAndBindSubBlock splits dim0 across two sub-blocks (one-to-two): + // reducedDim0 = ceil(dim0 / K_SUB_BLOCK_DIM), where K_SUB_BLOCK_DIM = 2. + int64_t reducedDim0 = (shape[0] + UBConstants::K_SUB_BLOCK_DIM - 1) / + UBConstants::K_SUB_BLOCK_DIM; + int64_t reducedElements = numElements / shape[0] * reducedDim0; + reducedSize = reducedElements * bitWidth; + + int64_t lastDim = shape.size() == 1 ? reducedDim0 : shape.back(); + int64_t alignUnit = getAlignUnit(elementType); + if (lastDim % alignUnit == 0) { + alignedSize = reducedSize; + return; + } + + int64_t alignedLastDim = (lastDim + alignUnit - 1) / alignUnit * alignUnit; + alignedSize = (reducedSize * alignedLastDim + lastDim - 1) / lastDim; + alignedSize = llvm::alignTo(alignedSize, UBConstants::ALIGN_UNIT_BITS); +} + +void triton::computeBufferSize(BufferInfo &buf) { + auto memrefType = mlir::cast(buf.allocOp.getResult().getType()); + computeShapedSize(memrefType, buf.originalSize, buf.reducedSize, + buf.alignedSize); +} + +void triton::computeTensorSize(TensorInfo &tensor) { + computeShapedSize(tensor.emptyOp.getType(), tensor.originalSize, + tensor.reducedSize, tensor.alignedSize); +} + +UBEstimateResult triton::checkUBOverflow(ArrayRef buffers, + ArrayRef tensors) { + UBEstimateResult result; + for (const auto &buf : buffers) { + if (buf.alignedSize > 0) + result.totalBits += buf.alignedSize * buf.multiBufferCount; + } + for (const auto &tensor : tensors) { + if (tensor.alignedSize > 0) + result.totalBits += tensor.alignedSize; + } + + LOG_DEBUG("UB = " << result.totalBits << " bits"); + return result; +} + +static SmallVector +collectPruneCandidates(ArrayRef buffers) { + SmallVector candidates; + for (size_t index = 0; index < buffers.size(); ++index) { + const auto &buf = buffers[index]; + if (buf.kind == BufferInfo::Kind::Annot && buf.markOp && + buf.alignedSize > 0 && !buf.fromHint) + candidates.push_back(index); + } + return candidates; +} + +static bool shouldPrune(const UBEstimateResult &result, + ArrayRef candidates) { + LOG_DEBUG("initial UB = " << result.totalBits << " bits, max = " + << UBConstants::UB_SPACE_SIZE_BITS << " bits"); + + if (result.totalBits <= UBConstants::UB_SPACE_SIZE_BITS) { + LOG_DEBUG("safe, no pruning needed"); + return false; + } + + if (candidates.empty()) { + LOG_DEBUG("no computable multi_buffer marks to prune"); + return false; + } + + LOG_DEBUG("collected " << candidates.size() << " annot marks"); + return true; +} + +LogicalResult triton::pruneMultiBufferMarks(ModuleOp module) { + // Step 1: collect buffers and tensors once, then compute their static sizes. + auto buffers = collectBuffers(module); + for (auto &buf : buffers) + computeBufferSize(buf); + + auto tensors = collectTensorEmpties(module); + for (auto &tensor : tensors) + computeTensorSize(tensor); + + // Step 2: compute the initial UB usage and collect pruning candidates. + auto result = checkUBOverflow(buffers, tensors); + auto candidates = collectPruneCandidates(buffers); + if (!shouldPrune(result, candidates)) + return success(); + + // Step 3: sort eligible non-hint marks by buffer size, largest first. + llvm::sort(candidates, [&](size_t lhs, size_t rhs) { + return buffers[lhs].alignedSize > buffers[rhs].alignedSize; + }); + + // Step 4: remove marks one by one, update BufferInfo, and fully re-estimate. + int deleted = 0; + for (size_t index : candidates) { + auto &buf = buffers[index]; + buf.markOp->removeAttr(hivm::MultiBufferAttr::name); + buf.kind = BufferInfo::Kind::Unannot; + buf.multiBufferCount = 1; + ++deleted; + + result = checkUBOverflow(buffers, tensors); + LOG_DEBUG("after deleting mark #" << deleted << " (size " << buf.alignedSize + << " bits): UB = " << result.totalBits + << " bits"); + + if (result.totalBits <= UBConstants::UB_SPACE_SIZE_BITS) + break; + } + + return success(); +} + +void UBOverflowCheckerPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + LOG_DEBUG("Enter UBOverflowChecker pass"); + + if (failed(pruneMultiBufferMarks(module))) { + LOG_DEBUG("pruneMultiBufferMarks failed (non-fatal)"); + } + + LOG_DEBUG("Process successfully"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createUBOverflowCheckerPass() { + return std::make_unique(); +} + +void registerUBOverflowCheckerPasses() { + registerPass([]() -> std::unique_ptr { + return createUBOverflowCheckerPass(); + }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromComputePass.cpp b/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromComputePass.cpp new file mode 100644 index 0000000000..be5dda7cf0 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SeparateMemoryFromComputePass.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromComputePass.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/MarkGMLoadPass.h" +#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.h" +#include "mlir/Pass/PassManager.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "separate-memory-from-compute"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(X) LLVM_DEBUG(DBGS() << (X) << "\n") + +using namespace mlir; +using namespace triton; + +void SeparateMemoryFromComputePass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + OpPassManager pm(module.getOperationName()); + LDBG("Enter SeparateMemoryFromCompute pass"); + + pm.addPass(createMarkGMLoadPass()); + pm.addPass(createUBOverflowCheckerPass()); + + if (failed(runPipeline(pm, module))) { + return; + } + + LDBG("Process successfully"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createSeparateMemoryFromComputePass() { + return std::make_unique(); +} + +void registerSeparateMemoryFromComputePasses() { + registerPass(createMarkGMLoadPass); + registerPass(createUBOverflowCheckerPass); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow.cpp new file mode 100644 index 0000000000..f41d3fdd45 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DynamicCVPipeline/PlanComputeBlock/ReorderOpsByBlockId.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/MarkMainLoop.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/SeparateCVScope.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflowPass.h" +#include "mlir/Pass/PassManager.h" +#include "llvm/Support/Debug.h" + +static constexpr const char *DEBUG_TYPE = "SplitDataflow"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") +#define LDBG(X) LLVM_DEBUG(DBGS() << (X) << "\n") + +using namespace mlir; +using namespace triton; + +// Run the pass +void SplitDataflowPass::runOnOperation() { + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + OpPassManager pm(module.getOperationName()); + LDBG("Enter pass."); + + pm.addPass(createCheckUnsupportedScenarioPass()); + + // Step 1: Add block_id for control flow operations + pm.addPass(createAddBlockIdForControlOpsPass()); + + // Step 2: Analyze data dependencies between Vector and Cube blocks + pm.addPass(createDataDependencyAnalysisPass()); + + // Step 3: Run InterCoreTransferAndSync + pm.addPass(createInterCoreTransferAndSyncPass()); + + // Step 4: Mark the main computation loop + pm.addPass(createMarkMainLoopPass()); + + // Step 5: Run SeparateCVScope + pm.addPass(createSeparateCVScopePass()); + + // Step 6: Canonicalize to preserve control flow attributes + pm.addPass(createPreserveControlAttrsCanonicalizePass()); + + // Step 7: Refine block id for iteration variables in main loops + pm.addPass(createRefineArgsBlockIdPass()); + + if (failed(runPipeline(pm, module))) { + if (!CVPipeline::hasFallbackAttr(module)) { + module->emitError() << "[" << DEBUG_TYPE << "] Pass failed!"; + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + return; + } + + LDBG("Process successfully"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createSplitDataflowPass() { + return std::make_unique(); +} +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.cpp new file mode 100644 index 0000000000..18f3c0fd6b --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.cpp @@ -0,0 +1,96 @@ + +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SplitDataflow/AddBlockIdForControlOps.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/Utils.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; + +static constexpr const char *DEBUG_TYPE = "add-block-id-for-control-ops"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir::triton; + +// Pass Entry Point +void AddBlockIdForControlOpsPass::runOnOperation() { + LOG_DEBUG("\n--- enter AddBlockIdForControlOpsPass --->\n"); + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + // Step 1: find the max block_id + int maxBlockId = CVPipeline::getAvailableBlockId(module) - 1; + + LOG_DEBUG("maxBlockId: " << maxBlockId << "\n"); + + // Step 2: add block_id for control flow ops + module.walk([&](Operation *op) { + // skip op with block_id + if (op->getAttrOfType(CVPipeline::kBlockId)) { + return; + } + + if (isa(op)) { + maxBlockId++; + setOpBlockId(op, maxBlockId); + LOG_DEBUG("Added block_id " << maxBlockId << " to " << *op << "\n"); + } + + Operation *parentOp = op->getParentOp(); + bool isControlTerminator = + (isa(op) && isa(parentOp)) || + (isa(op) && isa(parentOp)); + if (isControlTerminator) { + auto parentBlockIdOpt = CVPipeline::getOpBlockId(parentOp); + + int terminatorBlockId; + if (parentBlockIdOpt) { + terminatorBlockId = *parentBlockIdOpt; + } else { + maxBlockId++; + terminatorBlockId = maxBlockId; + } + setOpBlockId(op, terminatorBlockId); + LOG_DEBUG("Added block_id " << terminatorBlockId << " to " << *op + << "\n"); + } + }); + + LOG_DEBUG("\n--- exit AddBlockIdForControlOpsPass --->\n"); +} + +// Create the pass +namespace mlir { +namespace triton { + +std::unique_ptr> createAddBlockIdForControlOpsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/CheckUnsupportedScenario.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/CheckUnsupportedScenario.cpp new file mode 100644 index 0000000000..73fb1d8d08 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/CheckUnsupportedScenario.cpp @@ -0,0 +1,253 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "llvm/Support/Casting.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Support/WalkResult.h" + +#include "DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflowPass.h" + +using namespace mlir; +using namespace triton; +using namespace CVPipeline; + +static constexpr const char *DEBUG_TYPE = "ssbuf-check-unsupported-scenario"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << "\n[" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +// Returns the result of the enclosing scf.if if loadOp is a masked load +// directly yielded by the scf.if. +static Value getMaskedLoadIfResult(memref::LoadOp loadOp, scf::IfOp &parentIf) { + if (!loadOp->hasOneUse()) { + return nullptr; + } + parentIf = llvm::dyn_cast_if_present(loadOp->getParentOp()); + auto yieldOp = llvm::dyn_cast(*loadOp->getUsers().begin()); + if (!parentIf || !yieldOp || yieldOp->getParentOp() != parentIf) { + return nullptr; + } + + for (auto [idx, operand] : llvm::enumerate(yieldOp.getOperands())) { + if (operand == loadOp.getResult() && idx < parentIf.getNumResults()) { + return parentIf.getResult(idx); + } + } + return nullptr; +} + +// Returns true if root transitively depends on target within scope. +static bool dependsOnValue(Value root, Value target, Operation *scope) { + if (root == target) { + return true; + } + llvm::SmallVector worklist{root}; + llvm::DenseSet visited{root}; + + while (!worklist.empty()) { + Value cur = worklist.pop_back_val(); + if (cur == target) { + return true; + } + Operation *defOp = cur.getDefiningOp(); + if (!defOp || !scope->isProperAncestor(defOp)) { + continue; + } + for (Value operand : defOp->getOperands()) { + if (visited.insert(operand).second) { + worklist.push_back(operand); + } + } + for (Region ®ion : defOp->getRegions()) { + for (Block &block : region) { + if (Operation *term = block.getTerminator()) { + for (Value operand : term->getOperands()) { + if (visited.insert(operand).second) { + worklist.push_back(operand); + } + } + } + } + } + } + return false; +} + +// Returns true if val has a user within scope with a core type different from +// refCoreType. +static bool hasDifferentCoreTypeUser(Value val, CoreType refCoreType, + Operation *scope) { + llvm::SmallVector worklist{val}; + llvm::DenseSet visited{val}; + + while (!worklist.empty()) { + Value cur = worklist.pop_back_val(); + for (Operation *user : cur.getUsers()) { + if (!scope->isProperAncestor(user)) { + continue; + } + if (user->hasTrait() && + user->getParentOp() == scope) { + continue; + } + CoreType userCoreType = getOpCoreType(user); + if (userCoreType != CoreType::UNDETERMINED && + userCoreType != refCoreType && + userCoreType != CoreType::CUBE_AND_VECTOR) { + return true; + } + for (Value res : user->getResults()) { + if (visited.insert(res).second) { + worklist.push_back(res); + } + } + } + } + return false; +} + +// Checks if a loop carries an iter_arg updated by ifResult and used by a +// different core type. +static bool isLoopCarryingConflictingIterArg(Operation *loopOp, Value ifResult, + CoreType loadCoreType) { + if (!isa(loopOp)) { + return false; + } + MainLoop mainLoop(loopOp); + scf::YieldOp loopYield = MainLoop::getLoopYieldOp(loopOp); + if (!loopYield) { + return false; + } + + SmallVector iterArgs = mainLoop.getIterArgs(); + SmallVector beforeIterArgs = mainLoop.getBeforeIterArgs(); + + for (auto [argIdx, yieldVal] : llvm::enumerate(loopYield.getOperands())) { + if (!dependsOnValue(yieldVal, ifResult, loopOp)) { + continue; + } + if (argIdx < iterArgs.size() && + hasDifferentCoreTypeUser(iterArgs[argIdx], loadCoreType, loopOp)) { + return true; + } + if (argIdx < beforeIterArgs.size() && + hasDifferentCoreTypeUser(beforeIterArgs[argIdx], loadCoreType, + loopOp)) { + return true; + } + } + return false; +} + +// Pattern: +// for iter_arg(%arg21){ +// y = *operates on %arg21* {ssbuffer.core_type = "CUBE"} +// masked_load = if { +// a = memref.load +// yield a +// } else { +// yield b +// } { ssbuffer.core_type = "VECTOR" } +// yield masked_load +// } +static llvm::LogicalResult +checkNoMaskedMemrefLoadOfDifferentCoreTypes(ModuleOp module) { + auto result = module->walk([](memref::LoadOp loadOp) -> WalkResult { + scf::IfOp parentIf; + Value ifResult = getMaskedLoadIfResult(loadOp, parentIf); + if (!ifResult) { + return WalkResult::advance(); + } + + CoreType loadCoreType = getValueCoreType(ifResult); + if (loadCoreType == CoreType::UNDETERMINED) { + loadCoreType = getOpCoreType(parentIf); + } + if (loadCoreType == CoreType::UNDETERMINED) { + loadCoreType = getOpCoreType(loadOp); + } + if (loadCoreType == CoreType::UNDETERMINED) { + return WalkResult::advance(); + } + + for (Operation *currOp = parentIf->getParentOp(); currOp; + currOp = currOp->getParentOp()) { + if (isLoopCarryingConflictingIterArg(currOp, ifResult, loadCoreType)) { + LOG_DEBUG("Found unsupported masked memref load feeding loop iter_arg " + "used by different core type"); + return WalkResult::interrupt(); + } + } + + return WalkResult::advance(); + }); + return llvm::failure(result.wasInterrupted()); +} + +namespace { + +class CheckUnsupportedScenario + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CheckUnsupportedScenario) + + // Constructor + CheckUnsupportedScenario() = default; + + // Run the pass + void runOnOperation() override { + auto module = getOperation(); + if (hasFallbackAttr(module)) { + return; + } + if (checkNoMaskedMemrefLoadOfDifferentCoreTypes(module).failed()) { + setFallbackAttr(module, ERRCODE_IGNORED); + return; + } + } + + [[nodiscard]] llvm::StringRef getArgument() const final { + return "ssbuf-check-unsupported-scenario"; + } + + /// Return the dialect that must be loaded in the context before this pass. + void getDependentDialects(::mlir::DialectRegistry ®istry) const override { + registry.insert(); + } +}; + +} // namespace + +namespace mlir::triton { + +std::unique_ptr> createCheckUnsupportedScenarioPass() { + return std::make_unique(); +} + +} // namespace mlir::triton diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.cpp new file mode 100644 index 0000000000..c1d43cff16 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.cpp @@ -0,0 +1,1226 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h" +#include "ascend/include/DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/Utils.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "mlir/Analysis/AliasAnalysis.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/GPU/IR/GPUDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Dominance.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/IR/Value.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; + +static constexpr const char *DEBUG_TYPE = "data-dependency-analysis"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir::triton; +using namespace mlir::CVPipeline; + +// Attribute name constants +static constexpr const char *ssbufferCoreTypeCubeAttr = "CUBE"; +static constexpr const char *ssbufferCoreTypeVectorAttr = "VECTOR"; +static constexpr int ND_SHAPE_LENGTH = 2; +static constexpr int SHAPE_1D_LENGTH = 1; +static constexpr int constantIntType = 32; + +// Helper: ssbuffer.core_type +llvm::StringRef getSsbufferCoreType(Operation *op) { + if (auto attr = op->getAttrOfType(CVPipeline::kCoreType)) { + return attr.getValue(); + } + return ""; +} + +// Helper: Get CoreType from op and index +llvm::StringRef getCoreTypeWithIndex(Operation *op, int index) { + llvm::StringRef typeStr = getSsbufferCoreType(op); + if (typeStr.contains(", ")) { + llvm::SmallVector types; + typeStr.split(types, ", ", -1, false); + if (index < types.size()) { + return types[index].trim(); + } + LOG_DEBUG("Warning: Core type string has multiple types but value is not " + "an OpResult or index out of range.\n"); + return ""; + } + + return typeStr; +} + +void DataDependencyAnalysisPass::updateCoreTypeAtIndex( + Operation *op, int index, llvm::StringRef newCoreType) { + auto coreTypeAttr = + op->getAttrOfType(CVPipeline::kCoreType); + if (!coreTypeAttr) { + LOG_DEBUG("[warning]: failed to rewrite coretype\n"); + return; + } + + llvm::StringRef typeStr = coreTypeAttr.getValue(); + if (typeStr.contains(", ")) { + llvm::SmallVector types; + typeStr.split(types, ", ", -1, false); + if (index < static_cast(types.size())) { + types[index] = newCoreType; + } else { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + LOG_DEBUG("[info]: invalid index for updateCoreTypeAtIndex.\n"); + return; + } + std::string newTypeStr; + for (size_t i = 0; i < types.size(); ++i) { + if (i > 0) + newTypeStr += ", "; + newTypeStr += types[i].str(); + } + op->setAttr(CVPipeline::kCoreType, + StringAttr::get(op->getContext(), newTypeStr)); + } else { + if (index != 0) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + LOG_DEBUG("[info]: invalid index for updateCoreTypeAtIndex.\n"); + return; + } + op->setAttr(CVPipeline::kCoreType, + StringAttr::get(op->getContext(), newCoreType)); + } +} + +// Helper: Check if operation is control flow +bool DataDependencyAnalysisPass::isControlFlowOp(mlir::Operation *op) { + if (!op) + return false; + return isa(op) || isa(op) || + isa(op); +} + +bool DataDependencyAnalysisPass::isCubeAndVectorOp(mlir::Operation *op) { + if (isa(op)) { + return true; + } + return false; +} + +bool DataDependencyAnalysisPass::isValidShapeForDependency(mlir::Value value) { + auto tensorTy = dyn_cast(value.getType()); + if (!tensorTy) { + return false; + } + + if (tensorTy.getRank() != ND_SHAPE_LENGTH) { + return false; + } + return true; +} + +bool DataDependencyAnalysisPass::isValidScalarDependency(mlir::Value value) { + if (isa(value.getType())) { + auto defOp = value.getDefiningOp(); + if (defOp && isa(defOp)) { + return true; + } + } + return false; +} + +bool DataDependencyAnalysisPass::isValid1DValueForDependency( + mlir::Value value) { + auto tensorTy = dyn_cast(value.getType()); + if (tensorTy && tensorTy.getRank() == SHAPE_1D_LENGTH) { + return true; + } + return false; +} + +// Check if a value is only used by transpose ops whose users are all vector +// ops. Returns whether the condition holds and, if so, the yield op that uses +// the transposed value (nullptr if there is none). +std::pair> +DataDependencyAnalysisPass::isAllTransposedInVector(mlir::Value value) { + if (!isa(value.getDefiningOp())) { + return {false, std::nullopt}; + } + if (!llvm::hasSingleElement(value.getUsers())) { + return {false, std::nullopt}; + } + auto *userOp = *value.getUsers().begin(); + if (!isa(userOp)) + return {false, std::nullopt}; + for (mlir::Operation *transposeOpUser : userOp->getUsers()) { + if (getSsbufferCoreType(transposeOpUser) != ssbufferCoreTypeVectorAttr) + return {false, std::nullopt}; + } + + for (mlir::Operation *transposeOpUser : userOp->getUsers()) { + if (isa(transposeOpUser)) { + return {true, transposeOpUser}; + } + } + return {true, std::nullopt}; +} + +// Helper: Check if value is a valid tensor for dependency analysis +// Returns true if value is TensorType and not defined by EmptyOp/FillOp +bool DataDependencyAnalysisPass::isValidValueForDependency(mlir::Value value) { + if (isValidScalarDependency(value)) { + return true; + } + if (isValid1DValueForDependency(value)) { + return true; + } + if (!isValidShapeForDependency(value)) { + return false; + } + + Operation *defOp = value.getDefiningOp(); + // Op that can be processed both by CUBE and VECTOR should not be data + // dependency + if (defOp && isCubeAndVectorOp(defOp)) { + return false; + } + + return true; +} + +// Helper: Check if value is a BlockArgument +bool DataDependencyAnalysisPass::isOuterOpArg(mlir::Value value) { + if (auto blockArg = mlir::dyn_cast(value)) { + mlir::Block *ownerBlock = blockArg.getOwner(); + return true; + } + return false; +} + +// Resolve nested loop iterArg init values until reaching a non-BlockArgument. +mlir::Value DataDependencyAnalysisPass::resolveNestedIterArgInitValue( + mlir::Value initValue) { + llvm::DenseSet visited; + mlir::Value currentValue = initValue; + while (true) { + if (!visited.insert(currentValue).second) + break; + auto blockArg = dyn_cast(currentValue); + if (!blockArg) + break; + mlir::Operation *parentOp = blockArg.getOwner()->getParentOp(); + LOG_DEBUG("parentOp: " << *parentOp << "\n"); + + unsigned argIndex = blockArg.getArgNumber(); + ValueRange initArgs; + if (auto outerFor = dyn_cast(parentOp)) { + if (argIndex == 0) + break; + --argIndex; + initArgs = outerFor.getInitArgs(); + } else if (auto outerWhile = dyn_cast(parentOp)) { + // Triton keeps while before/after block args aligned 1:1 with init args. + initArgs = outerWhile.getInits(); + } else { + break; + } + + LOG_DEBUG("argIndex: " << argIndex << "\n"); + if (argIndex >= initArgs.size()) + break; + currentValue = initArgs[argIndex]; + } + LOG_DEBUG("currentValue: " << currentValue << "\n"); + return currentValue; +} + +// Helper: Build and record BlockInfo +void DataDependencyAnalysisPass::collectBlockInfo( + DataDependencyInfo &info, int blockId, + llvm::SmallVector &ops) { + if (ops.empty()) { + LOG_DEBUG("Warning: Block ID " << blockId << " has no operations.\n"); + return; + } + + BlockInfo blockInfo; + blockInfo.blockId = blockId; + blockInfo.isCube = false; + + // In cases with one or more core_types + // as long as there is a cube, it is necessary to check the dataflow. + StringRef coreType = getSsbufferCoreType(ops[0]); + if (coreType.contains(ssbufferCoreTypeCubeAttr)) { + blockInfo.isCube = true; + } + + blockInfo.isControl = false; + if (isControlFlowOp(ops[0])) { + blockInfo.isControl = true; + } + + llvm::DenseSet opSet(ops.begin(), ops.end()); + + for (auto *op : ops) { + blockInfo.Operations.push_back(op); + for (auto operand : op->getOperands()) { + mlir::Operation *defOp = operand.getDefiningOp(); + // If defOp is not null and defOp is not in current ops set, it's an + // external input + if (!defOp || opSet.find(defOp) == opSet.end()) { + blockInfo.inputs.insert(operand); + } + } + for (auto result : op->getResults()) { + // If any user is not in the current ops set, it's an external output + bool hasExternalUser = false; + for (mlir::Operation *user : result.getUsers()) { + if (opSet.find(user) == opSet.end()) { + hasExternalUser = true; + break; + } + } + if (hasExternalUser) { + blockInfo.outputs.push_back(result); + } + } + } + + info.getBlockInfoMap()[blockInfo.blockId] = blockInfo; + + LOG_DEBUG("Block_ID=" << blockInfo.blockId << "Processed!\n"); +} + +// Block Information Collection +void DataDependencyAnalysisPass::createBlockInfoMap(DataDependencyInfo &info) { + int currentId = -2; + static constexpr int startCurrId = -2; + llvm::SmallVector currentOps; + + module.walk([&](mlir::Operation *op) { + auto opBlockIdOpt = CVPipeline::getOpBlockId(op); + if (opBlockIdOpt) { + int opBlockId = *opBlockIdOpt; + // When the id changes, the block ends && Exclude the initial state + if (opBlockId != currentId && currentId != startCurrId) { + collectBlockInfo(info, currentId, currentOps); + currentOps.clear(); + } + currentId = opBlockId; + currentOps.push_back(op); + } + }); + // Process the last group + if (!currentOps.empty()) { + collectBlockInfo(info, currentId, currentOps); + } +} + +mlir::Operation *DataDependencyAnalysisPass::createBlockInfoConstOp( + OpBuilder &builder, Location loc, llvm::StringRef coreType, + DataDependencyInfo &info) { + int newId = CVPipeline::getAvailableBlockId(module); + auto constOp = builder.create(loc, 0, constantIntType); + setOpBlockId(constOp, newId); + setOpCoreType(constOp, coreType); + + BlockInfo blockInfo; + blockInfo.blockId = newId; + blockInfo.isCube = (coreType == ssbufferCoreTypeCubeAttr); + blockInfo.isControl = false; + blockInfo.Operations.push_back(constOp); + info.getBlockInfoMap()[newId] = blockInfo; + + return constOp; +} + +bool DataDependencyAnalysisPass::collectDepInfo( + mlir::Value depvalue, DependencyType dependencyType, + llvm::SmallVector &dependencies, int iniProdId, + int iniConsId, DataDependencyInfo &info, bool isAllTranspoesd) { + DependencyInfo depInfo; + depInfo.type = dependencyType; + depInfo.value = depvalue; + LOG_DEBUG("try finding common level block IDs\n"); + depInfo.iniProducerBlockId = iniProdId; + depInfo.iniConsumerBlockId = iniConsId; + std::pair commonLevelIds = + findCommonLevelBlockIds(info, iniProdId, iniConsId); + if (commonLevelIds.first == -1 || commonLevelIds.second == -1) { + LOG_DEBUG("Could not find common level block IDs for producer and consumer " + "blocks"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return false; + } + + depInfo.producerBlockId = commonLevelIds.first; + depInfo.consumerBlockId = commonLevelIds.second; + if (isAllTranspoesd) { + depInfo.isAllTranspoesd = true; + } + dependencies.push_back(depInfo); + return true; +} + +// Collects users of iterArg that have a different core type than initCoreType. +llvm::SmallVector +DataDependencyAnalysisPass::collectDiffCoreTypeUsers( + mlir::BlockArgument iterArg, llvm::StringRef initCoreType) { + llvm::SmallVector diffUsers; + + for (mlir::Operation *user : iterArg.getUsers()) { + if (isa(user)) { + continue; + } + if (isControlFlowOp(user)) { + LOG_DEBUG("cannot process nested iterarg!"); + continue; + } + + auto userCoreType = getCoreTypeWithIndex(user, 0); + if (userCoreType != initCoreType && !userCoreType.empty()) { + diffUsers.push_back(user); + } + } + + return diffUsers; +} + +// Inserts a producer block at the beginning of the loop body and records +// cross-core-type dependencies for each user in diffUsers. +void DataDependencyAnalysisPass::insertProducerAndRecordDeps( + mlir::LoopLikeOpInterface loopOp, mlir::BlockArgument loopArg, + llvm::StringRef initCoreType, + llvm::SmallVector &diffUsers, DataDependencyInfo &info) { + auto &v2cDependencies = info.getV2CDependencies(); + auto &c2vDependencies = info.getC2VDependencies(); + + Operation *loopOperation = loopOp.getOperation(); + CVPipeline::MainLoop loop(loopOperation); + OpBuilder builder(loopOperation); + builder.setInsertionPointToStart(loop.getBody()); + Location loc = loopOperation->getLoc(); + auto constOp = createBlockInfoConstOp(builder, loc, initCoreType, info); + int newId = *CVPipeline::getOpBlockId(constOp); + + llvm::DenseSet processedUserBlockIds; + for (auto &user : diffUsers) { + auto userBlockIdOpt = CVPipeline::getOpBlockId(user); + if (!userBlockIdOpt) { + LOG_DEBUG("Warning: User block ID not found for iterArg user.\n"); + continue; + } + int userBlockId = *userBlockIdOpt; + if (!processedUserBlockIds.insert(userBlockId).second) { + continue; + } + // Determine dependency type based on initCoreType + DependencyType depType; + if (initCoreType == ssbufferCoreTypeVectorAttr) { + depType = DependencyType::VectorToCube; + } else if (initCoreType == ssbufferCoreTypeCubeAttr) { + depType = DependencyType::CubeToVector; + } + + auto &targetDeps = (depType == DependencyType::VectorToCube) + ? v2cDependencies + : c2vDependencies; + if (!collectDepInfo(loopArg, depType, targetDeps, newId, userBlockId, + info)) { + continue; + } + + LOG_DEBUG("Recorded iterArg dependency: " + << initCoreType << " -> " + << (depType == DependencyType::VectorToCube + ? ssbufferCoreTypeCubeAttr + : ssbufferCoreTypeVectorAttr) + << ", producerBlockId=" << newId + << ", consumerBlockId=" << userBlockId << "\n"); + } +} + +void DataDependencyAnalysisPass::insertConsumerAndRecordDeps( + mlir::LoopLikeOpInterface loopOp, mlir::Value yieldedValue, + int iterArgIndex, llvm::StringRef initCoreType, DataDependencyInfo &info) { + auto &v2cDependencies = info.getV2CDependencies(); + auto &c2vDependencies = info.getC2VDependencies(); + + Operation *loopOperation = loopOp.getOperation(); + scf::YieldOp yieldOp = CVPipeline::MainLoop::getLoopYieldOp(loopOperation); + OpBuilder builder(yieldOp); + Location loc = yieldOp.getLoc(); + auto constOp = createBlockInfoConstOp(builder, loc, initCoreType, info); + int newId = *CVPipeline::getOpBlockId(constOp); + + Operation *yieldedDefOp = yieldedValue.getDefiningOp(); + auto yieldedDefBlockIdOpt = CVPipeline::getOpBlockId(yieldedDefOp); + if (!yieldedDefBlockIdOpt) { + LOG_DEBUG("Warning: Yielded defining op block ID not found.\n"); + return; + } + int yieldedDefBlockId = *yieldedDefBlockIdOpt; + + DependencyType depType; + if (initCoreType == ssbufferCoreTypeVectorAttr) { + depType = DependencyType::CubeToVector; + } else if (initCoreType == ssbufferCoreTypeCubeAttr) { + depType = DependencyType::VectorToCube; + } + + auto &targetDeps = (depType == DependencyType::VectorToCube) + ? v2cDependencies + : c2vDependencies; + LOG_DEBUG("iniProducerBlockId=" << yieldedDefBlockId + << ", iniConsumerBlockId=" << newId << "\n"); + if (!collectDepInfo(yieldedValue, depType, targetDeps, yieldedDefBlockId, + newId, info)) { + return; + } + targetDeps.back().consumerYieldOp = yieldOp; + + updateCoreTypeAtIndex(yieldOp, iterArgIndex, initCoreType); + updateCoreTypeAtIndex(loopOperation, iterArgIndex, initCoreType); + + LOG_DEBUG("Recorded yield producer dependency: " + << initCoreType << ", iniProducerBlockId=" << yieldedDefBlockId + << ", iniConsumerBlockId=" << newId << "\n"); +} + +void DataDependencyAnalysisPass::recordInitValueDeps( + mlir::LoopLikeOpInterface loopOp, mlir::Value initValue, + llvm::StringRef yieldCoreType, DataDependencyInfo &info) { + auto &v2cDependencies = info.getV2CDependencies(); + auto &c2vDependencies = info.getC2VDependencies(); + + Operation *initDefOp = initValue.getDefiningOp(); + auto initDefBlockIdOpt = CVPipeline::getOpBlockId(initDefOp); + if (!initDefBlockIdOpt) { + LOG_DEBUG("Warning: Init defining op block ID not found.\n"); + return; + } + int initDefBlockId = *initDefBlockIdOpt; + + auto loopBlockIdOpt = CVPipeline::getOpBlockId(loopOp.getOperation()); + if (!loopBlockIdOpt) { + LOG_DEBUG("Warning: Loop block ID not found.\n"); + return; + } + int loopBlockId = *loopBlockIdOpt; + + DependencyType depType; + if (yieldCoreType == ssbufferCoreTypeVectorAttr) { + depType = DependencyType::CubeToVector; + } else if (yieldCoreType == ssbufferCoreTypeCubeAttr) { + depType = DependencyType::VectorToCube; + } + + auto &targetDeps = (depType == DependencyType::VectorToCube) + ? v2cDependencies + : c2vDependencies; + LOG_DEBUG("iniProducerBlockId=" << initDefBlockId << ", iniConsumerBlockId=" + << loopBlockId << "\n"); + if (!collectDepInfo(initValue, depType, targetDeps, initDefBlockId, + loopBlockId, info)) { + return; + } + + LOG_DEBUG("Recorded init value dependency: " + << yieldCoreType << ", iniProducerBlockId=" << initDefBlockId + << ", iniConsumerBlockId=" << loopBlockId << "\n"); +} + +static bool checkLoopYieldCoreType(scf::YieldOp yieldOp) { + if (!yieldOp) { + return false; + } + for (unsigned index = 0; index < yieldOp.getNumOperands(); ++index) { + mlir::Value value = yieldOp.getOperand(index); + llvm::StringRef yieldCoreType = getCoreTypeWithIndex(yieldOp, index); + + mlir::Operation *definingOp = value.getDefiningOp(); + if (!definingOp || !isa(definingOp)) { + continue; + } + auto defResult = dyn_cast(value); + int resultIndex = defResult ? defResult.getResultNumber() : 0; + llvm::StringRef definingCoreType = + getCoreTypeWithIndex(definingOp, resultIndex); + + if (yieldCoreType != definingCoreType) { + return false; + } + } + return true; +} + +// Process iterArg dependencies for all supported loop-like operations. +void DataDependencyAnalysisPass::processIterArgDependencies() { + auto &info = getAnalysis(); + + llvm::SmallVector loopOps; + module.walk( + [&](mlir::LoopLikeOpInterface loopOp) { loopOps.push_back(loopOp); }); + LOG_DEBUG("Processing iterArg dependencies, found " << loopOps.size() + << " loop ops\n"); + + for (mlir::LoopLikeOpInterface loopOp : loopOps) { + Operation *loopOperation = loopOp.getOperation(); + if (!isa(loopOperation)) { + continue; + } + + CVPipeline::MainLoop loop(loopOperation); + scf::YieldOp yieldOp = CVPipeline::MainLoop::getLoopYieldOp(loopOperation); + if (!checkLoopYieldCoreType(yieldOp)) { + LOG_DEBUG("[ERROR]: Yield core type mismatch defining op\n"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + size_t numIterArgs = loopOp.getInits().size(); + SmallVector iterArgs = loop.getIterArgs(); + if (iterArgs.size() != numIterArgs) { + LOG_DEBUG("[ERROR]: Loop iter_arg count does not match init count\n"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + for (int iterArgIndex = 0; iterArgIndex < numIterArgs; ++iterArgIndex) { + mlir::Value initValue = loopOp.getInits()[iterArgIndex]; + mlir::BlockArgument iterArg = + cast(iterArgs[iterArgIndex]); + mlir::Value yieldedValue = loopOp.getYieldedValues()[iterArgIndex]; + LOG_DEBUG("initValue" << initValue << "\n"); + LOG_DEBUG("yieldedValue" << yieldedValue << "\n"); + + if (!isValid1DValueForDependency(iterArg) && + (!isValidShapeForDependency(initValue) || + !isValidShapeForDependency(yieldedValue))) { + LOG_DEBUG("iterarg: " << iterArg + << " is not valid tensor for dependency!"); + continue; + } + + Operation *initDefOp = initValue.getDefiningOp(); + Operation *yieldedDefOp = yieldedValue.getDefiningOp(); + if (!yieldedDefOp) { + continue; + } + auto yieldCoreType = getCoreTypeWithIndex(loopOperation, iterArgIndex); + + if (!initDefOp) { + auto realInitValue = resolveNestedIterArgInitValue(initValue); + auto realInitDefOp = realInitValue.getDefiningOp(); + auto realInitDefReuslt = dyn_cast(realInitValue); + if (!realInitDefOp || isCubeAndVectorOp(realInitDefOp)) { + continue; + } + if (getCoreTypeWithIndex(realInitDefOp, + realInitDefReuslt + ? realInitDefReuslt.getResultNumber() + : 0) != yieldCoreType) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + LOG_DEBUG("[info]: nested conflict iterarg!"); + return; + } + initDefOp = realInitDefOp; + initValue = realInitValue; + } + auto initDefReuslt = dyn_cast(initValue); + auto initCoreType = getCoreTypeWithIndex( + initDefOp, initDefReuslt ? initDefReuslt.getResultNumber() : 0); + + LOG_DEBUG("[initDefOp]: " << *initDefOp << "\n"); + if (isCubeAndVectorOp(initDefOp)) { + if (yieldCoreType == CVPipeline::kCoreTypeVector) { + auto diffUsers = collectDiffCoreTypeUsers(iterArg, yieldCoreType); + if (!diffUsers.empty()) { + insertProducerAndRecordDeps(loopOp, iterArg, yieldCoreType, + diffUsers, info); + } + } else if (yieldCoreType == CVPipeline::kCoreTypeCube) { + auto vectorUsers = + collectDiffCoreTypeUsers(iterArg, CVPipeline::kCoreTypeCube); + auto cubeUsers = + collectDiffCoreTypeUsers(iterArg, CVPipeline::kCoreTypeVector); + if (!vectorUsers.empty()) { + insertConsumerAndRecordDeps(loopOp, yieldedValue, iterArgIndex, + CVPipeline::kCoreTypeVector, info); + if (!cubeUsers.empty()) { + insertProducerAndRecordDeps(loopOp, iterArg, + CVPipeline::kCoreTypeVector, + cubeUsers, info); + } + } + } + } else if (initCoreType == yieldCoreType) { + auto diffUsers = collectDiffCoreTypeUsers(iterArg, yieldCoreType); + if (!diffUsers.empty()) { + insertProducerAndRecordDeps(loopOp, iterArg, yieldCoreType, diffUsers, + info); + } + } else { + llvm::SmallVector initCoreTypeUsers; + llvm::SmallVector yieldCoreTypeUsers; + + for (mlir::Operation *user : iterArg.getUsers()) { + if (isa(user)) { + continue; + } + if (isControlFlowOp(user)) { + LOG_DEBUG("cannot process nested iterarg!"); + continue; + } + auto userCoreType = getCoreTypeWithIndex(user, 0); + if (userCoreType == initCoreType) { + initCoreTypeUsers.push_back(user); + } else if (userCoreType == yieldCoreType) { + yieldCoreTypeUsers.push_back(user); + } + } + if (!initCoreTypeUsers.empty() && !yieldCoreTypeUsers.empty()) { + recordInitValueDeps(loopOp, initValue, yieldCoreType, info); + insertProducerAndRecordDeps(loopOp, iterArg, yieldCoreType, + yieldCoreTypeUsers, info); + } else if (!initCoreTypeUsers.empty() && yieldCoreTypeUsers.empty()) { + insertConsumerAndRecordDeps(loopOp, yieldedValue, iterArgIndex, + initCoreType, info); + } else if (initCoreTypeUsers.empty() && !yieldCoreTypeUsers.empty()) { + recordInitValueDeps(loopOp, initValue, yieldCoreType, info); + } else { + LOG_DEBUG("no dependencies with: " << iterArg << "\n"); + } + } + } + } +} + +// Analyze V->C and C->C +void DataDependencyAnalysisPass::analyzeExternalInputs( + DataDependencyInfo &info) { + auto &blockInfoMap = info.getBlockInfoMap(); + auto &v2cDependencies = info.getV2CDependencies(); + + LOG_DEBUG("Analyzing external inputs for Cube blocks...\n"); + for (auto &[id, blockInfo] : blockInfoMap) { + if (!blockInfo.isCube || blockInfo.isControl || blockInfo.inputs.empty()) + continue; + LOG_DEBUG("Analyzing external inputs for Cube Block ID: " << id << "\n"); + for (mlir::Value input : blockInfo.inputs) { + // Check if input is a value which can be produced by CUBE + if (!isValidValueForDependency(input)) { + LOG_DEBUG("Warning: [v->c] Input value is not a valid tensor for " + "dependency analysis.\n"); + continue; + } + // Check if input is a blockarg. + if (isOuterOpArg(input)) { + LOG_DEBUG("Warning: [v->c] Input value is a function/scf parameter.\n"); + continue; + } + + Operation *defOp = input.getDefiningOp(); + auto defReuslt = dyn_cast(input); + auto coreType = getCoreTypeWithIndex( + defOp, defReuslt ? defReuslt.getResultNumber() : 0); + if (coreType == "") { + LOG_DEBUG("Warning: [v->c] Input value has no core type attribute.\n"); + continue; + } + + // Case 1: Cube -> C->C dependency + if (coreType == ssbufferCoreTypeCubeAttr) { + LOG_DEBUG("Found external input with CUBE core type: " << input + << "\n"); + auto producerIdOpt = CVPipeline::getOpBlockId(input.getDefiningOp()); + if (!producerIdOpt) { + LOG_DEBUG("Warning: [c->c] Producer block ID not found for input " + "value.\n"); + continue; + } + int producerId = *producerIdOpt; + if (!collectDepInfo(input, DependencyType::CubeToCube, + info.getC2CDependencies(), producerId, + blockInfo.blockId, info)) { + continue; + } + continue; + } + // Case 2: Vector -> V->C dependency + if (coreType == ssbufferCoreTypeVectorAttr) { + LOG_DEBUG("Found external input with VECTOR core type: " << input + << "\n"); + auto producerIdOpt = CVPipeline::getOpBlockId(input.getDefiningOp()); + if (!producerIdOpt) { + LOG_DEBUG( + "Warning: [v->c] Producer block ID not found for input value.\n"); + continue; + } + int producerId = *producerIdOpt; + if (!collectDepInfo(input, DependencyType::VectorToCube, + v2cDependencies, producerId, blockInfo.blockId, + info)) { + continue; + } + } + } + } + LOG_DEBUG("External input analysis complete.\n"); +} + +// Analyze C->V +void DataDependencyAnalysisPass::analyzeExternalOutputs( + DataDependencyInfo &info) { + auto &blockInfoMap = info.getBlockInfoMap(); + auto &c2vDependencies = info.getC2VDependencies(); + + LOG_DEBUG("Analyzing external outputs for Cube blocks...\n"); + for (auto &[id, blockInfo] : blockInfoMap) { + if (!blockInfo.isCube || blockInfo.outputs.empty()) + continue; + + for (mlir::Value output : blockInfo.outputs) { + // Check if output is a value which can be produced by CUBE + if (!isValidValueForDependency(output)) { + LOG_DEBUG("Warning: [c->v] Output value is not a valid tensor for " + "dependency analysis.\n"); + continue; + } + if (isa(output.getType())) { + LOG_DEBUG("Warning: [c->v] Output value is a scalar, not a valid " + "tensor for dependency analysis.\n"); + continue; + } + + auto opResult = dyn_cast(output); + unsigned resultIndex = opResult.getResultNumber(); + StringRef resultCoreType = + getCoreTypeWithIndex(output.getDefiningOp(), resultIndex); + if (resultCoreType != ssbufferCoreTypeCubeAttr) { + continue; + } + + // Check who is using this output + llvm::DenseSet handledBlockIds; + + // if c->v value will be transposed and then used by vector op, the value + // can be transposed within fixpipe + auto [isAllTranspoesd, transposedYieldOp] = + isAllTransposedInVector(output); + + for (mlir::Operation *user : output.getUsers()) { + int outputIndex = 0; + if (isa(user)) { + for (unsigned i = 0; i < user->getNumOperands(); ++i) { + if (user->getOperand(i) == output) { + outputIndex = i; + break; + } + } + } + auto userCoreType = getCoreTypeWithIndex(user, outputIndex); + if ((userCoreType == "")) { + LOG_DEBUG( + "Warning: [c->v] Input value has no core type attribute.\n"); + continue; + } + if (userCoreType == ssbufferCoreTypeVectorAttr) { + LOG_DEBUG("Found external output used by VECTOR core type: " << output + << "\n"); + auto consumerIdOpt = CVPipeline::getOpBlockId(user); + if (!consumerIdOpt) { + LOG_DEBUG("Warning: [c->v] Consumer block ID not found for user " + "operation.\n"); + continue; + } + int consumerId = *consumerIdOpt; + auto inserted = handledBlockIds.insert(consumerId).second; + if (inserted) { + if (!collectDepInfo(output, DependencyType::CubeToVector, + c2vDependencies, blockInfo.blockId, consumerId, + info, isAllTranspoesd)) { + continue; + } + if (transposedYieldOp.has_value()) { + c2vDependencies.back().consumerYieldOp = *transposedYieldOp; + } + } + } + // If user belongs to Cube block, this C->C dependency was handled + // in the Input analysis phase, so here we only handle C->V. + } + } + } + LOG_DEBUG("External output analysis complete.\n"); +} + +// Trace an operand's defining op back to find the source matmul. +static linalg::MatmulOp resolveSameBlockMatmulProducer(mlir::Value operand, + int consumerBlockId) { + Operation *defOp = CVPipeline::getSourceThroughCIntermediateOps(operand); + + auto producer = dyn_cast_if_present(defOp); + if (!producer) { + return nullptr; + } + auto producerBlockIdOpt = CVPipeline::getOpBlockId(producer); + if (!producerBlockIdOpt || *producerBlockIdOpt != consumerBlockId) { + return nullptr; + } + return producer; +} + +// Analyze C->C dependencies between the matmuls inside the same computeBlock. +void DataDependencyAnalysisPass::analyzeInternalDeps(DataDependencyInfo &info) { + auto &blockInfoMap = info.getBlockInfoMap(); + auto &intraBlockDeps = info.getIntraC2CDependencies(); + + LOG_DEBUG("Analyzing intra-block matmul dependencies...\n"); + for (auto &[blockId, blockInfo] : blockInfoMap) { + if (!blockInfo.isCube) { + continue; + } + + llvm::SmallVector matmuls; + for (mlir::Operation *op : blockInfo.Operations) { + if (auto matmulOp = dyn_cast(op)) { + matmuls.push_back(matmulOp); + } + } + if (matmuls.size() < 2) { + continue; + } + + for (linalg::MatmulOp consumer : matmuls) { + for (OpOperand &opOperand : consumer->getOpOperands()) { + if (!resolveSameBlockMatmulProducer(opOperand.get(), blockId)) { + continue; + } + DependencyInfo depInfo; + depInfo.type = DependencyType::CubeToCube; + depInfo.value = opOperand.get(); + depInfo.operand = &opOperand; + depInfo.producerBlockId = blockId; + depInfo.consumerBlockId = blockId; + depInfo.iniProducerBlockId = blockId; + depInfo.iniConsumerBlockId = blockId; + intraBlockDeps.push_back(depInfo); + } + } + } + LOG_DEBUG("Intra-block matmul dependency analysis complete. Found " + << intraBlockDeps.size() << " dependencies.\n"); +} + +void DataDependencyAnalysisPass::collectMemDepInfo( + llvm::StringRef predCoreType, int producerBlockId, int consumerBlockId, + int predBlockId, int currBlockId, + llvm::SmallVector &memoryDependencies, + mlir::Operation *predOp, mlir::Operation *nextOp) { + DependencyInfo depInfo; + + if (predCoreType == ssbufferCoreTypeCubeAttr) { + depInfo.type = DependencyType::CubeToVector; + } else if (predCoreType == ssbufferCoreTypeVectorAttr) { + depInfo.type = DependencyType::VectorToCube; + } + depInfo.producerBlockId = producerBlockId; + depInfo.consumerBlockId = consumerBlockId; + depInfo.iniProducerBlockId = predBlockId; + depInfo.iniConsumerBlockId = currBlockId; + + depInfo.predOp = predOp; + depInfo.nextOp = nextOp; + + memoryDependencies.push_back(depInfo); +} + +void DataDependencyAnalysisPass::analyzeMemoryEffect(DataDependencyInfo &info) { + auto &memoryDependencies = info.getMemoryDependencies(); + LOG_DEBUG("\n=== start mem dep analysis ===\n"); + + auto &aliasAnalysis = getAnalysis(); + MemoryDependenceGraph memDepGraph(module, aliasAnalysis); + + auto walkResult = module.walk([&](mlir::Operation *op) -> WalkResult { + if (op->getNumRegions() > 0) { + return WalkResult::advance(); + } + if (isa(op)) { + return WalkResult::advance(); + } + auto currBlockIdOpt = CVPipeline::getOpBlockId(op); + llvm::StringRef currCoreType = getSsbufferCoreType(op); + if (!currBlockIdOpt || currCoreType.empty()) { + return WalkResult::advance(); + } + int currBlockId = *currBlockIdOpt; + + for (mlir::Operation *predOp : memDepGraph.getExecBefore(op)) { + if (isa(predOp)) { + continue; + } + if (predOp->getNumRegions() > 0) { + auto realdeps = memDepGraph.getRealDependency(predOp, op); + if (realdeps.empty()) { + return WalkResult::advance(); + } + for (mlir::Operation *realPredOp : realdeps) { + if (isa(realPredOp)) { + continue; + } + auto realPredBlockIdOpt = CVPipeline::getOpBlockId(realPredOp); + llvm::StringRef realPredCoreType = getSsbufferCoreType(realPredOp); + if (!realPredBlockIdOpt || realPredCoreType == currCoreType || + realPredCoreType.empty()) { + continue; + } + int realPredBlockId = *realPredBlockIdOpt; + auto [producerBlockId, consumerBlockId] = + findCommonLevelBlockIds(info, realPredBlockId, currBlockId); + if (producerBlockId == -1 || consumerBlockId == -1) { + LOG_DEBUG("Could not find common level block IDs for producer and " + "consumer blocks"); + return WalkResult::interrupt(); + } + collectMemDepInfo(realPredCoreType, producerBlockId, consumerBlockId, + realPredBlockId, currBlockId, memoryDependencies, + realPredOp, op); + + LOG_DEBUG("\n=op with region mem dep analysis= " + << "\nrealpredcoretype" << realPredCoreType + << "\nproducer Block: " << realPredBlockId + << "\nproducer Op: " << *realPredOp << "\nconsumer Block: " + << currBlockId << "\nconsumer Op: " << *op << "\n"); + } + continue; + } + auto predBlockIdOpt = CVPipeline::getOpBlockId(predOp); + llvm::StringRef predCoreType = getSsbufferCoreType(predOp); + if (!predBlockIdOpt || predCoreType == currCoreType || + predCoreType.empty()) { + continue; + } + int predBlockId = *predBlockIdOpt; + + auto [producerBlockId, consumerBlockId] = + findCommonLevelBlockIds(info, predBlockId, currBlockId); + if (producerBlockId == -1 || consumerBlockId == -1) { + LOG_DEBUG("Could not find common level block IDs for producer and " + "consumer blocks"); + return WalkResult::interrupt(); + } + if (producerBlockId == consumerBlockId) { + continue; + } + + collectMemDepInfo(predCoreType, producerBlockId, consumerBlockId, + predBlockId, currBlockId, memoryDependencies, predOp, + op); + + LOG_DEBUG("\n=mem dep analysis= " << "\npredcoretype" << predCoreType + << "\nproducer Block: " << predBlockId + << "\nproducer Op: " << *predOp + << "\nconsumer Block: " << currBlockId + << "\nconsumer Op: " << *op << "\n"); + } + return WalkResult::advance(); + }); + if (walkResult.wasInterrupted()) { + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + LOG_DEBUG("=== mem dep analysis complete ===\n"); +} + +// Producer/Consumer Hierarchy Analysis +std::pair DataDependencyAnalysisPass::findCommonLevelBlockIds( + DataDependencyInfo &info, int producerBlockId, int consumerBlockId) { + auto &blockInfoMap = info.getBlockInfoMap(); + + LOG_DEBUG("start findCommonLevelBlockIds...\n"); + + // Step 1: Get corresponding BlockInfo from Map + auto pIt = blockInfoMap.find(producerBlockId); + auto cIt = blockInfoMap.find(consumerBlockId); + // Defensive programming: if corresponding Block info not found, return + // original ID or error code + if (pIt == blockInfoMap.end() || cIt == blockInfoMap.end()) { + return {producerBlockId, consumerBlockId}; + } + + BlockInfo &pInfo = pIt->second; + BlockInfo &cInfo = cIt->second; + + // Take the first operation of each Block as representative to check hierarchy + // (Assumes all operations in a Block are closely related in hierarchy) + mlir::Operation *producerOp = pInfo.Operations[0]; + mlir::Operation *consumerOp = cInfo.Operations[0]; + + mlir::Block *pBlock = producerOp->getBlock(); + mlir::Block *cBlock = consumerOp->getBlock(); + + // Case 1: In the same MLIR Block + if (pBlock == cBlock) { + return {producerBlockId, consumerBlockId}; + } + + // Case 2: In different Blocks, find Lowest Common Ancestor (LCA) + // Step 1: Collect producer's ancestor chain + llvm::SmallVector pAncestors; + pAncestors.push_back(producerOp); + mlir::Operation *current = producerOp->getParentOp(); + while (current) { + pAncestors.push_back(current); + current = current->getParentOp(); + } + + // Step 2: Walk up consumer's ancestors, using current and before for rolling + mlir::Operation *before = consumerOp; // Initialize as consumerOp itself + current = consumerOp; // Initialize as parent + + while (current) { + // --- Found common ancestor --- + auto it = std::find(pAncestors.begin(), pAncestors.end(), current); + if (it != pAncestors.end()) { + size_t pIndex = std::distance(pAncestors.begin(), it); + if (pIndex == 0) { + break; + } + mlir::Operation *pPrevOp = pAncestors[pIndex - 1]; + auto pPrevIdOpt = CVPipeline::getOpBlockId(pPrevOp); + auto cPrevIdOpt = CVPipeline::getOpBlockId(before); + int pPrevId = pPrevIdOpt ? *pPrevIdOpt : -1; + int cPrevId = cPrevIdOpt ? *cPrevIdOpt : -1; + if (!pPrevIdOpt) { + LOG_DEBUG("Warning: Producer ancestor operation has no block ID " + "attribute.\n"); + } + if (!cPrevIdOpt) { + LOG_DEBUG("Warning: Consumer ancestor operation has no block ID " + "attribute.\n"); + } + return {pPrevId, cPrevId}; + } + + // Rolling: continue upward + before = current; + current = current->getParentOp(); + } + LOG_DEBUG("Warning: No common ancestor found for producer block " + << producerBlockId << " and consumer block " << consumerBlockId + << "\n"); + return {-1, -1}; +} + +// Deduplicate dependencies: remove duplicates with same value, +// iniConsumerBlockId, and iniProducerBlockId +void DataDependencyAnalysisPass::deduplicateDependencies( + llvm::SmallVector &dependencies) { + auto newEnd = + std::unique(dependencies.begin(), dependencies.end(), + [](const DependencyInfo &a, const DependencyInfo &b) { + return a.value == b.value && + a.iniConsumerBlockId == b.iniConsumerBlockId && + a.iniProducerBlockId == b.iniProducerBlockId; + }); + + dependencies.erase(newEnd, dependencies.end()); +} + +void DataDependencyAnalysisPass::runOnOperation() { + LOG_DEBUG("\n--- enter DataDependencyAnalysisPass --->\n"); + module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + auto &info = getAnalysis(); + + // Step 1: Collect block information (populate blockInfoMap) + createBlockInfoMap(info); + + // Step 2: Analyze iter_args dependencies + processIterArgDependencies(); + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + createBlockInfoMap(info); + + // Step 3: Analyze dependencies (v2c/c2v/c2c). Scalar V->C deps come from + // analyzeExternalInputs (VECTOR scalars crossing into CUBE). + analyzeExternalInputs(info); + + analyzeExternalOutputs(info); + + // Step 4: Analyze intra-block c2c dependencies + analyzeInternalDeps(info); + + // Step 5: Analyze memory dependencies (memdep sync) + analyzeMemoryEffect(info); + + // Step 6: Deduplicate dependencies (remove duplicates with same value, + // iniConsumerBlockId, iniProducerBlockId) + deduplicateDependencies(info.getV2CDependencies()); + deduplicateDependencies(info.getC2VDependencies()); + deduplicateDependencies(info.getC2CDependencies()); + deduplicateDependencies(info.getMemoryDependencies()); + + info.setValid(true); + + LOG_DEBUG("DataDependencyAnalysisPass: Analysis complete.\n"); + LOG_DEBUG(" V->C dependencies: " << info.getV2CDependencies().size() + << "\n"); + LOG_DEBUG(" C->V dependencies: " << info.getC2VDependencies().size() + << "\n"); + LOG_DEBUG(" C->C dependencies: " << info.getC2CDependencies().size() + << "\n"); + LOG_DEBUG(" Memory dependencies: " << info.getMemoryDependencies().size() + << "\n"); + LOG_DEBUG(" Intra-block C->C dependencies: " + << info.getIntraC2CDependencies().size() << "\n"); + + LOG_DEBUG("\n--- exit DataDependencyAnalysisPass --->\n"); +} + +// Create the pass +namespace mlir { +namespace triton { +std::unique_ptr> createDataDependencyAnalysisPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/FlagIdReuse.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/FlagIdReuse.cpp new file mode 100644 index 0000000000..28e98799de --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/FlagIdReuse.cpp @@ -0,0 +1,288 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SplitDataflow/FlagIdReuse.h" + +#include +#include +#include +#include + +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Operation.h" +#include "mlir/Support/LLVM.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Casting.h" + +static constexpr const char *DEBUG_TYPE = "flag-id-reuse"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir; +using namespace mlir::triton; +using namespace hivm; + +void FlagIdReuseManager::insertRelationBetweenSetAndWait(Operation *before, + Operation *after) { + if (!before || !after) { + LOG_DEBUG("Warning: inserting a relation with null operation, ignored."); + return; + } + + LOG_DEBUG("Add an edge from \n" << *before << " to \n" << *after << "\n"); + relations[before].push_back(after); + return; +} + +DenseMap FlagIdReuseManager::reuseInterCoreTransferFlagIds( + const llvm::SmallVector &syncOps) { + DenseMap remapResult; + if (syncOps.empty()) { + return remapResult; + } + + preworkForAnalyze(syncOps); + return colorInterferenceGraph(); +} + +void FlagIdReuseManager::preworkForAnalyze( + const llvm::SmallVector &syncOps) { + flagIdToOps.clear(); + opOrder.clear(); + // syncOps arrive in module-walk (program) order, so the index is a stable + // program-order rank used to pick the earliest set / latest wait of a flag. + int order = 0; + for (auto op : syncOps) { + opOrder[op] = order++; + auto flagId = getFlagId(op); + if (flagId == -1) { + continue; + } + flagIdToOps[flagId].push_back(op); + } +} + +Operation *FlagIdReuseManager::getEarliestSet(int flagId) { + Operation *earliest = nullptr; + for (auto op : flagIdToOps[flagId]) { + if (!llvm::isa(op)) { + continue; + } + if (!earliest || opOrder[op] < opOrder[earliest]) { + earliest = op; + } + } + return earliest; +} + +Operation *FlagIdReuseManager::getLatestWait(int flagId) { + Operation *latest = nullptr; + for (auto op : flagIdToOps[flagId]) { + if (!llvm::isa(op)) { + continue; + } + if (!latest || opOrder[op] > opOrder[latest]) { + latest = op; + } + } + return latest; +} + +bool FlagIdReuseManager::opPrecedes(Operation *p, Operation *q) { + if (!p || !q) { + return false; + } + if (p == q) { + return true; + } + // Same block: MLIR static program order is an exact, sound ordering and + // covers straight-line chains and sibling loops without needing a graph edge. + if (p->getBlock() == q->getBlock()) { + return p->isBeforeInBlock(q); + } + // Cross block: only the real happens-before edges (E1 per-pipe FIFO, E2 + // set->wait, E3 data, E4 read-wait->consumed data) may prove ordering. + llvm::SmallSet visited; + return hasPath(visited, p, q); +} + +// `before` is fully released before `after` is acquired iff the latest wait of +// `before` is ordered before the earliest set of `after`. +bool FlagIdReuseManager::flagReleasedBefore(int before, int after) { + Operation *release = getLatestWait(before); + Operation *acquire = getEarliestSet(after); + if (!release || !acquire) { + return false; + } + return opPrecedes(release, acquire); +} + +// A transfer's data-movement direction, read off its sync pipes. Cube->Vector +// transfers (fixpipe) ride the FIX/V pipes; Vector->Cube transfers (copy) ride +// the MTE*/M pipes. The two never share a pipe, so a single pipe decides. +FlagIdReuseManager::SyncDir FlagIdReuseManager::getFlagDirection(int flagId) { + for (auto *op : flagIdToOps[flagId]) { + PipeAttr srcPipe, dstPipe; + if (auto setOp = llvm::dyn_cast(op)) { + srcPipe = setOp.getTpipeAttr(); + dstPipe = setOp.getPipeAttr(); + } else if (auto waitOp = llvm::dyn_cast(op)) { + srcPipe = waitOp.getTpipeAttr(); + dstPipe = waitOp.getPipeAttr(); + } + for (PipeAttr pipe : {srcPipe, dstPipe}) { + if (pipe && (pipe.getPipe() == hivm::PIPE::PIPE_FIX || + pipe.getPipe() == hivm::PIPE::PIPE_V)) { + return SyncDir::CubeToVector; + } + } + } + return SyncDir::VectorToCube; +} + +// Conservative: two flags interfere unless one is provably released before the +// other is acquired. Any uncertainty falls back to "interfere" (no reuse), so a +// reused id is never shared by two concurrently-live transfers. +bool FlagIdReuseManager::flagsInterfere(int lhs, int rhs) { + // Opposite-direction transfers (Vector->Cube vs Cube->Vector) execute + // concurrently on the two cores, so their flag lifetimes always overlap + if (getFlagDirection(lhs) != getFlagDirection(rhs)) { + return true; + } + return !flagReleasedBefore(lhs, rhs) && !flagReleasedBefore(rhs, lhs); +} + +DenseMap FlagIdReuseManager::colorInterferenceGraph() { + // Flags ordered by first program appearance, for deterministic coloring and + // compact renumbering. + llvm::SmallVector flagIds; + for (auto &[flagId, ops] : flagIdToOps) { + if (!ops.empty()) { + flagIds.push_back(flagId); + } + } + auto firstOrder = [&](int flagId) { + int best = std::numeric_limits::max(); + for (auto op : flagIdToOps[flagId]) { + best = std::min(best, opOrder[op]); + } + return best; + }; + llvm::sort(flagIds, + [&](int a, int b) { return firstOrder(a) < firstOrder(b); }); + + // Build the (undirected) interference graph. + DenseMap> adj; + for (size_t i = 0; i < flagIds.size(); ++i) { + for (size_t j = i + 1; j < flagIds.size(); ++j) { + if (flagsInterfere(flagIds[i], flagIds[j])) { + adj[flagIds[i]].push_back(flagIds[j]); + adj[flagIds[j]].push_back(flagIds[i]); + } + } + } + + // Welsh-Powell greedy: color highest-degree nodes first (tie: earlier flag), + // each takes the smallest color unused by its already-colored neighbours. + llvm::SmallVector order(flagIds.begin(), flagIds.end()); + llvm::sort(order, [&](int a, int b) { + if (adj[a].size() != adj[b].size()) { + return adj[a].size() > adj[b].size(); + } + return firstOrder(a) < firstOrder(b); + }); + + DenseMap rawColor; + for (int flagId : order) { + llvm::SmallSet usedByNeighbours; + for (int nb : adj[flagId]) { + auto it = rawColor.find(nb); + if (it != rawColor.end()) { + usedByNeighbours.insert(it->second); + } + } + int color = 1; + while (usedByNeighbours.contains(color)) { + ++color; + } + rawColor[flagId] = color; + } + + // Compact renumber: walk flags in program order, assign 1,2,3,... to colors + // on first use, so output flag ids are minimal and stably ordered. + DenseMap colorToCompact; + int nextCompact = 1; + DenseMap remapResult; + for (int flagId : flagIds) { + int color = rawColor[flagId]; + if (!colorToCompact.contains(color)) { + colorToCompact[color] = nextCompact++; + } + remapResult[flagId] = colorToCompact[color]; + } + return remapResult; +} + +bool FlagIdReuseManager::hasPath( + llvm::SmallSet &visited, + Operation *from, Operation *to) { + if (from == to) { + return true; + } + if (visited.contains(from)) { + return false; + } + visited.insert(from); + for (auto nxt : relations[from]) { + if (hasPath(visited, nxt, to)) { + return true; + } + } + return false; +} + +int FlagIdReuseManager::getFlagId(Operation *op) { + if (auto setOp = llvm::dyn_cast(op)) { + if (auto staticFlagId = setOp.getStaticFlagId()) { + return staticFlagId->getInt(); + } + LOG_DEBUG( + "Warning: SyncBlockSetOp has no static flagId (dynamic?), skipped: \n" + << *op << "\n"); + return -1; + } + if (auto waitOp = llvm::dyn_cast(op)) { + if (auto staticFlagId = waitOp.getStaticFlagId()) { + return staticFlagId->getInt(); + } + LOG_DEBUG( + "Warning: SyncBlockWaitOp has no static flagId (dynamic?), skipped: \n" + << *op << "\n"); + return -1; + } + LOG_DEBUG("Warning: failed to get flagId from op: \n" << *op << "\n"); + return -1; +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.cpp new file mode 100644 index 0000000000..b520ee1530 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.cpp @@ -0,0 +1,2539 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SplitDataflow/InterCoreTransferAndSync.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/Utils.h" + +#include +#include +#include +#include + +#include "ascend/include/DynamicCVPipeline/Common/FlagIdManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/DataDependencyAnalysis.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/FlagIdReuse.h" + +#include "Utils/Utils.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/HIVM/IR/HIVMInterfaces.h" +#include "bishengir/Dialect/Utils/Util.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/Block.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/Dominance.h" +#include "mlir/IR/Operation.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; + +static constexpr const char *DEBUG_TYPE = "inter-core-transfer-and-sync"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir::triton; +using namespace hivm; + +static constexpr int kIntegerBitWidth = 32; +static constexpr int iniNzDimWidth = 16; + +static uint64_t getElemBytesForAlign(Type t) { + static constexpr uint64_t kBitsPerByte = 8; + if (auto ft = dyn_cast(t)) { + return (uint64_t)((ft.getWidth() + kBitsPerByte - 1) / kBitsPerByte); + } + if (auto it = dyn_cast(t)) { + return (uint64_t)((it.getWidth() + kBitsPerByte - 1) / kBitsPerByte); + } + if (isa(t)) { + return 8ULL; + } + if (auto ct = dyn_cast(t)) { + return 2ULL * getElemBytesForAlign(ct.getElementType()); + } + return 0ULL; +} + +static uint64_t getBlockElemsFor32BAlign(Type elemType) { + constexpr uint64_t kAlignBytes = 32; + uint64_t elemBytes = getElemBytesForAlign(elemType); + if (elemBytes == 0) { + return 0; + } + if (elemBytes < 0 || kAlignBytes % elemBytes != 0) { + return 0; + } + if (elemBytes >= kAlignBytes) { + return 1; + } + return kAlignBytes / elemBytes; +} + +std::pair +InterCoreTransferAndSyncPass::analyzeMatmulOperand(DependencyInfo &dep) { + bool isMatmulA = true; + bool isTranspose = false; + + Value startValue = dep.value; + + llvm::DenseSet visited; + SmallVector chain; + chain.push_back(startValue); + visited.insert(startValue); + + while (!chain.empty()) { + Value currentValue = chain.back(); + chain.pop_back(); + + for (Operation *userOp : currentValue.getUsers()) { + if (CVPipeline::getOpBlockId(userOp) != dep.iniConsumerBlockId) { + continue; + } + if (auto matmulOp = dyn_cast(userOp)) { + auto operands = matmulOp->getOperands(); + if (operands[0] == currentValue) { + isMatmulA = true; + } else if (operands[1] == currentValue) { + isMatmulA = false; + } else { + LOG_DEBUG("[error] matmul C is from Vector!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + } + + Operation *defOp = currentValue.getDefiningOp(); + if (defOp && isa(defOp)) { + isTranspose = true; + } + return {isMatmulA, isTranspose}; + } + + for (Value result : userOp->getResults()) { + if (visited.insert(result).second) { + chain.push_back(result); + } + } + } + } + + return {isMatmulA, isTranspose}; +} + +int InterCoreTransferAndSyncPass::getNzDimWidth(DependencyInfo &dep, + int64_t blk) { + auto [isA, isTransposed] = analyzeMatmulOperand(dep); + if (blk == 32) { + if (isA == isTransposed) { + return iniNzDimWidth * 2; + } + } + return iniNzDimWidth; +} + +static void attachCommonTags(Operation *op, int blockId, StringRef coreType) { + MLIRContext *ctx = op->getContext(); + setOpBlockId(op, blockId); + setOpCoreType(op, coreType); +} + +static void attachTransferTags(Operation *op, int blockId, StringRef coreType, + int transferId) { + MLIRContext *ctx = op->getContext(); + setOpBlockId(op, blockId); + setOpCoreType(op, coreType); + op->setAttr( + CVPipeline::kTransferId, + IntegerAttr::get(IntegerType::get(ctx, kIntegerBitWidth), transferId)); +} + +static void attachMemCrossDeps(Operation *op, int tid, int seqId, + OpBuilder &builder) { + op->setAttr(CVPipeline::kMemCrossDeps, + builder.getArrayAttr({builder.getI32IntegerAttr(tid), + builder.getI32IntegerAttr(seqId)})); +} + +static void attachCrossCoreDeps(Operation *op, int tid, int seqId, + OpBuilder &builder) { + op->setAttr(CVPipeline::kCrossCoreDeps, + builder.getArrayAttr({builder.getI32IntegerAttr(tid), + builder.getI32IntegerAttr(seqId)})); +} + +static void attachAnalyzeFlagIdTag(Operation *op) { + MLIRContext *ctx = op->getContext(); + op->setAttr(CVPipeline::kAnalyzeFlagId, UnitAttr::get(ctx)); +} + +/// Get the operation just before the terminating yield of the block that +/// contains \p op. Returns nullptr if the block has no yield terminator. +static Operation *getOpBeforeYield(Operation *op) { + if (!op || !op->getBlock() || !op->getBlock()->getTerminator()) { + return nullptr; + } + Operation *terminator = op->getBlock()->getTerminator(); + if (!isa(terminator)) { + return nullptr; + } + return terminator->getPrevNode(); +} + +/// Get the sub-block id of \p op, or std::nullopt if \p op is null or does +/// not carry the sub-block tag. +static std::optional getSubBlockId(Operation *op) { + if (!op) { + return std::nullopt; + } + auto attr = op->getAttrOfType(CVPipeline::kSubBlock); + if (!attr) + return std::nullopt; + return attr.getInt(); +} + +static bool isChannelSplitNeeded(RankedTensorType tensorType) { + static constexpr int32_t alignM = 16; + return mlir::utils::getNumPerBlock(tensorType) == alignM / 2; +} + +// Check whether a value is a valid C->C transfer candidate: at least one user +// whose block_id matches \p consumerBlockId must be a matmul op consuming the +// value as an A/B input. The value may also be used as init (outs) by other +// users (or the same matmul) — the replacement logic in handleCubeToCube +// ensures only the input operands are replaced. +static bool hasAnyMatmulABInputUser(Value value, int consumerBlockId) { + for (Operation *user : value.getUsers()) { + if (CVPipeline::getOpBlockId(user).value_or(-1) != consumerBlockId) { + continue; + } + auto matmulUser = dyn_cast(user); + if (!matmulUser) { + continue; + } + for (Value input : matmulUser.getDpsInputs()) { + if (input == value) { + return true; + } + } + } + return false; +} + +/// Check if \p value is a valid C2C matmul dependency. +/// Valid chains: matmul -> (intermediate_op)* -> matmul. +static bool isValidC2CMatmulDependency(Value value, int consumerBlockId) { + // Walk back through valid intermediate ops to find the source matmul. + Operation *defOp = CVPipeline::getSourceThroughCIntermediateOps(value); + + if (!isa_and_nonnull(defOp)) + return false; + + return hasAnyMatmulABInputUser(value, consumerBlockId); +} + +static void attachLoopIdTag(Operation *op, int loopId) { + MLIRContext *ctx = op->getContext(); + op->setAttr( + CVPipeline::kLoopId, + IntegerAttr::get(IntegerType::get(ctx, kIntegerBitWidth), loopId)); +} + +bool hasStartEndSyncWhitelistKernel(ModuleOp module) { + std::vector whitelist{ + "_swa_bwd_dq_kernel", + }; + + bool isWhitelistKernel = false; + for (auto func : module.getOps()) { + if (llvm::is_contained(whitelist, func.getName().str())) { + isWhitelistKernel = true; + break; + } + } + return isWhitelistKernel; +} + +// Block Start/End Operation Retrieval +std::pair +InterCoreTransferAndSyncPass::getBlockStartEnd(int targetId, + mlir::ModuleOp module) { + mlir::Operation *knownOpInBlock = nullptr; + module.walk([&](mlir::Operation *op) { + if (knownOpInBlock) { + return; + } + if (CVPipeline::getOpBlockId(op) == targetId) { + knownOpInBlock = op; + } + }); + + if (!knownOpInBlock) { + return {nullptr, nullptr}; + } + + mlir::Block *block = knownOpInBlock->getBlock(); + if (!block) + return {nullptr, nullptr}; + + mlir::Operation *start = nullptr; + mlir::Operation *end = nullptr; + + // Iterate through all operations in the current block + for (Operation &op : *block) { + auto blockIdOpt = CVPipeline::getOpBlockId(&op); + if (!blockIdOpt) { + continue; + } + int blockId = *blockIdOpt; + if (!start) { + if (targetId == blockId) { + start = &op; + end = &op; + } + } else { + if (targetId == blockId) { + end = &op; + } else { + break; + } + } + } + return {start, end}; +} + +std::pair +InterCoreTransferAndSyncPass::getSubBlockStartEnd(mlir::Operation *defOp) { + if (!defOp) { + return {nullptr, nullptr}; + } + auto targetSubBlockId = getSubBlockId(defOp); + if (!targetSubBlockId) { + return {nullptr, nullptr}; + } + if (!defOp->getBlock()) { + return {nullptr, nullptr}; + } + mlir::Operation *subBlockStart = nullptr; + mlir::Operation *subBlockEnd = nullptr; + for (Operation &op : *defOp->getBlock()) { + auto opSubBlockId = getSubBlockId(&op); + if (!opSubBlockId) { + continue; + } + if (!subBlockStart) { + if (*opSubBlockId == *targetSubBlockId) { + subBlockStart = &op; + subBlockEnd = &op; + } + } else { + if (*opSubBlockId == *targetSubBlockId) { + subBlockEnd = &op; + } else { + break; + } + } + } + return {subBlockStart, subBlockEnd}; +} + +bool InterCoreTransferAndSyncPass::isOuterLayerDependency( + size_t depIndex, mlir::Operation *currProdEnd, + mlir::Operation *currConsStart, + llvm::SmallVector &memDependencies) { + if (!currProdEnd || !currConsStart) { + return false; + } + mlir::Block *currBlock = currProdEnd->getBlock(); + if (currBlock != currConsStart->getBlock()) { + return false; + } + for (size_t i = 0; i < memDependencies.size(); ++i) { + if (i == depIndex) { + continue; + } + auto &otherDep = memDependencies[i]; + + if (otherDep.type != memDependencies[depIndex].type) { + continue; + } + + auto [otherProdStart, otherProdEnd] = + getBlockStartEnd(otherDep.producerBlockId, module); + auto [otherConsStart, otherConsEnd] = + getBlockStartEnd(otherDep.consumerBlockId, module); + + if (!otherProdEnd || !otherConsStart) { + continue; + } + + if (otherProdEnd->getBlock() != currBlock || + otherConsStart->getBlock() != currBlock) { + continue; + } + + // otherProdEnd is before currProdEnd + // AND currConsStart is before otherConsStart + bool isOtherInsideCurrent = !otherProdEnd->isBeforeInBlock(currProdEnd) && + !currConsStart->isBeforeInBlock(otherConsStart); + + if (otherProdEnd == currProdEnd && otherConsStart == currConsStart) { + if (i < depIndex) { + // if otherDep has smaller index, current dep is outer layer and can be + // skipped + return true; + } + } else if (isOtherInsideCurrent) { + return true; + } + } + + return false; +} + +// Nd2NzNormalizer +SmallVector +InterCoreTransferAndSyncPass::computeExpectedShape(mlir::Value depValue, + int NzDimWidth) { + auto tensorTy = dyn_cast(depValue.getType()); + static constexpr int NdShapeLength = 2; + if (!tensorTy || tensorTy.getRank() != NdShapeLength) { + LOG_DEBUG("source shape is not 2-dim!"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return {}; + } + + int64_t M = tensorTy.getDimSize(0); + int64_t N = tensorTy.getDimSize(1); + + // Compute bit width & Nwidth + int64_t nWidth = getBlockElemsFor32BAlign(tensorTy.getElementType()); + if (nWidth == 0) { + LOG_DEBUG("Unsupported element type for 32B alignment.\n"); + return {M, N}; + } + + int mRound = NzDimWidth; + int nRound = nWidth; + + // Calculate newM / newN using the formula + int64_t blM = (M + mRound - 1) / mRound; + int64_t newM = blM * mRound; + + int64_t blN = (N + nRound - 1) / nRound; + int64_t newN = blN * nRound; + LOG_DEBUG("newM" << newM << "\n"); + LOG_DEBUG("newN" << newN << "\n"); + + return {newM, newN}; // Return 2D shape +} + +bool InterCoreTransferAndSyncPass::isExpectedShape( + Value value, SmallVector &expectedShape) { + auto tensorTy = dyn_cast(value.getType()); + if (!tensorTy) { + return true; + } + ArrayRef currShape = tensorTy.getShape(); + bool isEqualedShape = currShape.equals(expectedShape); + + LOG_DEBUG("isEqualedShape" << isEqualedShape << "\n"); + return isEqualedShape; +} + +// insert copyop before store to avoid mte3 blocking (store and V->C use the +// same PIPE) +mlir::Operation *InterCoreTransferAndSyncPass::getCopyPointBeforeStore( + Value depValue, Operation *vectorEndOp, int iniProducerBlockId) { + Operation *curr = vectorEndOp; + Operation *firstStoreOpAfterProducer = nullptr; + while (curr) { + auto blockIdOpt = CVPipeline::getOpBlockId(curr); + if (blockIdOpt != iniProducerBlockId) { + break; + } + if (curr == depValue.getDefiningOp()) { + break; + } + if (CVPipeline::isStoreLike(curr)) { + firstStoreOpAfterProducer = curr->getPrevNode(); + LOG_DEBUG("firstStoreOpAfterProducer: " << *firstStoreOpAfterProducer + << "\n"); + } + curr = curr->getPrevNode(); + } + return firstStoreOpAfterProducer; +} + +// padding v->c tensor +mlir::Value InterCoreTransferAndSyncPass::alignShapeByInsertSlice( + OpBuilder &builder, DependencyInfo &dep, Location loc, + mlir::Value origValue, SmallVector expectedShape, + int originBlockId) { + auto origTensorType = dyn_cast(origValue.getType()); + if (!origTensorType) { + return origValue; + } + + int64_t iniM = origTensorType.getDimSize(0); + int64_t iniN = origTensorType.getDimSize(1); + Type elemType = origTensorType.getElementType(); + if (isa(origValue)) { + auto [originProdStart, originProdEnd] = + getBlockStartEnd(originBlockId, module); + builder.setInsertionPointAfter(originProdEnd); + } else { + builder.setInsertionPointAfter(origValue.getDefiningOp()); + } + + TypedAttr zeroAttr; + if (auto floatElemTy = dyn_cast(elemType)) { + zeroAttr = FloatAttr::get( + floatElemTy, APFloat::getZero(floatElemTy.getFloatSemantics())); + } else if (auto intElemTy = dyn_cast(elemType)) { + zeroAttr = IntegerAttr::get(intElemTy, 0); + } else { + LOG_DEBUG("Unsupported element type for alignShapeByInsertSlice: " + << elemType << "\n"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return origValue; + } + auto zeroConstOp = builder.create(loc, zeroAttr); + auto tensorEmptyOp = + builder.create(loc, expectedShape, elemType); + auto linalgFillOp = builder.create( + loc, zeroConstOp.getResult(), tensorEmptyOp.getResult()); + SmallVector offsets = {builder.getIndexAttr(0), + builder.getIndexAttr(0)}; + SmallVector insertsizes = {builder.getIndexAttr(iniM), + builder.getIndexAttr(iniN)}; + SmallVector strides = {builder.getIndexAttr(1), + builder.getIndexAttr(1)}; + auto tensorInsertSliceOp = builder.create( + loc, origValue, linalgFillOp->getResult(0), offsets, insertsizes, + strides); + + attachCommonTags(zeroConstOp, originBlockId, CVPipeline::kCoreTypeVector); + attachCommonTags(tensorEmptyOp, originBlockId, CVPipeline::kCoreTypeVector); + attachCommonTags(linalgFillOp, originBlockId, CVPipeline::kCoreTypeVector); + attachCommonTags(tensorInsertSliceOp, originBlockId, + CVPipeline::kCoreTypeVector); + + return tensorInsertSliceOp.getResult(); +} + +void InterCoreTransferAndSyncPass::Nd2NzNormalize(OpBuilder &builder, + DependencyInfo &dep, + Location loc) { + Value origValue = dep.value; + Value newValue = origValue; + // Step 0: Check if this Value has already been processed + auto it = ndnzValueMapping.find(origValue); + if (it != ndnzValueMapping.end()) { + return; + } + + Type elemType = cast(origValue.getType()).getElementType(); + int64_t blk = getBlockElemsFor32BAlign(elemType); + if (blk == 0) { + LOG_DEBUG("Invalid block size.\n"); + return; + } + + auto NzDimWidth = getNzDimWidth(dep, blk); + + // Step 1: Compute expected shape + SmallVector expectedShape = + computeExpectedShape(origValue, NzDimWidth); + int originBlockId = dep.iniProducerBlockId; + // Step 2: If shapes match, return original value + bool isEqualedShape = isExpectedShape(origValue, expectedShape); + LOG_DEBUG("newValue" << newValue << "\n"); + if (!isEqualedShape) { + newValue = alignShapeByInsertSlice(builder, dep, loc, origValue, + expectedShape, originBlockId); + } + + // Step 3: insert nd2nz + auto srcTensorType = cast(newValue.getType()); + int64_t M = srcTensorType.getDimSize(0); + int64_t N = srcTensorType.getDimSize(1); + + SmallVector shape3D = {M, N / blk, blk}; + SmallVector shapeTrans = {N / blk, M, blk}; + SmallVector shapeFinal = {N / blk, M / NzDimWidth, NzDimWidth, blk}; + + auto type3D = RankedTensorType::get(shape3D, elemType); + auto typeTrans = RankedTensorType::get(shapeTrans, elemType); + auto typeFinal = RankedTensorType::get(shapeFinal, elemType); + + Operation *origDefOp = origValue.getDefiningOp(); + std::optional subBlockId = getSubBlockId(origDefOp); + + auto [newProdStart, newProdEnd] = + getBlockStartEnd(dep.producerBlockId, module); + if (dep.iniProducerBlockId == dep.producerBlockId) { + auto producerPoint = + getCopyPointBeforeStore(newValue, newProdEnd, dep.iniProducerBlockId); + if (producerPoint) { + newProdEnd = producerPoint; + } + if (subBlockId) { + if (Operation *subBlockEnd = getSubBlockStartEnd(origDefOp).second) + newProdEnd = subBlockEnd; + } + } + builder.setInsertionPointAfter(newProdEnd); + + auto reshape3Dcst = + builder.create(loc, builder.getI64TensorAttr(shape3D)); + auto reshape3DOp = + builder.create(loc, type3D, newValue, reshape3Dcst); + + auto emptyTrans = builder.create(loc, shapeTrans, elemType); + SmallVector transposeOrder = {1, 0, 2}; + auto transposeOp = builder.create( + loc, reshape3DOp.getResult(), emptyTrans.getResult(), transposeOrder); + auto reshape4Dcst = builder.create( + loc, builder.getI64TensorAttr(shapeFinal)); + auto reshape4DOp = builder.create( + loc, typeFinal, transposeOp->getResult(0), reshape4Dcst); + + attachCommonTags(reshape3Dcst, originBlockId, CVPipeline::kCoreTypeVector); + attachCommonTags(reshape3DOp, originBlockId, CVPipeline::kCoreTypeVector); + attachCommonTags(emptyTrans, originBlockId, CVPipeline::kCoreTypeVector); + attachCommonTags(transposeOp, originBlockId, CVPipeline::kCoreTypeVector); + attachCommonTags(reshape4Dcst, originBlockId, CVPipeline::kCoreTypeVector); + attachCommonTags(reshape4DOp, originBlockId, CVPipeline::kCoreTypeVector); + if (subBlockId) { + CVPipeline::setSubBlockId(reshape3Dcst, *subBlockId); + CVPipeline::setSubBlockId(reshape3DOp, *subBlockId); + CVPipeline::setSubBlockId(emptyTrans, *subBlockId); + CVPipeline::setSubBlockId(transposeOp, *subBlockId); + CVPipeline::setSubBlockId(reshape4Dcst, *subBlockId); + CVPipeline::setSubBlockId(reshape4DOp, *subBlockId); + } + LOG_DEBUG("[reshape3DOp]: " << *reshape3DOp << "\n"); + LOG_DEBUG("[transposeOp]: " << *transposeOp << "\n"); + LOG_DEBUG("[reshape4DOp]: " << *reshape4DOp << "\n"); + ndnzValueMapping[origValue] = reshape4DOp.getResult(); +} + +// mark memref.alloc +mlir::Operation *InterCoreTransferAndSyncPass::annotateTightlyCoupledBuffer( + OpBuilder &builder, Operation *allocOp, Location loc) { + builder.setInsertionPointAfter(allocOp); + auto markAllocOp = + builder.create(loc, allocOp->getResult(0)); + auto writeAttr = builder.getStringAttr("write"); + auto readAttr = builder.getStringAttr("read"); + auto effectsAttr = builder.getArrayAttr({writeAttr, readAttr}); + markAllocOp->setAttr("effects", effectsAttr); + markAllocOp->setAttr( + hivm::HIVMTightlyCoupledBufferAttr::name, + HIVMTightlyCoupledBufferAttr::get(builder.getContext(), markAllocIndex)); + return markAllocOp; +} + +// find the insert point for memref.alloc +Operation * +InterCoreTransferAndSyncPass::findMainLoopforTransfer(Operation *endOp, + Operation *startOp) { + Operation *lca = endOp->getParentOp(); + Operation *current = lca; + while (current) { + if (isa(current)) { + return current; + } + current = current->getParentOp(); + } + return nullptr; +} + +std::pair +InterCoreTransferAndSyncPass::createTransferAllocs( + OpBuilder &builder, Location loc, ArrayRef shape, Type elemType, + hivm::AddressSpace addrSpace, Operation *prodEndOp, Operation *consStartOp, + int prodBlockId, int consBlockId, StringRef prodTag, StringRef consTag, + int transferIndex) { + auto addressSpaceAttr = builder.getAttr(addrSpace); + auto allocType = MemRefType::get(shape, elemType, nullptr, addressSpaceAttr); + + Operation *prodAllocOp = nullptr; + Operation *consAllocOp = nullptr; + + Operation *mainLoopOp = findMainLoopforTransfer(prodEndOp, consStartOp); + + if (mainLoopOp) { + builder.setInsertionPoint(mainLoopOp); + prodAllocOp = builder.create(loc, allocType); + auto markProdOp = annotateTightlyCoupledBuffer(builder, prodAllocOp, loc); + consAllocOp = builder.create(loc, allocType); + auto markConsOp = annotateTightlyCoupledBuffer(builder, consAllocOp, loc); + + int loopBlockId = CVPipeline::getOpBlockId(mainLoopOp).value_or(-1); + attachTransferTags(prodAllocOp, loopBlockId, prodTag, transferIndex); + attachTransferTags(consAllocOp, loopBlockId, consTag, transferIndex); + attachTransferTags(markProdOp, loopBlockId, prodTag, transferIndex); + attachTransferTags(markConsOp, loopBlockId, consTag, transferIndex); + + builder.setInsertionPointAfter(prodEndOp); + } else { + builder.setInsertionPointAfter(prodEndOp); + consAllocOp = builder.create(loc, allocType); + auto markConsOp = annotateTightlyCoupledBuffer(builder, consAllocOp, loc); + + prodAllocOp = builder.create(loc, allocType); + auto markProdOp = annotateTightlyCoupledBuffer(builder, prodAllocOp, loc); + + attachTransferTags(prodAllocOp, prodBlockId, prodTag, transferIndex); + attachTransferTags(consAllocOp, prodBlockId, consTag, transferIndex); + attachTransferTags(markProdOp, prodBlockId, prodTag, transferIndex); + attachTransferTags(markConsOp, prodBlockId, consTag, transferIndex); + } + markAllocIndex++; + + return {prodAllocOp, consAllocOp}; +} + +mlir::Operation *InterCoreTransferAndSyncPass::analyzeConsumerReadInsertPoint( + Value srcValue, int iniConsumerId) { + llvm::DenseSet consumerOps; + for (Operation *user : srcValue.getUsers()) { + auto userBlockIdOpt = CVPipeline::getOpBlockId(user); + if (userBlockIdOpt && *userBlockIdOpt == iniConsumerId) { + consumerOps.insert(user); + } + } + + mlir::Operation *firstFoundOp = nullptr; + + module->walk([&](mlir::Operation *op) { + if (consumerOps.contains(op)) { + firstFoundOp = op; + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + + return firstFoundOp; +} + +mlir::Operation * +InterCoreTransferAndSyncPass::getConsumerWaitPoint(int transferIndex) { + mlir::Operation *consumerWaitPoint = nullptr; + module.walk([&](mlir::Operation *op) { + if (consumerWaitPoint) { + return; + } + if (!isa(op) && + !isa(op) && !isa(op)) { + return; + } + auto transferIdAttr = + op->getAttrOfType(CVPipeline::kTransferId); + if (transferIdAttr && transferIdAttr.getInt() == transferIndex) { + consumerWaitPoint = op; + } + }); + return consumerWaitPoint; +} + +mlir::Operation *InterCoreTransferAndSyncPass::getFixpipePointAfterProducer( + Value srcValue, int producerBlockId) { + mlir::Operation *fixpipePoint = srcValue.getDefiningOp(); + int blockId = CVPipeline::getOpBlockId(fixpipePoint).value_or(-1); + if (blockId != producerBlockId) { + return nullptr; + } + return fixpipePoint; +} + +Operation *InterCoreTransferAndSyncPass::insertVectorToCubeTransfer( + OpBuilder &builder, Value srcValue, Value normalizedValue, + Operation *vectorEndOp, Operation *cubeStartOp, Location loc, + int transferIndex, DependencyInfo &dep, bool is1DTensor, + Operation **consumedDataOp) { + mlir::Operation *sendOp = nullptr; + mlir::Operation *receiveOp = nullptr; + Value receiveValue; + + int vecBlockId = CVPipeline::getOpBlockId(vectorEndOp).value_or(-1); + int cubeBlockId = CVPipeline::getOpBlockId(cubeStartOp).value_or(-1); + + if (isScalarDependency(dep.value)) { + builder.setInsertionPointAfter(vectorEndOp); + SmallVector writeOps; + LOG_DEBUG("before writeToSSBuffer\n"); + auto addrOpt = ssbufferManager.writeToSSBuffer(srcValue, builder, writeOps); + if (!addrOpt) { + LOG_DEBUG("[v->c] Failed to write scalar value to SSBuffer\n"); + return nullptr; + } + int64_t addr = *addrOpt; + LOG_DEBUG("after writeToSSBuffer\n"); + Operation *storeOp = nullptr; + for (Operation *op : writeOps) { + attachTransferTags(op, vecBlockId, CVPipeline::kCoreTypeVector, + transferIndex); + if (isa(op)) { + storeOp = op; + } + } + sendOp = storeOp; + attachCrossCoreDeps(sendOp, transferIndex, CVPipeline::crossCoreProducerId, + builder); + LOG_DEBUG("before readFromSSBuffer\n"); + builder.setInsertionPoint(cubeStartOp); + SmallVector readOps; + auto loadedValueOpt = + ssbufferManager.readFromSSBuffer(addr, builder, readOps); + if (!loadedValueOpt) { + LOG_DEBUG("[v->c] Failed to read scalar value from SSBuffer\n"); + return nullptr; + } + receiveValue = *loadedValueOpt; + LOG_DEBUG("after readFromSSBuffer\n"); + Operation *loadOp = nullptr; + for (Operation *op : readOps) { + attachTransferTags(op, cubeBlockId, CVPipeline::kCoreTypeCube, + transferIndex); + if (isa(op)) { + loadOp = op; + } + } + receiveOp = loadOp; + attachCrossCoreDeps(receiveOp, transferIndex, + CVPipeline::crossCoreConsumerId, builder); + } else { + // Step 1: Get input information (2D tensor: MxN) + auto srcTensorType = cast(srcValue.getType()); + auto normalizedTensorType = + cast(normalizedValue.getType()); + Type elemType = srcTensorType.getElementType(); + + auto [vecAllocOp, cubeAllocOp] = createTransferAllocs( + builder, loc, normalizedTensorType.getShape(), elemType, + hivm::AddressSpace::L1, vectorEndOp, cubeStartOp, vecBlockId, + cubeBlockId, CVPipeline::kCoreTypeVector, CVPipeline::kCoreTypeCube, + transferIndex); + + auto copyOp = builder.create( + loc, mlir::TypeRange{}, normalizedValue, vecAllocOp->getResult(0)); + + attachTransferTags(copyOp, vecBlockId, CVPipeline::kCoreTypeVector, + transferIndex); + attachCrossCoreDeps(copyOp, transferIndex, CVPipeline::crossCoreProducerId, + builder); + // Propagate the sub-block tag from the src defining op to the copy op. + if (Operation *srcDefOp = srcValue.getDefiningOp()) { + if (auto subBlockId = getSubBlockId(srcDefOp)) { + CVPipeline::setSubBlockId(copyOp, *subBlockId); + } + } + LOG_DEBUG("[copyOp]: " << *copyOp << "\n"); + + builder.setInsertionPoint(cubeStartOp); + + Value memValue = cubeAllocOp->getResult(0); + if (!is1DTensor) { + auto nzLayout = + hivm::DataLayoutAttr::get(builder.getContext(), hivm::DataLayout::nZ); + auto ndLayout = + hivm::DataLayoutAttr::get(builder.getContext(), hivm::DataLayout::ND); + auto cbufaddressSpaceAttr = + builder.getAttr(hivm::AddressSpace::L1); + auto newAllocType = MemRefType::get(srcTensorType.getShape(), elemType, + nullptr, cbufaddressSpaceAttr); + auto convertLayoutOp = + builder.create(loc, newAllocType, memValue, + nzLayout, // srcLayout + ndLayout // dstLayout + ); + memValue = convertLayoutOp.getResult(); + attachTransferTags(convertLayoutOp, cubeBlockId, + CVPipeline::kCoreTypeCube, transferIndex); + attachCrossCoreDeps(convertLayoutOp, transferIndex, + CVPipeline::crossCoreConsumerId, builder); + } + auto plainMemrefType = MemRefType::get(srcTensorType.getShape(), elemType); + auto memspaceCastOp = builder.create( + loc, plainMemrefType, memValue); + auto toTensorOp = builder.create( + loc, srcTensorType, memspaceCastOp.getResult(), true, true); + + attachTransferTags(memspaceCastOp, cubeBlockId, CVPipeline::kCoreTypeCube, + transferIndex); + if (is1DTensor) { + attachCrossCoreDeps(memspaceCastOp, transferIndex, + CVPipeline::crossCoreConsumerId, builder); + } + attachTransferTags(toTensorOp, cubeBlockId, CVPipeline::kCoreTypeCube, + transferIndex); + LOG_DEBUG("[toTensorOp]: " << *toTensorOp << "\n"); + sendOp = copyOp; + receiveOp = toTensorOp; + receiveValue = toTensorOp.getResult(); + } + + llvm::SmallVector users(srcValue.getUsers().begin(), + srcValue.getUsers().end()); + if (dep.consumerYieldOp) { + dep.consumerYieldOp->replaceUsesOfWith(srcValue, receiveValue); + } + for (Operation *user : users) { + LOG_DEBUG("[v->c user]" << *user << "\n"); + auto userBlockIdOpt = CVPipeline::getOpBlockId(user); + if (userBlockIdOpt && *userBlockIdOpt == dep.iniConsumerBlockId) { + user->replaceUsesOfWith(srcValue, receiveValue); + } + } + if (consumedDataOp) { + *consumedDataOp = receiveOp; + } + return sendOp; +} + +Operation *InterCoreTransferAndSyncPass::insertCubeToVectorTransfer( + OpBuilder &builder, Value srcValue, Operation *cubeEndOp, + Operation *vectorStartOp, Location loc, int transferIndex, + DependencyInfo &dep, Operation **consumedDataOp) { + LOG_DEBUG("Inserting [Cube->Vector] transfer for value: " << srcValue + << "\n"); + auto srcTensorType = cast(srcValue.getType()); + int64_t M = srcTensorType.getDimSize(0); + int64_t N = srcTensorType.getDimSize(1); + Type elemType = srcTensorType.getElementType(); + + int cubeBlockId = + CVPipeline::getOpBlockId(srcValue.getDefiningOp()).value_or(-1); + int vecBlockId = CVPipeline::getOpBlockId(vectorStartOp).value_or(-1); + + auto targetShape = dep.isAllTranspoesd ? std::vector{N, M} + : std::vector{M, N}; + auto targetTensorType = RankedTensorType::get(targetShape, elemType); + auto [cubeAllocOp, vecAllocOp] = createTransferAllocs( + builder, loc, targetShape, elemType, hivm::AddressSpace::UB, cubeEndOp, + vectorStartOp, cubeBlockId, vecBlockId, CVPipeline::kCoreTypeCube, + CVPipeline::kCoreTypeVector, transferIndex); + auto dmaModeAttr = FixpipeDMAModeAttr::get( + builder.getContext(), + dep.isAllTranspoesd ? FixpipeDMAMode::NZ2DN : FixpipeDMAMode::NZ2ND); + + auto realValue = dep.value; + if (dep.realValue) { + realValue = dep.realValue; + } + auto fixpipeOp = builder.create( + loc, mlir::TypeRange{}, // No return value + realValue, // src + cubeAllocOp->getResult(0), // dst + mlir::ValueRange{}, dmaModeAttr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, mlir::ArrayAttr{}, nullptr); + attachTransferTags(fixpipeOp, cubeBlockId, CVPipeline::kCoreTypeCube, + transferIndex); + attachCrossCoreDeps(fixpipeOp, transferIndex, CVPipeline::crossCoreProducerId, + builder); + LOG_DEBUG("[fixpipeOp]: " << *fixpipeOp << "\n"); + + // Vector side: memspace_cast + to_tensor + builder.setInsertionPoint(vectorStartOp); + + auto plainMemrefType = MemRefType::get(targetShape, elemType); + auto memspaceCastOp = builder.create( + loc, plainMemrefType, vecAllocOp->getResult(0)); + + auto toTensorOp = builder.create( + loc, targetTensorType, memspaceCastOp.getResult(), true, true); + + attachTransferTags(memspaceCastOp, vecBlockId, CVPipeline::kCoreTypeVector, + transferIndex); + attachCrossCoreDeps(memspaceCastOp, transferIndex, + CVPipeline::crossCoreConsumerId, builder); + attachTransferTags(toTensorOp, vecBlockId, CVPipeline::kCoreTypeVector, + transferIndex); + // Propagate the sub-block tag from vectorStartOp to the vector-side ops. + if (auto subBlockId = getSubBlockId(vectorStartOp)) { + CVPipeline::setSubBlockId(memspaceCastOp, *subBlockId); + CVPipeline::setSubBlockId(toTensorOp, *subBlockId); + } + LOG_DEBUG("[toTensorOp]: " << *toTensorOp << "\n"); + + if (dep.isAllTranspoesd) { + for (auto *userOp : srcValue.getUsers()) { + if (isa(userOp)) { + srcValue = userOp->getResults()[0]; + break; + } + } + } + llvm::SmallVector users(srcValue.getUsers().begin(), + srcValue.getUsers().end()); + if (dep.consumerYieldOp) { + dep.consumerYieldOp->replaceUsesOfWith(srcValue, toTensorOp.getResult()); + } + for (Operation *user : users) { + LOG_DEBUG("[c->v user]" << *user << "\n"); + auto userBlockIdOpt = CVPipeline::getOpBlockId(user); + if (userBlockIdOpt && *userBlockIdOpt == dep.iniConsumerBlockId) { + user->replaceUsesOfWith(srcValue, toTensorOp.getResult()); + } + } + + if (consumedDataOp) { + *consumedDataOp = toTensorOp; + } + return fixpipeOp; +} + +TransferPipeConfig +InterCoreTransferAndSyncPass::getTransferPipeConfig(Operation *transferOp, + bool isStoreDirectly) { + auto cubeCoreAttr = + hivm::TCoreTypeAttr::get(module.getContext(), hivm::TCoreType::CUBE); + auto vecCoreAttr = + hivm::TCoreTypeAttr::get(module.getContext(), hivm::TCoreType::VECTOR); + auto pipeFixAttr = PipeAttr::get(module.getContext(), hivm::PIPE::PIPE_FIX); + auto pipeVAttr = PipeAttr::get(module.getContext(), hivm::PIPE::PIPE_V); + auto pipeMte3Attr = PipeAttr::get(module.getContext(), hivm::PIPE::PIPE_MTE3); + auto pipeMte1Attr = PipeAttr::get(module.getContext(), hivm::PIPE::PIPE_MTE1); + auto pipeSAttr = PipeAttr::get(module.getContext(), hivm::PIPE::PIPE_S); + TransferPipeConfig config; + if (isa(transferOp)) { + if (isStoreDirectly) { + config.forReadTPipe = pipeFixAttr; + config.forReadPipe = pipeMte3Attr; + config.forWriteTPipe = pipeMte3Attr; + config.forWritePipe = pipeFixAttr; + config.srcCoreAttr = cubeCoreAttr; + config.dstCoreAttr = vecCoreAttr; + config.srcCoreType = CVPipeline::kCoreTypeCube; + config.dstCoreType = CVPipeline::kCoreTypeVector; + } else { + config.forReadTPipe = pipeFixAttr; + config.forReadPipe = pipeVAttr; + config.forWriteTPipe = pipeVAttr; + config.forWritePipe = pipeFixAttr; + config.srcCoreAttr = cubeCoreAttr; + config.dstCoreAttr = vecCoreAttr; + config.srcCoreType = CVPipeline::kCoreTypeCube; + config.dstCoreType = CVPipeline::kCoreTypeVector; + } + } else if (isa(transferOp)) { + config.forReadTPipe = pipeMte3Attr; + config.forReadPipe = pipeMte1Attr; + config.forWriteTPipe = pipeMte1Attr; + config.forWritePipe = pipeMte3Attr; + config.srcCoreAttr = vecCoreAttr; + config.dstCoreAttr = cubeCoreAttr; + config.srcCoreType = CVPipeline::kCoreTypeVector; + config.dstCoreType = CVPipeline::kCoreTypeCube; + } else if (isa(transferOp)) { + // Scalar sync uses PIPE_S to stay isolated from tensor flag space. + config.forReadTPipe = pipeSAttr; + config.forReadPipe = pipeSAttr; + config.forWriteTPipe = pipeSAttr; + config.forWritePipe = pipeSAttr; + config.srcCoreAttr = vecCoreAttr; + config.dstCoreAttr = cubeCoreAttr; + config.srcCoreType = CVPipeline::kCoreTypeVector; + config.dstCoreType = CVPipeline::kCoreTypeCube; + } + return config; +} + +// Check if a value fixpiped to ub is directly connected to a storage op +// Skip ViewLikeOpInterface and ExtractSliceOp during traversal +bool InterCoreTransferAndSyncPass::isStoreDirectlyInUserChain( + Value toTensorValue) { + // Traverse user chain starting from toTensorValue + llvm::SmallVector workList = {toTensorValue}; + llvm::DenseSet visited; + bool hasStore = false; + while (!workList.empty()) { + Value currVal = workList.pop_back_val(); + if (visited.count(currVal)) { + continue; + } + visited.insert(currVal); + + for (Operation *user : currVal.getUsers()) { + // Check if user is a storage op + if (CVPipeline::isStoreLike(user)) { + hasStore = true; + continue; + } + + // Check if user is in skip range + if (CVPipeline::isViewLike(user) || CVPipeline::isZeroAdd(user) || + user->hasAttr(CVPipeline::kForMayNotExec)) { + // Continue traversing through skip ops + for (Value result : user->getResults()) { + if (!visited.count(result)) { + workList.push_back(result); + } + } + } else { + return false; + } + } + } + return hasStore; +} + +void InterCoreTransferAndSyncPass::insertInterCoreSync( + OpBuilder &builder, Operation *transferOp, Operation *consumerStartOp, + Operation *consumerEndOp, int flag, Location loc, int transferIndex, + FlagIdReuseManager &flagIdReuseManager, Operation *consumedDataOp, + bool isStoreDirectly) { + LOG_DEBUG("Inserting inter-core synchronization for transferOp: " + << *transferOp << "\n"); + + auto flagId = builder.getIntegerAttr(builder.getI64Type(), flag); + + int producerBlockId = CVPipeline::getOpBlockId(transferOp).value_or(-1); + int consumerBlockId = CVPipeline::getOpBlockId(consumerStartOp).value_or(-1); + + Operation *mainLoopOp = findMainLoopforTransfer(transferOp, consumerStartOp); + + auto config = getTransferPipeConfig(transferOp, isStoreDirectly); + + builder.setInsertionPointAfter(transferOp); + auto setOpForRead = builder.create( + loc, config.srcCoreAttr, config.forReadTPipe, config.forReadPipe, flagId); + attachTransferTags(setOpForRead, producerBlockId, config.srcCoreType, + transferIndex); + // Propagate the sub-block tag from transferOp to the sync ops. + if (auto subBlockId = getSubBlockId(transferOp)) { + CVPipeline::setSubBlockId(setOpForRead, *subBlockId); + } + + builder.setInsertionPoint(consumerStartOp); + auto waitOpForRead = builder.create( + loc, config.dstCoreAttr, config.forReadTPipe, config.forReadPipe, flagId); + attachTransferTags(waitOpForRead, consumerBlockId, config.dstCoreType, + transferIndex); + if (auto subBlockId = getSubBlockId(consumerStartOp)) { + CVPipeline::setSubBlockId(waitOpForRead, *subBlockId); + } + + if (mainLoopOp) { + builder.setInsertionPoint(transferOp); + auto waitOpForWrite = builder.create( + loc, config.srcCoreAttr, config.forWriteTPipe, config.forWritePipe, + flagId); + attachTransferTags(waitOpForWrite, producerBlockId, config.srcCoreType, + transferIndex); + + builder.setInsertionPointAfter(consumerEndOp); + auto setOpForWrite = builder.create( + loc, config.dstCoreAttr, config.forWriteTPipe, config.forWritePipe, + flagId); + attachTransferTags(setOpForWrite, consumerBlockId, config.dstCoreType, + transferIndex); + + builder.setInsertionPoint(mainLoopOp); + auto setOpForStart = builder.create( + loc, config.dstCoreAttr, config.forWriteTPipe, config.forWritePipe, + flagId); + builder.setInsertionPointAfter(mainLoopOp); + auto waitOpForEnd = builder.create( + loc, config.srcCoreAttr, config.forWriteTPipe, config.forWritePipe, + flagId); + + int startEndBlockId = CVPipeline::getOpBlockId(mainLoopOp).value_or(-1); + attachTransferTags(setOpForStart, startEndBlockId, config.dstCoreType, + transferIndex); + attachTransferTags(waitOpForEnd, startEndBlockId, config.srcCoreType, + transferIndex); + + if (auto subBlockId = getSubBlockId(transferOp)) { + CVPipeline::setSubBlockId(waitOpForWrite, *subBlockId); + } + if (auto subBlockId = getSubBlockId(consumerEndOp)) { + CVPipeline::setSubBlockId(setOpForWrite, *subBlockId); + } + + attachAnalyzeFlagIdTag(setOpForRead); + attachAnalyzeFlagIdTag(waitOpForRead); + attachAnalyzeFlagIdTag(waitOpForWrite); + attachAnalyzeFlagIdTag(setOpForWrite); + attachAnalyzeFlagIdTag(setOpForStart); + attachAnalyzeFlagIdTag(waitOpForEnd); + + if (hasStartEndSyncWhitelistKernel(module)) { + auto loopId = + mainLoopOp->getAttrOfType(CVPipeline::kLoopId).getInt(); + attachLoopIdTag(setOpForStart, loopId); + attachLoopIdTag(waitOpForEnd, loopId); + } + + // E2: register every set->wait pair of this transfer, not just the + // loop start/end pair. Each pair is the only proof of cross-core + // ordering for the sync ops it connects. + flagIdReuseManager.insertRelationBetweenSetAndWait(setOpForRead, + waitOpForRead); + flagIdReuseManager.insertRelationBetweenSetAndWait(setOpForWrite, + waitOpForWrite); + flagIdReuseManager.insertRelationBetweenSetAndWait(setOpForStart, + waitOpForEnd); + // E4: link the read-wait to the consumed data it guards so the sync + // op is threaded into the downstream dataflow graph. + flagIdReuseManager.insertRelationBetweenSetAndWait(waitOpForRead, + consumedDataOp); + return; + } + attachAnalyzeFlagIdTag(setOpForRead); + attachAnalyzeFlagIdTag(waitOpForRead); + flagIdReuseManager.insertRelationBetweenSetAndWait(setOpForRead, + waitOpForRead); + flagIdReuseManager.insertRelationBetweenSetAndWait(waitOpForRead, + consumedDataOp); + return; +} + +bool hasMemDepSyncWhitelistKernel(ModuleOp module) { + std::vector whitelist{ + "_hstu_attn_fwd", + "parallel_path_fwd_kernel", + }; + + // Check if any func name matches the whitelist + bool hasWhitelistedKernel = false; + for (auto func : module.getOps()) { + if (llvm::is_contained(whitelist, func.getName().str())) { + hasWhitelistedKernel = true; + break; + } + } + return hasWhitelistedKernel; +} + +void InterCoreTransferAndSyncPass::insertMemDepSync( + OpBuilder &builder, Operation *producerStartOp, Operation *producerEndOp, + Operation *consumerStartOp, Operation *consumerEndOp, int flag, + Location loc, bool isCubeToVector, FlagIdReuseManager &flagIdReuseManager) { + LOG_DEBUG("Inserting Memdep sync: " + << (isCubeToVector ? "CUBE->VECTOR" : "VECTOR->CUBE") + << ", flag = " << flag << "\n"); + + // CUBE -> VECTOR: srcPipe = PIPE_FIX, srcCoreType = CUBE, dstCoreType = + // VECTOR VECTOR -> CUBE: srcPipe = PIPE_MTE3, srcCoreType = VECTOR, + // dstCoreType = CUBE + hivm::PIPE srcPipe = + isCubeToVector ? hivm::PIPE::PIPE_FIX : hivm::PIPE::PIPE_MTE3; + hivm::TCoreType srcCoreType = + isCubeToVector ? hivm::TCoreType::CUBE : hivm::TCoreType::VECTOR; + hivm::TCoreType dstCoreType = + isCubeToVector ? hivm::TCoreType::VECTOR : hivm::TCoreType::CUBE; + hivm::PIPE dstPipe = hivm::PIPE::PIPE_MTE2; + + auto srcCoreAttr = + hivm::TCoreTypeAttr::get(builder.getContext(), srcCoreType); + auto dstCoreAttr = + hivm::TCoreTypeAttr::get(builder.getContext(), dstCoreType); + auto srcPipeAttr = PipeAttr::get(builder.getContext(), srcPipe); + auto dstPipeAttr = PipeAttr::get(builder.getContext(), dstPipe); + auto flagId = builder.getIntegerAttr(builder.getI64Type(), flag); + + builder.setInsertionPointAfter(producerEndOp); + auto setOp = builder.create(loc, srcCoreAttr, srcPipeAttr, + dstPipeAttr, flagId); + + builder.setInsertionPoint(consumerStartOp); + auto waitOp = builder.create(loc, dstCoreAttr, srcPipeAttr, + dstPipeAttr, flagId); + + auto prodBlockIdOpt = CVPipeline::getOpBlockId(producerEndOp); + auto consBlockIdOpt = CVPipeline::getOpBlockId(consumerStartOp); + StringRef prodCoreType = + isCubeToVector ? CVPipeline::kCoreTypeCube : CVPipeline::kCoreTypeVector; + StringRef consCoreType = + isCubeToVector ? CVPipeline::kCoreTypeVector : CVPipeline::kCoreTypeCube; + if (prodBlockIdOpt) { + attachCommonTags(setOp, *prodBlockIdOpt, prodCoreType); + } + if (consBlockIdOpt) { + attachCommonTags(waitOp, *consBlockIdOpt, consCoreType); + } + attachAnalyzeFlagIdTag(setOp); + attachAnalyzeFlagIdTag(waitOp); + flagIdReuseManager.insertRelationBetweenSetAndWait(setOp, waitOp); + if (hasMemDepSyncWhitelistKernel(module)) { + Operation *mainLoopOp = + findMainLoopforTransfer(producerEndOp, consumerStartOp); + if (mainLoopOp) { + builder.setInsertionPoint(producerStartOp); + auto waitOpForWrite = builder.create( + loc, srcCoreAttr, dstPipeAttr, srcPipeAttr, flagId); + attachCommonTags(waitOpForWrite, *prodBlockIdOpt, prodCoreType); + + builder.setInsertionPointAfter(consumerEndOp); + auto setOpForWrite = builder.create( + loc, dstCoreAttr, dstPipeAttr, srcPipeAttr, flagId); + attachCommonTags(setOpForWrite, *consBlockIdOpt, consCoreType); + + builder.setInsertionPoint(mainLoopOp); + auto setOpForStart = builder.create( + loc, dstCoreAttr, dstPipeAttr, srcPipeAttr, flagId); + builder.setInsertionPointAfter(mainLoopOp); + auto waitOpForEnd = builder.create( + loc, srcCoreAttr, dstPipeAttr, srcPipeAttr, flagId); + + int startEndBlockId = CVPipeline::getOpBlockId(mainLoopOp).value_or(-1); + attachCommonTags(setOpForStart, startEndBlockId, consCoreType); + attachCommonTags(waitOpForEnd, startEndBlockId, prodCoreType); + + attachAnalyzeFlagIdTag(waitOpForWrite); + attachAnalyzeFlagIdTag(setOpForWrite); + attachAnalyzeFlagIdTag(setOpForStart); + attachAnalyzeFlagIdTag(waitOpForEnd); + flagIdReuseManager.insertRelationBetweenSetAndWait(setOpForWrite, + waitOpForWrite); + flagIdReuseManager.insertRelationBetweenSetAndWait(setOpForStart, + waitOpForEnd); + } + } + LOG_DEBUG("[PIPE_MTE2 setOp]: " << *setOp << "\n"); + LOG_DEBUG("[PIPE_MTE2 waitOp]: " << *waitOp << "\n"); +} + +namespace { + +// Walk `scfOp`'s regions looking for an `scf::YieldOp` that directly yields a +// VECTOR-side `bufferization.to_tensor` backed by a memref.alloc annotated with +// `hivm.tightly_coupled_buffer`. +std::optional findVectorToTensorInScfOp(Operation *scfOp) { + std::optional result; + scfOp->walk([&](scf::YieldOp yield) { + if (result) { + return; + } + for (Value yielded : yield.getResults()) { + auto toTensor = yielded.getDefiningOp(); + if (!toTensor) { + continue; + } + auto coreAttr = + toTensor->getAttrOfType(CVPipeline::kCoreType); + if (!coreAttr || coreAttr.getValue() != CVPipeline::kCoreTypeVector) { + continue; + } + Value memref = CVPipeline::traceBackToMemrefAlloc(toTensor.getBuffer()); + auto alloc = memref.getDefiningOp(); + if (!alloc) { + continue; + } + auto id = CVPipeline::getTightlyCoupledBufferId(alloc); + if (!id.has_value()) { + continue; + } + result = VectorToTensorInfo{toTensor, yield, *id}; + return; + } + }); + return result; +} + +// Walk downstream from `scfResult` through view-like ops looking for a +// store-like op. Returns the store op or null. +Operation *findStoreLikeAfterScfOp(Value scfResult) { + SmallVector worklist; + for (Operation *user : scfResult.getUsers()) { + if (CVPipeline::isStoreLike(user)) { + return user; + } + if (CVPipeline::isViewLike(user)) { + worklist.push_back(user); + } + } + while (!worklist.empty()) { + Operation *current = worklist.pop_back_val(); + for (Operation *user : current->getUsers()) { + if (CVPipeline::isStoreLike(user)) { + return user; + } + if (CVPipeline::isViewLike(user)) { + worklist.push_back(user); + } + } + } + return nullptr; +} + +} // namespace + +std::optional +InterCoreTransferAndSyncPass::matchCubeToVectorDirectStorePattern( + Operation *scfOp) { + if (!isa(scfOp)) { + return std::nullopt; + } + + auto anchor = findVectorToTensorInScfOp(scfOp); + if (!anchor) { + return std::nullopt; + } + + Value scfResult; + for (auto [idx, yielded] : llvm::enumerate(anchor->yield.getResults())) { + if (yielded == anchor->toTensor.getResult()) { + if (idx < scfOp->getNumResults()) { + scfResult = scfOp->getResult(idx); + } + break; + } + } + if (!scfResult) { + return std::nullopt; + } + + Operation *storeOp = findStoreLikeAfterScfOp(scfResult); + if (!storeOp) { + return std::nullopt; + } + + CubeToVectorDirectStoreInfo info; + info.tightlyCoupledBufferId = anchor->tightlyCoupledBufferId; + info.storeOp = storeOp; + return info; +} + +// Match CUBE -> VECTOR direct store: data reaches VECTOR through UB and is +// stored without VECTOR tensor computation. The extra sync guards the MTE3 +// store after the SCF region. +// Remove VECTOR add-from-matmul pseudo-ops from SCF yields. +void InterCoreTransferAndSyncPass::removeVectorPseudoOps() { + LOG_DEBUG("Removing VECTOR pseudo-ops (addf/addi carrying " + "ssbuffer.add_from_matmul)...\n"); + + module.walk([&](Operation *op) { + if (!isa(op)) { + return; + } + // (1) Must carry the marker attributes. + if (!op->hasAttr(CVPipeline::kAddFromMatmul)) { + return; + } + auto coreAttr = op->getAttrOfType(CVPipeline::kCoreType); + if (!coreAttr || coreAttr.getValue() != CVPipeline::kCoreTypeVector) { + return; + } + if (op->getNumOperands() != 2) { + return; + } + // (2) One operand must be a zero-filled tensor; the other is the + // data-flow source we want to keep yielding. + Value lhs = op->getOperand(0); + Value rhs = op->getOperand(1); + Value keptOperand = nullptr; + if (CVPipeline::isZeroFillValue(lhs)) { + keptOperand = rhs; + } else if (CVPipeline::isZeroFillValue(rhs)) { + keptOperand = lhs; + } else { + return; // No zero-fill operand: not the pseudo-op. + } + LOG_DEBUG("[pseudo-op] removing " << *op << "\n"); + op->getResult(0).replaceAllUsesWith(keptOperand); + op->erase(); + }); +} + +void InterCoreTransferAndSyncPass::processCubeToVectorDirectStoreSync( + OpBuilder &builder, FlagIdManager &flagManager, + FlagIdReuseManager &flagIdReuseManager) { + LOG_DEBUG("Processing cube-to-vector-direct-store sync pattern...\n"); + + module.walk([&](Operation *op) { + if (!isa(op)) { + return; + } + auto match = matchCubeToVectorDirectStorePattern(op); + if (!match.has_value()) { + return; + } + auto &info = *match; + Operation *storeOp = info.storeOp; + + int flagId = flagManager.acquireId(); + Location loc = op->getLoc(); + auto flagIdAttr = builder.getIntegerAttr(builder.getI64Type(), flagId); + auto cubeAttr = TCoreTypeAttr::get(builder.getContext(), TCoreType::CUBE); + auto vecAttr = TCoreTypeAttr::get(builder.getContext(), TCoreType::VECTOR); + auto pipeFixAttr = PipeAttr::get(builder.getContext(), PIPE::PIPE_FIX); + auto pipeMte3Attr = PipeAttr::get(builder.getContext(), PIPE::PIPE_MTE3); + + // Both set/wait share the same transfer id for this post-region store + // path; advance the class-level counter so subsequent transfers keep + // getting unique ids. + int syncTransferId = transferIndex++; + + // Insert the set after the SCF op and any trailing sync_block_* ops + // already appended by previous handlers in this pass. + Operation *setInsertPoint = op; + while (auto *next = setInsertPoint->getNextNode()) { + if (isa(next) || isa(next)) { + setInsertPoint = next; + } else { + break; + } + } + builder.setInsertionPointAfter(setInsertPoint); + auto setOp = builder.create(loc, cubeAttr, pipeFixAttr, + pipeMte3Attr, flagIdAttr); + // set: block_id matches the SCF op, core_type = CUBE. + if (auto opBlockIdOpt = CVPipeline::getOpBlockId(op)) { + attachTransferTags(setOp, *opBlockIdOpt, CVPipeline::kCoreTypeCube, + syncTransferId); + } + attachAnalyzeFlagIdTag(setOp); + + // Insert the wait right before the materialize_in_destination. + builder.setInsertionPoint(storeOp); + auto waitOp = builder.create(loc, vecAttr, pipeFixAttr, + pipeMte3Attr, flagIdAttr); + // wait: block_id matches the store, core_type = VECTOR. + if (auto storeBlockIdOpt = CVPipeline::getOpBlockId(storeOp)) { + attachTransferTags(waitOp, *storeBlockIdOpt, CVPipeline::kCoreTypeVector, + syncTransferId); + } + attachAnalyzeFlagIdTag(waitOp); + + // Register the set/wait pair so the analyze/flag-reuse pass can reuse + // the same flag id if ordering permits. + flagIdReuseManager.insertRelationBetweenSetAndWait(setOp, waitOp); + LOG_DEBUG("[cube-to-vector-direct-store] inserted set/wait, flag = " + << flagId << "\n"); + }); +} + +static bool isConcretePipe(hivm::PIPE pipe) { + return pipe != hivm::PIPE::PIPE_UNASSIGNED && pipe != hivm::PIPE::PIPE_ALL && + pipe != hivm::PIPE::PIPE_NUM; +} + +static std::optional getAnalyzeCoreType(Operation *op) { + if (auto coreAttr = + op->getAttrOfType(hivm::TCoreTypeAttr::name)) { + auto coreType = coreAttr.getTcoretype(); + if (coreType == hivm::TCoreType::CUBE || + coreType == hivm::TCoreType::VECTOR) { + return coreType; + } + } + + auto coreStringAttr = op->getAttrOfType(CVPipeline::kCoreType); + if (!coreStringAttr) { + return std::nullopt; + } + StringRef coreType = coreStringAttr.getValue(); + if (coreType == CVPipeline::kCoreTypeCube) { + return hivm::TCoreType::CUBE; + } + if (coreType == CVPipeline::kCoreTypeVector) { + return hivm::TCoreType::VECTOR; + } + return std::nullopt; +} + +static std::optional getMemRefAddressSpace(Type type) { + auto memRefType = dyn_cast(type); + if (!memRefType) { + return std::nullopt; + } + Attribute memorySpace = memRefType.getMemorySpace(); + if (!memorySpace) { + return std::nullopt; + } + auto addressSpaceAttr = dyn_cast(memorySpace); + if (!addressSpaceAttr) { + return std::nullopt; + } + return addressSpaceAttr.getAddressSpace(); +} + +static std::optional getCopyPipeForAnalyze(hivm::CopyOp copyOp) { + if (copyOp.hasPureBufferSemantics()) { + return copyOp.getPipe(); + } + + auto srcAddressSpace = getMemRefAddressSpace(copyOp.getSrcOperandType()); + auto dstAddressSpace = getMemRefAddressSpace(copyOp.getDstOperandType()); + if (srcAddressSpace && dstAddressSpace) { + if (*srcAddressSpace == hivm::AddressSpace::UB && + *dstAddressSpace == hivm::AddressSpace::UB) { + return hivm::PIPE::PIPE_V; + } + if (*srcAddressSpace == hivm::AddressSpace::L0C && + *dstAddressSpace == hivm::AddressSpace::GM) { + return hivm::PIPE::PIPE_FIX; + } + if (*srcAddressSpace == hivm::AddressSpace::GM && + *dstAddressSpace == hivm::AddressSpace::L1) { + return hivm::PIPE::PIPE_MTE2; + } + if (*srcAddressSpace == hivm::AddressSpace::UB && + *dstAddressSpace == hivm::AddressSpace::L1) { + return hivm::PIPE::PIPE_MTE3; + } + } + + // SplitDataflow inserts tensor-to-L1 copies before full bufferization. The + // sync pair for this transfer uses MTE3 on the vector side. + if (dstAddressSpace && *dstAddressSpace == hivm::AddressSpace::L1) { + return hivm::PIPE::PIPE_MTE3; + } + return std::nullopt; +} + +/// Returns the MatmulOp that is yielded by the split-if op. +/// Returns nullptr if the block has no yield terminator. +linalg::MatmulOp +InterCoreTransferAndSyncPass::findYieldMatmulInSplitIf(Value splittedIfResult) { + auto ifOp = splittedIfResult.getDefiningOp(); + if (!ifOp) { + return nullptr; + } + + auto result = dyn_cast(splittedIfResult); + if (!result) { + return nullptr; + } + unsigned resultIndex = result.getResultNumber(); + + auto yieldOp = ifOp.thenYield(); + if (!yieldOp || resultIndex >= yieldOp.getNumOperands()) + return nullptr; + auto definingOp = yieldOp.getOperand(resultIndex).getDefiningOp(); + + return dyn_cast(definingOp); +} + +void InterCoreTransferAndSyncPass::AnalyzeSplittedIf(DependencyInfo &dep) { + auto srcOp = dep.value.getDefiningOp(); + if (!srcOp || !srcOp->hasAttr(CVPipeline::kSplittedIf)) { + return; + } + Operation *consIfOp = nullptr; + for (Operation *nextOp = srcOp->getNextNode(); nextOp; + nextOp = nextOp->getNextNode()) { + auto op = dyn_cast(nextOp); + if (op && CVPipeline::getOpBlockId(op) == dep.consumerBlockId) { + consIfOp = op; + break; + } + } + if (!consIfOp || !consIfOp->hasAttr(CVPipeline::kSplittedIf)) { + return; + } + if (srcOp->getAttrOfType(CVPipeline::kSplittedIf) != + consIfOp->getAttrOfType(CVPipeline::kSplittedIf)) { + return; + } + if (auto matmulOp = findYieldMatmulInSplitIf(dep.value)) { + dep.isSplitedIf = true; + dep.realValue = matmulOp->getResult(0); + } +} + +// V->C Transfer Logic +LogicalResult InterCoreTransferAndSyncPass::handleVectorToCube( + OpBuilder &builder, DependencyInfo &dep, FlagIdManager &flagManager, + FlagIdReuseManager &flagIdReuseManager) { + mlir::Value srcValue = dep.value; + Location loc = dep.value.getLoc(); + Value normalizedVal = srcValue; + + // Step 1: Shape normalization (automatically insert slice) + auto it = ndnzValueMapping.find(dep.value); + if (it != ndnzValueMapping.end()) { + normalizedVal = it->second; + } + + // Get start/end operations for V/C blocks + auto [prodStart, prodEnd] = getBlockStartEnd(dep.producerBlockId, module); + auto [consStart, consEnd] = getBlockStartEnd(dep.consumerBlockId, module); + + Operation *consumedDataOp = nullptr; + if (dep.consumerBlockId == dep.iniConsumerBlockId) { + auto consumerPoint = + analyzeConsumerReadInsertPoint(srcValue, dep.iniConsumerBlockId); + if (consumerPoint && consumerPoint->getBlock() == consStart->getBlock()) { + consStart = consumerPoint; + } + } + if (dep.iniProducerBlockId == dep.producerBlockId) { + auto producerPoint = + getCopyPointBeforeStore(normalizedVal, prodEnd, dep.iniProducerBlockId); + if (producerPoint) { + prodEnd = producerPoint; + } + if (Operation *srcDefOp = srcValue.getDefiningOp(); + srcDefOp && getSubBlockId(srcDefOp)) { + if (Operation *subBlockEnd = getSubBlockStartEnd(srcDefOp).second) + prodEnd = subBlockEnd; + } + } + LOG_DEBUG("after analyzeConsumerReadInsertPoint\n"); + Operation *transferOp = insertVectorToCubeTransfer( + builder, srcValue, normalizedVal, prodEnd, consStart, loc, transferIndex, + dep, is1DTensorDependency(dep.value), &consumedDataOp); + if (!transferOp) { + return failure(); + } + int flagId = flagManager.acquireId(); + auto [newConsStart, newConsEnd] = + getBlockStartEnd(dep.consumerBlockId, module); + + if (dep.consumerBlockId == dep.iniConsumerBlockId) { + auto newconsumerPoint = getConsumerWaitPoint(transferIndex); + if (newconsumerPoint && + newConsStart->getBlock() == newconsumerPoint->getBlock()) { + newConsStart = newconsumerPoint; + } + } + + insertInterCoreSync(builder, transferOp, newConsStart, newConsEnd, flagId, + loc, transferIndex, flagIdReuseManager, consumedDataOp); + + transferIndex++; + LOG_DEBUG("Inserted V->C transfer and sync: block " + << dep.producerBlockId << " -> block " << dep.consumerBlockId + << "\n"); + return success(); +} + +// C->V Transfer Logic +LogicalResult InterCoreTransferAndSyncPass::handleCubeToVector( + OpBuilder &builder, DependencyInfo &dep, FlagIdManager &flagManager, + FlagIdReuseManager &flagIdReuseManager) { + mlir::Value srcValue = dep.value; + + Location loc = srcValue.getLoc(); + auto [prodStart, prodEnd] = + getBlockStartEnd(dep.producerBlockId, module); // C Block + auto [consStart, consEnd] = + getBlockStartEnd(dep.consumerBlockId, module); // V Block + LOG_DEBUG("[newProdStart]" << *prodStart << "\n"); + LOG_DEBUG("[newProdEnd]" << *prodEnd << "\n"); + LOG_DEBUG("[newConsStart]" << *consStart << "\n"); + LOG_DEBUG("[newConsEnd]" << *consEnd << "\n"); + + AnalyzeSplittedIf(dep); + + if (srcValue.getDefiningOp() && + !isa(srcValue.getDefiningOp())) { + auto producerPoint = + getFixpipePointAfterProducer(srcValue, dep.iniProducerBlockId); + if (producerPoint) { + prodEnd = producerPoint; + } + } + + // uses of srcValue. Only applied when the source op belongs to a + // sub-block. + Operation *consumerPoint = nullptr; + if (dep.consumerBlockId == dep.iniConsumerBlockId) { + consumerPoint = + analyzeConsumerReadInsertPoint(srcValue, dep.iniConsumerBlockId); + if (consumerPoint && getSubBlockId(consumerPoint)) { + consStart = getSubBlockStartEnd(consumerPoint).first; + } + } + + if (dep.isSplitedIf) { + prodEnd = dep.realValue.getDefiningOp(); + auto consumerPoint = + analyzeConsumerReadInsertPoint(srcValue, dep.iniConsumerBlockId); + if (consumerPoint) { + consStart = consumerPoint; + } + } + + Operation *consumedDataOp = nullptr; + Operation *transferOp = + insertCubeToVectorTransfer(builder, srcValue, prodEnd, consStart, loc, + transferIndex, dep, &consumedDataOp); + + auto [newProdStart, newProdEnd] = + getBlockStartEnd(dep.producerBlockId, module); // C Block + auto [newConsStart, newConsEnd] = + getBlockStartEnd(dep.consumerBlockId, module); // V Block + if (Operation *subBlockEnd = getSubBlockStartEnd(consumerPoint).second) { + newConsEnd = subBlockEnd; + } + int flagId = flagManager.acquireId(); + + bool isStoreDirectly = + isStoreDirectlyInUserChain(consumedDataOp->getResult(0)); + + if (dep.consumerBlockId == dep.iniConsumerBlockId) { + auto newconsumerPoint = getConsumerWaitPoint(transferIndex); + if (newconsumerPoint && getSubBlockId(consumerPoint)) { + // insert the wait for read at the sub-block start + newConsStart = getSubBlockStartEnd(consumerPoint).first; + } + } + if (dep.isSplitedIf) { + auto newconsumerPoint = getConsumerWaitPoint(transferIndex); + auto yieldBeforeOp = getOpBeforeYield(newconsumerPoint); + if (newconsumerPoint && yieldBeforeOp) { + newConsStart = newconsumerPoint; + newConsEnd = yieldBeforeOp; + } + } + insertInterCoreSync(builder, transferOp, newConsStart, newConsEnd, flagId, + loc, transferIndex, flagIdReuseManager, consumedDataOp, + isStoreDirectly); + + transferIndex++; + LOG_DEBUG("Inserted C->V transfer and sync: block " + << dep.producerBlockId << " -> block " << dep.consumerBlockId + << "\n"); + return success(); +} + +// C->C Shared L1 buffer allocation. +// Intra-block C->C: allocates right after the producing matmul (before the +// fixpipe). Cross-block C->C: allocates before the main loop if one encloses +// both producer and consumer, otherwise after the producer block end. +Operation *InterCoreTransferAndSyncPass::createC2CSharedL1Buffer( + OpBuilder &builder, Location loc, ArrayRef shape, Type elemType, + int prodBlockId, Operation *prodEnd, Operation *consStart, + bool isIntraC2C) { + auto addressSpaceAttr = + builder.getAttr(hivm::AddressSpace::L1); + auto allocType = MemRefType::get(shape, elemType, nullptr, addressSpaceAttr); + + Operation *allocOp = nullptr; + Operation *mainLoopOp = findMainLoopforTransfer(prodEnd, consStart); + if (mainLoopOp && !isIntraC2C) { + builder.setInsertionPoint(mainLoopOp); + allocOp = builder.create(loc, allocType); + int loopBlockId = CVPipeline::getOpBlockId(mainLoopOp).value_or(-1); + attachCommonTags(allocOp, loopBlockId, CVPipeline::kCoreTypeCube); + builder.setInsertionPointAfter(prodEnd); + } else { + builder.setInsertionPointAfter(prodEnd); + allocOp = builder.create(loc, allocType); + attachCommonTags(allocOp, prodBlockId, CVPipeline::kCoreTypeCube); + } + return allocOp; +} + +// C->C Transfer Logic +LogicalResult +InterCoreTransferAndSyncPass::handleCubeToCube(OpBuilder &builder, + DependencyInfo &dep) { + mlir::Value transferValue = dep.value; + Location loc = transferValue.getLoc(); + + // Check if this is a matmul -> trunc -> matmul pattern. + // If so, use the matmul result (pre-trunc) as the fixpipe source and + // fold the type conversion into the fixpipe as pre_quant. + Operation *truncOp = transferValue.getDefiningOp(); + mlir::Value fixpipeSrcValue = transferValue; + std::optional quantMode = + CVPipeline::getFixpipePreQuantMode(truncOp); + if (quantMode) { + fixpipeSrcValue = transferValue.getDefiningOp()->getOperand(0); + } + + auto [prodStart, prodEnd] = + getBlockStartEnd(dep.producerBlockId, module); // C Block + auto [consStart, consEnd] = + getBlockStartEnd(dep.consumerBlockId, module); // C Block + + // Adjust consStart to the first operation that actually reads transferValue + // within the consumer block, matching the logic in handleVectorToCube. + if (dep.consumerBlockId == dep.iniConsumerBlockId) { + auto consumerPoint = + analyzeConsumerReadInsertPoint(transferValue, dep.iniConsumerBlockId); + if (consumerPoint) { + consStart = consumerPoint; + } + } + + auto transferTensorType = cast(transferValue.getType()); + int64_t M = transferTensorType.getDimSize(0); + int64_t N = transferTensorType.getDimSize(1); + Type elemType = transferTensorType.getElementType(); + auto shape = std::vector{M, N}; + + int prodBlockId = + CVPipeline::getOpBlockId(fixpipeSrcValue.getDefiningOp()).value_or(-1); + int consBlockId = CVPipeline::getOpBlockId(consStart).value_or(-1); + + bool isIntraC2C = (dep.iniProducerBlockId == dep.iniConsumerBlockId); + + if (!isa( + transferValue.getDefiningOp())) { + auto producerPoint = + getFixpipePointAfterProducer(transferValue, dep.iniProducerBlockId); + if (producerPoint) { + prodEnd = producerPoint; + } + } + + // Allocate a single shared L1 buffer for producer and consumer. + Operation *allocOp = + createC2CSharedL1Buffer(builder, loc, shape, elemType, prodBlockId, + prodEnd, consStart, isIntraC2C); + + // Producer side: insert fixpipe to write matmul L0C output to L1 buffer + auto dmaModeAttr = + FixpipeDMAModeAttr::get(builder.getContext(), FixpipeDMAMode::NZ2NZ); + bool channelSplit = isChannelSplitNeeded(transferTensorType); + + // Build quantModeAttr when a trunc is being folded in as pre_quant. + FixpipePreQuantModeAttr quantModeAttr = nullptr; + if (quantMode.has_value()) { + quantModeAttr = + FixpipePreQuantModeAttr::get(builder.getContext(), quantMode.value()); + } + + auto fixpipeOp = builder.create( + loc, mlir::TypeRange{}, fixpipeSrcValue, allocOp->getResult(0), + mlir::ValueRange{}, dmaModeAttr, nullptr, nullptr, quantModeAttr, nullptr, + builder.getBoolAttr(channelSplit), nullptr, nullptr, mlir::ArrayAttr{}, + nullptr); + attachCommonTags(fixpipeOp, prodBlockId, CVPipeline::kCoreTypeCube); + // Tag C2C fixpipe as kIntraDeps producer when it is a cross-block C2C. + if (!isIntraC2C) { + fixpipeOp->setAttr( + CVPipeline::kIntraDeps, + builder.getI32ArrayAttr( + {intraDepsGroupId, CVPipeline::crossCoreProducerId})); + } + LOG_DEBUG("[fixpipeOp C->C]: " << *fixpipeOp << "\n"); + + // Consumer side: read L1 buffer via MemorySpaceCast + ToTensor + builder.setInsertionPoint(consStart); + auto plainMemrefType = MemRefType::get(shape, elemType); + auto memspaceCastOp = builder.create( + loc, plainMemrefType, allocOp->getResult(0)); + auto targetTensorType = RankedTensorType::get(shape, elemType); + auto toTensorOp = builder.create( + loc, targetTensorType, memspaceCastOp.getResult(), true, true); + attachCommonTags(memspaceCastOp, consBlockId, CVPipeline::kCoreTypeCube); + // Tag C2C memspaceCastOp as kIntraDeps consumer when it is a cross-block C2C. + if (!isIntraC2C) { + memspaceCastOp->setAttr( + CVPipeline::kIntraDeps, + builder.getI32ArrayAttr( + {intraDepsGroupId, CVPipeline::crossCoreConsumerId})); + } + attachCommonTags(toTensorOp, consBlockId, CVPipeline::kCoreTypeCube); + + // Replace uses of transferValue within the consumer block. + // For matmul ops, only replace input (A/B) operands so that the init (outs) + // still directly uses the producer matmul result when the same value is + // used as both input and init. + llvm::SmallVector users(transferValue.getUsers().begin(), + transferValue.getUsers().end()); + for (Operation *user : users) { + auto userBlockIdOpt = CVPipeline::getOpBlockId(user); + if (userBlockIdOpt && *userBlockIdOpt == dep.iniConsumerBlockId) { + if (user == fixpipeOp) { + continue; + } + if (auto matmulUser = dyn_cast(user)) { + for (int64_t i = 0; i < matmulUser.getNumDpsInputs(); ++i) { + if (matmulUser->getOpOperand(i).get() == transferValue) { + matmulUser->setOperand(i, toTensorOp.getResult()); + } + } + } else { + LOG_DEBUG( + "warning: c2c transferValue is used by non-matmul op: " << *user); + user->replaceUsesOfWith(transferValue, toTensorOp.getResult()); + } + } + } + + // Erase the trunc op after all uses are replaced (dead code elimination). + if (truncOp && CVPipeline::getFixpipePreQuantMode(truncOp).has_value()) { + truncOp->erase(); + } + + intraDepsGroupId++; + LOG_DEBUG("Inserted C->C fixpipe transfer: block " + << dep.producerBlockId << " -> block " << dep.consumerBlockId + << "\n"); + return success(); +} + +// Memory Dependency +LogicalResult InterCoreTransferAndSyncPass::handleMemoryDependency( + OpBuilder &builder, DependencyInfo &dep, size_t depIndex, + llvm::SmallVector memDependencies, + FlagIdManager &flagManager, FlagIdReuseManager &flagIdReuseManager) { + LOG_DEBUG("Handling memory dependency...\n"); + + // Get producer and consumer block start/end operations + auto [prodStart, prodEnd] = getBlockStartEnd(dep.producerBlockId, module); + auto [consStart, consEnd] = getBlockStartEnd(dep.consumerBlockId, module); + + if (!prodStart || !prodEnd || !consStart || !consEnd) { + LOG_DEBUG("[ERROR] Failed to get block start/end operations.\n"); + return failure(); + } + + if (isOuterLayerDependency(depIndex, prodEnd, consStart, memDependencies)) { + LOG_DEBUG("[MEMDEP] Skipping outer layer dependency: block " + << dep.producerBlockId << " -> block " << dep.consumerBlockId + << "\n"); + return success(); + } + + attachMemCrossDeps(dep.predOp, transferIndex, CVPipeline::crossCoreProducerId, + builder); + attachMemCrossDeps(dep.nextOp, transferIndex, CVPipeline::crossCoreConsumerId, + builder); + attachCrossCoreDeps(dep.predOp, transferIndex, + CVPipeline::crossCoreProducerId, builder); + attachCrossCoreDeps(dep.nextOp, transferIndex, + CVPipeline::crossCoreConsumerId, builder); + // Get flag ID + int flagId = flagManager.acquireId(); + + // Determine sync direction: CUBE->VECTOR or VECTOR->CUBE + bool isCubeToVector = (dep.type == DependencyType::CubeToVector); + + // Get location info + Location loc = prodEnd->getLoc(); + if (dep.iniProducerBlockId == dep.producerBlockId && + dep.iniConsumerBlockId == dep.consumerBlockId && + hasMemDepSyncWhitelistKernel(module)) { + prodEnd = dep.predOp; + prodStart = dep.predOp; + } + + insertMemDepSync(builder, prodStart, prodEnd, consStart, consEnd, flagId, loc, + isCubeToVector, flagIdReuseManager); + + transferIndex++; + + LOG_DEBUG("Inserted PIPE_MTE2 sync: block " + << dep.producerBlockId << " -> block " << dep.consumerBlockId + << ", flagId = " << flagId << "\n"); + + return success(); +} + +llvm::SmallVector +InterCoreTransferAndSyncPass::insertAnalyzeFlagRelations( + mlir::ModuleOp module, FlagIdReuseManager &flagIdReuseManager) { + using OpVector = llvm::SmallVector; + + // E1 (per-pipe FIFO) is isolated per MLIR block: ops on one (core, pipe) + llvm::DenseMap>> + sequenceOpMap; + llvm::DenseSet relationOpSet; + llvm::SmallVector relationOps; + llvm::SmallVector analyzeFlagIdOps; + + auto insertRelation = [&](Operation *before, Operation *after) { + if (!before || !after || before == after) { + return; + } + flagIdReuseManager.insertRelationBetweenSetAndWait(before, after); + }; + + auto noteRelationOp = [&](Operation *op) { + if (!relationOpSet.insert(op).second) { + return; + } + relationOps.push_back(op); + }; + + auto notePipeOp = [&](Operation *op, hivm::TCoreType coreType, + hivm::PIPE pipe) { + if (!isConcretePipe(pipe)) { + return; + } + if (Block *block = op->getBlock()) { + sequenceOpMap[block][coreType][pipe].push_back(op); + } + noteRelationOp(op); + }; + + module.walk([&](mlir::Operation *op) { + if (auto setOp = llvm::dyn_cast(op)) { + auto coreAttr = setOp.getTcoreType(); + auto pipeAttr = setOp.getTpipeAttr(); + if (coreAttr && pipeAttr) { + notePipeOp(op, coreAttr.getTcoretype(), pipeAttr.getPipe()); + } + if (op->hasAttr(CVPipeline::kAnalyzeFlagId)) { + analyzeFlagIdOps.push_back(op); + } + return; + } + + if (auto waitOp = llvm::dyn_cast(op)) { + auto coreAttr = waitOp.getTcoreType(); + auto pipeAttr = waitOp.getPipeAttr(); + if (coreAttr && pipeAttr) { + notePipeOp(op, coreAttr.getTcoretype(), pipeAttr.getPipe()); + } + if (op->hasAttr(CVPipeline::kAnalyzeFlagId)) { + analyzeFlagIdOps.push_back(op); + } + return; + } + + auto coreType = getAnalyzeCoreType(op); + if (!coreType) { + return; + } + + noteRelationOp(op); + if (auto pipeOp = llvm::dyn_cast(op)) { + if (auto copyOp = llvm::dyn_cast(op)) { + if (auto pipe = getCopyPipeForAnalyze(copyOp)) { + notePipeOp(op, *coreType, *pipe); + } + return; + } + if (pipeOp.isMacroOp()) { + notePipeOp(op, *coreType, pipeOp.getInPipe()); + notePipeOp(op, *coreType, pipeOp.getOutPipe()); + return; + } + notePipeOp(op, *coreType, pipeOp.getPipe()); + } + }); + + for (Operation *op : relationOps) { + for (Value operand : op->getOperands()) { + Operation *definingOp = operand.getDefiningOp(); + if (!definingOp) { + if (auto blockArgument = dyn_cast(operand)) { + definingOp = blockArgument.getOwner()->getParentOp(); + } + } + if (definingOp && relationOpSet.contains(definingOp)) { + insertRelation(definingOp, op); + } + } + } + + // E1: per-block, per-(core, pipe) FIFO chain. + for (auto &blockEntry : sequenceOpMap) { + for (auto &coreEntry : blockEntry.second) { + for (auto &pipeEntry : coreEntry.second) { + auto &ops = pipeEntry.second; + for (size_t i = 0; i + 1 < ops.size(); ++i) { + insertRelation(ops[i], ops[i + 1]); + } + } + } + } + return analyzeFlagIdOps; +} + +void InterCoreTransferAndSyncPass::remapInterCoreTransferFlagIds( + llvm::DenseMap &remapResult) { + module.walk([&](mlir::Operation *op) { + if (!llvm::isa(op) && + !llvm::isa(op)) { + return; + } + bool trackedForReuse = op->hasAttr(CVPipeline::kAnalyzeFlagId); + op->removeAttr(CVPipeline::kAnalyzeFlagId); + if (!trackedForReuse || remapResult.empty()) { + return; + } + if (auto intAttr = op->getAttrOfType("static_flag_id")) { + int flagId = static_cast(intAttr.getInt()); + auto it = remapResult.find(flagId); + if (it == remapResult.end()) { + return; + } + auto newFlagAttr = mlir::IntegerAttr::get(intAttr.getType(), it->second); + op->setAttr("static_flag_id", newFlagAttr); + } + }); +} + +void InterCoreTransferAndSyncPass::sortDependencies( + llvm::SmallVector &dependencies, mlir::ModuleOp module) { + if (dependencies.size() <= 1) { + return; + } + + // Step 1: Walk the entire module and assign a monotonically increasing order + // to each operation, representing its position in the IR. + llvm::DenseMap opOrder; + unsigned order = 0; + module.walk( + [&](mlir::Operation *op) { opOrder[op] = order++; }); + + // Step 2: Helper lambda — get the earliest user op of dep.value within the + // consumer compute block. + auto getFirstConsumerOp = + [&](const DependencyInfo &dep) -> mlir::Operation * { + mlir::Operation *firstConsumer = nullptr; + unsigned firstOrder = std::numeric_limits::max(); + for (auto *user : dep.value.getUsers()) { + auto userBlockIdOpt = CVPipeline::getOpBlockId(user); + if (userBlockIdOpt && *userBlockIdOpt == dep.consumerBlockId) { + auto it = opOrder.find(user); + if (it != opOrder.end() && it->second < firstOrder) { + firstOrder = it->second; + firstConsumer = user; + } + } + } + return firstConsumer; + }; + + // Step 3: Sort + std::sort(dependencies.begin(), dependencies.end(), + [&](const DependencyInfo &a, const DependencyInfo &b) { + // the dependency whose consumer op appears earlier comes first. + auto *aConsOp = getFirstConsumerOp(a); + auto *bConsOp = getFirstConsumerOp(b); + if (aConsOp && bConsOp) { + unsigned aConsOpOrder = opOrder.lookup(aConsOp); + unsigned bConsOpOrder = opOrder.lookup(bConsOp); + if (aConsOpOrder != bConsOpOrder) { + return aConsOpOrder < bConsOpOrder; + } + } + return false; + }); +} + +void InterCoreTransferAndSyncPass::analyzeLoopInclusion() { + loopInclusions.clear(); + int maxLoopId = -1; + module.walk([&](Operation *op) { + auto loopOp = dyn_cast(op); + if (!loopOp) { + return; + } + + int loopId = 0; + Operation *parentLoop = + loopOp.getOperation()->getParentOfType(); + while (parentLoop) { + ++loopId; + parentLoop = parentLoop->getParentOfType(); + } + loopOp->setAttr( + CVPipeline::kLoopId, + IntegerAttr::get( + IntegerType::get(loopOp->getContext(), kIntegerBitWidth), loopId)); + loopInclusions[loopId].push_back(loopOp); + maxLoopId = std::max(maxLoopId, loopId); + }); + for (int loopId = 0; loopId <= maxLoopId; ++loopId) { + if (loopInclusions[loopId].size() != 1) { + break; + } + bool allSingle = true; + for (int outerId = loopId - 1; outerId >= 0; --outerId) { + if (loopInclusions[outerId].size() != 1) { + allSingle = false; + break; + } + } + if (allSingle) { + singleLoopSet.insert(loopId); + } + } +} + +void InterCoreTransferAndSyncPass::moveStartEndSync(OpBuilder &builder) { + llvm::SmallVector startSyncOps; + llvm::SmallVector endSyncOps; + module.walk([&](Operation *op) { + if (!op->hasAttr(CVPipeline::kLoopId) || + !(isa(op))) { + return; + } + auto loopId = op->getAttrOfType(CVPipeline::kLoopId).getInt(); + auto it = singleLoopSet.find(loopId); + if (it != singleLoopSet.end()) { + if (isa(op)) { + startSyncOps.push_back(op); + } else if (isa(op)) { + endSyncOps.push_back(op); + } + } + }); + + if (loopInclusions.count(0) == 0 || loopInclusions[0].empty()) { + return; + } + Operation *outerLoop = loopInclusions[0][0]; + if (!outerLoop) { + return; + } + // move startSyncOps and endSyncOps to the outer loop + builder.setInsertionPoint(outerLoop); + for (auto it = startSyncOps.begin(); it != startSyncOps.end(); ++it) { + builder.clone(**it); + } + builder.setInsertionPointAfter(outerLoop); + for (auto it = endSyncOps.rbegin(); it != endSyncOps.rend(); ++it) { + builder.clone(**it); + } + for (auto op : startSyncOps) { + op->erase(); + } + for (auto op : endSyncOps) { + op->erase(); + } +} + +// Main Processing +LogicalResult InterCoreTransferAndSyncPass::processDependencies( + FlagIdManager &flagManager, FlagIdReuseManager &flagIdReuseManager) { + LOG_DEBUG("Starting InterCoreTransferAndSyncPass processDependencies...\n"); + OpBuilder builder(module.getContext()); + + auto &info = getAnalysis(); + if (!info.isValid()) { + LOG_DEBUG("Error: Data dependency analysis failed.\n"); + return failure(); + } + + // Analyze loop nesting before inserting inter-core transfer/sync. + if (hasStartEndSyncWhitelistKernel(module)) { + analyzeLoopInclusion(); + } + + llvm::SmallVector &V2CDependencies = + info.getV2CDependencies(); + sortDependencies(V2CDependencies, module); + LOG_DEBUG("[DEBUG] V2CDependencies size: " << V2CDependencies.size() << "\n"); + for (size_t i = 0; i < V2CDependencies.size(); ++i) { + auto &dep = V2CDependencies[i]; + LOG_DEBUG("[V2C-" << i << "] producerBlockId = " << dep.producerBlockId + << ", consumerBlockId = " << dep.consumerBlockId + << ", iniProducerBlockId = " << dep.iniProducerBlockId + << ", iniConsumerBlockId = " << dep.iniConsumerBlockId + << ", value = " << dep.value << "\n"); + } + LOG_DEBUG("Step 1: Handle V->C dependencies\n"); + // Step 1: Handle V->C dependencies + for (auto &dep : V2CDependencies) { + if (!isScalarDependency(dep.value) && !is1DTensorDependency(dep.value)) { + Location loc = dep.value.getLoc(); + Nd2NzNormalize(builder, dep, loc); + } + } + + for (auto &dep : V2CDependencies) { + LOG_DEBUG("[V->C] producerBlockId = " << dep.producerBlockId + << ", consumerBlockId = " + << dep.consumerBlockId << "\n"); + if (failed(handleVectorToCube(builder, dep, flagManager, + flagIdReuseManager))) { + LOG_DEBUG("[ERROR] V->C failed! producerBlockId = " + << dep.producerBlockId + << ", consumerBlockId = " << dep.consumerBlockId << "\n"); + return failure(); + } + } + LOG_DEBUG("Completed V->C transfers and syncs.\n"); + + llvm::SmallVector &C2VDependencies = + info.getC2VDependencies(); + sortDependencies(C2VDependencies, module); + LOG_DEBUG("[DEBUG] C2VDependencies size: " << C2VDependencies.size() << "\n"); + // Step 2: Handle C->V dependencies + for (auto &dep : C2VDependencies) { + LOG_DEBUG("[C->V] producerBlockId = " << dep.producerBlockId + << ", consumerBlockId = " + << dep.consumerBlockId << "\n"); + if (failed(handleCubeToVector(builder, dep, flagManager, + flagIdReuseManager))) { + LOG_DEBUG("[ERROR] C->V failed! producerBlockId = " + << dep.producerBlockId + << ", consumerBlockId = " << dep.consumerBlockId << "\n"); + return failure(); + } + } + LOG_DEBUG("Completed C->V transfers and syncs.\n"); + + // Step 3.1: Handle inter-block C->C dependencies (fixpipe L0C to L1) + llvm::SmallVector &C2CDependencies = + info.getC2CDependencies(); + sortDependencies(C2CDependencies, module); + LOG_DEBUG("[DEBUG] InterC2CDependencies size: " << C2CDependencies.size() + << "\n"); + for (auto &dep : C2CDependencies) { + LOG_DEBUG("[C->C] producerBlockId = " << dep.producerBlockId + << ", consumerBlockId = " + << dep.consumerBlockId << "\n"); + // Only handle C->C transfer when both the defining op and the consuming + // users of dep.value are matmul ops. + if (!isValidC2CMatmulDependency(dep.value, dep.consumerBlockId)) { + continue; + } + if (failed(handleCubeToCube(builder, dep))) { + LOG_DEBUG("[ERROR] Inter C->C failed! producerBlockId = " + << dep.producerBlockId + << ", consumerBlockId = " << dep.consumerBlockId << "\n"); + return failure(); + } + } + // Step 3: Handle intra-block C->C dependencies. + llvm::SmallVector &intraC2CDependencies = + info.getIntraC2CDependencies(); + sortDependencies(intraC2CDependencies, module); + LOG_DEBUG("[DEBUG] IntraC2CDependencies size: " << intraC2CDependencies.size() + << "\n"); + for (auto &dep : intraC2CDependencies) { + LOG_DEBUG("[IntraC2C] producer and consumer' BlockId = " + << dep.producerBlockId << "\n"); + if (!isValidC2CMatmulDependency(dep.value, dep.consumerBlockId)) { + continue; + } + // dep.operand must be an input (A/B) operand of the consumer matmul, + // not an init (outs) operand. + if (!dep.operand) { + continue; + } + auto consumerMatmul = dyn_cast(dep.operand->getOwner()); + if (!consumerMatmul || + dep.operand->getOperandNumber() >= + static_cast(consumerMatmul.getNumDpsInputs())) { + continue; + } + if (failed(handleCubeToCube(builder, dep))) { + LOG_DEBUG("[ERROR] IntraC2C failed! producer and consumer' BlockId = " + << dep.consumerBlockId << "\n"); + return failure(); + } + } + LOG_DEBUG("Completed C->C transfers and syncs.\n"); + + llvm::SmallVector &memDependencies = + info.getMemoryDependencies(); + LOG_DEBUG("[DEBUG] MemoryDependencies size: " << memDependencies.size() + << "\n"); + + for (size_t i = 0; i < memDependencies.size(); ++i) { + auto &dep = memDependencies[i]; + LOG_DEBUG("[MEMDEP] value = " + << dep.value << " producerBlockId = " << dep.producerBlockId + << ", consumerBlockId = " << dep.consumerBlockId << "\n"); + if (failed(handleMemoryDependency(builder, dep, i, memDependencies, + flagManager, flagIdReuseManager))) { + LOG_DEBUG("[ERROR] Memdep failed! producerBlockId = " + << dep.producerBlockId + << ", consumerBlockId = " << dep.consumerBlockId << "\n"); + return failure(); + } + } + LOG_DEBUG("Completed memory syncs.\n"); + LOG_DEBUG("=====================================================\n"); + + if (!flagManager.checkCurrentId()) { + llvm::SmallVector analyzeFlagIdOps = + insertAnalyzeFlagRelations(module, flagIdReuseManager); + DenseMap remapResult = + flagIdReuseManager.reuseInterCoreTransferFlagIds(analyzeFlagIdOps); + remapInterCoreTransferFlagIds(remapResult); + } + + // Remove VECTOR pseudo-ops before inserting the direct-store sync. + removeVectorPseudoOps(); + + // Synchronize CUBE's fixpipe with VECTOR's direct MTE3 store. No VECTOR + // tensor computation occurs between the transfer and the store. + processCubeToVectorDirectStoreSync(builder, flagManager, flagIdReuseManager); + + // move start/end sync ops + if (hasStartEndSyncWhitelistKernel(module)) { + moveStartEndSync(builder); + } + + LOG_DEBUG("InterCoreTransferAndSyncPass success!\n"); + + return success(); +} + +// Declare dependent dialects +void InterCoreTransferAndSyncPass::getDependentDialects( + DialectRegistry ®istry) const { + registry.insert(); +} + +// Pass Entry Point +void InterCoreTransferAndSyncPass::runOnOperation() { + LOG_DEBUG("\n--- enter InterCoreTransferAndSyncPass --->\n"); + module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + // Phase 1: Initialize FlagIdManager as local variable + FlagIdManager flagManager(module); + FlagIdReuseManager flagIdReuseManager; + + // Phase 2: Execute transfer and sync insertion + if (failed(processDependencies(flagManager, flagIdReuseManager))) { + LOG_DEBUG("Error: Inter-core transfer and sync failed.\n"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + + LOG_DEBUG("Module after InterCoreTransferAndSyncPass:\n" << module << "\n"); + + LOG_DEBUG("--- exit InterCoreTransferAndSyncPass --->\n"); +} + +// Create the pass +namespace mlir { +namespace triton { +std::unique_ptr> createInterCoreTransferAndSyncPass() { + return std::make_unique(); +} + +void registerInterCoreTransferAndSyncPasses() { + registerPass([]() -> std::unique_ptr { + return createInterCoreTransferAndSyncPass(); + }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/MarkMainLoop.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/MarkMainLoop.cpp new file mode 100644 index 0000000000..b641681abd --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/MarkMainLoop.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SplitDataflow/MarkMainLoop.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/IR/Operation.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; + +static constexpr const char *DEBUG_TYPE = "mark-main-loop"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir::triton; + +// Pass Entry Point +void MarkMainLoopPass::runOnOperation() { + LOG_DEBUG("\n--- enter MarkMainLoopPass --->\n"); + ModuleOp module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + int mainLoopIdCounter = 0; + SmallVector mainLoops; + + // Find all candidate main loops (ForOp + WhileOp) + auto isL1Fixpipe = [](Operation *op) -> bool { + auto fixpipeOp = dyn_cast(op); + if (!fixpipeOp) + return false; + auto dstType = dyn_cast(fixpipeOp.getDst().getType()); + if (!dstType) + return false; + auto addrSpaceAttr = + dyn_cast_or_null(dstType.getMemorySpace()); + return addrSpaceAttr && + addrSpaceAttr.getAddressSpace() == hivm::AddressSpace::L1; + }; + + module.walk([&](Operation *op) { + if (!isa(op)) + return; + + if (isL1Fixpipe(op)) + return; + + if (auto forOp = op->getParentOfType()) + mainLoops.push_back(forOp); + if (auto whileOp = op->getParentOfType()) + mainLoops.push_back(whileOp); + }); + + for (Operation *loopOp : mainLoops) { + if (!loopOp->hasAttr(CVPipeline::kMainLoop)) { + // Add attribute with integer value (current counter ID) + loopOp->setAttr( + CVPipeline::kMainLoop, + Builder(module.getContext()).getI32IntegerAttr(mainLoopIdCounter)); + mainLoopIdCounter++; + } + } + + // Remove main_loop attribute from outer loops if nested loops both have it + // Keep only the innermost main_loop + SmallVector allMainLoops; + module.walk([&](Operation *loopOp) { + if (CVPipeline::isMainLoopOp(loopOp)) { + allMainLoops.push_back(loopOp); + } + }); + + for (Operation *loopOp : allMainLoops) { + // Check if there's any nested loop with main_loop attribute + bool hasNestedMainLoop = false; + loopOp->walk([&](Operation *nestedLoopOp) { + if (nestedLoopOp != loopOp && CVPipeline::isMainLoopOp(nestedLoopOp)) { + hasNestedMainLoop = true; + } + }); + // Remove attribute from outer loop if inner loop also has it + if (hasNestedMainLoop) { + loopOp->removeAttr(CVPipeline::kMainLoop); + } + } + + LOG_DEBUG("--- exit MarkMainLoopPass --->\n"); +} + +// Create the pass +namespace mlir { +namespace triton { +std::unique_ptr> createMarkMainLoopPass() { + return std::make_unique(); +} +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.cpp new file mode 100644 index 0000000000..183aac77a2 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/ADT/SetVector.h" +#include "llvm/Support/Debug.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Pass/PassRegistry.h" +#include "mlir/Rewrite/FrozenRewritePatternSet.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" + +#include "ascend/include/DynamicCVPipeline/SplitDataflow/PreserveControlAttrsCanonicalize.h" + +#include "DynamicCVPipeline/Common/Utils.h" + +using namespace mlir; + +#define DEBUG_TYPE "preserve-control-attrs-canonicalize" +#define LOG_DEBUG(msg) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << msg) + +namespace { + +static void debugDumpIr(StringRef stage, Operation *op) { + LOG_DEBUG(stage << "\n"; op->print(llvm::dbgs()); llvm::dbgs() << "\n"); +} + +static bool isTrackedControlFlowOp(Operation *op) { + return isa(op); +} + +static bool canTransferAttrs(Operation *from, Operation *to) { + return from && to && from != to && isTrackedControlFlowOp(from) && + isTrackedControlFlowOp(to) && from->getName() == to->getName(); +} + +class PreserveControlAttrsListener : public RewriterBase::Listener { +public: + void notifyOperationInserted(Operation *op, OpBuilder::InsertPoint) override { + recentInserts.insert(op); + } + + void notifyOperationErased(Operation *op) override { + recentInserts.remove(op); + } + + void notifyOperationReplaced(Operation *op, Operation *newOp) override { + transferAttrs(op, newOp); + transferBlockIdToInsertedReplacement(op, newOp); + } + + void notifyOperationReplaced(Operation *op, ValueRange values) override { + if (Operation *newOp = findReplacementOp(op, values)) { + transferAttrs(op, newOp); + } + + for (Value value : values) { + if (!value) + continue; + if (Operation *defOp = value.getDefiningOp()) { + transferBlockIdToInsertedReplacement(op, defOp); + } + } + } + +private: + Operation *findReplacementOp(Operation *oldOp, + ValueRange replacements) const { + if (!isTrackedControlFlowOp(oldOp)) + return nullptr; + + // Prefer direct replacement from definingOps + for (Value value : replacements) { + if (!value) + continue; + Operation *defOp = value.getDefiningOp(); + if (defOp && recentInserts.contains(defOp) && + canTransferAttrs(oldOp, defOp)) + return defOp; + } + + Block *oldBlock = oldOp->getBlock(); + if (!oldBlock) + return nullptr; + + // Fallback: search recentInserts in reverse, but only accept same-block + // candidates. + for (Operation *candidate : llvm::reverse(recentInserts.getArrayRef())) { + if (!canTransferAttrs(oldOp, candidate)) + continue; + + if (candidate->getBlock() != oldBlock) + continue; + return candidate; + } + return nullptr; + } + + static void transferAttrs(Operation *from, Operation *to) { + if (!canTransferAttrs(from, to)) + return; + + for (NamedAttribute attr : from->getAttrs()) { + if (to->hasAttr(attr.getName())) + continue; + to->setAttr(attr.getName(), attr.getValue()); + } + } + + void transferBlockIdToInsertedReplacement(Operation *from, + Operation *to) const { + if (!from || !to || from == to || !isTrackedControlFlowOp(from)) { + return; + } + + Attribute blockId = from->getAttr(CVPipeline::kBlockId); + if (!blockId || to->hasAttr(CVPipeline::kBlockId)) { + return; + } + + if (!recentInserts.contains(to) || from->getBlock() != to->getBlock()) { + return; + } + + to->setAttr(CVPipeline::kBlockId, blockId); + } + + llvm::SetVector recentInserts; +}; + +static void populateCanonicalizationPatterns(MLIRContext *ctx, + RewritePatternSet &patterns) { + for (Dialect *dialect : ctx->getLoadedDialects()) { + dialect->getCanonicalizationPatterns(patterns); + } + + for (RegisteredOperationName opName : ctx->getRegisteredOperations()) { + opName.getCanonicalizationPatterns(patterns, ctx); + } +} + +} // namespace + +void mlir::triton::PreserveControlAttrsCanonicalizePass::runOnOperation() { + if (CVPipeline::hasFallbackAttr(getOperation())) { + return; + } + + debugDumpIr("before PreserveControlAttrsCanonicalizePass", getOperation()); + + RewritePatternSet patterns(&getContext()); + populateCanonicalizationPatterns(&getContext(), patterns); + + PreserveControlAttrsListener listener; + GreedyRewriteConfig config; + config.setListener(&listener); + + if (failed(applyPatternsGreedily(getOperation(), + FrozenRewritePatternSet(std::move(patterns)), + config))) { + getOperation()->emitError("PreserveControlAttrsCanonicalizePass failed"); + CVPipeline::setFallbackAttr(getOperation(), CVPipeline::ERRCODE_FAILED); + return; + } + + debugDumpIr("after PreserveControlAttrsCanonicalizePass", getOperation()); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> +createPreserveControlAttrsCanonicalizePass() { + return std::make_unique(); +} + +void registerPreserveControlAttrsCanonicalizePasses() { + registerPass([]() -> std::unique_ptr { + return createPreserveControlAttrsCanonicalizePass(); + }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.cpp new file mode 100644 index 0000000000..baad0e3f54 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.cpp @@ -0,0 +1,307 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.h" +#include "DynamicCVPipeline/Common/MemoryEffectsTracker.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/ComputeBlockOpt/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "ascend/include/DynamicCVPipeline/PlanComputeBlock/ComputeBlockIdManager.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; + +static constexpr const char *DEBUG_TYPE = "refine-args-block-id"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << " [" << DEBUG_TYPE << "] " << __VA_ARGS__) + +using namespace mlir::triton; + +static void eraseOpsWithUnusedUsers(Operation *op, Block *loopBlock, + CVPipeline::ComputeBlockIdManager &bm) { + llvm::SetVector worklist; + + worklist.insert(op); + + while (!worklist.empty()) { + Operation *cur = worklist.pop_back_val(); + // Stay inside the loop body the caller handed us. + if (cur->getBlock() != loopBlock) { + continue; + } + // No users left and no side effects to lose: the only condition under which + // erasing is legal. + if (!isOpTriviallyDead(cur)) { + continue; + } + // Operands have to be collected before erase(): afterwards `cur` is gone. + SmallVector producers; + for (Value operand : cur->getOperands()) { + if (Operation *defOp = operand.getDefiningOp()) { + producers.push_back(defOp); + } + } + bm.forgetOp(cur); + cur->erase(); + // Re-examine the producers now that their uses have actually been dropped. + worklist.insert(producers.begin(), producers.end()); + } +} + +static bool cloneDepSubgraph(Operation *yieldDefOp, Block *loopBlock, + SmallVector &clonedOps, + IRMapping &mapping, int targetBlockId, + CVPipeline::ComputeBlockIdManager &bm) { + SmallVector worklist; + llvm::SetVector visited; + SmallVector toClone; + + // First pass: BFS to collect ops to clone + visited.insert(yieldDefOp); + toClone.push_back(yieldDefOp); + worklist.push_back(yieldDefOp); + + while (!worklist.empty()) { + Operation *cur = worklist.pop_back_val(); + + for (Value operand : cur->getOperands()) { + Operation *defOp = operand.getDefiningOp(); + if (!defOp || defOp->getBlock() != loopBlock || visited.count(defOp)) { + continue; + } + if (defOp != yieldDefOp && + (defOp->getNumResults() != 1 || + !CVPipeline::isScalarLike(defOp->getResult(0)))) { + return false; + } + visited.insert(defOp); + toClone.push_back(defOp); + worklist.push_back(defOp); + } + } + + // Second pass: clone in code order and update mapping + llvm::sort(toClone, + [](Operation *l, Operation *r) { return l->isBeforeInBlock(r); }); + + for (Operation *cur : toClone) { + Operation *cloned = cur->clone(mapping); + loopBlock->push_back(cloned); + bm.updateBlockId(cloned, -1); + cloned->moveAfter(cur); + clonedOps.push_back(cloned); + mapping.map(cur->getResult(0), cloned->getResult(0)); + } + + // Move cloned ops to after the last op in target block + auto targetOps = bm.getOpsByBlockId(targetBlockId); + Operation *lastOpInTargetBlock = nullptr; + for (Operation *op : targetOps) { + if (op->getBlock() != loopBlock) + continue; + if (!lastOpInTargetBlock || lastOpInTargetBlock->isBeforeInBlock(op)) { + lastOpInTargetBlock = op; + } + } + if (lastOpInTargetBlock) { + for (Operation *cloned : llvm::reverse(clonedOps)) { + cloned->moveAfter(lastOpInTargetBlock); + bm.updateBlockId(cloned, targetBlockId); + } + } + + return true; +} + +static int findFirstUser(Value iterArg, Block *loopBlock, + CVPipeline::ComputeBlockIdManager &bm) { + llvm::SetVector visited; + SmallVector> worklist; + int firstUserBlockId = -1; + Operation *firstUserOp = nullptr; + + for (OpOperand &use : iterArg.getUses()) { + Operation *user = use.getOwner(); + auto userInblock = CVPipeline::getAncestorInBlock(user, loopBlock); + if (!userInblock) { + continue; + } + if (isa(userInblock)) { + continue; + } + if (firstUserOp == nullptr || userInblock->isBeforeInBlock(firstUserOp)) { + firstUserOp = userInblock; + firstUserBlockId = bm.getBlockIdByOp(userInblock); + } + } + return firstUserBlockId; +} + +static bool +isDependenceOther(Operation *yieldDefOp, Block *loopBlock, int argsId, + const CVPipeline::MemoryDependenceGraph &memGraph) { + // Now we only filter the different iter_arg dependency. + for (Value operand : yieldDefOp->getOperands()) { + if (operand.getDefiningOp()) + continue; + // if have block argument from for block. Skip; + if (CVPipeline::getLoopCarriedArgIndex(operand, loopBlock) != argsId) { + LOG_DEBUG("Yield def op depends on other arg:" + << CVPipeline::getLoopCarriedArgIndex(operand, loopBlock) + << "\n"); + return true; + } + } + + for (auto memDep : memGraph.getExecBefore(yieldDefOp)) { + auto userInBlock = CVPipeline::getAncestorInBlock(memDep, loopBlock); + if (userInBlock) { + LOG_DEBUG("Yield def op depends on other memory in for block: " << *memDep + << "\n"); + return true; + } + } + return false; +} + +static void processOneLoop(Operation *loopOp, + CVPipeline::ComputeBlockIdManager &bm, + const CVPipeline::MemoryDependenceGraph &memGraph) { + auto ml = CVPipeline::MainLoop(loopOp); + Block *loopBlock = nullptr; + for (Region ®ion : loopOp->getRegions()) { + if (auto *terminator = region.front().getTerminator()) { + if (isa(terminator)) { + loopBlock = ®ion.front(); + break; + } + } + } + if (!loopBlock) { + LOG_DEBUG("No yield op found in loop block\n"); + return; + } + auto yieldOp = cast(loopBlock->getTerminator()); + + SmallVector iterArgs = ml.getIterArgs(); + for (size_t i = 0; i < iterArgs.size(); ++i) { + auto argsi = iterArgs[i]; + + Value yieldOperand = yieldOp.getOperand(i); + Operation *yieldDefOp = yieldOperand.getDefiningOp(); + if (!yieldDefOp) { + LOG_DEBUG("Yield operand is a block argument, skip. Operand: " + << yieldOperand << "\n"); + continue; + } + if (llvm::isa(yieldDefOp)) { + LOG_DEBUG("Yield operand is a if result, skip. Operand: " << yieldOperand + << "\n"); + continue; + } + LOG_DEBUG("yieldDefOp: " << *yieldDefOp << "\n" + << "idx: " << i << "\n"); + if (isDependenceOther(yieldDefOp, loopBlock, i, memGraph)) { + continue; + } + + int updateBlockId = bm.getBlockIdByOp(yieldDefOp); + LOG_DEBUG("Update block id for yield def op: " << updateBlockId << "\n"); + + int firstUserBlockId = findFirstUser(argsi, loopBlock, bm); + LOG_DEBUG("First user block id: " << firstUserBlockId << "\n"); + + if (firstUserBlockId == -1 || updateBlockId == firstUserBlockId) { + continue; + } + + IRMapping mapping; + SmallVector clonedOps; + + // Clone entire depSubgraph (yieldDefOp + upstream deps) to target block + if (!cloneDepSubgraph(yieldDefOp, loopBlock, clonedOps, mapping, + firstUserBlockId, bm)) { + LOG_DEBUG("Skip iter_arg " << i << " due to non-scalar dependency\n"); + continue; + } + + // Check if clonedOps would create a cycle + if (willCreateCycle(clonedOps, memGraph, firstUserBlockId, bm)) { + LOG_DEBUG("Skip iter_arg " << i << " due to cycle detected\n"); + continue; + } + + // Update yield to use cloned yieldDefOp result + yieldOp.setOperand(i, mapping.lookup(yieldDefOp->getResult(0))); + + // Erase original yieldDefOp and its upstream ops that have no more users + eraseOpsWithUnusedUsers(yieldDefOp, loopBlock, bm); + LOG_DEBUG("Successfully moved iter_arg " << i << " from block " + << updateBlockId << " to block " + << firstUserBlockId << "\n"); + } +} + +void RefineArgsBlockIdPass::runOnOperation() { + LOG_DEBUG("\n--- enter RefineArgsBlockIdPass --->\n"); + ModuleOp moduleOp = getOperation(); + + if (CVPipeline::hasFallbackAttr(moduleOp)) { + return; + } + + CVPipeline::ComputeBlockIdManager bm(moduleOp); + auto &aa = getAnalysis(); + LOG_DEBUG(*moduleOp << "\n"); + moduleOp.walk([&](Operation *op) { + if (!op->hasAttr(CVPipeline::kMainLoop) || + !isa(op)) { + return; + } + auto memDepGraph = CVPipeline::MemoryDependenceGraph(op, aa); + processOneLoop(op, bm, memDepGraph); + }); + + LOG_DEBUG(*moduleOp << "\n"); + LOG_DEBUG("--- exit RefineArgsBlockIdPass --->\n"); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createRefineArgsBlockIdPass() { + return std::make_unique(); +} + +void registerRefineArgsBlockIdPasses() { + registerPass([]() -> std::unique_ptr { + return createRefineArgsBlockIdPass(); + }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/SeparateCVScope.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/SeparateCVScope.cpp new file mode 100644 index 0000000000..260fb2e44f --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/SeparateCVScope.cpp @@ -0,0 +1,1149 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include + +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include "ascend/include/DynamicCVPipeline/Common/SSBufferManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/SplitDataflow/SeparateCVScope.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/Interfaces/LoopLikeInterface.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/RegionUtils.h" + +using namespace mlir; + +using namespace mlir::triton; + +static constexpr const char *DEBUG_TYPE = "SeparateCVScope"; +#define DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") + +template static void logDebug(const Args &...args) { + LLVM_DEBUG({ + auto &debugStream = llvm::dbgs(); + debugStream << '[' << DEBUG_TYPE << "] "; + (debugStream << ... << args); + debugStream << "\n"; + }); +} + +static constexpr unsigned kForOpOperandPrefixCount = 3; +static constexpr unsigned kSeenValuesCapacity = 16; + +static void debugDumpOperation(StringRef prefix, Operation *op) { + LLVM_DEBUG({ + DBGS() << prefix << "\n"; + op->print(llvm::dbgs()); + llvm::dbgs() << "\n"; + }); +} + +static bool needsLoopCarryPreserve(Operation *owner, unsigned slotIndex, + StringRef scopeType); +static bool regionHasScopeContent(Region ®ion, StringRef scopeType); + +struct CoreTypeInfo { + SmallVector resultTypes; + + StringRef getResultType(size_t index) const { + if (index < resultTypes.size()) { + return resultTypes[index]; + } + return resultTypes.front(); + } + + bool hasResultForScope(StringRef scopeType) const { + for (StringRef t : resultTypes) { + if (t == scopeType) { + return true; + } + } + return false; + } + + bool allResultsMatchScope(StringRef scopeType, unsigned numResults) const { + if (numResults == 0) { + return getResultType(0) == scopeType; + } + for (unsigned i = 0; i < numResults; ++i) { + if (getResultType(i) != scopeType) { + return false; + } + } + return true; + } +}; + +static std::optional parseCoreTypeInfo(Operation *op) { + auto attr = op->getAttrOfType("ssbuffer.core_type"); + if (!attr) { + return std::nullopt; + } + + CoreTypeInfo info; + StringRef raw = attr.getValue(); + SmallVector parts; + raw.split(parts, ',', -1, false); + + if (parts.empty()) { + return std::nullopt; + } + + for (auto &part : parts) { + part = part.trim(); + } + + info.resultTypes.assign(parts.begin(), parts.end()); + return info; +} + +struct ScopeMatchInfo { + bool matches = false; + bool allResultsMatch = false; +}; + +static ScopeMatchInfo getScopeMatchInfo(Operation *op, StringRef scopeType) { + auto info = parseCoreTypeInfo(op); + if (!info) { + return {}; + } + + unsigned numResults = op->getNumResults(); + bool matches = numResults == 0 ? info->getResultType(0) == scopeType + : info->hasResultForScope(scopeType); + return {matches, info->allResultsMatchScope(scopeType, numResults)}; +} + +static bool matchesScope(Operation *op, StringRef scopeType) { + return getScopeMatchInfo(op, scopeType).matches; +} + +static Value buildNeutralValue(OpBuilder &builder, Value oldOperand, + Location loc, StringRef scopeType) { + if (!oldOperand) { + return Value(); + } + + Type type = oldOperand.getType(); + Operation *createdOp = nullptr; + + if (auto memrefTy = dyn_cast(type)) { + createdOp = builder.create(loc, memrefTy); + } else if (auto shapedTy = dyn_cast(type)) { + if (shapedTy.hasStaticShape() && !isa(type)) { + if (Attribute elemZero = builder.getZeroAttr(shapedTy.getElementType())) { + auto zeroDense = DenseElementsAttr::get(shapedTy, elemZero); + auto typedAttr = mlir::cast(zeroDense); + createdOp = builder.create(loc, type, typedAttr); + } + } + } else if (Attribute zeroAttr = builder.getZeroAttr(type)) { + if (auto typedZero = dyn_cast(zeroAttr)) { + createdOp = builder.create(loc, type, typedZero); + } + } + + return createdOp ? createdOp->getResult(0) : Value(); +} + +static FailureOr> +createTwoFullScopes(func::FuncOp funcOp) { + if (!llvm::hasSingleElement(funcOp.getBody())) { + logDebug("createTwoFullScopes failed for func '", funcOp.getName(), + "': expected single block"); + funcOp.emitError("SeparateCVScope only supports single-block functions"); + return failure(); + } + + Block &lastBlock = funcOp.getBody().back(); + SmallVector opsToMove; + for (Operation &op : lastBlock.without_terminator()) { + opsToMove.push_back(&op); + } + + if (opsToMove.empty()) { + return std::make_pair(scope::ScopeOp(), scope::ScopeOp()); + } + + Operation *lastOpToMove = opsToMove.back(); + OpBuilder builder(&lastBlock, ++lastOpToMove->getIterator()); + + auto vecScope = + builder.create(builder.getUnknownLoc(), ArrayRef{}); + vecScope.getBodyRegion().emplaceBlock(); + + Block *vecBlock = &vecScope.getBodyRegion().front(); + OpBuilder vecBuilder(vecBlock, vecBlock->end()); + + for (Operation *op : opsToMove) { + op->remove(); + vecBuilder.insert(op); + } + vecBuilder.create(builder.getUnknownLoc()); + + builder.setInsertionPointAfter(vecScope); + IRMapping mapping; + auto cloned = builder.clone(*vecScope.getOperation(), mapping); + auto cubeScope = cast(cloned); + + auto vecAttr = + hivm::TCoreTypeAttr::get(builder.getContext(), hivm::TCoreType::VECTOR); + auto cubeAttr = + hivm::TCoreTypeAttr::get(builder.getContext(), hivm::TCoreType::CUBE); + + vecScope->setAttr(hivm::TCoreTypeAttr::name, vecAttr); + cubeScope->setAttr(hivm::TCoreTypeAttr::name, cubeAttr); + + return std::make_pair(vecScope, cubeScope); +} + +static bool isPureForScope(Operation *op, StringRef scopeType) { + ScopeMatchInfo matchInfo = getScopeMatchInfo(op, scopeType); + if (!matchInfo.matches || !matchInfo.allResultsMatch) { + return false; + } + + for (Region ®ion : op->getRegions()) { + for (Block &block : region) { + for (Operation &nested : block) { + if (nested.hasTrait()) { + continue; + } + if (nested.getNumRegions() > 0) { + if (!isPureForScope(&nested, scopeType)) { + return false; + } + continue; + } + if (!getScopeMatchInfo(&nested, scopeType).matches) { + return false; + } + } + } + } + return true; +} + +enum class PendingActionKind { EraseDirectly, NormalizeControlFlow }; + +struct PendingAction { + Operation *op = nullptr; + PendingActionKind kind; +}; + +static bool isControlFlowOp(Operation *op) { + return isa(op); +} + +static void collectActionForOp(Operation *op, StringRef scopeType, + SmallVector &actions) { + ScopeMatchInfo matchInfo = getScopeMatchInfo(op, scopeType); + if (!matchInfo.matches) { + actions.push_back({op, PendingActionKind::EraseDirectly}); + } else if (!matchInfo.allResultsMatch) { + actions.push_back({op, PendingActionKind::NormalizeControlFlow}); + } +} + +static SmallVector collectActionsInRegion(Region ®ion, + StringRef scopeType) { + SmallVector actions; + for (Block &block : region) { + for (Operation &op : block) { + if (op.hasTrait()) + continue; + + if (op.getNumRegions() > 0) { + if (isControlFlowOp(&op)) { + if (!isPureForScope(&op, scopeType)) + actions.push_back({&op, PendingActionKind::NormalizeControlFlow}); + } else if (!matchesScope(&op, scopeType)) { + actions.push_back({&op, PendingActionKind::EraseDirectly}); + } + continue; + } + collectActionForOp(&op, scopeType, actions); + } + } + return actions; +} + +static bool hasLiveUsers(Operation *op) { + for (Value result : op->getResults()) { + for (OpOperand &use : result.getUses()) { + if (use.getOwner()) { + return true; + } + } + } + return false; +} + +// Check whether this use maps to a loop-carried slot that must stay live in the +// target scope. +static bool hasActiveNestedLoopCarryUse(OpOperand &use, StringRef scopeType) { + auto loopOp = dyn_cast(use.getOwner()); + if (!loopOp) { + return false; + } + + auto inits = loopOp.getInitsMutable(); + if (inits.empty()) { + return false; + } + + unsigned operandIndex = use.getOperandNumber(); + unsigned initsBegin = inits.front().getOperandNumber(); + unsigned initsEnd = initsBegin + inits.size(); + if (operandIndex < initsBegin || operandIndex >= initsEnd) { + return false; + } + + unsigned resultIndex = operandIndex - initsBegin; + return resultIndex < loopOp->getNumResults() && + needsLoopCarryPreserve(loopOp.getOperation(), resultIndex, scopeType); +} + +// Skip pure forwarding uses that only feed out-of-scope loop result slots. +static bool canSkipForwardingUse(OpOperand &use, StringRef scopeType) { + Operation *user = use.getOwner(); + auto info = parseCoreTypeInfo(user); + if (!info) { + return false; + } + + if (auto forOp = dyn_cast(user)) { + unsigned operandIndex = use.getOperandNumber(); + if (operandIndex < kForOpOperandPrefixCount) { + return false; + } + unsigned resultIndex = operandIndex - kForOpOperandPrefixCount; + return resultIndex < forOp.getNumResults() && + info->getResultType(resultIndex) != scopeType && + !needsLoopCarryPreserve(forOp, resultIndex, scopeType); + } + + if (auto whileOp = dyn_cast(user)) { + unsigned slotIdx = use.getOperandNumber(); + if (slotIdx >= whileOp.getNumResults()) { + return false; + } + if (info->getResultType(slotIdx) == scopeType) { + return false; + } + if (needsLoopCarryPreserve(whileOp, slotIdx, scopeType)) { + return false; + } + return llvm::none_of(whileOp->getUsers(), [&](Operation *user) { + return matchesScope(user, scopeType); + }); + } + return false; +} + +// Preserve yield-to-result tracking only when loop-carry evidence and +// mixed-scope sibling results require it. +static bool shouldPreserveFromYield(Operation *yieldOwner, + unsigned operandIndex, + StringRef scopeType) { + bool preserveLoopCarry = + needsLoopCarryPreserve(yieldOwner, operandIndex, scopeType); + if (!preserveLoopCarry) { + return false; + } + + auto loopOp = dyn_cast(yieldOwner); + if (!loopOp) { + return preserveLoopCarry; + } + + auto ownerInfo = parseCoreTypeInfo(yieldOwner); + if (!ownerInfo) { + return preserveLoopCarry; + } + + unsigned numResults = yieldOwner->getNumResults(); + return llvm::any_of( + llvm::seq(0, numResults), [&](unsigned resultIndex) { + return resultIndex != operandIndex && + ownerInfo->getResultType(resultIndex) == scopeType; + }); +} + +// Return `user` as a relevant result, honoring the ignoreTerminators flag. +static Operation *acceptUser(Operation *user, bool ignoreTerminators) { + if (!user || !user->getBlock()) { + return nullptr; + } + if (!ignoreTerminators || !user->hasTrait()) { + return user; + } + return nullptr; +} + +// Handle a scf.yield use: follow result slots transitively or mark the owner +// live. +static Operation *handleYieldUse(scf::YieldOp yieldOp, OpOperand &use, + StringRef scopeType, bool ignoreTerminators, + SmallVector &worklist) { + Operation *yieldOwner = yieldOp->getParentOp(); + unsigned operandIndex = use.getOperandNumber(); + + if (operandIndex < yieldOwner->getNumResults()) { + if (shouldPreserveFromYield(yieldOwner, operandIndex, scopeType)) { + return yieldOwner; + } + worklist.push_back(yieldOwner->getResult(operandIndex)); + return nullptr; + } + + if (matchesScope(yieldOwner, scopeType) || isControlFlowOp(yieldOwner)) { + return acceptUser(yieldOwner, ignoreTerminators); + } + return nullptr; +} + +// True when `op` holds scope content in any of its regions. +static bool controlFlowOpHasScopeContent(Operation *op, StringRef scopeType) { + for (Region ®ion : op->getRegions()) { + if (regionHasScopeContent(region, scopeType)) { + return true; + } + } + return false; +} + +// Returns whether a use is a control-flow gating operand (for/parallel bounds +// or if condition). +static bool isControlFlowGatingUse(OpOperand &use) { + Operation *user = use.getOwner(); + unsigned operandIndex = use.getOperandNumber(); + + if (isa(user)) { + return operandIndex < kForOpOperandPrefixCount; + } + if (auto parallelOp = dyn_cast(user)) { + return operandIndex < kForOpOperandPrefixCount * parallelOp.getNumLoops(); + } + if (isa(user)) { + return operandIndex == 0; + } + return false; +} + +// Classify a single use: return a scope-relevant user, or enqueue transitive +// values. +static Operation *classifyScopeRelevantUse(OpOperand &use, StringRef scopeType, + bool ignoreTerminators, + SmallVector &worklist) { + Operation *user = use.getOwner(); + if (!user || !user->getBlock() || canSkipForwardingUse(use, scopeType)) { + return nullptr; + } + + if (hasActiveNestedLoopCarryUse(use, scopeType)) { + return acceptUser(user, ignoreTerminators); + } + + if (auto conditionOp = dyn_cast(user)) { + Operation *parentOp = conditionOp.getParentOp(); + if (parentOp && matchesScope(parentOp, scopeType)) { + return acceptUser(parentOp, ignoreTerminators); + } + if (parentOp && use.getOperandNumber() == 0 && + controlFlowOpHasScopeContent(parentOp, scopeType)) { + return acceptUser(parentOp, ignoreTerminators); + } + return nullptr; + } + + if (auto yieldOp = dyn_cast(user)) { + return handleYieldUse(yieldOp, use, scopeType, ignoreTerminators, worklist); + } + + if (matchesScope(user, scopeType)) { + return acceptUser(user, ignoreTerminators); + } + + if (user->getNumResults() == 0) { + if (isControlFlowOp(user)) { + return acceptUser(user, ignoreTerminators); + } + return nullptr; + } + + if (isControlFlowGatingUse(use) && + controlFlowOpHasScopeContent(user, scopeType)) { + return acceptUser(user, ignoreTerminators); + } + + for (Value result : user->getResults()) { + worklist.push_back(result); + } + return nullptr; +} + +static Operation *findScopeRelevantUser(Value startValue, StringRef scopeType, + bool ignoreTerminators) { + SmallVector worklist{startValue}; + llvm::SmallPtrSet seenValues; + + while (!worklist.empty()) { + Value value = worklist.pop_back_val(); + if (!value || !seenValues.insert(value.getAsOpaquePointer()).second) { + continue; + } + + for (OpOperand &use : value.getUses()) { + if (Operation *found = classifyScopeRelevantUse( + use, scopeType, ignoreTerminators, worklist)) { + return found; + } + } + } + return nullptr; +} + +static Operation *findLiveUser(Value value, StringRef scopeType) { + return findScopeRelevantUser(value, scopeType, false); +} + +static Operation *findNonTermUser(Value value, StringRef scopeType) { + return findScopeRelevantUser(value, scopeType, true); +} + +enum class UseCheckResult { Skip, Active, Continue }; + +static UseCheckResult checkConditionUse(OpOperand &use, Operation *owner, + unsigned slotIndex, + StringRef scopeType) { + auto conditionOp = dyn_cast(use.getOwner()); + if (!conditionOp) { + return UseCheckResult::Skip; + } + + unsigned idx = use.getOperandNumber(); + if (conditionOp->getParentOp() != owner || idx == 0 || idx != slotIndex + 1) { + Operation *parentOp = conditionOp->getParentOp(); + if (parentOp && idx == 0 && + controlFlowOpHasScopeContent(parentOp, scopeType)) { + return UseCheckResult::Active; + } + if (parentOp && !matchesScope(parentOp, scopeType)) { + return UseCheckResult::Continue; + } + return UseCheckResult::Active; + } + return UseCheckResult::Continue; +} + +static UseCheckResult checkYieldUse(OpOperand &use, Operation *owner, + unsigned slotIndex, StringRef scopeType, + SmallVector &worklist) { + auto yieldOp = dyn_cast(use.getOwner()); + if (!yieldOp) { + return UseCheckResult::Skip; + } + + Operation *yieldOwner = yieldOp->getParentOp(); + unsigned idx = use.getOperandNumber(); + + if (yieldOwner == owner) { + if (idx != slotIndex && idx < yieldOwner->getNumResults()) { + worklist.push_back(yieldOwner->getResult(idx)); + } + return UseCheckResult::Continue; + } + + if (idx < yieldOwner->getNumResults()) { + worklist.push_back(yieldOwner->getResult(idx)); + return UseCheckResult::Continue; + } + + if (matchesScope(yieldOwner, scopeType) || isControlFlowOp(yieldOwner)) { + return UseCheckResult::Active; + } + return UseCheckResult::Continue; +} + +static UseCheckResult checkGeneralUse(OpOperand &use, StringRef scopeType, + SmallVector &worklist) { + Operation *user = use.getOwner(); + if (hasActiveNestedLoopCarryUse(use, scopeType)) { + return UseCheckResult::Active; + } + + if (user->getNumResults() == 0) { + if (matchesScope(user, scopeType) || isControlFlowOp(user)) { + return UseCheckResult::Active; + } + return UseCheckResult::Continue; + } + for (Value result : user->getResults()) { + worklist.push_back(result); + } + return UseCheckResult::Continue; +} + +static bool slotHasActiveUse(Value startValue, Operation *owner, + unsigned slotIndex, StringRef scopeType) { + SmallVector worklist{startValue}; + llvm::SmallPtrSet seenValues; + + while (!worklist.empty()) { + Value value = worklist.pop_back_val(); + if (!value || !seenValues.insert(value.getAsOpaquePointer()).second) { + continue; + } + + for (OpOperand &use : value.getUses()) { + if (!use.getOwner()) { + continue; + } + + UseCheckResult result; + if ((result = checkConditionUse(use, owner, slotIndex, scopeType)) != + UseCheckResult::Skip || + (result = checkYieldUse(use, owner, slotIndex, scopeType, + worklist)) != UseCheckResult::Skip || + (result = checkGeneralUse(use, scopeType, worklist)) != + UseCheckResult::Skip) { + if (result == UseCheckResult::Active) { + return true; + } + } + } + } + return false; +} + +static bool needsLoopCarryPreserve(Operation *owner, unsigned slotIndex, + StringRef scopeType) { + if (auto forOp = dyn_cast(owner)) { + Block &body = forOp.getRegion().front(); + unsigned iterArgIndex = slotIndex + 1; + if (iterArgIndex >= body.getNumArguments()) { + return false; + } + return slotHasActiveUse(body.getArgument(iterArgIndex), owner, slotIndex, + scopeType); + } + + if (auto whileOp = dyn_cast(owner)) { + Block &before = whileOp.getBefore().front(); + Block &after = whileOp.getAfter().front(); + if (slotIndex < before.getNumArguments() && + slotHasActiveUse(before.getArgument(slotIndex), owner, slotIndex, + scopeType)) { + return true; + } + if (slotIndex < after.getNumArguments() && + slotHasActiveUse(after.getArgument(slotIndex), owner, slotIndex, + scopeType)) { + return true; + } + } + return false; +} + +// Returns true when the producer op has no result belonging to scopeType . +static bool isProducedByForeignScope(Value operand, StringRef scopeType) { + Operation *producer = operand.getDefiningOp(); + if (!producer || !producer->hasAttr(CVPipeline::kCoreType)) { + return false; + } + + // Only treat as foreign when the producer has NO result in the current scope. + return !matchesScope(producer, scopeType); +} + +static LogicalResult +neutralizeCarriedTerminatorOperand(Operation *op, const CoreTypeInfo &info, + StringRef scopeType, Location loc, + unsigned slotIndex, OpOperand &operand) { + if (info.getResultType(slotIndex) == scopeType) { + return success(); + } + + // Preserve values read by an in-loop consumer through an iter_arg. + if (needsLoopCarryPreserve(op, slotIndex, scopeType)) { + return success(); + } + + Value oldOperand = operand.get(); + + // A foreign-scope producer must not be kept alive solely by the parent + // result-user check. + bool isLoopOp = isa(op); + if ((!isLoopOp || !isProducedByForeignScope(oldOperand, scopeType)) && + slotIndex < op->getNumResults()) { + if (Operation *resultUser = + findLiveUser(op->getResult(slotIndex), scopeType)) { + logDebug("skip neutralizing carried operand #", slotIndex, " for scope ", + scopeType, " because parent result #", slotIndex, + " still has live user '", resultUser->getName().getStringRef(), + "'"); + return success(); + } + } + + OpBuilder builder(operand.getOwner()); + Value replacement = buildNeutralValue(builder, oldOperand, loc, scopeType); + if (!replacement) { + logDebug("neutralizeRegionTerminators failed for op '", + op->getName().getStringRef(), "' at carried operand #", slotIndex, + " in scope ", scopeType); + return failure(); + } + operand.set(replacement); + return success(); +} + +static LogicalResult neutralizeRegionTerminators(Operation *op, + const CoreTypeInfo &info, + StringRef scopeType, + Location loc) { + if (op->getNumRegions() == 0) { + return success(); + } + + for (Region ®ion : op->getRegions()) { + if (region.empty()) { + continue; + } + + for (Block &block : region) { + Operation *terminator = block.getTerminator(); + unsigned carriedOperandOffset = 0; + if (isa(terminator)) { + // Operand 0 is the while condition; carried args start at operand 1. + carriedOperandOffset = 1; + } else if (!isa(terminator)) { + continue; + } + + unsigned numCarriedOperands = + terminator->getNumOperands() - carriedOperandOffset; + for (unsigned i = 0; i < numCarriedOperands; ++i) { + if (failed(neutralizeCarriedTerminatorOperand( + op, info, scopeType, loc, i, + terminator->getOpOperand(i + carriedOperandOffset)))) { + return failure(); + } + } + } + } + return success(); +} + +static LogicalResult neutralizeTerminatorUses(Operation *op, + const CoreTypeInfo &info, + StringRef scopeType) { + for (unsigned i = 0; i < op->getNumResults(); ++i) { + if (info.getResultType(i) == scopeType) { + continue; + } + + Value result = op->getResult(i); + if (Operation *extraUser = findNonTermUser(result, scopeType)) { + logDebug("skip neutralizing result #", i, " for scope ", scopeType, + " because the value still has live user '", + extraUser->getName().getStringRef(), "'"); + continue; + } + + SmallVector usesToNeutralize; + for (OpOperand &use : llvm::make_early_inc_range(result.getUses())) { + if (use.getOwner()->hasTrait()) { + usesToNeutralize.push_back(&use); + } + } + + for (OpOperand *use : usesToNeutralize) { + OpBuilder builder(use->getOwner()); + Value replacement = + buildNeutralValue(builder, result, op->getLoc(), scopeType); + if (!replacement) { + logDebug("neutralizeTerminatorUses failed for op '", + op->getName().getStringRef(), "' at result #", i, " in scope ", + scopeType); + return failure(); + } + use->set(replacement); + } + } + return success(); +} + +static LogicalResult executeActions(SmallVector &actions, + StringRef scopeType); + +static Operation *findLiveResultUser(Operation *op, StringRef scopeType) { + for (Value result : op->getResults()) { + if (Operation *user = findLiveUser(result, scopeType)) { + return user; + } + } + return nullptr; +} + +static LogicalResult normalizeRegionOp(Operation *op, StringRef scopeType) { + auto infoOpt = parseCoreTypeInfo(op); + if (!infoOpt) { + for (Region ®ion : op->getRegions()) { + SmallVector nestedActions = + collectActionsInRegion(region, scopeType); + if (failed(executeActions(nestedActions, scopeType))) { + return failure(); + } + } + return success(); + } + + auto info = *infoOpt; + Location loc = op->getLoc(); + + debugDumpOperation("before normalizeRegionOp", op); + + // Keep a loop intact when its body has no op for this scope but its results + // are still consumed here; neutralizing carried values would change them. + if (isa(op) && + !controlFlowOpHasScopeContent(op, scopeType)) { + if (Operation *resultUser = findLiveResultUser(op, scopeType)) { + logDebug("preserving complete loop '", op->getName().getStringRef(), + "' in scope ", scopeType, + " because its result still has live user '", + resultUser->getName().getStringRef(), "'"); + return success(); + } + } + + if (op->getNumRegions() > 0) { + if (failed(neutralizeRegionTerminators(op, info, scopeType, loc))) { + logDebug( + "normalizeRegionOp failed while neutralizing terminators for op '", + op->getName().getStringRef(), "' in scope ", scopeType); + return failure(); + } + + for (Region ®ion : op->getRegions()) { + SmallVector nestedActions = + collectActionsInRegion(region, scopeType); + if (failed(executeActions(nestedActions, scopeType))) { + logDebug( + "normalizeRegionOp failed while executing nested actions for op '", + op->getName().getStringRef(), "' in scope ", scopeType); + return failure(); + } + } + } + + debugDumpOperation("after normalizeRegionOp", op); + return success(); +} + +static LogicalResult normalizeNonRegionOp(Operation *op, StringRef scopeType) { + auto infoOpt = parseCoreTypeInfo(op); + if (!infoOpt) { + logDebug("normalizeNonRegionOp failed for op '", + op->getName().getStringRef(), + "': missing ssbuffer.core_type in scope ", scopeType); + op->emitError("missing ssbuffer.core_type"); + return failure(); + } + + debugDumpOperation("before normalizeNonRegionOp", op); + + LogicalResult result = neutralizeTerminatorUses(op, *infoOpt, scopeType); + if (succeeded(result)) { + debugDumpOperation("after normalizeNonRegionOp", op); + } + return result; +} + +static bool regionHasScopeContent(Region ®ion, StringRef scopeType) { + for (Block &block : region) { + for (Operation &op : block) { + if (op.hasTrait()) { + continue; + } + + if (op.getNumRegions() > 0) { + if (!isControlFlowOp(&op) && matchesScope(&op, scopeType)) { + return true; + } + + for (Region &subRegion : op.getRegions()) { + if (regionHasScopeContent(subRegion, scopeType)) { + return true; + } + } + continue; + } + + if (matchesScope(&op, scopeType)) { + return true; + } + } + } + return false; +} + +static bool isNormalizedDeadShell(Operation *op, StringRef scopeType) { + if (!op || op->getNumRegions() == 0) { + return false; + } + + for (Value result : op->getResults()) { + if (findLiveUser(result, scopeType)) { + return false; + } + } + + for (Region ®ion : op->getRegions()) { + if (regionHasScopeContent(region, scopeType)) { + return false; + } + } + return true; +} + +static LogicalResult executeActions(SmallVector &actions, + StringRef scopeType) { + for (auto it = actions.rbegin(); it != actions.rend(); ++it) { + Operation *op = it->op; + if (!op) { + continue; + } + + switch (it->kind) { + case PendingActionKind::EraseDirectly: + if (!hasLiveUsers(op)) { + op->erase(); + } + break; + case PendingActionKind::NormalizeControlFlow: + if (op->getNumRegions() > 0) { + if (failed(normalizeRegionOp(op, scopeType))) { + logDebug("failed to normalize region op '", + op->getName().getStringRef(), "' for scope ", scopeType); + return failure(); + } + } else { + if (failed(normalizeNonRegionOp(op, scopeType))) { + logDebug("failed to normalize non-region op '", + op->getName().getStringRef(), "' for scope ", scopeType); + return failure(); + } + } + if (op && op->getNumRegions() > 0 && + isNormalizedDeadShell(op, scopeType)) { + if (hasLiveUsers(op)) { + logDebug("preserving normalized shell '", + op->getName().getStringRef(), "' in scope ", scopeType, + " because it still has structural users"); + } else { + logDebug("erasing dead shell after normalize: '", + op->getName().getStringRef(), "' in scope ", scopeType); + op->erase(); + } + } + break; + } + } + return success(); +} + +static void removeSsbufferAttrs(Operation *op) { + op->removeAttr("ssbuffer.core_type"); +} + +static void cleanupSsbufferAttrs(Operation *rootOp) { + removeSsbufferAttrs(rootOp); + rootOp->walk([](Operation *op) { removeSsbufferAttrs(op); }); +} + +static LogicalResult separateScopes(func::FuncOp funcOp) { + debugDumpOperation("before SeparateCVScope on func", funcOp.getOperation()); + + FailureOr> scopes = + createTwoFullScopes(funcOp); + if (failed(scopes)) { + logDebug("SeparateCVScope failed to create VECTOR/CUBE scopes for func '", + funcOp.getName(), "'"); + return failure(); + } + + auto vecScope = scopes->first; + auto cubeScope = scopes->second; + + auto vecActions = collectActionsInRegion(vecScope.getRegion(), "VECTOR"); + auto cubeActions = collectActionsInRegion(cubeScope.getRegion(), "CUBE"); + if (failed(executeActions(vecActions, "VECTOR")) || + failed(executeActions(cubeActions, "CUBE"))) { + logDebug("SeparateCVScope failed while executing actions for func '", + funcOp.getName(), "'"); + return failure(); + } + + cleanupSsbufferAttrs(funcOp); + + debugDumpOperation("after SeparateCVScope on func", funcOp.getOperation()); + return success(); +} + +// Mechanism A: in VECTOR scopes, replace each redundant store→load pair that +// shares an ssbuffer.transfer_id with the stored value, then erase the load. +// +// The scope-clone step duplicates the CUBE-side scalar load into the VECTOR +// scope too; there it reads back a value this scope itself just stored, so the +// load is pure redundancy (not dead code — it has users), and DCE will not +// remove it. +static void replaceRedundantVectorStoreLoad(scope::ScopeOp scopeOp) { + auto coreTypeAttr = + scopeOp->getAttrOfType(hivm::TCoreTypeAttr::name); + if (!coreTypeAttr || coreTypeAttr.getTcoretype() != hivm::TCoreType::VECTOR) { + return; + } + + // Pass 1: collect the value each ssbuffer.transfer_id stores. + llvm::DenseMap storedValues; + scopeOp.walk([&](memref::StoreOp storeOp) { + auto transferIdAttr = + storeOp->getAttrOfType(CVPipeline::kTransferId); + if (!transferIdAttr) { + return; + } + int64_t tid = transferIdAttr.getInt(); + storedValues[tid] = storeOp.getValue(); + }); + + if (storedValues.empty()) { + return; + } + + // Pass 2: replace loads of the same transfer_id with the stored value. + llvm::SmallVector deadLoads; + scopeOp.walk([&](memref::LoadOp loadOp) { + auto transferIdAttr = + loadOp->getAttrOfType(CVPipeline::kTransferId); + if (!transferIdAttr) { + return; + } + int64_t tid = transferIdAttr.getInt(); + auto it = storedValues.find(tid); + if (it == storedValues.end()) { + return; + } + mlir::Value storeVal = it->second; + if (storeVal == loadOp.getResult()) { + return; + } + // Drop the volatile annotation the scalar read attached to this load; + // it only makes sense while the load survives in the CUBE scope. + for (Operation *user : + llvm::make_early_inc_range(loadOp.getResult().getUsers())) { + if (isa(user) && user->hasAttr(kMemrefExtVolatile)) { + user->erase(); + } + } + loadOp.replaceAllUsesWith(storeVal); + deadLoads.push_back(loadOp); + }); + for (memref::LoadOp loadOp : deadLoads) { + loadOp->erase(); + } +} + +// Declare dependent dialects +void mlir::triton::SeparateCVScopePass::getDependentDialects( + DialectRegistry ®istry) const { + registry + .insert(); +} + +void mlir::triton::SeparateCVScopePass::runOnOperation() { + auto module = getOperation(); + + if (CVPipeline::hasFallbackAttr(module)) { + return; + } + + SmallVector funcOps; + module.walk([&](func::FuncOp funcOp) { funcOps.push_back(funcOp); }); + + debugDumpOperation("before SeparateCVScopePass", module.getOperation()); + + for (auto funcOp : funcOps) { + if (funcOp.getBody().empty()) { + continue; + } + if (failed(separateScopes(funcOp))) { + logDebug("SeparateCVScopePass failed on func '", funcOp.getName(), "'"); + CVPipeline::setFallbackAttr(module, CVPipeline::ERRCODE_FAILED); + return; + } + } + + module.walk([](scope::ScopeOp scopeOp) { + scopeOp->setAttr(CVPipeline::kHIVMMatmulLimitedInCubeAttr, + UnitAttr::get(scopeOp->getContext())); + }); + + // Mechanism A: clean up redundant VECTOR-side store→load pairs. + module.walk( + [](scope::ScopeOp scopeOp) { replaceRedundantVectorStoreLoad(scopeOp); }); + + debugDumpOperation("after SeparateCVScopePass", module.getOperation()); +} + +namespace mlir { +namespace triton { + +std::unique_ptr> createSeparateCVScopePass() { + return std::make_unique(); +} + +void registerSeparateCVScopePasses() { + registerPass([]() -> std::unique_ptr { + return createSeparateCVScopePass(); + }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/Utils.cpp b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/Utils.cpp new file mode 100644 index 0000000000..9d20d8ecaa --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/SplitDataflow/Utils.cpp @@ -0,0 +1,57 @@ + +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/DynamicCVPipeline/SplitDataflow/Utils.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" + +#include "mlir/IR/Operation.h" +#include "llvm/ADT/StringRef.h" + +using namespace mlir; +using namespace llvm; + +void triton::setOpBlockId(mlir::Operation *op, int blockId) { + static constexpr int kIntegerBitWidth = 32; + op->setAttr( + CVPipeline::kBlockId, + IntegerAttr::get(IntegerType::get(op->getContext(), kIntegerBitWidth), + blockId)); +} + +void triton::setOpCoreType(mlir::Operation *op, llvm::StringRef coreType) { + op->setAttr(CVPipeline::kCoreType, + StringAttr::get(op->getContext(), coreType)); +} + +bool triton::isScalarDependency(mlir::Value depValue) { + return isa(depValue.getType()); +} + +bool triton::is1DTensorDependency(mlir::Value depValue) { + static constexpr int SHAPE_1D_LENGTH = 1; + auto tensorTy = dyn_cast(depValue.getType()); + if (tensorTy && tensorTy.getRank() == SHAPE_1D_LENGTH) { + return true; + } + return false; +} diff --git a/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp.cpp b/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp.cpp new file mode 100644 index 0000000000..6f18f1f79c --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" +#include + +#include "mlir/Pass/PassManager.h" +#include "mlir/Pass/PassRegistry.h" + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/StandardizeOp.h" +#include "ascend/include/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.h" + +using namespace mlir; +using namespace triton; +using namespace CVSplit; + +static constexpr const char *DEBUG_TYPE = "StandardizeOp"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << "\n[" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +namespace mlir::triton { + +void StandardizeOpPass::runOnOperation() { + auto op = getOperation(); + + if (CVPipeline::hasFallbackAttr(op)) { + return; + } + + LOG_DEBUG("Input mlir:\n" << op); + OpPassManager pm(op.getOperationName()); + pm.addPass(createPatternMatchRewritePass()); + + if (llvm::failed(runPipeline(pm, op))) { + LOG_DEBUG("Pipeline Failed!"); + CVPipeline::setFallbackAttr(op, CVPipeline::ERRCODE_FAILED); + return; + } + + bool findMayNotExec = false; + op->walk([&](linalg::MatmulOp matmulOp) { + if (matmulOp->hasAttr(CVPipeline::kMayNotExec)) { + findMayNotExec = true; + } + }); + + if (findMayNotExec) { + LOG_DEBUG("Matmul may not execute!"); + CVPipeline::setFallbackAttr(op, CVPipeline::ERRCODE_IGNORED); + return; + } +} + +std::unique_ptr> createStandardizeOpPass() { + return std::make_unique(); +}; + +void registerStandardizeOpPasses() { + registerPass(createStandardizeOpPass); + registerPass(createPatternMatchRewritePass); +} + +} // namespace mlir::triton diff --git a/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/FoldExpandExtCollapse.cpp b/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/FoldExpandExtCollapse.cpp new file mode 100644 index 0000000000..07b710ffa1 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/FoldExpandExtCollapse.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/PatternMatch.h" + +#include "ascend/include/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.h" + +using namespace mlir; +using namespace triton; +using namespace CVSplit; + +static constexpr const char *DEBUG_TYPE = "FoldExpandExtCollapse"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << "\n[" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +namespace mlir::triton::CVSplit { + +LogicalResult +FoldExpandExtCollapse::matchAndRewrite(tensor::CollapseShapeOp collapseOp, + PatternRewriter &rewriter) const { + auto collapseReassoc = collapseOp.getReassociationIndices(); + + if (collapseReassoc.size() != 1 || collapseReassoc[0].size() != 2 || + collapseReassoc[0][0] != 0 || collapseReassoc[0][1] != 1) { + return failure(); + } + + auto extOp = collapseOp.getSrc().getDefiningOp(); + if (!extOp) { + return failure(); + } + + auto extUIOp = dyn_cast(extOp); + auto extSIOp = dyn_cast(extOp); + if (!extUIOp && !extSIOp) { + return failure(); + } + + if (!extOp->hasOneUse()) { + return failure(); + } + + auto expandOp = extOp->getOperand(0).getDefiningOp(); + if (!expandOp) { + return failure(); + } + + if (!expandOp->hasOneUse()) { + return failure(); + } + + auto expandReassoc = expandOp.getReassociationIndices(); + if (expandReassoc.size() != 1 || expandReassoc[0].size() != 2 || + expandReassoc[0][0] != 0 || expandReassoc[0][1] != 1) { + return failure(); + } + + Value extInput = expandOp.getSrc(); + Type resultType = collapseOp.getResult().getType(); + + if (extUIOp) { + auto newExtUI = rewriter.create(collapseOp.getLoc(), + resultType, extInput); + rewriter.replaceOp(collapseOp, newExtUI.getResult()); + } else { + auto newExtSI = rewriter.create(collapseOp.getLoc(), + resultType, extInput); + rewriter.replaceOp(collapseOp, newExtSI.getResult()); + } + + return success(); +} + +} // namespace mlir::triton::CVSplit diff --git a/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.cpp b/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.cpp new file mode 100644 index 0000000000..0bf69d26b2 --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "llvm/Support/Debug.h" + +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" + +#include "ascend/include/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.h" + +#include "DynamicCVPipeline/Common/Utils.h" + +using namespace mlir; +using namespace triton; +using namespace CVSplit; + +static constexpr const char *DEBUG_TYPE = "PatternMatchRewrites"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << "\n[" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +static constexpr llvm::StringLiteral needSplitAllFuncNme[]{""}; + +void PatternMatchRewritePass::runOnOperation() { + auto moduleOp = getOperation(); + + if (CVPipeline::hasFallbackAttr(moduleOp)) { + return; + } + + LOG_DEBUG("Input mlir:\n" << moduleOp); + + bool needSplitAll = false; + moduleOp.walk([&](func::FuncOp funcOp) -> WalkResult { + if (!llvm::is_contained(needSplitAllFuncNme, funcOp.getSymName())) { + return WalkResult::advance(); + } + LOG_DEBUG("[INFO] Matmul should split anyway: " << funcOp.getSymName()); + needSplitAll = true; + return WalkResult::interrupt(); + }); + + auto *ctx = &getContext(); + RewritePatternSet patterns(ctx); + patterns.add(ctx, needSplitAll); + patterns.add(ctx); + + // the way we handle matmul dependencies across for blocks + // requres the patternmatching to go top-down + GreedyRewriteConfig config = GreedyRewriteConfig().setUseTopDownTraversal(); + if (llvm::failed( + applyPatternsGreedily(moduleOp, std::move(patterns), config))) { + LOG_DEBUG("matchAndRewrite does not converge!"); + CVPipeline::setFallbackAttr(moduleOp, CVPipeline::ERRCODE_FAILED); + return; + } + LOG_DEBUG("Output mlir:\n" << moduleOp); +} + +namespace mlir::triton::CVSplit { + +std::unique_ptr> createPatternMatchRewritePass() { + return std::make_unique(); +} + +} // namespace mlir::triton::CVSplit diff --git a/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/SplitMatmulPattern.cpp b/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/SplitMatmulPattern.cpp new file mode 100644 index 0000000000..3f637551bd --- /dev/null +++ b/third_party/ascend/lib/DynamicCVPipeline/StandardizeOp/SplitMatmulPattern.cpp @@ -0,0 +1,1003 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/IR/Verifier.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" + +#include "bishengir/Dialect/Utils/Util.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Dominance.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/LoopLikeInterface.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "mlir/Support/LLVM.h" + +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/StandardizeOp/PatternMatchRewrites.h" + +#include "DynamicCVPipeline/PlanComputeBlock/Common.h" +#include "bishengir/Dialect/HIVM/IR/HIVMImpl.h" + +using namespace llvm; +using namespace mlir; +using namespace triton; +using namespace CVSplit; + +static constexpr const char *DEBUG_TYPE = "SplitMatmul"; +#define LOG_DEBUG(...) \ + LLVM_DEBUG(llvm::dbgs() << "\n[" << DEBUG_TYPE << "] " << __VA_ARGS__ << "\n") + +constexpr llvm::StringLiteral kMatmulAtLeastOnceHint = "matmul_at_least_once"; + +namespace { + +struct MatmulInputs { + Value a; + Value b; + Value bias; +}; + +struct SplitInfo { + bool mayNotExec; + Value outerInValue; + Value outerOutValue; + bool shouldSplit; + bool supported = false; +}; + +} // namespace + +static inline MatmulInputs parseMatmulInputs(linalg::MatmulOp matmulOp) { + auto inits = matmulOp.getDpsInits(); + auto inputs = matmulOp.getDpsInputs(); + + return {inputs[0], inputs[1], inits[0]}; +} + +// The user is responsible for checking biasDefOp is not null. +static bool operationIsFillZero(Operation *op) { + auto fillOp = dyn_cast(op); + if (!fillOp) { + return false; + } + auto filledVal = fillOp.getInputs()[0]; + return matchPattern(filledVal, m_Zero()) || + matchPattern(filledVal, m_AnyZeroFloat()); +} + +// This generally should always be true, but just for safety... +static bool isFloatOrInt(RankedTensorType tensorType) { + auto elmType = tensorType.getElementType(); + return isa(elmType); +} + +static bool scfMayNotExec(Operation *op) { + return llvm::TypeSwitch(op) + .Case([&](scf::ForOp forOp) { + IntegerAttr ubAttr; + IntegerAttr lbAttr; + if (matchPattern(forOp.getUpperBound(), m_Constant(&ubAttr)) && + matchPattern(forOp.getLowerBound(), m_Constant(&lbAttr))) { + return ubAttr.getValue().sle(lbAttr.getValue()); + } + return true; + }) + .Case([&](scf::IfOp ifOp) { + return !matchPattern(ifOp.getCondition(), m_One()); + }) + .Default([&](auto) { return false; }); +} + +/** + * Search outward from args to obtain the following information: + * 1. Whether args is only used and updated by matmul: bool argsLimitedInMatmul + * 2. Whether matmul is guaranteed to execute: bool mayNotExec + * 3. Outermost initial value: Value outerInValue + * 4. Outermost for or if, i.e., where to insert if/else. + */ +static Value searchInArgsChain(Value nextValueOfC, bool &argsLimitedInMatmul, + bool &mayNotExec, Value &outerInValue) { + auto op = nextValueOfC.getDefiningOp(); + auto parentOp = op->getParentOp(); + if (outerInValue.getDefiningOp()) { + return nextValueOfC; + } + + // update argsLimitedInMatmul + Value nextSearchValue = nextValueOfC; + if (auto forOp = dyn_cast(parentOp)) { + auto blockArg = dyn_cast_if_present(outerInValue); + if (!blockArg || blockArg.getOwner() != forOp.getBody()) { + argsLimitedInMatmul = false; + return nextValueOfC; + } + int argIdx = blockArg.getArgNumber() - 1; + for (auto &use : blockArg.getUses()) { + auto user = use.getOwner(); + // Allowed: the op itself (mmad or inner for/if that chains to mmad). + auto userInBlock = CVPipeline::getAncestorInBlock(user, op->getBlock()); + if (userInBlock == op) { + continue; + } + if (auto yieldOp = dyn_cast(userInBlock)) { + argsLimitedInMatmul = (use.getOperandNumber() == argIdx); + } else { + argsLimitedInMatmul = false; + break; + } + } + // check res is only used by yield + for (auto &use : nextValueOfC.getUses()) { + auto user = use.getOwner(); + auto userInBlock = CVPipeline::getAncestorInBlock(user, op->getBlock()); + auto yieldOp = dyn_cast(userInBlock); + if (!yieldOp) { + argsLimitedInMatmul = false; + break; + } else if (yieldOp->getBlock() != forOp.getBody()) { + argsLimitedInMatmul = false; + break; + } else if (use.getOperandNumber() != argIdx) { + argsLimitedInMatmul = false; + break; + } + } + + if (argsLimitedInMatmul) { + outerInValue = forOp.getInitArgs()[argIdx]; + nextSearchValue = forOp->getResult(argIdx); + } + } else if (auto ifOp = dyn_cast(parentOp)) { + if (!ifOp.elseBlock()) { + argsLimitedInMatmul = false; + } + auto otherYieldOp = + op->getBlock() == ifOp.thenBlock() + ? cast(ifOp.elseBlock()->getTerminator()) + : cast(ifOp.thenBlock()->getTerminator()); + auto opYieldOp = + op->getBlock() == ifOp.thenBlock() + ? cast(ifOp.thenBlock()->getTerminator()) + : cast(ifOp.elseBlock()->getTerminator()); + int resultIdx = -1; + for (unsigned i = 0; i < otherYieldOp->getNumOperands(); ++i) { + if (otherYieldOp->getOperand(i) == outerInValue && + opYieldOp->getOperand(i) == nextSearchValue) { + resultIdx = i; + break; + } + } + if (resultIdx == -1) { + argsLimitedInMatmul = false; + } else { + nextSearchValue = ifOp.getResult(resultIdx); + } + } else { + argsLimitedInMatmul = false; + LOG_DEBUG("WARN: no for/if out to matmul."); + } + + if (!argsLimitedInMatmul) { + return nextValueOfC; + } + // update mayNotExec + mayNotExec = mayNotExec || scfMayNotExec(parentOp); + return searchInArgsChain(nextSearchValue, argsLimitedInMatmul, mayNotExec, + outerInValue); +} + +/** + * Traces the chain of users of a given value to find a matching operation. + * This function recursively follows the use-def chain through various operation + * types (ViewLikeOpInterface, scf::ForOp, scf::YieldOp) to locate an operation + * that matches the specified criteria. + * + * Parameters: + * value - The starting value whose users are to be traced. + * needSingle - If true, requires that the value has exactly one user in its + * parent block. If multiple users exist, returns std::nullopt. isMatchedOp - A + * predicate function that determines if an operation is the target match. + * Returns true if the operation matches the desired criteria. + * isSkipOp - A predicate function that determines if an operation should + * be skipped. If true and the operation has a single result, continues tracing + * through that result. + * + * Returns: + * The matched operation if found, or std::nullopt if no match is found or if + * the single-user requirement is violated. + */ +std::optional +traceChainUser(Value value, bool needSingle, + const std::function &isMatchedOp, + const std::function &isSkipOp) { + Operation *nextUser = nullptr; + for (auto &use : value.getUses()) { + Operation *user = use.getOwner(); + auto userInblock = + CVPipeline::getAncestorInBlock(user, value.getParentBlock()); + if (nextUser && userInblock != nextUser && needSingle) { + return std::nullopt; // not single user + } else if (!nextUser && userInblock) { + nextUser = userInblock; + } + } + + for (auto &use : value.getUses()) { + Operation *user = use.getOwner(); + if (llvm::isa(user)) { + return traceChainUser(user->getResult(0), needSingle, isMatchedOp, + isSkipOp); + } + + if (auto forOp = dyn_cast(user)) { + auto initArgs = forOp.getInitArgs(); + int initIndx = -1; + int useCnt = 0; + for (auto [i, arg] : llvm::enumerate(initArgs)) { + if (arg == value) { + initIndx = i; + useCnt++; + } + } + if (useCnt == 1) { + return traceChainUser(forOp.getRegionIterArgs()[initIndx], needSingle, + isMatchedOp, isSkipOp); + } + return std::nullopt; + } + + if (auto yieldOp = dyn_cast(user)) { + auto operands = yieldOp.getOperands(); + auto it = llvm::find(operands, value); + if (it == operands.end()) { + return std::nullopt; + } + auto index = std::distance(operands.begin(), it); + + Operation *parentOp = yieldOp->getParentOp(); + if (!parentOp || + static_cast(index) >= parentOp->getNumResults()) { + return std::nullopt; + } + + return traceChainUser(parentOp->getResult(index), needSingle, isMatchedOp, + isSkipOp); + } + + if (isMatchedOp(user, value)) { + return user; + } + if (isSkipOp(user, value)) { + if (user->getNumResults() == 1) { + return traceChainUser(user->getResult(0), needSingle, isMatchedOp, + isSkipOp); + } + return std::nullopt; + } + } + return std::nullopt; +} + +static bool hasCrossBlockCascadeL0CConsumer(Value result, + linalg::MatmulOp matmulOp) { + auto matchMatmulC = [](Operation *op, Value value) { + if (auto nextMatmulOp = dyn_cast(op)) { + auto inputs = parseMatmulInputs(nextMatmulOp); + return inputs.a != value && inputs.b != value && inputs.bias == value; + } + return false; + }; + auto usedByL0C = + traceChainUser(result, true, matchMatmulC, + [](Operation *op, Value value) { return false; }); + + // No cascade consumer, or the cascade consumer is in the same block as + // `result` (i.e. inside the current for-loop body). + if (!usedByL0C.has_value() || + usedByL0C.value()->getBlock() == result.getParentBlock()) { + return false; + } + + // Only support the cascade for in the same block + auto cascadeMatmul = llvm::dyn_cast(usedByL0C.value()); + auto cascadeForOp = cascadeMatmul + ? llvm::dyn_cast_if_present( + cascadeMatmul->getBlock()->getParentOp()) + : nullptr; + auto currentForOp = llvm::dyn_cast_if_present( + matmulOp->getBlock()->getParentOp()); + if (cascadeForOp && currentForOp && + cascadeForOp->getBlock() != currentForOp->getBlock()) { + return false; + } + currentForOp->setAttr(CVPipeline::kMayNotExecNPU, + BoolAttr::get(matmulOp.getContext(), true)); + return true; +} + +/** + * Checks whether `initVal` is defined by a linalg::MatmulOp (L0C -> L0C + * cascade producer) that lives in a different block than `matmulOp`'s + * for-loop. Such a cascade scenario should be split. + */ +static bool hasCrossBlockCascadeL0CProducer(Value initVal, + linalg::MatmulOp matmulOp) { + auto defMatmul = dyn_cast_if_present( + hivm::traceDefOp(initVal).value_or(nullptr)); + if (!defMatmul) { + return false; + } + // Cascade producer must be outside `matmulOp`'s block to be considered + // a cascade scenario at all. + auto defInMatmulBlock = + CVPipeline::getAncestorInBlock(defMatmul, matmulOp->getBlock()); + if (defInMatmulBlock) { + return false; + } + // Only support the cascade for in the same block + auto cascadeForOp = llvm::dyn_cast_if_present( + defMatmul->getBlock()->getParentOp()); + auto currentForOp = llvm::dyn_cast_if_present( + matmulOp->getBlock()->getParentOp()); + if (cascadeForOp && currentForOp && + cascadeForOp->getBlock() != currentForOp->getBlock()) { + return false; + } + return true; +} + +static bool isSubviewFromGlobalMemory(ViewLikeOpInterface viewOp) { + // Subview ops may be nested many layers deep through reinterpretation or + // other subviews. like, subview (subview (reinterpret_cast (subview + // (reinterpret_cast (arg0))))) so we need Search and only keep same block + // view-like op. + if (!viewOp) { + return false; + } + Value source = viewOp.getViewSource(); + while (true) { + LOG_DEBUG("Check view source: " << source << "\n"); + if (auto blockArg = dyn_cast(source)) { + Operation *parentOp = blockArg.getOwner()->getParentOp(); + if (isa(parentOp)) { + return true; + } else if (isa(parentOp)) { + auto argIndex = blockArg.getArgNumber(); + if (auto forOp = dyn_cast(parentOp)) { + if (argIndex > 0) { + source = forOp.getInitArgs()[argIndex - 1]; + } + } else if (auto whileOp = dyn_cast(parentOp)) { + source = whileOp.getInits()[argIndex]; + } else { + LOG_DEBUG("Subview source block argument is from unknow block."); + return false; + } + } + } + // From other view-like op + if (auto viewLike = dyn_cast(source.getDefiningOp())) { + source = viewLike.getViewSource(); + continue; + } + LOG_DEBUG( + "Subview source defining op is not ViewLikeOpInterface: " << source); + return false; + } + return false; +} + +/** + * Determines whether a matmul operation should be split based on its output + * usage. A split is needed when the matmul output is used in ways that would + * cause hardware issues: + * 1. (Specific scenarios Filter) Output is used by a nested matmul (L0C -> L0C) + * which causes NPUIR fixpipe errors. + * 2. Output is used as input A or B of another matmul (L0C -> L1), which + * requires materialization to global memory. + * + * Parameters: + * matmulOp - The matmul operation to check. + * outerOutValue - The outermost result value in the use-def chain. + * outerInValue - The outermost initial value (bias) in the use-def chain. + * + * Returns: + * true if the matmul should be split due to output usage, false otherwise. + */ +static bool isOutputFilter(linalg::MatmulOp matmulOp, Value &outerOutValue, + Value &outerInValue) { + // Specific scenario: used by store and nextUser is nested + auto matchStoreGm = [](Operation *op, Value value) { + if (auto nextStoreGM = + dyn_cast(op)) { + Value dest = nextStoreGM.getDest(); + auto viewOp = dest.getDefiningOp(); + return isSubviewFromGlobalMemory(viewOp); + } else if (auto hivmStore = dyn_cast(op)) { + auto dest = hivmStore.getDst(); + auto viewOp = dest.getDefiningOp(); + return isSubviewFromGlobalMemory(viewOp); + } + return false; + }; + auto storeToGM = + traceChainUser(outerOutValue, false, matchStoreGm, + [](Operation *op, Value value) { return false; }); + if (storeToGM.has_value() && + storeToGM.value()->getBlock() != outerOutValue.getParentBlock()) { + LOG_DEBUG("(store) Split because avoiding NPUIR insert fixpipe errors. " + << matmulOp); + return true; + } + + // Specific scenario: used by single L0C and nextUser is nested + auto matchMatmulC = [](Operation *op, Value value) { + if (auto nextMatmulOp = dyn_cast(op)) { + auto inputs = parseMatmulInputs(nextMatmulOp); + return inputs.a != value && inputs.b != value && inputs.bias == value; + } + return false; + }; + auto usedByL0C = + traceChainUser(outerOutValue, true, matchMatmulC, + [](Operation *op, Value value) { return false; }); + if (usedByL0C.has_value() && + usedByL0C.value()->getBlock() != outerOutValue.getParentBlock()) { + LOG_DEBUG("(first) Split because avoiding NPUIR insert fixpipe errors. " + << matmulOp); + return true; + } + return false; +} +static bool shouldSplitByOutput(linalg::MatmulOp matmulOp, Value &outerOutValue, + Value &outerInValue) { + if (isOutputFilter(matmulOp, outerOutValue, outerInValue)) { + return true; + } + // used by any L1 + auto matchMatmulAB = [](Operation *op, Value value) { + if (auto nextMatmulOp = dyn_cast(op)) { + auto inputs = parseMatmulInputs(nextMatmulOp); + return inputs.a == value || inputs.b == value; + } + return false; + }; + auto skipCubeop = [=](Operation *op, Value value) { + return isa(op); + }; + auto usedByL1 = + traceChainUser(outerOutValue, false, matchMatmulAB, skipCubeop); + if (usedByL1.has_value() && + usedByL1.value()->getBlock() != outerOutValue.getParentBlock()) { + LOG_DEBUG("Split avoid L0C -> L1. " << matmulOp); // S01-S08 + return true; + } + + return false; +} + +/** + * Determines whether a matmul operation should be split based on its input + * (bias) source. The split is avoided when: + * 1. The bias is filled with zeros (no need to split). + * 2. The bias comes from another matmul output (L0C remaining in the chain). + * + * Parameters: + * matmulOp - The matmul operation to check. + * outerOutValue - The outermost result value in the use-def chain. + * outerInValue - The outermost initial value (bias) in the use-def chain. + * + * Returns: + * true if the matmul should be split due to input source, false otherwise. + */ +static bool isInputFilter(linalg::MatmulOp matmulOp, Value &outerOutValue, + Value &outerInValue) { + // Specific scenario: outerInValue from L0C and nextUser is nested + auto defMatmul = dyn_cast_if_present( + hivm::traceDefOp(outerInValue).value_or(nullptr)); + if (defMatmul) { + auto defInMatmulBlock = + CVPipeline::getAncestorInBlock(defMatmul, matmulOp->getBlock()); + if (!defInMatmulBlock) { + LOG_DEBUG("(Second) Split because avoiding NPUIR insert fixpipe errors. " + << matmulOp); + return true; + } + } + return false; +} +static bool shouldSplitByInput(linalg::MatmulOp matmulOp, Value &outerOutValue, + Value &outerInValue) { + if (isInputFilter(matmulOp, outerOutValue, outerInValue)) { + return true; + } + auto outerInOp = outerInValue.getDefiningOp(); + if (!outerInOp) { + return true; + } + + if (operationIsFillZero(outerInValue.getDefiningOp())) { + LOG_DEBUG("Not split because bias is zero. " << matmulOp); + return false; + } + + auto broadcastOp = dyn_cast(outerInOp); + if (broadcastOp) { + if (auto btUsage = CVPipeline::getBTSizeFromValidBroadcastOp(broadcastOp)) { + if (btUsage != -1 && btUsage <= CVPipeline::CACHE_TABLE_BUFFER_SIZE) { + LOG_DEBUG("Not split because broadcast bias is small. " << matmulOp); + return false; + } + } + } + + auto defMatmul = dyn_cast_if_present( + hivm::traceDefOp(outerInValue).value_or(nullptr)); + if (defMatmul) { + LOG_DEBUG("Not split because L0C remain. " << matmulOp); + return false; + } + return true; +} + +/** + * Verifies whether a matmul operation is valid for split transformation. + * Returns false if the matmul should be skipped due to: + * 1. Already processed matmul (has kLoopCarriedL0C attribute) + * 2. Illegal matmul (missing inits or inputs) + * 3. Memref-type matmul (not tensor mode) + */ +static bool verifyMatmul(linalg::MatmulOp matmulOp) { + if (matmulOp->hasAttr(CVPipeline::kLoopCarriedL0C)) { + return false; + } + auto inits = matmulOp.getDpsInits(); + auto inputs = matmulOp.getDpsInputs(); + if (inits.empty() || inputs.size() < 2) { + LOG_DEBUG("Not split because op is illegal: " << matmulOp); + return false; + } + + auto bias = matmulOp.getDpsInits()[0]; + auto outputType = dyn_cast(bias.getType()); + if (!outputType) { + LOG_DEBUG("Not split because not tensor mode matmul: " << matmulOp); + return false; + } + if (!isFloatOrInt(outputType)) { + LOG_DEBUG("Not split because not integer or float: " << matmulOp); + return false; + } + return true; +} + +// Supports the most common case, where a loop-carried matmul is +// directly under a not executed for-loop +static std::optional handleMayNotExec(linalg::MatmulOp matmulOp, + Value outerInValue) { + auto *parentOp = matmulOp->getBlock()->getParentOp(); + auto forOp = llvm::dyn_cast(parentOp); + auto inputs = parseMatmulInputs(matmulOp); + auto bias = inputs.bias; + if (!forOp || !scfMayNotExec(forOp)) { + return std::nullopt; + } + auto blockArg = llvm::dyn_cast(bias); + if (!blockArg || blockArg.getOwner()->getParentOp() != forOp) { + return std::nullopt; + } + auto initVal = forOp.getTiedLoopInit(blockArg)->get(); + auto result = forOp.getTiedLoopResult(blockArg); + + // only support the scenario that initVal is zero not broadcast + if (operationIsFillZero(outerInValue.getDefiningOp())) { + // Check if the current matmul is used by a subsequent cross-block + // cascade matmul (L0C -> L0C). + if (hasCrossBlockCascadeL0CConsumer(result, matmulOp)) { + return SplitInfo{false, initVal, result, false, false}; + } + } + + // Check if the current matmul's bias (L0C) is defined by a cross-block + // cascade matmul (L0C -> L0C). + if (hasCrossBlockCascadeL0CProducer(initVal, matmulOp)) { + return SplitInfo{true, initVal, result, false, true}; + } + + return SplitInfo{true, initVal, result, true, false}; +} + +/** + * Determines whether a matmul operation should be split based on comprehensive + * analysis. This function performs a complete analysis of the matmul + * operation's context, including: + * 1. Tracing the use-def chain of the output to find the outermost scope. + * 2. Evaluating input-based split criteria via shouldSplitByInput() and + * shouldSplitByOutput(). + * + * Parameters: + * matmulOp - The matmul operation to analyze. + * + * Returns: + * An optional SplitInfo structure containing: + * - mayNotExec: Whether the matmul might not execute (e.g., in a conditional + * or empty loop). + * - outerInValue: The outermost initial value (original bias) in the chain. + * - outerOutValue: The outermost result value in the chain. + * - shouldSplit: Boolean indicating whether the split transformation should + * be applied. Returns std::nullopt if analysis cannot be performed. + */ +static std::optional shouldSplit(linalg::MatmulOp matmulOp, + bool needSplitAll) { + + if (!verifyMatmul(matmulOp)) { + return std::nullopt; + } + + auto matmulInput = parseMatmulInputs(matmulOp); + if (needSplitAll) { + LOG_DEBUG("Split because needSplitAll is true. " << matmulOp); + return SplitInfo{false, matmulInput.bias, matmulOp.getResult(0), true, + false}; + } + + bool argsLimitedInMatmul = true; + bool mayNotExec = false; + Value outerInValue = matmulInput.bias; + auto outerOutValue = searchInArgsChain( + matmulOp.getResult(0), argsLimitedInMatmul, mayNotExec, outerInValue); + if (utils::getAnnotateOpWithAttr(outerOutValue, kMatmulAtLeastOnceHint) + .has_value()) { + mayNotExec = false; + } + if (!argsLimitedInMatmul) { + LOG_DEBUG("Split because bias is not limited in args" << matmulOp); // S25 + return SplitInfo{mayNotExec, outerInValue, outerOutValue, true, false}; + } + + if (mayNotExec) { + auto splitInfoOpt = handleMayNotExec(matmulOp, outerInValue); + if (splitInfoOpt.has_value()) { + return splitInfoOpt; + } + } + + if (!shouldSplitByInput(matmulOp, outerOutValue, outerInValue) && + !shouldSplitByOutput(matmulOp, outerOutValue, outerInValue)) { + return SplitInfo{mayNotExec, outerInValue, outerOutValue, false, false}; + } + + return SplitInfo{mayNotExec, outerInValue, outerOutValue, true, false}; +} + +/** + * Collects all the scf::ForOps that gate the execution of the current + * matmul in a cascade scenario. + */ +static SmallVector collectCascadeForOps(const SplitInfo &splitInfo, + scf::ForOp currentForOp) { + SmallVector result; + if (!currentForOp) { + return result; + } + result.push_back(currentForOp); + + SmallPtrSet seen; + seen.insert(currentForOp); + + Value curIn = splitInfo.outerInValue; + while (true) { + auto defMatmul = dyn_cast_if_present( + hivm::traceDefOp(curIn).value_or(nullptr)); + if (!defMatmul) { + break; + } + if (auto producerForOp = llvm::dyn_cast_if_present( + defMatmul->getBlock()->getParentOp())) { + if (seen.insert(producerForOp).second) { + result.push_back(producerForOp); + } + } + // Continue tracing upward along the producer matmul's bias input. + auto inputs = parseMatmulInputs(defMatmul); + curIn = inputs.bias; + } + + return result; +} + +/** + * Handles the mayNotExec case by creating select operations. + * This function creates a select operation to choose between the actual result + * and a zero-filled result based on whether the loop will execute. + * + * Returns the select result if mayNotExec is handled, otherwise returns the + * original outerOutValue. + */ +static Value handleMayNotExecSelect(linalg::MatmulOp matmulOp, + PatternRewriter &rewriter, + SplitInfo &splitInfo, + bool replaceUses = true) { + auto forOp = llvm::dyn_cast_if_present( + splitInfo.outerOutValue.getDefiningOp()); + if (!forOp) { + return splitInfo.outerOutValue; + } + + auto outputType = + dyn_cast(parseMatmulInputs(matmulOp).bias.getType()); + if (!outputType) { + return splitInfo.outerOutValue; + } + auto elmType = outputType.getElementType(); + Location loc = matmulOp.getLoc(); + + rewriter.setInsertionPointAfterValue(splitInfo.outerOutValue); + + SmallVector cascadeForOps = + collectCascadeForOps(splitInfo, forOp); + + Value executed = nullptr; + for (auto loopOp : cascadeForOps) { + auto lb = loopOp.getLowerBound(); + auto ub = loopOp.getUpperBound(); + Value cmp = + rewriter.create(loc, arith::CmpIPredicate::sgt, ub, lb); + executed = + executed ? rewriter.create(loc, executed, cmp) : cmp; + } + + Value zeroValue; + if (auto floatType = dyn_cast(elmType)) { + APFloat zeroAPFloat = APFloat::getZero(floatType.getFloatSemantics()); + zeroValue = + rewriter.create(loc, floatType, zeroAPFloat) + .getResult(); + } else if (auto intType = dyn_cast(elmType)) { + zeroValue = + rewriter.create(loc, intType, 0).getResult(); + } + auto fillOp = + rewriter.create(loc, zeroValue, splitInfo.outerOutValue); + auto selectOp = rewriter.create( + loc, executed, splitInfo.outerOutValue, fillOp.getResult(0)); + + if (replaceUses) { + // Replace uses of outerOutValue with select result, except for the select + // and fill operations + splitInfo.outerOutValue.replaceUsesWithIf( + selectOp.getResult(), [&](OpOperand &operand) { + return operand.getOwner() != selectOp && operand.getOwner() != fillOp; + }); + } + + forOp->setAttr(CVPipeline::kHIVMMatmulLimitedInCubeAttr, + rewriter.getUnitAttr()); + + return selectOp.getResult(); +} + +static LogicalResult splitMatmul(linalg::MatmulOp matmulOp, + PatternRewriter &rewriter, + SplitInfo splitInfo) { + auto outputType = + dyn_cast(parseMatmulInputs(matmulOp).bias.getType()); + if (!outputType) { + LOG_DEBUG("Not tensor mode: " << matmulOp + << "; the caller does not ensure the " + "assumption. Cowardly doing nothing"); + return failure(); + } + auto elmType = outputType.getElementType(); + + Location loc = matmulOp.getLoc(); + + // [Step 1] Create tensor.empty for the new accumulator tensor + // Same shape and type as original matmul output + auto outerDefOp = splitInfo.outerOutValue.getDefiningOp(); + rewriter.setInsertionPoint(outerDefOp); + SmallVector dynamicSizes; + for (int64_t i = 0; i < outputType.getRank(); ++i) { + if (outputType.isDynamicDim(i)) { + dynamicSizes.push_back(rewriter.create( + loc, parseMatmulInputs(matmulOp).bias, i)); + } + } + auto emptyOp = + rewriter.create(loc, outputType, dynamicSizes); + Value zeroValue; + if (auto floatType = dyn_cast(elmType)) { + APFloat zeroAPFloat = APFloat::getZero(floatType.getFloatSemantics()); + zeroValue = + rewriter.create(loc, floatType, zeroAPFloat) + .getResult(); + } else if (auto intType = dyn_cast(elmType)) { + zeroValue = + rewriter.create(loc, intType, 0).getResult(); + } + auto fillOp = rewriter.create(emptyOp.getLoc(), zeroValue, + emptyOp.getResult()); + auto zeroVal = fillOp.getResult(0); + if (outerDefOp == matmulOp.getOperation()) { + // The matmul input and accumulator may alias. Replace only the accumulator. + matmulOp.getDpsInitsMutable()[0].assign(zeroVal); + } else { + splitInfo.outerInValue.replaceUsesWithIf(zeroVal, [&](OpOperand &opop) { + return opop.getOwner() == outerDefOp; + }); + } + + auto forOp = llvm::dyn_cast_if_present( + splitInfo.outerOutValue.getDefiningOp()); + if (!splitInfo.mayNotExec || !forOp) { + // [Step 2] Create new matmul using zero-filled tensor as accumulator + // New matmul runs entirely on CUBE with no VECTOR dependency + auto inputs = parseMatmulInputs(matmulOp); + auto a = inputs.a; + auto b = inputs.b; + auto bias = inputs.bias; + rewriter.setInsertionPoint(matmulOp); + auto newMatmul = rewriter.create(loc, ValueRange{a, b}, + ValueRange{bias}); + NamedAttrList attrs(matmulOp->getAttrDictionary()); + constexpr StringLiteral kShouldRemoveAttrs[] = {"operandSegmentSizes", + "res_attrs", "arg_attrs"}; + for (auto attr : kShouldRemoveAttrs) { + attrs.erase(attr); + } + newMatmul->setAttrs(attrs); + if (splitInfo.outerOutValue == matmulOp.getResult(0)) { + splitInfo.outerOutValue = newMatmul.getResult(0); + } + rewriter.replaceOp(matmulOp, newMatmul); + } + + auto newOutValue = splitInfo.outerOutValue; + + // [Step 3] Create add: add(new_matmul_result, outs_value) + // This is the "c" in a*b+c, added after the matmul result + rewriter.setInsertionPointAfterValue(splitInfo.outerOutValue); + + constexpr size_t kMaxPreservedUsers = 2; + SmallPtrSet preservedUsers; + if (splitInfo.mayNotExec && forOp) { + auto lb = forOp.getLowerBound(); + auto ub = forOp.getUpperBound(); + + Value executed = + rewriter.create(loc, arith::CmpIPredicate::sgt, ub, lb); + Value zeroValue; + if (auto floatType = dyn_cast(elmType)) { + APFloat zeroAPFloat = APFloat::getZero(floatType.getFloatSemantics()); + zeroValue = + rewriter.create(loc, floatType, zeroAPFloat) + .getResult(); + } else if (auto intType = dyn_cast(elmType)) { + zeroValue = + rewriter.create(loc, intType, 0).getResult(); + } + auto fillOp = rewriter.create(loc, zeroValue, + splitInfo.outerOutValue); + auto selectOp = rewriter.create( + loc, executed, splitInfo.outerOutValue, fillOp.getResult(0)); + fillOp->setAttr(CVPipeline::kForMayNotExec, rewriter.getUnitAttr()); + selectOp->setAttr(CVPipeline::kForMayNotExec, rewriter.getUnitAttr()); + newOutValue = selectOp.getResult(); + preservedUsers.insert(selectOp); + preservedUsers.insert(fillOp); + forOp->setAttr(CVPipeline::kHIVMMatmulLimitedInCubeAttr, + rewriter.getUnitAttr()); + } + + Operation *addOp; + if (isa(elmType)) { + addOp = + rewriter.create(loc, newOutValue, splitInfo.outerInValue) + .getOperation(); + } else { + addOp = + rewriter.create(loc, newOutValue, splitInfo.outerInValue) + .getOperation(); + } + if (preservedUsers.empty()) { + preservedUsers.insert(addOp); + } + addOp->setAttr(CVPipeline::kAddFromMatmul, rewriter.getUnitAttr()); + splitInfo.outerOutValue.replaceUsesWithIf( + addOp->getResult(0), [&](OpOperand &operand) { + return !preservedUsers.contains(operand.getOwner()); + }); + + return success(); +} + +LogicalResult +SplitMatmulPattern::matchAndRewrite(linalg::MatmulOp matmulOp, + PatternRewriter &rewriter) const { + if (CVPipeline::hasFallbackAttr(matmulOp->getParentOfType())) { + return failure(); + } + + LOG_DEBUG("check matmulOp = " << matmulOp); + auto splitInfoOpt = shouldSplit(matmulOp, needSplitAll); + if (!splitInfoOpt.has_value()) { + return failure(); + } + auto splitInfo = splitInfoOpt.value(); + LOG_DEBUG("-------------------"); + LOG_DEBUG("matmulOp = " << matmulOp); + LOG_DEBUG("outerOutValue = " << splitInfo.outerOutValue); + LOG_DEBUG("outerInValue = " << splitInfo.outerInValue); + LOG_DEBUG("mayNotExec = " << splitInfo.mayNotExec); + LOG_DEBUG("shouldSplit = " << splitInfo.shouldSplit); + LOG_DEBUG("-------------------"); + matmulOp->setAttr(CVPipeline::kLoopCarriedL0C, rewriter.getUnitAttr()); + + if (auto markOpOpt = utils::getAnnotateOpWithAttr(splitInfo.outerOutValue, + kMatmulAtLeastOnceHint); + markOpOpt.has_value()) { + auto markOp = markOpOpt.value(); + markOp->removeAttr(kMatmulAtLeastOnceHint); + if (markOp->getAttrs().empty() || + (markOp->getAttrs().size() == 1 && + markOp->getAttrs().front().getName() == "effects")) { + rewriter.eraseOp(markOp); + } + matmulOp->getParentOp()->setAttr(CVPipeline::kHIVMMatmulLimitedInCubeAttr, + rewriter.getUnitAttr()); + } + + if (splitInfo.shouldSplit) { + return splitMatmul(matmulOp, rewriter, splitInfo); + } + + if (splitInfo.mayNotExec) { + if (splitInfo.supported) { + handleMayNotExecSelect(matmulOp, rewriter, splitInfo); + } else { + matmulOp->setAttr(CVPipeline::kMayNotExec, rewriter.getUnitAttr()); + } + } + + return success(); +} diff --git a/third_party/ascend/lib/LibEntry/CMakeLists.txt b/third_party/ascend/lib/LibEntry/CMakeLists.txt new file mode 100644 index 0000000000..908a52d648 --- /dev/null +++ b/third_party/ascend/lib/LibEntry/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(entryC SHARED libentry.cpp) +if(APPLE) + target_link_libraries(entryC PRIVATE -undefined dynamic_lookup) +endif() diff --git a/third_party/ascend/lib/LibEntry/libentry.cpp b/third_party/ascend/lib/LibEntry/libentry.cpp new file mode 100644 index 0000000000..eec49eb167 --- /dev/null +++ b/third_party/ascend/lib/LibEntry/libentry.cpp @@ -0,0 +1,115 @@ +/* + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/LibEntry/libentry.h" + +using namespace libentry; + +void libentry::ArgProcessor::classifyArguments( + const py::list &args, const py::dict &kwargs, const py::list &jit_params, + const std::unordered_set &specialize_indices, + const std::unordered_set &do_not_specialize_indices) { + for (size_t i = 0; i < args.size(); ++i) { + if (specialize_indices.count(i)) { + k_args_.append(args[i]); + spec_args_.append(args[i]); + } else if (do_not_specialize_indices.count(i)) { + k_args_.append(args[i]); + dns_args_.append(args[i]); + } else { + const_args_.append(args[i]); + } + } + + for (size_t i = args.size(); i < jit_params.size(); ++i) { + const py::object ¶m = jit_params[i]; + py::object val; + + if (kwargs.contains(param.attr("name"))) { + val = kwargs[param.attr("name")]; + } else if (py::hasattr(param, "default") && + !param.attr("default").is_none()) { + val = param.attr("default"); + } else { + continue; + } + + if (param.attr("is_constexpr").cast()) { + const_args_.append(val); + } else if (param.attr("do_not_specialize").cast()) { + dns_args_.append(val); + k_args_.append(val); + } else { + spec_args_.append(val); + k_args_.append(val); + } + } +} + +KeyType libentry::ArgProcessor::generateKey() { + auto is_tensor = [](py::handle x) { return py::hasattr(x, "data_ptr"); }; + auto is_int = [](py::handle x) { return py::isinstance(x); }; + + py::list spec_key; + for (auto arg : spec_args_) { + if (is_tensor(arg)) { + auto dtype = arg.attr("dtype"); + uintptr_t data_ptr = arg.attr("data_ptr")().cast(); + bool aligned = (data_ptr & (divisibility_ - 1)) == 0; + spec_key.append(py::make_tuple(dtype, aligned)); + } else { + spec_key.append(py::make_tuple(py::type::of(arg), arg)); + } + } + + py::list dns_key; + for (auto arg : dns_args_) { + if (is_tensor(arg)) { + dns_key.append(arg.attr("dtype")); + } else if (!is_int(arg)) { + dns_key.append(py::type::of(arg)); + } else { + int64_t val = arg.cast(); + if (val >= -0x80000000LL && val <= 0x7FFFFFFFLL) { + dns_key.append(py::str("i32")); + } else if (val >= 0 && val <= 0xFFFFFFFFFFFFFFFFLL) { + dns_key.append(py::str("u64")); + } else { + dns_key.append(py::str("i64")); + } + } + } + + py::list result; + auto list_append = [&](const py::list &src) { + for (auto handle : src) { + result.append(handle); + } + }; + list_append(spec_key); + list_append(dns_key); + list_append(const_args_); + return result; +} + +py::list libentry::ArgProcessor::getKArgs() { return k_args_; } diff --git a/third_party/ascend/lib/TritonControlFlowOpt/BlockPtrDecompose.cpp b/third_party/ascend/lib/TritonControlFlowOpt/BlockPtrDecompose.cpp new file mode 100644 index 0000000000..2e03309024 --- /dev/null +++ b/third_party/ascend/lib/TritonControlFlowOpt/BlockPtrDecompose.cpp @@ -0,0 +1,439 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonControlFlowOpt/BlockPtrDecompose.h" + +#include "TritonControlFlowOpt/ControlFlowRewrite.h" +#include "Utils/Utils.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/STLExtras.h" + +using namespace mlir; +using namespace mlir::triton; +using namespace mlir::triton::controlflow; + +namespace { + +static constexpr unsigned kBaseComponent = 0; +static constexpr unsigned getShapeStart() { return kBaseComponent + 1; } +static constexpr unsigned getStrideStart(unsigned rank) { + return getShapeStart() + rank; +} +static constexpr unsigned getOffsetStart(unsigned rank) { + return getStrideStart(rank) + rank; +} + +/// Block-pointer component layout used only by this policy: +/// +/// components = [base_address, shape..., strides..., offsets...] +/// attributes = [order] +/// +/// Analysis tracks all components in this exact order, while a rewritten SCF +/// boundary carries only the components whose symbolic identities may change. +/// The base is represented as an i64 address rather than a pointer/memref so a +/// control-flow merge remains an SSA value selection and cannot be lowered as +/// a memory-object copy by the backend. +/// `order` remains policy-owned static metadata and must agree on every +/// incoming path. +/// +/// Keep rank/layout checks local to this file: the generic control-flow +/// machinery intentionally does not know the descriptor format of a policy. +// Extracts the block-pointer rank from the original type shared by analysis +// and rewrite states. Invalid pointer and pointee types fail consistently. +static FailureOr getRank(Type originalType) { + auto pointerType = dyn_cast(originalType); + if (!pointerType) + return failure(); + auto tensorType = dyn_cast(pointerType.getPointeeType()); + if (!tensorType) + return failure(); + return tensorType.getRank(); +} + +// Recovers the scalar pointer type accepted by tt.make_tensor_ptr from the +// BlockPtr result type. The address space is preserved across the temporary +// integer carrier. +static FailureOr getBasePointerType(Type originalType) { + auto pointerType = dyn_cast(originalType); + if (!pointerType) + return failure(); + auto tensorType = dyn_cast(pointerType.getPointeeType()); + if (!tensorType) + return failure(); + return triton::PointerType::get(tensorType.getElementType(), + pointerType.getAddressSpace()); +} + +// Materializes the integer form of a make_tensor_ptr base at a point dominated +// by the base itself, rather than unconditionally inside the make_tensor_ptr's +// block. This matters when two scf.if branches build equivalent descriptors +// from one outer base: analysis classifies the base as invariant, so the +// concrete address reused after the if must also dominate that use. +// +// Example: +// %base is a function argument +// scf.if ... { +// %a = tt.make_tensor_ptr %base, ... +// } else { +// %b = tt.make_tensor_ptr %base, ... +// } +// +// The shared tt.ptr_to_int is materialized among the leading conversions of a +// block-argument owner, or immediately after the base's defining operation. +// Both positions dominate later branches and the pointer rebuilt after them. +// Keeping the explicit tt.ptr_to_int even for a base produced by tt.int_to_ptr +// preserves the native pointer provenance used during invariant-base +// recomposition. +static FailureOr materializeBaseAddress(Value base, Location loc, + OpBuilder &builder) { + OpBuilder::InsertionGuard guard(builder); + if (auto blockArgument = dyn_cast(base)) { + Block *owner = blockArgument.getOwner(); + Operation *lastLeadingAddress = nullptr; + for (Operation &operation : *owner) { + auto existing = dyn_cast(&operation); + if (!existing) + break; + if (existing.getSrc() == base && + existing.getResult().getType().isInteger(64)) + return existing.getResult(); + lastLeadingAddress = &operation; + } + if (lastLeadingAddress) + builder.setInsertionPointAfter(lastLeadingAddress); + else + builder.setInsertionPointToStart(owner); + } else if (Operation *definingOp = base.getDefiningOp()) { + if (Operation *next = definingOp->getNextNode()) { + if (auto existing = dyn_cast(next); + existing && existing.getSrc() == base && + existing.getResult().getType().isInteger(64)) + return existing.getResult(); + } + builder.setInsertionPointAfter(definingOp); + } else { + return failure(); + } + return builder.create(loc, builder.getI64Type(), base) + .getResult(); +} + +// Recovers the original pointer when a concrete descriptor still holds the +// exact address produced for that pointer. An invariant base keeps the direct +// +// %address = tt.ptr_to_int %base +// +// result as component zero while offsets cross an SCF boundary. A changing +// base instead replaces component zero with an SCF result or region argument, +// so this lookup intentionally fails and recomposition must use tt.int_to_ptr. +static Value recoverNativeBase(Value baseAddress, + triton::PointerType expectedType) { + auto ptrToInt = baseAddress.getDefiningOp(); + if (!ptrToInt || ptrToInt.getSrc().getType() != expectedType) + return nullptr; + return ptrToInt.getSrc(); +} + +// Analysis and rewrite must agree on which advance dimensions change the +// descriptor. Only a literal integer zero is ignored; values that merely fold +// to zero after another rewrite remain conservative transfer candidates. +static bool isConstantZeroInteger(Value value) { + std::optional constant = getConstantIntValue(value); + return constant && *constant == 0; +} + +// Validates the common block-pointer schema for either state representation. +// Their component element types differ, but this check only needs field sizes. +template static bool hasValidLayout(const StateT &state) { + FailureOr rank = getRank(state.originalType); + return succeeded(rank) && state.components.size() == 1 + 3 * *rank && + state.attributes.size() == 1 && + isa(state.attributes.front()); +} + +/// Returns the complete, ordered descriptor range used to expand one +/// block-pointer control-flow slot. Keeping this policy-local prevents the +/// generic SCF rewrite from depending on the BlockPtr field layout. +static SmallVector +getAllComponentIndices(const AnalyzedValue &value) { + SmallVector indices; + indices.reserve(value.components.size()); + for (unsigned index = 0; index < value.components.size(); ++index) + indices.push_back(index); + return indices; +} + +class BlockPtrPolicy final : public ControlFlowRewritePolicy { +public: + bool matches(Type type) const override { + // A Triton block pointer is a scalar !tt.ptr whose pointee is a ranked + // tensor. Tensor-of-pointers are handled by their own decomposition. + auto pointerType = dyn_cast(type); + return pointerType && isa(pointerType.getPointeeType()); + } + + FailureOr + analyzeValue(Value value, + ControlFlowAnalysisContext &context) const override { + // Region arguments and control-flow results are installed by the generic + // analysis after merging their incoming abstract component states. + if (const AnalyzedValue *known = context.lookupValue(value)) { + if (!matches(known->originalType)) + return failure(); + return *known; + } + + if (auto makePtr = value.getDefiningOp()) { + // make_tensor_ptr exposes the complete descriptor directly. Record only + // types and symbolic identities here; this phase must not create IR. + AnalyzedValue result; + result.originalType = value.getType(); + Value base = makePtr.getBase(); + result.components.push_back( + {IntegerType::get(value.getContext(), 64), + ComponentIdentity::fromValue(base, kBaseComponent)}); + unsigned componentIndex = getShapeStart(); + auto appendComponents = [&](ValueRange values) { + for (Value component : values) { + result.components.push_back( + {component.getType(), + ComponentIdentity::fromValue(component, componentIndex++)}); + } + }; + appendComponents(makePtr.getShape()); + appendComponents(makePtr.getStrides()); + appendComponents(makePtr.getOffsets()); + result.attributes.push_back(makePtr.getOrderAttr()); + if (!hasValidLayout(result)) + return failure(); + return result; + } + + auto advance = value.getDefiningOp(); + if (!advance) + return failure(); + // tt.advance preserves base/shape/strides/order and produces new offsets. + // Give those offsets identities tied to the result so a loop/if merge can + // detect that they differ without materializing arith.addi operations. + FailureOr result = context.analyzeValue(advance.getPtr()); + if (failed(result) || !hasValidLayout(*result)) + return failure(); + unsigned rank = *getRank(result->originalType); + if (advance.getOffsets().size() != rank) + return failure(); + for (unsigned dimension = 0; dimension < rank; ++dimension) { + if (isConstantZeroInteger(advance.getOffsets()[dimension])) + continue; + unsigned componentIndex = getOffsetStart(rank) + dimension; + result->components[componentIndex].identity = + ComponentIdentity::fromValue(value, componentIndex); + } + result->originalType = value.getType(); + return *result; + } + + FailureOr> + getLoopCandidateComponents(const AnalyzedValue &value) const override { + if (!hasValidLayout(value)) + return failure(); + // Analyze the complete descriptor so changes to base, shape, stride, or + // offset remain visible. The transfer hook below decides which candidates + // actually become ordinary loop-carried SSA state. + return getAllComponentIndices(value); + } + + FailureOr> + getLoopTransferredComponents(const AnalyzedValue &initial, + const AnalyzedValue ®ionArgument, + const AnalyzedValue &next) const override { + if (!hasValidLayout(initial) || !hasValidLayout(regionArgument) || + !hasValidLayout(next) || + initial.originalType != regionArgument.originalType || + initial.originalType != next.originalType || + initial.attributes != regionArgument.attributes || + initial.attributes != next.attributes) + return failure(); + + SmallVector transferred; + for (unsigned index = 0; index < initial.components.size(); ++index) { + if (failed(joinComponentTypes(initial.components[index].type, + regionArgument.components[index].type)) || + failed(joinComponentTypes(initial.components[index].type, + next.components[index].type))) + return failure(); + const ComponentIdentity &nextIdentity = next.components[index].identity; + bool forwardsCurrent = + nextIdentity == regionArgument.components[index].identity; + bool restoresInitial = nextIdentity == initial.components[index].identity; + if (!forwardsCurrent && !restoresInitial) + transferred.push_back(index); + } + return transferred; + } + + FailureOr> + getIfTransferredComponents(const AnalyzedValue &thenValue, + const AnalyzedValue &elseValue) const override { + if (!hasValidLayout(thenValue) || !hasValidLayout(elseValue) || + thenValue.originalType != elseValue.originalType || + thenValue.attributes != elseValue.attributes) + return failure(); + + SmallVector transferred; + for (unsigned index = 0; index < thenValue.components.size(); ++index) { + if (failed(joinComponentTypes(thenValue.components[index].type, + elseValue.components[index].type))) + return failure(); + if (thenValue.components[index].identity != + elseValue.components[index].identity) + transferred.push_back(index); + } + return transferred; + } + + FailureOr joinComponentTypes(Type lhs, Type rhs) const override { + // Block-pointer descriptor operands must have identical types on all + // incoming paths. Tensor-pointer offsets use a more permissive integer + // width join in their own policy. + if (lhs != rhs) + return failure(); + return lhs; + } + + bool shouldDecomposeOperation(Operation *op) const override { + // Recording each cloned advance lets downstream advances reuse its + // flattened descriptor rather than walking through the rebuilt pointer. + return isa(op); + } + + bool requiresPointerDescriptorBoundaryMarker() const override { return true; } + + FailureOr decompose(Value value, + const ControlFlowRewriteContext &context, + OpBuilder &builder, + Location loc) const override { + // Prefer decompositions recorded while rebuilding the enclosing region; + // this is how analysis results cross nested SCF boundaries at rewrite time. + if (const DecomposedValue *known = context.lookup(value)) { + if (!matches(known->originalType)) + return failure(); + return *known; + } + + value = context.remap(value); + if (auto makePtr = value.getDefiningOp()) { + // Materialize the concrete counterpart of analyzeValue's descriptor. + DecomposedValue result; + result.originalType = value.getType(); + FailureOr baseAddress = + materializeBaseAddress(makePtr.getBase(), makePtr.getLoc(), builder); + if (failed(baseAddress)) + return failure(); + result.components.push_back(*baseAddress); + result.components.append(makePtr.getShape().begin(), + makePtr.getShape().end()); + result.components.append(makePtr.getStrides().begin(), + makePtr.getStrides().end()); + result.components.append(makePtr.getOffsets().begin(), + makePtr.getOffsets().end()); + result.attributes.push_back(makePtr.getOrderAttr()); + if (!hasValidLayout(result)) + return failure(); + return result; + } + + auto advance = value.getDefiningOp(); + if (!advance) + return failure(); + + FailureOr result = + decompose(advance.getPtr(), context, builder, loc); + if (failed(result) || !hasValidLayout(*result)) + return failure(); + FailureOr rank = getRank(result->originalType); + if (advance.getOffsets().size() != *rank) + return failure(); + + // Flatten an advance into offset arithmetic at the original operation's + // position. Base, shape, strides, and order are inherited unchanged. + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(advance); + for (auto [dim, delta] : llvm::enumerate(advance.getOffsets())) { + unsigned component = getOffsetStart(*rank) + dim; + Value currentOffset = result->components[component]; + if (isConstantZeroInteger(delta)) + continue; + Value remappedDelta = context.remap(delta); + if (!remappedDelta) + return failure(); + Value offset = dyn_cast( + addOpFoldResult(currentOffset, remappedDelta, advance.getLoc(), + builder, currentOffset.getType())); + if (!offset) + return failure(); + result->components[component] = offset; + } + result->originalType = value.getType(); + return *result; + } + + Value recompose(const DecomposedValue &value, OpBuilder &builder, + Location loc) const override { + // Rebuild the original pointer type immediately inside/after the rewritten + // control-flow boundary so ordinary users remain untouched. + if (!hasValidLayout(value)) + return nullptr; + unsigned rank = *getRank(value.originalType); + auto order = cast(value.attributes.front()); + FailureOr basePointerType = + getBasePointerType(value.originalType); + if (failed(basePointerType) || + value.components[kBaseComponent].getType() != builder.getI64Type()) + return nullptr; + Value base = + recoverNativeBase(value.components[kBaseComponent], *basePointerType); + if (!base) + base = builder.create( + loc, *basePointerType, value.components[kBaseComponent]); + return builder.create( + loc, value.originalType, base, + ValueRange(value.components).slice(getShapeStart(), rank), + ValueRange(value.components).slice(getStrideStart(rank), rank), + ValueRange(value.components).slice(getOffsetStart(rank), rank), order); + } +}; + +} // namespace + +namespace mlir::triton::controlflow { + +LogicalResult runBlockPtrDecompose(ModuleOp module) { + // Analyze the complete ordered descriptor for every BlockPtr that crosses a + // supported SCF boundary, carry only changing components, and rebuild the + // pointer at each use site. + BlockPtrPolicy policy; + return rewriteControlFlow(module, policy); +} + +} // namespace mlir::triton::controlflow diff --git a/third_party/ascend/lib/TritonControlFlowOpt/CFGStructuring.cpp b/third_party/ascend/lib/TritonControlFlowOpt/CFGStructuring.cpp new file mode 100644 index 0000000000..a8c9553f15 --- /dev/null +++ b/third_party/ascend/lib/TritonControlFlowOpt/CFGStructuring.cpp @@ -0,0 +1,917 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonControlFlowOpt/CFGStructuring.h" +#include "Utils/Utils.h" + +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Visitors.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" + +#include + +using namespace mlir; +using namespace triton; + +namespace { + +// This transformation handles acyclic CFGs made from cf.br/cf.cond_br and +// return-like terminators. A branch with a common successor becomes an scf.if +// whose results model that successor's block arguments. A branch whose arms +// terminate independently is handled by the terminal-path builder below. + +//===----------------------------------------------------------------------===// +// CFG discovery and join selection +//===----------------------------------------------------------------------===// + +static bool isSupportedReturn(Operation *op) { + return isa( + op); +} + +static SmallVector getCfgSuccessors(Block *block) { + Operation *term = block->getTerminator(); + if (auto br = dyn_cast(term)) + return {br.getDest()}; + if (auto condBr = dyn_cast(term)) + return {condBr.getTrueDest(), condBr.getFalseDest()}; + return {}; +} + +static DenseMap computeDistances(Block *start) { + DenseMap distances; + SmallVector worklist; + + distances[start] = 0; + worklist.push_back(start); + + for (unsigned i = 0; i < worklist.size(); ++i) { + Block *block = worklist[i]; + unsigned nextDistance = distances[block] + 1; + for (Block *successor : getCfgSuccessors(block)) { + if (successor->getParent() != start->getParent()) + continue; + if (distances.count(successor)) + continue; + distances[successor] = nextDistance; + worklist.push_back(successor); + } + } + + return distances; +} + +/// Finds the closest block reachable from both branch arms. Minimizing the +/// maximum arm distance prefers the earliest balanced convergence point; the +/// total distance provides deterministic tie-breaking. +static FailureOr findNearestCommonBlock(Block *lhs, Block *rhs, + Location loc, + bool emitDiagnostic = true) { + DenseMap lhsDistances = computeDistances(lhs); + DenseMap rhsDistances = computeDistances(rhs); + + Block *best = nullptr; + unsigned bestMaxDistance = std::numeric_limits::max(); + unsigned bestTotalDistance = std::numeric_limits::max(); + + for (auto &entry : lhsDistances) { + Block *candidate = entry.first; + auto rhsIt = rhsDistances.find(candidate); + if (rhsIt == rhsDistances.end()) + continue; + + unsigned lhsDistance = entry.second; + unsigned rhsDistance = rhsIt->second; + unsigned maxDistance = std::max(lhsDistance, rhsDistance); + unsigned totalDistance = lhsDistance + rhsDistance; + if (maxDistance < bestMaxDistance || + (maxDistance == bestMaxDistance && totalDistance < bestTotalDistance)) { + best = candidate; + bestMaxDistance = maxDistance; + bestTotalDistance = totalDistance; + } + } + + if (!best && emitDiagnostic) { + emitError(loc) << "unsupported non-tree control flow: branch arms do not " + "reach a common convergence block"; + return failure(); + } + if (!best) + return failure(); + + return best; +} + +/// Replaces a destination block's SSA arguments with the values carried by the +/// incoming branch. Callers erase the original CFG blocks after their bodies +/// have been moved or cloned. +static LogicalResult replaceBlockArguments(Block *block, ValueRange incoming, + Location loc) { + if (block->getNumArguments() != incoming.size()) { + emitError(loc) << "invalid branch operand count while structuring " + "control flow: " + << incoming.size() << " operands for " + << block->getNumArguments() << " block arguments"; + return failure(); + } + + for (auto [arg, value] : llvm::zip(block->getArguments(), incoming)) + arg.replaceAllUsesWith(value); + return success(); +} + +/// Moves non-terminator operations into the currently constructed SCF region. +/// The original terminator remains available to drive recursive CFG traversal. +static void moveBlockBodyBefore(Block *block, OpBuilder &builder) { + SmallVector movedOps = llvm::map_to_vector( + block->without_terminator(), [](Operation &op) { return &op; }); + for (Operation *op : movedOps) + op->moveBefore(builder.getInsertionBlock(), builder.getInsertionPoint()); +} + +struct ReturnPathResult { + // A terminal path cannot place a return inside an scf.if region. Bubble the + // return operands outward so the caller can yield them and emit one return + // after the structured conditional. + SmallVector operands; +}; + +//===----------------------------------------------------------------------===// +// Converging branch construction +//===----------------------------------------------------------------------===// + +static FailureOr> buildRegionPath(Block *block, + ValueRange incoming, + Block *stopBlock, + OpBuilder &builder); + +static FailureOr +buildReturnPath(Block *block, ValueRange incoming, OpBuilder &builder); + +static FailureOr buildTerminalValueIf(cf::CondBranchOp condBr, + OpBuilder &builder); + +static FailureOr buildStructuredIf(cf::CondBranchOp condBr, + Block *joinBlock, + OpBuilder &builder) { + // The join block arguments define the exact result contract of the new if. + // Each arm is recursively consumed up to that join and yields its incoming + // values in the same order. + SmallVector resultTypes; + resultTypes.reserve(joinBlock->getNumArguments()); + for (BlockArgument arg : joinBlock->getArguments()) + resultTypes.push_back(arg.getType()); + + auto ifOp = builder.create(condBr.getLoc(), resultTypes, + condBr.getCondition(), + /*withElseRegion=*/true); + + { + OpBuilder::InsertionGuard guard(builder); + Operation *autoYield = + resultTypes.empty() ? ifOp.thenBlock()->getTerminator() : nullptr; + if (autoYield) + builder.setInsertionPoint(autoYield); + else + builder.setInsertionPointToStart(ifOp.thenBlock()); + FailureOr> thenYield = buildRegionPath( + condBr.getTrueDest(), condBr.getTrueDestOperands(), joinBlock, builder); + if (failed(thenYield)) + return failure(); + if (thenYield->size() != resultTypes.size()) { + condBr.emitError("then branch yields ") + << thenYield->size() << " values, expected " << resultTypes.size(); + return failure(); + } + if (!autoYield) + builder.create(condBr.getLoc(), *thenYield); + } + + { + OpBuilder::InsertionGuard guard(builder); + Operation *autoYield = + resultTypes.empty() ? ifOp.elseBlock()->getTerminator() : nullptr; + if (autoYield) + builder.setInsertionPoint(autoYield); + else + builder.setInsertionPointToStart(ifOp.elseBlock()); + FailureOr> elseYield = + buildRegionPath(condBr.getFalseDest(), condBr.getFalseDestOperands(), + joinBlock, builder); + if (failed(elseYield)) + return failure(); + if (elseYield->size() != resultTypes.size()) { + condBr.emitError("else branch yields ") + << elseYield->size() << " values, expected " << resultTypes.size(); + return failure(); + } + if (!autoYield) + builder.create(condBr.getLoc(), *elseYield); + } + + return ifOp; +} + +static Operation *createReturnLike(OpBuilder &builder, Location loc, + Operation *sampleReturn, + ValueRange operands) { + // Preserve whether the containing callable uses tt.return or func.return, + // along with any dialect-specific attributes on that terminator. + OperationState state(loc, sampleReturn->getName()); + state.addOperands(operands); + state.addAttributes(sampleReturn->getAttrs()); + return builder.create(state); +} + +//===----------------------------------------------------------------------===// +// Terminal branch construction +//===----------------------------------------------------------------------===// + +/// Computes the value types returned by a terminal path without mutating the +/// CFG. Both arms of a terminal conditional must return the same signature so +/// they can become the results of a value-producing scf.if. +static FailureOr> +collectReturnPathTypes(Block *block, SmallPtrSetImpl &visiting) { + if (!visiting.insert(block).second) + return block->getTerminator()->emitError() + << "unsupported cyclic terminal control flow"; + + Operation *term = block->getTerminator(); + if (auto br = dyn_cast(term)) { + FailureOr> result = + collectReturnPathTypes(br.getDest(), visiting); + visiting.erase(block); + return result; + } + + if (auto condBr = dyn_cast(term)) { + FailureOr nestedJoin = findNearestCommonBlock( + condBr.getTrueDest(), condBr.getFalseDest(), condBr.getLoc(), + /*emitDiagnostic=*/false); + if (succeeded(nestedJoin)) { + FailureOr> result = + collectReturnPathTypes(*nestedJoin, visiting); + visiting.erase(block); + return result; + } + + FailureOr> thenTypes = + collectReturnPathTypes(condBr.getTrueDest(), visiting); + FailureOr> elseTypes = + collectReturnPathTypes(condBr.getFalseDest(), visiting); + visiting.erase(block); + if (failed(thenTypes) || failed(elseTypes)) + return failure(); + if (!haveSameTypes(TypeRange{*thenTypes}, TypeRange{*elseTypes})) { + condBr.emitError("terminal branch return types do not match"); + return failure(); + } + return *thenTypes; + } + + if (isSupportedReturn(term)) { + SmallVector types; + for (Value operand : term->getOperands()) + types.push_back(operand.getType()); + visiting.erase(block); + return types; + } + + visiting.erase(block); + return term->emitError() + << "unsupported terminator while analyzing terminal control flow"; +} + +static Operation *findReturnOnPath(Block *block, + SmallPtrSetImpl &visited) { + // The operation name/attributes of one reachable return are used as the + // template after terminal paths have been converted to yielded values. + if (!visited.insert(block).second) + return nullptr; + + Operation *term = block->getTerminator(); + if (isSupportedReturn(term)) + return term; + for (Block *successor : getCfgSuccessors(block)) { + if (successor->getParent() != block->getParent()) + continue; + if (Operation *returnOp = findReturnOnPath(successor, visited)) + return returnOp; + } + return nullptr; +} + +static SmallVector mapValues(ValueRange values, IRMapping &mapping) { + // lookupOrDefault is intentional for values captured from outside the cloned + // path; only block arguments and locally cloned results require mappings. + SmallVector mapped; + mapped.reserve(values.size()); + for (Value value : values) + mapped.push_back(mapping.lookupOrDefault(value)); + return mapped; +} + +static FailureOr> +buildClonedTerminalPath(Block *block, ValueRange incoming, OpBuilder &builder, + IRMapping mapping, SmallPtrSetImpl &visiting); + +static FailureOr> +buildClonedTerminalTerminator(Operation *term, OpBuilder &builder, + IRMapping mapping, + SmallPtrSetImpl &visiting) { + if (auto br = dyn_cast(term)) { + SmallVector incoming = mapValues(br.getDestOperands(), mapping); + return buildClonedTerminalPath(br.getDest(), incoming, builder, mapping, + visiting); + } + + if (auto condBr = dyn_cast(term)) { + SmallPtrSet thenVisiting; + FailureOr> thenTypes = + collectReturnPathTypes(condBr.getTrueDest(), thenVisiting); + SmallPtrSet elseVisiting; + FailureOr> elseTypes = + collectReturnPathTypes(condBr.getFalseDest(), elseVisiting); + if (failed(thenTypes) || failed(elseTypes)) + return failure(); + if (!haveSameTypes(TypeRange{*thenTypes}, TypeRange{*elseTypes})) { + condBr.emitError("terminal branch return types do not match"); + return failure(); + } + + auto ifOp = builder.create( + condBr.getLoc(), *thenTypes, + mapping.lookupOrDefault(condBr.getCondition()), + /*withElseRegion=*/true); + + { + OpBuilder::InsertionGuard guard(builder); + Operation *autoYield = + thenTypes->empty() ? ifOp.thenBlock()->getTerminator() : nullptr; + if (autoYield) + builder.setInsertionPoint(autoYield); + else + builder.setInsertionPointToStart(ifOp.thenBlock()); + SmallVector incoming = + mapValues(condBr.getTrueDestOperands(), mapping); + FailureOr> thenReturn = buildClonedTerminalPath( + condBr.getTrueDest(), incoming, builder, mapping, visiting); + if (failed(thenReturn)) + return failure(); + if (!haveSameTypes(TypeRange{ValueRange{*thenReturn}}, + TypeRange{*thenTypes})) { + condBr.emitError("then terminal branch returns incompatible values"); + return failure(); + } + if (!autoYield) + builder.create(condBr.getLoc(), *thenReturn); + } + + { + OpBuilder::InsertionGuard guard(builder); + Operation *autoYield = + thenTypes->empty() ? ifOp.elseBlock()->getTerminator() : nullptr; + if (autoYield) + builder.setInsertionPoint(autoYield); + else + builder.setInsertionPointToStart(ifOp.elseBlock()); + SmallVector incoming = + mapValues(condBr.getFalseDestOperands(), mapping); + FailureOr> elseReturn = buildClonedTerminalPath( + condBr.getFalseDest(), incoming, builder, mapping, visiting); + if (failed(elseReturn)) + return failure(); + if (!haveSameTypes(TypeRange{ValueRange{*elseReturn}}, + TypeRange{*thenTypes})) { + condBr.emitError("else terminal branch returns incompatible values"); + return failure(); + } + if (!autoYield) + builder.create(condBr.getLoc(), *elseReturn); + } + + return SmallVector(ifOp->getResults().begin(), + ifOp->getResults().end()); + } + + if (isSupportedReturn(term)) + return mapValues(term->getOperands(), mapping); + + return term->emitError() + << "unsupported terminator while structuring terminal control flow"; +} + +static FailureOr> +buildClonedTerminalPath(Block *block, ValueRange incoming, OpBuilder &builder, + IRMapping mapping, SmallPtrSetImpl &visiting) { + // Terminal paths are cloned rather than moved. Analysis and construction can + // therefore fail without partially consuming the original CFG; the old + // blocks are erased only after the complete replacement return is built. + if (!visiting.insert(block).second) + return block->getTerminator()->emitError() + << "unsupported cyclic terminal control flow"; + + if (block->getNumArguments() != incoming.size()) { + visiting.erase(block); + return block->getTerminator()->emitError() + << "invalid branch operand count while structuring terminal " + "control flow"; + } + + for (auto [arg, value] : llvm::zip(block->getArguments(), incoming)) + mapping.map(arg, value); + + for (Operation &op : block->without_terminator()) + builder.clone(op, mapping); + + FailureOr> result = buildClonedTerminalTerminator( + block->getTerminator(), builder, mapping, visiting); + visiting.erase(block); + return result; +} + +static bool hasNonTreeCondBranch(Region &body) { + // A branch without a join cannot be consumed by the move-based path. If any + // such branch exists, clone the complete terminal tree atomically instead. + for (Block &block : body) { + auto condBr = dyn_cast(block.getTerminator()); + if (!condBr) + continue; + if (failed(findNearestCommonBlock(condBr.getTrueDest(), + condBr.getFalseDest(), condBr.getLoc(), + /*emitDiagnostic=*/false))) + return true; + } + return false; +} + +static LogicalResult structureTerminalReturnBody(Operation *funcOp, + Region &body) { + // This path is used when branch arms do not reconverge but both end in + // compatible returns. The cloned scf.if produces the return operands, after + // which every original non-entry block can be removed together. + Block &entryBlock = body.front(); + Operation *entryTerm = entryBlock.getTerminator(); + SmallPtrSet visited; + Operation *sampleReturn = findReturnOnPath(&entryBlock, visited); + if (!sampleReturn) { + return funcOp->emitError() + << "unsupported non-tree control flow: no terminal return found"; + } + + OpBuilder builder(entryTerm); + IRMapping mapping; + SmallPtrSet visiting; + FailureOr> returnOperands = + buildClonedTerminalTerminator(entryTerm, builder, mapping, visiting); + if (failed(returnOperands)) + return failure(); + + createReturnLike(builder, entryTerm->getLoc(), sampleReturn, *returnOperands); + + SmallVector eraseBlocks; + for (Block &block : llvm::drop_begin(body.getBlocks())) + eraseBlocks.push_back(&block); + + entryTerm->erase(); + for (Block *block : eraseBlocks) { + for (Operation &op : *block) + op.dropAllReferences(); + } + for (Block *block : llvm::reverse(eraseBlocks)) + block->erase(); + + return success(); +} + +static FailureOr buildTerminalValueIf(cf::CondBranchOp condBr, + OpBuilder &builder) { + SmallPtrSet thenVisiting; + FailureOr> thenTypes = + collectReturnPathTypes(condBr.getTrueDest(), thenVisiting); + SmallPtrSet elseVisiting; + FailureOr> elseTypes = + collectReturnPathTypes(condBr.getFalseDest(), elseVisiting); + if (failed(thenTypes) || failed(elseTypes)) + return failure(); + if (!haveSameTypes(TypeRange{*thenTypes}, TypeRange{*elseTypes})) { + condBr.emitError("terminal branch return types do not match"); + return failure(); + } + + auto ifOp = builder.create(condBr.getLoc(), *thenTypes, + condBr.getCondition(), + /*withElseRegion=*/true); + + { + OpBuilder::InsertionGuard branchGuard(builder); + Operation *autoYield = + thenTypes->empty() ? ifOp.thenBlock()->getTerminator() : nullptr; + if (autoYield) + builder.setInsertionPoint(autoYield); + else + builder.setInsertionPointToStart(ifOp.thenBlock()); + FailureOr thenReturn = buildReturnPath( + condBr.getTrueDest(), condBr.getTrueDestOperands(), builder); + if (failed(thenReturn)) + return failure(); + if (!haveSameTypes(TypeRange{ValueRange{thenReturn->operands}}, + TypeRange{*thenTypes})) { + condBr.emitError("then terminal branch returns incompatible values"); + return failure(); + } + if (!autoYield) + builder.create(condBr.getLoc(), thenReturn->operands); + } + + { + OpBuilder::InsertionGuard branchGuard(builder); + Operation *autoYield = + thenTypes->empty() ? ifOp.elseBlock()->getTerminator() : nullptr; + if (autoYield) + builder.setInsertionPoint(autoYield); + else + builder.setInsertionPointToStart(ifOp.elseBlock()); + FailureOr elseReturn = buildReturnPath( + condBr.getFalseDest(), condBr.getFalseDestOperands(), builder); + if (failed(elseReturn)) + return failure(); + if (!haveSameTypes(TypeRange{ValueRange{elseReturn->operands}}, + TypeRange{*thenTypes})) { + condBr.emitError("else terminal branch returns incompatible values"); + return failure(); + } + if (!autoYield) + builder.create(condBr.getLoc(), elseReturn->operands); + } + + return ifOp; +} + +static FailureOr +buildReturnPath(Block *block, ValueRange incoming, OpBuilder &builder) { + Operation *term = block->getTerminator(); + if (failed(replaceBlockArguments(block, incoming, term->getLoc()))) + return failure(); + moveBlockBodyBefore(block, builder); + + if (auto br = dyn_cast(term)) + return buildReturnPath(br.getDest(), br.getDestOperands(), builder); + + if (auto condBr = dyn_cast(term)) { + FailureOr nestedJoin = findNearestCommonBlock( + condBr.getTrueDest(), condBr.getFalseDest(), condBr.getLoc(), + /*emitDiagnostic=*/false); + if (succeeded(nestedJoin)) { + FailureOr nestedIf = + buildStructuredIf(condBr, *nestedJoin, builder); + if (failed(nestedIf)) + return failure(); + + SmallVector nestedResults((*nestedIf)->getResults().begin(), + (*nestedIf)->getResults().end()); + return buildReturnPath(*nestedJoin, nestedResults, builder); + } + + FailureOr terminalIf = buildTerminalValueIf(condBr, builder); + if (failed(terminalIf)) { + condBr.emitError() << "unsupported non-tree control flow: branch arms do " + "not both terminate with compatible returns"; + return failure(); + } + + ReturnPathResult result; + result.operands.assign((*terminalIf)->getResults().begin(), + (*terminalIf)->getResults().end()); + return result; + } + + if (isSupportedReturn(term)) { + ReturnPathResult result; + result.operands.assign(term->getOperands().begin(), + term->getOperands().end()); + return result; + } + + return term->emitError() + << "unsupported terminator while structuring terminal control flow"; +} + +//===----------------------------------------------------------------------===// +// Top-level CFG consumption +//===----------------------------------------------------------------------===// + +/// Consumes one branch arm until `stopBlock`. Nested conditionals are converted +/// recursively, and their results become the incoming values of the next join. +static FailureOr> buildRegionPath(Block *block, + ValueRange incoming, + Block *stopBlock, + OpBuilder &builder) { + if (block == stopBlock) + return SmallVector(incoming.begin(), incoming.end()); + + Operation *term = block->getTerminator(); + if (failed(replaceBlockArguments(block, incoming, term->getLoc()))) + return failure(); + moveBlockBodyBefore(block, builder); + + if (auto br = dyn_cast(term)) { + SmallVector operands(br.getDestOperands().begin(), + br.getDestOperands().end()); + if (br.getDest() == stopBlock) + return operands; + return buildRegionPath(br.getDest(), operands, stopBlock, builder); + } + + if (auto condBr = dyn_cast(term)) { + FailureOr nestedJoin = findNearestCommonBlock( + condBr.getTrueDest(), condBr.getFalseDest(), condBr.getLoc()); + if (failed(nestedJoin)) + return failure(); + + FailureOr nestedIf = + buildStructuredIf(condBr, *nestedJoin, builder); + if (failed(nestedIf)) + return failure(); + + SmallVector nestedResults((*nestedIf)->getResults().begin(), + (*nestedIf)->getResults().end()); + if (*nestedJoin == stopBlock) + return nestedResults; + return buildRegionPath(*nestedJoin, nestedResults, stopBlock, builder); + } + + if (isSupportedReturn(term)) { + return term->emitError() + << "unsupported early return while structuring control flow"; + } + + return term->emitError() + << "unsupported terminator while structuring control flow"; +} + +static LogicalResult appendStructuredBlock(Block *block, ValueRange incoming, + OpBuilder &builder, + Operation *anchorTerminator); + +static LogicalResult appendStructuredTerminator(Operation *term, + OpBuilder &builder, + Operation *anchorTerminator) { + // Walk the entry CFG in execution order. A converging conditional is emitted + // as scf.if and traversal resumes at its join; a terminal conditional emits + // the final return and finishes the function body. + if (auto br = dyn_cast(term)) { + return appendStructuredBlock(br.getDest(), br.getDestOperands(), builder, + anchorTerminator); + } + + if (auto condBr = dyn_cast(term)) { + FailureOr joinBlock = findNearestCommonBlock( + condBr.getTrueDest(), condBr.getFalseDest(), condBr.getLoc(), + /*emitDiagnostic=*/false); + if (failed(joinBlock)) { + SmallPtrSet visited; + Operation *sampleReturn = findReturnOnPath(condBr.getTrueDest(), visited); + if (!sampleReturn) { + visited.clear(); + sampleReturn = findReturnOnPath(condBr.getFalseDest(), visited); + } + if (!sampleReturn) { + return condBr.emitError() + << "unsupported non-tree control flow: branch arms do not " + "reach a common convergence block"; + } + + FailureOr terminalIf = buildTerminalValueIf(condBr, builder); + if (failed(terminalIf)) + return failure(); + + SmallVector returnOperands((*terminalIf)->getResults().begin(), + (*terminalIf)->getResults().end()); + createReturnLike(builder, condBr.getLoc(), sampleReturn, returnOperands); + return success(); + } + + FailureOr ifOp = buildStructuredIf(condBr, *joinBlock, builder); + if (failed(ifOp)) + return failure(); + + return appendStructuredBlock(*joinBlock, (*ifOp)->getResults(), builder, + anchorTerminator); + } + + if (isSupportedReturn(term)) { + term->moveBefore(anchorTerminator); + return success(); + } + + return term->emitError() + << "unsupported entry terminator while structuring control flow"; +} + +static LogicalResult appendStructuredBlock(Block *block, ValueRange incoming, + OpBuilder &builder, + Operation *anchorTerminator) { + Operation *term = block->getTerminator(); + if (failed(replaceBlockArguments(block, incoming, term->getLoc()))) + return failure(); + + moveBlockBodyBefore(block, builder); + return appendStructuredTerminator(term, builder, anchorTerminator); +} + +static LogicalResult validateSupportedCfg(Region &body) { + // Validate every reachable block before move-based construction starts, so + // unsupported terminators cannot leave a partially consumed function body. + for (Block &block : body) { + Operation *term = block.getTerminator(); + if (!isa(term) && !isSupportedReturn(term)) + return term->emitError() + << "unsupported terminator in multi-block function"; + } + return success(); +} + +/// Collects blocks reachable from the function entry through supported CFG +/// successors. Restrict successor traversal to the current region because a +/// branch-like operation must not make a nested or enclosing block reachable. +static void collectReachableBlocks(Block *block, + SmallPtrSetImpl &reachable) { + if (!reachable.insert(block).second) + return; + + for (Block *successor : getCfgSuccessors(block)) { + if (successor->getParent() == block->getParent()) + collectReachableBlocks(successor, reachable); + } +} + +/// Removes blocks that cannot be reached from the entry block before CFG +/// validation and structuring. Frontend lowering may leave detached return +/// blocks behind; they are not part of the function's executable CFG and must +/// not affect join discovery, cycle checks or the entry-terminator decision. +static void eraseUnreachableBlocks(Region &body) { + if (body.empty() || body.hasOneBlock()) + return; + + SmallPtrSet reachable; + collectReachableBlocks(&body.front(), reachable); + + SmallVector eraseBlocks; + for (Block &block : body) { + if (!reachable.contains(&block)) + eraseBlocks.push_back(&block); + } + + // Break successor and operand references before erasing. This also handles + // unreachable blocks that refer to one another, including unreachable + // cycles. + for (Block *block : eraseBlocks) { + for (Operation &op : *block) + op.dropAllReferences(); + } + for (Block *block : llvm::reverse(eraseBlocks)) + block->erase(); +} + +/// Rejects backedges before any destructive block movement occurs. General +/// restructuring of cyclic CFGs is not supported. +static LogicalResult rejectCyclicCfg(Block *block, + SmallPtrSetImpl &visiting, + SmallPtrSetImpl &visited) { + if (visited.contains(block)) + return success(); + if (!visiting.insert(block).second) + return block->getTerminator()->emitError() + << "unsupported cyclic control flow in multi-block function"; + + for (Block *successor : getCfgSuccessors(block)) { + if (successor->getParent() == block->getParent() && + failed(rejectCyclicCfg(successor, visiting, visited))) + return failure(); + } + + visiting.erase(block); + visited.insert(block); + return success(); +} + +static LogicalResult structureFunctionBody(Operation *funcOp, Region &body) { + // Validation is deliberately completed before the move-based path starts. + // From that point onward the function is rewritten as one SCF entry block and + // the consumed CFG blocks are erased only after construction succeeds. + if (body.empty() || body.hasOneBlock()) + return success(); + + eraseUnreachableBlocks(body); + if (body.hasOneBlock()) + return success(); + + if (failed(validateSupportedCfg(body))) + return failure(); + + SmallPtrSet visiting; + SmallPtrSet visited; + if (failed(rejectCyclicCfg(&body.front(), visiting, visited))) + return failure(); + + if (hasNonTreeCondBranch(body)) + return structureTerminalReturnBody(funcOp, body); + + Block &entryBlock = body.front(); + Operation *entryTerm = entryBlock.getTerminator(); + if (isSupportedReturn(entryTerm)) { + return funcOp->emitError() + << "multi-block function entry cannot terminate with return"; + } + + SmallVector eraseBlocks; + for (Block &block : llvm::drop_begin(body.getBlocks())) + eraseBlocks.push_back(&block); + + OpBuilder builder(entryTerm); + if (failed(appendStructuredTerminator(entryTerm, builder, entryTerm))) + return failure(); + + entryTerm->erase(); + for (Block *block : eraseBlocks) { + for (Operation &op : *block) + op.dropAllReferences(); + } + for (Block *block : llvm::reverse(eraseBlocks)) + block->erase(); + + return success(); +} + +} // namespace + +namespace mlir::triton::controlflow { + +LogicalResult structureCFG(ModuleOp module) { + // Collect functions first because structureFunctionBody mutates their nested + // regions. This keeps the module walk independent of those mutations. + SmallVector functions; + module.walk([&](Operation *op) { + if (isa(op)) + functions.push_back(op); + }); + + // Handle both Triton callables and ordinary func.func wrappers; declarations + // and functions erased by an enclosing transformation are skipped. + for (Operation *op : functions) { + if (!op->getParentOp()) + continue; + + if (auto funcOp = dyn_cast(op)) { + if (!funcOp.isDeclaration() && + failed(structureFunctionBody(funcOp, funcOp.getBody()))) + return failure(); + continue; + } + + if (auto funcOp = dyn_cast(op)) { + if (!funcOp.isDeclaration() && + failed(structureFunctionBody(funcOp, funcOp.getBody()))) + return failure(); + continue; + } + + auto mapOp = cast(op); + if (failed(structureFunctionBody(mapOp, mapOp.getRegion()))) + return failure(); + } + + return success(); +} + +} // namespace mlir::triton::controlflow diff --git a/third_party/ascend/lib/TritonControlFlowOpt/CMakeLists.txt b/third_party/ascend/lib/TritonControlFlowOpt/CMakeLists.txt new file mode 100644 index 0000000000..3a3692b000 --- /dev/null +++ b/third_party/ascend/lib/TritonControlFlowOpt/CMakeLists.txt @@ -0,0 +1,24 @@ +add_triton_library(TritonControlFlowOpt + BlockPtrDecompose.cpp + CFGStructuring.cpp + ControlFlowAnalysis.cpp + ControlFlowRewrite.cpp + TensorPtrDecompose.cpp + TritonControlFlowOptPass.cpp + + DEPENDS + TritonControlFlowOptPassIncGen + + LINK_LIBS PUBLIC + BiShengIRScopeDialect + MLIRArithDialect + MLIRControlFlowDialect + MLIRFuncDialect + MLIRIR + MLIRPass + MLIRSCFDialect + MLIRSideEffectInterfaces + MLIRSupport + MLIRTritonNPUUtils + TritonIR +) diff --git a/third_party/ascend/lib/TritonControlFlowOpt/ControlFlowAnalysis.cpp b/third_party/ascend/lib/TritonControlFlowOpt/ControlFlowAnalysis.cpp new file mode 100644 index 0000000000..531249281c --- /dev/null +++ b/third_party/ascend/lib/TritonControlFlowOpt/ControlFlowAnalysis.cpp @@ -0,0 +1,646 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonControlFlowOpt/ControlFlowAnalysis.h" + +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" + +#include "llvm/ADT/STLExtras.h" + +#include + +using namespace mlir; + +namespace mlir::triton::controlflow { + +namespace { + +/// Control-flow kinds whose operand/result correspondence is understood by +/// both the analyzer and the mechanical rewrite. +static bool isSupportedControlFlow(Operation *op) { + return isa(op); +} + +/// Scope rewriting deliberately supports only the frontend's single-block, +/// operand-free form. Validating the complete shape here keeps the later +/// rewrite mechanical and prevents partially rewriting an unfamiliar Scope +/// region contract. +static FailureOr +getSupportedScopeReturn(scope::ScopeOp scopeOp) { + if (scopeOp->getNumOperands() != 0 || scopeOp->getNumRegions() != 1) + return failure(); + + Region ®ion = scopeOp.getBodyRegion(); + if (!llvm::hasSingleElement(region)) + return failure(); + Block &body = region.front(); + if (body.getNumArguments() != 0) + return failure(); + + auto returnOp = dyn_cast(body.getTerminator()); + if (!returnOp || returnOp.getNumOperands() != scopeOp.getNumResults()) + return failure(); + for (auto [operand, result] : + llvm::zip(returnOp.getOperands(), scopeOp.getResults())) { + if (operand.getType() != result.getType()) + return failure(); + } + return returnOp; +} + +/// Returns true when an identity can be reused while the defining Scope is +/// replaced. Internal values disappear with the old region and therefore must +/// be represented by an expanded result instead. +static bool isDefinedOutsideScope(scope::ScopeOp scopeOp, Value value) { + if (!value) + return false; + if (Operation *definingOp = value.getDefiningOp()) + return !scopeOp->isAncestor(definingOp); + + auto blockArgument = dyn_cast(value); + if (!blockArgument) + return false; + Operation *owner = blockArgument.getOwner()->getParentOp(); + return owner != scopeOp && (!owner || !scopeOp->isAncestor(owner)); +} + +static void setResultIdentity(AnalyzedValue &value, Value result, + ArrayRef componentIndices) { + // A transferred component is represented by the SCF result outside the op; + // non-transferred components retain their incoming symbolic identities. + for (unsigned index : componentIndices) + value.components[index].identity = + ComponentIdentity::fromValue(result, index); +} + +static ControlFlowSlotAnalysis +makeSlotAnalysis(unsigned oldIndex, unsigned componentCount, + ArrayRef componentIndices, + SmallVector componentTypes, + SmallVector resultAttributes) { + // Preserve the full classification for future Recomputed support while also + // storing a compact ordered list used by the current signature rewrite. + ControlFlowSlotAnalysis slot; + slot.oldIndex = oldIndex; + slot.componentKinds.assign(componentCount, ComponentTransferKind::Invariant); + for (unsigned index : componentIndices) + slot.componentKinds[index] = ComponentTransferKind::Transferred; + slot.componentIndices.append(componentIndices.begin(), + componentIndices.end()); + slot.componentTypes = std::move(componentTypes); + slot.resultAttributes = std::move(resultAttributes); + return slot; +} + +} // namespace + +//===----------------------------------------------------------------------===// +// Shared value analysis and nested traversal +//===----------------------------------------------------------------------===// + +const ControlFlowOpAnalysis * +ControlFlowRewritePlan::lookup(Operation *op) const { + auto it = operations.find(op); + return it == operations.end() ? nullptr : &it->second; +} + +const AnalyzedValue * +ControlFlowAnalysisContext::lookupValue(Value value) const { + auto it = analyzedValues.find(value); + return it == analyzedValues.end() ? nullptr : &it->second; +} + +const ControlFlowOpAnalysis * +ControlFlowAnalysisContext::lookup(Operation *op) const { + auto it = analyzedOps.find(op); + return it == analyzedOps.end() ? nullptr : &it->second; +} + +FailureOr ControlFlowAnalysisContext::analyzeValue(Value value) { + if (const AnalyzedValue *known = lookupValue(value)) + return *known; + + // A control-flow result is meaningful only after all incoming states have + // been merged. Analyze its owner first instead of letting a pointer policy + // treat the opaque result as a new base. + if (auto result = dyn_cast(value)) { + Operation *owner = result.getOwner(); + if (isSupportedControlFlow(owner)) { + if (failed(analyzeControlFlowOp(owner))) + return failure(); + if (const AnalyzedValue *known = lookupValue(value)) + return *known; + return failure(); + } + } + + FailureOr result = policy.analyzeValue(value, *this); + if (failed(result)) + return failure(); + analyzedValues.try_emplace(value, *result); + return *result; +} + +void ControlFlowAnalysisContext::bindRegionArgument( + Value argument, const AnalyzedValue &initial, + ArrayRef componentIndices) { + // Only candidate loop components acquire a new identity at region entry. + // Policy-owned non-carried components remain traceable to the initial + // descriptor and can therefore be checked at the backedge. + AnalyzedValue argumentState = initial; + for (unsigned index : componentIndices) + argumentState.components[index].identity = + ComponentIdentity::fromValue(argument, index); + analyzedValues[argument] = std::move(argumentState); +} + +FailureOr> ControlFlowAnalysisContext::getTransferredTypes( + const AnalyzedValue &lhs, const AnalyzedValue &rhs, + ArrayRef componentIndices) const { + // Type joining is policy-owned because tensor offsets may widen i32 to i64, + // whereas block-pointer descriptor fields currently require exact equality. + SmallVector types; + types.reserve(componentIndices.size()); + for (unsigned index : componentIndices) { + if (index >= lhs.components.size() || index >= rhs.components.size()) + return failure(); + FailureOr type = policy.joinComponentTypes( + lhs.components[index].type, rhs.components[index].type); + if (failed(type)) + return failure(); + types.push_back(*type); + } + return types; +} + +LogicalResult +ControlFlowAnalysisContext::analyzeNestedOperations(Block *block, + bool &hasNestedRewrite) { + // Walk in program order so an inner result requested by a later address + // expression is already cached. Nested SCF is analyzed recursively and its + // rewrite requirement is propagated to every enclosing supported SCF op. + for (Operation &operation : block->without_terminator()) { + if (isSupportedControlFlow(&operation)) { + FailureOr nested = + analyzeControlFlowOp(&operation); + if (failed(nested)) + return failure(); + hasNestedRewrite |= nested->needsRewrite(); + continue; + } + + // SCF may be wrapped in an ordinary region-owning operation. Such regions + // do not change pointer schemas, but ControlFlowRewrite does not yet clone + // arbitrary region operations recursively. Reject an affected nested SCF + // instead of reporting success and leaving it opaque in the rewritten IR. + for (Region ®ion : operation.getRegions()) { + for (Block &nestedBlock : region) { + bool regionNeedsRewrite = false; + if (failed(analyzeNestedOperations(&nestedBlock, regionNeedsRewrite)) || + regionNeedsRewrite) + return failure(); + } + } + } + return success(); +} + +//===----------------------------------------------------------------------===// +// scf.for schema analysis +//===----------------------------------------------------------------------===// + +FailureOr +ControlFlowAnalysisContext::analyzeFor(Operation *operation) { + auto forOp = cast(operation); + auto yieldOp = cast(forOp.getBody()->getTerminator()); + if (forOp.getInitArgs().size() != forOp.getRegionIterArgs().size() || + yieldOp.getNumOperands() != forOp.getRegionIterArgs().size()) + return failure(); + + SmallVector> initialStates( + forOp.getInitArgs().size()); + + // Bind an abstract component state to each pointer region argument. The body + // can then be analyzed recursively without constructing a replacement loop. + for (auto [index, iterArg] : llvm::enumerate(forOp.getRegionIterArgs())) { + if (!policy.isDecompositionTarget(iterArg)) + continue; + FailureOr initial = analyzeValue(forOp.getInitArgs()[index]); + if (failed(initial)) + return failure(); + FailureOr> candidates = + policy.getLoopCandidateComponents(*initial); + if (failed(candidates)) + return failure(); + initialStates[index] = *initial; + bindRegionArgument(iterArg, *initial, *candidates); + } + + ControlFlowOpAnalysis result; + if (failed(analyzeNestedOperations(forOp.getBody(), result.hasNestedRewrite))) + return failure(); + + // Compare the abstract init, region-argument and yielded states only after + // the complete body (including nested SCF) has been analyzed. + for (auto [index, iterArg] : llvm::enumerate(forOp.getRegionIterArgs())) { + if (!initialStates[index]) + continue; + FailureOr next = analyzeValue(yieldOp.getOperand(index)); + const AnalyzedValue *argument = lookupValue(iterArg); + if (failed(next) || !argument) + return failure(); + FailureOr> transferred = + policy.getLoopTransferredComponents(*initialStates[index], *argument, + *next); + if (failed(transferred)) + return failure(); + + AnalyzedValue resultState = *initialStates[index]; + // A decomposition-target slot remains in the plan even when every + // component is invariant. An empty component list means remove the old + // pointer slot and rebuild it from the initial descriptor without adding + // replacement loop operands/results. + FailureOr> types = + getTransferredTypes(*initialStates[index], *next, *transferred); + if (failed(types)) + return failure(); + FailureOr> resultAttributes = + policy.mergeControlFlowAttributes(*initialStates[index], *next); + if (failed(resultAttributes)) + return failure(); + resultState.attributes = *resultAttributes; + for (auto [component, type] : llvm::zip(*transferred, *types)) + resultState.components[component].type = type; + result.slots.push_back( + makeSlotAnalysis(index, resultState.components.size(), *transferred, + std::move(*types), std::move(*resultAttributes))); + setResultIdentity(resultState, forOp.getResult(index), *transferred); + analyzedValues[forOp.getResult(index)] = std::move(resultState); + } + + return result; +} + +//===----------------------------------------------------------------------===// +// scf.while schema analysis +//===----------------------------------------------------------------------===// + +FailureOr +ControlFlowAnalysisContext::analyzeWhile(Operation *operation) { + auto whileOp = cast(operation); + scf::ConditionOp conditionOp = whileOp.getConditionOp(); + scf::YieldOp yieldOp = whileOp.getYieldOp(); + if (whileOp.getInits().size() != whileOp.getBeforeArguments().size() || + conditionOp.getArgs().size() != whileOp.getAfterArguments().size() || + yieldOp.getNumOperands() != whileOp.getBeforeArguments().size()) + return failure(); + + SmallVector> initialStates( + whileOp.getInits().size()); + SmallVector> candidateIndices( + whileOp.getInits().size()); + + // Bind the initial descriptor to the before-region arguments first. The + // condition and backedge are analyzed as two consecutive state transfers. + for (auto [index, beforeArg] : + llvm::enumerate(whileOp.getBeforeArguments())) { + if (!policy.isDecompositionTarget(beforeArg)) + continue; + FailureOr initial = analyzeValue(whileOp.getInits()[index]); + if (failed(initial)) + return failure(); + FailureOr> candidates = + policy.getLoopCandidateComponents(*initial); + if (failed(candidates)) + return failure(); + initialStates[index] = *initial; + candidateIndices[index] = *candidates; + bindRegionArgument(beforeArg, *initial, *candidates); + } + + ControlFlowOpAnalysis result; + if (failed(analyzeNestedOperations(whileOp.getBeforeBody(), + result.hasNestedRewrite))) + return failure(); + + // The after-region argument receives the value forwarded by scf.condition. + // Bind it before visiting the after region so address expressions there can + // recursively resolve the same abstract schema. + SmallVector> conditionStates( + conditionOp.getArgs().size()); + for (auto [index, afterArg] : llvm::enumerate(whileOp.getAfterArguments())) { + if (!initialStates[index]) + continue; + FailureOr condition = + analyzeValue(conditionOp.getArgs()[index]); + if (failed(condition)) + return failure(); + conditionStates[index] = *condition; + bindRegionArgument(afterArg, *condition, candidateIndices[index]); + } + + if (failed(analyzeNestedOperations(whileOp.getAfterBody(), + result.hasNestedRewrite))) + return failure(); + + for (auto [index, beforeArg] : + llvm::enumerate(whileOp.getBeforeArguments())) { + if (!initialStates[index]) + continue; + FailureOr next = analyzeValue(yieldOp.getOperand(index)); + const AnalyzedValue *argument = lookupValue(beforeArg); + if (failed(next) || !argument || !conditionStates[index]) + return failure(); + + // The policy merge is monotone over {Invariant, Transferred}: once either + // region changes a candidate component it must be present in the loop + // signature. Re-running the union would not remove a transferred bit, so + // this is the fixed point for the current two-state domain. + FailureOr> fromCondition = + policy.getLoopTransferredComponents(*initialStates[index], *argument, + *conditionStates[index]); + const AnalyzedValue *afterArgument = + lookupValue(whileOp.getAfterArguments()[index]); + if (!afterArgument || failed(fromCondition)) + return failure(); + FailureOr> fromBackedge = + policy.getLoopTransferredComponents(*conditionStates[index], + *afterArgument, *next); + if (failed(fromBackedge)) + return failure(); + + SmallVector transferred = *fromCondition; + for (unsigned component : *fromBackedge) { + if (!llvm::is_contained(transferred, component)) + transferred.push_back(component); + } + llvm::sort(transferred); + + AnalyzedValue resultState = *initialStates[index]; + FailureOr> types = getTransferredTypes( + *initialStates[index], *conditionStates[index], transferred); + if (failed(types)) + return failure(); + FailureOr> resultAttributes = + policy.mergeControlFlowAttributes(*initialStates[index], + *conditionStates[index]); + if (failed(resultAttributes)) + return failure(); + for (auto [component, type] : llvm::zip(transferred, *types)) { + FailureOr joined = + policy.joinComponentTypes(type, next->components[component].type); + if (failed(joined)) + return failure(); + type = *joined; + resultState.components[component].type = type; + } + // The initial/condition schema is already the fixed point for the common + // analyzer. TensorPtr policy metadata is monotone and is joined again + // against the yielded state below. + AnalyzedValue conditionMergedState = resultState; + conditionMergedState.attributes = *resultAttributes; + FailureOr> finalAttributes = + policy.mergeControlFlowAttributes(conditionMergedState, *next); + if (failed(finalAttributes)) + return failure(); + resultState.attributes = *finalAttributes; + result.slots.push_back( + makeSlotAnalysis(index, resultState.components.size(), transferred, + std::move(*types), std::move(*finalAttributes))); + setResultIdentity(resultState, whileOp.getResult(index), transferred); + analyzedValues[whileOp.getResult(index)] = std::move(resultState); + } + + return result; +} + +//===----------------------------------------------------------------------===// +// scf.if schema analysis +//===----------------------------------------------------------------------===// + +FailureOr +ControlFlowAnalysisContext::analyzeIf(Operation *operation) { + auto ifOp = cast(operation); + ControlFlowOpAnalysis result; + + if (failed( + analyzeNestedOperations(ifOp.thenBlock(), result.hasNestedRewrite))) + return failure(); + if (ifOp.elseBlock() && failed(analyzeNestedOperations( + ifOp.elseBlock(), result.hasNestedRewrite))) + return failure(); + + if (ifOp.getNumResults() == 0) + // A result-less if may still need rebuilding solely to rewrite nested SCF. + return result; + if (!ifOp.elseBlock()) + return failure(); + + scf::YieldOp thenYield = ifOp.thenYield(); + scf::YieldOp elseYield = ifOp.elseYield(); + if (thenYield.getNumOperands() != ifOp.getNumResults() || + elseYield.getNumOperands() != ifOp.getNumResults()) + return failure(); + + // Each result position is independent. Only policy-matching pointer results + // are expanded; all other result positions keep their original type/order. + for (auto [index, opResult] : llvm::enumerate(ifOp.getResults())) { + if (!policy.isDecompositionTarget(opResult)) + continue; + FailureOr thenState = + analyzeValue(thenYield.getOperand(index)); + FailureOr elseState = + analyzeValue(elseYield.getOperand(index)); + if (failed(thenState) || failed(elseState)) + return failure(); + FailureOr> transferred = + policy.getIfTransferredComponents(*thenState, *elseState); + if (failed(transferred)) + return failure(); + + AnalyzedValue resultState = *thenState; + FailureOr> types = + getTransferredTypes(*thenState, *elseState, *transferred); + if (failed(types)) + return failure(); + FailureOr> resultAttributes = + policy.mergeControlFlowAttributes(*thenState, *elseState); + if (failed(resultAttributes)) + return failure(); + resultState.attributes = *resultAttributes; + for (auto [component, type] : llvm::zip(*transferred, *types)) + resultState.components[component].type = type; + result.slots.push_back( + makeSlotAnalysis(index, resultState.components.size(), *transferred, + std::move(*types), std::move(*resultAttributes))); + setResultIdentity(resultState, opResult, *transferred); + analyzedValues[opResult] = std::move(resultState); + } + + return result; +} + +//===----------------------------------------------------------------------===// +// scope.scope schema analysis +//===----------------------------------------------------------------------===// + +FailureOr +ControlFlowAnalysisContext::analyzeScope(Operation *operation) { + auto scopeOp = cast(operation); + FailureOr returnOp = getSupportedScopeReturn(scopeOp); + if (failed(returnOp)) + return failure(); + + ControlFlowOpAnalysis result; + Block &body = scopeOp.getBodyRegion().front(); + if (failed(analyzeNestedOperations(&body, result.hasNestedRewrite))) + return failure(); + + // Scope has no incoming region argument and no backedge. Each pointer result + // is therefore described by the value returned from the body. The policy + // chooses the components that become explicit Scope results; every omitted + // component must already name an SSA value defined outside this Scope. + for (auto [index, scopeResult] : llvm::enumerate(scopeOp.getResults())) { + if (!policy.isDecompositionTarget(scopeResult)) + continue; + + FailureOr returned = + analyzeValue(returnOp->getOperand(index)); + if (failed(returned) || returned->originalType != scopeResult.getType()) + return failure(); + FailureOr> candidates = + policy.getScopeCandidateComponents(*returned); + if (failed(candidates)) + return failure(); + + SmallVector isCandidate(returned->components.size(), false); + std::optional previousIndex; + for (unsigned componentIndex : *candidates) { + if (componentIndex >= returned->components.size() || + (previousIndex && componentIndex <= *previousIndex)) + return failure(); + isCandidate[componentIndex] = true; + previousIndex = componentIndex; + } + + for (auto [componentIndex, component] : + llvm::enumerate(returned->components)) { + if (isCandidate[componentIndex]) + continue; + if (component.identity.kind != ComponentIdentity::Kind::Value || + !isDefinedOutsideScope(scopeOp, component.identity.value)) + return failure(); + } + + SmallVector componentTypes; + componentTypes.reserve(candidates->size()); + for (unsigned componentIndex : *candidates) + componentTypes.push_back(returned->components[componentIndex].type); + + AnalyzedValue resultState = *returned; + result.slots.push_back( + makeSlotAnalysis(index, resultState.components.size(), *candidates, + std::move(componentTypes), resultState.attributes)); + setResultIdentity(resultState, scopeResult, *candidates); + analyzedValues[scopeResult] = std::move(resultState); + } + + return result; +} + +//===----------------------------------------------------------------------===// +// Stage-wide caching, plan freezing and entry-point discovery +//===----------------------------------------------------------------------===// + +FailureOr +ControlFlowAnalysisContext::analyzeControlFlowOp(Operation *op) { + if (const ControlFlowOpAnalysis *known = lookup(op)) + return *known; + // The in-progress set prevents accidental cyclic re-entry when value + // analysis asks to analyze the control-flow op that owns that value. + if (!isSupportedControlFlow(op) || !operationsBeingAnalyzed.insert(op).second) + return failure(); + + FailureOr result = failure(); + if (isa(op)) + result = analyzeFor(op); + else if (isa(op)) + result = analyzeWhile(op); + else if (isa(op)) + result = analyzeIf(op); + else if (isa(op)) + result = analyzeScope(op); + + operationsBeingAnalyzed.erase(op); + if (failed(result)) + return failure(); + analyzedOps.try_emplace(op, *result); + return *result; +} + +ControlFlowRewritePlan ControlFlowAnalysisContext::takeRewritePlan() && { + // analyzedValues intentionally dies with this context. Rewrite only needs + // the position/type decisions and must not retain Value handles that may be + // invalidated as earlier roots are replaced. + return ControlFlowRewritePlan{std::move(analyzedOps)}; +} + +SmallVector collectOutermostControlFlowOps(ModuleOp module) { + // Rewriting an outer root recursively replaces all affected descendants. + // Returning nested ops as independent roots would leave stale pointers after + // the parent is erased, so filter them here instead of relying on walk order. + SmallVector roots; + module.walk([&](Operation *operation) { + if (!isSupportedControlFlow(operation)) + return; + for (Operation *parent = operation->getParentOp(); parent; + parent = parent->getParentOp()) { + if (isSupportedControlFlow(parent)) + return; + } + roots.push_back(operation); + }); + return roots; +} + +FailureOr +analyzeControlFlow(ModuleOp module, const ControlFlowAnalysisPolicy &policy) { + SmallVector roots = collectOutermostControlFlowOps(module); + ControlFlowAnalysisContext context(policy); + + // One context covers the complete decomposition stage. Consequently a + // later root can reuse the merged state of a preceding sibling control-flow + // op, and no IR is mutated until every root has proved analyzable. + for (Operation *root : roots) { + if (failed(context.analyzeControlFlowOp(root))) { + root->emitError( + "failed to analyze pointer components across control flow"); + return failure(); + } + } + + return std::move(context).takeRewritePlan(); +} + +} // namespace mlir::triton::controlflow diff --git a/third_party/ascend/lib/TritonControlFlowOpt/ControlFlowRewrite.cpp b/third_party/ascend/lib/TritonControlFlowOpt/ControlFlowRewrite.cpp new file mode 100644 index 0000000000..529dca83eb --- /dev/null +++ b/third_party/ascend/lib/TritonControlFlowOpt/ControlFlowRewrite.cpp @@ -0,0 +1,1429 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonControlFlowOpt/ControlFlowRewrite.h" +#include "Utils/Utils.h" + +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include + +using namespace mlir; +using mlir::triton::controlflow::ControlFlowOpAnalysis; +using mlir::triton::controlflow::ControlFlowRewriteContext; +using mlir::triton::controlflow::ControlFlowRewritePlan; +using mlir::triton::controlflow::ControlFlowRewritePolicy; +using mlir::triton::controlflow::ControlFlowSlotAnalysis; +using mlir::triton::controlflow::DecomposedValue; +using mlir::triton::controlflow::kPointerDescriptorBoundaryAttr; +using mlir::triton::controlflow::kPointerDescriptorRebuildAttr; + +namespace mlir::triton::controlflow { + +Value ControlFlowRewriteContext::remap(Value value) const { + if (Value mapped = valueMapping.lookupOrNull(value)) + return mapped; + return value; +} + +const DecomposedValue *ControlFlowRewriteContext::lookup(Value value) const { + auto it = decomposedValues.find(value); + return it == decomposedValues.end() ? nullptr : &it->second; +} + +} // namespace mlir::triton::controlflow + +namespace { + +// Keep the mechanical structured-control-flow and Scope rewrite in one +// translation unit. These handlers are mutually recursive through +// rewriteBodyOps(), share one +// short-lived RewriteEnv, and must agree on signature expansion, nested-op +// ordering and failure cleanup. Splitting them by op kind would expose those +// private constraints through additional internal headers without creating an +// independently reusable component. +// +//===----------------------------------------------------------------------===// +// Per-rewrite state and generic component manipulation +//===----------------------------------------------------------------------===// + +/// Carries everything needed to translate values from one original +/// control-flow path into its replacement path. +/// +/// valueMapping answers which new SSA value replaces an old SSA value. +/// decomposedValues remembers the policy-specific pieces of an old pointer. +/// policy defines what those pieces mean and how to rebuild the pointer, +/// while plan says which control-flow values must be expanded into such pieces. +/// +/// For example, after a block-pointer loop argument is expanded into scalar +/// offsets, a region-local environment can contain: +/// valueMapping: %old_ptr_arg -> %rebuilt_ptr +/// decomposedValues: %old_ptr_arg -> { +/// components = [%base, %shape0, %stride0, %new_offset0], +/// attributes = [order] +/// } +/// Operations cloned into that region use the mapping, while pointer +/// decomposition uses the stored components. +struct RewriteEnv { + /// Starts a rewrite path with no old-to-new mappings or decomposed values. + /// The referenced policy and analysis plan are shared by child environments; + /// only the two mutable state tables above are copied for each nested region. + /// + /// For example, the top-level environment may enter a rewritten scf.for. + /// The loop body copies it, then adds mappings from the old body arguments to + /// the new body arguments without changing the state of sibling regions: + /// RewriteEnv env(blockPtrPolicy, rewritePlan); + /// RewriteEnv bodyEnv = env; + /// bodyEnv.valueMapping.map(oldBodyArg, newBodyArg); + RewriteEnv(const ControlFlowRewritePolicy &policy, + const ControlFlowRewritePlan &plan) + : policy(policy), plan(plan) {} + + /// Gives a decomposition policy read-only access to this path's two state + /// tables. The context can resolve an old SSA value to its replacement and + /// retrieve a previously stored DecomposedValue; it does not own or copy + /// either table and therefore must not outlive this environment. + /// + /// For example, while the block-pointer policy decomposes this operation, + /// it needs both the replacement delta and the saved state of the input: + /// %next = tt.advance %old_ptr, [%old_delta] + /// + /// valueMapping: %old_delta -> %new_delta + /// decomposedValues: %old_ptr -> ptrInfo + /// + /// auto context = bodyEnv.getRewriteContext(); + /// context.remap(oldDelta); // The Value for %new_delta. + /// context.lookup(oldPtr); // A pointer to ptrInfo. + ControlFlowRewriteContext getRewriteContext() const { + return ControlFlowRewriteContext(valueMapping, decomposedValues); + } + + /// Translates one SSA value referenced by the original IR into the value that + /// must be used in the replacement IR. It queries valueMapping; a value + /// defined outside the rewritten area is already valid and is returned + /// unchanged when the table has no entry. + /// + /// This prevents a newly built operation from referring back to a block + /// argument or result owned by the old region. For example: + /// valueMapping: %old_iter_arg -> %new_iter_arg + /// + /// // Original: scf.yield %old_iter_arg, %outer_value + /// newYieldOperands = { + /// bodyEnv.remap(oldIterArg), // The Value for %new_iter_arg. + /// bodyEnv.remap(outerValue) // The unchanged %outer_value. + /// }; + /// The returned values are then used as operands of the replacement yield. + Value remap(Value value) const { return getRewriteContext().remap(value); } + + /// Asks the active policy to express one high-level SSA value as the runtime + /// components that may cross an expanded supported control-flow boundary. + /// value is the original pointer-like SSA value. The policy + /// receives the current rewrite context so it can reuse a known decomposition + /// and remap operands to the replacement IR. It may use builder and loc + /// to insert scalar address arithmetic at the correct rewrite position. + /// + /// For the block-pointer policy, decomposing an advance conceptually changes + /// only its offset components; base, shape, strides, and order are preserved: + /// %next = tt.advance %ptr, [%delta0, %delta1] + /// + /// decomposeValue(%next) -> DecomposedValue { + /// components = [%base, %shape0, %shape1, %stride0, %stride1, + /// %offset0 + %delta0, %offset1 + %delta1], + /// attributes = [order] + /// } + /// The caller can put selected components into a new control-flow signature + /// or pass the whole descriptor to policy.recompose(). Unsupported values + /// or inconsistent component layouts return failure. + FailureOr decomposeValue(Value value, OpBuilder &builder, + Location loc) const { + return policy.decompose(value, getRewriteContext(), builder, loc); + } + + /// Records both ways in which later rewriting must understand an original + /// value. oldValue is the key from the original IR, info is its component + /// descriptor, and rebuiltValue is the pointer-like SSA value created in the + /// replacement IR. Pointer-aware code may encounter either SSA value, so + /// both keys resolve to info; ordinary cloned users read rebuiltValue through + /// valueMapping. + /// + /// For example, after rebuilding an expanded loop argument: + /// %rebuilt_ptr = tt.make_tensor_ptr %base, ... %new_offset ... + /// bodyEnv.recordDecomposition(oldPtrArg, ptrInfo, rebuiltPtr); + /// + /// // An ordinary cloned tt.load receives %rebuilt_ptr. + /// bodyEnv.remap(oldPtrArg); + /// + /// // A later tt.advance/yield reuses the flattened ptrInfo directly. + /// bodyEnv.getRewriteContext().lookup(oldPtrArg); + void recordDecomposition(Value oldValue, const DecomposedValue &info, + Value rebuiltValue) { + decomposedValues[oldValue] = info; + // A later cloned operation may consume the rebuilt value directly rather + // than the original value. Cache the same descriptor under that SSA value + // so chained pointer producers reuse the existing decomposition when the + // active policy says that the rebuilt representation is downstream-safe. + if (policy.shouldCacheRebuiltDecomposition(info)) + decomposedValues[rebuiltValue] = info; + valueMapping.map(oldValue, rebuiltValue); + } + + // Maps values from the original region to values in the replacement region. + IRMapping valueMapping; + // Concrete component state keyed by original and rebuilt pointer values. + // Keeping this alongside the mapping lets chained pointer producers and + // nested rewrites reuse an already materialized descriptor. + DenseMap decomposedValues; + const ControlFlowRewritePolicy &policy; + const ControlFlowRewritePlan &plan; +}; + +// RewriteEnv is copied when entering a newly built region. The copy inherits +// mappings visible at the region boundary and records additional mappings only +// for that recursive rewrite. Nothing is stored on the IR or shared between +// decomposition policies. + +struct LoopPointerInfo { + // Original iter-argument/result position before signature expansion. + unsigned oldIndex = 0; + // Concrete descriptor used as the reconstruction template. + DecomposedValue initInfo; + // Ordered schema decided by ControlFlowSlotAnalysis. + SmallVector componentIndices; + SmallVector componentTypes; + // Policy metadata after all loop incoming paths have been normalized. + SmallVector resultAttributes; + // Positions occupied by those components in the replacement operation. + SmallVector newIndices; +}; + +struct IfPointerInfo { + unsigned oldIndex = 0; + SmallVector componentIndices; + SmallVector componentTypes; + SmallVector resultAttributes; + std::optional thenInfo; +}; + +struct ScopePointerInfo { + unsigned oldIndex = 0; + SmallVector componentIndices; + SmallVector componentTypes; + SmallVector resultAttributes; + std::optional returnedInfo; +}; + +/// Returns the terminator of the single-block Scope form supported by the +/// dedicated rewrite. The analyzer already checked these conditions, but the +/// frozen plan is consumed after analysis, so the rewrite validates them again +/// before creating replacement IR. +static FailureOr +getSupportedScopeReturn(scope::ScopeOp scopeOp) { + if (scopeOp->getNumOperands() != 0 || scopeOp->getNumRegions() != 1) + return failure(); + Region ®ion = scopeOp.getBodyRegion(); + if (!llvm::hasSingleElement(region)) + return failure(); + Block &body = region.front(); + if (body.getNumArguments() != 0) + return failure(); + + auto returnOp = dyn_cast(body.getTerminator()); + if (!returnOp || returnOp.getNumOperands() != scopeOp.getNumResults()) + return failure(); + for (auto [operand, result] : + llvm::zip(returnOp.getOperands(), scopeOp.getResults())) { + if (operand.getType() != result.getType()) + return failure(); + } + return returnOp; +} + +// Updates the downstream descriptor marker after a loop signature expansion. +// Existing slots are remapped through oldToNewStart before slots introduced by +// the active policy are merged. The array is always expressed in the +// replacement loop's iter-argument/result coordinate space. +// +// Example: +// old slots = [1], oldToNewStart = [0, 1, 3] +// the active policy expands old slot 2 into new slots [3, 4] +// result = [1, 3, 4] +static LogicalResult updatePointerDescriptorBoundaryMarker( + Operation *loop, ArrayRef pointerInfos, + ArrayRef oldToNewStart, const ControlFlowRewritePolicy &policy) { + SmallVector descriptorSlots; + bool ownsPointerBoundary = false; + llvm::SmallDenseSet seenSlots; + auto appendSlot = [&](unsigned slot) -> LogicalResult { + if (slot > static_cast(std::numeric_limits::max())) + return failure(); + if (!seenSlots.insert(slot).second) + return failure(); + descriptorSlots.push_back(static_cast(slot)); + return success(); + }; + + if (Attribute oldMarker = loop->getAttr(kPointerDescriptorBoundaryAttr)) { + ownsPointerBoundary = true; + auto oldSlots = dyn_cast(oldMarker); + if (!oldSlots) + return failure(); + for (int32_t oldSlot : oldSlots.asArrayRef()) { + if (oldSlot < 0 || + static_cast(oldSlot) >= oldToNewStart.size() || + failed(appendSlot(oldToNewStart[oldSlot]))) + return failure(); + } + } + + if (policy.requiresPointerDescriptorBoundaryMarker()) { + ownsPointerBoundary |= !pointerInfos.empty(); + for (const LoopPointerInfo &pointerInfo : pointerInfos) { + for (unsigned newSlot : pointerInfo.newIndices) { + if (failed(appendSlot(newSlot))) + return failure(); + } + } + } + + if (!ownsPointerBoundary) { + loop->removeAttr(kPointerDescriptorBoundaryAttr); + return success(); + } + llvm::sort(descriptorSlots); + loop->setAttr(kPointerDescriptorBoundaryAttr, + DenseI32ArrayAttr::get(loop->getContext(), descriptorSlots)); + return success(); +} + +// Marks policy-created pointer descriptor reconstructions. Their defining +// operation consumes the complete descriptor, including invariant components +// omitted from a replacement SCF signature. TritonToLinalg uses these operands +// as precise preservation roots and removes the marker after conversion. +static LogicalResult +markPointerDescriptorRebuild(Value rebuiltPointer, const DecomposedValue &value, + const ControlFlowRewritePolicy &policy) { + if (!policy.requiresPointerDescriptorBoundaryMarker()) + return success(); + Operation *definingOp = rebuiltPointer.getDefiningOp(); + if (!definingOp) + return failure(); + definingOp->setAttr(kPointerDescriptorRebuildAttr, + UnitAttr::get(definingOp->getContext())); + return policy.annotatePointerDescriptorRebuild(definingOp, value); +} + +// Copies the values selected by indices into a new owning vector while +// preserving their input order. Indices must be unique and in bounds; this +// function reports failure instead of deduplicating or accessing invalid input. +// `sourceValues` is only borrowed while this function executes. +// +// Example: sourceValues = [shape, stride, offset] and indices = [2, 0, 1] +// produce [offset, shape, stride]. Indices [2, 0, 2] produce failure. +static FailureOr> gatherValues(ValueRange sourceValues, + ArrayRef indices) { + SmallVector values; + values.reserve(indices.size()); + llvm::SmallDenseSet seenIndices; + for (unsigned index : indices) { + if (index >= sourceValues.size() || !seenIndices.insert(index).second) + return failure(); + values.push_back(sourceValues[index]); + } + return values; +} + +// Returns a copy of decomposition with selected component values replaced. +// componentIndices and replacements are paired by position. The replacement +// fails if the ranges have different sizes, an index is out of bounds, or a +// replacement changes the component type; the input object remains unchanged. +// +// Example: +// decomposition.components = [base, shape, stride, originalOffset] +// componentIndices = [3], replacements = [nextOffset] +// result.components = [base, shape, stride, nextOffset] +static FailureOr +withReplacedComponents(DecomposedValue decomposition, + ArrayRef componentIndices, + ArrayRef replacements) { + if (componentIndices.size() != replacements.size()) + return failure(); + for (auto [componentIndex, replacement] : + llvm::zip(componentIndices, replacements)) { + if (componentIndex >= decomposition.components.size() || + decomposition.components[componentIndex].getType() != + replacement.getType()) + return failure(); + decomposition.components[componentIndex] = replacement; + } + return decomposition; +} + +static LogicalResult castPlannedComponents(DecomposedValue &value, + ArrayRef componentIndices, + ArrayRef componentTypes, + OpBuilder &builder, Location loc) { + if (componentIndices.size() != componentTypes.size()) + return failure(); + for (auto [index, type] : llvm::zip(componentIndices, componentTypes)) { + if (index >= value.components.size()) + return failure(); + FailureOr component = + castIntegerLike(builder, loc, value.components[index], type); + if (failed(component)) + return failure(); + value.components[index] = *component; + } + return success(); +} + +//===----------------------------------------------------------------------===// +// Shared recursive body rewrite +//===----------------------------------------------------------------------===// + +// Returns the first pointer descriptor whose oldIndex matches the requested +// position in the original control-flow signature. The range is an lvalue +// reference so the returned pointer refers to caller-owned storage, and the +// return type preserves whether that range exposes mutable or const elements. +// A missing index returns nullptr; duplicate indices keep first-match behavior. +// +// Example: [{oldIndex = 1}, {oldIndex = 3}] queried with 3 returns a pointer to +// the second element, while a query for 2 returns nullptr. +template +static auto findPointerInfoByOldIndex(InfoRange &pointerInfos, + unsigned oldIndex) + -> decltype(pointerInfos.data()) { + for (auto &info : pointerInfos) { + if (info.oldIndex == oldIndex) + return &info; + } + return nullptr; +} + +// A replacement loop carries policy-selected scalar or tensor descriptor +// components instead of the original pointer iter-argument. BlockPtr selects +// every component that analysis found dynamic; TensorPtr selects only the +// base/offset fields whose symbolic identities change at that boundary. +// Operations cloned from the original body still expect one pointer-typed +// block argument, so this function reconstructs that pointer at the replacement +// region entry. +// `pointerInfo.newIndices` selects the current component values from +// `newRegionArguments`, while +// `pointerInfo.componentIndices` identifies the descriptor fields that those +// values replace. The untouched fields come from `pointerInfo.initInfo`. +// +// On success, this function updates both parts of `regionEnv`: remapping the +// old region argument produces the rebuilt pointer, and looking up its +// decomposition returns the descriptor containing the current components. +// Operations cloned later in the same region can therefore use the complete +// pointer or its flattened state without referring to the old loop block. +// +// Example: +// oldRegionArgument = %old_ptr +// newRegionArguments = [%ordinary, %base, %shape, %stride, %offset] +// pointerInfo.newIndices = [1, 2, 3, 4] +// pointerInfo.componentIndices = [0, 1, 2, 3] +// pointerInfo.initInfo.components = +// [initial_base, initial_shape, initial_stride, initial_offset] +// +// The rebuilt BlockPtr descriptor replaces all four fields with the current +// loop values and records `%old_ptr -> %rebuilt_ptr` in `regionEnv`. Invalid +// indices, incompatible component types, or a policy that cannot recompose the +// descriptor return failure without recording a partial binding; the enclosing +// loop rewrite owns cleanup of inserted IR. +static LogicalResult bindLoopCarriedPointer(Value oldRegionArgument, + const LoopPointerInfo &pointerInfo, + ValueRange newRegionArguments, + OpBuilder &builder, Location loc, + RewriteEnv ®ionEnv) { + FailureOr> carriedComponentValues = + gatherValues(newRegionArguments, pointerInfo.newIndices); + if (failed(carriedComponentValues)) + return failure(); + + FailureOr argumentInfo = + withReplacedComponents(pointerInfo.initInfo, pointerInfo.componentIndices, + *carriedComponentValues); + if (failed(argumentInfo) || failed(regionEnv.policy.normalizeLoopCarriedValue( + *argumentInfo, pointerInfo.componentIndices, + pointerInfo.resultAttributes, builder, loc))) + return failure(); + + Value rebuiltPointer = + regionEnv.policy.recompose(*argumentInfo, builder, loc); + if (!rebuiltPointer || failed(markPointerDescriptorRebuild( + rebuiltPointer, *argumentInfo, regionEnv.policy))) + return failure(); + + regionEnv.recordDecomposition(oldRegionArgument, *argumentInfo, + rebuiltPointer); + return success(); +} + +// Binds every original loop region argument to its replacement-region state. +// A pointer slot delegates to bindLoopCarriedPointer because one original +// pointer may occupy several component positions in the new signature. An +// ordinary slot keeps one SSA value and is mapped through oldToNewStart. +// +// Example: +// oldRegionArguments = [%x, %old_ptr, %y] +// newRegionArguments = [%new_x, %offset0, %offset1, %new_y] +// pointerInfos = [{oldIndex = 1, newIndices = [1, 2]}] +// oldToNewStart = [0, 1, 3] +// +// The resulting environment maps %x to %new_x and %y to %new_y. For +// %old_ptr, it gathers %offset0 and %offset1, rebuilds the complete pointer, +// and records both its SSA mapping and current decomposition. +// +// The function deliberately visits every argument after a binding failure. +// The surrounding builder callback may still need all available mappings to +// construct structurally valid temporary IR before its new loop is erased. +// It therefore accumulates failure and reports it only after the full range. +static LogicalResult bindLoopRegionArguments( + ValueRange oldRegionArguments, ValueRange newRegionArguments, + ArrayRef pointerInfos, ArrayRef oldToNewStart, + OpBuilder &builder, Location loc, RewriteEnv ®ionEnv) { + bool allArgumentsBound = true; + for (auto [oldIndex, oldRegionArgument] : + llvm::enumerate(oldRegionArguments)) { + const LoopPointerInfo *pointerInfo = + findPointerInfoByOldIndex(pointerInfos, oldIndex); + if (pointerInfo) { + if (failed(bindLoopCarriedPointer(oldRegionArgument, *pointerInfo, + newRegionArguments, builder, loc, + regionEnv))) + allArgumentsBound = false; + continue; + } + + if (oldIndex >= oldToNewStart.size() || + oldToNewStart[oldIndex] >= newRegionArguments.size()) { + allArgumentsBound = false; + continue; + } + regionEnv.valueMapping.map(oldRegionArgument, + newRegionArguments[oldToNewStart[oldIndex]]); + } + return allArgumentsBound ? success() : failure(); +} + +// Rewrites one original loop terminator operand list to the expanded signature +// used by the replacement loop. Ordinary operands remain one value and are +// remapped through `regionEnv`. A pointer operand is decomposed, normalized to +// the component types frozen by analysis, and expanded into the components +// selected by its `LoopPointerInfo`. +// +// Example: +// oldOperands = [%next_ptr, %sum] +// pointerInfo = { +// oldIndex = 0, componentIndices = [0, 1, 2, 3], +// newIndices = [0, 1, 2, 3] +// } +// currentRegionArguments = +// [%current_base, %current_shape, %current_stride, %current_offset, +// %sum_arg] +// +// A valid `%next_ptr` decomposition produces +// `[%next_base, %next_shape, %next_stride, %next_offset, %mapped_sum]`. If +// pointer decomposition or component normalization fails, the output instead +// uses the four current descriptor arguments followed by `%mapped_sum`. The +// fallback keeps the temporary scf.yield/scf.condition structurally complete +// until the enclosing failed loop rewrite erases it. +// +// The output vector is separate from the LogicalResult intentionally. The +// function visits every old operand and fills all available fallback positions +// even after a pointer failure, then reports whether every pointer succeeded. +static LogicalResult rewriteLoopTerminatorOperands( + ValueRange oldOperands, ValueRange currentRegionArguments, + ArrayRef pointerInfos, OpBuilder &builder, Location loc, + RewriteEnv ®ionEnv, SmallVectorImpl &newOperands) { + bool allOperandsValid = true; + newOperands.clear(); + newOperands.reserve(currentRegionArguments.size()); + + auto appendFallbackComponents = [&](const LoopPointerInfo &pointerInfo) { + for (unsigned newIndex : pointerInfo.newIndices) + newOperands.push_back(currentRegionArguments[newIndex]); + }; + + for (auto [oldIndex, oldOperand] : llvm::enumerate(oldOperands)) { + const LoopPointerInfo *pointerInfo = + findPointerInfoByOldIndex(pointerInfos, oldIndex); + if (!pointerInfo) { + newOperands.push_back(regionEnv.remap(oldOperand)); + continue; + } + + FailureOr nextInfo = + regionEnv.decomposeValue(oldOperand, builder, loc); + if (failed(nextInfo) || + failed(castPlannedComponents(*nextInfo, pointerInfo->componentIndices, + pointerInfo->componentTypes, builder, + loc)) || + failed(regionEnv.policy.normalizeLoopCarriedValue( + *nextInfo, pointerInfo->componentIndices, + pointerInfo->resultAttributes, builder, loc))) { + allOperandsValid = false; + appendFallbackComponents(*pointerInfo); + continue; + } + + FailureOr> carriedComponentValues = + gatherValues(nextInfo->components, pointerInfo->componentIndices); + if (failed(carriedComponentValues)) { + allOperandsValid = false; + appendFallbackComponents(*pointerInfo); + continue; + } + newOperands.append(carriedComponentValues->begin(), + carriedComponentValues->end()); + } + + return allOperandsValid ? success() : failure(); +} + +// Rebuilds and records every result produced by a replacement loop. Ordinary +// results still occupy one position and are mapped through `oldToNewStart`. +// A pointer result occupies the positions listed in its `LoopPointerInfo`; the +// function gathers those components, writes them into the initial descriptor, +// recomposes the complete pointer, and records both its SSA mapping and current +// decomposition in `env`. +// +// Example: +// oldResults = [%old_sum, %old_ptr, %old_flag] +// newResults = +// [%new_sum, %base, %shape, %stride, %offset, %new_flag] +// pointerInfo = { +// oldIndex = 1, componentIndices = [0, 1, 2, 3], +// newIndices = [1, 2, 3, 4] +// } +// oldToNewStart = [0, 1, 5] +// +// The function maps `%old_sum -> %new_sum` and `%old_flag -> %new_flag`. It +// inserts all four results into the pointer descriptor, rebuilds `%old_ptr`, +// and records `%old_ptr -> %rebuilt_ptr` plus that decomposition. +// +// The caller must set the builder insertion point after the replacement loop, +// so any rebuilt pointer dominates later operations. On failure this function +// returns immediately; the caller still owns and erases the replacement loop. +static LogicalResult +rebuildAndMapLoopResults(ValueRange oldResults, ValueRange newResults, + ArrayRef pointerInfos, + ArrayRef oldToNewStart, OpBuilder &builder, + RewriteEnv &env) { + for (auto [oldIndex, oldResult] : llvm::enumerate(oldResults)) { + const LoopPointerInfo *pointerInfo = + findPointerInfoByOldIndex(pointerInfos, oldIndex); + if (!pointerInfo) { + env.valueMapping.map(oldResult, newResults[oldToNewStart[oldIndex]]); + continue; + } + + FailureOr> resultComponentValues = + gatherValues(newResults, pointerInfo->newIndices); + if (failed(resultComponentValues)) + return failure(); + + FailureOr resultInfo = withReplacedComponents( + pointerInfo->initInfo, pointerInfo->componentIndices, + *resultComponentValues); + if (failed(resultInfo) || + failed(env.policy.normalizeLoopCarriedValue( + *resultInfo, pointerInfo->componentIndices, + pointerInfo->resultAttributes, builder, oldResult.getLoc()))) + return failure(); + + Value rebuiltPointer = + env.policy.recompose(*resultInfo, builder, oldResult.getLoc()); + if (!rebuiltPointer || failed(markPointerDescriptorRebuild( + rebuiltPointer, *resultInfo, env.policy))) + return failure(); + + env.recordDecomposition(oldResult, *resultInfo, rebuiltPointer); + } + + return success(); +} + +static LogicalResult rewriteControlFlowOp(Operation *op, OpBuilder &builder, + RewriteEnv &env); + +static LogicalResult materializePointerResult(Operation &originalOp, + Operation *clonedOp, + OpBuilder &builder, + RewriteEnv &env) { + // Each policy decides which pointer-producing operations need their exact + // components recorded immediately after cloning. + if (!env.policy.shouldDecomposeOperation(&originalOp)) + return success(); + + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointAfter(clonedOp); + + bool decomposedAllResults = clonedOp->getNumResults() != 0; + for (auto [oldResult, clonedResult] : + llvm::zip(originalOp.getResults(), clonedOp->getResults())) { + if (!env.policy.isDecompositionTarget(oldResult)) { + decomposedAllResults = false; + continue; + } + + FailureOr info = + env.decomposeValue(clonedResult, builder, oldResult.getLoc()); + if (failed(info)) + return failure(); + + Value rebuilt = env.policy.recompose(*info, builder, oldResult.getLoc()); + if (!rebuilt) + return failure(); + if (env.policy.shouldMarkOperationRecomposition(&originalOp) && + failed(markPointerDescriptorRebuild(rebuilt, *info, env.policy))) + return failure(); + env.recordDecomposition(oldResult, *info, rebuilt); + } + + // The replacement is recorded in the SSA mapping, so a side-effect-free + // clone whose every result was decomposed is redundant once it has no users. + if (decomposedAllResults && clonedOp->use_empty() && + isMemoryEffectFree(clonedOp)) + clonedOp->erase(); + + return success(); +} + +static LogicalResult rewriteBodyOps(Block *oldBlock, OpBuilder &builder, + RewriteEnv &env) { + // Process operations in program order. Nested control flow is rewritten + // recursively with the same policy; ordinary operations are cloned through + // the current SSA mapping. + for (Operation &originalOp : oldBlock->without_terminator()) { + if (isa(originalOp)) { + const ControlFlowOpAnalysis *analysis = env.plan.lookup(&originalOp); + if (!analysis) + return failure(); + if (analysis->needsRewrite()) { + if (failed(rewriteControlFlowOp(&originalOp, builder, env))) + return failure(); + continue; + } + } + Operation *clonedOp = builder.clone(originalOp, env.valueMapping); + if (failed(materializePointerResult(originalOp, clonedOp, builder, env))) + return failure(); + } + return success(); +} + +//===----------------------------------------------------------------------===// +// scf.for rewrite +//===----------------------------------------------------------------------===// + +static LogicalResult rewriteForOp(scf::ForOp forOp, OpBuilder &builder, + RewriteEnv &env) { + const ControlFlowOpAnalysis *analysis = env.plan.lookup(forOp); + if (!analysis || !analysis->needsRewrite()) + return failure(); + auto yieldOp = cast(forOp.getBody()->getTerminator()); + SmallVector pointerInfos; + + // The read-only analysis has already fixed every expanded slot and type. + // Materialization here recovers only the concrete values for that schema. + for (const ControlFlowSlotAnalysis &slot : analysis->slots) { + unsigned idx = slot.oldIndex; + if (idx >= forOp.getInitArgs().size() || idx >= yieldOp.getNumOperands() || + !env.policy.matches(forOp.getRegionIterArgs()[idx].getType())) + return failure(); + + FailureOr initInfo = + env.decomposeValue(forOp.getInitArgs()[idx], builder, forOp.getLoc()); + if (failed(initInfo) || + failed(env.policy.normalizeLoopCarriedValue( + *initInfo, slot.componentIndices, slot.resultAttributes, builder, + forOp.getLoc())) || + failed(castPlannedComponents(*initInfo, slot.componentIndices, + slot.componentTypes, builder, + forOp.getLoc()))) + return failure(); + pointerInfos.push_back(LoopPointerInfo{idx, + *initInfo, + slot.componentIndices, + slot.componentTypes, + slot.resultAttributes, + {}}); + } + + SmallVector newInitArgs; + SmallVector oldToNewStart(forOp.getInitArgs().size(), 0); + // Expand each owned pointer init into its runtime components. Non-pointer and + // other-policy slots retain one position in the new signature. + for (auto [idx, initArg] : llvm::enumerate(forOp.getInitArgs())) { + oldToNewStart[idx] = newInitArgs.size(); + if (LoopPointerInfo *info = findPointerInfoByOldIndex(pointerInfos, idx)) { + FailureOr> initComponents = + gatherValues(info->initInfo.components, info->componentIndices); + if (failed(initComponents)) + return failure(); + for (Value component : *initComponents) { + info->newIndices.push_back(newInitArgs.size()); + newInitArgs.push_back(component); + } + continue; + } + newInitArgs.push_back(env.remap(initArg)); + } + + bool bodyOk = true; + auto newForOp = builder.create( + forOp.getLoc(), env.remap(forOp.getLowerBound()), + env.remap(forOp.getUpperBound()), env.remap(forOp.getStep()), newInitArgs, + [&](OpBuilder &bodyBuilder, Location loc, Value iv, + ValueRange newRegionArgs) { + RewriteEnv bodyEnv = env; + bodyEnv.valueMapping.map(forOp.getInductionVar(), iv); + + // Bind pointer and ordinary iter-arguments before cloning body users. + if (failed(bindLoopRegionArguments( + forOp.getRegionIterArgs(), newRegionArgs, pointerInfos, + oldToNewStart, bodyBuilder, loc, bodyEnv))) + bodyOk = false; + + if (failed(rewriteBodyOps(forOp.getBody(), bodyBuilder, bodyEnv))) + bodyOk = false; + + SmallVector newYieldOperands; + if (failed(rewriteLoopTerminatorOperands( + yieldOp.getOperands(), newRegionArgs, pointerInfos, bodyBuilder, + yieldOp.getLoc(), bodyEnv, newYieldOperands))) + bodyOk = false; + + bodyBuilder.create(yieldOp.getLoc(), newYieldOperands); + }); + newForOp->setAttrs(forOp->getAttrs()); + if (analysis->rewritesOwnSignature() && + failed(updatePointerDescriptorBoundaryMarker( + newForOp, pointerInfos, oldToNewStart, env.policy))) { + newForOp.erase(); + return failure(); + } + + if (!bodyOk) { + newForOp.erase(); + return failure(); + } + + builder.setInsertionPointAfter(newForOp); + if (failed(rebuildAndMapLoopResults(forOp.getResults(), newForOp.getResults(), + pointerInfos, oldToNewStart, builder, + env))) { + newForOp.erase(); + return failure(); + } + + return success(); +} + +//===----------------------------------------------------------------------===// +// scf.while rewrite +//===----------------------------------------------------------------------===// + +static LogicalResult rewriteWhileOp(scf::WhileOp whileOp, OpBuilder &builder, + RewriteEnv &env) { + const ControlFlowOpAnalysis *analysis = env.plan.lookup(whileOp); + if (!analysis || !analysis->needsRewrite()) + return failure(); + scf::ConditionOp conditionOp = whileOp.getConditionOp(); + scf::YieldOp yieldOp = whileOp.getYieldOp(); + SmallVector pointerInfos; + + // The before arguments, condition forwarded values, after arguments and + // yield operands all consume the same precomputed positional schema. + for (const ControlFlowSlotAnalysis &slot : analysis->slots) { + unsigned idx = slot.oldIndex; + if (idx >= whileOp.getBeforeArguments().size() || + !env.policy.matches(whileOp.getBeforeArguments()[idx].getType()) || + idx >= whileOp.getInits().size() || + idx >= conditionOp.getArgs().size() || idx >= yieldOp.getNumOperands()) + return failure(); + + FailureOr initInfo = + env.decomposeValue(whileOp.getInits()[idx], builder, whileOp.getLoc()); + if (failed(initInfo) || + failed(env.policy.normalizeLoopCarriedValue( + *initInfo, slot.componentIndices, slot.resultAttributes, builder, + whileOp.getLoc())) || + failed(castPlannedComponents(*initInfo, slot.componentIndices, + slot.componentTypes, builder, + whileOp.getLoc()))) + return failure(); + pointerInfos.push_back(LoopPointerInfo{idx, + *initInfo, + slot.componentIndices, + slot.componentTypes, + slot.resultAttributes, + {}}); + } + + // Expand inits and result types in lockstep. oldToNewStart keeps untouched + // positions addressable even when earlier pointer slots expand by rank. + SmallVector newInits; + SmallVector newResultTypes; + SmallVector oldToNewStart(whileOp.getInits().size(), 0); + for (auto [idx, initArg] : llvm::enumerate(whileOp.getInits())) { + oldToNewStart[idx] = newInits.size(); + if (LoopPointerInfo *info = findPointerInfoByOldIndex(pointerInfos, idx)) { + FailureOr> initComponents = + gatherValues(info->initInfo.components, info->componentIndices); + if (failed(initComponents)) + return failure(); + for (Value component : *initComponents) { + info->newIndices.push_back(newInits.size()); + newInits.push_back(component); + newResultTypes.push_back(component.getType()); + } + continue; + } + newInits.push_back(env.remap(initArg)); + newResultTypes.push_back(whileOp.getResult(idx).getType()); + } + + bool bodyOk = true; + auto newWhileOp = builder.create( + whileOp.getLoc(), newResultTypes, newInits, + [&](OpBuilder &bodyBuilder, Location loc, ValueRange newRegionArgs) { + RewriteEnv beforeEnv = env; + // Bind the before-region arguments, then rewrite the body and the + // values forwarded by scf.condition. + if (failed(bindLoopRegionArguments( + whileOp.getBeforeArguments(), newRegionArgs, pointerInfos, + oldToNewStart, bodyBuilder, loc, beforeEnv))) + bodyOk = false; + + if (failed(rewriteBodyOps(whileOp.getBeforeBody(), bodyBuilder, + beforeEnv))) + bodyOk = false; + + SmallVector newConditionArgs; + if (failed(rewriteLoopTerminatorOperands( + conditionOp.getArgs(), newRegionArgs, pointerInfos, bodyBuilder, + conditionOp.getLoc(), beforeEnv, newConditionArgs))) + bodyOk = false; + + bodyBuilder.create( + conditionOp.getLoc(), beforeEnv.remap(conditionOp.getCondition()), + newConditionArgs); + }, + [&](OpBuilder &bodyBuilder, Location loc, ValueRange newRegionArgs) { + RewriteEnv afterEnv = env; + // Bind the after-region arguments before rewriting the body and its + // backedge yield. + if (failed(bindLoopRegionArguments( + whileOp.getAfterArguments(), newRegionArgs, pointerInfos, + oldToNewStart, bodyBuilder, loc, afterEnv))) + bodyOk = false; + + if (failed( + rewriteBodyOps(whileOp.getAfterBody(), bodyBuilder, afterEnv))) + bodyOk = false; + + SmallVector newYieldOperands; + if (failed(rewriteLoopTerminatorOperands( + yieldOp.getOperands(), newRegionArgs, pointerInfos, bodyBuilder, + yieldOp.getLoc(), afterEnv, newYieldOperands))) + bodyOk = false; + + bodyBuilder.create(yieldOp.getLoc(), newYieldOperands); + }); + newWhileOp->setAttrs(whileOp->getAttrs()); + if (analysis->rewritesOwnSignature() && + failed(updatePointerDescriptorBoundaryMarker( + newWhileOp, pointerInfos, oldToNewStart, env.policy))) { + newWhileOp.erase(); + return failure(); + } + + if (!bodyOk) { + newWhileOp.erase(); + return failure(); + } + + builder.setInsertionPointAfter(newWhileOp); + if (failed(rebuildAndMapLoopResults(whileOp.getResults(), + newWhileOp.getResults(), pointerInfos, + oldToNewStart, builder, env))) { + newWhileOp.erase(); + return failure(); + } + + return success(); +} + +//===----------------------------------------------------------------------===// +// scf.if component planning and rewrite +//===----------------------------------------------------------------------===// + +static LogicalResult rewriteIfOp(scf::IfOp ifOp, OpBuilder &builder, + RewriteEnv &env) { + const ControlFlowOpAnalysis *analysis = env.plan.lookup(ifOp); + if (!analysis || !analysis->needsRewrite() || + (!ifOp.elseBlock() && analysis->rewritesOwnSignature())) + return failure(); + + bool hasElse = static_cast(ifOp.elseBlock()); + scf::YieldOp thenYield = ifOp.thenYield(); + scf::YieldOp elseYield = hasElse ? ifOp.elseYield() : scf::YieldOp(); + SmallVector pointerInfos; + + for (const ControlFlowSlotAnalysis &slot : analysis->slots) { + if (slot.oldIndex >= ifOp.getNumResults() || + !env.policy.matches(ifOp.getResult(slot.oldIndex).getType()) || + slot.componentIndices.size() != slot.componentTypes.size()) + return failure(); + pointerInfos.push_back(IfPointerInfo{slot.oldIndex, slot.componentIndices, + slot.componentTypes, + slot.resultAttributes, std::nullopt}); + } + + // Expand only result positions selected by analysis. An if with no pointer + // result may still be rebuilt because one of its nested operations changes. + SmallVector newResultTypes; + for (auto [idx, result] : llvm::enumerate(ifOp.getResults())) { + if (const IfPointerInfo *info = + findPointerInfoByOldIndex(pointerInfos, idx)) { + newResultTypes.append(info->componentTypes.begin(), + info->componentTypes.end()); + continue; + } + newResultTypes.push_back(result.getType()); + } + + bool bodyOk = true; + auto buildBranch = [&](OpBuilder &branchBuilder, + bool isThen) -> LogicalResult { + // Each branch gets an isolated environment because values defined in one + // branch must never be visible while cloning the other branch. + RewriteEnv branchEnv = env; + Block *oldBlock = isThen ? ifOp.thenBlock() : ifOp.elseBlock(); + scf::YieldOp oldYield = isThen ? thenYield : elseYield; + if (failed(rewriteBodyOps(oldBlock, branchBuilder, branchEnv))) + return failure(); + + SmallVector newYieldOperands; + for (auto [idx, oldOperand] : llvm::enumerate(oldYield.getOperands())) { + if (IfPointerInfo *info = findPointerInfoByOldIndex(pointerInfos, idx)) { + FailureOr branchInfo = branchEnv.decomposeValue( + oldOperand, branchBuilder, oldYield.getLoc()); + if (failed(branchInfo) || + failed(castPlannedComponents(*branchInfo, info->componentIndices, + info->componentTypes, branchBuilder, + oldYield.getLoc())) || + failed(branchEnv.policy.normalizeControlFlowValue( + *branchInfo, info->resultAttributes, branchBuilder, + oldYield.getLoc()))) + return failure(); + if (isThen) + info->thenInfo = *branchInfo; + FailureOr> values = + gatherValues(branchInfo->components, info->componentIndices); + if (failed(values)) + return failure(); + newYieldOperands.append(values->begin(), values->end()); + continue; + } + newYieldOperands.push_back(branchEnv.remap(oldOperand)); + } + branchBuilder.create(oldYield.getLoc(), newYieldOperands); + return success(); + }; + + // Create the shell first, then clone each old branch into the corresponding + // new region with independent mappings. + auto newIfOp = builder.create( + ifOp.getLoc(), newResultTypes, env.remap(ifOp.getCondition()), hasElse); + newIfOp->setAttrs(ifOp->getAttrs()); + + // The then region always exists, including for a result-less one-arm if. + // Rewriting it is still required when it contains affected nested SCF. + { + OpBuilder::InsertionGuard guard(builder); + if (newResultTypes.empty()) { + newIfOp.thenBlock()->getTerminator()->erase(); + builder.setInsertionPointToEnd(newIfOp.thenBlock()); + } else { + builder.setInsertionPointToStart(newIfOp.thenBlock()); + } + if (failed(buildBranch(builder, /*isThen=*/true))) + bodyOk = false; + } + // An else region exists only for the two-arm form. In particular, do not + // access elseBlock() merely because the then region contains nested work. + if (hasElse) { + OpBuilder::InsertionGuard guard(builder); + if (newResultTypes.empty()) { + newIfOp.elseBlock()->getTerminator()->erase(); + builder.setInsertionPointToEnd(newIfOp.elseBlock()); + } else { + builder.setInsertionPointToStart(newIfOp.elseBlock()); + } + if (failed(buildBranch(builder, /*isThen=*/false))) + bodyOk = false; + } + + if (!bodyOk) { + newIfOp.erase(); + return failure(); + } + + // Reassemble the pointer immediately after the replacement if. Downstream + // operations therefore keep their original operand types; decomposition is + // limited to the control-flow boundary itself. + builder.setInsertionPointAfter(newIfOp); + unsigned newResultIndex = 0; + for (auto [idx, oldResult] : llvm::enumerate(ifOp.getResults())) { + if (const IfPointerInfo *info = + findPointerInfoByOldIndex(pointerInfos, idx)) { + SmallVector componentValues; + for (unsigned i = 0; i < info->componentIndices.size(); ++i) + componentValues.push_back(newIfOp.getResult(newResultIndex++)); + FailureOr resultInfo = withReplacedComponents( + *info->thenInfo, info->componentIndices, componentValues); + if (failed(resultInfo) || failed(env.policy.normalizeControlFlowValue( + *resultInfo, info->resultAttributes, + builder, oldResult.getLoc()))) { + newIfOp.erase(); + return failure(); + } + Value rebuilt = + env.policy.recompose(*resultInfo, builder, oldResult.getLoc()); + if (!rebuilt || failed(markPointerDescriptorRebuild(rebuilt, *resultInfo, + env.policy))) { + newIfOp.erase(); + return failure(); + } + env.recordDecomposition(oldResult, *resultInfo, rebuilt); + continue; + } + env.valueMapping.map(oldResult, newIfOp.getResult(newResultIndex++)); + } + + return success(); +} + +//===----------------------------------------------------------------------===// +// scope.scope rewrite +//===----------------------------------------------------------------------===// + +/// Erases the replacement Scope and any pointer reconstruction operations +/// inserted between it and the original Scope. Body-local operations disappear +/// with the replacement region; post-Scope operations are erased in reverse +/// order so their SSA dependencies remain valid during cleanup. +static void eraseScopeReplacement(scope::ScopeOp replacement, + scope::ScopeOp original) { + SmallVector followingOperations; + for (Operation *operation = replacement->getNextNode(); + operation && operation != original.getOperation(); + operation = operation->getNextNode()) + followingOperations.push_back(operation); + for (Operation *operation : llvm::reverse(followingOperations)) + operation->erase(); + replacement.erase(); +} + +static LogicalResult rewriteScopeOp(scope::ScopeOp scopeOp, OpBuilder &builder, + RewriteEnv &env) { + const ControlFlowOpAnalysis *analysis = env.plan.lookup(scopeOp); + FailureOr oldReturn = getSupportedScopeReturn(scopeOp); + if (!analysis || !analysis->needsRewrite() || failed(oldReturn)) + return failure(); + + SmallVector pointerInfos; + llvm::SmallDenseSet claimedResultIndices; + pointerInfos.reserve(analysis->slots.size()); + for (const ControlFlowSlotAnalysis &slot : analysis->slots) { + if (slot.oldIndex >= scopeOp.getNumResults() || + slot.componentIndices.size() != slot.componentTypes.size() || + !claimedResultIndices.insert(slot.oldIndex).second || + !env.policy.isDecompositionTarget(scopeOp.getResult(slot.oldIndex))) + return failure(); + pointerInfos.push_back(ScopePointerInfo{ + slot.oldIndex, slot.componentIndices, slot.componentTypes, + slot.resultAttributes, std::nullopt}); + } + + SmallVector newResultTypes; + for (auto [oldIndex, oldResult] : llvm::enumerate(scopeOp.getResults())) { + if (const ScopePointerInfo *pointerInfo = + findPointerInfoByOldIndex(pointerInfos, oldIndex)) { + newResultTypes.append(pointerInfo->componentTypes.begin(), + pointerInfo->componentTypes.end()); + continue; + } + newResultTypes.push_back(oldResult.getType()); + } + + auto newScope = + builder.create(scopeOp.getLoc(), newResultTypes); + newScope->setAttrs(scopeOp->getAttrs()); + // PointerDescriptorBoundary describes loop-carried slot positions and has no + // meaning for an operand-free Scope result boundary. + newScope->removeAttr(kPointerDescriptorBoundaryAttr); + newScope.getBodyRegion().emplaceBlock(); + + bool bodyOk = true; + { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToEnd(&newScope.getBodyRegion().front()); + RewriteEnv scopeEnv = env; + if (failed(rewriteBodyOps(&scopeOp.getBodyRegion().front(), builder, + scopeEnv))) { + bodyOk = false; + } else { + SmallVector newReturnOperands; + newReturnOperands.reserve(newResultTypes.size()); + for (auto [oldIndex, oldOperand] : + llvm::enumerate(oldReturn->getOperands())) { + ScopePointerInfo *pointerInfo = + findPointerInfoByOldIndex(pointerInfos, oldIndex); + if (!pointerInfo) { + newReturnOperands.push_back(scopeEnv.remap(oldOperand)); + continue; + } + + FailureOr returnedInfo = + scopeEnv.decomposeValue(oldOperand, builder, oldReturn->getLoc()); + if (failed(returnedInfo) || + failed(castPlannedComponents( + *returnedInfo, pointerInfo->componentIndices, + pointerInfo->componentTypes, builder, oldReturn->getLoc())) || + failed(scopeEnv.policy.normalizeControlFlowValue( + *returnedInfo, pointerInfo->resultAttributes, builder, + oldReturn->getLoc()))) { + bodyOk = false; + break; + } + + FailureOr> returnedComponents = gatherValues( + returnedInfo->components, pointerInfo->componentIndices); + if (failed(returnedComponents)) { + bodyOk = false; + break; + } + pointerInfo->returnedInfo = *returnedInfo; + newReturnOperands.append(returnedComponents->begin(), + returnedComponents->end()); + } + + if (bodyOk) + builder.create(oldReturn->getLoc(), newReturnOperands); + } + } + + if (!bodyOk) { + eraseScopeReplacement(newScope, scopeOp); + return failure(); + } + + // Pointer values are reconstructed immediately outside Scope. Scope itself + // returns only policy-selected components and deliberately does not receive + // PointerDescriptorBoundary; the reconstruction operation remains the + // precise downstream ownership marker. + builder.setInsertionPointAfter(newScope); + unsigned newResultIndex = 0; + for (auto [oldIndex, oldResult] : llvm::enumerate(scopeOp.getResults())) { + ScopePointerInfo *pointerInfo = + findPointerInfoByOldIndex(pointerInfos, oldIndex); + if (!pointerInfo) { + if (newResultIndex >= newScope.getNumResults()) { + eraseScopeReplacement(newScope, scopeOp); + return failure(); + } + env.valueMapping.map(oldResult, newScope.getResult(newResultIndex++)); + continue; + } + + if (!pointerInfo->returnedInfo || + newResultIndex + pointerInfo->componentIndices.size() > + newScope.getNumResults()) { + eraseScopeReplacement(newScope, scopeOp); + return failure(); + } + SmallVector componentValues; + componentValues.reserve(pointerInfo->componentIndices.size()); + for (unsigned componentIndex = 0; + componentIndex < pointerInfo->componentIndices.size(); + ++componentIndex) + componentValues.push_back(newScope.getResult(newResultIndex++)); + + FailureOr resultInfo = + withReplacedComponents(*pointerInfo->returnedInfo, + pointerInfo->componentIndices, componentValues); + if (failed(resultInfo) || failed(env.policy.normalizeControlFlowValue( + *resultInfo, pointerInfo->resultAttributes, + builder, oldResult.getLoc()))) { + eraseScopeReplacement(newScope, scopeOp); + return failure(); + } + + Value rebuilt = + env.policy.recompose(*resultInfo, builder, oldResult.getLoc()); + if (!rebuilt || failed(markPointerDescriptorRebuild(rebuilt, *resultInfo, + env.policy))) { + eraseScopeReplacement(newScope, scopeOp); + return failure(); + } + env.recordDecomposition(oldResult, *resultInfo, rebuilt); + } + + if (newResultIndex != newScope.getNumResults()) { + eraseScopeReplacement(newScope, scopeOp); + return failure(); + } + return success(); +} + +static LogicalResult rewriteControlFlowOp(Operation *op, OpBuilder &builder, + RewriteEnv &env) { + // Keep the operation dispatch next to the shared recursive implementation: + // all supported region operations must obey the same mapping and cleanup + // rules. Pointer-specific semantics enter only through env.policy. + if (auto forOp = dyn_cast(op)) + return rewriteForOp(forOp, builder, env); + if (auto whileOp = dyn_cast(op)) + return rewriteWhileOp(whileOp, builder, env); + if (auto ifOp = dyn_cast(op)) + return rewriteIfOp(ifOp, builder, env); + if (auto scopeOp = dyn_cast(op)) + return rewriteScopeOp(scopeOp, builder, env); + return failure(); +} + +static FailureOr> +collectReplacements(Operation *op, const RewriteEnv &env) { + SmallVector replacements; + replacements.reserve(op->getNumResults()); + for (Value result : op->getResults()) { + // Unlike RewriteEnv::remap(), replacement collection must not fall back to + // the original result. Such a fallback would hide an unhandled result slot + // and ask replaceOp to replace a value with itself. + Value replacement = env.valueMapping.lookupOrNull(result); + if (!replacement) + return failure(); + replacements.push_back(replacement); + } + return replacements; +} + +static LogicalResult +tryDecoupleControlFlowOp(Operation *op, IRRewriter &rewriter, + const ControlFlowRewritePolicy &policy, + const ControlFlowRewritePlan &plan) { + // Build a replacement beside the original operation. The original operation + // itself remains until every result has a valid mapped value, after which the + // standard rewriter performs the externally visible replacement. + // TODO: Track and erase policy materializations created outside the new SCF + // operation if an unexpected rewrite-time validation fails. Read-only + // analysis makes that path exceptional, but failure should still be atomic. + RewriteEnv env(policy, plan); + rewriter.setInsertionPoint(op); + if (failed(rewriteControlFlowOp(op, rewriter, env))) + return failure(); + + FailureOr> replacements = collectReplacements(op, env); + if (failed(replacements)) + return failure(); + rewriter.replaceOp(op, *replacements); + return success(); +} + +} // namespace + +namespace mlir::triton::controlflow { + +LogicalResult applyControlFlowRewritePlan( + ModuleOp module, const ControlFlowRewritePolicy &policy, + const ControlFlowRewritePlan &plan, bool emitDiagnostics) { + IRRewriter rewriter(module.getContext()); + // Analysis and application are consecutive and no IR mutation occurs in + // between, so rediscover the roots from the module instead of duplicating + // traversal state in the immutable operation plan. + for (Operation *root : collectOutermostControlFlowOps(module)) { + const ControlFlowOpAnalysis *rootAnalysis = plan.lookup(root); + if (!rootAnalysis) { + if (emitDiagnostics) + root->emitError("missing frozen control-flow rewrite decision"); + return failure(); + } + if (!rootAnalysis->needsRewrite()) + continue; + if (failed(tryDecoupleControlFlowOp(root, rewriter, policy, plan))) { + if (emitDiagnostics) + root->emitError("failed to apply analyzed pointer decomposition"); + return failure(); + } + } + return success(); +} + +LogicalResult rewriteControlFlow(ModuleOp module, + const ControlFlowRewritePolicy &policy, + bool allowUnsupportedFallback) { + // Tensor-pointer decomposition is an optimization, not the only legal + // representation of a TensorPtr. Probe the complete rewrite on a detached + // clone before mutating the real module. If analysis or application rejects + // any root, discard the clone and keep the original TensorPtr graph intact; + // this prevents a partially materialized descriptor from reaching T2L. + if (allowUnsupportedFallback) { + Operation *probeOperation = module->clone(); + auto probeModule = cast(probeOperation); + bool probeSucceeded = false; + { + ScopedDiagnosticHandler suppressDiagnostics( + module.getContext(), [](Diagnostic &) { return success(); }); + FailureOr probePlan = + analyzeControlFlow(probeModule, policy); + probeSucceeded = + succeeded(probePlan) && + succeeded(applyControlFlowRewritePlan(probeModule, policy, *probePlan, + /*emitDiagnostics=*/false)); + } + probeModule->erase(); + if (!probeSucceeded) + return success(); + } + + FailureOr plan = analyzeControlFlow(module, policy); + if (failed(plan)) + return failure(); + return applyControlFlowRewritePlan(module, policy, *plan); +} + +} // namespace mlir::triton::controlflow diff --git a/third_party/ascend/lib/TritonControlFlowOpt/TensorPtrDecompose.cpp b/third_party/ascend/lib/TritonControlFlowOpt/TensorPtrDecompose.cpp new file mode 100644 index 0000000000..0ecfd91a28 --- /dev/null +++ b/third_party/ascend/lib/TritonControlFlowOpt/TensorPtrDecompose.cpp @@ -0,0 +1,2497 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonControlFlowOpt/TensorPtrDecompose.h" + +#include "TritonControlFlowOpt/ControlFlowRewrite.h" +#include "Utils/Utils.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Matchers.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/APInt.h" +#include "llvm/ADT/STLExtras.h" + +#include +#include + +using namespace mlir; +using namespace mlir::triton; +using namespace mlir::triton::controlflow; + +namespace { + +static constexpr unsigned kBaseComponent = 0; +static constexpr unsigned kStrideComponent = 1; +static constexpr unsigned kOffsetComponent = 2; +static constexpr unsigned kResidualComponent = 3; +static constexpr unsigned kBaseIsScalarAttribute = 0; +static constexpr unsigned kAxisKindsAttribute = 1; + +enum class AxisKind : int32_t { Opaque = 0, Structured = 1 }; + +static constexpr unsigned getStrideComponent(unsigned axis) { + return kBaseComponent + 1 + axis; +} + +static constexpr unsigned getUniformOffsetComponent(unsigned rank) { + return kBaseComponent + 1 + rank; +} + +static constexpr unsigned getOpaqueContributionComponent(unsigned rank) { + return getUniformOffsetComponent(rank) + 1; +} + +static SmallVector getAxisKinds(unsigned rank, AxisKind kind) { + return SmallVector(rank, kind); +} + +static DenseI32ArrayAttr getAxisKindsAttr(MLIRContext *context, + ArrayRef kinds) { + SmallVector values; + values.reserve(kinds.size()); + for (AxisKind kind : kinds) + values.push_back(static_cast(kind)); + return DenseI32ArrayAttr::get(context, values); +} + +template +static FailureOr> getAxisKinds(const StateT &value) { + auto pointerTensor = dyn_cast(value.originalType); + if (!pointerTensor || value.attributes.size() != 2) + return failure(); + auto attribute = + dyn_cast(value.attributes[kAxisKindsAttribute]); + if (!attribute || static_cast(attribute.asArrayRef().size()) != + pointerTensor.getRank()) + return failure(); + SmallVector kinds; + kinds.reserve(attribute.asArrayRef().size()); + for (int32_t rawKind : attribute.asArrayRef()) { + if (rawKind != static_cast(AxisKind::Opaque) && + rawKind != static_cast(AxisKind::Structured)) + return failure(); + kinds.push_back(static_cast(rawKind)); + } + return kinds; +} + +static bool isTensorPointerType(Type type) { + auto tensorType = dyn_cast(type); + return tensorType && isa(tensorType.getElementType()); +} + +static bool isCompatiblePointerBroadcast(Type sourceType, Type resultType) { + auto sourceTensor = dyn_cast(sourceType); + auto resultTensor = dyn_cast(resultType); + if (!sourceTensor || !resultTensor || !isTensorPointerType(sourceType) || + !isTensorPointerType(resultType) || + sourceTensor.getRank() != resultTensor.getRank() || + sourceTensor.getElementType() != resultTensor.getElementType() || + sourceTensor.getEncoding() != resultTensor.getEncoding()) + return false; + + for (auto [sourceExtent, resultExtent] : + llvm::zip(sourceTensor.getShape(), resultTensor.getShape())) { + if (sourceExtent != resultExtent && sourceExtent != 1) + return false; + } + return true; +} + +static bool isRankOneTensorPointer(Type type) { + auto tensorType = dyn_cast(type); + return tensorType && tensorType.getRank() == 1 && + isa(tensorType.getElementType()); +} + +static RankedTensorType getDefaultOffsetsType(Type pointerType) { + auto pointerTensor = cast(pointerType); + return RankedTensorType::get(pointerTensor.getShape(), + IntegerType::get(pointerType.getContext(), 32), + pointerTensor.getEncoding()); +} + +static bool isCompatibleOffsetTensor(Type pointerType, Type offsetType) { + auto pointerTensor = dyn_cast(pointerType); + auto offsetTensor = dyn_cast(offsetType); + return pointerTensor && offsetTensor && + pointerTensor.getShape() == offsetTensor.getShape() && + pointerTensor.getEncoding() == offsetTensor.getEncoding() && + isa(offsetTensor.getElementType()); +} + +static bool hasValidSchema(Type originalType, TypeRange componentTypes, + ArrayRef attributes) { + auto pointerTensor = dyn_cast(originalType); + if (!pointerTensor || !isTensorPointerType(originalType) || + attributes.size() != 2) + return false; + + auto baseIsScalar = dyn_cast(attributes[kBaseIsScalarAttribute]); + if (!baseIsScalar) + return false; + Type expectedBaseType = baseIsScalar.getValue() + ? IntegerType::get(originalType.getContext(), 64) + : originalType; + unsigned rank = pointerTensor.getRank(); + if (rank == 0 || componentTypes.size() != rank + 3 || + componentTypes[kBaseComponent] != expectedBaseType) + return false; + + auto axisKinds = dyn_cast(attributes[kAxisKindsAttribute]); + if (!axisKinds || axisKinds.asArrayRef().size() != rank) + return false; + for (int32_t kind : axisKinds.asArrayRef()) { + if (kind != static_cast(AxisKind::Opaque) && + kind != static_cast(AxisKind::Structured)) + return false; + } + + auto uniformType = + dyn_cast(componentTypes[getUniformOffsetComponent(rank)]); + auto opaqueType = dyn_cast( + componentTypes[getOpaqueContributionComponent(rank)]); + if (!uniformType || !opaqueType || + opaqueType.getElementType() != uniformType || + !isCompatibleOffsetTensor(originalType, opaqueType)) + return false; + for (unsigned axis = 0; axis < rank; ++axis) { + if (componentTypes[getStrideComponent(axis)] != uniformType) + return false; + } + return true; +} + +static bool hasValidLayout(const DecomposedValue &value) { + return hasValidSchema(value.originalType, + TypeRange{ValueRange{value.components}}, + value.attributes); +} + +static bool hasValidLayout(const AnalyzedValue &value) { + SmallVector componentTypes; + componentTypes.reserve(value.components.size()); + for (const AnalyzedComponent &component : value.components) + componentTypes.push_back(component.type); + return hasValidSchema(value.originalType, componentTypes, value.attributes); +} + +template static bool hasScalarBase(const StateT &value) { + return cast(value.attributes[kBaseIsScalarAttribute]).getValue(); +} + +static bool hasSameRepresentation(const AnalyzedValue &lhs, + const AnalyzedValue &rhs) { + if (!hasValidLayout(lhs) || !hasValidLayout(rhs) || + lhs.originalType != rhs.originalType || + hasScalarBase(lhs) != hasScalarBase(rhs)) + return false; + if (hasScalarBase(lhs)) + return true; + return lhs.components[kBaseComponent].type == + rhs.components[kBaseComponent].type && + lhs.components[kBaseComponent].identity == + rhs.components[kBaseComponent].identity; +} + +static FailureOr getWiderIntegerLikeType(Type lhs, Type rhs) { + if (lhs == rhs) + return lhs; + + if (auto lhsInt = dyn_cast(lhs)) { + auto rhsInt = dyn_cast(rhs); + if (!rhsInt) + return failure(); + return lhsInt.getWidth() >= rhsInt.getWidth() ? lhs : rhs; + } + + auto lhsTensor = dyn_cast(lhs); + auto rhsTensor = dyn_cast(rhs); + if (!lhsTensor || !rhsTensor || + lhsTensor.getShape() != rhsTensor.getShape() || + lhsTensor.getEncoding() != rhsTensor.getEncoding()) + return failure(); + auto lhsInt = dyn_cast(lhsTensor.getElementType()); + auto rhsInt = dyn_cast(rhsTensor.getElementType()); + if (!lhsInt || !rhsInt) + return failure(); + return lhsInt.getWidth() >= rhsInt.getWidth() ? lhs : rhs; +} + +static bool isZeroIdentity(const ComponentIdentity &identity) { + return identity.kind == ComponentIdentity::Kind::Zero; +} + +static ComponentIdentity getAddIdentity(const ComponentIdentity &lhs, + const ComponentIdentity &rhs, + Value result, unsigned componentIndex) { + if (isZeroIdentity(lhs)) + return rhs; + if (isZeroIdentity(rhs)) + return lhs; + return ComponentIdentity::fromValue(result, componentIndex); +} + +static ComponentIdentity getSubIdentity(const ComponentIdentity &lhs, + const ComponentIdentity &rhs, + Value result, unsigned componentIndex) { + if (isZeroIdentity(rhs)) + return lhs; + return ComponentIdentity::fromValue(result, componentIndex); +} + +static std::optional getScalarConstant(Value value) { + return getConstantIntValue(value); +} + +struct Rank1AnalyzedOffset { + AnalyzedComponent stride; + AnalyzedComponent offset; + AnalyzedComponent residual; +}; + +struct Rank1OffsetValues { + Value stride; + Value offset; + Value residual; +}; + +static FailureOr getRank1IntegerTensorType(Value value, + Type pointerType) { + auto tensorType = dyn_cast(value.getType()); + if (!tensorType || tensorType.getRank() != 1 || + !isCompatibleOffsetTensor(pointerType, tensorType)) + return failure(); + return tensorType; +} + +struct DenseIntegerSplat { + APInt value; +}; + +static FailureOr getSplatScalar(Value value) { + auto splat = value.getDefiningOp(); + if (!splat || !isa(splat.getSrc().getType())) + return failure(); + return splat.getSrc(); +} + +/// Returns the exact scalar bit pattern represented by an integer dense splat. +/// Lane-varying dense constants deliberately fail this check and remain opaque. +static FailureOr getDenseIntegerSplat(Value value) { + auto tensorType = dyn_cast(value.getType()); + auto constant = value.getDefiningOp(); + if (!tensorType || tensorType.getRank() == 0 || + !tensorType.hasStaticShape() || + !isa(tensorType.getElementType()) || !constant) + return failure(); + auto dense = dyn_cast(constant.getValue()); + if (!dense || !dense.isSplat() || dense.getType() != tensorType) + return failure(); + APInt splatValue = dense.getSplatValue(); + if (splatValue.getBitWidth() != + cast(tensorType.getElementType()).getWidth()) + return failure(); + return DenseIntegerSplat{std::move(splatValue)}; +} + +/// Returns true when every tensor lane is produced by the same integer scalar. +/// Both an explicit tt.splat and an integer dense splat carry this guarantee. +/// Lane-varying tensor constants are intentionally excluded. +static bool isUniformIntegerScale(Value value) { + return succeeded(getSplatScalar(value)) || + succeeded(getDenseIntegerSplat(value)); +} + +static bool isUniformIntegerScaleConstant(Value scale, int64_t expected) { + if (FailureOr scalar = getSplatScalar(scale); succeeded(scalar)) + return getScalarConstant(*scalar) == std::optional(expected); + FailureOr dense = getDenseIntegerSplat(scale); + if (failed(dense)) + return false; + return expected == 0 ? dense->value.isZero() + : expected == 1 && dense->value.isOne(); +} + +static ComponentIdentity getScaledIdentity(const ComponentIdentity &input, + Value scale, Value result, + unsigned componentIndex) { + if (isZeroIdentity(input) || isUniformIntegerScaleConstant(scale, 0)) + return ComponentIdentity::zero(componentIndex); + if (isUniformIntegerScaleConstant(scale, 1)) + return input; + return ComponentIdentity::fromValue(result, componentIndex); +} + +static Rank1AnalyzedOffset getFallbackAnalyzedOffset(Value value, Type) { + auto tensorType = cast(value.getType()); + Type scalarType = tensorType.getElementType(); + return { + {scalarType, ComponentIdentity::zero(kStrideComponent)}, + {scalarType, ComponentIdentity::zero(kOffsetComponent)}, + {tensorType, ComponentIdentity::fromValue(value, kResidualComponent)}}; +} + +static bool isSafeIntegerExtensionLeaf(Value value) { + if (value.getDefiningOp()) + return true; + return succeeded(getSplatScalar(value)) || + succeeded(getDenseIntegerSplat(value)); +} + +static FailureOr analyzeRank1Offset(Value value, + Type pointerType) { + FailureOr tensorType = + getRank1IntegerTensorType(value, pointerType); + if (failed(tensorType)) + return failure(); + Type scalarType = (*tensorType).getElementType(); + + if (auto range = value.getDefiningOp()) { + int64_t extent = (*tensorType).getShape()[0]; + if (range.getEnd() - range.getStart() != extent) + return getFallbackAnalyzedOffset(value, pointerType); + ComponentIdentity offsetIdentity = + range.getStart() == 0 + ? ComponentIdentity::zero(kOffsetComponent) + : ComponentIdentity::fromValue(value, kOffsetComponent); + return Rank1AnalyzedOffset{ + {scalarType, ComponentIdentity::fromValue(value, kStrideComponent)}, + {scalarType, offsetIdentity}, + {*tensorType, ComponentIdentity::zero(kResidualComponent)}}; + } + + if (auto splat = value.getDefiningOp()) { + if (!isa(splat.getSrc().getType())) + return getFallbackAnalyzedOffset(value, pointerType); + ComponentIdentity offsetIdentity = + getScalarConstant(splat.getSrc()) == std::optional(0) + ? ComponentIdentity::zero(kOffsetComponent) + : ComponentIdentity::fromValue(splat.getSrc(), kOffsetComponent); + return Rank1AnalyzedOffset{ + {scalarType, ComponentIdentity::zero(kStrideComponent)}, + {scalarType, offsetIdentity}, + {*tensorType, ComponentIdentity::zero(kResidualComponent)}}; + } + + if (FailureOr dense = getDenseIntegerSplat(value); + succeeded(dense)) { + ComponentIdentity offsetIdentity = + dense->value.isZero() + ? ComponentIdentity::zero(kOffsetComponent) + : ComponentIdentity::fromValue(value, kOffsetComponent); + return Rank1AnalyzedOffset{ + {scalarType, ComponentIdentity::zero(kStrideComponent)}, + {scalarType, offsetIdentity}, + {*tensorType, ComponentIdentity::zero(kResidualComponent)}}; + } + + if (auto extension = value.getDefiningOp()) { + Value source = extension.getIn(); + if (!isSafeIntegerExtensionLeaf(source)) + return getFallbackAnalyzedOffset(value, pointerType); + FailureOr sourceInfo = + analyzeRank1Offset(source, pointerType); + if (failed(sourceInfo)) + return failure(); + auto extendedIdentity = [&](const AnalyzedComponent &component, + unsigned componentIndex) { + return isZeroIdentity(component.identity) + ? ComponentIdentity::zero(componentIndex) + : ComponentIdentity::fromValue(value, componentIndex); + }; + return Rank1AnalyzedOffset{ + {scalarType, extendedIdentity(sourceInfo->stride, kStrideComponent)}, + {scalarType, extendedIdentity(sourceInfo->offset, kOffsetComponent)}, + {*tensorType, + extendedIdentity(sourceInfo->residual, kResidualComponent)}}; + } + + auto analyzeBinary = [&](Value lhs, Value rhs, + bool subtract) -> FailureOr { + FailureOr lhsInfo = + analyzeRank1Offset(lhs, pointerType); + FailureOr rhsInfo = + analyzeRank1Offset(rhs, pointerType); + if (failed(lhsInfo) || failed(rhsInfo) || + lhsInfo->stride.type != rhsInfo->stride.type || + lhsInfo->offset.type != rhsInfo->offset.type || + lhsInfo->residual.type != rhsInfo->residual.type) + return getFallbackAnalyzedOffset(value, pointerType); + + // One rank-1 axis is represented either entirely by scalar affine fields + // or entirely by the opaque carrier. Do not retain a partial affine term + // beside an existing residual. + if (!isZeroIdentity(lhsInfo->residual.identity) || + !isZeroIdentity(rhsInfo->residual.identity)) + return getFallbackAnalyzedOffset(value, pointerType); + + auto combine = [&](const ComponentIdentity &lhsIdentity, + const ComponentIdentity &rhsIdentity, + unsigned componentIndex) { + return subtract ? getSubIdentity(lhsIdentity, rhsIdentity, value, + componentIndex) + : getAddIdentity(lhsIdentity, rhsIdentity, value, + componentIndex); + }; + return Rank1AnalyzedOffset{ + {lhsInfo->stride.type, + combine(lhsInfo->stride.identity, rhsInfo->stride.identity, + kStrideComponent)}, + {lhsInfo->offset.type, + combine(lhsInfo->offset.identity, rhsInfo->offset.identity, + kOffsetComponent)}, + {lhsInfo->residual.type, + combine(lhsInfo->residual.identity, rhsInfo->residual.identity, + kResidualComponent)}}; + }; + + if (auto add = value.getDefiningOp()) + return analyzeBinary(add.getLhs(), add.getRhs(), false); + if (auto sub = value.getDefiningOp()) + return analyzeBinary(sub.getLhs(), sub.getRhs(), true); + + if (auto mul = value.getDefiningOp()) { + Value affineOperand = mul.getLhs(); + Value scale = mul.getRhs(); + if (!isUniformIntegerScale(scale)) { + affineOperand = mul.getRhs(); + scale = mul.getLhs(); + } + if (!isUniformIntegerScale(scale)) + return getFallbackAnalyzedOffset(value, pointerType); + FailureOr input = + analyzeRank1Offset(affineOperand, pointerType); + if (failed(input) || !isZeroIdentity(input->residual.identity)) + return getFallbackAnalyzedOffset(value, pointerType); + return Rank1AnalyzedOffset{ + {input->stride.type, getScaledIdentity(input->stride.identity, scale, + value, kStrideComponent)}, + {input->offset.type, getScaledIdentity(input->offset.identity, scale, + value, kOffsetComponent)}, + {input->residual.type, + getScaledIdentity(input->residual.identity, scale, value, + kResidualComponent)}}; + } + + // Pushing an extension through a previously computed tensor expression can + // change the source-width overflow semantics. Keep the already-evaluated + // tensor as residual until the schema records arithmetic width explicitly. + return getFallbackAnalyzedOffset(value, pointerType); +} + +static bool isConstantZero(Value value) { + if (!value) + return false; + if (std::optional scalar = getConstantIntValue(value)) + return *scalar == 0; + auto constant = value.getDefiningOp(); + if (!constant) + return false; + auto dense = dyn_cast(constant.getValue()); + return dense && dense.isSplat() && dense.getSplatValue().isZero(); +} + +static Value createScalarConstant(OpBuilder &builder, Location loc, Type type, + int64_t value) { + auto intType = dyn_cast(type); + if (!intType) + return nullptr; + return builder.create( + loc, builder.getIntegerAttr(intType, value)); +} + +static Value createScalarConstant(OpBuilder &builder, Location loc, Type type, + const APInt &value) { + auto intType = dyn_cast(type); + if (!intType || intType.getWidth() != value.getBitWidth()) + return nullptr; + return builder.create(loc, + IntegerAttr::get(intType, value)); +} + +/// Materializes the scalar represented by an integer-uniform tensor. Dense +/// splats are recreated with their exact APInt width so affine stride analysis +/// and concrete descriptor construction observe identical arithmetic. +static FailureOr +materializeUniformIntegerScale(Value value, OpBuilder &builder, Location loc) { + if (FailureOr scalar = getSplatScalar(value); succeeded(scalar)) + return *scalar; + FailureOr dense = getDenseIntegerSplat(value); + if (failed(dense)) + return failure(); + auto tensorType = cast(value.getType()); + Value scalar = createScalarConstant(builder, loc, tensorType.getElementType(), + dense->value); + if (!scalar) + return failure(); + return scalar; +} + +static Value createZeroOffsets(OpBuilder &builder, Location loc, + RankedTensorType offsetsType) { + auto elementType = cast(offsetsType.getElementType()); + return builder.create( + loc, DenseElementsAttr::get(offsetsType, + builder.getIntegerAttr(elementType, 0))); +} + +static Value createZeroOffsets(OpBuilder &builder, Location loc, + Type pointerType) { + return createZeroOffsets(builder, loc, getDefaultOffsetsType(pointerType)); +} + +static Value createIntegerAdd(OpBuilder &builder, Location loc, Value lhs, + Value rhs) { + if (isConstantZero(lhs)) + return rhs; + if (isConstantZero(rhs)) + return lhs; + FailureOr type = getWiderIntegerLikeType(lhs.getType(), rhs.getType()); + if (failed(type)) + return nullptr; + FailureOr convertedLhs = castIntegerLike(builder, loc, lhs, *type); + FailureOr convertedRhs = castIntegerLike(builder, loc, rhs, *type); + if (failed(convertedLhs) || failed(convertedRhs)) + return nullptr; + return builder.create(loc, *convertedLhs, *convertedRhs); +} + +static Value createIntegerSub(OpBuilder &builder, Location loc, Value lhs, + Value rhs) { + if (isConstantZero(rhs)) + return lhs; + FailureOr type = getWiderIntegerLikeType(lhs.getType(), rhs.getType()); + if (failed(type)) + return nullptr; + FailureOr convertedLhs = castIntegerLike(builder, loc, lhs, *type); + FailureOr convertedRhs = castIntegerLike(builder, loc, rhs, *type); + if (failed(convertedLhs) || failed(convertedRhs)) + return nullptr; + return builder.create(loc, *convertedLhs, *convertedRhs); +} + +static Value scaleIntegerValue(OpBuilder &builder, Location loc, Value value, + Value scale) { + if (isConstantZero(value)) + return value; + std::optional constant = getScalarConstant(scale); + if (constant && *constant == 0) { + if (auto tensorType = dyn_cast(value.getType())) + return createZeroOffsets(builder, loc, tensorType); + return createScalarConstant(builder, loc, value.getType(), 0); + } + if (constant && *constant == 1) + return value; + + if (auto tensorType = dyn_cast(value.getType())) { + FailureOr convertedScale = + castIntegerLike(builder, loc, scale, tensorType.getElementType()); + if (failed(convertedScale)) + return nullptr; + Value scaleTensor = + builder.create(loc, tensorType, *convertedScale); + return builder.create(loc, value, scaleTensor); + } + + FailureOr type = + getWiderIntegerLikeType(value.getType(), scale.getType()); + if (failed(type)) + return nullptr; + FailureOr convertedValue = castIntegerLike(builder, loc, value, *type); + FailureOr convertedScale = castIntegerLike(builder, loc, scale, *type); + if (failed(convertedValue) || failed(convertedScale)) + return nullptr; + return builder.create(loc, *convertedValue, *convertedScale); +} + +static FailureOr materializeRank1Offset(Value value, + Type pointerType, + OpBuilder &builder, + Location loc) { + FailureOr tensorType = + getRank1IntegerTensorType(value, pointerType); + if (failed(tensorType)) + return failure(); + + // Component identities are keyed by the analyzed tensor expression. Create + // its concrete scalar fields next to that expression, where they dominate + // every reuse (including sibling scf.if arms). Materializing at each addptr + // user would make equal abstract identities refer to branch-local Values. + OpBuilder::InsertionGuard insertionGuard(builder); + if (Operation *definingOp = value.getDefiningOp()) + builder.setInsertionPointAfter(definingOp); + else if (auto blockArgument = dyn_cast(value)) + builder.setInsertionPointToStart(blockArgument.getOwner()); + + Type scalarType = (*tensorType).getElementType(); + + auto fallback = [&]() -> FailureOr { + Value stride = createScalarConstant(builder, loc, scalarType, 0); + Value offset = createScalarConstant(builder, loc, scalarType, 0); + if (!stride || !offset) + return failure(); + return Rank1OffsetValues{stride, offset, value}; + }; + + if (auto range = value.getDefiningOp()) { + if (range.getEnd() - range.getStart() != (*tensorType).getShape()[0]) + return fallback(); + Value stride = createScalarConstant(builder, loc, scalarType, 1); + Value offset = + createScalarConstant(builder, loc, scalarType, range.getStart()); + Value residual = createZeroOffsets(builder, loc, *tensorType); + if (!stride || !offset || !residual) + return failure(); + return Rank1OffsetValues{stride, offset, residual}; + } + + if (auto splat = value.getDefiningOp()) { + if (!isa(splat.getSrc().getType())) + return fallback(); + FailureOr offset = + castIntegerLike(builder, loc, splat.getSrc(), scalarType); + Value stride = createScalarConstant(builder, loc, scalarType, 0); + Value residual = createZeroOffsets(builder, loc, *tensorType); + if (failed(offset) || !stride || !residual) + return failure(); + return Rank1OffsetValues{stride, *offset, residual}; + } + + if (FailureOr dense = getDenseIntegerSplat(value); + succeeded(dense)) { + Value stride = createScalarConstant(builder, loc, scalarType, 0); + Value offset = createScalarConstant(builder, loc, scalarType, dense->value); + Value residual = createZeroOffsets(builder, loc, *tensorType); + if (!stride || !offset || !residual) + return failure(); + return Rank1OffsetValues{stride, offset, residual}; + } + + if (auto extension = value.getDefiningOp()) { + Value source = extension.getIn(); + if (!isSafeIntegerExtensionLeaf(source)) + return fallback(); + FailureOr sourceInfo = + materializeRank1Offset(source, pointerType, builder, loc); + if (failed(sourceInfo)) + return failure(); + auto extendComponent = [&](Value component, + Type targetType) -> FailureOr { + if (!isConstantZero(component)) + return castIntegerLike(builder, loc, component, targetType); + if (auto targetTensor = dyn_cast(targetType)) + return createZeroOffsets(builder, loc, targetTensor); + Value zero = createScalarConstant(builder, loc, targetType, 0); + if (!zero) + return failure(); + return zero; + }; + FailureOr stride = extendComponent(sourceInfo->stride, scalarType); + FailureOr offset = extendComponent(sourceInfo->offset, scalarType); + FailureOr residual = + extendComponent(sourceInfo->residual, *tensorType); + if (failed(stride) || failed(offset) || failed(residual)) + return failure(); + return Rank1OffsetValues{*stride, *offset, *residual}; + } + + auto materializeBinary = [&](Value lhs, Value rhs, + bool subtract) -> FailureOr { + FailureOr lhsInfo = + materializeRank1Offset(lhs, pointerType, builder, loc); + FailureOr rhsInfo = + materializeRank1Offset(rhs, pointerType, builder, loc); + if (failed(lhsInfo) || failed(rhsInfo)) + return failure(); + if (!isConstantZero(lhsInfo->residual) || + !isConstantZero(rhsInfo->residual)) + return fallback(); + auto combine = [&](Value lhsValue, Value rhsValue) { + return subtract ? createIntegerSub(builder, loc, lhsValue, rhsValue) + : createIntegerAdd(builder, loc, lhsValue, rhsValue); + }; + Value stride = combine(lhsInfo->stride, rhsInfo->stride); + Value offset = combine(lhsInfo->offset, rhsInfo->offset); + Value residual = combine(lhsInfo->residual, rhsInfo->residual); + if (!stride || !offset || !residual) + return failure(); + return Rank1OffsetValues{stride, offset, residual}; + }; + + if (auto add = value.getDefiningOp()) + return materializeBinary(add.getLhs(), add.getRhs(), false); + if (auto sub = value.getDefiningOp()) + return materializeBinary(sub.getLhs(), sub.getRhs(), true); + + if (auto mul = value.getDefiningOp()) { + Value affineOperand = mul.getLhs(); + FailureOr scale = + materializeUniformIntegerScale(mul.getRhs(), builder, loc); + if (failed(scale)) { + affineOperand = mul.getRhs(); + scale = materializeUniformIntegerScale(mul.getLhs(), builder, loc); + } + if (failed(scale)) + return fallback(); + FailureOr input = + materializeRank1Offset(affineOperand, pointerType, builder, loc); + if (failed(input)) + return failure(); + if (!isConstantZero(input->residual)) + return fallback(); + Value stride = scaleIntegerValue(builder, loc, input->stride, *scale); + Value offset = scaleIntegerValue(builder, loc, input->offset, *scale); + Value residual = scaleIntegerValue(builder, loc, input->residual, *scale); + if (!stride || !offset || !residual) + return failure(); + return Rank1OffsetValues{stride, offset, residual}; + } + + return fallback(); +} + +static Value materializeRank1CompleteOffsets(const DecomposedValue &value, + OpBuilder &builder, Location loc) { + if (!hasValidLayout(value) || !isRankOneTensorPointer(value.originalType)) + return nullptr; + Value stride = value.components[kStrideComponent]; + Value offset = value.components[kOffsetComponent]; + Value residual = value.components[kResidualComponent]; + auto residualType = cast(residual.getType()); + int64_t extent = residualType.getShape()[0]; + + // Zero is a policy-level identity, not an SSA owner. A concrete zero may + // have been created inside one arm of an scf.if and therefore cannot be used + // by the rebuild after the if. Rematerialize every proven zero at the current + // insertion point so empty/invariant slots remain dominance-correct. + if (isConstantZero(stride)) + stride = createScalarConstant(builder, loc, stride.getType(), 0); + if (isConstantZero(offset)) + offset = createScalarConstant(builder, loc, offset.getType(), 0); + if (isConstantZero(residual)) + residual = createZeroOffsets(builder, loc, residualType); + if (!stride || !offset || !residual) + return nullptr; + + if (isConstantZero(stride) && isConstantZero(offset)) + return residual; + if (extent < 0 || extent > std::numeric_limits::max()) + return nullptr; + + auto rangeType = + RankedTensorType::get(residualType.getShape(), builder.getI32Type(), + residualType.getEncoding()); + Value range = builder.create( + loc, rangeType, 0, static_cast(extent)); + FailureOr convertedRange = + castIntegerLike(builder, loc, range, residualType); + FailureOr convertedStride = + castIntegerLike(builder, loc, stride, residualType.getElementType()); + FailureOr convertedOffset = + castIntegerLike(builder, loc, offset, residualType.getElementType()); + if (failed(convertedRange) || failed(convertedStride) || + failed(convertedOffset)) + return nullptr; + + Value strideTensor = + builder.create(loc, residualType, *convertedStride); + Value offsetTensor = + builder.create(loc, residualType, *convertedOffset); + Value affine = + builder.create(loc, *convertedRange, strideTensor); + affine = createIntegerAdd(builder, loc, affine, offsetTensor); + if (!affine) + return nullptr; + if (isConstantZero(residual)) + return affine; + return createIntegerAdd(builder, loc, affine, residual); +} + +struct AnalyzedTensorOffset { + SmallVector strides; + AnalyzedComponent uniformOffset; + AnalyzedComponent opaqueContribution; + SmallVector axisKinds; +}; + +struct TensorOffsetValues { + SmallVector strides; + Value uniformOffset; + Value opaqueContribution; + SmallVector axisKinds; +}; + +static FailureOr getIntegerTensorType(Value value) { + auto type = dyn_cast(value.getType()); + if (!type || type.getRank() == 0 || !isa(type.getElementType())) + return failure(); + return type; +} + +// Sign-extension preserves the affine provenance of a tensor offset when it +// only changes the integer element width. Keep this check shared by +// analysis and materialization so an extension is never classified as +// Structured in one phase and rebuilt as Opaque in the other. +static bool isCompatibleIntegerExtension(Value source, + RankedTensorType resultType) { + auto sourceType = dyn_cast(source.getType()); + if (!sourceType || sourceType.getRank() != resultType.getRank() || + sourceType.getShape() != resultType.getShape() || + sourceType.getEncoding() != resultType.getEncoding()) + return false; + auto sourceElement = dyn_cast(sourceType.getElementType()); + auto resultElement = dyn_cast(resultType.getElementType()); + return sourceElement && resultElement && + sourceElement.getWidth() < resultElement.getWidth(); +} + +static bool hasAnyOpaqueAxis(ArrayRef kinds) { + return llvm::is_contained(kinds, AxisKind::Opaque); +} + +static bool hasMixedAxisKinds(ArrayRef kinds) { + return hasAnyOpaqueAxis(kinds) && + llvm::is_contained(kinds, AxisKind::Structured); +} + +// A pure uniform update changes every pointer lane by the same scalar amount. +// Keeping that amount in the scalar uniform-offset component avoids turning a +// loop update into a full residual tensor when only some axes are opaque. +static bool isPureUniformUpdate(const AnalyzedTensorOffset &offset) { + return llvm::all_of( + offset.axisKinds, + [](AxisKind kind) { return kind == AxisKind::Structured; }) && + llvm::all_of(offset.strides, + [](const AnalyzedComponent &stride) { + return isZeroIdentity(stride.identity); + }) && + isZeroIdentity(offset.opaqueContribution.identity); +} + +static bool isPureUniformUpdate(const TensorOffsetValues &offset) { + return llvm::all_of( + offset.axisKinds, + [](AxisKind kind) { return kind == AxisKind::Structured; }) && + llvm::all_of(offset.strides, + [](Value stride) { return isConstantZero(stride); }) && + isConstantZero(offset.opaqueContribution); +} + +// These offsets can be decomposed and widened without moving an extension +// across arithmetic. In particular, sext(i32(a + b)) is not sext(a) + sext(b). +static bool isShapeOnlyRangeOffset(Value value) { + if (value.getDefiningOp()) + return true; + if (auto expand = value.getDefiningOp()) + return isShapeOnlyRangeOffset(expand.getSrc()); + if (auto broadcast = value.getDefiningOp()) + return isShapeOnlyRangeOffset(broadcast.getSrc()); + if (auto extension = value.getDefiningOp()) { + auto resultType = dyn_cast(value.getType()); + return resultType && + isCompatibleIntegerExtension(extension.getIn(), resultType) && + isShapeOnlyRangeOffset(extension.getIn()); + } + return false; +} + +static AnalyzedTensorOffset getOpaqueAnalyzedOffset(Value value) { + auto tensorType = cast(value.getType()); + Type scalarType = tensorType.getElementType(); + unsigned rank = tensorType.getRank(); + AnalyzedTensorOffset result; + result.axisKinds = getAxisKinds(rank, AxisKind::Opaque); + for (unsigned axis = 0; axis < rank; ++axis) { + result.strides.push_back( + {scalarType, ComponentIdentity::zero(getStrideComponent(axis))}); + } + result.uniformOffset = { + scalarType, ComponentIdentity::zero(getUniformOffsetComponent(rank))}; + result.opaqueContribution = { + tensorType, ComponentIdentity::fromValue( + value, getOpaqueContributionComponent(rank))}; + return result; +} + +static FailureOr analyzeTensorOffset(Value value) { + FailureOr tensorType = getIntegerTensorType(value); + if (failed(tensorType)) + return failure(); + unsigned rank = (*tensorType).getRank(); + Type scalarType = (*tensorType).getElementType(); + + if (rank == 1) { + FailureOr rankOne = + analyzeRank1Offset(value, value.getType()); + if (failed(rankOne)) + return failure(); + bool opaque = !isZeroIdentity(rankOne->residual.identity); + if (opaque) + return getOpaqueAnalyzedOffset(value); + return AnalyzedTensorOffset{{rankOne->stride}, + rankOne->offset, + rankOne->residual, + {AxisKind::Structured}}; + } + + auto makeStructuredZero = [&]() { + AnalyzedTensorOffset result; + result.axisKinds = getAxisKinds(rank, AxisKind::Structured); + for (unsigned axis = 0; axis < rank; ++axis) { + result.strides.push_back( + {scalarType, ComponentIdentity::zero(getStrideComponent(axis))}); + } + result.uniformOffset = { + scalarType, ComponentIdentity::zero(getUniformOffsetComponent(rank))}; + result.opaqueContribution = { + *tensorType, + ComponentIdentity::zero(getOpaqueContributionComponent(rank))}; + return result; + }; + + if (auto range = value.getDefiningOp()) { + if (rank != 1 || + range.getEnd() - range.getStart() != (*tensorType).getShape()[0]) + return getOpaqueAnalyzedOffset(value); + AnalyzedTensorOffset result = makeStructuredZero(); + result.strides[0].identity = + ComponentIdentity::fromValue(value, getStrideComponent(0)); + if (range.getStart() != 0) + result.uniformOffset.identity = + ComponentIdentity::fromValue(value, getUniformOffsetComponent(rank)); + return result; + } + + if (auto splat = value.getDefiningOp()) { + if (!isa(splat.getSrc().getType())) + return getOpaqueAnalyzedOffset(value); + AnalyzedTensorOffset result = makeStructuredZero(); + if (getScalarConstant(splat.getSrc()) != std::optional(0)) + result.uniformOffset.identity = ComponentIdentity::fromValue( + splat.getSrc(), getUniformOffsetComponent(rank)); + return result; + } + + if (FailureOr dense = getDenseIntegerSplat(value); + succeeded(dense)) { + AnalyzedTensorOffset result = makeStructuredZero(); + if (!dense->value.isZero()) + result.uniformOffset.identity = + ComponentIdentity::fromValue(value, getUniformOffsetComponent(rank)); + return result; + } + + if (auto extension = value.getDefiningOp()) { + Value source = extension.getIn(); + if (!isCompatibleIntegerExtension(source, *tensorType)) + return getOpaqueAnalyzedOffset(value); + FailureOr sourceInfo = analyzeTensorOffset(source); + if (failed(sourceInfo) || sourceInfo->strides.size() != rank) + return getOpaqueAnalyzedOffset(value); + + AnalyzedTensorOffset result = makeStructuredZero(); + result.axisKinds = sourceInfo->axisKinds; + for (unsigned axis = 0; axis < rank; ++axis) { + if (!isZeroIdentity(sourceInfo->strides[axis].identity)) + result.strides[axis].identity = + ComponentIdentity::fromValue(value, getStrideComponent(axis)); + } + if (!isZeroIdentity(sourceInfo->uniformOffset.identity)) + result.uniformOffset.identity = + ComponentIdentity::fromValue(value, getUniformOffsetComponent(rank)); + if (!isZeroIdentity(sourceInfo->opaqueContribution.identity)) + result.opaqueContribution.identity = ComponentIdentity::fromValue( + value, getOpaqueContributionComponent(rank)); + return result; + } + + if (auto expand = value.getDefiningOp()) { + FailureOr source = + analyzeTensorOffset(expand.getSrc()); + if (failed(source) || source->strides.size() + 1 != rank || + expand.getAxis() >= rank) + return getOpaqueAnalyzedOffset(value); + AnalyzedTensorOffset result = makeStructuredZero(); + unsigned sourceAxis = 0; + for (unsigned axis = 0; axis < rank; ++axis) { + if (axis == expand.getAxis()) + continue; + result.axisKinds[axis] = source->axisKinds[sourceAxis]; + if (!isZeroIdentity(source->strides[sourceAxis].identity)) + result.strides[axis].identity = + ComponentIdentity::fromValue(value, getStrideComponent(axis)); + ++sourceAxis; + } + if (!isZeroIdentity(source->uniformOffset.identity)) + result.uniformOffset.identity = + ComponentIdentity::fromValue(value, getUniformOffsetComponent(rank)); + if (!isZeroIdentity(source->opaqueContribution.identity)) + result.opaqueContribution.identity = ComponentIdentity::fromValue( + value, getOpaqueContributionComponent(rank)); + return result; + } + + if (auto broadcast = value.getDefiningOp()) { + FailureOr source = + analyzeTensorOffset(broadcast.getSrc()); + auto sourceType = dyn_cast(broadcast.getSrc().getType()); + if (failed(source) || !sourceType || sourceType.getRank() != rank) + return getOpaqueAnalyzedOffset(value); + AnalyzedTensorOffset result = makeStructuredZero(); + for (unsigned axis = 0; axis < rank; ++axis) { + bool expandsScalarAxis = sourceType.getShape()[axis] == 1 && + (*tensorType).getShape()[axis] != 1; + if (expandsScalarAxis) + continue; + result.axisKinds[axis] = source->axisKinds[axis]; + if (!isZeroIdentity(source->strides[axis].identity)) + result.strides[axis].identity = + ComponentIdentity::fromValue(value, getStrideComponent(axis)); + } + if (!isZeroIdentity(source->uniformOffset.identity)) + result.uniformOffset.identity = + ComponentIdentity::fromValue(value, getUniformOffsetComponent(rank)); + if (!isZeroIdentity(source->opaqueContribution.identity)) + result.opaqueContribution.identity = ComponentIdentity::fromValue( + value, getOpaqueContributionComponent(rank)); + return result; + } + + auto analyzeBinary = [&](Value lhs, Value rhs, + bool subtract) -> FailureOr { + FailureOr lhsInfo = analyzeTensorOffset(lhs); + FailureOr rhsInfo = analyzeTensorOffset(rhs); + if (failed(lhsInfo) || failed(rhsInfo) || lhsInfo->strides.size() != rank || + rhsInfo->strides.size() != rank) + return getOpaqueAnalyzedOffset(value); + AnalyzedTensorOffset result = makeStructuredZero(); + for (unsigned axis = 0; axis < rank; ++axis) { + result.axisKinds[axis] = + lhsInfo->axisKinds[axis] == AxisKind::Structured && + rhsInfo->axisKinds[axis] == AxisKind::Structured + ? AxisKind::Structured + : AxisKind::Opaque; + if (result.axisKinds[axis] == AxisKind::Structured) { + result.strides[axis].identity = + subtract ? getSubIdentity(lhsInfo->strides[axis].identity, + rhsInfo->strides[axis].identity, value, + getStrideComponent(axis)) + : getAddIdentity(lhsInfo->strides[axis].identity, + rhsInfo->strides[axis].identity, value, + getStrideComponent(axis)); + } + } + if (hasAnyOpaqueAxis(result.axisKinds)) { + result.opaqueContribution.identity = ComponentIdentity::fromValue( + value, getOpaqueContributionComponent(rank)); + return result; + } + result.uniformOffset.identity = + subtract ? getSubIdentity(lhsInfo->uniformOffset.identity, + rhsInfo->uniformOffset.identity, value, + getUniformOffsetComponent(rank)) + : getAddIdentity(lhsInfo->uniformOffset.identity, + rhsInfo->uniformOffset.identity, value, + getUniformOffsetComponent(rank)); + return result; + }; + + if (auto add = value.getDefiningOp()) + return analyzeBinary(add.getLhs(), add.getRhs(), false); + if (auto sub = value.getDefiningOp()) + return analyzeBinary(sub.getLhs(), sub.getRhs(), true); + + if (auto mul = value.getDefiningOp()) { + Value affineOperand = mul.getLhs(); + Value scale = mul.getRhs(); + if (!isUniformIntegerScale(scale)) { + affineOperand = mul.getRhs(); + scale = mul.getLhs(); + } + if (!isUniformIntegerScale(scale)) + return getOpaqueAnalyzedOffset(value); + FailureOr input = analyzeTensorOffset(affineOperand); + if (failed(input) || input->strides.size() != rank) + return getOpaqueAnalyzedOffset(value); + AnalyzedTensorOffset result = makeStructuredZero(); + result.axisKinds = input->axisKinds; + for (unsigned axis = 0; axis < rank; ++axis) { + if (result.axisKinds[axis] == AxisKind::Structured) + result.strides[axis].identity = + getScaledIdentity(input->strides[axis].identity, scale, value, + getStrideComponent(axis)); + } + if (hasAnyOpaqueAxis(result.axisKinds)) { + result.opaqueContribution.identity = ComponentIdentity::fromValue( + value, getOpaqueContributionComponent(rank)); + } else { + result.uniformOffset.identity = + getScaledIdentity(input->uniformOffset.identity, scale, value, + getUniformOffsetComponent(rank)); + } + return result; + } + + return getOpaqueAnalyzedOffset(value); +} + +// Updating only existing structured axes does not change the opaque residual. +// Restrict this to an already-wide descriptor: widening a narrow descriptor's +// fields separately could lose overflow in its complete offset. Check the +// original offset in both phases so analysis and materialization agree on +// the residual's identity even when operands are remapped. +static bool canPreserveMixedResidual(Value offset, Type currentResidualType, + bool scalarBase, + ArrayRef currentKinds, + ArrayRef resultKinds) { + auto currentType = dyn_cast(currentResidualType); + auto offsetType = dyn_cast(offset.getType()); + if (!scalarBase || !hasMixedAxisKinds(currentKinds) || + currentKinds != resultKinds || !currentType || !offsetType || + !currentType.getElementType().isInteger(64) || + !isa(offsetType.getElementType()) || + offsetType.getElementType().getIntOrFloatBitWidth() > 64 || + !isShapeOnlyRangeOffset(offset)) + return false; + FailureOr delta = analyzeTensorOffset(offset); + if (failed(delta) || delta->strides.size() != currentKinds.size() || + !isZeroIdentity(delta->opaqueContribution.identity)) + return false; + for (unsigned axis = 0; axis < currentKinds.size(); ++axis) { + if (currentKinds[axis] == AxisKind::Opaque && + !isZeroIdentity(delta->strides[axis].identity)) + return false; + } + return true; +} + +static FailureOr materializeAxisRange(OpBuilder &builder, Location loc, + RankedTensorType tensorType, + unsigned axis) { + if (axis >= static_cast(tensorType.getRank())) + return failure(); + int64_t extent = tensorType.getShape()[axis]; + if (extent < 0 || extent > std::numeric_limits::max()) + return failure(); + auto rangeType = RankedTensorType::get({extent}, builder.getI32Type()); + Value range = builder.create( + loc, rangeType, 0, static_cast(extent)); + auto elementRangeType = + RankedTensorType::get({extent}, tensorType.getElementType()); + FailureOr converted = + castIntegerLike(builder, loc, range, elementRangeType); + if (failed(converted)) + return failure(); + Value expanded = *converted; + for (unsigned dimension = 0; + dimension < static_cast(tensorType.getRank()); ++dimension) { + if (dimension == axis) + continue; + expanded = builder.create(loc, expanded, dimension); + } + if (expanded.getType() != tensorType) + expanded = builder.create(loc, tensorType, expanded); + return expanded; +} + +static FailureOr materializeStructuredContribution( + ArrayRef strides, ArrayRef axisKinds, + RankedTensorType tensorType, OpBuilder &builder, Location loc) { + if (strides.size() != axisKinds.size() || + strides.size() != static_cast(tensorType.getRank())) + return failure(); + Value contribution = createZeroOffsets(builder, loc, tensorType); + for (unsigned axis = 0; axis < strides.size(); ++axis) { + if (axisKinds[axis] != AxisKind::Structured || + isConstantZero(strides[axis])) + continue; + FailureOr range = + materializeAxisRange(builder, loc, tensorType, axis); + FailureOr stride = castIntegerLike(builder, loc, strides[axis], + tensorType.getElementType()); + if (failed(range) || failed(stride)) + return failure(); + Value strideTensor = + builder.create(loc, tensorType, *stride); + Value axisContribution = + builder.create(loc, *range, strideTensor); + contribution = + createIntegerAdd(builder, loc, contribution, axisContribution); + if (!contribution) + return failure(); + } + return contribution; +} + +static FailureOr +materializeTensorOffsetFields(Value value, OpBuilder &builder, Location loc) { + FailureOr tensorType = getIntegerTensorType(value); + if (failed(tensorType)) + return failure(); + unsigned rank = (*tensorType).getRank(); + Type scalarType = (*tensorType).getElementType(); + + auto makeZero = [&](AxisKind kind) -> FailureOr { + TensorOffsetValues result; + result.axisKinds = getAxisKinds(rank, kind); + for (unsigned axis = 0; axis < rank; ++axis) { + Value stride = createScalarConstant(builder, loc, scalarType, 0); + if (!stride) + return failure(); + result.strides.push_back(stride); + } + result.uniformOffset = createScalarConstant(builder, loc, scalarType, 0); + result.opaqueContribution = createZeroOffsets(builder, loc, *tensorType); + if (!result.uniformOffset || !result.opaqueContribution) + return failure(); + return result; + }; + + auto fallback = [&]() -> FailureOr { + FailureOr result = makeZero(AxisKind::Opaque); + if (failed(result)) + return failure(); + result->opaqueContribution = value; + return result; + }; + + if (rank == 1) { + FailureOr rankOne = + materializeRank1Offset(value, value.getType(), builder, loc); + if (failed(rankOne)) + return failure(); + if (!isConstantZero(rankOne->residual)) + return fallback(); + return TensorOffsetValues{{rankOne->stride}, + rankOne->offset, + rankOne->residual, + {AxisKind::Structured}}; + } + + if (auto range = value.getDefiningOp()) { + if (rank != 1 || + range.getEnd() - range.getStart() != (*tensorType).getShape()[0]) + return fallback(); + FailureOr result = makeZero(AxisKind::Structured); + if (failed(result)) + return failure(); + result->strides[0] = createScalarConstant(builder, loc, scalarType, 1); + result->uniformOffset = + createScalarConstant(builder, loc, scalarType, range.getStart()); + if (!result->strides[0] || !result->uniformOffset) + return failure(); + return result; + } + + if (auto splat = value.getDefiningOp()) { + if (!isa(splat.getSrc().getType())) + return fallback(); + FailureOr result = makeZero(AxisKind::Structured); + if (failed(result)) + return failure(); + FailureOr uniform = + castIntegerLike(builder, loc, splat.getSrc(), scalarType); + if (failed(uniform)) + return failure(); + result->uniformOffset = *uniform; + return result; + } + + if (FailureOr dense = getDenseIntegerSplat(value); + succeeded(dense)) { + OpBuilder::InsertionGuard insertionGuard(builder); + builder.setInsertionPointAfter(value.getDefiningOp()); + FailureOr result = makeZero(AxisKind::Structured); + if (failed(result)) + return failure(); + Value uniform = + createScalarConstant(builder, loc, scalarType, dense->value); + if (!uniform) + return failure(); + result->uniformOffset = uniform; + return result; + } + + if (auto extension = value.getDefiningOp()) { + if (!isCompatibleIntegerExtension(extension.getIn(), *tensorType)) + return fallback(); + FailureOr source = + materializeTensorOffsetFields(extension.getIn(), builder, loc); + if (failed(source) || source->strides.size() != rank) + return fallback(); + + FailureOr result = makeZero(AxisKind::Structured); + if (failed(result)) + return failure(); + result->axisKinds = source->axisKinds; + for (unsigned axis = 0; axis < rank; ++axis) { + FailureOr stride = + castIntegerLike(builder, loc, source->strides[axis], scalarType); + if (failed(stride)) + return fallback(); + result->strides[axis] = *stride; + } + FailureOr uniform = + castIntegerLike(builder, loc, source->uniformOffset, scalarType); + if (failed(uniform)) + return fallback(); + result->uniformOffset = *uniform; + if (!isConstantZero(source->opaqueContribution)) { + result->opaqueContribution = builder.create( + loc, *tensorType, source->opaqueContribution); + } + return result; + } + + if (auto expand = value.getDefiningOp()) { + FailureOr source = + materializeTensorOffsetFields(expand.getSrc(), builder, loc); + if (failed(source) || source->strides.size() + 1 != rank || + expand.getAxis() >= rank) + return fallback(); + FailureOr result = makeZero(AxisKind::Structured); + if (failed(result)) + return failure(); + unsigned sourceAxis = 0; + for (unsigned axis = 0; axis < rank; ++axis) { + if (axis == expand.getAxis()) + continue; + result->strides[axis] = source->strides[sourceAxis]; + result->axisKinds[axis] = source->axisKinds[sourceAxis]; + ++sourceAxis; + } + result->uniformOffset = source->uniformOffset; + return result; + } + + if (auto broadcast = value.getDefiningOp()) { + FailureOr source = + materializeTensorOffsetFields(broadcast.getSrc(), builder, loc); + auto sourceType = dyn_cast(broadcast.getSrc().getType()); + if (failed(source) || !sourceType || sourceType.getRank() != rank) + return fallback(); + FailureOr result = makeZero(AxisKind::Structured); + if (failed(result)) + return failure(); + for (unsigned axis = 0; axis < rank; ++axis) { + bool expandsScalarAxis = sourceType.getShape()[axis] == 1 && + (*tensorType).getShape()[axis] != 1; + if (expandsScalarAxis) + continue; + result->strides[axis] = source->strides[axis]; + result->axisKinds[axis] = source->axisKinds[axis]; + } + result->uniformOffset = source->uniformOffset; + return result; + } + + auto materializeBinary = [&](Value lhs, Value rhs, + bool subtract) -> FailureOr { + FailureOr lhsInfo = + materializeTensorOffsetFields(lhs, builder, loc); + FailureOr rhsInfo = + materializeTensorOffsetFields(rhs, builder, loc); + if (failed(lhsInfo) || failed(rhsInfo) || lhsInfo->strides.size() != rank || + rhsInfo->strides.size() != rank) + return fallback(); + FailureOr result = makeZero(AxisKind::Structured); + if (failed(result)) + return failure(); + for (unsigned axis = 0; axis < rank; ++axis) { + result->axisKinds[axis] = + lhsInfo->axisKinds[axis] == AxisKind::Structured && + rhsInfo->axisKinds[axis] == AxisKind::Structured + ? AxisKind::Structured + : AxisKind::Opaque; + if (result->axisKinds[axis] == AxisKind::Structured) { + result->strides[axis] = + subtract ? createIntegerSub(builder, loc, lhsInfo->strides[axis], + rhsInfo->strides[axis]) + : createIntegerAdd(builder, loc, lhsInfo->strides[axis], + rhsInfo->strides[axis]); + if (!result->strides[axis]) + return failure(); + } + } + if (!hasAnyOpaqueAxis(result->axisKinds)) { + result->uniformOffset = + subtract ? createIntegerSub(builder, loc, lhsInfo->uniformOffset, + rhsInfo->uniformOffset) + : createIntegerAdd(builder, loc, lhsInfo->uniformOffset, + rhsInfo->uniformOffset); + if (!result->uniformOffset) + return failure(); + } + return result; + }; + + FailureOr result = failure(); + if (auto add = value.getDefiningOp()) + result = materializeBinary(add.getLhs(), add.getRhs(), false); + else if (auto sub = value.getDefiningOp()) + result = materializeBinary(sub.getLhs(), sub.getRhs(), true); + else if (auto mul = value.getDefiningOp()) { + Value affineOperand = mul.getLhs(); + FailureOr scale = + materializeUniformIntegerScale(mul.getRhs(), builder, loc); + if (failed(scale)) { + affineOperand = mul.getRhs(); + scale = materializeUniformIntegerScale(mul.getLhs(), builder, loc); + } + if (failed(scale)) + return fallback(); + result = materializeTensorOffsetFields(affineOperand, builder, loc); + if (succeeded(result)) { + for (unsigned axis = 0; axis < rank; ++axis) { + if (result->axisKinds[axis] == AxisKind::Structured) + result->strides[axis] = + scaleIntegerValue(builder, loc, result->strides[axis], *scale); + if (!result->strides[axis]) + return failure(); + } + if (!hasAnyOpaqueAxis(result->axisKinds)) { + result->uniformOffset = + scaleIntegerValue(builder, loc, result->uniformOffset, *scale); + if (!result->uniformOffset) + return failure(); + } + } + } else { + return fallback(); + } + + if (failed(result)) + return failure(); + if (!hasAnyOpaqueAxis(result->axisKinds)) + return result; + + FailureOr structured = materializeStructuredContribution( + result->strides, result->axisKinds, *tensorType, builder, loc); + if (failed(structured)) + return failure(); + FailureOr complete = + castIntegerLike(builder, loc, value, (*tensorType)); + if (failed(complete)) + return failure(); + Value opaque = createIntegerSub(builder, loc, *complete, *structured); + if (!opaque) + return failure(); + result->uniformOffset = createScalarConstant(builder, loc, scalarType, 0); + result->opaqueContribution = opaque; + for (unsigned axis = 0; axis < rank; ++axis) { + if (result->axisKinds[axis] == AxisKind::Opaque) + result->strides[axis] = createScalarConstant(builder, loc, scalarType, 0); + if (!result->strides[axis]) + return failure(); + } + if (!result->uniformOffset) + return failure(); + return result; +} + +static Value materializeTensorCompleteOffsets(const DecomposedValue &value, + OpBuilder &builder, + Location loc) { + if (!hasValidLayout(value)) + return nullptr; + auto pointerTensor = cast(value.originalType); + unsigned rank = pointerTensor.getRank(); + if (rank == 1) + return materializeRank1CompleteOffsets(value, builder, loc); + + auto offsetsType = cast( + value.components[getOpaqueContributionComponent(rank)].getType()); + SmallVector strides; + strides.reserve(rank); + for (unsigned axis = 0; axis < rank; ++axis) + strides.push_back(value.components[getStrideComponent(axis)]); + SmallVector allStructured = + getAxisKinds(rank, AxisKind::Structured); + FailureOr structured = materializeStructuredContribution( + strides, allStructured, offsetsType, builder, loc); + if (failed(structured)) + return nullptr; + + Value uniform = value.components[getUniformOffsetComponent(rank)]; + if (!isConstantZero(uniform)) { + FailureOr converted = + castIntegerLike(builder, loc, uniform, offsetsType.getElementType()); + if (failed(converted)) + return nullptr; + Value uniformTensor = + builder.create(loc, offsetsType, *converted); + *structured = createIntegerAdd(builder, loc, *structured, uniformTensor); + if (!*structured) + return nullptr; + } + + Value opaque = value.components[getOpaqueContributionComponent(rank)]; + if (isConstantZero(opaque)) + return *structured; + return createIntegerAdd(builder, loc, *structured, opaque); +} + +static FailureOr materializeScalarBaseAddress(OpBuilder &builder, + Location loc, Value base) { + if (!isa(base.getType())) + return failure(); + + if (auto intToPtr = base.getDefiningOp(); + intToPtr && intToPtr.getSrc().getType().isInteger(64)) + return intToPtr.getSrc(); + + // The symbolic base identity is the scalar pointer Value, so its concrete + // i64 address must dominate every splat of that Value. In particular, two + // sibling if arms may each build a tensor pointer from one function argument + // while the rebuilt result lives after the if. + OpBuilder::InsertionGuard insertionGuard(builder); + if (auto blockArgument = dyn_cast(base)) { + Block *owner = blockArgument.getOwner(); + Operation *lastLeadingAddress = nullptr; + for (Operation &operation : *owner) { + auto existing = dyn_cast(&operation); + if (!existing) + break; + if (existing.getSrc() == base && + existing.getResult().getType().isInteger(64)) + return existing.getResult(); + lastLeadingAddress = &operation; + } + if (lastLeadingAddress) + builder.setInsertionPointAfter(lastLeadingAddress); + else + builder.setInsertionPointToStart(owner); + } else if (Operation *definingOp = base.getDefiningOp()) { + if (Operation *next = definingOp->getNextNode()) { + if (auto existing = dyn_cast(next); + existing && existing.getSrc() == base && + existing.getResult().getType().isInteger(64)) + return existing.getResult(); + } + builder.setInsertionPointAfter(definingOp); + } else { + return failure(); + } + + return builder.create(loc, builder.getI64Type(), base) + .getResult(); +} + +// Recover the original scalar pointer only when the descriptor component is +// the direct address produced from that pointer. This preserves native +// memref provenance for loop-invariant tensor-of-pointer bases while keeping +// the conservative int-to-ptr fallback for a base that was actually changed +// by an SCF boundary. +static Value recoverNativeScalarBase(Value baseAddress, + triton::PointerType expectedType) { + auto ptrToInt = baseAddress.getDefiningOp(); + if (!ptrToInt || ptrToInt.getSrc().getType() != expectedType) + return nullptr; + return ptrToInt.getSrc(); +} + +static SmallVector +getCandidateComponents(const AnalyzedValue &value) { + SmallVector indices; + unsigned first = hasScalarBase(value) ? kBaseComponent : kBaseComponent + 1; + for (unsigned index = first; index < value.components.size(); ++index) + indices.push_back(index); + return indices; +} + +static SmallVector getOffsetComponentIndices(Type pointerType) { + unsigned rank = cast(pointerType).getRank(); + SmallVector indices; + for (unsigned axis = 0; axis < rank; ++axis) + indices.push_back(getStrideComponent(axis)); + indices.push_back(getUniformOffsetComponent(rank)); + indices.push_back(getOpaqueContributionComponent(rank)); + return indices; +} + +static bool offsetComponentTypesDiffer(const AnalyzedValue &lhs, + const AnalyzedValue &rhs) { + for (unsigned component : getOffsetComponentIndices(lhs.originalType)) { + if (lhs.components[component].type != rhs.components[component].type) + return true; + } + return false; +} + +static bool canSafelyJoinOffsetTypes(const AnalyzedValue &lhs, + const AnalyzedValue &rhs) { + if (!offsetComponentTypesDiffer(lhs, rhs)) + return true; + unsigned rank = cast(lhs.originalType).getRank(); + auto lhsType = dyn_cast( + lhs.components[getUniformOffsetComponent(rank)].type); + auto rhsType = dyn_cast( + rhs.components[getUniformOffsetComponent(rank)].type); + if (!lhsType || !rhsType || lhsType.getWidth() == rhsType.getWidth()) + return false; + const AnalyzedValue &narrower = + lhsType.getWidth() < rhsType.getWidth() ? lhs : rhs; + for (unsigned axis = 0; axis < rank; ++axis) { + if (!isZeroIdentity(narrower.components[getStrideComponent(axis)].identity)) + return false; + } + return isZeroIdentity( + narrower.components[getUniformOffsetComponent(rank)].identity); +} + +static FailureOr> +joinAxisKinds(const AnalyzedValue &lhs, const AnalyzedValue &rhs) { + FailureOr> lhsKinds = getAxisKinds(lhs); + FailureOr> rhsKinds = getAxisKinds(rhs); + if (failed(lhsKinds) || failed(rhsKinds) || + lhsKinds->size() != rhsKinds->size()) + return failure(); + SmallVector result; + for (auto [lhsKind, rhsKind] : llvm::zip(*lhsKinds, *rhsKinds)) { + result.push_back(lhsKind == AxisKind::Structured && + rhsKind == AxisKind::Structured + ? AxisKind::Structured + : AxisKind::Opaque); + } + return result; +} + +// Keep only identity-changing fields that survive normalization to the joined +// axis schema. For example, joining a Structured axis with an Opaque axis +// makes that axis stride and the shared uniform offset canonical zero values; +// carrying either value through SCF would create dead positional slots. Any +// change represented by those fields is transferred through the complete +// opaque contribution instead. +static FailureOr> +filterTransferredComponentsForJoinedAxes( + const AnalyzedValue &value, ArrayRef joinedKinds, + ArrayRef identityChangingComponents) { + if (!hasValidLayout(value)) + return failure(); + unsigned rank = cast(value.originalType).getRank(); + if (joinedKinds.size() != rank) + return failure(); + + bool hasOpaqueAxis = hasAnyOpaqueAxis(joinedKinds); + bool opaqueContributionChanges = false; + SmallVector transferred; + for (unsigned component : identityChangingComponents) { + if (component >= value.components.size()) + return failure(); + if (component == kBaseComponent) { + transferred.push_back(component); + continue; + } + if (component >= getStrideComponent(0) && + component < getStrideComponent(rank)) { + unsigned axis = component - getStrideComponent(0); + if (joinedKinds[axis] == AxisKind::Structured) + transferred.push_back(component); + else + opaqueContributionChanges = true; + continue; + } + if (component == getUniformOffsetComponent(rank)) { + if (!hasOpaqueAxis) + transferred.push_back(component); + else + opaqueContributionChanges = true; + continue; + } + if (component == getOpaqueContributionComponent(rank)) { + if (hasOpaqueAxis) + opaqueContributionChanges = true; + continue; + } + return failure(); + } + if (opaqueContributionChanges) + transferred.push_back(getOpaqueContributionComponent(rank)); + llvm::sort(transferred); + return transferred; +} + +static SmallVector +getTensorPointerAttributes(MLIRContext *context, bool baseIsScalar, + ArrayRef axisKinds) { + return {BoolAttr::get(context, baseIsScalar), + getAxisKindsAttr(context, axisKinds)}; +} + +static LogicalResult +normalizeOffsetComponents(DecomposedValue &value, + ArrayRef targetKinds, OpBuilder &builder, + Location loc, bool preserveMixedUniform = false) { + if (!hasValidLayout(value)) + return failure(); + unsigned rank = cast(value.originalType).getRank(); + FailureOr> currentKinds = getAxisKinds(value); + if (failed(currentKinds) || targetKinds.size() != rank) + return failure(); + for (unsigned axis = 0; axis < rank; ++axis) { + if ((*currentKinds)[axis] == AxisKind::Opaque && + targetKinds[axis] == AxisKind::Structured) + return failure(); + } + + // A selected loop-carried uniform offset is already a canonical scalar + // component. When a mixed per-axis schema is unchanged, folding that scalar + // back into the opaque tensor would recreate a large residual on every + // backedge. This fast path is enabled only by the loop-specific policy hook; + // if and scope normalization retain the established complete-offset schema. + bool sameKinds = llvm::equal(*currentKinds, targetKinds); + if (preserveMixedUniform && sameKinds && hasMixedAxisKinds(targetKinds)) + return success(); + + Value complete = materializeTensorCompleteOffsets(value, builder, loc); + if (!complete) + return failure(); + auto completeType = cast(complete.getType()); + Type scalarType = completeType.getElementType(); + SmallVector retainedStrides; + for (unsigned axis = 0; axis < rank; ++axis) { + FailureOr stride = castIntegerLike( + builder, loc, value.components[getStrideComponent(axis)], scalarType); + if (failed(stride)) + return failure(); + if (targetKinds[axis] == AxisKind::Opaque) + *stride = createScalarConstant(builder, loc, scalarType, 0); + if (!*stride) + return failure(); + retainedStrides.push_back(*stride); + value.components[getStrideComponent(axis)] = *stride; + } + + if (hasAnyOpaqueAxis(targetKinds)) { + FailureOr structured = materializeStructuredContribution( + retainedStrides, targetKinds, completeType, builder, loc); + if (failed(structured)) + return failure(); + Value opaque = createIntegerSub(builder, loc, complete, *structured); + Value zero = createScalarConstant(builder, loc, scalarType, 0); + if (!opaque || !zero) + return failure(); + value.components[getUniformOffsetComponent(rank)] = zero; + value.components[getOpaqueContributionComponent(rank)] = opaque; + } else { + FailureOr uniform = castIntegerLike( + builder, loc, value.components[getUniformOffsetComponent(rank)], + scalarType); + if (failed(uniform)) + return failure(); + value.components[getUniformOffsetComponent(rank)] = *uniform; + value.components[getOpaqueContributionComponent(rank)] = + createZeroOffsets(builder, loc, completeType); + } + return success(); +} + +class TensorPtrDecomposePolicy final : public ControlFlowRewritePolicy { +public: + bool matches(Type type) const override { return isTensorPointerType(type); } + + FailureOr + analyzeValue(Value value, + ControlFlowAnalysisContext &context) const override { + if (const AnalyzedValue *known = context.lookupValue(value)) { + if (!matches(known->originalType)) + return failure(); + return *known; + } + + // Broadcasting a scalar-base descriptor preserves its axis kinds. An + // expanded unit dimension has stride zero, and the opaque contribution + // must be broadcast along with the pointers. Unknown tensor bases retain + // the complete-pointer fallback below. + if (auto broadcast = value.getDefiningOp()) { + FailureOr result = + context.analyzeValue(broadcast.getSrc()); + if (succeeded(result) && hasValidLayout(*result) && + hasScalarBase(*result) && + isCompatiblePointerBroadcast(broadcast.getSrc().getType(), + value.getType())) { + FailureOr> kinds = getAxisKinds(*result); + auto sourceType = cast(broadcast.getSrc().getType()); + auto resultType = cast(value.getType()); + unsigned rank = resultType.getRank(); + if (succeeded(kinds)) { + for (unsigned axis = 0; axis < rank; ++axis) { + if (sourceType.getShape()[axis] == 1 && + resultType.getShape()[axis] != 1) { + result->components[getStrideComponent(axis)].identity = + ComponentIdentity::zero(getStrideComponent(axis)); + } + } + Type scalarType = + result->components[getUniformOffsetComponent(rank)].type; + auto resultOffsetsType = RankedTensorType::get( + resultType.getShape(), scalarType, resultType.getEncoding()); + auto &opaque = + result->components[getOpaqueContributionComponent(rank)]; + if (opaque.type != resultOffsetsType && + !isZeroIdentity(opaque.identity)) + opaque.identity = ComponentIdentity::fromValue( + value, getOpaqueContributionComponent(rank)); + opaque.type = resultOffsetsType; + result->originalType = value.getType(); + result->attributes[kAxisKindsAttribute] = + getAxisKindsAttr(value.getContext(), *kinds); + return *result; + } + } + } + + if (auto addPtr = value.getDefiningOp()) { + FailureOr result = context.analyzeValue(addPtr.getPtr()); + FailureOr delta = + analyzeTensorOffset(addPtr.getOffset()); + if (failed(result) || !hasValidLayout(*result) || failed(delta)) + return failure(); + FailureOr> currentKinds = getAxisKinds(*result); + if (failed(currentKinds) || currentKinds->size() != delta->strides.size()) + return failure(); + unsigned rank = currentKinds->size(); + FailureOr joinedResidualType = getWiderIntegerLikeType( + result->components[getOpaqueContributionComponent(rank)].type, + delta->opaqueContribution.type); + if (failed(joinedResidualType)) + return failure(); + auto joinedTensorType = cast(*joinedResidualType); + Type scalarType = joinedTensorType.getElementType(); + SmallVector resultKinds; + for (unsigned axis = 0; axis < rank; ++axis) { + AxisKind kind = hasScalarBase(*result) && + (*currentKinds)[axis] == AxisKind::Structured && + delta->axisKinds[axis] == AxisKind::Structured + ? AxisKind::Structured + : AxisKind::Opaque; + resultKinds.push_back(kind); + result->components[getStrideComponent(axis)] = { + scalarType, + kind == AxisKind::Structured + ? getAddIdentity( + result->components[getStrideComponent(axis)].identity, + delta->strides[axis].identity, value, + getStrideComponent(axis)) + : ComponentIdentity::zero(getStrideComponent(axis))}; + } + bool preserveMixedResidual = + hasMixedAxisKinds(resultKinds) && + (isPureUniformUpdate(*delta) || + canPreserveMixedResidual( + addPtr.getOffset(), + result->components[getOpaqueContributionComponent(rank)].type, + hasScalarBase(*result), *currentKinds, resultKinds)) && + isa( + result->components[getOpaqueContributionComponent(rank)].type); + if (preserveMixedResidual) { + result->components[getUniformOffsetComponent(rank)] = { + scalarType, + getAddIdentity( + result->components[getUniformOffsetComponent(rank)].identity, + delta->uniformOffset.identity, value, + getUniformOffsetComponent(rank))}; + result->components[getOpaqueContributionComponent(rank)].type = + joinedTensorType; + } else if (hasAnyOpaqueAxis(resultKinds)) { + result->components[getUniformOffsetComponent(rank)] = { + scalarType, + ComponentIdentity::zero(getUniformOffsetComponent(rank))}; + result->components[getOpaqueContributionComponent(rank)] = { + joinedTensorType, ComponentIdentity::fromValue( + value, getOpaqueContributionComponent(rank))}; + } else { + result->components[getUniformOffsetComponent(rank)] = { + scalarType, + getAddIdentity( + result->components[getUniformOffsetComponent(rank)].identity, + delta->uniformOffset.identity, value, + getUniformOffsetComponent(rank))}; + result->components[getOpaqueContributionComponent(rank)] = { + joinedTensorType, + ComponentIdentity::zero(getOpaqueContributionComponent(rank))}; + } + result->originalType = value.getType(); + result->attributes[kAxisKindsAttribute] = + getAxisKindsAttr(value.getContext(), resultKinds); + return *result; + } + + if (auto splat = value.getDefiningOp()) { + if (!isa(splat.getSrc().getType())) + return failure(); + auto offsetsType = getDefaultOffsetsType(value.getType()); + unsigned rank = cast(value.getType()).getRank(); + Type scalarType = offsetsType.getElementType(); + SmallVector components = { + {IntegerType::get(value.getContext(), 64), + ComponentIdentity::fromValue(splat.getSrc(), kBaseComponent)}}; + for (unsigned axis = 0; axis < rank; ++axis) + components.push_back( + {scalarType, ComponentIdentity::zero(getStrideComponent(axis))}); + components.push_back({scalarType, ComponentIdentity::zero( + getUniformOffsetComponent(rank))}); + components.push_back( + {offsetsType, + ComponentIdentity::zero(getOpaqueContributionComponent(rank))}); + return AnalyzedValue{ + value.getType(), std::move(components), + getTensorPointerAttributes(value.getContext(), true, + getAxisKinds(rank, AxisKind::Structured))}; + } + + if (!matches(value.getType())) + return failure(); + auto offsetsType = getDefaultOffsetsType(value.getType()); + unsigned rank = cast(value.getType()).getRank(); + Type scalarType = offsetsType.getElementType(); + SmallVector components = { + {value.getType(), ComponentIdentity::fromValue(value, kBaseComponent)}}; + for (unsigned axis = 0; axis < rank; ++axis) + components.push_back( + {scalarType, ComponentIdentity::zero(getStrideComponent(axis))}); + components.push_back( + {scalarType, ComponentIdentity::zero(getUniformOffsetComponent(rank))}); + components.push_back( + {offsetsType, + ComponentIdentity::zero(getOpaqueContributionComponent(rank))}); + return AnalyzedValue{ + value.getType(), std::move(components), + getTensorPointerAttributes(value.getContext(), false, + getAxisKinds(rank, AxisKind::Opaque))}; + } + + FailureOr> + getLoopCandidateComponents(const AnalyzedValue &value) const override { + if (!hasValidLayout(value)) + return failure(); + return getCandidateComponents(value); + } + + FailureOr> + getLoopTransferredComponents(const AnalyzedValue &initial, + const AnalyzedValue ®ionArgument, + const AnalyzedValue &next) const override { + if (!hasSameRepresentation(initial, regionArgument) || + !hasSameRepresentation(initial, next) || + !canSafelyJoinOffsetTypes(initial, next)) + return failure(); + FailureOr> joinedKinds = joinAxisKinds(initial, next); + if (failed(joinedKinds)) + return failure(); + + SmallVector identityChangingComponents; + for (unsigned component : getCandidateComponents(initial)) { + const ComponentIdentity &nextIdentity = + next.components[component].identity; + bool forwardsCurrent = + nextIdentity == regionArgument.components[component].identity; + bool restoresInitial = + nextIdentity == initial.components[component].identity; + if (!forwardsCurrent && !restoresInitial) + identityChangingComponents.push_back(component); + } + unsigned rank = cast(initial.originalType).getRank(); + unsigned uniformComponent = getUniformOffsetComponent(rank); + unsigned opaqueComponent = getOpaqueContributionComponent(rank); + bool carriesOnlyUniform = + hasMixedAxisKinds(*joinedKinds) && + identityChangingComponents.size() == 1 && + identityChangingComponents.front() == uniformComponent && + next.components[opaqueComponent].identity == + regionArgument.components[opaqueComponent].identity; + + SmallVector transferred; + if (carriesOnlyUniform) { + transferred.push_back(uniformComponent); + } else { + FailureOr> filtered = + filterTransferredComponentsForJoinedAxes(initial, *joinedKinds, + identityChangingComponents); + if (failed(filtered)) + return failure(); + transferred = std::move(*filtered); + } + for (unsigned component : transferred) { + if (failed(joinComponentTypes(initial.components[component].type, + next.components[component].type))) + return failure(); + } + return transferred; + } + + FailureOr> + getIfTransferredComponents(const AnalyzedValue &thenValue, + const AnalyzedValue &elseValue) const override { + if (!hasSameRepresentation(thenValue, elseValue) || + !canSafelyJoinOffsetTypes(thenValue, elseValue)) + return failure(); + FailureOr> joinedKinds = + joinAxisKinds(thenValue, elseValue); + if (failed(joinedKinds)) + return failure(); + SmallVector identityChangingComponents; + for (unsigned component : getCandidateComponents(thenValue)) { + if (thenValue.components[component].identity != + elseValue.components[component].identity) + identityChangingComponents.push_back(component); + } + FailureOr> transferred = + filterTransferredComponentsForJoinedAxes(thenValue, *joinedKinds, + identityChangingComponents); + if (failed(transferred)) + return failure(); + for (unsigned component : *transferred) { + if (failed(joinComponentTypes(thenValue.components[component].type, + elseValue.components[component].type))) + return failure(); + } + return *transferred; + } + + FailureOr> + mergeControlFlowAttributes(const AnalyzedValue &lhs, + const AnalyzedValue &rhs) const override { + if (!hasValidLayout(lhs) || !hasValidLayout(rhs) || + lhs.originalType != rhs.originalType || + hasScalarBase(lhs) != hasScalarBase(rhs)) + return failure(); + FailureOr> kinds = joinAxisKinds(lhs, rhs); + if (failed(kinds)) + return failure(); + return getTensorPointerAttributes(lhs.originalType.getContext(), + hasScalarBase(lhs), *kinds); + } + + FailureOr joinComponentTypes(Type lhs, Type rhs) const override { + return getWiderIntegerLikeType(lhs, rhs); + } + + bool shouldDecomposeOperation(Operation *op) const override { + return isa(op); + } + + bool + shouldCacheRebuiltDecomposition(const DecomposedValue &value) const override { + // A scalar base can be reconstructed as an integer address by T2U. An + // opaque tensor-of-pointers base must instead be re-analyzed so T2U can + // lower each lane to an integer address before T2L sees it. + return hasValidLayout(value) && hasScalarBase(value); + } + + bool requiresPointerDescriptorBoundaryMarker() const override { return true; } + + bool shouldMarkOperationRecomposition(Operation *op) const override { + return isa(op); + } + + LogicalResult normalizeControlFlowValue(DecomposedValue &value, + ArrayRef targetAttributes, + OpBuilder &builder, + Location loc) const override { + if (!hasValidLayout(value) || targetAttributes.size() != 2) + return failure(); + auto targetBase = + dyn_cast(targetAttributes[kBaseIsScalarAttribute]); + auto targetKinds = + dyn_cast(targetAttributes[kAxisKindsAttribute]); + if (!targetBase || !targetKinds || + targetBase.getValue() != hasScalarBase(value)) + return failure(); + SmallVector kinds; + for (int32_t rawKind : targetKinds.asArrayRef()) { + if (rawKind != static_cast(AxisKind::Opaque) && + rawKind != static_cast(AxisKind::Structured)) + return failure(); + kinds.push_back(static_cast(rawKind)); + } + if (failed(normalizeOffsetComponents(value, kinds, builder, loc))) + return failure(); + value.attributes.assign(targetAttributes.begin(), targetAttributes.end()); + return success(); + } + + LogicalResult normalizeLoopCarriedValue(DecomposedValue &value, + ArrayRef carriedComponents, + ArrayRef targetAttributes, + OpBuilder &builder, + Location loc) const override { + if (!hasValidLayout(value) || targetAttributes.size() != 2) + return failure(); + auto targetBase = + dyn_cast(targetAttributes[kBaseIsScalarAttribute]); + auto targetKinds = + dyn_cast(targetAttributes[kAxisKindsAttribute]); + unsigned rank = cast(value.originalType).getRank(); + // Keep the base representation selected by analysis. Loop normalization + // may use the compact mixed-uniform path only for scalar bases, but tensor + // bases must remain valid loop values and follow the regular normalization + // below instead of being rejected solely because the target attribute is + // false. + if (!targetBase || !targetKinds || targetKinds.size() != rank || + targetBase.getValue() != hasScalarBase(value)) + return failure(); + + SmallVector kinds; + for (int32_t rawKind : targetKinds.asArrayRef()) { + if (rawKind != static_cast(AxisKind::Opaque) && + rawKind != static_cast(AxisKind::Structured)) + return failure(); + kinds.push_back(static_cast(rawKind)); + } + + bool preserveMixedUniform = + hasScalarBase(value) && carriedComponents.size() == 1 && + carriedComponents.front() == getUniformOffsetComponent(rank); + if (failed(normalizeOffsetComponents(value, kinds, builder, loc, + preserveMixedUniform))) + return failure(); + value.attributes.assign(targetAttributes.begin(), targetAttributes.end()); + return success(); + } + + LogicalResult annotatePointerDescriptorRebuild( + Operation *rebuildOp, const DecomposedValue &value) const override { + if (!hasValidLayout(value) || !isa(rebuildOp)) + return failure(); + FailureOr> kinds = getAxisKinds(value); + if (failed(kinds)) + return failure(); + rebuildOp->setAttr(kPointerDescriptorStructuredAxesAttr, + getAxisKindsAttr(rebuildOp->getContext(), *kinds)); + if (kinds->size() == 1 && kinds->front() == AxisKind::Structured && + hasScalarBase(value) && + isConstantZero(value.components[kResidualComponent])) { + rebuildOp->setAttr( + kPointerDescriptorOffsetFormAttr, + StringAttr::get(rebuildOp->getContext(), kStrided1DOffsetForm)); + } else { + rebuildOp->removeAttr(kPointerDescriptorOffsetFormAttr); + } + return success(); + } + + FailureOr decompose(Value value, + const ControlFlowRewriteContext &context, + OpBuilder &builder, + Location loc) const override { + if (const DecomposedValue *known = context.lookup(value)) { + if (!matches(known->originalType)) + return failure(); + return *known; + } + + value = context.remap(value); + if (auto broadcast = value.getDefiningOp()) { + FailureOr result = + decompose(broadcast.getSrc(), context, builder, loc); + if (succeeded(result) && hasValidLayout(*result) && + hasScalarBase(*result) && + isCompatiblePointerBroadcast(broadcast.getSrc().getType(), + value.getType())) { + FailureOr> kinds = getAxisKinds(*result); + auto sourceType = cast(broadcast.getSrc().getType()); + auto resultType = cast(value.getType()); + unsigned rank = resultType.getRank(); + if (succeeded(kinds)) { + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(broadcast); + for (unsigned axis = 0; axis < rank; ++axis) { + if (sourceType.getShape()[axis] == 1 && + resultType.getShape()[axis] != 1) { + Type strideType = + result->components[getStrideComponent(axis)].getType(); + result->components[getStrideComponent(axis)] = + createScalarConstant(builder, broadcast.getLoc(), strideType, + 0); + if (!result->components[getStrideComponent(axis)]) + return failure(); + } + } + Type scalarType = + result->components[getUniformOffsetComponent(rank)].getType(); + auto resultOffsetsType = RankedTensorType::get( + resultType.getShape(), scalarType, resultType.getEncoding()); + Value &opaque = + result->components[getOpaqueContributionComponent(rank)]; + if (isConstantZero(opaque)) { + opaque = createZeroOffsets(builder, broadcast.getLoc(), + resultOffsetsType); + if (!opaque) + return failure(); + } else if (opaque.getType() != resultOffsetsType) { + opaque = triton::BroadcastOp::create(builder, broadcast.getLoc(), + resultOffsetsType, opaque); + } + result->originalType = value.getType(); + result->attributes[kAxisKindsAttribute] = + getAxisKindsAttr(value.getContext(), *kinds); + return *result; + } + } + } + + if (auto addPtr = value.getDefiningOp()) { + FailureOr result = + decompose(addPtr.getPtr(), context, builder, loc); + if (failed(result) || !hasValidLayout(*result)) + return failure(); + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(addPtr); + Value deltaValue = context.remap(addPtr.getOffset()); + FailureOr delta = + materializeTensorOffsetFields(deltaValue, builder, addPtr.getLoc()); + FailureOr> currentKinds = getAxisKinds(*result); + if (failed(delta) || failed(currentKinds) || + delta->strides.size() != currentKinds->size()) + return failure(); + unsigned rank = currentKinds->size(); + SmallVector resultKinds; + SmallVector resultStrides; + for (unsigned axis = 0; axis < rank; ++axis) { + AxisKind kind = hasScalarBase(*result) && + (*currentKinds)[axis] == AxisKind::Structured && + delta->axisKinds[axis] == AxisKind::Structured + ? AxisKind::Structured + : AxisKind::Opaque; + resultKinds.push_back(kind); + Value stride = + kind == AxisKind::Structured + ? createIntegerAdd(builder, addPtr.getLoc(), + result->components[getStrideComponent(axis)], + delta->strides[axis]) + : createScalarConstant(builder, addPtr.getLoc(), + delta->strides[axis].getType(), 0); + if (!stride) + return failure(); + resultStrides.push_back(stride); + } + + if (hasMixedAxisKinds(resultKinds) && + (isPureUniformUpdate(*delta) || + canPreserveMixedResidual( + addPtr.getOffset(), + result->components[getOpaqueContributionComponent(rank)] + .getType(), + hasScalarBase(*result), *currentKinds, resultKinds))) { + auto opaqueType = dyn_cast( + result->components[getOpaqueContributionComponent(rank)].getType()); + FailureOr joinedType = getWiderIntegerLikeType( + result->components[getOpaqueContributionComponent(rank)].getType(), + delta->opaqueContribution.getType()); + auto joinedOpaqueType = succeeded(joinedType) + ? dyn_cast(*joinedType) + : RankedTensorType(); + Type scalarType = + joinedOpaqueType ? joinedOpaqueType.getElementType() : Type(); + FailureOr currentUniform = castIntegerLike( + builder, addPtr.getLoc(), + result->components[getUniformOffsetComponent(rank)], scalarType); + FailureOr deltaUniform = castIntegerLike( + builder, addPtr.getLoc(), delta->uniformOffset, scalarType); + if (opaqueType && joinedOpaqueType && succeeded(currentUniform) && + succeeded(deltaUniform)) { + Value uniform = createIntegerAdd(builder, addPtr.getLoc(), + *currentUniform, *deltaUniform); + SmallVector normalizedStrides; + normalizedStrides.reserve(rank); + bool compatible = static_cast(uniform); + for (unsigned axis = 0; axis < rank && compatible; ++axis) { + FailureOr stride = castIntegerLike( + builder, addPtr.getLoc(), resultStrides[axis], scalarType); + if (failed(stride)) { + compatible = false; + break; + } + normalizedStrides.push_back(*stride); + } + Value normalizedOpaque; + if (compatible) { + FailureOr opaque = castIntegerLike( + builder, addPtr.getLoc(), + result->components[getOpaqueContributionComponent(rank)], + joinedOpaqueType); + if (failed(opaque)) + compatible = false; + else + normalizedOpaque = *opaque; + } + if (compatible) { + for (unsigned axis = 0; axis < rank; ++axis) + result->components[getStrideComponent(axis)] = + normalizedStrides[axis]; + result->components[getOpaqueContributionComponent(rank)] = + normalizedOpaque; + result->components[getUniformOffsetComponent(rank)] = uniform; + result->originalType = value.getType(); + result->attributes[kAxisKindsAttribute] = + getAxisKindsAttr(value.getContext(), resultKinds); + return *result; + } + } + } + + Value currentComplete = + materializeTensorCompleteOffsets(*result, builder, addPtr.getLoc()); + Value nextComplete = currentComplete + ? createIntegerAdd(builder, addPtr.getLoc(), + currentComplete, deltaValue) + : nullptr; + if (!nextComplete) + return failure(); + auto nextType = cast(nextComplete.getType()); + Type scalarType = nextType.getElementType(); + for (unsigned axis = 0; axis < rank; ++axis) { + FailureOr converted = castIntegerLike( + builder, addPtr.getLoc(), resultStrides[axis], scalarType); + if (failed(converted)) + return failure(); + result->components[getStrideComponent(axis)] = *converted; + } + if (hasAnyOpaqueAxis(resultKinds)) { + SmallVector strides; + for (unsigned axis = 0; axis < rank; ++axis) + strides.push_back(result->components[getStrideComponent(axis)]); + FailureOr structured = materializeStructuredContribution( + strides, resultKinds, nextType, builder, addPtr.getLoc()); + if (failed(structured)) + return failure(); + Value opaque = createIntegerSub(builder, addPtr.getLoc(), nextComplete, + *structured); + Value zero = + createScalarConstant(builder, addPtr.getLoc(), scalarType, 0); + if (!opaque || !zero) + return failure(); + result->components[getUniformOffsetComponent(rank)] = zero; + result->components[getOpaqueContributionComponent(rank)] = opaque; + } else { + Value uniform = createIntegerAdd( + builder, addPtr.getLoc(), + result->components[getUniformOffsetComponent(rank)], + delta->uniformOffset); + if (!uniform) + return failure(); + FailureOr converted = + castIntegerLike(builder, addPtr.getLoc(), uniform, scalarType); + if (failed(converted)) + return failure(); + result->components[getUniformOffsetComponent(rank)] = *converted; + result->components[getOpaqueContributionComponent(rank)] = + createZeroOffsets(builder, addPtr.getLoc(), nextType); + } + result->originalType = value.getType(); + result->attributes[kAxisKindsAttribute] = + getAxisKindsAttr(value.getContext(), resultKinds); + return *result; + } + + if (auto splat = value.getDefiningOp()) { + if (!isa(splat.getSrc().getType())) + return failure(); + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(splat); + FailureOr baseAddress = + materializeScalarBaseAddress(builder, splat.getLoc(), splat.getSrc()); + if (failed(baseAddress)) + return failure(); + RankedTensorType offsetsType = getDefaultOffsetsType(value.getType()); + unsigned rank = cast(value.getType()).getRank(); + Type scalarType = offsetsType.getElementType(); + SmallVector components{*baseAddress}; + for (unsigned axis = 0; axis < rank; ++axis) + components.push_back( + createScalarConstant(builder, splat.getLoc(), scalarType, 0)); + components.push_back( + createScalarConstant(builder, splat.getLoc(), scalarType, 0)); + components.push_back( + createZeroOffsets(builder, splat.getLoc(), offsetsType)); + if (llvm::any_of(components, [](Value component) { return !component; })) + return failure(); + return DecomposedValue{ + value.getType(), std::move(components), + getTensorPointerAttributes(builder.getContext(), true, + getAxisKinds(rank, AxisKind::Structured))}; + } + + if (!matches(value.getType())) + return failure(); + OpBuilder::InsertionGuard guard(builder); + if (Operation *definingOp = value.getDefiningOp()) + builder.setInsertionPointAfter(definingOp); + else if (auto blockArg = dyn_cast(value)) + builder.setInsertionPointToStart(blockArg.getOwner()); + RankedTensorType offsetsType = getDefaultOffsetsType(value.getType()); + unsigned rank = cast(value.getType()).getRank(); + Type scalarType = offsetsType.getElementType(); + SmallVector components{value}; + for (unsigned axis = 0; axis < rank; ++axis) + components.push_back(createScalarConstant(builder, loc, scalarType, 0)); + components.push_back(createScalarConstant(builder, loc, scalarType, 0)); + components.push_back(createZeroOffsets(builder, loc, offsetsType)); + if (llvm::any_of(components, [](Value component) { return !component; })) + return failure(); + return DecomposedValue{ + value.getType(), std::move(components), + getTensorPointerAttributes(builder.getContext(), false, + getAxisKinds(rank, AxisKind::Opaque))}; + } + + Value recompose(const DecomposedValue &value, OpBuilder &builder, + Location loc) const override { + if (!hasValidLayout(value)) + return nullptr; + auto pointerTensor = cast(value.originalType); + Value base = value.components[kBaseComponent]; + if (hasScalarBase(value)) { + auto scalarPointerType = + cast(pointerTensor.getElementType()); + Value nativeBase = recoverNativeScalarBase(base, scalarPointerType); + if (!nativeBase) + nativeBase = + builder.create(loc, scalarPointerType, base); + base = nativeBase; + base = builder.create(loc, value.originalType, base); + } + + Value completeOffsets = + materializeTensorCompleteOffsets(value, builder, loc); + if (!completeOffsets) + return nullptr; + return builder.create(loc, value.originalType, base, + completeOffsets); + } +}; + +} // namespace + +namespace mlir::triton::controlflow { + +LogicalResult runTensorPtrDecompose(ModuleOp module) { + TensorPtrDecomposePolicy policy; + return rewriteControlFlow(module, policy, /*allowUnsupportedFallback=*/true); +} + +} // namespace mlir::triton::controlflow diff --git a/third_party/ascend/lib/TritonControlFlowOpt/TritonControlFlowOptPass.cpp b/third_party/ascend/lib/TritonControlFlowOpt/TritonControlFlowOptPass.cpp new file mode 100644 index 0000000000..0a0d8082a9 --- /dev/null +++ b/third_party/ascend/lib/TritonControlFlowOpt/TritonControlFlowOptPass.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonControlFlowOpt/TritonControlFlowOptPass.h" + +#include "TritonControlFlowOpt/BlockPtrDecompose.h" +#include "TritonControlFlowOpt/CFGStructuring.h" +#include "TritonControlFlowOpt/TensorPtrDecompose.h" + +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Verifier.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +namespace mlir::triton { + +void TritonControlFlowOptPass::getDependentDialects( + DialectRegistry ®istry) const { + // CFG structuring creates SCF operations, while pointer decomposition + // materializes arith and Triton pointer operations. + registry + .insert(); +} + +void TritonControlFlowOptPass::runOnOperation() { + ModuleOp module = getOperation(); + + // Apply the control-flow preprocessing pipeline in dependency order: + // 1. normalize supported cf graphs to scf; + // 2. decompose block-pointer descriptors across supported boundaries; + // 3. replace common-base tensor pointers at those boundaries with offsets. + // TODO: Extend stage 3 to carry both the base and complete offsets, then add + // StructuredOffsetsDecompose to further split structured offsets into a + // base offset and per-dimension strides. + // Keep these calls explicit so each transformation has one owner, can be + // tested independently and can be implemented without changing pass + // registration. + if (failed(controlflow::structureCFG(module)) || + failed(controlflow::runBlockPtrDecompose(module)) || + failed(controlflow::runTensorPtrDecompose(module)) || + failed(verify(module))) + signalPassFailure(); +} + +std::unique_ptr> createTritonControlFlowOptPass() { + // Keep construction in this translation unit; registration is generated + // from Passes.td and exposes only this public factory. + return std::make_unique(); +} + +} // namespace mlir::triton diff --git a/third_party/ascend/lib/TritonMemoryAccess/CMakeLists.txt b/third_party/ascend/lib/TritonMemoryAccess/CMakeLists.txt new file mode 100644 index 0000000000..73d789bec2 --- /dev/null +++ b/third_party/ascend/lib/TritonMemoryAccess/CMakeLists.txt @@ -0,0 +1,25 @@ +add_triton_library(TritonMemoryAccessCore + OpFoldResultUtils.cpp + + LINK_LIBS PUBLIC + MLIRArithDialect + MLIRDialectUtils + MLIRIR + MLIRSupport +) + +add_triton_library(TritonMemoryAccessAnalysis + MaskAnalysis.cpp + + LINK_LIBS PUBLIC + MLIRArithDialect + MLIRDialectUtils + MLIRIR + MLIRMemRefDialect + MLIRSCFDialect + MLIRSupport + MLIRTensorDialect + MLIRTransforms + TritonIR + TritonMemoryAccessCore +) diff --git a/third_party/ascend/lib/TritonMemoryAccess/MaskAnalysis.cpp b/third_party/ascend/lib/TritonMemoryAccess/MaskAnalysis.cpp new file mode 100644 index 0000000000..8b635c99e3 --- /dev/null +++ b/third_party/ascend/lib/TritonMemoryAccess/MaskAnalysis.cpp @@ -0,0 +1,1063 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonMemoryAccess/LoadStoreMaskAnalysis.h" +#include "TritonMemoryAccess/MemoryAccessTags.h" +#include "TritonMemoryAccess/OpFoldResultUtils.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Operation.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Transforms/DialectConversion.h" + +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Debug.h" +#include +#include + +#define DEBUG_TYPE "mask-analysis" + +namespace mlir { + +namespace triton { + +namespace { + +template +std::optional runMaskAnalysisImpl(MemAccOpTy op, + OpBuilder &builder) { + auto mask = op.getMask(); + if (!mask) { + return std::nullopt; + } + + PatternRewriter::InsertionGuard insertGuard(builder); + builder.setInsertionPoint(op); + + MaskState mstate; + if (mstate.parse(mask, op.getLoc(), builder).failed()) { + return std::nullopt; + } + return mstate; +} + +bool isZeroMaskConstant(const OpFoldResult &value) { + // Keep the actual mlir::isZeroInteger implementation used by the source + // baseline: a zero IntegerAttr or a Value matched as a constant integer. + if (auto constant = getConstantIntValue(value)) + return *constant == 0; + return false; +} + +static Value unwrapRuntimeExtentUnsignedOperand(Value operand, + const Location &loc, + OpBuilder &builder) { + if (auto extend = operand.getDefiningOp()) + return extend.getIn(); + + auto splat = operand.getDefiningOp(); + if (!splat) + return Value(); + auto extend = splat.getSrc().getDefiningOp(); + if (!extend) + return Value(); + + auto resultType = dyn_cast(splat.getType()); + if (!resultType || !isa(extend.getIn().getType())) + return Value(); + auto narrowedType = + RankedTensorType::get(resultType.getShape(), extend.getIn().getType(), + resultType.getEncoding()); + return builder.create(loc, narrowedType, extend.getIn()); +} + +// Follow an i1 tensor to its init only when all corresponding loop edges +// forward the same value. A while may permute condition/result slots, so do +// not assume its before and after argument numbers are interchangeable. +Value getInvariantLoopMaskInit(BlockArgument argument) { + Operation *parent = argument.getOwner()->getParentOp(); + if (auto loop = dyn_cast(parent)) { + if (argument == loop.getInductionVar()) + return {}; + unsigned slot = argument.getArgNumber() - 1; + auto yield = cast(loop.getBody()->getTerminator()); + return yield.getOperand(slot) == argument ? loop.getInitArgs()[slot] + : Value(); + } + if (auto loop = dyn_cast(parent)) { + auto condition = loop.getConditionOp(); + auto yield = loop.getYieldOp(); + BlockArgument before, after; + if (argument.getOwner() == &loop.getBefore().front()) { + before = argument; + after = dyn_cast(yield.getOperand(before.getArgNumber())); + if (!after || after.getOwner() != &loop.getAfter().front() || + condition.getArgs()[after.getArgNumber()] != before) + return {}; + } else { + after = argument; + before = + dyn_cast(condition.getArgs()[after.getArgNumber()]); + if (!before || before.getOwner() != &loop.getBefore().front() || + yield.getOperand(before.getArgNumber()) != after) + return {}; + } + return loop.getInits()[before.getArgNumber()]; + } + return {}; +} + +} // namespace + +OpFoldResult MaskState::clampToNonNegativeIndex(const OpFoldResult value, + const Location &loc, + OpBuilder &builder, + bool clampDynamic) const { + if (auto cst = getConstantIntValue(value)) { + return builder.getIndexAttr(std::max(0, *cst)); + } + + // Only minStates needs the dynamic clamp (disjoint intersection -> negative + // extent); the cmp paths are already non-negative, and clamping them + // regressed the atomic max/min UT. + if (clampDynamic) { + return maxOpFoldResult(value, builder.getIndexAttr(0), loc, builder); + } + return value; +} + +LogicalResult MaskState::parse(Value operand, const Location &loc, + OpBuilder &builder) { + Type operandType = operand.getType(); + if (isa(operandType) || operandType.isIndex()) { + return parseIntScalar(operand, loc, builder); + } + + if (auto blockArgument = dyn_cast(operand)) { + auto parentOp = blockArgument.getOwner()->getParentOp(); + if (auto loopOp = dyn_cast(parentOp)) { + auto type = dyn_cast(operand.getType()); + if (type && type.getElementType().isInteger(1)) { + Value init = getInvariantLoopMaskInit(blockArgument); + return init ? parse(init, loc, builder) : failure(); + } + OpOperand *initArgOperand = loopOp.getTiedLoopInit(blockArgument); + if (initArgOperand) { + if (!isa(operand.getType())) + return failure(); + + // Parse the init value to get the base range structure + if (failed(parse(initArgOperand->get(), loc, builder))) + return failure(); + + // Only scf.for loops are handled + auto forOp = dyn_cast(parentOp); + if (!forOp) + return failure(); + + unsigned slot = blockArgument.getArgNumber() - 1; + if (slot >= forOp.getYieldedValues().size()) + return failure(); + Value yielded = forOp.getYieldedValues()[slot]; + if (yielded == operand) { + // iter_arg is unchanged across iterations: the init value is the + // current value, so the parsed state needs no adjustment. + return success(); + } + + // Detect yield == iter_arg + const_tensor (or const_tensor + iter_arg). + auto addOp = yielded.getDefiningOp(); + if (!addOp) + return failure(); + Value incrementValue; + if (addOp.getLhs() == operand) + incrementValue = addOp.getRhs(); + else if (addOp.getRhs() == operand) + incrementValue = addOp.getLhs(); + else + return failure(); + + // The increment must be a splat integer constant tensor or a scalar + // integer constant (i.e. iteration-independent). + auto constOp = incrementValue.getDefiningOp(); + if (!constOp) + return failure(); + int64_t increment = 0; + if (auto denseAttr = dyn_cast(constOp.getValue())) { + if (!denseAttr.isSplat() || + !isa(denseAttr.getElementType())) + return failure(); + increment = denseAttr.getSplatValue().getInt(); + } else if (auto intAttr = dyn_cast(constOp.getValue())) { + increment = intAttr.getInt(); + } else { + return failure(); + } + + // The iteration count n = (iv - lb) / step relates the induction + // variable to the per-iteration increment: current = init + n*delta. + // lb/step may be dynamic; cast to index and compute at runtime. + FailureOr ivIndex = castIntegerLike( + builder, loc, forOp.getInductionVar(), builder.getIndexType()); + if (failed(ivIndex)) + return failure(); + + Value iterCount = *ivIndex; + + // iterCount = iv - lb (handle dynamic or constant lb) + auto lb = getConstantIntValue(forOp.getLowerBound()); + if (!lb || *lb != 0) { + FailureOr lbIndex = castIntegerLike( + builder, loc, forOp.getLowerBound(), builder.getIndexType()); + if (failed(lbIndex)) + return failure(); + iterCount = builder.create(loc, iterCount, *lbIndex); + } + + // iterCount = (iv - lb) / step (handle dynamic or constant step) + auto stepCst = getConstantIntValue(forOp.getStep()); + if (stepCst && *stepCst == 0) + return failure(); + if (!stepCst || *stepCst != 1) { + FailureOr stepIndex = castIntegerLike( + builder, loc, forOp.getStep(), builder.getIndexType()); + if (failed(stepIndex)) + return failure(); + iterCount = + builder.create(loc, iterCount, *stepIndex); + } + + OpFoldResult offset = + mulOpFoldResult(iterCount, builder.getIndexAttr(increment), loc, + builder, builder.getIndexType()); + if (!offset) + return failure(); + + if (this->start && this->end) { + this->start = addOpFoldResult(this->start, offset, loc, builder, + builder.getIndexType()); + this->end = addOpFoldResult(this->end, offset, loc, builder, + builder.getIndexType()); + if (!this->start || !this->end) + return failure(); + } else if (this->scalar) { + this->scalar = addOpFoldResult(this->scalar, offset, loc, builder, + builder.getIndexType()); + if (!this->scalar) + return failure(); + } else { + return failure(); + } + return success(); + } + } + } + + auto definingOp = operand.getDefiningOp(); + if (!definingOp) + return failure(); + + LLVM_DEBUG({ + llvm::dbgs() << "[MaskState]==> parse op\n" + << *definingOp << "\n[MaskState]<==\n"; + }); + return TypeSwitch(definingOp) + .Case( + [&](auto op) { return this->parseConstant(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseAdd(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseSub(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseAnd(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseCmp(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseMakeRange(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseBroadcast(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseSplat(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseExpandDims(op, loc, builder); }) + .Case( + [&](auto op) { return this->parse(op.getIn(), loc, builder); }) + .Case( + [&](auto op) { return this->parseDiv(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseSel(op, loc, builder); }) + .Case( + [&](auto op) { return this->parseInsert(op, loc, builder); }) + .Default([&](Operation *op) { return failure(); }); +} + +// extractSlice +tensor::ExtractSliceOp MaskState::getExtractSlice(Value source, + const Location &loc, + OpBuilder &builder) const { + auto sourceRType = cast(source.getType()); + SmallVector strides(getRank(), builder.getIndexAttr(1)); + + auto dstRType = tensor::ExtractSliceOp::inferResultType(sourceRType, offsets, + dims, strides); + return builder.create(loc, dstRType, source, offsets, + dims, strides); +} + +tensor::ExtractSliceOp MaskState::getExtractSlice( + Value source, const Location &loc, OpBuilder &builder, + SmallVector offsets, SmallVector dims) const { + auto sourceRType = cast(source.getType()); + SmallVector strides(getRank(), builder.getIndexAttr(1)); + + auto dstRType = tensor::ExtractSliceOp::inferResultType(sourceRType, offsets, + dims, strides); + return builder.create(loc, dstRType, source, offsets, + dims, strides); +} + +// insertSlice +tensor::InsertSliceOp MaskState::getInsertSlice(Value source, Value dest, + const Location &loc, + OpBuilder &builder) const { + SmallVector strides(getRank(), builder.getIndexAttr(1)); + return builder.create(loc, source, dest, offsets, dims, + strides); +} + +tensor::InsertSliceOp +MaskState::getInsertSlice(Value source, Value dest, const Location &loc, + OpBuilder &builder, SmallVector offsets, + SmallVector dims) const { + SmallVector strides(getRank(), builder.getIndexAttr(1)); + return builder.create(loc, source, dest, offsets, dims, + strides); +} + +memref::SubViewOp MaskState::getSubview(Value source, const Location &loc, + OpBuilder &builder) const { + auto sourceType = cast(source.getType()); + int64_t rank = sourceType.getRank(); + SmallVector strides(rank, builder.getIndexAttr(1)); + + SmallVector fixedOffsets(offsets.begin(), offsets.end()); + SmallVector fixedDims(dims.begin(), dims.end()); + fixedOffsets.resize(rank, builder.getIndexAttr(0)); + fixedDims.resize(rank, builder.getIndexAttr(1)); + + auto dstType = memref::SubViewOp::inferResultType(sourceType, fixedOffsets, + fixedDims, strides); + return builder.create( + loc, cast(dstType), source, fixedOffsets, fixedDims, strides); +} + +// In llvm later version, for each dimension, assert that: +// 0 <= offset < dim_size +// 0 <= offset + (size - 1) *stride < dim_size +// To adatpt to this change, add this verification to avoid llvm assert error +// And currently, in the function calling scenario, the stride coefficient is +// always 1 +bool MaskState::isMemrefSubviewValid(Value source, OpBuilder &builder) const { + auto sourceType = cast(source.getType()); + int64_t rank = sourceType.getRank(); + + SmallVector fixedOffsets(offsets.begin(), offsets.end()); + SmallVector fixedDims(dims.begin(), dims.end()); + fixedOffsets.resize(rank, builder.getIndexAttr(0)); + fixedDims.resize(rank, builder.getIndexAttr(1)); + + for (int64_t i = 0; i < rank; ++i) { + int64_t sourceSize = sourceType.getDimSize(i); + if (ShapedType::isDynamic(sourceSize)) + continue; + std::optional offsetVal = + mlir::getConstantIntValue(fixedOffsets[i]); + std::optional dimVal = mlir::getConstantIntValue(fixedDims[i]); + if (offsetVal.has_value() && dimVal.has_value()) { + if (offsetVal.value() >= sourceSize || offsetVal.value() < 0) { + LLVM_DEBUG({ + llvm::dbgs() << "MemrefSubview offset check faied at dim " << i + << "sourceSize :" << sourceSize + << "offsetVal :" << offsetVal << "\n"; + }); + return false; + } + + int64_t computedEnd = offsetVal.value() + dimVal.value(); + if (computedEnd > sourceSize) { + LLVM_DEBUG({ + llvm::dbgs() << "MemrefSubview offset end check faied at dim " << i + << "dimVal :" << dimVal << "offsetVal :" << offsetVal + << "\n"; + }); + return false; + } + } + } + return true; +} + +static memref::SubViewOp createSubview(Value src, const Location &loc, + OpBuilder &builder, + ArrayRef offsets, + ArrayRef sizes, + ArrayRef strides) { + auto srcType = cast(src.getType()); + auto dstType = + memref::SubViewOp::inferResultType(srcType, offsets, sizes, strides); + return builder.create(loc, cast(dstType), src, + offsets, sizes, strides); +} + +LogicalResult MaskState::addStateScalar(const MaskState &state, + const OpFoldResult scalar, + const Location &loc, + OpBuilder &builder) { + start = addOpFoldResult(state.start, scalar, loc, builder, + builder.getIndexType()); + end = + addOpFoldResult(state.end, scalar, loc, builder, builder.getIndexType()); + dims = state.dims; + offsets = state.offsets; + + bool allDimsOne = llvm::all_of(state.dims, [](OpFoldResult dim) { + return getConstantIntValue(dim).value() == std::optional(1); + }); + if (allDimsOne) { + this->scalar = this->start; + } + + return success(); +} + +LogicalResult MaskState::addStates(const MaskState &lhsState, + const MaskState &rhsState, + const Location &loc, OpBuilder &builder) { + if (lhsState.scalar && rhsState.scalar) { + InFlightDiagnostic diag = + emitWarning(loc) + << "Unexpected case where both lhs and rhs are scalars"; + return failure(); + } + if (!lhsState.scalar && !rhsState.scalar) { + InFlightDiagnostic diag = + emitWarning(loc) + << "Unsupported scenario where neither lhs nor rhs is a scalar"; + return failure(); + } + + if (lhsState.scalar) { + return addStateScalar(rhsState, lhsState.scalar, loc, builder); + } else { + return addStateScalar(lhsState, rhsState.scalar, loc, builder); + } +} + +LogicalResult MaskState::subStateScalar(const MaskState &state, + const OpFoldResult scalar, + const Location &loc, + OpBuilder &builder) { + start = subOpFoldResult(state.start, scalar, loc, builder); + end = subOpFoldResult(state.end, scalar, loc, builder); + if (!start || !end) + return failure(); + dims = state.dims; + offsets = state.offsets; + + bool allDimsOne = llvm::all_of(state.dims, [](OpFoldResult dim) { + return getConstantIntValue(dim).value() == std::optional(1); + }); + if (allDimsOne) { + this->scalar = this->start; + } + + return success(); +} + +LogicalResult MaskState::subStates(const MaskState &lhsState, + const MaskState &rhsState, + const Location &loc, OpBuilder &builder) { + // Contiguous mask indices only keep a monotonic range when subtracting a + // scalar from a range: (arange - C). scalar - range is rejected. + if (!lhsState.scalar && rhsState.scalar) + return subStateScalar(lhsState, rhsState.scalar, loc, builder); + + InFlightDiagnostic diag = + emitWarning(loc) + << "Unsupported subi for continuous mask: only (range - scalar) is " + "supported"; + return failure(); +} + +LogicalResult MaskState::divStateScalar(const MaskState &state, + const OpFoldResult scalar, + const Location &loc, + OpBuilder &builder) { + start = divOpFoldResult(state.start, scalar, loc, builder); + end = divOpFoldResult(state.end, scalar, loc, builder); + dims = state.dims; + offsets = state.offsets; + return success(); +} + +LogicalResult MaskState::divStates(const MaskState &lhsState, + const MaskState &rhsState, + const Location &loc, OpBuilder &builder) { + if (!lhsState.scalar && rhsState.scalar) { + if (isZeroMaskConstant(rhsState.scalar)) { + InFlightDiagnostic diag = + emitError(loc) + << "Unsupported scenario where rhs is zero constant in divide!"; + return failure(); + } + + return divStateScalar(lhsState, rhsState.scalar, loc, builder); + } + + InFlightDiagnostic diag = emitWarning(loc) + << "Supported scenario where only rhs is a scalar"; + return failure(); +} + +LogicalResult MaskState::minStates(const MaskState &lhsState, + const MaskState &rhsState, + const Location &loc, OpBuilder &builder) { + if (lhsState.getRank() != rhsState.getRank()) { + InFlightDiagnostic diag = + emitError(loc) + << "Unexpected case where lhs and rhs have different ranks"; + return failure(); + } + + for (uint32_t i = 0; i < lhsState.getRank(); i++) { + auto lhsOffset = lhsState.offsets[i]; + auto rhsOffset = rhsState.offsets[i]; + auto newOffset = maxOpFoldResult(lhsOffset, rhsOffset, loc, builder); + auto lhsDim = lhsState.dims[i]; + auto rhsDim = rhsState.dims[i]; + auto lhsEnd = addOpFoldResult(lhsOffset, lhsDim, loc, builder, + builder.getIndexType()); + auto rhsEnd = addOpFoldResult(rhsOffset, rhsDim, loc, builder, + builder.getIndexType()); + auto newEnd = minOpFoldResult(lhsEnd, rhsEnd, loc, builder); + auto newDim = subOpFoldResult(newEnd, newOffset, loc, builder); + auto clampedNewDim = + clampToNonNegativeIndex(newDim, loc, builder, /*clampDynamic=*/true); + + offsets.push_back(newOffset); + dims.push_back(clampedNewDim); + } + return success(); +} + +// Helper func for MaskState::parse() +LogicalResult MaskState::parseConstant(arith::ConstantOp constOp, + const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + + if (isa(constOp.getValue())) { + auto attr = cast(constOp.getValue()); + auto elementType = attr.getElementType(); + assert(attr.isSplat() && isa(elementType) && + "All elements must share a single integer constant value"); + + if (elementType.isInteger(1) && + isa(constOp.getValue().getType())) { + auto shapedType = cast(constOp.getValue().getType()); + auto shape = shapedType.getShape(); + for (size_t i = 0; i < shape.size(); i++) { + this->dims.push_back(builder.getIndexAttr(shape[i])); + this->offsets.push_back(builder.getIndexAttr(0)); + } + } else { + this->scalar = builder.getIndexAttr( + attr.getSplatValue().getValue().getSExtValue()); + } + } else { + auto value = cast(constOp.getValue()).getInt(); + this->scalar = builder.getIndexAttr(value); + } + return success(); +} + +// parseIntScalar +LogicalResult MaskState::parseIntScalar(Value scalar, const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + + this->scalar = getOpFoldResultOfLayoutInfo(scalar, builder); + return success(); +} + +LogicalResult MaskState::parseAdd(arith::AddIOp addOp, const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + MaskState lhsState; + if (failed(lhsState.parse(addOp.getLhs(), loc, builder))) { + return failure(); + } + + MaskState rhsState; + if (failed(rhsState.parse(addOp.getRhs(), loc, builder))) { + return failure(); + } + return this->addStates(lhsState, rhsState, loc, builder); +} + +LogicalResult MaskState::parseSub(arith::SubIOp subOp, const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + MaskState lhsState; + if (failed(lhsState.parse(subOp.getLhs(), loc, builder))) { + return failure(); + } + + MaskState rhsState; + if (failed(rhsState.parse(subOp.getRhs(), loc, builder))) { + return failure(); + } + return this->subStates(lhsState, rhsState, loc, builder); +} + +LogicalResult MaskState::parseDiv(arith::DivSIOp divOp, const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + return failure(); // temporarily disable parseDiv + MaskState lhsState; + if (failed(lhsState.parse(divOp.getLhs(), loc, builder))) { + return failure(); + } + + MaskState rhsState; + if (failed(rhsState.parse(divOp.getRhs(), loc, builder))) { + return failure(); + } + return this->divStates(lhsState, rhsState, loc, builder); +} + +LogicalResult MaskState::parseAnd(arith::AndIOp andOp, const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + MaskState lhsState; + if (failed(lhsState.parse(andOp.getLhs(), loc, builder)) || + !lhsState.isMask()) { + return failure(); + } + + MaskState rhsState; + if (failed(rhsState.parse(andOp.getRhs(), loc, builder)) || + !rhsState.isMask()) { + return failure(); + } + + if (!lhsState.isMask() && !rhsState.isMask()) { + return failure(); + } + + // Only support both lhs and rhs satisfy `isMask` condition + return this->minStates(lhsState, rhsState, loc, builder); +} + +LogicalResult MaskState::parseSel(arith::SelectOp selOp, const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + auto trueValue = selOp.getTrueValue(); + auto falseValue = selOp.getFalseValue(); + + MaskState condState; + auto condition = selOp.getCondition(); + auto cmpOp = condition.getDefiningOp(); + if (!cmpOp || failed(condState.parse(condition, loc, builder))) { + return failure(); + } + + MaskState trueState; + if (failed(trueState.parse(trueValue, loc, builder)) || !trueState.scalar) { + return failure(); + } + + MaskState falseState; + if (failed(falseState.parse(falseValue, loc, builder)) || + !falseState.scalar) { + return failure(); + } + + auto trueScalar = + dyn_cast_if_present(dyn_cast(trueState.scalar)); + auto falseScalar = + dyn_cast_if_present(dyn_cast(falseState.scalar)); + + if (trueScalar && falseScalar) { + if (trueScalar.getInt() == 1 && falseScalar.getInt() == 0) { + start = condState.start; + end = condState.end; + dims = condState.dims; + offsets = condState.offsets; + return success(); + } + } + + return failure(); +} + +LogicalResult MaskState::parseCmp(arith::CmpIOp cmpOp, const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + auto predicate = cmpOp.getPredicate(); + bool isProvenRuntimeExtentUnsignedMask = + cmpOp->hasAttr( + mlir::triton::memory_access::IATRuntimeExtentUnsignedMaskTAG) || + cmpOp->hasAttr( + mlir::triton::memory_access::PTSMRuntimeExtentUnsignedMaskTAG); + bool isTaggedUnsignedUpperBound = isProvenRuntimeExtentUnsignedMask && + (predicate == arith::CmpIPredicate::ult || + predicate == arith::CmpIPredicate::ule); + if (predicate != arith::CmpIPredicate::slt && + predicate != arith::CmpIPredicate::sle && + predicate != arith::CmpIPredicate::sge && + predicate != arith::CmpIPredicate::eq && + predicate != arith::CmpIPredicate::ne && !isTaggedUnsignedUpperBound) { + LLVM_DEBUG({ llvm::dbgs() << "Unsupported cmpi predicate\n"; }); + return failure(); + } + + MaskState lhsState; + MaskState rhsState; + auto lhs = cmpOp.getLhs(); + auto rhs = cmpOp.getRhs(); + + if (isTaggedUnsignedUpperBound) { + lhs = unwrapRuntimeExtentUnsignedOperand(lhs, loc, builder); + rhs = unwrapRuntimeExtentUnsignedOperand(rhs, loc, builder); + if (!lhs || !rhs) + return failure(); + predicate = predicate == arith::CmpIPredicate::ult + ? arith::CmpIPredicate::slt + : arith::CmpIPredicate::sle; + } + + if (predicate == arith::CmpIPredicate::ne) { + auto selOp = lhs.getDefiningOp(); + auto constantOp = rhs.getDefiningOp(); + if (!selOp || !constantOp) { + return failure(); + } + } + + if (failed(lhsState.parse(lhs, loc, builder))) { + return failure(); + } + + if (failed(rhsState.parse(rhs, loc, builder))) { + return failure(); + } + + if (!(!lhsState.scalar && rhsState.scalar)) { + InFlightDiagnostic diag = emitWarning(loc) + << "[MaskState] Unsupported cmpi scenario"; + return failure(); + } + + int32_t cmpDim = -1; + for (int32_t i = 0; i < lhsState.getRank(); i++) { + auto constDimLength = getConstantIntValue(lhsState.dims[i]); + if (!constDimLength || constDimLength.value() != 1) { + if (cmpDim != -1) { + InFlightDiagnostic diag = emitWarning(loc) + << "Unsupported cmpi with more than one " + "dimension with size larger than 1"; + return failure(); + } + cmpDim = i; + } + } + + assert(cmpDim != -1 && + "Unexpected case where no dimension has size larger than 1"); + + this->offsets = lhsState.offsets; + this->dims = lhsState.dims; + switch (predicate) { + case arith::CmpIPredicate::slt: { + auto realBound = + maxOpFoldResult(lhsState.start, rhsState.scalar, loc, builder); + auto newEnd = minOpFoldResult(lhsState.end, realBound, loc, builder); + auto newDim = subOpFoldResult(newEnd, lhsState.start, loc, builder); + auto clampedNewDim = clampToNonNegativeIndex(newDim, loc, builder); + + this->dims[cmpDim] = clampedNewDim; + break; + } + case arith::CmpIPredicate::sle: { + // lhs <= rhs <=> lhs < rhs + 1 + auto rhsPlusOne = addOpFoldResult(rhsState.scalar, builder.getIndexAttr(1), + loc, builder, builder.getIndexType()); + auto realBound = maxOpFoldResult(lhsState.start, rhsPlusOne, loc, builder); + auto newEnd = minOpFoldResult(lhsState.end, realBound, loc, builder); + auto newDim = subOpFoldResult(newEnd, lhsState.start, loc, builder); + auto clampedNewDim = clampToNonNegativeIndex(newDim, loc, builder); + + this->dims[cmpDim] = clampedNewDim; + break; + } + case arith::CmpIPredicate::sge: { + auto realBound = + maxOpFoldResult(lhsState.start, rhsState.scalar, loc, builder); + auto newStart = minOpFoldResult(lhsState.end, realBound, loc, builder); + auto newOffset = subOpFoldResult(newStart, lhsState.start, loc, builder); + auto newDim = subOpFoldResult(lhsState.end, newStart, loc, builder); + auto clampedNewDim = clampToNonNegativeIndex(newDim, loc, builder); + + this->offsets[cmpDim] = newOffset; + this->dims[cmpDim] = clampedNewDim; + break; + } + case arith::CmpIPredicate::eq: { + auto newOffset = + subOpFoldResult(rhsState.scalar, lhsState.start, loc, builder); + auto newDim = builder.getIndexAttr(1); + + this->offsets[cmpDim] = newOffset; + this->dims[cmpDim] = newDim; + break; + } + case arith::CmpIPredicate::ne: { + // only support lhs != 0 + auto rhsScalar = dyn_cast(cast(rhsState.scalar)); + if (!rhsScalar || rhsScalar.getInt() != 0) { + return failure(); + } + + start = lhsState.start; + end = lhsState.end; + break; + } + default: + return failure(); + } + return success(); +} + +LogicalResult MaskState::parseMakeRange(triton::MakeRangeOp rangeOp, + const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + auto shape = cast(rangeOp.getType()).getShape(); + auto start = rangeOp.getStart(); + auto end = rangeOp.getEnd(); + auto stride = (end - start + shape[0] - 1) / shape[0]; + + if (stride != 1) { + InFlightDiagnostic diag = + emitWarning(loc) + << "stride must be 1 for make_range whose result is used " + "as load or store masks"; + return failure(); + } + + this->start = builder.getIndexAttr(start); + this->end = builder.getIndexAttr(end); + this->dims.push_back(builder.getIndexAttr(shape[0])); + this->offsets.push_back(builder.getIndexAttr(0)); + return success(); +} + +LogicalResult MaskState::parseBroadcast(triton::BroadcastOp broadcastOp, + const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + auto src = broadcastOp.getSrc(); + auto dst = broadcastOp.getResult(); + assert(isa(src.getType()) && + "input to tt.broadcast should be a tensor"); + + auto srcShape = cast(src.getType()).getShape(); + auto dstShape = cast(dst.getType()).getShape(); + assert(srcShape.size() == dstShape.size() && + "rank of source and destination should match"); + + if (failed(parse(src, loc, builder))) { + return failure(); + } + for (size_t i = 0; i < srcShape.size(); i++) { + if (srcShape[i] == dstShape[i]) + continue; + else if (srcShape[i] < dstShape[i]) + this->dims[i] = builder.getIndexAttr(dstShape[i]); + else + llvm_unreachable("unexpected dimensions used in broadcast"); + } + return success(); +} + +LogicalResult MaskState::parseSplat(triton::SplatOp splatOp, + const Location &loc, OpBuilder &builder) { + assert(this->isEmpty()); + + auto src = splatOp.getSrc(); + auto dst = splatOp.getResult(); + auto dstShape = cast(dst.getType()).getShape(); + + if (!isa(src.getType())) { + InFlightDiagnostic diag = + emitWarning(loc) + << "splat source must be an integer scalar for load/store masks"; + return failure(); + } + + if (failed(this->parse(src, loc, builder))) + return failure(); + + auto splatAsMask = [&](Operation *userOp) -> bool { + return TypeSwitch(userOp) + .Case([&](arith::AndIOp andOp) { return true; }) + .Case([&](arith::SelectOp selectOp) { + return selectOp.getCondition() == dst; + }) + .Case( + [&](triton::LoadOp loadOp) { return loadOp.getMask() == dst; }) + .Case( + [&](triton::StoreOp storeOp) { return storeOp.getMask() == dst; }) + .Case([&](triton::AtomicRMWOp atomicOp) { + return atomicOp.getMask() == dst; + }) + .Default([&](Operation *op) { return false; }); + }; + + if (src.getType().isInteger(1) && !splatOp->use_empty() && + llvm::all_of(splatOp->getUsers(), splatAsMask)) { + for (auto s : dstShape) { + auto currentDim = mulOpFoldResult(builder.getIndexAttr(s), this->scalar, + loc, builder, builder.getIndexType()); + this->dims.push_back(currentDim); + this->offsets.push_back(builder.getIndexAttr(0)); + } + + this->scalar = nullptr; + return success(); + } + + for (auto s : dstShape) { + this->dims.push_back(builder.getIndexAttr(s)); + this->offsets.push_back(builder.getIndexAttr(0)); + } + return success(); +} + +LogicalResult MaskState::parseExpandDims(triton::ExpandDimsOp expandDimsOp, + const Location &loc, + OpBuilder &builder) { + assert(this->isEmpty()); + + if (failed(this->parse(expandDimsOp.getSrc(), loc, builder))) { + return failure(); + } + + auto dstShape = + cast(expandDimsOp.getResult().getType()).getShape(); + auto axis = expandDimsOp.getAxis(); + assert(dstShape[axis] == 1 && + "Expect changed dimention to be 1 in expand_dims"); + this->dims.insert(this->dims.begin() + axis, builder.getIndexAttr(1)); + this->offsets.insert(this->offsets.begin() + axis, builder.getIndexAttr(0)); + + return success(); +} + +LogicalResult MaskState::parseInsert(tensor::InsertOp insertOp, + const Location &loc, OpBuilder &builder) { + if (auto tensorType = dyn_cast(insertOp.getType())) { + if (llvm::all_of(tensorType.getShape(), + [](int64_t dim) { return dim == 1; })) { + SmallVector indices( + tensorType.getRank(), builder.create(loc, 0)); + auto scalarlizeTensor = + builder.create(loc, insertOp, indices).getResult(); + this->offsets = SmallVector(tensorType.getRank(), + builder.getIndexAttr(0)); + this->dims = SmallVector(tensorType.getRank(), + builder.getIndexAttr(1)); + this->scalar = getOpFoldResultOfLayoutInfo(scalarlizeTensor, builder); + return success(); + } + } + return failure(); +} + +void MaskState::eraseInsertedOps(Operation *rawOp, PatternRewriter &rewriter) { + auto moduleOp = rawOp->getParentOfType(); + SmallVector worklist; + moduleOp->walk([&](Operation *op) { + if (isOpTriviallyDead(op) && op->use_empty()) { + worklist.push_back(op); + } + }); + while (!worklist.empty()) { + Operation *op = worklist.pop_back_val(); + if (!isOpTriviallyDead(op) || !op->use_empty()) { + continue; + } + if (!op->getBlock()) { + continue; + } + SmallVector operandDefOps; + for (Value value : op->getOperands()) { + if (auto defOp = value.getDefiningOp()) { + if (defOp->getBlock()) { + operandDefOps.push_back(defOp); + } + } + } + LLVM_DEBUG({ + llvm::dbgs() << "[MaskState]==> inserted op: \n" + << *op << "\n[MaskState]<== is removed\n"; + }); + + rewriter.eraseOp(op); + for (auto defOp : operandDefOps) { + worklist.push_back(defOp); + } + } +} + +std::optional runMaskAnalysis(Operation *op, OpBuilder &builder) { + if (auto loadOp = dyn_cast(op)) { + return runMaskAnalysisImpl(loadOp, builder); + } + if (auto storeOp = dyn_cast(op)) { + return runMaskAnalysisImpl(storeOp, builder); + } + if (auto atomicRMWOp = dyn_cast(op)) { + return runMaskAnalysisImpl(atomicRMWOp, builder); + } + return std::nullopt; +} + +} // namespace triton + +} // namespace mlir diff --git a/third_party/ascend/lib/TritonMemoryAccess/OpFoldResultUtils.cpp b/third_party/ascend/lib/TritonMemoryAccess/OpFoldResultUtils.cpp new file mode 100644 index 0000000000..3c9a1e9d9c --- /dev/null +++ b/third_party/ascend/lib/TritonMemoryAccess/OpFoldResultUtils.cpp @@ -0,0 +1,463 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonMemoryAccess/OpFoldResultUtils.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/Value.h" + +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/Support/ErrorHandling.h" + +#include +#include +#include + +namespace mlir { +namespace { + +Value createConstIndexValueOp(const Location &loc, OpBuilder &builder, + int64_t value) { + return builder.create(loc, builder.getIndexAttr(value)) + .getResult(); +} + +FailureOr materializeIndexOperand(const OpFoldResult &operand, + std::optional constant, + Location loc, OpBuilder &builder) { + if (constant) + return createConstIndexValueOp(loc, builder, *constant); + auto value = dyn_cast(operand); + if (!value) + return failure(); + return castIntegerLike(builder, loc, value, builder.getIndexType()); +} + +Type getOpFoldResultType(const OpFoldResult &operand) { + if (auto value = dyn_cast(operand)) + return value.getType(); + if (auto attribute = dyn_cast(operand)) { + if (auto typedAttribute = dyn_cast(attribute)) + return typedAttribute.getType(); + } + return Type(); +} + +bool isSupportedScalarIntegerType(Type type) { + return type && (type.isIndex() || type.isSignlessInteger()); +} + +FailureOr resolveIntegerResultType(const OpFoldResult &lhs, + const OpFoldResult &rhs, + Type requestedType) { + Type lhsType = getOpFoldResultType(lhs); + Type rhsType = getOpFoldResultType(rhs); + if (!isSupportedScalarIntegerType(lhsType) || + !isSupportedScalarIntegerType(rhsType) || + (requestedType && !isSupportedScalarIntegerType(requestedType))) + return failure(); + + if (requestedType) + return requestedType; + if (lhsType == rhsType) + return lhsType; + + // Index has no fixed IR-level width, so it does not participate in default + // widening against ordinary integers. + auto lhsInteger = dyn_cast(lhsType); + auto rhsInteger = dyn_cast(rhsType); + if (!lhsInteger || !rhsInteger || + lhsInteger.getWidth() == rhsInteger.getWidth()) + return failure(); + return lhsInteger.getWidth() > rhsInteger.getWidth() ? lhsType : rhsType; +} + +FailureOr castIntegerFoldResult(const OpFoldResult &operand, + Type targetType, Location loc, + OpBuilder &builder) { + if (std::optional constant = getConstantOfAttr(operand)) + return OpFoldResult(builder.getIntegerAttr(targetType, *constant)); + + auto value = dyn_cast(operand); + if (!value) + return failure(); + FailureOr converted = castIntegerLike(builder, loc, value, targetType); + if (failed(converted)) + return failure(); + return OpFoldResult(*converted); +} + +FailureOr materializeIntegerOperand(const OpFoldResult &operand, + std::optional constant, + Type targetType, Location loc, + OpBuilder &builder) { + if (constant) + return builder + .create( + loc, builder.getIntegerAttr(targetType, *constant)) + .getResult(); + auto value = dyn_cast(operand); + if (!value) + return failure(); + return castIntegerLike(builder, loc, value, targetType); +} + +} // namespace + +std::optional getConstantOfAttr(const OpFoldResult &arg) { + if (isa(arg)) + return getConstantIntValue(arg); + return std::nullopt; +} + +FailureOr castIntegerLike(OpBuilder &builder, Location loc, Value value, + Type targetType, + IntegerExtensionKind extension) { + if (!value || !targetType) + return failure(); + + Type sourceType = value.getType(); + if (sourceType == targetType) + return value; + + if ((sourceType.isIndex() && isa(targetType)) || + (isa(sourceType) && targetType.isIndex())) { + if (extension == IntegerExtensionKind::Unsigned) + return builder.create(loc, targetType, value) + .getResult(); + return builder.create(loc, targetType, value) + .getResult(); + } + + auto extendOrTruncate = [&](unsigned sourceWidth, + unsigned targetWidth) -> FailureOr { + if (sourceWidth < targetWidth) { + if (extension == IntegerExtensionKind::Unsigned) + return builder.create(loc, targetType, value) + .getResult(); + return builder.create(loc, targetType, value).getResult(); + } + if (sourceWidth > targetWidth) + return builder.create(loc, targetType, value) + .getResult(); + return failure(); + }; + + auto sourceInteger = dyn_cast(sourceType); + auto targetInteger = dyn_cast(targetType); + if (sourceInteger && targetInteger) + return extendOrTruncate(sourceInteger.getWidth(), targetInteger.getWidth()); + + auto sourceTensor = dyn_cast(sourceType); + auto targetTensor = dyn_cast(targetType); + if (!sourceTensor || !targetTensor || + sourceTensor.getShape() != targetTensor.getShape() || + sourceTensor.getEncoding() != targetTensor.getEncoding()) + return failure(); + + auto sourceElement = dyn_cast(sourceTensor.getElementType()); + auto targetElement = dyn_cast(targetTensor.getElementType()); + if (!sourceElement || !targetElement) + return failure(); + + return extendOrTruncate(sourceElement.getWidth(), targetElement.getWidth()); +} + +OpFoldResult addOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &b, + Type resultType) { + FailureOr resolvedType = resolveIntegerResultType(lhs, rhs, resultType); + if (failed(resolvedType)) + return OpFoldResult(); + + auto lhsInt = getConstantOfAttr(lhs); + auto rhsInt = getConstantOfAttr(rhs); + + if (lhsInt && rhsInt) + return b.getIntegerAttr(*resolvedType, *lhsInt + *rhsInt); + + if (!lhsInt && rhsInt && *rhsInt == 0) { + FailureOr result = + castIntegerFoldResult(lhs, *resolvedType, loc, b); + return succeeded(result) ? *result : OpFoldResult(); + } + if (!rhsInt && lhsInt && *lhsInt == 0) { + FailureOr result = + castIntegerFoldResult(rhs, *resolvedType, loc, b); + return succeeded(result) ? *result : OpFoldResult(); + } + + FailureOr lhsValue = + materializeIntegerOperand(lhs, lhsInt, *resolvedType, loc, b); + FailureOr rhsValue = + materializeIntegerOperand(rhs, rhsInt, *resolvedType, loc, b); + if (failed(lhsValue) || failed(rhsValue)) + return OpFoldResult(); + + return b.create(loc, *lhsValue, *rhsValue).getResult(); +} + +OpFoldResult subOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &b) { + auto lhsInt = getConstantOfAttr(lhs); + auto rhsInt = getConstantOfAttr(rhs); + + if (lhsInt && rhsInt) + return b.getIndexAttr(lhsInt.value() - rhsInt.value()); + + if (!lhsInt && rhsInt && rhsInt.value() == 0) + return lhs; + + FailureOr lhsValue = materializeIndexOperand(lhs, lhsInt, loc, b); + FailureOr rhsValue = materializeIndexOperand(rhs, rhsInt, loc, b); + if (failed(lhsValue) || failed(rhsValue)) + return OpFoldResult(); + + return b.create(loc, *lhsValue, *rhsValue).getResult(); +} + +OpFoldResult mulOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &b, + Type resultType) { + FailureOr resolvedType = resolveIntegerResultType(lhs, rhs, resultType); + if (failed(resolvedType)) + return OpFoldResult(); + + auto lhsInt = getConstantOfAttr(lhs); + auto rhsInt = getConstantOfAttr(rhs); + + if (lhsInt && rhsInt) + return b.getIntegerAttr(*resolvedType, *lhsInt * *rhsInt); + + if (lhsInt) { + if (*lhsInt == 0) + return b.getIntegerAttr(*resolvedType, 0); + if (*lhsInt == 1) { + FailureOr result = + castIntegerFoldResult(rhs, *resolvedType, loc, b); + return succeeded(result) ? *result : OpFoldResult(); + } + } + if (rhsInt) { + if (*rhsInt == 0) + return b.getIntegerAttr(*resolvedType, 0); + if (*rhsInt == 1) { + FailureOr result = + castIntegerFoldResult(lhs, *resolvedType, loc, b); + return succeeded(result) ? *result : OpFoldResult(); + } + } + + FailureOr lhsValue = + materializeIntegerOperand(lhs, lhsInt, *resolvedType, loc, b); + FailureOr rhsValue = + materializeIntegerOperand(rhs, rhsInt, *resolvedType, loc, b); + if (failed(lhsValue) || failed(rhsValue)) + return OpFoldResult(); + + return b.create(loc, *lhsValue, *rhsValue).getResult(); +} + +OpFoldResult divOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &b) { + auto lhsInt = getConstantOfAttr(lhs); + auto rhsInt = getConstantOfAttr(rhs); + + if (rhsInt && rhsInt.value() == 0) { + emitError(loc) << "cannot div 0!"; + return OpFoldResult(); + } + + if (lhsInt && rhsInt) + return b.getIndexAttr(lhsInt.value() / rhsInt.value()); + + if (lhsInt) { + if (lhsInt.value() == 0) + return lhs; + } + + if (rhsInt) { + if (rhsInt.value() == 1) + return lhs; + } + + FailureOr lhsValue = materializeIndexOperand(lhs, lhsInt, loc, b); + FailureOr rhsValue = materializeIndexOperand(rhs, rhsInt, loc, b); + if (failed(lhsValue) || failed(rhsValue)) + return OpFoldResult(); + + return b.create(loc, *lhsValue, *rhsValue).getResult(); +} + +OpFoldResult remOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &b) { + auto lhsInt = getConstantOfAttr(lhs); + auto rhsInt = getConstantOfAttr(rhs); + + if (rhsInt && rhsInt.value() == 0) { + emitError(loc) << "cannot remainder by 0!"; + return OpFoldResult(); + } + + if (lhsInt && rhsInt) + return b.getIndexAttr(lhsInt.value() % rhsInt.value()); + + if (lhsInt) { + if (lhsInt.value() == 0) + return lhs; + } + + FailureOr lhsValue = materializeIndexOperand(lhs, lhsInt, loc, b); + FailureOr rhsValue = materializeIndexOperand(rhs, rhsInt, loc, b); + if (failed(lhsValue) || failed(rhsValue)) + return OpFoldResult(); + + return b.create(loc, *lhsValue, *rhsValue).getResult(); +} + +OpFoldResult minOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &b) { + auto lhsInt = getConstantOfAttr(lhs); + auto rhsInt = getConstantOfAttr(rhs); + if (lhsInt && rhsInt) + return b.getIndexAttr(std::min(lhsInt.value(), rhsInt.value())); + + FailureOr lhsValue = materializeIndexOperand(lhs, lhsInt, loc, b); + FailureOr rhsValue = materializeIndexOperand(rhs, rhsInt, loc, b); + if (failed(lhsValue) || failed(rhsValue)) + return OpFoldResult(); + + return b.create(loc, *lhsValue, *rhsValue).getResult(); +} + +OpFoldResult maxOpFoldResult(const OpFoldResult &lhs, const OpFoldResult &rhs, + const Location &loc, OpBuilder &b) { + auto lhsInt = getConstantOfAttr(lhs); + auto rhsInt = getConstantOfAttr(rhs); + if (lhsInt && rhsInt) + return b.getIndexAttr(std::max(lhsInt.value(), rhsInt.value())); + + FailureOr lhsValue = materializeIndexOperand(lhs, lhsInt, loc, b); + FailureOr rhsValue = materializeIndexOperand(rhs, rhsInt, loc, b); + if (failed(lhsValue) || failed(rhsValue)) + return OpFoldResult(); + + return b.create(loc, *lhsValue, *rhsValue).getResult(); +} +std::optional getIntAttr(const OpFoldResult ofr) { + Attribute attr; + if (auto val = dyn_cast(ofr)) { + if (!val.getDefiningOp()) + return std::nullopt; + attr = cast(val.getDefiningOp().getValue()); + } else { + attr = dyn_cast(ofr); + } + if (attr && isa(attr)) + return dyn_cast(attr).getInt(); + return std::nullopt; +} + +Value materializeValue(OpBuilder &builder, Location loc, OpFoldResult ofr) { + if (auto val = ofr.dyn_cast()) { + return val; + } + + auto intVal = getIntAttr(ofr); + if (intVal.has_value()) { + return builder.create( + loc, builder.getI32IntegerAttr(intVal.value())); + } + assert(intVal.has_value()); + return Value(); + + // return builder.create( + // loc, dyn_cast(attr).getInt()); +} + +bool isZero(const OpFoldResult ofr) { + auto staticOfr = getIntAttr(ofr); + return staticOfr.has_value() && staticOfr.value() == 0; +} + +bool isOne(const OpFoldResult ofr) { + auto staticOfr = getIntAttr(ofr); + return staticOfr.has_value() && staticOfr.value() == 1; +} + +Value convertToIndexIfNeeded(Value input, const Location &loc, OpBuilder &b) { + auto inputType = input.getType(); + if (auto intType = dyn_cast(inputType)) { + if (intType.isInteger(32) || intType.isInteger(64)) { + return b.create(loc, b.getIndexType(), input); + } + } + return input; +} + +RankedTensorType getExtractSlicedType(ArrayRef shape, + const llvm::SmallBitVector &droppedDims, + Type elemType) { + SmallVector targetShape; + for (auto [idx, dimOfr] : llvm::enumerate(shape)) { + if (!droppedDims[idx]) { + if (auto dim = getConstantIntValue(dimOfr)) { + targetShape.push_back(dim.value()); + } else { + targetShape.push_back(ShapedType::kDynamic); + } + } + } + return RankedTensorType::get(targetShape, elemType); +} + +// Fold layout constant info to attr, otherwise convert to index type value. +OpFoldResult getOpFoldResultOfLayoutInfo(Value value, OpBuilder &builder) { + OpFoldResult constantFold = getAsOpFoldResult(value); + if (llvm::isa(constantFold)) { + assert(isa(cast(constantFold))); + return constantFold; + } + + Type sourceType = value.getType(); + if (!sourceType.isIndex() && !isa(sourceType)) + llvm_unreachable("Illegal data type when parse block data layout info"); + + IntegerExtensionKind extension = sourceType.isInteger(/*width=*/1) + ? IntegerExtensionKind::Unsigned + : IntegerExtensionKind::Signed; + FailureOr converted = castIntegerLike( + builder, value.getLoc(), value, builder.getIndexType(), extension); + if (failed(converted)) + llvm_unreachable("Failed to convert block data layout info to index"); + return *converted; +} + +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToAnnotation/CMakeLists.txt b/third_party/ascend/lib/TritonToAnnotation/CMakeLists.txt new file mode 100644 index 0000000000..b8f47ced6b --- /dev/null +++ b/third_party/ascend/lib/TritonToAnnotation/CMakeLists.txt @@ -0,0 +1,15 @@ +add_triton_library(TritonToAnnotation + TritonToAnnotation.cpp + + DEPENDS + TritonToAnnotationConversionPassIncGen + + LINK_LIBS + BiShengIRAnnotationDialect + BiShengIRDialectUtils + MLIRIR + MLIRPass + MLIRTransforms + MLIRSupport + TritonIR +) diff --git a/third_party/ascend/lib/TritonToAnnotation/TritonToAnnotation.cpp b/third_party/ascend/lib/TritonToAnnotation/TritonToAnnotation.cpp new file mode 100644 index 0000000000..b59f8b3598 --- /dev/null +++ b/third_party/ascend/lib/TritonToAnnotation/TritonToAnnotation.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToAnnotation/Passes.h" + +#include "Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +namespace mlir { +namespace triton { +#define GEN_PASS_DEF_TRITONTOANNOTATION +#include "ascend/include/TritonToAnnotation/Passes.h.inc" +} // namespace triton +} // namespace mlir + +using namespace mlir; + +namespace { +struct TritonToAnnotationPass + : public mlir::triton::impl::TritonToAnnotationBase< + TritonToAnnotationPass> { + void runOnOperation() override; +}; +} // namespace + +struct TritonAnnotationConversionPattern + : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(mlir::triton::ascend::AnnotationOp op, + PatternRewriter &rewriter) const final { + auto markOp = rewriter.create(op.getLoc(), op.getSrc()); + // Forward all annotations. + markOp->setAttrs(op->getAttrs()); + rewriter.eraseOp(op); + return success(); + } +}; + +void TritonToAnnotationPass::runOnOperation() { + auto module = getOperation(); + ConversionTarget target(getContext()); + target.addLegalDialect(); + + RewritePatternSet patterns(&getContext()); + patterns.add(patterns.getContext()); + if (failed(applyPartialConversion(module, target, std::move(patterns)))) { + signalPassFailure(); + } +} + +std::unique_ptr> +mlir::triton::createTritonToAnnotationPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToGraph/AliasAnalysis.cpp b/third_party/ascend/lib/TritonToGraph/AliasAnalysis.cpp new file mode 100644 index 0000000000..525ef0a505 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/AliasAnalysis.cpp @@ -0,0 +1,294 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/AliasAnalysis.h" +#include "TritonToGraph/ControlFlowGraph.h" +#include "TritonToGraph/MemorySSA.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "triton/Dialect/Triton/IR/OpInterfaces.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/raw_ostream.h" + +#define DEBUG_TYPE "alias-analysis" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +//===----------------------------------------------------------------------===// +// AliasAnalysis Implementation +//===----------------------------------------------------------------------===// + +bool AliasAnalysis::beginDataFlowGraphBorrow() { + assert(!dataFlowGraphBorrowed && + "an AliasAnalysis can only be borrowed by one DataFlowGraph"); + if (dataFlowGraphBorrowed) + return false; + dataFlowGraphBorrowed = true; + return true; +} + +void AliasAnalysis::endDataFlowGraphBorrow() { + assert(dataFlowGraphBorrowed && "unbalanced DataFlowGraph alias borrow"); + if (!dataFlowGraphBorrowed) + return; + dataFlowGraphBorrowed = false; +} + +void AliasAnalysis::analyzePointerAliases(ControlFlowGraph &cfg) { + LLVM_DEBUG(llvm::dbgs() << "=== Starting Pointer Alias Analysis ===\n"); + + assert(!dataFlowGraphBorrowed && + "cannot rebuild aliases while a DataFlowGraph borrows them"); + if (dataFlowGraphBorrowed) + return; + aliasMap.clear(); + baseTensorMap.clear(); + ownedTensorObjects.clear(); + + // 步骤1: 识别全局内存参数 + triton::FuncOp func = cfg.getFunction(); + + LLVM_DEBUG(llvm::dbgs() << "Analyzing function: " << func.getName() << "\n"); + + for (BlockArgument arg : func.getArguments()) { + Type argType = arg.getType(); + + if (isPointerType(argType) && isGlobalMemoryType(argType)) { + // 为入参创建tensor对象 + std::string paramName = "param_" + std::to_string(arg.getArgNumber()); + + SmallVector shape; + Type elementType; + + // 使用辅助函数提取shape和element type + extractShapeAndElementType(argType, shape, elementType); + + auto tensorOwner = std::make_unique( + paramName, shape, argType, elementType, + TensorObject::TensorKind::GLOBAL_MEMORY); + TensorObject *tensor = tensorOwner.get(); + ownedTensorObjects.push_back(std::move(tensorOwner)); + + // 记录alias关系 [param, param, tensor] + addAlias(arg, arg, tensor); + + LLVM_DEBUG(llvm::dbgs() + << " Found global memory parameter " << arg.getArgNumber() + << ": " << tensor->getName() << "\n"); + } + } + + LLVM_DEBUG(llvm::dbgs() << "Found " << aliasMap.size() + << " global memory parameters\n"); + + // 步骤2: 分析alias操作 + size_t aliasOpsFound = 0; + cfg.traverse([&](BasicBlock &bb) { + for (const auto &instPtr : bb.getInstructions()) { + const Instruction *inst = instPtr.get(); + Operation *op = inst->getOperation(); + + if (!op) + continue; + + if (auto addptrOp = dyn_cast(op)) { + analyzeAddPtrOp(addptrOp); + aliasOpsFound++; + } else if (auto makeTensorPtrOp = dyn_cast(op)) { + analyzeMakeTensorPtrOp(makeTensorPtrOp); + aliasOpsFound++; + } else if (auto loadOp = dyn_cast(op)) { + analyzeLoadOp(loadOp); + } else if (auto storeOp = dyn_cast(op)) { + analyzeStoreOp(storeOp); + } else if (auto broadcastOp = dyn_cast(op)) { + analyzeBroadcastOp(broadcastOp); + aliasOpsFound++; + } else if (auto splatOp = dyn_cast(op)) { + analyzeSplatOp(splatOp); + aliasOpsFound++; + } else if (auto expandDimsOp = dyn_cast(op)) { + analyzeExpandDimsOp(expandDimsOp); + aliasOpsFound++; + } + } + }); + + LLVM_DEBUG( + llvm::dbgs() << "=== Alias Analysis Complete ===\n" + << "Total tracked aliases: " << aliasMap.size() << "\n" + << "Alias operations found: " << aliasOpsFound << "\n"); +} + +Value AliasAnalysis::getBasePointer(Value ptr) const { + // 递归查找base pointer + auto it = aliasMap.find(ptr); + if (it == aliasMap.end()) { + // 没有找到,返回自身(可能是原始base pointer) + return ptr; + } + + Value base = it->second; + + // 如果base不是自己,继续递归查找 + if (base != ptr) { + return getBasePointer(base); + } + + return base; +} + +void AliasAnalysis::analyzeAddPtrOp(mlir::triton::AddPtrOp addptrOp) { + // %new = tt.addptr %ptr, %offset + Value ptr = addptrOp.getPtr(); + Value result = addptrOp.getResult(); + + // result是ptr的alias,指向同一个tensor + Value basePtr = getBasePointer(ptr); + TensorObject *tensor = getTensorObject(basePtr); + + if (tensor) { + addAlias(result, basePtr, tensor); + + LLVM_DEBUG(llvm::dbgs() << " AddPtr: " << result << " -> " << basePtr + << " [" << tensor->getName() << "]\n"); + } +} + +void AliasAnalysis::analyzeMakeTensorPtrOp(mlir::triton::MakeTensorPtrOp op) { + // %tensor_ptr = tt.make_tensor_ptr %base, ... + Value basePtr = op.getBase(); + Value result = op.getResult(); + + TensorObject *tensor = getTensorObject(basePtr); + + if (tensor) { + addAlias(result, basePtr, tensor); + + LLVM_DEBUG(llvm::dbgs() << " MakeTensorPtr: " << result << " -> " + << basePtr << " [" << tensor->getName() << "]\n"); + } +} + +void AliasAnalysis::analyzeLoadOp(mlir::triton::LoadOp loadOp) { + // tt.load操作通常不改变alias关系, + // 但可以验证load的ptr是否被正确跟踪 + + Value ptr = loadOp.getPtr(); + + // 验证ptr是否被alias analysis跟踪 + TensorObject *tensor = getTensorObject(ptr); + + if (tensor) { + LLVM_DEBUG(llvm::dbgs() + << " Load from tracked tensor: " << tensor->getName() << "\n"); + } else { + LLVM_DEBUG(llvm::dbgs() << " Load from untracked pointer\n"); + } +} + +void AliasAnalysis::analyzeStoreOp(mlir::triton::StoreOp storeOp) { + // tt.store操作不改变alias关系, + // 但可以验证store的目标是否被正确跟踪 + + Value ptr = storeOp.getPtr(); + + // 验证ptr是否被alias analysis跟踪 + TensorObject *tensor = getTensorObject(ptr); + + if (tensor) { + LLVM_DEBUG(llvm::dbgs() + << " Store to tracked tensor: " << tensor->getName() << "\n"); + } else { + LLVM_DEBUG(llvm::dbgs() << " Store to untracked pointer\n"); + } +} + +void AliasAnalysis::analyzeBroadcastOp(mlir::triton::BroadcastOp broadcastOp) { + // %out = tt.broadcast %src : tensor> -> tensor> + // 输入和输出都是指针tensor,输出tensor的每个元素与输入tensor对应行/列元素是别名关系 + + Value src = broadcastOp.getSrc(); + Value result = broadcastOp.getResult(); + + // 只处理元素类型为指针的情况 + Type srcElemType = getElementTypeOrSelf(src.getType()); + if (!isPointerType(srcElemType)) + return; + + // 查找输入tensor关联的TensorObject + // 注意:src本身是指针tensor,需要获取其base pointer对应的tensor + Value srcBasePtr = getBasePointer(src); + TensorObject *tensor = getTensorObject(srcBasePtr); + + if (tensor) { + // broadcast操作保持指针值不变(只是复制到更多位置), + // 因此result与src指向相同的底层tensor对象 + addAlias(result, srcBasePtr, tensor); + + LLVM_DEBUG(llvm::dbgs() << " Broadcast: " << result << " -> " << srcBasePtr + << " [" << tensor->getName() << "]\n"); + } +} + +void AliasAnalysis::analyzeSplatOp(mlir::triton::SplatOp splatOp) { + // %out = tt.splat %src : !tt.ptr -> tensor> + // 将标量指针广播到tensor的每个位置,所有输出元素都是输入指针的别名 + + Value src = splatOp.getSrc(); // 标量指针 + Value result = splatOp.getResult(); // 指针tensor + + // 确认输入是指针类型 + if (!isPointerType(src.getType())) + return; + + // 获取标量指针的base pointer和对应的TensorObject + Value basePtr = getBasePointer(src); + TensorObject *tensor = getTensorObject(basePtr); + + if (tensor) { + // splat操作将同一个指针值复制到tensor的每个元素, + // 因此result tensor中所有元素都与src是别名关系(指向同一内存对象) + addAlias(result, basePtr, tensor); + + LLVM_DEBUG(llvm::dbgs() << " Splat: " << result << " -> " << basePtr + << " [" << tensor->getName() << "]\n"); + } +} + +void AliasAnalysis::analyzeExpandDimsOp( + mlir::triton::ExpandDimsOp expandDimsOp) { + Value src = expandDimsOp.getSrc(); + Value result = expandDimsOp.getResult(); + if (!isPointerType(getElementTypeOrSelf(src.getType()))) + return; + + Value basePtr = getBasePointer(src); + TensorObject *tensor = getTensorObject(basePtr); + if (!tensor) + return; + + addAlias(result, basePtr, tensor); + LLVM_DEBUG(llvm::dbgs() << " ExpandDims: " << result << " -> " << basePtr + << " [" << tensor->getName() << "]\n"); +} diff --git a/third_party/ascend/lib/TritonToGraph/CMakeLists.txt b/third_party/ascend/lib/TritonToGraph/CMakeLists.txt new file mode 100644 index 0000000000..d2af77355d --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/CMakeLists.txt @@ -0,0 +1,65 @@ +add_triton_library(TritonToGraph + AliasAnalysis.cpp + ControlFlowGraph.cpp + ControlFlowGraphBuilder.cpp + DataflowGraph.cpp + EntryArgPointerAliasAnalysis.cpp + GraphOptimizationContext.cpp + ResourceCostModel.cpp + GraphOptimizationPass.cpp + LayoutMemoryCompatibilityPass.cpp + MemorySsaBuilder.cpp + MergeConcatLoadBuffer.cpp + PermutationAnalysis.cpp + ProgramAxisDependenceAnalysis.cpp + ProgramMappingScalarSpecialization.cpp + ProgramGridTransform.cpp + MergeConcatLoadBuffer.cpp + Passes.cpp + LegacyMemoryAccess/ChunkCoalescing.cpp + LegacyMemoryAccess/RowCoalescing.cpp + LegacyMemoryAccess/StridedAxisCoalescing.cpp + LegacyMemoryAccess/StridedLoadStoreRewrite.cpp + Rules/ConvertModuloToMaskRule.cpp + Rules/ReservedGraphOptimizationRule.cpp + Rules/DiagonalMaskRemovalRule.cpp + Rules/IndependentAxisTensorizeRule.cpp + Rules/PersistentTaskStripMiningRule.cpp + Rules/ResidentLoadForwardingRule.cpp + Rules/IntermediatePrecisionBoundaryElisionRule.cpp + Rules/StoreCoveragePlanningRule.cpp + Rules/ContiguousBlockAccessFormationRule.cpp + Rules/LoadStoreTransposeRule.cpp + Rules/RowCoalescingRule.cpp + Rules/DotRowCoalescing.cpp + Rules/TransposePointwiseReorderRule.cpp + Rules/StoreCoalescingRule.cpp + + DEPENDS + TritonToGraphPassIncGen + + LINK_LIBS PUBLIC + BiShengIRHACCUtils + MLIRArithDialect + MLIRBufferizationDialect + MLIRControlFlowDialect + MLIRDialectUtils + MLIRIR + MLIRFuncDialect + MLIRLinalgDialect + MLIRMemRefDialect + MLIRMathDialect + MLIRLinalgDialect + MLIRMemRefDialect + MLIRPass + MLIRSCFDialect + MLIRSupport + MLIRTensorDialect + MLIRTransforms + MLIRTritonNPUUtils + TritonAscendIR + TritonIR + TritonMemoryAccessAnalysis + TritonToStructured + TritonTransforms +) diff --git a/third_party/ascend/lib/TritonToGraph/ControlFlowGraph.cpp b/third_party/ascend/lib/TritonToGraph/ControlFlowGraph.cpp new file mode 100644 index 0000000000..f63de7a109 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/ControlFlowGraph.cpp @@ -0,0 +1,1140 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/ControlFlowGraph.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" + +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include + +using namespace mlir; +using namespace triton; +using namespace cfg; + +//===----------------------------------------------------------------------===// +// Instruction +//===----------------------------------------------------------------------===// + +std::string Instruction::getAsString() const { + if (!operation) + return ""; + std::string str; + llvm::raw_string_ostream os(str); + operation->print(os); + return str; +} + +void Instruction::print(raw_ostream &os, unsigned indent) const { + std::string instStr = getAsString(); + os << "Inst[" << id << "]: "; + + if (operation) { + BlockType parentType = parentBlock->getType(); + if (parentType == BlockType::IF_COND || parentType == BlockType::FOR_COND || + parentType == BlockType::WHILE_COND || + parentType == BlockType::COND_BR || parentType == BlockType::BR) { + size_t newlinePos = instStr.find('\n'); + if (newlinePos != std::string::npos) { + instStr = instStr.substr(0, newlinePos) + " ..."; + } + } + os.indent(indent) << instStr; + } else { + os << "\n"; + } +} + +void Instruction::dump() const { print(llvm::errs()); } + +//===----------------------------------------------------------------------===// +// BasicBlock +//===----------------------------------------------------------------------===// + +void BasicBlock::addInstruction(std::unique_ptr inst) { + instructions.push_back(std::move(inst)); +} + +Instruction *BasicBlock::getInstruction(size_t idx) const { + if (idx < instructions.size()) { + return instructions[idx].get(); + } + return nullptr; +} + +void BasicBlock::addSuccessor(BasicBlock *succ) { + if (!succ) + return; + // 避免重复添加 + for (auto *s : successors) { + if (s == succ) + return; + } + successors.push_back(succ); + succ->addPredecessor(this); +} + +void BasicBlock::addPredecessor(BasicBlock *pred) { + if (!pred) + return; + // 避免重复添加 + for (auto *p : predecessors) { + if (p == pred) + return; + } + predecessors.push_back(pred); +} + +std::string BasicBlock::getName() const { + std::string name = "BB"; + name += std::to_string(id); + return name; +} + +bool BasicBlock::endsWithReturnOp() const { + // 空块检查 + if (instructions.empty()) { + return false; + } + + // 获取最后一条指令 + const Instruction *lastInst = instructions.back().get(); + if (!lastInst) { + return false; + } + + // 获取对应的 Operation + Operation *op = lastInst->getOperation(); + if (!op) { + return false; + } + + // 检查是否为 triton::ReturnOp + return isa(op); +} + +StringRef BasicBlock::getTypeString() const { + switch (type) { + case BlockType::NORMAL: + return "NORMAL"; + case BlockType::ENTRY: + return "ENTRY"; + case BlockType::EXIT: + return "EXIT"; + case BlockType::IF_COND: + return "IF_COND"; + case BlockType::FOR_COND: + return "FOR_COND"; + case BlockType::WHILE_COND: + return "WHILE_COND"; + case BlockType::COND_BR: + return "COND_BR"; + case BlockType::BR: + return "BR"; + case BlockType::LOOP_BODY: + return "LOOP_BODY"; + case BlockType::LOOP_EXIT: + return "LOOP_EXIT"; + } + return "UNKNOWN"; +} + +void BasicBlock::print(raw_ostream &os) const { + os << "============================================================\n"; + os << "BasicBlock " << getName() << " [" << getTypeString() << "]\n"; + if (parentStructure) { + os << " Parent Structure: " << parentStructure->getName() << "\n"; + } + + // 打印前驱 + if (!predecessors.empty()) { + os << " Predecessors: ["; + for (size_t i = 0; i < predecessors.size(); ++i) { + if (i > 0) + os << ", "; + os << predecessors[i]->getName(); + } + os << "]\n"; + } + + // 打印后继 + if (!successors.empty()) { + os << " Successors: ["; + for (size_t i = 0; i < successors.size(); ++i) { + if (i > 0) + os << ", "; + os << successors[i]->getName(); + } + os << "]\n"; + } + + // 打印指令 - COND节点只打印第一行 + os << " Instructions (" << instructions.size() << "):\n"; + for (const auto &inst : instructions) { + std::string instStr = inst->getAsString(); + + // 对COND节点截断:只保留第一行 + if (type == BlockType::IF_COND || type == BlockType::FOR_COND || + type == BlockType::WHILE_COND) { + size_t newlinePos = instStr.find('\n'); + if (newlinePos != std::string::npos) { + instStr = instStr.substr(0, newlinePos) + " ..."; + } + } + + os.indent(4) << "Inst[" << inst->getId() << "]: " << instStr << "\n"; + } + os << "\n"; +} + +void BasicBlock::dump() const { print(llvm::errs()); } + +void BasicBlock::exportToJSON(raw_ostream &os, unsigned indent) const { + std::string ind(indent, ' '); + os << ind << "{\n"; + os << ind << " \"id\": " << id << ",\n"; + os << ind << " \"name\": \"" << getName() << "\",\n"; + os << ind << " \"type\": \"" << getTypeString() << "\",\n"; + os << ind << " \"parentStructure\": " + << (parentStructure ? std::to_string(parentStructure->getId()) : "null") + << ",\n"; + + // 前驱 + os << ind << " \"predecessors\": ["; + for (size_t i = 0; i < predecessors.size(); ++i) { + if (i > 0) + os << ", "; + os << predecessors[i]->getId(); + } + os << "],\n"; + + // 后继 + os << ind << " \"successors\": ["; + for (size_t i = 0; i < successors.size(); ++i) { + if (i > 0) + os << ", "; + os << successors[i]->getId(); + } + os << "],\n"; + + // 指令 + os << ind << " \"instructions\": [\n"; + for (size_t i = 0; i < instructions.size(); ++i) { + os << ind << " {\n"; + os << ind << " \"id\": " << instructions[i]->getId() << ",\n"; + // 转义字符串用于 JSON + std::string instStr = instructions[i]->getAsString(); + + // COND节点只取第一行 + if (type == BlockType::IF_COND || type == BlockType::FOR_COND || + type == BlockType::WHILE_COND) { + size_t newlinePos = instStr.find('\n'); + if (newlinePos != std::string::npos) { + instStr = instStr.substr(0, newlinePos) + " ..."; + } + } + + // 简单的 JSON 字符串转义 + std::string escaped; + for (char c : instStr) { + if (c == '"') + escaped += "\\\""; + else if (c == '\\') + escaped += "\\\\"; + else if (c == '\n') + escaped += "\\n"; + else if (c == '\r') + escaped += "\\r"; + else if (c == '\t') + escaped += "\\t"; + else if ((unsigned char)c < 0x20) { + char buf[8]; + snprintf(buf, sizeof(buf), "\\u%04x", c); + escaped += buf; + } else { + escaped += c; + } + } + os << ind << " \"operation\": \"" << escaped << "\"\n"; + os << ind << " }"; + if (i < instructions.size() - 1) + os << ","; + os << "\n"; + } + os << ind << " ]\n"; + os << ind << "}"; +} + +//===----------------------------------------------------------------------===// +// ControlFlowGraph +//===----------------------------------------------------------------------===// + +ControlFlowGraph::ControlFlowGraph(triton::FuncOp func) : function(func) {} + +ControlFlowGraph::~ControlFlowGraph() = default; + +BasicBlock *ControlFlowGraph::createBasicBlock(BlockType type, + BasicBlock *parentStructure) { + auto bb = std::make_unique(nextBlockId++, type, parentStructure); + BasicBlock *bbPtr = bb.get(); + basicBlocks.push_back(std::move(bb)); + + // 自动设置入口/出口块 + if (type == BlockType::ENTRY) + entryBlock = bbPtr; + if (type == BlockType::EXIT) + exitBlock = bbPtr; + + return bbPtr; +} + +void ControlFlowGraph::addEdge(BasicBlock *from, BasicBlock *to) { + if (!from || !to) + return; + from->addSuccessor(to); +} + +// 判断是否为回边(从循环体回到循环头) +bool ControlFlowGraph::isBackEdge(BasicBlock *from, BasicBlock *to) const { + // 回边定义:指向 FOR_COND/WHILE_COND 且 from 是该循环的后代 + if (to->getType() != BlockType::FOR_COND && + to->getType() != BlockType::WHILE_COND) + return false; + + // 检查 from 是否属于以 to 为头的循环 + // 方法:检查 from 的 parentStructure 链是否包含 to + BasicBlock *current = from; + while (current) { + if (current == to) + return true; + current = current->getParentStructure(); + } + return false; +} + +void ControlFlowGraph::searchNormalBlock(BasicBlock *block, + OperationVisitor callback) const { + for (const auto &inst : block->getInstructions()) { + if (Operation *op = inst->getOperation()) { + callback(op); + } + } +} + +void ControlFlowGraph::searchCondBlock(BasicBlock *block, + OperationVisitor callback) const { + if (!block) + return; + + // 获取该 Cond block 对应的 exit block(停止条件) + BasicBlock *exitBlock = block->getExitBlock(); + + // 遍历 Cond block 的每一个 successor + for (BasicBlock *succ : block->getSuccessors()) { + if (!succ) + continue; + + // 从每个 successor 开始,沿着 block 链条向下遍历 + // 使用队列进行 BFS 遍历该分支 + std::deque workList; + workList.push_back(succ); + + while (!workList.empty()) { + BasicBlock *current = workList.front(); + workList.pop_front(); + + // 如果碰到 exit block,停止该分支的遍历 + if (current == exitBlock) + break; + + // 对当前 block 调用 searchBlock + searchBlock(current, callback); + + if (current->getType() == BlockType::NORMAL) { + // 将后继加入队列(排除回边) + for (BasicBlock *next : current->getSuccessors()) { + // 跳过回边避免无限循环 + if (isBackEdge(current, next)) + continue; + workList.push_back(next); + } + } else if (block->getType() == BlockType::IF_COND || + block->getType() == BlockType::FOR_COND || + block->getType() == BlockType::WHILE_COND) { + BasicBlock *next = block->getExitBlock(); + if (isBackEdge(current, next)) + continue; + workList.push_back(next); + } + } + } +} + +void ControlFlowGraph::searchBlock(BasicBlock *block, + OperationVisitor callback) const { + if (block->getType() == BlockType::NORMAL) { + searchNormalBlock(block, callback); + } else if (block->getType() == BlockType::IF_COND || + block->getType() == BlockType::FOR_COND || + block->getType() == BlockType::WHILE_COND) { + searchCondBlock(block, callback); + } +} + +void ControlFlowGraph::traverse(BlockVisitor visitor) { + // 从入口块开始进行拓扑排序 + std::vector topoOrder; + DenseSet visited; + + // 使用DFS进行拓扑排序 + std::function dfs = [&](BasicBlock *bb) { + if (!bb || visited.contains(bb)) + return; + + visited.insert(bb); + + // 递归访问所有后继块 + for (BasicBlock *succ : bb->getSuccessors()) { + dfs(succ); + } + + // 在后序位置添加,最后需要反转 + topoOrder.push_back(bb); + }; + + // 从入口块开始DFS + dfs(entryBlock); + + // 反转得到拓扑序 + std::reverse(topoOrder.begin(), topoOrder.end()); + + // 按照拓扑序遍历 + for (BasicBlock *bb : topoOrder) { + visitor(*bb); + } +} + +void ControlFlowGraph::print(raw_ostream &os) const { + auto funcName = const_cast(function).getName(); + os << "=================================================================\n"; + os << "Control Flow Graph for function '" + << (funcName.empty() ? "unnamed" : funcName) << "'\n"; + os << "Number of blocks: " << basicBlocks.size() << "\n"; + os << "=================================================================\n\n"; + + for (const auto &bb : basicBlocks) { + bb->print(os); + } +} + +void ControlFlowGraph::dump() const { print(llvm::errs()); } + +void ControlFlowGraph::exportDOT(raw_ostream &os) const { + auto funcName = const_cast(function).getName(); + std::string funcNameStr = funcName.empty() ? "unnamed" : funcName.str(); + + // 清理函数名用于 DOT 标识符 + std::string cleanFuncName; + for (char c : funcNameStr) { + if (isalnum(c) || c == '_') + cleanFuncName += c; + else + cleanFuncName += '_'; + } + + os << "digraph CFG_" << cleanFuncName << " {\n"; + os << " label=\"CFG for " << funcNameStr << "\";\n"; + os << " labelloc=t;\n"; + os << " rankdir=TB;\n"; + os << " splines=true;\n"; // 使用曲线边 + os << " overlap=false;\n"; // 防止节点重叠 + os << " nodesep=0.6;\n"; // 节点水平间距 + os << " ranksep=1.2;\n"; // 层间距 + os << " fontsize=12;\n\n"; + + // 设置节点样式 + for (const auto &bb : basicBlocks) { + os << " \"" << bb->getName() << "\" ["; + os << "label=\"" << bb->getName() << "\\n(" << bb->getTypeString() << ")"; + + // 指令摘要:COND节点只取第一行,其他节点最多3条 + size_t numInsts = bb->getNumInstructions(); + if (numInsts > 0) { + os << "\\n"; + + // COND节点只显示第一条指令的第一行 + if (bb->getType() == BlockType::IF_COND || + bb->getType() == BlockType::FOR_COND || + bb->getType() == BlockType::WHILE_COND) { + std::string instStr = bb->getInstruction(0)->getAsString(); + size_t newlinePos = instStr.find('\n'); + if (newlinePos != std::string::npos) { + instStr = instStr.substr(0, newlinePos); + } + if (instStr.length() > 40) + instStr = instStr.substr(0, 37) + "..."; + + // 转义 + std::string escaped; + for (char c : instStr) { + if (c == '"') + escaped += "\\\""; + else if (c == '\\') + escaped += "\\\\"; + else if (c == '\n') + escaped += "\\n"; + else + escaped += c; + } + os << escaped; + if (numInsts > 1) + os << "\\n... (" << (numInsts - 1) << " more)"; + } else { + // 其他节点显示最多3条 + for (size_t i = 0; i < std::min(numInsts, (size_t)3); ++i) { + std::string instStr = bb->getInstruction(i)->getAsString(); + // 只取第一行 + size_t newlinePos = instStr.find('\n'); + if (newlinePos != std::string::npos) { + instStr = instStr.substr(0, newlinePos) + "..."; + } + if (instStr.length() > 40) + instStr = instStr.substr(0, 37) + "..."; + + std::string escaped; + for (char c : instStr) { + if (c == '"') + escaped += "\\\""; + else if (c == '\\') + escaped += "\\\\"; + else if (c == '\n') + escaped += "\\n"; + else + escaped += c; + } + os << escaped << "\\n"; + } + if (numInsts > 3) + os << "... (" << (numInsts - 3) << " more)\\n"; + } + } + + os << "\", "; + + // 形状和颜色 + switch (bb->getType()) { + case BlockType::ENTRY: + os << "style=filled, fillcolor=lightgreen, shape=ellipse"; + break; + case BlockType::EXIT: + os << "style=filled, fillcolor=lightcoral, shape=ellipse"; + break; + case BlockType::IF_COND: + os << "style=filled, fillcolor=lightyellow, shape=diamond"; + break; + case BlockType::FOR_COND: + case BlockType::WHILE_COND: + os << "style=filled, fillcolor=lightblue, shape=diamond"; + break; + case BlockType::LOOP_BODY: + os << "style=filled, fillcolor=lightcyan, shape=box"; + break; + case BlockType::LOOP_EXIT: + os << "style=filled, fillcolor=lightpink, shape=box"; + break; + default: + os << "shape=box"; + break; + } + os << ", fontsize=10];\n"; + } + + os << "\n"; + + // 输出边:回边红色,其他绿色 + for (const auto &bb : basicBlocks) { + for (auto *succ : bb->getSuccessors()) { + bool isBack = isBackEdge(bb.get(), succ); + os << " \"" << bb->getName() << "\" -> \"" << succ->getName() << "\""; + os << " [color=" << (isBack ? "red" : "green"); + os << ", penwidth=" << (isBack ? "2.5" : "1.5") << ""; + if (isBack) { + os << ", style=dashed"; // 回边用虚线 + } + os << "];\n"; + } + } + + os << "}\n"; +} + +llvm::Error ControlFlowGraph::exportToFile(StringRef filename) const { + std::error_code ec; + llvm::raw_fd_ostream os(filename, ec, llvm::sys::fs::OF_Text); + + if (ec) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Failed to open file: " + filename); + } + + // 判断文件扩展名 + if (filename.ends_with(".dot")) { + exportDOT(os); + } else if (filename.ends_with(".json")) { + exportToJSON(os); + } else { + print(os); + } + + os.close(); + return llvm::Error::success(); +} + +void ControlFlowGraph::exportToJSON(raw_ostream &os) const { + auto funcName = const_cast(function).getName(); + std::string funcNameStr = funcName.empty() ? "unnamed" : funcName.str(); + + os << "{\n"; + os << " \"functionName\": \"" << funcNameStr << "\",\n"; + os << " \"numBlocks\": " << basicBlocks.size() << ",\n"; + os << " \"blocks\": [\n"; + + for (size_t i = 0; i < basicBlocks.size(); ++i) { + basicBlocks[i]->exportToJSON(os, 4); + if (i < basicBlocks.size() - 1) + os << ","; + os << "\n"; + } + + // 添加边信息(带类型标记) + os << " ],\n"; + os << " \"edges\": [\n"; + + bool first = true; + for (const auto &bb : basicBlocks) { + for (auto *succ : bb->getSuccessors()) { + if (!first) + os << ",\n"; + first = false; + + bool isBack = isBackEdge(bb.get(), succ); + os << " {\"from\": " << bb->getId() << ", \"to\": " << succ->getId() + << ", \"fromName\": \"" << bb->getName() << "\"" + << ", \"toName\": \"" << succ->getName() << "\"" + << ", \"type\": \"" << (isBack ? "back" : "normal") << "\"}"; + } + } + os << "\n ]\n"; + os << "}\n"; +} + +llvm::Error ControlFlowGraph::exportToHTML(StringRef filename) const { + std::error_code ec; + llvm::raw_fd_ostream os(filename, ec, llvm::sys::fs::OF_Text); + + if (ec) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Failed to open file: " + filename); + } + + auto funcName = const_cast(function).getName(); + std::string funcNameStr = funcName.empty() ? "unnamed" : funcName.str(); + + os << R"html( + + + + CFG for )html" + << funcNameStr << R"html( + + + + + + +
+ +
+
+
正常控制流 (向下)
+
循环回边 (向上)
+
+ + + + +)html"; + + os.close(); + return llvm::Error::success(); +} diff --git a/third_party/ascend/lib/TritonToGraph/ControlFlowGraphBuilder.cpp b/third_party/ascend/lib/TritonToGraph/ControlFlowGraphBuilder.cpp new file mode 100644 index 0000000000..b3f535e559 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/ControlFlowGraphBuilder.cpp @@ -0,0 +1,906 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/ControlFlowGraphBuilder.h" +#include "TritonToGraph/AliasAnalysis.h" +#include "TritonToGraph/DataflowGraph.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/Support/Debug.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" + +#define DEBUG_TYPE "build-cfg" + +using namespace mlir; +using namespace mlir::triton; +using namespace cfg; + +//===----------------------------------------------------------------------===// +// BuildCFGPass Implementation +//===----------------------------------------------------------------------===// + +void BuildCFGPass::runOnOperation() { + auto module = getOperation(); + llvm::errs() << "Building CFG for module\n"; + + // 获取输出目录(从命令行选项,默认为当前目录) + // TableGen 生成的选项成员变量名为 outputDir + std::string outputDir = this->outputDir; + + // 创建输出目录 + llvm::SmallString<128> outputPath(outputDir); + llvm::sys::fs::create_directories(outputPath); + + llvm::errs() << "CFG output directory: " << outputPath << "\n"; + + // 遍历模块中的所有 Triton 函数 (tt.func) + for (triton::FuncOp func : module.getOps()) { + llvm::errs() << "Processing function: " << func.getName() << "\n"; + + auto cfg = buildForFunction(func); + if (!cfg) { + func.emitError() << "Failed to build CFG for function"; + signalPassFailure(); + return; + } + + // 打印 CFG 到标准输出 + // cfg->print(llvm::outs()); + + // 导出到文件 + std::string baseName = func.getName().str(); + + // 导出文本格式 + llvm::SmallString<128> textPath(outputPath); + llvm::sys::path::append(textPath, baseName + "_cfg.txt"); + if (auto err = cfg->exportToFile(textPath)) { + llvm::errs() << "Failed to export CFG to " << textPath << "\n"; + } else { + llvm::errs() << "Exported CFG to " << textPath << "\n"; + } + + // 导出 DOT 格式 + llvm::SmallString<128> dotPath(outputPath); + llvm::sys::path::append(dotPath, baseName + "_cfg.dot"); + if (auto err = cfg->exportToFile(dotPath)) { + llvm::errs() << "Failed to export CFG to " << dotPath << "\n"; + } else { + llvm::errs() << "Exported CFG to " << dotPath << "\n"; + } + + // 导出 JSON 格式 + llvm::SmallString<128> jsonPath(outputPath); + llvm::sys::path::append(jsonPath, baseName + "_cfg.json"); + if (auto err = cfg->exportToFile(jsonPath)) { + llvm::errs() << "Failed to export CFG to " << jsonPath << "\n"; + } else { + llvm::errs() << "Exported CFG to " << jsonPath << "\n"; + } + + // 导出 HTML 格式(网页可视化) + llvm::SmallString<128> htmlPath(outputPath); + llvm::sys::path::append(htmlPath, baseName + "_cfg.html"); + if (auto err = cfg->exportToHTML(htmlPath)) { + llvm::errs() << "Failed to export CFG to " << htmlPath << "\n"; + } else { + llvm::errs() << "Exported CFG to " << htmlPath << "\n"; + } + + // 构建 DataFlowGraph(包含 Memory SSA 分析) + llvm::errs() << " Building DataFlowGraph with Memory SSA...\n"; + AliasAnalysis aliasAnalysis; + aliasAnalysis.analyzePointerAliases(*cfg); + DataFlowGraph dataFlowGraph(*cfg, aliasAnalysis); + dataFlowGraph.build(); + + // 导出 DataFlowGraph + std::error_code ec; + llvm::SmallString<128> dataflowPath(outputPath); + llvm::sys::path::append(dataflowPath, baseName + "_dataflow.json"); + llvm::raw_fd_ostream dfOs(dataflowPath, ec); + if (!ec) { + dataFlowGraph.exportToJSON(dfOs); + llvm::errs() << " Exported DataFlowGraph to " << dataflowPath << "\n"; + } + } +} + +std::unique_ptr +BuildCFGPass::buildForFunction(triton::FuncOp func) { + ControlFlowGraphBuilder cfgBuilder; + return cfgBuilder.build(func); +} + +ControlFlowGraphBuilder::RegionBlocks ControlFlowGraphBuilder::buildForRegion( + Region ®ion, cfg::ControlFlowGraph &cfg, cfg::BasicBlock *entryBlock, + cfg::BasicBlock *parentStructure) { + cfg::BasicBlock *currentBB = entryBlock; + cfg::BasicBlock *lastBlock = entryBlock; + + // 首先为 region 中的所有 block 创建对应的 BasicBlock 映射 + // 这样可以确保在处理 cf.cond_br 等跳转指令时目标块已存在 + for (Block &block : region) { + if (!blockToBasicBlockMap.count(&block)) { + auto *bb = cfg.createBasicBlock(cfg::BlockType::NORMAL, parentStructure); + registerBlockMapping(&block, bb); + } + } + + // 遍历 region 中的所有 block + for (Block &block : region) { + // 获取该 block 对应的 BasicBlock + cfg::BasicBlock *blockBB = blockToBasicBlockMap[&block]; + + // 如果是第一个 block,合并到 entryBlock + if (blockBB == blockToBasicBlockMap.lookup(®ion.front())) { + // 将 entryBlock 的指令移动到 blockBB(或者反过来) + // 这里简化处理:使用 entryBlock 继续处理 + currentBB = processBlock(block, cfg, currentBB, parentStructure); + } else { + // 确保从上一个 block 的结尾连接到这个 block + if (lastBlock && lastBlock != blockBB) { + // 检查是否已经有边连接 + bool hasEdge = false; + for (auto *succ : lastBlock->getSuccessors()) { + if (succ == blockBB) { + hasEdge = true; + break; + } + } + if (!hasEdge && !lastBlock->endsWithReturnOp()) { + cfg.addEdge(lastBlock, blockBB); + } + } + currentBB = processBlock(block, cfg, blockBB, parentStructure); + } + + if (currentBB) { + lastBlock = currentBB; + if (lastBlock->endsWithReturnOp()) { + cfg.addEdge(lastBlock, cfg.getExitBlock()); + } + } + } + + return {entryBlock, lastBlock}; +} + +cfg::BasicBlock * +ControlFlowGraphBuilder::processBlock(Block &block, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *currentBB, + cfg::BasicBlock *parentStructure) { + if (!currentBB) + return nullptr; + + // 遍历 block 中的所有操作 + for (Operation &op : block) { + // 检查是否是控制流操作 + if (isa(op)) { + // 为 if 条件创建单独的 basic block + auto *ifCondBB = + cfg.createBasicBlock(BlockType::IF_COND, parentStructure); + + // 将当前 if 指令添加到 ifCondBB + createInstruction(&op, ifCondBB, cfg); + + // 连接当前块到 if 条件块 + cfg.addEdge(currentBB, ifCondBB); + + // 处理 if 操作,返回 if 后面的块 + currentBB = + handleIfOp(cast(op), cfg, ifCondBB, parentStructure); + } else if (isa(op)) { + // 为 for 条件创建单独的 basic block + auto *forCondBB = + cfg.createBasicBlock(BlockType::FOR_COND, parentStructure); + + // 将当前 for 指令添加到 forCondBB + createInstruction(&op, forCondBB, cfg); + + // 连接当前块到 for 条件块 + cfg.addEdge(currentBB, forCondBB); + + // 处理 for 操作,返回 for 后面的块 + currentBB = + handleForOp(cast(op), cfg, forCondBB, parentStructure); + } else if (isa(op)) { + // 为 while 条件创建单独的 basic block + auto *whileCondBB = + cfg.createBasicBlock(BlockType::WHILE_COND, parentStructure); + + // 将当前 while 指令添加到 whileCondBB + createInstruction(&op, whileCondBB, cfg); + + // 连接当前块到 while 条件块 + cfg.addEdge(currentBB, whileCondBB); + + // 处理 while 操作,返回 while 后面的块 + currentBB = handleWhileOp(cast(op), cfg, whileCondBB, + parentStructure); + } else if (isa(op)) { + // yield 操作:创建指令并继续(后续由循环处理逻辑连接) + createInstruction(&op, currentBB, cfg); + } else if (isa(op)) { + // condition 操作(while 循环条件):创建指令并继续 + createInstruction(&op, currentBB, cfg); + } else if (isa(op)) { + // cf.cond_br 条件分支 - 创建专门的 COND_BR 块并处理 + auto *condBrBB = + cfg.createBasicBlock(BlockType::COND_BR, parentStructure); + + // 将 cond_br 指令添加到 condBrBB + createInstruction(&op, condBrBB, cfg); + + // 连接当前块到 COND_BR 块 + cfg.addEdge(currentBB, condBrBB); + + // 处理 cond_br 操作,返回后续的基本块 + currentBB = handleCondBranchOp(cast(op), cfg, condBrBB, + parentStructure); + } else if (isa(op)) { + // cf.br 无条件跳转 - 创建专门的 BR 块并处理 + auto *brBB = cfg.createBasicBlock(BlockType::BR, parentStructure); + + // 将 br 指令添加到 brBB + createInstruction(&op, brBB, cfg); + + // 连接当前块到 BR 块 + cfg.addEdge(currentBB, brBB); + + // 处理 br 操作 + currentBB = + handleBranchOp(cast(op), cfg, brBB, parentStructure); + } else if (isa(op)) { + // return 操作 + createInstruction(&op, currentBB, cfg); + } else if (op.getNumRegions() > 0) { + // 有内部区域的 Triton 操作 (如 tt.reduce, tt.scan 等) + // 先创建指令 + auto *inst = createInstruction(&op, currentBB, cfg); + + // 为该操作创建子图 + auto subGraph = + std::make_unique(cfg.getFunction()); + auto *subEntry = subGraph->createBasicBlock(cfg::BlockType::ENTRY); + subGraph->setEntryBlock(subEntry); + auto *subExit = subGraph->createBasicBlock(cfg::BlockType::EXIT); + subGraph->setExitBlock(subExit); + + // 用于子图中生成唯一指令 ID + size_t subInstId = 0; + + // 遍历所有区域 + for (size_t regionIdx = 0; regionIdx < op.getNumRegions(); ++regionIdx) { + Region ®ion = op.getRegion(regionIdx); + if (!region.empty()) { + // 为每个区域构建 CFG + cfg::BasicBlock *regionEntryBB = nullptr; + cfg::BasicBlock *regionLastBB = nullptr; + + for (Block ®ionBlock : region) { + auto *bb = subGraph->createBasicBlock(cfg::BlockType::NORMAL); + if (!regionEntryBB) + regionEntryBB = bb; + + // 将区域中的操作添加到子图的基本块 + for (Operation ®ionOp : regionBlock) { + auto regionInst = std::make_unique( + subInstId++, ®ionOp, bb); + cfg::Instruction *instPtr = regionInst.get(); + bb->addInstruction(std::move(regionInst)); + + // 添加到op到instruction的映射 + subGraph->addOpToInstruction(®ionOp, instPtr); + } + + // 连接基本块 + if (regionLastBB) { + subGraph->addEdge(regionLastBB, bb); + } + regionLastBB = bb; + } + + // 连接区域入口到子图入口 + if (regionEntryBB) { + subGraph->addEdge(subEntry, regionEntryBB); + } + // 连接区域出口到子图出口 + if (regionLastBB) { + subGraph->addEdge(regionLastBB, subExit); + } + } + } + + // 设置子图 + inst->setSubGraph(std::move(subGraph)); + } else { + // 普通操作,直接添加到当前 basic block + createInstruction(&op, currentBB, cfg); + } + } + + return currentBB; +} + +cfg::BasicBlock * +ControlFlowGraphBuilder::handleIfOp(scf::IfOp ifOp, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *ifCondBB, + cfg::BasicBlock *parentStructure) { + // 创建 if 后面的汇合块 + auto *mergeBB = cfg.createBasicBlock(BlockType::NORMAL, parentStructure); + + // 设置 ifCondBB 的出口块为 mergeBB + ifCondBB->setExitBlock(mergeBB); + + // 处理 then 分支 + cfg::BasicBlock *thenExitBB = nullptr; + if (!ifOp.getThenRegion().empty()) { + // 创建 then 区域的入口块 + auto *thenEntryBB = cfg.createBasicBlock(BlockType::NORMAL, ifCondBB); + cfg.addEdge(ifCondBB, thenEntryBB); + + // 构建 then 区域的 CFG + auto result = + buildForRegion(ifOp.getThenRegion(), cfg, thenEntryBB, ifCondBB); + thenExitBB = result.exitBlock; + } + + // 处理 else 分支 + cfg::BasicBlock *elseExitBB = nullptr; + bool hasElse = !ifOp.getElseRegion().empty(); + + if (hasElse) { + // 创建 else 区域的入口块 + auto *elseEntryBB = cfg.createBasicBlock(BlockType::NORMAL, ifCondBB); + cfg.addEdge(ifCondBB, elseEntryBB); + + // 构建 else 区域的 CFG + auto result = + buildForRegion(ifOp.getElseRegion(), cfg, elseEntryBB, ifCondBB); + elseExitBB = result.exitBlock; + } + + // 连接 then 分支到汇合块 + if (thenExitBB) { + cfg.addEdge(thenExitBB, mergeBB); + } else { + // 空的 then 分支,直接从 ifCondBB 连接到 mergeBB + cfg.addEdge(ifCondBB, mergeBB); + } + + // 连接 else 分支到汇合块 + if (hasElse) { + if (elseExitBB) { + cfg.addEdge(elseExitBB, mergeBB); + } else { + // 空的 else 分支,直接从 ifCondBB 连接到 mergeBB + cfg.addEdge(ifCondBB, mergeBB); + } + } else { + // 没有 else 分支,ifCondBB 直接连接到 mergeBB(else 路径) + cfg.addEdge(ifCondBB, mergeBB); + } + + return mergeBB; +} + +cfg::BasicBlock *ControlFlowGraphBuilder::handleForOp( + scf::ForOp forOp, cfg::ControlFlowGraph &cfg, cfg::BasicBlock *forCondBB, + cfg::BasicBlock *parentStructure) { + // 创建循环体入口块 + auto *loopBodyEntryBB = cfg.createBasicBlock(BlockType::LOOP_BODY, forCondBB); + cfg.addEdge(forCondBB, loopBodyEntryBB); + + // 创建循环出口块 + auto *loopExitBB = + cfg.createBasicBlock(BlockType::LOOP_EXIT, parentStructure); + + // 设置 forCondBB 的出口块为 loopExitBB + forCondBB->setExitBlock(loopExitBB); + + // 构建循环体的 CFG + auto result = + buildForRegion(forOp.getRegion(), cfg, loopBodyEntryBB, forCondBB); + + // 循环体结束需要回到循环头(通过 yield 操作) + if (result.exitBlock) { + cfg.addEdge(result.exitBlock, forCondBB); + } + + // 循环出口 + cfg.addEdge(forCondBB, loopExitBB); + + return loopExitBB; +} + +cfg::BasicBlock *ControlFlowGraphBuilder::handleWhileOp( + scf::WhileOp whileOp, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *whileCondBB, cfg::BasicBlock *parentStructure) { + // while 操作有两个区域:before(条件)和 after(循环体) + // 控制流: + // whileCondBB (包含 scf.while 指令) + // ↓ + // beforeEntryBB (条件计算区域) + // ↓ + // scf.condition 分支:真 → afterEntryBB, 假 → loopExitBB + // ↓ + // afterEntryBB (循环体) + // ↓ + // scf.yield + // ↓ + // whileCondBB (回到 while 头,重新进入 before) + + // 创建 before 区域的入口块(条件计算) + auto *beforeEntryBB = cfg.createBasicBlock(BlockType::LOOP_BODY, whileCondBB); + cfg.addEdge(whileCondBB, beforeEntryBB); + + // 创建 after 区域的入口块(循环体) + auto *afterEntryBB = cfg.createBasicBlock(BlockType::LOOP_BODY, whileCondBB); + + // 创建循环出口块 + auto *loopExitBB = + cfg.createBasicBlock(BlockType::LOOP_EXIT, parentStructure); + + // 设置 whileCondBB 的出口块为 loopExitBB + whileCondBB->setExitBlock(loopExitBB); + + // 构建 before 区域的 CFG(条件计算区域) + // before 区域以一个 scf.condition 操作结束 + auto beforeResult = + buildForRegion(whileOp.getBefore(), cfg, beforeEntryBB, whileCondBB); + + // 构建 after 区域的 CFG(循环体区域) + // after 区域以 scf.yield 结束,yield 的参数会传递给 before 区域的参数 + auto afterResult = + buildForRegion(whileOp.getAfter(), cfg, afterEntryBB, whileCondBB); + + // 处理 before 区域结束后的分支 + // before 区域应该以一个 scf.condition 操作结束 + // 该操作决定是进入 after 区域还是退出循环 + if (beforeResult.exitBlock) { + // 从 before 出口连接到 after 入口(条件为真时) + cfg.addEdge(beforeResult.exitBlock, afterEntryBB); + + // 从 before 出口连接到循环出口(条件为假时) + // 注意:在实际的 scf.condition 中,条件为假会直接退出循环 + cfg.addEdge(beforeResult.exitBlock, loopExitBB); + } + + // after 区域结束后回到 whileCondBB(重新进入 before 区域进行条件检查) + if (afterResult.exitBlock) { + cfg.addEdge(afterResult.exitBlock, whileCondBB); + } + + return loopExitBB; +} + +cfg::Instruction *ControlFlowGraphBuilder::createInstruction( + Operation *op, cfg::BasicBlock *parentBlock, cfg::ControlFlowGraph &cfg) { + if (!op || !parentBlock) + return nullptr; + + auto inst = std::make_unique(getNextInstructionId(), op, + parentBlock); + cfg::Instruction *instPtr = inst.get(); + parentBlock->addInstruction(std::move(inst)); + + // 将 Operation 到 Instruction 的映射添加到 CFG + cfg.addOpToInstruction(op, instPtr); + + return instPtr; +} + +//===----------------------------------------------------------------------===// +// ControlFlowGraphBuilder Implementation +//===----------------------------------------------------------------------===// + +std::unique_ptr +ControlFlowGraphBuilder::build(triton::FuncOp func) { + auto cfg = std::make_unique(func); + + if (func.getBody().empty()) { + // 空函数,只创建入口和出口 + auto *entry = cfg->createBasicBlock(BlockType::ENTRY); + auto *exit = cfg->createBasicBlock(BlockType::EXIT); + cfg->addEdge(entry, exit); + return cfg; + } + + // 创建入口块 + auto *entryBlock = cfg->createBasicBlock(BlockType::ENTRY); + cfg->setEntryBlock(entryBlock); + + // 创建出口块 + auto *exitBlock = cfg->createBasicBlock(BlockType::EXIT); + cfg->setExitBlock(exitBlock); + + // 为函数体构建 CFG + auto result = buildForRegion(func.getBody(), *cfg, entryBlock, nullptr); + + // 连接函数体出口到函数出口 + if (result.exitBlock) { + cfg->addEdge(result.exitBlock, exitBlock); + } + + return cfg; +} + +std::vector> +ControlFlowGraphBuilder::buildForModule(ModuleOp module) { + std::vector> cfgs; + + for (triton::FuncOp func : module.getOps()) { + auto cfg = build(func); + if (cfg) { + cfgs.push_back(std::move(cfg)); + } + } + + return cfgs; +} + +cfg::BasicBlock *ControlFlowGraphBuilder::handleCondBranchOp( + cf::CondBranchOp condBrOp, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *condBrBB, cfg::BasicBlock *parentStructure) { + // 创建汇合块(用于 cond_br 之后的代码) + auto *mergeBB = cfg.createBasicBlock(BlockType::NORMAL, parentStructure); + + // 设置 condBrBB 的出口块为 mergeBB + condBrBB->setExitBlock(mergeBB); + + // 获取条件值 + Value condition = condBrOp.getCondition(); + + // 获取 true 分支的目标块和参数 + Block *trueDest = condBrOp.getTrueDest(); + SmallVector trueOperands(condBrOp.getTrueDestOperands()); + + // 获取 false 分支的目标块和参数 + Block *falseDest = condBrOp.getFalseDest(); + SmallVector falseOperands(condBrOp.getFalseDestOperands()); + + LLVM_DEBUG(llvm::dbgs() << " CondBr: condition=" << condition << "\n"); + LLVM_DEBUG(llvm::dbgs() << " True dest: " << trueDest << "\n"); + LLVM_DEBUG(llvm::dbgs() << " False dest: " << falseDest << "\n"); + + // 为 true 分支创建入口块(如果目标块还没有对应的 BasicBlock) + cfg::BasicBlock *trueEntryBB = + getOrCreateBasicBlockForBlock(trueDest, cfg, parentStructure); + + // 为 false 分支创建入口块 + cfg::BasicBlock *falseEntryBB = + getOrCreateBasicBlockForBlock(falseDest, cfg, parentStructure); + + // 连接 COND_BR 块到两个分支 + cfg.addEdge(condBrBB, trueEntryBB); + cfg.addEdge(condBrBB, falseEntryBB); + + // 存储分支信息到指令的 MemorySSAInfo 中(用于后续查询) + if (condBrBB->getNumInstructions() > 0) { + cfg::Instruction *inst = condBrBB->getInstruction(0); + // 可以通过 inst->getMemorySSAInfo() 存储额外信息 + } + + // 返回汇合块,后续代码将在此块中继续 + return mergeBB; +} + +cfg::BasicBlock *ControlFlowGraphBuilder::handleBranchOp( + cf::BranchOp brOp, cfg::ControlFlowGraph &cfg, cfg::BasicBlock *brBB, + cfg::BasicBlock *parentStructure) { + // 无条件跳转没有汇合块,直接连接到目标块 + + // 获取目标块和参数 + Block *dest = brOp.getDest(); + SmallVector destOperands(brOp.getDestOperands()); + + LLVM_DEBUG(llvm::dbgs() << " Br: unconditional branch\n"); + LLVM_DEBUG(llvm::dbgs() << " Dest: " << dest << "\n"); + + // 获取或创建目标块对应的 BasicBlock + cfg::BasicBlock *destBB = + getOrCreateBasicBlockForBlock(dest, cfg, parentStructure); + + // 连接 BR 块到目标块 + cfg.addEdge(brBB, destBB); + + // 无条件跳转没有后续代码,返回 nullptr 表示当前路径结束 + return nullptr; +} + +cfg::BasicBlock *ControlFlowGraphBuilder::getOrCreateBasicBlockForBlock( + Block *block, cfg::ControlFlowGraph &cfg, + cfg::BasicBlock *parentStructure) { + // 检查是否已经有对应的 BasicBlock + auto it = blockToBasicBlockMap.find(block); + if (it != blockToBasicBlockMap.end()) { + return it->second; + } + + // 创建新的 BasicBlock + auto *bb = cfg.createBasicBlock(BlockType::NORMAL, parentStructure); + + // 注册映射关系 + registerBlockMapping(block, bb); + + return bb; +} + +void ControlFlowGraphBuilder::registerBlockMapping(Block *mlirBlock, + cfg::BasicBlock *cfgBlock) { + blockToBasicBlockMap[mlirBlock] = cfgBlock; +} + +SmallVector +ControlFlowGraphBuilder::collectCondBrBlocks(cfg::ControlFlowGraph &cfg) { + SmallVector condBrBlocks; + + // 遍历 CFG 中的所有基本块 + for (size_t i = 0; i < cfg.getNumBlocks(); ++i) { + cfg::BasicBlock *bb = cfg.getBasicBlock(i); + if (bb && bb->getType() == BlockType::COND_BR) { + condBrBlocks.push_back(bb); + } + } + + return condBrBlocks; +} + +std::optional +ControlFlowGraphBuilder::getCondBranchMapping(cfg::BasicBlock *condBrBB) { + // 验证输入基本块类型 + if (!condBrBB || condBrBB->getType() != BlockType::COND_BR) { + return std::nullopt; + } + + // 获取 COND_BR 块中的指令(应该包含 cf.cond_br 操作) + if (condBrBB->getNumInstructions() == 0) { + return std::nullopt; + } + + cfg::Instruction *inst = condBrBB->getInstruction(0); + Operation *op = inst->getOperation(); + + // 确保是 cf.cond_br 操作 + auto condBrOp = dyn_cast(op); + if (!condBrOp) { + return std::nullopt; + } + + CondBranchMapping mapping; + + // 收集条件值 + mapping.condition = condBrOp.getCondition(); + + // 收集 true 分支信息 + mapping.trueDest = condBrOp.getTrueDest(); + for (Value operand : condBrOp.getTrueDestOperands()) { + mapping.trueOperands.push_back(operand); + } + + // 收集 false 分支信息 + mapping.falseDest = condBrOp.getFalseDest(); + for (Value operand : condBrOp.getFalseDestOperands()) { + mapping.falseOperands.push_back(operand); + } + + return mapping; +} + +SmallVector +ControlFlowGraphBuilder::collectBrBlocks(cfg::ControlFlowGraph &cfg) { + SmallVector brBlocks; + + // 遍历 CFG 中的所有基本块 + for (size_t i = 0; i < cfg.getNumBlocks(); ++i) { + cfg::BasicBlock *bb = cfg.getBasicBlock(i); + if (bb && bb->getType() == BlockType::BR) { + brBlocks.push_back(bb); + } + } + + return brBlocks; +} + +std::optional +ControlFlowGraphBuilder::getBranchMapping(cfg::BasicBlock *brBB) { + // 验证输入基本块类型 + if (!brBB || brBB->getType() != BlockType::BR) { + return std::nullopt; + } + + // 获取 BR 块中的指令(应该包含 cf.br 操作) + if (brBB->getNumInstructions() == 0) { + return std::nullopt; + } + + cfg::Instruction *inst = brBB->getInstruction(0); + Operation *op = inst->getOperation(); + + // 确保是 cf.br 操作 + auto brOp = dyn_cast(op); + if (!brOp) { + return std::nullopt; + } + + BranchMapping mapping; + + // 收集目标块信息 + mapping.dest = brOp.getDest(); + for (Value operand : brOp.getDestOperands()) { + mapping.destOperands.push_back(operand); + } + + return mapping; +} + +std::unique_ptr> +mlir::triton::cfg::createBuildCFGPass() { + return std::unique_ptr>(new BuildCFGPass()); +} + +//===----------------------------------------------------------------------===// +// ControlFlowGraphBuilderWithQueries Implementation +//===----------------------------------------------------------------------===// + +SmallVector +ControlFlowGraphBuilder::collectIfCondBlocks(cfg::ControlFlowGraph &cfg) { + SmallVector ifCondBlocks; + + // 遍历 CFG 中的所有基本块 + for (size_t i = 0; i < cfg.getNumBlocks(); ++i) { + cfg::BasicBlock *bb = cfg.getBasicBlock(i); + if (bb && bb->getType() == BlockType::IF_COND) { + ifCondBlocks.push_back(bb); + } + } + + return ifCondBlocks; +} + +SmallVector +ControlFlowGraphBuilder::collectForCondBlocks(cfg::ControlFlowGraph &cfg) { + SmallVector forCondBlocks; + + // 遍历 CFG 中的所有基本块 + for (size_t i = 0; i < cfg.getNumBlocks(); ++i) { + cfg::BasicBlock *bb = cfg.getBasicBlock(i); + if (bb && bb->getType() == BlockType::FOR_COND) { + forCondBlocks.push_back(bb); + } + } + + return forCondBlocks; +} + +std::optional +ControlFlowGraphBuilder::getIfYieldResultMapping(cfg::BasicBlock *ifCondBB) { + // 验证输入基本块类型 + if (!ifCondBB || ifCondBB->getType() != BlockType::IF_COND) { + return std::nullopt; + } + + // 获取 IF_COND 块中的指令(应该包含 scf.if 操作) + if (ifCondBB->getNumInstructions() == 0) { + return std::nullopt; + } + + cfg::Instruction *inst = ifCondBB->getInstruction(0); + Operation *op = inst->getOperation(); + + // 确保是 scf.if 操作 + auto ifOp = dyn_cast(op); + if (!ifOp) { + return std::nullopt; + } + + IfYieldResultMapping mapping; + + // 收集 result values + for (Value result : ifOp.getResults()) { + mapping.resultValues.push_back(result); + } + + // 处理 then 分支的 yield values + if (!ifOp.getThenRegion().empty()) { + Block &thenBlock = ifOp.getThenRegion().back(); + // 查找 then 区域末尾的 scf.yield 操作 + for (Operation &thenOp : thenBlock) { + if (auto yieldOp = dyn_cast(thenOp)) { + for (Value operand : yieldOp.getOperands()) { + mapping.thenYieldValues.push_back(operand); + } + break; + } + } + } + + // 处理 else 分支的 yield values(如果有) + if (!ifOp.getElseRegion().empty()) { + Block &elseBlock = ifOp.getElseRegion().back(); + // 查找 else 区域末尾的 scf.yield 操作 + for (Operation &elseOp : elseBlock) { + if (auto yieldOp = dyn_cast(elseOp)) { + for (Value operand : yieldOp.getOperands()) { + mapping.elseYieldValues.push_back(operand); + } + break; + } + } + } + + return mapping; +} + +std::optional +ControlFlowGraphBuilder::getForYieldIterArgMapping(cfg::BasicBlock *forCondBB) { + // 验证输入基本块类型 + if (!forCondBB || forCondBB->getType() != BlockType::FOR_COND) { + return std::nullopt; + } + + // 获取 FOR_COND 块中的指令(应该包含 scf.for 操作) + if (forCondBB->getNumInstructions() == 0) { + return std::nullopt; + } + + cfg::Instruction *inst = forCondBB->getInstruction(0); + Operation *op = inst->getOperation(); + + // 确保是 scf.for 操作 + auto forOp = dyn_cast(op); + if (!forOp) { + return std::nullopt; + } + + ForYieldIterArgMapping mapping; + + // 收集 iter_args(循环初始参数) + for (Value iterArg : forOp.getRegionIterArgs()) { + mapping.iterArgValues.push_back(iterArg); + } + + // 收集 result values + for (Value result : forOp.getResults()) { + mapping.resultValues.push_back(result); + } + + // 处理循环体的 yield values + if (!forOp.getRegion().empty()) { + Block &loopBlock = forOp.getRegion().back(); + // 查找循环体末尾的 scf.yield 操作 + for (Operation &loopOp : loopBlock) { + if (auto yieldOp = dyn_cast(loopOp)) { + for (Value operand : yieldOp.getOperands()) { + mapping.yieldValues.push_back(operand); + } + break; + } + } + } + + return mapping; +} diff --git a/third_party/ascend/lib/TritonToGraph/DataflowGraph.cpp b/third_party/ascend/lib/TritonToGraph/DataflowGraph.cpp new file mode 100644 index 0000000000..421f63b85a --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/DataflowGraph.cpp @@ -0,0 +1,311 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/DataflowGraph.h" +#include "TritonToGraph/AliasAnalysis.h" +#include "TritonToGraph/ControlFlowGraph.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/raw_ostream.h" + +#define DEBUG_TYPE "dataflow-graph" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +//===----------------------------------------------------------------------===// +// DataFlowInfo +//===----------------------------------------------------------------------===// + +// Memory SSA相关接口 +MemorySSADef *DataFlowInfo::getMemoryDefinition(Value value) const { + auto it = memoryDefinitions.find(value); + return (it != memoryDefinitions.end()) ? it->second : nullptr; +} + +void DataFlowInfo::addMemoryDefinition(Value value, MemorySSADef *def) { + memoryDefinitions[value] = def; + // 确保没有旧的uses冲突 + memoryUses.erase(value); +} + +SmallVector DataFlowInfo::getMemoryUses(Value value) const { + auto it = memoryUses.find(value); + if (it != memoryUses.end()) { + return it->second; + } + return SmallVector(); +} + +void DataFlowInfo::addMemoryUse(Value value, const MemorySSAUse &use) { + memoryUses[value].push_back(use); +} + +void DataFlowInfo::removeMemoryDefinition(Value value) { + memoryDefinitions.erase(value); + memoryUses.erase(value); + invalidateDefUseCache(); +} + +void DataFlowInfo::clearMemoryUses(Value value) { + memoryUses[value].clear(); + invalidateDefUseCache(); +} + +std::unique_ptr DataFlowInfo::queryDataFlow(Value value) const { + // 1. 优先查询Memory SSA + if (MemorySSADef *def = getMemoryDefinition(value)) { + auto result = std::make_unique(def->getDefOp(), def); + result->getUses() = getSSAUses(value); + return result; + } + + // 2. 查询传统SSA + if (Operation *defOp = value.getDefiningOp()) { + auto result = std::make_unique(defOp, defOp); + result->getUses() = getSSAUses(value); + return result; + } + + // 3. 入参 + auto result = std::make_unique(nullptr, nullptr); + result->getUses() = getSSAUses(value); + return result; +} + +SmallVector DataFlowInfo::getUses(MemorySSADef *def) const { + SmallVector result; + + // 遍历所有uses,查找使用该definition的 + for (const auto &entry : memoryUses) { + for (const MemorySSAUse &use : entry.second) { + if (use.getDefinition() == def) { + result.push_back(use); + } + } + } + + return result; +} + +SmallVector +DataFlowInfo::getUsesByUserOp(Operation *userOp) const { + SmallVector result; + + for (const auto &entry : memoryUses) { + for (const MemorySSAUse &use : entry.second) { + if (use.getUserOp() == userOp) { + result.push_back(use); + } + } + } + + return result; +} + +void DataFlowInfo::buildDefUseCache() const { + if (defUseCacheValid) + return; + + defUseCache.clear(); + + for (const auto &entry : memoryUses) { + for (const MemorySSAUse &use : entry.second) { + MemorySSADef *def = use.getDefinition(); + if (def) { + defUseCache[def].push_back(use); + } + } + } + + defUseCacheValid = true; +} + +void DataFlowInfo::forEachDefinition( + llvm::function_ref func) const { + for (const auto &entry : memoryDefinitions) { + func(entry.first, entry.second); + } +} + +void DataFlowInfo::forEachUse( + llvm::function_ref func) const { + for (const auto &entry : memoryUses) { + for (const MemorySSAUse &use : entry.second) { + func(use); + } + } +} + +void DataFlowInfo::print(llvm::raw_ostream &os) const { + os << "=== Data Flow Information ===" << "\n"; + + os << "Memory Definitions: " << memoryDefinitions.size() << "\n"; + for (const auto &entry : memoryDefinitions) { + os << " " << entry.first << " -> "; + entry.second->print(os); + os << "\n"; + } + + os << "Memory Uses: " << "\n"; + for (const auto &entry : memoryUses) { + os << " " << entry.first << ": "; + for (const MemorySSAUse &use : entry.second) { + os << "[" << use.getDefinition()->getId() << "] "; + } + os << "\n"; + } + + os << "Phis: " << Phis.size() << "\n"; + for (const auto &entry : Phis) { + os << " " << entry.first << ": "; + switch (entry.second.type) { + case PhiInfo::ITER_ARG: + os << "ITER_ARG"; + break; + case PhiInfo::IF_RESULT: + os << "IF_RESULT"; + break; + case PhiInfo::WHILE_ARG: + os << "WHILE_ARG"; + break; + } + os << "\n"; + } +} + +void DataFlowInfo::exportToJSON(llvm::raw_ostream &os) const { + os << "{\n"; + os << " \"memoryDefinitions\": {\n"; + bool first = true; + for (const auto &entry : memoryDefinitions) { + if (!first) + os << ",\n"; + first = false; + os << " \"" << entry.first << "\": {\n"; + os << " \"id\": \"" << entry.second->getId() << "\",\n"; + os << " \"tensor\": \"" << entry.second->getTensor()->getName() + << "\",\n"; + os << " \"version\": " << entry.second->getVersion(); + os << "\n }"; + } + os << "\n },\n"; + + os << " \"Phis\": {\n"; + first = true; + for (const auto &entry : Phis) { + if (!first) + os << ",\n"; + first = false; + os << " \"" << entry.first << "\": {\n"; + os << " \"type\": " << entry.second.type << "\n"; + os << " }"; + } + os << "\n }\n"; + os << "}\n"; +} + +//===----------------------------------------------------------------------===// +// DataFlowGraph +//===----------------------------------------------------------------------===// + +DataFlowGraph::~DataFlowGraph() { + if (!memorySSABuilder) + return; + cfg.traverse([](BasicBlock &block) { + for (const auto &instruction : block.getInstructions()) + instruction->getMemorySSAInfo().clear(); + }); + aliasAnalysis.endDataFlowGraphBorrow(); +} + +void DataFlowGraph::build() { + if (memorySSABuilder) + return; + LLVM_DEBUG(llvm::dbgs() << "=== Starting Data Flow Graph Build ===" << "\n"); + + auto builder = + std::make_unique(cfg, aliasAnalysis, dataFlowInfo); + if (!aliasAnalysis.beginDataFlowGraphBorrow()) + return; + memorySSABuilder = std::move(builder); + memorySSABuilder->build(); + + LLVM_DEBUG(llvm::dbgs() << "Memory SSA build complete" << "\n"); + + buildDefUseGraph(); + + LLVM_DEBUG(llvm::dbgs() << "=== Data Flow Graph Build Complete ===" << "\n"); +} + +void DataFlowGraph::buildDefUseGraph() { + // 构建def-use图(在DataFlowInfo中实现) + dataFlowInfo.buildDefUseCache(); + + LLVM_DEBUG(llvm::dbgs() << "Def-use graph built" << "\n"); +} + +void DataFlowGraph::print(llvm::raw_ostream &os) const { + os << "=== Data Flow Graph ===" << "\n"; + dataFlowInfo.print(os); +} + +void DataFlowGraph::dump() const { print(llvm::errs()); } + +void DataFlowGraph::exportToJSON(llvm::raw_ostream &os) const { + auto funcOp = cfg.getFunction(); + auto funcName = funcOp.getName(); + std::string funcNameStr = funcName.empty() ? "unnamed" : funcName.str(); + + os << "{\n"; + os << " \"function\": \"" << funcNameStr << "\"," << "\n"; + os << " \"dataFlow\": "; + dataFlowInfo.exportToJSON(os); + os << "}\n"; +} + +void DataFlowGraph::exportDefUseToDOT(llvm::raw_ostream &os) const { + os << "digraph DefUseGraph {\n"; + os << " rankdir=TB;\n"; + os << " node [shape=box];\n\n"; + + // 遍历所有definitions并导出 + size_t nodeId = 0; + DenseMap defToNode; + + cfg.traverse([&](const BasicBlock &bb) { + for (const auto &instPtr : bb.getInstructions()) { + const Instruction *inst = instPtr.get(); + const MemorySSAInfo &ssaInfo = inst->getMemorySSAInfo(); + + for (MemorySSADef *def : ssaInfo.definitions) { + if (def && !defToNode.count(def)) { + defToNode[def] = nodeId++; + os << " node_" << defToNode[def] << " [label=\"" << def->getId() + << "\"];\n"; + } + } + } + }); + + os << "}\n"; +} diff --git a/third_party/ascend/lib/TritonToGraph/EntryArgPointerAliasAnalysis.cpp b/third_party/ascend/lib/TritonToGraph/EntryArgPointerAliasAnalysis.cpp new file mode 100644 index 0000000000..18fa9a7818 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/EntryArgPointerAliasAnalysis.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/EntryArgPointerAliasAnalysis.h" + +#include "TritonToGraph/AliasAnalysis.h" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +EntryArgPointerAliasAnalysis::EntryArgPointerAliasAnalysis( + triton::FuncOp function, const AliasAnalysis &aliasAnalysis) + : aliasAnalysis(aliasAnalysis) { + for (BlockArgument argument : function.getArguments()) { + if (!AliasAnalysis::isPointerType(argument.getType()) || + !AliasAnalysis::isGlobalMemoryType(argument.getType()) || + !aliasAnalysis.getTensorObject(argument)) + continue; + entryPointerRoots.push_back(argument); + } +} + +EntryArgPointerRelation +EntryArgPointerAliasAnalysis::classify(Value lhs, Value rhs) const { + Value lhsRoot = getEntryPointerRoot(lhs); + Value rhsRoot = getEntryPointerRoot(rhs); + if (!lhsRoot || !rhsRoot) + return EntryArgPointerRelation::Unknown; + + return lhsRoot == rhsRoot ? EntryArgPointerRelation::SameEntryRoot + : EntryArgPointerRelation::DistinctEntryRoots; +} + +Value EntryArgPointerAliasAnalysis::getEntryPointerRoot(Value pointer) const { + if (!pointer || !AliasAnalysis::isPointerType(pointer.getType())) + return {}; + + Value base = aliasAnalysis.getBasePointer(pointer); + if (!base || !AliasAnalysis::isPointerType(base.getType())) + return {}; + + // AliasAnalysis only creates TensorObjects for global-memory function + // arguments. Require that tracked provenance before treating a root as an + // entry ABI pointer. + if (!aliasAnalysis.getTensorObject(base)) + return {}; + + for (Value entryPointerRoot : entryPointerRoots) { + if (entryPointerRoot == base) + return entryPointerRoot; + } + + return {}; +} diff --git a/third_party/ascend/lib/TritonToGraph/GraphAnalysis.cpp b/third_party/ascend/lib/TritonToGraph/GraphAnalysis.cpp new file mode 100644 index 0000000000..6b2cc3be72 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/GraphAnalysis.cpp @@ -0,0 +1,627 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + */ + +#include "TritonToGraph/GraphAnalysis.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "graph-analysis" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +//===----------------------------------------------------------------------===// +// CFGTraverser Implementation +//===----------------------------------------------------------------------===// + +void CFGTraverser::dfsForward(CFGTraversalBase &visitor) { + DenseSet visited; + TraversalContext ctx; + dfsForwardImpl(cfg.getEntryBlock(), visited, ctx, visitor); +} + +void CFGTraverser::dfsForward(BasicBlock *start, CFGTraversalBase &visitor) { + DenseSet visited; + TraversalContext ctx; + dfsForwardImpl(start, visited, ctx, visitor); +} + +void CFGTraverser::dfsForwardImpl(BasicBlock *block, + DenseSet &visited, + TraversalContext &ctx, + CFGTraversalBase &visitor) { + if (!block || visited.contains(block)) + return; + + visited.insert(block); + + // pre-visit block + visitor.preVisitBlock(block, ctx); + + // visit instructions in block + for (auto &instPtr : block->getInstructions()) { + Instruction *inst = instPtr.get(); + + // check for structure entry + if (inst->hasSubGraph()) { + visitor.onEnterStructure(block, ctx); + ctx.push(block); + } + + visitor.VisitInstruction(inst, ctx); + + // check for structure exit (last instruction in structure block) + if (inst->hasSubGraph()) { + ctx.pop(); + visitor.onExitStructure(block, ctx); + } + } + + // visit successors + for (BasicBlock *succ : block->getSuccessors()) { + if (visited.contains(succ)) + continue; + + // check for back edge + if (cfg.isBackEdge(block, succ)) { + visitor.onBackEdge(block, succ, ctx); + } + + dfsForwardImpl(succ, visited, ctx, visitor); + } + + visitor.postVisitBlock(block, ctx); +} + +void CFGTraverser::dfsBackward(BasicBlock *start, CFGTraversalBase &visitor) { + DenseSet visited; + TraversalContext ctx; + dfsBackwardImpl(start, visited, ctx, visitor); +} + +void CFGTraverser::dfsBackwardImpl(BasicBlock *block, + DenseSet &visited, + TraversalContext &ctx, + CFGTraversalBase &visitor) { + if (!block || visited.contains(block)) + return; + + visited.insert(block); + + visitor.preVisitBlock(block, ctx); + + // visit instructions in reverse order + auto &insts = block->getInstructions(); + for (auto it = insts.rbegin(); it != insts.rend(); ++it) { + Instruction *inst = it->get(); + visitor.VisitInstruction(inst, ctx); + } + + // visit predecessors + for (BasicBlock *pred : block->getPredecessors()) { + if (cfg.isBackEdge(pred, block)) { + visitor.onBackEdge(pred, block, ctx); + } + dfsBackwardImpl(pred, visited, ctx, visitor); + } + + visitor.postVisitBlock(block, ctx); +} + +void CFGTraverser::bfsForward(CFGTraversalBase &visitor) { + bfsForward(cfg.getEntryBlock(), visitor); +} + +void CFGTraverser::bfsForward(BasicBlock *start, CFGTraversalBase &visitor) { + DenseSet visited; + SmallVector> worklist; + + worklist.push_back({start, TraversalContext()}); + visited.insert(start); + + while (!worklist.empty()) { + auto [block, ctx] = worklist.pop_back_val(); + + preVisitBlock(block, const_cast(ctx)); + + for (auto &instPtr : block->getInstructions()) { + Instruction *inst = instPtr.get(); + visitor.VisitInstruction(inst, const_cast(ctx)); + } + + visitor.postVisitBlock(block, const_cast(ctx)); + + for (BasicBlock *succ : block->getSuccessors()) { + if (!visited.contains(succ)) { + visited.insert(succ); + worklist.push_back({succ, ctx}); + } + } + } +} + +void CFGTraverser::bfsBackward(BasicBlock *start, CFGTraversalBase &visitor) { + DenseSet visited; + SmallVector> worklist; + + worklist.push_back({start, TraversalContext()}); + visited.insert(start); + + while (!worklist.empty()) { + auto [block, ctx] = worklist.pop_back_val(); + + visitor.preVisitBlock(block, const_cast(ctx)) + + for (auto it = insts.rbegin(); it != insts.rend(); ++it) { + Instruction *inst = it->get(); + visitor.VisitInstruction(inst, ctx); + } + + visitor.postVisitBlock(block, const_cast(ctx)); + + for (BasicBlock *pred : block->getPredecessors()) { + if (!visited.contains(pred)) { + visited.insert(pred); + worklist.push_back({pred, ctx}); + } + } + } +} + +//===----------------------------------------------------------------------===// +// DFGTraverser Implementation +//===----------------------------------------------------------------------===// + +void DFGTraverser::dfsBackward(Value seed, DFGTraversalBase &visitor, + const Options &opts) { + DenseSet visited; + dfsBackwardImpl(seed, visitor, visited, pts, 0); +} + +void DFGTraverser::dfsBackward(ArrayRef seeds, DFGTraversalBase &visitor, + const Options &opts) { + DenseSet visited; + for (Value seed : seeds) { + dfsBackwardImpl(seed, visitor, visited, opts, 0); + } +} + +void DFGTraverser::dfsBackwardImpl(Value value, DFGTraversalBase &visitor, + DenseSet &visited, + const Options &opts, int depth) { + if (opts.maxDepth >= 0 && depth > opts.maxDepth) + return; + + // get definition + Operation *defOp = nullptr; + if (opts.useMemorySSA) { + auto result = dfg.queryDataFlow(value); + if (auto *memResult = dyn_cast(result.get())) { + defOp = memResult->getDefinition()->getDefOp(); + } + } else { + defOp = value.getDefiningOp(); + } + + if (!defOp) + return; + + if (visited.contains(defOp)) + return; + + if (opts.stopOps.contains(defOp)) + return; + + visited.insert(defOp); + + visitor.VisitDef(value, defOp, depth); + + // recursively visit operands + for (Value operand : defOp->getOperands()) { + dfsBackwardImpl(operand, visitor, visited, opts, depth + 1); + } + + // handle phi/iter_arg + if (opts.followPhi) { + auto &dataFlowInfo = dfg.getDataFlowInfo(); + if (dataFlowInfo.hasPhi(value)) { + auto &phiInfo = dataFlowInfo.getPhi(value); + visitor.onPhi(value, phiInfo, depth); + } + } +} + +void DFGTraverser::dfsForward(Value seed, DFGTraversalBase &visitor, + const Options &opts) { + DenseSet visited; + dfsForwardImpl(seed, visitor, visited, ctx, opts, 0); +} + +void DFGTraverser::dfsForwardImpl(Value value, DFGTraversalBase &visitor, + DenseSet &visited, + const Options &opts, int depth) { + if (opts.maxDepth >= 0 && depth > opts.maxDepth) + return; + + SmallVector uses; + if (opts.useMemorySSA) { + uses = dfg.getDataFlowInfo().getSSAUses(value); + } else { + for (OpOperand &use : value.getUses()) { + uses.push_back(&use); + } + } + + for (OpOperand *use : uses) { + Operation *userOp = use->getOwner(); + + if (visited.contains(userOp)) + continue; + + if (opts.stopOps.contains(userOp)) + continue; + + visited.insert(userOp); + + visitor.VisitUse(value, use, depth); + + // visit results of this operation + for (Value result : userOp->getResults()) { + dfsForwardImpl(result, visitor, visited, opts, depth + 1); + } + } +} + +//===----------------------------------------------------------------------===// +// Region Implementation +//===----------------------------------------------------------------------===// + +void Region::add(Instruction *inst) { instSet_.insert(inst); } + +void Region::add(Operation *op, ControlFlowGraph &cfg) { + if (Instruction *inst = cfg.getInstruction(op)) { + add(inst); + } +} + +void Region::addAll(ArrayRef insts) { + for (Instruction *inst : insts) { + add(inst); + } +} + +bool Region::contains(Instruction *inst) const { + return instSet_.contains(inst); +} + +bool Region::contains(Operation *op) const { + // note: requires cfg to be available - caller must ensure this + // use the version that takes cfg as parameter for proper lookup + return false; +} + +void Region::remove(Instruction *inst) { instSet_.erase(inst); } + +void Region::clear() { instSet_.clear(); } + +SmallVector Region::orderedInstructions() const { + SmallVector result(instSet_.begin(), instSet_.end()); + // sort by block id then instruction index + llvm::sort(result, [](Instruction *a, Instruction *b) { + auto *bbA = a->getParentBlock(); + auto *bbB = b->getParentBlock(); + if (bbA != bbB) + return bbA->getId() < bbB->getId(); + // within same block, find position + // this is approximate - exact order requires full block scan + return a->getId() < b->getId(); + }); + return result; +} + +SmallVector Region::operations() const { + SmallVector result; + for (Instruction *inst : instSet_) { + if (Operation *op = inst->getOperation()) { + result.push_back(op); + } + } + return result; +} + +//===----------------------------------------------------------------------===// +// RegionAnalyzer Implementation +//===----------------------------------------------------------------------===// + +bool RegionAnalyzer::hasDependency(const Region &from, const Region &to) const { + auto deps = getDependencies(from, to); + return !deps.empty(); +} + +SmallVector +RegionAnalyzer::getDependencies(const Region &from, const Region &to) const { + SmallVector deps; + + // check for data dependencies: from defines, to uses + for (Instruction *fromInst : from) { + for (Value result : fromInst->getOperation()->getResults()) { + for (OpOperand &use : result.getUses()) { + Operation *userOp = use.getOwner(); + if (Instruction *toInst = cfg.getInstruction(userOp)) { + if (to.contains(toInst)) { + deps.push_back({Dependency::DATA, result, fromInst, toInst}); + } + } + } + } + } + + return deps; +} + +RegionAnalyzer::ExternalDeps +RegionAnalyzer::analyzeExternalDeps(const Region ®ion) const { + ExternalDeps result; + + // find inputs: external definitions used inside region + for (Instruction *inst : region) { + for (Value operand : inst->getOperation()->getOperands()) { + Operation *defOp = operand.getDefiningOp(); + if (!defOp) { + // block argument - treat as external input + bool alreadyTracked = false; + for (auto &input : result.inputs) { + if (input.value == operand) { + input.internalUses.push_back(inst); + alreadyTracked = true; + break; + } + } + if (!alreadyTracked) { + result.inputs.push_back({operand, nullptr, {inst}}); + } + } else if (Instruction *defInst = cfg.getInstruction(defOp)) { + if (!region.contains(defInst)) { + // external definition + bool alreadyTracked = false; + for (auto &input : result.inputs) { + if (input.value == operand) { + input.internalUses.push_back(inst); + alreadyTracked = true; + break; + } + } + if (!alreadyTracked) { + result.inputs.push_back({operand, defInst, {inst}}); + } + } + } + } + } + + // find outputs: internal definitions used outside region + for (Instruction *inst : region) { + for (Value result : inst->getOperation()->getResults()) { + SmallVector externalUses; + for (OpOperand &use : result.getUses()) { + Operation *userOp = use.getOwner(); + if (Instruction *userInst = cfg.getInstruction(userOp)) { + if (!region.contains(userInst)) { + externalUses.push_back(userInst); + } + } + } + if (!externalUses.empty()) { + result.outputs.push_back({result, inst, externalUses}); + } + } + } + + return result; +} + +//===----------------------------------------------------------------------===// +// ProgramSlicer Implementation +//===----------------------------------------------------------------------===// + +ProgramSlice ProgramSlicer::compute(const SliceCriterion &criterion) { + ProgramSlice slice; + + DFGTraverser dfgTraverser(dfg); + + for (Value seed : criterion.seeds) { + class SliceBuilder : public DFGTraversalBase { + public: + SliceBuilder(ProgramSlice &slice, ControlFlowGraph &cfg) + : slice(slice), cfg(cfg) {} + + bool VisitDef(Value value, Operation *defOp, int depth) override { + if (Instruction *inst = cfg.getInstruction(defOp)) { + slice.add(inst); + } + return true; + } + + bool VisitUse(Value value, OpOperand *use, int depth) override { + Operation *userOp = use->getOwner(); + if (Instruction *inst = cfg.getInstruction(userOp)) { + slice.add(inst); + } + return true; + } + + private: + ProgramSlice &slice; + ControlFlowGraph &cfg; + }; + + SliceBuilder builder(slice, cfg); + + switch (criterion.dir) { + case SliceCriterion::BACKWARD: + dfgTraverser.dfsBackward(seed, builder, criterion.dfgOpts); + break; + case SliceCriterion::FORWARD: + dfgTraverser.dfsForward(seed, builder, criterion.dfgOpts); + break; + case SliceCriterion::BIDIRECTIONAL: + dfgTraverser.dfsBackward(seed, builder, criterion.dfgOpts); + dfgTraverser.dfsForward(seed, builder, criterion.dfgOpts); + break; + } + } + + return slice; +} + +ProgramSlice ProgramSlicer::sliceFromYields(ArrayRef yields, + SliceCriterion::Direction dir) { + SliceCriterion criterion; + criterion.seeds = SmallVector(yields); + criterion.dir = dir; + return compute(criterion); +} + +void ProgramSlice::merge(const ProgramSlice &other) { + for (Instruction *inst : other) { + instructions_.insert(inst); + } +} + +void ProgramSlice::intersect(const ProgramSlice &other) { + DenseSet toRemove; + for (Instruction *inst : instructions_) { + if (!other.contains(inst)) { + toRemove.insert(inst); + } + } + for (Instruction *inst : toRemove) { + instructions_.erase(inst); + } +} + +void ProgramSlice::subtract(const ProgramSlice &other) { + for (Instruction *inst : other) { + instructions_.erase(inst); + } +} + +Region ProgramSlice::toRegion(StringRef name) const { + Region region(name); + for (Instruction *inst : instructions_) { + region.add(inst); + } + return region; +} + +//===----------------------------------------------------------------------===// +// RegionAbsorber Implementation +//===----------------------------------------------------------------------===// + +void RegionAbsorber::absorb(Region ®ion, ArrayRef seeds, + const AbsorptionPolicy &policy) { + DenseSet visited; + + for (Instruction *seed : seeds) { + region.add(seed); + + if (policy.dir == AbsorptionPolicy::UPSTREAM || + policy.dir == AbsorptionPolicy::BOTH) { + absorbUpstream(region, seed, policy, visited, 0); + } + + if (policy.dir == AbsorptionPolicy::DOWNSTREAM || + policy.dir == AbsorptionPolicy::BOTH) { + absorbDownstream(region, seed, policy, visited, 0); + } + } +} + +void RegionAbsorber::absorbUpstream(Region ®ion, Instruction *inst, + const AbsorptionPolicy &policy, + DenseSet &visited, + int depth) { + if (policy.maxDepth >= 0 && depth > policy.maxDepth) + return; + + if (visited.contains(inst)) + return; + visited.insert(inst); + + if (policy.shouldStop && policy.shouldStop(inst)) + return; + + if (policy.stopOps.contains(inst->getOperation())) + return; + + // add to region + region.add(inst); + + // visit operands + for (Value operand : inst->getOperation()->getOperands()) { + if (Operation *defOp = operand.getDefiningOp()) { + if (Instruction *defInst = cfg.getInstruction(defOp)) { + if (!policy.crossRegionBoundary && region.contains(defInst)) + continue; + + absorbUpstream(region, defInst, policy, visited, depth + 1); + } + } + } +} + +void RegionAbsorber::absorbDownstream(Region ®ion, Instruction *inst, + const AbsorptionPolicy &policy, + DenseSet &visited, + int depth) { + if (policy.maxDepth >= 0 && depth > policy.maxDepth) + return; + + if (visited.contains(inst)) + return; + visited.insert(inst); + + if (policy.shouldStop && policy.shouldStop(inst)) + return; + + if (policy.stopOps.contains(inst->getOperation())) + return; + + // add to region + region.add(inst); + + // visit uses + for (Value result : inst->getOperation()->getResults()) { + for (OpOperand &use : result.getUses()) { + Operation *userOp = use.getOwner(); + if (Instruction *userInst = cfg.getInstruction(userOp)) { + if (!policy.crossRegionBoundary && region.contains(userInst)) + continue; + + absorbDownstream(region, userInst, policy, visited, depth + 1); + } + } + } +} + +void RegionAbsorber::absorbFromValue(Region ®ion, Value value, + const AbsorptionPolicy &policy) { + Operation *defOp = value.getDefiningOp(); + if (!defOp) + return; + + Instruction *inst = cfg.getInstruction(defOp); + if (!inst) + return; + + absorb(region, {inst}, policy); +} + +void RegionAbsorber::absorbUntilBoundary( + Region ®ion, ArrayRef seeds, + std::function isBoundary) { + AbsorptionPolicy policy; + policy.shouldStop = isBoundary; + absorb(region, seeds, policy); +} diff --git a/third_party/ascend/lib/TritonToGraph/GraphOptimizationContext.cpp b/third_party/ascend/lib/TritonToGraph/GraphOptimizationContext.cpp new file mode 100644 index 0000000000..a5dbd81f98 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/GraphOptimizationContext.cpp @@ -0,0 +1,165 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationContext.h" + +#include "TritonToGraph/AliasAnalysis.h" +#include "TritonToGraph/ControlFlowGraphBuilder.h" +#include "TritonToGraph/DataflowGraph.h" +#include "TritonToGraph/EntryArgPointerAliasAnalysis.h" +#include "TritonToGraph/ProgramAxisDependenceAnalysis.h" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +GraphOptimizationContext::GraphOptimizationContext(triton::FuncOp function, + ResourceSnapshot resources) + : function(function), resources(resources) { + assert(this->function.getOperation() && + "GraphOptimizationContext requires a valid tt.func"); +} + +GraphOptimizationContext::~GraphOptimizationContext() = default; + +LogicalResult +GraphOptimizationContext::ensure(AnalysisRequirement requirements) { + if (hasAnalysisRequirement(requirements, AnalysisRequirement::CFG) || + hasAnalysisRequirement(requirements, AnalysisRequirement::Alias) || + hasAnalysisRequirement(requirements, AnalysisRequirement::DataFlow) || + hasAnalysisRequirement(requirements, AnalysisRequirement::MemorySSA) || + hasAnalysisRequirement(requirements, + AnalysisRequirement::EntryArgPointerAlias)) { + if (failed(ensureControlFlowGraph())) + return failure(); + } + + if (hasAnalysisRequirement(requirements, + AnalysisRequirement::ProgramAxisDependence)) { + if (failed(ensureProgramAxisDependenceAnalysis())) + return failure(); + } + + if (hasAnalysisRequirement(requirements, AnalysisRequirement::Alias) || + hasAnalysisRequirement(requirements, AnalysisRequirement::DataFlow) || + hasAnalysisRequirement(requirements, AnalysisRequirement::MemorySSA) || + hasAnalysisRequirement(requirements, + AnalysisRequirement::EntryArgPointerAlias)) { + if (failed(ensureAliasAnalysis())) + return failure(); + } + + if (hasAnalysisRequirement(requirements, + AnalysisRequirement::EntryArgPointerAlias)) { + if (failed(ensureEntryArgPointerAliasAnalysis())) + return failure(); + } + + if (hasAnalysisRequirement(requirements, AnalysisRequirement::DataFlow) || + hasAnalysisRequirement(requirements, AnalysisRequirement::MemorySSA)) { + if (failed(ensureDataFlowGraph())) + return failure(); + } + + if (hasAnalysisRequirement(requirements, AnalysisRequirement::ResourceCost)) { + if (failed(ensureResourceCostAnalysis())) + return failure(); + } + + return success(); +} + +void GraphOptimizationContext::invalidate() { + resourceCostAnalysis.reset(); + programAxisDependenceAnalysis.reset(); + dataFlowGraph.reset(); + entryArgPointerAliasAnalysis.reset(); + aliasAnalysis.reset(); + controlFlowGraph.reset(); + ++epoch; +} + +LogicalResult GraphOptimizationContext::ensureControlFlowGraph() { + if (controlFlowGraph) + return success(); + if (!function.getOperation()) + return failure(); + + ControlFlowGraphBuilder builder; + controlFlowGraph = builder.build(function); + return controlFlowGraph ? success() : failure(); +} + +LogicalResult GraphOptimizationContext::ensureAliasAnalysis() { + if (failed(ensureControlFlowGraph())) + return failure(); + if (aliasAnalysis) + return success(); + + aliasAnalysis = std::make_unique(); + aliasAnalysis->analyzePointerAliases(*controlFlowGraph); + return success(); +} + +LogicalResult GraphOptimizationContext::ensureEntryArgPointerAliasAnalysis() { + if (failed(ensureAliasAnalysis())) + return failure(); + if (entryArgPointerAliasAnalysis) + return success(); + + entryArgPointerAliasAnalysis = + std::make_unique(function, *aliasAnalysis); + return success(); +} + +LogicalResult GraphOptimizationContext::ensureDataFlowGraph() { + if (failed(ensureAliasAnalysis())) + return failure(); + if (dataFlowGraph) + return success(); + + dataFlowGraph = + std::make_unique(*controlFlowGraph, *aliasAnalysis); + dataFlowGraph->build(); + return success(); +} + +LogicalResult GraphOptimizationContext::ensureProgramAxisDependenceAnalysis() { + if (programAxisDependenceAnalysis) + return success(); + if (!function.getOperation()) + return failure(); + programAxisDependenceAnalysis = + std::make_unique(function); + return success(); +} + +LogicalResult GraphOptimizationContext::ensureResourceCostAnalysis() { + if (resourceCostAnalysis) + return success(); + if (!function.getOperation()) + return failure(); + + resourceCostAnalysis = std::make_unique( + function.getOperation(), resources); + return success(); +} diff --git a/third_party/ascend/lib/TritonToGraph/GraphOptimizationPass.cpp b/third_party/ascend/lib/TritonToGraph/GraphOptimizationPass.cpp new file mode 100644 index 0000000000..dd7e8d4018 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/GraphOptimizationPass.cpp @@ -0,0 +1,573 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationContext.h" +#include "TritonToGraph/GraphOptimizationRule.h" +#include "TritonToGraph/Passes.h" +#include "TritonToGraph/ProgramGridTransform.h" +#include "TritonToGraph/ResourceCostModel.h" +#include "Utils/Utils.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/Passes.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +namespace mlir { +namespace triton { +namespace cfg { +#define GEN_PASS_DEF_GRAPHOPTIMIZE +#include "ascend/include/TritonToGraph/Passes.h.inc" +} // namespace cfg +} // namespace triton +} // namespace mlir + +namespace mlir { +namespace triton { +namespace cfg { +namespace { + +constexpr std::array kRulePhases = { + GraphOptimizationRulePhase::DiagonalMaskRemoval, + GraphOptimizationRulePhase::ConvertModuloToMask, + GraphOptimizationRulePhase::ProgramMapping, + GraphOptimizationRulePhase::PersistentTaskMapping, + GraphOptimizationRulePhase::LoadStoreTranspose, + GraphOptimizationRulePhase::TransposePointwiseReorder, + GraphOptimizationRulePhase::ResidentLoadForwarding, + GraphOptimizationRulePhase::IntermediatePrecisionBoundaryElision, + GraphOptimizationRulePhase::StoreCoveragePlanning, + GraphOptimizationRulePhase::StoreCoalescing, + GraphOptimizationRulePhase::ContiguousBlockAccessFormation, +}; + +using ProgramOrderMap = llvm::DenseMap; + +ProgramOrderMap buildProgramOrderMap(triton::FuncOp function) { + ProgramOrderMap programOrder; + unsigned nextOrder = 0; + function.walk( + [&](Operation *operation) { programOrder[operation] = nextOrder++; }); + return programOrder; +} + +unsigned getProgramOrder(const RewritePlan &plan, + const ProgramOrderMap &programOrder) { + Operation *anchor = plan.getAnchor(); + if (!anchor) + return std::numeric_limits::max(); + + auto it = programOrder.find(anchor); + if (it == programOrder.end()) + return std::numeric_limits::max(); + return it->second; +} + +bool isRuleEnabled(GraphOptimizationRuleMask ruleMask, + GraphOptimizationRuleId ruleId) { + return (ruleMask & getGraphOptimizationRuleMask(ruleId)) != 0; +} + +constexpr bool requiresProgramMappingCleanup(GraphOptimizationRuleId ruleId) { + return ruleId == GraphOptimizationRuleId::IndependentAxisTensorize || + ruleId == GraphOptimizationRuleId::PersistentTaskStripMining; +} + +constexpr bool isPlanHigherPriority(unsigned lhsBenefit, unsigned lhsOrder, + GraphOptimizationRuleId lhsRuleId, + unsigned rhsBenefit, unsigned rhsOrder, + GraphOptimizationRuleId rhsRuleId) { + if (lhsBenefit != rhsBenefit) + return lhsBenefit > rhsBenefit; + if (lhsOrder != rhsOrder) + return lhsOrder < rhsOrder; + return getGraphOptimizationRuleMask(lhsRuleId) < + getGraphOptimizationRuleMask(rhsRuleId); +} + +class GraphOptimizePass final + : public impl::GraphOptimizeBase { +public: + GraphOptimizePass() = default; + + explicit GraphOptimizePass(const GraphOptimizationOptions &options) { + this->ruleMask = options.enabledRuleMask; + this->maxRewritesPerFunction = options.maxRewritesPerFunction; + this->ubCapacityBytes = options.ubCapacityBytes; + this->mappingUBCapacityBytes = options.mappingUBCapacityBytes; + this->storeCoalescingUBBudgetBytes = options.storeCoalescingUBBudgetBytes; + this->ubSafetyPercent = options.ubSafetyPercent; + this->reservedUBBytes = options.reservedUBBytes; + this->compileMode = options.compileMode; + } + + void getDependentDialects(DialectRegistry ®istry) const override { + registry.insert(); + } + + void runOnOperation() override; + +private: + LogicalResult getStableOptions(GraphOptimizationOptions &options); + LogicalResult runStructuralCleanup(); +}; + +LogicalResult GraphOptimizePass::runStructuralCleanup() { + PassManager cleanup(&getContext(), getOperation().getOperationName()); + cleanup.addPass(createCanonicalizerPass()); + cleanup.addPass(createCSEPass()); + cleanup.addPass(createLoopInvariantCodeMotionPass()); + cleanup.addPass(createCanonicalizerPass()); + cleanup.addPass(createCSEPass()); + return runPipeline(cleanup, getOperation()); +} + +LogicalResult +GraphOptimizePass::getStableOptions(GraphOptimizationOptions &options) { + const uint64_t cliRuleMask = this->ruleMask; + const uint64_t cliMaxRewrites = this->maxRewritesPerFunction; + const uint64_t cliUBCapacityBytes = this->ubCapacityBytes; + const uint64_t cliMappingUBCapacityBytes = this->mappingUBCapacityBytes; + const uint64_t cliStoreCoalescingUBBudgetBytes = + this->storeCoalescingUBBudgetBytes; + const uint64_t cliUBSafetyPercent = this->ubSafetyPercent; + const uint64_t cliReservedUBBytes = this->reservedUBBytes; + + if (cliRuleMask > std::numeric_limits::max() || + !isValidGraphOptimizationRuleMask( + static_cast(cliRuleMask))) { + getOperation().emitError() + << "graph-optimize rule-mask contains unknown or out-of-range bits: " + << cliRuleMask; + return failure(); + } + + if (cliMaxRewrites > std::numeric_limits::max()) { + getOperation().emitError() + << "graph-optimize max-rewrites-per-function is out of range: " + << cliMaxRewrites; + return failure(); + } + + if (cliUBCapacityBytes > std::numeric_limits::max()) { + getOperation().emitError() + << "graph-optimize ub-capacity-bytes is out of range: " + << cliUBCapacityBytes; + return failure(); + } + + if (cliMappingUBCapacityBytes > std::numeric_limits::max() || + cliStoreCoalescingUBBudgetBytes > std::numeric_limits::max()) { + getOperation().emitError() + << "graph-optimize explicit UB budget is out of range: mapping=" + << cliMappingUBCapacityBytes + << " store-coalescing=" << cliStoreCoalescingUBBudgetBytes; + return failure(); + } + + const uint64_t effectiveMappingUBCapacity = cliMappingUBCapacityBytes != 0 + ? cliMappingUBCapacityBytes + : cliUBCapacityBytes; + const uint64_t effectiveStoreCoalescingUBBudget = + cliStoreCoalescingUBBudgetBytes != 0 ? cliStoreCoalescingUBBudgetBytes + : cliUBCapacityBytes; + + if (cliUBSafetyPercent == 0 || cliUBSafetyPercent > 100 || + cliReservedUBBytes > std::numeric_limits::max() || + cliReservedUBBytes > effectiveMappingUBCapacity) { + getOperation().emitError() + << "graph-optimize resource options are invalid: ub-safety-percent=" + << cliUBSafetyPercent << " reserved-ub-bytes=" << cliReservedUBBytes; + return failure(); + } + + const auto compileMode = triton::ascend::parseCompileMode(this->compileMode); + if (!compileMode) { + getOperation().emitError() + << "graph-optimize compile-mode is invalid: " << this->compileMode; + return failure(); + } + + options.enabledRuleMask = static_cast(cliRuleMask); + options.maxRewritesPerFunction = static_cast(cliMaxRewrites); + options.ubCapacityBytes = + static_cast(effectiveStoreCoalescingUBBudget); + options.mappingUBCapacityBytes = + static_cast(effectiveMappingUBCapacity); + options.storeCoalescingUBBudgetBytes = + static_cast(effectiveStoreCoalescingUBBudget); + options.ubSafetyPercent = static_cast(cliUBSafetyPercent); + options.reservedUBBytes = static_cast(cliReservedUBBytes); + options.compileMode = this->compileMode; + options.independentAxisTensorize.enabledForCompileMode = + *compileMode != triton::ascend::CompileMode::SimtOnly; + options.independentAxisTensorize.iatAndPtsmEnabled = + isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::IndependentAxisTensorize) && + isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::PersistentTaskStripMining); + options.persistentTaskStripMining.enabledForCompileMode = + *compileMode != triton::ascend::CompileMode::SimtOnly; + options.storeCoalescing.enabledForCompileMode = + *compileMode != triton::ascend::CompileMode::SimtOnly; + return success(); +} + +void GraphOptimizePass::runOnOperation() { + GraphOptimizationOptions options; + if (failed(getStableOptions(options))) { + signalPassFailure(); + return; + } + + ModuleOp module = getOperation(); + SmallVector> ownedRules; + populateBuiltinGraphOptimizationRules(options, ownedRules); + + SmallVector enabledRules; + llvm::DenseSet registeredRuleMasks; + for (const std::unique_ptr &rule : ownedRules) { + if (!rule) { + getOperation().emitError() + << "graph-optimize builtin rule factory returned null"; + signalPassFailure(); + return; + } + + const GraphOptimizationRuleMask ruleMask = + getGraphOptimizationRuleMask(rule->getId()); + if (ruleMask == 0 || !isValidGraphOptimizationRuleMask(ruleMask) || + !isRuleEnabled(options.enabledRuleMask, rule->getId()) || + !registeredRuleMasks.insert(ruleMask).second) { + getOperation().emitError() + << "graph-optimize registered an invalid, disabled, or duplicate " + "rule ID: " + << ruleMask; + signalPassFailure(); + return; + } + enabledRules.push_back(rule.get()); + } + + for (triton::FuncOp function : module.getOps()) { + const ResourceSnapshot resources = ResourceSnapshot::fromExplicit( + options.mappingUBCapacityBytes, options.ubSafetyPercent, + options.reservedUBBytes); + GraphOptimizationContext context(function, resources); + unsigned rewriteCount = 0; + + for (GraphOptimizationRulePhase phase : kRulePhases) { + while (rewriteCount < options.maxRewritesPerFunction) { + ProgramOrderMap programOrder = buildProgramOrderMap(function); + SmallVector> plans; + + for (GraphOptimizationRule *rule : enabledRules) { + if (getGraphOptimizationRulePhase(rule->getId()) != phase) + continue; + + if (failed(context.ensure(rule->getAnalysisRequirements()))) { + function.emitError() << "graph-optimize failed to build analyses"; + signalPassFailure(); + return; + } + + if (failed(rule->findCandidates(context, plans))) { + function.emitError() + << "graph-optimize failed to discover rewrite candidates"; + signalPassFailure(); + return; + } + } + + plans.erase( + std::remove_if(plans.begin(), plans.end(), + [phase](const std::unique_ptr &plan) { + return !plan || getGraphOptimizationRulePhase( + plan->getRuleId()) != phase; + }), + plans.end()); + if (plans.empty()) + break; + + std::stable_sort( + plans.begin(), plans.end(), + [&programOrder](const std::unique_ptr &lhs, + const std::unique_ptr &rhs) { + const unsigned lhsOrder = getProgramOrder(*lhs, programOrder); + const unsigned rhsOrder = getProgramOrder(*rhs, programOrder); + return isPlanHigherPriority(lhs->getBenefit(), lhsOrder, + lhs->getRuleId(), rhs->getBenefit(), + rhsOrder, rhs->getRuleId()); + }); + + std::unique_ptr selectedPlan; + for (std::unique_ptr &plan : plans) { + if (plan->getCreationEpoch() != context.getEpoch()) + continue; + if (failed(plan->revalidate(context))) { + LLVM_DEBUG( + llvm::dbgs() + << "[" DEBUG_TYPE "] dropped stale graph optimization rule " + << getGraphOptimizationRuleMask(plan->getRuleId()) << " (" + << getGraphOptimizationRuleName(plan->getRuleId()) << ")\n"); + continue; + } + + selectedPlan = std::move(plan); + break; + } + + if (!selectedPlan) { + plans.clear(); + break; + } + + const GraphOptimizationRuleId appliedRuleId = selectedPlan->getRuleId(); + IRRewriter rewriter(&getContext()); + if (failed(selectedPlan->apply(rewriter))) { + selectedPlan.reset(); + plans.clear(); + function.emitError() << "graph-optimize failed to apply rewrite"; + signalPassFailure(); + return; + } + if (requiresProgramMappingCleanup(appliedRuleId) && + failed(runStructuralCleanup())) { + selectedPlan.reset(); + plans.clear(); + function.emitError() + << "graph-optimize failed structural rewrite cleanup"; + signalPassFailure(); + return; + } + + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] applied graph optimization rule " + << getGraphOptimizationRuleMask(appliedRuleId) << " (" + << getGraphOptimizationRuleName(appliedRuleId) << ")\n"); + + // Plans can retain pointers into analysis results, so destroy all of + // them before invalidating the context for the next IR epoch. + selectedPlan.reset(); + plans.clear(); + context.invalidate(); + ++rewriteCount; + } + + if (rewriteCount == options.maxRewritesPerFunction) + break; + } + + // RowCoalescing has the same function-local candidate/rewrite interface + // as the native graph rules, but it has distinct launch semantics. Its + // historical pass ran once and was not subject to the generic rewrite + // budget, so run it once after LoadStoreTranspose, + // TransposePointwiseReorder, and StoreCoalescing even when that budget + // has already been exhausted. + if (!isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::RowCoalescing)) + continue; + if (module->hasAttr(kProgramGridTransformsAttr)) + continue; + + GraphOptimizationRule *rowRule = nullptr; + for (GraphOptimizationRule *rule : enabledRules) { + if (rule->getId() == GraphOptimizationRuleId::RowCoalescing) { + rowRule = rule; + break; + } + } + if (!rowRule) + continue; + + if (failed(context.ensure(rowRule->getAnalysisRequirements()))) { + function.emitError() << "graph-optimize failed to build Row analyses"; + signalPassFailure(); + return; + } + + SmallVector> rowPlans; + if (failed(rowRule->findCandidates(context, rowPlans))) { + function.emitError() << "graph-optimize failed to discover Row candidate"; + signalPassFailure(); + return; + } + rowPlans.erase( + std::remove_if(rowPlans.begin(), rowPlans.end(), + [](const std::unique_ptr &plan) { + return !plan || + plan->getRuleId() != + GraphOptimizationRuleId::RowCoalescing; + }), + rowPlans.end()); + if (rowPlans.empty()) + continue; + + ProgramOrderMap programOrder = buildProgramOrderMap(function); + std::stable_sort( + rowPlans.begin(), rowPlans.end(), + [&programOrder](const std::unique_ptr &lhs, + const std::unique_ptr &rhs) { + const unsigned lhsOrder = getProgramOrder(*lhs, programOrder); + const unsigned rhsOrder = getProgramOrder(*rhs, programOrder); + return isPlanHigherPriority(lhs->getBenefit(), lhsOrder, + lhs->getRuleId(), rhs->getBenefit(), + rhsOrder, rhs->getRuleId()); + }); + + std::unique_ptr selectedRowPlan; + for (std::unique_ptr &plan : rowPlans) { + if (plan->getCreationEpoch() != context.getEpoch()) + continue; + if (failed(plan->revalidate(context))) + continue; + selectedRowPlan = std::move(plan); + break; + } + if (!selectedRowPlan) + continue; + + IRRewriter rewriter(&getContext()); + switch (selectedRowPlan->applyWithResult(rewriter)) { + case RewritePlanApplyResult::Applied: + break; + case RewritePlanApplyResult::NotApplicable: + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE << "] declined graph optimization rule " + << static_cast( + GraphOptimizationRuleId::RowCoalescing) + << " (" + << getGraphOptimizationRuleName( + GraphOptimizationRuleId::RowCoalescing) + << ") after detached materialization\n"); + selectedRowPlan.reset(); + rowPlans.clear(); + continue; + case RewritePlanApplyResult::Failed: + selectedRowPlan.reset(); + rowPlans.clear(); + function.emitError() << "graph-optimize failed to apply Row rewrite"; + signalPassFailure(); + return; + } + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] applied graph optimization rule " + << getGraphOptimizationRuleMask( + GraphOptimizationRuleId::RowCoalescing) + << " (" + << getGraphOptimizationRuleName( + GraphOptimizationRuleId::RowCoalescing) + << ")\n"); + + selectedRowPlan.reset(); + rowPlans.clear(); + context.invalidate(); + } +} + +} // namespace + +void populateBuiltinGraphOptimizationRules( + const GraphOptimizationOptions &options, + SmallVectorImpl> &rules) { + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::DiagonalMaskRemoval)) { + rules.push_back(createDiagonalMaskRemovalRule()); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::ConvertModuloToMask)) { + rules.push_back(createConvertModuloToMaskRule()); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::IndependentAxisTensorize)) { + rules.push_back( + createIndependentAxisTensorizeRule(options.independentAxisTensorize)); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::PersistentTaskStripMining)) { + rules.push_back( + createPersistentTaskStripMiningRule(options.persistentTaskStripMining)); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::LoadStoreTranspose)) { + rules.push_back(createLoadStoreTransposeRule()); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::TransposePointwiseReorder)) { + rules.push_back(createTransposePointwiseReorderRule()); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::StoreCoalescing)) { + rules.push_back(createStoreCoalescingRule( + options.storeCoalescingUBBudgetBytes, options.storeCoalescing)); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::ResidentLoadForwarding)) { + rules.push_back( + createResidentLoadForwardingRule(options.residentLoadForwarding)); + } + if (isRuleEnabled( + options.enabledRuleMask, + GraphOptimizationRuleId::IntermediatePrecisionBoundaryElision)) { + rules.push_back(createIntermediatePrecisionBoundaryElisionRule( + options.intermediatePrecisionBoundaryElision)); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::StoreCoveragePlanning)) { + rules.push_back( + createStoreCoveragePlanningRule(options.storeCoveragePlanning)); + } + if (isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::ContiguousBlockAccessFormation)) { + rules.push_back(createContiguousBlockAccessFormationRule( + options.contiguousBlockAccessFormation)); + } + const auto compileMode = + triton::ascend::parseCompileMode(options.compileMode); + if (compileMode && isRuleEnabled(options.enabledRuleMask, + GraphOptimizationRuleId::RowCoalescing)) { + rules.push_back(createRowCoalescingRule( + *compileMode == triton::ascend::CompileMode::SimtOnly)); + } +} + +std::unique_ptr> +createGraphOptimizePass(GraphOptimizationOptions options) { + return std::make_unique(options); +} + +} // namespace cfg +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToGraph/InterProceduralCFG.cpp b/third_party/ascend/lib/TritonToGraph/InterProceduralCFG.cpp new file mode 100644 index 0000000000..e9f8320e48 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/InterProceduralCFG.cpp @@ -0,0 +1,351 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/InterProceduralCFG.h" +#include "TritonToGraph/ControlFlowGraphBuilder.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/raw_ostream.h" + +#define DEBUG_TYPE "icfg" + +using namespace mlir; +using namespace mlir::triton; + +namespace mlir { +namespace triton { +namespace cfg { + +InterProceduralCFG::InterProceduralCFG(ModuleOp module) : module(module) {} + +InterProceduralCFG::~InterProceduralCFG() = default; + +void InterProceduralCFG::build() { + LLVM_DEBUG(llvm::dbgs() << "Building ICFG for module\n"); + + ControlFlowGraphBuilder builder; + + // 遍历模块中的所有函数 + for (triton::FuncOp func : module.getOps()) { + LLVM_DEBUG(llvm::dbgs() + << "Building CFG for function: " << func.getName() << "\n"); + + // 为每个函数构建CFG + auto cfg = builder.build(func); + functionCFGs[func] = std::move(cfg); + } +} + +ControlFlowGraph *InterProceduralCFG::getFunctionCFG(triton::FuncOp func) { + auto it = functionCFGs.find(func); + if (it != functionCFGs.end()) { + return it->second.get(); + } + return nullptr; +} + +const ControlFlowGraph * +InterProceduralCFG::getFunctionCFG(triton::FuncOp func) const { + auto it = functionCFGs.find(func); + if (it != functionCFGs.end()) { + return it->second.get(); + } + return nullptr; +} + +ControlFlowGraph *InterProceduralCFG::getFunctionCFG(StringRef funcName) { + for (auto &[func, cfg] : functionCFGs) { + if (func.getName() == funcName) { + return cfg.get(); + } + } + return nullptr; +} + +void InterProceduralCFG::connectCallGraph() { + LLVM_DEBUG(llvm::dbgs() << "Connecting call graph\n"); + + // 遍历所有操作,查找调用点 + module.walk([&](Operation *op) { + // 检查是否是调用操作 + if (auto callInterface = dyn_cast(op)) { + // 尝试获取被调用函数 + auto callable = callInterface.getCallableForCallee(); + + // 从符号引用获取函数名 + StringRef calleeName; + if (auto symbolRef = callable.dyn_cast()) { + calleeName = symbolRef.getRootReference(); + } else if (auto flatSymbolRef = + op->getAttrOfType("callee")) { + calleeName = flatSymbolRef.getValue(); + } + + if (!calleeName.empty()) { + // 在模块中查找被调用函数 + if (auto calleeFunc = module.lookupSymbol(calleeName)) { + // 获取调用者的函数 + Operation *parentOp = op->getParentOp(); + while (parentOp && !isa(parentOp)) { + parentOp = parentOp->getParentOp(); + } + + if (auto callerFunc = dyn_cast(parentOp)) { + auto callerCFG = getFunctionCFG(callerFunc); + + if (callerCFG) { + // 找到调用点的基本块 + BasicBlock *callBlock = nullptr; + Instruction *callInst = nullptr; + + // 遍历查找包含该操作的 basic block 和 instruction + for (size_t i = 0; i < callerCFG->getNumBlocks(); ++i) { + auto *bb = callerCFG->getBasicBlock(i); + for (size_t j = 0; j < bb->getNumInstructions(); ++j) { + auto *inst = bb->getInstruction(j); + if (inst->getOperation() == op) { + callBlock = bb; + callInst = inst; + break; + } + } + if (callBlock) + break; + } + + if (callBlock) { + // 创建调用点记录 + CallSite callSite; + callSite.callOp = op; + callSite.caller = callerFunc; + callSite.callee = calleeFunc; + callSite.callBlock = callBlock; + callSite.callInst = callInst; + + callSites.push_back(callSite); + + // 添加到调用图 + callGraph[callerFunc].push_back(calleeFunc); + reverseCallGraph[calleeFunc].push_back(callerFunc); + + LLVM_DEBUG(llvm::dbgs() + << "Found call: " << callerFunc.getName() << " -> " + << calleeFunc.getName() << "\n"); + } + } + } + } + } + } + }); +} + +SmallVector +InterProceduralCFG::getCallees(triton::FuncOp caller) const { + auto it = callGraph.find(caller); + if (it != callGraph.end()) { + return it->second; + } + return SmallVector(); +} + +SmallVector +InterProceduralCFG::getCallers(triton::FuncOp callee) const { + auto it = reverseCallGraph.find(callee); + if (it != reverseCallGraph.end()) { + return it->second; + } + return SmallVector(); +} + +void InterProceduralCFG::computeReachability() { + LLVM_DEBUG(llvm::dbgs() << "Computing reachability\n"); + + // 使用简单的递归DFS计算可达性 + std::function &)> dfs = + [&](triton::FuncOp func, DenseSet &visited) { + if (visited.contains(func)) { + return; + } + visited.insert(func); + + for (auto callee : getCallees(func)) { + reachability[func].insert(callee); + dfs(callee, reachability[func]); + } + }; + + // 对每个函数计算可达性 + for (auto &[func, cfg] : functionCFGs) { + reachability[func].clear(); + dfs(func, reachability[func]); + } +} + +bool InterProceduralCFG::isReachable(triton::FuncOp from, + triton::FuncOp to) const { + auto it = reachability.find(from); + if (it != reachability.end()) { + return it->second.contains(to); + } + return false; +} + +void InterProceduralCFG::dumpToDot(const std::string &filename) const { + std::error_code ec; + llvm::raw_fd_ostream file(filename, ec); + + if (ec) { + llvm::errs() << "Failed to open file: " << filename << "\n"; + return; + } + + file << "digraph ICFG {\n"; + file << " node [shape=box];\n\n"; + + // 输出所有函数节点 + for (const auto &[func, cfg] : functionCFGs) { + StringRef name = const_cast(func).getName(); + file << " \"" << name << "\" [label=\"" << name << "\"];\n"; + } + + file << "\n"; + + // 输出调用边 + for (const auto &[caller, callees] : callGraph) { + StringRef callerName = const_cast(caller).getName(); + for (auto callee : callees) { + StringRef calleeName = const_cast(callee).getName(); + file << " \"" << callerName << "\" -> \"" << calleeName + << "\" [label=\"call\"];\n"; + } + } + + file << "}\n"; +} + +void InterProceduralCFG::print(raw_ostream &os) const { + os << "InterProcedural Control Flow Graph:\n"; + os << "Functions: " << functionCFGs.size() << "\n"; + os << "CallSites: " << callSites.size() << "\n"; + + os << "\nCall Graph:\n"; + for (const auto &[caller, callees] : callGraph) { + StringRef callerName = const_cast(caller).getName(); + os << " " << callerName << " -> "; + for (auto callee : callees) { + StringRef calleeName = const_cast(callee).getName(); + os << calleeName << " "; + } + os << "\n"; + } +} + +llvm::Error +InterProceduralCFG::exportToHTML(const std::string &filename) const { + std::error_code ec; + llvm::raw_fd_ostream os(filename, ec, llvm::sys::fs::OF_Text); + + if (ec) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Failed to open file: " + filename); + } + + os << R"html( + + + + Inter-Procedural CFG + + + + +

Inter-Procedural CFG

+
+ + + +)html"; + + os.close(); + return llvm::Error::success(); +} + +} // namespace cfg +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToGraph/LayoutMemoryCompatibilityPass.cpp b/third_party/ascend/lib/TritonToGraph/LayoutMemoryCompatibilityPass.cpp new file mode 100644 index 0000000000..2071bdecff --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/LayoutMemoryCompatibilityPass.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimization.h" +#include "TritonToGraph/LayoutMemoryOptimization.h" +#include "TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.h" +#include "TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.h" +#include "TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.h" + +#include "Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +namespace mlir { +namespace triton { +namespace cfg { +namespace { + +static void emitAppliedGraphOptimizationRule(GraphOptimizationRuleId rule) { + LLVM_DEBUG(llvm::dbgs() << "[" DEBUG_TYPE "] applied graph optimization rule " + << static_cast(rule) << " (" + << getGraphOptimizationRuleName(rule) << ")\n"); +} + +static std::size_t countStridedLoadStoreRewrites(ModuleOp moduleOp) { + std::size_t count = 0; + moduleOp.walk([&](Operation *op) { + if (op->hasAttr( + StridedLoadStoreRewrite::RewrittenByStridedLoadStoreRewriteTAG)) + ++count; + }); + return count; +} + +class LayoutMemoryCompatibilityPass final + : public PassWrapper> { +public: + explicit LayoutMemoryCompatibilityPass(LayoutMemoryCompatibilityPhase phase) + : phase(phase) {} + + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(LayoutMemoryCompatibilityPass) + + void getDependentDialects(DialectRegistry ®istry) const override { + registry + .insert(); + } + + void runOnOperation() override { + ModuleOp moduleOp = getOperation(); + bool wasCoalesced = false; + switch (phase) { + case LayoutMemoryCompatibilityPhase::BeforeDiagonal: + // Preserve the original module-wide, no-op-on-bailout semantics. This + // phase intentionally has no cleanup pass. + wasCoalesced = moduleOp->hasAttr("hacc.coalesce_factor"); + StridedAxisCoalescing::rewriteStridedAxisCoalesce(moduleOp); + if (!wasCoalesced && moduleOp->hasAttr("hacc.coalesce_factor")) + emitAppliedGraphOptimizationRule( + GraphOptimizationRuleId::StridedAxisCoalescing); + return; + case LayoutMemoryCompatibilityPhase::AfterDiagonal: + // Keep the legacy order. Chunk sees Axis's module attr and consumes the + // static-grid hint before the greedy StridedLoadStore patterns run. + wasCoalesced = moduleOp->hasAttr("hacc.coalesce_factor"); + ChunkCoalescing::rewriteChunkCoalesce(moduleOp); + if (!wasCoalesced && moduleOp->hasAttr("hacc.coalesce_factor")) + emitAppliedGraphOptimizationRule( + GraphOptimizationRuleId::ChunkCoalescing); + break; + } + + // Count rewrite tags only while this DEBUG_TYPE is active. The tag lets us + // emit one pass-level match line even when several load/store ops rewrite. + bool emitStridedLoadStoreMatch = false; + LLVM_DEBUG(emitStridedLoadStoreMatch = true;); + const std::size_t stridedLoadStoreRewritesBefore = + emitStridedLoadStoreMatch ? countStridedLoadStoreRewrites(moduleOp) : 0; + RewritePatternSet patterns(&getContext()); + patterns.add( + patterns.getContext()); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + signalPassFailure(); + return; + } + if (emitStridedLoadStoreMatch && countStridedLoadStoreRewrites(moduleOp) > + stridedLoadStoreRewritesBefore) + emitAppliedGraphOptimizationRule( + GraphOptimizationRuleId::StridedLoadStoreRewrite); + + // CSE/Canonicalizer deliberately remain in TritonToLinalg at the original + // call site. PtrAnalysis may create helper IR even on an SLS no-op, and + // the original T2L pipeline owns the unconditional cleanup timing. + } + +private: + LayoutMemoryCompatibilityPhase phase; +}; + +} // namespace + +std::unique_ptr> +createLayoutMemoryCompatibilityPass(LayoutMemoryCompatibilityPhase phase) { + return std::make_unique(phase); +} + +} // namespace cfg +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.cpp b/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.cpp new file mode 100644 index 0000000000..e9c257647d --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.cpp @@ -0,0 +1,563 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.h" + +#include "TritonToGraph/GraphOptimization.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "graph-optimize" + +#include +#include +#include + +namespace ChunkCoalescing { + +using namespace mlir; +using namespace triton; + +namespace { + +constexpr int64_t kMinContigBytes = 512; +constexpr int64_t kUBBytesBudget = 128 * 1024; +constexpr int64_t kMaxCoalesceTilesCeil = 16; + +constexpr llvm::StringLiteral kCoalesceFactorAttr = "hacc.coalesce_factor"; +constexpr llvm::StringLiteral kCoalesceAxisAttr = "hacc.coalesce_axis"; +constexpr llvm::StringLiteral kGridNumTilesAttr = "hacc.grid_num_tiles"; + +struct TileSeed { + triton::GetProgramIdOp pid; + int32_t axis = 0; + int64_t tileLen = 0; + int64_t bound = 0; + Value mask; +}; + +static bool getConstInt(Value v, int64_t &out) { + APInt ap; + if (matchPattern(v, m_ConstantInt(&ap))) { + out = ap.getSExtValue(); + return true; + } + DenseElementsAttr dea; + if (matchPattern(v, m_Constant(&dea)) && dea.isSplat() && + isa(dea.getElementType())) { + out = dea.getSplatValue().getSExtValue(); + return true; + } + return false; +} + +// tt.assert is liftable: a group covers exactly H tiles of the original grid, +// so a widened condition traps on the same inputs, only grouped differently. +static bool isLiftable(Operation *op) { + if (auto *d = op->getDialect()) { + StringRef ns = d->getNamespace(); + if (ns == arith::ArithDialect::getDialectNamespace() || + ns == math::MathDialect::getDialectNamespace()) + return true; + } + return isa(op); +} + +// A pid-dependent predicate that can only trap does not constrain the rewrite: +// tt.assert has no results, so nothing reaches an offset, a mask or a select. +// The permitted sink is whitelisted rather than the forbidden ones blacklisted, +// because the rewrite deletes the seed mask outright and so must fail closed. +// sanitize_overflow is what makes this matter: it is on by default and +// instruments nearly every pid-derived index. +static bool onlyFeedsDeviceAssert(Value predicate) { + SmallVector wl{predicate}; + DenseSet visited{predicate}; + while (!wl.empty()) { + Value cur = wl.pop_back_val(); + for (Operation *u : cur.getUsers()) { + if (isa(u)) + continue; + // Only the instrumentation's own boolean combines may extend the chain. + if (!isa(u)) + return false; + for (Value r : u->getResults()) + if (visited.insert(r).second) + wl.push_back(r); + } + } + return true; +} + +static std::optional findSeed(ModuleOp moduleOp) { + int32_t maxAxis = -1; + moduleOp.walk([&](triton::GetProgramIdOp pid) { + maxAxis = std::max(maxAxis, pid.getAxisAsInt()); + }); + + int32_t maxAxisPids = 0; + moduleOp.walk([&](triton::GetProgramIdOp pid) { + if (pid.getAxisAsInt() == maxAxis) + ++maxAxisPids; + }); + if (maxAxisPids != 1) + return std::nullopt; + + bool readsMaxAxisNumPrograms = false; + moduleOp.walk([&](triton::GetNumProgramsOp np) { + if (np.getAxisAsInt() == maxAxis) + readsMaxAxisNumPrograms = true; + }); + if (readsMaxAxisNumPrograms) + return std::nullopt; + + std::optional result; + moduleOp.walk([&](triton::GetProgramIdOp pid) { + if (result || pid.getAxisAsInt() != maxAxis) + return; + for (Operation *u : pid.getResult().getUsers()) { + auto mul = dyn_cast(u); + if (!mul) + continue; + Value other = + (mul.getLhs() == pid.getResult()) ? mul.getRhs() : mul.getLhs(); + int64_t T = 0; + if (!getConstInt(other, T) || T <= 1) + continue; + + for (Operation *mu : mul.getResult().getUsers()) { + auto sp = dyn_cast(mu); + if (!sp) + continue; + for (Operation *su : sp.getResult().getUsers()) { + auto add = dyn_cast(su); + if (!add) + continue; + Value rangeV = + (add.getLhs() == sp.getResult()) ? add.getRhs() : add.getLhs(); + auto range = rangeV.getDefiningOp(); + if (!range || range.getStart() != 0 || range.getEnd() != T) + continue; + + int64_t bound = 0; + Value mask; + bool unsafe = false; + DenseSet taint; + SmallVector twl; + taint.insert(pid.getResult()); + twl.push_back(pid.getResult()); + while (!twl.empty() && !unsafe) { + Value cur = twl.pop_back_val(); + for (Operation *tu : cur.getUsers()) { + if (auto cmp = dyn_cast(tu)) { + if (mask && cmp.getResult() == mask) + continue; + int64_t b = 0; + if (!mask && cur == add.getResult() && cmp.getLhs() == cur && + cmp.getPredicate() == arith::CmpIPredicate::slt && + getConstInt(cmp.getRhs(), b) && b >= T && b % T == 0) { + bound = b; + mask = cmp.getResult(); + continue; + } + if (onlyFeedsDeviceAssert(cmp.getResult())) + continue; + unsafe = true; + break; + } + if (isa(tu)) { + unsafe = true; + break; + } + bool propagates = isa(tu); + if (auto *d = tu->getDialect()) + propagates |= d->getNamespace() == + arith::ArithDialect::getDialectNamespace(); + if (!propagates) + continue; + for (Value r : tu->getResults()) + if (taint.insert(r).second) + twl.push_back(r); + } + } + if (unsafe) + return; + if (!mask) { + auto hintAttr = + moduleOp->getAttrOfType(kGridNumTilesAttr); + if (!hintAttr) + return; + int64_t numTiles = hintAttr.getInt(); + if (numTiles <= 0) + return; + bound = numTiles * T; + result = TileSeed{pid, maxAxis, T, bound, /*mask=*/Value()}; + return; + } + + result = TileSeed{pid, maxAxis, T, bound, mask}; + return; + } + } + } + }); + return result; +} + +static bool collectRegion(TileSeed &seed, ModuleOp moduleOp, + DenseSet ®ion, + SmallVectorImpl &ordered) { + SmallVector wl; + DenseSet visited; + bool hasStore = false; + for (Operation *u : seed.pid.getResult().getUsers()) + wl.push_back(u); + while (!wl.empty()) { + Operation *op = wl.pop_back_val(); + if (!visited.insert(op).second) + continue; + if (!isLiftable(op)) + return false; + if (isa(op)) + hasStore = true; + region.insert(op); + for (Value r : op->getResults()) + for (Operation *u : r.getUsers()) + wl.push_back(u); + } + if (!hasStore) + return false; + + for (Operation *op : region) { + if (isa(op)) + continue; + for (Value r : op->getResults()) + for (Operation *u : r.getUsers()) + if (!region.count(u)) + return false; + } + + moduleOp.walk([&](Operation *op) { + if (region.count(op)) + ordered.push_back(op); + }); + return true; +} + +static int64_t chooseH(int64_t numTiles, int64_t tileLen, int64_t elemBytes, + int64_t maxH) { + int64_t blockBytes = tileLen * elemBytes; + int64_t hMin = (kMinContigBytes + blockBytes - 1) / blockBytes; + if (hMin < 2) + hMin = 2; + if (maxH < hMin) + return 0; + if (numTiles <= 0) + return 0; + + for (int64_t c = maxH; c >= hMin; --c) + if (numTiles % c == 0) + return c; + return 0; +} + +static void rewriteModule(ModuleOp moduleOp, IRRewriter &rw) { + if (moduleOp->hasAttr(kCoalesceFactorAttr)) + return; + + auto seed = findSeed(moduleOp); + if (!seed) + return; + + DenseSet region; + SmallVector ordered; + if (!collectRegion(*seed, moduleOp, region, ordered)) + return; + + int64_t elemBytes = 0; + for (Operation *op : ordered) + if (auto ld = dyn_cast(op)) { + auto rt = dyn_cast(ld.getResult().getType()); + if (rt) + elemBytes = rt.getElementTypeBitWidth() / 8; + break; + } + if (elemBytes == 0) + return; + + auto tensorBytes = [](Type t) -> int64_t { + auto rt = dyn_cast(t); + if (!rt) + return 0; + Type et = rt.getElementType(); + if (!et.isIntOrFloat()) + return 0; + return rt.getNumElements() * ((et.getIntOrFloatBitWidth() + 7) / 8); + }; + int64_t footprintUnit = 0; + for (Operation *op : ordered) { + if (auto ld = dyn_cast(op)) + footprintUnit += tensorBytes(ld.getResult().getType()); + else if (auto st = dyn_cast(op)) + footprintUnit += tensorBytes(st.getValue().getType()); + else + for (Type t : op->getResultTypes()) + if (auto rt = dyn_cast(t)) + if (isa(rt.getElementType())) + footprintUnit += tensorBytes(t); + } + int64_t maxH = kMaxCoalesceTilesCeil; + if (footprintUnit > 0) + maxH = std::min(maxH, kUBBytesBudget / footprintUnit); + if (maxH < 2) + return; // even H=2 would overflow UB + + int64_t numTiles = (seed->bound + seed->tileLen - 1) / seed->tileLen; + int64_t H = chooseH(numTiles, seed->tileLen, elemBytes, maxH); + if (H <= 1) + return; + + Value pidVal = seed->pid.getResult(); + Location ploc = seed->pid.getLoc(); + Value seedMask = seed->mask; + Operation *seedMaskOp = seedMask ? seedMask.getDefiningOp() : nullptr; + + if (seedMask) { + for (Operation *u : seedMask.getUsers()) { + auto ld = dyn_cast(u); + auto st = dyn_cast(u); + bool okAsLoadMask = ld && ld.getMask() == seedMask; + bool okAsStoreMask = st && st.getMask() == seedMask; + if (!okAsLoadMask && !okAsStoreMask) + return; + } + } + + Block *pidBlock = seed->pid->getBlock(); + for (Operation *op : ordered) { + if (op->getBlock() != pidBlock) + return; + if (!isa(op) && op->getNumOperands() == 0) + return; + if (auto sel = dyn_cast(op)) { + bool condTensor = isa(sel.getCondition().getType()); + bool valTensor = isa(sel.getTrueValue().getType()); + if (condTensor != valTensor) + return; + } + if (auto ld = dyn_cast(op)) + if (!ld.getBoundaryCheck().empty()) + return; + if (auto st = dyn_cast(op)) + if (!st.getBoundaryCheck().empty()) + return; + } + + auto liftTy = [&](Type t) -> RankedTensorType { + if (auto rt = dyn_cast(t)) { + SmallVector s; + s.push_back(H); + s.append(rt.getShape().begin(), rt.getShape().end()); + return RankedTensorType::get(s, rt.getElementType()); + } + return RankedTensorType::get({H}, t); + }; + + LLVM_DEBUG(llvm::dbgs() << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast( + cfg::GraphOptimizationRuleId::ChunkCoalescing) + << " (" + << cfg::getGraphOptimizationRuleName( + cfg::GraphOptimizationRuleId::ChunkCoalescing) + << ") at " << ploc << ": tileLen=" << seed->tileLen + << " bound=" << seed->bound + << " numTiles=" << numTiles << " maxH=" << maxH + << " H=" << H << "\n"); + + rw.setInsertionPointAfter(seed->pid); + Value cH = rw.create(ploc, H, 32); + Value pidH = rw.create(ploc, pidVal, cH); + auto i32 = rw.getI32Type(); + auto hVecTy = RankedTensorType::get({H}, i32); + Value rangeH = rw.create(ploc, hVecTy, 0, H); + Value splatPidH = rw.create(ploc, hVecTy, pidH); + Value tileVec = rw.create(ploc, splatPidH, rangeH); + + DenseMap vmap; + std::function lift = [&](Value v) -> Value { + if (v == pidVal) + return tileVec; + auto it = vmap.find(v); + if (it != vmap.end()) + return it->second; + Operation *def = v.getDefiningOp(); + if (def && region.count(def)) { + auto rebuilt = vmap.find(v); + assert(rebuilt != vmap.end() && "region value used before its rebuild"); + return rebuilt->second; + } + if (!isa(v.getType())) + return v; + Value e = rw.create(v.getLoc(), v, 0); + Value b = + rw.create(v.getLoc(), liftTy(v.getType()), e); + vmap[v] = b; + return b; + }; + auto liftOpd = [&](Value v) -> Value { + Value lv = lift(v); + if (!isa(lv.getType())) + lv = rw.create(lv.getLoc(), liftTy(v.getType()), lv); + return lv; + }; + + auto liftOpdOrNull = [&](Value v) -> Value { + return v ? liftOpd(v) : Value(); + }; + auto bumpBoundary = [&](ArrayRef bc) { + return llvm::map_to_vector(bc, [](int32_t i) { return i + 1; }); + }; + auto copyAttrs = [&](Operation *from, Operation *to) { + for (NamedAttribute a : from->getAttrs()) + if (!to->hasAttr(a.getName())) + to->setAttr(a.getName(), a.getValue()); + }; + + for (Operation *op : ordered) { + if (op == seedMaskOp) + continue; + + rw.setInsertionPoint(op); + Location loc = op->getLoc(); + + if (auto sp = dyn_cast(op)) { + Value lin = lift(sp.getSrc()); + if (!isa(lin.getType())) { + vmap[sp.getResult()] = + rw.create(loc, liftTy(sp.getType()), lin); + } else { + int addDims = cast(sp.getType()).getRank(); + Value cur = lin; + for (int k = 0; k < addDims; ++k) + cur = rw.create( + loc, cur, cast(cur.getType()).getRank()); + vmap[sp.getResult()] = + rw.create(loc, liftTy(sp.getType()), cur); + } + continue; + } + if (auto ed = dyn_cast(op)) { + vmap[ed.getResult()] = rw.create( + loc, liftOpd(ed.getSrc()), ed.getAxis() + 1); + continue; + } + if (auto bc = dyn_cast(op)) { + vmap[bc.getResult()] = rw.create( + loc, liftTy(bc.getType()), liftOpd(bc.getSrc())); + continue; + } + if (auto scan = dyn_cast(op)) { + SmallVector srcs = llvm::map_to_vector( + scan.getSrcs(), [&](Value s) { return liftOpd(s); }); + auto nu = rw.create(loc, srcs, scan.getAxis() + 1, + scan.getReverse()); + rw.cloneRegionBefore(scan.getCombineOp(), nu.getCombineOp(), + nu.getCombineOp().end()); + copyAttrs(scan, nu); + for (auto [o, n] : llvm::zip(scan.getResults(), nu.getResults())) + vmap[o] = n; + continue; + } + if (auto red = dyn_cast(op)) { + SmallVector srcs = llvm::map_to_vector( + red.getSrcs(), [&](Value s) { return liftOpd(s); }); + auto nu = rw.create(loc, srcs, red.getAxis() + 1); + rw.cloneRegionBefore(red.getCombineOp(), nu.getCombineOp(), + nu.getCombineOp().end()); + copyAttrs(red, nu); + for (auto [o, n] : llvm::zip(red.getResults(), nu.getResults())) + vmap[o] = n; + continue; + } + if (auto ld = dyn_cast(op)) { + bool dropMask = ld.getMask() == seedMask; + Value m = dropMask ? Value() : liftOpdOrNull(ld.getMask()); + Value o = dropMask ? Value() : liftOpdOrNull(ld.getOther()); + auto bc = bumpBoundary(ld.getBoundaryCheck()); + auto nu = rw.create(loc, liftOpd(ld.getPtr()), m, o, bc, + ld.getPadding(), ld.getCache(), + ld.getEvict(), ld.getIsVolatile()); + copyAttrs(ld, nu); + vmap[ld.getResult()] = nu.getResult(); + continue; + } + if (auto st = dyn_cast(op)) { + bool dropMask = st.getMask() == seedMask; + Value m = dropMask ? Value() : liftOpdOrNull(st.getMask()); + auto bc = bumpBoundary(st.getBoundaryCheck()); + rw.create(loc, liftOpd(st.getPtr()), + liftOpd(st.getValue()), m, bc, st.getCache(), + st.getEvict()); + continue; + } + + SmallVector operands = llvm::map_to_vector( + op->getOperands(), [&](Value o) { return liftOpd(o); }); + SmallVector resTypes = llvm::map_to_vector( + op->getResultTypes(), [&](Type t) -> Type { return liftTy(t); }); + Operation *nu = rw.create(loc, op->getName().getIdentifier(), operands, + resTypes, op->getAttrs()); + for (auto [oldR, newR] : llvm::zip(op->getResults(), nu->getResults())) + vmap[oldR] = newR; + } + + for (auto it = ordered.rbegin(); it != ordered.rend(); ++it) + rw.eraseOp(*it); + + auto i32Ty = IntegerType::get(moduleOp.getContext(), 32); + moduleOp->setAttr(kCoalesceFactorAttr, IntegerAttr::get(i32Ty, H)); + moduleOp->setAttr(kCoalesceAxisAttr, IntegerAttr::get(i32Ty, seed->axis)); +} + +} // namespace + +void rewriteChunkCoalesce(ModuleOp moduleOp) { + IRRewriter rw(moduleOp.getContext()); + rewriteModule(moduleOp, rw); + moduleOp->removeAttr(kGridNumTilesAttr); +} + +} // namespace ChunkCoalescing diff --git a/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/RowCoalescing.cpp b/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/RowCoalescing.cpp new file mode 100644 index 0000000000..33bda61b6e --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/RowCoalescing.cpp @@ -0,0 +1,658 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/LegacyMemoryAccess/RowCoalescing.h" + +#include "TritonToGraph/GraphOptimization.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Matchers.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +namespace RowCoalescing { + +using namespace mlir; +using namespace triton; + +namespace { + +constexpr llvm::StringLiteral kCoalesceFactorAttr = "hacc.coalesce_factor"; +constexpr llvm::StringLiteral kCoalesceAxisAttr = "hacc.coalesce_axis"; +constexpr llvm::StringLiteral kCoalesceGridCeilDivAttr = + "hacc.coalesce_grid_ceil_div"; + +// Default to the original FBGEMM dense-token point for very small row tiles. +constexpr int64_t kDefaultRowsPerProgram = 8; +constexpr int64_t kMaxBaseElementsPerLift = 1024; + +struct RowSeed { + triton::GetProgramIdOp pid; + int32_t axis = 0; + Value validCount; + arith::CmpIOp entryGuard; + Block *workBlock = nullptr; +}; + +static int64_t inferRowsPerProgram(int64_t maxBaseElements) { + if (maxBaseElements > kMaxBaseElementsPerLift) + return 1; + if (maxBaseElements >= 1024) + return 2; + if (maxBaseElements > 16) + return 4; + return kDefaultRowsPerProgram; +} + +static bool readsAxisNumPrograms(ModuleOp moduleOp, int32_t axis) { + bool reads = false; + moduleOp.walk([&](triton::GetNumProgramsOp np) { + if (np.getAxisAsInt() == axis) + reads = true; + }); + return reads; +} + +static bool isScalarIntegerLike(Value value) { + Type type = value.getType(); + if (isa(type)) + return true; + auto intTy = dyn_cast(type); + return intTy && intTy.getWidth() > 1; +} + +static bool isAutomaticOverflowAssert(triton::AssertOp assertOp) { + if (!assertOp || !assertOp->hasAttr("tt.auto_overflow_assert")) + return false; + auto message = dyn_cast(assertOp.getMessageAttr()); + return message && + message.getValue().contains("overflow detected for operation"); +} + +// Match the canonical rowwise guard: +// +// %pid = tt.get_program_id x +// %n = tt.load %valid_count_ptr +// %p = arith.cmpi sge, %pid, %n +// cf.cond_br %p, ^return_block, ^work_block +// +// This is intentionally a seed matcher only. It proves that pid is a row id +// guarded by a runtime row count and identifies the work block that should be +// lifted. The actual IR rewrite is kept separate. +static std::optional matchRowSeed(ModuleOp moduleOp) { + if (moduleOp->hasAttr(kCoalesceFactorAttr)) + return std::nullopt; + + SmallVector pids; + moduleOp.walk([&](triton::GetProgramIdOp pid) { pids.push_back(pid); }); + if (pids.size() != 1) + return std::nullopt; + + triton::GetProgramIdOp pid = pids.front(); + int32_t axis = pid.getAxisAsInt(); + if (readsAxisNumPrograms(moduleOp, axis)) + return std::nullopt; + + for (Operation *user : pid.getResult().getUsers()) { + auto cmp = dyn_cast(user); + if (!cmp || cmp.getPredicate() != arith::CmpIPredicate::sge || + cmp.getLhs() != pid.getResult()) + continue; + + Block *cmpBlock = cmp->getBlock(); + if (!cmpBlock || !cmpBlock->mightHaveTerminator()) + continue; + auto cond = dyn_cast_or_null(cmpBlock->getTerminator()); + if (!cond || cond.getCondition() != cmp.getResult()) + continue; + + Block *trueBlock = cond.getTrueDest(); + Block *falseBlock = cond.getFalseDest(); + if (!trueBlock || !falseBlock) + continue; + if (!trueBlock->mightHaveTerminator()) + continue; + if (!isa(trueBlock->getTerminator())) + continue; + if (!isScalarIntegerLike(cmp.getRhs())) + continue; + + return RowSeed{pid, axis, cmp.getRhs(), cmp, falseBlock}; + } + + return std::nullopt; +} + +static bool isRowLiftable(Operation *op) { + if (isa(op)) + return false; + if (auto assertOp = dyn_cast(op)) + return isAutomaticOverflowAssert(assertOp); + if (auto *dialect = op->getDialect()) { + StringRef ns = dialect->getNamespace(); + if (ns == arith::ArithDialect::getDialectNamespace() || + ns == math::MathDialect::getDialectNamespace()) + return true; + } + return isa(op); +} + +static bool collectRowRegion(const RowSeed &seed, + SmallVectorImpl &ordered) { + if (!seed.workBlock) + return false; + + bool hasStore = false; + for (Operation &op : seed.workBlock->without_terminator()) { + bool safe = true; + op.walk([&](Operation *nested) { + if (nested == &op) + return; + if (isa( + nested)) + return; + if (!isRowLiftable(nested)) + safe = false; + }); + if (!safe || !isRowLiftable(&op)) + return false; + if (isa(op)) + hasStore = true; + ordered.push_back(&op); + } + + return hasStore; +} + +static int64_t getMaxStaticTensorElements(ArrayRef ordered) { + int64_t maxElements = 1; + auto update = [&](Type type) { + auto rt = dyn_cast(type); + if (!rt) + return; + if (!rt.hasStaticShape()) { + maxElements = kMaxBaseElementsPerLift + 1; + return; + } + maxElements = std::max(maxElements, rt.getNumElements()); + }; + + for (Operation *op : ordered) { + for (Value operand : op->getOperands()) + update(operand.getType()); + for (Value result : op->getResults()) + update(result.getType()); + } + return maxElements; +} + +static bool rewriteMatchedRow(ModuleOp moduleOp, const RowSeed &seed, + ArrayRef ordered, IRRewriter &rw, + int64_t H) { + if (H <= 1) + return false; + triton::GetProgramIdOp pid = seed.pid; + Value pidVal = pid.getResult(); + Location loc = pid.getLoc(); + if (!seed.entryGuard || !seed.workBlock) + return false; + + auto liftTy = [&](Type t) -> RankedTensorType { + if (auto rt = dyn_cast(t)) { + SmallVector shape; + shape.push_back(H); + shape.append(rt.getShape().begin(), rt.getShape().end()); + return RankedTensorType::get(shape, rt.getElementType()); + } + return RankedTensorType::get({H}, t); + }; + + auto makeZero = [&](Location zloc, Type ty) -> Value { + OpBuilder::InsertionGuard guard(rw); + auto rt = dyn_cast(ty); + Type elemTy = rt ? rt.getElementType() : ty; + if (auto intTy = dyn_cast(elemTy)) { + TypedAttr zero = IntegerAttr::get(intTy, 0); + if (rt) + return rw.create(zloc, ty, + DenseElementsAttr::get(rt, zero)); + return rw.create(zloc, ty, zero); + } + if (auto fpTy = dyn_cast(elemTy)) { + TypedAttr zero = FloatAttr::get(fpTy, 0.0); + if (rt) + return rw.create(zloc, ty, + DenseElementsAttr::get(rt, zero)); + return rw.create(zloc, ty, zero); + } + return Value(); + }; + + rw.setInsertionPoint(seed.entryGuard); + Value cH = rw.create(loc, H, 32); + Value pidH = rw.create(loc, pidVal, cH); + auto hI32Ty = RankedTensorType::get({H}, rw.getI32Type()); + Value lane = rw.create(loc, hI32Ty, 0, H); + Value pidHSplat = rw.create(loc, hI32Ty, pidH); + Value rows = rw.create(loc, pidHSplat, lane); + Value validSplat = rw.create(loc, hI32Ty, seed.validCount); + Value rowMask = rw.create(loc, arith::CmpIPredicate::slt, rows, + validSplat); + + DenseMap vmap; + vmap[pidVal] = rows; + vmap[seed.validCount] = validSplat; + + auto maskForType = [&](Location mloc, Type type) -> Value { + auto rt = dyn_cast(type); + if (!rt) + return Value(); + if (rt.getRank() == 1) + return rowMask; + Value cur = rowMask; + for (int64_t rank = 1; rank < rt.getRank(); ++rank) { + cur = rw.create( + mloc, cur, cast(cur.getType()).getRank()); + } + auto maskTy = RankedTensorType::get(rt.getShape(), rw.getI1Type()); + return rw.create(mloc, maskTy, cur); + }; + + std::function *)> lift = + [&](Value v, DenseMap *localMap) -> Value { + if (localMap) { + auto lit = localMap->find(v); + if (lit != localMap->end()) + return lit->second; + } + auto it = vmap.find(v); + if (it != vmap.end()) + return it->second; + if (!isa(v.getType())) + return v; + Value expanded = rw.create(v.getLoc(), v, 0); + Value broadcast = rw.create( + v.getLoc(), liftTy(v.getType()), expanded); + if (localMap) + (*localMap)[v] = broadcast; + else + vmap[v] = broadcast; + return broadcast; + }; + + auto liftOperand = [&](Value v, DenseMap *localMap) -> Value { + Value lv = lift(v, localMap); + if (!lv) + return Value(); + if (!isa(lv.getType())) + return rw.create(lv.getLoc(), liftTy(v.getType()), lv); + return lv; + }; + + auto copyAttrs = [&](Operation *from, Operation *to) { + for (NamedAttribute attr : from->getAttrs()) + if (!to->hasAttr(attr.getName())) + to->setAttr(attr.getName(), attr.getValue()); + }; + + auto liftConstant = [&](arith::ConstantOp cst, Location opLoc, + DenseMap *localMap) -> bool { + Operation *nu = rw.clone(*cst.getOperation()); + Value result = nu->getResult(0); + if (isa(cst.getType())) { + Value expanded = rw.create(opLoc, result, 0); + result = rw.create(opLoc, liftTy(cst.getType()), + expanded); + } + (*localMap)[cst.getResult()] = result; + return true; + }; + + std::function *, bool)> rebuildOp; + rebuildOp = [&](Operation *op, DenseMap *localMap, + bool resetInsertionPoint) -> bool { + if (resetInsertionPoint) + rw.setInsertionPoint(op); + Location opLoc = op->getLoc(); + + if (auto cst = dyn_cast(op)) + return liftConstant(cst, opLoc, localMap); + + if (auto range = dyn_cast(op)) { + Value oneD = rw.create( + opLoc, range.getType(), range.getStart(), range.getEnd()); + Value expanded = rw.create(opLoc, oneD, 0); + (*localMap)[range.getResult()] = rw.create( + opLoc, liftTy(range.getType()), expanded); + return true; + } + + if (auto sp = dyn_cast(op)) { + Value src = lift(sp.getSrc(), localMap); + if (!src) + return false; + Value result; + if (!isa(src.getType())) { + result = rw.create(opLoc, liftTy(sp.getType()), src); + } else { + Value cur = src; + int64_t addDims = cast(sp.getType()).getRank(); + for (int64_t i = 0; i < addDims; ++i) { + cur = rw.create( + opLoc, cur, cast(cur.getType()).getRank()); + } + result = + rw.create(opLoc, liftTy(sp.getType()), cur); + } + (*localMap)[sp.getResult()] = result; + return true; + } + + if (auto ed = dyn_cast(op)) { + Value src = liftOperand(ed.getSrc(), localMap); + if (!src) + return false; + (*localMap)[ed.getResult()] = + rw.create(opLoc, src, ed.getAxis() + 1); + return true; + } + + if (auto bc = dyn_cast(op)) { + Value src = liftOperand(bc.getSrc(), localMap); + if (!src) + return false; + (*localMap)[bc.getResult()] = + rw.create(opLoc, liftTy(bc.getType()), src); + return true; + } + + if (auto red = dyn_cast(op)) { + SmallVector srcs; + for (Value src : red.getSrcs()) { + Value lifted = liftOperand(src, localMap); + if (!lifted) + return false; + srcs.push_back(lifted); + } + auto nu = rw.create(opLoc, srcs, red.getAxis() + 1); + rw.cloneRegionBefore(red.getCombineOp(), nu.getCombineOp(), + nu.getCombineOp().end()); + copyAttrs(red, nu); + for (auto [oldR, newR] : llvm::zip(red.getResults(), nu.getResults())) + (*localMap)[oldR] = newR; + return true; + } + + if (auto scan = dyn_cast(op)) { + SmallVector srcs; + for (Value src : scan.getSrcs()) { + Value lifted = liftOperand(src, localMap); + if (!lifted) + return false; + srcs.push_back(lifted); + } + auto nu = rw.create(opLoc, srcs, scan.getAxis() + 1, + scan.getReverse()); + rw.cloneRegionBefore(scan.getCombineOp(), nu.getCombineOp(), + nu.getCombineOp().end()); + copyAttrs(scan, nu); + for (auto [oldR, newR] : llvm::zip(scan.getResults(), nu.getResults())) + (*localMap)[oldR] = newR; + return true; + } + + if (auto ld = dyn_cast(op)) { + Value ptr = liftOperand(ld.getPtr(), localMap); + if (!ptr) + return false; + Value mask = ld.getMask() ? liftOperand(ld.getMask(), localMap) : Value(); + Value rowMaskForLoad = + maskForType(opLoc, cast(ptr.getType())); + if (rowMaskForLoad) { + mask = mask ? rw.create(opLoc, mask, rowMaskForLoad) + : rowMaskForLoad; + } + Value other = ld.getOther() + ? liftOperand(ld.getOther(), localMap) + : makeZero(opLoc, liftTy(ld.getResult().getType())); + auto nu = rw.create( + opLoc, ptr, mask, other, ld.getBoundaryCheck(), ld.getPadding(), + ld.getCache(), ld.getEvict(), ld.getIsVolatile()); + copyAttrs(ld, nu); + (*localMap)[ld.getResult()] = nu.getResult(); + return true; + } + + if (auto st = dyn_cast(op)) { + Value ptr = liftOperand(st.getPtr(), localMap); + Value val = liftOperand(st.getValue(), localMap); + if (!ptr || !val) + return false; + Value mask = st.getMask() ? liftOperand(st.getMask(), localMap) : Value(); + Value rowMaskForStore = + maskForType(opLoc, cast(ptr.getType())); + if (rowMaskForStore) { + mask = mask ? rw.create(opLoc, mask, rowMaskForStore) + : rowMaskForStore; + } + rw.create(opLoc, ptr, val, mask, st.getBoundaryCheck(), + st.getCache(), st.getEvict()); + return true; + } + + if (auto forOp = dyn_cast(op)) { + OpBuilder::InsertionGuard guard(rw); + Value lowerBound = lift(forOp.getLowerBound(), localMap); + Value upperBound = lift(forOp.getUpperBound(), localMap); + Value step = lift(forOp.getStep(), localMap); + if (!lowerBound || !upperBound || !step || + isa(lowerBound.getType()) || + isa(upperBound.getType()) || + isa(step.getType())) + return false; + SmallVector initArgs; + for (Value init : forOp.getInitArgs()) { + Value lifted = liftOperand(init, localMap); + if (!lifted) + return false; + initArgs.push_back(lifted); + } + auto newFor = + rw.create(opLoc, lowerBound, upperBound, step, initArgs); + copyAttrs(forOp, newFor); + + DenseMap loopMap = *localMap; + loopMap[forOp.getInductionVar()] = newFor.getInductionVar(); + for (auto [oldArg, newArg] : + llvm::zip(forOp.getRegionIterArgs(), newFor.getRegionIterArgs())) { + loopMap[oldArg] = newArg; + } + + Block *oldBody = forOp.getBody(); + Block *newBody = newFor.getBody(); + if (!oldBody || !newBody || !oldBody->mightHaveTerminator()) + return false; + if (!newBody->mightHaveTerminator()) { + SmallVector passthrough; + for (Value arg : newFor.getRegionIterArgs()) + passthrough.push_back(arg); + rw.setInsertionPointToEnd(newBody); + rw.create(opLoc, passthrough); + } + if (!newBody->mightHaveTerminator()) + return false; + Operation *newTerm = newBody->getTerminator(); + rw.setInsertionPoint(newTerm); + for (Operation &bodyOp : oldBody->without_terminator()) { + if (!rebuildOp(&bodyOp, &loopMap, false)) + return false; + } + auto oldYield = cast(oldBody->getTerminator()); + SmallVector yieldVals; + for (Value y : oldYield.getResults()) { + auto mapped = loopMap.find(y); + if (mapped == loopMap.end() || !mapped->second) + return false; + yieldVals.push_back(mapped->second); + } + rw.setInsertionPoint(newTerm); + rw.create(oldYield.getLoc(), yieldVals); + rw.eraseOp(newTerm); + + for (auto [oldR, newR] : + llvm::zip(forOp.getResults(), newFor.getResults())) + (*localMap)[oldR] = newR; + rw.setInsertionPointAfter(newFor); + return true; + } + + SmallVector operands; + bool hasTensorOperand = false; + for (Value operand : op->getOperands()) { + Value lifted = lift(operand, localMap); + if (!lifted) + return false; + hasTensorOperand |= isa(lifted.getType()); + operands.push_back(lifted); + } + if (hasTensorOperand) { + for (size_t idx = 0; idx < operands.size(); ++idx) { + if (isa(operands[idx].getType())) + continue; + Value operand = op->getOperand(idx); + operands[idx] = rw.create( + operands[idx].getLoc(), liftTy(operand.getType()), operands[idx]); + } + } + SmallVector resultTypes; + for (Type resultTy : op->getResultTypes()) { + if (hasTensorOperand) + resultTypes.push_back(liftTy(resultTy)); + else + resultTypes.push_back(resultTy); + } + Operation *nu = rw.create(opLoc, op->getName().getIdentifier(), operands, + resultTypes, op->getAttrs()); + for (auto [oldR, newR] : llvm::zip(op->getResults(), nu->getResults())) + (*localMap)[oldR] = newR; + return true; + }; + + DenseMap topMap; + for (Operation *op : ordered) { + if (!rebuildOp(op, &topMap, true)) + return false; + } + + Block *entryBlock = seed.entryGuard->getBlock(); + if (!entryBlock || !entryBlock->mightHaveTerminator()) + return false; + auto cond = dyn_cast(entryBlock->getTerminator()); + if (!cond) + return false; + rw.setInsertionPoint(cond); + rw.create(cond.getLoc(), seed.workBlock); + rw.eraseOp(cond); + + std::function eraseNestedOps = [&](Operation *op) { + for (Region ®ion : op->getRegions()) { + for (Block &block : llvm::make_early_inc_range(region)) { + while (!block.empty()) { + Operation &nested = block.back(); + eraseNestedOps(&nested); + nested.dropAllUses(); + rw.eraseOp(&nested); + } + } + } + }; + for (auto it = ordered.rbegin(); it != ordered.rend(); ++it) { + Operation *op = *it; + eraseNestedOps(op); + op->dropAllUses(); + rw.eraseOp(op); + } + if (seed.entryGuard->use_empty()) + rw.eraseOp(seed.entryGuard); + + (void)moduleOp; + return true; +} + +static void rewriteModule(ModuleOp moduleOp, IRRewriter &rw) { + auto seed = matchRowSeed(moduleOp); + if (!seed) + return; + + SmallVector ordered; + if (!collectRowRegion(*seed, ordered)) + return; + int64_t maxBaseElements = getMaxStaticTensorElements(ordered); + int64_t rowsPerProgram = inferRowsPerProgram(maxBaseElements); + if (rowsPerProgram <= 1) + return; + + if (!rewriteMatchedRow(moduleOp, *seed, ordered, rw, rowsPerProgram)) + return; + + LLVM_DEBUG(llvm::dbgs() << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast( + cfg::GraphOptimizationRuleId::RowCoalescing) + << " (" + << cfg::getGraphOptimizationRuleName( + cfg::GraphOptimizationRuleId::RowCoalescing) + << ") at " << seed->pid.getLoc() + << ": axis=" << seed->axis + << " rowsPerProgram=" << rowsPerProgram << "\n"); + + auto i32Ty = IntegerType::get(moduleOp.getContext(), 32); + moduleOp->setAttr(kCoalesceFactorAttr, + IntegerAttr::get(i32Ty, rowsPerProgram)); + moduleOp->setAttr(kCoalesceAxisAttr, IntegerAttr::get(i32Ty, seed->axis)); + moduleOp->setAttr(kCoalesceGridCeilDivAttr, IntegerAttr::get(i32Ty, 1)); +} + +} // namespace + +void rewriteRowCoalesce(ModuleOp moduleOp) { + IRRewriter rw(moduleOp.getContext()); + rewriteModule(moduleOp, rw); +} + +} // namespace RowCoalescing diff --git a/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.cpp b/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.cpp new file mode 100644 index 0000000000..a0c349c5d6 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.cpp @@ -0,0 +1,596 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.h" + +#include "TritonToGraph/GraphOptimization.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" +#include "llvm/Support/Debug.h" + +#include + +#define DEBUG_TYPE "graph-optimize" + +namespace StridedAxisCoalescing { + +using namespace mlir; +using namespace triton; + +// Detects the FLA per-head strided base `base + (pid % S)` produced by +// splitting the H axis (the contiguous axis folded onto the grid). Returns the +// matching AddPtrOp, or a null AddPtrOp if `base` is not such an ih-split ptr. +static triton::AddPtrOp findIhAddPtr(Value base, int64_t S) { + Value src = base; + while (auto addptr = src.getDefiningOp()) { + if (isa(addptr.getPtr().getType())) + break; + if (auto rem = addptr.getOffset().getDefiningOp()) { + APInt cC; + if (matchPattern(rem.getRhs(), m_ConstantInt(&cC)) && + std::abs(cC.getSExtValue()) == S) { + Value lhs = rem.getLhs(); + while (true) { + if (auto e = lhs.getDefiningOp()) { + lhs = e.getIn(); + continue; + } + if (auto t = lhs.getDefiningOp()) { + lhs = t.getIn(); + continue; + } + break; + } + if (lhs.getDefiningOp()) + return addptr; + } + } + src = addptr.getPtr(); + } + return triton::AddPtrOp(); +} + +// Mirror of findIhAddPtr that returns the program_id axis driving the ih split +// (i.e. the grid dim the host launcher must divide by S), or -1 if `base` is +// not such an ih-split ptr. Whenever findIhAddPtr succeeds this does too. +static int32_t findIhAxis(Value base, int64_t S) { + Value src = base; + while (auto addptr = src.getDefiningOp()) { + if (isa(addptr.getPtr().getType())) + break; + if (auto rem = addptr.getOffset().getDefiningOp()) { + APInt cC; + if (matchPattern(rem.getRhs(), m_ConstantInt(&cC)) && + std::abs(cC.getSExtValue()) == S) { + Value lhs = rem.getLhs(); + while (true) { + if (auto e = lhs.getDefiningOp()) { + lhs = e.getIn(); + continue; + } + if (auto t = lhs.getDefiningOp()) { + lhs = t.getIn(); + continue; + } + break; + } + if (auto pid = lhs.getDefiningOp()) + return pid.getAxisAsInt(); + } + } + src = addptr.getPtr(); + } + return -1; +} + +// Returns the i_h value `pid % S` (the per-head index feeding the ih split), or +// null. Mirror of findIhAddPtr but yields the RemSIOp result itself, used to +// check whether i_h also feeds a per-head scalar load (which coalescing cannot +// lane-expand -- see the correctness guard in rewriteStridedAxisCoalesce). +static Value findIhRem(Value base, int64_t S) { + Value src = base; + while (auto addptr = src.getDefiningOp()) { + if (isa(addptr.getPtr().getType())) + break; + if (auto rem = addptr.getOffset().getDefiningOp()) { + APInt cC; + if (matchPattern(rem.getRhs(), m_ConstantInt(&cC)) && + std::abs(cC.getSExtValue()) == S) + return rem.getResult(); + } + src = addptr.getPtr(); + } + return Value(); +} + +static Value build2DBlockPtr(IRRewriter &rw, triton::MakeTensorPtrOp m1d, + int64_t S, int64_t BT) { + triton::AddPtrOp ih = findIhAddPtr(m1d.getBase(), S); + if (!ih) + return Value(); + auto loc = m1d.getLoc(); + rw.setInsertionPoint(m1d); + Value newBase = ih.getPtr(); + Value cH = rw.create(loc, rw.getI64IntegerAttr(S)); + Value c1 = rw.create(loc, rw.getI64IntegerAttr(1)); + Value c0 = rw.create(loc, rw.getI32IntegerAttr(0)); + SmallVector shape{m1d.getShape()[0], cH}; + SmallVector strides{m1d.getStrides()[0], c1}; + SmallVector offsets{m1d.getOffsets()[0], c0}; + SmallVector blockShape{static_cast(BT), + static_cast(S)}; + SmallVector order{1, 0}; + auto p = rw.create(loc, newBase, shape, strides, + offsets, blockShape, order); + return p.getResult(); +} + +// Lift a rank-1 tensor type tensor to tensor (append the folded +// H axis as the inner lane). Scalars / non-rank-1 types pass through unchanged. +static Type lift2D(Type t, int64_t S) { + auto rt = dyn_cast(t); + if (!rt || rt.getRank() != 1) + return t; + return RankedTensorType::get({rt.getShape()[0], S}, rt.getElementType()); +} + +// An op is safe to 2D-ify (lane-parallel over the appended H axis) iff every +// lane s computes independently: a pure elementwise arith/math op, a cast, a +// splat, or a scan/reduce ALONG THE T axis (axis 0). On the 2D tile [BT,S] a +// T-axis reduce is per-lane (the S lanes stay independent, output [S]) and a +// T-axis scan likewise, so both lift directly -- no need to pre-collapse the +// reverse-cumsum idiom into a single scan. Ops that mix or move the lane +// (transpose, tt.dot, reshape, reduce/scan along the lane) are NOT here, so the +// caller bails and keeps the original (indirect) path. +static bool is2DSafe(Operation *op) { + if (isa(op)) + return true; + // Every math dialect op (exp/log/sqrt/tanh/erf/...) is a per-lane scalar + // elementwise map -> 2D-safe. This covers gate activations expressed as + // tensor math (softplus = log(1+exp), silu, gelu, ...) without enumerating + // each op, so such cumsum kernels coalesce without per-idiom pattern match. + if (isa(op->getDialect())) + return true; + if (isa(op)) + return true; + if (auto scan = dyn_cast(op)) + return scan.getAxis() == 0 && scan->getNumResults() == 1; + if (auto reduce = dyn_cast(op)) + return reduce.getAxis() == 0 && reduce->getNumResults() == 1; + return false; +} + +void rewriteStridedAxisCoalesce(ModuleOp moduleOp) { + IRRewriter rw(moduleOp.getContext()); + + // Collect the strided ih-base 1D loads (seeds). All must share one stride S + // (the folded H axis); BT is the per-chunk tile length. + SmallVector seeds; + int64_t S = 0, BT = 0; + moduleOp.walk([&](triton::LoadOp l) { + auto m = l.getPtr().getDefiningOp(); + if (!m) + return; + auto rt = dyn_cast(l.getResult().getType()); + if (!rt || rt.getRank() != 1) + return; + auto strides = m.getStrides(); + if (strides.empty()) + return; + APInt sC; + if (!matchPattern(strides.back(), m_ConstantInt(&sC))) + return; + int64_t s = std::abs(sC.getSExtValue()); + if (s <= 1) + return; + if (!findIhAddPtr(m.getBase(), s)) + return; + if (S == 0) { + S = s; + BT = rt.getShape()[0]; + } + if (s != S) + return; + seeds.push_back(l); + }); + if (seeds.empty()) + return; + + // The grid axis the launcher will divide by S (the pid feeding `pid % S`). + int32_t coalesceAxis = -1; + if (auto m0 = seeds.front().getPtr().getDefiningOp()) + coalesceAxis = findIhAxis(m0.getBase(), S); + if (coalesceAxis < 0) + return; // cannot identify the axis -> do not coalesce + + // Full TA path: the launcher divides grid[coalesceAxis] by S, so the + // kernel-visible num_programs(coalesceAxis) becomes grid/S. If the kernel + // reads it, coalescing would change that value -> wrong results. Bail (the + // kernel keeps its original, correct, uncoalesced path). + bool readsAxisNumPrograms = false; + moduleOp.walk([&](triton::GetNumProgramsOp np) { + if (np.getAxisAsInt() == coalesceAxis) + readsAxisNumPrograms = true; + }); + if (readsAxisNumPrograms) + return; + + // The H axis is folded into the inner lane, so every per-head value must be + // expanded across the S lanes. Block-ptr loads are lane-expanded by + // build2DBlockPtr; a per-head SCALAR load (A_log[i_h] / dt_bias[i_h] in + // gdn-style gates) is collected here and later lifted to an [S] per-lane + // vector load (lane s -> base[s], matching the folded i_h = s). The scalar + // chain on top of it (exp/neg/...) is lifted to [S] by get2D, and the splat + // that feeds it into the tile becomes an [S]->[BT,S] broadcast. Bail only if + // i_h reaches something we cannot lane-expand: a scalar load feeding an + // address (indirect gather), or a tensor load not via make_tensor_ptr. + SmallVector headLoads; + if (auto m0 = + seeds.front().getPtr().getDefiningOp()) { + if (Value ihRem = findIhRem(m0.getBase(), S)) { + SmallVector wl2(ihRem.getUsers().begin(), + ihRem.getUsers().end()); + DenseSet seen2; + while (!wl2.empty()) { + Operation *u = wl2.pop_back_val(); + if (!seen2.insert(u).second) + continue; + if (isa(u)) + continue; // block-ptr path: fine + if (auto ld = dyn_cast(u)) { + // per-head scalar load: must be scalar and must not itself feed + // an address (indirect gather). Then it is liftable to [S]. + if (isa(ld.getResult().getType())) + return; + for (Operation *ru : ld.getResult().getUsers()) + if (isa(ru)) + return; // indirect -> bail + headLoads.push_back(ld); + continue; + } + if (isa(u)) + for (Operation *uu : u->getResult(0).getUsers()) + wl2.push_back(uu); + } + } + } + + // Discover the load->store subgraph by forward reachability from the seeds. + // Every op on the way must be 2D-safe (elementwise / cast / splat / T-axis + // scan or reduce); stores are the sinks. A T-axis reduce is kept as-is and + // lifted in place (no idiom-specific pre-collapse) -- see is2DSafe. + // Any unsafe op (or a value escaping to one) aborts the whole rewrite. + DenseSet region; + SmallVector sinks; + DenseSet visited; + SmallVector wl; + for (auto s : seeds) + for (Operation *u : s.getResult().getUsers()) + wl.push_back(u); + while (!wl.empty()) { + Operation *op = wl.pop_back_val(); + if (!visited.insert(op).second) + continue; + if (auto st = dyn_cast(op)) { + sinks.push_back(st); + continue; + } + if (!is2DSafe(op)) + return; // bail: unsafe consumer in the chain + region.insert(op); + for (Value r : op->getResults()) + for (Operation *u : r.getUsers()) + wl.push_back(u); + } + if (sinks.empty()) + return; + + // Every sink store must also be a matching 1D stride-S ih-base block ptr. + for (auto st : sinks) { + auto m = st.getPtr().getDefiningOp(); + if (!m || !findIhAddPtr(m.getBase(), S)) + return; + auto os = m.getStrides(); + if (os.empty()) + return; + APInt soC; + if (!matchPattern(os.back(), m_ConstantInt(&soC)) || + std::abs(soC.getSExtValue()) != S) + return; + } + + // Map each 1D value to its 2D counterpart, materializing splats/constants + // on demand. Returns null to signal an un-liftable operand (bail). + DenseMap vmap; + std::function get2D = [&](Value v) -> Value { + auto it = vmap.find(v); + if (it != vmap.end()) + return it->second; + if (!isa(v.getType())) { + // Scalar: if it derives from a per-head load (whose [S] lane vector is + // already in vmap), lift the elementwise scalar chain to [S] so each + // lane gets its own value. i_h-independent scalars stay scalar (they + // splat uniformly across lanes, which is correct). + Operation *def = v.getDefiningOp(); + bool liftable = + def && (isa(def->getDialect()) || + isa(def)); + if (liftable) { + SmallVector ops2; + bool anyLane = false; + for (Value o : def->getOperands()) { + Value n = get2D(o); + if (!n) + return Value(); + if (isa(n.getType())) + anyLane = true; + ops2.push_back(n); + } + if (anyLane) { + OpBuilder::InsertionGuard g(rw); + rw.setInsertionPointAfter(def); + for (Value &o : ops2) + if (!isa(o.getType())) + o = rw.create( + def->getLoc(), RankedTensorType::get({S}, o.getType()), o); + OperationState st(def->getLoc(), def->getName()); + st.addOperands(ops2); + st.addAttributes(def->getAttrs()); + for (Value r : def->getResults()) + st.addTypes(RankedTensorType::get({S}, r.getType())); + Operation *nu = rw.create(st); + vmap[v] = nu->getResult(0); + return nu->getResult(0); + } + } + return v; // i_h-independent scalar, splats uniformly + } + // Save/restore the insertion point: the materializers below move it next + // to the original splat/constant (which may sit at the top of the func). + // Without this the caller's `rw.setInsertionPoint(op)` would be clobbered + // and the rebuilt op emitted before its operands -> dominance violation. + OpBuilder::InsertionGuard guard(rw); + if (auto sp = v.getDefiningOp()) { + Value src2 = get2D(sp.getSrc()); + if (!src2) + return Value(); + rw.setInsertionPointAfter(sp); + Value n; + if (isa(src2.getType())) { + // src2 is the per-lane reduce result [S] (the 1D scalar source + // became a vector once the reduce was 2D-ified). Broadcast it + // across T: [S] -> expand_dims(0) -> [1,S] -> broadcast [BT,S]. + Value ex = rw.create(sp.getLoc(), src2, 0); + n = rw.create(sp.getLoc(), lift2D(sp.getType(), S), + ex); + } else { + // True scalar splat: lift to a 2D splat over [BT,S]. + n = rw.create(sp.getLoc(), lift2D(sp.getType(), S), + src2); + } + vmap[v] = n; + return n; + } + if (auto c = v.getDefiningOp()) { + if (auto dea = dyn_cast(c.getValue())) { + if (dea.isSplat()) { + auto nt = cast(lift2D(c.getType(), S)); + rw.setInsertionPointAfter(c); + Value n = rw.create( + c.getLoc(), nt, + DenseElementsAttr::get(nt, dea.getSplatValue())); + vmap[v] = n; + return n; + } + } + } + return Value(); + }; + + // Build 2D loads for the seeds. + for (auto l : seeds) { + auto m = l.getPtr().getDefiningOp(); + Value p2 = build2DBlockPtr(rw, m, S, BT); + if (!p2) + return; + rw.setInsertionPoint(l); + auto nl = rw.create(l.getLoc(), p2, ArrayRef{0, 1}, + l.getPadding(), l.getCache(), + l.getEvict(), l.getIsVolatile()); + vmap[l.getResult()] = nl.getResult(); + } + + // Lift each per-head scalar load to an [S] per-lane vector load: base[0:S] + // (lane s = base[s], matching the folded i_h = s). The offset must be exactly + // i_h = pid % S so that lane s maps to base[s]; otherwise bail. + for (auto ld : headLoads) { + auto ap = ld.getPtr().getDefiningOp(); + if (!ap) + return; + Value off = ap.getOffset(); + while (auto e = off.getDefiningOp()) + off = e.getIn(); + while (auto t = off.getDefiningOp()) + off = t.getIn(); + if (!off.getDefiningOp()) + return; // offset not pure i_h + Value base = ap.getPtr(); + Type elemTy = ld.getResult().getType(); + rw.setInsertionPoint(ld); + auto loc = ld.getLoc(); + Value cS = rw.create(loc, rw.getI64IntegerAttr(S)); + Value c1 = rw.create(loc, rw.getI64IntegerAttr(1)); + Value c0 = rw.create(loc, rw.getI32IntegerAttr(0)); + SmallVector shape{cS}, strides{c1}, offsets{c0}; + SmallVector blockShape{static_cast(S)}, order{0}; + auto p = rw.create(loc, base, shape, strides, + offsets, blockShape, order); + auto vl = rw.create( + loc, p.getResult(), ArrayRef{0}, + triton::PaddingOption::PAD_ZERO, triton::CacheModifier::NONE, + triton::EvictionPolicy::NORMAL, false); + (void)elemTy; + vmap[ld.getResult()] = vl.getResult(); + } + + // Rebuild the region ops in IR (topological) order as 2D. + SmallVector ordered; + moduleOp.walk([&](Operation *op) { + if (region.count(op)) + ordered.push_back(op); + }); + for (Operation *op : ordered) { + rw.setInsertionPoint(op); + if (auto scan = dyn_cast(op)) { + Value in = get2D(scan.getOperand(0)); + if (!in) + return; + auto ns = rw.create(scan.getLoc(), ValueRange{in}, + static_cast(scan.getAxis()), + scan.getReverse()); + rw.cloneRegionBefore(scan.getCombineOp(), ns.getCombineOp(), + ns.getCombineOp().end()); + vmap[scan->getResult(0)] = ns->getResult(0); + continue; + } + if (auto reduce = dyn_cast(op)) { + Value in = get2D(reduce.getOperand(0)); + if (!in) + return; + // T-axis reduce on the 2D tile [BT,S] -> [S]: one independent + // reduction per lane (the S lanes do not mix). The 1D result was a + // scalar; its 2D counterpart is the per-lane vector [S], which a + // downstream splat turns into an expand_dims+broadcast (see get2D). + auto nr = rw.create(reduce.getLoc(), ValueRange{in}, + static_cast(reduce.getAxis())); + rw.cloneRegionBefore(reduce.getCombineOp(), nr.getCombineOp(), + nr.getCombineOp().end()); + vmap[reduce->getResult(0)] = nr->getResult(0); + continue; + } + // Splats are materialized on demand by get2D (which lifts a scalar splat + // to a 2D splat, and a per-lane reduce result [S] to expand_dims + + // broadcast). Skip here so it is not rebuilt as an invalid 2D splat. + if (isa(op)) + continue; + SmallVector operands; + for (Value o : op->getOperands()) { + Value n = get2D(o); + if (!n) + return; + operands.push_back(n); + } + OperationState st(op->getLoc(), op->getName()); + st.addOperands(operands); + st.addAttributes(op->getAttrs()); + for (Value r : op->getResults()) + st.addTypes(lift2D(r.getType(), S)); + Operation *nu = rw.create(st); + for (auto [oldR, newR] : llvm::zip(op->getResults(), nu->getResults())) + vmap[oldR] = newR; + } + + // Build the 2D stores. + for (auto st : sinks) { + Value val = get2D(st.getValue()); + if (!val) + return; + auto m = st.getPtr().getDefiningOp(); + Value p2 = build2DBlockPtr(rw, m, S, BT); + if (!p2) + return; + rw.setInsertionPoint(st); + rw.create(st.getLoc(), p2, val, ArrayRef{0, 1}, + st.getCache(), st.getEvict()); + } + + // i_b = divsi(get_program_id, S): with the H axis folded into the inner + // tile the per-instance i_b becomes the raw program id. Redirect it (this + // also fixes the new 2D block ptr bases, which reuse i_b). + if (auto m0 = + seeds.front().getPtr().getDefiningOp()) { + if (triton::AddPtrOp ihA = findIhAddPtr(m0.getBase(), S)) { + if (auto rem = ihA.getOffset().getDefiningOp()) { + Value lhs = rem.getLhs(); + while (true) { + if (auto e = lhs.getDefiningOp()) { + lhs = e.getIn(); + continue; + } + if (auto t = lhs.getDefiningOp()) { + lhs = t.getIn(); + continue; + } + break; + } + SmallVector divs; + for (Operation *u : lhs.getUsers()) + if (auto dv = dyn_cast(u)) { + APInt dC; + if (dv.getLhs() == lhs && + matchPattern(dv.getRhs(), m_ConstantInt(&dC)) && + std::abs(dC.getSExtValue()) == S) + divs.push_back(dv); + } + for (auto dv : divs) + rw.replaceAllUsesWith(dv.getResult(), lhs); + } + } + } + + LLVM_DEBUG( + llvm::dbgs() << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast( + cfg::GraphOptimizationRuleId::StridedAxisCoalescing) + << " (" + << cfg::getGraphOptimizationRuleName( + cfg::GraphOptimizationRuleId::StridedAxisCoalescing) + << ") at " << seeds.front().getLoc() << ": stride=" << S + << " tileLen=" << BT << " axis=" << coalesceAxis << " loads=" + << seeds.size() << " stores=" << sinks.size() << "\n"); + + // Erase the original chain (sinks, then region in reverse order, then seeds). + for (auto st : sinks) + rw.eraseOp(st); + for (auto it = ordered.rbegin(); it != ordered.rend(); ++it) + rw.eraseOp(*it); + for (auto l : seeds) + rw.eraseOp(l); + + auto i32t = IntegerType::get(moduleOp.getContext(), 32); + moduleOp->setAttr("hacc.coalesce_factor", IntegerAttr::get(i32t, S)); + moduleOp->setAttr("hacc.coalesce_axis", IntegerAttr::get(i32t, coalesceAxis)); +} + +} // namespace StridedAxisCoalescing diff --git a/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.cpp b/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.cpp new file mode 100644 index 0000000000..ba735b3668 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.cpp @@ -0,0 +1,1381 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.h" +#include "TritonMemoryAccess/LoadStoreMaskAnalysis.h" +#include "TritonMemoryAccess/MemoryAccessTags.h" +#include "TritonToStructured/PtrAnalysis.h" +#include "Utils/Utils.h" + +#include "Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "TritonToGraph/GraphOptimization.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" + +#include "llvm/ADT/Twine.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +namespace StridedLoadStoreRewrite { + +using namespace mlir; +using namespace triton; + +namespace { + +// V1 fast-path supports up to 5D tensors, mirroring +// UnstructureConversionPass::tryRewriteIndirectFastPath. +constexpr size_t kFastPathRankLimit = 5; + +// Funnel for the eight rewrite variants, which differ only in how the pointer +// was formed and which op they produce. +static void logMatch(Location loc, const llvm::Twine &pointerKind, + const llvm::Twine &rewrite, int64_t lastStride) { + LLVM_DEBUG( + llvm::dbgs() << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast( + cfg::GraphOptimizationRuleId::StridedLoadStoreRewrite) + << " (" + << cfg::getGraphOptimizationRuleName( + cfg::GraphOptimizationRuleId::StridedLoadStoreRewrite) + << ") at " << loc << ": [" << pointerKind << "] " << rewrite + << " lastStride=" << lastStride << "\n"); +} + +// Returns true iff `v` is a static integer constant with |v| > 1. +static bool isStaticConstAbsGtOne(Value v) { + IntegerAttr scalarAttr; + if (matchPattern(v, m_Constant(&scalarAttr))) + return std::abs(scalarAttr.getValue().getSExtValue()) > 1; + DenseElementsAttr denseAttr; + if (matchPattern(v, m_Constant(&denseAttr)) && denseAttr.isSplat() && + denseAttr.getElementType().isInteger()) + return std::abs(denseAttr.getSplatValue().getSExtValue()) > 1; + // Transparently see through tt.splat of a scalar constant. + if (auto splatOp = v.getDefiningOp()) + return isStaticConstAbsGtOne(splatOp.getSrc()); + return false; +} + +static std::optional getStaticConstInt(Value v) { + IntegerAttr scalarAttr; + if (matchPattern(v, m_Constant(&scalarAttr))) + return scalarAttr.getValue().getSExtValue(); + DenseElementsAttr denseAttr; + if (matchPattern(v, m_Constant(&denseAttr)) && denseAttr.isSplat() && + denseAttr.getElementType().isInteger()) + return denseAttr.getSplatValue().getSExtValue(); + if (auto splatOp = v.getDefiningOp()) + return getStaticConstInt(splatOp.getSrc()); + return std::nullopt; +} + +static std::optional getStaticMaskUpperBound(Value mask) { + if (!mask) + return std::nullopt; + if (auto cmp = mask.getDefiningOp()) { + auto bound = getStaticConstInt(cmp.getRhs()); + if (!bound) + return std::nullopt; + if (cmp.getPredicate() == arith::CmpIPredicate::slt) + return *bound; + if (cmp.getPredicate() == arith::CmpIPredicate::sle) + return *bound + 1; + return std::nullopt; + } + if (auto andOp = mask.getDefiningOp()) { + auto lhsBound = getStaticMaskUpperBound(andOp.getLhs()); + auto rhsBound = getStaticMaskUpperBound(andOp.getRhs()); + if (lhsBound && rhsBound) + return std::min(*lhsBound, *rhsBound); + return lhsBound ? lhsBound : rhsBound; + } + return std::nullopt; +} + +static bool +shouldRouteMaskedSingleTilePow2ToIndirect(Value mask, + RankedTensorType tensorType) { + if (!mask || tensorType.getRank() != 1) + return false; + int64_t blockSize = tensorType.getShape()[0]; + if (ShapedType::isDynamic(blockSize)) + return false; + auto upperBound = getStaticMaskUpperBound(mask); + return upperBound && *upperBound <= blockSize; +} + +// Cheaply detect tensor-level static stride > 1 before running PtrAnalysis, +// which mutates IR. Dynamic stride stays on the structured SIMD path, and +// scalar offset arithmetic does not affect per-element stride. +static bool offsetMayContainStrideGtOne(Value offset, int depthBudget = 16) { + if (depthBudget <= 0) { + return true; // Give up cheaply and let PtrAnalysis decide downstream. + } + if (!isa(offset.getType())) { + return false; + } + Operation *defOp = offset.getDefiningOp(); + if (!defOp) { + return false; + } + if (auto mul = dyn_cast(defOp)) { + if (isStaticConstAbsGtOne(mul.getLhs()) || + isStaticConstAbsGtOne(mul.getRhs())) { + return true; + } + return offsetMayContainStrideGtOne(mul.getLhs(), depthBudget - 1) || + offsetMayContainStrideGtOne(mul.getRhs(), depthBudget - 1); + } + // arith.shli %a, %k effectively multiplies by 2^k; treat shift by >=1 as + // "may contain stride > 1". + if (auto shl = dyn_cast(defOp)) { + APInt c; + if (matchPattern(shl.getRhs(), m_ConstantInt(&c)) && + c.getSExtValue() >= 1) { + return true; + } + DenseElementsAttr denseAttr; + if (matchPattern(shl.getRhs(), m_Constant(&denseAttr)) && + denseAttr.isSplat() && denseAttr.getElementType().isInteger() && + denseAttr.getSplatValue().getSExtValue() >= 1) { + return true; + } + return offsetMayContainStrideGtOne(shl.getLhs(), depthBudget - 1); + } + for (Value operand : defOp->getOperands()) { + if (offsetMayContainStrideGtOne(operand, depthBudget - 1)) { + return true; + } + } + return false; +} + +// Walk through shape-only wrappers to find the underlying scalar !tt.ptr. +// ChunkCoalescing lifts invariant pointer tensors as +// broadcast(expand_dims(splat(ptr))), which is still a scalar base pointer for +// indirect access construction. +static Value getScalarBasePtr(Value tensorPtr, int depthBudget = 8) { + if (depthBudget <= 0) + return Value(); + if (auto splatOp = tensorPtr.getDefiningOp()) { + Value src = splatOp.getSrc(); + if (isa(src.getType())) { + return src; + } + } + if (auto broadcastOp = tensorPtr.getDefiningOp()) + return getScalarBasePtr(broadcastOp.getSrc(), depthBudget - 1); + if (auto expandDimsOp = tensorPtr.getDefiningOp()) + return getScalarBasePtr(expandDimsOp.getSrc(), depthBudget - 1); + return Value(); +} + +// Ensure the per-element offset tensor has i64 element type, matching the +// convention used elsewhere (UnstructureConversionPass::parseAddPtr). +static Value ensureI64OffsetTensor(Value offsetTensor, Location loc, + PatternRewriter &rewriter) { + auto tensorTy = dyn_cast(offsetTensor.getType()); + if (!tensorTy) + return Value(); + auto eltTy = dyn_cast(tensorTy.getElementType()); + if (!eltTy) + return Value(); + if (eltTy.getWidth() == 64) + return offsetTensor; + auto newTy = + RankedTensorType::get(tensorTy.getShape(), rewriter.getIntegerType(64)); + return rewriter.create(loc, newTy, offsetTensor); +} + +// Promote a scalar to i64. Handles i32/i64 integers and index types. +static Value ensureI64Scalar(Value v, Location loc, PatternRewriter &rewriter) { + Type ty = v.getType(); + if (auto intTy = dyn_cast(ty)) { + if (intTy.getWidth() == 64) + return v; + return rewriter.create(loc, rewriter.getI64Type(), v); + } + if (isa(ty)) { + return rewriter.create(loc, rewriter.getI64Type(), v); + } + return Value(); // Unsupported scalar type. +} + +static LogicalResult unwrapScalarAddPtrChain(Value scalarPtr, Value &src, + Value &scalarOffset, Location loc, + PatternRewriter &rewriter) { + src = scalarPtr; + scalarOffset = Value(); + while (auto addPtrOp = src.getDefiningOp()) { + if (isa(addPtrOp.getPtr().getType())) + break; + if (!scalarOffset) + scalarOffset = rewriter.create( + loc, rewriter.getI64IntegerAttr(0)); + Value offset = ensureI64Scalar(addPtrOp.getOffset(), loc, rewriter); + if (!offset) + return failure(); + scalarOffset = rewriter.create(loc, scalarOffset, offset); + src = addPtrOp.getPtr(); + } + return success(); +} + +static Value addScalarOffsetToTensor(Value offsetTensor, Value scalarOffset, + Location loc, PatternRewriter &rewriter) { + if (!scalarOffset) + return offsetTensor; + APInt scalarOffsetConst; + if (matchPattern(scalarOffset, m_ConstantInt(&scalarOffsetConst)) && + scalarOffsetConst.isZero()) + return offsetTensor; + auto tensorType = cast(offsetTensor.getType()); + Value scalarOffsetTensor = + rewriter.create(loc, tensorType, scalarOffset); + return rewriter.create(loc, offsetTensor, scalarOffsetTensor); +} + +static Value getStrideLoadOtherScalar(triton::LoadOp op, + RankedTensorType resultType, + PatternRewriter &rewriter) { + auto loc = op.getLoc(); + Type elementType = resultType.getElementType(); + if (Value other = op.getOther()) { + if (auto splatOp = other.getDefiningOp()) + return splatOp.getSrc(); + + DenseElementsAttr denseAttr; + if (matchPattern(other, m_Constant(&denseAttr)) && denseAttr.isSplat()) { + if (auto floatType = dyn_cast(elementType)) { + return rewriter.create( + loc, rewriter.getFloatAttr(elementType, + denseAttr.getSplatValue())); + } + if (isa(elementType)) { + return rewriter.create( + loc, rewriter.getIntegerAttr(elementType, + denseAttr.getSplatValue())); + } + } + return Value(); + } + + if (op.getPadding().has_value() && + op.getPadding().value() == triton::PaddingOption::PAD_NAN) { + auto floatTy = dyn_cast(elementType); + if (!floatTy) + return Value(); + return rewriter.create( + loc, rewriter.getFloatAttr( + elementType, APFloat::getNaN(floatTy.getFloatSemantics()))); + } + return rewriter.create(loc, + rewriter.getZeroAttr(elementType)); +} + +static Value createStrideLoadOp(Location loc, RankedTensorType resultType, + Value src, Value offset, Value other, + ArrayRef strides, ArrayRef numels, + PatternRewriter &rewriter) { + int64_t rank = resultType.getRank(); + if (static_cast(strides.size()) != rank || + static_cast(numels.size()) != rank) { + return Value(); + } + + Type indexType = rewriter.getI32Type(); + auto chooseIndexType = [&](Value value) -> bool { + Type type = value.getType(); + if (isa(type) || type.isInteger(64)) { + indexType = rewriter.getI64Type(); + return true; + } + return type.isInteger(32); + }; + auto castToIndexType = [&](Value value) -> Value { + Type type = value.getType(); + if (type == indexType) + return value; + if (isa(type)) + return rewriter.create(loc, indexType, value); + if (type.isInteger(32) && indexType.isInteger(64)) + return rewriter.create(loc, indexType, value); + return Value(); + }; + auto chooseAll = [&](ArrayRef values) -> bool { + for (Value value : values) { + if (!chooseIndexType(value)) + return false; + } + return true; + }; + + if (!chooseIndexType(offset) || !chooseAll(strides) || !chooseAll(numels)) + return Value(); + offset = castToIndexType(offset); + if (!offset) + return Value(); + SmallVector castStrides; + SmallVector castNumels; + castStrides.reserve(rank); + castNumels.reserve(rank); + for (Value stride : strides) { + stride = castToIndexType(stride); + if (!stride) + return Value(); + castStrides.push_back(stride); + } + for (Value numel : numels) { + numel = castToIndexType(numel); + if (!numel) + return Value(); + castNumels.push_back(numel); + } + + auto strideLoad = rewriter.create( + loc, resultType, src, offset, other, castStrides, castNumels); + strideLoad->setAttr(RewrittenByStridedLoadStoreRewriteTAG, + UnitAttr::get(rewriter.getContext())); + return strideLoad->getResult(0); +} + +static Operation *createStrideStoreOp(Location loc, RankedTensorType valueType, + Value dst, Value src, Value offset, + ArrayRef strides, + ArrayRef numels, + PatternRewriter &rewriter) { + int64_t rank = valueType.getRank(); + if (static_cast(strides.size()) != rank || + static_cast(numels.size()) != rank) { + return nullptr; + } + + Type indexType = rewriter.getI32Type(); + auto chooseIndexType = [&](Value value) -> bool { + Type type = value.getType(); + if (isa(type) || type.isInteger(64)) { + indexType = rewriter.getI64Type(); + return true; + } + return type.isInteger(32); + }; + auto castToIndexType = [&](Value value) -> Value { + Type type = value.getType(); + if (type == indexType) + return value; + if (isa(type)) + return rewriter.create(loc, indexType, value); + if (type.isInteger(32) && indexType.isInteger(64)) + return rewriter.create(loc, indexType, value); + return Value(); + }; + auto chooseAll = [&](ArrayRef values) -> bool { + for (Value value : values) { + if (!chooseIndexType(value)) + return false; + } + return true; + }; + + if (!chooseIndexType(offset) || !chooseAll(strides) || !chooseAll(numels)) + return nullptr; + offset = castToIndexType(offset); + if (!offset) + return nullptr; + SmallVector castStrides; + SmallVector castNumels; + castStrides.reserve(rank); + castNumels.reserve(rank); + for (Value stride : strides) { + stride = castToIndexType(stride); + if (!stride) + return nullptr; + castStrides.push_back(stride); + } + for (Value numel : numels) { + numel = castToIndexType(numel); + if (!numel) + return nullptr; + castNumels.push_back(numel); + } + + auto strideStore = rewriter.create( + loc, dst, src, offset, castStrides, castNumels); + strideStore->setAttr(RewrittenByStridedLoadStoreRewriteTAG, + UnitAttr::get(rewriter.getContext())); + return strideStore.getOperation(); +} + +static Value materializeI64(OpFoldResult ofr, Location loc, + PatternRewriter &rewriter) { + if (auto attr = ofr.dyn_cast()) { + int64_t value = cast(attr).getInt(); + return rewriter.create( + loc, rewriter.getI64IntegerAttr(value)); + } + return ensureI64Scalar(llvm::cast(ofr), loc, rewriter); +} + +static Value clampI64(Value value, Value lower, Value upper, Location loc, + PatternRewriter &rewriter) { + value = rewriter.create(loc, value, lower); + return rewriter.create(loc, value, upper); +} + +static Value getPrefixMaskNumel(Operation *op, Value mask, + RankedTensorType resultType, + PatternRewriter &rewriter) { + if (!mask) { + return rewriter.create( + op->getLoc(), + rewriter.getI64IntegerAttr(resultType.getShape().front())); + } + + auto maskState = triton::runMaskAnalysis(op, rewriter); + if (!maskState || maskState->getRank() != 1) + return Value(); + + auto offset = getConstantIntValue(maskState->offsets.front()); + if (!offset.has_value() || offset.value() != 0) + return Value(); + + auto loc = op->getLoc(); + Value zero = + rewriter.create(loc, rewriter.getI64IntegerAttr(0)); + Value block = rewriter.create( + loc, rewriter.getI64IntegerAttr(resultType.getShape().front())); + Value numel = materializeI64(maskState->dims.front(), loc, rewriter); + if (!numel) + return Value(); + return clampI64(numel, zero, block, loc, rewriter); +} + +static FailureOr> +getAddPtrStrideNumels(Operation *op, Value mask, RankedTensorType resultType, + PatternRewriter &rewriter) { + auto loc = op->getLoc(); + int64_t rank = resultType.getRank(); + ArrayRef shape = resultType.getShape(); + + if (!mask) { + SmallVector numels; + for (int64_t d = 0; d < rank; ++d) + numels.push_back(rewriter.create( + loc, rewriter.getI64IntegerAttr(shape[d]))); + return numels; + } + + if (rank == 1) { + Value numel = getPrefixMaskNumel(op, mask, resultType, rewriter); + if (!numel) + return failure(); + return SmallVector{numel}; + } + + auto maskState = triton::runMaskAnalysis(op, rewriter); + if (!maskState || maskState->getRank() != rank) + return failure(); + + for (int64_t d = 0; d < rank; ++d) { + auto offsetVal = getConstantIntValue(maskState->offsets[d]); + if (!offsetVal.has_value() || offsetVal.value() != 0) + return failure(); + } + + Value zero = + rewriter.create(loc, rewriter.getI64IntegerAttr(0)); + SmallVector numels; + numels.reserve(rank); + for (int64_t d = 0; d < rank; ++d) { + Value block = rewriter.create( + loc, rewriter.getI64IntegerAttr(shape[d])); + Value numel = materializeI64(maskState->dims[d], loc, rewriter); + if (!numel) + return failure(); + numel = clampI64(numel, zero, block, loc, rewriter); + numels.push_back(numel); + } + return numels; +} + +static FailureOr> getBlockPtrStrideNumels( + Operation *op, Value mask, ArrayRef boundaryCheck, + triton::MakeTensorPtrOp mtpt, RankedTensorType resultType, + ArrayRef logicalOffsets, PatternRewriter &rewriter) { + auto loc = op->getLoc(); + Value zero = + rewriter.create(loc, rewriter.getI64IntegerAttr(0)); + ArrayRef shape = resultType.getShape(); + int64_t rank = resultType.getRank(); + SmallVector numels; + numels.reserve(rank); + + if (mask && rank != 1) + return failure(); + Value prefixMaskNumel; + if (mask) { + prefixMaskNumel = getPrefixMaskNumel(op, mask, resultType, rewriter); + if (!prefixMaskNumel) + return failure(); + } + + for (int64_t d = 0; d < rank; ++d) { + Value block = rewriter.create( + loc, rewriter.getI64IntegerAttr(shape[d])); + Value numel = (d == 0 && prefixMaskNumel) ? prefixMaskNumel : block; + + bool hasBoundaryCheck = false; + for (int32_t axis : boundaryCheck) { + if (axis == static_cast(d)) { + hasBoundaryCheck = true; + break; + } + } + if (hasBoundaryCheck) { + Value parentSize = ensureI64Scalar(mtpt.getShape()[d], loc, rewriter); + if (!parentSize) + return failure(); + Value remaining = + rewriter.create(loc, parentSize, logicalOffsets[d]); + Value boundaryNumel = clampI64(remaining, zero, block, loc, rewriter); + numel = rewriter.create(loc, numel, boundaryNumel); + } + numels.push_back(numel); + } + + return numels; +} + +// Expand a 1D tensor `v` of length `targetShape[axis]` into a rank-N tensor +// with size `targetShape[axis]` at `axis` and size 1 elsewhere, then +// broadcast to `targetShape`. Used to materialise the per-axis contribution +// `arange(0, B_d) * stride_d` into the full N-D offset tensor. +static Value expandAndBroadcastForAxis(Value v, int axis, + ArrayRef targetShape, + Type elementType, Location loc, + PatternRewriter &rewriter) { + Value cur = v; + int rank = static_cast(targetShape.size()); + for (int d = 0; d < rank; ++d) { + if (d == axis) + continue; + int curRank = + static_cast(cast(cur.getType()).getRank()); + int expandAt = (d < axis) ? 0 : curRank; + cur = rewriter.create(loc, cur, expandAt); + } + auto targetTy = RankedTensorType::get(targetShape, elementType); + return rewriter.create(loc, targetTy, cur); +} + +// Build the per-element OOB mask for block_ptr's boundary_check. +// For each axis d in `boundaryCheck`: +// idx_d[i] = arange(0, B_d)[i] + effective_offset_d +// in_bounds_d[i] = (idx_d[i] >= 0) AND (idx_d[i] < parentShape[d]) +// Then broadcast each axis's mask to full `blockShape` and AND all together. +// Returns a tensor<...xi1> of shape `blockShape`, or null if boundaryCheck +// is empty. +static Value buildBoundaryMask(Location loc, PatternRewriter &rewriter, + ArrayRef blockShape, + ArrayRef boundaryCheck, + ArrayRef effectiveOffsetsI32, + ValueRange parentShape) { + if (boundaryCheck.empty()) + return Value(); + + auto i32Ty = rewriter.getIntegerType(32); + auto i64Ty = rewriter.getIntegerType(64); + auto i1Ty = rewriter.getIntegerType(1); + + Value combined; + for (int32_t axisRaw : boundaryCheck) { + int axis = static_cast(axisRaw); + int64_t blockD = blockShape[axis]; + + auto rangeTy32 = RankedTensorType::get({blockD}, i32Ty); + auto rangeTy64 = RankedTensorType::get({blockD}, i64Ty); + + Value arange = rewriter.create( + loc, rangeTy32, /*start=*/0, /*end=*/static_cast(blockD)); + Value offSplat = rewriter.create( + loc, rangeTy32, effectiveOffsetsI32[axis]); + Value idx32 = rewriter.create(loc, arange, offSplat); + Value idx64 = rewriter.create(loc, rangeTy64, idx32); + + Value shapeSplat = + rewriter.create(loc, rangeTy64, parentShape[axis]); + Value zeroDense = rewriter.create( + loc, DenseElementsAttr::get(rangeTy64, llvm::APInt(64, 0))); + Value cmpLower = rewriter.create( + loc, arith::CmpIPredicate::sge, idx64, zeroDense); + Value cmpUpper = rewriter.create( + loc, arith::CmpIPredicate::slt, idx64, shapeSplat); + Value axisMask = rewriter.create(loc, cmpLower, cmpUpper); + + Value bcast = expandAndBroadcastForAxis(axisMask, axis, blockShape, i1Ty, + loc, rewriter); + combined = + combined + ? rewriter.create(loc, combined, bcast).getResult() + : bcast; + } + return combined; +} + +// Build the padding "other" tensor for tt.indirect_load. Honours +// PaddingOption::PAD_NAN for float element types; otherwise (including +// PAD_ZERO or unspecified) returns a zero-splat tensor of `resultType`. +// Returns null if PAD_NAN is requested but the element type is not float +// (caller should treat as a hard failure). +static Value buildPaddingOther(Location loc, PatternRewriter &rewriter, + RankedTensorType resultType, + std::optional padding) { + Type elementType = resultType.getElementType(); + Value padScalar; + if (padding.has_value() && + padding.value() == triton::PaddingOption::PAD_NAN) { + auto floatTy = dyn_cast(elementType); + if (!floatTy) + return Value(); + auto nan = llvm::APFloat::getNaN(floatTy.getFloatSemantics()); + padScalar = rewriter.create( + loc, rewriter.getFloatAttr(elementType, nan)); + } else { + padScalar = rewriter.create( + loc, rewriter.getZeroAttr(elementType)); + } + return rewriter.create(loc, resultType, padScalar); +} + +// AddPtr path: tt.load(tt.addptr(tt.splat(%scalar_ptr), %offsets)). +// Uses PtrAnalysis (which has IR side effects), so this function must +// stamp InspectedByStridedLoadStoreRewriteTAG on every "PtrAnalysis ran but +// don't rewrite" path -- see comment block inside. +static LogicalResult tryRewriteAddPtrLoad(triton::LoadOp op, + triton::AddPtrOp addPtrOp, + RankedTensorType resultType, + PatternRewriter &rewriter) { + auto loc = op.getLoc(); + + // The base must resolve to a scalar pointer through shape-only wrappers. + Value scalarBase = getScalarBasePtr(addPtrOp.getPtr()); + if (!scalarBase) + return failure(); + + // Pre-filter without mutating IR: if no per-element multiplication by a + // constant > 1 exists in the offset chain, last stride must be 1. + if (!offsetMayContainStrideGtOne(addPtrOp.getOffset())) { + return failure(); + } + + // From here, PtrAnalysis may insert helper IR. Every early-out path + // MUST stamp InspectedByStridedLoadStoreRewriteTAG and return success() so + // the greedy driver does not re-walk the same op (which would re-run + // PtrAnalysis and accumulate dead IR until maxIterations). + TritonToStructured::PtrAnalysis ptrAnalysis; + TritonToStructured::PtrState ptrState; + auto markInspectedAndReturn = [&]() { + op->setAttr(InspectedByStridedLoadStoreRewriteTAG, + UnitAttr::get(rewriter.getContext())); + return success(); + }; + if (ptrAnalysis.visitOperand(op.getPtr(), ptrState, loc, rewriter).failed()) + return markInspectedAndReturn(); + if (ptrState.stateInfo.empty()) + return markInspectedAndReturn(); + ptrState.analyzePermute(); + if (ptrState.isPermuted) + return markInspectedAndReturn(); + + // Use SIMT indirect only for static non-pow2 or masked single-tile pow2 + // strides; keep dynamic strides on the structured SIMD path. + auto lastStrideOpt = getConstantIntValue(ptrState.stateInfo.back().stride); + if (!lastStrideOpt.has_value()) + return markInspectedAndReturn(); + int64_t lastStride = std::abs(lastStrideOpt.value()); + if (lastStride <= 1) + return markInspectedAndReturn(); + bool routeMaskedPow2ToIndirect = + shouldRouteMaskedSingleTilePow2ToIndirect(op.getMask(), resultType); + if (lastStride == 2 && !routeMaskedPow2ToIndirect) + return markInspectedAndReturn(); // even -> deinterleave; odd -> strided DMA + if ((lastStride & (lastStride - 1)) == 0 && !routeMaskedPow2ToIndirect) + return markInspectedAndReturn(); // power-of-two >= 4 -> strided DMA + + bool useStrideLoad = !routeMaskedPow2ToIndirect; + if (useStrideLoad && resultType.getRank() >= 1 && resultType.getRank() <= 3 && + resultType.hasStaticShape() && + static_cast(ptrState.stateInfo.size()) == resultType.getRank()) { + Value src = ptrState.source; + SmallVector strides; + SmallVector numels; + strides.reserve(resultType.getRank()); + + Value baseOffset = materializeI64(ptrState.offset, loc, rewriter); + bool operandsReady = src && baseOffset; + for (int64_t d = 0; operandsReady && d < resultType.getRank(); ++d) { + Value stride = + materializeI64(ptrState.stateInfo[d].stride, loc, rewriter); + if (!stride) { + operandsReady = false; + break; + } + strides.push_back(stride); + } + + if (operandsReady) { + auto numelsResult = getAddPtrStrideNumels(op.getOperation(), op.getMask(), + resultType, rewriter); + if (succeeded(numelsResult)) { + numels = *numelsResult; + } else { + operandsReady = false; + } + } + Value other = operandsReady + ? getStrideLoadOtherScalar(op, resultType, rewriter) + : Value(); + operandsReady = operandsReady && other; + + if (operandsReady) { + Value strideLoadResult = createStrideLoadOp( + loc, resultType, src, baseOffset, other, strides, numels, rewriter); + if (!strideLoadResult) + return markInspectedAndReturn(); + + logMatch(loc, "AddPtr", "tt.load -> ttasc.stride_load", lastStride); + rewriter.replaceOp(op, strideLoadResult); + return success(); + } + } + + if (useStrideLoad && resultType.getRank() <= 3) + return markInspectedAndReturn(); + + Value offsetTensor = + ensureI64OffsetTensor(addPtrOp.getOffset(), loc, rewriter); + if (!offsetTensor) + return failure(); + + Value src; + Value scalarOffset; + if (failed(unwrapScalarAddPtrChain(scalarBase, src, scalarOffset, loc, + rewriter))) + return markInspectedAndReturn(); + offsetTensor = + addScalarOffsetToTensor(offsetTensor, scalarOffset, loc, rewriter); + + auto indirectLoad = rewriter.create( + loc, resultType, src, offsetTensor, op.getMask(), op.getOther(), + ConverterUtils::requiresVolatileIndirectLoad(op.getPtr(), op)); + indirectLoad->setAttr(RewrittenByStridedLoadStoreRewriteTAG, + UnitAttr::get(rewriter.getContext())); + + logMatch(loc, "AddPtr", "tt.load -> tt.indirect_load", lastStride); + rewriter.replaceOp(op, indirectLoad.getResult()); + return success(); +} + +// Block-ptr path: tt.load(tt.make_tensor_ptr ...) or tt.load(tt.advance ...). +// Unlike the AddPtr path this does NOT use PtrAnalysis -- strides come +// directly from mtpt.getStrides() / order from mtpt.getOrder() -- so we +// can decide everything before touching IR and just return failure() if +// we don't want to rewrite. +static LogicalResult tryRewriteBlockPtrLoad(triton::LoadOp op, + triton::MakeTensorPtrOp mtpt, + triton::AdvanceOp advance, + RankedTensorType resultType, + PatternRewriter &rewriter) { + auto loc = op.getLoc(); + auto i64Ty = rewriter.getIntegerType(64); + ArrayRef shape = resultType.getShape(); + int64_t rank = static_cast(shape.size()); + + // ---- order must match the "non-permuted" layout: order[i] == rank-1-i, + // i.e. innermost (fastest-changing) is the last dim of the tensor. + // ImplicitPermute handles permuted layouts via tt.trans, so we + // leave anything non-canonical alone. + auto order = mtpt.getOrder(); + if (static_cast(order.size()) != rank) + return failure(); + for (int64_t i = 0; i < rank; ++i) { + if (order[i] != rank - 1 - i) { + return failure(); + } + } + + // ---- stride check ---- + auto strides = mtpt.getStrides(); + if (strides.empty() || static_cast(strides.size()) != rank) + return failure(); + // Stride dispatch: strided DMA on the MTE engine only supports power-of-two + // strides; a non-power-of-two stride would degrade to a slow scalar access. + // Dynamic strides stay on the structured SIMD path because they may be + // runtime stride 1 or power-of-two, where SIMT stride is slower. So we + // only rewrite to SIMT stride for *static non-power-of-two* strides: + // stride 1 -> contiguous; stride 2 (even dim) -> deinterleave; + // stride >= 4 (power of two) -> (compact) strided DMA. + APInt lastStrideC; + if (!matchPattern(strides.back(), m_ConstantInt(&lastStrideC))) + return failure(); + int64_t lastStride = std::abs(lastStrideC.getSExtValue()); + if (lastStride <= 1) + return failure(); + if (lastStride == 2) + return failure(); // even -> deinterleave; odd -> strided DMA + if ((lastStride & (lastStride - 1)) == 0) + return failure(); // power-of-two >= 4 -> strided DMA + + // ---- Compute per-axis effective base offsets: mtpt.offsets[d] + + // (advance.offsets[d] if present) + ValueRange mtptOffsets = mtpt.getOffsets(); + ValueRange advOffsets = advance ? advance.getOffsets() : ValueRange{}; + if (static_cast(mtptOffsets.size()) != rank) + return failure(); + if (advance && static_cast(advOffsets.size()) != rank) + return failure(); + + // ---- All checks passed: from here on IR mutation is committed. ---- + + // Unwrap any `tt.addptr` chain on the SCALAR base ptr (e.g. when the + // kernel writes `tl.make_block_ptr(s + bos*H + i_h, ...)`). If we left + // those scalar AddPtrs in place, the AddPtrConverter would lower each + // into a `memref.reinterpret_cast ... sizes: [1]` single-element view. By + // walking the scalar AddPtr chain here we (a) fold its scalar offsets into + // `scalarBaseAdj` and (b) recover the original underlying `!tt.ptr` to + // use as our src. + Value src = mtpt.getBase(); + Value scalarBaseAdj = + rewriter.create(loc, rewriter.getI64IntegerAttr(0)); + while (auto addptr = src.getDefiningOp()) { + // Only unwrap scalar AddPtr chains -- tensor-of-ptrs AddPtrs are not + // expected here (mtpt.getBase() is always a scalar ptr). + if (isa(addptr.getPtr().getType())) + break; + Value off = addptr.getOffset(); + Value offI64 = ensureI64Scalar(off, loc, rewriter); + if (!offI64) + return failure(); + scalarBaseAdj = rewriter.create(loc, scalarBaseAdj, offI64); + src = addptr.getPtr(); + } + + // Build the scalar base offset: + // scalarBaseAdj + sum_d (mtpt.offsets[d] + adv.offsets[d]) * strides[d]. + // Boundary checks are expressed in logical make_tensor_ptr coordinates, so + // numels must use the unstrided per-axis offsets rather than this physical + // GM offset. + Value scalarBase = scalarBaseAdj; + SmallVector logicalOffsets; + SmallVector strideOperands; + logicalOffsets.reserve(rank); + strideOperands.reserve(rank); + for (int64_t d = 0; d < rank; ++d) { + Value baseOff = ensureI64Scalar(mtptOffsets[d], loc, rewriter); + if (!baseOff) + return failure(); + if (advance) { + Value advStep = ensureI64Scalar(advOffsets[d], loc, rewriter); + if (!advStep) + return failure(); + baseOff = rewriter.create(loc, baseOff, advStep); + } + logicalOffsets.push_back(baseOff); + Value strI64 = ensureI64Scalar(strides[d], loc, rewriter); + if (!strI64) + return failure(); + strideOperands.push_back(strI64); + Value prod = rewriter.create(loc, baseOff, strI64); + scalarBase = rewriter.create(loc, scalarBase, prod); + } + + if (resultType.getRank() >= 1 && resultType.getRank() <= 3 && + resultType.hasStaticShape()) { + FailureOr> numels = getBlockPtrStrideNumels( + op.getOperation(), op.getMask(), op.getBoundaryCheck(), mtpt, + resultType, logicalOffsets, rewriter); + if (succeeded(numels)) { + Value other = getStrideLoadOtherScalar(op, resultType, rewriter); + if (!other) + return failure(); + Value strideLoadResult = + createStrideLoadOp(loc, resultType, src, scalarBase, other, + strideOperands, *numels, rewriter); + if (!strideLoadResult) + return failure(); + + logMatch(loc, llvm::Twine("BlockPtr") + (advance ? "+Advance" : ""), + "tt.load -> ttasc.stride_load", lastStride); + rewriter.replaceOp(op, strideLoadResult); + return success(); + } + } + + if (rank <= 3) + return failure(); + + auto i64TensorTy = RankedTensorType::get(shape, i64Ty); + Value offsetTensor = + rewriter.create(loc, i64TensorTy, scalarBase); + for (int64_t d = 0; d < rank; ++d) { + auto axisTy = RankedTensorType::get({shape[d]}, rewriter.getI32Type()); + Value arange = rewriter.create( + loc, axisTy, /*start=*/0, /*end=*/static_cast(shape[d])); + Value arangeI64 = rewriter.create( + loc, RankedTensorType::get({shape[d]}, i64Ty), arange); + Value strI64 = ensureI64Scalar(strides[d], loc, rewriter); + Value strSplat = rewriter.create( + loc, RankedTensorType::get({shape[d]}, i64Ty), strI64); + Value stridedArange = + rewriter.create(loc, arangeI64, strSplat); + Value broadcasted = expandAndBroadcastForAxis( + stridedArange, static_cast(d), shape, i64Ty, loc, rewriter); + offsetTensor = + rewriter.create(loc, offsetTensor, broadcasted); + } + + // ---- boundary_check: build OOB mask + padding "other" ---- + Value mask = op.getMask(); + Value other = op.getOther(); + auto boundaryCheck = op.getBoundaryCheck(); + if (!boundaryCheck.empty()) { + // Effective offset per axis = mtpt.offsets[d] + (advance.offsets[d] if any) + SmallVector effOffsets; + for (int64_t d = 0; d < rank; ++d) { + Value off = mtptOffsets[d]; + if (advance) { + off = rewriter.create(loc, off, advOffsets[d]); + } + effOffsets.push_back(off); + } + Value boundaryMask = buildBoundaryMask(loc, rewriter, shape, boundaryCheck, + effOffsets, mtpt.getShape()); + mask = mask ? rewriter.create(loc, mask, boundaryMask) + .getResult() + : boundaryMask; + if (!other) { + other = buildPaddingOther(loc, rewriter, resultType, op.getPadding()); + if (!other) { + // PAD_NAN requested on non-float element type: bail to legacy + // path (which would also assert there). + return failure(); + } + } + } + + // ---- Emit tt.indirect_load ---- + auto indirectLoad = rewriter.create( + loc, resultType, src, offsetTensor, mask, other, + ConverterUtils::requiresVolatileIndirectLoad(op.getPtr(), op)); + indirectLoad->setAttr(RewrittenByStridedLoadStoreRewriteTAG, + UnitAttr::get(rewriter.getContext())); + + logMatch(loc, + llvm::Twine("BlockPtr") + (advance ? "+Advance" : "") + + (boundaryCheck.empty() ? "" : "+Boundary"), + "tt.load -> tt.indirect_load", lastStride); + rewriter.replaceOp(op, indirectLoad.getResult()); + return success(); +} + +// V2 (Store) helpers ---------------------------------------------------------- + +// AddPtr path for tt.store. Mirrors tryRewriteAddPtrLoad but emits +// triton::ascend::StrideStoreOp when the mask can be represented by per-axis +// numel bounds. +static LogicalResult tryRewriteAddPtrStore(triton::StoreOp op, + triton::AddPtrOp addPtrOp, + RankedTensorType valueType, + PatternRewriter &rewriter) { + auto loc = op.getLoc(); + + Value scalarBase = getScalarBasePtr(addPtrOp.getPtr()); + if (!scalarBase) + return failure(); + + if (!offsetMayContainStrideGtOne(addPtrOp.getOffset())) + return failure(); + + TritonToStructured::PtrAnalysis ptrAnalysis; + TritonToStructured::PtrState ptrState; + auto markInspectedAndReturn = [&]() { + op->setAttr(InspectedByStridedLoadStoreRewriteTAG, + UnitAttr::get(rewriter.getContext())); + return success(); + }; + if (ptrAnalysis.visitOperand(op.getPtr(), ptrState, loc, rewriter).failed()) + return markInspectedAndReturn(); + if (ptrState.stateInfo.empty()) + return markInspectedAndReturn(); + ptrState.analyzePermute(); + if (ptrState.isPermuted) + return markInspectedAndReturn(); + + // Use SIMT indirect only for static non-pow2 or masked single-tile pow2 + // strides; keep dynamic strides on the structured SIMD path. + auto lastStrideOpt = getConstantIntValue(ptrState.stateInfo.back().stride); + if (!lastStrideOpt.has_value()) + return markInspectedAndReturn(); + int64_t lastStride = std::abs(lastStrideOpt.value()); + if (lastStride <= 1) + return markInspectedAndReturn(); + bool routeMaskedPow2ToIndirect = + shouldRouteMaskedSingleTilePow2ToIndirect(op.getMask(), valueType); + if (lastStride == 2 && !routeMaskedPow2ToIndirect) + return markInspectedAndReturn(); // even -> deinterleave; odd -> strided DMA + if ((lastStride & (lastStride - 1)) == 0 && !routeMaskedPow2ToIndirect) + return markInspectedAndReturn(); // power-of-two >= 4 -> strided DMA + + bool useStrideStore = !routeMaskedPow2ToIndirect; + if (useStrideStore && valueType.getRank() >= 1 && valueType.getRank() <= 3 && + valueType.hasStaticShape() && + static_cast(ptrState.stateInfo.size()) == valueType.getRank()) { + Value dst = ptrState.source; + SmallVector strides; + SmallVector numels; + strides.reserve(valueType.getRank()); + + Value baseOffset = materializeI64(ptrState.offset, loc, rewriter); + bool operandsReady = dst && baseOffset; + for (int64_t d = 0; operandsReady && d < valueType.getRank(); ++d) { + Value stride = + materializeI64(ptrState.stateInfo[d].stride, loc, rewriter); + if (!stride) { + operandsReady = false; + break; + } + strides.push_back(stride); + } + + if (operandsReady) { + auto numelsResult = getAddPtrStrideNumels(op.getOperation(), op.getMask(), + valueType, rewriter); + if (succeeded(numelsResult)) { + numels = *numelsResult; + } else { + operandsReady = false; + } + } + + if (operandsReady) { + Operation *strideStore = + createStrideStoreOp(loc, valueType, dst, op.getValue(), baseOffset, + strides, numels, rewriter); + if (!strideStore) + return markInspectedAndReturn(); + + logMatch(loc, "AddPtr", "tt.store -> ttasc.stride_store", lastStride); + rewriter.eraseOp(op); + return success(); + } + } + + if (useStrideStore && valueType.getRank() <= 3) + return markInspectedAndReturn(); + + Value offsetTensor = + ensureI64OffsetTensor(addPtrOp.getOffset(), loc, rewriter); + if (!offsetTensor) + return failure(); + + Value src; + Value scalarOffset; + if (failed(unwrapScalarAddPtrChain(scalarBase, src, scalarOffset, loc, + rewriter))) + return markInspectedAndReturn(); + offsetTensor = + addScalarOffsetToTensor(offsetTensor, scalarOffset, loc, rewriter); + + auto indirectStore = rewriter.create( + loc, src, offsetTensor, op.getValue(), op.getMask()); + indirectStore->setAttr(RewrittenByStridedLoadStoreRewriteTAG, + UnitAttr::get(rewriter.getContext())); + + logMatch(loc, "AddPtr", "tt.store -> tt.indirect_store", lastStride); + rewriter.eraseOp(op); + return success(); +} + +// Block-ptr path for tt.store. Mirrors tryRewriteBlockPtrLoad. +static LogicalResult tryRewriteBlockPtrStore(triton::StoreOp op, + triton::MakeTensorPtrOp mtpt, + triton::AdvanceOp advance, + RankedTensorType valueType, + PatternRewriter &rewriter) { + auto loc = op.getLoc(); + auto i64Ty = rewriter.getIntegerType(64); + ArrayRef shape = valueType.getShape(); + int64_t rank = static_cast(shape.size()); + + auto order = mtpt.getOrder(); + if (static_cast(order.size()) != rank) + return failure(); + for (int64_t i = 0; i < rank; ++i) { + if (order[i] != rank - 1 - i) + return failure(); + } + + auto strides = mtpt.getStrides(); + if (strides.empty() || static_cast(strides.size()) != rank) + return failure(); + // Stride dispatch (mirrors tryRewriteBlockPtrLoad): dynamic strides stay on + // the structured SIMD path; only static non-power-of-two strides fall + // through to SIMT stride_store. + APInt lastStrideC; + if (!matchPattern(strides.back(), m_ConstantInt(&lastStrideC))) + return failure(); + int64_t lastStride = std::abs(lastStrideC.getSExtValue()); + if (lastStride <= 1) + return failure(); + if (lastStride == 2) + return failure(); // even -> deinterleave; odd -> strided DMA + if ((lastStride & (lastStride - 1)) == 0) + return failure(); // power-of-two >= 4 -> strided DMA + + ValueRange mtptOffsets = mtpt.getOffsets(); + ValueRange advOffsets = advance ? advance.getOffsets() : ValueRange{}; + if (static_cast(mtptOffsets.size()) != rank) + return failure(); + if (advance && static_cast(advOffsets.size()) != rank) + return failure(); + + // ---- All checks passed: from here on IR mutation is committed. ---- + + // Unwrap scalar AddPtr chain on the base (see comment in + // tryRewriteBlockPtrLoad for why this is required to avoid lowering to a + // size-1 reinterpret_cast view). + Value src = mtpt.getBase(); + Value scalarBaseAdj = + rewriter.create(loc, rewriter.getI64IntegerAttr(0)); + while (auto addptr = src.getDefiningOp()) { + if (isa(addptr.getPtr().getType())) + break; + Value off = addptr.getOffset(); + Value offI64 = ensureI64Scalar(off, loc, rewriter); + if (!offI64) + return failure(); + scalarBaseAdj = rewriter.create(loc, scalarBaseAdj, offI64); + src = addptr.getPtr(); + } + + Value scalarBase = scalarBaseAdj; + SmallVector logicalOffsets; + SmallVector strideOperands; + logicalOffsets.reserve(rank); + strideOperands.reserve(rank); + for (int64_t d = 0; d < rank; ++d) { + Value baseOff = ensureI64Scalar(mtptOffsets[d], loc, rewriter); + if (!baseOff) + return failure(); + if (advance) { + Value advStep = ensureI64Scalar(advOffsets[d], loc, rewriter); + if (!advStep) + return failure(); + baseOff = rewriter.create(loc, baseOff, advStep); + } + logicalOffsets.push_back(baseOff); + Value strI64 = ensureI64Scalar(strides[d], loc, rewriter); + if (!strI64) + return failure(); + strideOperands.push_back(strI64); + Value prod = rewriter.create(loc, baseOff, strI64); + scalarBase = rewriter.create(loc, scalarBase, prod); + } + + auto boundaryCheck = op.getBoundaryCheck(); + if (valueType.getRank() >= 1 && valueType.getRank() <= 3 && + valueType.hasStaticShape()) { + FailureOr> numels = + getBlockPtrStrideNumels(op.getOperation(), op.getMask(), boundaryCheck, + mtpt, valueType, logicalOffsets, rewriter); + if (succeeded(numels)) { + Operation *strideStore = + createStrideStoreOp(loc, valueType, src, op.getValue(), scalarBase, + strideOperands, *numels, rewriter); + if (strideStore) { + logMatch(loc, + llvm::Twine("BlockPtr") + (advance ? "+Advance" : "") + + (boundaryCheck.empty() ? "" : "+Boundary"), + "tt.store -> ttasc.stride_store", lastStride); + rewriter.eraseOp(op); + return success(); + } + } + } + + if (rank <= 3) + return failure(); + + auto i64TensorTy = RankedTensorType::get(shape, i64Ty); + Value offsetTensor = + rewriter.create(loc, i64TensorTy, scalarBase); + for (int64_t d = 0; d < rank; ++d) { + auto axisTy = RankedTensorType::get({shape[d]}, rewriter.getI32Type()); + Value arange = rewriter.create( + loc, axisTy, /*start=*/0, /*end=*/static_cast(shape[d])); + Value arangeI64 = rewriter.create( + loc, RankedTensorType::get({shape[d]}, i64Ty), arange); + Value strI64 = ensureI64Scalar(strides[d], loc, rewriter); + Value strSplat = rewriter.create( + loc, RankedTensorType::get({shape[d]}, i64Ty), strI64); + Value stridedArange = + rewriter.create(loc, arangeI64, strSplat); + Value broadcasted = expandAndBroadcastForAxis( + stridedArange, static_cast(d), shape, i64Ty, loc, rewriter); + offsetTensor = + rewriter.create(loc, offsetTensor, broadcasted); + } + + // ---- boundary_check: build OOB mask (store has no "other") ---- + Value mask = op.getMask(); + if (!boundaryCheck.empty()) { + SmallVector effOffsets; + for (int64_t d = 0; d < rank; ++d) { + Value off = mtptOffsets[d]; + if (advance) { + off = rewriter.create(loc, off, advOffsets[d]); + } + effOffsets.push_back(off); + } + Value boundaryMask = buildBoundaryMask(loc, rewriter, shape, boundaryCheck, + effOffsets, mtpt.getShape()); + mask = mask ? rewriter.create(loc, mask, boundaryMask) + .getResult() + : boundaryMask; + } + + auto indirectStore = rewriter.create( + loc, src, offsetTensor, op.getValue(), mask); + indirectStore->setAttr(RewrittenByStridedLoadStoreRewriteTAG, + UnitAttr::get(rewriter.getContext())); + + logMatch(loc, + llvm::Twine("BlockPtr") + (advance ? "+Advance" : "") + + (boundaryCheck.empty() ? "" : "+Boundary"), + "tt.store -> tt.indirect_store", lastStride); + rewriter.eraseOp(op); + return success(); +} + +} // namespace + +LogicalResult LoadConverter::matchAndRewrite(triton::LoadOp op, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + (void)loc; + + // ---- Re-entry / cross-step guards ---- + if (op->hasAttr(InspectedByStridedLoadStoreRewriteTAG)) + return failure(); + if (op->hasAttr(RewrittenByStridedLoadStoreRewriteTAG)) + return failure(); + if (op->hasAttr(mlir::triton::memory_access::ImplicitPermuteHandledTAG)) + return failure(); + if (op->hasAttr(mlir::ConverterUtils::discreteAttrName)) + return failure(); + + // ---- Common early checks (apply to both AddPtr and block-ptr paths) ---- + // boundary_check is legal only on make_tensor_ptr loads; block-ptr handler + // builds an OOB mask + padding "other" for it. AddPtr loads should never + // have boundary_check (defensive bail kept in tryRewriteAddPtrLoad below). + auto resultType = dyn_cast(op.getResult().getType()); + if (!resultType) + return failure(); + if (resultType.getShape().size() > kFastPathRankLimit) + return failure(); + + // ---- Dispatch on source op ---- + Value ptr = op.getPtr(); + if (auto addPtrOp = ptr.getDefiningOp()) { + if (!op.getBoundaryCheck().empty()) + return failure(); // defensive + return tryRewriteAddPtrLoad(op, addPtrOp, resultType, rewriter); + } + if (auto mtptOp = ptr.getDefiningOp()) { + return tryRewriteBlockPtrLoad(op, mtptOp, /*advance=*/nullptr, resultType, + rewriter); + } + if (auto advOp = ptr.getDefiningOp()) { + // V1: one-level advance only. Nested advance / scf.for iter-arg block + // ptr falls through to the legacy strided memref.copy path. + if (auto baseMtpt = + advOp.getPtr().getDefiningOp()) { + return tryRewriteBlockPtrLoad(op, baseMtpt, advOp, resultType, rewriter); + } + return failure(); + } + return failure(); +} + +LogicalResult StoreConverter::matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const { + // ---- Re-entry / cross-step guards (same convention as LoadConverter) ---- + if (op->hasAttr(InspectedByStridedLoadStoreRewriteTAG)) + return failure(); + if (op->hasAttr(RewrittenByStridedLoadStoreRewriteTAG)) + return failure(); + if (op->hasAttr(mlir::triton::memory_access::ImplicitPermuteHandledTAG)) + return failure(); + if (op->hasAttr(mlir::ConverterUtils::discreteAttrName)) + return failure(); + + // boundary_check is legal only on make_tensor_ptr stores; block-ptr handler + // builds an OOB mask for it. AddPtr stores should never have boundary_check + // (defensive bail). + auto valueType = dyn_cast(op.getValue().getType()); + if (!valueType) + return failure(); + if (valueType.getShape().size() > kFastPathRankLimit) + return failure(); + + Value ptr = op.getPtr(); + if (auto addPtrOp = ptr.getDefiningOp()) { + if (!op.getBoundaryCheck().empty()) + return failure(); // defensive + return tryRewriteAddPtrStore(op, addPtrOp, valueType, rewriter); + } + if (auto mtptOp = ptr.getDefiningOp()) { + return tryRewriteBlockPtrStore(op, mtptOp, /*advance=*/nullptr, valueType, + rewriter); + } + if (auto advOp = ptr.getDefiningOp()) { + if (auto baseMtpt = + advOp.getPtr().getDefiningOp()) { + return tryRewriteBlockPtrStore(op, baseMtpt, advOp, valueType, rewriter); + } + return failure(); + } + return failure(); +} + +} // namespace StridedLoadStoreRewrite diff --git a/third_party/ascend/lib/TritonToGraph/MemorySsaBuilder.cpp b/third_party/ascend/lib/TritonToGraph/MemorySsaBuilder.cpp new file mode 100644 index 0000000000..e0b6aa9460 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/MemorySsaBuilder.cpp @@ -0,0 +1,631 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/MemorySsaBuilder.h" +#include "TritonToGraph/ControlFlowGraph.h" +#include "TritonToGraph/DataflowGraph.h" +#include "TritonToGraph/tensor.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "triton/Dialect/Triton/IR/OpInterfaces.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/raw_ostream.h" + +#define DEBUG_TYPE "memory-ssa-builder" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +//===----------------------------------------------------------------------===// +// MemorySSABuilder +//===----------------------------------------------------------------------===// + +MemorySSABuilder::~MemorySSABuilder() = default; + +void MemorySSABuilder::build() { + LLVM_DEBUG(llvm::dbgs() << "=== Starting Memory SSA Build ===\n"); + + // 步骤1: 拓扑排序,确定处理顺序 + std::vector topoOrder; + { + // 简单的拓扑排序实现 + DenseSet visited; + std::function dfs = [&](BasicBlock *bb) { + if (visited.contains(bb)) + return; + visited.insert(bb); + for (BasicBlock *succ : bb->getSuccessors()) { + dfs(succ); + } + topoOrder.push_back(bb); + }; + + // 从入口块开始DFS + dfs(cfg.getEntryBlock()); + std::reverse(topoOrder.begin(), topoOrder.end()); + } + + LLVM_DEBUG(llvm::dbgs() << "Topological order: " << topoOrder.size() + << " blocks\n"); + + // 步骤2: 初始化函数的参数 + createParameterDefinitions(); + + LLVM_DEBUG(llvm::dbgs() << "Created parameter definitions\n"); + + // 步骤3: 按拓扑序遍历每个BasicBlock + for (BasicBlock *bb : topoOrder) { + LLVM_DEBUG(llvm::dbgs() << "Processing BB" << bb->getId() << "\n"); + + // 处理block + processBasicBlock(bb); + } + + LLVM_DEBUG(llvm::dbgs() << "=== Memory SSA Build Complete ===\n" + << "Processed blocks: " << topoOrder.size() << "\n" + << "Total definitions: " << allDefinitions.size() + << "\n"); +} + +void MemorySSABuilder::createParameterDefinitions() { + triton::FuncOp func = cfg.getFunction(); + + LLVM_DEBUG(llvm::dbgs() << "Creating parameter definitions for " + << func.getName() << "\n"); + + // 遍历函数参数 + for (BlockArgument arg : func.getArguments()) { + Type argType = arg.getType(); + + // 检查是否是我们关心的类型 + if (isTensorType(argType)) { + // 创建参数名称 + std::string paramName = "param_" + std::to_string(arg.getArgNumber()); + + // 如果是指针类型,从aliasAnalysis获取tensor对象 + TensorObject *tensor = nullptr; + if (aliasAnalysis.isPointerType(argType)) { + tensor = aliasAnalysis.getTensorObject(arg); + } + + // 如果没有找到tensor对象,创建一个新的 + if (!tensor) { + // 从类型推断shape和element type + SmallVector shape; + Type elementType; + + extractShapeAndElementType(argType, shape, elementType); + + auto tensorOwner = std::make_unique( + paramName, shape, argType, elementType, + TensorObject::TensorKind::GLOBAL_MEMORY); + tensor = tensorOwner.get(); + ownedTensorObjects.push_back(std::move(tensorOwner)); + } + + tensor->print(llvm::outs()); + llvm::outs() << "\n"; + + // 为入参创建definition + MemorySSADef *def = createDefinition(tensor, nullptr); + + // 记录到dataFlowInfo + dataFlowInfo.addMemoryDefinition(arg, def); + + LLVM_DEBUG(llvm::dbgs() + << " Created parameter definition: " << def->getId() << "\n"); + } + } +} + +void MemorySSABuilder::processBasicBlock(BasicBlock *bb) { + if (!bb) + return; + + // 处理block内的所有指令 + for (auto &instPtr : bb->getInstructions()) { + Instruction *inst = instPtr.get(); + inst->print(llvm::outs()); + llvm::outs() << "\n"; + processInstruction(inst); + MemorySSAInfo &ssaInfo = inst->getMemorySSAInfo(); + ssaInfo.print(llvm::outs()); + llvm::outs() << "\n"; + } +} + +void MemorySSABuilder::processInstruction(Instruction *inst) { + if (!inst) + return; + + Operation *op = inst->getOperation(); + if (!op) + return; + + LLVM_DEBUG(llvm::dbgs() << "Processing: " << op->getName() << "\n"); + + MemorySSAInfo &ssaInfo = inst->getMemorySSAInfo(); + + // 1. 处理operands:创建uses + LLVM_DEBUG(llvm::dbgs() << " Processing operands...\n"); + + for (OpOperand &operand : op->getOpOperands()) { + Value operandValue = operand.get(); + unsigned operandIdx = operand.getOperandNumber(); + + // 检查是否是tensor或pointer类型 + if (isTensorType(operandValue.getType()) || + aliasAnalysis.isPointerType(operandValue.getType())) { + + // 查找operand的definition + MemorySSADef *def = dataFlowInfo.getMemoryDefinition(operandValue); + + if (def) { + // 创建use + MemorySSAUse use = createUse(def, op, operandIdx); + ssaInfo.uses.push_back(use); + + // 记录到全局map + dataFlowInfo.addMemoryUse(operandValue, use); + + LLVM_DEBUG(llvm::dbgs() + << " MemorySSAUse: " << def->getId() << " in " + << op->getName() << " [operand #" << operandIdx << "]\n"); + } + } + } + + // 2. 按操作类型处理:store、load、tensor writer、pointer op + LLVM_DEBUG(llvm::dbgs() << " Processing by operation type...\n"); + + // 对store操作(内存写入)- 为第一个operand指向的tensor创建新definition + if (isMemoryWriter(op)) { + LLVM_DEBUG(llvm::dbgs() << " Memory writer: " << op->getName() << "\n"); + + // store的第一个operand是ptr(第二个是value) + // 针对Triton算子场景做了简化,此处不是流敏感分析 + if (op->getNumOperands() >= 2) { + Value ptr = op->getOperand(0); + if (isTensorType(ptr.getType()) || + aliasAnalysis.isPointerType(ptr.getType())) { + // 获取ptr当前的definition(修改前的状态) + MemorySSADef *oldDef = dataFlowInfo.getMemoryDefinition(ptr); + if (oldDef) { + // store会修改内存,为同一个tensor创建新的definition + TensorObject *tensor = oldDef->getTensor(); + MemorySSADef *newDef = createDefinition(tensor, op); + + // store之后,ptr指向的内存状态改变,更新definition + dataFlowInfo.addMemoryDefinition(ptr, newDef); + + LLVM_DEBUG(llvm::dbgs() << " Store from: " << oldDef->getId() + << " to: " << newDef->getId() << "\n"); + } + } + } + } + // 对load操作(内存读取)- 第一个operand的definition作为result的definition + else if (isMemoryReader(op)) { + LLVM_DEBUG(llvm::dbgs() << " Memory reader: " << op->getName() << "\n"); + + if (op->getNumOperands() > 0 && op->getNumResults() > 0) { + Value ptr = op->getOperand(0); + Value result = op->getResult(0); + + // 获取ptr的definition + MemorySSADef *ptrDef = dataFlowInfo.getMemoryDefinition(ptr); + if (ptrDef) { + ssaInfo.definitions.push_back(ptrDef); + dataFlowInfo.addMemoryDefinition(result, ptrDef); + + LLVM_DEBUG(llvm::dbgs() + << " Load creates new def: " << ptrDef->getId() + << " from " << ptrDef->getId() << "\n"); + } + } + } + // 对返回新Tensor的操作(tensor writer) + else if (isTensorWriter(op)) { + LLVM_DEBUG(llvm::dbgs() << " Tensor writer: " << op->getName() << "\n"); + + for (Value result : op->getResults()) { + Type resultType = result.getType(); + + // 检查是否是tensor类型 + if (isTensorType(resultType)) { + // 创建tensor对象 + TensorObject *tensor = createTensorObject(op); + + // Tensor writer:创建新definition + MemorySSADef *newDef = createDefinition(tensor, op); + ssaInfo.definitions.push_back(newDef); + dataFlowInfo.addMemoryDefinition(result, newDef); + + LLVM_DEBUG(llvm::dbgs() << " Tensor definition: " << newDef->getId() + << " for " << result << "\n"); + } + } + } + // 对pointer操作(addptr, make_tensor_ptr, [broadcast, splat]) + else if (isPointerOp(op)) { + LLVM_DEBUG(llvm::dbgs() + << " Pointer operation: " << op->getName() << "\n"); + + for (Value result : op->getResults()) { + Type resultType = result.getType(); + + // 检查是否是pointer类型 + if (aliasAnalysis.isPointerType(resultType)) { + // Pointer op:复用base pointer的definition(alias) + Value basePtr = aliasAnalysis.getBasePointer(result); + MemorySSADef *baseDef = dataFlowInfo.getMemoryDefinition(basePtr); + + if (baseDef) { + ssaInfo.definitions.push_back(baseDef); + dataFlowInfo.addMemoryDefinition(result, baseDef); + + LLVM_DEBUG(llvm::dbgs() << " Pointer alias: " << baseDef->getId() + << " for " << result << "\n"); + } + } + } + } + + // 3. 特殊处理控制流操作 + if (auto ifOp = dyn_cast(op)) { + processIfOp(ifOp, inst, nullptr, nullptr); + } else if (auto forOp = dyn_cast(op)) { + processForOp(forOp, inst, nullptr); + } else if (auto whileOp = dyn_cast(op)) { + // processWhileOp(whileOp, inst, nullptr, nullptr); + } + + LLVM_DEBUG(llvm::dbgs() << " Done\n"); +} + +void MemorySSABuilder::processIfOp(scf::IfOp ifOp, Instruction *inst, + BasicBlock *thenEntryBB, + BasicBlock *elseEntryBB) { + // 实现scf.if的phi节点处理 + // 从then和else区域收集yield的values,创建phi definitions + + LLVM_DEBUG(llvm::dbgs() << "Processing IfOp: " << ifOp << "\n"); + + // 获取if指令 + // scf::IfOp ifOp = + // cast(ifCondBB->getInstruction(0)->getOperation()); + + // 为每个result创建phi节点 + for (size_t i = 0; i < ifOp.getNumResults(); ++i) { + Value ifResult = ifOp.getResult(i); + Type resultType = ifResult.getType(); + + // 只处理tensor/pointer类型 + if (!isTensorType(resultType) && !aliasAnalysis.isPointerType(resultType)) { + continue; + } + + // 从then区域获取yield的value + Operation *thenYield = + MemorySSABuilderHelper::getYieldOp(ifOp.getThenRegion()); + Value thenValue = thenYield ? thenYield->getOperand(i) : Value(); + MemorySSADef *thenDef = + thenValue ? dataFlowInfo.getMemoryDefinition(thenValue) : nullptr; + + // 从else区域获取yield的value + Operation *elseYield = + MemorySSABuilderHelper::getYieldOp(ifOp.getElseRegion()); + Value elseValue = elseYield ? elseYield->getOperand(i) : Value(); + MemorySSADef *elseDef = + elseValue ? dataFlowInfo.getMemoryDefinition(elseValue) : nullptr; + + // 如果then和else都返回相同的definition,可以直接使用 + if (thenDef && elseDef && thenDef == elseDef) { + dataFlowInfo.addMemoryDefinition(ifResult, thenDef); + + LLVM_DEBUG(llvm::dbgs() + << " If result #" << i + << " uses same definition: " << thenDef->getId() << "\n"); + continue; + } + + // 创建phi definition + if (thenDef || elseDef) { + TensorObject *tensor = thenDef ? thenDef->getTensor() + : elseDef ? elseDef->getTensor() + : nullptr; + + if (tensor) { + std::string phiName = "phi_" + std::to_string(ifOp.getNumResults()) + + "_" + std::to_string(i); + auto phiTensorOwner = std::make_unique( + phiName, tensor->getShape(), resultType, tensor->getElementType(), + tensor->getKind()); + TensorObject *phiTensor = phiTensorOwner.get(); + ownedTensorObjects.push_back(std::move(phiTensorOwner)); + + MemorySSADef *phiDef = createDefinition(phiTensor, ifOp.getOperation()); + + // 记录if result的definition + dataFlowInfo.addMemoryDefinition(ifResult, phiDef); + + // 为phi节点的operands创建uses + if (thenDef && thenValue) { + MemorySSAUse thenUse(thenDef, ifOp.getOperation(), /*operandIdx=*/i); + dataFlowInfo.addMemoryUse(thenValue, thenUse); + } + if (elseDef && elseValue) { + MemorySSAUse elseUse(elseDef, ifOp.getOperation(), /*operandIdx=*/i); + dataFlowInfo.addMemoryUse(elseValue, elseUse); + } + + // 创建phi信息 + PhiInfo phiInfo; + phiInfo.type = PhiInfo::IF_RESULT; + phiInfo.loopHeader = nullptr; + phiInfo.comingFrom.initialValue = thenDef; + phiInfo.comingFrom.yieldValue = elseDef; + + dataFlowInfo.addPhi(ifResult, phiInfo); + + LLVM_DEBUG(llvm::dbgs() << " Phi: " << phiDef->getId() + << " for if result #" << i << "\n"); + } + } + } +} + +void MemorySSABuilder::processForOp(scf::ForOp forOp, Instruction *inst, + BasicBlock *loopBodyEntryBB) { + // 实现scf.for的iter_args处理 + // iter_args在所有迭代中共享同一个definition + + LLVM_DEBUG(llvm::dbgs() << "Processing ForOp: " << forOp << "\n"); + + unsigned numIterArgs = forOp.getInitArgs().size(); + + for (unsigned i = 0; i < numIterArgs; i++) { + Value iterArg = forOp.getRegionIterArg(i); + Value initValue = forOp.getInitArgs()[i]; + + // 查找initValue的definition + MemorySSADef *initDef = dataFlowInfo.getMemoryDefinition(initValue); + + if (initDef) { + // iter_arg使用同一个definition,不创建新版本 + dataFlowInfo.addMemoryDefinition(iterArg, initDef); + + // 创建PhiInfo(用于跟踪循环依赖) + PhiInfo phiInfo; + phiInfo.type = PhiInfo::ITER_ARG; + phiInfo.loopHeader = nullptr; // 需要根据CFG确定 + phiInfo.comingFrom.initialValue = initDef; + phiInfo.comingFrom.yieldValue = nullptr; // 将在处理yield时更新 + + dataFlowInfo.addPhi(iterArg, phiInfo); + + // 为initValue创建use + MemorySSAUse initUse(initDef, forOp.getOperation(), /*operandIdx=*/i); + dataFlowInfo.addMemoryUse(initValue, initUse); + + LLVM_DEBUG(llvm::dbgs() << " IterArg #" << i << ": " << iterArg << " -> " + << initDef->getId() << "\n"); + } + } + + // 处理yield操作(在循环体中) + Operation *yieldOp = MemorySSABuilderHelper::getYieldOp(forOp.getRegion()); + if (yieldOp) { + for (unsigned i = 0; i < yieldOp->getNumOperands(); i++) { + Value yieldedValue = yieldOp->getOperand(i); + Value iterArg = forOp.getRegionIterArg(i); + + MemorySSADef *yieldedDef = dataFlowInfo.getMemoryDefinition(yieldedValue); + + if (yieldedDef) { + // 更新PhiInfo + if (PhiInfo *phiInfo = &dataFlowInfo.getPhi(iterArg)) { + phiInfo->comingFrom.yieldValue = yieldedDef; + + // 为yieldedValue创建use + MemorySSAUse yieldUse(yieldedDef, yieldOp, /*operandIdx=*/i); + dataFlowInfo.addMemoryUse(yieldedValue, yieldUse); + + LLVM_DEBUG(llvm::dbgs() << " Yield #" << i << ": " << yieldedValue + << " updates iter_arg\n"); + } + } + } + } +} + +MemorySSADef *MemorySSABuilder::createDefinition(TensorObject *tensor, + Operation *op) { + unsigned version = isParameter(op) ? 0 : ++nextVersion[tensor]; + auto definition = std::make_unique(tensor, op, version); + MemorySSADef *result = definition.get(); + allDefinitions.push_back(std::move(definition)); + return result; +} + +MemorySSAUse MemorySSABuilder::createUse(MemorySSADef *def, Operation *userOp, + unsigned operandIdx) { + return MemorySSAUse(def, userOp, operandIdx); +} + +TensorObject *MemorySSABuilder::createTensorObject(Operation *op) { + if (!op) + return nullptr; + + // 根据操作创建tensor对象,使用独立的Tensor ID确保唯一性 + std::string name = getOpName(op); + Type resultType = op->getResultTypes().front(); + + SmallVector shape; + Type elementType; + + // 使用Tensor.h中的辅助函数提取shape和element type + extractShapeAndElementType(resultType, shape, elementType); + + // 设置默认的kind(可以根据操作类型推断) + TensorObject::TensorKind kind = TensorObject::TensorKind::GLOBAL_MEMORY; + + auto tensorOwner = std::make_unique(name, shape, resultType, + elementType, kind); + TensorObject *tensor = tensorOwner.get(); + ownedTensorObjects.push_back(std::move(tensorOwner)); + + // 缓存tensor对象 + if (!op->getResults().empty()) { + tensorObjectCache[op->getResult(0)] = tensor; + } + + return tensor; +} + +std::string MemorySSABuilder::getOpName(Operation *op) { + if (!op) + return "unknown"; + + // 基于操作类型和独立的Tensor ID生成名称 + std::string opName = op->getName().getStringRef().str(); + std::replace(opName.begin(), opName.end(), '.', '_'); + + // 使用独立的Tensor ID生成器,确保唯一性 + return opName + "_tensor_" + std::to_string(++nextTensor[opName]); +} + +//===----------------------------------------------------------------------===// +// MemorySSABuilderHelper +//===----------------------------------------------------------------------===// + +namespace mlir { +namespace triton { +namespace cfg { +namespace MemorySSABuilderHelper { + +Type getResultType(Operation *op, unsigned resultIdx) { + if (!op || resultIdx >= op->getNumResults()) + return Type(); + return op->getResultTypes()[resultIdx]; +} + +SmallVector getShapeFromValue(Value value) { + Type type = value.getType(); + SmallVector shape; + + if (auto rankedType = mlir::dyn_cast(type)) { + shape.append(rankedType.getShape().begin(), rankedType.getShape().end()); + } + + return shape; +} + +bool shapesEqual(ArrayRef shape1, ArrayRef shape2) { + if (shape1.size() != shape2.size()) + return false; + return std::equal(shape1.begin(), shape1.end(), shape2.begin()); +} + +Operation *getYieldOp(Region ®ion) { + if (region.empty()) + return nullptr; + + Block &block = region.back(); + if (block.empty()) + return nullptr; + + Operation &lastOp = block.back(); + if (isa(&lastOp)) { + return &lastOp; + } + + return nullptr; +} + +std::string createUniqueTensorName(StringRef prefix, size_t id) { + return prefix.str() + "_" + std::to_string(id); +} + +bool shouldCreateNewVersion(Operation *op, MemorySSADef *currentDef) { + if (!op || !currentDef) + return true; + + // 如果操作会修改tensor内容,则创建新版本 + // 例如:tt.store、tt.trans等 + if (isa(op)) + return true; + if (isa(op)) + return true; + + // 其他操作可能复用当前definition + return false; +} + +} // namespace MemorySSABuilderHelper +} // namespace cfg +} // namespace triton +} // namespace mlir + +bool MemorySSABuilder::isPointerBroadcastOrSplat(mlir::Operation *op) const { + if (auto broadcastOp = mlir::dyn_cast(op)) { + Type elemType = getElementTypeOrSelf(broadcastOp.getResult().getType()); + return mlir::isa(elemType); + } + + // 处理 SplatOp: 检查第一个 operand (src) 是否是指针 + if (auto splatOp = mlir::dyn_cast(op)) { + return mlir::isa(splatOp.getSrc().getType()); + } + + return false; +} + +// 判断是否是返回新Tensor的操作(根据返回值类型判断,排除load) +bool MemorySSABuilder::isTensorWriter(Operation *op) const { + if (!op || op->getNumResults() == 0) + return false; + + // 检查返回值类型 + for (Value result : op->getResults()) { + Type resultType = result.getType(); + // 如果是RankedTensorType(不是指针),则是TensorWriter + if (mlir::isa(resultType)) { + // 但排除load(虽然load返回tensor,但它是从内存读取,不是"写入"或"创建") + if (mlir::isa(op) || mlir::isa(op)) + return false; + + if (mlir::isa(op)) + return false; + + if (isPointerBroadcastOrSplat(op)) + return false; + + if (auto ptrType = mlir::dyn_cast( + getElementTypeOrSelf(resultType))) { + return false; + } + return true; + } + } + return false; +} diff --git a/third_party/ascend/lib/TritonToGraph/MergeConcatLoadBuffer.cpp b/third_party/ascend/lib/TritonToGraph/MergeConcatLoadBuffer.cpp new file mode 100644 index 0000000000..7df6faa4c7 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/MergeConcatLoadBuffer.cpp @@ -0,0 +1,577 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/Passes.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Debug.h" + +#include + +#define DEBUG_TYPE "merge-concat-load-buffer" + +namespace mlir { +namespace triton { +namespace cfg { +#define GEN_PASS_DEF_MERGECONCATLOADBUFFER +#include "ascend/include/TritonToGraph/Passes.h.inc" +} // namespace cfg +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace mlir::triton::cfg; + +namespace { + +//===----------------------------------------------------------------------===// +// OpFoldResult / box geometry +//===----------------------------------------------------------------------===// + +static bool isSameOFR(OpFoldResult a, OpFoldResult b) { + if (a == b) + return true; + std::optional ca = getConstantIntValue(a); + std::optional cb = getConstantIntValue(b); + return ca && cb && *ca == *cb; +} + +static bool allUnitStrides(ArrayRef strides) { + return llvm::all_of(strides, + [](OpFoldResult s) { return isConstantIntValue(s, 1); }); +} + +static std::optional addConst(OpFoldResult offset, OpFoldResult size) { + std::optional off = getConstantIntValue(offset); + std::optional sz = getConstantIntValue(size); + if (!off || !sz) + return std::nullopt; + // No real tile comes anywhere near overflowing here, but a box that does is + // reported as unknown rather than wrapping around into a bogus bound. + if (*off < 0 || *sz < 0 || *off > std::numeric_limits::max() - *sz) + return std::nullopt; + return *off + *sz; +} + +struct Box { + SmallVector offsets; + SmallVector sizes; + + unsigned rank() const { return offsets.size(); } +}; + +static Box makeFullBox(ArrayRef shape, MLIRContext *ctx) { + Builder b(ctx); + Box box; + box.offsets.resize(shape.size(), b.getIndexAttr(0)); + for (int64_t dim : shape) + box.sizes.push_back(b.getIndexAttr(dim)); + return box; +} + +// Prove [wOff, wOff+wSz) ⊆ [iOff, iOff+iSz). Dynamic write sizes are assumed +// to be in-bounds for a well-formed subview of a static alloc when the insert +// region covers that entire dimension. +static bool intervalContained(OpFoldResult wOff, OpFoldResult wSz, + OpFoldResult iOff, OpFoldResult iSz, + int64_t allocDim) { + if (isSameOFR(wOff, iOff) && isSameOFR(wSz, iSz)) + return true; + + std::optional wOffC = getConstantIntValue(wOff); + std::optional wSzC = getConstantIntValue(wSz); + std::optional iOffC = getConstantIntValue(iOff); + std::optional iSzC = getConstantIntValue(iSz); + if (wOffC && wSzC && iOffC && iSzC) + return *wOffC >= *iOffC && (*wOffC + *wSzC) <= (*iOffC + *iSzC); + + if (isSameOFR(wOff, iOff) && isConstantIntValue(wOff, 0) && iSzC && + *iSzC == allocDim) + return true; + return false; +} + +static bool boxContainedIn(const Box &inner, const Box &outer, + ArrayRef allocShape) { + if (inner.rank() != outer.rank() || inner.rank() != allocShape.size()) + return false; + for (unsigned d = 0; d < inner.rank(); ++d) { + if (!intervalContained(inner.offsets[d], inner.sizes[d], outer.offsets[d], + outer.sizes[d], allocShape[d])) + return false; + } + return true; +} + +static bool intervalsDisjoint(OpFoldResult aOff, OpFoldResult aSz, + OpFoldResult bOff, OpFoldResult bSz) { + std::optional aEnd = addConst(aOff, aSz); + std::optional bEnd = addConst(bOff, bSz); + std::optional aOffC = getConstantIntValue(aOff); + std::optional bOffC = getConstantIntValue(bOff); + if (aEnd && bOffC && *aEnd <= *bOffC) + return true; + if (bEnd && aOffC && *bEnd <= *aOffC) + return true; + return false; +} + +static bool boxesDisjoint(const Box &a, const Box &b) { + if (a.rank() != b.rank()) + return false; + for (unsigned d = 0; d < a.rank(); ++d) { + if (intervalsDisjoint(a.offsets[d], a.sizes[d], b.offsets[d], b.sizes[d])) + return true; + } + return false; +} + +// Union boxes that differ on exactly one dimension and abut on that dimension. +static std::optional unionBoxesAlongOneDim(ArrayRef boxes, + MLIRContext *ctx) { + if (boxes.empty()) + return std::nullopt; + if (boxes.size() == 1) + return Box(boxes.front()); + + unsigned rank = boxes.front().rank(); + for (const Box &box : boxes) { + if (box.rank() != rank) + return std::nullopt; + } + + SmallVector diffDims; + for (unsigned d = 0; d < rank; ++d) { + bool same = llvm::all_of(boxes, [&](const Box &box) { + return isSameOFR(box.offsets[d], boxes.front().offsets[d]) && + isSameOFR(box.sizes[d], boxes.front().sizes[d]); + }); + if (!same) + diffDims.push_back(d); + } + if (diffDims.size() != 1) + return std::nullopt; + + unsigned dim = diffDims.front(); + struct Interval { + int64_t start; + int64_t end; + }; + SmallVector intervals; + intervals.reserve(boxes.size()); + for (const Box &box : boxes) { + std::optional start = getConstantIntValue(box.offsets[dim]); + std::optional end = addConst(box.offsets[dim], box.sizes[dim]); + if (!start || !end || *end < *start) + return std::nullopt; + intervals.push_back({*start, *end}); + } + llvm::sort(intervals, + [](Interval lhs, Interval rhs) { return lhs.start < rhs.start; }); + for (size_t i = 1; i < intervals.size(); ++i) { + if (intervals[i].start != intervals[i - 1].end) + return std::nullopt; + } + + Builder b(ctx); + Box result = boxes.front(); + result.offsets[dim] = b.getIndexAttr(intervals.front().start); + result.sizes[dim] = + b.getIndexAttr(intervals.back().end - intervals.front().start); + return result; +} + +//===----------------------------------------------------------------------===// +// Alloc user collection +//===----------------------------------------------------------------------===// + +struct FillInfo { + linalg::FillOp fill; + scf::IfOp wrappingIf; + Value fillValue; + bool isBare = false; +}; + +struct AllocInfo { + memref::AllocOp alloc; + bufferization::ToTensorOp toTensor; + SmallVector writeBoxes; + SmallVector copies; + FillInfo fill; +}; + +static bool sameFillValue(Value a, Value b) { + if (a == b) + return true; + auto ca = a.getDefiningOp(); + auto cb = b.getDefiningOp(); + return ca && cb && ca.getValue() == cb.getValue(); +} + +static scf::IfOp wrappingTrivialFillIf(linalg::FillOp fill) { + auto ifOp = dyn_cast(fill->getParentOp()); + if (!ifOp || ifOp.getNumResults() != 0) + return nullptr; + + unsigned thenCount = 0; + for (Operation &op : ifOp.getThenRegion().front()) { + if (isa(op)) + continue; + if (&op != fill.getOperation()) + return nullptr; + ++thenCount; + } + if (thenCount != 1) + return nullptr; + + if (!ifOp.getElseRegion().empty()) { + for (Operation &op : ifOp.getElseRegion().front()) { + if (!isa(op)) + return nullptr; + } + } + return ifOp; +} + +// Recursively find the unique memref.copy that writes `v` (looking through +// memref.cast). Returns null if `v` has any other user or is used as a copy +// source. +static memref::CopyOp getUniqueCopyWriting(Value v) { + memref::CopyOp found; + for (Operation *user : v.getUsers()) { + if (auto copy = dyn_cast(user)) { + if (copy.getTarget() != v || found) + return nullptr; + found = copy; + continue; + } + if (auto cast = dyn_cast(user)) { + memref::CopyOp inner = getUniqueCopyWriting(cast.getResult()); + if (!inner || found) + return nullptr; + found = inner; + continue; + } + return nullptr; + } + return found; +} + +// A whole-buffer fill only acts as padding if it runs before the copies that +// overwrite parts of it. A fill placed after them is what the concatenated +// value actually observes, so it is neither dead nor safe to reorder. +static bool fillPrecedesCopies(const FillInfo &fill, + ArrayRef copies) { + if (!fill.fill) + return true; + scf::IfOp wrappingIf = fill.wrappingIf; + linalg::FillOp fillOp = fill.fill; + Operation *fillPoint = + wrappingIf ? wrappingIf.getOperation() : fillOp.getOperation(); + for (memref::CopyOp copy : copies) { + Operation *copyPoint = copy.getOperation(); + if (fillPoint->getBlock() != copyPoint->getBlock() || + !fillPoint->isBeforeInBlock(copyPoint)) + return false; + } + return true; +} + +static std::optional collectAllocInfo(Value mem) { + auto alloc = mem.getDefiningOp(); + if (!alloc || !alloc.getDynamicSizes().empty()) + return std::nullopt; + + auto memType = dyn_cast(alloc.getType()); + if (!memType || !memType.hasStaticShape()) + return std::nullopt; + + AllocInfo info; + info.alloc = alloc; + + for (Operation *user : alloc.getResult().getUsers()) { + if (auto fill = dyn_cast(user)) { + if (info.fill.fill) + return std::nullopt; + if (fill.getDpsInits().empty() || fill.getDpsInputs().empty() || + fill.getDpsInits().front() != alloc.getResult()) + return std::nullopt; + info.fill.fill = fill; + info.fill.fillValue = fill.getDpsInputs().front(); + info.fill.wrappingIf = wrappingTrivialFillIf(fill); + info.fill.isBare = fill->getBlock() == alloc->getBlock(); + continue; + } + + if (auto toTensor = dyn_cast(user)) { + if (info.toTensor) + return std::nullopt; + if (!toTensor.getRestrict() || !toTensor.getWritable()) + return std::nullopt; + if (toTensor.getBuffer() != alloc.getResult()) + return std::nullopt; + info.toTensor = toTensor; + continue; + } + + if (auto subview = dyn_cast(user)) { + if (!allUnitStrides(subview.getMixedStrides())) + return std::nullopt; + memref::CopyOp copy = getUniqueCopyWriting(subview.getResult()); + if (!copy) + return std::nullopt; + info.copies.push_back(copy); + info.writeBoxes.push_back( + Box{subview.getMixedOffsets(), subview.getMixedSizes()}); + continue; + } + + return std::nullopt; + } + + if (!info.toTensor || info.writeBoxes.empty()) + return std::nullopt; + if (!fillPrecedesCopies(info.fill, info.copies)) + return std::nullopt; + return info; +} + +static void eraseFill(PatternRewriter &rewriter, const FillInfo &fill) { + if (!fill.fill) + return; + if (fill.wrappingIf) + rewriter.eraseOp(fill.wrappingIf); + else + rewriter.eraseOp(fill.fill); +} + +//===----------------------------------------------------------------------===// +// Rewrite pattern +//===----------------------------------------------------------------------===// + +class MergeConcatLoadBufferPattern + : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(tensor::InsertSliceOp insertOp, + PatternRewriter &rewriter) const override { + if (!allUnitStrides(insertOp.getMixedStrides())) + return rewriter.notifyMatchFailure(insertOp, "non-unit insert strides"); + + auto extractOp = + insertOp.getSource().getDefiningOp(); + if (!extractOp || !extractOp->hasOneUse()) + return rewriter.notifyMatchFailure(insertOp, + "source is not a single-use extract"); + if (!allUnitStrides(extractOp.getMixedStrides())) + return rewriter.notifyMatchFailure(insertOp, "non-unit extract strides"); + + // Identity move: extract and insert must use the same offsets/sizes so + // the source buffer can be RAUW'd onto the dest buffer without shifting. + if (extractOp.getMixedOffsets().size() != + insertOp.getMixedOffsets().size() || + extractOp.getMixedSizes().size() != insertOp.getMixedSizes().size()) + return failure(); + for (auto [eOff, iOff] : + llvm::zip(extractOp.getMixedOffsets(), insertOp.getMixedOffsets())) { + if (!isSameOFR(eOff, iOff)) + return rewriter.notifyMatchFailure(insertOp, + "extract/insert offsets differ"); + } + for (auto [eSz, iSz] : + llvm::zip(extractOp.getMixedSizes(), insertOp.getMixedSizes())) { + if (!isSameOFR(eSz, iSz)) + return rewriter.notifyMatchFailure(insertOp, + "extract/insert sizes differ"); + } + + auto toTensorA = + extractOp.getSource().getDefiningOp(); + auto toTensorB = + insertOp.getDest().getDefiningOp(); + if (!toTensorA || !toTensorB || toTensorA == toTensorB) + return failure(); + if (!toTensorA->hasOneUse() || !toTensorB->hasOneUse()) + return rewriter.notifyMatchFailure( + insertOp, "to_tensor must be used only by this concat"); + + std::optional infoA = collectAllocInfo(toTensorA.getBuffer()); + std::optional infoB = collectAllocInfo(toTensorB.getBuffer()); + if (!infoA || !infoB) + return rewriter.notifyMatchFailure(insertOp, + "alloc users are not a load buffer"); + if (infoA->toTensor != toTensorA || infoB->toTensor != toTensorB) + return failure(); + + auto typeA = dyn_cast(infoA->alloc.getType()); + auto typeB = dyn_cast(infoB->alloc.getType()); + if (!typeA || !typeB || typeA != typeB) + return rewriter.notifyMatchFailure(insertOp, "alloc types differ"); + + if (infoA->alloc->getBlock() != infoB->alloc->getBlock() || + insertOp->getBlock() != infoA->alloc->getBlock() || + extractOp->getBlock() != insertOp->getBlock() || + toTensorA->getBlock() != insertOp->getBlock() || + toTensorB->getBlock() != insertOp->getBlock()) + return rewriter.notifyMatchFailure(insertOp, "ops not in the same block"); + + Box insertBox{insertOp.getMixedOffsets(), insertOp.getMixedSizes()}; + ArrayRef shape = typeA.getShape(); + for (const Box &write : infoA->writeBoxes) { + if (!boxContainedIn(write, insertBox, shape)) + return rewriter.notifyMatchFailure( + insertOp, "source write is not inside the insert region"); + } + for (const Box &write : infoB->writeBoxes) { + if (!boxesDisjoint(write, insertBox)) + return rewriter.notifyMatchFailure( + insertOp, "dest write overlaps the insert region"); + } + + SmallVector allCopies; + allCopies.append(infoA->copies.begin(), infoA->copies.end()); + allCopies.append(infoB->copies.begin(), infoB->copies.end()); + for (memref::CopyOp copy : allCopies) { + if (copy->getBlock() != toTensorB->getBlock() || + !copy->isBeforeInBlock(toTensorB)) + return rewriter.notifyMatchFailure( + insertOp, "to_tensor B does not dominate all copies"); + } + + // Read region of the concatenated value: extract_slice users contribute + // their boxes; any other user conservatively reads the whole buffer. + // to_tensor B is required to have a single use (the insert), so only + // insert result users matter. + SmallVector readBoxes; + bool readsFull = false; + for (Operation *user : insertOp.getResult().getUsers()) { + if (auto readExtract = dyn_cast(user)) { + if (!allUnitStrides(readExtract.getMixedStrides())) { + readsFull = true; + break; + } + readBoxes.push_back( + Box{readExtract.getMixedOffsets(), readExtract.getMixedSizes()}); + continue; + } + readsFull = true; + break; + } + if (readsFull) { + readBoxes.clear(); + readBoxes.push_back(makeFullBox(shape, insertOp.getContext())); + } + + SmallVector writeUnionInput; + writeUnionInput.append(infoA->writeBoxes.begin(), infoA->writeBoxes.end()); + writeUnionInput.append(infoB->writeBoxes.begin(), infoB->writeBoxes.end()); + std::optional writeUnion = + unionBoxesAlongOneDim(writeUnionInput, insertOp.getContext()); + + bool fillDead = false; + if (writeUnion) { + fillDead = llvm::all_of(readBoxes, [&](const Box &read) { + return boxContainedIn(read, *writeUnion, shape); + }); + } + + // Keep the earlier alloc so RAUW cannot create uses-before-def. + bool aIsEarlier = infoA->alloc->isBeforeInBlock(infoB->alloc); + memref::AllocOp survivor = aIsEarlier ? infoA->alloc : infoB->alloc; + memref::AllocOp dropped = aIsEarlier ? infoB->alloc : infoA->alloc; + const FillInfo &survivorFill = aIsEarlier ? infoA->fill : infoB->fill; + const FillInfo &droppedFill = aIsEarlier ? infoB->fill : infoA->fill; + + bool keepSingleFill = false; + if (!fillDead) { + // Once merged, the surviving fill initialises the shared buffer, so it + // must run before every copy and not just before the ones that used to + // target its own buffer. + if (infoA->fill.isBare && infoB->fill.isBare && infoA->fill.fill && + infoB->fill.fill && + sameFillValue(infoA->fill.fillValue, infoB->fill.fillValue) && + fillPrecedesCopies(survivorFill, allCopies)) { + keepSingleFill = true; + } else if (infoA->fill.fill || infoB->fill.fill) { + return rewriter.notifyMatchFailure( + insertOp, "cannot prove fill dead and cannot merge fills"); + } + } + + LLVM_DEBUG(llvm::dbgs() << "[" DEBUG_TYPE "] matched at " + << insertOp.getLoc() << ": fillDead=" << fillDead + << " keepSingleFill=" << keepSingleFill << "\n"); + + if (fillDead) { + eraseFill(rewriter, infoA->fill); + eraseFill(rewriter, infoB->fill); + } else if (keepSingleFill) { + eraseFill(rewriter, droppedFill); + } + + rewriter.replaceOp(insertOp, toTensorB.getResult()); + rewriter.eraseOp(extractOp); + rewriter.eraseOp(toTensorA); + rewriter.replaceAllUsesWith(dropped.getResult(), survivor.getResult()); + rewriter.eraseOp(dropped); + return success(); + } +}; + +struct MergeConcatLoadBufferPass + : public mlir::triton::cfg::impl::MergeConcatLoadBufferBase< + MergeConcatLoadBufferPass> { + void getDependentDialects(DialectRegistry ®istry) const override { + registry + .insert(); + } + + void runOnOperation() override { + RewritePatternSet patterns(&getContext()); + patterns.add(&getContext()); + if (failed(applyPatternsGreedily(getOperation(), std::move(patterns)))) + signalPassFailure(); + } +}; + +} // namespace + +std::unique_ptr> +mlir::triton::cfg::createMergeConcatLoadBufferPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToGraph/Passes.cpp b/third_party/ascend/lib/TritonToGraph/Passes.cpp new file mode 100644 index 0000000000..801a568e6f --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Passes.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/Passes.h" +#include "TritonToGraph/ControlFlowGraphBuilder.h" + +#include "mlir/Pass/PassRegistry.h" + +#define GEN_PASS_REGISTRATION +#include "ascend/include/TritonToGraph/Passes.h.inc" + +namespace mlir { +namespace triton { + +// registerTritonToCFGPasses() 由 Passes.h.inc 生成 +// createBuildCFGPass() 的实现需要在 ControlFlowGraphBuilder.cpp 中 + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToGraph/PermutationAnalysis.cpp b/third_party/ascend/lib/TritonToGraph/PermutationAnalysis.cpp new file mode 100644 index 0000000000..c66452b603 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/PermutationAnalysis.cpp @@ -0,0 +1,1042 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/PermutationAnalysis.h" + +#include "TritonToGraph/EntryArgPointerAliasAnalysis.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Interfaces/CallInterfaces.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" + +#include +#include +#include +#include + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +// This static assertion fixes the compose order in source as well as in the +// public documentation: apply [2, 0, 1], then [1, 2, 0] == identity. +constexpr int32_t kComposeBefore[] = {2, 0, 1}; +constexpr int32_t kComposeAfter[] = {1, 2, 0}; +static_assert(kComposeBefore[kComposeAfter[0]] == 0 && + kComposeBefore[kComposeAfter[1]] == 1 && + kComposeBefore[kComposeAfter[2]] == 2, + "Permutation::compose applies the left operand before the " + "right operand"); + +struct ParsedRankOneOffset { + ProofOutcome outcome; + triton::MakeRangeOp range; + // Null for a fully static offset. When non-null, `staticOffset` is the + // residual to add to this one shared dynamic scalar origin. + Value dynamicOrigin; + int64_t staticOffset = 0; +}; + +struct OffsetBounds { + int64_t first = 0; + int64_t last = 0; +}; + +struct ParsedAffineAxis { + ProofOutcome outcome; + StaticAccessAxis axis; + OffsetBounds bounds; +}; + +bool haveSameShape(RankedTensorType lhs, RankedTensorType rhs) { + return lhs.getShape() == rhs.getShape(); +} + +bool isUnencodedRankedTensor(RankedTensorType type) { + return type.hasStaticShape() && !type.getEncoding(); +} + +bool isI32Tensor(RankedTensorType type) { + auto elementType = dyn_cast(type.getElementType()); + return elementType && elementType.getWidth() == 32; +} + +bool getStaticI32Constant(Value value, int64_t &result) { + auto integerType = dyn_cast(value.getType()); + if (!integerType || integerType.getWidth() != 32) + return false; + + auto constant = value.getDefiningOp(); + if (!constant) + return false; + auto integer = dyn_cast(constant.getValue()); + if (!integer || integer.getType() != integerType) + return false; + + result = integer.getValue().getSExtValue(); + return true; +} + +bool isSignedI32(int64_t value) { + return value >= std::numeric_limits::min() && + value <= std::numeric_limits::max(); +} + +bool isScalarI32(Value value) { + auto integerType = dyn_cast(value.getType()); + return integerType && integerType.getWidth() == 32; +} + +bool getSignedI32RangeBounds(triton::MakeRangeOp range, int64_t &start, + int64_t &end) { + if (!range) + return false; + + IntegerAttr startAttr = range.getStartAttr(); + IntegerAttr endAttr = range.getEndAttr(); + auto startType = + startAttr ? dyn_cast(startAttr.getType()) : IntegerType(); + auto endType = + endAttr ? dyn_cast(endAttr.getType()) : IntegerType(); + if (!startAttr || !endAttr || !startType || !endType || + startType.getWidth() != 32 || endType.getWidth() != 32) + return false; + + start = startAttr.getInt(); + end = endAttr.getInt(); + return true; +} + +bool multiplyWouldOverflow(int64_t lhs, int64_t rhs) { + constexpr int64_t kMin = std::numeric_limits::min(); + constexpr int64_t kMax = std::numeric_limits::max(); + + if (lhs == 0 || rhs == 0) + return false; + if (lhs == -1) + return rhs == kMin; + if (rhs == -1) + return lhs == kMin; + if (lhs > 0) { + if (rhs > 0) + return lhs > kMax / rhs; + return rhs < kMin / lhs; + } + if (rhs > 0) + return lhs < kMin / rhs; + return lhs < kMax / rhs; +} + +bool addWouldOverflow(int64_t lhs, int64_t rhs) { + constexpr int64_t kMin = std::numeric_limits::min(); + constexpr int64_t kMax = std::numeric_limits::max(); + if (rhs > 0) + return lhs > kMax - rhs; + return lhs < kMin - rhs; +} + +bool checkedAddI64(int64_t lhs, int64_t rhs, int64_t &result) { + if (addWouldOverflow(lhs, rhs)) + return false; + result = lhs + rhs; + return true; +} + +bool checkedMulI64(int64_t lhs, int64_t rhs, int64_t &result) { + if (multiplyWouldOverflow(lhs, rhs)) + return false; + result = lhs * rhs; + return true; +} + +// Decompose the scalar source of a rank-one offset splat into a single +// dynamic SSA origin and one signed i32 static residual. A non-add scalar is +// intentionally treated as the opaque origin: the proof relies only on SSA +// identity, never on algebraic equivalence. Scalar addi is accepted only for +// an explicit origin + constant (in either order); multiple dynamic terms and +// flagged arithmetic are rejected rather than normalized speculatively. +ParsedRankOneOffset parseScalarRankOneOrigin(Value scalar) { + if (!isScalarI32(scalar)) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + + int64_t constant = 0; + if (getStaticI32Constant(scalar, constant)) + return {ProofOutcome::proven(), {}, Value(), constant}; + + auto add = scalar.getDefiningOp(); + if (!add || add.getResult() != scalar) + return {ProofOutcome::proven(), {}, scalar, 0}; + if (add.getOverflowFlags() != arith::IntegerOverflowFlags::none) + return {ProofOutcome::rejected(ProofReason::OverflowFlags), {}, Value(), 0}; + + int64_t lhsConstant = 0; + int64_t rhsConstant = 0; + const bool lhsIsConstant = getStaticI32Constant(add.getLhs(), lhsConstant); + const bool rhsIsConstant = getStaticI32Constant(add.getRhs(), rhsConstant); + if (lhsIsConstant == rhsIsConstant) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + + Value origin = lhsIsConstant ? add.getRhs() : add.getLhs(); + if (!isScalarI32(origin)) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + return {ProofOutcome::proven(), + {}, + origin, + lhsIsConstant ? lhsConstant : rhsConstant}; +} + +// Accept the old direct make_range form, or exactly +// addi(splat(origin + static_i32), make_range) +// in either operand order. The caller supplies the fully checked rank-one +// offset tensor type so this helper cannot accidentally admit a broadcast or +// a differently shaped add. +ParsedRankOneOffset parseNormalizedRankOneOffset(Value offset, + RankedTensorType offsetType) { + if (!offsetType || !isUnencodedRankedTensor(offsetType) || + offsetType.getRank() != 1 || !isI32Tensor(offsetType) || + offset.getType() != offsetType) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + + if (auto range = offset.getDefiningOp()) { + if (range.getResult() != offset || + range.getResult().getType() != offsetType) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + return {ProofOutcome::proven(), range, Value(), 0}; + } + + auto add = offset.getDefiningOp(); + if (!add || add.getResult() != offset || + add.getOverflowFlags() != arith::IntegerOverflowFlags::none) + return {ProofOutcome::rejected(add && add.getResult() == offset + ? ProofReason::OverflowFlags + : ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + if (add.getLhs().getType() != offsetType || + add.getRhs().getType() != offsetType) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + + auto lhsRange = add.getLhs().getDefiningOp(); + auto rhsRange = add.getRhs().getDefiningOp(); + if (lhsRange && lhsRange.getResult() != add.getLhs()) + lhsRange = triton::MakeRangeOp(); + if (rhsRange && rhsRange.getResult() != add.getRhs()) + rhsRange = triton::MakeRangeOp(); + if (static_cast(lhsRange) == static_cast(rhsRange)) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + + triton::MakeRangeOp range = lhsRange ? lhsRange : rhsRange; + Value splatValue = lhsRange ? add.getRhs() : add.getLhs(); + if (range.getResult().getType() != offsetType) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + + auto splat = splatValue.getDefiningOp(); + if (!splat || splat.getResult() != splatValue || + splat.getResult().getType() != offsetType) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), + {}, + Value(), + 0}; + + ParsedRankOneOffset parsedOrigin = parseScalarRankOneOrigin(splat.getSrc()); + if (!parsedOrigin.outcome.isProven()) + return parsedOrigin; + parsedOrigin.range = range; + return parsedOrigin; +} + +ParsedAffineAxis parseAffineAxis(Value value, RankedTensorType fullOffsetType) { + auto broadcast = value.getDefiningOp(); + if (!broadcast || broadcast.getResult() != value) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), {}}; + + auto broadcastType = dyn_cast(value.getType()); + if (!broadcastType || !isUnencodedRankedTensor(broadcastType) || + !isI32Tensor(broadcastType) || + !haveSameShape(broadcastType, fullOffsetType)) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), {}}; + + auto multiply = broadcast.getSrc().getDefiningOp(); + if (!multiply || multiply.getResult() != broadcast.getSrc()) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), {}}; + if (multiply.getOverflowFlags() != arith::IntegerOverflowFlags::none) + return {ProofOutcome::rejected(ProofReason::OverflowFlags), {}}; + + auto strideSplat = multiply.getRhs().getDefiningOp(); + if (!strideSplat || strideSplat.getResult() != multiply.getRhs()) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), {}}; + + auto expandedType = dyn_cast(multiply.getLhs().getType()); + auto strideType = + dyn_cast(strideSplat.getResult().getType()); + if (!expandedType || !strideType || !isUnencodedRankedTensor(expandedType) || + !isUnencodedRankedTensor(strideType) || !isI32Tensor(expandedType) || + !isI32Tensor(strideType) || expandedType != strideType || + multiply.getResult().getType() != expandedType) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), {}}; + + int64_t stride = 0; + if (!getStaticI32Constant(strideSplat.getSrc(), stride)) + return {ProofOutcome::rejected(ProofReason::DynamicStride), {}}; + + Value rangeValue = multiply.getLhs(); + unsigned outputAxis = 0; + bool hasOutputAxis = false; + unsigned expectedRank = fullOffsetType.getRank(); + while (auto expanded = rangeValue.getDefiningOp()) { + if (expanded.getResult() != rangeValue) + return { + ProofOutcome::rejected(ProofReason::InvalidExpandDimsChain), {}, {}}; + auto sourceType = dyn_cast(expanded.getSrc().getType()); + auto resultType = + dyn_cast(expanded.getResult().getType()); + if (!sourceType || !resultType || !isUnencodedRankedTensor(sourceType) || + !isUnencodedRankedTensor(resultType) || !isI32Tensor(sourceType) || + !isI32Tensor(resultType) || + resultType.getRank() != sourceType.getRank() + 1 || + resultType.getRank() > expectedRank || + expanded.getAxis() >= resultType.getRank()) + return { + ProofOutcome::rejected(ProofReason::InvalidExpandDimsChain), {}, {}}; + if (hasOutputAxis && expanded.getAxis() <= outputAxis) + ++outputAxis; + else if (!hasOutputAxis) { + outputAxis = 0; + hasOutputAxis = true; + if (expanded.getAxis() == 0) + ++outputAxis; + } + for (unsigned axis = 0; axis < resultType.getRank(); ++axis) { + const int64_t expected = + axis == expanded.getAxis() + ? 1 + : sourceType + .getShape()[axis < expanded.getAxis() ? axis : axis - 1]; + if (resultType.getShape()[axis] != expected) + return {ProofOutcome::rejected(ProofReason::InvalidExpandDimsChain), + {}, + {}}; + } + rangeValue = expanded.getSrc(); + } + + auto range = rangeValue.getDefiningOp(); + if (!range || range.getResult() != rangeValue) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), {}}; + + auto rangeType = dyn_cast(range.getResult().getType()); + if (!rangeType || !isUnencodedRankedTensor(rangeType) || + !isI32Tensor(rangeType) || rangeType.getRank() != 1) + return {ProofOutcome::rejected(ProofReason::UnsupportedRank), {}}; + + int64_t rangeStart = 0; + int64_t rangeEnd = 0; + if (!getSignedI32RangeBounds(range, rangeStart, rangeEnd)) + return {ProofOutcome::rejected(ProofReason::InvalidMakeRange), {}}; + const int64_t extent = rangeType.getShape().front(); + if (rangeEnd <= rangeStart || rangeEnd - rangeStart != extent) + return {ProofOutcome::rejected(ProofReason::InvalidMakeRange), {}}; + + if (!hasOutputAxis || expandedType.getRank() != expectedRank || + outputAxis >= expectedRank || + expandedType.getShape()[outputAxis] != extent) + return {ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), {}}; + for (unsigned axis = 0; axis < expectedRank; ++axis) { + if (axis != outputAxis && expandedType.getShape()[axis] != 1) + return { + ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset), {}, {}}; + } + + int64_t first = 0; + int64_t last = 0; + if (!checkedMulI64(rangeStart, stride, first) || + !checkedMulI64(rangeEnd - 1, stride, last)) + return {ProofOutcome::rejected(ProofReason::OffsetOverflow), {}, {}}; + + return {ProofOutcome::proven(), + StaticAccessAxis{range, rangeStart, rangeEnd, stride, outputAxis}, + OffsetBounds{first, last}}; +} + +StaticAccessProof rejectAccess(ProofOutcome outcome) { + return {outcome, std::nullopt}; +} + +bool isBarrierLike(Operation *operation) { + return operation->getName().getStringRef().contains("barrier"); +} + +// A distinct entry pointer root removes only the aliasing question. It does +// not make predicated, boundary-checked, padded, or volatile accesses safe to +// move across a delayed store, so retain the direct-access subset accepted by +// StaticAccessAnalysis before taking the ABI no-alias fast path. +bool isSupportedInterveningLoad(triton::LoadOp load) { + return !load.getMask() && !load.getOther() && + load.getBoundaryCheck().empty() && !load.getPadding() && + !load.getIsVolatile(); +} + +bool isSupportedInterveningStore(triton::StoreOp store) { + return !store.getMask() && store.getBoundaryCheck().empty(); +} + +} // namespace + +llvm::StringRef cfg::getProofReasonMessage(ProofReason reason) { + switch (reason) { + case ProofReason::None: + return "proof established"; + case ProofReason::InvalidPermutationRank: + return "permutation rank must be non-zero and representable"; + case ProofReason::InvalidPermutationAxis: + return "permutation contains an out-of-range axis"; + case ProofReason::DuplicatePermutationAxis: + return "permutation contains a duplicate axis"; + case ProofReason::PermutationRankMismatch: + return "permutations have different ranks"; + case ProofReason::ShapeRankMismatch: + return "shape rank does not match permutation rank"; + case ProofReason::InvalidOldAxis: + return "old axis is out of range"; + case ProofReason::NullValue: + return "analysis received a null value"; + case ProofReason::UnresolvedPointer: + return "pointer has no normalized defining operation"; + case ProofReason::UnsupportedPointerType: + return "pointer is not a ranked tensor of pointers"; + case ProofReason::UnsupportedPointerForm: + return "pointer is not normalized as tt.splat plus tt.addptr"; + case ProofReason::UnsupportedRank: + return "access rank is outside the supported static subset"; + case ProofReason::UnsupportedEncoding: + return "encoded tensors are outside the pre-layout static subset"; + case ProofReason::NonSquareShape: + return "rank-2 access must have a static square shape"; + case ProofReason::UnsupportedIndexElementType: + return "access indices must be unencoded i32 tensors"; + case ProofReason::DynamicShape: + return "access shape is dynamic"; + case ProofReason::UnsupportedAffineOffset: + return "offset is outside the supported static affine form"; + case ProofReason::DynamicStride: + return "stride is not a static integer"; + case ProofReason::NegativeStride: + return "stride is negative"; + case ProofReason::ZeroStride: + return "stride is zero"; + case ProofReason::DuplicateStride: + return "strides are duplicated"; + case ProofReason::InvalidAxisProvenance: + return "axis provenance is out of range"; + case ProofReason::DuplicateAxisProvenance: + return "multiple access axes have the same provenance"; + case ProofReason::DuplicateRangeSource: + return "multiple access axes reuse the same tt.make_range source"; + case ProofReason::InvalidMakeRange: + return "tt.make_range does not match its static result shape"; + case ProofReason::InvalidExpandDimsChain: + return "affine axis does not have a valid expand_dims chain"; + case ProofReason::OffsetOverflow: + return "static affine offset arithmetic overflows"; + case ProofReason::OverflowFlags: + return "affine arithmetic has overflow flags that V1 cannot preserve"; + case ProofReason::NonRowMajorContiguous: + return "access is not logically row-major contiguous"; + case ProofReason::NonInjectiveLanes: + return "lane address map is not proven injective"; + case ProofReason::MaskedAccess: + return "masked or predicated access is unsupported"; + case ProofReason::BoundaryCheck: + return "boundary or padding behavior is unsupported"; + case ProofReason::VolatileLoad: + return "volatile load is unsupported"; + case ProofReason::NullOperation: + return "analysis received a null operation"; + case ProofReason::DifferentBlocks: + return "protected interval crosses MLIR blocks"; + case ProofReason::InvalidProtectedInterval: + return "protected interval endpoints are not in program order"; + case ProofReason::RegionOperation: + return "protected interval contains a region operation"; + case ProofReason::CallOperation: + return "protected interval contains a call"; + case ProofReason::BarrierOperation: + return "protected interval contains a barrier"; + case ProofReason::UnknownMemoryEffect: + return "protected interval contains an operation with unknown memory " + "effects"; + case ProofReason::InterveningMemoryEffect: + return "protected interval contains a memory effect"; + case ProofReason::DifferentAccessBase: + return "accesses do not share the same proven SSA base"; + case ProofReason::OverlappingAccessRange: + return "static access ranges overlap"; + case ProofReason::UnsupportedInterveningMemoryAccess: + return "intervening memory access is unsupported or cannot be proven " + "static"; + } + return "unknown proof reason"; +} + +ProofOutcome Permutation::validate(llvm::ArrayRef perm) { + if (perm.empty() || + perm.size() > static_cast(std::numeric_limits::max())) + return ProofOutcome::rejected(ProofReason::InvalidPermutationRank); + + llvm::SmallVector seen(perm.size(), 0); + for (int32_t oldAxis : perm) { + if (oldAxis < 0 || static_cast(oldAxis) >= perm.size()) + return ProofOutcome::rejected(ProofReason::InvalidPermutationAxis); + if (seen[oldAxis]) + return ProofOutcome::rejected(ProofReason::DuplicatePermutationAxis); + seen[oldAxis] = 1; + } + return ProofOutcome::proven(); +} + +FailureOr Permutation::create(llvm::ArrayRef perm) { + if (!validate(perm).isProven()) + return failure(); + return Permutation(perm); +} + +Permutation Permutation::inverse() const { + llvm::SmallVector oldToNew(rank()); + for (unsigned newAxis = 0; newAxis < rank(); ++newAxis) + oldToNew[newToOld[newAxis]] = static_cast(newAxis); + return Permutation(oldToNew); +} + +FailureOr Permutation::compose(const Permutation &after) const { + if (rank() != after.rank()) + return failure(); + + llvm::SmallVector combined; + combined.reserve(rank()); + for (unsigned newAxis = 0; newAxis < rank(); ++newAxis) + combined.push_back(newToOld[after.newToOld[newAxis]]); + return Permutation(combined); +} + +int32_t Permutation::mapOldAxisToNew(int32_t oldAxis) const { + if (oldAxis < 0 || static_cast(oldAxis) >= rank()) + return -1; + for (unsigned newAxis = 0; newAxis < rank(); ++newAxis) { + if (newToOld[newAxis] == oldAxis) + return static_cast(newAxis); + } + return -1; +} + +FailureOr> +Permutation::permuteShape(llvm::ArrayRef shape) const { + if (shape.size() != rank()) + return failure(); + + llvm::SmallVector result; + result.reserve(rank()); + for (unsigned newAxis = 0; newAxis < rank(); ++newAxis) + result.push_back(shape[newToOld[newAxis]]); + return result; +} + +ProofOutcome StaticAccessAnalysis::proveLaneInjectivity( + llvm::ArrayRef shape, llvm::ArrayRef strides, + llvm::ArrayRef axisProvenance) { + constexpr int64_t kSignedI32Max = std::numeric_limits::max(); + if (shape.empty() || shape.size() != strides.size() || + shape.size() != axisProvenance.size()) + return ProofOutcome::rejected(ProofReason::UnsupportedRank); + + for (unsigned axis = 0; axis < shape.size(); ++axis) { + if (shape[axis] < 0) + return ProofOutcome::rejected(ProofReason::DynamicShape); + if (shape[axis] == 0) + return ProofOutcome::rejected(ProofReason::UnsupportedRank); + if (strides[axis] < 0) + return ProofOutcome::rejected(ProofReason::NegativeStride); + if (strides[axis] == 0) + return ProofOutcome::rejected(ProofReason::ZeroStride); + if (strides[axis] > kSignedI32Max) + return ProofOutcome::rejected(ProofReason::OffsetOverflow); + if (axisProvenance[axis] >= shape.size()) + return ProofOutcome::rejected(ProofReason::InvalidAxisProvenance); + + for (unsigned previous = 0; previous < axis; ++previous) { + if (axisProvenance[previous] == axisProvenance[axis]) + return ProofOutcome::rejected(ProofReason::DuplicateAxisProvenance); + if (strides[previous] == strides[axis]) + return ProofOutcome::rejected(ProofReason::DuplicateStride); + } + } + + llvm::SmallVector axes; + axes.reserve(shape.size()); + for (unsigned axis = 0; axis < shape.size(); ++axis) + axes.push_back(axis); + std::sort(axes.begin(), axes.end(), [&](unsigned lhs, unsigned rhs) { + return strides[lhs] < strides[rhs]; + }); + + int64_t reachableSpan = 0; + for (unsigned axis : axes) { + if (shape[axis] == 1) + continue; + if (strides[axis] <= reachableSpan) + return ProofOutcome::rejected(ProofReason::NonInjectiveLanes); + + const int64_t extent = shape[axis] - 1; + if (multiplyWouldOverflow(extent, strides[axis])) + return ProofOutcome::rejected(ProofReason::OffsetOverflow); + const int64_t contribution = extent * strides[axis]; + // The IR offsets are i32 tensors. i64 host arithmetic is used only to + // prove the expression safely; it must not authorize an address map that + // would overflow the actual TTIR arithmetic. Existing tensor-size limits + // keep current fixtures below this bound, but the check is intentional for + // future configurations as well. + if (contribution > kSignedI32Max || + reachableSpan > kSignedI32Max - contribution) + return ProofOutcome::rejected(ProofReason::OffsetOverflow); + if (addWouldOverflow(reachableSpan, contribution)) + return ProofOutcome::rejected(ProofReason::OffsetOverflow); + reachableSpan += contribution; + } + + return ProofOutcome::proven(); +} + +bool StaticAccess::isLogicalRowMajorContiguous() const { + if (!lanesInjective || shape.empty() || shape.size() != strides.size() || + shape.size() != axes.size() || elementCount <= 0) + return false; + + int64_t expectedStride = 1; + for (size_t index = shape.size(); index != 0; --index) { + const size_t axis = index - 1; + if (shape[axis] <= 0 || axes[axis].outputAxis != axis || + strides[axis] != expectedStride) + return false; + if (multiplyWouldOverflow(expectedStride, shape[axis])) + return false; + expectedStride *= shape[axis]; + } + return expectedStride == elementCount; +} + +StaticAccessProof StaticAccessAnalysis::analyzePointer(Value pointer) const { + if (!pointer) + return rejectAccess(ProofOutcome::rejected(ProofReason::NullValue)); + + auto pointerType = dyn_cast(pointer.getType()); + if (!pointerType) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedPointerType)); + if (pointerType.getRank() < 1) + return rejectAccess(ProofOutcome::rejected(ProofReason::UnsupportedRank)); + if (!pointerType.hasStaticShape()) + return rejectAccess(ProofOutcome::rejected(ProofReason::DynamicShape)); + if (pointerType.getEncoding()) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedEncoding)); + + auto pointerElement = + dyn_cast(pointerType.getElementType()); + if (!pointerElement || + triton::isTensorPointerType(pointerType.getElementType())) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedPointerType)); + + auto addPtr = pointer.getDefiningOp(); + if (!addPtr) { + if (!pointer.getDefiningOp()) + return rejectAccess( + ProofOutcome::unknown(ProofReason::UnresolvedPointer)); + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedPointerForm)); + } + + auto baseSplat = addPtr.getPtr().getDefiningOp(); + if (!baseSplat) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedPointerForm)); + Type baseType = baseSplat.getSrc().getType(); + if (!isa(baseType) || + triton::isTensorPointerType(baseType)) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedPointerForm)); + + auto baseSplatType = + dyn_cast(baseSplat.getResult().getType()); + auto offsetType = dyn_cast(addPtr.getOffset().getType()); + if (!baseSplatType || !offsetType || !baseSplatType.hasStaticShape() || + !offsetType.hasStaticShape() || + !haveSameShape(pointerType, baseSplatType) || + !haveSameShape(pointerType, offsetType)) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedPointerForm)); + if (baseSplatType.getEncoding() || offsetType.getEncoding()) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedEncoding)); + + if (pointerType.getRank() == 1) { + ParsedRankOneOffset parsedOffset = + parseNormalizedRankOneOffset(addPtr.getOffset(), offsetType); + if (!parsedOffset.outcome.isProven()) + return rejectAccess(parsedOffset.outcome); + + auto rangeType = + dyn_cast(parsedOffset.range.getResult().getType()); + if (!rangeType || rangeType.getRank() != 1 || !rangeType.hasStaticShape() || + rangeType.getEncoding() || !haveSameShape(pointerType, rangeType)) + return rejectAccess(ProofOutcome::rejected(ProofReason::UnsupportedRank)); + + int64_t rangeStart = 0; + int64_t rangeEnd = 0; + if (!getSignedI32RangeBounds(parsedOffset.range, rangeStart, rangeEnd)) + return rejectAccess( + ProofOutcome::rejected(ProofReason::InvalidMakeRange)); + if (rangeEnd <= rangeStart || + rangeType.getShape().front() != rangeEnd - rangeStart) + return rejectAccess( + ProofOutcome::rejected(ProofReason::InvalidMakeRange)); + + int64_t firstOffset = 0; + int64_t lastOffset = 0; + if (!checkedAddI64(rangeStart, parsedOffset.staticOffset, firstOffset) || + !checkedAddI64(rangeEnd - 1, parsedOffset.staticOffset, lastOffset) || + !isSignedI32(firstOffset) || !isSignedI32(lastOffset)) + return rejectAccess(ProofOutcome::rejected(ProofReason::OffsetOverflow)); + + StaticAccess access; + access.pointer = pointer; + access.offset = addPtr.getOffset(); + access.base = baseSplat.getSrc(); + access.dynamicOrigin = parsedOffset.dynamicOrigin; + access.shape.push_back(rangeType.getShape().front()); + access.strides.push_back(1); + access.axisProvenance.push_back(0); + access.axes.push_back( + StaticAccessAxis{parsedOffset.range, rangeStart, rangeEnd, 1, 0}); + + ProofOutcome injectivity = proveLaneInjectivity( + access.shape, access.strides, access.axisProvenance); + if (!injectivity.isProven()) + return rejectAccess(injectivity); + + access.firstOffset = firstOffset; + access.lastOffset = lastOffset; + access.elementCount = rangeType.getShape().front(); + access.lanesInjective = true; + return {ProofOutcome::proven(), std::move(access)}; + } + + if (!isI32Tensor(offsetType)) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedIndexElementType)); + SmallVector pendingTerms = {addPtr.getOffset()}; + SmallVector affineTerms; + while (!pendingTerms.empty()) { + Value term = pendingTerms.pop_back_val(); + if (auto add = term.getDefiningOp()) { + if (add.getResult() != term) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset)); + if (add.getOverflowFlags() != arith::IntegerOverflowFlags::none) + return rejectAccess(ProofOutcome::rejected(ProofReason::OverflowFlags)); + pendingTerms.push_back(add.getLhs()); + pendingTerms.push_back(add.getRhs()); + continue; + } + affineTerms.push_back(term); + } + if (affineTerms.size() != pointerType.getRank()) + return rejectAccess( + ProofOutcome::rejected(ProofReason::UnsupportedAffineOffset)); + + SmallVector axes(pointerType.getRank()); + SmallVector bounds(pointerType.getRank()); + SmallVector seenOutputAxis(pointerType.getRank(), false); + for (Value term : affineTerms) { + ParsedAffineAxis parsedAxis = parseAffineAxis(term, offsetType); + if (!parsedAxis.outcome.isProven()) + return rejectAccess(parsedAxis.outcome); + if (parsedAxis.axis.outputAxis >= pointerType.getRank() || + seenOutputAxis[parsedAxis.axis.outputAxis]) + return rejectAccess( + ProofOutcome::rejected(ProofReason::DuplicateAxisProvenance)); + seenOutputAxis[parsedAxis.axis.outputAxis] = true; + axes[parsedAxis.axis.outputAxis] = parsedAxis.axis; + bounds[parsedAxis.axis.outputAxis] = parsedAxis.bounds; + } + + StaticAccess access; + access.pointer = pointer; + access.offset = addPtr.getOffset(); + access.base = baseSplat.getSrc(); + access.shape.append(pointerType.getShape().begin(), + pointerType.getShape().end()); + access.axes = std::move(axes); + + int64_t firstOffset = 0; + int64_t lastOffset = 0; + int64_t expectedStride = 1; + for (size_t index = access.shape.size(); index != 0; --index) { + const unsigned axis = static_cast(index - 1); + const StaticAccessAxis &parsedAxis = access.axes[axis]; + if (!seenOutputAxis[axis] || !parsedAxis.range || + parsedAxis.outputAxis != axis || + parsedAxis.rangeEnd <= parsedAxis.rangeStart || + parsedAxis.rangeEnd - parsedAxis.rangeStart != access.shape[axis]) + return rejectAccess( + ProofOutcome::rejected(ProofReason::InvalidMakeRange)); + if (parsedAxis.stride != expectedStride) + return rejectAccess( + ProofOutcome::rejected(ProofReason::NonRowMajorContiguous)); + if (!checkedMulI64(expectedStride, access.shape[axis], expectedStride)) + return rejectAccess(ProofOutcome::rejected(ProofReason::OffsetOverflow)); + } + + for (unsigned axis = 0; axis < access.shape.size(); ++axis) { + const StaticAccessAxis &parsedAxis = access.axes[axis]; + for (unsigned prior = 0; prior < axis; ++prior) { + if (access.axes[prior].range.operator->() == + parsedAxis.range.operator->()) + return rejectAccess( + ProofOutcome::rejected(ProofReason::DuplicateRangeSource)); + } + if (!checkedAddI64(firstOffset, bounds[axis].first, firstOffset) || + !checkedAddI64(lastOffset, bounds[axis].last, lastOffset)) + return rejectAccess(ProofOutcome::rejected(ProofReason::OffsetOverflow)); + access.strides.push_back(parsedAxis.stride); + access.axisProvenance.push_back(axis); + } + + if (firstOffset < std::numeric_limits::min() || + firstOffset > std::numeric_limits::max() || + lastOffset < std::numeric_limits::min() || + lastOffset > std::numeric_limits::max()) + return rejectAccess(ProofOutcome::rejected(ProofReason::OffsetOverflow)); + + access.firstOffset = firstOffset; + access.lastOffset = lastOffset; + access.elementCount = expectedStride; + access.lanesInjective = true; + if (!access.isLogicalRowMajorContiguous()) + return rejectAccess( + ProofOutcome::rejected(ProofReason::NonRowMajorContiguous)); + return {ProofOutcome::proven(), std::move(access)}; +} + +StaticAccessProof StaticAccessAnalysis::analyzeLoad(triton::LoadOp load) const { + if (!load.getOperation()) + return rejectAccess(ProofOutcome::rejected(ProofReason::NullOperation)); + if (load.getMask() || load.getOther()) + return rejectAccess(ProofOutcome::rejected(ProofReason::MaskedAccess)); + if (!load.getBoundaryCheck().empty() || load.getPadding()) + return rejectAccess(ProofOutcome::rejected(ProofReason::BoundaryCheck)); + if (load.getIsVolatile()) + return rejectAccess(ProofOutcome::rejected(ProofReason::VolatileLoad)); + return analyzePointer(load.getPtr()); +} + +StaticAccessProof +StaticAccessAnalysis::analyzeStore(triton::StoreOp store) const { + if (!store.getOperation()) + return rejectAccess(ProofOutcome::rejected(ProofReason::NullOperation)); + if (store.getMask()) + return rejectAccess(ProofOutcome::rejected(ProofReason::MaskedAccess)); + if (!store.getBoundaryCheck().empty()) + return rejectAccess(ProofOutcome::rejected(ProofReason::BoundaryCheck)); + return analyzePointer(store.getPtr()); +} + +ProofOutcome +StaticAccessAnalysis::proveSameBaseDisjoint(const StaticAccess &lhs, + const StaticAccess &rhs) const { + if (!lhs.base || !rhs.base || lhs.base != rhs.base || + lhs.dynamicOrigin != rhs.dynamicOrigin) + return ProofOutcome::rejected(ProofReason::DifferentAccessBase); + if (!lhs.lanesInjective || !rhs.lanesInjective || + lhs.firstOffset > lhs.lastOffset || rhs.firstOffset > rhs.lastOffset) + return ProofOutcome::rejected( + ProofReason::UnsupportedInterveningMemoryAccess); + if (lhs.lastOffset < rhs.firstOffset || rhs.lastOffset < lhs.firstOffset) + return ProofOutcome::proven(); + return ProofOutcome::rejected(ProofReason::OverlappingAccessRange); +} + +ProofOutcome +ProtectedIntervalAnalysis::proveNoMemoryEffects(Operation *first, + Operation *last) const { + if (!first || !last) + return ProofOutcome::rejected(ProofReason::NullOperation); + if (first == last) + return ProofOutcome::rejected(ProofReason::InvalidProtectedInterval); + if (first->getBlock() != last->getBlock()) + return ProofOutcome::rejected(ProofReason::DifferentBlocks); + + for (Operation *operation = first->getNextNode(); operation; + operation = operation->getNextNode()) { + if (operation == last) + return ProofOutcome::proven(); + if (operation->getNumRegions() != 0) + return ProofOutcome::rejected(ProofReason::RegionOperation); + if (isa(operation)) + return ProofOutcome::rejected(ProofReason::CallOperation); + if (isBarrierLike(operation)) + return ProofOutcome::rejected(ProofReason::BarrierOperation); + + if (auto memoryEffects = dyn_cast(operation)) { + llvm::SmallVector, 4> + effects; + memoryEffects.getEffects(effects); + if (!effects.empty()) + return ProofOutcome::rejected(ProofReason::InterveningMemoryEffect); + } + if (!isMemoryEffectFree(operation)) + return ProofOutcome::rejected(ProofReason::UnknownMemoryEffect); + } + + return ProofOutcome::rejected(ProofReason::InvalidProtectedInterval); +} + +ProofOutcome ProtectedIntervalAnalysis::proveNoConflictingLoadStoreEffects( + Operation *first, Operation *last, + llvm::ArrayRef protectedAccesses, + const EntryArgPointerAliasAnalysis *entryArgPointerAliases) const { + if (!first || !last) + return ProofOutcome::rejected(ProofReason::NullOperation); + if (protectedAccesses.empty() || first == last) + return ProofOutcome::rejected(ProofReason::InvalidProtectedInterval); + if (first->getBlock() != last->getBlock()) + return ProofOutcome::rejected(ProofReason::DifferentBlocks); + + StaticAccessAnalysis accessAnalysis; + auto proveDisjoint = [&](Value effectPointer, auto &&getProof) { + SmallVector sameRootAccesses; + for (const StaticAccess &protectedAccess : protectedAccesses) { + if (!entryArgPointerAliases) { + sameRootAccesses.push_back(&protectedAccess); + continue; + } + + EntryArgPointerRelation relation = + entryArgPointerAliases->classify(effectPointer, protectedAccess.base); + if (relation == EntryArgPointerRelation::DistinctEntryRoots) + continue; + if (relation == EntryArgPointerRelation::Unknown) + return ProofOutcome::rejected( + ProofReason::UnsupportedInterveningMemoryAccess); + sameRootAccesses.push_back(&protectedAccess); + } + + // All protected stores are rooted at a distinct entry pointer. This is + // sufficient under the StoreCoalescing ABI contract, including scalar or + // dynamically indexed accesses that StaticAccessAnalysis cannot parse. + if (sameRootAccesses.empty()) + return ProofOutcome::proven(); + + StaticAccessProof proof = getProof(); + if (!proof.isProven()) + return ProofOutcome::rejected( + ProofReason::UnsupportedInterveningMemoryAccess); + for (const StaticAccess *protectedAccess : sameRootAccesses) { + ProofOutcome outcome = + accessAnalysis.proveSameBaseDisjoint(*proof.access, *protectedAccess); + if (!outcome.isProven()) + return outcome; + } + return ProofOutcome::proven(); + }; + + for (Operation *operation = first->getNextNode(); operation; + operation = operation->getNextNode()) { + if (operation == last) + return ProofOutcome::proven(); + if (operation->getNumRegions() != 0) + return ProofOutcome::rejected(ProofReason::RegionOperation); + if (isa(operation)) + return ProofOutcome::rejected(ProofReason::CallOperation); + if (isBarrierLike(operation)) + return ProofOutcome::rejected(ProofReason::BarrierOperation); + + if (auto load = dyn_cast(operation)) { + if (!isSupportedInterveningLoad(load)) + return ProofOutcome::rejected( + ProofReason::UnsupportedInterveningMemoryAccess); + ProofOutcome outcome = proveDisjoint( + load.getPtr(), [&]() { return accessAnalysis.analyzeLoad(load); }); + if (!outcome.isProven()) + return outcome; + continue; + } + if (auto store = dyn_cast(operation)) { + if (!isSupportedInterveningStore(store)) + return ProofOutcome::rejected( + ProofReason::UnsupportedInterveningMemoryAccess); + ProofOutcome outcome = proveDisjoint( + store.getPtr(), [&]() { return accessAnalysis.analyzeStore(store); }); + if (!outcome.isProven()) + return outcome; + continue; + } + + if (auto memoryEffects = dyn_cast(operation)) { + llvm::SmallVector, 4> + effects; + memoryEffects.getEffects(effects); + if (!effects.empty()) + return ProofOutcome::rejected(ProofReason::InterveningMemoryEffect); + } + if (!isMemoryEffectFree(operation)) + return ProofOutcome::rejected(ProofReason::UnknownMemoryEffect); + } + + return ProofOutcome::rejected(ProofReason::InvalidProtectedInterval); +} diff --git a/third_party/ascend/lib/TritonToGraph/ProgramAxisDependenceAnalysis.cpp b/third_party/ascend/lib/TritonToGraph/ProgramAxisDependenceAnalysis.cpp new file mode 100644 index 0000000000..71b61a4afa --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/ProgramAxisDependenceAnalysis.cpp @@ -0,0 +1,496 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#include "TritonToGraph/ProgramAxisDependenceAnalysis.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Matchers.h" +#include "mlir/Interfaces/CallInterfaces.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" + +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::triton; +using namespace mlir::triton::cfg; + +namespace { + +struct OffsetForm { + bool valid = true; + bool symbolicStride = false; + int64_t programCoefficient = 0; + int64_t laneMin = 0; + int64_t laneMax = 0; +}; + +bool addNoOverflow(int64_t lhs, int64_t rhs, int64_t &result) { + if ((rhs > 0 && lhs > std::numeric_limits::max() - rhs) || + (rhs < 0 && lhs < std::numeric_limits::min() - rhs)) + return false; + result = lhs + rhs; + return true; +} + +bool multiplyNoOverflow(int64_t lhs, int64_t rhs, int64_t &result) { + return !__builtin_mul_overflow(lhs, rhs, &result); +} + +std::optional getConstantInt(Value value) { + APInt constant; + if (!matchPattern(value, m_ConstantInt(&constant))) + return std::nullopt; + return constant.getSExtValue(); +} + +std::optional getUniformConstantInt(Value value) { + if (std::optional constant = getConstantInt(value)) + return constant; + DenseElementsAttr elements; + if (!matchPattern(value, m_Constant(&elements)) || !elements.isSplat() || + !isa(elements.getElementType())) + return std::nullopt; + return elements.getSplatValue().getSExtValue(); +} + +OffsetForm invalidOffsetForm() { + OffsetForm form; + form.valid = false; + return form; +} + +OffsetForm addOffsetForms(const OffsetForm &lhs, const OffsetForm &rhs, + bool subtractRhs) { + if (!lhs.valid || !rhs.valid) + return invalidOffsetForm(); + if (subtractRhs && rhs.symbolicStride) + return invalidOffsetForm(); + if ((lhs.symbolicStride && + (rhs.symbolicStride || rhs.programCoefficient != 0)) || + (rhs.symbolicStride && lhs.programCoefficient != 0)) + return invalidOffsetForm(); + const int64_t rhsCoefficient = + subtractRhs ? -rhs.programCoefficient : rhs.programCoefficient; + const int64_t rhsMin = subtractRhs ? -rhs.laneMax : rhs.laneMin; + const int64_t rhsMax = subtractRhs ? -rhs.laneMin : rhs.laneMax; + OffsetForm result; + result.symbolicStride = lhs.symbolicStride || rhs.symbolicStride; + if (!addNoOverflow(lhs.programCoefficient, rhsCoefficient, + result.programCoefficient) || + !addNoOverflow(lhs.laneMin, rhsMin, result.laneMin) || + !addNoOverflow(lhs.laneMax, rhsMax, result.laneMax)) + return invalidOffsetForm(); + return result; +} + +OffsetForm scaleOffsetForm(const OffsetForm &input, int64_t factor) { + if (!input.valid) + return invalidOffsetForm(); + if (input.symbolicStride && factor != 1) + return invalidOffsetForm(); + OffsetForm result; + result.symbolicStride = input.symbolicStride; + if (!multiplyNoOverflow(input.programCoefficient, factor, + result.programCoefficient) || + !multiplyNoOverflow(input.laneMin, factor, result.laneMin) || + !multiplyNoOverflow(input.laneMax, factor, result.laneMax)) + return invalidOffsetForm(); + if (factor < 0) + std::swap(result.laneMin, result.laneMax); + return result; +} + +bool valueDependsOnAxis(Value value, int32_t targetAxis, DenseSet &seen); + +bool isRuntimeIntegerEntryArgument(Value value) { + if (auto argument = dyn_cast(value)) { + auto integer = dyn_cast(argument.getType()); + return integer && argument.getOwner() && + isa(argument.getOwner()->getParentOp()); + } + if (auto splat = value.getDefiningOp()) + return isRuntimeIntegerEntryArgument(splat.getSrc()); + if (auto broadcast = value.getDefiningOp()) + return isRuntimeIntegerEntryArgument(broadcast.getSrc()); + if (auto expand = value.getDefiningOp()) + return isRuntimeIntegerEntryArgument(expand.getSrc()); + return false; +} + +OffsetForm analyzeOffset(Value value, int32_t targetAxis, + DenseSet &visited) { + if (!visited.insert(value).second) + return invalidOffsetForm(); + + auto finish = [&](OffsetForm form) { + visited.erase(value); + return form; + }; + + if (auto pid = value.getDefiningOp()) { + if (pid.getAxisAsInt() != targetAxis) + return finish(invalidOffsetForm()); + OffsetForm form; + form.programCoefficient = 1; + return finish(form); + } + if (value.getDefiningOp()) + return finish(invalidOffsetForm()); + if (std::optional constant = getConstantInt(value)) { + OffsetForm form; + form.laneMin = *constant; + form.laneMax = *constant; + return finish(form); + } + if (auto range = value.getDefiningOp()) { + if (range.getEnd() <= range.getStart()) + return finish(invalidOffsetForm()); + OffsetForm form; + form.laneMin = range.getStart(); + form.laneMax = range.getEnd() - 1; + return finish(form); + } + DenseSet dependenceSeen; + if (!valueDependsOnAxis(value, targetAxis, dependenceSeen)) + return finish(OffsetForm{}); + if (auto splat = value.getDefiningOp()) + return finish(analyzeOffset(splat.getSrc(), targetAxis, visited)); + if (auto broadcast = value.getDefiningOp()) + return finish(analyzeOffset(broadcast.getSrc(), targetAxis, visited)); + if (auto expand = value.getDefiningOp()) + return finish(analyzeOffset(expand.getSrc(), targetAxis, visited)); + if (auto add = value.getDefiningOp()) { + OffsetForm lhs = analyzeOffset(add.getLhs(), targetAxis, visited); + OffsetForm rhs = analyzeOffset(add.getRhs(), targetAxis, visited); + return finish(addOffsetForms(lhs, rhs, false)); + } + if (auto subtract = value.getDefiningOp()) { + OffsetForm lhs = analyzeOffset(subtract.getLhs(), targetAxis, visited); + OffsetForm rhs = analyzeOffset(subtract.getRhs(), targetAxis, visited); + return finish(addOffsetForms(lhs, rhs, true)); + } + if (auto multiply = value.getDefiningOp()) { + if (std::optional lhs = getConstantInt(multiply.getLhs())) + return finish(scaleOffsetForm( + analyzeOffset(multiply.getRhs(), targetAxis, visited), *lhs)); + if (std::optional rhs = getConstantInt(multiply.getRhs())) + return finish(scaleOffsetForm( + analyzeOffset(multiply.getLhs(), targetAxis, visited), *rhs)); + DenseSet lhsSeen; + DenseSet rhsSeen; + const bool lhsDepends = + valueDependsOnAxis(multiply.getLhs(), targetAxis, lhsSeen); + const bool rhsDepends = + valueDependsOnAxis(multiply.getRhs(), targetAxis, rhsSeen); + if (lhsDepends == rhsDepends) + return finish(invalidOffsetForm()); + Value pidTerm = lhsDepends ? multiply.getLhs() : multiply.getRhs(); + Value strideTerm = lhsDepends ? multiply.getRhs() : multiply.getLhs(); + DenseSet pidSeen; + OffsetForm pidForm = analyzeOffset(pidTerm, targetAxis, pidSeen); + if (!pidForm.valid || pidForm.symbolicStride || + pidForm.programCoefficient != 1 || pidForm.laneMin != 0 || + pidForm.laneMax != 0 || !isRuntimeIntegerEntryArgument(strideTerm)) + return finish(invalidOffsetForm()); + OffsetForm form; + form.symbolicStride = true; + return finish(form); + } + return finish(invalidOffsetForm()); +} + +bool valueDependsOnAxis(Value value, int32_t targetAxis, + DenseSet &seen) { + if (!seen.insert(value).second) + return false; + if (auto pid = value.getDefiningOp()) + return pid.getAxisAsInt() == targetAxis; + Operation *definition = value.getDefiningOp(); + if (!definition) + return false; + return llvm::any_of(definition->getOperands(), [&](Value operand) { + return valueDependsOnAxis(operand, targetAxis, seen); + }); +} + +OffsetForm analyzePointerOffset(Value pointer, int32_t targetAxis, + DenseSet &visited) { + if (!visited.insert(pointer).second) + return invalidOffsetForm(); + auto finish = [&](OffsetForm form) { + visited.erase(pointer); + return form; + }; + + if (auto addPtr = pointer.getDefiningOp()) { + OffsetForm base = + analyzePointerOffset(addPtr.getPtr(), targetAxis, visited); + DenseSet offsetVisited; + OffsetForm offset = + analyzeOffset(addPtr.getOffset(), targetAxis, offsetVisited); + return finish(addOffsetForms(base, offset, false)); + } + if (auto splat = pointer.getDefiningOp()) + return finish(analyzePointerOffset(splat.getSrc(), targetAxis, visited)); + if (auto broadcast = pointer.getDefiningOp()) + return finish( + analyzePointerOffset(broadcast.getSrc(), targetAxis, visited)); + if (auto expand = pointer.getDefiningOp()) + return finish(analyzePointerOffset(expand.getSrc(), targetAxis, visited)); + + DenseSet dependenceSeen; + return finish(valueDependsOnAxis(pointer, targetAxis, dependenceSeen) + ? invalidOffsetForm() + : OffsetForm{}); +} + +std::optional +getConjunctiveOffsetUpperBound(Value mask, Value offset, + DenseSet &visited) { + if (!mask || !visited.insert(mask).second) + return std::nullopt; + if (auto compare = mask.getDefiningOp()) { + if (compare.getPredicate() == arith::CmpIPredicate::slt && + compare.getLhs() == offset) { + std::optional bound = getUniformConstantInt(compare.getRhs()); + if (bound && *bound > 0) + return bound; + } + return std::nullopt; + } + if (auto conjunction = mask.getDefiningOp()) { + std::optional lhs = + getConjunctiveOffsetUpperBound(conjunction.getLhs(), offset, visited); + std::optional rhs = + getConjunctiveOffsetUpperBound(conjunction.getRhs(), offset, visited); + if (lhs && rhs) + return std::min(*lhs, *rhs); + return lhs ? lhs : rhs; + } + return std::nullopt; +} + +Value peelPointerShapeOps(Value pointer) { + while (true) { + if (auto splat = pointer.getDefiningOp()) { + pointer = splat.getSrc(); + continue; + } + if (auto broadcast = pointer.getDefiningOp()) { + pointer = broadcast.getSrc(); + continue; + } + if (auto expand = pointer.getDefiningOp()) { + pointer = expand.getSrc(); + continue; + } + return pointer; + } +} + +OffsetForm analyzeStorePointerOffset(triton::StoreOp store, + int32_t targetAxis) { + Value pointer = peelPointerShapeOps(store.getPtr()); + auto finalAddPtr = pointer.getDefiningOp(); + if (!finalAddPtr) { + DenseSet pointerSeen; + return analyzePointerOffset(store.getPtr(), targetAxis, pointerSeen); + } + + DenseSet baseSeen; + OffsetForm base = + analyzePointerOffset(finalAddPtr.getPtr(), targetAxis, baseSeen); + DenseSet offsetSeen; + OffsetForm offset = + analyzeOffset(finalAddPtr.getOffset(), targetAxis, offsetSeen); + if (!base.valid || !offset.valid) + return invalidOffsetForm(); + + DenseSet maskSeen; + std::optional exclusiveBound = getConjunctiveOffsetUpperBound( + store.getMask(), finalAddPtr.getOffset(), maskSeen); + if (exclusiveBound && *exclusiveBound > offset.laneMin) { + const int64_t maskedMax = *exclusiveBound - 1; + if (maskedMax < offset.laneMax) + offset.laneMax = maskedMax; + } + return addOffsetForms(base, offset, false); +} + +StoreAddressIndependence classifyStoreAddress(triton::StoreOp store, + int32_t targetAxis, + bool &pointerDependsOnAxis) { + DenseSet dependencySeen; + pointerDependsOnAxis = + valueDependsOnAxis(store.getPtr(), targetAxis, dependencySeen); + if (!pointerDependsOnAxis) + return StoreAddressIndependence::NotProgramDependent; + + OffsetForm offset = analyzeStorePointerOffset(store, targetAxis); + if (!offset.valid || + (!offset.symbolicStride && offset.programCoefficient == 0) || + offset.laneMax < offset.laneMin) + return StoreAddressIndependence::Unknown; + + if (offset.symbolicStride) + return StoreAddressIndependence::CanonicalDynamicStride; + + const uint64_t span = static_cast(offset.laneMax) - + static_cast(offset.laneMin); + const uint64_t coefficient = + offset.programCoefficient < 0 + ? static_cast(-(offset.programCoefficient + 1)) + 1 + : static_cast(offset.programCoefficient); + return coefficient > span ? StoreAddressIndependence::ProvenDisjoint + : StoreAddressIndependence::Unknown; +} + +bool isControlFlowEscape(Operation *operation) { + if (isa(operation)) + return true; + StringRef name = operation->getName().getStringRef(); + return name == "cf.br" || name == "cf.cond_br" || name.ends_with(".yield"); +} + +bool hasUnsupportedEffect(Operation *operation) { + if (isa(operation)) + return false; + if (isa(operation)) + return true; + StringRef name = operation->getName().getStringRef(); + if (name.contains("atomic") || name.contains("barrier") || + name.contains("print")) + return true; + auto effects = dyn_cast(operation); + if (!effects) + return false; + SmallVector effectInstances; + effects.getEffects(effectInstances); + return llvm::any_of(effectInstances, [](const auto &effect) { + return isa(effect.getEffect()); + }); +} + +void recordOperationFacts(Operation *operation, ProgramAxisDependence &info, + DenseSet &seenStores) { + if (auto reduce = dyn_cast(operation)) + info.reductionAxes.push_back(reduce.getAxis()); + if (auto scan = dyn_cast(operation)) + info.reductionAxes.push_back(scan.getAxis()); + + if (isControlFlowEscape(operation)) + info.escapes = true; + if (auto store = dyn_cast(operation)) { + info.hasSideEffects = true; + if (seenStores.insert(operation).second) { + bool pointerDependsOnAxis = false; + StoreAddressIndependence independence = + classifyStoreAddress(store, info.axis, pointerDependsOnAxis); + info.stores.push_back({operation, pointerDependsOnAxis, independence}); + } + return; + } + if (hasUnsupportedEffect(operation)) { + info.hasSideEffects = true; + info.hasUnsupportedSideEffects = true; + } +} + +void buildClosure(ProgramAxisDependence &info) { + DenseSet visitedValues; + DenseSet visitedOperations; + DenseSet seenStores; + SmallVector worklist(info.programIds.begin(), info.programIds.end()); + + while (!worklist.empty()) { + Value value = worklist.pop_back_val(); + if (!visitedValues.insert(value).second) + continue; + for (OpOperand &use : value.getUses()) { + Operation *operation = use.getOwner(); + if (!visitedOperations.insert(operation).second) + continue; + info.dependenceClosure.push_back(operation); + recordOperationFacts(operation, info, seenStores); + for (Value result : operation->getResults()) + worklist.push_back(result); + } + } +} + +} // namespace + +bool ProgramAxisDependence::hasOnlyDisjointStoreAddresses() const { + return !stores.empty() && llvm::all_of(stores, [](const auto &store) { + return store.pointerDependsOnAxis && + store.independence == StoreAddressIndependence::ProvenDisjoint; + }); +} + +bool ProgramAxisDependence::hasOnlyLiftableStoreAddressesForProgramMapping() + const { + return !stores.empty() && llvm::all_of(stores, [](const auto &store) { + return store.pointerDependsOnAxis && + (store.independence == StoreAddressIndependence::ProvenDisjoint || + store.independence == + StoreAddressIndependence::CanonicalDynamicStride); + }); +} + +bool ProgramAxisDependence::isIndependentAxisTransformCandidate() const { + return hasProgramId() && !escapes && !hasUnsupportedSideEffects && + !readsNumPrograms && hasOnlyDisjointStoreAddresses(); +} + +bool ProgramAxisDependence::isProgramMappingTransformCandidate() const { + return hasProgramId() && !escapes && !hasUnsupportedSideEffects && + !readsNumPrograms && hasOnlyLiftableStoreAddressesForProgramMapping(); +} + +ProgramAxisDependenceAnalysis::ProgramAxisDependenceAnalysis( + triton::FuncOp function) { + for (int32_t axis = 0; axis < 3; ++axis) + axes[axis].axis = axis; + + function.walk([&](triton::GetProgramIdOp pid) { + const int32_t axis = pid.getAxisAsInt(); + if (axis >= 0 && axis < static_cast(axes.size())) + axes[axis].programIds.push_back(pid.getResult()); + }); + function.walk([&](triton::GetNumProgramsOp programs) { + const int32_t axis = programs.getAxisAsInt(); + if (axis >= 0 && axis < static_cast(axes.size())) + axes[axis].readsNumPrograms = true; + }); + for (ProgramAxisDependence &axis : axes) + buildClosure(axis); +} + +const ProgramAxisDependence & +ProgramAxisDependenceAnalysis::get(int32_t axis) const { + assert(axis >= 0 && axis < static_cast(axes.size()) && + "program axis must be x, y, or z"); + return axes[axis]; +} diff --git a/third_party/ascend/lib/TritonToGraph/ProgramGridTransform.cpp b/third_party/ascend/lib/TritonToGraph/ProgramGridTransform.cpp new file mode 100644 index 0000000000..af51dd0862 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/ProgramGridTransform.cpp @@ -0,0 +1,345 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/ProgramGridTransform.h" + +#include "mlir/IR/Builders.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" + +#include +#include + +using namespace mlir; +using namespace mlir::triton::cfg; + +namespace { + +constexpr llvm::StringLiteral kVersion = "version"; +constexpr llvm::StringLiteral kExtentSource = "extent_source"; +constexpr llvm::StringLiteral kHiddenExtentAxes = "hidden_extent_axes"; +constexpr llvm::StringLiteral kHiddenArgumentOrder = "hidden_argument_order"; +constexpr llvm::StringLiteral kHiddenArgumentTypes = "hidden_argument_types"; +constexpr llvm::StringLiteral kTransforms = "transforms"; +constexpr llvm::StringLiteral kOrder = "order"; +constexpr llvm::StringLiteral kKind = "kind"; +constexpr llvm::StringLiteral kAxis = "axis"; +constexpr llvm::StringLiteral kFactor = "factor"; +constexpr llvm::StringLiteral kPersistentCoverage = "persistent_coverage"; +constexpr llvm::StringLiteral kGridStrideAbiVerified = + "grid_stride_abi_verified"; +constexpr llvm::StringLiteral kCeilDiv = "ceil_div"; +constexpr llvm::StringLiteral kRuntimeOriginalGrid = "runtime_original_grid"; +constexpr llvm::StringLiteral kOriginalGridX = "originalGridX"; +constexpr llvm::StringLiteral kOriginalGridY = "originalGridY"; +constexpr llvm::StringLiteral kI32 = "i32"; +constexpr llvm::StringLiteral kHiddenXName = "__hacc_original_grid_x"; +constexpr llvm::StringLiteral kHiddenYName = "__hacc_original_grid_y"; + +bool hasExactKeys(DictionaryAttr dictionary, ArrayRef keys) { + if (dictionary.size() != keys.size()) + return false; + return llvm::all_of(keys, + [&](llvm::StringRef key) { return dictionary.get(key); }); +} + +std::optional getInteger(DictionaryAttr dictionary, + llvm::StringRef name) { + auto value = dyn_cast_or_null(dictionary.get(name)); + if (!value) + return std::nullopt; + return value.getInt(); +} + +std::optional getBoolean(DictionaryAttr dictionary, + llvm::StringRef name) { + auto value = dyn_cast_or_null(dictionary.get(name)); + if (!value) + return std::nullopt; + return value.getValue(); +} + +bool hasExactIntegerArray(Attribute attribute, ArrayRef expected) { + auto values = dyn_cast_or_null(attribute); + if (!values || values.size() != expected.size()) + return false; + for (auto [value, wanted] : llvm::zip(values, expected)) { + auto integer = dyn_cast(value); + if (!integer || integer.getInt() != wanted) + return false; + } + return true; +} + +bool hasExactStringArray(Attribute attribute, + ArrayRef expected) { + auto values = dyn_cast_or_null(attribute); + if (!values || values.size() != expected.size()) + return false; + for (auto [value, wanted] : llvm::zip(values, expected)) { + auto string = dyn_cast(value); + if (!string || string.getValue() != wanted) + return false; + } + return true; +} + +bool isPublicEntry(triton::FuncOp function) { + auto visibility = function->getAttrOfType("sym_visibility"); + return !visibility || visibility.getValue() == "public"; +} + +bool hasNamedI32Argument(BlockArgument argument, llvm::StringRef name) { + auto integer = dyn_cast(argument.getType()); + auto location = dyn_cast(argument.getLoc()); + return integer && integer.getWidth() == 32 && location && + location.getName().getValue() == name; +} + +bool isIAT16(const ProgramGridTransform &transform) { + return transform.axis == 1 && transform.factor == 16 && + !transform.persistentCoverage && !transform.gridStrideAbiVerified; +} + +bool isPTSM4(const ProgramGridTransform &transform) { + return transform.axis == 0 && transform.factor == 4 && + transform.persistentCoverage && transform.gridStrideAbiVerified; +} + +bool isPTSM64(const ProgramGridTransform &transform) { + return transform.axis == 0 && transform.factor == 64 && + transform.persistentCoverage && transform.gridStrideAbiVerified; +} + +bool isDotRowTransform(const ProgramGridTransform &transform) { + return transform.axis == 0 && !transform.persistentCoverage && + !transform.gridStrideAbiVerified && + (transform.factor == 2 || transform.factor == 4 || + transform.factor == 8 || transform.factor == 16 || + transform.factor == 32); +} + +bool isSupportedSequence(ArrayRef transforms) { + if (transforms.size() == 1) + return isIAT16(transforms.front()) || isPTSM64(transforms.front()) || + isDotRowTransform(transforms.front()); + return transforms.size() == 2 && isIAT16(transforms[0]) && + isPTSM4(transforms[1]); +} + +} // namespace + +bool mlir::triton::cfg::hasProgramGridHiddenExtentArguments( + triton::FuncOp function) { + if (function.getNumArguments() < 2) + return false; + const unsigned xIndex = function.getNumArguments() - 2; + return hasNamedI32Argument(function.getArgument(xIndex), kHiddenXName) && + hasNamedI32Argument(function.getArgument(xIndex + 1), kHiddenYName); +} + +LogicalResult mlir::triton::cfg::addProgramGridHiddenExtentArguments( + triton::FuncOp function) { + if (hasProgramGridHiddenExtentArguments(function)) + return success(); + for (BlockArgument argument : function.getArguments()) { + auto location = dyn_cast(argument.getLoc()); + if (location && (location.getName().getValue() == kHiddenXName || + location.getName().getValue() == kHiddenYName)) + return failure(); + } + + MLIRContext *context = function.getContext(); + SmallVector originalArgAttrs; + const bool hasArgAttrs = static_cast(function.getAllArgAttrs()); + if (hasArgAttrs) { + function.getAllArgAttrs(originalArgAttrs); + if (originalArgAttrs.size() != function.getNumArguments()) + return failure(); + } + + Type i32 = IntegerType::get(context, 32); + DictionaryAttr hiddenArgAttrs = + hasArgAttrs ? DictionaryAttr::get(context) : DictionaryAttr(); + if (failed(function.insertArgument( + function.getNumArguments(), i32, hiddenArgAttrs, + NameLoc::get(StringAttr::get(context, kHiddenXName), + function.getLoc())))) + return failure(); + if (failed(function.insertArgument( + function.getNumArguments(), i32, hiddenArgAttrs, + NameLoc::get(StringAttr::get(context, kHiddenYName), + function.getLoc())))) + return failure(); + + if (hasArgAttrs) { + originalArgAttrs.push_back(DictionaryAttr::get(context)); + originalArgAttrs.push_back(DictionaryAttr::get(context)); + function.setAllArgAttrs(originalArgAttrs); + } else { + function->removeAttr("arg_attrs"); + } + return success(); +} + +LogicalResult mlir::triton::cfg::commitProgramGridFunctionFromSandbox( + triton::FuncOp destination, triton::FuncOp source) { + if (source.getNumArguments() != source.getFunctionType().getNumInputs() || + !hasProgramGridHiddenExtentArguments(source)) + return failure(); + + SmallVector sourceArgAttrs; + const bool sourceHasArgAttrs = static_cast(source.getAllArgAttrs()); + if (sourceHasArgAttrs) { + source.getAllArgAttrs(sourceArgAttrs); + if (sourceArgAttrs.size() != source.getNumArguments()) + return failure(); + } + + destination.setFunctionType(source.getFunctionType()); + if (sourceHasArgAttrs) + destination.setAllArgAttrs(sourceArgAttrs); + else + destination->removeAttr("arg_attrs"); + destination.getRegion().takeBody(source.getRegion()); + return success(); +} + +FailureOr +mlir::triton::cfg::parseProgramGridTransformContract(Attribute attribute) { + auto contract = dyn_cast_or_null(attribute); + if (!contract) + return failure(); + if (!hasExactKeys(contract, + {kVersion, kExtentSource, kHiddenExtentAxes, + kHiddenArgumentOrder, kHiddenArgumentTypes, kTransforms})) + return failure(); + + std::optional version = getInteger(contract, kVersion); + auto extentSource = dyn_cast_or_null(contract.get(kExtentSource)); + constexpr std::array kAxes = {0, 1}; + constexpr std::array kArgumentOrder = {kOriginalGridX, + kOriginalGridY}; + constexpr std::array kArgumentTypes = {kI32, kI32}; + if (!version || *version != kProgramGridTransformsVersion || !extentSource || + extentSource.getValue() != kRuntimeOriginalGrid || + !hasExactIntegerArray(contract.get(kHiddenExtentAxes), kAxes) || + !hasExactStringArray(contract.get(kHiddenArgumentOrder), + kArgumentOrder) || + !hasExactStringArray(contract.get(kHiddenArgumentTypes), kArgumentTypes)) + return failure(); + + auto transforms = dyn_cast_or_null(contract.get(kTransforms)); + if (!transforms || transforms.empty() || transforms.size() > 2) + return failure(); + + ProgramGridTransformContract parsed; + parsed.version = *version; + bool seenAxis[2] = {false, false}; + for (auto [expectedOrder, rawTransform] : llvm::enumerate(transforms)) { + auto transform = dyn_cast(rawTransform); + if (!transform || + !hasExactKeys(transform, {kOrder, kKind, kAxis, kFactor, + kPersistentCoverage, kGridStrideAbiVerified})) + return failure(); + std::optional order = getInteger(transform, kOrder); + auto kind = dyn_cast_or_null(transform.get(kKind)); + std::optional axis = getInteger(transform, kAxis); + std::optional factor = getInteger(transform, kFactor); + std::optional persistent = getBoolean(transform, kPersistentCoverage); + std::optional gridStride = + getBoolean(transform, kGridStrideAbiVerified); + if (!order || !kind || !axis || !factor || !persistent || !gridStride || + *order != static_cast(expectedOrder) || + kind.getValue() != kCeilDiv || *axis < 0 || *axis > 1 || *factor < 2 || + *persistent != *gridStride || seenAxis[*axis]) + return failure(); + seenAxis[*axis] = true; + parsed.transforms.push_back(ProgramGridTransform{ + static_cast(*order), static_cast(*axis), *factor, + *persistent, *gridStride}); + } + if (!isSupportedSequence(parsed.transforms)) + return failure(); + return parsed; +} + +DictionaryAttr mlir::triton::cfg::serializeProgramGridTransformContract( + MLIRContext *context, const ProgramGridTransformContract &contract) { + Builder builder(context); + SmallVector transforms; + transforms.reserve(contract.transforms.size()); + for (const ProgramGridTransform &transform : contract.transforms) { + transforms.push_back(DictionaryAttr::get( + context, + {{builder.getStringAttr(kOrder), + builder.getI64IntegerAttr(transform.order)}, + {builder.getStringAttr(kKind), builder.getStringAttr(kCeilDiv)}, + {builder.getStringAttr(kAxis), + builder.getI64IntegerAttr(transform.axis)}, + {builder.getStringAttr(kFactor), + builder.getI64IntegerAttr(transform.factor)}, + {builder.getStringAttr(kPersistentCoverage), + builder.getBoolAttr(transform.persistentCoverage)}, + {builder.getStringAttr(kGridStrideAbiVerified), + builder.getBoolAttr(transform.gridStrideAbiVerified)}})); + } + return DictionaryAttr::get( + context, + {{builder.getStringAttr(kVersion), + builder.getI64IntegerAttr(contract.version)}, + {builder.getStringAttr(kExtentSource), + builder.getStringAttr(kRuntimeOriginalGrid)}, + {builder.getStringAttr(kHiddenExtentAxes), + ArrayAttr::get(context, {builder.getI64IntegerAttr(0), + builder.getI64IntegerAttr(1)})}, + {builder.getStringAttr(kHiddenArgumentOrder), + ArrayAttr::get(context, {builder.getStringAttr(kOriginalGridX), + builder.getStringAttr(kOriginalGridY)})}, + {builder.getStringAttr(kHiddenArgumentTypes), + ArrayAttr::get(context, {builder.getStringAttr(kI32), + builder.getStringAttr(kI32)})}, + {builder.getStringAttr(kTransforms), + ArrayAttr::get(context, transforms)}}); +} + +LogicalResult mlir::triton::cfg::setProgramGridTransformContract( + ModuleOp module, const ProgramGridTransformContract &contract) { + DictionaryAttr serialized = + serializeProgramGridTransformContract(module.getContext(), contract); + if (failed(parseProgramGridTransformContract(serialized))) + return failure(); + + triton::FuncOp entry; + for (triton::FuncOp function : module.getOps()) { + if (!isPublicEntry(function)) + continue; + if (entry) + return failure(); + entry = function; + } + if (!entry || !hasProgramGridHiddenExtentArguments(entry)) + return failure(); + module->setAttr(kProgramGridTransformsAttr, serialized); + return success(); +} diff --git a/third_party/ascend/lib/TritonToGraph/ProgramMappingScalarSpecialization.cpp b/third_party/ascend/lib/TritonToGraph/ProgramMappingScalarSpecialization.cpp new file mode 100644 index 0000000000..ec6391a060 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/ProgramMappingScalarSpecialization.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#include "TritonToGraph/ProgramMappingScalarSpecialization.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/IR/Builders.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" + +#include +#include + +using namespace mlir; +using namespace mlir::triton::cfg; + +namespace { + +constexpr llvm::StringLiteral kVersion = "version"; +constexpr llvm::StringLiteral kArguments = "arguments"; +constexpr llvm::StringLiteral kIndex = "index"; +constexpr llvm::StringLiteral kName = "name"; +constexpr llvm::StringLiteral kValue = "value"; + +bool hasExactKeys(DictionaryAttr dictionary, ArrayRef keys) { + if (dictionary.size() != keys.size()) + return false; + return llvm::all_of(keys, + [&](llvm::StringRef key) { return dictionary.get(key); }); +} + +std::optional getInteger(DictionaryAttr dictionary, + llvm::StringRef name) { + auto value = dyn_cast_or_null(dictionary.get(name)); + if (!value) + return std::nullopt; + return value.getInt(); +} + +std::optional getString(DictionaryAttr dictionary, + llvm::StringRef name) { + auto value = dyn_cast_or_null(dictionary.get(name)); + if (!value || value.getValue().empty()) + return std::nullopt; + return value.getValue(); +} + +void setAttrOnFunctions(ModuleOp module, llvm::StringRef name, + Attribute attribute) { + module.walk([&](Operation *operation) { + if (operation->getName().getStringRef() == "tt.func") + operation->setAttr(name, attribute); + }); +} + +void removeAttrFromFunctions(ModuleOp module, llvm::StringRef name) { + module.walk([&](Operation *operation) { + if (operation->getName().getStringRef() == "tt.func") + operation->removeAttr(name); + }); +} + +bool isPublicEntry(triton::FuncOp function) { + auto visibility = function->getAttrOfType("sym_visibility"); + return !visibility || visibility.getValue() == "public"; +} + +bool sameScalarSpecialization(const ProgramMappingScalarSpecialization &lhs, + const ProgramMappingScalarSpecialization &rhs) { + if (lhs.version != rhs.version || + lhs.arguments.size() != rhs.arguments.size()) + return false; + for (unsigned index = 0; index < lhs.arguments.size(); ++index) { + const ProgramMappingScalarArgument &left = lhs.arguments[index]; + const ProgramMappingScalarArgument &right = rhs.arguments[index]; + if (left.index != right.index || left.name != right.name || + left.value != right.value) + return false; + } + return true; +} + +bool isRepresentableInIntegerType(int64_t value, IntegerType type) { + const unsigned width = type.getWidth(); + if (width == 0) + return false; + if (width >= 64) + return true; + const int64_t minimum = -(int64_t{1} << (width - 1)); + const int64_t maximum = (int64_t{1} << (width - 1)) - 1; + return value >= minimum && value <= maximum; +} + +std::optional findEntryArgumentByName(triton::FuncOp function, + llvm::StringRef name) { + std::optional found; + for (unsigned index = 0; index < function.getNumArguments(); ++index) { + BlockArgument argument = function.getArgument(index); + auto namedLocation = dyn_cast(argument.getLoc()); + if (!namedLocation || namedLocation.getName().getValue() != name) + continue; + if (found) + return std::nullopt; + found = argument; + } + return found; +} + +} // namespace + +FailureOr +mlir::triton::cfg::parseProgramMappingScalarSpecialization( + Attribute attribute) { + auto dictionary = dyn_cast_or_null(attribute); + if (!dictionary || !hasExactKeys(dictionary, {kVersion, kArguments})) + return failure(); + + std::optional version = getInteger(dictionary, kVersion); + auto arguments = dyn_cast_or_null(dictionary.get(kArguments)); + if (!version || + (*version != kProgramMappingScalarSpecializationLegacyVersion && + *version != kProgramMappingScalarSpecializationVersion) || + !arguments || arguments.empty()) + return failure(); + + ProgramMappingScalarSpecialization parsed; + parsed.version = *version; + std::optional previousIndex; + for (Attribute attribute : arguments) { + auto argument = dyn_cast(attribute); + const bool isLegacy = + *version == kProgramMappingScalarSpecializationLegacyVersion; + if (!argument || (isLegacy && !hasExactKeys(argument, {kIndex, kValue})) || + (!isLegacy && !hasExactKeys(argument, {kIndex, kName, kValue}))) + return failure(); + std::optional index = getInteger(argument, kIndex); + std::optional value = getInteger(argument, kValue); + std::optional name = + isLegacy ? std::optional(llvm::StringRef()) + : getString(argument, kName); + if (!index || !value || *index < 0 || + *index > std::numeric_limits::max() || !name || + (previousIndex && *index <= *previousIndex)) + return failure(); + parsed.arguments.push_back(ProgramMappingScalarArgument{ + static_cast(*index), name->str(), *value}); + previousIndex = *index; + } + return parsed; +} + +DictionaryAttr mlir::triton::cfg::serializeProgramMappingScalarSpecialization( + MLIRContext *context, + const ProgramMappingScalarSpecialization &specialization) { + Builder builder(context); + SmallVector arguments; + arguments.reserve(specialization.arguments.size()); + for (const ProgramMappingScalarArgument &argument : + specialization.arguments) { + SmallVector fields; + fields.emplace_back(builder.getStringAttr(kIndex), + builder.getI64IntegerAttr(argument.index)); + if (specialization.version == kProgramMappingScalarSpecializationVersion) + fields.emplace_back(builder.getStringAttr(kName), + builder.getStringAttr(argument.name)); + fields.emplace_back(builder.getStringAttr(kValue), + builder.getI64IntegerAttr(argument.value)); + arguments.push_back(DictionaryAttr::get(context, fields)); + } + return DictionaryAttr::get( + context, {{builder.getStringAttr(kVersion), + builder.getI64IntegerAttr(specialization.version)}, + {builder.getStringAttr(kArguments), + ArrayAttr::get(context, arguments)}}); +} + +LogicalResult mlir::triton::cfg::setProgramMappingScalarSpecialization( + ModuleOp module, const ProgramMappingScalarSpecialization &specialization) { + DictionaryAttr serialized = serializeProgramMappingScalarSpecialization( + module.getContext(), specialization); + if (failed(parseProgramMappingScalarSpecialization(serialized))) + return failure(); + module->setAttr(kProgramMappingScalarSpecializationAttr, serialized); + setAttrOnFunctions(module, kProgramMappingScalarSpecializationAttr, + serialized); + return success(); +} + +void mlir::triton::cfg::clearProgramMappingScalarSpecialization( + ModuleOp module) { + module->removeAttr(kProgramMappingScalarSpecializationAttr); + removeAttrFromFunctions(module, kProgramMappingScalarSpecializationAttr); +} + +LogicalResult +mlir::triton::cfg::applyProgramMappingScalarSpecialization(ModuleOp module) { + Attribute attribute = + module->getAttr(kProgramMappingScalarSpecializationAttr); + if (!attribute) { + clearProgramMappingScalarSpecialization(module); + return success(); + } + + FailureOr specialization = + parseProgramMappingScalarSpecialization(attribute); + if (failed(specialization)) { + clearProgramMappingScalarSpecialization(module); + return failure(); + } + + for (triton::FuncOp function : module.getOps()) { + Attribute functionAttribute = + function->getAttr(kProgramMappingScalarSpecializationAttr); + if (functionAttribute) { + FailureOr functionSpecialization = + parseProgramMappingScalarSpecialization(functionAttribute); + if (failed(functionSpecialization) || + !sameScalarSpecialization(*specialization, *functionSpecialization)) { + clearProgramMappingScalarSpecialization(module); + return failure(); + } + } + if (!isPublicEntry(function) || function->getNumRegions() != 1 || + function.getBody().empty()) + continue; + + Block &entry = function.getBody().front(); + OpBuilder builder(function.getContext()); + builder.setInsertionPointToStart(&entry); + for (const ProgramMappingScalarArgument &argument : + specialization->arguments) { + std::optional blockArgument; + if (specialization->version == + kProgramMappingScalarSpecializationLegacyVersion) { + if (argument.index < function.getNumArguments()) + blockArgument = function.getArgument(argument.index); + } else { + blockArgument = findEntryArgumentByName(function, argument.name); + } + if (!blockArgument) + continue; + auto integerType = dyn_cast(blockArgument->getType()); + if (!integerType || + !isRepresentableInIntegerType(argument.value, integerType)) + continue; + Value constant = builder.create( + function.getLoc(), argument.value, integerType.getWidth()); + blockArgument->replaceAllUsesWith(constant); + } + } + + clearProgramMappingScalarSpecialization(module); + return success(); +} diff --git a/third_party/ascend/lib/TritonToGraph/ResourceCostModel.cpp b/third_party/ascend/lib/TritonToGraph/ResourceCostModel.cpp new file mode 100644 index 0000000000..a49ebe00b1 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/ResourceCostModel.cpp @@ -0,0 +1,727 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/ResourceCostModel.h" + +#include "mlir/IR/BuiltinTypes.h" +#include "triton/Dialect/Triton/IR/Types.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/raw_ostream.h" + +#if TRITON_ASCEND_HAS_INPROC_COSTMODEL +#include "AscendModel/Analysis/HardwareConfig.h" +#endif + +#include +#include +#include +#include + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +bool checkedAdd(uint64_t lhs, uint64_t rhs, uint64_t &result) { + if (lhs > std::numeric_limits::max() - rhs) + return false; + result = lhs + rhs; + return true; +} + +bool checkedMul(uint64_t lhs, uint64_t rhs, uint64_t &result) { + if (lhs != 0 && rhs > std::numeric_limits::max() / lhs) + return false; + result = lhs * rhs; + return true; +} + +bool checkedScale(uint64_t value, uint64_t weight, uint64_t &result) { + return checkedMul(value, weight, result); +} + +bool checkedAccumulate(uint64_t value, uint64_t &total) { + uint64_t next = 0; + if (!checkedAdd(total, value, next)) + return false; + total = next; + return true; +} + +bool isTensorValue(Value value) { + return value && isa(value.getType()); +} + +bool isUBResidentTensor(Value value) { + if (!value) + return false; + auto tensor = dyn_cast(value.getType()); + return tensor && tensor.hasStaticShape() && + isa(tensor.getElementType()); +} + +bool isStorageViewOperation(Operation *operation) { + if (!operation) + return false; + const StringRef name = operation->getName().getStringRef(); + return name == "tt.expand_dims" || name == "tt.broadcast" || + name == "tt.reshape" || name == "tensor.cast" || + name == "tensor.collapse_shape" || name == "tensor.expand_shape"; +} + +bool isVirtualTensorOperation(Operation *operation) { + if (!operation) + return false; + const StringRef name = operation->getName().getStringRef(); + return name == "tt.make_range" || name == "tt.splat" || + name == "arith.constant"; +} + +std::optional getElementBitWidth(Type elementType) { + if (auto integer = dyn_cast(elementType)) + return integer.getWidth(); + if (auto floating = dyn_cast(elementType)) + return floating.getWidth(); + if (isa(elementType)) + return 64; + if (isa(elementType)) + return 64; + return std::nullopt; +} + +Value findCanonicalValue(Value value, const llvm::DenseMap &map) { + Value current = value; + while (true) { + auto it = map.find(current); + if (it == map.end() || it->second == current) + return current; + current = it->second; + } +} + +uint64_t +getDefinitionOrder(Value value, + const llvm::DenseMap &order) { + if (auto result = dyn_cast(value)) { + auto it = order.find(result.getOwner()); + return it == order.end() ? 0 : it->second; + } + if (auto argument = dyn_cast(value)) { + Operation *parent = argument.getOwner()->getParentOp(); + auto it = order.find(parent); + return it == order.end() ? 0 : it->second; + } + return 0; +} + +bool isDefinedInside(Operation *valueOwner, Operation *containingOp) { + return valueOwner && containingOp && + (valueOwner == containingOp || containingOp->isAncestor(valueOwner)); +} + +bool isDefinedInside(Value value, Operation *containingOp) { + if (auto result = dyn_cast(value)) + return isDefinedInside(result.getOwner(), containingOp); + if (auto argument = dyn_cast(value)) + return isDefinedInside(argument.getOwner()->getParentOp(), containingOp); + return false; +} + +bool isForLikeLoop(Operation *operation) { + return operation && operation->getName().getStringRef() == "scf.for" && + operation->getNumRegions() == 1 && !operation->getRegion(0).empty(); +} + +Operation *getValueDefiningOperation(Value value) { + if (auto result = dyn_cast(value)) + return result.getOwner(); + if (auto argument = dyn_cast(value)) + return argument.getOwner()->getParentOp(); + return nullptr; +} + +struct ConditionalRegion { + Operation *conditional = nullptr; + unsigned region = 0; +}; + +std::optional getConditionalRegion(Value value) { + Operation *operation = getValueDefiningOperation(value); + while (operation) { + Region *region = operation->getParentRegion(); + if (!region) + return std::nullopt; + Operation *parent = region->getParentOp(); + if (!parent) + return std::nullopt; + if (parent->getName().getStringRef() == "scf.if") { + for (unsigned index = 0; index < parent->getNumRegions(); ++index) + if (&parent->getRegion(index) == region) + return ConditionalRegion{parent, index}; + } + operation = parent; + } + return std::nullopt; +} + +bool areMutuallyExclusive(Value lhs, Value rhs) { + std::optional left = getConditionalRegion(lhs); + std::optional right = getConditionalRegion(rhs); + return left && right && left->conditional == right->conditional && + left->region != right->region; +} + +void addCanonicalFactors(llvm::ArrayRef input, + llvm::SmallVectorImpl &output) { + for (unsigned value : input) { + if (value != 0) + output.push_back(value); + } + if (output.empty()) + output.push_back(1); + llvm::sort(output); + output.erase(std::unique(output.begin(), output.end()), output.end()); +} + +bool addWeightedChange(uint64_t before, uint64_t after, uint64_t weight, + uint64_t &gain, uint64_t &penalty) { + const uint64_t difference = before >= after ? before - after : after - before; + uint64_t weighted = 0; + if (!checkedScale(difference, weight, weighted)) + return false; + return before >= after ? checkedAccumulate(weighted, gain) + : checkedAccumulate(weighted, penalty); +} + +CandidateEvaluation reject(const CandidateCost &candidate, + ResourceCostRejectReason reason, + uint64_t safeUBBudget = 0, + uint64_t requiredParallelPrograms = 0) { + CandidateEvaluation evaluation; + evaluation.candidate = candidate; + evaluation.reason = reason; + evaluation.safeUBBudgetBytes = safeUBBudget; + evaluation.requiredParallelPrograms = requiredParallelPrograms; + evaluation.remark = formatCandidateRemark(evaluation); + return evaluation; +} + +} // namespace + +const char * +cfg::getResourceCostRejectReasonName(ResourceCostRejectReason reason) { + switch (reason) { + case ResourceCostRejectReason::None: + return "none"; + case ResourceCostRejectReason::UnknownResource: + return "unknown_resource"; + case ResourceCostRejectReason::DynamicShape: + return "dynamic_shape"; + case ResourceCostRejectReason::UnknownElementType: + return "unknown_element_type"; + case ResourceCostRejectReason::Overflow: + return "overflow"; + case ResourceCostRejectReason::UBOverflow: + return "ub_overflow"; + case ResourceCostRejectReason::InvalidCandidate: + return "invalid_candidate"; + } + return "unknown_resource"; +} + +bool ResourceSnapshot::isKnown() const { + return ubCapacityBytes != 0 && reservedUBBytes <= ubCapacityBytes && + ubSafetyPercent > 0 && ubSafetyPercent <= 100; +} + +std::optional ResourceSnapshot::getSafeUBBudget() const { + if (!isKnown()) + return std::nullopt; + + const uint64_t available = ubCapacityBytes - reservedUBBytes; + const uint64_t wholePercent = available / 100; + const uint64_t remainder = available % 100; + uint64_t budget = 0; + if (!checkedMul(wholePercent, ubSafetyPercent, budget)) + return std::nullopt; + uint64_t remainderBudget = 0; + if (!checkedMul(remainder, ubSafetyPercent, remainderBudget)) + return std::nullopt; + if (!checkedAdd(budget, remainderBudget / 100, budget)) + return std::nullopt; + return budget; +} + +ResourceSnapshot ResourceSnapshot::fromExplicit(uint64_t ubCapacity, + unsigned safetyPercent, + uint64_t reservedUB) { + ResourceSnapshot result; + result.ubCapacityBytes = ubCapacity; + result.reservedUBBytes = reservedUB; + result.ubSafetyPercent = safetyPercent; + return result; +} + +ResourceSnapshot +ResourceSnapshot::fromHardwareConfig(const ascend::HardwareConfig &hardware, + unsigned safetyPercent, + uint64_t reservedUB) { +#if TRITON_ASCEND_HAS_INPROC_COSTMODEL + const ascend::MemorySpace *ub = hardware.getMemorySpace("ub"); + if (!ub || ub->sizeBytes == 0 || + ub->sizeBytes > std::numeric_limits::max()) + return {}; + return fromExplicit(static_cast(ub->sizeBytes), safetyPercent, + reservedUB); +#else + (void)hardware; + (void)safetyPercent; + (void)reservedUB; + return {}; +#endif +} + +std::optional cfg::getStaticTensorBytes(Type type) { + auto tensor = dyn_cast(type); + if (!tensor || !tensor.hasStaticShape()) + return std::nullopt; + + std::optional elementBits = + getElementBitWidth(tensor.getElementType()); + if (!elementBits || *elementBits == 0) + return std::nullopt; + + uint64_t elements = 1; + for (int64_t dimension : tensor.getShape()) { + if (dimension < 0) + return std::nullopt; + if (!checkedMul(elements, static_cast(dimension), elements)) + return std::nullopt; + } + + uint64_t totalBits = 0; + if (!checkedMul(elements, *elementBits, totalBits)) + return std::nullopt; + if (totalBits > std::numeric_limits::max() - 7) + return std::nullopt; + return (totalBits + 7) / 8; +} + +LiveByteEstimate cfg::estimatePeakLiveBytes(Operation *root) { + LiveByteEstimate estimate; + if (!root) + return estimate; + + llvm::DenseMap order; + llvm::SmallVector operations; + root->walk([&](Operation *operation) { + order[operation] = operations.size(); + operations.push_back(operation); + }); + + llvm::DenseMap canonicalValues; + for (Operation *operation : operations) { + if (!isForLikeLoop(operation)) + continue; + + Block &body = operation->getRegion(0).front(); + const unsigned initCount = + operation->getNumOperands() < 3 ? 0 : operation->getNumOperands() - 3; + if (body.getNumArguments() < initCount + 1 || + operation->getNumResults() != initCount) + continue; + + for (unsigned index = 0; index < initCount; ++index) + canonicalValues[body.getArgument(index + 1)] = + findCanonicalValue(operation->getOperand(3 + index), canonicalValues); + + Operation *terminator = body.getTerminator(); + if (!terminator || terminator->getName().getStringRef() != "scf.yield" || + terminator->getNumOperands() != initCount) + continue; + for (unsigned index = 0; index < initCount; ++index) + canonicalValues[operation->getResult(index)] = + findCanonicalValue(terminator->getOperand(index), canonicalValues); + } + + for (Operation *operation : operations) { + if (!isStorageViewOperation(operation) || operation->getNumOperands() != 1) + continue; + Value source = operation->getOperand(0); + if (!isTensorValue(source)) + continue; + Value owner = findCanonicalValue(source, canonicalValues); + for (Value result : operation->getResults()) + if (isTensorValue(result)) + canonicalValues[result] = owner; + } + + struct IntervalState { + Value value; + uint64_t begin = 0; + uint64_t end = 0; + uint64_t bytes = 0; + }; + llvm::DenseMap intervals; + + auto addValue = [&](Value value) { + if (!isTensorValue(value) || !isUBResidentTensor(value)) + return; + Value canonical = findCanonicalValue(value, canonicalValues); + if (intervals.count(canonical)) + return; + + if (Operation *owner = getValueDefiningOperation(canonical); + isVirtualTensorOperation(owner)) + return; + + std::optional bytes = getStaticTensorBytes(canonical.getType()); + if (!bytes) { + estimate.reason = isa(canonical.getType()) || + (isa(canonical.getType()) && + !cast(canonical.getType()) + .hasStaticShape()) + ? ResourceCostRejectReason::DynamicShape + : ResourceCostRejectReason::UnknownElementType; + return; + } + const uint64_t begin = getDefinitionOrder(canonical, order); + intervals.try_emplace(canonical, + IntervalState{canonical, begin, begin, *bytes}); + }; + + for (Operation *operation : operations) { + for (Value value : operation->getResults()) + addValue(value); + for (Region ®ion : operation->getRegions()) { + for (Block &block : region) + for (BlockArgument argument : block.getArguments()) + addValue(argument); + } + } + + if (estimate.reason == ResourceCostRejectReason::DynamicShape || + estimate.reason == ResourceCostRejectReason::UnknownElementType) + return estimate; + + for (Operation *operation : operations) { + const uint64_t useOrder = order.lookup(operation); + for (Value operand : operation->getOperands()) { + Value canonical = findCanonicalValue(operand, canonicalValues); + auto it = intervals.find(canonical); + if (it != intervals.end()) + it->second.end = std::max(it->second.end, useOrder); + } + } + + for (Operation *loop : operations) { + if (!isForLikeLoop(loop)) + continue; + + uint64_t loopEnd = order.lookup(loop); + loop->getRegion(0).walk([&](Operation *nested) { + loopEnd = std::max(loopEnd, order.lookup(nested)); + }); + loop->getRegion(0).walk([&](Operation *nested) { + for (Value operand : nested->getOperands()) { + Value canonical = findCanonicalValue(operand, canonicalValues); + auto it = intervals.find(canonical); + if (it != intervals.end() && !isDefinedInside(canonical, loop)) + it->second.end = std::max(it->second.end, loopEnd); + } + }); + } + + llvm::SmallVector positions; + positions.reserve(intervals.size() * 2); + for (const auto &entry : intervals) { + const IntervalState &state = entry.second; + estimate.intervals.push_back( + LiveTensorInterval{state.value, state.begin, state.end, state.bytes}); + positions.push_back(state.begin); + if (state.end == std::numeric_limits::max()) { + estimate.overflow = true; + estimate.reason = ResourceCostRejectReason::Overflow; + return estimate; + } + positions.push_back(state.end + 1); + } + + llvm::sort(positions); + positions.erase(std::unique(positions.begin(), positions.end()), + positions.end()); + for (uint64_t position : positions) { + llvm::SmallVector live; + for (const auto &entry : intervals) { + const IntervalState &state = entry.second; + if (state.begin <= position && position <= state.end) + live.push_back(&state); + } + llvm::sort(live, [](const IntervalState *lhs, const IntervalState *rhs) { + if (lhs->bytes != rhs->bytes) + return lhs->bytes > rhs->bytes; + return lhs->begin < rhs->begin; + }); + uint64_t liveBytes = 0; + llvm::SmallVector selected; + for (const IntervalState *state : live) { + bool exclusive = false; + for (const IntervalState *other : selected) { + if (areMutuallyExclusive(state->value, other->value)) { + exclusive = true; + break; + } + } + if (exclusive) + continue; + if (!checkedAdd(liveBytes, state->bytes, liveBytes)) { + estimate.overflow = true; + estimate.reason = ResourceCostRejectReason::Overflow; + return estimate; + } + selected.push_back(state); + } + estimate.peakLiveBytes = std::max(estimate.peakLiveBytes, liveBytes); + } + + estimate.known = true; + estimate.reason = ResourceCostRejectReason::None; + return estimate; +} + +CandidateEvaluation +cfg::evaluateCandidateCost(const ResourceSnapshot &resources, + const CandidateCost &candidate, + const CostModelWeights &weights) { + if (!resources.isKnown()) + return reject(candidate, ResourceCostRejectReason::UnknownResource); + + std::optional safeBudget = resources.getSafeUBBudget(); + if (!safeBudget) + return reject(candidate, ResourceCostRejectReason::Overflow); + + if (candidate.hasDynamicShape) + return reject(candidate, ResourceCostRejectReason::DynamicShape, + *safeBudget); + if (candidate.hasUnknownResource || !candidate.hasPeakLiveBytes) + return reject(candidate, ResourceCostRejectReason::UnknownResource, + *safeBudget); + if (candidate.plan.tensorizeFactor == 0 || candidate.plan.blockT == 0 || + candidate.plan.staticAxisFusionFactor == 0) + return reject(candidate, ResourceCostRejectReason::InvalidCandidate, + *safeBudget); + if (candidate.estimatedPeakLiveBytes > *safeBudget) + return reject(candidate, ResourceCostRejectReason::UBOverflow, *safeBudget); + + uint64_t gains = 0; + uint64_t penalties = 0; + if (!addWeightedChange(candidate.launchesBefore, candidate.launchesAfter, + weights.launchReduction, gains, penalties) || + !addWeightedChange(candidate.gmReadBytesBefore, + candidate.gmReadBytesAfter, weights.gmByte, gains, + penalties) || + !addWeightedChange(candidate.gmWriteBytesBefore, + candidate.gmWriteBytesAfter, weights.gmByte, gains, + penalties) || + !addWeightedChange(candidate.storeCountBefore, candidate.storeCountAfter, + weights.store, gains, penalties) || + !addWeightedChange(candidate.addressCalculationsBefore, + candidate.addressCalculationsAfter, + weights.addressCalculation, gains, penalties) || + !addWeightedChange(candidate.baselinePeakLiveBytes, + candidate.estimatedPeakLiveBytes, weights.liveByte, + gains, penalties) || + !addWeightedChange(candidate.tokenOnlyRepeatedBytesBefore, + candidate.tokenOnlyRepeatedBytesAfter, + weights.tokenOnlyRepeatedByte, gains, penalties) || + gains > static_cast(std::numeric_limits::max()) || + penalties > static_cast(std::numeric_limits::max())) + return reject(candidate, ResourceCostRejectReason::Overflow, *safeBudget, + 0); + + CandidateEvaluation evaluation; + evaluation.candidate = candidate; + evaluation.accepted = true; + evaluation.reason = ResourceCostRejectReason::None; + evaluation.benefitScore = + static_cast(gains) - static_cast(penalties); + evaluation.safeUBBudgetBytes = *safeBudget; + evaluation.requiredParallelPrograms = 0; + evaluation.effectiveWorkPerProgram = 0; + evaluation.physicalWavesBefore = 0; + evaluation.physicalWavesAfter = 0; + evaluation.persistentLoopTripsBefore = 0; + evaluation.persistentLoopTripsAfter = 0; + evaluation.tokenOnlyRepeatedBytesBefore = + candidate.tokenOnlyRepeatedBytesBefore; + evaluation.tokenOnlyRepeatedBytesAfter = + candidate.tokenOnlyRepeatedBytesAfter; + evaluation.remark = formatCandidateRemark(evaluation); + return evaluation; +} + +llvm::SmallVector +cfg::enumerateCandidatePlans(llvm::ArrayRef tensorizeFactors, + llvm::ArrayRef blockTFactors, + llvm::ArrayRef staticAxisFusionFactors, + llvm::StringRef stableIdPrefix) { + llvm::SmallVector tensorize; + llvm::SmallVector blockT; + llvm::SmallVector staticFusion; + addCanonicalFactors(tensorizeFactors, tensorize); + addCanonicalFactors(blockTFactors, blockT); + addCanonicalFactors(staticAxisFusionFactors, staticFusion); + + llvm::SmallVector plans; + uint64_t ordinal = 0; + for (unsigned tensorizeFactor : tensorize) { + for (unsigned tokenFactor : blockT) { + for (unsigned staticFactor : staticFusion) { + CandidatePlan plan; + plan.tensorizeFactor = tensorizeFactor; + plan.blockT = tokenFactor; + plan.staticAxisFusionFactor = staticFactor; + plan.stableId = + (llvm::Twine(stableIdPrefix) + ".t" + llvm::Twine(tensorizeFactor) + + ".b" + llvm::Twine(tokenFactor) + ".f" + llvm::Twine(staticFactor)) + .str(); + plan.sourceOrdinal = ordinal++; + plans.push_back(std::move(plan)); + } + } + } + return plans; +} + +void cfg::sortCandidateEvaluations( + llvm::MutableArrayRef values) { + std::stable_sort( + values.begin(), values.end(), + [](const CandidateEvaluation &lhs, const CandidateEvaluation &rhs) { + if (lhs.accepted != rhs.accepted) + return lhs.accepted; + if (lhs.benefitScore != rhs.benefitScore) + return lhs.benefitScore > rhs.benefitScore; + const CandidatePlan &left = lhs.candidate.plan; + const CandidatePlan &right = rhs.candidate.plan; + return std::tie(left.tensorizeFactor, left.blockT, + left.staticAxisFusionFactor, left.stableId, + left.sourceOrdinal) < + std::tie(right.tensorizeFactor, right.blockT, + right.staticAxisFusionFactor, right.stableId, + right.sourceOrdinal); + }); +} + +std::string cfg::formatCandidateRemark(const CandidateEvaluation &evaluation) { + const CandidateCost &candidate = evaluation.candidate; + std::string remark; + llvm::raw_string_ostream stream(remark); + stream << "resource-cost candidate=" << candidate.plan.stableId + << " accepted=" << (evaluation.accepted ? "true" : "false") + << " reason=" << getResourceCostRejectReasonName(evaluation.reason) + << " score=" << evaluation.benefitScore + << " tensorize_factor=" << candidate.plan.tensorizeFactor + << " block_t=" << candidate.plan.blockT + << " static_axis_fusion_factor=" + << candidate.plan.staticAxisFusionFactor + << " logical_programs=" << candidate.logicalTasksBefore << "->" + << candidate.logicalTasksAfter + << " physical_blocks=" << candidate.actualProgramsBefore << "->" + << candidate.actualProgramsAfter + << " physical_waves=" << evaluation.physicalWavesBefore << "->" + << evaluation.physicalWavesAfter << " legacy_auto_map=" + << (candidate.legacyAutoMapBefore ? "true" : "false") << "->" + << (candidate.legacyAutoMapAfter ? "true" : "false") + << " required_programs=" << evaluation.requiredParallelPrograms + << " gm_read_bytes=" << candidate.gmReadBytesBefore << "->" + << candidate.gmReadBytesAfter + << " gm_write_bytes=" << candidate.gmWriteBytesBefore << "->" + << candidate.gmWriteBytesAfter + << " stores=" << candidate.storeCountBefore << "->" + << candidate.storeCountAfter + << " address_calculations=" << candidate.addressCalculationsBefore + << "->" << candidate.addressCalculationsAfter + << " peak_live_bytes=" << candidate.baselinePeakLiveBytes << "->" + << candidate.estimatedPeakLiveBytes + << " ub_budget_bytes=" << evaluation.safeUBBudgetBytes + << " work_per_program=" << evaluation.effectiveWorkPerProgram + << " persistent_loop_trips=" << evaluation.persistentLoopTripsBefore + << "->" << evaluation.persistentLoopTripsAfter + << " token_only_repeated_bytes=" + << evaluation.tokenOnlyRepeatedBytesBefore << "->" + << evaluation.tokenOnlyRepeatedBytesAfter + << " persistent=" << (candidate.persistent ? "true" : "false"); + return std::move(stream.str()); +} + +void cfg::emitCandidateRemark(Operation *anchor, + const CandidateEvaluation &evaluation) { + if (anchor) + anchor->emitRemark() << evaluation.remark; +} + +std::string +cfg::formatProfilerCalibrationRecord(const ProfilerCalibrationRecord &record) { + std::string formatted; + llvm::raw_string_ostream stream(formatted); + stream << "resource-cost-calibration schema=v1 kernel=" << record.kernelName + << " profiler_artifact=" << record.profilerArtifact + << " samples=" << record.sampleCount + << " median_ns=" << record.medianNanoseconds + << " p90_ns=" << record.p90Nanoseconds << " " + << formatCandidateRemark(record.evaluation); + return std::move(stream.str()); +} + +ResourceCostAnalysis::ResourceCostAnalysis(Operation *root, + ResourceSnapshot resources) + : resources(resources), liveBytes(estimatePeakLiveBytes(root)) {} + +CandidateEvaluation +ResourceCostAnalysis::evaluate(const CandidateCost &candidate) const { + CandidateCost effectiveCandidate = candidate; + if (!effectiveCandidate.hasPeakLiveBytes && liveBytes.known) { + effectiveCandidate.hasPeakLiveBytes = true; + effectiveCandidate.baselinePeakLiveBytes = liveBytes.peakLiveBytes; + effectiveCandidate.estimatedPeakLiveBytes = liveBytes.peakLiveBytes; + } + if (!liveBytes.known) { + if (liveBytes.reason == ResourceCostRejectReason::DynamicShape) + effectiveCandidate.hasDynamicShape = true; + else + effectiveCandidate.hasUnknownResource = true; + } + CandidateEvaluation evaluation = + evaluateCandidateCost(resources, effectiveCandidate); + if (liveBytes.reason == ResourceCostRejectReason::Overflow) { + evaluation.accepted = false; + evaluation.reason = ResourceCostRejectReason::Overflow; + evaluation.benefitScore = 0; + evaluation.remark = formatCandidateRemark(evaluation); + } + return evaluation; +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/ContiguousBlockAccessFormationRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/ContiguousBlockAccessFormationRule.cpp new file mode 100644 index 0000000000..0c07024a3b --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/ContiguousBlockAccessFormationRule.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include + +using namespace mlir; +using namespace triton; +using namespace cfg; + +std::unique_ptr +cfg::createContiguousBlockAccessFormationRule( + const ContiguousBlockAccessFormationRuleOptions &options) { + static_cast(options); + return createReservedGraphOptimizationRule( + {GraphOptimizationRuleId::ContiguousBlockAccessFormation, + "contiguous-block-access-formation"}); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/ConvertModuloToMaskRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/ConvertModuloToMaskRule.cpp new file mode 100644 index 0000000000..63a3865ce3 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/ConvertModuloToMaskRule.cpp @@ -0,0 +1,566 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Verifier.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +// A modulo whose result is only ever used to compute load addresses, and whose +// wrapped lanes are already discarded by a store mask. Such a modulo can be +// dropped so the addresses become linear again, provided the loads that consume +// them get a boundary mask so the wrapped lanes read in-bounds zeros instead of +// the data the wrap used to fetch. +struct ModuloCandidate { + // The op defining the modulo result: either arith.remsi, or the subi of the + // divsi/muli/subi expansion that canonicalization leaves behind. + Operation *anchor = nullptr; + Value dividend; + Value divisor; + Value boundScalar; + Value result; + // anchor plus, for the expanded form, the muli and divsi that die with it. + SmallVector patternOps; + SmallVector loads; + int64_t tileSize = 0; + // The axis that expand_dims inserted on the modulo result. The mask has to + // be expanded on the same axis to vary along the same dimension of the load. + int64_t axis = 0; +}; + +Value getSplatScalar(Value value) { + if (auto splat = value.getDefiningOp()) + return splat.getSrc(); + return nullptr; +} + +// Reports the extent of the tt.make_range that the tile offset is built from. +// The offset is `splat(pid * BLOCK) + make_range(0, BLOCK)`, so the range is +// reached through the addi. +int64_t getMakeRangeExtent(Value value) { + if (auto range = value.getDefiningOp()) + return static_cast(range.getEnd()) - + static_cast(range.getStart()); + if (auto add = value.getDefiningOp()) { + if (int64_t lhs = getMakeRangeExtent(add.getLhs())) + return lhs; + return getMakeRangeExtent(add.getRhs()); + } + return 0; +} + +bool isCompileTimeConstant(Value value) { + APInt constant; + return matchPattern(value, m_ConstantInt(&constant)); +} + +// Reports whether `mask` reaches the mask operand of a tt.store. Shape ops and +// arith.andi are transparent because a store mask is normally the conjunction +// of one comparison per output axis, each broadcast to the output shape. +bool feedsStoreMask(Value mask, SmallPtrSetImpl &visited) { + for (OpOperand &use : mask.getUses()) { + Operation *user = use.getOwner(); + if (auto store = dyn_cast(user)) { + if (store.getMask() == mask) + return true; + continue; + } + if (!isa(user)) + continue; + if (!visited.insert(user).second) + continue; + if (feedsStoreMask(user->getResult(0), visited)) + return true; + } + return false; +} + +// Reports whether some `cmpi slt` of the un-wrapped tile offset against the +// same bound guards a store. This is the proof that the kernel itself treats +// offsets at or beyond the bound as absent, so the values the wrap used to +// fetch for those lanes never reach memory. +// +// It also pins the modulo to an axis of the output. A modulo on a reduction +// axis could not be rewritten this way, because zeros injected there would +// change every output element rather than only the discarded lanes, and such a +// modulo has no store-mask guard precisely because the axis has no output +// coordinate. +bool hasStoreMaskGuard(Value offset, Value boundScalar) { + SmallVector forms = {offset}; + for (OpOperand &use : offset.getUses()) { + if (isa(use.getOwner())) + forms.push_back(use.getOwner()->getResult(0)); + } + + for (Value form : forms) { + for (OpOperand &use : form.getUses()) { + auto compare = dyn_cast(use.getOwner()); + if (!compare || compare.getPredicate() != arith::CmpIPredicate::slt || + compare.getLhs() != form) + continue; + Value bound = getSplatScalar(compare.getRhs()); + if (bound != boundScalar) + continue; + SmallPtrSet visited; + if (feedsStoreMask(compare.getResult(), visited)) + return true; + } + } + return false; +} + +// Returns true when two distinct loop-carried paths cross nested scf.for ops. +// An address passed through one loop may still be lowered as a direct access, +// but the current structured lowering does not preserve every component of a +// nested loop-carried pointer state. Keep the modulo in that shape until the +// lowerer can prove the relay lossless. +bool crossesNestedCarriedFor(ArrayRef carriedForChain, + scf::ForOp nextFor) { + Operation *next = nextFor.getOperation(); + return llvm::any_of(carriedForChain, [next](Operation *carriedFor) { + if (carriedFor == next) + return false; + return carriedFor->isProperAncestor(next) || + next->isProperAncestor(carriedFor); + }); +} + +// Walks forward from the modulo result and collects the loads it addresses. +// Returns false as soon as the value reaches anything else, which is what makes +// dropping the wrap unobservable: no other consumer can see the widened index. +// +// The allowlist is restricted to ops that preserve dimension positions, so the +// axis recorded for the modulo result stays the axis it varies along in every +// load that it reaches. tt.trans and tt.reshape are therefore rejected. +bool collectAddressedLoads(Value value, SmallPtrSetImpl &visited, + SmallVectorImpl &loads, + ArrayRef carriedForChain) { + if (!visited.insert(value).second) + return true; + + for (OpOperand &use : value.getUses()) { + Operation *user = use.getOwner(); + + if (auto load = dyn_cast(user)) { + // Reject a value that is the `other` operand rather than the address: + // rewriting it would change the loaded data itself. + if (load.getPtr() != value) + return false; + loads.push_back(load); + continue; + } + if (isa(user)) { + for (Value result : user->getResults()) { + if (!collectAddressedLoads(result, visited, loads, carriedForChain)) + return false; + } + continue; + } + if (auto forOp = dyn_cast(user)) { + // An address carried around a loop stays an address only if the iter arg + // and the loop result are both used as one. + for (auto [index, initArg] : llvm::enumerate(forOp.getInitArgs())) { + if (initArg != value) + continue; + if (crossesNestedCarriedFor(carriedForChain, forOp)) + return false; + SmallVector nextChain(carriedForChain); + nextChain.push_back(forOp.getOperation()); + if (!collectAddressedLoads(forOp.getRegionIterArg(index), visited, + loads, nextChain)) + return false; + } + continue; + } + if (auto yield = dyn_cast(user)) { + auto forOp = dyn_cast(yield->getParentOp()); + if (!forOp) + return false; + unsigned index = use.getOperandNumber(); + if (index >= forOp.getNumResults()) + return false; + if (crossesNestedCarriedFor(carriedForChain, forOp)) + return false; + SmallVector nextChain(carriedForChain); + nextChain.push_back(forOp.getOperation()); + if (!collectAddressedLoads(forOp.getResult(index), visited, loads, + nextChain)) + return false; + continue; + } + + return false; + } + return true; +} + +// Reports the single axis that expand_dims inserted on the modulo result. +// Distinct axes mean the one index addresses two different dimensions, and no +// single mask orientation would be correct for both, so such a result is +// rejected rather than rewritten on a guessed axis. +std::optional getUniqueExpandDimsAxis(Value value) { + std::optional axis; + for (OpOperand &use : value.getUses()) { + auto expand = dyn_cast(use.getOwner()); + if (!expand) + continue; + int64_t current = expand.getAxis(); + if (axis && *axis != current) + return std::nullopt; + axis = current; + } + return axis; +} + +// Reports whether a boundary mask of `tileSize` lanes, expanded on `axis`, can +// be broadcast to every loaded shape. Checking it up front means the mask ops +// this rule creates are valid by construction. +bool areLoadsMaskable(ArrayRef loads, int64_t axis, + int64_t tileSize) { + if (loads.empty() || axis < 0 || axis > 1) + return false; + for (triton::LoadOp load : loads) { + auto type = dyn_cast(load.getResult().getType()); + if (!type || !type.hasStaticShape() || type.getRank() != 2 || + type.getShape()[1 - axis] != tileSize) + return false; + // A load carrying `other` without a mask reads it nowhere, so injecting a + // mask would suddenly make that operand observable. + if (!load.getMask() && load.getOther()) + return false; + if (!load.getBoundaryCheck().empty() || load.getIsVolatile()) + return false; + } + return true; +} + +// Matches the divsi/muli/subi expansion of a modulo. It is equivalent to +// `x % d` by the signed-division identity x == (x / d) * d + x % d. +bool matchExpandedModulo(arith::SubIOp subtract, Value ÷nd, + Value &divisor, + SmallVectorImpl &patternOps) { + auto multiply = subtract.getRhs().getDefiningOp(); + if (!multiply) + return false; + + arith::DivSIOp divide; + Value candidateDivisor; + if (auto lhs = multiply.getLhs().getDefiningOp()) { + divide = lhs; + candidateDivisor = multiply.getRhs(); + } else if (auto rhs = multiply.getRhs().getDefiningOp()) { + divide = rhs; + candidateDivisor = multiply.getLhs(); + } else { + return false; + } + + if (divide.getLhs() != subtract.getLhs() || + divide.getRhs() != candidateDivisor) + return false; + + dividend = subtract.getLhs(); + divisor = candidateDivisor; + patternOps.assign({subtract.getOperation(), multiply.getOperation(), + divide.getOperation()}); + return true; +} + +std::optional analyzeModulo(Operation *op) { + ModuloCandidate candidate; + if (auto remainder = dyn_cast(op)) { + candidate.dividend = remainder.getLhs(); + candidate.divisor = remainder.getRhs(); + candidate.patternOps.push_back(op); + } else if (auto subtract = dyn_cast(op)) { + if (!matchExpandedModulo(subtract, candidate.dividend, candidate.divisor, + candidate.patternOps)) + return std::nullopt; + } else { + return std::nullopt; + } + + candidate.anchor = op; + candidate.result = op->getResult(0); + + // A 1-D i32 index is the shape every tile offset has at this point in the + // pipeline. Anything else is left alone rather than reasoned about. + auto type = dyn_cast(candidate.result.getType()); + if (!type || !type.hasStaticShape() || type.getRank() != 1 || + !type.getElementType().isInteger(32) || type.getEncoding()) + return std::nullopt; + + candidate.tileSize = getMakeRangeExtent(candidate.dividend); + if (candidate.tileSize <= 0 || candidate.tileSize != type.getShape()[0]) + return std::nullopt; + + candidate.boundScalar = getSplatScalar(candidate.divisor); + if (!candidate.boundScalar) + return std::nullopt; + + // A constant divisor belongs to TritonToStructured, whose visitOperandRem + // keeps the wrap and re-expresses it as a strided access. That is exactly + // equivalent, so it is always preferable to discarding the wrap here. + if (isCompileTimeConstant(candidate.boundScalar)) + return std::nullopt; + + if (!hasStoreMaskGuard(candidate.dividend, candidate.boundScalar)) + return std::nullopt; + + std::optional axis = getUniqueExpandDimsAxis(candidate.result); + if (!axis) + return std::nullopt; + candidate.axis = *axis; + + SmallPtrSet visited; + SmallVector carriedForChain; + if (!collectAddressedLoads(candidate.result, visited, candidate.loads, + carriedForChain)) + return std::nullopt; + if (!areLoadsMaskable(candidate.loads, candidate.axis, candidate.tileSize)) + return std::nullopt; + + return candidate; +} + +bool matchesCandidate(const ModuloCandidate &candidate, + const ModuloCandidate ¤t) { + return candidate.anchor == current.anchor && + candidate.dividend == current.dividend && + candidate.divisor == current.divisor && + candidate.boundScalar == current.boundScalar && + candidate.result == current.result && + candidate.tileSize == current.tileSize && + candidate.axis == current.axis && + candidate.patternOps.size() == current.patternOps.size() && + std::equal(candidate.patternOps.begin(), candidate.patternOps.end(), + current.patternOps.begin()) && + candidate.loads.size() == current.loads.size() && + std::equal(candidate.loads.begin(), candidate.loads.end(), + current.loads.begin()); +} + +void eraseCreatedOperations(IRRewriter &rewriter, + ArrayRef created) { + for (Operation *operation : llvm::reverse(created)) + rewriter.eraseOp(operation); +} + +bool recordVerifiedOperation(Operation *operation, + SmallVectorImpl &created) { + if (!operation) + return false; + created.push_back(operation); + return succeeded(mlir::verify(operation)); +} + +// The mask that a single load needs, plus the zero fill for a load that had no +// mask before. Nothing is attached to the load until every load's operands +// have been built and verified. +struct LoadMask { + triton::LoadOp load; + Value mask; + Value other; +}; + +LogicalResult applyCandidate(IRRewriter &rewriter, + const ModuloCandidate &candidate) { + SmallVector created; + auto fail = [&]() { + eraseCreatedOperations(rewriter, created); + return failure(); + }; + + Location loc = candidate.anchor->getLoc(); + rewriter.setInsertionPointAfter(candidate.anchor); + auto boundary = rewriter.create( + loc, arith::CmpIPredicate::slt, candidate.dividend, candidate.divisor); + if (!recordVerifiedOperation(boundary.getOperation(), created)) + return fail(); + + SmallVector loadMasks; + for (triton::LoadOp load : candidate.loads) { + auto loadType = dyn_cast(load.getResult().getType()); + if (!loadType) + return fail(); + + rewriter.setInsertionPoint(load); + Location loadLoc = load.getLoc(); + auto expanded = rewriter.create( + loadLoc, boundary.getResult(), static_cast(candidate.axis)); + if (!recordVerifiedOperation(expanded.getOperation(), created)) + return fail(); + + auto maskType = + RankedTensorType::get(loadType.getShape(), rewriter.getI1Type()); + auto broadcast = rewriter.create(loadLoc, maskType, + expanded.getResult()); + if (!recordVerifiedOperation(broadcast.getOperation(), created)) + return fail(); + + LoadMask loadMask{load, broadcast.getResult(), nullptr}; + if (Value existing = load.getMask()) { + auto combined = rewriter.create(loadLoc, existing, + broadcast.getResult()); + if (!recordVerifiedOperation(combined.getOperation(), created)) + return fail(); + loadMask.mask = combined.getResult(); + } else { + Type elementType = loadType.getElementType(); + Attribute zero; + if (isa(elementType)) + zero = rewriter.getFloatAttr(elementType, 0.0); + else if (isa(elementType)) + zero = rewriter.getIntegerAttr(elementType, 0); + else + return fail(); + auto fill = rewriter.create( + loadLoc, DenseElementsAttr::get(loadType, zero)); + if (!recordVerifiedOperation(fill.getOperation(), created)) + return fail(); + loadMask.other = fill.getResult(); + } + loadMasks.push_back(loadMask); + } + if (loadMasks.empty()) + return fail(); + + // Everything needed has been built and verified, so the observable rewrite + // can now run without any step that could still fail. + for (LoadMask &loadMask : loadMasks) { + loadMask.load.getMaskMutable().assign(loadMask.mask); + if (loadMask.other) + loadMask.load.getOtherMutable().assign(loadMask.other); + } + + rewriter.replaceAllUsesWith(candidate.result, candidate.dividend); + for (Operation *operation : candidate.patternOps) { + if (operation->use_empty()) + rewriter.eraseOp(operation); + } + return success(); +} + +class ConvertModuloToMaskPlan final : public RewritePlan { +public: + ConvertModuloToMaskPlan(ModuloCandidate candidate, unsigned epoch) + : candidate(std::move(candidate)), epoch(epoch) {} + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::ConvertModuloToMask; + } + + // Every load that stops wrapping can become a contiguous transfer. + unsigned getBenefit() const override { + return static_cast(std::min( + candidate.loads.size(), std::numeric_limits::max())); + } + + Operation *getAnchor() const override { return candidate.anchor; } + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (candidate.anchor->getParentOfType() != + context.getFunction()) + return failure(); + std::optional current = analyzeModulo(candidate.anchor); + return current && matchesCandidate(candidate, *current) ? success() + : failure(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + // Re-prove locally so that a stale plan can never mutate the IR. + std::optional current = analyzeModulo(candidate.anchor); + if (!current || !matchesCandidate(candidate, *current)) + return failure(); + return applyCandidate(rewriter, *current); + } + +private: + ModuloCandidate candidate; + unsigned epoch; +}; + +class ConvertModuloToMaskRule final : public GraphOptimizationRule { +public: + GraphOptimizationRuleId getId() const override { + return GraphOptimizationRuleId::ConvertModuloToMask; + } + + AnalysisRequirement getAnalysisRequirements() const override { + return AnalysisRequirement::None; + } + + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + context.getFunction().walk([&](Operation *op) { + if (!isa(op)) + return; + if (std::optional candidate = analyzeModulo(op)) { + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast(getId()) << " (" + << getGraphOptimizationRuleName(getId()) << ") at " + << op->getLoc() << ": " << op->getName() + << " tileSize=" << candidate->tileSize + << " maskedLoads=" << candidate->loads.size() << "\n"); + plans.push_back(std::make_unique( + std::move(*candidate), context.getEpoch())); + } + }); + return success(); + } +}; + +} // namespace + +std::unique_ptr cfg::createConvertModuloToMaskRule() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/DiagonalMaskRemovalRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/DiagonalMaskRemovalRule.cpp new file mode 100644 index 0000000000..95a50782a2 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/DiagonalMaskRemovalRule.cpp @@ -0,0 +1,541 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// Replaces the O(N^2) "diagonal mask + reduce" vector shift with the O(N) +// cumulative-sum identity. +// +// A shift-by-one of a scan result is commonly expressed in the DSL as +// +// mask = (row == col + 1) +// matrix = tl.where(mask, tl.cumsum(x, axis=0)[None, :], 0) +// shifted = tl.sum(matrix, axis=1) +// +// which materializes an N x N intermediate to read scan[i - 1]. Because an +// inclusive scan satisfies scan[i] - x[i] == scan[i - 1] (and the reverse scan +// satisfies scan[i] - x[i] == scan[i + 1]), the whole pattern collapses into a +// single elementwise subtraction. +// +// Only a unit shift can be rewritten. For |shift| > 1 the pattern denotes a +// genuine data movement (scan[i - s] is scan[i] minus a sliding-window sum), +// and TTIR has no cheap way to express that, which is precisely why the DSL +// resorts to the diagonal-mask formulation in the first place. +// +// For floating point the rewrite is not bit-exact: scan[i] is computed as +// fl(scan[i - 1] + x[i]), so fl(scan[i] - x[i]) may differ from scan[i - 1] by +// about eps * |scan[i]|. The boundary element, where nothing is selected, is +// still exact because x - x is exactly +0. This trade is intentional and +// matches the DSL author's intent of avoiding the quadratic intermediate. + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Verifier.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +// The scan and the reduce must both combine with an add, and the rewrite has +// to materialize its inverse, so track which arithmetic family it belongs to. +enum class AddCombineKind { Float, Int }; + +std::optional getAddCombineKind(Region ®ion) { + if (region.empty()) + return std::nullopt; + + Block &body = region.front(); + if (body.getNumArguments() != 2) + return std::nullopt; + + auto bodyOps = body.without_terminator(); + if (std::distance(bodyOps.begin(), bodyOps.end()) != 1) + return std::nullopt; + + Operation &combine = *bodyOps.begin(); + if (combine.getNumResults() != 1 || combine.getNumOperands() != 2) + return std::nullopt; + + Operation *terminator = body.getTerminator(); + if (terminator->getNumOperands() != 1 || + terminator->getOperand(0) != combine.getResult(0)) + return std::nullopt; + + // Reject a combine that does not consume both block arguments, such as + // `a + a`, which is not an accumulation. + Value lhs = combine.getOperand(0); + Value rhs = combine.getOperand(1); + Value first = body.getArgument(0); + Value second = body.getArgument(1); + if (!((lhs == first && rhs == second) || (lhs == second && rhs == first))) + return std::nullopt; + + if (isa(combine)) + return AddCombineKind::Float; + if (isa(combine)) + return AddCombineKind::Int; + return std::nullopt; +} + +// Peels producers that only reshape a uniform value. A splat constant does +// not always reach this rule in splat form: reorder-broadcast runs after +// canonicalization in make_ttir and leaves constants behind a tt.broadcast +// that nothing folds away again. +Value peelUniformShapeOps(Value value) { + while (Operation *definingOp = value.getDefiningOp()) { + if (auto splat = dyn_cast(definingOp)) { + value = splat.getSrc(); + continue; + } + if (auto broadcast = dyn_cast(definingOp)) { + value = broadcast.getSrc(); + continue; + } + if (auto expand = dyn_cast(definingOp)) { + value = expand.getSrc(); + continue; + } + return value; + } + return value; +} + +// Matches a compile-time integer held by a scalar constant or by a uniform +// constant tensor, however it was shaped. +bool matchConstantInt(Value value, int64_t &result) { + Value uniform = peelUniformShapeOps(value); + + APInt scalar; + if (matchPattern(uniform, m_ConstantInt(&scalar))) { + result = scalar.getSExtValue(); + return true; + } + + DenseElementsAttr elements; + if (matchPattern(uniform, m_Constant(&elements)) && elements.isSplat() && + isa(elements.getElementType())) { + result = elements.getSplatValue().getSExtValue(); + return true; + } + return false; +} + +// The unselected lanes must contribute the reduce combine's identity, so that +// a row selecting nothing reduces to zero, which is exactly what the +// subtraction produces at the boundary element. +bool isAddIdentity(Value value, AddCombineKind kind) { + if (kind == AddCombineKind::Int) { + int64_t scalar = 0; + return matchConstantInt(value, scalar) && scalar == 0; + } + + Value uniform = peelUniformShapeOps(value); + + // Negative zero is rejected so the boundary element stays bit-identical to + // the +0 produced by `x - x`. + APFloat scalar(0.0); + if (matchPattern(uniform, m_ConstantFloat(&scalar))) + return scalar.isZero() && !scalar.isNegative(); + + DenseElementsAttr elements; + if (matchPattern(uniform, m_Constant(&elements)) && elements.isSplat() && + isa(elements.getElementType())) { + APFloat splatValue = elements.getSplatValue(); + return splatValue.isZero() && !splatValue.isNegative(); + } + return false; +} + +// Describes a value that is elementwise equal to `coordinate(dim) + offset`, +// where `coordinate(dim)` runs along dimension `dim` of the value's own tensor +// type. `length` is the extent of the originating tt.make_range, which is +// needed to prove that both sides of the diagonal span the same axis. +struct AxisIndex { + int64_t dim = 0; + int64_t offset = 0; + int64_t length = 0; +}; + +// Peels expand_dims / broadcast / constant offsets off an index expression +// down to its tt.make_range. Accepting the offset at any level, on either +// side of the add, keeps the match robust against canonicalization moving it +// across the reshapes. +std::optional matchAxisIndex(Value value) { + // Reshaping ops, outermost first. + SmallVector reshapes; + int64_t offset = 0; + Value current = value; + triton::MakeRangeOp range; + + while (true) { + Operation *definingOp = current.getDefiningOp(); + if (!definingOp) + return std::nullopt; + + if (auto makeRange = dyn_cast(definingOp)) { + range = makeRange; + break; + } + if (auto broadcast = dyn_cast(definingOp)) { + // tt.broadcast only stretches unit dimensions, so dimension indices are + // preserved and it needs no dimension bookkeeping. + current = broadcast.getSrc(); + continue; + } + if (auto expand = dyn_cast(definingOp)) { + reshapes.push_back(definingOp); + current = expand.getSrc(); + continue; + } + if (auto add = dyn_cast(definingOp)) { + int64_t constant = 0; + if (matchConstantInt(add.getRhs(), constant)) { + offset += constant; + current = add.getLhs(); + continue; + } + if (matchConstantInt(add.getLhs(), constant)) { + offset += constant; + current = add.getRhs(); + continue; + } + return std::nullopt; + } + if (auto sub = dyn_cast(definingOp)) { + // Only `index - constant` keeps the coordinate's unit stride; the + // mirrored `constant - index` would negate it. + int64_t constant = 0; + if (matchConstantInt(sub.getRhs(), constant)) { + offset -= constant; + current = sub.getLhs(); + continue; + } + return std::nullopt; + } + return std::nullopt; + } + + // The range varies along dimension 0 of its own type; replay the inserted + // dimensions from the innermost outwards to find where it ended up. + int64_t dim = 0; + for (Operation *reshape : llvm::reverse(reshapes)) { + auto expand = cast(reshape); + if (dim >= static_cast(expand.getAxis())) + ++dim; + } + + const int64_t start = static_cast(range.getStart()); + const int64_t length = static_cast(range.getEnd()) - start; + if (length <= 0) + return std::nullopt; + + // A non-zero range start shifts the diagonal just like an explicit offset + // does, so it has to participate in the shift computation. + return AxisIndex{dim, offset + start, length}; +} + +struct DiagonalCandidate { + triton::ReduceOp reduce; + arith::SelectOp select; + triton::BroadcastOp broadcast; + triton::ExpandDimsOp expand; + triton::ScanOp scan; + Value scanResult; + Value scanInput; + AddCombineKind kind = AddCombineKind::Float; + // The rewrite reads scanResult[p + shift] at result position p. + int64_t shift = 0; + int64_t eliminatedElements = 0; +}; + +std::optional analyzeDiagonal(triton::ReduceOp reduce) { + if (reduce.getNumOperands() != 1 || reduce->getNumResults() != 1) + return std::nullopt; + + std::optional kind = getAddCombineKind(reduce.getCombineOp()); + if (!kind) + return std::nullopt; + + auto select = reduce.getOperand(0).getDefiningOp(); + if (!select || !isAddIdentity(select.getFalseValue(), *kind)) + return std::nullopt; + + auto broadcast = select.getTrueValue().getDefiningOp(); + if (!broadcast) + return std::nullopt; + auto maskType = dyn_cast(broadcast.getType()); + if (!maskType || maskType.getRank() < 2 || !maskType.hasStaticShape()) + return std::nullopt; + + auto expand = broadcast.getSrc().getDefiningOp(); + if (!expand) + return std::nullopt; + Value scanResult = expand.getSrc(); + auto scanResultType = dyn_cast(scanResult.getType()); + if (!scanResultType || !scanResultType.hasStaticShape()) + return std::nullopt; + + // The replacement is `scan_result - scan_input`, so the reduction has to + // produce exactly the scan result's type. This subsumes the historical + // square-matrix requirement and extends it to leading batch dimensions. + if (reduce->getResult(0).getType() != scanResult.getType()) + return std::nullopt; + + const int64_t expandAxis = static_cast(expand.getAxis()); + const int64_t fastestDim = scanResultType.getRank() - 1; + // The replicated dimension must be adjacent to the scan's own fastest + // dimension. Inserting it anywhere else would transpose the reduced result + // against the scan result, and the subtraction would no longer be + // equivalent even though both types still match. + if (expandAxis != fastestDim && expandAxis != fastestDim + 1) + return std::nullopt; + + // Where the scan result's fastest dimension ends up inside the mask. + const int64_t reducedDim = + expandAxis == fastestDim ? fastestDim + 1 : fastestDim; + if (static_cast(reduce.getAxis()) != reducedDim) + return std::nullopt; + + auto scan = scanResult.getDefiningOp(); + if (!scan || scan.getNumOperands() != 1 || scan->getNumResults() != 1) + return std::nullopt; + if (static_cast(scan.getAxis()) != fastestDim) + return std::nullopt; + std::optional scanKind = + getAddCombineKind(scan.getCombineOp()); + if (!scanKind || *scanKind != *kind) + return std::nullopt; + + auto compare = select.getCondition().getDefiningOp(); + if (!compare || compare.getPredicate() != arith::CmpIPredicate::eq) + return std::nullopt; + + std::optional lhs = matchAxisIndex(compare.getLhs()); + std::optional rhs = matchAxisIndex(compare.getRhs()); + if (!lhs || !rhs) + return std::nullopt; + + // One side indexes the reduced dimension; the other indexes the replicated + // dimension and survives as the result's own coordinate. + const AxisIndex *reduced = nullptr; + const AxisIndex *kept = nullptr; + if (lhs->dim == reducedDim && rhs->dim == expandAxis) { + reduced = &*lhs; + kept = &*rhs; + } else if (rhs->dim == reducedDim && lhs->dim == expandAxis) { + reduced = &*rhs; + kept = &*lhs; + } else { + return std::nullopt; + } + + // Both sides must span their whole axis, and the two axes must agree; + // otherwise the element picked per row is not simply scanResult[p + shift]. + if (reduced->length != kept->length || + reduced->length != maskType.getDimSize(reduced->dim) || + kept->length != maskType.getDimSize(kept->dim)) + return std::nullopt; + + // The mask holds `kept + keptOffset == reduced + reducedOffset`, so the row + // at coordinate p selects source index p + (keptOffset - reducedOffset). + const int64_t shift = kept->offset - reduced->offset; + if (shift != 1 && shift != -1) + return std::nullopt; + + // shift == -1 reads scan[p - 1], which only the forward identity + // scan[p] - x[p] == scan[p - 1] provides; shift == +1 reads scan[p + 1], + // which is the reverse-scan identity. + const bool needsReverseScan = shift == 1; + if (scan.getReverse() != needsReverseScan) + return std::nullopt; + + return DiagonalCandidate{reduce, + select, + broadcast, + expand, + scan, + scanResult, + scan.getOperand(0), + *kind, + shift, + maskType.getNumElements()}; +} + +bool matchesCandidate(const DiagonalCandidate &candidate, + const DiagonalCandidate ¤t) { + return candidate.reduce == current.reduce && + candidate.select == current.select && + candidate.broadcast == current.broadcast && + candidate.expand == current.expand && candidate.scan == current.scan && + candidate.scanResult == current.scanResult && + candidate.scanInput == current.scanInput && + candidate.kind == current.kind && candidate.shift == current.shift; +} + +// Erases the quadratic intermediate once it is unused. The walk is restricted +// to the side-effect-free ops this pattern is built from, so it can never +// remove anything observable. +void eraseDeadPatternOp(IRRewriter &rewriter, Operation *op) { + if (!op || !op->use_empty()) + return; + if (!isa(op)) + return; + + SmallVector producers; + for (Value operand : op->getOperands()) { + if (Operation *producer = operand.getDefiningOp()) + producers.push_back(producer); + } + + rewriter.eraseOp(op); + for (Operation *producer : producers) + eraseDeadPatternOp(rewriter, producer); +} + +class DiagonalMaskRemovalPlan final : public RewritePlan { +public: + DiagonalMaskRemovalPlan(DiagonalCandidate candidate, unsigned epoch) + : candidate(candidate), epoch(epoch) {} + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::DiagonalMaskRemoval; + } + + // Larger intermediates are worth collapsing first. + unsigned getBenefit() const override { + constexpr int64_t maxBenefit = std::numeric_limits::max(); + return static_cast( + std::min(candidate.eliminatedElements, maxBenefit)); + } + + Operation *getAnchor() const override { return candidate.reduce; } + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (candidate.reduce->getParentOfType() != + context.getFunction()) + return failure(); + std::optional current = + analyzeDiagonal(candidate.reduce); + return current && matchesCandidate(candidate, *current) ? success() + : failure(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + // Re-prove locally so that a stale plan can never mutate the IR. + std::optional current = + analyzeDiagonal(candidate.reduce); + if (!current || !matchesCandidate(candidate, *current)) + return failure(); + + Value reduceResult = candidate.reduce->getResult(0); + if (reduceResult.getType() != candidate.scanResult.getType() || + candidate.scanInput.getType() != candidate.scanResult.getType()) + return failure(); + + rewriter.setInsertionPoint(candidate.reduce); + Location loc = candidate.reduce.getLoc(); + Value replacement = + candidate.kind == AddCombineKind::Float + ? rewriter + .create(loc, candidate.scanResult, + candidate.scanInput) + .getResult() + : rewriter + .create(loc, candidate.scanResult, + candidate.scanInput) + .getResult(); + + Operation *replacementOp = replacement.getDefiningOp(); + if (failed(mlir::verify(replacementOp))) { + rewriter.eraseOp(replacementOp); + return failure(); + } + + rewriter.replaceAllUsesWith(reduceResult, replacement); + + arith::SelectOp select = candidate.select; + rewriter.eraseOp(candidate.reduce); + eraseDeadPatternOp(rewriter, select); + return success(); + } + +private: + DiagonalCandidate candidate; + unsigned epoch; +}; + +class DiagonalMaskRemovalRule final : public GraphOptimizationRule { +public: + GraphOptimizationRuleId getId() const override { + return GraphOptimizationRuleId::DiagonalMaskRemoval; + } + + AnalysisRequirement getAnalysisRequirements() const override { + return AnalysisRequirement::None; + } + + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + context.getFunction().walk([&](triton::ReduceOp reduce) { + if (std::optional candidate = + analyzeDiagonal(reduce)) { + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast(getId()) << " (" + << getGraphOptimizationRuleName(getId()) << ") at " + << reduce.getLoc() << "\n"); + plans.push_back(std::make_unique( + *candidate, context.getEpoch())); + } + }); + return success(); + } +}; + +} // namespace + +std::unique_ptr cfg::createDiagonalMaskRemovalRule() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/DotRowCoalescing.cpp b/third_party/ascend/lib/TritonToGraph/Rules/DotRowCoalescing.cpp new file mode 100644 index 0000000000..de1b687a29 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/DotRowCoalescing.cpp @@ -0,0 +1,819 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/DotRowCoalescing.h" +#include "TritonToGraph/ProgramGridTransform.h" + +#include "bishengir/Dialect/HACC/Utils/Utils.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +// Internal RowCoalescing pattern for a counted, independent row matmul loop. +// No kernel name, Python argument name, or public tuning option is inspected. +// A static x-grid selects an exact divisor. Without it, use the existing +// original-grid hidden arguments and mask the final partial group. Indirect +// scalar loads deliberately remain on the original path. + +constexpr llvm::StringLiteral kCoalesceFactorAttr = "hacc.coalesce_factor"; +constexpr llvm::StringLiteral kCoalesceAxisAttr = "hacc.coalesce_axis"; +constexpr llvm::StringLiteral kCoalesceGridCeilDivAttr = + "hacc.coalesce_grid_ceil_div"; + +static std::optional getConstantIntValue(Value v) { + APInt value; + if (matchPattern(v, m_ConstantInt(&value))) + return value.getSExtValue(); + + return std::nullopt; +} + +struct IntegerBounds { + int64_t lo; + int64_t hi; +}; + +// Prove monotonicity together with a signed range. A syntactically increasing +// expression is not monotonic if an intermediate integer operation wraps. +static std::optional inferMonotonicBounds(Value value, Value pid, + int64_t grid) { + if (value == pid) + return IntegerBounds{0, grid - 1}; + + if (auto constant = getConstantIntValue(value)) + return IntegerBounds{*constant, *constant}; + + if (auto ext = value.getDefiningOp()) + return inferMonotonicBounds(ext.getIn(), pid, grid); + + Operation *op = value.getDefiningOp(); + if (!op || op->getNumOperands() != 2) + return std::nullopt; + + auto lhs = inferMonotonicBounds(op->getOperand(0), pid, grid); + auto rhs = inferMonotonicBounds(op->getOperand(1), pid, grid); + if (!lhs || !rhs) + return std::nullopt; + + IntegerBounds result; + if (isa(op)) { + if (__builtin_add_overflow(lhs->lo, rhs->lo, &result.lo) || + __builtin_add_overflow(lhs->hi, rhs->hi, &result.hi)) + return std::nullopt; + } else if (isa(op)) { + if (lhs->lo < 0 || rhs->lo < 0 || + __builtin_mul_overflow(lhs->lo, rhs->lo, &result.lo) || + __builtin_mul_overflow(lhs->hi, rhs->hi, &result.hi)) + return std::nullopt; + } else if (isa(op) && rhs->lo == rhs->hi && rhs->lo > 0) { + result = {lhs->lo / rhs->lo, lhs->hi / rhs->lo}; + } else { + return std::nullopt; + } + + auto integer = dyn_cast(value.getType()); + if (!integer || integer.getWidth() > 64 || + !APInt(64, result.lo, true).isSignedIntN(integer.getWidth()) || + !APInt(64, result.hi, true).isSignedIntN(integer.getWidth())) + return std::nullopt; + + return result; +} + +static bool dependsOnValue(Value v, Value pid) { + if (v == pid) + return true; + + Operation *op = v.getDefiningOp(); + return op && llvm::any_of(op->getOperands(), + [&](Value a) { return dependsOnValue(a, pid); }); +} + +struct RowAddressRange { + int64_t stride = 0, lo = 0, hi = 0; +}; + +// Scalar offsets independent of x translate every lane by the same amount. +// Ignore that common translation, but retain the constant x coefficient and +// tensor lane span. Unknown products/casts fail closed. +static std::optional analyzeRowAddress(Value v, Value pid) { + if (v == pid) + return RowAddressRange{1, 0, 0}; + + if (!isa(v.getType()) && !dependsOnValue(v, pid)) + return RowAddressRange{}; + + Operation *op = v.getDefiningOp(); + if (!op) + return std::nullopt; + + if (auto range = dyn_cast(op)) + return RowAddressRange{0, range.getStart(), range.getEnd() - 1}; + + if (isa(op)) + return analyzeRowAddress(op->getOperand(0), pid); + + if (isa(op)) { + auto a = analyzeRowAddress(op->getOperand(0), pid), + b = analyzeRowAddress(op->getOperand(1), pid); + if (!a || !b) + return std::nullopt; + + RowAddressRange out; + if (__builtin_add_overflow(a->stride, b->stride, &out.stride) || + __builtin_add_overflow(a->lo, b->lo, &out.lo) || + __builtin_add_overflow(a->hi, b->hi, &out.hi)) + return std::nullopt; + + return out; + } + + if (auto mul = dyn_cast(op)) { + auto c = getConstantIntValue(mul.getRhs()); + Value other = mul.getLhs(); + if (!c) { + c = getConstantIntValue(mul.getLhs()); + other = mul.getRhs(); + } + + if (!c || *c <= 0) + return std::nullopt; + + auto a = analyzeRowAddress(other, pid); + if (!a) + return std::nullopt; + + RowAddressRange out; + if (__builtin_mul_overflow(a->stride, *c, &out.stride) || + __builtin_mul_overflow(a->lo, *c, &out.lo) || + __builtin_mul_overflow(a->hi, *c, &out.hi)) + return std::nullopt; + + return out; + } + + return std::nullopt; +} + +static int64_t chooseRowCoalescingFactor(int64_t grid, int64_t maxDotRows, + int64_t maxTensorBytes) { + // Profitability policy, not a correctness requirement. Bound the widened M + // dimension and the largest materialized value, including pointer tensors. + // This is deliberately conservative, not a claim about peak live UB use or + // an autotuned optimum. A known grid selects an exact factor; zero denotes + // a runtime extent whose partial group is masked. + constexpr int64_t targetDotRows = 128; + constexpr int64_t maxLiftedValueBytes = 128 * 1024; + + for (int64_t factor : {32, 16, 8, 4, 2}) { + if ((!grid || grid % factor == 0) && maxDotRows <= targetDotRows / factor && + maxTensorBytes <= maxLiftedValueBytes / factor) { + return factor; + } + } + + return 1; +} + +struct RowCandidate { + triton::FuncOp function; + Operation *anchor = nullptr; + int64_t rowsPerProgram = 1; + bool runtimeGrid = false; +}; + +static std::optional matchDotRows(triton::FuncOp fn) { + ModuleOp module = fn->getParentOfType(); + auto grid = module->getAttrOfType("hacc.grid_num_tiles"); + if ((grid && (grid.getInt() <= 0 || grid.getInt() > INT32_MAX)) || + !llvm::hasSingleElement(fn.getBody()) || !fn.isPublic() || + llvm::count_if(module.getOps(), + [](triton::FuncOp entry) { return entry.isPublic(); }) != + 1 || + module->hasAttr(kCoalesceFactorAttr) || + module->hasAttr("hacc.program_grid_transforms")) + return std::nullopt; + + LLVM_DEBUG(llvm::dbgs() << "dot-row: grid gate passed\n"); + + triton::GetProgramIdOp pid; + scf::ForOp loop; + triton::StoreOp output; + unsigned pids = 0, loops = 0, dots = 0, stores = 0; + int64_t maxDotRows = 1; + int64_t maxTensorBytes = 1; + bool safe = true; + DenseSet lifted; + + // Propagate row dependence while checking operations in definition order. + fn.walk([&](Operation *op) { + bool dependent = llvm::any_of( + op->getOperands(), [&](Value value) { return lifted.contains(value); }); + if (dependent) + lifted.insert(op->getResults().begin(), op->getResults().end()); + + for (Type type : op->getResultTypes()) { + if (auto tensor = dyn_cast(type)) { + Type element = tensor.getElementType(); + int64_t bytes = isa(element) + ? 8 + : (element.getIntOrFloatBitWidth() + 7) / 8; + int64_t size; + if (__builtin_mul_overflow(tensor.getNumElements(), bytes, &size)) + safe = false; + else + maxTensorBytes = std::max(maxTensorBytes, size); + } + } + + if (auto p = dyn_cast(op)) { + if (p.getAxisAsInt() == 0) { + pid = p; + ++pids; + lifted.insert(p.getResult()); + } + return; + } + + if (auto f = dyn_cast(op)) { + loop = f; + ++loops; + // Loop states are per-row even when their initial values are uniform. + lifted.insert(f.getRegionIterArgs().begin(), f.getRegionIterArgs().end()); + lifted.insert(f.getResults().begin(), f.getResults().end()); + if (f->getBlock() != &fn.getBody().front()) + safe = false; + return; + } + + if (auto d = dyn_cast(op)) { + ++dots; + auto a = dyn_cast(d.getA().getType()); + auto b = dyn_cast(d.getB().getType()); + if (!a || !b || a.getRank() != 2 || b.getRank() != 2 || + !(a.getElementType().isF16() || a.getElementType().isBF16()) || + (dependent && + (!lifted.contains(d.getA()) || lifted.contains(d.getB())))) + safe = false; + else + maxDotRows = std::max(maxDotRows, a.getShape()[0]); + return; + } + + if (auto l = dyn_cast(op)) { + // A scalar metadata load is the varlen/indirect case, not this pattern. + if (!isa(l.getType()) || l.getIsVolatile() || + (dependent && op->getParentOfType())) + safe = false; + return; + } + + if (auto store = dyn_cast(op)) { + output = store; + ++stores; + if (op->getBlock() != &fn.getBody().front()) + safe = false; + return; + } + + // Reduction regions are outside this pattern's supported rewrite scope. + if (isa(op)) { + safe = false; + return; + } + + if (isa(op)) + return; + StringRef ns = op->getName().getDialectNamespace(); + // A partial group evaluates arithmetic for inactive rows as well. + if (!grid && dependent && (ns == "arith" || ns == "math") && + !isSpeculatable(op)) + safe = false; + if ((ns != "arith" && ns != "math") || op->getNumRegions()) + safe = false; + }); + + LLVM_DEBUG(llvm::dbgs() << "dot-row: safe=" << safe << " pids=" << pids + << " loops=" << loops << " dots=" << dots + << " stores=" << stores << "\n"); + if (!safe || pids != 1 || loops != 1 || dots == 0 || stores != 1 || + loop.getNumResults() == 0 || + getConstantIntValue(loop.getLowerBound()) != std::optional(0) || + (dependsOnValue(loop.getUpperBound(), pid.getResult()) && + !inferMonotonicBounds(loop.getUpperBound(), pid.getResult(), + grid ? grid.getInt() : INT32_MAX))) + return std::nullopt; + + // Merging unequal trip counts speculates pure arithmetic for inactive rows. + // Preserve the old accumulator, but do not speculate potentially trapping + // operations such as integer division with an unproven nonzero divisor. + if (dependsOnValue(loop.getUpperBound(), pid.getResult())) { + for (Operation &op : loop.getBody()->without_terminator()) { + StringRef ns = op.getName().getDialectNamespace(); + if ((ns == "arith" || ns == "math") && !isSpeculatable(&op)) + return std::nullopt; + } + } + + for (Value init : loop.getInitArgs()) + if (!isa(init.getType())) + return std::nullopt; + + auto address = analyzeRowAddress(output.getPtr(), pid.getResult()); + if (!address || address->stride <= 0 || address->hi < address->lo || + static_cast(address->hi) - static_cast(address->lo) >= + static_cast(address->stride)) + return std::nullopt; + + auto step = getConstantIntValue(loop.getStep()); + if (!step || *step <= 0) + return std::nullopt; + + int64_t rows = chooseRowCoalescingFactor(grid ? grid.getInt() : 0, maxDotRows, + maxTensorBytes); + if (rows <= 1) + return std::nullopt; + + return RowCandidate{fn, pid.getOperation(), rows, !grid}; +} + +// Lift only query-dependent values. Invariant K/V loads stay two-dimensional; +// a lifted lhs [BT,G,K] is reshaped to [BT*G,K] at each dot, not batched dots. +class DotRowBuilder { + IRRewriter &rewriter; + RowCandidate candidate; + + // Original SSA values mapped to the rebuilt computation. + IRMapping valueMapping; + // Scalar values evaluated at the last row to form the shared loop bound. + IRMapping lastRowMapping; + Value validRows; + +public: + DotRowBuilder(IRRewriter &rewriter, RowCandidate candidate) + : rewriter(rewriter), candidate(candidate) {} + + bool run(triton::FuncOp fn) { + Block *old = &fn.getBody().front(); + Value upperBound = (*old->getOps().begin()).getUpperBound(); + auto *fresh = new Block(); + fn.getBody().push_back(fresh); + + for (BlockArgument arg : old->getArguments()) { + Value newArg = fresh->addArgument(arg.getType(), arg.getLoc()); + valueMapping.map(arg, newArg); + lastRowMapping.map(arg, newArg); + } + + rewriter.setInsertionPointToStart(fresh); + for (Operation &op : old->without_terminator()) { + if (&op == candidate.anchor) { + auto pid = cast(rewriter.clone(op)); + Value base = rewriter.create( + op.getLoc(), pid.getResult(), + rewriter.create( + op.getLoc(), candidate.rowsPerProgram, 32)); + Value last = rewriter.create( + op.getLoc(), base, + rewriter.create( + op.getLoc(), candidate.rowsPerProgram - 1, 32)); + if (candidate.runtimeGrid) { + Value extent = fresh->getArgument(fresh->getNumArguments() - 2); + Value lastValid = rewriter.create( + op.getLoc(), extent, + rewriter.create(op.getLoc(), 1, 32)); + last = rewriter.create(op.getLoc(), last, lastValid); + } + lastRowMapping.map(op.getResult(0), last); + + auto t = getLiftedType(op.getResult(0).getType()); + Value lanes = rewriter.create( + op.getLoc(), t, 0, candidate.rowsPerProgram); + Value start = rewriter.create(op.getLoc(), t, base); + valueMapping.map( + op.getResult(0), + rewriter.create(op.getLoc(), start, lanes) + .getResult()); + if (candidate.runtimeGrid) { + Value extent = fresh->getArgument(fresh->getNumArguments() - 2); + Value limit = + rewriter.create(op.getLoc(), t, extent); + validRows = rewriter.create( + op.getLoc(), arith::CmpIPredicate::slt, + valueMapping.lookup(op.getResult(0)), limit); + } + continue; + } + + // Only clone the pure scalar bound slice for the last-row evaluation. + if (op.getNumResults() == 1 && + !isa(op.getResult(0).getType()) && + op.getName().getDialectNamespace() == "arith" && + dependsOnValue(upperBound, op.getResult(0))) + rewriter.clone(op, lastRowMapping); + + if (!rewrite(&op)) + return false; + + for (Value v : op.getResults()) + if (!hasRowDimension(v) && !lastRowMapping.contains(v)) + lastRowMapping.map(v, getMappedValue(v)); + } + + rewriter.create(fn.getLoc()); + old->dropAllDefinedValueUses(); + old->erase(); + return true; + } + +private: + bool rewrite(Operation *op) { + if (auto loop = dyn_cast(op)) + return rewriteLoop(loop); + + bool dependent = llvm::any_of(op->getOperands(), + [&](Value v) { return hasRowDimension(v); }); + if (!dependent) { + rewriter.clone(*op, valueMapping); + return true; + } + + if (isa(op) && op->getParentOfType()) + return false; // No speculative query-dependent memory accesses. + if (auto dot = dyn_cast(op)) { + if (!hasRowDimension(dot.getA()) || hasRowDimension(dot.getB())) + return false; + + auto result = cast(dot.getType()); + SmallVector operands{ + flattenDotRows(ensureRowDimension(dot.getA())), + getMappedValue(dot.getB()), + flattenDotRows(ensureRowDimension(dot.getC()))}; + Type flatType = RankedTensorType::get( + {candidate.rowsPerProgram * result.getShape()[0], + result.getShape()[1]}, + result.getElementType()); + Operation *d = + rewriter.create(op->getLoc(), op->getName().getIdentifier(), operands, + TypeRange{flatType}, op->getAttrs()); + valueMapping.map( + dot.getResult(), + reshape(d->getResult(0), getLiftedType(result).getShape())); + return true; + } + + if (auto splat = dyn_cast(op)) { + valueMapping.map(splat.getResult(), + broadcastRowValue(ensureRowDimension(splat.getSrc()), + getLiftedType(splat.getType()))); + return true; + } + + if (auto expand = dyn_cast(op)) { + valueMapping.map( + expand.getResult(), + rewriter + .create(op->getLoc(), + ensureRowDimension(expand.getSrc()), + expand.getAxis() + 1) + .getResult()); + return true; + } + + if (auto view = dyn_cast(op)) { + // Preserve each row's element order. Allowing arbitrary reordering of + // the lifted tensor could mix values belonging to different + // programs. + valueMapping.map(view.getResult(), + reshape(ensureRowDimension(view.getSrc()), + getLiftedType(view.getType()).getShape())); + return true; + } + + if (auto trans = dyn_cast(op)) { + SmallVector order{0}; + for (int32_t axis : trans.getOrder()) + order.push_back(axis + 1); + valueMapping.map( + trans.getResult(), + rewriter + .create( + op->getLoc(), ensureRowDimension(trans.getSrc()), order) + .getResult()); + return true; + } + + if (candidate.runtimeGrid) { + if (auto load = dyn_cast(op)) { + Value pointer = ensureRowDimension(load.getPtr()); + Value mask = getMemoryMask(pointer, load.getMask()); + auto type = getLiftedType(load.getType()); + Value other = load.getOther() + ? ensureRowDimension(load.getOther()) + : rewriter.create( + op->getLoc(), rewriter.getZeroAttr(type)); + auto replacement = rewriter.create( + op->getLoc(), pointer, mask, other, load.getBoundaryCheck(), + load.getPadding(), load.getCache(), load.getEvict(), + load.getIsVolatile()); + valueMapping.map(load.getResult(), replacement.getResult()); + return true; + } + + if (auto store = dyn_cast(op)) { + Value pointer = ensureRowDimension(store.getPtr()); + rewriter.create( + op->getLoc(), pointer, ensureRowDimension(store.getValue()), + getMemoryMask(pointer, store.getMask()), store.getBoundaryCheck(), + store.getCache(), store.getEvict()); + return true; + } + } + + SmallVector operands; + for (Value v : op->getOperands()) + operands.push_back(ensureRowDimension(v)); + + SmallVector types; + for (Type t : op->getResultTypes()) + types.push_back(getLiftedType(t)); + + Operation *cloned = + rewriter.create(op->getLoc(), op->getName().getIdentifier(), operands, + types, op->getAttrs()); + valueMapping.map(op->getResults(), cloned->getResults()); + return true; + } + + Value getMemoryMask(Value pointer, Value originalMask) { + auto type = cast(pointer.getType()); + Value mask = broadcastRowValue( + validRows, + RankedTensorType::get(type.getShape(), rewriter.getI1Type())); + if (originalMask) + mask = rewriter.create(pointer.getLoc(), mask, + ensureRowDimension(originalMask)); + return mask; + } + + bool rewriteLoop(scf::ForOp old) { + SmallVector init; + for (Value arg : old.getInitArgs()) + init.push_back(ensureRowDimension(arg)); + + // The monotonic upper-bound proof means the last row supplies the maximum. + auto loop = rewriter.create( + old.getLoc(), getMappedValue(old.getLowerBound()), + lastRowMapping.lookup(old.getUpperBound()), + getMappedValue(old.getStep()), init); + loop->setAttrs(old->getAttrs()); + + valueMapping.map(old.getInductionVar(), loop.getInductionVar()); + valueMapping.map(old.getRegionIterArgs(), loop.getRegionIterArgs()); + + rewriter.setInsertionPointToStart(loop.getBody()); + for (Operation &op : old.getBody()->without_terminator()) + if (!rewrite(&op)) + return false; + + auto yield = cast(old.getBody()->getTerminator()); + Value iv = rewriter.create( + old.getLoc(), getLiftedType(old.getInductionVar().getType()), + loop.getInductionVar()); + Value active = rewriter.create( + old.getLoc(), arith::CmpIPredicate::slt, iv, + ensureRowDimension(old.getUpperBound())); + + SmallVector output; + for (auto [v, prev] : + llvm::zip(yield.getOperands(), loop.getRegionIterArgs())) { + auto t = cast(prev.getType()); + Value mask = broadcastRowValue( + active, RankedTensorType::get(t.getShape(), rewriter.getI1Type())); + // Retain exactly the previous accumulator for rows whose original loop + // did not execute this iteration (also preserves NaN/Inf behavior). + output.push_back(rewriter.create( + old.getLoc(), mask, ensureRowDimension(v), prev)); + } + + if (!loop.getBody()->empty() && isa(loop.getBody()->back())) + rewriter.eraseOp(loop.getBody()->getTerminator()); + rewriter.setInsertionPointToEnd(loop.getBody()); + rewriter.create(old.getLoc(), output); + + valueMapping.map(old.getResults(), loop.getResults()); + + rewriter.setInsertionPointAfter(loop); + return true; + } + + RankedTensorType getLiftedType(Type old) { + SmallVector shape{candidate.rowsPerProgram}; + Type elem = old; + if (auto tensor = dyn_cast(old)) { + shape.append(tensor.getShape().begin(), tensor.getShape().end()); + elem = tensor.getElementType(); + } + return RankedTensorType::get(shape, elem); + } + + Value getMappedValue(Value old) { return valueMapping.lookup(old); } + + // Every lifted value gains a leading row dimension, so its type already + // records this state; no separate boolean needs to be maintained. + bool hasRowDimension(Value old) { + return getMappedValue(old).getType() != old.getType(); + } + + Value ensureRowDimension(Value old) { + Value value = getMappedValue(old); + if (hasRowDimension(old)) + return value; + + if (!isa(old.getType())) + return rewriter.create( + old.getLoc(), getLiftedType(old.getType()), value); + + Value expanded = + rewriter.create(old.getLoc(), value, 0); + return rewriter.create( + old.getLoc(), getLiftedType(old.getType()), expanded); + } + + Value reshape(Value v, ArrayRef shape) { + auto t = cast(v.getType()); + return rewriter.create( + v.getLoc(), RankedTensorType::get(shape, t.getElementType()), v, false); + } + + Value flattenDotRows(Value v) { + auto t = cast(v.getType()); + return reshape(v, {t.getShape()[0] * t.getShape()[1], t.getShape()[2]}); + } + + Value broadcastRowValue(Value v, RankedTensorType target) { + while (cast(v.getType()).getRank() < target.getRank()) + v = rewriter.create( + v.getLoc(), v, cast(v.getType()).getRank()); + return rewriter.create(v.getLoc(), target, v); + } +}; + +class DotRowPlan final : public RewritePlan { + RowCandidate candidate; + unsigned epoch; + OwningOpRef prepared; + + DotRowPlan(RowCandidate candidate, unsigned epoch, + OwningOpRef prepared) + : candidate(candidate), epoch(epoch), prepared(std::move(prepared)) {} + +public: + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::RowCoalescing; + } + + unsigned getBenefit() const override { return 1; } + + Operation *getAnchor() const override { return candidate.function; } + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (context.getFunction() != candidate.function) + return failure(); + + auto current = matchDotRows(candidate.function); + return success(current && current->anchor == candidate.anchor && + current->rowsPerProgram == candidate.rowsPerProgram && + current->runtimeGrid == candidate.runtimeGrid); + } + + static std::unique_ptr create(RowCandidate candidate, + unsigned epoch) { + // Prepare and verify privately before offering a plan to the scheduler. + // An unsupported rewrite must leave the input and its diagnostics + // untouched. + ModuleOp module = candidate.function->getParentOfType(); + OwningOpRef sandbox( + ModuleOp::create(candidate.function.getLoc())); + sandbox->getOperation()->setAttrs(module->getAttrs()); + + IRMapping mapping; + sandbox->getBody()->push_back(candidate.function->clone(mapping)); + + RowCandidate copy = candidate; + copy.function = cast(&sandbox->getBody()->front()); + copy.anchor = + mapping.lookup(candidate.anchor->getResult(0)).getDefiningOp(); + + IRRewriter rw(module.getContext()); + ScopedDiagnosticHandler diagnostics( + module.getContext(), [](Diagnostic &diag) { + LLVM_DEBUG(llvm::dbgs() + << "dot-row: skipped candidate: " << diag << "\n"); + return success(); + }); + if (copy.runtimeGrid && + failed(addProgramGridHiddenExtentArguments(copy.function))) + return nullptr; + + DotRowBuilder builder(rw, copy); + if (!builder.run(copy.function) || failed(verify(*sandbox))) + return nullptr; + + if (copy.runtimeGrid) { + ProgramGridTransformContract contract; + contract.transforms.push_back({0, 0, copy.rowsPerProgram, false, false}); + if (failed(setProgramGridTransformContract(*sandbox, contract))) + return nullptr; + } + + return std::unique_ptr( + new DotRowPlan(candidate, epoch, std::move(sandbox))); + } + + LogicalResult apply(IRRewriter &rw) override { + // Commit the verified body and, for runtime grids, its hidden arguments. + ModuleOp module = candidate.function->getParentOfType(); + auto replacement = cast(&prepared->getBody()->front()); + if (candidate.runtimeGrid) { + if (failed(commitProgramGridFunctionFromSandbox(candidate.function, + replacement))) + return failure(); + module->setAttr( + kProgramGridTransformsAttr, + prepared->getOperation()->getAttr(kProgramGridTransformsAttr)); + return success(); + } + + candidate.function.getBody().takeBody(replacement.getBody()); + module->setAttr(kCoalesceFactorAttr, + rw.getI32IntegerAttr(candidate.rowsPerProgram)); + module->setAttr(kCoalesceAxisAttr, rw.getI32IntegerAttr(0)); + module->setAttr(kCoalesceGridCeilDivAttr, rw.getI32IntegerAttr(1)); + return success(); + } +}; + +} // namespace + +std::unique_ptr +cfg::createDotRowCoalescingPlan(triton::FuncOp function, unsigned epoch) { + // Dot row coalescing is supported only on Ascend 950. + auto module = function->getParentOfType(); + if (!module || !hacc::utils::isAscend950(module)) + return nullptr; + + auto candidate = matchDotRows(function); + if (!candidate) + return nullptr; + + return DotRowPlan::create(*candidate, epoch); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/IndependentAxisTensorizeRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/IndependentAxisTensorizeRule.cpp new file mode 100644 index 0000000000..56462c4322 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/IndependentAxisTensorizeRule.cpp @@ -0,0 +1,1236 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonMemoryAccess/MemoryAccessTags.h" +#include "TritonToGraph/EntryArgPointerAliasAnalysis.h" +#include "TritonToGraph/GraphOptimizationRule.h" +#include "TritonToGraph/IndependentRowReductionAnalysis.h" +#include "TritonToGraph/ProgramAxisDependenceAnalysis.h" +#include "TritonToGraph/ProgramGridTransform.h" +#include "TritonToGraph/ResourceCostModel.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/CallInterfaces.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/Passes.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +constexpr llvm::StringLiteral kIndependentAxisTensorizeMarkerAttr = + "hacc.independent_axis_tensorize"; +constexpr llvm::StringLiteral kPersistentTaskStripMiningMarkerAttr = + "hacc.persistent_task_strip_mining"; +constexpr llvm::StringLiteral kCoalesceFactorAttr = "hacc.coalesce_factor"; +constexpr llvm::StringLiteral kCoalesceAxisAttr = "hacc.coalesce_axis"; +constexpr llvm::StringLiteral kCoalesceGridCeilDivAttr = + "hacc.coalesce_grid_ceil_div"; + +constexpr std::array kMergeTensorizeFactors = {2, 4, 8}; +constexpr std::array kMergeLargeTensorizeFactors = {16, 32}; +constexpr std::array kNormTensorizeFactors = {2, 4, 8, 16}; +constexpr std::array kNormPersistentBlockTCandidates = { + 2, 4, 8, 16, 32, 64, 128}; + +constexpr uint64_t kMergeSplitMaxTensorizedPlanes = 16; + +constexpr int64_t kMergeSplitD192SourceTile = 256; +constexpr int64_t kMergeSplitD192TargetTile = 192; +constexpr unsigned kDynamicSingleMomentBlockT = 4; + +enum class TensorizeForm : uint8_t { + MergeSplit, + NormRope, +}; + +enum class IATCandidateScope : uint8_t { + Default, + MergeSplitLarge, +}; + +struct IATCandidate { + triton::FuncOp function; + Operation *anchor = nullptr; + TensorizeForm form = TensorizeForm::MergeSplit; + int32_t axis = 1; + int64_t splitExtent = 0; + int64_t dimExtent = 0; + unsigned factor = 1; + bool usesMergeSplitSubCorePolicy = false; + bool usesMergeSplitD192Tile = false; + bool requiresPersistentChaining = false; + ResourceSnapshot resources; + CandidateEvaluation evaluation; +}; + +struct JointProgramMappingCandidate { + IATCandidate iat; + unsigned blockT = 0; + std::array finalLogicalGrid = {1, 1, 1}; + std::array finalPhysicalGrid = {1, 1, 1}; + CandidateEvaluation evaluation; +}; + +struct TensorizeReductionShape { + int64_t splitExtent = 0; + int64_t dimExtent = 0; +}; + +struct TensorizeFormMatch { + TensorizeForm form; + TensorizeReductionShape reduction; +}; + +struct MappedValue { + Value value; + bool tensorized = false; + int64_t laneAxis = 0; +}; + +std::optional findOnlyProgramId(triton::FuncOp function, + int32_t axis) { + std::optional result; + unsigned count = 0; + function.walk([&](triton::GetProgramIdOp pid) { + if (pid.getAxisAsInt() != axis) + return; + ++count; + if (count == 1) + result = pid; + }); + return count == 1 ? result : std::nullopt; +} + +std::optional +classifyTensorizeForm(triton::FuncOp function) { + std::optional mergeReduction; + std::optional normReduction; + function.walk([&](triton::ReduceOp reduce) { + if (reduce.getSrcs().size() != 1 || reduce.getResults().size() != 1 || + reduce.getAxis() != 0) + return; + auto source = + dyn_cast(reduce.getSrcs().front().getType()); + if (!source || !source.hasStaticShape()) + return; + Type result = reduce.getResults().front().getType(); + auto rankedResult = dyn_cast(result); + if (source.getRank() == 2 && rankedResult && rankedResult.getRank() == 1 && + rankedResult.hasStaticShape() && + source.getShape()[1] == rankedResult.getShape()[0]) { + if (!mergeReduction) + mergeReduction = + TensorizeReductionShape{source.getShape()[0], source.getShape()[1]}; + } else if (source.getRank() == 1 && !isa(result)) { + if (!normReduction) + normReduction = TensorizeReductionShape{0, source.getShape()[0]}; + } + }); + if (mergeReduction) + return TensorizeFormMatch{TensorizeForm::MergeSplit, *mergeReduction}; + if (normReduction) + return TensorizeFormMatch{TensorizeForm::NormRope, *normReduction}; + return std::nullopt; +} + +bool hasSupportedControlFlow(triton::FuncOp function) { + Region &body = function.getBody(); + if (!body.hasOneBlock()) + return false; + for (Operation &operation : body.front()) { + if (isa(operation)) + return false; + StringRef name = operation.getName().getStringRef(); + if (name.contains("atomic") || name.contains("barrier") || + name == "cf.br" || name == "cf.cond_br" || name.starts_with("scf.")) + return false; + if (operation.getNumRegions() != 0 && + !isa(operation)) + return false; + } + return true; +} + +bool hasConflictingLaunchContract(ModuleOp module) { + return module->hasAttr(kIndependentAxisTensorizeMarkerAttr) || + module->hasAttr(kProgramGridTransformsAttr) || + module->hasAttr(kCoalesceFactorAttr) || + module->hasAttr(kCoalesceAxisAttr) || + module->hasAttr(kCoalesceGridCeilDivAttr); +} + +bool hasDisjointWriteReadRoots( + const ProgramAxisDependence &dependence, + const EntryArgPointerAliasAnalysis &entryPointerAliases) { + for (const StoreAddressDependence &store : dependence.stores) { + auto storeOp = dyn_cast_or_null(store.store); + if (!storeOp) + return false; + for (Operation *operation : dependence.dependenceClosure) { + auto load = dyn_cast(operation); + if (!load) + continue; + if (entryPointerAliases.classify(storeOp.getPtr(), load.getPtr()) != + EntryArgPointerRelation::DistinctEntryRoots) + return false; + } + } + return true; +} + +bool hasTensorPointerInProgramAxisClosure( + const ProgramAxisDependence &dependence) { + // A tensor pointer requires a scalar base pointer. IAT turns the dependent + // program id into a vector of lanes, and the current materializer cannot + // reconstruct a legal tensor pointer from that vector. Reject this case + // while discovering candidates instead of scheduling a plan that fails in + // apply(). + return llvm::any_of(dependence.dependenceClosure, [](Operation *operation) { + return isa(operation); + }); +} + +int64_t getPreferredLaneAxis(Type originalType, const IATCandidate &candidate) { + auto tensor = dyn_cast(originalType); + if (!tensor || candidate.form == TensorizeForm::NormRope) + return 0; + const int64_t rank = tensor.getRank(); + if (rank == 0) + return 0; + if (rank == 1) + return tensor.getShape().front() == candidate.splitExtent ? 1 : 0; + if (tensor.getShape().front() == candidate.splitExtent || + tensor.getShape().front() == 1) + return 1; + return 0; +} + +std::optional +getLiftedTensorType(Type originalType, const IATCandidate &candidate, + int64_t laneAxis) { + Type elementType = originalType; + SmallVector shape; + if (auto tensor = dyn_cast(originalType)) { + if (laneAxis < 0 || laneAxis > tensor.getRank()) + return std::nullopt; + shape.assign(tensor.getShape().begin(), tensor.getShape().end()); + elementType = tensor.getElementType(); + } else if (laneAxis != 0) { + return std::nullopt; + } + shape.insert(shape.begin() + laneAxis, candidate.factor); + return RankedTensorType::get(shape, elementType); +} + +std::optional +getSameShapeTensorType(Type originalType, ArrayRef shape) { + Type elementType = originalType; + if (auto tensor = dyn_cast(originalType)) + elementType = tensor.getElementType(); + return RankedTensorType::get(shape, elementType); +} + +bool hasSameShape(RankedTensorType lhs, RankedTensorType rhs) { + return lhs.getRank() == rhs.getRank() && + llvm::equal(lhs.getShape(), rhs.getShape()); +} + +Value makeZero(IRRewriter &rewriter, Location loc, Type type) { + auto tensor = dyn_cast(type); + Type elementType = tensor ? tensor.getElementType() : type; + if (auto integer = dyn_cast(elementType)) { + TypedAttr zero = IntegerAttr::get(integer, 0); + return tensor ? rewriter.create( + loc, type, DenseElementsAttr::get(tensor, zero)) + : rewriter.create(loc, type, zero); + } + if (auto floating = dyn_cast(elementType)) { + TypedAttr zero = FloatAttr::get(floating, 0.0); + return tensor ? rewriter.create( + loc, type, DenseElementsAttr::get(tensor, zero)) + : rewriter.create(loc, type, zero); + } + return Value(); +} + +bool rebuildTensorizedFunction(triton::FuncOp function, + const IATCandidate &candidate, + IRRewriter &rewriter) { + if (!hasSupportedControlFlow(function)) + return false; + if (failed(addProgramGridHiddenExtentArguments(function))) + return false; + Value originalExtent = function.getArgument(function.getNumArguments() - 1); + auto originalExtentType = dyn_cast(originalExtent.getType()); + if (!originalExtentType || originalExtentType.getWidth() != 32) + return false; + std::optional targetPid = + findOnlyProgramId(function, candidate.axis); + if (!targetPid) + return false; + + ProgramAxisDependenceAnalysis analysis(function); + const ProgramAxisDependence &dependence = analysis.get(candidate.axis); + if (!dependence.isProgramMappingTransformCandidate()) + return false; + DenseSet closure; + for (Operation *operation : dependence.dependenceClosure) + closure.insert(operation); + + Block &block = function.getBody().front(); + SmallVector originals; + originals.reserve(block.getOperations().size()); + for (Operation &operation : block) + originals.push_back(&operation); + + DenseMap values; + auto lookup = [&](Value value) -> MappedValue { + auto it = values.find(value); + return it == values.end() ? MappedValue{value, false, 0} : it->second; + }; + + auto broadcastTo = [&](Location loc, Value value, + RankedTensorType target) -> Value { + auto source = dyn_cast(value.getType()); + if (!source || source.getElementType() != target.getElementType() || + source.getRank() != target.getRank()) + return Value(); + for (auto [sourceDim, targetDim] : + llvm::zip(source.getShape(), target.getShape())) + if (sourceDim != targetDim && sourceDim != 1) + return Value(); + return hasSameShape(source, target) + ? value + : rewriter.create(loc, target, value); + }; + + auto alignTensorized = [&](Location loc, MappedValue mapped, + RankedTensorType target, + int64_t targetLaneAxis) -> Value { + auto source = dyn_cast(mapped.value.getType()); + if (!source || mapped.laneAxis < 0 || mapped.laneAxis >= source.getRank() || + targetLaneAxis < 0 || targetLaneAxis >= target.getRank() || + source.getElementType() != target.getElementType()) + return Value(); + const int64_t sourceLaneAxis = mapped.laneAxis; + const int64_t sourceTail = source.getRank() - sourceLaneAxis - 1; + const int64_t targetTail = target.getRank() - targetLaneAxis - 1; + if (sourceLaneAxis > targetLaneAxis || sourceTail > targetTail || + source.getShape()[sourceLaneAxis] != candidate.factor || + target.getShape()[targetLaneAxis] != candidate.factor) + return Value(); + + SmallVector sourceToTarget(source.getRank(), -1); + for (int64_t index = 0; index < sourceLaneAxis; ++index) + sourceToTarget[index] = index; + sourceToTarget[sourceLaneAxis] = targetLaneAxis; + for (int64_t index = 0; index < sourceTail; ++index) + sourceToTarget[sourceLaneAxis + 1 + index] = + target.getRank() - sourceTail + index; + + SmallVector targetMapped(target.getRank(), false); + for (int64_t sourceIndex = 0; sourceIndex < source.getRank(); + ++sourceIndex) { + const int64_t targetIndex = sourceToTarget[sourceIndex]; + if (targetIndex < 0 || targetIndex >= target.getRank() || + targetMapped[targetIndex]) + return Value(); + targetMapped[targetIndex] = true; + const int64_t sourceDim = source.getShape()[sourceIndex]; + const int64_t targetDim = target.getShape()[targetIndex]; + if (sourceDim != targetDim && sourceDim != 1) + return Value(); + } + + Value current = mapped.value; + int64_t currentAxis = 0; + for (int64_t targetIndex = 0; targetIndex < target.getRank(); + ++targetIndex) { + if (targetMapped[targetIndex]) { + ++currentAxis; + continue; + } + current = + rewriter.create(loc, current, currentAxis); + ++currentAxis; + } + return broadcastTo(loc, current, target); + }; + + auto promoteUniform = [&](Location loc, Value value, Type oldType, + RankedTensorType target, + int64_t targetLaneAxis) -> Value { + if (targetLaneAxis < 0 || targetLaneAxis >= target.getRank()) + return Value(); + if (auto oldTensor = dyn_cast(oldType)) { + if (oldTensor.getRank() != target.getRank() - 1 || + oldTensor.getElementType() != target.getElementType()) + return Value(); + SmallVector shape(target.getShape().begin(), + target.getShape().end()); + shape.erase(shape.begin() + targetLaneAxis); + auto unlaned = RankedTensorType::get(shape, target.getElementType()); + Value base = broadcastTo(loc, value, unlaned); + if (!base) + return Value(); + Value expanded = + rewriter.create(loc, base, targetLaneAxis); + return broadcastTo(loc, expanded, target); + } + return rewriter.create(loc, target, value); + }; + + auto liftOperandTo = [&](Value value, RankedTensorType target, + int64_t targetLaneAxis) -> Value { + MappedValue mapped = lookup(value); + return mapped.tensorized + ? alignTensorized(value.getLoc(), mapped, target, targetLaneAxis) + : promoteUniform(value.getLoc(), mapped.value, value.getType(), + target, targetLaneAxis); + }; + + auto copyMissingAttrs = [](Operation *from, Operation *to) { + for (NamedAttribute attribute : from->getAttrs()) + if (!to->hasAttr(attribute.getName())) + to->setAttr(attribute.getName(), attribute.getValue()); + }; + + Value laneMask; + auto maskForPointerType = [&](Location loc, RankedTensorType pointerType, + int64_t laneAxis) -> Value { + if (!laneMask) + return Value(); + auto maskType = + RankedTensorType::get(pointerType.getShape(), rewriter.getI1Type()); + return alignTensorized(loc, MappedValue{laneMask, true, 0}, maskType, + laneAxis); + }; + + auto createUnchanged = [&](Operation *operation) -> bool { + IRMapping mapping; + for (Value operand : operation->getOperands()) { + Value mapped = lookup(operand).value; + if (!mapped) + return false; + mapping.map(operand, mapped); + } + Operation *replacement = rewriter.clone(*operation, mapping); + if (replacement->getNumResults() != operation->getNumResults()) + return false; + for (auto [oldResult, newResult] : + llvm::zip(operation->getResults(), replacement->getResults())) + values[oldResult] = {newResult, false, 0}; + return true; + }; + + auto originalTypesHaveSameShape = [](Type lhs, Type rhs) { + auto left = dyn_cast(lhs); + auto right = dyn_cast(rhs); + if (!left || !right) + return !left && !right; + return hasSameShape(left, right); + }; + + auto createElementwise = [&](Operation *operation) -> bool { + if (operation->getNumResults() == 0) + return false; + const bool isReshape = isa(operation); + if (!operation->hasTrait() && !isReshape) + return false; + if (isReshape && + (!originalTypesHaveSameShape(operation->getOperand(0).getType(), + operation->getResult(0).getType()))) + return false; + + int64_t laneAxis = + getPreferredLaneAxis(operation->getResult(0).getType(), candidate); + for (Value operand : operation->getOperands()) { + MappedValue mapped = lookup(operand); + if (!mapped.tensorized || + !originalTypesHaveSameShape(operand.getType(), + operation->getResult(0).getType())) + continue; + laneAxis = mapped.laneAxis; + break; + } + std::optional firstResultType = getLiftedTensorType( + operation->getResult(0).getType(), candidate, laneAxis); + if (!firstResultType) + return false; + + SmallVector resultTypes; + resultTypes.reserve(operation->getNumResults()); + for (Type resultType : operation->getResultTypes()) { + std::optional lifted = + getLiftedTensorType(resultType, candidate, laneAxis); + if (!lifted || !hasSameShape(*lifted, *firstResultType)) + return false; + resultTypes.push_back(*lifted); + } + + SmallVector operands; + operands.reserve(operation->getNumOperands()); + for (Value operand : operation->getOperands()) { + std::optional operandType = getSameShapeTensorType( + operand.getType(), firstResultType->getShape()); + if (!operandType) + return false; + Value lifted = liftOperandTo(operand, *operandType, laneAxis); + if (!lifted) + return false; + operands.push_back(lifted); + } + + Operation *replacement = rewriter.create( + operation->getLoc(), operation->getName().getIdentifier(), operands, + resultTypes, operation->getAttrs()); + if (replacement->getNumResults() != operation->getNumResults()) + return false; + for (auto [oldResult, newResult] : + llvm::zip(operation->getResults(), replacement->getResults())) + values[oldResult] = {newResult, true, laneAxis}; + return true; + }; + + for (Operation *operation : originals) { + rewriter.setInsertionPoint(operation); + + if (operation == targetPid->getOperation()) { + Operation *scalarPid = rewriter.clone(*operation); + if (scalarPid->getNumResults() != 1) + return false; + Value scalar = scalarPid->getResult(0); + auto integer = dyn_cast(scalar.getType()); + if (!integer || integer.getWidth() != 32) + return false; + Value factor = rewriter.create( + operation->getLoc(), candidate.factor, integer.getWidth()); + Value base = + rewriter.create(operation->getLoc(), scalar, factor); + auto laneType = + RankedTensorType::get({candidate.factor}, scalar.getType()); + Value lane = rewriter.create( + operation->getLoc(), laneType, 0, candidate.factor); + Value baseSplat = + rewriter.create(operation->getLoc(), laneType, base); + Value logicalIds = + rewriter.create(operation->getLoc(), baseSplat, lane); + Type i64 = rewriter.getI64Type(); + auto laneI64Type = RankedTensorType::get({candidate.factor}, i64); + Value logicalIdsI64 = rewriter.create( + operation->getLoc(), laneI64Type, logicalIds); + Value extentI64 = rewriter.create(operation->getLoc(), + i64, originalExtent); + Value extentSplat = rewriter.create( + operation->getLoc(), laneI64Type, extentI64); + auto runtimeExtentMask = rewriter.create( + operation->getLoc(), arith::CmpIPredicate::ult, logicalIdsI64, + extentSplat); + runtimeExtentMask->setAttr( + mlir::triton::memory_access::IATRuntimeExtentUnsignedMaskTAG, + rewriter.getUnitAttr()); + laneMask = runtimeExtentMask; + values[operation->getResult(0)] = {logicalIds, true, 0}; + continue; + } + + bool tensorized = closure.contains(operation); + for (Value operand : operation->getOperands()) + tensorized |= lookup(operand).tensorized; + + if (auto splat = dyn_cast(operation)) { + if (!tensorized) { + if (!createUnchanged(operation)) + return false; + continue; + } + const int64_t laneAxis = getPreferredLaneAxis(splat.getType(), candidate); + std::optional resultType = + getLiftedTensorType(splat.getType(), candidate, laneAxis); + if (!resultType) + return false; + Value result = liftOperandTo(splat.getSrc(), *resultType, laneAxis); + if (!result) + return false; + values[splat.getResult()] = {result, true, laneAxis}; + continue; + } + + if (auto expand = dyn_cast(operation)) { + if (!tensorized) { + if (!createUnchanged(operation)) + return false; + continue; + } + MappedValue sourceMapped = lookup(expand.getSrc()); + const int64_t oldLaneAxis = + sourceMapped.tensorized + ? sourceMapped.laneAxis + : getPreferredLaneAxis(expand.getSrc().getType(), candidate); + std::optional sourceType = getLiftedTensorType( + expand.getSrc().getType(), candidate, oldLaneAxis); + if (!sourceType) + return false; + Value source = liftOperandTo(expand.getSrc(), *sourceType, oldLaneAxis); + if (!source) + return false; + const int64_t newAxis = + expand.getAxis() + (expand.getAxis() >= oldLaneAxis ? 1 : 0); + const int64_t resultLaneAxis = + oldLaneAxis + (expand.getAxis() < oldLaneAxis ? 1 : 0); + Value result = rewriter.create(operation->getLoc(), + source, newAxis); + std::optional expected = + getLiftedTensorType(expand.getType(), candidate, resultLaneAxis); + if (!expected || result.getType() != *expected) + return false; + values[expand.getResult()] = {result, true, resultLaneAxis}; + continue; + } + + if (auto broadcast = dyn_cast(operation)) { + if (!tensorized) { + if (!createUnchanged(operation)) + return false; + continue; + } + MappedValue sourceMapped = lookup(broadcast.getSrc()); + const int64_t laneAxis = + sourceMapped.tensorized + ? sourceMapped.laneAxis + : getPreferredLaneAxis(broadcast.getSrc().getType(), candidate); + std::optional resultType = + getLiftedTensorType(broadcast.getType(), candidate, laneAxis); + if (!resultType) + return false; + Value result = liftOperandTo(broadcast.getSrc(), *resultType, laneAxis); + if (!result) + return false; + values[broadcast.getResult()] = {result, true, laneAxis}; + continue; + } + + if (auto reduce = dyn_cast(operation)) { + if (!tensorized) { + if (!createUnchanged(operation)) + return false; + continue; + } + if (reduce.getSrcs().empty()) + return false; + auto oldInputType = + dyn_cast(reduce.getSrcs().front().getType()); + if (!oldInputType) + return false; + MappedValue firstSource = lookup(reduce.getSrcs().front()); + const int64_t laneAxis = + firstSource.tensorized + ? firstSource.laneAxis + : getPreferredLaneAxis(oldInputType, candidate); + std::optional firstSourceType = + getLiftedTensorType(oldInputType, candidate, laneAxis); + if (!firstSourceType) + return false; + SmallVector sources; + for (Value source : reduce.getSrcs()) { + std::optional sourceType = + getLiftedTensorType(source.getType(), candidate, laneAxis); + if (!sourceType || !hasSameShape(*sourceType, *firstSourceType)) + return false; + Value lifted = liftOperandTo(source, *sourceType, laneAxis); + if (!lifted) + return false; + sources.push_back(lifted); + } + const int64_t axis = + reduce.getAxis() + (reduce.getAxis() >= laneAxis ? 1 : 0); + const int64_t resultLaneAxis = laneAxis - (axis < laneAxis ? 1 : 0); + auto replacement = + rewriter.create(operation->getLoc(), sources, axis); + rewriter.cloneRegionBefore(reduce.getCombineOp(), + replacement.getCombineOp(), + replacement.getCombineOp().end()); + copyMissingAttrs(operation, replacement.getOperation()); + for (auto [oldResult, newResult] : + llvm::zip(reduce.getResults(), replacement.getResults())) { + std::optional expected = + getLiftedTensorType(oldResult.getType(), candidate, resultLaneAxis); + if (!expected || newResult.getType() != *expected) + return false; + values[oldResult] = {newResult, true, resultLaneAxis}; + } + continue; + } + + if (auto scan = dyn_cast(operation)) { + if (!tensorized) { + if (!createUnchanged(operation)) + return false; + continue; + } + if (scan.getSrcs().empty()) + return false; + auto oldInputType = + dyn_cast(scan.getSrcs().front().getType()); + if (!oldInputType) + return false; + MappedValue firstSource = lookup(scan.getSrcs().front()); + const int64_t laneAxis = + firstSource.tensorized + ? firstSource.laneAxis + : getPreferredLaneAxis(oldInputType, candidate); + std::optional firstSourceType = + getLiftedTensorType(oldInputType, candidate, laneAxis); + if (!firstSourceType) + return false; + SmallVector sources; + for (Value source : scan.getSrcs()) { + std::optional sourceType = + getLiftedTensorType(source.getType(), candidate, laneAxis); + if (!sourceType || !hasSameShape(*sourceType, *firstSourceType)) + return false; + Value lifted = liftOperandTo(source, *sourceType, laneAxis); + if (!lifted) + return false; + sources.push_back(lifted); + } + const int64_t axis = + scan.getAxis() + (scan.getAxis() >= laneAxis ? 1 : 0); + auto replacement = rewriter.create( + operation->getLoc(), sources, static_cast(axis), + scan.getReverse()); + rewriter.cloneRegionBefore(scan.getCombineOp(), + replacement.getCombineOp(), + replacement.getCombineOp().end()); + copyMissingAttrs(operation, replacement.getOperation()); + for (auto [oldResult, newResult] : + llvm::zip(scan.getResults(), replacement.getResults())) { + std::optional expected = + getLiftedTensorType(oldResult.getType(), candidate, laneAxis); + if (!expected || newResult.getType() != *expected) + return false; + values[oldResult] = {newResult, true, laneAxis}; + } + continue; + } + + if (auto load = dyn_cast(operation)) { + if (!tensorized) { + if (!createUnchanged(operation)) + return false; + continue; + } + MappedValue pointerMapped = lookup(load.getPtr()); + const int64_t laneAxis = + pointerMapped.tensorized + ? pointerMapped.laneAxis + : getPreferredLaneAxis(load.getPtr().getType(), candidate); + std::optional pointerType = + getLiftedTensorType(load.getPtr().getType(), candidate, laneAxis); + if (!pointerType) + return false; + Value pointer = liftOperandTo(load.getPtr(), *pointerType, laneAxis); + std::optional maskType = + getSameShapeTensorType(rewriter.getI1Type(), pointerType->getShape()); + Value mask = load.getMask() + ? liftOperandTo(load.getMask(), *maskType, laneAxis) + : Value(); + Value laneMaskForLoad = + maskForPointerType(operation->getLoc(), *pointerType, laneAxis); + if (laneMaskForLoad) + mask = mask ? rewriter.create(operation->getLoc(), mask, + laneMaskForLoad) + : laneMaskForLoad; + std::optional otherType = getSameShapeTensorType( + load.getResult().getType(), pointerType->getShape()); + Value other = load.getOther() + ? liftOperandTo(load.getOther(), *otherType, laneAxis) + : makeZero(rewriter, operation->getLoc(), *otherType); + if (!pointer || !maskType || !otherType || !other) + return false; + auto replacement = rewriter.create( + operation->getLoc(), pointer, mask, other, load.getBoundaryCheck(), + load.getPadding(), load.getCache(), load.getEvict(), + load.getIsVolatile()); + copyMissingAttrs(operation, replacement.getOperation()); + if (replacement.getResult().getType() != *otherType) + return false; + values[load.getResult()] = {replacement.getResult(), true, laneAxis}; + continue; + } + + if (auto store = dyn_cast(operation)) { + if (!tensorized) { + if (!createUnchanged(operation)) + return false; + continue; + } + MappedValue pointerMapped = lookup(store.getPtr()); + const int64_t laneAxis = + pointerMapped.tensorized + ? pointerMapped.laneAxis + : getPreferredLaneAxis(store.getPtr().getType(), candidate); + std::optional pointerType = + getLiftedTensorType(store.getPtr().getType(), candidate, laneAxis); + if (!pointerType) + return false; + Value pointer = liftOperandTo(store.getPtr(), *pointerType, laneAxis); + std::optional valueType = getSameShapeTensorType( + store.getValue().getType(), pointerType->getShape()); + if (!valueType) + return false; + Value value = liftOperandTo(store.getValue(), *valueType, laneAxis); + auto maskType = + RankedTensorType::get(pointerType->getShape(), rewriter.getI1Type()); + Value mask = store.getMask() + ? liftOperandTo(store.getMask(), maskType, laneAxis) + : Value(); + Value laneMaskForStore = + maskForPointerType(operation->getLoc(), *pointerType, laneAxis); + if (laneMaskForStore) + mask = mask ? rewriter.create(operation->getLoc(), mask, + laneMaskForStore) + : laneMaskForStore; + if (!pointer || !value || !laneMaskForStore) + return false; + auto replacement = rewriter.create( + operation->getLoc(), pointer, value, mask, store.getBoundaryCheck(), + store.getCache(), store.getEvict()); + copyMissingAttrs(operation, replacement.getOperation()); + continue; + } + + if (isa(operation)) { + for (Value operand : operation->getOperands()) + if (lookup(operand).tensorized) + return false; + if (!createUnchanged(operation)) + return false; + continue; + } + + if (!tensorized) { + if (!createUnchanged(operation)) + return false; + continue; + } + if (!createElementwise(operation)) + return false; + } + + if (!laneMask) + return false; + for (Operation *operation : llvm::reverse(originals)) { + operation->dropAllUses(); + rewriter.eraseOp(operation); + } + return true; +} + +LogicalResult runProgramMappingStructuralCleanup(ModuleOp module) { + PassManager cleanup(module.getContext(), module.getOperationName()); + cleanup.addPass(createCanonicalizerPass()); + cleanup.addPass(createCSEPass()); + cleanup.addPass(createLoopInvariantCodeMotionPass()); + cleanup.addPass(createCanonicalizerPass()); + cleanup.addPass(createCSEPass()); + return cleanup.run(module); +} + +LogicalResult materializeIATCandidateToSandbox(ModuleOp module, + triton::FuncOp function, + const IATCandidate &candidate) { + if (!module || module->hasAttr(kIndependentAxisTensorizeMarkerAttr) || + module->hasAttr(kProgramGridTransformsAttr)) + return failure(); + IRRewriter rewriter(module.getContext()); + if (!rebuildTensorizedFunction(function, candidate, rewriter)) + return failure(); + ProgramGridTransformContract contract; + contract.transforms.push_back(ProgramGridTransform{ + 0, candidate.axis, static_cast(candidate.factor), false, false}); + if (failed(setProgramGridTransformContract(module, contract))) + return failure(); + module->setAttr(kIndependentAxisTensorizeMarkerAttr, + UnitAttr::get(module.getContext())); + return mlir::verify(module.getOperation()); +} + +ModuleOp createProgramMappingSandbox(ModuleOp module, triton::FuncOp function) { + (void)module; + ModuleOp sandbox = ModuleOp::create(function.getLoc()); + sandbox.getBody()->push_back(function->clone()); + return sandbox; +} + +std::optional +analyzeDynamicSingleMomentCandidate(GraphOptimizationContext &context) { + triton::FuncOp function = context.getFunction(); + ModuleOp module = function->getParentOfType(); + if (!module || hasConflictingLaunchContract(module) || + !hasSupportedControlFlow(function) || + classifyIndependentRowReduction(function) != + IndependentRowReductionKind::SingleMoment || + !findOnlyProgramId(function, 0)) + return std::nullopt; + + std::optional formMatch = classifyTensorizeForm(function); + if (!formMatch || formMatch->form != TensorizeForm::NormRope) + return std::nullopt; + std::optional headPid = + findOnlyProgramId(function, 1); + if (!headPid) + return std::nullopt; + + const ProgramAxisDependence &dependence = + context.getProgramAxisDependenceAnalysis().get(1); + if (!dependence.isProgramMappingTransformCandidate() || + hasTensorPointerInProgramAxisClosure(dependence) || + !hasDisjointWriteReadRoots(dependence, + context.getEntryArgPointerAliasAnalysis())) + return std::nullopt; + const LiveByteEstimate &liveBytes = + context.getResourceCostAnalysis().getLiveByteEstimate(); + if (!liveBytes.known) + return std::nullopt; + uint64_t estimatedPeak = 0; + if (__builtin_mul_overflow(liveBytes.peakLiveBytes, uint64_t{16}, + &estimatedPeak)) + return std::nullopt; + + IATCandidate candidate; + candidate.function = function; + candidate.anchor = headPid->getOperation(); + candidate.form = TensorizeForm::NormRope; + candidate.axis = 1; + candidate.dimExtent = formMatch->reduction.dimExtent; + candidate.factor = 16; + candidate.requiresPersistentChaining = true; + candidate.resources = context.getResourceCostAnalysis().getResourceSnapshot(); + + CandidateCost cost; + cost.plan.tensorizeFactor = candidate.factor; + cost.plan.blockT = kDynamicSingleMomentBlockT; + cost.plan.staticAxisFusionFactor = 1; + cost.plan.stableId = "iat.ptsm.single-moment.dynamic.f16.b4"; + cost.storeCountBefore = dependence.stores.size(); + cost.storeCountAfter = dependence.stores.size(); + cost.addressCalculationsBefore = dependence.dependenceClosure.size(); + cost.addressCalculationsAfter = dependence.dependenceClosure.size(); + cost.hasPeakLiveBytes = true; + cost.baselinePeakLiveBytes = liveBytes.peakLiveBytes; + cost.estimatedPeakLiveBytes = estimatedPeak; + candidate.evaluation = context.getResourceCostAnalysis().evaluate(cost); + return candidate.evaluation.accepted ? std::optional(candidate) + : std::nullopt; +} + +bool sameDynamicSingleMomentCandidate(const IATCandidate &lhs, + const IATCandidate &rhs); + +class DynamicSingleMomentProgramMappingPlan final : public RewritePlan { +public: + DynamicSingleMomentProgramMappingPlan(IATCandidate candidate, unsigned epoch) + : candidate(std::move(candidate)), epoch(epoch) {} + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::IndependentAxisTensorize; + } + unsigned getBenefit() const override { return 1; } + Operation *getAnchor() const override { return candidate.anchor; } + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (context.getFunction() != candidate.function) + return failure(); + std::optional current = + analyzeDynamicSingleMomentCandidate(context); + return current && sameDynamicSingleMomentCandidate(candidate, *current) + ? success() + : failure(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + (void)rewriter; + ModuleOp module = candidate.function->getParentOfType(); + if (!module || hasConflictingLaunchContract(module)) + return failure(); + + ModuleOp sandbox = createProgramMappingSandbox(module, candidate.function); + auto clonedFunction = dyn_cast(&sandbox.getBody()->front()); + CandidateEvaluation persistentEvaluation; + if (!clonedFunction || + failed(materializeIATCandidateToSandbox(sandbox, clonedFunction, + candidate)) || + failed(materializePersistentTaskStripMiningCandidate( + sandbox, clonedFunction, candidate.resources, + kDynamicSingleMomentBlockT, &persistentEvaluation, true)) || + failed(runProgramMappingStructuralCleanup(sandbox)) || + failed(mlir::verify(sandbox.getOperation()))) + return failure(); + + const LiveByteEstimate baseline = + estimatePeakLiveBytes(candidate.function.getOperation()); + const LiveByteEstimate final = + estimatePeakLiveBytes(clonedFunction.getOperation()); + if (!baseline.known || !final.known) + return failure(); + CandidateCost finalCost; + finalCost.plan.tensorizeFactor = candidate.factor; + finalCost.plan.blockT = kDynamicSingleMomentBlockT; + finalCost.plan.staticAxisFusionFactor = 1; + finalCost.plan.stableId = "iat.ptsm.single-moment.dynamic.f16.b4"; + finalCost.hasPeakLiveBytes = true; + finalCost.baselinePeakLiveBytes = baseline.peakLiveBytes; + finalCost.estimatedPeakLiveBytes = final.peakLiveBytes; + if (!evaluateCandidateCost(candidate.resources, finalCost).accepted) + return failure(); + + Attribute transforms = sandbox->getAttr(kProgramGridTransformsAttr); + if (!transforms) + return failure(); + FailureOr parsed = + parseProgramGridTransformContract(transforms); + if (failed(parsed) || parsed->transforms.size() != 2 || + !hasProgramGridHiddenExtentArguments(clonedFunction)) + return failure(); + + if (failed(commitProgramGridFunctionFromSandbox(candidate.function, + clonedFunction))) + return failure(); + module->setAttr(kProgramGridTransformsAttr, transforms); + module->setAttr(kIndependentAxisTensorizeMarkerAttr, + UnitAttr::get(module.getContext())); + module->setAttr(kPersistentTaskStripMiningMarkerAttr, + UnitAttr::get(module.getContext())); + return success(); + } + +private: + IATCandidate candidate; + unsigned epoch; +}; + +std::optional +analyzeDynamicMergeSplitCandidate(GraphOptimizationContext &context) { + triton::FuncOp function = context.getFunction(); + ModuleOp module = function->getParentOfType(); + if (!module || hasConflictingLaunchContract(module) || + !hasSupportedControlFlow(function)) + return std::nullopt; + + std::optional formMatch = classifyTensorizeForm(function); + if (!formMatch || formMatch->form != TensorizeForm::MergeSplit || + !findOnlyProgramId(function, 0)) + return std::nullopt; + std::optional headPid = + findOnlyProgramId(function, 1); + if (!headPid) + return std::nullopt; + + const ProgramAxisDependence &dependence = + context.getProgramAxisDependenceAnalysis().get(1); + if (!dependence.isProgramMappingTransformCandidate() || + hasTensorPointerInProgramAxisClosure(dependence) || + !hasDisjointWriteReadRoots(dependence, + context.getEntryArgPointerAliasAnalysis())) + return std::nullopt; + const LiveByteEstimate &liveBytes = + context.getResourceCostAnalysis().getLiveByteEstimate(); + if (!liveBytes.known) + return std::nullopt; + uint64_t estimatedPeak = 0; + if (__builtin_mul_overflow(liveBytes.peakLiveBytes, uint64_t{16}, + &estimatedPeak)) + return std::nullopt; + + IATCandidate candidate; + candidate.function = function; + candidate.anchor = headPid->getOperation(); + candidate.form = TensorizeForm::MergeSplit; + candidate.axis = 1; + candidate.splitExtent = formMatch->reduction.splitExtent; + candidate.dimExtent = formMatch->reduction.dimExtent; + candidate.factor = 16; + candidate.resources = context.getResourceCostAnalysis().getResourceSnapshot(); + + CandidateCost cost; + cost.plan.tensorizeFactor = candidate.factor; + cost.plan.blockT = 1; + cost.plan.staticAxisFusionFactor = 1; + cost.plan.stableId = "iat.merge.dynamic.f16"; + cost.storeCountBefore = dependence.stores.size(); + cost.storeCountAfter = dependence.stores.size(); + cost.addressCalculationsBefore = dependence.dependenceClosure.size(); + cost.addressCalculationsAfter = dependence.dependenceClosure.size(); + cost.hasPeakLiveBytes = true; + cost.baselinePeakLiveBytes = liveBytes.peakLiveBytes; + cost.estimatedPeakLiveBytes = estimatedPeak; + candidate.evaluation = context.getResourceCostAnalysis().evaluate(cost); + return candidate.evaluation.accepted ? std::optional(candidate) + : std::nullopt; +} + +bool sameDynamicMergeSplitCandidate(const IATCandidate &lhs, + const IATCandidate &rhs) { + return lhs.function == rhs.function && lhs.anchor == rhs.anchor && + lhs.form == rhs.form && lhs.axis == rhs.axis && + lhs.splitExtent == rhs.splitExtent && lhs.dimExtent == rhs.dimExtent && + lhs.factor == rhs.factor; +} + +bool sameDynamicSingleMomentCandidate(const IATCandidate &lhs, + const IATCandidate &rhs) { + return lhs.function == rhs.function && lhs.anchor == rhs.anchor && + lhs.form == rhs.form && lhs.axis == rhs.axis && + lhs.dimExtent == rhs.dimExtent && lhs.factor == rhs.factor && + lhs.requiresPersistentChaining == rhs.requiresPersistentChaining; +} + +class DynamicMergeSplitIATPlan final : public RewritePlan { +public: + DynamicMergeSplitIATPlan(IATCandidate candidate, unsigned epoch) + : candidate(std::move(candidate)), epoch(epoch) {} + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::IndependentAxisTensorize; + } + unsigned getBenefit() const override { return 1; } + Operation *getAnchor() const override { return candidate.anchor; } + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (context.getFunction() != candidate.function) + return failure(); + std::optional current = + analyzeDynamicMergeSplitCandidate(context); + return current && sameDynamicMergeSplitCandidate(candidate, *current) + ? success() + : failure(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + (void)rewriter; + ModuleOp module = candidate.function->getParentOfType(); + if (!module || hasConflictingLaunchContract(module)) + return failure(); + ModuleOp sandbox = createProgramMappingSandbox(module, candidate.function); + auto clonedFunction = dyn_cast(&sandbox.getBody()->front()); + if (!clonedFunction || + failed(materializeIATCandidateToSandbox(sandbox, clonedFunction, + candidate)) || + failed(runProgramMappingStructuralCleanup(sandbox)) || + failed(mlir::verify(sandbox.getOperation()))) + return failure(); + Attribute transforms = sandbox->getAttr(kProgramGridTransformsAttr); + if (!transforms || failed(parseProgramGridTransformContract(transforms)) || + !hasProgramGridHiddenExtentArguments(clonedFunction)) + return failure(); + + if (failed(commitProgramGridFunctionFromSandbox(candidate.function, + clonedFunction))) + return failure(); + module->setAttr(kProgramGridTransformsAttr, transforms); + module->setAttr(kIndependentAxisTensorizeMarkerAttr, + UnitAttr::get(module.getContext())); + return success(); + } + +private: + IATCandidate candidate; + unsigned epoch; +}; + +class DynamicIndependentAxisTensorizeRule final : public GraphOptimizationRule { +public: + DynamicIndependentAxisTensorizeRule(bool enabledForCompileMode, + bool iatAndPtsmEnabled) + : enabledForCompileMode(enabledForCompileMode), + iatAndPtsmEnabled(iatAndPtsmEnabled) {} + + GraphOptimizationRuleId getId() const override { + return GraphOptimizationRuleId::IndependentAxisTensorize; + } + AnalysisRequirement getAnalysisRequirements() const override { + return AnalysisRequirement::EntryArgPointerAlias | + AnalysisRequirement::ProgramAxisDependence | + AnalysisRequirement::ResourceCost; + } + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + if (!enabledForCompileMode) + return success(); + std::optional candidate = + analyzeDynamicMergeSplitCandidate(context); + if (candidate) { + emitCandidateRemark(candidate->anchor, candidate->evaluation); + plans.push_back(std::make_unique( + std::move(*candidate), context.getEpoch())); + return success(); + } + if (!iatAndPtsmEnabled) + return success(); + candidate = analyzeDynamicSingleMomentCandidate(context); + if (candidate) { + emitCandidateRemark(candidate->anchor, candidate->evaluation); + plans.push_back(std::make_unique( + std::move(*candidate), context.getEpoch())); + } + return success(); + } + +private: + bool enabledForCompileMode; + bool iatAndPtsmEnabled; +}; + +} // namespace + +std::unique_ptr cfg::createIndependentAxisTensorizeRule( + const IndependentAxisTensorizeRuleOptions &options) { + return std::make_unique( + options.enabledForCompileMode, options.iatAndPtsmEnabled); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/IntermediatePrecisionBoundaryElisionRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/IntermediatePrecisionBoundaryElisionRule.cpp new file mode 100644 index 0000000000..b35eaed8ea --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/IntermediatePrecisionBoundaryElisionRule.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include + +using namespace mlir; +using namespace triton; +using namespace cfg; + +std::unique_ptr +cfg::createIntermediatePrecisionBoundaryElisionRule( + const IntermediatePrecisionBoundaryElisionRuleOptions &options) { + static_cast(options); + return createReservedGraphOptimizationRule( + {GraphOptimizationRuleId::IntermediatePrecisionBoundaryElision, + "intermediate-precision-boundary-elision"}); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/LoadStoreTransposeRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/LoadStoreTransposeRule.cpp new file mode 100644 index 0000000000..bbc228939a --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/LoadStoreTransposeRule.cpp @@ -0,0 +1,2218 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationRule.h" +#include "TritonToGraph/PermutationAnalysis.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +// This rule deliberately does not reuse StaticAccessAnalysis. That analysis +// is the strict, static, row-major proof used by StoreCoalescing. Here we +// need a separate fail-closed affine-layout proof which accepts symbolic +// physical strides (for example N and 256 * N), masks, and scf loop-carried +// pointers. Keeping it local also prevents a load/store layout extension from +// changing StoreCoalescing admission semantics. +struct ScaleExpression { + int64_t constantFactor = 1; + SmallVector symbols; +}; + +struct AffineLaneAxis { + unsigned outputAxis = 0; + // The rank-R value carrying this lane before its final broadcast. Keeping + // the provenance lets the dynamic-mask proof bind a symbolic bound to the + // exact inner lane rather than merely observe that the mask mentions N. + Value lane; + ScaleExpression scale; +}; + +struct AffineLayoutAccess { + Value pointer; + Value base; + Value offset; + RankedTensorType type; + SmallVector axes; + SmallVector permutation; +}; + +struct ReductionPort { + triton::ReduceOp reduce; + unsigned forResultIndex = 0; +}; + +// A tt.assert is a side use rather than a result-producing node, so it is not +// discovered by the ordinary backwards Value clone. Keep it as an explicit +// port of the layout closure. The assert itself is deliberately retained: we +// only replace its condition after the complete replacement closure verifies. +struct ExternalAssertPort { + Operation *assertOperation = nullptr; + Value oldCondition; +}; + +struct LayoutPermutationCandidate { + scf::ForOp loop; + // A local basic-block candidate is used when the layout closure does not + // cross a structured-control-flow port (for example a load/store pair in an + // scf.if branch). `loop` and `block` are mutually exclusive. + Block *block = nullptr; + Operation *anchor = nullptr; + SmallVector permutation; + SmallVector externalTensorValues; + SmallVector reductionPorts; + SmallVector transformedResultIndices; + SmallVector blockOperations; + SmallVector externalAssertPorts; + // Pure, full-rank address/guard producers whose use closure has been + // proven closed. They are explicitly reclaimed after the old endpoint is + // removed; graph-optimize is not followed by a mandatory DCE pass. + SmallVector retireOperations; + unsigned endpointCount = 0; +}; + +bool isUnencodedStaticTensor(Type type) { + auto tensor = dyn_cast(type); + return tensor && tensor.hasStaticShape() && !tensor.getEncoding(); +} + +bool hasLayoutRank(Type type, unsigned rank) { + auto tensor = dyn_cast(type); + return tensor && tensor.getRank() == rank; +} + +bool isLayoutTensor(Type type, unsigned rank) { + return hasLayoutRank(type, rank) && isUnencodedStaticTensor(type); +} + +bool getStaticInteger(Value value, int64_t &result) { + auto constant = value.getDefiningOp(); + if (!constant) + return false; + auto integer = dyn_cast(constant.getValue()); + if (!integer) + return false; + result = integer.getValue().getSExtValue(); + return true; +} + +bool multiplyConstantFactor(ScaleExpression &scale, int64_t factor) { + if (factor <= 0) + return false; + if (scale.constantFactor > std::numeric_limits::max() / factor) + return false; + scale.constantFactor *= factor; + return true; +} + +bool collectScaleExpression(Value value, ScaleExpression &scale, + DenseSet &visiting) { + if (!value || !visiting.insert(value).second) + return false; + + int64_t constant = 0; + if (getStaticInteger(value, constant)) + return multiplyConstantFactor(scale, constant); + + if (auto multiply = value.getDefiningOp()) { + if (multiply.getOverflowFlags() != arith::IntegerOverflowFlags::none) + return false; + return collectScaleExpression(multiply.getLhs(), scale, visiting) && + collectScaleExpression(multiply.getRhs(), scale, visiting); + } + + if (!isa(value.getType())) + return false; + scale.symbols.push_back(value); + return true; +} + +bool normalizeScaleExpression(ScaleExpression &scale) { + if (scale.constantFactor <= 0) + return false; + llvm::sort(scale.symbols, [](Value lhs, Value rhs) { + return lhs.getAsOpaquePointer() < rhs.getAsOpaquePointer(); + }); + return true; +} + +bool collectUniformTensorScale(Value value, ScaleExpression &scale) { + if (auto splat = value.getDefiningOp()) { + DenseSet visiting; + return collectScaleExpression(splat.getSrc(), scale, visiting) && + normalizeScaleExpression(scale); + } + + auto constant = value.getDefiningOp(); + if (!constant) + return false; + auto dense = dyn_cast(constant.getValue()); + if (!dense || !dense.isSplat()) + return false; + auto integer = dyn_cast(dense.getSplatValue()); + return integer && multiplyConstantFactor(scale, integer.getInt()) && + normalizeScaleExpression(scale); +} + +bool containsMakeRange(Value value, DenseSet &visited) { + if (!value || !visited.insert(value).second) + return false; + if (value.getDefiningOp()) + return true; + + Operation *operation = value.getDefiningOp(); + if (!operation || operation->getNumRegions() != 0 || + !isMemoryEffectFree(operation)) + return false; + for (Value operand : operation->getOperands()) { + if (containsMakeRange(operand, visited)) + return true; + } + return false; +} + +std::optional getSingleVaryingAxis(Value value, unsigned rank) { + auto type = dyn_cast(value.getType()); + if (!type || !type.hasStaticShape() || type.getEncoding() || + type.getRank() != rank) + return std::nullopt; + + std::optional varyingAxis; + for (unsigned axis = 0; axis < rank; ++axis) { + if (type.getShape()[axis] == 1) + continue; + if (varyingAxis) + return std::nullopt; + varyingAxis = axis; + } + return varyingAxis; +} + +std::optional parseAffineLaneTerm(Value value, unsigned rank) { + auto broadcast = value.getDefiningOp(); + if (!broadcast || broadcast.getResult() != value) + return std::nullopt; + + Value laneValue = broadcast.getSrc(); + ScaleExpression scale; + if (auto multiply = laneValue.getDefiningOp()) { + ScaleExpression lhsScale; + ScaleExpression rhsScale; + const bool lhsUniform = + collectUniformTensorScale(multiply.getLhs(), lhsScale); + const bool rhsUniform = + collectUniformTensorScale(multiply.getRhs(), rhsScale); + if (lhsUniform == rhsUniform) + return std::nullopt; + if (lhsUniform) { + scale = std::move(lhsScale); + laneValue = multiply.getRhs(); + } else { + scale = std::move(rhsScale); + laneValue = multiply.getLhs(); + } + } else if (!normalizeScaleExpression(scale)) { + return std::nullopt; + } + + std::optional axis = getSingleVaryingAxis(laneValue, rank); + if (!axis) + return std::nullopt; + + DenseSet visited; + if (!containsMakeRange(laneValue, visited)) + return std::nullopt; + return AffineLaneAxis{*axis, laneValue, std::move(scale)}; +} + +bool isSubsetOf(ArrayRef subset, ArrayRef superset) { + size_t subsetIndex = 0; + size_t supersetIndex = 0; + while (subsetIndex < subset.size() && supersetIndex < superset.size()) { + const void *subsetValue = subset[subsetIndex].getAsOpaquePointer(); + const void *supersetValue = superset[supersetIndex].getAsOpaquePointer(); + if (subsetValue == supersetValue) { + ++subsetIndex; + ++supersetIndex; + continue; + } + if (subsetValue < supersetValue) + return false; + ++supersetIndex; + } + return subsetIndex == subset.size(); +} + +// Returns 1 if lhs is structurally a larger physical stride, -1 if rhs is, +// and 0 when the ordering cannot be certified. We intentionally compare the +// symbolic factor structure rather than runtime values: N may be 1 at run +// time, but the masked active domain still establishes the target layout. +int compareScaleExpressions(const ScaleExpression &lhs, + const ScaleExpression &rhs) { + const bool lhsContainsRhs = isSubsetOf(rhs.symbols, lhs.symbols); + const bool rhsContainsLhs = isSubsetOf(lhs.symbols, rhs.symbols); + if (lhsContainsRhs != rhsContainsLhs) + return lhsContainsRhs ? 1 : -1; + + if (!lhsContainsRhs) + return 0; + if (lhs.constantFactor == rhs.constantFactor) + return 0; + return lhs.constantFactor > rhs.constantFactor ? 1 : -1; +} + +bool isIdentityPermutation(ArrayRef permutation) { + for (unsigned index = 0; index < permutation.size(); ++index) { + if (permutation[index] != static_cast(index)) + return false; + } + return true; +} + +bool haveSamePermutation(ArrayRef lhs, ArrayRef rhs) { + return lhs.size() == rhs.size() && llvm::equal(lhs, rhs); +} + +std::optional analyzeAffineLayout(Value pointer) { + auto pointerType = dyn_cast(pointer.getType()); + if (!pointerType || !pointerType.hasStaticShape() || + pointerType.getEncoding() || pointerType.getRank() < 2 || + !isa(pointerType.getElementType()) || + triton::isTensorPointerType(pointerType.getElementType())) + return std::nullopt; + + auto addPtr = pointer.getDefiningOp(); + if (!addPtr || addPtr.getResult() != pointer) + return std::nullopt; + auto baseSplat = addPtr.getPtr().getDefiningOp(); + if (!baseSplat || !isa(baseSplat.getSrc().getType()) || + triton::isTensorPointerType(baseSplat.getSrc().getType())) + return std::nullopt; + + auto offsetType = dyn_cast(addPtr.getOffset().getType()); + if (!offsetType || !offsetType.hasStaticShape() || offsetType.getEncoding() || + offsetType.getShape() != pointerType.getShape()) + return std::nullopt; + + SmallVector pending = {addPtr.getOffset()}; + SmallVector affineTerms; + while (!pending.empty()) { + Value term = pending.pop_back_val(); + if (auto add = term.getDefiningOp()) { + pending.push_back(add.getLhs()); + pending.push_back(add.getRhs()); + continue; + } + affineTerms.push_back(term); + } + + SmallVector axes; + for (Value term : affineTerms) { + if (std::optional axis = + parseAffineLaneTerm(term, pointerType.getRank())) { + axes.push_back(std::move(*axis)); + continue; + } + + // A full-tile splat is an invariant offset and does not affect the lane + // permutation. Other unrecognized terms make the proof fail closed. + if (!term.getDefiningOp()) + return std::nullopt; + } + + if (axes.size() != pointerType.getRank()) + return std::nullopt; + for (unsigned axis = 0; axis < axes.size(); ++axis) { + for (unsigned previous = 0; previous < axis; ++previous) { + if (axes[axis].outputAxis == axes[previous].outputAxis) + return std::nullopt; + } + } + + SmallVector remaining; + for (unsigned axis = 0; axis < axes.size(); ++axis) + remaining.push_back(axis); + SmallVector permutation; + permutation.reserve(axes.size()); + while (!remaining.empty()) { + std::optional selected; + for (unsigned candidate : remaining) { + bool largerThanAll = true; + for (unsigned other : remaining) { + if (candidate == other) + continue; + if (compareScaleExpressions(axes[candidate].scale, axes[other].scale) != + 1) { + largerThanAll = false; + break; + } + } + if (!largerThanAll) + continue; + if (selected) + return std::nullopt; + selected = candidate; + } + if (!selected) + return std::nullopt; + permutation.push_back(static_cast(axes[*selected].outputAxis)); + remaining.erase(llvm::find(remaining, *selected)); + } + + if (failed(Permutation::create(permutation)) || + isIdentityPermutation(permutation)) + return std::nullopt; + + return AffineLayoutAccess{ + pointer, baseSplat.getSrc(), addPtr.getOffset(), + pointerType, std::move(axes), std::move(permutation)}; +} + +const AffineLaneAxis *getAxisForOutputAxis(const AffineLayoutAccess &access, + unsigned outputAxis) { + for (const AffineLaneAxis &axis : access.axes) { + if (axis.outputAxis == outputAxis) + return &axis; + } + return nullptr; +} + +bool isStaticInjectiveAccess(const AffineLayoutAccess &access) { + int64_t innerReachableOffset = 0; + for (int index = static_cast(access.permutation.size()) - 1; index >= 0; + --index) { + const auto outputAxis = static_cast(access.permutation[index]); + const AffineLaneAxis *axis = getAxisForOutputAxis(access, outputAxis); + if (!axis || !axis->scale.symbols.empty() || + axis->scale.constantFactor <= innerReachableOffset) + return false; + + const int64_t extent = access.type.getShape()[outputAxis]; + if (extent <= 0 || + axis->scale.constantFactor > + (std::numeric_limits::max() - innerReachableOffset) / + (extent - 1 == 0 ? 1 : extent - 1)) + return false; + const int64_t contribution = axis->scale.constantFactor * (extent - 1); + if (innerReachableOffset > + std::numeric_limits::max() - contribution) + return false; + innerReachableOffset += contribution; + } + return true; +} + +bool collectMaskComparisons(Value value, + SmallVectorImpl &comparisons, + DenseSet &visited) { + if (!value || !visited.insert(value).second) + return false; + if (auto comparison = value.getDefiningOp()) { + comparisons.push_back(comparison); + return true; + } + if (auto conjunction = value.getDefiningOp()) { + return collectMaskComparisons(conjunction.getLhs(), comparisons, visited) && + collectMaskComparisons(conjunction.getRhs(), comparisons, visited); + } + if (auto broadcast = value.getDefiningOp()) + return collectMaskComparisons(broadcast.getSrc(), comparisons, visited); + return false; +} + +bool collectUniformScaleExpression(Value value, ScaleExpression &scale) { + if (isa(value.getType())) + return collectUniformTensorScale(value, scale); + + DenseSet visiting; + return collectScaleExpression(value, scale, visiting) && + normalizeScaleExpression(scale); +} + +std::optional getStrictUpperBoundFor(arith::CmpIOp comparison, + Value lane) { + const arith::CmpIPredicate predicate = comparison.getPredicate(); + if ((predicate == arith::CmpIPredicate::slt || + predicate == arith::CmpIPredicate::ult) && + comparison.getLhs() == lane) { + ScaleExpression bound; + if (collectUniformScaleExpression(comparison.getRhs(), bound)) + return bound; + } + if ((predicate == arith::CmpIPredicate::sgt || + predicate == arith::CmpIPredicate::ugt) && + comparison.getRhs() == lane) { + ScaleExpression bound; + if (collectUniformScaleExpression(comparison.getLhs(), bound)) + return bound; + } + return std::nullopt; +} + +bool multiplyScaleExpressions(const ScaleExpression &lhs, + const ScaleExpression &rhs, + ScaleExpression &result) { + if (lhs.constantFactor <= 0 || rhs.constantFactor <= 0 || + lhs.constantFactor > + std::numeric_limits::max() / rhs.constantFactor) + return false; + result.constantFactor = lhs.constantFactor * rhs.constantFactor; + result.symbols.clear(); + result.symbols.reserve(lhs.symbols.size() + rhs.symbols.size()); + result.symbols.append(lhs.symbols.begin(), lhs.symbols.end()); + result.symbols.append(rhs.symbols.begin(), rhs.symbols.end()); + return normalizeScaleExpression(result); +} + +// This comparison intentionally requires identical symbolic monomials. It +// never assumes an arbitrary SSA symbol is positive, ordered, or at least one; +// any such unproven relation must keep the candidate fail-closed. +bool dominatesScaleExpression(const ScaleExpression &lhs, + const ScaleExpression &rhs) { + return lhs.constantFactor >= rhs.constantFactor && + lhs.symbols.size() == rhs.symbols.size() && + llvm::equal(lhs.symbols, rhs.symbols); +} + +void collectAxisCapacities(const AffineLaneAxis &axis, int64_t staticExtent, + ArrayRef comparisons, + SmallVectorImpl &capacities) { + if (staticExtent > 0) + capacities.push_back(ScaleExpression{staticExtent, {}}); + for (arith::CmpIOp comparison : comparisons) { + std::optional bound = + getStrictUpperBoundFor(comparison, axis.lane); + if (bound) + capacities.push_back(std::move(*bound)); + } +} + +// Dynamic physical strides need a domain proof, not a runtime argsort. In +// physical outer-to-inner order, prove every adjacent mixed-radix capacity: +// +// outerStride >= innerStride * capacity(innerAxis) +// +// A static tile extent is one capacity; a mask conjunct `lane < B` supplies a +// symbolic capacity B. By induction, the span of every suffix is strictly +// smaller than its leading stride, so active lanes remain injective. This +// handles rank-N canonical forms such as M*N, N, 1 without assuming a runtime +// ordering of M or N. Non-monomial scales or missing bounds still reject. +bool hasDynamicMaskProof(Value mask, const AffineLayoutAccess &access) { + if (!mask || access.type.getRank() != access.permutation.size() || + access.type.getRank() < 2) + return false; + + SmallVector comparisons; + DenseSet visited; + if (!collectMaskComparisons(mask, comparisons, visited)) + return false; + + for (unsigned outerIndex = 0; outerIndex + 1 < access.permutation.size(); + ++outerIndex) { + const auto outerOutputAxis = + static_cast(access.permutation[outerIndex]); + const auto innerOutputAxis = + static_cast(access.permutation[outerIndex + 1]); + const AffineLaneAxis *outer = getAxisForOutputAxis(access, outerOutputAxis); + const AffineLaneAxis *inner = getAxisForOutputAxis(access, innerOutputAxis); + if (!outer || !inner) + return false; + + SmallVector capacities; + collectAxisCapacities(*inner, access.type.getShape()[innerOutputAxis], + comparisons, capacities); + bool hasDominatingCapacity = false; + for (const ScaleExpression &capacity : capacities) { + ScaleExpression required; + if (multiplyScaleExpressions(inner->scale, capacity, required) && + dominatesScaleExpression(outer->scale, required)) { + hasDominatingCapacity = true; + break; + } + } + if (!hasDominatingCapacity) + return false; + } + return true; +} + +bool requiresDynamicMaskProof(const AffineLayoutAccess &access) { + return llvm::any_of(access.axes, [](const AffineLaneAxis &axis) { + return !axis.scale.symbols.empty(); + }); +} + +bool hasValidEndpointMask(Value mask, const AffineLayoutAccess &access) { + if (!requiresDynamicMaskProof(access)) + return isStaticInjectiveAccess(access) && + (!mask || isLayoutTensor(mask.getType(), access.type.getRank())); + return hasDynamicMaskProof(mask, access); +} + +bool isUniformOther(Value value) { + if (!value) + return true; + if (!isa(value.getType())) + return true; + if (value.getDefiningOp()) + return true; + auto constant = value.getDefiningOp(); + if (!constant) + return false; + auto dense = dyn_cast(constant.getValue()); + return dense && dense.isSplat(); +} + +bool hasUnsupportedLoadAttributes(triton::LoadOp load) { + if (auto volatileAttr = load->getAttrOfType("isVolatile")) { + if (volatileAttr.getValue()) + return true; + } + if (load->getAttr("padding")) + return true; + if (auto boundary = load->getAttrOfType("boundaryCheck")) { + if (!boundary.asArrayRef().empty()) + return true; + } + return false; +} + +bool hasUnsupportedStoreAttributes(triton::StoreOp store) { + if (auto boundary = + store->getAttrOfType("boundaryCheck")) { + if (!boundary.asArrayRef().empty()) + return true; + } + return false; +} + +bool isSupportedClonableOperation(Operation *operation) { + if (!operation || operation->getNumRegions() != 0) + return false; + if (isa(operation)) + return true; + return operation->hasTrait() && + isMemoryEffectFree(operation); +} + +std::optional +getPermutedTensorType(RankedTensorType type, const Permutation &permutation) { + if (!type.hasStaticShape() || type.getEncoding() || + type.getRank() != permutation.rank()) + return std::nullopt; + FailureOr> shape = + permutation.permuteShape(type.getShape()); + if (failed(shape)) + return std::nullopt; + return type.clone(*shape); +} + +std::optional getPermutedType(Type type, const Permutation &permutation) { + auto tensor = dyn_cast(type); + if (!tensor || tensor.getRank() != permutation.rank()) + return type; + std::optional permuted = + getPermutedTensorType(tensor, permutation); + if (!permuted) + return std::nullopt; + return Type(*permuted); +} + +bool isPermutedTensorValue(Value value, const Permutation &permutation) { + return hasLayoutRank(value.getType(), permutation.rank()); +} + +// ExpandDims is not a simple axis-attribute permutation. Its source has one +// fewer dimensions, so a rank-R permutation cannot be applied to that source +// verbatim. For the affine layout form we accept here, an expand chain carries +// exactly one make_range-derived lane axis. Rebuild the complete chain from +// its rank-1 source and place that lane at its mapped full-rank axis. This is +// what makes a non-self-inverse 3D permutation such as [1, 2, 0] correct. +std::optional getExpandRankOneSource(triton::ExpandDimsOp expand) { + Value source = expand.getSrc(); + while (auto parent = source.getDefiningOp()) + source = parent.getSrc(); + auto type = dyn_cast(source.getType()); + if (!type || !type.hasStaticShape() || type.getEncoding() || + type.getRank() != 1) + return std::nullopt; + return source; +} + +std::optional +rebuildFullRankExpand(IRRewriter &rewriter, triton::ExpandDimsOp expand, + Value rankOneSource, const Permutation &permutation, + SmallVectorImpl *created = nullptr) { + auto oldType = dyn_cast(expand.getResult().getType()); + std::optional newType = + oldType ? getPermutedTensorType(oldType, permutation) : std::nullopt; + std::optional oldAxis = + oldType ? getSingleVaryingAxis(expand.getResult(), permutation.rank()) + : std::nullopt; + if (!newType || !oldAxis) + return std::nullopt; + const int32_t newVaryingAxis = + permutation.mapOldAxisToNew(static_cast(*oldAxis)); + if (newVaryingAxis < 0) + return std::nullopt; + + Value current = rankOneSource; + auto currentType = dyn_cast(current.getType()); + if (!currentType || currentType.getRank() != 1) + return std::nullopt; + for (unsigned axis = 0; axis < permutation.rank(); ++axis) { + if (axis == static_cast(newVaryingAxis)) + continue; + SmallVector shape(currentType.getShape()); + shape.insert(shape.begin() + axis, 1); + RankedTensorType expandedType = newType->clone(shape); + auto rebuilt = rewriter.create( + expand.getLoc(), expandedType, current, axis); + if (created) + created->push_back(rebuilt.getOperation()); + current = rebuilt.getResult(); + currentType = expandedType; + } + if (current.getType() != *newType) + return std::nullopt; + return current; +} + +// All full-rank values in this rule carry the same layout P. For an explicit +// TTIR transpose Q, the reindexed transpose must satisfy +// +// P[Q'[new]] == Q[P[new]], +// +// hence Q' = P.inverse() \u2218 Q \u2218 P under the shared +// new-axis-to-old-axis convention. This is deliberately computed with +// Permutation::compose rather than a 2-D swap shortcut: non-self-inverse 3-D +// orders are the regression case that would otherwise silently be wrong. +std::optional> +getReindexedTransOrder(triton::TransOp trans, const Permutation &layout) { + FailureOr original = Permutation::create(trans.getOrder()); + if (failed(original) || original->rank() != layout.rank()) + return std::nullopt; + FailureOr originalAfterLayout = original->compose(layout); + if (failed(originalAfterLayout)) + return std::nullopt; + FailureOr reindexed = + layout.inverse().compose(*originalAfterLayout); + if (failed(reindexed)) + return std::nullopt; + return SmallVector(reindexed->getNewToOld().begin(), + reindexed->getNewToOld().end()); +} + +std::optional +rebuildTransOperation(IRRewriter &rewriter, triton::TransOp trans, Value input, + Type resultType, const Permutation &permutation, + SmallVectorImpl *created = nullptr) { + auto newResultType = dyn_cast(resultType); + if (!newResultType || !isLayoutTensor(input.getType(), permutation.rank()) || + !isLayoutTensor(newResultType, permutation.rank())) + return std::nullopt; + std::optional> order = + getReindexedTransOrder(trans, permutation); + if (!order) + return std::nullopt; + FailureOr reindexedOrder = Permutation::create(*order); + if (failed(reindexedOrder)) + return std::nullopt; + auto inputType = cast(input.getType()); + FailureOr> expectedShape = + reindexedOrder->permuteShape(inputType.getShape()); + if (failed(expectedShape) || *expectedShape != newResultType.getShape()) + return std::nullopt; + + auto rebuilt = rewriter.create( + trans.getLoc(), newResultType, input, ArrayRef(*order)); + for (NamedAttribute attribute : trans->getAttrs()) { + if (attribute.getName().getValue() != "order") + rebuilt->setAttr(attribute.getName(), attribute.getValue()); + } + if (created) + created->push_back(rebuilt.getOperation()); + return rebuilt.getOperation(); +} + +bool isRebuildableExternalTensor(Value value, const Permutation &permutation, + DenseSet &visiting) { + if (!isPermutedTensorValue(value, permutation)) + return true; + if (!visiting.insert(value).second) + return false; + + // `visiting` is a recursion stack, not a memo set. A legal external DAG can + // fan out and feed the same full-rank value into multiple operands (for + // example `arith.andi %mask, %mask`). LayoutValueCloner has a value cache + // and rebuilds that shape once, so rejecting the second DAG edge here would + // be an unnecessary false negative. Erase this value on every return while + // retaining cycle detection along the active ancestry. + auto finish = [&](bool result) { + visiting.erase(value); + return result; + }; + if (!isLayoutTensor(value.getType(), permutation.rank())) + return finish(false); + + Operation *operation = value.getDefiningOp(); + if (!operation || !isSupportedClonableOperation(operation)) + return finish(false); + if (auto constant = dyn_cast(operation)) { + auto dense = dyn_cast(constant.getValue()); + return finish(dense && dense.isSplat()); + } + if (auto expand = dyn_cast(operation)) { + if (isPermutedTensorValue(expand.getResult(), permutation) && + !getExpandRankOneSource(expand)) + return finish(false); + } + + for (Value operand : operation->getOperands()) { + if (!isRebuildableExternalTensor(operand, permutation, visiting)) + return finish(false); + } + return finish(true); +} + +bool isAutomaticOverflowAssert(triton::AssertOp assertOp) { + if (!assertOp) + return false; + // The message is useful diagnostic context, but not a provenance proof: a + // user-authored device_assert can deliberately use the same text. The + // frontend places this private marker only on sanitize_overflow assertions. + if (!assertOp->hasAttr("tt.auto_overflow_assert")) + return false; + Attribute message = assertOp.getMessageAttr(); + auto string = dyn_cast(message); + return string && + string.getValue().contains("overflow detected for operation"); +} + +bool isPermutableOverflowAssertCondition(Value condition, + const Permutation &permutation) { + auto type = dyn_cast(condition.getType()); + if (!type || !isLayoutTensor(type, permutation.rank())) + return false; + auto elementType = dyn_cast(type.getElementType()); + return elementType && elementType.getWidth() == 1; +} + +// `isSupportedClonableOperation` intentionally accepts load/store/assert for +// the loop-body rewriter. They cannot be part of the old address/guard slice +// we retire, because that slice is only safe to erase when it is pure. +bool isRetirablePureProducer(Operation *operation) { + return operation && operation->getNumRegions() == 0 && + operation->getNumResults() == 1 && + !isa(operation) && + isSupportedClonableOperation(operation) && + isMemoryEffectFree(operation); +} + +struct LayoutClosure { + DenseSet values; + llvm::SetVector operations; +}; + +// Collect the old full-rank layout slice that LayoutValueCloner will recreate. +// Rank-0/rank-1 leaves (for example N and make_range sources) remain shared +// external inputs; they neither need a layout permutation nor can they be +// retired until their ordinary users are gone. +bool collectLayoutClosure(Value value, const Permutation &permutation, + LayoutClosure &closure, Block *sourceBlock) { + if (!isPermutedTensorValue(value, permutation)) + return true; + if (!isLayoutTensor(value.getType(), permutation.rank())) + return false; + if (!closure.values.insert(value).second) + return true; + + Operation *operation = value.getDefiningOp(); + if (!isRetirablePureProducer(operation) || + (sourceBlock && operation->getBlock() != sourceBlock)) + return false; + if (auto constant = dyn_cast(operation)) { + auto dense = dyn_cast(constant.getValue()); + if (!dense || !dense.isSplat()) + return false; + } + if (auto expand = dyn_cast(operation)) { + if (isPermutedTensorValue(expand.getResult(), permutation) && + !getExpandRankOneSource(expand)) + return false; + } + + // LayoutValueCloner rebuilds every result of an operation. Account for all + // of its full-rank results here as well, so an unselected use of a sibling + // result cannot escape the proof. + for (Value result : operation->getResults()) { + if (!isPermutedTensorValue(result, permutation)) + continue; + if (!isLayoutTensor(result.getType(), permutation.rank())) + return false; + closure.values.insert(result); + } + closure.operations.insert(operation); + + for (Value operand : operation->getOperands()) { + if (!collectLayoutClosure(operand, permutation, closure, sourceBlock)) + return false; + } + return true; +} + +void mergeLayoutClosure(LayoutClosure &into, const LayoutClosure &from) { + for (Value value : from.values) + into.values.insert(value); + for (Operation *operation : from.operations) + into.operations.insert(operation); +} + +bool hasSharedLayoutProvenance(Value value, + const DenseSet &addressValues, + DenseSet &visited) { + if (addressValues.contains(value)) + return true; + if (!value || !visited.insert(value).second) + return false; + Operation *operation = value.getDefiningOp(); + if (!operation || operation->getNumRegions() != 0) + return false; + return llvm::any_of(operation->getOperands(), [&](Value operand) { + return hasSharedLayoutProvenance(operand, addressValues, visited); + }); +} + +// This deliberately performs only a read-only walk. It is used to decide +// whether a preheader assert is relevant before we impose the stronger +// same-Block/pure-producer closure proof. In particular, an ordinary +// load/store candidate with no relevant automatic assert must retain the +// pre-existing admission behavior for externally supplied mask/pointer +// values. +void collectLayoutProvenance(Value value, const Permutation &permutation, + DenseSet &provenance, + DenseSet &visited) { + if (!value || !visited.insert(value).second) + return; + if (isPermutedTensorValue(value, permutation)) + provenance.insert(value); + + Operation *operation = value.getDefiningOp(); + if (!operation || operation->getNumRegions() != 0) + return; + for (Value operand : operation->getOperands()) + collectLayoutProvenance(operand, permutation, provenance, visited); +} + +bool isClosureInBlock(const LayoutClosure &closure, Block *block) { + return block && llvm::all_of(closure.operations, [&](Operation *operation) { + return operation && operation->getBlock() == block; + }); +} + +bool verifyLayoutClosureUses(const LayoutClosure &closure, Operation *endpoint, + ArrayRef selectedBlockOperations, + ArrayRef assertPorts) { + if (!endpoint) + return false; + + DenseSet permittedConsumers; + for (Operation *operation : closure.operations) + permittedConsumers.insert(operation); + permittedConsumers.insert(endpoint); + for (Operation *operation : selectedBlockOperations) + permittedConsumers.insert(operation); + for (const ExternalAssertPort &port : assertPorts) { + if (!port.assertOperation) + return false; + permittedConsumers.insert(port.assertOperation); + } + + for (Value value : closure.values) { + for (OpOperand &use : value.getUses()) { + Operation *owner = use.getOwner(); + if (permittedConsumers.contains(owner)) + continue; + return false; + } + } + return true; +} + +// Discover preheader (or pre-anchor) automatic overflow assertions that are +// side users of the same pointer-layout provenance. Any other side use of a +// layout value in the resulting closure makes the whole candidate fail closed. +bool collectAutomaticOverflowAssertClosure( + LayoutPermutationCandidate &candidate, Block *assertBlock, Operation *limit, + ArrayRef pointerRoots, const Permutation &permutation) { + if (!assertBlock || !limit || limit->getBlock() != assertBlock) + return false; + + DenseSet pointerProvenance; + DenseSet provenanceVisited; + for (Value value : pointerRoots) { + collectLayoutProvenance(value, permutation, pointerProvenance, + provenanceVisited); + } + + SmallVector ports; + bool foundLimit = false; + for (Operation &operation : *assertBlock) { + if (&operation == limit) { + foundLimit = true; + break; + } + auto assertOp = dyn_cast(operation); + if (!assertOp || !isAutomaticOverflowAssert(assertOp)) + continue; + + Value condition = assertOp.getCondition(); + DenseSet conditionVisited; + if (!hasSharedLayoutProvenance(condition, pointerProvenance, + conditionVisited)) + continue; + if (!isPermutableOverflowAssertCondition(condition, permutation)) + return false; + ports.push_back(ExternalAssertPort{assertOp.getOperation(), condition}); + } + if (!foundLimit) + return false; + // No relevant marked auto assert: preserve the existing candidate behavior. + // Do not require an external mask/pointer to be a locally-retirable guard + // producer merely because this extension is enabled. + if (ports.empty()) + return true; + + LayoutClosure pointerClosure; + for (Value value : pointerRoots) { + if (!collectLayoutClosure(value, permutation, pointerClosure, assertBlock)) + return false; + } + + LayoutClosure guardClosure; + for (const ExternalAssertPort &port : ports) { + LayoutClosure conditionClosure; + if (!collectLayoutClosure(port.oldCondition, permutation, conditionClosure, + assertBlock)) + return false; + mergeLayoutClosure(guardClosure, conditionClosure); + } + + LayoutClosure addressClosure = pointerClosure; + mergeLayoutClosure(addressClosure, guardClosure); + + // A pointer-layout producer is commonly shared by the external mask (and + // occasionally by `other`/loop init values) that the ordinary rewrite will + // clone into the new endpoint. Admit only those external slices that + // actually depend on the address/guard closure. This restores their valid + // use as closed consumers without imposing any same-Block/pure requirement + // on unrelated external inputs when no automatic assert is present. + for (Value value : candidate.externalTensorValues) { + DenseSet externalVisited; + if (!hasSharedLayoutProvenance(value, addressClosure.values, + externalVisited)) + continue; + LayoutClosure externalConsumerClosure; + if (!collectLayoutClosure(value, permutation, externalConsumerClosure, + assertBlock)) + return false; + mergeLayoutClosure(addressClosure, externalConsumerClosure); + } + + // A guarded block/loop may not require a layout value through a Region port. + // The source-block proof above is intentionally exact: this rule does not + // synthesize scf.if/scf.while value ports for a type-changing rewrite. + if (!isClosureInBlock(addressClosure, assertBlock)) + return false; + + Operation *endpoint = + candidate.loop ? candidate.loop.getOperation() : nullptr; + if (!endpoint && candidate.block) + endpoint = candidate.anchor; + if (!verifyLayoutClosureUses(addressClosure, endpoint, + candidate.blockOperations, ports)) + return false; + + // Some loop-only external tensors (for example the full-rank pointer + // increment splat) do not share address provenance with a guard, but become + // dead solely because the old endpoint is replaced. Reclaim them when the + // same closed-use proof succeeds; an unrelated/external value simply stays + // outside the retirement set and cannot make this assert extension reject a + // valid candidate. + LayoutClosure retireClosure = addressClosure; + for (Value value : candidate.externalTensorValues) { + if (addressClosure.values.contains(value)) + continue; + LayoutClosure auxiliaryClosure; + if (!collectLayoutClosure(value, permutation, auxiliaryClosure, + assertBlock) || + !isClosureInBlock(auxiliaryClosure, assertBlock) || + !verifyLayoutClosureUses(auxiliaryClosure, endpoint, + candidate.blockOperations, ports)) + continue; + mergeLayoutClosure(retireClosure, auxiliaryClosure); + } + + candidate.externalAssertPorts = std::move(ports); + candidate.retireOperations.append(retireClosure.operations.begin(), + retireClosure.operations.end()); + return true; +} + +bool isSupportedLoopBodyOperation(Operation *operation) { + if (isa(operation)) + return true; + return isSupportedClonableOperation(operation); +} + +bool valueDependsOnLoad(Value value, const DenseSet &loadResults, + DenseSet &visited) { + if (!value || !visited.insert(value).second) + return false; + if (loadResults.contains(value)) + return true; + Operation *operation = value.getDefiningOp(); + if (!operation || operation->getNumRegions() != 0) + return false; + for (Value operand : operation->getOperands()) { + if (valueDependsOnLoad(operand, loadResults, visited)) + return true; + } + return false; +} + +std::optional getForIterArgIndex(scf::ForOp loop, Value value) { + auto blockArgument = dyn_cast(value); + if (!blockArgument || blockArgument.getOwner() != loop.getBody() || + blockArgument.getArgNumber() == 0) + return std::nullopt; + const unsigned index = blockArgument.getArgNumber() - 1; + if (index >= loop.getInitArgs().size()) + return std::nullopt; + return index; +} + +std::optional getReducedResultType(RankedTensorType inputType, + int32_t axis) { + if (axis < 0 || static_cast(axis) >= inputType.getRank()) + return std::nullopt; + SmallVector shape(inputType.getShape()); + shape.erase(shape.begin() + axis); + return inputType.clone(shape); +} + +std::optional matchLoopCandidate(scf::ForOp loop) { + if (!loop || !loop.getBody() || !loop.getBody()->hasNoPredecessors() || + loop.getBody()->getNumArguments() != loop.getInitArgs().size() + 1) + return std::nullopt; + + Block *body = loop.getBody(); + SmallVector loads; + SmallVector stores; + SmallVector accesses; + DenseSet loadResults; + std::optional> permutation; + + for (Operation &operation : *body) { + if (auto load = dyn_cast(operation)) { + if (hasUnsupportedLoadAttributes(load) || + !isUniformOther(load.getOther())) + return std::nullopt; + std::optional iterArgIndex = + getForIterArgIndex(loop, load.getPtr()); + auto pointerType = dyn_cast(load.getPtr().getType()); + if (!iterArgIndex || !pointerType || !pointerType.hasStaticShape() || + pointerType.getEncoding() || pointerType.getRank() < 2) + return std::nullopt; + std::optional access = + analyzeAffineLayout(loop.getInitArgs()[*iterArgIndex]); + if (!access || + !isLayoutTensor(load.getResult().getType(), access->type.getRank()) || + !hasValidEndpointMask(load.getMask(), *access)) + return std::nullopt; + if (permutation && + !haveSamePermutation(*permutation, access->permutation)) + return std::nullopt; + permutation = access->permutation; + loadResults.insert(load.getResult()); + loads.push_back(load); + accesses.push_back(std::move(*access)); + continue; + } + + if (auto store = dyn_cast(operation)) { + if (hasUnsupportedStoreAttributes(store)) + return std::nullopt; + std::optional iterArgIndex = + getForIterArgIndex(loop, store.getPtr()); + if (!iterArgIndex) + return std::nullopt; + std::optional access = + analyzeAffineLayout(loop.getInitArgs()[*iterArgIndex]); + if (!access || + !isLayoutTensor(store.getValue().getType(), access->type.getRank()) || + !hasValidEndpointMask(store.getMask(), *access)) + return std::nullopt; + if (permutation && + !haveSamePermutation(*permutation, access->permutation)) + return std::nullopt; + permutation = access->permutation; + stores.push_back(store); + accesses.push_back(std::move(*access)); + } + } + + if (loads.empty() || stores.empty() || !permutation) + return std::nullopt; + FailureOr layoutPermutation = Permutation::create(*permutation); + if (failed(layoutPermutation) || isIdentityPermutation(*permutation)) + return std::nullopt; + + bool hasDataPath = false; + for (triton::StoreOp store : stores) { + DenseSet visited; + if (valueDependsOnLoad(store.getValue(), loadResults, visited)) { + hasDataPath = true; + break; + } + } + if (!hasDataPath) + return std::nullopt; + + llvm::SetVector externalValues; + for (Value init : loop.getInitArgs()) { + if (isPermutedTensorValue(init, *layoutPermutation)) + externalValues.insert(init); + } + + for (Operation &operation : *body) { + if (auto assertOp = dyn_cast(operation)) { + // External automatic overflow assertions are handled by the dedicated + // preheader closure below. Do not silently transpose a tensor-valued + // user assertion inside the loop body. + if (isPermutedTensorValue(assertOp.getCondition(), *layoutPermutation)) + return std::nullopt; + } + if (!isSupportedLoopBodyOperation(&operation)) + return std::nullopt; + for (Value result : operation.getResults()) { + if (isPermutedTensorValue(result, *layoutPermutation) && + !isLayoutTensor(result.getType(), layoutPermutation->rank())) + return std::nullopt; + } + for (Value operand : operation.getOperands()) { + if (!isPermutedTensorValue(operand, *layoutPermutation)) + continue; + if (!isLayoutTensor(operand.getType(), layoutPermutation->rank())) + return std::nullopt; + auto blockArgument = dyn_cast(operand); + if (blockArgument && blockArgument.getOwner() == body) + continue; + Operation *definingOperation = operand.getDefiningOp(); + if (definingOperation && definingOperation->getBlock() == body) + continue; + externalValues.insert(operand); + } + } + + for (Value value : externalValues) { + DenseSet visiting; + if (!isRebuildableExternalTensor(value, *layoutPermutation, visiting)) + return std::nullopt; + } + + LayoutPermutationCandidate candidate; + candidate.loop = loop; + candidate.anchor = loads.front().getOperation(); + candidate.permutation = *permutation; + candidate.externalTensorValues.append(externalValues.begin(), + externalValues.end()); + // The loop rewrite clones exactly these direct body operations. Record them + // explicitly for the guard use-closure proof instead of treating an + // arbitrary descendant Region operation as an old-loop consumer. + for (Operation &operation : *body) + candidate.blockOperations.push_back(&operation); + candidate.endpointCount = loads.size() + stores.size(); + + for (auto [index, result] : llvm::enumerate(loop.getResults())) { + if (!isPermutedTensorValue(result, *layoutPermutation)) + continue; + candidate.transformedResultIndices.push_back(index); + for (OpOperand &use : result.getUses()) { + auto reduce = dyn_cast(use.getOwner()); + if (!reduce || reduce.getNumOperands() != 1 || + use.getOperandNumber() != 0 || reduce->getNumResults() != 1) + return std::nullopt; + const int32_t newAxis = + layoutPermutation->mapOldAxisToNew(reduce.getAxis()); + auto oldInputType = dyn_cast(result.getType()); + FailureOr> newInputShape = + layoutPermutation->permuteShape(oldInputType.getShape()); + if (newAxis < 0 || failed(newInputShape)) + return std::nullopt; + RankedTensorType newInputType = oldInputType.clone(*newInputShape); + std::optional expectedResultType = + getReducedResultType(newInputType, newAxis); + if (!expectedResultType || + *expectedResultType != reduce->getResult(0).getType()) + return std::nullopt; + candidate.reductionPorts.push_back( + ReductionPort{reduce, static_cast(index)}); + } + } + + llvm::SetVector pointerRoots; + for (const AffineLayoutAccess &access : accesses) + pointerRoots.insert(access.pointer); + if (!collectAutomaticOverflowAssertClosure( + candidate, loop->getBlock(), loop.getOperation(), + SmallVector(pointerRoots.begin(), pointerRoots.end()), + *layoutPermutation)) + return std::nullopt; + + return candidate; +} + +// A local candidate never crosses a Region port. This covers a plain function +// block and a block nested in scf.if/scf.while/index_switch when every value +// that carries the rewritten layout is consumed in that same Block. scf.for +// is handled by the dedicated complete port adapter above. +bool isSafeExternalBlockTensor(Value value, const Permutation &permutation) { + DenseSet rebuildVisiting; + if (!isRebuildableExternalTensor(value, permutation, rebuildVisiting)) + return false; + + SmallVector pending = {value}; + DenseSet visited; + while (!pending.empty()) { + Value current = pending.pop_back_val(); + if (!isPermutedTensorValue(current, permutation) || + !visited.insert(current).second) + continue; + Operation *operation = current.getDefiningOp(); + if (!operation || + isa(operation)) + return false; + for (Value operand : operation->getOperands()) + pending.push_back(operand); + } + return true; +} + +std::optional matchBlockCandidate(Block *block) { + if (!block || block->empty()) + return std::nullopt; + if (auto loop = dyn_cast_or_null(block->getParentOp())) { + if (loop.getBody() == block) + return std::nullopt; + } + + SmallVector loads; + SmallVector stores; + SmallVector accesses; + DenseSet loadResults; + std::optional> permutation; + + auto recordAccess = [&](Value pointer, Value mask, Type valueType) -> bool { + auto pointerType = dyn_cast(pointer.getType()); + // Rank-0/rank-1 operations are outside this layout closure and can remain + // in the block unchanged (for example a post-reduction scalar store). + if (!pointerType || pointerType.getRank() < 2) + return true; + std::optional access = analyzeAffineLayout(pointer); + if (!access || !isLayoutTensor(valueType, access->type.getRank()) || + !hasValidEndpointMask(mask, *access)) + return false; + if (permutation && !haveSamePermutation(*permutation, access->permutation)) + return false; + permutation = access->permutation; + accesses.push_back(std::move(*access)); + return true; + }; + + for (Operation &operation : *block) { + if (auto load = dyn_cast(operation)) { + if (hasUnsupportedLoadAttributes(load) || + !isUniformOther(load.getOther()) || + !recordAccess(load.getPtr(), load.getMask(), + load.getResult().getType())) + return std::nullopt; + auto pointerType = dyn_cast(load.getPtr().getType()); + if (pointerType && pointerType.getRank() >= 2) { + loads.push_back(load); + loadResults.insert(load.getResult()); + } + continue; + } + if (auto store = dyn_cast(operation)) { + if (hasUnsupportedStoreAttributes(store) || + !recordAccess(store.getPtr(), store.getMask(), + store.getValue().getType())) + return std::nullopt; + auto pointerType = dyn_cast(store.getPtr().getType()); + if (pointerType && pointerType.getRank() >= 2) + stores.push_back(store); + } + } + + if (loads.empty() || stores.empty() || !permutation) + return std::nullopt; + FailureOr layoutPermutation = Permutation::create(*permutation); + if (failed(layoutPermutation) || isIdentityPermutation(*permutation)) + return std::nullopt; + + bool hasDataPath = false; + for (triton::StoreOp store : stores) { + DenseSet visited; + if (valueDependsOnLoad(store.getValue(), loadResults, visited)) { + hasDataPath = true; + break; + } + } + if (!hasDataPath) + return std::nullopt; + + DenseSet selected; + for (triton::LoadOp load : loads) + selected.insert(load.getOperation()); + for (triton::StoreOp store : stores) + selected.insert(store.getOperation()); + + bool changed = true; + while (changed) { + changed = false; + for (Operation &operation : *block) { + if (selected.contains(&operation)) + continue; + bool usesSelectedValue = + llvm::any_of(operation.getOperands(), [&](Value operand) { + Operation *definingOperation = operand.getDefiningOp(); + return definingOperation && selected.contains(definingOperation); + }); + if (!usesSelectedValue) + continue; + // Assert is a zero-result side use, not part of the forward data DAG. + // In particular, a user assertion must not be cloned/deleted as an + // incidental consumer of a transformed tensor. + if (isa(&operation)) + return std::nullopt; + if (!isSupportedClonableOperation(&operation)) + return std::nullopt; + selected.insert(&operation); + changed = true; + } + } + + llvm::SetVector externalValues; + SmallVector blockOperations; + for (Operation &operation : *block) { + if (!selected.contains(&operation)) + continue; + blockOperations.push_back(&operation); + if (!isSupportedClonableOperation(&operation)) + return std::nullopt; + for (Value result : operation.getResults()) { + if (isPermutedTensorValue(result, *layoutPermutation) && + !isLayoutTensor(result.getType(), layoutPermutation->rank())) + return std::nullopt; + for (OpOperand &use : result.getUses()) { + if (use.getOwner()->getBlock() != block || + !selected.contains(use.getOwner())) + return std::nullopt; + } + } + for (Value operand : operation.getOperands()) { + if (!isPermutedTensorValue(operand, *layoutPermutation)) + continue; + if (!isLayoutTensor(operand.getType(), layoutPermutation->rank())) + return std::nullopt; + Operation *definingOperation = operand.getDefiningOp(); + if (!definingOperation || !selected.contains(definingOperation)) + externalValues.insert(operand); + } + } + + for (Value value : externalValues) { + if (!isSafeExternalBlockTensor(value, *layoutPermutation)) + return std::nullopt; + } + + LayoutPermutationCandidate candidate; + candidate.block = block; + candidate.anchor = loads.front().getOperation(); + candidate.permutation = *permutation; + candidate.externalTensorValues.append(externalValues.begin(), + externalValues.end()); + candidate.blockOperations = std::move(blockOperations); + candidate.endpointCount = loads.size() + stores.size(); + llvm::SetVector pointerRoots; + for (const AffineLayoutAccess &access : accesses) + pointerRoots.insert(access.pointer); + // Unlike scf.for's dedicated preheader adapter, a generic Block rewrite + // does not synthesize Region arguments/results for a type-changing pointer. + // A branch-local load/store may therefore only use a pointer materialized in + // that exact Block; accepting a parent Block pointer would leave an old/new + // layout port across scf.if/scf.while. + for (Value pointer : pointerRoots) { + Operation *definingOperation = pointer.getDefiningOp(); + if (!definingOperation || definingOperation->getBlock() != block) + return std::nullopt; + } + if (!collectAutomaticOverflowAssertClosure( + candidate, block, candidate.anchor, + SmallVector(pointerRoots.begin(), pointerRoots.end()), + *layoutPermutation)) + return std::nullopt; + return candidate; +} + +class LayoutValueCloner { +public: + LayoutValueCloner(IRRewriter &rewriter, const Permutation &permutation, + SmallVectorImpl &created) + : rewriter(rewriter), permutation(permutation), created(created) {} + + std::optional clone(Value value) { + if (!isPermutedTensorValue(value, permutation)) + return value; + if (auto found = values.find(value); found != values.end()) + return found->second; + if (!isLayoutTensor(value.getType(), permutation.rank()) || + !visiting.insert(value).second) + return std::nullopt; + + Operation *operation = value.getDefiningOp(); + if (!operation || !isSupportedClonableOperation(operation)) + return std::nullopt; + + std::optional rebuilt = rebuildOperation(operation); + if (!rebuilt) + return std::nullopt; + for (auto [oldResult, newResult] : + llvm::zip(operation->getResults(), (*rebuilt)->getResults())) + values[oldResult] = newResult; + return values.lookup(value); + } + + std::optional get(Value value) const { + if (!isPermutedTensorValue(value, permutation)) + return value; + auto found = values.find(value); + if (found == values.end()) + return std::nullopt; + return found->second; + } + +private: + std::optional mapExternalOperand(Value value) { + if (!isPermutedTensorValue(value, permutation)) + return value; + return clone(value); + } + + std::optional rebuildOperation(Operation *operation) { + if (auto constant = dyn_cast(operation)) { + auto dense = dyn_cast(constant.getValue()); + auto oldType = dyn_cast(constant.getResult().getType()); + std::optional newType = + oldType ? getPermutedTensorType(oldType, permutation) : std::nullopt; + if (!dense || !dense.isSplat() || !newType) + return std::nullopt; + SmallVector elements = {dense.getSplatValue()}; + auto newDense = DenseElementsAttr::get(*newType, elements); + auto rebuilt = rewriter.create(operation->getLoc(), + *newType, newDense); + created.push_back(rebuilt.getOperation()); + return rebuilt.getOperation(); + } + + SmallVector operands; + operands.reserve(operation->getNumOperands()); + for (Value operand : operation->getOperands()) { + std::optional mapped = mapExternalOperand(operand); + if (!mapped) + return std::nullopt; + operands.push_back(*mapped); + } + + SmallVector resultTypes; + resultTypes.reserve(operation->getNumResults()); + for (Value result : operation->getResults()) { + std::optional type = getPermutedType(result.getType(), permutation); + if (!type) + return std::nullopt; + resultTypes.push_back(*type); + } + + if (auto trans = dyn_cast(operation)) { + if (operands.size() != 1 || resultTypes.size() != 1) + return std::nullopt; + return rebuildTransOperation(rewriter, trans, operands.front(), + resultTypes.front(), permutation, &created); + } + + if (auto expand = dyn_cast(operation)) { + if (isPermutedTensorValue(expand.getResult(), permutation)) { + std::optional source = getExpandRankOneSource(expand); + if (!source) + return std::nullopt; + std::optional mappedSource = mapExternalOperand(*source); + if (!mappedSource) + return std::nullopt; + std::optional rebuilt = rebuildFullRankExpand( + rewriter, expand, *mappedSource, permutation, &created); + if (!rebuilt) + return std::nullopt; + return rebuilt->getDefiningOp(); + } + + if (resultTypes.size() != 1) + return std::nullopt; + auto rebuilt = rewriter.create( + operation->getLoc(), cast(resultTypes.front()), + operands.front(), expand.getAxis()); + created.push_back(rebuilt.getOperation()); + return rebuilt.getOperation(); + } + + OperationState state(operation->getLoc(), operation->getName()); + state.addOperands(operands); + state.addTypes(resultTypes); + state.addAttributes(operation->getAttrs()); + Operation *rebuilt = rewriter.create(state); + created.push_back(rebuilt); + return rebuilt; + } + + IRRewriter &rewriter; + const Permutation &permutation; + SmallVectorImpl &created; + DenseMap values; + DenseSet visiting; +}; + +std::optional mapLoopValue(Value value, IRMapping &mapping, + LayoutValueCloner &externalValues, + const Permutation &permutation) { + if (mapping.contains(value)) + return mapping.lookup(value); + if (!isPermutedTensorValue(value, permutation)) + return value; + return externalValues.get(value); +} + +std::optional +cloneLoopOperation(IRRewriter &rewriter, Operation *operation, + IRMapping &mapping, LayoutValueCloner &externalValues, + const Permutation &permutation, + SmallVectorImpl *created = nullptr) { + if (!isSupportedClonableOperation(operation)) + return std::nullopt; + + if (auto constant = dyn_cast(operation)) { + auto dense = dyn_cast(constant.getValue()); + auto oldType = dyn_cast(constant.getResult().getType()); + if (!dense || !dense.isSplat() || !oldType) + return std::nullopt; + std::optional newType = + getPermutedTensorType(oldType, permutation); + if (!newType) + return std::nullopt; + SmallVector elements = {dense.getSplatValue()}; + auto rebuilt = rewriter.create( + operation->getLoc(), *newType, + DenseElementsAttr::get(*newType, elements)); + if (created) + created->push_back(rebuilt.getOperation()); + mapping.map(constant.getResult(), rebuilt.getResult()); + return rebuilt.getOperation(); + } + + SmallVector operands; + operands.reserve(operation->getNumOperands()); + for (Value operand : operation->getOperands()) { + std::optional mapped = + mapLoopValue(operand, mapping, externalValues, permutation); + if (!mapped) + return std::nullopt; + operands.push_back(*mapped); + } + + SmallVector resultTypes; + resultTypes.reserve(operation->getNumResults()); + for (Value result : operation->getResults()) { + std::optional type = getPermutedType(result.getType(), permutation); + if (!type) + return std::nullopt; + resultTypes.push_back(*type); + } + + Operation *rebuilt = nullptr; + if (auto trans = dyn_cast(operation)) { + if (operands.size() != 1 || resultTypes.size() != 1) + return std::nullopt; + std::optional rebuiltTrans = + rebuildTransOperation(rewriter, trans, operands.front(), + resultTypes.front(), permutation, created); + if (!rebuiltTrans) + return std::nullopt; + rebuilt = *rebuiltTrans; + } else if (auto expand = dyn_cast(operation)) { + if (isPermutedTensorValue(expand.getResult(), permutation)) { + std::optional source = getExpandRankOneSource(expand); + if (!source) + return std::nullopt; + std::optional mappedSource = + mapLoopValue(*source, mapping, externalValues, permutation); + if (!mappedSource) + return std::nullopt; + std::optional rebuiltValue = rebuildFullRankExpand( + rewriter, expand, *mappedSource, permutation, created); + if (!rebuiltValue) + return std::nullopt; + rebuilt = rebuiltValue->getDefiningOp(); + } else { + if (resultTypes.size() != 1) + return std::nullopt; + rebuilt = rewriter + .create( + operation->getLoc(), + cast(resultTypes.front()), + operands.front(), expand.getAxis()) + .getOperation(); + if (created) + created->push_back(rebuilt); + } + } else { + OperationState state(operation->getLoc(), operation->getName()); + state.addOperands(operands); + state.addTypes(resultTypes); + state.addAttributes(operation->getAttrs()); + rebuilt = rewriter.create(state); + if (created) + created->push_back(rebuilt); + } + + for (auto [oldResult, newResult] : + llvm::zip(operation->getResults(), rebuilt->getResults())) + mapping.map(oldResult, newResult); + return rebuilt; +} + +std::optional cloneReduction(IRRewriter &rewriter, + triton::ReduceOp reduce, + Value newInput, + const Permutation &permutation) { + if (reduce.getNumOperands() != 1 || reduce->getNumResults() != 1) + return std::nullopt; + const int32_t newAxis = permutation.mapOldAxisToNew(reduce.getAxis()); + if (newAxis < 0) + return std::nullopt; + + auto rebuilt = rewriter.create( + reduce.getLoc(), ValueRange{newInput}, static_cast(newAxis)); + rewriter.cloneRegionBefore(reduce.getCombineOp(), rebuilt.getCombineOp(), + rebuilt.getCombineOp().end()); + for (NamedAttribute attribute : reduce->getAttrs()) { + if (!rebuilt->hasAttr(attribute.getName())) + rebuilt->setAttr(attribute.getName(), attribute.getValue()); + } + return rebuilt; +} + +using AssertConditionReplacement = std::pair; + +std::optional> +cloneExternalAssertConditions(IRRewriter &rewriter, + ArrayRef assertPorts, + LayoutValueCloner &externalValues, + const Permutation &permutation) { + SmallVector replacements; + replacements.reserve(assertPorts.size()); + for (const ExternalAssertPort &port : assertPorts) { + auto assertOp = dyn_cast_or_null(port.assertOperation); + if (!assertOp || assertOp.getCondition() != port.oldCondition) + return std::nullopt; + std::optional expectedType = + getPermutedType(port.oldCondition.getType(), permutation); + if (!expectedType) + return std::nullopt; + + // Keep the assert in place. Inserting its replacement condition directly + // before it preserves both the original lexical assertion order and the + // overflow check's required dominance over the checked i32 operation. + rewriter.setInsertionPoint(assertOp); + std::optional condition = externalValues.clone(port.oldCondition); + if (!condition || condition->getType() != *expectedType) + return std::nullopt; + replacements.push_back({assertOp.getOperation(), *condition}); + } + return replacements; +} + +bool verifyCreatedOperations(ArrayRef created) { + return llvm::all_of(created, [](Operation *operation) { + return operation && succeeded(mlir::verify(operation)); + }); +} + +void commitAssertConditionReplacements( + IRRewriter &rewriter, ArrayRef replacements) { + for (const AssertConditionReplacement &replacement : replacements) { + rewriter.modifyOpInPlace(replacement.first, [&] { + replacement.first->setOperand(0, replacement.second); + }); + } +} + +void eraseDeadPureClosure(IRRewriter &rewriter, + ArrayRef retireOperations) { + SmallVector pending(retireOperations.begin(), + retireOperations.end()); + bool erasedAny = true; + while (erasedAny) { + erasedAny = false; + for (Operation *&operation : llvm::reverse(pending)) { + if (!operation || !isRetirablePureProducer(operation)) + continue; + if (llvm::any_of(operation->getResults(), + [](Value result) { return !result.use_empty(); })) + continue; + rewriter.eraseOp(operation); + operation = nullptr; + erasedAny = true; + } + } +} + +bool canCommitLoopRewrite(const LayoutPermutationCandidate &candidate, + scf::ForOp oldFor, scf::ForOp newFor) { + if (!oldFor || !newFor || oldFor.getNumResults() != newFor.getNumResults()) + return false; + + DenseSet transformed(candidate.transformedResultIndices.begin(), + candidate.transformedResultIndices.end()); + DenseSet expectedReductions; + for (const ReductionPort &port : candidate.reductionPorts) { + if (!port.reduce || port.forResultIndex >= oldFor.getNumResults()) + return false; + expectedReductions.insert(static_cast(port.reduce)); + } + + for (auto [index, oldResult] : llvm::enumerate(oldFor.getResults())) { + if (!transformed.contains(index)) { + if (oldResult.getType() != newFor.getResult(index).getType()) + return false; + continue; + } + for (OpOperand &use : oldResult.getUses()) { + if (!expectedReductions.contains(use.getOwner())) + return false; + } + } + return true; +} + +bool canCommitBlockRewrite(const LayoutPermutationCandidate &candidate, + const IRMapping &mapping) { + for (Operation *operation : candidate.blockOperations) { + if (!operation || operation->getBlock() != candidate.block) + return false; + for (Value result : operation->getResults()) { + if (!mapping.contains(result)) + return false; + for (OpOperand &use : result.getUses()) { + if (use.getOwner()->getBlock() != candidate.block || + !llvm::is_contained(candidate.blockOperations, use.getOwner())) + return false; + } + } + } + return true; +} + +void eraseCreatedOperations(IRRewriter &rewriter, + ArrayRef created) { + for (Operation *operation : llvm::reverse(created)) { + if (operation) + rewriter.eraseOp(operation); + } +} + +LogicalResult applyCandidate(IRRewriter &rewriter, + const LayoutPermutationCandidate &candidate) { + FailureOr permutation = + Permutation::create(candidate.permutation); + if (failed(permutation)) + return failure(); + + scf::ForOp oldFor = candidate.loop; + if (!oldFor || !oldFor.getBody()) + return failure(); + + SmallVector created; + rewriter.setInsertionPoint(oldFor); + LayoutValueCloner externalValues(rewriter, *permutation, created); + std::optional> assertReplacements = + cloneExternalAssertConditions(rewriter, candidate.externalAssertPorts, + externalValues, *permutation); + if (!assertReplacements) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + rewriter.setInsertionPoint(oldFor); + for (Value value : candidate.externalTensorValues) { + if (!externalValues.clone(value)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + } + + SmallVector newInitArgs; + newInitArgs.reserve(oldFor.getInitArgs().size()); + for (Value init : oldFor.getInitArgs()) { + std::optional mapped = externalValues.get(init); + if (!mapped) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + newInitArgs.push_back(*mapped); + } + + auto newFor = rewriter.create( + oldFor.getLoc(), oldFor.getLowerBound(), oldFor.getUpperBound(), + oldFor.getStep(), newInitArgs); + created.push_back(newFor.getOperation()); + Block *newBody = newFor.getBody(); + // The generated SCF builder may leave a newly created body empty. Do not + // query getTerminator() in that state: Block intentionally asserts when it + // has not yet been marked as containing a terminator. + if (!newBody->empty()) + rewriter.eraseOp(&newBody->back()); + + IRMapping mapping; + Block *oldBody = oldFor.getBody(); + mapping.map(oldBody->getArgument(0), newBody->getArgument(0)); + for (unsigned index = 0; index < oldFor.getInitArgs().size(); ++index) + mapping.map(oldBody->getArgument(index + 1), + newBody->getArgument(index + 1)); + + rewriter.setInsertionPointToEnd(newBody); + for (Operation &operation : *oldBody) { + if (auto yield = dyn_cast(operation)) { + SmallVector operands; + operands.reserve(yield.getNumOperands()); + for (Value operand : yield.getOperands()) { + std::optional mapped = + mapLoopValue(operand, mapping, externalValues, *permutation); + if (!mapped) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + operands.push_back(*mapped); + } + rewriter.create(yield.getLoc(), operands); + continue; + } + + if (!cloneLoopOperation(rewriter, &operation, mapping, externalValues, + *permutation)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + } + + if (failed(mlir::verify(newFor.getOperation()))) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + SmallVector, 4> reductions; + for (const ReductionPort &port : candidate.reductionPorts) { + if (!port.reduce || port.forResultIndex >= newFor.getNumResults()) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + rewriter.setInsertionPoint(port.reduce); + std::optional rebuilt = + cloneReduction(rewriter, port.reduce, + newFor.getResult(port.forResultIndex), *permutation); + if (rebuilt) + created.push_back((*rebuilt).getOperation()); + if (!rebuilt || + (*rebuilt)->getResult(0).getType() != + port.reduce->getResult(0).getType() || + failed(mlir::verify((*rebuilt).getOperation()))) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + reductions.push_back({port.reduce, *rebuilt}); + } + + if (!verifyCreatedOperations(created) || + !canCommitLoopRewrite(candidate, oldFor, newFor)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + // All fallible work has completed. Keep the old assert operations and only + // change their conditions, then replace the old loop as one committed unit. + commitAssertConditionReplacements(rewriter, *assertReplacements); + DenseSet transformed(candidate.transformedResultIndices.begin(), + candidate.transformedResultIndices.end()); + for (auto [index, oldResult] : llvm::enumerate(oldFor.getResults())) { + if (transformed.contains(index)) + continue; + oldResult.replaceAllUsesWith(newFor.getResult(index)); + } + for (auto &pair : reductions) + pair.first->getResult(0).replaceAllUsesWith(pair.second->getResult(0)); + for (auto &pair : reductions) + rewriter.eraseOp(pair.first.getOperation()); + + rewriter.eraseOp(oldFor.getOperation()); + eraseDeadPureClosure(rewriter, candidate.retireOperations); + return success(); +} + +LogicalResult applyBlockCandidate(IRRewriter &rewriter, + const LayoutPermutationCandidate &candidate) { + if (!candidate.block || !candidate.anchor || + candidate.blockOperations.empty()) + return failure(); + FailureOr permutation = + Permutation::create(candidate.permutation); + if (failed(permutation)) + return failure(); + + SmallVector created; + rewriter.setInsertionPoint(candidate.anchor); + LayoutValueCloner externalValues(rewriter, *permutation, created); + std::optional> assertReplacements = + cloneExternalAssertConditions(rewriter, candidate.externalAssertPorts, + externalValues, *permutation); + if (!assertReplacements) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + rewriter.setInsertionPoint(candidate.anchor); + for (Value value : candidate.externalTensorValues) { + if (!externalValues.clone(value)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + } + + IRMapping mapping; + for (Operation *operation : candidate.blockOperations) { + if (!operation || operation->getBlock() != candidate.block) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + rewriter.setInsertionPoint(operation); + std::optional rebuilt = cloneLoopOperation( + rewriter, operation, mapping, externalValues, *permutation, &created); + if (!rebuilt || failed(mlir::verify(*rebuilt))) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + } + + // Complete every validation before changing an old use. The freshly cloned + // operations are inserted in the same Block, so the preflight must target + // only the original selected closure, not merely every operation in Block. + if (!verifyCreatedOperations(created) || + !canCommitBlockRewrite(candidate, mapping)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + commitAssertConditionReplacements(rewriter, *assertReplacements); + + // Rewire only the original selected closure. The freshly cloned operations + // are inserted in the same Block, so checking the Block alone would also + // rewrite their already-correct operands. matchBlockCandidate has proven + // that every old full-rank result use stays inside this selected component; + // no layout value is allowed to escape through a Region port. + for (Operation *operation : candidate.blockOperations) { + for (Value result : operation->getResults()) { + Value replacement = mapping.lookup(result); + result.replaceUsesWithIf(replacement, [&](OpOperand &use) { + return use.getOwner()->getBlock() == candidate.block && + llvm::is_contained(candidate.blockOperations, use.getOwner()); + }); + } + } + + // The preflight above proves each old result is now dead after consumers are + // rewired, so no fallible path remains after the assert conditions commit. + for (Operation *operation : llvm::reverse(candidate.blockOperations)) { + rewriter.eraseOp(operation); + } + eraseDeadPureClosure(rewriter, candidate.retireOperations); + return success(); +} + +class LayoutPermutationPlan final : public RewritePlan { +public: + LayoutPermutationPlan(LayoutPermutationCandidate candidate, unsigned epoch) + : loop(candidate.loop), block(candidate.block), anchor(candidate.anchor), + permutation(std::move(candidate.permutation)), epoch(epoch) {} + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::LoadStoreTranspose; + } + + unsigned getBenefit() const override { return 1; } + + Operation *getAnchor() const override { return anchor; } + + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (context.getEpoch() != epoch || !anchor || (!loop && !block) || + (loop && block)) + return failure(); + triton::FuncOp function = anchor->getParentOfType(); + if (!function || function != context.getFunction()) + return failure(); + std::optional current = + loop ? matchLoopCandidate(loop) : matchBlockCandidate(block); + if (!current || current->anchor != anchor || + !haveSamePermutation(current->permutation, permutation)) + return failure(); + return success(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + std::optional candidate = + loop ? matchLoopCandidate(loop) : matchBlockCandidate(block); + if (!candidate || candidate->anchor != anchor || + !haveSamePermutation(candidate->permutation, permutation)) + return failure(); + return loop ? applyCandidate(rewriter, *candidate) + : applyBlockCandidate(rewriter, *candidate); + } + +private: + scf::ForOp loop; + Block *block; + Operation *anchor; + SmallVector permutation; + unsigned epoch; +}; + +class LoadStoreTransposeRule final : public GraphOptimizationRule { +public: + GraphOptimizationRuleId getId() const override { + return GraphOptimizationRuleId::LoadStoreTranspose; + } + + AnalysisRequirement getAnalysisRequirements() const override { + return AnalysisRequirement::None; + } + + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + context.getFunction().walk([&](scf::ForOp loop) { + std::optional candidate = + matchLoopCandidate(loop); + if (candidate) { + LLVM_DEBUG({ + llvm::dbgs() << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast(getId()) << " (" + << getGraphOptimizationRuleName(getId()) << ") at loop " + << candidate->anchor->getLoc() << ": perm=["; + llvm::interleaveComma(candidate->permutation, llvm::dbgs()); + llvm::dbgs() << "] endpoints=" << candidate->endpointCount << "\n"; + }); + plans.push_back(std::make_unique( + std::move(*candidate), context.getEpoch())); + } + }); + + llvm::SetVector blocks; + auto collectBlocks = [&](Operation *operation) { + for (Region ®ion : operation->getRegions()) { + for (Block &block : region) + blocks.insert(&block); + } + }; + collectBlocks(context.getFunction().getOperation()); + context.getFunction().walk( + [&](Operation *operation) { collectBlocks(operation); }); + for (Block *block : blocks) { + std::optional candidate = + matchBlockCandidate(block); + if (candidate) { + LLVM_DEBUG({ + llvm::dbgs() << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast(getId()) << " (" + << getGraphOptimizationRuleName(getId()) << ") at block " + << candidate->anchor->getLoc() << ": perm=["; + llvm::interleaveComma(candidate->permutation, llvm::dbgs()); + llvm::dbgs() << "] endpoints=" << candidate->endpointCount << "\n"; + }); + plans.push_back(std::make_unique( + std::move(*candidate), context.getEpoch())); + } + } + return success(); + } +}; + +} // namespace + +std::unique_ptr cfg::createLoadStoreTransposeRule() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/PersistentTaskStripMiningRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/PersistentTaskStripMiningRule.cpp new file mode 100644 index 0000000000..7ec81a7ab8 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/PersistentTaskStripMiningRule.cpp @@ -0,0 +1,961 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonMemoryAccess/MemoryAccessTags.h" +#include "TritonToGraph/EntryArgPointerAliasAnalysis.h" +#include "TritonToGraph/GraphOptimizationRule.h" +#include "TritonToGraph/IndependentRowReductionAnalysis.h" +#include "TritonToGraph/ProgramAxisDependenceAnalysis.h" +#include "TritonToGraph/ProgramGridTransform.h" +#include "TritonToGraph/ResourceCostModel.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/CallInterfaces.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/Passes.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +constexpr int32_t kTokenAxis = 0; +constexpr llvm::StringLiteral kPersistentTaskStripMiningMarkerAttr = + "hacc.persistent_task_strip_mining"; +constexpr unsigned kSingleMomentBlockT = 4; +constexpr unsigned kTwoMomentBlockT = 64; + +struct PTSMCandidate { + triton::FuncOp function; + Operation *anchor = nullptr; + unsigned blockT = 0; + unsigned existingTransformCount = 0; + IndependentRowReductionKind reductionKind = + IndependentRowReductionKind::Other; + CandidateEvaluation evaluation; +}; + +LogicalResult +materializePersistentTaskStripMining(triton::FuncOp function, + const PTSMCandidate &candidate); +LogicalResult runProgramMappingCandidateCleanup(ModuleOp module); +std::optional +estimateFinalPersistentPeak(triton::FuncOp function, + const PTSMCandidate &candidate); + +struct MappedValue { + Value value; + bool tensorized = false; +}; + +bool isPublicEntry(triton::FuncOp function) { + auto visibility = function->getAttrOfType("sym_visibility"); + return !visibility || visibility.getValue() == "public"; +} + +bool isOnlyPublicEntry(ModuleOp module, triton::FuncOp function) { + unsigned entries = 0; + for (triton::FuncOp candidate : module.getOps()) { + if (!isPublicEntry(candidate)) + continue; + ++entries; + if (candidate != function) + return false; + } + return entries == 1; +} + +bool hasDirectCall(triton::FuncOp function) { + bool hasCall = false; + function.walk([&](Operation *operation) { + if (isa(operation)) + hasCall = true; + }); + return hasCall; +} + +bool hasDisallowedEffectOrControlFlow(triton::FuncOp function) { + bool disallowed = false; + function.walk([&](Operation *operation) { + if (operation == function.getOperation()) + return; + const StringRef name = operation->getName().getStringRef(); + if (isa(operation) || name.starts_with("scf.") || + name.contains_insensitive("atomic") || + name.contains_insensitive("barrier") || + name.contains_insensitive("random") || + name.contains_insensitive("rng") || + name.contains_insensitive("print")) { + disallowed = true; + return; + } + if (operation->getNumRegions() != 0 && + !isa(operation)) + disallowed = true; + }); + return disallowed; +} + +std::optional findOnlyProgramId(triton::FuncOp function, + int32_t axis) { + SmallVector ids; + function.walk([&](triton::GetProgramIdOp pid) { + if (pid.getAxisAsInt() == axis) + ids.push_back(pid); + }); + if (ids.size() != 1) + return std::nullopt; + return ids.front(); +} + +bool hasVoidReturn(triton::FuncOp function) { + if (function->getNumRegions() != 1 || function->getRegion(0).empty()) + return false; + Block &block = function->getRegion(0).front(); + auto result = dyn_cast(block.getTerminator()); + return result && result.getNumOperands() == 0; +} + +bool hasPersistentRowReductionForm(triton::FuncOp function, + IndependentRowReductionKind expected) { + return classifyIndependentRowReduction(function) == expected; +} + +bool hasDisjointWriteReadRoots( + triton::FuncOp function, const ProgramAxisDependence &dependence, + const EntryArgPointerAliasAnalysis &entryPointerAliases) { + SmallVector loads; + function.walk([&](triton::LoadOp load) { loads.push_back(load); }); + for (const StoreAddressDependence &store : dependence.stores) { + auto storeOp = dyn_cast_or_null(store.store); + if (!storeOp) + return false; + for (triton::LoadOp load : loads) { + if (entryPointerAliases.classify(storeOp.getPtr(), load.getPtr()) != + EntryArgPointerRelation::DistinctEntryRoots) + return false; + } + } + return true; +} + +std::optional +getComposableLaunchContract(ModuleOp module) { + Attribute attribute = module->getAttr(kProgramGridTransformsAttr); + if (!attribute) + return ProgramGridTransformContract{}; + FailureOr parsed = + parseProgramGridTransformContract(attribute); + if (failed(parsed)) + return std::nullopt; + for (const ProgramGridTransform &transform : parsed->transforms) { + if (transform.axis == kTokenAxis || transform.persistentCoverage || + transform.gridStrideAbiVerified) + return std::nullopt; + } + return *parsed; +} + +CandidateCost buildResourceCandidate(const PTSMCandidate &candidate, + const ProgramAxisDependence &dependence, + const LiveByteEstimate &baselineLiveBytes, + const LiveByteEstimate *finalLiveBytes) { + CandidateCost cost; + cost.plan.tensorizeFactor = 1; + cost.plan.blockT = candidate.blockT; + cost.plan.staticAxisFusionFactor = 1; + cost.plan.stableId = (llvm::Twine("persistent-task-strip-mining.b") + + llvm::Twine(candidate.blockT)) + .str(); + cost.launchesBefore = 1; + cost.launchesAfter = 1; + cost.storeCountBefore = dependence.stores.size(); + cost.storeCountAfter = dependence.stores.size(); + cost.addressCalculationsBefore = dependence.dependenceClosure.size(); + cost.addressCalculationsAfter = dependence.dependenceClosure.size(); + cost.persistent = true; + + if (!baselineLiveBytes.known || !finalLiveBytes || !finalLiveBytes->known) { + const ResourceCostRejectReason reason = + !baselineLiveBytes.known + ? baselineLiveBytes.reason + : (finalLiveBytes ? finalLiveBytes->reason + : ResourceCostRejectReason::UnknownResource); + cost.hasDynamicShape = reason == ResourceCostRejectReason::DynamicShape; + cost.hasUnknownResource = !cost.hasDynamicShape; + return cost; + } + cost.hasPeakLiveBytes = true; + cost.baselinePeakLiveBytes = baselineLiveBytes.peakLiveBytes; + cost.estimatedPeakLiveBytes = finalLiveBytes->peakLiveBytes; + return cost; +} + +std::optional +analyzeCandidate(GraphOptimizationContext &context, bool emitRejectRemark, + std::optional requestedBlockT = std::nullopt, + bool deferIntermediateResourceRejection = false) { + triton::FuncOp function = context.getFunction(); + ModuleOp module = function->getParentOfType(); + const IndependentRowReductionKind reductionKind = + classifyIndependentRowReduction(function); + if (!module || module->hasAttr(kPersistentTaskStripMiningMarkerAttr) || + !isPublicEntry(function) || !isOnlyPublicEntry(module, function) || + reductionKind == IndependentRowReductionKind::Other || + !hasVoidReturn(function) || hasDirectCall(function) || + hasDisallowedEffectOrControlFlow(function)) + return std::nullopt; + + std::optional launchContract = + getComposableLaunchContract(module); + if (!launchContract) + return std::nullopt; + const bool hasIAT = launchContract->transforms.size() == 1; + if (launchContract->transforms.size() > 1 || + (hasIAT && reductionKind != IndependentRowReductionKind::SingleMoment) || + (!hasIAT && reductionKind != IndependentRowReductionKind::TwoMoment)) + return std::nullopt; + const unsigned expectedBlockT = + hasIAT ? kSingleMomentBlockT : kTwoMomentBlockT; + if (requestedBlockT && *requestedBlockT != expectedBlockT) + return std::nullopt; + std::optional tokenPid = + findOnlyProgramId(function, kTokenAxis); + if (!tokenPid) + return std::nullopt; + + const ProgramAxisDependence &dependence = + context.getProgramAxisDependenceAnalysis().get(kTokenAxis); + if (!dependence.isProgramMappingTransformCandidate() || + !hasDisjointWriteReadRoots(function, dependence, + context.getEntryArgPointerAliasAnalysis())) + return std::nullopt; + + const LiveByteEstimate &liveBytes = + context.getResourceCostAnalysis().getLiveByteEstimate(); + PTSMCandidate candidate; + candidate.function = function; + candidate.anchor = tokenPid->getOperation(); + candidate.blockT = expectedBlockT; + candidate.existingTransformCount = launchContract->transforms.size(); + candidate.reductionKind = reductionKind; + std::optional finalLiveBytes = + estimateFinalPersistentPeak(function, candidate); + candidate.evaluation = context.getResourceCostAnalysis().evaluate( + buildResourceCandidate(candidate, dependence, liveBytes, + finalLiveBytes ? &*finalLiveBytes : nullptr)); + const bool mayDeferResourceRejection = + requestedBlockT && deferIntermediateResourceRejection; + if (!candidate.evaluation.accepted && !mayDeferResourceRejection) + return std::nullopt; + if (emitRejectRemark) + emitCandidateRemark(tokenPid->getOperation(), candidate.evaluation); + return candidate; +} + +LogicalResult +applyPersistentCandidateToSandbox(ModuleOp module, triton::FuncOp function, + const PTSMCandidate &candidate) { + if (!module || module->hasAttr(kPersistentTaskStripMiningMarkerAttr)) + return failure(); + std::optional existing = + getComposableLaunchContract(module); + if (!existing || + existing->transforms.size() != candidate.existingTransformCount) + return failure(); + if (failed(materializePersistentTaskStripMining(function, candidate))) + return failure(); + + ProgramGridTransformContract contract = *existing; + contract.transforms.push_back(ProgramGridTransform{ + static_cast(contract.transforms.size()), kTokenAxis, + static_cast(candidate.blockT), true, true}); + if (failed(setProgramGridTransformContract(module, contract))) + return failure(); + module->setAttr(kPersistentTaskStripMiningMarkerAttr, + UnitAttr::get(module.getContext())); + return mlir::verify(module.getOperation()); +} + +bool sameCandidate(const PTSMCandidate &lhs, const PTSMCandidate &rhs) { + return lhs.function == rhs.function && lhs.blockT == rhs.blockT && + lhs.existingTransformCount == rhs.existingTransformCount && + lhs.reductionKind == rhs.reductionKind; +} + +std::optional getBatchedTensorType(Type originalType, + unsigned blockT) { + Type elementType = originalType; + SmallVector shape; + if (auto tensor = dyn_cast(originalType)) { + if (!tensor.hasStaticShape()) + return std::nullopt; + elementType = tensor.getElementType(); + shape.assign(tensor.getShape().begin(), tensor.getShape().end()); + } + shape.insert(shape.begin(), blockT); + return RankedTensorType::get(shape, elementType); +} + +std::optional +getSameShapeTensorType(Type originalType, ArrayRef shape) { + Type elementType = originalType; + if (auto tensor = dyn_cast(originalType)) { + if (!tensor.hasStaticShape()) + return std::nullopt; + elementType = tensor.getElementType(); + } + return RankedTensorType::get(shape, elementType); +} + +bool hasSameShape(RankedTensorType lhs, RankedTensorType rhs) { + return lhs.getRank() == rhs.getRank() && + llvm::equal(lhs.getShape(), rhs.getShape()); +} + +Value makeZero(IRRewriter &rewriter, Location loc, Type type) { + auto tensor = dyn_cast(type); + Type elementType = tensor ? tensor.getElementType() : type; + if (auto integer = dyn_cast(elementType)) { + TypedAttr zero = IntegerAttr::get(integer, 0); + return tensor ? rewriter.create( + loc, type, DenseElementsAttr::get(tensor, zero)) + : rewriter.create(loc, type, zero); + } + if (auto floating = dyn_cast(elementType)) { + TypedAttr zero = FloatAttr::get(floating, 0.0); + return tensor ? rewriter.create( + loc, type, DenseElementsAttr::get(tensor, zero)) + : rewriter.create(loc, type, zero); + } + return Value(); +} + +Value alignTensor(IRRewriter &rewriter, Location loc, Value source, + RankedTensorType target, bool sourceIsBatched, + unsigned blockT) { + auto sourceType = dyn_cast(source.getType()); + if (!sourceType || !sourceType.hasStaticShape() || + sourceType.getElementType() != target.getElementType() || + target.getRank() == 0 || target.getShape().front() != blockT) + return Value(); + if (sourceIsBatched && + (sourceType.getRank() == 0 || sourceType.getShape().front() != blockT)) + return Value(); + + SmallVector sourceToTarget(sourceType.getRank(), -1); + unsigned sourceStart = 0; + unsigned targetStart = 1; + if (sourceIsBatched) { + sourceToTarget[0] = 0; + sourceStart = 1; + } + std::function mapTail = + [&](unsigned sourceIndex, unsigned targetIndex) -> bool { + if (sourceIndex == sourceType.getRank()) + return true; + for (unsigned candidate = targetIndex; candidate < target.getRank(); + ++candidate) { + const int64_t sourceDim = sourceType.getShape()[sourceIndex]; + const int64_t targetDim = target.getShape()[candidate]; + if (sourceDim != targetDim && sourceDim != 1) + continue; + sourceToTarget[sourceIndex] = candidate; + if (mapTail(sourceIndex + 1, candidate + 1)) + return true; + sourceToTarget[sourceIndex] = -1; + } + return false; + }; + if (!mapTail(sourceStart, targetStart)) + return Value(); + + SmallVector targetMapped(target.getRank(), false); + for (unsigned sourceIndex = 0; sourceIndex < sourceType.getRank(); + ++sourceIndex) { + const int64_t targetIndex = sourceToTarget[sourceIndex]; + if (targetIndex < 0 || targetMapped[targetIndex]) + return Value(); + targetMapped[targetIndex] = true; + } + Value current = source; + int64_t currentAxis = 0; + for (int64_t targetIndex = 0; targetIndex < target.getRank(); ++targetIndex) { + if (!targetMapped[targetIndex]) + current = + rewriter.create(loc, current, currentAxis); + ++currentAxis; + } + auto currentType = dyn_cast(current.getType()); + if (!currentType || currentType.getElementType() != target.getElementType() || + currentType.getRank() != target.getRank()) + return Value(); + for (auto [currentDim, targetDim] : + llvm::zip(currentType.getShape(), target.getShape())) + if (currentDim != targetDim && currentDim != 1) + return Value(); + return hasSameShape(currentType, target) + ? current + : rewriter.create(loc, target, current); +} + +LogicalResult +materializePersistentTaskStripMining(triton::FuncOp function, + const PTSMCandidate &candidate) { + if (!hasVoidReturn(function) || + !hasPersistentRowReductionForm(function, candidate.reductionKind) || + hasDisallowedEffectOrControlFlow(function) || + failed(addProgramGridHiddenExtentArguments(function))) + return failure(); + Value originalTokenExtent = + function.getArgument(function.getNumArguments() - 2); + auto originalTokenExtentType = + dyn_cast(originalTokenExtent.getType()); + if (!originalTokenExtentType || originalTokenExtentType.getWidth() != 32) + return failure(); + std::optional tokenPid = + findOnlyProgramId(function, kTokenAxis); + if (!tokenPid) + return failure(); + + ProgramAxisDependenceAnalysis axisAnalysis(function); + const ProgramAxisDependence &dependence = axisAnalysis.get(kTokenAxis); + if (!dependence.isProgramMappingTransformCandidate()) + return failure(); + DenseSet tokenClosure; + for (Operation *operation : dependence.dependenceClosure) + tokenClosure.insert(operation); + + Block &block = function.getBody().front(); + SmallVector originals; + originals.reserve(block.getOperations().size()); + for (Operation &operation : block.without_terminator()) + originals.push_back(&operation); + + IRRewriter rewriter(function.getContext()); + rewriter.setInsertionPoint(originals.empty() ? block.getTerminator() + : originals.front()); + DenseMap values; + auto lookup = [&](Value value) -> MappedValue { + auto it = values.find(value); + return it == values.end() ? MappedValue{value, false} : it->second; + }; + auto mapResults = [&](Operation *original, Operation *replacement, + bool tensorized) -> bool { + if (!original || !replacement || + original->getNumResults() != replacement->getNumResults()) + return false; + for (auto [oldResult, newResult] : + llvm::zip(original->getResults(), replacement->getResults())) + values[oldResult] = {newResult, tensorized}; + return true; + }; + + Value physicalPid; + for (Operation *operation : originals) { + if (operation == tokenPid->getOperation()) { + Operation *replacement = rewriter.clone(*operation); + if (!replacement || replacement->getNumResults() != 1) + return failure(); + physicalPid = replacement->getResult(0); + continue; + } + if (tokenClosure.contains(operation)) + continue; + IRMapping mapping; + for (Value operand : operation->getOperands()) + mapping.map(operand, lookup(operand).value); + Operation *replacement = rewriter.clone(*operation, mapping); + if (!mapResults(operation, replacement, false)) + return failure(); + } + if (!physicalPid) + return failure(); + auto pidType = dyn_cast(physicalPid.getType()); + if (!pidType || pidType.getWidth() != 32) + return failure(); + + const Location loc = tokenPid->getLoc(); + Value actualPrograms = + rewriter.create(loc, triton::ProgramIDDim::X); + if (actualPrograms.getType() != physicalPid.getType()) + return failure(); + Type i64 = rewriter.getI64Type(); + Value originalTokenExtentI64 = + rewriter.create(loc, i64, originalTokenExtent); + Value blockT64 = + rewriter.create(loc, candidate.blockT, 64); + Value ceilNumerator = rewriter.create( + loc, originalTokenExtentI64, + rewriter.create(loc, candidate.blockT - 1, 64)); + Value logicalTiles = + rewriter.create(loc, ceilNumerator, blockT64); + Value physicalPidI64 = rewriter.create(loc, i64, physicalPid); + Value actualProgramsI64 = + rewriter.create(loc, i64, actualPrograms); + auto outerLoop = rewriter.create(loc, physicalPidI64, + logicalTiles, actualProgramsI64); + Block *loopBody = outerLoop.getBody(); + if (!loopBody || !loopBody->mightHaveTerminator()) + return failure(); + rewriter.setInsertionPointToStart(loopBody); + + Value tokenBaseI64 = rewriter.create( + loc, outerLoop.getInductionVar(), blockT64); + Value tokenBase = + rewriter.create(loc, pidType, tokenBaseI64); + auto tokenType = RankedTensorType::get( + {static_cast(candidate.blockT)}, physicalPid.getType()); + Value tokenRange = rewriter.create( + loc, tokenType, 0, static_cast(candidate.blockT)); + Value tokenBaseSplat = + rewriter.create(loc, tokenType, tokenBase); + Value logicalTokens = + rewriter.create(loc, tokenBaseSplat, tokenRange); + auto tokenI64Type = + RankedTensorType::get({static_cast(candidate.blockT)}, i64); + Value logicalTokensI64 = + rewriter.create(loc, tokenI64Type, logicalTokens); + Value tokenExtentSplat = rewriter.create( + loc, tokenI64Type, originalTokenExtentI64); + auto tokenTailMask = rewriter.create( + loc, arith::CmpIPredicate::ult, logicalTokensI64, tokenExtentSplat); + tokenTailMask->setAttr( + mlir::triton::memory_access::PTSMRuntimeExtentUnsignedMaskTAG, + rewriter.getUnitAttr()); + values[tokenPid->getResult()] = {logicalTokens, true}; + + auto copyMissingAttrs = [](Operation *from, Operation *to) { + for (NamedAttribute attribute : from->getAttrs()) + if (!to->hasAttr(attribute.getName())) + to->setAttr(attribute.getName(), attribute.getValue()); + }; + auto liftOperandTo = [&](Value original, RankedTensorType target) -> Value { + MappedValue mapped = lookup(original); + if (mapped.tensorized) + return alignTensor(rewriter, original.getLoc(), mapped.value, target, + true, candidate.blockT); + if (isa(mapped.value.getType())) + return alignTensor(rewriter, original.getLoc(), mapped.value, target, + false, candidate.blockT); + if (mapped.value.getType() != target.getElementType()) + return Value(); + return rewriter.create(original.getLoc(), target, + mapped.value); + }; + auto tailMaskFor = [&](Location location, RankedTensorType target) -> Value { + return alignTensor(rewriter, location, tokenTailMask, target, true, + candidate.blockT); + }; + auto createElementwise = [&](Operation *operation) -> bool { + if (operation->getNumResults() == 0) + return false; + const bool isReshape = isa(operation); + if (!operation->hasTrait() && !isReshape) + return false; + if (isReshape) { + if (operation->getNumOperands() != 1) + return false; + auto input = + dyn_cast(operation->getOperand(0).getType()); + auto output = + dyn_cast(operation->getResult(0).getType()); + if (!input || !output || !hasSameShape(input, output)) + return false; + } + std::optional firstResult = getBatchedTensorType( + operation->getResult(0).getType(), candidate.blockT); + if (!firstResult) + return false; + SmallVector resultTypes; + for (Type result : operation->getResultTypes()) { + std::optional lifted = + getBatchedTensorType(result, candidate.blockT); + if (!lifted) + return false; + resultTypes.push_back(*lifted); + } + SmallVector operands; + for (Value operand : operation->getOperands()) { + std::optional operandType = + getSameShapeTensorType(operand.getType(), firstResult->getShape()); + if (!operandType) + return false; + Value lifted = liftOperandTo(operand, *operandType); + if (!lifted) + return false; + operands.push_back(lifted); + } + Operation *replacement = rewriter.create( + operation->getLoc(), operation->getName().getIdentifier(), operands, + resultTypes, operation->getAttrs()); + return mapResults(operation, replacement, true); + }; + + for (Operation *operation : originals) { + if (!tokenClosure.contains(operation) || + operation == tokenPid->getOperation()) + continue; + rewriter.setInsertionPoint(loopBody->getTerminator()); + if (auto splat = dyn_cast(operation)) { + std::optional target = + getBatchedTensorType(splat.getType(), candidate.blockT); + if (!target) + return failure(); + Value value = liftOperandTo(splat.getSrc(), *target); + if (!value) + return failure(); + values[splat.getResult()] = {value, true}; + continue; + } + if (auto expand = dyn_cast(operation)) { + std::optional sourceTarget = + getBatchedTensorType(expand.getSrc().getType(), candidate.blockT); + std::optional resultTarget = + getBatchedTensorType(expand.getType(), candidate.blockT); + if (!sourceTarget || !resultTarget) + return failure(); + Value source = liftOperandTo(expand.getSrc(), *sourceTarget); + if (!source) + return failure(); + Value result = rewriter.create( + operation->getLoc(), source, expand.getAxis() + 1); + if (result.getType() != *resultTarget) + return failure(); + values[expand.getResult()] = {result, true}; + continue; + } + if (auto broadcast = dyn_cast(operation)) { + std::optional sourceTarget = + getBatchedTensorType(broadcast.getSrc().getType(), candidate.blockT); + std::optional resultTarget = + getBatchedTensorType(broadcast.getType(), candidate.blockT); + if (!sourceTarget || !resultTarget) + return failure(); + Value source = liftOperandTo(broadcast.getSrc(), *sourceTarget); + if (!source) + return failure(); + Value result = rewriter.create( + operation->getLoc(), *resultTarget, source); + values[broadcast.getResult()] = {result, true}; + continue; + } + if (auto reduce = dyn_cast(operation)) { + if (reduce.getSrcs().empty()) + return failure(); + SmallVector sources; + for (Value source : reduce.getSrcs()) { + std::optional target = + getBatchedTensorType(source.getType(), candidate.blockT); + if (!target) + return failure(); + Value lifted = liftOperandTo(source, *target); + if (!lifted) + return failure(); + sources.push_back(lifted); + } + auto replacement = rewriter.create( + operation->getLoc(), sources, reduce.getAxis() + 1); + rewriter.cloneRegionBefore(reduce.getCombineOp(), + replacement.getCombineOp(), + replacement.getCombineOp().end()); + copyMissingAttrs(operation, replacement.getOperation()); + if (!mapResults(operation, replacement.getOperation(), true)) + return failure(); + continue; + } + if (auto scan = dyn_cast(operation)) { + if (scan.getSrcs().empty()) + return failure(); + SmallVector sources; + for (Value source : scan.getSrcs()) { + std::optional target = + getBatchedTensorType(source.getType(), candidate.blockT); + if (!target) + return failure(); + Value lifted = liftOperandTo(source, *target); + if (!lifted) + return failure(); + sources.push_back(lifted); + } + auto replacement = rewriter.create( + operation->getLoc(), sources, + static_cast(scan.getAxis() + 1), scan.getReverse()); + rewriter.cloneRegionBefore(scan.getCombineOp(), + replacement.getCombineOp(), + replacement.getCombineOp().end()); + copyMissingAttrs(operation, replacement.getOperation()); + if (!mapResults(operation, replacement.getOperation(), true)) + return failure(); + continue; + } + if (auto load = dyn_cast(operation)) { + std::optional pointerType = + getBatchedTensorType(load.getPtr().getType(), candidate.blockT); + if (!pointerType) + return failure(); + Value pointer = liftOperandTo(load.getPtr(), *pointerType); + auto maskType = + RankedTensorType::get(pointerType->getShape(), rewriter.getI1Type()); + Value mask = + load.getMask() ? liftOperandTo(load.getMask(), maskType) : Value(); + Value tailMask = tailMaskFor(operation->getLoc(), maskType); + if (!pointer || !tailMask) + return failure(); + mask = mask ? rewriter.create(operation->getLoc(), mask, + tailMask) + : tailMask; + std::optional resultType = getSameShapeTensorType( + load.getResult().getType(), pointerType->getShape()); + if (!resultType) + return failure(); + Value other = load.getOther() + ? liftOperandTo(load.getOther(), *resultType) + : makeZero(rewriter, operation->getLoc(), *resultType); + if (!other) + return failure(); + auto replacement = rewriter.create( + operation->getLoc(), pointer, mask, other, load.getBoundaryCheck(), + load.getPadding(), load.getCache(), load.getEvict(), + load.getIsVolatile()); + copyMissingAttrs(operation, replacement.getOperation()); + if (!mapResults(operation, replacement.getOperation(), true)) + return failure(); + continue; + } + if (auto store = dyn_cast(operation)) { + std::optional pointerType = + getBatchedTensorType(store.getPtr().getType(), candidate.blockT); + if (!pointerType) + return failure(); + Value pointer = liftOperandTo(store.getPtr(), *pointerType); + std::optional valueType = getSameShapeTensorType( + store.getValue().getType(), pointerType->getShape()); + if (!valueType) + return failure(); + Value value = liftOperandTo(store.getValue(), *valueType); + auto maskType = + RankedTensorType::get(pointerType->getShape(), rewriter.getI1Type()); + Value mask = + store.getMask() ? liftOperandTo(store.getMask(), maskType) : Value(); + Value tailMask = tailMaskFor(operation->getLoc(), maskType); + if (!pointer || !value || !tailMask) + return failure(); + mask = mask ? rewriter.create(operation->getLoc(), mask, + tailMask) + : tailMask; + auto replacement = rewriter.create( + operation->getLoc(), pointer, value, mask, store.getBoundaryCheck(), + store.getCache(), store.getEvict()); + copyMissingAttrs(operation, replacement.getOperation()); + continue; + } + if (!createElementwise(operation)) + return failure(); + } + + for (Operation *operation : llvm::reverse(originals)) { + operation->dropAllUses(); + rewriter.eraseOp(operation); + } + return mlir::verify(function.getOperation()); +} + +LogicalResult runProgramMappingCandidateCleanup(ModuleOp module) { + PassManager cleanup(module.getContext(), module.getOperationName()); + cleanup.addPass(createCanonicalizerPass()); + cleanup.addPass(createCSEPass()); + cleanup.addPass(createLoopInvariantCodeMotionPass()); + cleanup.addPass(createCanonicalizerPass()); + cleanup.addPass(createCSEPass()); + return cleanup.run(module); +} + +std::optional +estimateFinalPersistentPeak(triton::FuncOp function, + const PTSMCandidate &candidate) { + ModuleOp sandbox = ModuleOp::create(function.getLoc()); + sandbox.getBody()->push_back(function->clone()); + auto clonedFunction = dyn_cast(&sandbox.getBody()->front()); + if (!clonedFunction || + failed(materializePersistentTaskStripMining(clonedFunction, candidate)) || + failed(runProgramMappingCandidateCleanup(sandbox)) || + failed(mlir::verify(sandbox.getOperation()))) + return std::nullopt; + LiveByteEstimate estimate = + estimatePeakLiveBytes(clonedFunction.getOperation()); + return estimate.known ? std::optional(std::move(estimate)) + : std::nullopt; +} + +class PersistentTaskStripMiningPlan final : public RewritePlan { +public: + PersistentTaskStripMiningPlan(PTSMCandidate candidate, unsigned epoch) + : candidate(std::move(candidate)), epoch(epoch) {} + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::PersistentTaskStripMining; + } + unsigned getBenefit() const override { + const int64_t score = candidate.evaluation.benefitScore; + if (score <= 0) + return 1; + return static_cast(std::min( + score, static_cast(std::numeric_limits::max()))); + } + Operation *getAnchor() const override { return candidate.anchor; } + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (context.getFunction() != candidate.function) + return failure(); + std::optional current = analyzeCandidate(context, false); + return current && sameCandidate(candidate, *current) ? success() + : failure(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + (void)rewriter; + ModuleOp module = candidate.function->getParentOfType(); + if (!module || module->hasAttr(kPersistentTaskStripMiningMarkerAttr)) + return failure(); + std::optional existing = + getComposableLaunchContract(module); + if (!existing || + existing->transforms.size() != candidate.existingTransformCount) + return failure(); + + ModuleOp sandbox = ModuleOp::create(candidate.function.getLoc()); + if (Attribute oldContract = module->getAttr(kProgramGridTransformsAttr)) + sandbox->setAttr(kProgramGridTransformsAttr, oldContract); + sandbox.getBody()->push_back(candidate.function->clone()); + auto clonedFunction = dyn_cast(&sandbox.getBody()->front()); + if (!clonedFunction || failed(applyPersistentCandidateToSandbox( + sandbox, clonedFunction, candidate))) + return failure(); + + Attribute transforms = sandbox->getAttr(kProgramGridTransformsAttr); + if (!transforms || failed(parseProgramGridTransformContract(transforms)) || + !hasProgramGridHiddenExtentArguments(clonedFunction)) + return failure(); + if (failed(commitProgramGridFunctionFromSandbox(candidate.function, + clonedFunction))) + return failure(); + module->setAttr(kProgramGridTransformsAttr, transforms); + module->setAttr(kPersistentTaskStripMiningMarkerAttr, + sandbox->getAttr(kPersistentTaskStripMiningMarkerAttr)); + return success(); + } + +private: + PTSMCandidate candidate; + unsigned epoch; +}; + +class PersistentTaskStripMiningRule final : public GraphOptimizationRule { +public: + explicit PersistentTaskStripMiningRule(bool enabledForCompileMode) + : enabledForCompileMode(enabledForCompileMode) {} + + GraphOptimizationRuleId getId() const override { + return GraphOptimizationRuleId::PersistentTaskStripMining; + } + AnalysisRequirement getAnalysisRequirements() const override { + return AnalysisRequirement::EntryArgPointerAlias | + AnalysisRequirement::ProgramAxisDependence | + AnalysisRequirement::ResourceCost; + } + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + if (!enabledForCompileMode) + return success(); + std::optional candidate = analyzeCandidate(context, true); + if (!candidate) + return success(); + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast(getId()) << " (" + << getGraphOptimizationRuleName(getId()) << ") in @" + << candidate->function.getName() << ": dynamic-token-extent" + << " block_t=" << candidate->blockT << "\n"); + plans.push_back(std::make_unique( + std::move(*candidate), context.getEpoch())); + return success(); + } + +private: + bool enabledForCompileMode; +}; + +} // namespace + +LogicalResult cfg::materializePersistentTaskStripMiningCandidate( + ModuleOp module, triton::FuncOp function, const ResourceSnapshot &resources, + unsigned requestedBlockT, CandidateEvaluation *evaluation, + bool deferIntermediateResourceRejection) { + if (!module || !function || function->getParentOfType() != module || + requestedBlockT == 0) + return failure(); + GraphOptimizationContext context(function, resources); + const AnalysisRequirement requirements = + AnalysisRequirement::EntryArgPointerAlias | + AnalysisRequirement::ProgramAxisDependence | + AnalysisRequirement::ResourceCost; + if (failed(context.ensure(requirements))) + return failure(); + std::optional candidate = analyzeCandidate( + context, false, requestedBlockT, deferIntermediateResourceRejection); + if (!candidate || + failed(applyPersistentCandidateToSandbox(module, function, *candidate))) + return failure(); + if (evaluation) + *evaluation = candidate->evaluation; + return success(); +} + +std::unique_ptr cfg::createPersistentTaskStripMiningRule( + const PersistentTaskStripMiningRuleOptions &options) { + return std::make_unique( + options.enabledForCompileMode); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/ReservedGraphOptimizationRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/ReservedGraphOptimizationRule.cpp new file mode 100644 index 0000000000..cb0a10948c --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/ReservedGraphOptimizationRule.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include "llvm/Support/Debug.h" + +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +class ReservedGraphOptimizationRule final : public GraphOptimizationRule { +public: + explicit ReservedGraphOptimizationRule( + ReservedGraphOptimizationRuleOptions options) + : options(options) { + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] registered graph optimization rule " + << static_cast(options.id) << " (" + << getGraphOptimizationRuleName(options.id) + << ") namespace=" << options.optionNamespace << "\n"); + } + + GraphOptimizationRuleId getId() const override { return options.id; } + + AnalysisRequirement getAnalysisRequirements() const override { + return AnalysisRequirement::None; + } + + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + static_cast(context); + static_cast(plans); + ++rejectedUnimplemented; + LLVM_DEBUG(llvm::dbgs() << "[" DEBUG_TYPE "] graph optimization rule " + << getGraphOptimizationRuleName(options.id) + << " namespace=" << options.optionNamespace + << " candidates=0 rejected.unimplemented=" + << rejectedUnimplemented << "\n"); + return success(); + } + +private: + ReservedGraphOptimizationRuleOptions options; + unsigned rejectedUnimplemented = 0; +}; + +} // namespace + +std::unique_ptr cfg::createReservedGraphOptimizationRule( + ReservedGraphOptimizationRuleOptions options) { + return std::make_unique(options); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/ResidentLoadForwardingRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/ResidentLoadForwardingRule.cpp new file mode 100644 index 0000000000..ff7da40e2c --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/ResidentLoadForwardingRule.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include + +using namespace mlir; +using namespace triton; +using namespace cfg; + +std::unique_ptr cfg::createResidentLoadForwardingRule( + const ResidentLoadForwardingRuleOptions &options) { + static_cast(options); + return createReservedGraphOptimizationRule( + {GraphOptimizationRuleId::ResidentLoadForwarding, + "resident-load-forwarding"}); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/RowCoalescingRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/RowCoalescingRule.cpp new file mode 100644 index 0000000000..8bc1a56661 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/RowCoalescingRule.cpp @@ -0,0 +1,466 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/DotRowCoalescing.h" +#include "TritonToGraph/GraphOptimizationRule.h" +#include "TritonToGraph/LegacyMemoryAccess/RowCoalescing.h" +#include "TritonToGraph/ProgramAxisDependenceAnalysis.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/CallInterfaces.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +constexpr llvm::StringLiteral kCoalesceFactorAttr = "hacc.coalesce_factor"; +constexpr llvm::StringLiteral kCoalesceAxisAttr = "hacc.coalesce_axis"; +constexpr llvm::StringLiteral kCoalesceGridCeilDivAttr = + "hacc.coalesce_grid_ceil_div"; +constexpr llvm::StringLiteral kIndependentAxisTensorizeMarkerAttr = + "hacc.independent_axis_tensorize"; +constexpr llvm::StringLiteral kPersistentTaskStripMiningMarkerAttr = + "hacc.persistent_task_strip_mining"; + +constexpr int64_t kDefaultRowsPerProgram = 8; +constexpr int64_t kMaxBaseElementsPerLift = 1024; + +struct RowSeed { + triton::GetProgramIdOp pid; + int32_t axis = 0; + Value validCount; + arith::CmpIOp entryGuard; + Block *workBlock = nullptr; +}; + +struct RowCandidate { + triton::FuncOp function; + Operation *anchor = nullptr; + int32_t axis = 0; + int64_t rowsPerProgram = 1; +}; + +bool isPublicEntry(triton::FuncOp function) { + auto visibility = function->getAttrOfType("sym_visibility"); + return !visibility || visibility.getValue() == "public"; +} + +bool isOnlyPublicEntry(ModuleOp module, triton::FuncOp function) { + unsigned publicEntries = 0; + for (triton::FuncOp candidate : module.getOps()) { + if (!isPublicEntry(candidate)) + continue; + ++publicEntries; + if (candidate != function) + return false; + } + return publicEntries == 1; +} + +bool hasDirectCall(triton::FuncOp function) { + bool hasCall = false; + function.walk([&](Operation *operation) { + if (isa(operation)) + hasCall = true; + }); + return hasCall; +} + +bool readsAxisNumPrograms( + triton::FuncOp function, int32_t axis, + const ProgramAxisDependenceAnalysis *programAxisAnalysis = nullptr) { + if (programAxisAnalysis) + return programAxisAnalysis->get(axis).readsNumPrograms; + bool reads = false; + function.walk([&](triton::GetNumProgramsOp np) { + if (np.getAxisAsInt() == axis) + reads = true; + }); + return reads; +} + +bool isScalarIntegerLike(Value value) { + Type type = value.getType(); + if (isa(type)) + return true; + auto integerType = dyn_cast(type); + return integerType && integerType.getWidth() > 1; +} + +bool isAutomaticOverflowAssert(triton::AssertOp assertOp) { + if (!assertOp || !assertOp->hasAttr("tt.auto_overflow_assert")) + return false; + auto message = dyn_cast(assertOp.getMessageAttr()); + return message && + message.getValue().contains("overflow detected for operation"); +} + +bool isInWorkRegion(Operation *operation, Block *workBlock) { + for (Operation *current = operation; current; + current = current->getParentOp()) { + if (current->getBlock() == workBlock) + return true; + } + return false; +} + +bool isRowLiftable(Operation *operation) { + if (isa(operation)) + return false; + if (auto assertOp = dyn_cast(operation)) + return isAutomaticOverflowAssert(assertOp); + if (Dialect *dialect = operation->getDialect()) { + StringRef dialectNamespace = dialect->getNamespace(); + if (dialectNamespace == arith::ArithDialect::getDialectNamespace() || + dialectNamespace == math::MathDialect::getDialectNamespace()) + return true; + } + return isa(operation); +} + +bool collectRowRegion(const RowSeed &seed, + SmallVectorImpl &ordered) { + if (!seed.workBlock || !seed.workBlock->mightHaveTerminator() || + !isa(seed.workBlock->getTerminator())) + return false; + + bool hasStore = false; + for (Operation &operation : seed.workBlock->without_terminator()) { + bool safe = true; + operation.walk([&](Operation *nested) { + if (nested == &operation) + return; + if (isa( + nested)) + return; + if (!isRowLiftable(nested)) + safe = false; + }); + if (!safe || !isRowLiftable(&operation)) + return false; + hasStore |= isa(operation); + ordered.push_back(&operation); + } + return hasStore; +} + +int64_t getMaxStaticTensorElements(ArrayRef ordered) { + int64_t maxElements = 1; + auto update = [&](Type type) { + auto rankedType = dyn_cast(type); + if (!rankedType) + return; + if (!rankedType.hasStaticShape()) { + maxElements = kMaxBaseElementsPerLift + 1; + return; + } + maxElements = std::max(maxElements, rankedType.getNumElements()); + }; + for (Operation *operation : ordered) { + for (Value operand : operation->getOperands()) + update(operand.getType()); + for (Value result : operation->getResults()) + update(result.getType()); + } + return maxElements; +} + +int64_t inferRowsPerProgram(int64_t maxBaseElements) { + if (maxBaseElements > kMaxBaseElementsPerLift) + return 1; + if (maxBaseElements >= 1024) + return 2; + if (maxBaseElements > 16) + return 4; + return kDefaultRowsPerProgram; +} + +bool hasPidDerivedValueOutsideWork(RowSeed &seed) { + DenseSet visited; + SmallVector worklist{seed.pid.getResult()}; + while (!worklist.empty()) { + Value value = worklist.pop_back_val(); + if (!visited.insert(value).second) + continue; + for (Operation *user : value.getUsers()) { + // The canonical guard intentionally consumes the scalar PID before the + // work block. Every other PID-derived calculation must be rebuilt in + // the lifted region; otherwise legacy Row would splat it as uniform. + if (user == seed.entryGuard.getOperation()) + continue; + if (!isInWorkRegion(user, seed.workBlock)) + return true; + for (Value result : user->getResults()) + worklist.push_back(result); + } + } + return false; +} + +bool hasEscapingWorkResult(ArrayRef ordered, Block *workBlock) { + for (Operation *operation : ordered) { + for (Value result : operation->getResults()) { + for (Operation *user : result.getUsers()) { + if (!isInWorkRegion(user, workBlock)) + return true; + } + } + } + return false; +} + +std::optional matchRowSeed( + triton::FuncOp function, + const ProgramAxisDependenceAnalysis *programAxisAnalysis = nullptr) { + SmallVector pids; + function.walk([&](triton::GetProgramIdOp pid) { pids.push_back(pid); }); + if (pids.size() != 1) + return std::nullopt; + + triton::GetProgramIdOp pid = pids.front(); + const int32_t axis = pid.getAxisAsInt(); + if (readsAxisNumPrograms(function, axis, programAxisAnalysis)) + return std::nullopt; + + for (Operation *user : pid.getResult().getUsers()) { + auto guard = dyn_cast(user); + if (!guard || guard.getPredicate() != arith::CmpIPredicate::sge || + guard.getLhs() != pid.getResult() || !guard.getResult().hasOneUse()) + continue; + + Block *entryBlock = guard->getBlock(); + if (!entryBlock || entryBlock != pid->getBlock() || + !entryBlock->mightHaveTerminator()) + continue; + auto branch = + dyn_cast_or_null(entryBlock->getTerminator()); + if (!branch || branch.getCondition() != guard.getResult()) + continue; + + Block *returnBlock = branch.getTrueDest(); + Block *workBlock = branch.getFalseDest(); + if (!returnBlock || !workBlock || returnBlock->getNumArguments() != 0 || + workBlock->getNumArguments() != 0 || + !returnBlock->mightHaveTerminator() || + !isa(returnBlock->getTerminator()) || + returnBlock->getTerminator()->getNumOperands() != 0 || + !isScalarIntegerLike(guard.getRhs())) + continue; + auto predecessors = workBlock->getPredecessors(); + if (!llvm::hasSingleElement(predecessors) || + *predecessors.begin() != entryBlock) + continue; + + RowSeed seed{pid, axis, guard.getRhs(), guard, workBlock}; + if (hasPidDerivedValueOutsideWork(seed)) + continue; + return seed; + } + return std::nullopt; +} + +std::optional +analyzeRow(triton::FuncOp function, + const ProgramAxisDependenceAnalysis *programAxisAnalysis = nullptr) { + ModuleOp module = function->getParentOfType(); + if (!module || !isPublicEntry(function) || + !isOnlyPublicEntry(module, function) || function->getNumRegions() != 1 || + function->getRegion(0).empty() || hasDirectCall(function) || + module->hasAttr(kIndependentAxisTensorizeMarkerAttr) || + module->hasAttr(kPersistentTaskStripMiningMarkerAttr) || + module->hasAttr(kCoalesceFactorAttr) || + module->hasAttr(kCoalesceAxisAttr) || + module->hasAttr(kCoalesceGridCeilDivAttr)) + return std::nullopt; + + std::optional seed = matchRowSeed(function, programAxisAnalysis); + if (!seed) + return std::nullopt; + + SmallVector ordered; + if (!collectRowRegion(*seed, ordered) || + hasEscapingWorkResult(ordered, seed->workBlock)) + return std::nullopt; + + const int64_t rowsPerProgram = + inferRowsPerProgram(getMaxStaticTensorElements(ordered)); + if (rowsPerProgram <= 1) + return std::nullopt; + return RowCandidate{function, seed->entryGuard.getOperation(), seed->axis, + rowsPerProgram}; +} + +bool matchesCandidate(const RowCandidate &candidate, + const RowCandidate ¤t) { + return candidate.function == current.function && + candidate.anchor == current.anchor && candidate.axis == current.axis && + candidate.rowsPerProgram == current.rowsPerProgram; +} + +class RowCoalescingPlan final : public RewritePlan { +public: + RowCoalescingPlan(RowCandidate candidate, unsigned epoch) + : candidate(candidate), epoch(epoch) {} + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::RowCoalescing; + } + + unsigned getBenefit() const override { return 1; } + Operation *getAnchor() const override { return candidate.anchor; } + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (context.getFunction() != candidate.function) + return failure(); + std::optional current = analyzeRow(context.getFunction()); + return current && matchesCandidate(candidate, *current) ? success() + : failure(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + return applyWithResult(rewriter) == RewritePlanApplyResult::Applied + ? success() + : failure(); + } + + RewritePlanApplyResult applyWithResult(IRRewriter &rewriter) override { + (void)rewriter; + std::optional current = analyzeRow(candidate.function); + if (!current || !matchesCandidate(candidate, *current)) + return RewritePlanApplyResult::NotApplicable; + + ModuleOp module = candidate.function->getParentOfType(); + if (!module) + return RewritePlanApplyResult::Failed; + + // The old Row implementation can still encounter a late unsupported + // shape while materializing. Run it on a detached one-function module; + // the original function and its launch attrs remain untouched until the + // cloned IR verifies successfully. + ModuleOp sandbox = ModuleOp::create(candidate.function.getLoc()); + sandbox.getBody()->push_back(candidate.function->clone()); + auto clonedFunction = dyn_cast(&sandbox.getBody()->front()); + if (!clonedFunction) + return RewritePlanApplyResult::Failed; + + RowCoalescing::rewriteRowCoalesce(sandbox); + auto factor = sandbox->getAttrOfType(kCoalesceFactorAttr); + auto axis = sandbox->getAttrOfType(kCoalesceAxisAttr); + auto ceilDiv = + sandbox->getAttrOfType(kCoalesceGridCeilDivAttr); + if (!factor || !axis || !ceilDiv || + factor.getInt() != candidate.rowsPerProgram || + axis.getInt() != candidate.axis || ceilDiv.getInt() != 1 || + failed(mlir::verify(sandbox.getOperation()))) + return RewritePlanApplyResult::NotApplicable; + + // takeBody() is non-failing. Commit the function IR first, then publish + // the complete launcher contract as one final, non-failing step. + candidate.function->getRegion(0).takeBody(clonedFunction->getRegion(0)); + auto i32Type = IntegerType::get(module.getContext(), 32); + module->setAttr(kCoalesceFactorAttr, + IntegerAttr::get(i32Type, candidate.rowsPerProgram)); + module->setAttr(kCoalesceAxisAttr, + IntegerAttr::get(i32Type, candidate.axis)); + module->setAttr(kCoalesceGridCeilDivAttr, IntegerAttr::get(i32Type, 1)); + return RewritePlanApplyResult::Applied; + } + +private: + RowCandidate candidate; + unsigned epoch; +}; + +class RowCoalescingRule final : public GraphOptimizationRule { + bool enableLegacyPattern; + +public: + explicit RowCoalescingRule(bool enableLegacyPattern) + : enableLegacyPattern(enableLegacyPattern) {} + + GraphOptimizationRuleId getId() const override { + return GraphOptimizationRuleId::RowCoalescing; + } + + AnalysisRequirement getAnalysisRequirements() const override { + return AnalysisRequirement::ProgramAxisDependence; + } + + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + // The legacy pattern is enabled only for simt_only. Dot row coalescing + // belongs to the non-pure-SIMT path and must not override that pattern. + if (!enableLegacyPattern) { + if (auto plan = createDotRowCoalescingPlan(context.getFunction(), + context.getEpoch())) + plans.push_back(std::move(plan)); + return success(); + } + + if (std::optional candidate = + analyzeRow(context.getFunction(), + &context.getProgramAxisDependenceAnalysis())) { + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast(getId()) << " (" + << getGraphOptimizationRuleName(getId()) << ") in @" + << candidate->function.getName() + << ": axis=" << candidate->axis + << " rowsPerProgram=" << candidate->rowsPerProgram << "\n"); + plans.push_back( + std::make_unique(*candidate, context.getEpoch())); + } + return success(); + } +}; + +} // namespace + +std::unique_ptr +cfg::createRowCoalescingRule(bool enableLegacy) { + return std::make_unique(enableLegacy); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/StoreCoalescingRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/StoreCoalescingRule.cpp new file mode 100644 index 0000000000..fdd4f62726 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/StoreCoalescingRule.cpp @@ -0,0 +1,775 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/EntryArgPointerAliasAnalysis.h" +#include "TritonToGraph/GraphOptimizationRule.h" +#include "TritonToGraph/PermutationAnalysis.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Dominance.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Verifier.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +struct StoreCandidate { + Operation *operation; + StaticAccess access; + RankedTensorType pointerType; + RankedTensorType valueType; + RankedTensorType offsetType; + Type pointeeType; + uint64_t elementBytes; + int64_t elementCount; + Value value; + triton::CacheModifier cache; + triton::EvictionPolicy evict; + DictionaryAttr attributes; +}; + +struct StoreCoalescingRun { + SmallVector addressOrderStores; + SmallVector programOrderStores; + Operation *anchor = nullptr; + int64_t firstOffset = 0; + int64_t endExclusive = 0; + int64_t totalElements = 0; + uint64_t totalBytes = 0; +}; + +bool fitsI32(int64_t value) { + return value >= std::numeric_limits::min() && + value <= std::numeric_limits::max(); +} + +bool checkedAddI64(int64_t lhs, int64_t rhs, int64_t &result) { + if (rhs > 0 && lhs > std::numeric_limits::max() - rhs) + return false; + if (rhs < 0 && lhs < std::numeric_limits::min() - rhs) + return false; + result = lhs + rhs; + return true; +} + +bool checkedSubI64(int64_t lhs, int64_t rhs, int64_t &result) { + if (rhs > 0 && lhs < std::numeric_limits::min() + rhs) + return false; + if (rhs < 0 && lhs > std::numeric_limits::max() + rhs) + return false; + result = lhs - rhs; + return true; +} + +bool checkedMulU64(uint64_t lhs, uint64_t rhs, uint64_t &result) { + if (lhs != 0 && rhs > std::numeric_limits::max() / lhs) + return false; + result = lhs * rhs; + return true; +} + +std::optional getByteWidth(Type type) { + unsigned bitWidth = 0; + if (auto integerType = dyn_cast(type)) { + bitWidth = integerType.getWidth(); + } else if (auto floatType = dyn_cast(type)) { + bitWidth = floatType.getWidth(); + } else { + return std::nullopt; + } + + // i1 and sub-byte integer/float element types are deliberately excluded: + // capacity is accounted in concrete addressable bytes, never rounded lanes. + if (bitWidth == 0 || bitWidth % 8 != 0) + return std::nullopt; + return static_cast(bitWidth / 8); +} + +std::optional getStaticElementCount(RankedTensorType type) { + if (!type || !type.hasStaticShape() || type.getEncoding() || + type.getRank() < 1) + return std::nullopt; + + int64_t elementCount = 1; + for (int64_t dimension : type.getShape()) { + if (dimension <= 0 || + (elementCount != 0 && + dimension > std::numeric_limits::max() / elementCount)) + return std::nullopt; + elementCount *= dimension; + } + return elementCount; +} + +bool isUnencodedStaticTensor(RankedTensorType type) { + return getStaticElementCount(type).has_value(); +} + +bool isUnencodedStaticRankOneTensor(RankedTensorType type) { + return isUnencodedStaticTensor(type) && type.getRank() == 1; +} + +bool isDirectFunctionBodyStore(triton::StoreOp store, triton::FuncOp function, + Block *block) { + if (!store || !function || !block || store->getBlock() != block || + store->getNumRegions() != 0) + return false; + if (block->getParent() != &function.getBody()) + return false; + triton::FuncOp parent = store->getParentOfType(); + return parent && parent.getOperation() == function.getOperation(); +} + +bool hasExpectedStoreTypes(const StoreCandidate &candidate) { + if (!isUnencodedStaticTensor(candidate.pointerType) || + !isUnencodedStaticTensor(candidate.valueType) || + !isUnencodedStaticTensor(candidate.offsetType)) + return false; + if (candidate.pointerType.getShape() != candidate.valueType.getShape() || + candidate.pointerType.getShape() != candidate.offsetType.getShape() || + candidate.pointerType.getEncoding() != + candidate.valueType.getEncoding() || + candidate.pointerType.getEncoding() != candidate.offsetType.getEncoding()) + return false; + + auto pointerElement = + dyn_cast(candidate.pointerType.getElementType()); + if (!pointerElement || triton::isTensorPointerType(pointerElement) || + pointerElement.getPointeeType() != candidate.pointeeType || + candidate.valueType.getElementType() != candidate.pointeeType) + return false; + + int64_t span = 0; + if (!checkedSubI64(candidate.access.lastOffset, candidate.access.firstOffset, + span) || + !checkedAddI64(span, 1, span) || span <= 0 || + span != candidate.elementCount) + return false; + return true; +} + +std::optional +matchStore(triton::StoreOp store, triton::FuncOp function, Block *block) { + if (!isDirectFunctionBodyStore(store, function, block) || store.getMask() || + !store.getBoundaryCheck().empty()) + return std::nullopt; + + auto pointerType = dyn_cast(store.getPtr().getType()); + auto valueType = dyn_cast(store.getValue().getType()); + if (!pointerType || !valueType || !isUnencodedStaticTensor(pointerType) || + !isUnencodedStaticTensor(valueType)) + return std::nullopt; + + auto pointerElement = + dyn_cast(pointerType.getElementType()); + if (!pointerElement || triton::isTensorPointerType(pointerElement) || + pointerElement.getPointeeType() != valueType.getElementType() || + pointerType.getShape() != valueType.getShape() || + pointerType.getEncoding() != valueType.getEncoding()) + return std::nullopt; + + std::optional elementBytes = + getByteWidth(valueType.getElementType()); + if (!elementBytes) + return std::nullopt; + std::optional elementCount = getStaticElementCount(valueType); + if (!elementCount) + return std::nullopt; + + StaticAccessAnalysis accessAnalysis; + StaticAccessProof proof = accessAnalysis.analyzeStore(store); + if (!proof.isProven() || !proof.access->isLogicalRowMajorContiguous() || + proof.access->pointer != store.getPtr() || !proof.access->base || + proof.access->shape != pointerType.getShape() || + proof.access->elementCount != *elementCount) + return std::nullopt; + + auto offsetType = dyn_cast(proof.access->offset.getType()); + if (!offsetType) + return std::nullopt; + + StoreCandidate candidate{ + store.getOperation(), std::move(*proof.access), + pointerType, valueType, + offsetType, pointerElement.getPointeeType(), + *elementBytes, *elementCount, + store.getValue(), store.getCache(), + store.getEvict(), store->getAttrDictionary()}; + if (!hasExpectedStoreTypes(candidate)) + return std::nullopt; + return candidate; +} + +bool canShareBucket(const StoreCandidate &lhs, const StoreCandidate &rhs) { + // Base and dynamic-origin comparisons are deliberately SSA identity only. + // This rule never consults alias analysis to coalesce output stores. + return lhs.operation->getBlock() == rhs.operation->getBlock() && + lhs.access.base == rhs.access.base && + lhs.access.dynamicOrigin == rhs.access.dynamicOrigin && + lhs.pointeeType == rhs.pointeeType && + lhs.valueType.getElementType() == rhs.valueType.getElementType() && + lhs.pointerType.getEncoding() == rhs.pointerType.getEncoding() && + lhs.valueType.getEncoding() == rhs.valueType.getEncoding() && + lhs.cache == rhs.cache && lhs.evict == rhs.evict && + lhs.attributes == rhs.attributes; +} + +bool isBeforeInProgramOrder(Operation *lhs, Operation *rhs) { + return lhs && rhs && lhs != rhs && lhs->getBlock() == rhs->getBlock() && + lhs->isBeforeInBlock(rhs); +} + +bool isLocalValueBeforeAnchor(Value value, Operation *anchor, + DominanceInfo &dominance) { + if (!value || !anchor || !dominance.dominates(value, anchor)) + return false; + + if (auto blockArgument = dyn_cast(value)) + return blockArgument.getOwner() == anchor->getBlock(); + + Operation *definition = value.getDefiningOp(); + return definition && definition->getBlock() == anchor->getBlock() && + definition->isBeforeInBlock(anchor); +} + +bool valuesDominateAnchor(const StoreCoalescingRun &run, + triton::FuncOp function) { + if (!run.anchor || !function) + return false; + + DominanceInfo dominance(function.getOperation()); + for (const StoreCandidate &candidate : run.addressOrderStores) { + if (!candidate.value || !candidate.access.base || + !isLocalValueBeforeAnchor(candidate.value, run.anchor, dominance) || + !isLocalValueBeforeAnchor(candidate.access.base, run.anchor, + dominance) || + (candidate.access.dynamicOrigin && + !isLocalValueBeforeAnchor(candidate.access.dynamicOrigin, run.anchor, + dominance))) + return false; + } + return true; +} + +std::optional +buildRun(ArrayRef addressOrderStores, triton::FuncOp function, + unsigned ubCapacityBytes, + const EntryArgPointerAliasAnalysis *entryAliases) { + if (!function || !entryAliases || ubCapacityBytes == 0 || + addressOrderStores.size() < 2) + return std::nullopt; + + const StoreCandidate &first = addressOrderStores.front(); + const StoreCandidate &last = addressOrderStores.back(); + for (const StoreCandidate &candidate : addressOrderStores) { + if (candidate.elementBytes != first.elementBytes) + return std::nullopt; + } + int64_t endExclusive = 0; + int64_t totalElements = 0; + if (!checkedAddI64(last.access.lastOffset, 1, endExclusive) || + !checkedSubI64(endExclusive, first.access.firstOffset, totalElements) || + totalElements <= 0 || !fitsI32(first.access.firstOffset) || + !fitsI32(endExclusive)) + return std::nullopt; + + int64_t summedElements = 0; + for (const StoreCandidate &candidate : addressOrderStores) { + if (candidate.elementCount <= 0 || + !checkedAddI64(summedElements, candidate.elementCount, summedElements)) + return std::nullopt; + } + if (summedElements != totalElements) + return std::nullopt; + + uint64_t totalBytes = 0; + if (!checkedMulU64(static_cast(totalElements), first.elementBytes, + totalBytes) || + totalBytes > static_cast(ubCapacityBytes)) + return std::nullopt; + + StoreCoalescingRun run; + run.addressOrderStores.append(addressOrderStores.begin(), + addressOrderStores.end()); + run.firstOffset = first.access.firstOffset; + run.endExclusive = endExclusive; + run.totalElements = totalElements; + run.totalBytes = totalBytes; + for (const StoreCandidate &candidate : run.addressOrderStores) + run.programOrderStores.push_back(candidate.operation); + std::stable_sort(run.programOrderStores.begin(), run.programOrderStores.end(), + isBeforeInProgramOrder); + run.anchor = run.programOrderStores.back(); + + if (!valuesDominateAnchor(run, function)) + return std::nullopt; + + // Keep the StaticAccess descriptions aligned with program order. Address + // order determines packed-value layout, while program order determines which + // stores are delayed across a given intervening operation. + SmallVector programOrderAccesses; + programOrderAccesses.reserve(run.programOrderStores.size()); + for (Operation *operation : run.programOrderStores) { + auto candidate = std::find_if(run.addressOrderStores.begin(), + run.addressOrderStores.end(), + [operation](const StoreCandidate &store) { + return store.operation == operation; + }); + if (candidate == run.addressOrderStores.end()) + return std::nullopt; + programOrderAccesses.push_back(candidate->access); + } + + // The combined store is inserted at the program-order last store. For the + // gap (S[i - 1], S[i]), only S[0] ... S[i - 1] move from before the gap to + // after it. A load/store in the gap may overlap a later planned store whose + // relative order is unchanged, but must be statically disjoint from every + // delayed prefix store. All other memory effects remain fail-closed. + ProtectedIntervalAnalysis intervalAnalysis; + for (size_t index = 1; index < run.programOrderStores.size(); ++index) { + llvm::ArrayRef delayedAccesses(programOrderAccesses.data(), + index); + if (!intervalAnalysis + .proveNoConflictingLoadStoreEffects( + run.programOrderStores[index - 1], + run.programOrderStores[index], delayedAccesses, entryAliases) + .isProven()) + return std::nullopt; + } + + if (run.addressOrderStores.size() - 1 > + static_cast(std::numeric_limits::max())) + return std::nullopt; + return run; +} + +SmallVector +findRuns(triton::FuncOp function, unsigned ubCapacityBytes, + const EntryArgPointerAliasAnalysis *entryAliases) { + SmallVector runs; + if (!function || !entryAliases || ubCapacityBytes == 0) + return runs; + + for (Block &block : function.getBody()) { + SmallVector stores; + for (Operation &operation : block) { + auto store = dyn_cast(&operation); + if (!store) + continue; + std::optional candidate = + matchStore(store, function, &block); + if (candidate) + stores.push_back(std::move(*candidate)); + } + + SmallVector, 4> buckets; + for (StoreCandidate &candidate : stores) { + auto bucket = std::find_if( + buckets.begin(), buckets.end(), [&](const auto &existing) { + return !existing.empty() && + canShareBucket(existing.front(), candidate); + }); + if (bucket == buckets.end()) { + buckets.emplace_back(); + buckets.back().push_back(candidate); + } else { + bucket->push_back(candidate); + } + } + + for (SmallVector &bucket : buckets) { + if (bucket.size() < 2) + continue; + + std::stable_sort( + bucket.begin(), bucket.end(), + [](const StoreCandidate &lhs, const StoreCandidate &rhs) { + if (lhs.access.firstOffset != rhs.access.firstOffset) + return lhs.access.firstOffset < rhs.access.firstOffset; + if (lhs.access.lastOffset != rhs.access.lastOffset) + return lhs.access.lastOffset < rhs.access.lastOffset; + return isBeforeInProgramOrder(lhs.operation, rhs.operation); + }); + + // An overlap invalidates every candidate in this bucket. In particular, + // do not try to salvage a later non-overlapping sub-run. + bool hasOverlap = false; + for (size_t index = 1; index < bucket.size(); ++index) { + if (bucket[index].access.firstOffset <= + bucket[index - 1].access.lastOffset) { + hasOverlap = true; + break; + } + } + if (hasOverlap) + continue; + + size_t runBegin = 0; + for (size_t index = 1; index <= bucket.size(); ++index) { + bool endsRun = index == bucket.size(); + if (!endsRun) { + int64_t expectedNextOffset = 0; + endsRun = !checkedAddI64(bucket[index - 1].access.lastOffset, 1, + expectedNextOffset) || + expectedNextOffset != bucket[index].access.firstOffset; + } + + if (!endsRun) + continue; + if (index - runBegin >= 2) { + std::optional run = + buildRun(ArrayRef(bucket).slice(runBegin, + index - runBegin), + function, ubCapacityBytes, entryAliases); + if (run) + runs.push_back(std::move(*run)); + } + runBegin = index; + } + } + } + return runs; +} + +bool hasSameAddressOrder(const StoreCoalescingRun &run, + ArrayRef operations) { + if (run.addressOrderStores.size() != operations.size()) + return false; + for (size_t index = 0; index < operations.size(); ++index) { + if (run.addressOrderStores[index].operation != operations[index]) + return false; + } + return true; +} + +std::optional +findRunByOperations(triton::FuncOp function, unsigned ubCapacityBytes, + const EntryArgPointerAliasAnalysis *entryAliases, + ArrayRef operations) { + for (StoreCoalescingRun &run : + findRuns(function, ubCapacityBytes, entryAliases)) { + if (hasSameAddressOrder(run, operations)) + return run; + } + return std::nullopt; +} + +void eraseCreatedOperations(IRRewriter &rewriter, + ArrayRef created) { + for (Operation *operation : llvm::reverse(created)) + rewriter.eraseOp(operation); +} + +bool recordVerifiedOperation(Operation *operation, + SmallVectorImpl &created) { + if (!operation) + return false; + created.push_back(operation); + return succeeded(mlir::verify(operation)); +} + +std::optional +materializeRowMajorFlatValue(IRRewriter &rewriter, + const StoreCandidate &candidate, + SmallVectorImpl &created) { + SmallVector flatShape = {candidate.elementCount}; + RankedTensorType flatType = candidate.valueType.clone(flatShape); + if (!isUnencodedStaticRankOneTensor(flatType)) + return std::nullopt; + if (candidate.valueType == flatType) + return candidate.value; + + auto reshape = rewriter.create( + candidate.operation->getLoc(), flatType, candidate.value); + if (!recordVerifiedOperation(reshape.getOperation(), created)) + return std::nullopt; + return reshape.getResult(); +} + +LogicalResult applyRun(IRRewriter &rewriter, const StoreCoalescingRun &run) { + if (!run.anchor || run.addressOrderStores.size() < 2 || + run.programOrderStores.size() != run.addressOrderStores.size() || + run.totalElements <= 0 || !fitsI32(run.firstOffset) || + !fitsI32(run.endExclusive)) + return failure(); + + const StoreCandidate &exemplar = run.addressOrderStores.front(); + SmallVector packedShape = {run.totalElements}; + // clone() retains any type details carried by the original tensor. V1 has + // already required unencoded types. + RankedTensorType packedValueType = exemplar.valueType.clone(packedShape); + RankedTensorType packedPointerType = exemplar.pointerType.clone(packedShape); + RankedTensorType packedOffsetType = exemplar.offsetType.clone(packedShape); + if (!isUnencodedStaticRankOneTensor(packedValueType) || + !isUnencodedStaticRankOneTensor(packedPointerType) || + !isUnencodedStaticRankOneTensor(packedOffsetType) || + packedPointerType.getShape() != packedValueType.getShape() || + packedPointerType.getShape() != packedOffsetType.getShape()) + return failure(); + + SmallVector created; + rewriter.setInsertionPoint(run.anchor); + auto empty = rewriter.create(run.anchor->getLoc(), + packedValueType, ValueRange{}); + if (!recordVerifiedOperation(empty.getOperation(), created)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + Value packedValue = empty.getResult(); + int64_t packedOffset = 0; + for (const StoreCandidate &candidate : run.addressOrderStores) { + const int64_t sliceLength = candidate.elementCount; + if (sliceLength <= 0) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + std::optional flatValue = + materializeRowMajorFlatValue(rewriter, candidate, created); + if (!flatValue) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + SmallVector offsets = { + rewriter.getIndexAttr(packedOffset)}; + SmallVector sizes = {rewriter.getIndexAttr(sliceLength)}; + SmallVector strides = {rewriter.getIndexAttr(1)}; + auto inserted = rewriter.create( + run.anchor->getLoc(), *flatValue, packedValue, offsets, sizes, strides); + if (!recordVerifiedOperation(inserted.getOperation(), created)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + packedValue = inserted.getResult(); + + int64_t nextPackedOffset = 0; + if (!checkedAddI64(packedOffset, sliceLength, nextPackedOffset)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + packedOffset = nextPackedOffset; + } + if (packedOffset != run.totalElements) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + auto range = rewriter.create( + run.anchor->getLoc(), packedOffsetType, + rewriter.getI32IntegerAttr(static_cast(run.firstOffset)), + rewriter.getI32IntegerAttr(static_cast(run.endExclusive))); + if (!recordVerifiedOperation(range.getOperation(), created)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + Value packedPointerOffset = range.getResult(); + if (exemplar.access.dynamicOrigin) { + auto originSplat = rewriter.create( + run.anchor->getLoc(), packedOffsetType, exemplar.access.dynamicOrigin); + if (!recordVerifiedOperation(originSplat.getOperation(), created)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + auto offsetAdd = rewriter.create( + run.anchor->getLoc(), originSplat.getResult(), range.getResult()); + if (!recordVerifiedOperation(offsetAdd.getOperation(), created)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + packedPointerOffset = offsetAdd.getResult(); + } + + auto baseSplat = rewriter.create( + run.anchor->getLoc(), packedPointerType, exemplar.access.base); + if (!recordVerifiedOperation(baseSplat.getOperation(), created)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + auto pointer = rewriter.create( + run.anchor->getLoc(), packedPointerType, baseSplat.getResult(), + packedPointerOffset); + if (!recordVerifiedOperation(pointer.getOperation(), created)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + auto combinedStore = rewriter.create( + run.anchor->getLoc(), pointer.getResult(), packedValue, exemplar.cache, + exemplar.evict); + // Bucket construction required complete attribute equality. Copying the + // full dictionary therefore preserves every store property and extra attr. + combinedStore->setAttrs(exemplar.attributes); + if (!recordVerifiedOperation(combinedStore.getOperation(), created)) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + StaticAccessAnalysis accessAnalysis; + StaticAccessProof rebuiltProof = + accessAnalysis.analyzePointer(pointer.getResult()); + if (!rebuiltProof.isProven() || !rebuiltProof.access->isRankOneContiguous() || + rebuiltProof.access->base != exemplar.access.base || + rebuiltProof.access->dynamicOrigin != exemplar.access.dynamicOrigin || + rebuiltProof.access->firstOffset != run.firstOffset || + rebuiltProof.access->lastOffset != run.endExclusive - 1) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + // All operations have verified and the replacement pointer has re-proven + // its static interval before any original memory effect is erased. + for (Operation *operation : llvm::reverse(run.programOrderStores)) + rewriter.eraseOp(operation); + return success(); +} + +class StoreCoalescingPlan final : public RewritePlan { +public: + StoreCoalescingPlan(const StoreCoalescingRun &run, triton::FuncOp function, + const EntryArgPointerAliasAnalysis *entryAliases, + unsigned ubCapacityBytes, unsigned epoch) + : function(function), anchor(run.anchor), entryAliases(entryAliases), + ubCapacityBytes(ubCapacityBytes), epoch(epoch) { + for (const StoreCandidate &candidate : run.addressOrderStores) + addressOrderStores.push_back(candidate.operation); + } + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::StoreCoalescing; + } + + unsigned getBenefit() const override { + return addressOrderStores.size() < 2 + ? 0 + : static_cast(addressOrderStores.size() - 1); + } + + Operation *getAnchor() const override { return anchor; } + + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (context.getEpoch() != epoch || !function || !anchor || !entryAliases || + context.getFunction().getOperation() != function.operator->() || + &context.getEntryArgPointerAliasAnalysis() != entryAliases) + return failure(); + + std::optional current = findRunByOperations( + function, ubCapacityBytes, entryAliases, addressOrderStores); + if (!current || current->anchor != anchor) + return failure(); + return success(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + if (!function || !anchor || !entryAliases) + return failure(); + std::optional current = findRunByOperations( + function, ubCapacityBytes, entryAliases, addressOrderStores); + if (!current || current->anchor != anchor) + return failure(); + return applyRun(rewriter, *current); + } + +private: + triton::FuncOp function; + Operation *anchor; + SmallVector addressOrderStores; + const EntryArgPointerAliasAnalysis *entryAliases; + unsigned ubCapacityBytes; + unsigned epoch; +}; + +class StoreCoalescingRule final : public GraphOptimizationRule { +public: + StoreCoalescingRule(unsigned ubCapacityBytes, bool enabledForCompileMode) + : ubCapacityBytes(ubCapacityBytes), + enabledForCompileMode(enabledForCompileMode) {} + + GraphOptimizationRuleId getId() const override { + return GraphOptimizationRuleId::StoreCoalescing; + } + + AnalysisRequirement getAnalysisRequirements() const override { + return !enabledForCompileMode || ubCapacityBytes == 0 + ? AnalysisRequirement::None + : AnalysisRequirement::EntryArgPointerAlias; + } + + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + if (!enabledForCompileMode || ubCapacityBytes == 0) + return success(); + + const EntryArgPointerAliasAnalysis *entryAliases = + &context.getEntryArgPointerAliasAnalysis(); + for (const StoreCoalescingRun &run : + findRuns(context.getFunction(), ubCapacityBytes, entryAliases)) { + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast(getId()) << " (" + << getGraphOptimizationRuleName(getId()) << ") at " + << run.anchor->getLoc() + << ": stores=" << run.programOrderStores.size() << " elements=" + << run.totalElements << " bytes=" << run.totalBytes << "\n"); + plans.push_back(std::make_unique( + run, context.getFunction(), entryAliases, ubCapacityBytes, + context.getEpoch())); + } + return success(); + } + +private: + unsigned ubCapacityBytes; + bool enabledForCompileMode; +}; + +} // namespace + +std::unique_ptr +cfg::createStoreCoalescingRule(unsigned ubCapacityBytes, + const StoreCoalescingRuleOptions &options) { + return std::make_unique(ubCapacityBytes, + options.enabledForCompileMode); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/StoreCoveragePlanningRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/StoreCoveragePlanningRule.cpp new file mode 100644 index 0000000000..e4fce01ae6 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/StoreCoveragePlanningRule.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include + +using namespace mlir; +using namespace triton; +using namespace cfg; + +std::unique_ptr cfg::createStoreCoveragePlanningRule( + const StoreCoveragePlanningRuleOptions &options) { + static_cast(options); + return createReservedGraphOptimizationRule( + {GraphOptimizationRuleId::StoreCoveragePlanning, + "store-coverage-planning"}); +} diff --git a/third_party/ascend/lib/TritonToGraph/Rules/TransposePointwiseReorderRule.cpp b/third_party/ascend/lib/TritonToGraph/Rules/TransposePointwiseReorderRule.cpp new file mode 100644 index 0000000000..246b3681e0 --- /dev/null +++ b/third_party/ascend/lib/TritonToGraph/Rules/TransposePointwiseReorderRule.cpp @@ -0,0 +1,412 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToGraph/GraphOptimizationRule.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/TritonGPU/IR/Dialect.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Debug.h" + +#include +#include +#include + +#define DEBUG_TYPE "graph-optimize" + +using namespace mlir; +using namespace triton; +using namespace cfg; + +namespace { + +struct MatchedChain { + triton::DotOp dot; + unsigned operandIndex; + triton::TransOp trans; + SmallVector unaryOps; +}; + +enum class UnaryMoveKind { ElementTypeCast, TypePreserving }; + +bool hasSingleResultAndOperand(Operation *operation, Value expectedResult) { + return operation && operation->getNumOperands() == 1 && + operation->getNumResults() == 1 && + operation->getResult(0) == expectedResult && + operation->getNumRegions() == 0; +} + +bool hasOnlyExpectedUse(Value value, Operation *expectedUser) { + if (!value || !expectedUser || !value.hasOneUse()) + return false; + return value.use_begin()->getOwner() == expectedUser; +} + +std::optional classifyTrustedUnary(Operation *operation) { + if (!operation) + return std::nullopt; + + if (isa(operation)) + return UnaryMoveKind::ElementTypeCast; + + if (operation->hasTrait()) + return UnaryMoveKind::TypePreserving; + + return std::nullopt; +} + +bool hasCompatibleUnaryTensorTypes(Operation *operation, UnaryMoveKind kind) { + if (!operation || operation->getNumOperands() != 1 || + operation->getNumResults() != 1) + return false; + + auto operandType = + dyn_cast(operation->getOperand(0).getType()); + auto resultType = + dyn_cast(operation->getResult(0).getType()); + if (!operandType || !resultType || !operandType.hasStaticShape() || + !resultType.hasStaticShape()) + return false; + + if (operandType.getShape() != resultType.getShape() || + operandType.getEncoding() != resultType.getEncoding()) + return false; + + return kind != UnaryMoveKind::TypePreserving || operandType == resultType; +} + +bool isTrustedUnary(Operation *operation, Value expectedResult, + UnaryMoveKind kind) { + return hasSingleResultAndOperand(operation, expectedResult) && + isMemoryEffectFree(operation) && + hasCompatibleUnaryTensorTypes(operation, kind); +} + +bool hasValidTransposeOrder(triton::TransOp trans, + RankedTensorType sourceType) { + if (!trans.getOrderAttr()) + return false; + + ArrayRef order = trans.getOrder(); + if (order.empty() || order.size() != sourceType.getRank()) + return false; + + SmallVector seen(order.size(), 0); + for (int32_t axis : order) { + if (axis < 0 || static_cast(axis) >= order.size() || seen[axis]) + return false; + seen[axis] = true; + } + return true; +} + +bool inferTransposeReturnTypes(triton::TransOp trans, Value input, + SmallVectorImpl &inferredTypes) { + if (!trans || !input || !isa(input.getType())) + return false; + + auto rankedInputType = dyn_cast(input.getType()); + if (!rankedInputType || !hasValidTransposeOrder(trans, rankedInputType)) + return false; + + auto inputType = cast(input.getType()); + if (Attribute encoding = inputType.getEncoding()) { + Attribute probeEncoding; + auto *interface = + encoding.getDialect() + .getRegisteredInterface(); + if (!interface || failed(interface->inferTransOpEncoding( + encoding, rankedInputType.getShape(), + trans.getOrder(), probeEncoding, trans.getLoc()))) + return false; + } + + return succeeded(triton::TransOp::inferReturnTypes( + trans->getContext(), trans.getLoc(), ValueRange{input}, + trans->getAttrDictionary(), trans->getPropertiesStorage(), + trans->getRegions(), inferredTypes)) && + inferredTypes.size() == 1; +} + +std::optional matchCandidate(triton::DotOp dot, + unsigned operandIndex) { + if (operandIndex > 1 || !dot || dot->getNumRegions() != 0) + return std::nullopt; + + // Deliberately select only A or B. The accumulator C is never read by + // this rule. + Value value = operandIndex == 0 ? dot.getA() : dot.getB(); + Block *block = dot->getBlock(); + if (!value || !block) + return std::nullopt; + + Operation *expectedUser = dot.getOperation(); + SmallVector reverseUnaryOps; + while (Operation *operation = value.getDefiningOp()) { + std::optional kind = classifyTrustedUnary(operation); + if (!kind) + break; + + if (operation->getBlock() != block || + !hasOnlyExpectedUse(value, expectedUser) || + !isTrustedUnary(operation, value, *kind)) + return std::nullopt; + + reverseUnaryOps.push_back(operation); + value = operation->getOperand(0); + expectedUser = operation; + } + + if (reverseUnaryOps.empty()) + return std::nullopt; + + triton::TransOp trans = value.getDefiningOp(); + if (!trans || trans->getBlock() != block || + !hasSingleResultAndOperand(trans, value) || + !hasOnlyExpectedUse(value, expectedUser)) + return std::nullopt; + + auto sourceType = dyn_cast(trans.getSrc().getType()); + auto resultType = dyn_cast(trans.getResult().getType()); + if (!sourceType || !resultType || !sourceType.hasStaticShape() || + !resultType.hasStaticShape() || + !hasValidTransposeOrder(trans, sourceType)) + return std::nullopt; + + SmallVector originalInferredTypes; + if (!inferTransposeReturnTypes(trans, trans.getSrc(), + originalInferredTypes) || + originalInferredTypes.front() != trans.getResult().getType()) + return std::nullopt; + + SmallVector unaryOps(reverseUnaryOps.rbegin(), + reverseUnaryOps.rend()); + return MatchedChain{dot, operandIndex, trans, std::move(unaryOps)}; +} + +std::optional getMovedUnaryResultType(Operation *unary, + Value input) { + if (!unary || unary->getNumResults() != 1) + return std::nullopt; + + std::optional kind = classifyTrustedUnary(unary); + auto inputType = dyn_cast(input.getType()); + auto oldResultType = + dyn_cast(unary->getResult(0).getType()); + if (!kind || !inputType || !oldResultType || !inputType.hasStaticShape() || + !oldResultType.hasStaticShape()) + return std::nullopt; + + if (*kind == UnaryMoveKind::TypePreserving) + return inputType; + return inputType.clone(oldResultType.getElementType()); +} + +Operation *createMovedUnary(IRRewriter &rewriter, Operation *unary, Value input, + RankedTensorType resultType) { + IRMapping mapping; + mapping.map(unary->getOperand(0), input); + Operation *replacement = rewriter.clone(*unary, mapping); + replacement->getResult(0).setType(resultType); + return replacement; +} + +void eraseCreatedOperations(IRRewriter &rewriter, + ArrayRef created) { + for (Operation *operation : llvm::reverse(created)) + rewriter.eraseOp(operation); +} + +class TransposePointwiseReorderPlan final : public RewritePlan { +public: + TransposePointwiseReorderPlan(MatchedChain chain, unsigned epoch) + : dot(chain.dot), dotOperation(chain.dot.getOperation()), + operandIndex(chain.operandIndex), trans(chain.trans), + transOperation(chain.trans.getOperation()), + unaryOps(std::move(chain.unaryOps)), epoch(epoch) {} + + GraphOptimizationRuleId getRuleId() const override { + return GraphOptimizationRuleId::TransposePointwiseReorder; + } + + unsigned getBenefit() const override { + return static_cast(unaryOps.size()); + } + + Operation *getAnchor() const override { return dotOperation; } + + unsigned getCreationEpoch() const override { return epoch; } + + LogicalResult revalidate(GraphOptimizationContext &context) const override { + if (context.getEpoch() != epoch || !dot) + return failure(); + + triton::FuncOp function = dot->getParentOfType(); + if (!function || + function.getOperation() != context.getFunction().getOperation()) + return failure(); + + std::optional current = matchCandidate(dot, operandIndex); + if (!current || current->trans.getOperation() != transOperation || + current->unaryOps.size() != unaryOps.size()) + return failure(); + + for (auto [currentUnary, storedUnary] : + llvm::zip(current->unaryOps, unaryOps)) { + if (currentUnary != storedUnary) + return failure(); + } + return success(); + } + + LogicalResult apply(IRRewriter &rewriter) override { + // Apply defensively revalidates as well: a plan must never depend on a + // stale operation/use graph even if a caller accidentally skips the + // driver's revalidation gate. + Block *block = dot ? dot->getBlock() : nullptr; + std::optional current = matchCandidate(dot, operandIndex); + if (!block || !current || current->trans.getOperation() != transOperation || + current->unaryOps.size() != unaryOps.size()) + return failure(); + for (auto [currentUnary, storedUnary] : + llvm::zip(current->unaryOps, unaryOps)) { + if (currentUnary != storedUnary) + return failure(); + } + + Value originalDotOperand = operandIndex == 0 ? dot.getA() : dot.getB(); + rewriter.setInsertionPoint(trans); + Value currentValue = trans.getSrc(); + SmallVector created; + for (Operation *unary : unaryOps) { + std::optional movedResultType = + getMovedUnaryResultType(unary, currentValue); + if (!movedResultType) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + Operation *replacement = + createMovedUnary(rewriter, unary, currentValue, *movedResultType); + if (!replacement) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + created.push_back(replacement); + currentValue = replacement->getResult(0); + } + + // Preflight the inferred result before calling the generated builder. In + // particular, an unsupported operand encoding must reject the plan rather + // than reaching a builder that may assert while inferring its result. + SmallVector inferredTransposeTypes; + if (!inferTransposeReturnTypes(trans, currentValue, + inferredTransposeTypes) || + inferredTransposeTypes.front() != originalDotOperand.getType()) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + triton::TransOp replacementTrans = rewriter.create( + trans.getLoc(), currentValue, trans.getOrderAttr()); + replacementTrans->setAttrs(trans->getAttrs()); + created.push_back(replacementTrans.getOperation()); + + // Nothing original is changed until every replacement has passed its own + // verifier and the inferred transpose result exactly matches dot A or B. + for (Operation *operation : created) { + if (failed(mlir::verify(operation))) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + } + if (replacementTrans.getResult().getType() != + originalDotOperand.getType()) { + eraseCreatedOperations(rewriter, created); + return failure(); + } + + rewriter.modifyOpInPlace(dot.getOperation(), [&] { + dot->setOperand(operandIndex, replacementTrans.getResult()); + }); + + for (Operation *unary : llvm::reverse(unaryOps)) + rewriter.eraseOp(unary); + rewriter.eraseOp(trans); + return success(); + } + +private: + triton::DotOp dot; + Operation *dotOperation; + unsigned operandIndex; + triton::TransOp trans; + Operation *transOperation; + SmallVector unaryOps; + unsigned epoch; +}; + +class TransposePointwiseReorderRule final : public GraphOptimizationRule { +public: + GraphOptimizationRuleId getId() const override { + return GraphOptimizationRuleId::TransposePointwiseReorder; + } + + AnalysisRequirement getAnalysisRequirements() const override { + return AnalysisRequirement::None; + } + + LogicalResult findCandidates( + GraphOptimizationContext &context, + SmallVectorImpl> &plans) override { + context.getFunction().walk([&](triton::DotOp dot) { + for (unsigned operandIndex : {0u, 1u}) { + std::optional chain = matchCandidate(dot, operandIndex); + if (chain) { + LLVM_DEBUG(llvm::dbgs() + << "[" DEBUG_TYPE "] matched graph optimization rule " + << static_cast(getId()) << " (" + << getGraphOptimizationRuleName(getId()) << ") at " + << dot.getLoc() << ": operand=" << operandIndex + << " pointwiseOps=" << chain->unaryOps.size() << "\n"); + plans.push_back(std::make_unique( + std::move(*chain), context.getEpoch())); + } + } + }); + return success(); + } +}; + +} // namespace + +std::unique_ptr +cfg::createTransposePointwiseReorderRule() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToHFusion/CMakeLists.txt b/third_party/ascend/lib/TritonToHFusion/CMakeLists.txt new file mode 100644 index 0000000000..f69c1bfd55 --- /dev/null +++ b/third_party/ascend/lib/TritonToHFusion/CMakeLists.txt @@ -0,0 +1,14 @@ +add_triton_library(TritonToHFusion + TritonToHFusion.cpp + + DEPENDS + TritonToHFusionConversionPassIncGen + + LINK_LIBS + BiShengIRHFusionDialect + MLIRIR + MLIRPass + MLIRTransforms + MLIRSupport + TritonIR +) diff --git a/third_party/ascend/lib/TritonToHFusion/TritonToHFusion.cpp b/third_party/ascend/lib/TritonToHFusion/TritonToHFusion.cpp new file mode 100644 index 0000000000..1e8bedd415 --- /dev/null +++ b/third_party/ascend/lib/TritonToHFusion/TritonToHFusion.cpp @@ -0,0 +1,373 @@ +/* + * Copyright (c) Huawei Technologies Co. + * Licensed under the MIT license. + */ + +#include "TritonToHFusion/Passes.h" + +#include "Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "bishengir/Dialect/HFusion/IR/HFusion.h" +#include "bishengir/Dialect/HFusion/IR/HFusionImpl.h" +#include "bishengir/Dialect/Tensor/IR/TensorImpl.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Attributes.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/LogicalResult.h" +#include + +namespace mlir { +namespace triton { +#define GEN_PASS_DEF_TRITONTOHFUSION +#include "ascend/include/TritonToHFusion/Passes.h.inc" +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace hfusion; + +namespace { +struct TritonModToHFusionConversion : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::ascend::ModOp op, + PatternRewriter &rewriter) const final { + auto lhsType = dyn_cast(op.getLhs().getType()); + auto rhsType = dyn_cast(op.getRhs().getType()); + if (!lhsType || !rhsType) { + return failure(); + } + + auto emptyTensor = rewriter.create( + op.getLoc(), lhsType.getShape(), lhsType.getElementType()); + auto newOp = + hfusion::createBinaryOp( + rewriter, op.getLoc(), hfusion::BinaryFn::mod, + ValueRange({op.getLhs(), op.getRhs()}), + ValueRange({emptyTensor.getResult()})); + + rewriter.replaceOp(op, newOp->getResult(0)); + return success(); + } +}; + +struct TritonHistogramToHFusionConversion + : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::HistogramOp op, + PatternRewriter &rewriter) const final { + auto loc = op.getLoc(); + Value input = op.getSrc(); + Value mask = op.getMask(); + auto resultType = op.getResult().getType(); + + int64_t numBins = 256; // 256 is default fallback. + if (auto rankedTy = dyn_cast(resultType)) + if (rankedTy.hasStaticShape() && rankedTy.getNumElements() > 0) + numBins = rankedTy.getNumElements(); + + auto numBinsAttr = rewriter.getI64IntegerAttr(numBins); + + auto newOp = rewriter.create(loc, resultType, input, + numBinsAttr, mask); + + rewriter.replaceOp(op, newOp.getResult()); + return success(); + } +}; + +struct TritonFpToFpToHFusionConversion : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::FpToFpOp op, + PatternRewriter &rewriter) const final { + auto loc = op.getLoc(); + Value input = op.getSrc(); + auto resultType = op.getResult().getType(); + + // RTNE (default) rounding is handled by TritonToLinalg using + // arith.truncf/extf. Check this before querying tensor-only properties: + // scalar fp_to_fp operations are valid Triton IR too. + auto roundingMode = op.getRounding(); + if (!roundingMode.has_value() || + roundingMode.value() == triton::RoundingMode::RTNE) { + // RTNE or no rounding mode specified: let TritonToLinalg handle it + return failure(); + } + + // HFusion owns only non-RTNE tensor conversions. Keep the tensor + // precondition checked here; a scalar must not silently fall through. + auto srcType = cast(input.getType()); + auto dstType = cast(resultType); + if (!srcType.getElementType().isIntOrFloat() || + !dstType.getElementType().isIntOrFloat()) { + return failure(); + } + + // Map non-RTNE rounding modes to HFusion rounding mode + hfusion::RoundMode hfusionRoundMode; + switch (roundingMode.value()) { + case triton::RoundingMode::RTZ: + hfusionRoundMode = hfusion::RoundMode::TRUNC; + break; + default: + return op.emitError("Unsupported rounding mode for HFusion conversion"); + } + // Note: Only RTZ (and potential future non-RTNE modes) reach here + + // Get or create destination tensor (destination-style) + SmallVector dsts; + if (failed(tensor::getOrCreateDestinations(rewriter, loc, op, dsts))) + return failure(); + + // Create the HFusion cast operation with round_mode attribute + auto roundModeAttr = + hfusion::RoundModeAttr::get(rewriter.getContext(), hfusionRoundMode); + auto modeAttr = rewriter.getNamedAttr("mode", roundModeAttr); + + rewriter.replaceOpWithNewOp( + op, ValueRange{input}, ValueRange{dsts}, ArrayRef{modeAttr}); + + return success(); + } +}; + +struct TritonConv1dToHFusionConversion + : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::ascend::Conv1dOp op, + PatternRewriter &rewriter) const final { + auto loc = op.getLoc(); + + Value input = op.getInput(); + Value weight = op.getWeight(); + Value biasValue = op.getBias(); + int64_t stride = op.getStride(); + int64_t padding_size = op.getPaddingSize(); + int64_t dilation = op.getDilation(); + int64_t groups = op.getGroups(); + + auto inputType = mlir::cast(input.getType()); + auto weightType = mlir::cast(weight.getType()); + if (!inputType.hasStaticShape() || !weightType.hasStaticShape()) { + return failure(); + } + + ArrayRef inputShape = inputType.getShape(); + ArrayRef weightShape = weightType.getShape(); + + bool isBatched = inputShape.size() == 3; + int64_t N; + if (isBatched) + N = inputShape[0]; + int64_t L_in = inputShape[isBatched ? 2 : 1]; + int64_t C_out = weightShape[0]; + int64_t kernel_size = weightShape[2]; + + if (stride == 0) { + return failure(); + } + int64_t L_out = + (L_in + 2 * padding_size - dilation * (kernel_size - 1) - 1) / stride + + 1; + + auto resultType = mlir::cast(op.getResult().getType()); + Type resultElementType = resultType.getElementType(); + + constexpr int64_t dim2 = 2; + constexpr int64_t dim3 = 3; + Value initTensor; + if (isBatched) { + SmallVector outputShape{N, C_out, L_out}; + initTensor = + rewriter.create(loc, outputShape, resultElementType); + } else { + SmallVector outputShape{C_out, L_out}; + initTensor = + rewriter.create(loc, outputShape, resultElementType); + } + + SmallVector ins; + ins.push_back(input); + ins.push_back(weight); + if (biasValue) { + ins.push_back(biasValue); + } + + auto newOp = rewriter.create( + loc, ins, initTensor, stride, padding_size, dilation, groups); + + rewriter.replaceOp(op, newOp.getResult()); + + return success(); + } +}; + +struct TritonConv2dToHFusionConversion + : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::ascend::Conv2dOp op, + PatternRewriter &rewriter) const final { + auto loc = op.getLoc(); + + Value input = op.getInput(); + Value weight = op.getWeight(); + Value biasValue = op.getBias(); + int64_t groups = op.getGroups(); + + // Parse an integer (uniform scalar) or integer array attribute. + auto parseParam = + [](Attribute attr) -> std::optional> { + if (auto intAttr = dyn_cast(attr)) + return SmallVector{intAttr.getInt()}; + if (auto arrayAttr = dyn_cast(attr)) { + SmallVector values; + for (int32_t v : arrayAttr.asArrayRef()) + values.push_back(static_cast(v)); + return values; + } + return std::nullopt; + }; + auto stride = parseParam(op.getStride()); + auto padding = parseParam(op.getPadding()); + auto dilation = parseParam(op.getDilation()); + if (!stride || !padding || !dilation) { + return failure(); + } + + auto inputType = mlir::cast(input.getType()); + auto weightType = mlir::cast(weight.getType()); + if (!inputType.hasStaticShape() || !weightType.hasStaticShape()) { + return failure(); + } + + ArrayRef inputShape = inputType.getShape(); + ArrayRef weightShape = weightType.getShape(); + + bool isBatched = inputShape.size() == 4; + int64_t N; + if (isBatched) + N = inputShape[0]; + int64_t H_in = inputShape[isBatched ? 2 : 1]; + int64_t W_in = inputShape[isBatched ? 3 : 2]; + int64_t C_out = weightShape[0]; + int64_t kH = weightShape[2]; + int64_t kW = weightShape[3]; + + auto computeOutDim = [](int64_t in_size, int64_t pad_before, + int64_t pad_after, int64_t dil, int64_t k, + int64_t str) -> int64_t { + return (in_size + pad_before + pad_after - dil * (k - 1) - 1) / str + 1; + }; + + // Expand padding to [pad_top, pad_bottom, pad_left, pad_right]. + SmallVector pads; + if (padding->size() == 1) { + pads.assign(4, (*padding)[0]); + } else if (padding->size() == 2) { + pads = {(*padding)[0], (*padding)[0], (*padding)[1], (*padding)[1]}; + } else { + pads.assign(padding->begin(), padding->end()); + } + // Uniform scalar or pair: front/back covers both forms. + int64_t strideH = stride->front(); + int64_t strideW = stride->back(); + int64_t dilH = dilation->front(); + int64_t dilW = dilation->back(); + + // pads = [pad_top, pad_bottom, pad_left, pad_right] + int64_t H_out = computeOutDim(H_in, pads[0], pads[1], dilH, kH, strideH); + int64_t W_out = computeOutDim(W_in, pads[2], pads[3], dilW, kW, strideW); + + auto resultType = mlir::cast(op.getResult().getType()); + Type resultElementType = resultType.getElementType(); + + constexpr int64_t dim3 = 3; + constexpr int64_t dim4 = 4; + Value initTensor; + if (isBatched) { + SmallVector outputShape{N, C_out, H_out, W_out}; + initTensor = + rewriter.create(loc, outputShape, resultElementType); + } else { + SmallVector outputShape{C_out, H_out, W_out}; + initTensor = + rewriter.create(loc, outputShape, resultElementType); + } + + SmallVector ins; + ins.push_back(input); + ins.push_back(weight); + if (biasValue) { + ins.push_back(biasValue); + } + + // Preserve the user's form: an integer stays a scalar IntegerAttr, a + // 2-element sequence stays [pad_h, pad_w], a 4-element sequence stays + // [pad_top, pad_bottom, pad_left, pad_right]. `pads` (the 4-element + // expansion) is only used for the output-dim computation above. + auto buildParamAttr = [&](const SmallVector &values) -> Attribute { + if (values.size() == 1) + return rewriter.getI64IntegerAttr(values[0]); + return rewriter.getDenseI64ArrayAttr(values); + }; + Attribute strideAttr = buildParamAttr(*stride); + Attribute dilationAttr = buildParamAttr(*dilation); + Attribute paddingAttr = buildParamAttr(*padding); + + auto newOp = rewriter.create( + loc, ins, initTensor, strideAttr, paddingAttr, dilationAttr, + static_cast(groups)); + + rewriter.replaceOp(op, newOp.getResult()); + + return success(); + } +}; +} // namespace + +namespace { +struct TritonToHFusionPass + : public mlir::triton::impl::TritonToHFusionBase { + bool compileOn91095 = false; + void runOnOperation() override; +}; +} // namespace + +void TritonToHFusionPass::runOnOperation() { + auto module = getOperation(); + + // Use greedy pattern rewriter for simpler pattern matching + // Patterns decide themselves whether to convert (via returning + // success/failure) + RewritePatternSet patterns(&getContext()); + // On 950 (910B4/91095), histogram is handled by + // TTOpConverters::HistogramConverter in TritonToLinalg pass. On other + // targets, use the HFusion lowering path. + if (!compileOn91095) { + patterns.add(patterns.getContext()); + } + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + + // Apply patterns with greedy rewriting + // This allows patterns to return failure() without causing pass failure + if (failed(applyPatternsGreedily(module, std::move(patterns)))) { + signalPassFailure(); + } +} + +std::unique_ptr> +mlir::triton::createTritonToHFusionPass(bool compileOn91095) { + auto pass = std::make_unique(); + pass->compileOn91095 = compileOn91095; + return pass; +} diff --git a/third_party/ascend/lib/TritonToHIVM/CMakeLists.txt b/third_party/ascend/lib/TritonToHIVM/CMakeLists.txt new file mode 100644 index 0000000000..52a3d532da --- /dev/null +++ b/third_party/ascend/lib/TritonToHIVM/CMakeLists.txt @@ -0,0 +1,14 @@ +add_triton_library(TritonToHIVM + TritonToHIVM.cpp + + DEPENDS + TritonToHIVMConversionPassIncGen + + LINK_LIBS + BiShengIRHIVMDialect + MLIRIR + MLIRPass + MLIRTransforms + MLIRSupport + TritonIR +) diff --git a/third_party/ascend/lib/TritonToHIVM/TritonToHIVM.cpp b/third_party/ascend/lib/TritonToHIVM/TritonToHIVM.cpp new file mode 100644 index 0000000000..03313e30a2 --- /dev/null +++ b/third_party/ascend/lib/TritonToHIVM/TritonToHIVM.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToHIVM/Passes.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/IR/Attributes.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/LogicalResult.h" + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" + +namespace mlir { +namespace triton { +#define GEN_PASS_DEF_TRITONTOHIVM +#include "ascend/include/TritonToHIVM/Passes.h.inc" +} // namespace triton +} // namespace mlir + +using namespace mlir; +using namespace hivm; + +namespace { + +struct CoreAndPipes { + TCoreTypeAttr core; + PipeAttr producer; + PipeAttr consumer; +}; + +static LogicalResult EmitUnknownOpError(Operation *op, llvm::StringRef opName) { + op->emitError("Unknown custom operation: ") << opName; + return failure(); +} + +static void CreateSyncBlock(PatternRewriter &rewriter, Location loc, + MLIRContext *ctx, Operation *op, int64_t id, + hivm::SyncBlockMode mode, PipeAttr pipe1, + PipeAttr pipe2) { + auto syncMode = hivm::SyncBlockModeAttr::get(ctx, mode); + auto newOp = rewriter.create( + loc, syncMode, rewriter.getI16IntegerAttr(id), Value{}, pipe1, pipe2); + rewriter.replaceOp(op, newOp); +} + +static CoreAndPipes GetCoreAndPipes(MLIRContext *ctx, llvm::StringRef opName, + llvm::StringRef sender) { + // Step 1: Decide pipes + PipeAttr producer; + PipeAttr consumer = PipeAttr::get(ctx, PIPE::PIPE_MTE2); + + if (sender == "cube") { + producer = PipeAttr::get(ctx, PIPE::PIPE_FIX); + } else { + producer = PipeAttr::get(ctx, PIPE::PIPE_MTE3); + } + + // Step 2: Decide core type + TCoreTypeAttr core; + if (sender == "cube") { + if (opName == "sync_block_set") + core = TCoreTypeAttr::get(ctx, TCoreType::CUBE); + else + core = TCoreTypeAttr::get(ctx, TCoreType::VECTOR); + } else { + if (opName == "sync_block_set") + core = TCoreTypeAttr::get(ctx, TCoreType::VECTOR); + else + core = TCoreTypeAttr::get(ctx, TCoreType::CUBE); + } + + return {core, producer, consumer}; +} + +} // end anonymous namespace +namespace { +struct TritonToHIVMPass + : public mlir::triton::impl::TritonToHIVMBase { + void runOnOperation() override; +}; +} // namespace + +struct TritonCustomOpToHIVMSyncOpConversion + : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::ascend::CustomOp op, + PatternRewriter &rewriter) const final { + auto *ctx = op->getContext(); + auto loc = op->getLoc(); + auto args = op.getStrArgs(); + auto argAttr = dyn_cast(args[0]); + auto id = dyn_cast(args[1]).getInt(); + llvm::StringRef opName = op.getOpName(); + llvm::StringRef arg = argAttr.getValue(); + + if (opName == "sync_block_all") { + if (arg == "all_cube") { + CreateSyncBlock(rewriter, loc, ctx, op, id, + hivm::SyncBlockMode::ALL_CUBE, + PipeAttr::get(ctx, PIPE::PIPE_FIX), hivm::PipeAttr{}); + } else if (arg == "all_vector") { + CreateSyncBlock(rewriter, loc, ctx, op, id, + hivm::SyncBlockMode::ALL_VECTOR, hivm::PipeAttr{}, + PipeAttr::get(ctx, PIPE::PIPE_MTE3)); + } else if (arg == "all") { + CreateSyncBlock(rewriter, loc, ctx, op, id, hivm::SyncBlockMode::ALL, + PipeAttr::get(ctx, PIPE::PIPE_FIX), + PipeAttr::get(ctx, PIPE::PIPE_MTE3)); + } else { + return EmitUnknownOpError(op, opName); + } + return success(); + } + + if (opName == "sync_block_set") { + auto [coreAttr, prodPipe, consPipe] = GetCoreAndPipes(ctx, opName, arg); + rewriter.replaceOp(op, rewriter.create( + loc, coreAttr, prodPipe, consPipe, + rewriter.getIndexAttr(id))); + return success(); + } + + if (opName == "sync_block_wait") { + auto [coreAttr, prodPipe, consPipe] = GetCoreAndPipes(ctx, opName, arg); + rewriter.replaceOp(op, rewriter.create( + loc, coreAttr, prodPipe, consPipe, + rewriter.getIndexAttr(id))); + return success(); + } + + return EmitUnknownOpError(op, opName); + } +}; + +void TritonToHIVMPass::runOnOperation() { + auto module = getOperation(); + ConversionTarget target(getContext()); + target.addLegalDialect(); + + RewritePatternSet patterns(&getContext()); + patterns.add(patterns.getContext()); + if (failed(applyPartialConversion(module, target, std::move(patterns)))) { + signalPassFailure(); + } +} + +std::unique_ptr> +mlir::triton::createTritonToHIVMPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToLLVM/CMakeLists.txt b/third_party/ascend/lib/TritonToLLVM/CMakeLists.txt new file mode 100644 index 0000000000..03b1ae1c83 --- /dev/null +++ b/third_party/ascend/lib/TritonToLLVM/CMakeLists.txt @@ -0,0 +1,14 @@ +add_triton_library(TritonToLLVM + NormalizeDebugLineLocations.cpp + TritonToLLVM.cpp + + DEPENDS + TritonToLLVMConversionPassIncGen + + LINK_LIBS + MLIRIR + MLIRPass + MLIRTransforms + MLIRSupport + TritonIR +) diff --git a/third_party/ascend/lib/TritonToLLVM/NormalizeDebugLineLocations.cpp b/third_party/ascend/lib/TritonToLLVM/NormalizeDebugLineLocations.cpp new file mode 100644 index 0000000000..a0e5e20eea --- /dev/null +++ b/third_party/ascend/lib/TritonToLLVM/NormalizeDebugLineLocations.cpp @@ -0,0 +1,676 @@ +#include "TritonToLLVM/Passes.h" + +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/IR/Value.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSwitch.h" + +#include +#include + +namespace mlir { +namespace triton { +#define GEN_PASS_DEF_NORMALIZEDEBUGLINELOCATIONS +#include "ascend/include/TritonToLLVM/Passes.h.inc" +} // namespace triton +} // namespace mlir + +using namespace mlir; + +namespace { + +constexpr llvm::StringLiteral kOriginAttr = "triton.debug_line.origin"; +constexpr llvm::StringLiteral kClassAttr = "triton.debug_line.class"; + +enum class DebugLineLocClass { Semantic, Control, Synthetic }; + +struct SourceLine { + StringAttr file; + unsigned line = 0; + + explicit operator bool() const { return file && line != 0; } + + bool operator==(const SourceLine &rhs) const { + return file == rhs.file && line == rhs.line; + } + + bool operator!=(const SourceLine &rhs) const { return !(*this == rhs); } + + std::string getKey() const { + if (!*this) + return {}; + + std::string key = file.getValue().str(); + key += ":"; + key += std::to_string(line); + return key; + } +}; + +bool isInternalName(StringRef name) { + return name.contains("synthetic") || name.contains("lowering") || + name.contains("helper") || name.contains("tmp") || + name.contains("__") || name.starts_with("llvm.") || + name.starts_with("triton."); +} + +bool hasRealFileLineColLoc(Location loc) { + return static_cast(loc->findInstanceOf()); +} + +Location canonicalizeSourceLoc(Location loc) { + if (isa(loc)) + return loc; + + // If debug scopes were already materialized, keep the fused metadata intact. + if (loc->findInstanceOf>()) + return loc; + + if (auto nameLoc = dyn_cast(loc)) { + Location child = nameLoc.getChildLoc(); + if (hasRealFileLineColLoc(child) && + isInternalName(nameLoc.getName().getValue())) + return child; + } + + return loc; +} + +SourceLine getSourceLine(Location loc) { + if (auto fileLoc = loc->findInstanceOf()) + return {fileLoc.getFilename(), fileLoc.getLine()}; + + return {}; +} + +bool hasSourceLikeLocation(Operation *op) { + Location loc = canonicalizeSourceLoc(op->getLoc()); + return !isa(loc) && hasRealFileLineColLoc(loc); +} + +bool isContainerOp(Operation *op) { + return llvm::StringSwitch(op->getName().getStringRef()) + .Case("builtin.module", true) + .Case("func.func", true) + .Case("llvm.func", true) + .Case("tt.func", true) + .Default(false); +} + +bool isControlOp(Operation *op) { + StringRef name = op->getName().getStringRef(); + + return llvm::StringSwitch(name) + .Case("scf.for", true) + .Case("scf.while", true) + .Case("scf.if", true) + .Case("scf.condition", true) + .Case("cf.br", true) + .Case("cf.cond_br", true) + .Case("cf.switch", true) + .Case("llvm.br", true) + .Case("llvm.cond_br", true) + .Case("llvm.switch", true) + .Case("llvm.return", true) + .Case("func.return", true) + .Default(false); +} + +bool isAlwaysSyntheticOp(Operation *op) { + StringRef name = op->getName().getStringRef(); + // Do not classify memref.copy as always synthetic. In TTIR-to-TTAdapter + // lowering, a user-visible `tt.load` may become a memref.copy-based sequence. + // Keeping memref.copy source locations preserves stable DWARF anchors for + // load lines that otherwise may disappear from the final line table. + return llvm::StringSwitch(name) + .Case("scf.yield", true) + .Case("builtin.unrealized_conversion_cast", true) + .Case("llvm.mlir.undef", true) + .Case("llvm.mlir.zero", true) + .Case("llvm.mlir.constant", true) + .Case("llvm.mlir.poison", true) + .Case("llvm.mlir.none", true) + .Default(false); +} + +bool isTensorOnlyLinalgFillOp(Operation *op) { + if (op->getName().getStringRef() != "linalg.fill") + return false; + + if (op->getNumResults() == 0) + return false; + + return llvm::all_of(op->getResultTypes(), + [](Type type) { return isa(type); }); +} + +bool isExplicitlyMarkedSynthetic(Operation *op) { + if (auto classAttr = op->getAttrOfType(kClassAttr)) + return classAttr.getValue() == "synthetic"; + + for (NamedAttribute attr : op->getAttrs()) { + StringRef name = attr.getName().getValue(); + if ((name.contains("synthetic") || name.contains("lowering")) && + (isa(attr.getValue()) || + (isa(attr.getValue()) && + cast(attr.getValue()).getValue()))) + return true; + } + + return false; +} + +bool isRealMemoryOrCallOp(Operation *op) { + StringRef name = op->getName().getStringRef(); + + return name.ends_with(".load") || name.ends_with(".store") || + name.contains("atomic") || name.ends_with(".call") || + name == "func.call" || name == "llvm.call"; +} + +bool isArithmeticOrCastOp(Operation *op) { + StringRef name = op->getName().getStringRef(); + + return name.starts_with("arith.") || name.ends_with(".add") || + name.ends_with(".sub") || name.ends_with(".mul") || + name.ends_with(".div") || name.ends_with(".rem") || + name.ends_with(".and") || name.ends_with(".or") || + name.ends_with(".xor") || name.ends_with(".shl") || + name.ends_with(".shr") || name.contains(".cmp") || + name.contains("cast") || name.contains("ext") || + name.contains("trunc"); +} + +bool isUserVisibleLoadAnchorOp(Operation *op) { + return op->getName().getStringRef() == "memref.copy"; +} + +bool isUserVisibleStoreAnchorOp(Operation *op) { + return op->getName().getStringRef() == + "bufferization.materialize_in_destination"; +} + +bool isDebugNop(Operation *op) { + auto inlineAsm = dyn_cast(op); + return inlineAsm && inlineAsm.getAsmString() == "nop"; +} + +bool isDestinationViewOp(Operation *op) { + StringRef name = op->getName().getStringRef(); + + return name == "memref.reinterpret_cast" || name == "memref.subview"; +} + +bool isValuePreparationOp(Operation *op) { + StringRef name = op->getName().getStringRef(); + + return llvm::StringSwitch(name) + .Case("arith.constant", true) + .Case("tensor.empty", true) + .Case("tensor.from_elements", true) + .Case("bufferization.alloc_tensor", true) + .Case("bufferization.to_tensor", true) + .Case("bufferization.to_buffer", true) + .Default(false) || + isTensorOnlyLinalgFillOp(op); +} + +bool isFloatingPointExtensionOrTruncationOp(Operation *op) { + return llvm::StringSwitch(op->getName().getStringRef()) + .Case("arith.extf", true) + .Case("arith.truncf", true) + .Default(false); +} + +bool isAddressComputationOp(Operation *op) { + StringRef name = op->getName().getStringRef(); + + if (isFloatingPointExtensionOrTruncationOp(op)) + return false; + + return llvm::StringSwitch(name) + .Case("memref.reinterpret_cast", true) + .Case("memref.subview", true) + .Case("memref.memory_space_cast", true) + .Default(false) || + name.contains("cast") || name.contains("ext") || + name.contains("trunc"); +} + +// `previousLine` is the last seen source line of a semantic or control +// operation. A semantic arithmetic operation that points to an earlier source +// line was likely reordered by lowering, so treat it as synthetic. +bool isReorderedBackwardStep(Operation *op, DebugLineLocClass locClass, + SourceLine previousLine) { + if (locClass != DebugLineLocClass::Semantic || !previousLine) + return false; + + SourceLine line = getSourceLine(canonicalizeSourceLoc(op->getLoc())); + if (!line || line.file != previousLine.file || line.line >= previousLine.line) + return false; + + return isArithmeticOrCastOp(op); +} + +std::optional getUniqueStoreLoc(llvm::ArrayRef candidates) { + std::optional uniqueLoc; + + for (Location candidate : candidates) { + Location canonicalLoc = canonicalizeSourceLoc(candidate); + if (!uniqueLoc) { + uniqueLoc = canonicalLoc; + continue; + } + + if (*uniqueLoc != canonicalLoc) + return std::nullopt; + } + + return uniqueLoc; +} + +void collectUserVisibleStoreLocsForDestinationView( + Operation *view, llvm::SmallVectorImpl &candidates) { + if (!isDestinationViewOp(view) || view->getNumResults() != 1) + return; + + SourceLine viewLine = getSourceLine(canonicalizeSourceLoc(view->getLoc())); + if (!viewLine) + return; + + Value result = view->getResult(0); + + for (Operation *user : result.getUsers()) { + if (!isUserVisibleStoreAnchorOp(user)) + continue; + + SourceLine userLine = getSourceLine(canonicalizeSourceLoc(user->getLoc())); + + if (!userLine || userLine != viewLine) + continue; + + for (OpOperand &operand : user->getOpOperands()) { + if (operand.get() != result) + continue; + + // bufferization.materialize_in_destination: + // operand #0 = source tensor/value + // operand #1 = destination memref/view + if (operand.getOperandNumber() == 1) + candidates.push_back(user->getLoc()); + } + } +} + +void collectUserVisibleStoreLocsThroughDestinationView( + Operation *op, llvm::SmallVectorImpl &candidates) { + if (op->getNumResults() != 1) + return; + + Value result = op->getResult(0); + + for (Operation *viewUser : result.getUsers()) { + collectUserVisibleStoreLocsForDestinationView(viewUser, candidates); + } +} + +void collectUserVisibleStoreLocsThroughTensorInsert( + Operation *op, llvm::SmallVectorImpl &candidates) { + if (op->getNumResults() != 1) + return; + + Value result = op->getResult(0); + + for (Operation *insertUser : result.getUsers()) { + if (insertUser->getName().getStringRef() != "tensor.insert") + continue; + + // tensor.insert: + // operand #0 = scalar/value being inserted + if (insertUser->getNumOperands() == 0 || + insertUser->getOperand(0) != result) + continue; + + if (insertUser->getNumResults() != 1) + continue; + + SourceLine insertLine = + getSourceLine(canonicalizeSourceLoc(insertUser->getLoc())); + if (!insertLine) + continue; + + Value insertedTensor = insertUser->getResult(0); + + for (Operation *storeUser : insertedTensor.getUsers()) { + if (!isUserVisibleStoreAnchorOp(storeUser)) + continue; + + SourceLine storeLine = + getSourceLine(canonicalizeSourceLoc(storeUser->getLoc())); + + if (!storeLine || storeLine != insertLine) + continue; + + for (OpOperand &operand : storeUser->getOpOperands()) { + if (operand.get() != insertedTensor) + continue; + + // bufferization.materialize_in_destination: + // operand #0 = source tensor/value + // operand #1 = destination memref/view + if (operand.getOperandNumber() == 0) + candidates.push_back(storeUser->getLoc()); + } + } + } +} + +DebugLineLocClass +classifyOperation(Operation *op, + const llvm::StringMap &semanticAnchors) { + if (isContainerOp(op)) + return DebugLineLocClass::Semantic; + + if (isControlOp(op)) + return DebugLineLocClass::Control; + + if (isExplicitlyMarkedSynthetic(op)) + return DebugLineLocClass::Synthetic; + + if (!hasSourceLikeLocation(op)) + return DebugLineLocClass::Synthetic; + + if (isUserVisibleLoadAnchorOp(op) || isUserVisibleStoreAnchorOp(op)) + return DebugLineLocClass::Semantic; + + if (isRealMemoryOrCallOp(op)) + return DebugLineLocClass::Semantic; + + if (isValuePreparationOp(op)) + return DebugLineLocClass::Synthetic; + + SourceLine line = getSourceLine(canonicalizeSourceLoc(op->getLoc())); + bool hasSemanticAnchorOnSameLine = + line && semanticAnchors.lookup(line.getKey()) > 0; + + if (isFloatingPointExtensionOrTruncationOp(op)) + return hasSemanticAnchorOnSameLine ? DebugLineLocClass::Synthetic + : DebugLineLocClass::Semantic; + + if (isAddressComputationOp(op)) + return DebugLineLocClass::Synthetic; + + if (isArithmeticOrCastOp(op)) { + if (hasSemanticAnchorOnSameLine) + return DebugLineLocClass::Synthetic; + return DebugLineLocClass::Semantic; + } + + if (isAlwaysSyntheticOp(op)) + return DebugLineLocClass::Synthetic; + + return DebugLineLocClass::Semantic; +} + +StringRef stringifyClass(DebugLineLocClass locClass) { + switch (locClass) { + case DebugLineLocClass::Semantic: + return "semantic"; + case DebugLineLocClass::Control: + return "control"; + case DebugLineLocClass::Synthetic: + return "synthetic"; + } + llvm_unreachable("unknown debug line location class"); +} + +Location makeSyntheticDebugLoc(Operation *op, Location originalLoc) { + MLIRContext *context = op->getContext(); + + Location sourceLoc = canonicalizeSourceLoc(originalLoc); + + if (auto fileLoc = sourceLoc->findInstanceOf()) { + // Do not use UnknownLoc here: downstream LLVM/Ascend debug-info lowering + // expects a file-like location for some operations and may fail on + // UnknownLoc. + // + // DWARF line 0 is used as a non-user source anchor: it preserves a valid + // file identity while preventing synthetic/helper ops from being associated + // with real Python source lines. + return FileLineColLoc::get(context, fileLoc.getFilename().getValue(), 0, 0); + } + + return originalLoc; +} + +void preserveOrigin(Operation *op, Location originalLoc) { + if (!isa(originalLoc) && !op->hasAttr(kOriginAttr)) + op->setAttr(kOriginAttr, originalLoc); +} + +void markRetargetedSyntheticOp(Operation *op, Location originalLoc, + Location storeLoc) { + MLIRContext *context = op->getContext(); + + op->setAttr( + kClassAttr, + StringAttr::get(context, stringifyClass(DebugLineLocClass::Semantic))); + preserveOrigin(op, originalLoc); + op->setLoc(storeLoc); +} + +bool retargetSyntheticOpToStoreLoc(Operation *op, Location originalLoc) { + llvm::SmallVector candidates; + + if (isAddressComputationOp(op) || isArithmeticOrCastOp(op)) { + collectUserVisibleStoreLocsForDestinationView(op, candidates); + collectUserVisibleStoreLocsThroughDestinationView(op, candidates); + } + + if (isArithmeticOrCastOp(op)) + collectUserVisibleStoreLocsThroughTensorInsert(op, candidates); + + if (std::optional storeLoc = getUniqueStoreLoc(candidates)) { + SourceLine originalLine = getSourceLine(canonicalizeSourceLoc(originalLoc)); + SourceLine targetLine = getSourceLine(canonicalizeSourceLoc(*storeLoc)); + + if (!originalLine || !targetLine || originalLine != targetLine) + return false; + + markRetargetedSyntheticOp(op, originalLoc, *storeLoc); + return true; + } + + return false; +} + +void markOperation(Operation *op, DebugLineLocClass locClass) { + MLIRContext *context = op->getContext(); + + Location originalLoc = op->getLoc(); + Location loc = canonicalizeSourceLoc(originalLoc); + + op->setAttr(kClassAttr, StringAttr::get(context, stringifyClass(locClass))); + + if (locClass == DebugLineLocClass::Synthetic) { + if (retargetSyntheticOpToStoreLoc(op, originalLoc)) + return; + + preserveOrigin(op, originalLoc); + + op->setLoc(makeSyntheticDebugLoc(op, originalLoc)); + return; + } + + if (loc != originalLoc) + op->setLoc(loc); +} + +void collectSemanticAnchors(Block &block, + llvm::StringMap &semanticAnchors) { + for (Operation &op : block) { + if (isContainerOp(&op) || isControlOp(&op) || + isExplicitlyMarkedSynthetic(&op) || !hasSourceLikeLocation(&op)) + continue; + + if (!isUserVisibleLoadAnchorOp(&op) && !isUserVisibleStoreAnchorOp(&op) && + !isRealMemoryOrCallOp(&op)) + continue; + + if (SourceLine line = getSourceLine(canonicalizeSourceLoc(op.getLoc()))) + ++semanticAnchors[line.getKey()]; + } +} + +void relocateLoadDebugNops(Operation *root) { + llvm::StringMap loadAnchors; + llvm::StringMap ambiguousLoadLines; + + root->walk([&](Operation *op) { + if (!isUserVisibleLoadAnchorOp(op)) + return; + + SourceLine line = getSourceLine(canonicalizeSourceLoc(op->getLoc())); + if (!line) + return; + + std::string key = line.getKey(); + auto [it, inserted] = loadAnchors.try_emplace(key, op); + if (!inserted && it->second != op) + ambiguousLoadLines[key] = true; + }); + + struct Relocation { + Operation *nop; + Operation *anchor; + }; + + llvm::SmallVector relocations; + llvm::StringMap scheduledLines; + + root->walk([&](Operation *op) { + if (!isDebugNop(op)) + return; + + SourceLine line = getSourceLine(canonicalizeSourceLoc(op->getLoc())); + if (!line) + return; + + std::string key = line.getKey(); + if (ambiguousLoadLines.lookup(key) || scheduledLines.lookup(key)) + return; + + auto anchorIt = loadAnchors.find(key); + if (anchorIt == loadAnchors.end()) + return; + + Operation *anchor = anchorIt->second; + + // If the NOP is already immediately before the load anchor, keep it. + if (op->getBlock() == anchor->getBlock() && op->getNextNode() == anchor) { + scheduledLines[key] = true; + return; + } + + scheduledLines[key] = true; + relocations.push_back({op, anchor}); + }); + + // A debug NOP has no operands or results, so it is safe to move it across + // blocks within the same function and place it immediately before the + // corresponding memref.copy. + for (const Relocation &relocation : relocations) + relocation.nop->moveBefore(relocation.anchor); +} + +void demoteRedundantStoreDebugNops(Operation *root) { + llvm::StringMap storeAnchorLines; + + root->walk([&](Operation *op) { + if (!isUserVisibleStoreAnchorOp(op)) + return; + + if (SourceLine line = getSourceLine(canonicalizeSourceLoc(op->getLoc()))) + ++storeAnchorLines[line.getKey()]; + }); + + llvm::SmallVector redundantNops; + root->walk([&](Operation *op) { + if (!isDebugNop(op)) + return; + + SourceLine line = getSourceLine(canonicalizeSourceLoc(op->getLoc())); + if (!line || storeAnchorLines.lookup(line.getKey()) == 0) + return; + + redundantNops.push_back(op); + }); + + for (Operation *nop : redundantNops) + markOperation(nop, DebugLineLocClass::Synthetic); +} + +struct NormalizeDebugLineLocationsPass + : public triton::impl::NormalizeDebugLineLocationsBase< + NormalizeDebugLineLocationsPass> { + void processBlock(Block &block) { + llvm::StringMap semanticAnchors; + collectSemanticAnchors(block, semanticAnchors); + + SourceLine previousLine; + for (Operation &op : block) { + if (isContainerOp(&op)) + continue; + + DebugLineLocClass locClass = classifyOperation(&op, semanticAnchors); + if (isReorderedBackwardStep(&op, locClass, previousLine)) + locClass = DebugLineLocClass::Synthetic; + + markOperation(&op, locClass); + + if (locClass == DebugLineLocClass::Semantic || + locClass == DebugLineLocClass::Control) { + if (SourceLine line = getSourceLine(op.getLoc())) + previousLine = line; + } + } + } + + void processRegions(Operation *op) { + for (Region ®ion : op->getRegions()) { + for (Block &block : region) { + processBlock(block); + for (Operation &nestedOp : block) + processRegions(&nestedOp); + } + } + } + + void runOnOperation() override { + Operation *root = getOperation().getOperation(); + + relocateLoadDebugNops(root); + + processRegions(root); + + demoteRedundantStoreDebugNops(root); + } +}; + +} // namespace + +std::unique_ptr> +mlir::triton::createNormalizeDebugLineLocationsPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToLLVM/TritonToLLVM.cpp b/third_party/ascend/lib/TritonToLLVM/TritonToLLVM.cpp new file mode 100644 index 0000000000..3eee21de82 --- /dev/null +++ b/third_party/ascend/lib/TritonToLLVM/TritonToLLVM.cpp @@ -0,0 +1,269 @@ +#include "TritonToLLVM/Passes.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Attributes.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/LogicalResult.h" + +namespace mlir { +namespace triton { +#define GEN_PASS_DEF_TRITONTOLLVM +#include "ascend/include/TritonToLLVM/Passes.h.inc" +} // namespace triton +} // namespace mlir + +using namespace mlir; + +namespace { +struct TritonToLLVMPass + : public mlir::triton::impl::TritonToLLVMBase { + void runOnOperation() override; +}; +} // namespace + +namespace { + +static Type getElementType(Value value) { + auto type = value.getType(); + if (auto tensorType = dyn_cast(type)) + return tensorType.getElementType(); + return type; +} + +static int64_t getTensorNumElements(Value tensor) { + auto type = mlir::cast(tensor.getType()); + return type.getNumElements(); +} + +static Value getInt32Value(RewriterBase &rewriter, Location loc, int val) { + Type ty = rewriter.getI32Type(); + return rewriter.create(loc, ty, + rewriter.getIntegerAttr(ty, val)); +} + +// If operand size is smaller than 32 bits, pack in groups of 32 bits. +SmallVector packOperands(mlir::triton::ElementwiseInlineAsmOp op, + const SmallVector> &operands, + RewriterBase &rewriter, Location loc) { + SmallVector packedOperands; + unsigned numPackedElements = op.getPackedElement(); + for (int i = 0, e = op.getNumOperands(); i < e; i++) { + Type elemTy = getElementType(op.getOperand(i)); + unsigned bitWidth = + elemTy.isIntOrFloat() ? elemTy.getIntOrFloatBitWidth() : 64; + unsigned numElementPerReg = std::max(32 / bitWidth, 1u); + numElementPerReg = std::min(numElementPerReg, numPackedElements); + for (int j = 0; j < numPackedElements; j += numElementPerReg) { + if (numElementPerReg == 1) { + packedOperands.push_back(operands[j][i]); + continue; + } + Type t = VectorType::get(numElementPerReg, elemTy); + Value packed = rewriter.create(loc, t); + for (int k = 0; k < numElementPerReg; k++) { + packed = rewriter.create( + loc, packed, operands[j + k][i], getInt32Value(rewriter, loc, k)); + } + packedOperands.push_back(packed); + } + } + return packedOperands; +} + +static SmallVector unpackElements(Location loc, Value packedValues, + RewriterBase &rewriter) { + auto type = mlir::cast(packedValues.getType()); + auto elementType = type.getElementType(); + auto shape = type.getShape(); + + int64_t numElements = type.getNumElements(); + + SmallVector result; + for (int64_t linearIdx = 0; linearIdx < numElements; linearIdx++) { + SmallVector indexes(shape.size()); + int64_t remaining = linearIdx; + for (int64_t dim = shape.size() - 1; dim >= 0; dim--) { + indexes[dim] = + rewriter.create(loc, remaining % shape[dim]); + remaining /= shape[dim]; + } + Value extracted = rewriter.create(loc, elementType, + packedValues, indexes); + result.push_back(extracted); + } + + return result; +} + +static SmallVector> +createDestOps(triton::ElementwiseInlineAsmOp op, RewriterBase &rewriter, + const SmallVector> operands, Location loc) { + auto ctx = op->getContext(); + if (operands.size() % op.getPackedElement() != 0) + llvm::report_fatal_error("Inline asm op has more packed elements than " + "number of elements per thread."); + + // Pack elems smaller than 32 bits into 32-bit registers. + SmallVector packedOperands = packOperands(op, operands, rewriter, loc); + + // Types returned by the LLVM asm op. If there's more than one, they'll be + // wrapped in a type tuple. + SmallVector asmRetTypes; + for (auto result : op.getResult()) { + auto ty = getElementType(result); + + // Pack return elements into 32-bits. + unsigned bitWidth = ty.isIntOrFloat() ? ty.getIntOrFloatBitWidth() : 64; + unsigned numElemsPerReg = + std::min(std::max(32 / bitWidth, 1u), op.getPackedElement()); + assert(op.getPackedElement() % numElemsPerReg == 0); + if (numElemsPerReg > 1) { + ty = VectorType::get(numElemsPerReg, ty); + } + for (unsigned i = 0; i < op.getPackedElement() / numElemsPerReg; i++) { + asmRetTypes.push_back(ty); + } + } + Type asmRetType = asmRetTypes.size() > 1 + ? LLVM::LLVMStructType::getLiteral(ctx, asmRetTypes) + : asmRetTypes[0]; + + Value asmResults = + rewriter + .create( + loc, asmRetType, + /*operands=*/packedOperands, + /*asm_string=*/op.getAsmString(), + /*constraints=*/op.getConstraints(), + /*has_side_effects=*/!op.getPure(), + /*is_align_stack=*/false, + /*tail_call_kind=*/LLVM::tailcallkind::TailCallKind::None, + /*asm_dialect=*/ + LLVM::AsmDialectAttr::get(rewriter.getContext(), + LLVM::AsmDialect::AD_ATT), + /*operand_attrs=*/ArrayAttr()) + ->getResult(0); + + // asmResults is a flat struct; pack its values into + // [return_value][op.getPackedElement()]. + SmallVector> ret(op->getNumResults()); + int structIdx = 0; + for (int i = 0; i < op->getNumResults(); i++) { + for (int j = 0; j < op.getPackedElement(); j++) { + Value val; + if (asmRetTypes.size() > 1) { + val = + rewriter.create(loc, asmResults, structIdx++); + } else { + val = asmResults; + } + if (auto vectorTy = dyn_cast(val.getType())) { + for (int k = 0; k < vectorTy.getNumElements(); k++) { + ret[i].push_back(rewriter.create( + loc, val, getInt32Value(rewriter, loc, k))); + } + j += vectorTy.getNumElements() - 1; + } else { + ret[i].push_back(val); + } + } + } + return ret; +} + +static LogicalResult processScalarInlineAsm(triton::ElementwiseInlineAsmOp op, + PatternRewriter &rewriter) { + Location loc = op.getLoc(); + + auto outsWrapped = createDestOps(op, rewriter, {}, loc); + + SmallVector outs; + for (const auto &resWrapped : outsWrapped) { + outs.push_back(resWrapped[0]); + } + rewriter.replaceOp(op, outs); + + return success(); +} + +static LogicalResult processVectorInlineAsm(triton::ElementwiseInlineAsmOp op, + PatternRewriter &rewriter) { + Location loc = op.getLoc(); + + SmallVector> unpackedOperands; + for (auto operand : op.getOperands()) { + auto unpackedOperand = unpackElements(loc, operand, rewriter); + unpackedOperands.push_back(unpackedOperand); + } + + int64_t resultLength = getTensorNumElements(op->getResult(0)); + if (resultLength % op.getPackedElement()) { + op.emitError("Result tensor should be diveded to pack"); + return failure(); + } + + SmallVector> unpackedResults(op->getNumResults()); + for (int64_t i = 0; i < resultLength; i += op.getPackedElement()) { + // Block of elements to process with one call to the inline asm. This is + // ordered opposite `unpackedResults`: The outer dim is + // op.getPackedElement(), and the inner dim is the operand. + SmallVector> block(op.getPackedElement()); + for (auto &os : unpackedOperands) { + for (int j = 0; j < op.getPackedElement(); j++) { + block[j].push_back(os[i + j]); + } + } + auto cur = createDestOps(op, rewriter, block, loc); + assert(cur.size() == unpackedResults.size()); + for (unsigned j = 0; j < cur.size(); j++) { + unpackedResults[j].insert(unpackedResults[j].end(), cur[j].begin(), + cur[j].end()); + } + } + // Reorder and pack the results. + SmallVector outs; + for (int i = 0; i < unpackedResults.size(); i++) { + outs.push_back(rewriter.create( + loc, op->getResult(i).getType(), unpackedResults[i])); + } + rewriter.replaceOp(op, outs); + + return success(); +} + +} // namespace + +struct ElementwiseInlineAsmOpConversion + : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::ElementwiseInlineAsmOp op, + PatternRewriter &rewriter) const final { + return op.getOperands().empty() ? processScalarInlineAsm(op, rewriter) + : processVectorInlineAsm(op, rewriter); + } +}; + +void TritonToLLVMPass::runOnOperation() { + auto module = getOperation(); + ConversionTarget target(getContext()); + target.addLegalDialect(); + + RewritePatternSet patterns(&getContext()); + patterns.add(patterns.getContext()); + if (failed(applyPartialConversion(module, target, std::move(patterns)))) { + signalPassFailure(); + } +} + +std::unique_ptr> +mlir::triton::createTritonToLLVMPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToLinalg/ArgMinMaxConverter.cpp b/third_party/ascend/lib/TritonToLinalg/ArgMinMaxConverter.cpp new file mode 100644 index 0000000000..4362e11818 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/ArgMinMaxConverter.cpp @@ -0,0 +1,237 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/ArgMinMaxConverter.h" +#include +#include + +namespace TTOpConverters { +using namespace mlir; +using namespace triton; + +// ArgMinConverter functions +LogicalResult ArgMinConverter::matchComparisonResult( + Value currValue, Value currIndex, Value reduceValue, Value reduceIndex, + mlir::Block::iterator &it, Value &comparisonResult) { + LLVM_DEBUG(llvm::dbgs() << "Matching: " << *it << "\n"); + + auto cmpOp = dyn_cast(*it); + auto cmpIOp = dyn_cast(*it++); + if (!cmpOp && !cmpIOp) + return failure(); + + if (cmpOp) { + if (cmpOp.getPredicate() != arith::CmpFPredicate::OLT || + currValue != cmpOp.getLhs() || reduceValue != cmpOp.getRhs()) { + return failure(); + } + comparisonResult = cmpOp; + } + + if (cmpIOp) { + if ((cmpIOp.getPredicate() != arith::CmpIPredicate::slt && + cmpIOp.getPredicate() != arith::CmpIPredicate::ult) || + currValue != cmpIOp.getLhs() || reduceValue != cmpIOp.getRhs()) { + return failure(); + } + comparisonResult = cmpIOp; + } + + return success(); +} + +float ArgMinConverter::getBaseReductionValue() { + return std::numeric_limits::infinity(); +} + +int8_t ArgMinConverter::getBaseReductionIntValue() { + return std::numeric_limits::max(); +} +uint8_t ArgMinConverter::getBaseReductionUIntValue() { + return std::numeric_limits::max(); +} + +// ArgMaxConverter functions +LogicalResult ArgMaxConverter::matchComparisonResult( + Value currValue, Value currIndex, Value reduceValue, Value reduceIndex, + mlir::Block::iterator &it, Value &comparisonResult) { + auto cmpOp = dyn_cast(*it); + auto cmpIOp = dyn_cast(*it++); + if (!cmpOp && !cmpIOp) + return failure(); + + if (cmpOp) { + if (cmpOp.getPredicate() != arith::CmpFPredicate::OGT || + currValue != cmpOp.getLhs() || reduceValue != cmpOp.getRhs()) { + return failure(); + } + comparisonResult = cmpOp; + } + + if (cmpIOp) { + if ((cmpIOp.getPredicate() != arith::CmpIPredicate::sgt && + cmpIOp.getPredicate() != arith::CmpIPredicate::ugt) || + currValue != cmpIOp.getLhs() || reduceValue != cmpIOp.getRhs()) { + return failure(); + } + comparisonResult = cmpIOp; + } + + return success(); +} + +float ArgMaxConverter::getBaseReductionValue() { + return -std::numeric_limits::infinity(); +} + +int8_t ArgMaxConverter::getBaseReductionIntValue() { + return std::numeric_limits::min(); +} +uint8_t ArgMaxConverter::getBaseReductionUIntValue() { + return std::numeric_limits::min(); +} + +} // namespace TTOpConverters + +namespace { +struct FoldOneHotGatherAfterReduceWithIndex + : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(linalg::ReduceOp op, + PatternRewriter &rewriter) const final { + if (op->getAttrOfType("reduce_mode")) + return failure(); + if (op.getNumResults() != 1 || op.getInputs().size() != 1) + return failure(); + if (!isAddFReduction(op)) + return failure(); + + auto mulOp = op.getInputs()[0].getDefiningOp(); + if (!mulOp) + return failure(); + + Value logits; + Value maskAsFloat; + if (mulOp.getRhs().getDefiningOp()) { + logits = mulOp.getLhs(); + maskAsFloat = mulOp.getRhs(); + } else if (mulOp.getLhs().getDefiningOp()) { + logits = mulOp.getRhs(); + maskAsFloat = mulOp.getLhs(); + } else { + return failure(); + } + + auto castOp = maskAsFloat.getDefiningOp(); + if (!castOp) + return failure(); + + auto cmpOp = castOp.getIn().getDefiningOp(); + if (!cmpOp || cmpOp.getPredicate() != arith::CmpIPredicate::eq) + return failure(); + + Value lhsBase = stripShapeAndBroadcast(cmpOp.getLhs()); + Value rhsBase = stripShapeAndBroadcast(cmpOp.getRhs()); + + linalg::ReduceOp reduceWithIndex = getMaxWithIndexFromIndexValue(lhsBase); + Value arangeBase = rhsBase; + if (!reduceWithIndex) { + reduceWithIndex = getMaxWithIndexFromIndexValue(rhsBase); + arangeBase = lhsBase; + } + if (!reduceWithIndex) + return failure(); + + if (reduceWithIndex.getNumResults() != 2 || + reduceWithIndex.getInputs().size() < 2) + return failure(); + + Value reduceIndexBase = + stripShapeAndBroadcast(reduceWithIndex.getInputs()[1]); + if (arangeBase != reduceIndexBase) + return failure(); + if (op.getDimensionsAttr() != reduceWithIndex.getDimensionsAttr()) + return failure(); + + Value maxInput = reduceWithIndex.getInputs()[0]; + auto selectOp = maxInput.getDefiningOp(); + if (selectOp && selectOp.getTrueValue() != logits && + selectOp.getFalseValue() != logits) + return failure(); + if (!selectOp && maxInput != logits) + return failure(); + + if (op.getResult(0).getType() != reduceWithIndex.getResult(0).getType()) + return failure(); + + rewriter.replaceOp(op, reduceWithIndex.getResult(0)); + return success(); + } + +private: + static bool isAddFReduction(linalg::ReduceOp op) { + Region ®ion = op.getRegion(); + if (!region.hasOneBlock()) + return false; + Block &block = region.front(); + auto yieldOp = dyn_cast(block.getTerminator()); + if (!yieldOp || yieldOp.getNumOperands() != 1) + return false; + return yieldOp.getOperand(0).getDefiningOp() != nullptr; + } + + static Value stripShapeAndBroadcast(Value value) { + while (Operation *def = value.getDefiningOp()) { + if (isa(def) || + def->getName().getStringRef() == "linalg.broadcast") { + if (def->getNumOperands() == 0) + break; + value = def->getOperand(0); + continue; + } + break; + } + return value; + } + + static linalg::ReduceOp getMaxWithIndexFromIndexValue(Value value) { + auto result = dyn_cast(value); + if (!result || result.getResultNumber() != 1) + return nullptr; + + auto reduceOp = dyn_cast(result.getOwner()); + if (!reduceOp) + return nullptr; + + auto reduceMode = reduceOp->getAttrOfType("reduce_mode"); + if (!reduceMode || reduceMode != "max_with_index") + return nullptr; + return reduceOp; + } +}; +} // namespace + +void TTOpConverters::populatePostConversionCanonicalizationPatterns( + RewritePatternSet &patterns) { + patterns.add(patterns.getContext()); +} diff --git a/third_party/ascend/lib/TritonToLinalg/BlockPtrAnalysis.cpp b/third_party/ascend/lib/TritonToLinalg/BlockPtrAnalysis.cpp new file mode 100644 index 0000000000..6f5b5c7e67 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/BlockPtrAnalysis.cpp @@ -0,0 +1,3291 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/BlockPtrAnalysis.h" +#include "ascend/include/TritonControlFlowOpt/ControlFlowRewrite.h" +#include "ascend/include/TritonToLinalg/TritonToLinalgPass.h" +#include "ascend/include/Utils/DebugUtils.h" +#include "ascend/include/Utils/Utils.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/IR/Types.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/Transforms/DialectConversion.h" +#include "triton/Dialect/Triton/IR/Types.h" + +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/FormatVariadic.h" +#include +#include +#include + +#define DEBUG_TYPE "triton-block-ptr-analysis" +namespace mlir { +namespace triton { + +hivm::PointerCastOp createScalarPointerCast(OpBuilder &builder, Location loc, + MemRefType resultType, + Value address) { + SmallVector dynamicSizes; + if (resultType.getNumDynamicDims() != 0) { + Value defaultSize = builder.create(loc, 1); + dynamicSizes.assign(resultType.getNumDynamicDims(), defaultSize); + } + auto pointerCast = builder.create( + loc, resultType, ValueRange{address}, ValueRange{dynamicSizes}); + pointerCast->setAttr(kScalarPointerCarrierAttr, + UnitAttr::get(builder.getContext())); + return pointerCast; +} + +// Recognize original scalar-pointer producers whose converted result may act +// as a memref carrier. The parse site still requires BaseMemRefType, so merely +// appearing in this list never makes an unconverted pointer an opaque source. +static bool isScalarPointerTransport(Operation *op) { + return op && isa(op); +} + +// Returns true only for sources known to carry a complete scalar integer +// address. Before dialect conversion the source is tt.int_to_ptr; after +// IntToPtrConverter it is a memref-producing HIVM PointerCast with explicit +// ScalarPointerCarrier provenance. Ordinary PointerCast descriptors must keep +// using the existing unstructured fallback. +static bool isScalarPointerCarrierSource(Value source) { + if (!source) + return false; + if (source.getDefiningOp()) + return true; + auto pointerCast = source.getDefiningOp(); + return pointerCast && pointerCast->hasAttr(kScalarPointerCarrierAttr); +} + +// Dialect conversion may temporarily materialize a scalar Triton pointer from +// a memref carrier so that an operation which has not been rewritten yet can +// keep its original type. Scalar pointer transport must consume the carrier +// directly; otherwise the conversion driver is forced to leave a live +// memref-to-!tt.ptr cast next to tt.load/tt.store. Only peel the narrow, +// one-to-one UCC form whose input is already a BaseMemRefType. Unknown casts, +// multi-value casts, and non-memref inputs remain unsupported and therefore +// continue through the existing diagnostics. +static Value unwrapScalarPointerMemRefCarrier(Value value) { + while (auto castOp = value.getDefiningOp()) { + if (castOp.getInputs().size() != 1 || castOp.getOutputs().size() != 1) + break; + Value input = castOp.getInputs().front(); + Value output = castOp.getOutputs().front(); + if (!isa(input.getType()) || + !isa(output.getType())) + break; + value = input; + } + return value; +} + +// MemAccType selectMaxMemAccTy(const MemAccType &v1, const MemAccType &v2) { +// return (v1 > v2) ? v1 : v2; +// } + +SmallVector &BlockData::getOffsetsRef() { return this->offsets; } + +SmallVector &BlockData::getSizesRef() { return this->sizes; } + +SmallVector &BlockData::getStridesRef() { return this->strides; } + +Value &BlockData::getSourceRef() { return this->source; } + +OpFoldResult &BlockData::getScalarRef() { return this->scalar; } + +SmallVector BlockData::getOffsets() const { + return this->offsets; +} + +SmallVector BlockData::getSizes() const { return this->sizes; } + +SmallVector BlockData::getStrides() const { + return this->strides; +} + +OpFoldResult BlockData::getOffset(int index) const { + return this->offsets[index]; +} + +OpFoldResult BlockData::getSize(int index) const { return this->sizes[index]; } + +OpFoldResult BlockData::getStride(int index) const { + return this->strides[index]; +} + +OpFoldResult BlockData::getScalar() const { return this->scalar; } + +Value BlockData::getSource() const { return this->source; } + +MemAccType BlockData::getMemAccType() const { return this->memAccTy; }; + +MemAccType &BlockData::getMemAccTypeRef() { return this->memAccTy; }; + +bool BlockData::isScalar() const { return !(this->scalar).isNull(); } + +bool BlockData::isEmpty() const { + return !(this->getRank() || this->source || !(this->scalar).isNull()); +} + +bool BlockData::hasSource() const { return this->source != nullptr; } + +void BlockData::removeSource() { this->source = nullptr; }; + +bool BlockData::hasResElemTy() const { return this->resElemTy != nullptr; } + +Type &BlockData::getResElemTyRef() { return this->resElemTy; } + +Type BlockData::getResElemTy() const { return this->resElemTy; } + +int64_t BlockData::getRank() const { + assert(offsets.size() == sizes.size() && offsets.size() == strides.size()); + return this->offsets.size(); +} + +void BlockData::setResElemTy(const Type &Ty) { this->resElemTy = Ty; } + +void BlockData::setScalar(const OpFoldResult &scalar) { this->scalar = scalar; } + +void BlockData::setSource(const Value &src) { this->source = src; } + +void BlockData::setOffsets(const SmallVector &offsets) { + this->offsets = offsets; +} + +void BlockData::setStrides(const SmallVector &strides) { + this->strides = strides; +} + +void BlockData::setSizes(const SmallVector &szs) { + this->sizes = szs; +} + +void BlockData::setMemAccTy(const MemAccType &v) { this->memAccTy = v; } + +void BlockData::setMemAccVal(const MemAccVal v) { this->memAccTy.value = v; } + +OpFoldResult BlockData::inferBlockOffset(const Location &loc, + OpBuilder &builder) const { + OpFoldResult retOffset = builder.getIndexAttr(0); + for (auto ofr : offsets) { + retOffset = + addOpFoldResult(retOffset, ofr, loc, builder, builder.getIndexType()); + } + return retOffset; +} + +FailureOr +BlockData::getResultMemrefType(int64_t offset, + ArrayRef resultShape) const { + SmallVector staticStrides; + SmallVector dynamicStrides; + dispatchIndexOpFoldResults(strides, dynamicStrides, staticStrides); + + if (!this->source) + return failure(); + auto baseMemrefType = dyn_cast(this->source.getType()); + if (!baseMemrefType) + return failure(); + auto elementType = baseMemrefType.getElementType(); + auto layout = + StridedLayoutAttr::get(this->source.getContext(), offset, staticStrides); + return MemRefType::get(resultShape, elementType, layout); +} + +void BlockData::addBlock(BlockData &lBlock, BlockData &rBlock, Location loc, + ConversionPatternRewriter &rewriter) { + assert(this->isEmpty() && lBlock.getRank() == rBlock.getRank()); + // When both left block and right block have source, it is indirect load. + assert(!(lBlock.hasSource() && rBlock.hasSource()) && + "Don't support each BlockData has own base source pointer"); + this->source = + lBlock.hasSource() ? lBlock.getSourceRef() : rBlock.getSourceRef(); + + assert(!(lBlock.hasResElemTy() && rBlock.hasResElemTy())); + if (lBlock.hasResElemTy()) { + assert(lBlock.hasSource()); + this->resElemTy = lBlock.getResElemTyRef(); + } else if (rBlock.hasResElemTy()) { + assert(rBlock.hasSource()); + this->resElemTy = rBlock.getResElemTyRef(); + } + + // Acctually `scalar` should be accumulated into `offset` and `stride` finally + // In addBlock, just pass `scalar` when: + // 1. both lhs and rhs have `scalar` + // 2. otherwise, both lhs and rhs are scalar type with rank 0 + // Except above, original `scalar` has been fused into `offset` under add. + if (lBlock.isScalar() && rBlock.isScalar()) { + auto addScalar = + addOpFoldResult(lBlock.getScalarRef(), rBlock.getScalarRef(), loc, + rewriter, rewriter.getIndexType()); + this->scalar = addScalar; + } else if (lBlock.getRank() == 0) { + // When both lhs and rhs are scalar type with rank 0, just try passing + // potential `scalar` + this->scalar = + lBlock.isScalar() ? lBlock.getScalarRef() : rBlock.getScalarRef(); + } + + for (const auto &[lOffset, rOffset] : + llvm::zip(lBlock.getOffsetsRef(), rBlock.getOffsetsRef())) { + this->offsets.push_back(addOpFoldResult(lOffset, rOffset, loc, rewriter, + rewriter.getIndexType())); + } + + for (const auto &[lStride, rStride] : + llvm::zip(lBlock.getStridesRef(), rBlock.getStridesRef())) { + this->strides.push_back(addOpFoldResult(lStride, rStride, loc, rewriter, + rewriter.getIndexType())); + } + + // Both sizes are same implicitly under `add` + this->sizes = lBlock.getSizesRef(); + + this->getMemAccTypeRef().merge(lBlock.getMemAccTypeRef()); + this->getMemAccTypeRef().merge(rBlock.getMemAccTypeRef()); + // this->setMemAccTy(selectMaxMemAccTy(lBlock.getMemAccType(), + // rBlock.getMemAccType())); +} + +void BlockData::subBlock(BlockData &lBlock, BlockData &rBlock, Location loc, + ConversionPatternRewriter &rewriter) { + assert(this->isEmpty() && lBlock.getRank() == rBlock.getRank()); + + if (lBlock.isScalar() && rBlock.isScalar()) { + auto subScalar = subOpFoldResult(lBlock.getScalarRef(), + rBlock.getScalarRef(), loc, rewriter); + this->scalar = subScalar; + } else if (lBlock.getRank() == 0) { + // When both lhs and rhs are scalar type with rank 0, just try passing + // potential `scalar` + this->scalar = + lBlock.isScalar() ? lBlock.getScalarRef() : rBlock.getScalarRef(); + } + + for (const auto &[lOffset, rOffset] : + llvm::zip(lBlock.getOffsetsRef(), rBlock.getOffsetsRef())) { + this->offsets.push_back(subOpFoldResult(lOffset, rOffset, loc, rewriter)); + } + + for (const auto &[lStride, rStride] : + llvm::zip(lBlock.getStridesRef(), rBlock.getStridesRef())) { + this->strides.push_back(subOpFoldResult(lStride, rStride, loc, rewriter)); + } + + // Both sizes are same implicitly under `sub` + this->sizes = lBlock.getSizesRef(); + + this->getMemAccTypeRef().merge(lBlock.getMemAccTypeRef()); + this->getMemAccTypeRef().merge(rBlock.getMemAccTypeRef()); + // this->setMemAccTy(selectMaxMemAccTy(lBlock.getMemAccType(), + // rBlock.getMemAccType())); +} + +void BlockData::mulBlock(BlockData &lBlock, BlockData &rBlock, Location loc, + ConversionPatternRewriter &rewriter) { + assert(this->isEmpty() && lBlock.getRank() == rBlock.getRank()); + + assert(!(lBlock.hasSource() && rBlock.hasSource())); + + if (lBlock.isScalar() && rBlock.isScalar()) { + LLVM_DEBUG({ + llvm::dbgs() << "lBlock.scalar:" << lBlock.getScalar() + << " rBlbock.scalar:" << rBlock.getScalar() << "\n"; + }); + + auto scalar = mulOpFoldResult(lBlock.getScalar(), rBlock.getScalar(), loc, + rewriter, rewriter.getIndexType()); + this->scalar = scalar; + } + + // assert( + // (lBlock.isScalar() ^ rBlock.isScalar()) && + // "Currently only support one and only one scalar in function + // mulBlock()"); + + BlockData *lb = &lBlock; + BlockData *rb = &rBlock; + if (lb->isScalar()) { + std::swap(lb, rb); + } + + // In mulBlock, `scalar` will be accumulated into `offset` and `stride` + OpFoldResult rScalar = rb->getScalarRef(); + for (const auto &lOffset : lb->getOffsetsRef()) { + this->offsets.push_back(mulOpFoldResult(lOffset, rScalar, loc, rewriter, + rewriter.getIndexType())); + } + + for (const auto &lStride : lb->getStridesRef()) { + this->strides.push_back(mulOpFoldResult(lStride, rScalar, loc, rewriter, + rewriter.getIndexType())); + } + + this->sizes = lb->getSizesRef(); + + this->getMemAccTypeRef().merge(lBlock.getMemAccTypeRef()); + this->getMemAccTypeRef().merge(rBlock.getMemAccTypeRef()); + // this->setMemAccTy(selectMaxMemAccTy(lBlock.getMemAccType(), + // rBlock.getMemAccType())); +} + +void BlockData::divBlock(BlockData &lBlock, BlockData &rBlock, Location loc, + ConversionPatternRewriter &rewriter) { + assert(this->isEmpty() && lBlock.getRank() == rBlock.getRank()); + + assert(!(lBlock.hasSource() && rBlock.hasSource())); + assert(lBlock.isScalar() && rBlock.isScalar()); + + auto rScalar = rBlock.getScalar(); + this->scalar = divOpFoldResult(lBlock.getScalar(), rScalar, loc, rewriter); + + for (auto lOffset : lBlock.getOffsetsRef()) { + this->offsets.push_back(divOpFoldResult(lOffset, rScalar, loc, rewriter)); + } + + for (auto lStride : lBlock.getStridesRef()) { + this->strides.push_back(divOpFoldResult(lStride, rScalar, loc, rewriter)); + } + + this->sizes = lBlock.getSizesRef(); + + this->getMemAccTypeRef().merge(lBlock.getMemAccTypeRef()); + this->getMemAccTypeRef().merge(rBlock.getMemAccTypeRef()); + // this->setMemAccTy(selectMaxMemAccTy(lBlock.getMemAccType(), + // rBlock.getMemAccType())); +} + +FailureOr +BlockData::createCastOp(ArrayRef resultShape, const Location &loc, + OpBuilder &builder) const { + OpFoldResult resOffset = this->inferBlockOffset(loc, builder); + int64_t staticOffset = ShapedType::kDynamic; + if (isa(resOffset)) { + auto constantOffset = getConstantIntValue(resOffset); + if (!constantOffset) + return failure(); + staticOffset = *constantOffset; + } + FailureOr resultType = + this->getResultMemrefType(staticOffset, resultShape); + if (failed(resultType)) + return failure(); + + SmallVector strides(this->strides); + for (size_t i = 0; i < strides.size(); i++) { + if (resultShape[i] == 1) { + if (auto strideValue = dyn_cast(strides[i])) { + auto oneIdx = + builder.create(loc, builder.getIndexAttr(1)); + strides[i] = builder.create(loc, strideValue, oneIdx) + .getResult(); + } + } + } + + return builder.create( + loc, *resultType, this->source, resOffset, this->sizes, strides); +} + +void BlockData::dump() const { + llvm::outs() << "[INFO][BEG] BlockData info\n"; + llvm::outs() << "offsets has " << offsets.size() << " items\n"; + int cnt = 0; + for (auto it = offsets.begin(); it != offsets.end(); ++it) { + llvm::outs() << "offsets[" << cnt++ << "] = " << *it << "\n"; + } + llvm::outs() << "sizes has " << sizes.size() << " items\n"; + cnt = 0; + for (auto it = sizes.begin(); it != sizes.end(); ++it) { + llvm::outs() << "sizes[" << cnt++ << "] = " << *it << "\n"; + } + llvm::outs() << "strides has " << strides.size() << " items\n"; + cnt = 0; + for (auto it = strides.begin(); it != strides.end(); ++it) { + llvm::outs() << "strides[" << cnt++ << "] = " << *it << "\n"; + } + llvm::outs() << "source = " << source << "\n"; + llvm::outs() << "scalar = " << scalar << "\n"; + llvm::outs() << "resElemTy = " << resElemTy << "\n"; + llvm::outs() << "memAccTy = " << memAccTy.toString() << "\n"; + llvm::outs() << "[INFO][END] BlockData info\n"; +} + +FailureOr +BlockDataParser::getScalarMemRef(Value ptr, Value memref, const Location &loc, + ConversionPatternRewriter &rewriter) { + if (!ptr || !memref) + return failure(); + auto pointerType = dyn_cast(ptr.getType()); + if (!pointerType || isa(pointerType.getPointeeType())) + return failure(); + + memref = unwrapScalarPointerMemRefCarrier(memref); + + // A complete scalar address reconstructed after an SCF boundary is exposed + // as tt.int_to_ptr in the Triton IR. Its converted operand is already the + // canonical ScalarPointerCarrier memref; handle it before the legacy + // block-argument path so a direct load/store does not request a reverse + // memref-to-pointer materialization. + if (ptr.getDefiningOp()) { + if (!isa(memref.getType())) + return failure(); + // IntToPtrConverter's rank-1 carrier is still a dynamic base memref, not + // the canonical scalar view consumed by indirect loads. Normalize every + // int_to_ptr carrier to the one-element identity view below. + BlockData data; + data.setSource(memref); + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + auto castOp = data.createCastOp(SmallVector(1, 1), loc, rewriter); + if (failed(castOp)) + return failure(); + return (*castOp).getResult(); + } + + if (ptr.getDefiningOp()) { + if (auto castOp = memref.getDefiningOp()) + return castOp.getResult(); + if (auto pointerCast = memref.getDefiningOp(); + pointerCast && pointerCast->hasAttr(kScalarPointerCarrierAttr)) + return memref; + return failure(); + } + + // A scalar pointer produced by structured control flow or select is already + // represented by a converted memref. Give it the same one-element view used + // for a scalar block argument so direct tt.load/tt.store users can consume + // the transport result without attempting memref-to-pointer materialization. + if (auto definingOp = ptr.getDefiningOp(); + definingOp && isScalarPointerTransport(definingOp)) { + if (!isa(memref.getType())) + return failure(); + if (auto memrefType = dyn_cast(memref.getType()); + memrefType && memrefType.getRank() == 1) + return memref; + BlockData data; + data.setSource(memref); + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + auto castOp = data.createCastOp(SmallVector(1, 1), loc, rewriter); + if (failed(castOp)) + return failure(); + return (*castOp).getResult(); + } + + if (!isa(ptr) || !isa(memref.getType())) + return failure(); + + BlockData data; + data.setSource(memref); + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + auto castOp = data.createCastOp(SmallVector(1, 1), loc, rewriter); + if (failed(castOp)) + return failure(); + return (*castOp).getResult(); +} + +LogicalResult +BlockDataParser::parse(Value operand, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + if (known.find(operand) != known.end()) { + data = known.lookup(operand); + return success(); + } + + if (isa(operand.getType())) { + data.setScalar(getOpFoldResultOfLayoutInfo(operand, rewriter)); + return success(); + } + + // + if (isa(operand.getType())) { + // Just consider two state: ptr and ptr> + Value remappedPtr = rewriter.getRemappedValue(operand); + if (!remappedPtr) { + if (Operation *definingOp = operand.getDefiningOp()) + return definingOp->emitError("scalar pointer has no converted value"); + emitError(loc) << "scalar pointer block argument has no converted value: " + << operand; + return failure(); + } + // A not-yet-rewritten pointer user may be represented by a one-to-one + // UCC from a converted memref. Consume that memref as the transport + // source instead of asking the driver to materialize the pointer back. + remappedPtr = unwrapScalarPointerMemRefCarrier(remappedPtr); + if (auto op = operand.getDefiningOp()) { + if (auto addPtrOp = dyn_cast(op)) { + return parseAddPtr(addPtrOp, data, loc, rewriter, known); + } else if (auto bitcastOp = dyn_cast(op)) { + return parseBitcast(bitcastOp, data, loc, rewriter, known); + } else if (auto makeTensorPtrOp = dyn_cast(op)) { + return parseTensorPtr(makeTensorPtrOp, data, loc, rewriter, known); + } else if (auto advanceOp = dyn_cast(op)) { + // To support + // ptr_0 = tl.advance(ptr) + // ptr_1 = tl.advance(ptr_0) + return parseTensorPtr(advanceOp, data, loc, rewriter, known); + } else if (auto intToPtrOp = dyn_cast(op)) { + if (!isa(remappedPtr.getType())) { + return op->emitError( + "int_to_ptr did not convert to a memref carrier"); + } + data.setSource(remappedPtr); + // An address reconstructed from an i64 is a complete scalar pointer, + // but AddPtr still needs the ordinary one-element BlockData schema to + // form a memref.reinterpret_cast for a later load/store. Without + // this identity view the generic path sees an empty rank and may + // attempt to materialize a pointer from a non-memref source. + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + } else if (isDistributedTypeCustomOp(op)) { + data.setSource(remappedPtr); + } else if (isScalarPointerTransport(op)) { + if (!isa(remappedPtr.getType())) { + return op->emitError( + "scalar pointer transport did not convert to a memref"); + } + data.setSource(remappedPtr); + // Transport producers carry a complete scalar address but do not + // expose BlockData dimensions. Model the address as a one-element + // identity view so a following addptr can add a scalar offset without + // falling into the rank-mismatch/materialization failure path. + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + } else { + return op->emitError() + << "unsupported scalar pointer producer '" << op->getName() + << "' with original type " << operand.getType() + << " and converted type " << remappedPtr.getType(); + } + } else { + data.setSource(remappedPtr); + } + return success(); + } + + // not a scalar pointer + if (auto addOp = operand.getDefiningOp()) { + return parseAdd(addOp, data, loc, rewriter, known); + } else if (auto subOp = operand.getDefiningOp()) { + return parseSub(subOp, data, loc, rewriter, known); + } else if (auto mulOp = operand.getDefiningOp()) { + return parseMul(mulOp, data, loc, rewriter, known); + } else if (auto addPtrOp = operand.getDefiningOp()) { + return parseAddPtr(addPtrOp, data, loc, rewriter, known); + } else if (auto constOp = operand.getDefiningOp()) { + parseConstSplat(constOp, data, loc, rewriter, known); + return success(); + } else if (auto broadcastOp = operand.getDefiningOp()) { + return parseBroadcast(broadcastOp, data, loc, rewriter, known); + } else if (auto splatOp = operand.getDefiningOp()) { + return parseSplat(splatOp, data, loc, rewriter, known); + } else if (auto expandDimsOp = + operand.getDefiningOp()) { + return parseExpandDims(expandDimsOp, data, loc, rewriter, known); + } else if (auto remOp = operand.getDefiningOp()) { + return parseRem(remOp, data, loc, rewriter, known); + } else if (auto bitcastOp = operand.getDefiningOp()) { + return parseBitcast(bitcastOp, data, loc, rewriter, known); + } else if (auto extsiOp = operand.getDefiningOp()) { + return parseExtSI(extsiOp, data, loc, rewriter, known); + } else if (auto divOp = operand.getDefiningOp()) { + return parseDiv(divOp, data, loc, rewriter, known); + } else if (auto makeRangeOp = operand.getDefiningOp()) { + parseMakeRange(makeRangeOp, data, loc, rewriter, known); + return success(); + } else if (auto reduceOp = operand.getDefiningOp()) { + return parseReduce(reduceOp, data, loc, rewriter, known); + } else if (auto loadOp = operand.getDefiningOp()) { + parseIndirectLoad(loadOp, data, loc, rewriter, known); + return success(); + } else if (auto castOp = operand.getDefiningOp()) { + parseIndirectLoad(castOp, data, loc, rewriter, known); + return success(); + } else if (auto extractSliceOp = + operand.getDefiningOp()) { + return parseExtractSlice(extractSliceOp, data, loc, rewriter, known); + } else if (auto forOp = operand.getDefiningOp()) { + auto opResult = dyn_cast(operand); + if (!opResult) + return forOp.emitOpError("expected an OpResult while parsing its result"); + unsigned resultIdx = opResult.getResultNumber(); + parseIndirectLoad(forOp, data, loc, rewriter, known, resultIdx); + return success(); + } else if (auto tensorCastOp = operand.getDefiningOp()) { + // Used for identity operation. + return parse(tensorCastOp.getSource(), data, loc, rewriter, known); + } else if (auto fillOp = operand.getDefiningOp()) { + return parseFill(fillOp, data, loc, rewriter, known); + } else if (auto selectOp = operand.getDefiningOp()) { + if (auto resultType = dyn_cast(selectOp.getType()); + resultType && isa(resultType.getElementType())) + return selectOp.emitOpError( + "tensor-of-pointers select must be lowered before BlockData parsing"); + return parseSelect(selectOp, data, loc, rewriter, known); + } else if (isDistributedTypeCustomOp(operand.getDefiningOp())) { + auto opResult = dyn_cast(operand); + if (!opResult) + return emitError(loc, "expected a custom operation result"); + return parseStructuredCustomOp(operand.getDefiningOp(), data, loc, rewriter, + known, opResult.getResultNumber()); + } else if (auto genericOp = operand.getDefiningOp()) { + if (genericOp->hasAttr("tt.from_make_range")) { + parseLinalgGenericFromMakeRange(genericOp, data, loc, rewriter, known); + return success(); + } + return genericOp.emitOpError( + "cannot parse a generic operation without tt.from_make_range"); + } else if (auto atomicRMWOp = operand.getDefiningOp()) { + parseAtomicRmw(atomicRMWOp, data, loc, rewriter, known); + return success(); + } + + if (Operation *producer = operand.getDefiningOp()) + return producer->emitError() + << "unsupported BlockData producer '" << producer->getName() + << "' with result type " << operand.getType(); + emitError(loc) << "unsupported BlockData block argument of type " + << operand.getType(); + return failure(); +} + +void BlockDataParser::parseAtomicRmw( + triton::AtomicRMWOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + auto opRes = op->getResult(0); + auto opResTy = opRes.getType(); + std::vector resShape; + if (auto shapedResTy = dyn_cast(opResTy)) { + resShape = shapedResTy.getShape().vec(); + if (resShape.size() == 1 && resShape[0] == 1) { + Value zeroIdx = rewriter.create(loc, 0); + Value extracted = + rewriter.create(loc, opRes, ValueRange{zeroIdx}); + Value scalarIdx = rewriter.create( + loc, rewriter.getIndexType(), extracted); + data.setMemAccVal(MemAccVal::StrucMemAcc); + data.setScalar(scalarIdx); + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(0)); + data.getOffsetsRef().push_back(scalarIdx); + return; + } + // For now, we consider this is UnstrucMemAcc because we have no other info. + // Visiting other ops may change the type due to more info. + data.setMemAccVal(MemAccVal::UnstrucMemAcc); + } else { + data.setMemAccVal(MemAccVal::StrucMemAcc); + resShape.push_back(1); + } + for (auto &s : resShape) { + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(s)); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + } + data.setSource(opRes); +} + +LogicalResult +BlockDataParser::parseAdd(arith::AddIOp op, BlockData &data, + const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + BlockData lBlock, rBlock; + if (failed(parse(op.getLhs(), lBlock, loc, rewriter, known)) || + failed(parse(op.getRhs(), rBlock, loc, rewriter, known))) + return failure(); + data.addBlock(lBlock, rBlock, loc, rewriter); + return success(); +} + +LogicalResult +BlockDataParser::parseSub(arith::SubIOp op, BlockData &data, + const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + BlockData lBlock, rBlock; + if (failed(parse(op.getLhs(), lBlock, loc, rewriter, known)) || + failed(parse(op.getRhs(), rBlock, loc, rewriter, known))) + return failure(); + data.subBlock(lBlock, rBlock, loc, rewriter); + return success(); +} + +LogicalResult +BlockDataParser::parseMul(arith::MulIOp op, BlockData &data, + const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + BlockData lBlock, rBlock; + if (failed(parse(op.getLhs(), lBlock, loc, rewriter, known)) || + failed(parse(op.getRhs(), rBlock, loc, rewriter, known))) + return failure(); + + data.mulBlock(lBlock, rBlock, loc, rewriter); + return success(); +} + +LogicalResult +BlockDataParser::parseDiv(arith::DivSIOp op, BlockData &data, + const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + BlockData lBlock, rBlock; + if (failed(parse(op.getLhs(), lBlock, loc, rewriter, known)) || + failed(parse(op.getRhs(), rBlock, loc, rewriter, known))) + return failure(); + data.divBlock(lBlock, rBlock, loc, rewriter); + return success(); +} + +// TODO : support modulos +LogicalResult +BlockDataParser::parseRem(arith::RemSIOp op, BlockData &data, + const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + return op.emitOpError( + "address expressions with modulo are not supported by BlockDataParser"); +} + +void BlockDataParser::parseMakeRange( + triton::MakeRangeOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + insertDebugNop(op.getLoc(), rewriter); + assert(data.isEmpty()); + auto shape = dyn_cast(op.getType()).getShape(); + + auto start = op.getStart(); + auto end = op.getEnd(); + auto stride = (end >= start) && (end - start <= shape[0]); + assert(stride == 1 && + "make_range op should always return a tensor of stride 1"); + + data.getOffsetsRef().push_back(rewriter.getIndexAttr(start)); + data.getSizesRef().push_back(rewriter.getIndexAttr(shape[0])); + data.getStridesRef().push_back(rewriter.getIndexAttr(stride)); +} + +void BlockDataParser::parseLinalgGenericFromMakeRange( + linalg::GenericOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + assert(op->hasAttr("tt.from_make_range") && + "expected tt.from_make_range attribute"); + + auto offsetAttr = op->getAttr("tt.make_range_offset"); + auto sizeAttr = op->getAttr("tt.make_range_size"); + assert(offsetAttr && sizeAttr && + "tt.make_range_offset and tt.make_range_size required"); + + int64_t offset = cast(offsetAttr).getInt(); + int64_t size = cast(sizeAttr).getInt(); + + data.getOffsetsRef().push_back(rewriter.getIndexAttr(offset)); + data.getSizesRef().push_back(rewriter.getIndexAttr(size)); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); +} + +LogicalResult BlockDataParser::parseExpandDims( + triton::ExpandDimsOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + + if (failed(parse(op.getSrcMutable().get(), data, loc, rewriter, known))) + return failure(); + auto resShape = dyn_cast(op.getResult().getType()).getShape(); + auto axis = op.getAxis(); + + assert(resShape[axis] == 1 && + "The destiny shape of changed dimension should be 1"); + + data.getOffsetsRef().insert(data.getOffsetsRef().begin() + axis, + rewriter.getIndexAttr(0)); + data.getSizesRef().insert(data.getSizesRef().begin() + axis, + rewriter.getIndexAttr(1)); + data.getStridesRef().insert(data.getStridesRef().begin() + axis, + rewriter.getIndexAttr(0)); + return success(); +} + +LogicalResult BlockDataParser::parseExtractSlice( + tensor::ExtractSliceOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + const std::string scenarioMessages = + "PtsAnalysis supports indirectly block load in the " + "following scenario\n" + "B = tl.load(Aptr + Aoffset) # B is 1D tensor\n" + "s = tl.extract_slice(indices, offsets= (i,), sizes= " + "(1,), strides= (1,)) # s is a tensor<1x$dtype>\n" + "D = tl.load(Cptr + s + Coffset) # s is used as the " + "scalar offset\n"; // tensor<2x$dtype> will be support + // soon + + auto extract_src = op->getOperand(0); + BlockData srcBlock; + if (failed(parse(extract_src, srcBlock, loc, rewriter, known))) + return failure(); + if (!srcBlock.hasSource()) + return op.emitOpError(scenarioMessages); + // Use isa_and_nonnull for LLVM 21 compatibility + if (!isa_and_nonnull(srcBlock.getSource().getDefiningOp())) + return op.emitOpError(scenarioMessages); + + auto extract_result = op->getResult(0); + auto shaped_ty = dyn_cast(extract_result.getType()); + auto shape = shaped_ty.getShape(); + if (shape.size() > 1 || shape[0] > 1) + return op.emitOpError(scenarioMessages); + auto castOp = rewriter.create( + loc, RankedTensorType::get(shape, rewriter.getIndexType()), + extract_result); + auto offset = castOp.getResult(); + if (data.isEmpty()) { + data.getOffsetsRef().push_back(offset); + data.getSizesRef().push_back(rewriter.getIndexAttr(shape[0])); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + } else { + return op.emitOpError( + "extract_slice parsing with a pre-populated offset is unsupported"); + } + return success(); +} + +LogicalResult BlockDataParser::parseBitcast( + triton::BitcastOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + if (failed(parse(op.getSrc(), data, loc, rewriter, known))) + return failure(); + + auto resType = op.getResult().getType(); + Type resElemPointeeTy = nullptr; + if (auto resShapedTy = dyn_cast(resType)) { + auto resElemTy = resShapedTy.getElementType(); + resElemPointeeTy = + dyn_cast(resElemTy).getPointeeType(); + } else { + auto srcPointeeType = + cast(op.getSrc().getType()).getPointeeType(); + auto resPointeeType = cast(resType).getPointeeType(); + + // Handling special case + // If Op is MetaUse or src is i1 block argument and dst is i8, + // it should be converted to UnrealizedConversionCast + if (op->hasAttr("MetaUse") || + (isa(op.getSrc()) && + srcPointeeType == rewriter.getIntegerType(1) && + resPointeeType == rewriter.getIntegerType(8))) { + resElemPointeeTy = resPointeeType; + } else { + auto remappedValue = rewriter.getRemappedValue(op); + if (!remappedValue) + return op.emitOpError("bitcast result has no converted value"); + data.setSource(remappedValue); + LLVM_DEBUG({ + llvm::dbgs() << "Remapping bitcastOp:\n"; + llvm::dbgs() << op << "\nto \n"; + llvm::dbgs() << remappedValue << "\n"; + }); + } + } + data.setResElemTy(resElemPointeeTy); + return success(); +} + +LogicalResult BlockDataParser::parseExtSI( + arith::ExtSIOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + return parse(op.getIn(), data, loc, rewriter, known); +} + +LogicalResult BlockDataParser::parseBroadcast( + triton::BroadcastOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + + auto src = op.getSrcMutable().get(); + auto dst = op.getResult(); + assert(isa(src.getType()) && + "tt.broadcast's input should be a tensor"); + + auto srcShape = dyn_cast(src.getType()).getShape(); + auto dstShape = dyn_cast(dst.getType()).getShape(); + assert(srcShape.size() == dstShape.size() && + "rank of source shoule be equal to destnation"); + + if (failed(parse(src, data, loc, rewriter, known))) + return failure(); + + for (const auto &[idx, src_dst] : + llvm::enumerate(llvm::zip(srcShape, dstShape))) { + const auto &[srcAxis, dstAxis] = src_dst; + if (srcAxis == dstAxis) { + continue; + } + assert(srcAxis < dstAxis && + "srcShape of broadcastOp must be less than dstShape."); + data.getSizesRef()[idx] = rewriter.getIndexAttr(dstAxis); + } + return success(); +} + +LogicalResult BlockDataParser::parseSplat( + triton::SplatOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + auto src = op.getSrc(); + auto dst = op.getResult(); + auto dstShape = dyn_cast(dst.getType()).getShape(); + + if (failed(parse(src, data, loc, rewriter, known))) + return failure(); + + // A ScalarPointerCarrier stores the complete integer address passed to + // tt.int_to_ptr, so an addptr displacement exists only in BlockData and must + // survive the following splat. Ordinary scalar pointers already carry their + // displacement in the converted memref descriptor; retain the established + // behavior of resetting those offsets while constructing the tensor layout. + OpFoldResult splatOffset; + if (isa(src.getType()) && + isScalarPointerCarrierSource(data.getSource())) { + SmallVector pointerOffsets = data.getOffsets(); + if (pointerOffsets.size() > 1) + return op.emitOpError( + "scalar pointer carrier splat requires at most one BlockData offset"); + splatOffset = pointerOffsets.empty() + ? OpFoldResult(rewriter.getIndexAttr(0)) + : pointerOffsets.front(); + } else if (data.isScalar()) { + splatOffset = data.getScalarRef(); + } + + if (isa(src.getType()) || + isa(src.getType())) { + if (!data.isEmpty()) { + data.getOffsetsRef().clear(); + data.getSizesRef().clear(); + data.getStridesRef().clear(); + } + for (auto dstAxis : dstShape) { + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(dstAxis)); + data.getStridesRef().push_back(rewriter.getIndexAttr(0)); + } + } else { + return op.emitOpError("BlockDataParser does not support this splat source"); + } + if (!splatOffset.isNull()) + data.getOffsetsRef()[0] = splatOffset; + return success(); +} + +void BlockDataParser::parseConstSplat( + arith::ConstantOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + + DenseElementsAttr denseAttr = dyn_cast(op.getValue()); + assert(denseAttr && denseAttr.isSplat() && + isa(denseAttr.getElementType())); + + auto innerVal = denseAttr.getValues()[0].getValue(); + auto innerValIndexAttr = rewriter.getIndexAttr(innerVal.getSExtValue()); + + // for mul state + data.setScalar(innerValIndexAttr); + + auto resType = dyn_cast(op.getResult().getType()); + size_t loopLimit = resType.getShape().size(); + for (auto i = 0; i < loopLimit; i++) { + // Add original dense val to first dim offset for add state + if (i == 0) { + data.getOffsetsRef().push_back(innerValIndexAttr); + } else { + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + } + data.getSizesRef().push_back(rewriter.getIndexAttr(resType.getShape()[i])); + data.getStridesRef().push_back(rewriter.getIndexAttr(0)); + } +} + +template +std::enable_if_t || + std::is_same_v, + LogicalResult> +BlockDataParser::parseTensorPtr( + T op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + + Value remappedValue = rewriter.getRemappedValue(op); + if (!remappedValue) + return op.emitOpError("tensor pointer has no converted value"); + if (auto castOp = remappedValue.getDefiningOp()) { + parseReinterpretCast(castOp, data, loc, rewriter, known); + return success(); + } + return op.emitOpError( + "expected the converted tensor pointer to be a memref.reinterpret_cast"); +} + +LogicalResult BlockDataParser::parseAddPtr( + triton::AddPtrOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + + BlockData ptrBlock, offsetBlock; + if (failed(parse(op.getPtr(), ptrBlock, op.getLoc(), rewriter, known)) || + failed(parse(op.getOffset(), offsetBlock, op.getLoc(), rewriter, known))) + return failure(); + + if (!ptrBlock.hasSource()) + return op.emitOpError( + "could not resolve a source/base pointer for the addptr operand"); + // offset has source means offset is from tl.load and other ops(TODO) + if (offsetBlock.hasSource()) { + ptrBlock.setMemAccTy(offsetBlock.getMemAccType()); + offsetBlock.removeSource(); + } + + // handle for loop & scalar + if (ptrBlock.getRank() == 1 && offsetBlock.getRank() == 0) { + offsetBlock.getSizesRef().push_back(rewriter.getIndexAttr(1)); + offsetBlock.getOffsetsRef().push_back(offsetBlock.getScalarRef()); + offsetBlock.getStridesRef().push_back(rewriter.getIndexAttr(0)); + } + + if (ptrBlock.getRank() != offsetBlock.getRank()) + return op.emitOpError("pointer and offset BlockData ranks do not match"); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "[parseAddPtr][BEG] =========================\n"; + os << "[parseAddPtr] op is " << op << "\n"; + for (int i = 0; i < ptrBlock.getRank(); i++) { + os << "ptrBlock.getOffsetsRef()[" << i + << "] = " << ptrBlock.getOffsetsRef()[i] << "\n"; + os << "ptrBlock.getSizesRef()[" << i + << "] = " << ptrBlock.getSizesRef()[i] << "\n"; + os << "ptrBlock.getStridesRef()[" << i + << "] = " << ptrBlock.getStridesRef()[i] << "\n"; + os << "offsetBlock.getOffsetsRef()[" << i + << "] = " << offsetBlock.getOffsetsRef()[i] << "\n"; + os << "offsetBlock.getSizesRef()[" << i + << "] = " << offsetBlock.getSizesRef()[i] << "\n"; + os << "offsetBlock.getStridesRef()[" << i + << "] = " << offsetBlock.getStridesRef()[i] << "\n"; + } + os << "[parseAddPtr][END] -------------------------\n"; + }); + data.addBlock(ptrBlock, offsetBlock, op.getLoc(), rewriter); + return success(); +} + +void BlockDataParser::parseReinterpretCast( + memref::ReinterpretCastOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + assert(data.isEmpty()); + + data.setOffsets(op.getMixedOffsets()); + data.setSizes(op.getMixedSizes()); + data.setStrides(op.getMixedStrides()); + data.setSource(op.getSource()); + + // In memref::ReinterpretCastOp, offset means the total of collapsing multiple + // dimensions, which corresponds to first dim offset in block data. + // Here populate the rest of the dimensions with zeroes. + assert(data.getOffsetsRef().size() == 1); + size_t loopLimit = data.getSizesRef().size(); + for (size_t i = 1; i < loopLimit; i++) { + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + } +} + +LogicalResult BlockDataParser::parseReduce( + triton::ReduceOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + + const std::string scenarioMessages = + "PtsAnalysis supports indirectly block load in the following scenario\n" + "B = tl.load(Aptr + Aoffset) # B is 1D tensor\n" + "s = tl.min(B) # s is a scalar\n" + "D = tl.load(Cptr + s + Coffset) # s is used as the scalar offset\n"; + + auto reduce_src = op->getOperand(0); + BlockData srcBlock; + if (failed(parse(reduce_src, srcBlock, loc, rewriter, known))) + return failure(); + if (!srcBlock.hasSource()) + return op.emitOpError(scenarioMessages); + // Use isa_and_nonnull for LLVM 21 compatibility + if (!isa_and_nonnull(srcBlock.getSource().getDefiningOp())) + return op.emitOpError(scenarioMessages); + + auto reduce_result = op->getResult(0); + auto shaped_ty = dyn_cast(reduce_result.getType()); + auto shape = shaped_ty.getShape(); + auto ops = llvm::map_to_vector(op.getBody()->without_terminator(), + [](Operation &op) { return &op; }); + // Support only the case: scalar = tl.load(1D tensor) + if (shape.size() != 1 || op.getAxis() != 0 || ops.size() != 1 || + !isa(ops.front())) + return op.emitOpError(scenarioMessages); + + auto castOp = rewriter.create( + loc, RankedTensorType::get(shape, rewriter.getIndexType()), + reduce_result); + auto offset = castOp.getResult(); + if (data.isEmpty()) { + data.getOffsetsRef().push_back(offset); + data.getSizesRef().push_back(rewriter.getIndexAttr(shape[0])); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + } else { + return op.emitOpError( + "reduce parsing with a pre-populated offset is unsupported"); + } + return success(); +} + +template +void parseIndirectLoad(OpTy op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known, + unsigned resultIdx) { + assert(resultIdx < op->getNumResults() && + "resultIdx out of range for parseIndirectLoad"); + auto opRes = op->getResult(resultIdx); + auto opResTy = opRes.getType(); + std::vector resShape; + if (auto shapedResTy = dyn_cast(opResTy)) { + // For now, we consider this is UnstrucMemAcc because we have no other info. + // Visiting other ops may change the type due to more info. + resShape = shapedResTy.getShape().vec(); + auto numOperands = 3; + if (resShape.size() == 1 && resShape[0] == 1 && + op->getNumOperands() == numOperands) { + Value zeroIdx = rewriter.create(loc, 0); + Value extracted = + rewriter.create(loc, opRes, ValueRange{zeroIdx}); + Value scalarIdx = rewriter.create( + loc, rewriter.getIndexType(), extracted); + data.setMemAccVal(MemAccVal::StrucMemAcc); + data.setScalar(scalarIdx); + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(0)); + data.getOffsetsRef().push_back(scalarIdx); + return; + } + data.setMemAccVal(MemAccVal::UnstrucMemAcc); + } else { + // scalar load means this is used as offset. It is StrucMemAcc. + data.setMemAccVal(MemAccVal::StrucMemAcc); + resShape.push_back(1); + } + for (auto &s : resShape) { + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(s)); + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + } + // set the source in BlockData so that we know an indirect-load op exists in + // the chain. + data.setSource(opRes); +} + +namespace { +template +LogicalResult +parseStructuredCustomOpImpl(CustomOpT op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known, + unsigned resultIdx) { + auto srcValArrayAttr = op->template getAttrOfType( + ConverterUtils::customSrcPtrIndexAttrName); + assert(srcValArrayAttr && + "structure hivm custom op should present src tensor"); + auto srcValArray = srcValArrayAttr.asArrayRef(); + assert(srcValArray[resultIdx] != -1 && + "tensor result should map to src tensor"); + if (failed(BlockDataParser::parse(op->getOperand(srcValArray[resultIdx]), + data, loc, rewriter, known))) + return failure(); + Value remappedResult = rewriter.getRemappedValue(op->getResult(resultIdx)); + if (!remappedResult) + return op.emitOpError("custom operation result has no converted value"); + data.setSource(remappedResult); + return success(); +} + +template +CustomOpT createRewrittenStructuredCustomOp( + ConversionPatternRewriter &rewriter, Location loc, + llvm::ArrayRef resultTypes, CustomOpT op, + typename CustomOpT::Adaptor &adaptor, ValueRange newOutputs) { + if constexpr (std::is_same_v) { + return rewriter.create( + loc, resultTypes, op.getName(), adaptor.getInputs(), newOutputs, + adaptor.getTempBuffers(), adaptor.getSyncRelatedArgs()); + } else { + return rewriter.create(loc, resultTypes, op.getName(), + adaptor.getInputs(), newOutputs, + adaptor.getTempBuffers()); + } +} + +template +void rewriteStructuredCustomOpImpl(CustomOpT op, + typename CustomOpT::Adaptor &adaptor, + ConversionPatternRewriter &rewriter) { + if (isDistributedTypeCustomOp(op)) { + auto ip = rewriter.saveInsertionPoint(); + rewriter.setInsertionPoint(op); + auto loc = op.getLoc(); + llvm::SmallVector newOutputs; + for (auto out : op.getOutputs()) { + auto tensorTy = llvm::cast(out.getType()); + if (llvm::isa(tensorTy.getElementType())) { + continue; + } + newOutputs.emplace_back(rewriter.getRemappedValue(out)); + } + llvm::SmallVector resultTypes; + for (auto ty : op->getResultTypes()) { + if (auto ptrTy = llvm::dyn_cast(ty)) { + resultTypes.emplace_back( + MemRefType::get({ShapedType::kDynamic}, ptrTy.getPointeeType())); + continue; + } + if (auto tensorTy = llvm::dyn_cast(ty)) { + if (auto ptrTy = llvm::dyn_cast( + tensorTy.getElementType())) { + resultTypes.emplace_back( + MemRefType::get(tensorTy.getShape(), ptrTy.getPointeeType())); + continue; + } + } + resultTypes.emplace_back(ty); + } + auto newOp = createRewrittenStructuredCustomOp(rewriter, loc, resultTypes, + op, adaptor, newOutputs); + auto operandSegmentSizesAttr = newOp->getAttr("operandSegmentSizes"); + newOp->setAttrs(op->getAttrs()); + newOp->setAttr("operandSegmentSizes", operandSegmentSizesAttr); + rewriter.replaceOp(op, newOp.getResults()); + rewriter.restoreInsertionPoint(ip); + } else { + SmallVector resultTypes(op->getResultTypes().begin(), + op->getResultTypes().end()); + auto newOp = createRewrittenStructuredCustomOp( + rewriter, op.getLoc(), resultTypes, op, adaptor, adaptor.getOutputs()); + auto operandSegmentSizesAttr = newOp->getAttr("operandSegmentSizes"); + newOp->setAttrs(op->getAttrs()); + newOp->setAttr("operandSegmentSizes", operandSegmentSizesAttr); + rewriter.replaceOp(op, newOp); + } +} +} // namespace + +void BlockDataParser::rewriteStructuredCustomOp( + hivm::CustomOp op, hivm::CustomOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter) { + rewriteStructuredCustomOpImpl(op, adaptor, rewriter); +} + +void BlockDataParser::rewriteStructuredCustomOp( + hivm::CustomMacroOp op, hivm::CustomMacroOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter) { + rewriteStructuredCustomOpImpl(op, adaptor, rewriter); +} + +LogicalResult BlockDataParser::parseStructuredCustomOp( + Operation *op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known, unsigned resultIdx) { + if (auto customOp = dyn_cast(op)) { + return parseStructuredCustomOpImpl(customOp, data, loc, rewriter, known, + resultIdx); + } else if (auto macroOp = dyn_cast(op)) { + return parseStructuredCustomOpImpl(macroOp, data, loc, rewriter, known, + resultIdx); + } + return op->emitError("expected a structured hivm custom operation"); +} + +void BlockDataParser::rewriteStructuredCustomOp( + Operation *op, ConversionPatternRewriter &rewriter) { + if (auto customOp = dyn_cast(op)) { + hivm::CustomOp::Adaptor adaptor(customOp); + rewriteStructuredCustomOpImpl(customOp, adaptor, rewriter); + } else if (auto macroOp = dyn_cast(op)) { + hivm::CustomMacroOp::Adaptor adaptor(macroOp); + rewriteStructuredCustomOpImpl(macroOp, adaptor, rewriter); + } else { + llvm_unreachable("expected hivm custom op"); + } +} + +LogicalResult +BlockDataParser::parseFill(linalg::FillOp op, BlockData &data, + const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + auto src = op.getInputs()[0]; + auto dst = op.getResult(0); + auto dstShape = dyn_cast(dst.getType()).getShape(); + + if (failed(parse(src, data, loc, rewriter, known))) + return failure(); + + if (isa(src.getType())) { + if (!data.isEmpty()) { + data.getOffsetsRef().clear(); + data.getSizesRef().clear(); + data.getStridesRef().clear(); + } + for (auto dstAxis : dstShape) { + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + data.getSizesRef().push_back(rewriter.getIndexAttr(dstAxis)); + data.getStridesRef().push_back(rewriter.getIndexAttr(0)); + } + } else { + return op.emitOpError("BlockDataParser does not support this fill pattern"); + } + if (data.isScalar()) { + data.getOffsetsRef()[0] = data.getScalarRef(); + } + return success(); +} + +LogicalResult BlockDataParser::parseSelect( + arith::SelectOp op, BlockData &data, const Location &loc, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + if (!data.isEmpty()) + return op.emitOpError( + "select parsing requires an empty BlockData destination"); + + auto res = op.getResult(); + auto resType = dyn_cast(res.getType()); + if (!resType || (!isa(resType.getElementType()) && + !isa(resType.getElementType()))) + return op.emitOpError( + "BlockData select requires a shaped integer or index result"); + + OpFoldResult indexOfr; + size_t loopLimit = resType.getShape().size(); + + Value cond = op.getCondition(); + bool condIsScalarI1 = isa(cond.getType()) && + cast(cond.getType()).getWidth() == 1 && + !isa(cond.getType()); + + auto trueConst = + dyn_cast_or_null(op.getTrueValue().getDefiningOp()); + auto falseConst = + dyn_cast_or_null(op.getFalseValue().getDefiningOp()); + auto trueDense = trueConst ? dyn_cast(trueConst.getValue()) + : DenseElementsAttr(); + auto falseDense = falseConst + ? dyn_cast(falseConst.getValue()) + : DenseElementsAttr(); + + bool denseConstCase = condIsScalarI1 && trueDense && falseDense && + trueDense.isSplat() && falseDense.isSplat(); + + if (denseConstCase) { + // if cond is scalar i1 and both true and false value are splat dense const, + // we can directly use the value of the dense const to create scalar select + // op. + Attribute trueFirst = *trueDense.value_begin(); + Attribute falseFirst = *falseDense.value_begin(); + + Value trueScalar = nullptr; + Value falseScalar = nullptr; + if (auto tInt = dyn_cast(trueFirst)) { + trueScalar = rewriter.create(loc, tInt).getResult(); + } else { + return op.emitOpError( + "BlockData select requires integer dense true values"); + } + + if (auto fInt = dyn_cast(falseFirst)) { + falseScalar = rewriter.create(loc, fInt).getResult(); + } else { + return op.emitOpError( + "BlockData select requires integer dense false values"); + } + + if (trueScalar.getType() != falseScalar.getType()) + return op.emitOpError( + "scalarized BlockData select values must have the same type"); + + auto scalarSelect = rewriter.create( + loc, trueScalar.getType(), cond, trueScalar, falseScalar); + + indexOfr = getOpFoldResultOfLayoutInfo(scalarSelect.getResult(), rewriter); + } else { + if (!llvm::all_of(resType.getShape(), [](int64_t dim) { return dim == 1; })) + return op.emitOpError( + "BlockData select supports non-splat values only when every result " + "dimension is one"); + + SmallVector indices; + indices.reserve(loopLimit); + for (size_t i = 0; i < loopLimit; ++i) { + indices.push_back(rewriter.create(loc, 0)); + } + + auto extractOp = rewriter.create(loc, res, indices); + indexOfr = extractOp.getResult(); + if (isa(extractOp.getType())) { + indexOfr = getOpFoldResultOfLayoutInfo(extractOp.getResult(), rewriter); + } + } + + // Set scalar for mul state + data.setScalar(indexOfr); + + for (size_t i = 0; i < loopLimit; ++i) { + // Add scalar to first dim offset for add state + if (i == 0) { + data.getOffsetsRef().push_back(indexOfr); + } else { + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + } + data.getSizesRef().push_back(rewriter.getIndexAttr(resType.getShape()[i])); + data.getStridesRef().push_back(rewriter.getIndexAttr(0)); + } + return success(); +} + +LogicalResult +BlockDataParser::rewriteAddPtr(triton::AddPtrOp op, + triton::AddPtrOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known) { + ConversionPatternRewriter::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + + Location offLoc = op.getLoc(); + if (Value off = op.getOffset()) + if (Operation *defOp = off.getDefiningOp()) + offLoc = defOp->getLoc(); + insertDebugNop(offLoc, rewriter); + BlockData data; + if (failed(parseAddPtr(op, data, op.getLoc(), rewriter, known))) + return failure(); + + if (data.getMemAccTypeRef().isUnstructured() && + !isScalarPointerCarrierSource(data.getSource())) { + // TODO: Based on more info, try to create a performant IR + if (failed(rewriteAddPtrToUnstrucMemAcc(op, adaptor, rewriter, data))) + return failure(); + LLVM_DEBUG({ llvm::dbgs() << *getModuleOpFromOperation(op) << "\n"; }); + return success(); + } + + if (data.getSizesRef().size() == 0) { + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(0)); + data.getOffsetsRef().push_back(data.getScalarRef()); + } + + ArrayRef resultShape; + // shape {1,} is stub for single ptr + SmallVector stubScalarTypeShape(1, 1); + if (auto shapedType = dyn_cast(op.getResult().getType())) { + resultShape = shapedType.getShape(); + } else { + assert(data.getRank() == 1); + resultShape = stubScalarTypeShape; + } + + known[op.getResult()] = data; + + // If there are dimensions with size 1 and stride 0, replace 0 stride with the + // product of sizes of all lower dimensions. This avoids creating memref with + // zero stride. + // And here store the unmodified state into known ptrs, since any following + // pointer arithmetic operations should still use the original 0 stride. + auto inferedSize = 1; + auto hoistDim = op->getAttrOfType("hoist_dim"); + for (int i = data.getSizesRef().size() - 1; i >= 0; i--) { + auto strideConst = getConstantIntValue(data.getStridesRef()[i]); + auto sizeConst = getConstantIntValue(data.getSizesRef()[i]); + assert(sizeConst.has_value()); + bool shouldReplaceStride = + (sizeConst.value() == 1) || (hoistDim && hoistDim.getValue() == i); + if (shouldReplaceStride && strideConst && strideConst.value() == 0) { + data.getStridesRef()[i] = rewriter.getIndexAttr(inferedSize); + } + inferedSize *= sizeConst.value(); + } + + auto &offsets = data.getOffsetsRef(); + for (size_t i = 0; i < offsets.size(); ++i) { + if (auto constVal = getConstantIntValue(offsets[i])) { + if (constVal.value() < 0) { + LLVM_DEBUG({ + llvm::dbgs() << "[NegOffsetElim] Detected negative offset: " + << constVal.value() << " at dim " << i << "\n"; + }); + + Value negOffsetVal = rewriter.create( + op.getLoc(), constVal.value()); + offsets[i] = negOffsetVal; + } + } + } + + if (auto intToPtrOp = dyn_cast_or_null( + data.getSourceRef().getDefiningOp())) { + auto rtype = cast(intToPtrOp.getResult().getType()); + auto memrefType = + MemRefType::get({ShapedType::kDynamic}, rtype.getPointeeType()); + auto hivmPointCastOp = createScalarPointerCast( + rewriter, intToPtrOp.getLoc(), memrefType, intToPtrOp.getSrc()); + data.setSource(hivmPointCastOp.getResult()); + } + + if (data.hasResElemTy()) { + // Handle bitcast scenario + auto memrefType = dyn_cast(data.getSourceRef().getType()) + .cloneWith(std::nullopt, data.getResElemTyRef()); + UnrealizedConversionCastOp castOp = + rewriter.create( + op.getLoc(), memrefType, data.getSourceRef()); + data.setSource(castOp.getOutputs()[0]); + } + + // ToDo: need to handle module scenario + + FailureOr castOp = + data.createCastOp(resultShape, op.getLoc(), rewriter); + if (failed(castOp)) + return op.emitOpError( + "could not materialize a memref from the source type"); + Value src = (*castOp).getResult(); + LLVM_DEBUG({ + llvm::dbgs() << "cast MemRefType:\n"; + (*castOp).getOperation()->print(llvm::dbgs(), + OpPrintingFlags().printGenericOpForm()); + llvm::dbgs() << "\n"; + }); + + rewriter.replaceOp(op, src); + return success(); +} + +FailureOr BlockDataParser::materializePointer( + Value ptr, ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known) { + SmallVector resultShape; + if (auto resultTy = dyn_cast(ptr.getType())) { + if (!isa(resultTy.getElementType())) + return failure(); + resultShape.append(resultTy.getShape().begin(), resultTy.getShape().end()); + } else if (auto pointerTy = dyn_cast(ptr.getType())) { + // A pointer to a shaped value is a block pointer, not a scalar element + // pointer. It is materialized by the make_tensor_ptr path instead. + if (isa(pointerTy.getPointeeType())) + return failure(); + resultShape.push_back(1); + } else { + return failure(); + } + + Operation *defOp = ptr.getDefiningOp(); + if (!defOp) + return failure(); + + ConversionPatternRewriter::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(defOp); + + BlockData data; + if (failed(parse(ptr, data, ptr.getLoc(), rewriter, known)) || + !data.hasSource() || data.getMemAccType().isUnstructured()) + return failure(); + + if (data.getSizesRef().empty()) { + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + data.getStridesRef().push_back(rewriter.getIndexAttr(0)); + data.getOffsetsRef().push_back(data.getScalarRef().isNull() + ? OpFoldResult(rewriter.getIndexAttr(0)) + : data.getScalarRef()); + } + + if (data.getRank() != static_cast(resultShape.size())) + return failure(); + + known[ptr] = data; + + // A unit dimension with zero stride is represented as a normal contiguous + // unit dimension. Non-unit zero strides are intentional (for example a + // splatted base pointer) and must be preserved. + int64_t inferredSize = 1; + for (int64_t i = data.getRank() - 1; i >= 0; --i) { + auto strideConst = getConstantIntValue(data.getStridesRef()[i]); + auto sizeConst = getConstantIntValue(data.getSizesRef()[i]); + if (!sizeConst) + return failure(); + if (sizeConst.value() == 1 && strideConst && strideConst.value() == 0) + data.getStridesRef()[i] = rewriter.getIndexAttr(inferredSize); + inferredSize *= sizeConst.value(); + } + + // Keep negative static offsets as SSA values. This mirrors rewriteAddPtr and + // avoids unsigned attribute handling in later reinterpret_cast lowering. + auto &offsets = data.getOffsetsRef(); + for (OpFoldResult &offset : offsets) { + if (auto constVal = getConstantIntValue(offset); + constVal && constVal.value() < 0) { + offset = + rewriter + .create(ptr.getLoc(), constVal.value()) + .getResult(); + } + } + + if (auto intToPtrOp = dyn_cast_or_null( + data.getSourceRef().getDefiningOp())) { + auto pointerTy = + cast(intToPtrOp.getResult().getType()); + auto memrefTy = + MemRefType::get({ShapedType::kDynamic}, pointerTy.getPointeeType()); + auto pointerCast = createScalarPointerCast(rewriter, intToPtrOp.getLoc(), + memrefTy, intToPtrOp.getSrc()); + data.setSource(pointerCast.getResult()); + } + + if (data.hasResElemTy()) { + auto sourceTy = dyn_cast(data.getSourceRef().getType()); + if (!sourceTy) + return failure(); + auto castTy = sourceTy.cloneWith(std::nullopt, data.getResElemTyRef()); + if (sourceTy != castTy) { + auto cast = rewriter.create( + ptr.getLoc(), castTy, data.getSourceRef()); + data.setSource(cast.getResult(0)); + } + } + + FailureOr castOp = + data.createCastOp(resultShape, ptr.getLoc(), rewriter); + if (failed(castOp)) + return failure(); + return (*castOp).getResult(); +} + +static FailureOr +getBaseMemRefOffset(Value convertedBase, ConversionPatternRewriter &rewriter) { + auto memrefType = dyn_cast(convertedBase.getType()); + if (!memrefType) + return failure(); + // Preserve the existing foldable path for a directly converted tt.addptr. + // Reinterpret-casting a reinterpret cast does not compose offsets, so the + // first cast's absolute offset must be carried into the new descriptor. + if (auto baseRecast = + convertedBase.getDefiningOp()) + return baseRecast.getConstifiedMixedOffset(); + + auto stridedLayout = memrefType.getStridesAndOffset(); + int64_t staticOffset = stridedLayout.second; + if (!ShapedType::isDynamic(staticOffset)) + return OpFoldResult(rewriter.getIndexAttr(staticOffset)); + + // A control-flow-carried BlockPtr base uses the canonical identity-layout + // memref and therefore has static offset zero. Dynamic hidden offsets are not + // valid BlockPtr bases; their displacement belongs in descriptor offsets. + return failure(); +} + +LogicalResult BlockDataParser::rewriteCustomOp( + hivm::CustomOp op, hivm::CustomOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter, + const llvm::SmallDenseMap &known) { + ConversionPatternRewriter::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + auto loc = op.getLoc(); + llvm::SmallVector newInputs; + llvm::SmallVector newOutputs; + auto convertIntToPtr = [&rewriter](BlockData &data) { + if (auto intToPtrOp = dyn_cast_or_null( + data.getSourceRef().getDefiningOp())) { + auto rtype = cast(intToPtrOp.getResult().getType()); + auto memrefType = + MemRefType::get({ShapedType::kDynamic}, rtype.getPointeeType()); + auto hivmPointCastOp = createScalarPointerCast( + rewriter, intToPtrOp.getLoc(), memrefType, intToPtrOp.getSrc()); + if (data.getSizesRef().size() == 0) { + data.getSizesRef().push_back(rewriter.getIndexAttr(1)); + if (data.getScalarRef().isNull()) { + data.getOffsetsRef().push_back(rewriter.getIndexAttr(0)); + } else { + data.getOffsetsRef().push_back(data.getScalarRef()); + } + data.getStridesRef().push_back(rewriter.getIndexAttr(1)); + } + data.setSource(hivmPointCastOp.getResult()); + } + }; + for (auto in : op.getInputs()) { + in = rewriter.getRemappedValue(in); + if (!in) + return op.emitOpError("custom operation input has no converted value"); + BlockData blockData; + auto curInput = in; + if (llvm::isa(in.getType())) { + if (failed(parse(in, blockData, loc, rewriter, known))) + return failure(); + convertIntToPtr(blockData); + FailureOr castOp = + blockData.createCastOp({ShapedType::kDynamic}, loc, rewriter); + if (failed(castOp)) + return failure(); + curInput = (*castOp).getResult(); + } else if (auto tensor = llvm::dyn_cast(in.getType())) { + if (llvm::isa(tensor.getElementType())) { + if (failed(parse(in, blockData, loc, rewriter, known))) + return failure(); + convertIntToPtr(blockData); + FailureOr castOp = + blockData.createCastOp(tensor.getShape(), loc, rewriter); + if (failed(castOp)) + return failure(); + curInput = (*castOp).getResult(); + } + } + newInputs.emplace_back(curInput); + } + for (auto out : op.getOutputs()) { + auto tensorTy = llvm::cast(out.getType()); + if (llvm::isa(tensorTy.getElementType())) { + // simd library shouldn't output tensor + // after rewrite, delete the tensor output value + continue; + } + newOutputs.emplace_back(rewriter.getRemappedValue(out)); + } + llvm::SmallVector resultTypes; + for (auto ty : op->getResultTypes()) { + if (auto ptrTy = llvm::dyn_cast(ty)) { + resultTypes.emplace_back( + MemRefType::get({ShapedType::kDynamic}, ptrTy.getPointeeType())); + continue; + } + if (auto tensorTy = llvm::dyn_cast(ty)) { + if (auto ptrTy = + llvm::dyn_cast(tensorTy.getElementType())) { + resultTypes.emplace_back( + MemRefType::get(tensorTy.getShape(), ptrTy.getPointeeType())); + continue; + } + } + resultTypes.emplace_back(ty); + } + auto newCustomOp = + rewriter.create(loc, resultTypes, op.getName(), newInputs, + newOutputs, adaptor.getTempBuffers()); + auto operandSegmentSizesAttr = newCustomOp->getAttr("operandSegmentSizes"); + newCustomOp->setAttrs(op->getAttrs()); + newCustomOp->setAttr("operandSegmentSizes", operandSegmentSizesAttr); + rewriter.replaceOp(op, newCustomOp.getResults()); + return success(); +} + +// Design for load/store boundary_check. +FailureOr +createRedundantOp(triton::MakeTensorPtrOp op, OpFoldResult sourceBaseOffset, + ConversionPatternRewriter &rewriter, BlockData &data) { + auto loc = op.getLoc(); + // to do boundary_check in tt.load, we need to keep the parent tensor's + // shape info in the IR. + // use the parent tensor's shape to create a cast + auto resultSizes = data.getSizes(); + auto resultOffsets = data.getOffsets(); + data.getSizesRef().clear(); + data.getOffsetsRef().clear(); + data.getSizesRef() = + std::move(llvm::map_to_vector(op.getShape(), [&](Value v) { + return getOpFoldResultOfLayoutInfo(v, rewriter); + })); + + // This redundant ReinterpretCastOp is to describe full tensor_ptr, so each + // dim offset from base is initialized as zero. + SmallVector curOffsets(op.getOffsets().size(), + rewriter.getIndexAttr(0)); + // Both the full-shape descriptor and the final block descriptor use the + // same absolute source offset. Reusing this value avoids both dropping it + // across SCF and accidentally composing it twice. + curOffsets.front() = sourceBaseOffset; + + for (auto offset : curOffsets) { + data.getOffsetsRef().push_back(offset); + } + + SmallVector staticShapes; + SmallVector dynamicShapes; + dispatchIndexOpFoldResults(data.getSizesRef(), dynamicShapes, staticShapes); + auto castOp = data.createCastOp(staticShapes, loc, rewriter); + // restore sizes and offsets + data.getSizesRef().clear(); + for (auto &s : resultSizes) { + data.getSizesRef().push_back(s); + } + data.getOffsetsRef().clear(); + for (auto &offset : resultOffsets) { + data.getOffsetsRef().push_back(offset); + } + return castOp; +} + +// Carries the converted runtime descriptor and records whether the resolver +// has already proven the complete ptr-to-ptr normalization contract. +// That exact fallback can initialize BlockData without consulting conversion +// state attached to the original bitcast result. +struct ResolvedMakeTensorPtrBase { + Value value; + bool normalizedI1ToI8 = false; +}; + +static FailureOr +resolveMakeTensorPtrBase(triton::MakeTensorPtrOp op, Value adaptorBase, + ConversionPatternRewriter &rewriter) { + if (adaptorBase && isa(adaptorBase.getType())) + return ResolvedMakeTensorPtrBase{adaptorBase}; + + auto bitcast = op.getBase().getDefiningOp(); + if (!bitcast) + return failure(); + + auto sourceArgument = dyn_cast(bitcast.getSrc()); + if (!sourceArgument) + return failure(); + + auto sourcePointer = + dyn_cast(bitcast.getSrc().getType()); + auto resultPointer = + dyn_cast(bitcast.getResult().getType()); + if (!sourcePointer || !resultPointer || + !sourcePointer.getPointeeType().isInteger(1) || + !resultPointer.getPointeeType().isInteger(8)) + return failure(); + + Value convertedSource = rewriter.getRemappedValue(bitcast.getSrc()); + if (!convertedSource) + return failure(); + auto sourceMemRef = dyn_cast(convertedSource.getType()); + auto expectedType = + MemRefType::get({ShapedType::kDynamic}, rewriter.getIntegerType(8)); + if (!sourceMemRef || sourceMemRef != expectedType) + return failure(); + + return ResolvedMakeTensorPtrBase{convertedSource, + /*normalizedI1ToI8=*/true}; +} + +LogicalResult BlockDataParser::rewriteMakeTensorPtrOp( + triton::MakeTensorPtrOp op, Value convertedBase, + ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known) { + FailureOr resolvedBase = + resolveMakeTensorPtrBase(op, convertedBase, rewriter); + if (failed(resolvedBase)) { + op.emitOpError("expected the converted base to be a memref descriptor"); + return failure(); + } + convertedBase = resolvedBase->value; + Location loc = op.getLoc(); + BlockData data; + + if (resolvedBase->normalizedI1ToI8) { + // The resolver has already established the complete normalized mask-base + // contract. Avoid querying the bitcast result mapping while dialect + // conversion may still be rewriting its owner block. + data.setSource(convertedBase); + data.setResElemTy(rewriter.getIntegerType(8)); + } else { + // Parse the original producer only for semantic information such as a + // bitcast element type. The runtime source always comes from the resolved + // converted base so SCF-selected memref descriptors are not bypassed. + if (failed( + BlockDataParser::parse(op.getBase(), data, loc, rewriter, known))) + return failure(); + } + if (!data.hasSource()) { + op.emitOpError("failed to resolve the converted scalar base"); + return failure(); + } + if (data.hasResElemTy()) { + auto sourceType = dyn_cast(convertedBase.getType()); + if (!sourceType) { + op.emitOpError("bitcast base did not resolve to a memref descriptor"); + return failure(); + } + auto memrefType = + sourceType.cloneWith(std::nullopt, data.getResElemTyRef()); + if (sourceType == memrefType) { + data.setSource(convertedBase); + } else { + UnrealizedConversionCastOp castOp = + rewriter.create(loc, memrefType, + convertedBase); + data.setSource(castOp.getOutputs()[0]); + } + } else { + data.setSource(convertedBase); + } + + data.getOffsetsRef() = + std::move(llvm::map_to_vector(op.getOffsets(), [&](Value v) { + auto zeroVal = rewriter.create( + loc, rewriter.getI32IntegerAttr(0)); + v = rewriter.create(loc, v, zeroVal); + return getOpFoldResultOfLayoutInfo(v, rewriter); + })); + data.getStridesRef() = + std::move(llvm::map_to_vector(op.getStrides(), [&](Value v) { + return getOpFoldResultOfLayoutInfo(v, rewriter); + })); + + SmallVector newOffsets; + for (auto [offset, stride] : + llvm::zip(data.getOffsetsRef(), data.getStridesRef())) + newOffsets.push_back(mulOpFoldResult(offset, stride, loc, rewriter, + rewriter.getIndexType())); + + if (newOffsets.empty()) { + op.emitOpError("expected at least one block pointer dimension"); + return failure(); + } + + FailureOr sourceBaseOffset = + getBaseMemRefOffset(convertedBase, rewriter); + if (failed(sourceBaseOffset)) { + op.emitOpError("could not extract the converted base offset"); + return failure(); + } + newOffsets.front() = addOpFoldResult(newOffsets.front(), *sourceBaseOffset, + loc, rewriter, rewriter.getIndexType()); + + data.getOffsetsRef().clear(); + + for (auto offset : newOffsets) { + data.getOffsetsRef().push_back(offset); + } + + ArrayRef resultShape; + auto pointerType = cast(op.getResult().getType()); + if (auto shapedType = dyn_cast(pointerType.getPointeeType())) { + resultShape = shapedType.getShape(); + data.getSizesRef().clear(); + for (auto dim_size : resultShape) { + data.getSizesRef().push_back( + IntegerAttr::get(IntegerType::get(op.getContext(), 64), dim_size)); + } + } else { + // scalar pointer, should produce a one dimensional memref + SmallVector scalarShape(1, 1); + resultShape = scalarShape; + assert(data.getRank() == 1); + } + + // special handling for davinci + // create redundant reinterpret_cast op for record shape info + FailureOr redundantOp = + createRedundantOp(op, *sourceBaseOffset, rewriter, data); + if (failed(redundantOp)) + return op.emitOpError("could not materialize the full tensor pointer"); + (*redundantOp)->setAttr("tensor_ptr_full_shape", rewriter.getUnitAttr()); + + // create reinterpret_cast op for the target block + data.setSource((*redundantOp).getResult()); + known[op.getResult()] = data; + FailureOr castOp = + data.createCastOp(resultShape, loc, rewriter); + if (failed(castOp)) + return op.emitOpError("could not materialize the block pointer"); + rewriter.replaceOp(op, (*castOp).getResult()); + + if (nd2nzFlag) { + auto basePtr = (*castOp).getResult(); + int original_rank = op.getShape().size() + 1; + std::string shapeStr; + + auto baseMemrefType = mlir::dyn_cast(basePtr.getType()); + assert(baseMemrefType && "basePtr is not a memref type"); + auto shape = baseMemrefType.getShape(); + + if (auto memrefType = mlir::dyn_cast(basePtr.getType())) { + for (auto dim : memrefType.getShape()) { + shapeStr += llvm::formatv("_{0}", dim); + } + } + std::string elemTypeName; + Type elemType = baseMemrefType.getElementType(); + if (auto intType = mlir::dyn_cast(elemType)) { + elemTypeName = llvm::formatv("i{0}", intType.getWidth()); + } else if (auto floatType = mlir::dyn_cast(elemType)) { + std::string floatTypeName; + llvm::raw_string_ostream os(floatTypeName); + floatType.print(os); + os.flush(); + elemTypeName = floatTypeName; + } else { + std::string typeName; + llvm::raw_string_ostream os(typeName); + elemType.print(os); + os.flush(); + elemTypeName = typeName; + } + + std::string memrefTypeStr; + llvm::raw_string_ostream os(memrefTypeStr); + baseMemrefType.print(os); + os.flush(); + + std::string laydbgsuffix; + for (char c : memrefTypeStr) { + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || c == '_' || c == ',' || c == '[' || + c == ']') { + laydbgsuffix += c; + } + } + auto funcName = rewriter.getStringAttr( + llvm::formatv("__hmf_original_shape{0}d{1}_{2}_{3}", original_rank, + shapeStr, elemTypeName, laydbgsuffix)); + MemRefType targetMemrefType = MemRefType::get( + baseMemrefType.getShape(), baseMemrefType.getElementType(), + baseMemrefType.getLayout()); + const int vectorSize = 4; + SmallVector srcElemTys; + for (auto sz : op.getShape()) { + srcElemTys.push_back(sz.getType()); + } + srcElemTys.push_back(targetMemrefType); + Type dstElemTy = rewriter.getNoneType(); + FunctionType hintFuncType = + FunctionType::get(rewriter.getContext(), srcElemTys, {dstElemTy}); + + auto mod = SymbolTable::getNearestSymbolTable(op); + auto extFunc = dyn_cast_or_null( + SymbolTable::lookupSymbolIn(mod, funcName)); + SmallVector args; + for (auto sz : op.getShape()) { + args.push_back(sz); + } + args.push_back(basePtr); + if (!extFunc) { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToStart(&mod->getRegion(0).front()); + extFunc = rewriter.create(rewriter.getUnknownLoc(), + funcName, hintFuncType); + extFunc.setPrivate(); + extFunc->setAttr(LLVM::LLVMDialect::getReadnoneAttrName(), + UnitAttr::get(rewriter.getContext())); + rewriter.setInsertionPoint(op); + } + rewriter.create(loc, funcName, dstElemTy, args); + } + + return success(); +} + +LogicalResult BlockDataParser::rewriteAdvanceOp( + triton::AdvanceOp op, ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known) { + OpBuilder::InsertionGuard insertionGuard(rewriter); + rewriter.setInsertionPoint(op); + auto loc = op.getLoc(); + + BlockData blockData; + if (failed(parse(op.getOperand(0), blockData, loc, rewriter, known))) + return failure(); + + // region [BUGFIX] Add the code block below following the same logic as + // 'BlockDataParser::rewriteAddPtr' function. + known[op.getResult()] = blockData; + auto inferedSize = 1; + for (int i = blockData.getSizesRef().size() - 1; i >= 0; i--) { + auto strideConst = getConstantIntValue(blockData.getStridesRef()[i]); + auto sizeConst = getConstantIntValue(blockData.getSizesRef()[i]); + assert(sizeConst.has_value()); + if (sizeConst.value() == 1 && strideConst && strideConst.value() == 0) { + blockData.getStridesRef()[i] = rewriter.getIndexAttr(inferedSize); + } + inferedSize *= sizeConst.value(); + } + // endregion + + SmallVector incrementOffsets = + llvm::map_to_vector(op.getOffsets(), [&](Value offset) { + return getOpFoldResultOfLayoutInfo(offset, rewriter); + }); + + SmallVector newOffsets; + for (const auto [increment, originalOffset, stride] : + llvm::zip(incrementOffsets, blockData.getOffsetsRef(), + blockData.getStridesRef())) { + auto curDimOffset = + addOpFoldResult(mulOpFoldResult(increment, stride, loc, rewriter, + rewriter.getIndexType()), + originalOffset, loc, rewriter, rewriter.getIndexType()); + + newOffsets.push_back(curDimOffset); + } + + blockData.getOffsetsRef().clear(); + + for (auto offset : newOffsets) + blockData.getOffsetsRef().push_back(offset); + + SmallVector scalarShape(1, 1); // Stub shape + ArrayRef resultShape; + auto pointerType = cast(op.getResult().getType()); + + if (auto shapedType = dyn_cast(pointerType.getPointeeType())) { + resultShape = shapedType.getShape(); + } else { + // scalar pointer, should produce a one dimensional memref + resultShape = scalarShape; + assert(blockData.getRank() == 1); + } + + FailureOr newOp = + blockData.createCastOp(resultShape, loc, rewriter); + if (failed(newOp)) + return op.emitOpError("could not materialize the advanced pointer"); + rewriter.replaceOp(op, (*newOp).getResult()); + + known[(*newOp).getResult()] = blockData; + return success(); +} + +static bool isIntegerTensorBlockDataValue(Value value) { + auto tensorType = dyn_cast(value.getType()); + return tensorType && isa(tensorType.getElementType()); +} + +static bool containsLegacyTritonPointer(Type type) { + if (isa(type)) + return true; + auto shapedType = dyn_cast(type); + return shapedType && isa(shapedType.getElementType()); +} + +// Before expanding a loop signature, reject backedges that are already known +// to be opaque to BlockData. Triton pointer values may still receive a legal +// converted mapping later, while complete memrefs require an explicit +// reinterpret-cast producer. In particular, this deliberately rejects an +// arbitrary fixed-layout memref returned by func.call. +static bool canAnalyzeLegacyBlockDataBackedge(LoopLikeOpInterface loopOp, + unsigned slot, Value value) { + if (containsLegacyTritonPointer(value.getType()) || + isIntegerTensorBlockDataValue(value) || + value.getDefiningOp()) + return true; + + // Forwarding the same loop-carried block argument preserves the descriptor + // state already parsed from the init. A captured function/block argument is + // not equivalent and remains opaque to this analysis. + if (slot < loopOp.getRegionIterArgs().size() && + value == loopOp.getRegionIterArgs()[slot]) + return true; + if (auto whileOp = dyn_cast(loopOp.getOperation())) { + return slot < whileOp.getAfterArguments().size() && + value == whileOp.getAfterArguments()[slot]; + } + return false; +} + +// Resolve a selected legacy BlockData state after dialect-conversion remapping. +// The legacy representation is intentionally limited to reinterpret-cast +// descriptors and integer tensors. An opaque memref returned by func.call is a +// complete value, not an analyzable descriptor producer, and must never be +// guessed across the call boundary. +static FailureOr +resolveBlockDataStateValue(Value originalValue, + ConversionPatternRewriter &rewriter) { + Value value = originalValue; + if (Value mappedValue = rewriter.getRemappedValue(originalValue)) { + if (originalValue.getDefiningOp() || + originalValue.getDefiningOp() || + originalValue.getDefiningOp()) { + if (!mappedValue.getDefiningOp()) + return failure(); + } else if (auto tensorType = dyn_cast(mappedValue.getType()); + tensorType && + isa(tensorType.getElementType())) { + return failure(); + } + value = mappedValue; + } + + if (value.getDefiningOp() || + isIntegerTensorBlockDataValue(value)) + return value; + return failure(); +} + +template +std::enable_if_t || + std::is_same_v, + LogicalResult> +BlockDataParser::rewriteTerminator( + T op, ConversionPatternRewriter &rewriter, + const llvm::SmallDenseSet &blockArgIdxSet, + ArrayRef iterArgIdxMap, + const llvm::SmallDenseMap &known) { + // Any inserted instruction should be before this yield + OpBuilder::InsertionGuard insertionGuard{rewriter}; + rewriter.setInsertionPoint(op); + + auto adaptor = typename T::Adaptor(op); + ValueRange args; + if constexpr (std::is_same_v) { + args = adaptor.getOperands(); + } else { + args = adaptor.getArgs(); + } + + SmallVector initArgState; + SmallVector operands; + + operands.reserve(op->getNumOperands()); + for (const auto &[oper, newIterArgIdx] : + llvm::zip_equal(args, iterArgIdxMap)) { + if (newIterArgIdx != -1) + operands.push_back(oper); + } + + // For each of the init arg that we added additional Values in for loop, we + // need to add corresponding Values as yield operands. The loop below gathers + // BlockData for those values. + for (auto [i, originalValue] : llvm::enumerate(args)) { + if (blockArgIdxSet.find(i) == blockArgIdxSet.end()) + continue; + + Value knownKey = rewriter.getRemappedValue(originalValue); + if (!knownKey) + knownKey = originalValue; + if (auto knownState = known.find(knownKey); knownState != known.end()) { + initArgState.push_back(knownState->second); + continue; + } + + FailureOr stateValue = + resolveBlockDataStateValue(originalValue, rewriter); + if (failed(stateValue)) { + InFlightDiagnostic diagnostic = op.emitError( + "legacy BlockData loop rewrite cannot analyze carried slot "); + diagnostic << i << " produced by "; + if (Operation *producer = originalValue.getDefiningOp()) + diagnostic << producer->getName(); + else + diagnostic << "a block argument"; + return failure(); + } + + Value v = *stateValue; + auto reintCastOp = v.getDefiningOp(); + + BlockData state; + if (reintCastOp) { + parseReinterpretCast(reintCastOp, state, op.getLoc(), rewriter, known); + } else { + if (failed(parse(v, state, op.getLoc(), rewriter, known))) + return failure(); + } + initArgState.push_back(state); + } + + // For each of the BlockData recorded in the last step, extract value + // that correspond to offset and stride for each dimension and append + // them to yield operands. + for (auto state : initArgState) { + for (auto offset : state.getOffsetsRef()) { + // offsets can be IntAttr zeroes, since reinterpret_cast collapses + // them for the input memref, and the for loop may not update + // offsets other than offsets[0]. Create constants Values for those + // zeroes. + if (isa(offset)) { + auto constOffset = cast(offset); + assert(isa(constOffset) && + dyn_cast(constOffset).getInt() == 0 && + "attribute offsets should be zeroes"); + auto constOp = rewriter.create( + op.getLoc(), rewriter.getIndexAttr(0)); + operands.push_back(constOp.getResult()); + } else { + operands.push_back(cast(offset)); + } + } + + auto sizesRef = state.getSizesRef(); + size_t dimIdx = 0; + for (OpFoldResult stride : state.getStridesRef()) { + if (isa(stride)) { + auto constStride = cast(stride); + assert(isa(constStride) && + "attribute strides should be IntegerAttr"); + + auto strideVal = dyn_cast(constStride).getInt(); + bool isSizeOne = + (dimIdx < sizesRef.size() && isa(sizesRef[dimIdx]) && + cast(cast(sizesRef[dimIdx])).getInt() == + 1); + assert((strideVal == 1 || (strideVal == 0 && isSizeOne)) && + "attribute strides should be ones"); + auto constOp = rewriter.create( + op.getLoc(), rewriter.getIndexAttr(1)); + operands.push_back(constOp.getResult()); + } else { + operands.push_back(cast(stride)); + } + dimIdx++; + } + } + + // Yield is a terminator op that must be at the end of the function + rewriter.setInsertionPointAfter(op); + Operation *newOp; + if constexpr (std::is_same_v) { + newOp = rewriter.replaceOpWithNewOp(op, operands); + } else { + newOp = rewriter.replaceOpWithNewOp(op, op.getCondition(), + operands); + } + + assert(op->getNumResults() == 0); + + LLVM_DEBUG({ + llvm::dbgs() << "new terminator: "; + newOp->print(llvm::dbgs(), OpPrintingFlags().printGenericOpForm()); + llvm::dbgs() << "\n"; + }); + return success(); +} + +// This function is util function for rewriteLoopOp that +// check if given regionIterArg is used with given condition +bool isUsedWithCondition(Value v, std::function cond, + int depth = 0, + llvm::SmallSetVector *visited = nullptr) { + llvm::SmallSetVector localVisited; + if (!visited) { + visited = &localVisited; + } + + if (visited->contains(v)) { + return false; + } + visited->insert(v); + + for (auto &use : v.getUses()) { + auto *user = use.getOwner(); + if (user->hasAttr(ConverterUtils::discreteAttrName) || + isa(user)) + continue; + if (cond(&use)) + return true; + if (auto loopOp = dyn_cast(user); + loopOp && !loopOp->hasAttr("ExtractedLoadOrStore")) { + Value tiedArg = loopOp.getTiedLoopRegionIterArg(&use); + if (tiedArg && isUsedWithCondition(tiedArg, cond, depth + 1, visited)) + return true; + } else if (auto yieldOp = dyn_cast(user); + yieldOp && !isa(user->getParentOp())) { + if (depth && isUsedWithCondition(yieldOp->getParentOp()->getResult( + use.getOperandNumber()), + cond, depth - 1, visited)) + return true; + } else if (auto conditionOp = dyn_cast(user); + conditionOp && use.getOperandNumber() > 0) { + auto whileOp = cast(conditionOp->getParentOp()); + if (depth && + isUsedWithCondition(whileOp->getResult(use.getOperandNumber() - 1), + cond, depth - 1, visited)) + return true; + if (isUsedWithCondition( + whileOp.getAfterArguments()[use.getOperandNumber() - 1], cond, + depth, visited)) + return true; + } + for (auto res : user->getResults()) { + if (isUsedWithCondition(res, cond, depth, visited)) + return true; + } + } + return false; +} + +// A loop-carried value may be consumed through a region argument, through a +// while after-argument, or only after the loop result. Check every semantic +// view of the same carried slot so an identity tensor.cast after the loop +// cannot hide an AddPtr/load/store use from the decomposition decision. +bool isLoopCarriedValueUsedWithCondition( + LoopLikeOpInterface loopOp, unsigned index, + const std::function &condition) { + if (index >= loopOp.getRegionIterArgs().size() || + index >= loopOp->getNumResults()) + return false; + if (isUsedWithCondition(loopOp.getRegionIterArgs()[index], condition)) + return true; + if (auto whileOp = dyn_cast(loopOp.getOperation())) { + if (index < whileOp.getAfterArguments().size() && + isUsedWithCondition(whileOp.getAfterArguments()[index], condition)) + return true; + } + return isUsedWithCondition(loopOp->getResult(index), condition); +} + +// The legacy BlockData route only needs address uses reached from inside the +// loop. Reuse the established traversal for the region arguments, but do not +// start another traversal from the loop result: a numerical carrier can feed +// an unrelated reduction after the loop and eventually reach an address use. +static bool isLoopCarriedAddressValueUsed( + LoopLikeOpInterface loopOp, unsigned index, + const std::function &condition) { + if (index >= loopOp.getRegionIterArgs().size() || + index >= loopOp->getNumResults()) + return false; + if (isUsedWithCondition(loopOp.getRegionIterArgs()[index], condition)) + return true; + if (auto whileOp = dyn_cast(loopOp.getOperation())) { + if (index < whileOp.getAfterArguments().size() && + isUsedWithCondition(whileOp.getAfterArguments()[index], condition)) + return true; + } + return false; +} + +bool needsLegacyBlockDataLoopRewrite(LoopLikeOpInterface loopOp) { + auto hasPointerValue = [&](auto values) { + return llvm::any_of(values, [&](Value value) { + return containsLegacyTritonPointer(value.getType()); + }); + }; + + auto isScalarPointerValue = [](Value value) { + auto pointerType = dyn_cast(value.getType()); + return pointerType && !isa(pointerType.getPointeeType()); + }; + auto isTensorPointerValue = [](Value value) { + auto pointerType = dyn_cast(value.getType()); + return pointerType && isa(pointerType.getPointeeType()); + }; + + SmallVector boundaryValues; + boundaryValues.append(loopOp.getInits().begin(), loopOp.getInits().end()); + boundaryValues.append(loopOp.getRegionIterArgs().begin(), + loopOp.getRegionIterArgs().end()); + boundaryValues.append(loopOp->getResults().begin(), + loopOp->getResults().end()); + if (auto whileOp = dyn_cast(loopOp.getOperation())) + boundaryValues.append(whileOp.getAfterArguments().begin(), + whileOp.getAfterArguments().end()); + + bool hasScalarPointer = llvm::any_of(boundaryValues, isScalarPointerValue); + bool hasTensorPointer = llvm::any_of(boundaryValues, isTensorPointerValue); + // An opaque scalar-pointer loop has no legacy BlockData schema. Leave it + // untouched so the conversion driver reports an ordinary unsupported + // boundary instead of entering the descriptor parser with a non-memref + // source. Tensor-pointer loops continue through their established path. + if (hasScalarPointer && !hasTensorPointer) + return false; + + // Inspect the original SCF boundary, before the dialect converter remaps a + // Triton pointer to a reinterpret-cast memref. Such loops still belong to the + // legacy pointer conversion even though their converted init may look like + // an ordinary memref later in rewriteLoopOp(). + if (hasPointerValue(loopOp.getInits()) || + hasPointerValue(loopOp.getRegionIterArgs()) || + llvm::any_of(loopOp->getResultTypes(), containsLegacyTritonPointer)) + return true; + if (auto whileOp = dyn_cast(loopOp.getOperation())) { + if (hasPointerValue(whileOp.getAfterArguments())) + return true; + } + + // Preserve the pre-existing tensor-offset/mask path. These pointer-free + // integer tensors are nevertheless expanded by BlockData when they feed the + // address operands recognized by the legacy analysis. + for (auto [index, init] : llvm::enumerate(loopOp.getInits())) { + auto tensorType = dyn_cast(init.getType()); + if (!tensorType) + continue; + auto integerType = dyn_cast(tensorType.getElementType()); + if (!integerType || integerType.getWidth() == 1) + continue; + if (isLoopCarriedAddressValueUsed(loopOp, index, [](OpOperand *use) { + Operation *user = use->getOwner(); + return isa(user) || + (isa(user) && use->getOperandNumber() == 1) || + (isa(user) && use->getOperandNumber() == 2); + })) + return true; + } + return false; +} + +// This function is util function for rewriteLoopOp that create value from data. +// Assume data is structured, and from regionIterArg from LoopLikeOpInterface. +// +// For example, +// +// %7 = scf.for %arg2 = %c0_i32 to %c3_i32 step %c1_i32 iter_args(%arg3 = %4) -> +// (tensor<128xi32>) : i32 { +// %8 = tt.addptr %5, %arg3 : tensor<128x!tt.ptr>, tensor<128xi32> +// ... +// } +// +// is converted to +// +// %7 = scf.for %arg2 = %c0_i32 to %c3_i32 step %c1_i32 iter_args(%arg3 = %4, +// %arg4 = %5, %arg5 = %6) -> (tensor<128xi32>) : i32 { +// %scalarOffset = arith.index_cast %arg4 : index to i32 +// %scalarStride = arith.index_cast %arg5 : index to i32 +// ... +// %newRes = arith.addi %offset, %stride : tensor<128xi32> +// %8 = tt.addptr %5, %newRes : tensor<128x!tt.ptr>, tensor<128xi32> +// } +Value createFromData(RankedTensorType resType, const BlockData &data, + const Location &loc, OpBuilder &builder, + bool isMaskIterArg) { + auto resShape = resType.getShape(); + Value newRes = nullptr; + for (size_t i = 0; i < resShape.size(); i++) { + auto axisType = + RankedTensorType::get({resShape[i]}, resType.getElementType()); + auto axisI32Type = + RankedTensorType::get({resShape[i]}, builder.getIntegerType(32)); + Value axisValue = + builder.create(loc, axisI32Type, 0, resShape[i]); + if (axisType != axisI32Type) { + axisValue = builder.create(loc, axisType, axisValue); + } + Value offset = cast(data.getOffset(i)); + Value offsetValue = builder.create( + loc, resType.getElementType(), offset); + offsetValue = builder.create(loc, axisType, offsetValue); + Value stride = cast(data.getStride(i)); + if (!isMaskIterArg) { + Value strideValue = builder.create( + loc, resType.getElementType(), stride); + strideValue = builder.create(loc, axisType, strideValue); + axisValue = builder.create(loc, axisValue, strideValue); + } + axisValue = builder.create(loc, axisValue, offsetValue); + + for (size_t j = 0; j < resShape.size(); j++) { + if (i != j) + axisValue = builder.create(loc, axisValue, j); + } + axisValue = builder.create(loc, resType, axisValue); + if (newRes) { + newRes = builder.create(loc, newRes, axisValue); + } else { + newRes = axisValue; + } + } + return newRes; +} + +LogicalResult +BlockDataParser::rewriteLoopOp(LoopLikeOpInterface op, + ConversionPatternRewriter &rewriter, + llvm::SmallDenseMap &known, + ArrayRef onlyIndexTensorSlots) { + SmallVector newInitArgs; + SmallVector iterArgIdxMap; + SmallVector maskIterArgs; + int64_t argCnt = 0; + + SmallVector, 5> initArgIndexIfBlockData; + SmallVector, 5> knownPtrsTmp; + llvm::SmallDenseSet blockArgIdxSet; + + // Create a new list of init args + for (auto [i, arg] : llvm::enumerate(op.getInits())) { + auto mappedV = rewriter.getRemappedValue(arg); + memref::ReinterpretCastOp reintCastOp; + maskIterArgs.push_back(false); + + // If this init arg is supposed to be remapped, use the remapped + // value instead. + // In addition, if this init arg is a memref created by a reinterpret_cast + // or a tensor of index, there is a chance that it will be used in addptr. + // Create BlockData for each such init arg. + if (mappedV) { + // TODO: + // Passing a block argument pointer directly into a for loop not + // supported. + assert(!(isa(mappedV) && + isa(mappedV.getType())) && + "cannot take pointer block argument as init arg for for loop"); + if (auto reinterpretCastOp = + mappedV.getDefiningOp()) { + // Record memref::ReinterpretCastOp + reintCastOp = reinterpretCastOp; + newInitArgs.push_back(mappedV); + iterArgIdxMap.push_back(argCnt++); + } else { + newInitArgs.push_back(mappedV); + iterArgIdxMap.push_back(argCnt++); + } + } else { + newInitArgs.push_back(arg); + iterArgIdxMap.push_back(argCnt++); + } + + auto indexTensor = + isa(arg.getType()) && + isa(cast(arg.getType()).getElementType()) && + cast(cast(arg.getType()).getElementType()) + .getWidth() != 1 && + isLoopCarriedValueUsedWithCondition(op, i, [](OpOperand *use) { + auto *user = use->getOwner(); + return isa(user) || + (isa(user) && use->getOperandNumber() == 1) || + (isa(user) && use->getOperandNumber() == 2); + }); + + if (!onlyIndexTensorSlots.empty() && + !llvm::is_contained(onlyIndexTensorSlots, static_cast(i))) + indexTensor = false; + + // Handle memref::ReinterpretCastOp and tensor specially + if (!reintCastOp && !indexTensor) + continue; + + BlockData data; + if (reintCastOp) { + parseReinterpretCast(reintCastOp, data, op.getLoc(), rewriter, + llvm::SmallDenseMap(0)); + } else { + if (failed(parse(arg, data, op.getLoc(), rewriter, + llvm::SmallDenseMap(0)))) + return failure(); + } + + maskIterArgs[i] = + indexTensor && + isLoopCarriedValueUsedWithCondition(op, i, [](OpOperand *use) { + auto *user = use->getOwner(); + return (isa(user) && use->getOperandNumber() == 1) || + (isa(user) && use->getOperandNumber() == 2); + }); + + if (indexTensor) { + newInitArgs.back() = nullptr; + iterArgIdxMap.back() = -1; + argCnt--; + } + + // Record the BlockData for later processing + initArgIndexIfBlockData.push_back(std::make_pair(i, data)); + } + + // Validate every structural backedge before creating constants or a new + // loop. This prevents a partially expanded signature when an opaque memref + // source cannot provide offsets and strides. + auto preflightBackedge = [&](ValueRange values, + StringRef edgeName) -> LogicalResult { + for (auto [index, data] : initArgIndexIfBlockData) { + (void)data; + if (index < 0 || static_cast(index) >= values.size()) { + op->emitError("legacy BlockData preflight found a missing ") + << edgeName << " value for carried slot " << index; + return failure(); + } + Value value = values[index]; + if (!canAnalyzeLegacyBlockDataBackedge(op, index, value)) { + InFlightDiagnostic diagnostic = op->emitError( + "legacy BlockData preflight cannot analyze carried slot "); + diagnostic << index << " on the " << edgeName << " edge produced by "; + if (Operation *producer = value.getDefiningOp()) + diagnostic << producer->getName(); + else + diagnostic << "an unknown source"; + return failure(); + } + } + return success(); + }; + if (auto forOp = dyn_cast(op.getOperation())) { + if (failed(preflightBackedge(forOp.getYieldedValues(), "yield"))) + return failure(); + } else if (auto whileOp = dyn_cast(op.getOperation())) { + if (failed(preflightBackedge(whileOp.getConditionOp().getArgs(), + "condition")) || + failed(preflightBackedge(whileOp.getYieldOp().getOperands(), "yield"))) + return failure(); + } + + // Set insertion point to be before the for loop for new variables passed + // into the new loop. + auto origIp = rewriter.saveInsertionPoint(); + rewriter.setInsertionPoint(op); + + // For each of the BlockData recorded in the last step, insert new + // instructions to describe offset and stride for each dimension and append + // them to init args + for (auto [i, data] : initArgIndexIfBlockData) { + // For each dimension, if the corresponding offset and stride is an + // integer attribute, create a constant value and append them at the + // end of init arg list, which is prepared for calculate layout info with + // loop interation index + for (auto &dataOffset : data.getOffsetsRef()) { + if (isa(dataOffset)) { + auto constDataOffset = cast(dataOffset); + assert(isa(constDataOffset)); + auto constOp = rewriter.create( + op.getLoc(), rewriter.getIndexAttr( + dyn_cast(constDataOffset).getInt())); + newInitArgs.push_back(constOp.getResult()); + dataOffset = constOp.getResult(); + } else { + assert(isa(cast(dataOffset).getType())); + newInitArgs.push_back(cast(dataOffset)); + } + } + + for (auto &dataStride : data.getStridesRef()) { + if (isa(dataStride)) { + auto constDataStride = cast(dataStride); + assert(isa(constDataStride)); + auto constOp = rewriter.create( + op.getLoc(), rewriter.getIndexAttr( + dyn_cast(constDataStride).getInt())); + newInitArgs.push_back(constOp.getResult()); + dataStride = constOp.getResult(); + } else { + assert(isa(cast(dataStride).getType())); + newInitArgs.push_back(cast(dataStride)); + } + } + + // Note that we want the knownPtrs to be indexed by block arg, but we + // only have index for now. Also, the blockdata we record is the init + // arg, but want to to use newly created block arg. These block args + // are not created yet. We will translate this mapping later. + knownPtrsTmp.push_back(std::make_pair(i, data)); + blockArgIdxSet.insert(i); + + // If the original init arg is a memref produced by reinterpret_cast, + // create a new memref using new strides and offsets created above. + // This produces a canonicalized memref, which will match what the + // for loop generates if it modifies the memref. E.g., original + // reinterpret_cast can produce a memref with const stride: + // - memref<4x256xbf16, affine_map<(d0, d1)[s0, s1] -> (d0 * 256 + + // s0 + d1 + // * s1)>> + // The new reinterpret_cast will always have dynamic stride and + // offset: + // - memref<4x256xbf16, affine_map<(d0, d1)[s0, s1, s2] -> (d0 * s1 + // + s0 + d1 * s2)>> + if (newInitArgs[i] && + newInitArgs[i].getDefiningOp()) { + SmallVector resultShape; + for (auto size : data.getSizesRef()) { + auto constSize = getConstantIntValue(size); + assert(constSize && "expected constant size"); + resultShape.push_back(constSize.value()); + } + + // In current block data layout info, strides and offsets must be dynamic + // value + FailureOr castOp = + data.createCastOp(resultShape, op.getLoc(), rewriter); + if (failed(castOp)) + return op.emitOpError( + "could not materialize a loop-carried memref from the source type"); + if (resultShape.size() > 1) { + auto originalOffset = dyn_cast(data.getOffsetsRef()[0]); + for (auto &offsets : newInitArgs) { + if (offsets == originalOffset) { + offsets = (*castOp).getOffsets()[0]; + break; + } + } + data.getOffsetsRef()[0] = (*castOp).getOffsets()[0]; + } + + LLVM_DEBUG({ + llvm::dbgs() << "new reinterpret_cast with dynamic sizes " + "and offsets:"; + (*castOp).print(llvm::dbgs(), OpPrintingFlags().printGenericOpForm()); + llvm::dbgs() << "\n"; + }); + + newInitArgs[i] = (*castOp).getResult(); + } + } + + rewriter.restoreInsertionPoint(origIp); + IRMapping mapping; + + // Create a new LoopOp that uses updated init args and same loop body + LoopLikeOpInterface newOp; + auto newInits = to_vector( + make_filter_range(newInitArgs, [](Value v) { return v != nullptr; })); + auto commonBodyBuilder = [&](OpBuilder &b, Location loc, bool useInit, + ValueRange newRegionArgs, Region ®ion, + Block::BlockArgListType regionArgs, + ArrayRef isUsedForRegionArgs, + ArrayRef maskIterArgs) { + auto newArgIter = newRegionArgs.begin(); + for (const auto &[regionArg, isUsedForRegionArg] : + llvm::zip(regionArgs, isUsedForRegionArgs)) { + if (isUsedForRegionArg) { + mapping.map(regionArg, *newArgIter); + ++newArgIter; + } + } + + // Convert the book-keeping data structure to use the correct key and value. + // Key is converted from init arg index to newly created block arg, and + // Value's BlockData fields are converted from init arg to newly created + // block arg + + // TODO: remove (useInit = true) logic after supporting make_tensor_ptr + if (useInit) { + for (auto [i, data] : knownPtrsTmp) { + for (auto &offset : data.getOffsetsRef()) { + offset = *newArgIter; + ++newArgIter; + } + + for (auto &stride : data.getStridesRef()) { + stride = *newArgIter; + ++newArgIter; + } + + auto regionArg = regionArgs[i]; + auto key = mapping.lookupOrNull(regionArg); + if (!key) { + // Create IndexTensor regionArg from computed offset and stride data + key = createFromData(cast(regionArg.getType()), + data, op.getLoc(), rewriter, maskIterArgs[i]); + mapping.map(regionArg, key); + } + known.insert(std::make_pair(key, data)); + } + } else { + for (auto [i, isUsedForRegionArg] : + llvm::enumerate(isUsedForRegionArgs)) { + if (!isUsedForRegionArg) { + BlockData data; + auto regionArg = regionArgs[i]; + auto regionArgType = cast(regionArg.getType()); + data.getOffsetsRef().resize(regionArgType.getRank()); + data.getStridesRef().resize(regionArgType.getRank()); + for (auto &offset : data.getOffsetsRef()) { + offset = *newArgIter; + ++newArgIter; + } + for (auto &dim : regionArgType.getShape()) { + data.getSizesRef().push_back(rewriter.getIndexAttr(dim)); + } + for (auto &stride : data.getStridesRef()) { + stride = *newArgIter; + ++newArgIter; + } + + auto key = mapping.lookupOrNull(regionArg); + if (!key) { + // Create IndexTensor regionArg from computed offset and stride data + key = createFromData(regionArgType, data, op.getLoc(), rewriter, + maskIterArgs[i]); + mapping.map(regionArg, key); + } + known.insert(std::make_pair(key, data)); + } + } + } + + for (auto &bodyOp : region.getOps()) + b.clone(bodyOp, mapping); + }; + for (const auto &[initArg, newInitArg] : + llvm::zip(op.getInits(), newInitArgs)) { + if (newInitArg) { + mapping.map(initArg, newInitArg); + } + } + SmallVector newResults; + SmallVector markerSlotMap; + if (auto forOp = dyn_cast(op.getOperation())) { + SmallVector usedForRegionArgs; + for (auto newInitArg : newInitArgs) { + usedForRegionArgs.push_back(newInitArg ? true : false); + } + newOp = rewriter.create( + forOp.getLoc(), forOp.getLowerBound(), forOp.getUpperBound(), + forOp.getStep(), newInits, + [&](OpBuilder &b, Location loc, Value iv, ValueRange args) { + mapping.map(forOp.getInductionVar(), iv); + commonBodyBuilder(b, loc, true, args, forOp.getRegion(), + op.getRegionIterArgs(), usedForRegionArgs, + maskIterArgs); + }); + + // Replace only the results that correspond to the original scf.for + auto newResultIter = newOp->result_begin(); + rewriter.setInsertionPointAfter(newOp); + for (const auto &[res, regionArg, newIterArgIdx, mask] : + llvm::zip_equal(op->getResults(), op.getRegionIterArgs(), + iterArgIdxMap, maskIterArgs)) { + if (newIterArgIdx != -1) { + newResults.push_back(*newResultIter); + ++newResultIter; + } else { + auto key = mapping.lookup(regionArg); + auto data = known.at(key); + for (auto &offset : data.getOffsetsRef()) + offset = + newOp.getTiedLoopResult(cast(cast(offset))); + for (auto &stride : data.getStridesRef()) + stride = + newOp.getTiedLoopResult(cast(cast(stride))); + auto newRes = + createFromData(cast(regionArg.getType()), data, + op.getLoc(), rewriter, mask); + newResults.push_back(newRes); + } + } + markerSlotMap = iterArgIdxMap; + } else if (auto whileOp = dyn_cast(op.getOperation())) { + SmallVector resultTypes; + SmallVector usedForBeforeRegionArgs; + SmallVector usedForAfterRegionArgs; + llvm::SmallDenseSet blockArgIdxSetForAfter; + SmallVector iterArgIdxMapForAfter; + SmallVector maskIterArgsForAfter(whileOp->getNumResults()); + + int64_t indexCnt = 0; + + for (auto newInitArg : newInitArgs) { + usedForBeforeRegionArgs.push_back(newInitArg ? true : false); + } + for (size_t i = 0; i < whileOp->getNumResults(); i++) { + auto resType = whileOp->getResultTypes()[i]; + auto indexTensor = + isa(resType) && + isa(cast(resType).getElementType()) && + isLoopCarriedValueUsedWithCondition(whileOp, i, [](OpOperand *use) { + auto *user = use->getOwner(); + return isa(user) || + (isa(user) && + use->getOperandNumber() == 1) || + (isa(user) && use->getOperandNumber() == 2); + }); + if (indexTensor) { + indexCnt += 2 * cast(resType).getRank(); + usedForAfterRegionArgs.push_back(false); + iterArgIdxMapForAfter.push_back(-1); + maskIterArgsForAfter[i] = + isLoopCarriedValueUsedWithCondition(whileOp, i, [](OpOperand *use) { + auto *user = use->getOwner(); + return (isa(user) && + use->getOperandNumber() == 1) || + (isa(user) && + use->getOperandNumber() == 2); + }); + blockArgIdxSetForAfter.insert(i); + } else { + resultTypes.push_back(resType); + usedForAfterRegionArgs.push_back(true); + iterArgIdxMapForAfter.push_back(argCnt++); + } + } + resultTypes.append(indexCnt, rewriter.getIndexType()); + newOp = rewriter.create( + whileOp.getLoc(), resultTypes, newInits, + [&](OpBuilder &b, Location loc, ValueRange args) { + commonBodyBuilder(b, loc, true, args, whileOp.getBefore(), + whileOp.getBeforeArguments(), + usedForBeforeRegionArgs, maskIterArgs); + }, + [&](OpBuilder &b, Location loc, ValueRange args) { + commonBodyBuilder(b, loc, false, args, whileOp.getAfter(), + whileOp.getAfterArguments(), usedForAfterRegionArgs, + maskIterArgsForAfter); + }); + + auto newResultIter = newOp->result_begin(); + rewriter.setInsertionPointAfter(newOp); + for (const auto &[res, regionArg, newIterArgIdx, mask] : + llvm::zip_equal(op->getResults(), whileOp.getAfterArguments(), + iterArgIdxMapForAfter, maskIterArgsForAfter)) { + if (newIterArgIdx != -1) { + newResults.push_back(*newResultIter); + ++newResultIter; + } else { + auto key = mapping.lookup(regionArg); + auto data = known.at(key); + for (auto &offset : data.getOffsetsRef()) + offset = newOp->getResult( + cast(cast(offset)).getArgNumber()); + for (auto &stride : data.getStridesRef()) + stride = newOp->getResult( + cast(cast(stride)).getArgNumber()); + auto newRes = + createFromData(cast(regionArg.getType()), data, + op.getLoc(), rewriter, mask); + newResults.push_back(newRes); + } + } + + auto conditionOp = + cast(newOp.getOperation()).getConditionOp(); + if (failed(rewriteTerminator(conditionOp, rewriter, blockArgIdxSetForAfter, + iterArgIdxMapForAfter, known))) + return failure(); + markerSlotMap = iterArgIdxMapForAfter; + } + + if (!newOp || newResults.size() != op->getNumResults()) + return op->emitError( + "loop rewrite produced a result list with incompatible arity"); + + // Copy all attributes from op to newOp + newOp->setAttrs(op->getAttrs()); + if (!onlyIndexTensorSlots.empty()) { + if (Attribute marker = + op->getAttr(controlflow::kPointerDescriptorBoundaryAttr)) { + auto descriptorSlots = dyn_cast(marker); + if (!descriptorSlots) { + rewriter.eraseOp(newOp.getOperation()); + return op->emitError("invalid pointer descriptor boundary marker"); + } + + SmallVector remappedSlots; + remappedSlots.reserve(descriptorSlots.size()); + for (int32_t slot : descriptorSlots.asArrayRef()) { + if (slot < 0 || static_cast(slot) >= markerSlotMap.size() || + markerSlotMap[slot] < 0 || + markerSlotMap[slot] > std::numeric_limits::max()) { + rewriter.eraseOp(newOp.getOperation()); + return op->emitError( + "pointer descriptor slot cannot be remapped after range rewrite"); + } + remappedSlots.push_back(static_cast(markerSlotMap[slot])); + } + newOp->setAttr(controlflow::kPointerDescriptorBoundaryAttr, + DenseI32ArrayAttr::get(op->getContext(), remappedSlots)); + } + } + rewriter.replaceOp(op, newResults); + + // Update the loop body. Manually invoke the rewrite logic on addptr and yield + // in the loop body, so we can take advantage of the states we built up + for (auto *region : newOp.getLoopRegions()) { + for (auto &bodyOp : region->getOps()) { + if (isDistributedTypeCustomOp(&bodyOp)) { + rewriteStructuredCustomOp(&bodyOp, rewriter); + } else if (auto addptrOp = dyn_cast(bodyOp)) { + // FIXME: Constructed adaptor here does not hold the transformed op + // info. + auto adaptor = triton::AddPtrOp::Adaptor(addptrOp); + if (failed(rewriteAddPtr(addptrOp, adaptor, rewriter, known))) + return failure(); + } else if (auto advanceOp = dyn_cast(bodyOp)) { + if (failed(rewriteAdvanceOp(advanceOp, rewriter, known))) + return failure(); + } else if (auto makeTensorPtrOp = + dyn_cast(bodyOp)) { + ConversionPatternRewriter::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(makeTensorPtrOp); + if (failed(rewriteMakeTensorPtrOp( + makeTensorPtrOp, + rewriter.getRemappedValue(makeTensorPtrOp.getBase()), rewriter, + known))) + return failure(); + } else if (auto loopOp = dyn_cast(bodyOp); + loopOp && !loopOp->hasAttr("ExtractedLoadOrStore")) { + ConversionPatternRewriter::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(loopOp); + // Remove UnhandledLoopOp attr before process + rewriter.modifyOpInPlace( + loopOp, [&]() { loopOp->removeAttr("UnhandledLoopOp"); }); + if (failed(rewriteLoopOp(loopOp, rewriter, known))) + return failure(); + } + } + } + + if (!op.getRegionIterArgs().empty()) { + auto yieldOp = cast( + newOp.getLoopRegions().back()->back().getTerminator()); + if (failed(rewriteTerminator(yieldOp, rewriter, blockArgIdxSet, + iterArgIdxMap, known))) + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "new loop\n"; + newOp.getOperation()->print(llvm::dbgs(), + OpPrintingFlags().printGenericOpForm()); + llvm::dbgs() << "\n"; + }); + return success(); +} + +/// @brief Rewrite the triton::AddPtrOp to handle unstructured memory access. +/// @param op The triton::AddPtrOp to be rewritten. +/// @param adaptor The adaptor of the triton::AddPtrOp, used to get operands. +/// @param rewriter The pattern rewriter used to modify the IR. +/// @param data The BlockData containing information about the memory access. +LogicalResult BlockDataParser::rewriteAddPtrToUnstrucMemAcc( + triton::AddPtrOp op, triton::AddPtrOp::Adaptor &adaptor, + ConversionPatternRewriter &rewriter, BlockData &data) { + auto loc = op.getLoc(); + auto &offsets = data.getOffsetsRef(); + auto &blockSizes = data.getSizesRef(); + auto &strides = data.getStridesRef(); + Value ptrOffset = adaptor.getOffset(); + Value zeroIdx = + rewriter.create(loc, rewriter.getIndexAttr(0)); + Value oneIdx = + rewriter.create(loc, rewriter.getIndexAttr(1)); + auto addptrRes = op.getResult(); + assert(addptrRes.hasOneUse() && "Invalid: tt.addptr has multiple users"); + auto loadOp = *(addptrRes.user_begin()); + + // Prepare empty tensor for loop based scalar load + // FIXME: We use cast here because addptr must return tensor>. + // True? + auto resTy = cast(addptrRes.getType()); + auto resEPtrTy = resTy.getElementType(); + auto resETy = cast(resEPtrTy).getPointeeType(); + Value loaded = rewriter.create(loc, blockSizes, resETy); + SmallVector initArgs; + initArgs.push_back(loaded); + + SmallVector forLBs; + SmallVector forUBs; + SmallVector forSteps; + for (auto &s : offsets) { + forLBs.push_back(zeroIdx); + } + for (auto &s : blockSizes) { + forUBs.push_back(getValueOrCreateConstantIndexOp(rewriter, loc, s)); + } + for (auto &s : strides) { + forSteps.push_back(oneIdx); + } + SmallVector ivs; + bool castFailed = false; + OpBuilder builder(op); + auto loop = createNestedLoops( + builder, loc, 0, blockSizes.size(), forLBs, forUBs, forSteps, ivs, + initArgs, + [&](OpBuilder &bB, Location bLoc, SmallVector &allIVs, + ValueRange iterArgs) { + OpBuilder::InsertionGuard g(bB); + bB.setInsertionPointToStart(bB.getBlock()); + + Value scalarOffsetRaw = + bB.create(bLoc, ptrOffset, allIVs); + Value scalarOffset = bB.create( + bLoc, bB.getIndexType(), scalarOffsetRaw); + OpFoldResult baseOffset = bB.getIndexAttr(0); + for (auto ofr : data.getOffsetsRef()) { + baseOffset = + addOpFoldResult(baseOffset, ofr, bLoc, bB, bB.getIndexType()); + } + Value baseVal = getValueOrCreateConstantIndexOp(bB, bLoc, baseOffset); + Value combinedOffset = + bB.create(bLoc, baseVal, scalarOffset); + // Replace offset & size. Only single element. + data.getOffsetsRef().clear(); + data.getOffsetsRef().push_back(combinedOffset); + data.getSizesRef().clear(); + data.getSizesRef().push_back(bB.getIndexAttr(1)); + data.getStridesRef().clear(); + data.getStridesRef().push_back(bB.getIndexAttr(1)); + FailureOr castOp = + data.createCastOp({1}, bLoc, bB); + if (failed(castOp)) { + castFailed = true; + return; + } + rewriter.replaceOp(op, (*castOp).getResult()); + // Move tt.load using this tt.addptr into this block + loadOp->moveAfter((*castOp).getOperation()); + loadOp->setAttr("IndirectLoad", UnitAttr::get(op.getContext())); + bB.create(bLoc, iterArgs); + }); + if (castFailed) + return op.emitOpError("could not materialize the indirect pointer source"); + return success(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToLinalg/CMakeLists.txt b/third_party/ascend/lib/TritonToLinalg/CMakeLists.txt new file mode 100644 index 0000000000..9d01a991dd --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/CMakeLists.txt @@ -0,0 +1,40 @@ +add_triton_library(TritonToLinalg + TritonToLinalgPass.cpp + LoadStoreConverter.cpp + FunctionConverter.cpp + ArgMinMaxConverter.cpp + TritonOpConverter.cpp + HoistBroadcast.cpp + BlockPtrAnalysis.cpp + UseAnalysis.cpp + ImplicitPermute.cpp + DescriptorConverter.cpp + MarkTensorKindPass.cpp + DevicePrintOffsetRewrite.cpp + DeduplicateDebugNopsPass.cpp + CanonicalizeDebugLocationsPass.cpp + + DEPENDS + TritonToLinalgConversionPassIncGen + + LINK_LIBS PUBLIC + MLIRArithDialect + MLIRDialectUtils + MLIRIR + MLIRLLVMDialect + MLIRMathDialect + MLIRPass + MLIRTensorDialect + MLIRTransforms + MLIRSupport + BiShengIRHIVMDialect + BiShengIRScopeDialect + TritonIR + TritonTransforms + TritonAnalysis + TritonMemoryAccessAnalysis + TritonToGraph + MLIRTritonNPUUtils + MLIRSCFTransforms + MLIRLinalgTransforms +) diff --git a/third_party/ascend/lib/TritonToLinalg/CanonicalizeDebugLocationsPass.cpp b/third_party/ascend/lib/TritonToLinalg/CanonicalizeDebugLocationsPass.cpp new file mode 100644 index 0000000000..f767d1c947 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/CanonicalizeDebugLocationsPass.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +//===----------------------------------------------------------------------===// +// CanonicalizeDebugLocationsPass +// +// Collapses call-site locations whose callee is an inlined Triton stdlib +// helper (under site-packages) down to their caller (user-file) frame, so the +// emitted DWARF references the user's source line instead of the library. +// Fixes phantom line-table rows such as standard.py:306 produced by an inlined +// tl.sum reduction. +// +// Run BEFORE DeduplicateDebugNopsPass so NOP anchors are deduped against +// already-canonicalized (user-frame) locations. +// +// Gated by env var LLVM_EXTRACT_DI_LOCAL_VARIABLES=1. +//===----------------------------------------------------------------------===// + +#include "TritonToLinalg/CanonicalizeDebugLocationsPass.h" +#include "Utils/DebugUtils.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Location.h" +#include "mlir/Pass/Pass.h" + +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "canonicalize-debug-locations" + +using namespace mlir; +using namespace mlir::triton; + +namespace { + +struct CanonicalizeDebugLocationsPass + : public PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CanonicalizeDebugLocationsPass) + + StringRef getArgument() const final { return "canonicalize-debug-locations"; } + + StringRef getDescription() const final { + return "Collapse inlined stdlib call-site locations to the user frame."; + } + + void runOnOperation() override { + if (!mlir::triton::debug::isDebugNopEnabled()) + return; + + [[maybe_unused]] unsigned rewritten = 0; + getOperation().walk([&](Operation *op) { + Location oldLoc = op->getLoc(); + Location newLoc = mlir::triton::debug::collapseForeignCallsites(oldLoc); + if (newLoc != oldLoc) { + op->setLoc(newLoc); + ++rewritten; + } + }); + + LLVM_DEBUG(llvm::dbgs() << "[canon-debug-locs] rewrote " << rewritten + << " foreign call-site locations\n"); + } +}; + +} // namespace + +namespace mlir { +namespace triton { + +std::unique_ptr createCanonicalizeDebugLocationsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToLinalg/DeduplicateDebugNopsPass.cpp b/third_party/ascend/lib/TritonToLinalg/DeduplicateDebugNopsPass.cpp new file mode 100644 index 0000000000..47ac32aab4 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/DeduplicateDebugNopsPass.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +//===----------------------------------------------------------------------===// +// DeduplicateDebugNopsPass +// +// Walks each function and removes redundant `llvm.inline_asm "nop"` ops that +// were inserted as DWARF anchors by the triton-to-linalg converters. Keeps +// the first NOP per unique source line; erases the rest. +// +// Source lines are resolved with the shared user-frame resolver +// (debug::unwrapToUserFileLineCol), so a NOP carrying an inlined call-site loc +// keys on the user's line rather than the inlined callee. Run this AFTER +// CanonicalizeDebugLocationsPass. +// +// Runtime-gated via the LLVM_EXTRACT_DI_LOCAL_VARIABLES environment variable. +//===----------------------------------------------------------------------===// + +#include "TritonToLinalg/DeduplicateDebugNopsPass.h" +#include "Utils/DebugUtils.h" + +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Location.h" +#include "mlir/Pass/Pass.h" + +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "dedup-debug-nops" + +using namespace mlir; +using namespace mlir::triton; + +namespace { + +struct DeduplicateDebugNopsPass + : public PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(DeduplicateDebugNopsPass) + + StringRef getArgument() const final { return "deduplicate-debug-nops"; } + + StringRef getDescription() const final { + return "Deduplicate llvm.inline_asm 'nop' debug-anchor ops by source line."; + } + + void getDependentDialects(DialectRegistry ®istry) const override { + registry.insert(); + } + + void runOnOperation() override { + // Opt-in via env var; do nothing in production builds. + if (!mlir::triton::debug::isDebugNopEnabled()) + return; + + ModuleOp moduleOp = getOperation(); + // Only read inside LLVM_DEBUG, which is a no-op under NDEBUG. + [[maybe_unused]] unsigned totalDropped = 0; + [[maybe_unused]] unsigned totalKept = 0; + + moduleOp.walk([&](func::FuncOp func) { + // Per-function dedup: key = (filename, line). Column intentionally + // ignored -- for debugger stepping behaviour, two NOPs on the same line + // at different columns are equivalent. The StringRef points into a + // StringAttr owned by the MLIRContext, which outlives this pass. + llvm::DenseSet> seen; + llvm::SmallVector toErase; + + func.walk([&](Operation *op) { + if (!mlir::triton::debug::isDebugNop(op)) + return; + + // Resolve to the user's source line (caller frame for call sites). + FileLineColLoc flc = + mlir::triton::debug::unwrapToUserFileLineCol(op->getLoc()); + if (!flc) { + // No resolvable source location; leave the NOP alone. + LLVM_DEBUG(llvm::dbgs() + << "[dedup-nops] NOP with no FileLineColLoc, keeping: " + << *op << "\n"); + ++totalKept; + return; + } + + auto key = std::make_pair(flc.getFilename().getValue(), flc.getLine()); + if (!seen.insert(key).second) { + toErase.push_back(op); + } + }); + + totalKept += seen.size(); + totalDropped += toErase.size(); + + for (Operation *op : toErase) + op->erase(); + }); + + LLVM_DEBUG(llvm::dbgs() + << "[dedup-nops] dropped " << totalDropped + << " duplicate NOPs, kept " << totalKept << " unique anchors\n"); + } +}; + +} // namespace + +namespace mlir { +namespace triton { + +std::unique_ptr createDeduplicateDebugNopsPass() { + return std::make_unique(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToLinalg/DescriptorConverter.cpp b/third_party/ascend/lib/TritonToLinalg/DescriptorConverter.cpp new file mode 100644 index 0000000000..d204d0ea3d --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/DescriptorConverter.cpp @@ -0,0 +1,566 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToLinalg/DescriptorConverter.h" +#include "TritonToLinalg/BlockPtrAnalysis.h" +#include "TritonToLinalg/MaskAnalysis.h" +#include "TritonToLinalg/TritonOpConverter.h" +#include "TritonToLinalg/TritonToLinalgPass.h" +#include "Utils/Utils.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" +#include + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/Transforms/DialectConversion.h" + +namespace DescriptorConverter { +using namespace mlir; +using namespace triton; + +bool hasATensorDescriptorType(mlir::TypeRange types) { + return llvm::any_of(types, [](mlir::Type t) { + return llvm::isa(t); + }); +} + +Descriptor unpackDescriptor(TensorDescType type, Value desc, + ConversionPatternRewriter &rewriter) { + auto makeDescOp = desc.getDefiningOp(); + assert(makeDescOp && "Descriptor must be defined by MakeTensorDescOp"); + + Descriptor res; + + res.base = makeDescOp.getBase(); + for (auto s : makeDescOp.getShape()) { + res.shape.push_back(rewriter.createOrFold( + makeDescOp.getLoc(), rewriter.getI64Type(), s)); + } + for (auto st : makeDescOp.getStrides()) { + res.strides.push_back(rewriter.createOrFold( + makeDescOp.getLoc(), rewriter.getI64Type(), st)); + } + res.padding = makeDescOp.getPaddingAttr(); + + return res; +} + +SmallVector computeOrder(ArrayRef shape) { + SmallVector order; + int rank = shape.size(); + order.reserve(rank); + // default by [dims - 1, ..., 0] + for (int i = rank - 1; i >= 0; --i) { + order.push_back(i); + } + return order; +} + +DenseI32ArrayAttr getFullBoundaryCheckAttr(ConversionPatternRewriter &rewriter, + ArrayRef shape) { + SmallVector boundaryCheck; + boundaryCheck.reserve(shape.size()); + for (int32_t dim = 0; dim < static_cast(shape.size()); ++dim) { + boundaryCheck.push_back(dim); + } + return rewriter.getDenseI32ArrayAttr(boundaryCheck); +} + +Value expandOffsets(OpBuilder &builder, Location loc, + ArrayRef blockShape, Value offsets, unsigned dim) { + Value expandedResult = offsets; + for (size_t j = 0; j < blockShape.size(); ++j) { + if (j == dim) { + continue; + } + expandedResult = + builder.create(loc, expandedResult, j); + } + + return expandedResult; +} + +Value getExpandedOffsetWithRange(OpBuilder &builder, const Location &loc, + ArrayRef blockShape, + Value offset, unsigned dim) { + // Add range + auto indexI32RowType = + RankedTensorType::get({blockShape[dim]}, builder.getI32Type()); + auto indexRowType = + RankedTensorType::get({blockShape[dim]}, builder.getI64Type()); + Value splatOffset = + builder.create(loc, indexRowType, offset); + Value range = builder.create(loc, indexI32RowType, 0, + blockShape[dim]); + Value i64Range = builder.create(loc, indexRowType, range); + + Value offsets = builder.create(loc, splatOffset, i64Range); + return expandOffsets(builder, loc, blockShape, offsets, dim); +} + +Value generatePtrFromOffsetRanges(OpBuilder &builder, Location loc, + ArrayRef blockShape, + Descriptor &desc, ValueRange offsets) { + assert(blockShape.size() == desc.shape.size()); + assert(blockShape.size() == offsets.size()); + auto indexTensorType = + RankedTensorType::get(blockShape, builder.getI64Type()); + auto ptrType = cast(desc.base.getType()); + auto ptrTensorType = RankedTensorType::get(blockShape, ptrType); + + // Generate offsets per dimension + Value ptr = builder.create(loc, ptrTensorType, desc.base); + for (unsigned i = 0; i < blockShape.size(); ++i) { + // We must splat strides into the expanded shape not a row for retaining + // the divisibility information given by strides + Value splatStride = builder.create( + loc, offsets[i].getType(), desc.strides[i]); + Value offsetWithStride = + builder.create(loc, offsets[i], splatStride); + Value broadcasted = builder.create( + loc, indexTensorType, offsetWithStride); + + // Add to the pointer + ptr = + builder.create(loc, ptrTensorType, ptr, broadcasted); + } + + return ptr; +} + +Value generatePtr(OpBuilder &builder, const Location &loc, + ArrayRef blockShape, Descriptor &desc, + ValueRange offsets) { + assert(blockShape.size() == desc.shape.size()); + assert(blockShape.size() == offsets.size()); + SmallVector offsetRanges; + for (unsigned i = 0; i < blockShape.size(); ++i) { + auto offsetWithRange = + getExpandedOffsetWithRange(builder, loc, blockShape, offsets[i], i); + offsetRanges.push_back(offsetWithRange); + } + + return generatePtrFromOffsetRanges(builder, loc, blockShape, desc, + offsetRanges); +} + +Value generateMaskFromOffsetRanges(OpBuilder &builder, const Location &loc, + ArrayRef blockShape, + Descriptor &desc, ValueRange offsetRanges) { + assert(blockShape.size() == desc.shape.size()); + assert(blockShape.size() == offsetRanges.size()); + + // Generate mask per dimension + auto maskTensorType = RankedTensorType::get(blockShape, builder.getI1Type()); + Value mask; + for (std::size_t i = 0; i < blockShape.size(); ++i) { + auto offsetWithRange = offsetRanges[i]; + + // Compare with lower bound + Value lowerBound = builder.create( + loc, builder.getI64Type(), 0); + Value splatLowerBound = builder.create( + loc, offsetWithRange.getType(), lowerBound); + Value cmpLower = builder.create( + loc, arith::CmpIPredicate::sge, offsetWithRange, splatLowerBound); + + // Compare with upper bound + Value splatUpperBound = builder.create( + loc, offsetWithRange.getType(), desc.shape[i]); + Value cmpUpper = builder.create( + loc, arith::CmpIPredicate::slt, offsetWithRange, splatUpperBound); + + // And and broadcast + Value andResult = builder.create(loc, cmpLower, cmpUpper); + Value broadcasted = + builder.create(loc, maskTensorType, andResult); + + // And up all results + if (!mask) { + mask = broadcasted; + } else { + mask = builder.create(loc, mask, broadcasted); + } + } + + return mask; +} + +Value generateMask(OpBuilder &builder, const Location &loc, + ArrayRef blockShape, Descriptor &desc, + ValueRange offsets) { + assert(blockShape.size() == desc.shape.size()); + assert(blockShape.size() == offsets.size()); + SmallVector offsetRanges; + for (unsigned i = 0; i < blockShape.size(); ++i) { + auto offsetWithRange = + getExpandedOffsetWithRange(builder, loc, blockShape, offsets[i], i); + offsetRanges.push_back(offsetWithRange); + } + + return generateMaskFromOffsetRanges(builder, loc, blockShape, desc, + offsetRanges); +} + +SmallVector castToI64(OpBuilder &builder, + mlir::ValueRange values) { + auto i64Type = builder.getI64Type(); + return llvm::map_to_vector(values, [&](mlir::Value v) { + return builder.createOrFold(v.getLoc(), i64Type, v); + }); +} + +LogicalResult DescriptorLoadConverter::matchAndRewrite( + triton::DescriptorLoadOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + const auto blockShape = op.getDesc().getType().getBlockType().getShape(); + auto descTy = op.getDesc().getType(); + auto indices = op.getIndices(); + + // 1. unpack descriptor + auto desc = unpackDescriptor(descTy, adaptor.getDesc(), rewriter); + + // 2. create make_tensor_ptr + SmallVector tensorShapeValues; + for (auto dim : blockShape) { + tensorShapeValues.push_back(static_cast(dim)); + } + Value tensorPtr = + rewriter.create(loc, + desc.base, // base + desc.shape, // shape + desc.strides, // strides + indices, // offset + tensorShapeValues, // tensorShape + computeOrder(blockShape) // order + ); + // 3. replace tt.load + auto boundaryCheck = getFullBoundaryCheckAttr(rewriter, blockShape); + triton::PaddingOptionAttr padding = desc.padding; + auto cache = triton::CacheModifierAttr::get(rewriter.getContext(), + triton::CacheModifier::NONE); + auto evict = triton::EvictionPolicyAttr::get(rewriter.getContext(), + triton::EvictionPolicy::NORMAL); + auto isVolatile = rewriter.getBoolAttr(false); + + if (auto a = op->getAttrOfType("cache")) + cache = a; + if (auto a = op->getAttrOfType("evict")) + evict = a; + if (auto a = op->getAttrOfType("isVolatile")) + isVolatile = a; + + auto newLoad = rewriter.create( + loc, descTy.getSignlessBlockType(), tensorPtr, + Value(), // mask + Value(), // other + boundaryCheck, padding, cache, evict, isVolatile); + + rewriter.replaceOp(op, newLoad.getResult()); + + return success(); +} + +LogicalResult DescriptorStoreConverter::matchAndRewrite( + triton::DescriptorStoreOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + const auto blockShape = op.getDesc().getType().getBlockType().getShape(); + auto descTy = op.getDesc().getType(); + auto indices = op.getIndices(); + + // 1. unpack descriptor + auto desc = unpackDescriptor(descTy, adaptor.getDesc(), rewriter); + + // 2. create make_tensor_ptr + SmallVector tensorShapeValues; + for (auto dim : blockShape) { + tensorShapeValues.push_back(static_cast(dim)); + } + Value tensorPtr = + rewriter.create(loc, + desc.base, // base + desc.shape, // shape + desc.strides, // strides + indices, // offset + tensorShapeValues, // tensorShape + computeOrder(blockShape) // order + ); + + // 3. replace tt.store + Value valueToStore = adaptor.getSrc(); + + auto maskType = RankedTensorType::get(blockShape, rewriter.getI1Type()); + rewriter.create(loc, + DenseElementsAttr::get(maskType, true)); + auto boundaryCheck = getFullBoundaryCheckAttr(rewriter, blockShape); + auto cacheModifier = triton::CacheModifierAttr::get( + rewriter.getContext(), triton::CacheModifier::NONE); + auto evictionPolicy = triton::EvictionPolicyAttr::get( + rewriter.getContext(), triton::EvictionPolicy::NORMAL); + + auto newStore = rewriter.create(loc, tensorPtr, valueToStore, + Value(), // mask + boundaryCheck, cacheModifier, + evictionPolicy); + + rewriter.eraseOp(op); + return success(); +} + +LogicalResult DescriptorScatterConverter::matchAndRewrite( + triton::DescriptorScatterOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto descTy = op.getDesc().getType(); + auto srcType = cast(op.getSrc().getType()); + const auto rowBlockShape = descTy.getSignlessBlockType().getShape(); + + auto desc = unpackDescriptor(descTy, adaptor.getDesc(), rewriter); + SmallVector tensorShapeValues; + tensorShapeValues.reserve(rowBlockShape.size()); + for (auto dim : rowBlockShape) { + tensorShapeValues.push_back(static_cast(dim)); + } + + auto zeroIndex = rewriter.create(loc, 0); + auto oneIndex = rewriter.create(loc, 1); + Value rowUpperBound; + if (srcType.isDynamicDim(0)) { + rowUpperBound = rewriter.create(loc, adaptor.getSrc(), 0); + } else { + rowUpperBound = + rewriter.create(loc, srcType.getShape()[0]); + } + auto rowBoundaryCheck = getFullBoundaryCheckAttr(rewriter, rowBlockShape); + auto cacheModifier = triton::CacheModifierAttr::get( + rewriter.getContext(), triton::CacheModifier::NONE); + auto evictionPolicy = triton::EvictionPolicyAttr::get( + rewriter.getContext(), triton::EvictionPolicy::NORMAL); + + auto loop = rewriter.create( + loc, zeroIndex, rowUpperBound, oneIndex, ValueRange{}, + [&](OpBuilder &nestedBuilder, Location nestedLoc, Value rowIv, + ValueRange) { + Value xOffset = nestedBuilder.create( + nestedLoc, adaptor.getXOffsets(), ValueRange{rowIv}); + Value tensorPtr = nestedBuilder.create( + nestedLoc, desc.base, desc.shape, desc.strides, + ValueRange{xOffset, adaptor.getYOffset()}, tensorShapeValues, + computeOrder(rowBlockShape)); + SmallVector extractOffsets{rowIv, + nestedBuilder.getIndexAttr(0)}; + SmallVector extractSizes{ + nestedBuilder.getIndexAttr(rowBlockShape[0]), + nestedBuilder.getIndexAttr(rowBlockShape[1])}; + SmallVector extractStrides{nestedBuilder.getIndexAttr(1), + nestedBuilder.getIndexAttr(1)}; + auto rowValue = nestedBuilder.create( + nestedLoc, adaptor.getSrc(), extractOffsets, extractSizes, + extractStrides); + auto rowStore = nestedBuilder.create( + nestedLoc, tensorPtr, rowValue.getResult(), Value(), + rowBoundaryCheck, cacheModifier, evictionPolicy); + rowStore->setAttr(ConverterUtils::discreteAttrName, + nestedBuilder.getUnitAttr()); + nestedBuilder.create(nestedLoc); + }); + loop->setAttr("ExtractedLoadOrStore", rewriter.getUnitAttr()); + loop->setAttr("hivm.parallel_loop", rewriter.getUnitAttr()); + + rewriter.eraseOp(op); + return success(); +} + +SmallVector filterSegmentSizes(ArrayRef attrs) { + SmallVector filteredAttrs; + llvm::copy_if(attrs, std::back_inserter(filteredAttrs), + [](const NamedAttribute &attr) { + return attr.getName().getValue() != "operandSegmentSizes"; + }); + return filteredAttrs; +} + +LogicalResult DescriptorGatherConverter::matchAndRewrite( + triton::DescriptorGatherOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto descTy = cast(op.getDesc().getType()); + auto resultType = cast(op.getResult().getType()); + const auto blockShape = resultType.getShape(); + const auto rowBlockShape = descTy.getSignlessBlockType().getShape(); + + auto desc = unpackDescriptor(descTy, adaptor.getDesc(), rewriter); + SmallVector tensorShapeValues; + tensorShapeValues.reserve(rowBlockShape.size()); + for (auto dim : rowBlockShape) { + tensorShapeValues.push_back(static_cast(dim)); + } + + auto zeroIndex = rewriter.create(loc, 0); + auto oneIndex = rewriter.create(loc, 1); + Value rowUpperBound = + rewriter.create(loc, adaptor.getXOffsets(), zeroIndex); + auto rowBoundaryCheck = getFullBoundaryCheckAttr(rewriter, rowBlockShape); + auto cache = triton::CacheModifierAttr::get(rewriter.getContext(), + triton::CacheModifier::NONE); + auto evict = triton::EvictionPolicyAttr::get(rewriter.getContext(), + triton::EvictionPolicy::NORMAL); + auto isVolatile = rewriter.getBoolAttr(false); + + if (auto attr = op->getAttrOfType("cache")) + cache = attr; + if (auto attr = op->getAttrOfType("evict")) + evict = attr; + if (auto attr = op->getAttrOfType("isVolatile")) + isVolatile = attr; + + SmallVector dynamicResultSizes; + dynamicResultSizes.reserve(resultType.getNumDynamicDims()); + for (const auto &[dim, size] : llvm::enumerate(blockShape)) { + if (!ShapedType::isDynamic(size)) + continue; + if (dim == 0) { + dynamicResultSizes.push_back(rowUpperBound); + continue; + } + dynamicResultSizes.push_back( + rewriter.create(loc, rowBlockShape[dim])); + } + + auto initialTensor = rewriter.create( + loc, blockShape, resultType.getElementType(), dynamicResultSizes); + auto loop = rewriter.create( + loc, zeroIndex, rowUpperBound, oneIndex, + ValueRange{initialTensor.getResult()}, + [&](OpBuilder &nestedBuilder, Location nestedLoc, Value rowIv, + ValueRange iterArgs) { + Value xOffset = nestedBuilder.create( + nestedLoc, adaptor.getXOffsets(), ValueRange{rowIv}); + Value tensorPtr = nestedBuilder.create( + nestedLoc, desc.base, desc.shape, desc.strides, + ValueRange{xOffset, adaptor.getYOffset()}, tensorShapeValues, + computeOrder(rowBlockShape)); + auto rowLoad = nestedBuilder.create( + nestedLoc, descTy.getSignlessBlockType(), tensorPtr, + Value(), // mask + Value(), // other + rowBoundaryCheck, desc.padding, cache, evict, isVolatile); + for (const auto &attr : filterSegmentSizes(op->getAttrs())) { + if (!rowLoad->hasAttr(attr.getName())) { + rowLoad->setAttr(attr.getName(), attr.getValue()); + } + } + rowLoad->setAttr(ConverterUtils::discreteAttrName, + nestedBuilder.getUnitAttr()); + + auto insertSlice = nestedBuilder.create( + nestedLoc, rowLoad.getResult(), iterArgs[0], + SmallVector{rowIv, nestedBuilder.getIndexAttr(0)}, + SmallVector{ + nestedBuilder.getIndexAttr(rowBlockShape[0]), + nestedBuilder.getIndexAttr(rowBlockShape[1])}, + SmallVector{nestedBuilder.getIndexAttr(1), + nestedBuilder.getIndexAttr(1)}); + insertSlice->setAttr(ConverterUtils::discreteAttrName, + nestedBuilder.getUnitAttr()); + nestedBuilder.create(nestedLoc, insertSlice.getResult()); + }); + loop->setAttr("ExtractedLoadOrStore", rewriter.getUnitAttr()); + + rewriter.replaceOp(op, loop.getResult(0)); + return success(); +} + +std::optional translateReduceKind(DescriptorReduceKind kind, + TensorDescType ty) { + auto scalarTy = ty.getBlockType().getElementType(); + switch (kind) { + case DescriptorReduceKind::ADD: + return scalarTy.isInteger() ? RMWOp::ADD : RMWOp::FADD; + case DescriptorReduceKind::MIN: + if (scalarTy.isUnsignedInteger()) { + return RMWOp::UMIN; + } else if (scalarTy.isSignedInteger()) { + return RMWOp::MIN; + } + return {}; + case DescriptorReduceKind::MAX: + if (scalarTy.isUnsignedInteger()) { + return RMWOp::UMAX; + } else if (scalarTy.isSignedInteger()) { + return RMWOp::MAX; + } + return {}; + case DescriptorReduceKind::AND: + return RMWOp::AND; + case DescriptorReduceKind::OR: + return RMWOp::OR; + case DescriptorReduceKind::XOR: + return RMWOp::XOR; + default: + break; + } + return {}; +} + +LogicalResult DescriptorReduceConverter::matchAndRewrite( + triton::DescriptorReduceOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto descTy = op.getDesc().getType(); + const auto blockShape = descTy.getBlockType().getShape(); + auto desc = unpackDescriptor(descTy, adaptor.getDesc(), rewriter); + auto offsets = castToI64(rewriter, op.getIndices()); + auto rmwOp = translateReduceKind(op.getKind(), descTy); + if (!rmwOp) { + std::string msgstring; + llvm::raw_string_ostream msg(msgstring); + msg << "Cannot fallback on descriptor atomic op, unsupported for type " + << descTy.getBlockType().getElementType(); + return op->emitError(msgstring); + } + + rewriter.create( + loc, descTy.getSignlessBlockType(), *rmwOp, + generatePtr(rewriter, loc, blockShape, desc, offsets), op.getSrc(), + generateMask(rewriter, loc, blockShape, desc, offsets), + MemSemantic::ACQUIRE_RELEASE, MemSyncScope::GPU); + op.erase(); + return success(); +} + +} // namespace DescriptorConverter diff --git a/third_party/ascend/lib/TritonToLinalg/DevicePrintOffsetRewrite.cpp b/third_party/ascend/lib/TritonToLinalg/DevicePrintOffsetRewrite.cpp new file mode 100644 index 0000000000..b23a01c8e6 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/DevicePrintOffsetRewrite.cpp @@ -0,0 +1,591 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/DevicePrintOffsetRewrite.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Dominance.h" +#include "mlir/IR/OpDefinition.h" + +#include "llvm/ADT/APInt.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +#include + +namespace mlir { +namespace triton { +namespace { + +static bool hasOnlyPrintUsers(Value v) { + if (v.use_empty()) + return false; + for (Operation *u : v.getUsers()) { + auto callOp = dyn_cast(u); + if (!callOp) + return false; + if (!callOp.getCallee().starts_with("triton_print")) + return false; + } + return true; +} + +static Value materializeIndex(OpBuilder &bd, Location loc, OpFoldResult ofr) { + if (auto attr = ofr.dyn_cast()) { + int64_t v = cast(attr).getInt(); + return bd.create(loc, v); + } + auto val = llvm::cast(ofr); + Type ty = val.getType(); + if (ty.isIndex()) + return val; + if (isa(ty)) + return bd.create(loc, bd.getIndexType(), val); + return nullptr; +} + +static bool isStaticEqual(OpFoldResult ofr, int64_t target) { + if (auto attr = ofr.dyn_cast()) + return cast(attr).getInt() == target; + return false; +} + +class ScalarChainWalker { +public: + ScalarChainWalker(OpBuilder &bd, Location loc) : bd(bd), loc(loc) {} + + Value emit(Value v, ArrayRef indices) { + Operation *def = v.getDefiningOp(); + if (!def) + return nullptr; + + if (auto cstOp = dyn_cast(def)) + return scalarFromConstant(cstOp); + if (auto fillOp = dyn_cast(def)) + return scalarFromFill(fillOp); + if (auto generic = dyn_cast(def)) + return scalarFromMakeRange(generic, indices); + + if (auto bc = dyn_cast(def)) + return emitBroadcast(bc, indices); + if (auto exp = dyn_cast(def)) + return emitExpandShape(exp, indices); + if (auto col = dyn_cast(def)) + return emitCollapseShape(col, indices); + + if (def->getNumOperands() != 2) + return nullptr; + Value lhs = emit(def->getOperand(0), indices); + Value rhs = emit(def->getOperand(1), indices); + if (!lhs || !rhs) + return nullptr; + return emitBinaryArith(def, lhs, rhs); + } + +private: + Value scalarFromConstant(arith::ConstantOp op) { + auto i32Ty = bd.getI32Type(); + if (auto dense = dyn_cast(op.getValue())) + if (dense.isSplat()) + return bd.create( + loc, dense.getSplatValue().getSExtValue(), i32Ty.getWidth()); + if (auto intAttr = dyn_cast(op.getValue())) + return bd.create(loc, intAttr.getInt(), + i32Ty.getWidth()); + return nullptr; + } + + Value scalarFromFill(linalg::FillOp op) { + if (op.getInputs().empty()) + return nullptr; + Value sc = op.getInputs()[0]; + auto i32Ty = bd.getI32Type(); + auto scTy = sc.getType(); + + if (auto cstOp = sc.getDefiningOp()) { + if (auto intAttr = dyn_cast(cstOp.getValue())) + return bd.create(loc, intAttr.getInt(), + i32Ty.getWidth()); + return nullptr; + } + if (scTy.isInteger(32)) + return sc; + if (scTy.isIndex()) + return bd.create(loc, i32Ty, sc); + if (auto intTy = dyn_cast(scTy)) { + if (intTy.getWidth() < 32) + return bd.create(loc, i32Ty, sc); + if (intTy.getWidth() > 32) + return bd.create(loc, i32Ty, sc); + } + return nullptr; + } + + Value scalarFromMakeRange(linalg::GenericOp op, ArrayRef indices) { + auto i32Ty = bd.getI32Type(); + if (!op->hasAttr("tt.from_make_range")) + return nullptr; + if (indices.size() != 1) + return nullptr; + int64_t off = 0; + if (auto a = op->getAttrOfType("tt.make_range_offset")) + off = a.getInt(); + Value v32 = bd.create(loc, i32Ty, indices[0]); + if (off != 0) { + Value oc = bd.create(loc, off, i32Ty.getWidth()); + v32 = bd.create(loc, v32, oc); + } + return v32; + } + + Value emitBroadcast(linalg::BroadcastOp op, ArrayRef indices) { + auto bcDims = op.getDimensions(); + llvm::DenseSet dropSet(bcDims.begin(), bcDims.end()); + SmallVector inputIdx; + for (size_t k = 0; k < indices.size(); ++k) + if (!dropSet.count(k)) + inputIdx.push_back(indices[k]); + return emit(op.getInput(), inputIdx); + } + + Value emitExpandShape(tensor::ExpandShapeOp op, ArrayRef indices) { + auto resTy = cast(op.getResult().getType()); + SmallVector inputIdx; + for (auto group : op.getReassociationIndices()) { + Value combined; + for (size_t i = 0; i < group.size(); ++i) { + int64_t d = group[i]; + int64_t weight = 1; + for (size_t j = i + 1; j < group.size(); ++j) + weight *= resTy.getDimSize(group[j]); + Value term = indices[d]; + if (weight != 1) { + Value w = bd.create(loc, weight); + term = bd.create(loc, term, w); + } + combined = + !combined ? term : bd.create(loc, combined, term); + } + if (!combined) + combined = bd.create(loc, 0); + inputIdx.push_back(combined); + } + return emit(op.getSrc(), inputIdx); + } + + Value emitCollapseShape(tensor::CollapseShapeOp op, ArrayRef indices) { + auto inTy = cast(op.getSrc().getType()); + SmallVector inputIdx(inTy.getRank()); + Value c0 = bd.create(loc, 0); + + auto reassoc = op.getReassociationIndices(); + for (size_t outDim = 0; outDim < indices.size(); ++outDim) { + auto &group = reassoc[outDim]; + Value out = indices[outDim]; + + int64_t leftmostNonOne = -1; + for (size_t i = 0; i < group.size(); ++i) + if (inTy.getDimSize(group[i]) != 1) { + leftmostNonOne = (int64_t)i; + break; + } + + for (size_t i = 0; i < group.size(); ++i) { + int64_t d = group[i]; + int64_t size = inTy.getDimSize(d); + if (size == 1) { + inputIdx[d] = c0; + continue; + } + int64_t weight = 1; + for (size_t j = i + 1; j < group.size(); ++j) + weight *= inTy.getDimSize(group[j]); + Value v = out; + if (weight != 1) { + Value w = bd.create(loc, weight); + v = bd.create(loc, v, w); + } + if ((int64_t)i != leftmostNonOne) { + Value s = bd.create(loc, size); + v = bd.create(loc, v, s); + } + inputIdx[d] = v; + } + } + return emit(op.getSrc(), inputIdx); + } + + Value emitBinaryArith(Operation *def, Value lhs, Value rhs) { + if (isa(def)) + return bd.create(loc, lhs, rhs); + if (isa(def)) + return bd.create(loc, lhs, rhs); + if (isa(def)) + return bd.create(loc, lhs, rhs); + if (isa(def)) + return bd.create(loc, lhs, rhs); + if (isa(def)) + return bd.create(loc, lhs, rhs); + if (isa(def)) + return bd.create(loc, lhs, rhs); + if (isa(def)) + return bd.create(loc, lhs, rhs); + return nullptr; + } + + OpBuilder &bd; + Location loc; +}; + +static void emitLoopsAndPrint( + OpBuilder &builder, Location loc, func::FuncOp scalarPrintFn, + ArrayRef shape, + llvm::function_ref)> + scalarGen) { + auto i32Ty = builder.getI32Type(); + Value c0 = builder.create(loc, 0); + Value c1 = builder.create(loc, 1); + + SmallVector ivs(shape.size(), c0); + OpBuilder bd = builder; + for (size_t k = 0; k < shape.size(); ++k) { + if (shape[k] <= 1) + continue; + Value upper = bd.create(loc, shape[k]); + auto forOp = bd.create(loc, c0, upper, c1); + ivs[k] = forOp.getInductionVar(); + bd.setInsertionPoint(forOp.getBody()->getTerminator()); + } + + Value scalar = scalarGen(bd, loc, ivs); + if (!scalar) + return; + if (scalar.getType().isIndex()) + scalar = bd.create(loc, i32Ty, scalar); + else if (!scalar.getType().isInteger(32)) + return; + bd.create(loc, scalarPrintFn, ValueRange{scalar}); +} + +enum class ChainKind { + Unsupported, + Linear, + Linearized, +}; + +struct OffsetChainInfo { + ChainKind kind = ChainKind::Unsupported; + StringRef rejectReason; + SmallVector shape; + Value chainRoot; + SmallVector strides; + OpFoldResult baseOffset; +}; + +static void classifyChain(Value root, bool &canWalk, bool &hasDivMod) { + canWalk = true; + hasDivMod = false; + + llvm::DenseSet visited; + std::function walk = [&](Value v) { + if (!canWalk) + return; + auto ty = dyn_cast(v.getType()); + if (!ty) + return; + if (!ty.hasStaticShape()) { + canWalk = false; + return; + } + + Operation *def = v.getDefiningOp(); + if (!def) { + canWalk = false; + return; + } + if (!visited.insert(def).second) + return; + + if (auto fillOp = dyn_cast(def)) { + if (fillOp.getInputs().empty() || + !fillOp.getInputs()[0].getType().isIntOrIndex()) + canWalk = false; + return; + } + if (isa(def)) + return; + if (auto g = dyn_cast(def)) { + if (!g->hasAttr("tt.from_make_range")) + canWalk = false; + return; + } + if (auto bc = dyn_cast(def)) { + walk(bc.getInput()); + return; + } + if (auto exp = dyn_cast(def)) { + auto resTy = cast(exp.getResult().getType()); + for (auto group : exp.getReassociationIndices()) + for (int64_t d : group) + if (ShapedType::isDynamic(resTy.getDimSize(d))) { + canWalk = false; + return; + } + walk(exp.getSrc()); + return; + } + if (auto col = dyn_cast(def)) { + auto inTy = cast(col.getSrc().getType()); + for (auto group : col.getReassociationIndices()) + for (int64_t d : group) + if (ShapedType::isDynamic(inTy.getDimSize(d))) { + canWalk = false; + return; + } + walk(col.getSrc()); + return; + } + if (isa( + def)) { + hasDivMod = true; + for (Value operand : def->getOperands()) + walk(operand); + return; + } + if (isa(def)) { + for (Value operand : def->getOperands()) + walk(operand); + return; + } + canWalk = false; + }; + + walk(root); +} + +static memref::ReinterpretCastOp +findMatchingCast(func::FuncOp funcOp, ArrayRef targetShape) { + memref::ReinterpretCastOp best; + unsigned bestScore = 0; + funcOp.walk([&](memref::ReinterpretCastOp c) { + auto resTy = dyn_cast(c.getResult().getType()); + if (!resTy) + return; + if (resTy.getShape() != targetShape) + return; + unsigned score = 0; + if (auto blockArg = dyn_cast(c.getSource())) { + unsigned idx = blockArg.getArgNumber(); + if (auto argDict = funcOp.getArgAttrDict(idx)) + if (argDict.contains("tt.tensor_kind")) + score = 1; + } + if (score >= bestScore) { + bestScore = score; + best = c; + } + }); + return best; +} + +static OffsetChainInfo analyzeChain(Value arg, func::FuncOp funcOp, + Operation *anchorOp, + DominanceInfo &domInfo) { + OffsetChainInfo info; + + auto argTy = dyn_cast(arg.getType()); + if (!argTy) { + info.rejectReason = "arg not RankedTensorType"; + return info; + } + if (!argTy.hasStaticShape()) { + info.rejectReason = "arg has dynamic shape"; + return info; + } + if (!hasOnlyPrintUsers(arg)) { + info.rejectReason = "arg has non-print users"; + return info; + } + + info.shape.assign(argTy.getShape().begin(), argTy.getShape().end()); + info.chainRoot = arg; + + bool canWalk = false; + bool hasDivMod = false; + classifyChain(arg, canWalk, hasDivMod); + if (!canWalk) { + info.rejectReason = "chain contains unsupported op"; + return info; + } + + if (argTy.getRank() > 1 && !hasDivMod) { + auto castOp = findMatchingCast(funcOp, argTy.getShape()); + if (castOp) { + SmallVector mixedStrides = castOp.getMixedStrides(); + SmallVector mixedOffsets = castOp.getMixedOffsets(); + if (mixedOffsets.size() == 1 && + (int64_t)mixedStrides.size() == argTy.getRank()) { + auto domOk = [&](OpFoldResult ofr) -> bool { + if (auto val = ofr.dyn_cast()) + return domInfo.dominates(val, anchorOp); + return true; + }; + bool domAllOk = domOk(mixedOffsets[0]); + if (domAllOk) + for (auto s : mixedStrides) + if (!domOk(s)) { + domAllOk = false; + break; + } + if (domAllOk) { + info.kind = ChainKind::Linear; + info.strides = std::move(mixedStrides); + info.baseOffset = mixedOffsets[0]; + return info; + } + } + } + } + + info.kind = ChainKind::Linearized; + return info; +} + +struct RewriteCandidate { + func::CallOp callOp; + OffsetChainInfo info; +}; + +static void rewriteCandidate(RewriteCandidate &cand, ModuleOp moduleOp) { + auto i32Ty = IntegerType::get(moduleOp.getContext(), 32); + auto oldFn = dyn_cast_or_null( + SymbolTable::lookupSymbolIn(moduleOp, cand.callOp.getCalleeAttr())); + if (!oldFn) + return; + + auto newFnTy = FunctionType::get(moduleOp.getContext(), {i32Ty}, {}); + oldFn.setFunctionType(newFnTy); + + OpBuilder b(cand.callOp); + Location loc = cand.callOp.getLoc(); + OffsetChainInfo &info = cand.info; + + switch (info.kind) { + case ChainKind::Linear: { + auto gen = [&info](OpBuilder &bd, Location loc, + ArrayRef ivs) -> Value { + Value acc; + if (!isStaticEqual(info.baseOffset, 0)) { + acc = materializeIndex(bd, loc, info.baseOffset); + if (!acc) + return nullptr; + } + for (size_t k = 0; k < info.shape.size(); ++k) { + if (info.shape[k] <= 1) + continue; + Value idx = ivs[k]; + if (!isStaticEqual(info.strides[k], 1)) { + Value s = materializeIndex(bd, loc, info.strides[k]); + if (!s) + return nullptr; + idx = bd.create(loc, idx, s); + } + if (!acc) + acc = idx; + else + acc = bd.create(loc, acc, idx); + } + if (!acc) + acc = bd.create(loc, 0); + return acc; + }; + emitLoopsAndPrint(b, loc, oldFn, info.shape, gen); + break; + } + + case ChainKind::Linearized: { + Value root = info.chainRoot; + auto gen = [root](OpBuilder &bd, Location loc, + ArrayRef ivs) -> Value { + ScalarChainWalker walker(bd, loc); + return walker.emit(root, ivs); + }; + emitLoopsAndPrint(b, loc, oldFn, info.shape, gen); + break; + } + + case ChainKind::Unsupported: + return; + } + + cand.callOp.erase(); +} + +static void rewriteOneFunc(func::FuncOp funcOp) { + auto moduleOp = funcOp->getParentOfType(); + if (!moduleOp) + return; + + DominanceInfo domInfo(funcOp); + SmallVector candidates; + + funcOp.walk([&](func::CallOp callOp) { + if (!callOp.getCallee().starts_with("triton_print")) + return; + if (callOp.getNumOperands() != 1) + return; + + OffsetChainInfo info = analyzeChain(callOp.getOperand(0), funcOp, + callOp.getOperation(), domInfo); + if (info.kind == ChainKind::Unsupported) + return; + + candidates.push_back({callOp, std::move(info)}); + }); + + for (auto &cand : candidates) + rewriteCandidate(cand, moduleOp); +} + +} // namespace + +void rewriteDevicePrintOffsets(ModuleOp moduleOp) { + moduleOp.walk([&](func::FuncOp funcOp) { + if (funcOp.isPrivate()) + return; + rewriteOneFunc(funcOp); + }); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToLinalg/FunctionConverter.cpp b/third_party/ascend/lib/TritonToLinalg/FunctionConverter.cpp new file mode 100644 index 0000000000..d0f425c1bd --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/FunctionConverter.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/FunctionConverter.h" +#include "ascend/include/Utils/DebugUtils.h" + +namespace FunctionConverter { +using namespace mlir; +using namespace triton; + +LogicalResult GetProgramIDConverter::matchAndRewrite( + triton::GetProgramIdOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto axis = (uint32_t)op.getAxis(); + assert(axis < GetProgramIDConverter::LAUNCH_GRID_RANK && + "Invalid axis for GetProgramIdOp"); + auto func = op->getParentOfType(); + auto numArgs = func.getNumArguments(); + auto id = func.getArgument(numArgs - GetProgramIDConverter::LAUNCH_GRID_RANK + + axis); + + Location pidLoc = op.getLoc(); + insertDebugNop(pidLoc, rewriter); + rewriter.replaceOp(op, id); + return success(); +} + +LogicalResult GetNumProgramsConverter::matchAndRewrite( + triton::GetNumProgramsOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto axis = (uint32_t)op.getAxis(); + assert(axis < GetNumProgramsConverter::LAUNCH_GRID_RANK && + "Invalid axis for GetNumProgramsOp"); + auto func = op->getParentOfType(); + auto numArgs = func.getNumArguments(); + auto id = func.getArgument( + numArgs - GetNumProgramsConverter::LAUNCH_GRID_RANK * 2 + axis); + + Location numProgsLoc = op.getLoc(); + insertDebugNop(numProgsLoc, rewriter); + rewriter.replaceOp(op, id); + return success(); +} +} // namespace FunctionConverter diff --git a/third_party/ascend/lib/TritonToLinalg/HoistBroadcast.cpp b/third_party/ascend/lib/TritonToLinalg/HoistBroadcast.cpp new file mode 100644 index 0000000000..d16262e169 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/HoistBroadcast.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/HoistBroadcast.h" +#include "ascend/include/TritonToLinalg/TritonToLinalgPass.h" +#include "ascend/include/Utils/Utils.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" +#include + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/MemRef/Transforms/Passes.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/ValueRange.h" + +namespace HoistBroadcast { +using namespace mlir; +using namespace triton; + +LogicalResult +BroadcastConverter::matchAndRewrite(triton::BroadcastOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + assert(op->getNumResults() == 1 && "BroadcastOp assumes single result"); + + if (!isa(op.getType().getElementType())) { + return rewriter.notifyMatchFailure( + op, "only support hoist broadcast for tt.ptr tensor right now."); + } + auto loc = op.getLoc(); + BroadcastHoister hoister(op); + + if (!hoister.canBroadcast()) { + return failure(); + } + + if (hoister.parse(op.getSrc(), loc, rewriter).failed()) { + return failure(); + } + if (hoister.replaceBroadcastOp(op, rewriter).failed()) { + return failure(); + } + return success(); +} + +BroadcastHoister::BroadcastHoister(triton::BroadcastOp op) { + source = nullptr; + if (findSrc(op.getSrc()).failed()) { + LLVM_DEBUG({ llvm::dbgs() << "No legal source found for broadcast op\n"; }); + } + opToHoist = op; + auto resultType = dyn_cast(op.getType()); + for (size_t i = 0; i < resultType.getShape().size(); ++i) { + tensorSizes.push_back(resultType.getShape()[i]); + } +} + +LogicalResult BroadcastHoister::findSrc(Value operand) { + // ptr tensor can only be defined by AddPtrOp or SplatOp in this converter + // another broadcast to be complemented + if (auto op = operand.getDefiningOp()) { + return findSrc(op.getPtr()); + } else if (auto op = operand.getDefiningOp()) { + source = op.getSrc(); + return success(); + } else { + LLVM_DEBUG({ + llvm::dbgs() << "Unsupported operation in BroadcastHoister::findSrc: " + << *operand.getDefiningOp() << "\n"; + }); + return failure(); + } +} + +LogicalResult BroadcastHoister::parse(Value operand, const Location &loc, + ConversionPatternRewriter &rewriter) { + if (auto op = operand.getDefiningOp()) { + return parseAddptr(op, loc, rewriter); + } else if (auto op = operand.getDefiningOp()) { + return parseSplat(op, loc, rewriter); + } else if (auto op = operand.getDefiningOp()) { + return parseBroadcast(op, loc, rewriter); + } else { + // Handle other cases or throw an error + LLVM_DEBUG({ + llvm::dbgs() << "Unsupported operation in BroadcastHoister::parse: " + << *operand.getDefiningOp() << "\n"; + }); + return failure(); + } +} + +LogicalResult +BroadcastHoister::parseAddptr(triton::AddPtrOp addptrOp, const Location &loc, + ConversionPatternRewriter &rewriter) { + // Implementation for parsing AddptrOp + if (parse(addptrOp.getPtr(), loc, rewriter).failed()) { + return failure(); + } + auto broadcastedPtr = broadcastMap[addptrOp.getPtr()]; + + RankedTensorType offsetType = + dyn_cast(addptrOp.getOffset().getType()); + if (!offsetType || !offsetType.hasStaticShape()) { + LLVM_DEBUG({ + llvm::dbgs() << "Offset must be a ranked tensor with static shape.\n"; + }); + return failure(); + } + + auto elementType = offsetType.getElementType(); + auto broadcastType = RankedTensorType::get({tensorSizes}, elementType); + auto broadcastedOffset = rewriter.create( + loc, broadcastType, addptrOp.getOffset()); + + auto ptrType = dyn_cast(source.getType()); + auto ptrTensorType = RankedTensorType::get({tensorSizes}, ptrType); + + auto newAddPtrOp = rewriter.create( + loc, ptrTensorType, broadcastedPtr, broadcastedOffset); + + size_t hoistDim = -1; + for (size_t i = 0; i < offsetType.getShape().size(); ++i) { + if (offsetType.getShape()[i] == 1 && + offsetType.getShape()[i] != tensorSizes[i]) { + hoistDim = i; + break; + } + } + if (hoistDim == -1) { + LLVM_DEBUG({ + llvm::dbgs() << "No dimension to hoist found in AddPtrOp offset.\n"; + }); + } + newAddPtrOp->setAttr( + "hoist_dim", rewriter.getI64IntegerAttr(static_cast(hoistDim))); + broadcastMap[addptrOp.getResult()] = newAddPtrOp.getResult(); + return success(); +} + +LogicalResult +BroadcastHoister::parseSplat(triton::SplatOp splatOp, const Location &loc, + ConversionPatternRewriter &rewriter) { + // End of parse: splat for ptr + auto src = splatOp.getSrc(); + auto dst = splatOp.getResult(); + if (!isa(src.getType())) { + LLVM_DEBUG( + { llvm::dbgs() << "SplatOp source must be of pointer type.\n"; }); + return failure(); + } + + auto ptrType = dyn_cast(source.getType()); + auto ptrTensorType = RankedTensorType::get({tensorSizes}, ptrType); + auto newSplatOp = rewriter.create(loc, ptrTensorType, src); + broadcastMap[splatOp.getResult()] = newSplatOp.getResult(); + return success(); +} + +LogicalResult +BroadcastHoister::parseBroadcast(triton::BroadcastOp broadcastOp, + const Location &loc, + ConversionPatternRewriter &rewriter) { + // Another broadcast for ptr tensor + // To be fixed if needed in future + LLVM_DEBUG({ + llvm::dbgs() << "Now cannot handle multi broadcast for ptr tensor.\n"; + }); + return failure(); +} + +LogicalResult +BroadcastHoister::replaceBroadcastOp(triton::BroadcastOp op, + ConversionPatternRewriter &rewriter) { + auto newOp = broadcastMap[op.getSrc()]; + rewriter.replaceOp(op, newOp); + return success(); +} + +bool BroadcastHoister::canBroadcast() { + auto resultType = dyn_cast(opToHoist.getType()); + auto srcType = dyn_cast(opToHoist.getSrc().getType()); + int broadcastedDims = 0; + for (size_t i = 0; i < resultType.getShape().size(); ++i) { + if (srcType.getShape()[i] == 1 && resultType.getShape()[i] != 1) { + broadcastedDims++; + } + } + if (broadcastedDims != 1) { + LLVM_DEBUG({ + llvm::dbgs() << "Now cannot handle broadcast for ptr tensor with multi " + "broadcasted dimension.\n"; + }); + return false; + } + return source != nullptr && isa(source.getType()); +} +} // namespace HoistBroadcast diff --git a/third_party/ascend/lib/TritonToLinalg/ImplicitPermute.cpp b/third_party/ascend/lib/TritonToLinalg/ImplicitPermute.cpp new file mode 100644 index 0000000000..354486c69c --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/ImplicitPermute.cpp @@ -0,0 +1,636 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include + +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Passes.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/MathExtras.h" + +#include "llvm/Support/Debug.h" + +#include "TritonToLinalg/ImplicitPermute.h" +#include "TritonToLinalg/TritonToLinalgPass.h" +#include "TritonToStructured/CannonicalizerConverter.h" +#include "TritonToStructured/MaskAnalysis.h" +#include "TritonToStructured/PtrAnalysis.h" + +#include "Utils/InterleaveOptimization.h" +#include "Utils/Utils.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/Utils/Utils.h" + +#define DEBUG_TYPE "triton-to-linalg-implicit-permute" + +namespace ImplicitPermute { +using namespace mlir; +using namespace triton; +using namespace TritonToStructured; + +LogicalResult LoadConverter::matchAndRewrite(triton::LoadOp op, + PatternRewriter &rewriter) const { + // no need to analyze and rewrite + if (compileOn91095Flag && !existDotFlag) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "compileOn91095Flag :" << compileOn91095Flag << "\n"; + llvm::dbgs() << "!existDotFlag :" << !existDotFlag << "\n"; + llvm::dbgs() << "no need to analyze and rewrite Load" << "\n"; + llvm::dbgs() << "----------------------------------------------\n"; + }); + return success(); + } + + auto loc = op.getLoc(); + auto oldPtr = op.getPtr(); + auto oldMask = op.getMask(); + auto oldOther = op.getOther(); + + MemOpTransformer tf(MemOpTransformer::MemType::load); + + Value newPtr = nullptr; + if (oldPtr.getDefiningOp()) { + newPtr = tf.createNewTensorPtr(oldPtr, loc, rewriter); + } else if (oldPtr.getDefiningOp()) { + newPtr = tf.createNewAdvancePtr(oldPtr, loc, rewriter); + } else if (oldPtr.getDefiningOp()) { + newPtr = tf.createNewAddPtr(oldPtr, loc, rewriter); + } else { + InFlightDiagnostic diag = emitWarning(loc) + << "PtrAnalysis: only MakeTensorPtrOp, " + "AdvanceOp, and AddPtrOp are supported."; + return success(); + } + if (!tf.ptrState.isPermuted) { + // no need to rewrite + return success(); + } + + auto newMask = tf.createNewMask(oldMask, loc, rewriter); + auto newOther = tf.createNewOther(oldOther, loc, rewriter); + + if (!newPtr) { + InFlightDiagnostic diag = emitWarning(loc) + << "PtrAnalysis: failed to analyze load pointer."; + return failure(); + } + + if (oldMask && !newMask) { + InFlightDiagnostic diag = emitWarning(loc) + << "MaskAnalysis: failed to analyze load mask."; + return failure(); + } + + auto newBoundaryCheck = tf.getBoundaryCheck(op.getBoundaryCheck()); + + auto loadOp = rewriter.create( + loc, newPtr, newMask, newOther, newBoundaryCheck, op.getPadding(), + op.getCache(), op.getEvict(), op.getIsVolatile()); + loadOp->setAttr(ImplicitPermuteHandledTAG, + UnitAttr::get(rewriter.getContext())); + + auto permuteResult = + tf.materializeImplicitPermute(loadOp.getResult(), loc, rewriter); + + rewriter.replaceOp(op, permuteResult); + return success(); +} + +LogicalResult StoreConverter::matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto oldPtr = op.getPtr(); + auto oldMask = op.getMask(); + auto oldValue = op.getValue(); + + MemOpTransformer tf(MemOpTransformer::MemType::store); + Value newPtr = nullptr; + if (oldPtr.getDefiningOp()) { + newPtr = tf.createNewTensorPtr(oldPtr, loc, rewriter); + } else if (oldPtr.getDefiningOp()) { + newPtr = tf.createNewAdvancePtr(oldPtr, loc, rewriter); + } else if (oldPtr.getDefiningOp()) { + newPtr = tf.createNewAddPtr(oldPtr, loc, rewriter); + } else { + InFlightDiagnostic diag = emitWarning(loc) + << "PtrAnalysis: only MakeTensorPtrOp, " + "AdvanceOp, and AddPtrOp are supported."; + return success(); + } + if (!tf.ptrState.isPermuted) { + // no need to rewrite + return success(); + } + auto newMask = tf.createNewMask(oldMask, loc, rewriter); + + if (!newPtr) { + InFlightDiagnostic diag = + emitWarning(loc) << "PtrAnalysis: failed to analyze store pointer."; + return failure(); + } + + if (oldMask && !newMask) { + InFlightDiagnostic diag = emitWarning(loc) + << "MaskAnalysis: failed to analyze store mask."; + return failure(); + } + + auto permuteResult = tf.materializeImplicitPermute(oldValue, loc, rewriter); + + auto newBoundaryCheck = tf.getBoundaryCheck(op.getBoundaryCheck()); + + auto storeOp = rewriter.create(loc, newPtr, permuteResult, + newMask, newBoundaryCheck, + op.getCache(), op.getEvict()); + storeOp->setAttr(ImplicitPermuteHandledTAG, + UnitAttr::get(rewriter.getContext())); + + rewriter.eraseOp(op); + return success(); +} + +LogicalResult +AtomicRMWConverter::matchAndRewrite(triton::AtomicRMWOp op, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto oldPtr = op.getPtr(); + auto oldMask = op.getMask(); + auto oldVal = op.getVal(); + + MemOpTransformer tf(MemOpTransformer::MemType::store); + + Value newPtr = nullptr; + if (oldPtr.getDefiningOp()) { + newPtr = tf.createNewAddPtr(oldPtr, loc, rewriter); + } else { + emitWarning(loc) << "PtrAnalysis: AtomicRMW only support AddPtrOp."; + return success(); + } + + Value newMask = tf.createNewMask(oldMask, loc, rewriter); + + if (!tf.ptrState.isPermuted) { + return success(); + } + if (!newPtr) { + emitWarning(loc) << "PtrAnalysis: failed to analyze atomic_rmw pointer."; + return failure(); + } + if (oldMask && !newMask) { + emitWarning(loc) << "MaskAnalysis: failed to analyze atomic_rmw mask."; + return failure(); + } + + Value newVal = tf.materializeImplicitPermute(oldVal, loc, rewriter); + + Type newAtomicResTy = newVal.getType(); + + auto newAtomic = rewriter.create( + loc, newAtomicResTy, op.getAtomicRmwOp(), newPtr, newVal, newMask, + op.getSem(), op.getScope()); + newAtomic->setAttr(ImplicitPermuteHandledTAG, + UnitAttr::get(rewriter.getContext())); + + // The returned old value should be in OLD layout for users => permute back + // (load-side). + MemOpTransformer tfLoad(MemOpTransformer::MemType::load); + tfLoad.ptrState = tf.ptrState; + Value permutedRes = + tfLoad.materializeImplicitPermute(newAtomic.getResult(), loc, rewriter); + + rewriter.replaceOp(op, permutedRes); + return success(); +} + +LogicalResult +AtomicCASConverter::matchAndRewrite(triton::AtomicCASOp op, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto oldPtr = op.getPtr(); + auto oldCmp = op.getCmp(); + auto oldVal = op.getVal(); + + MemOpTransformer tf(MemOpTransformer::MemType::store); + + Value newPtr = nullptr; + if (oldPtr.getDefiningOp()) { + newPtr = tf.createNewAddPtr(oldPtr, loc, rewriter); + } else { + emitWarning(loc) << "PtrAnalysis: AtomicRMW only support AddPtrOp."; + return success(); + } + + if (!tf.ptrState.isPermuted) { + return success(); + } + if (!newPtr) { + emitWarning(loc) << "PtrAnalysis: failed to analyze atomic_cas pointer."; + return failure(); + } + + Value newCmp = tf.materializeImplicitPermute(oldCmp, loc, rewriter); + Value newVal = tf.materializeImplicitPermute(oldVal, loc, rewriter); + + // CAS result (old value) must have same shape as cmp/val operands. + Type newAtomicResTy = newVal.getType(); + + auto newAtomic = rewriter.create( + loc, newAtomicResTy, newPtr, newCmp, newVal, op.getSem(), op.getScope()); + newAtomic->setAttr(ImplicitPermuteHandledTAG, + UnitAttr::get(rewriter.getContext())); + + MemOpTransformer tfLoad(MemOpTransformer::MemType::load); + tfLoad.ptrState = tf.ptrState; + Value permutedRes = + tfLoad.materializeImplicitPermute(newAtomic.getResult(), loc, rewriter); + + rewriter.replaceOp(op, permutedRes); + return success(); +} + +Value MemOpTransformer::materializeImplicitPermute(Value srcTensor, + const Location loc, + PatternRewriter &rewriter) { + auto inTy = dyn_cast(srcTensor.getType()); + if (!inTy || !ptrState.isPermuted) + return srcTensor; + + auto inShape = inTy.getShape(); + auto orderSize = ptrState.sizes.size(); + SmallVector permuteOrder(orderSize); + for (size_t i = 0; i < orderSize; ++i) { + if (currentType == MemType::load) { + if (ptrState.isBlockPtr()) { + permuteOrder[i] = orderSize - 1 - ptrState.order[i]; + } else { + permuteOrder[ptrState.permuteIds[i]] = i; + } + } else { + if (ptrState.isBlockPtr()) { + permuteOrder[i] = ptrState.order[orderSize - 1 - i]; + } else { + permuteOrder[i] = ptrState.permuteIds[i]; + } + } + } + SmallVector outShape(permuteOrder.size()); + if (inShape.size() != outShape.size()) { + InFlightDiagnostic diag = emitWarning(loc) + << "PtrAnalysis: incompatible shape for permute"; + return srcTensor; + } + + for (size_t i = 0; i < outShape.size(); ++i) { + outShape[i] = inShape[permuteOrder[i]]; + } + + auto outTy = RankedTensorType::get(outShape, inTy.getElementType()); + auto transOp = + rewriter.create(loc, outTy, srcTensor, permuteOrder); + return transOp.getResult(); +} + +Value MemOpTransformer::createNewAddPtr(Value oldPtr, const Location loc, + PatternRewriter &rewriter) { + TritonToStructured::PtrAnalysis ptrAnalysis; + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "PtrAnalysis: analyzing load/store's ptr.\n"; + }); + + if (ptrAnalysis.visitOperand(oldPtr, ptrState, loc, rewriter).failed()) { + ptrState.shouldLinearize = false; + InFlightDiagnostic diag = + emitWarning(loc) << "PtranAlysis: failed to analyze load/store ptr."; + return oldPtr; + } + + // compute missing strides + // if stateinfo.shape is 1 and sizes[dimIndex] is 1, + // then the stride is the accumulated size of all dimensions on the right side + // ie. for shape [1, 128], sizes [1, 128], originally stride is [0, 1], + // after normalization, stride is [128, 1] + OpFoldResult maxStride = rewriter.getIndexAttr(1); + for (auto it = ptrState.stateInfo.rbegin(); it != ptrState.stateInfo.rend(); + ++it) { + if (TritonToStructured::isOne(it->shape) && isZero(it->stride)) { + it->stride = maxStride; + } + maxStride = maxOpFoldResult(maxStride, it->stride, loc, rewriter); + } + + ptrState.analyzePermute(); + return ptrState.createAddPtrOp(rewriter, loc); +} + +Value MemOpTransformer::createNewTensorPtr(Value oldPtr, const Location loc, + PatternRewriter &rewriter) { + TritonToStructured::PtrAnalysis ptrAnalysis; + auto makeTPtrOp = oldPtr.getDefiningOp(); + if (!makeTPtrOp) { + InFlightDiagnostic diag = emitWarning(loc) + << "PtrAnalysis: load pointer must originate " + "from 'make_tensor_ptr' operation"; + return oldPtr; + } + if (ptrAnalysis.visitOperandMakeTensorPtr(makeTPtrOp, ptrState, loc, rewriter) + .failed()) { + ptrState.isPermuted = false; + return oldPtr; + } + ptrState.analyzePermute(); + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "After ptrState.analyzePermute:\n"; + llvm::dbgs() << "compileOn91095Flag: " << compileOn91095Flag << "\n"; + ptrState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return ptrState.createMakeTensorPtrOp(rewriter, loc); +} + +Value MemOpTransformer::createNewAdvancePtr(Value oldPtr, const Location loc, + PatternRewriter &rewriter) { + TritonToStructured::PtrAnalysis ptrAnalysis; + auto advOp = oldPtr.getDefiningOp(); + if (!advOp) { + emitWarning(loc) + << "PtrAnalysis: pointer must originate from 'advance' operation"; + return oldPtr; + } + + Value basePtr = advOp.getPtr(); + if (!basePtr || !basePtr.getDefiningOp()) { + emitWarning(loc) << "PtrAnalysis: advance base ptr must originate from " + "'make_tensor_ptr' operation"; + return oldPtr; + } + auto newBasePtr = createNewTensorPtr(basePtr, loc, rewriter); + if (!newBasePtr) + return oldPtr; + + if (!ptrState.isPermuted) + return oldPtr; + // 2) Rewrite advance offsets according to the new make_tensor_ptr layout. + auto oldOffsets = advOp.getOffsets(); + SmallVector newOffsets; + size_t rank = ptrState.order.size(); + // iterate reversed safely: i = rank-1, ..., 0 + for (size_t i = rank; i-- > 0;) { + newOffsets.push_back(oldOffsets[ptrState.order[i]]); + } + return rewriter.create(loc, newBasePtr.getType(), + newBasePtr, newOffsets); +} + +Value MemOpTransformer::createNewMask(Value oldMask, const Location loc, + PatternRewriter &rewriter) { + if (!oldMask) + return nullptr; + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskAnalysis: analyzing load/store mask.\n"; + }); + + if (!oldMask || maskState.analysisMask(oldMask).failed()) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskAnalysis: no mask or failed to analyze mask.\n"; + llvm::dbgs() << "oldMask:" << oldMask << "\n"; + maskState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + InFlightDiagnostic diag = + emitWarning(loc) << "MaskAnalysis: failed to analyze load/store mask."; + return nullptr; + } + + SmallVector newMaskInfo; + auto itPtr = ptrState.stateInfo.begin(); + auto itMask = maskState.stateInfo.begin(); + + // match and create new mask info + while (itPtr != ptrState.stateInfo.end() && + itMask != maskState.stateInfo.end()) { + // ptr'shape must be multiple of mask'shape or vice versa + if (!isMultiple(itMask->shape, itPtr->shape)) { + InFlightDiagnostic diag = + emitWarning(loc) + << "MaskAnalysis: incompatible shapes between ptr and mask."; + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + ptrState.dump(); + llvm::dbgs() << "oldMask:" << oldMask << "\n"; + maskState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return nullptr; + } + + auto newShape = minOpFoldResult(itMask->shape, itPtr->shape, loc, rewriter); + if (isLess(newShape, itMask->shape) && !itMask->hasBroadCast) { + InFlightDiagnostic diag = + emitWarning(loc) + << "MaskAnalysis: the mask shape is incompatible with ptr shape."; + return nullptr; + } + + TritonToStructured::dimInfo newInfo(itMask->offset, newShape, + itMask->dimIndex, itMask->hasBroadCast, + itMask->currentType, itMask->rhs); + + if (!isZero(itPtr->stride)) { + newMaskInfo.emplace_back(newInfo); + } + + ++itPtr; + if (isEqual(itMask->shape, newShape)) { + ++itMask; + } + } + + if (itPtr != ptrState.stateInfo.end() || + itMask != maskState.stateInfo.end()) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskAnalysis: failed to apply permute on mask.\n"; + ptrState.dump(); + llvm::dbgs() << "oldMask:" << oldMask << "\n"; + maskState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + InFlightDiagnostic diag = emitWarning(loc) + << "MaskAnalysis: incompatible number of " + "dimensions between ptr and mask."; + return nullptr; + } + + maskState.stateInfo = newMaskInfo; + + if (ptrState.isPermuted && !applyPermuteOnMask()) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskAnalysis: failed to apply permute on mask.\n"; + ptrState.dump(); + llvm::dbgs() << "oldMask:" << oldMask << "\n"; + maskState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + InFlightDiagnostic diag = + emitWarning(loc) << "MaskAnalysis: failed to apply permute on mask."; + return nullptr; + } + + LLVM_DEBUG({ + llvm::dbgs() << "After matching MaskState: \n"; + for (auto info : newMaskInfo) { + info.dump(); + } + llvm::dbgs() << "----------------------------------------------\n"; + }); + + auto newMask = maskState.createNewMask(loc, rewriter); + return newMask; +} + +Value MemOpTransformer::createNewOther(Value oldOther, const Location loc, + PatternRewriter &rewriter) { + if (!oldOther || !maskState.newMask) + return nullptr; + + auto ptrType = dyn_cast(ptrState.source.getType()); + if (!ptrType) { + InFlightDiagnostic diag = + emitWarning(loc) + << "PtrAnalysis: source of ptrState is not a pointer type."; + return nullptr; + } + Type elementType = ptrType.getPointeeType(); + + SmallVector targetShape; + for (auto info : maskState.stateInfo) { + auto staticShape = getIntAttr(info.shape); + if (!staticShape.has_value()) { + InFlightDiagnostic diag = + emitWarning(loc) + << "MaskAnalysis: dynamic shape is not supported in reshape\n"; + return oldOther; + } + targetShape.emplace_back(staticShape.value()); + } + auto targetShapeAttr = DenseIntElementsAttr::get( + RankedTensorType::get({static_cast(targetShape.size())}, + rewriter.getI64Type()), + targetShape); + auto targetShapeType = RankedTensorType::get(targetShape, elementType); + auto targetShapeValue = + rewriter.create(loc, targetShapeAttr); + + auto reshapeOp = rewriter.create( + loc, targetShapeType, oldOther, targetShapeValue); + + return reshapeOp.getResult(); +} + +// Remap boundary_check for block ptr implicit permute. +// Formula +// - newAxis = rank - 1 - position(oldAxis in ptrState.order) +// Rules implemented +// - ptrState.order records original axes in memory-priority order. +// - createMakeTensorPtrOp rebuilds the new block ptr from +// reverse(ptrState.order), +// so the rebuilt ptr is canonicalized to descending order [rank-1, ..., 0]. +// - So each old boundary_check axis must be translated into the axis index +// of the rewritten block ptr before creating the new load/store. +// Examples +// - order=[1,0], boundary_check=[0] => [0] +// - order=[0,1], boundary_check=[0,1] => [1,0] +// - order=[2,0,1], boundary_check=[0,2] => [1,2] +SmallVector +MemOpTransformer::getBoundaryCheck(ArrayRef oldBoundaryCheck) const { + SmallVector newBoundaryCheck(oldBoundaryCheck.begin(), + oldBoundaryCheck.end()); + if (!ptrState.isPermuted || !ptrState.isBlockPtr() || + newBoundaryCheck.empty()) { + return newBoundaryCheck; + } + + int32_t rank = static_cast(ptrState.order.size()); + for (auto &boundaryAxis : newBoundaryCheck) { + auto pos = llvm::find(ptrState.order, static_cast(boundaryAxis)); + if (pos == ptrState.order.end()) { + continue; + } + boundaryAxis = + rank - 1 - + static_cast(std::distance(ptrState.order.begin(), pos)); + } + return newBoundaryCheck; +} + +bool MemOpTransformer::applyPermuteOnMask() { + if (!ptrState.isPermuted || maskState.isEmpty()) { + return true; + } + if (ptrState.permuteIds.size() != maskState.stateInfo.size()) { + return false; + } + SmallVector newMaskInfo; + for (auto id : ptrState.permuteIds) { + newMaskInfo.push_back(maskState.stateInfo[id]); + } + maskState.stateInfo = newMaskInfo; + return true; +} + +} // namespace ImplicitPermute diff --git a/third_party/ascend/lib/TritonToLinalg/LoadStoreConverter.cpp b/third_party/ascend/lib/TritonToLinalg/LoadStoreConverter.cpp new file mode 100644 index 0000000000..8367307039 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/LoadStoreConverter.cpp @@ -0,0 +1,1650 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/LoadStoreConverter.h" +#include "ascend/include/TritonToLinalg/BlockPtrAnalysis.h" +#include "ascend/include/TritonToLinalg/MaskAnalysis.h" +#include "ascend/include/TritonToLinalg/TritonToLinalgPass.h" +#include "ascend/include/Utils/DebugUtils.h" +#include "ascend/include/Utils/InterleaveOptimization.h" +#include "ascend/include/Utils/Utils.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HFusion/IR/HFusion.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" + +#include "Utils/Utils.h" +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Passes.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/MathExtras.h" + +#include "llvm/Support/Debug.h" + +#include +#include +#include + +#define DEBUG_TYPE "triton-load-store-converter" + +namespace LoadStoreConverter { +using namespace mlir; +using namespace triton; + +const std::string MayImplicitTransposeWithLastAxisTAG = + "MayImplicitTransposeWithLastAxis"; +static constexpr llvm::StringLiteral kAlreadySyncAttr = "already_sync"; + +// During dialect conversion the adaptor may expose the converted memref +// directly, while a failed rewrite later leaves a one-input, one-output UCC in +// the diagnostic IR. Accept both representations, but only for the canonical +// i1-to-i8 scalar pointer normalization. +static Value +unwrapCanonicalI8ScalarPointerMaterialization(Value originalPointer, + Value convertedPointer) { + auto bitcastOp = originalPointer.getDefiningOp(); + if (!bitcastOp) + return nullptr; + + auto sourceType = dyn_cast(bitcastOp.getSrc().getType()); + auto resultType = dyn_cast(originalPointer.getType()); + if (!sourceType || !resultType || !sourceType.getPointeeType().isInteger(1) || + !resultType.getPointeeType().isInteger(8)) + return nullptr; + + Value memref = convertedPointer; + if (auto castOp = memref.getDefiningOp()) { + if (castOp.getInputs().size() != 1 || castOp.getOutputs().size() != 1 || + castOp.getOutputs().front() != memref) + return nullptr; + memref = castOp.getInputs().front(); + } + + auto memrefType = dyn_cast(memref.getType()); + if (!memrefType || memrefType.getRank() != 1 || + !memrefType.getElementType().isInteger(8) || + !memrefType.getLayout().isIdentity() || memrefType.getMemorySpace() || + memrefType.getDimSize(0) != ShapedType::kDynamic) + return nullptr; + + return memref; +} + +static bool shouldMaterializeCustomPointer(Value ptr) { + auto result = dyn_cast(ptr); + if (!result) + return false; + + Operation *defOp = result.getOwner(); + if (!defOp || !isDistributedTypeCustomOp(defOp)) + return false; + + auto srcIndices = defOp->getAttrOfType( + ConverterUtils::customSrcPtrIndexAttrName); + if (!srcIndices) + return false; + + auto values = srcIndices.asArrayRef(); + unsigned resultIdx = result.getResultNumber(); + return resultIdx < values.size() && values[resultIdx] >= 0; +} + +static FailureOr +resolveMemoryPointer(Value originalPtr, Value convertedPtr, + ConversionPatternRewriter &rewriter) { + llvm::SmallDenseMap known; + + if (shouldMaterializeCustomPointer(originalPtr)) + return BlockDataParser::materializePointer(originalPtr, rewriter, known); + + if (isa(convertedPtr.getType())) + return convertedPtr; + + return BlockDataParser::materializePointer(originalPtr, rewriter, known); +} + +LogicalResult +AddPtrConverter::matchAndRewrite(triton::AddPtrOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Location loc = op.getLoc(); + insertDebugNop(loc, rewriter); + llvm::SmallDenseMap known; + return BlockDataParser::rewriteAddPtr(op, adaptor, rewriter, known); +} + +LogicalResult LoadConverter::toTensorAndReplace( + triton::LoadOp &op, RankedTensorType &tensorType, Value localMem, + bool mayImplicitTransposeWithLastAxis, const Location &loc, + ConversionPatternRewriter &rewriter) const { + Value loadedTensor = rewriter.create( + loc, tensorType, localMem, true, true); + propagateWasBoolToInt8Attr(op.getOperation(), loadedTensor.getDefiningOp(), + rewriter); + + if (mayImplicitTransposeWithLastAxis) { + auto markOp = rewriter.create(loc, loadedTensor); + markOp->setAttr(MayImplicitTransposeWithLastAxisTAG, + UnitAttr::get(rewriter.getContext())); + } + rewriter.replaceOp(op, loadedTensor); + return success(); +} + +/// @brief Check whether the triton::LoadOp has been modified to the specified +/// state by the AddPtrConverter. +/// @param op The triton::LoadOp operation to be checked. +/// @return Return success if the operation conforms to the specified state; +/// otherwise, return failure. +LogicalResult +LoadConverter::checkModifiedByAddPtrConverter(triton::LoadOp &op) const { + if (!isa(op->getParentOp())) { + return failure(); + } + if (!op->hasAttr("IndirectLoad")) { + return failure(); + } + auto ptrOp = op.getPtr().getDefiningOp(); + auto ptrBlock = ptrOp->getBlock(); + auto opBlock = op->getBlock(); + if (ptrBlock == opBlock) { + return failure(); + } + + return success(); +} + +void LoadConverter::propagateWasBoolToInt8Attr( + Operation *srcLoadOp, Operation *dstOp, PatternRewriter &rewriter) const { + const std::string WasBoolToInt8TAG = "was_bool_to_int8"; + if (!srcLoadOp || !dstOp) + return; + if (srcLoadOp->hasAttr(WasBoolToInt8TAG)) { + dstOp->setAttr(WasBoolToInt8TAG, rewriter.getBoolAttr(true)); + } +} + +/// @brief Continue to modify the triton::LoadOp from the state modified by the +/// AddPtrConverter. +/// @param op The triton::LoadOp operation to be processed. +/// @param adaptor The adaptor for the operation, used to obtain operands. +/// @param rewriter The pattern rewriter used to rewrite the operation. +/// @return Return success if the operation is successful; otherwise, return +/// failure. +LogicalResult LoadConverter::continueModifyFromAddPtrConverter( + triton::LoadOp &op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto forOp = op->getParentOfType(); + Operation *firstOp = &forOp.getBody()->front(); + auto extractOp = cast(firstOp); + auto ivs = extractOp.getIndices(); + // Single iterArg which is inserted by AddPtrConverter. + auto iterArg = forOp.getRegionIterArg(0); + auto ptr = adaptor.getPtr(); + + rewriter.setInsertionPointAfter(op); + Value castVal = ptr.getDefiningOp(); + Value idxZero = + rewriter.create(loc, rewriter.getIndexAttr(0)); + Value loadVal = + rewriter.create(loc, castVal, ValueRange{idxZero}); + propagateWasBoolToInt8Attr(op.getOperation(), loadVal.getDefiningOp(), + rewriter); + Value insertedVal = + rewriter.create(loc, loadVal, iterArg, ValueRange{ivs}); + // a yield op is already created by AddPtrConverter. + // so we need to replace it with a new yield op. + Operation *terminator = forOp.getBody()->getTerminator(); + scf::YieldOp oldYieldOp = cast(terminator); + auto yieldOp = rewriter.create(loc, ValueRange{insertedVal}); + rewriter.replaceOp(oldYieldOp, yieldOp); + // Now the scf.for is complete, we can replace tt.load with it. + auto rank = cast(op.getResult().getType()).getShape().size(); + Operation *rootForOp = op; + while (rank != 0) { + rank--; + rootForOp = rootForOp->getParentOfType(); + } + rewriter.replaceOp(op, rootForOp); + LLVM_DEBUG({ llvm::dbgs() << *getModuleOpFromOperation(rootForOp) << "\n"; }); + return success(); +} + +void LoadConverter::fillTensorWithOtherForMaskScenario( + Value other, Value localMem, ArrayRef maskDim, + ConversionPatternRewriter &rewriter) const { + auto loc = localMem.getLoc(); + MemRefType originalType = cast(localMem.getType()); + assert(originalType.hasStaticShape() && "only support static shape"); + assert(originalType.getRank() == maskDim.size() && + "shape and mask must have same rank"); + + auto fillFlag = + rewriter.create(loc, rewriter.getBoolAttr(false)) + .getResult(); + + for (size_t i = 0; i < originalType.getShape().size(); ++i) { + // Use dynamic value to judge whether overstep boundary + auto shapeVal = rewriter.create( + loc, rewriter.getIndexAttr(originalType.getDimSize(i))); + + Value maskDimVal; + if (isa(maskDim[i])) + maskDimVal = rewriter.create( + loc, cast(cast(maskDim[i]))); + else + maskDimVal = cast(maskDim[i]); + + auto curCmp = rewriter.create(loc, arith::CmpIPredicate::slt, + maskDimVal, shapeVal); + + fillFlag = rewriter.create(loc, fillFlag, curCmp.getResult()) + .getResult(); + } + auto ifOp = rewriter.create(loc, fillFlag); + { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToStart(&ifOp.getThenRegion().front()); + rewriter.create(loc, ValueRange{other}, + ValueRange{localMem}); + } + ifOp->setAttr(rewriter.getStringAttr("hivm.unlikely_condition"), + UnitAttr::get(rewriter.getContext())); +} + +LoadConverter::LoadConverter(MLIRContext *context) + : OpConversionPattern(context) {} + +// Masked load whose `other` is a non-scalar tensor. +// --------------------------------------------------------------------------- +// Case 1: other = mask (Python: tl.load(ptr, mask=mask, other=mask)) +// +// DSL: +// mask = offs < N +// x = tl.load(in_ptr + offs, mask=mask, other=mask) +// tl.store(out_ptr + offs, x, mask=mask) +// +// TTIR (fp — uitofp; int — extui/extsi): +// %mask = arith.cmpi slt, %offs, %N : tensor +// %other = arith.uitofp %mask … // or arith.extui for i8 +// %x = tt.load %ptr, %mask, %other +// +// Lowering (linalg): +// %alloc = memref.alloc() +// memref.copy %src_view, %dst_view // active SRC only +// %loaded = bufferization.to_tensor %alloc +// %zeros = linalg.fill 0 // [OTHER] cast(mask)→0 on pad +// %x = arith.select %mask, %loaded, %zeros +// --------------------------------------------------------------------------- +// Case 2: other = prior tensor +// (Python: other = tl.load(fill_ptr+offs); x = tl.load(ptr, mask=mask, +// other=other); tl.store(out, x) # full store — pad must stay OTHER) +// +// TTIR: +// %other = tt.load %fill_ptr +// %x = tt.load %ptr, %mask, %other +// +// Lowering: +// %other_t = bufferization.to_tensor … // prior load +// %alloc = memref.alloc() +// memref.copy … // active SRC +// %loaded = bufferization.to_tensor %alloc +// %x = arith.select %mask, %loaded, %other_t +// --------------------------------------------------------------------------- +LogicalResult LoadConverter::replaceMaskedLoadWithTensorOther( + triton::LoadOp op, Value alloc, bool mayImplicitTransposeWithLastAxis, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto tensorType = cast(op.getResult().getType()); + Value mask = op.getMask(); + Value tensorOther = op.getOther(); + if (!mask || !tensorOther) { + return rewriter.notifyMatchFailure( + op, + "replaceMaskedLoadWithTensorOther requires non-null mask and other"); + } + + Value loadedTensor = rewriter.create( + loc, tensorType, alloc, true, true); + propagateWasBoolToInt8Attr(op.getOperation(), loadedTensor.getDefiningOp(), + rewriter); + if (mayImplicitTransposeWithLastAxis) { + auto markOp = rewriter.create(loc, loadedTensor); + markOp->setAttr(MayImplicitTransposeWithLastAxisTAG, + UnitAttr::get(rewriter.getContext())); + } + + // Case 1: cast(mask) → inactive is 0. Cover fp (uitofp/sitofp) and int + // (extui/extsi). Case 2: keep prior tensor as-is. + Value otherVal = tensorOther; + if (Value remappedOther = rewriter.getRemappedValue(tensorOther)) + otherVal = remappedOther; + + bool otherIsZeroOnInactive = false; + if (auto uitofp = tensorOther.getDefiningOp()) + otherIsZeroOnInactive = (uitofp.getIn() == mask); + else if (auto sitofp = tensorOther.getDefiningOp()) + otherIsZeroOnInactive = (sitofp.getIn() == mask); + else if (auto extui = tensorOther.getDefiningOp()) + otherIsZeroOnInactive = (extui.getIn() == mask); + else if (auto extsi = tensorOther.getDefiningOp()) + otherIsZeroOnInactive = (extsi.getIn() == mask); + + if (otherIsZeroOnInactive) { + auto empty = rewriter.create(loc, tensorType.getShape(), + tensorType.getElementType()); + Value zero = rewriter.create( + loc, rewriter.getZeroAttr(tensorType.getElementType())); + otherVal = + rewriter + .create(loc, ValueRange{zero}, ValueRange{empty}) + .getResult(0); + } + + if (Value remappedMask = rewriter.getRemappedValue(mask)) + mask = remappedMask; + + Value result = + rewriter.create(loc, mask, loadedTensor, otherVal); + rewriter.replaceOp(op, result); + return success(); +} + +static bool skipNonComplementaryStaticDeinterleave(triton::LoadOp lhs) { + if (lhs->hasAttr("skip_deinterleave")) + return true; + auto lhsAddPtr = lhs.getPtr().getDefiningOp(); + if (!lhsAddPtr) + return false; + auto lhsOffset = getConstantIntValue(lhsAddPtr.getOffset()); + if (!lhsOffset) + return false; + auto pairBase = [](int64_t offset) { + int64_t lane = offset % 2; + return offset - (lane < 0 ? lane + 2 : lane); + }; + + for (triton::LoadOp rhs : lhs->getBlock()->getOps()) { + if (lhs == rhs || rhs.getMask() || rhs.getOther() || + lhs.getResult().getType() != rhs.getResult().getType()) + continue; + auto rhsAddPtr = rhs.getPtr().getDefiningOp(); + if (!rhsAddPtr || lhsAddPtr.getPtr() != rhsAddPtr.getPtr()) + continue; + auto rhsOffset = getConstantIntValue(rhsAddPtr.getOffset()); + if (!rhsOffset) + continue; + if (lhsOffset.value() != rhsOffset.value() && + pairBase(lhsOffset.value()) == pairBase(rhsOffset.value())) + continue; + // The first load can be rewritten before its sibling; tag both now. + auto skipAttr = UnitAttr::get(lhs->getContext()); + lhs->setAttr("skip_deinterleave", skipAttr); + rhs->setAttr("skip_deinterleave", skipAttr); + return true; + } + return false; +} + +LogicalResult +LoadConverter::matchAndRewrite(triton::LoadOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + + // Check if tt.load is modified by AddPtrConverter to a specified state. + if (checkModifiedByAddPtrConverter(op).succeeded()) { + return continueModifyFromAddPtrConverter(op, adaptor, rewriter); + } + + auto ptr = adaptor.getPtr(); + auto mask = op.getMask(); + auto other = op.getOther(); + auto loc = op.getLoc(); + insertDebugNopForMask(mask, rewriter); + + bool isScalarLoad = !isa(op.getResult().getType()); + Value normalizedScalarMemref; + if (isScalarLoad && op.getResult().getType().isInteger(8)) + normalizedScalarMemref = + unwrapCanonicalI8ScalarPointerMaterialization(op.getPtr(), ptr); + + if (normalizedScalarMemref) { + ptr = normalizedScalarMemref; + } else { + FailureOr resolvedPtr = + resolveMemoryPointer(op.getPtr(), ptr, rewriter); + if (failed(resolvedPtr)) + return rewriter.notifyMatchFailure( + op, "unable to materialize the load pointer as a memref"); + ptr = *resolvedPtr; + } + // handling scalar + if (isScalarLoad) { + Value scalarMemref = normalizedScalarMemref; + if (!scalarMemref) { + FailureOr resolvedScalarMemref = + BlockDataParser::getScalarMemRef(op.getPtr(), ptr, loc, rewriter); + if (failed(resolvedScalarMemref)) + return rewriter.notifyMatchFailure( + op, "scalar pointer operand has not been converted to a memref"); + scalarMemref = *resolvedScalarMemref; + } + + auto resTy = op.getResult().getType(); + auto idxZero = + rewriter.create(loc, rewriter.getIndexAttr(0)); + auto createScalarLoad = [&]() -> Value { + auto load = rewriter.create(loc, resTy, scalarMemref, + idxZero.getResult()); + propagateWasBoolToInt8Attr(op.getOperation(), load.getOperation(), + rewriter); + return load.getResult(); + }; + if (!mask) { + rewriter.replaceOp(op, createScalarLoad()); + return success(); + } + + // Keep scalar results scalar, and guard the access itself: a select after + // an unconditional load would still read a masked-off, possibly invalid + // address. + auto ifOp = rewriter.create(loc, TypeRange{resTy}, + adaptor.getMask(), true); + { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToStart(&ifOp.getThenRegion().front()); + rewriter.create(loc, createScalarLoad()); + + rewriter.setInsertionPointToStart(&ifOp.getElseRegion().front()); + Value inactiveValue = adaptor.getOther(); + if (!inactiveValue) { + // Without `other`, the inactive value is undefined. Choose zero + // without accessing memory or introducing a tensor temporary. + inactiveValue = rewriter.create( + loc, resTy, rewriter.getZeroAttr(resTy)); + } + rewriter.create(loc, inactiveValue); + } + rewriter.replaceOp(op, ifOp.getResults()); + return success(); + } + + int64_t lastStride = -1; + if (isa(ptr)) { + auto u = ptr; + while (auto blkArg = dyn_cast(u)) { + if (auto forOp = dyn_cast(blkArg.getOwner()->getParentOp())) { + auto prt = forOp->getOperand(3 + blkArg.getArgNumber() - 1); + u = prt; + } else { + u = nullptr; + break; + } + } + if (u && isa(u.getDefiningOp())) { + auto ret = mlir::ConverterUtils::getLastStrideOfReinterpretCastOp( + dyn_cast(u.getDefiningOp())); + if (ret.has_value()) + lastStride = *ret; + } + } + + // handling no mask + auto memRefType = dyn_cast(ptr.getType()); + if (!memRefType) { + return rewriter.notifyMatchFailure( + op, "LoadOp expects a memref, not a memref of pointers"); + } + if (!op->hasAttr(ConverterUtils::GeneratedByMakeTensorPtrTAG)) { + auto memrefOp = dyn_cast(ptr.getDefiningOp()); + auto ret = mlir::ConverterUtils::getLastStrideOfReinterpretCastOp(memrefOp); + if (ret.has_value()) + lastStride = *ret; + } + bool mayImplicitTransposeWithLastAxis = + (existDotFlag) && + (!op->hasAttr(ConverterUtils::GeneratedByMakeTensorPtrTAG)) && + (lastStride != 1 && + mlir::ConverterUtils::isaPermutedMemRefType(memRefType)); + auto memRefShape = memRefType.getShape(); + auto memRefElementType = memRefType.getElementType(); + + Value allocOp; + Value allocOpTmp; + auto parentLoop = dyn_cast(op->getParentOp()); + bool hasValidDiscreteLoop = op->hasAttr(ConverterUtils::discreteAttrName) && + parentLoop && + parentLoop->hasAttr("ExtractedLoadOrStore"); + if (hasValidDiscreteLoop) { + Operation *loop = op->getParentOp(); + int extractedLoopCount = 1; + for (auto parentOp = loop->getParentOp(); + parentOp->hasAttr("ExtractedLoadOrStore"); + parentOp = parentOp->getParentOp()) { + loop = parentOp; + extractedLoopCount++; + } + rewriter.setInsertionPoint(loop); + auto loopOp = cast(loop); + auto fullMemRefShape = + cast(loopOp.getInitArgs()[0].getType()).getShape(); + auto fullMemRefType = MemRefType::get(fullMemRefShape, memRefElementType); + bool isIndexSelectScenario = + (extractedLoopCount == 1) && (fullMemRefShape.size() > 1u); + if (isIndexSelectScenario) + loopOp->setAttr("hivm.parallel_loop", rewriter.getUnitAttr()); + allocOp = rewriter.create(loc, fullMemRefType); + allocOpTmp = allocOp; + rewriter.setInsertionPointAfter(loop); + auto toTensorOp = rewriter.create( + loc, RankedTensorType::get(fullMemRefShape, memRefElementType), allocOp, + true, true); + rewriter.replaceAllUsesWith(loopOp->getResult(0), toTensorOp->getResult(0)); + tensor::InsertSliceOp insertSliceOp = nullptr; + for (auto *user : op->getUsers()) { + if (auto targetOp = dyn_cast(user)) { + insertSliceOp = targetOp; + break; + } + } + auto offsets = insertSliceOp.getMixedOffsets(); + auto sizes = insertSliceOp.getMixedSizes(); + auto strides = insertSliceOp.getMixedStrides(); + auto allocType = memref::SubViewOp::inferResultType(fullMemRefType, offsets, + sizes, strides); + rewriter.setInsertionPoint(op); + allocOp = rewriter.create( + loc, cast(allocType), allocOp, offsets, sizes, strides); + rewriter.replaceAllUsesExcept(insertSliceOp.getResult(), + insertSliceOp.getDest(), insertSliceOp); + rewriter.eraseOp(insertSliceOp); + } else { + allocOp = rewriter.create( + loc, MemRefType::get(memRefShape, memRefElementType)); + } + + auto tensorType = RankedTensorType::get(memRefShape, memRefElementType); + // boundary check + auto boundaryCheck = op.getBoundaryCheck(); + if (!boundaryCheck.empty()) { + auto makeTensorPtrOp = op.getPtr().getDefiningOp(); + auto boundarySizes = mlir::ConverterUtils::getBoundarySizes( + boundaryCheck, /*remapped*/ ptr, loc, rewriter); + // handle the padding + auto padding = op.getPadding(); + SmallVector srcOffsets(boundarySizes.size(), + rewriter.getIndexAttr(0)); + SmallVector dstOffsets; + if (makeTensorPtrOp) { + auto zeroVal = rewriter.createOrFold( + loc, rewriter.getI32IntegerAttr(0)); + for (auto [idx, offVal] : llvm::enumerate(makeTensorPtrOp.getOffsets())) { + if (llvm::find(boundaryCheck, idx) == boundaryCheck.end()) { + dstOffsets.push_back(srcOffsets[idx]); + continue; + } + Value offset = + rewriter.createOrFold(loc, zeroVal, offVal); + Value size = + getValueOrCreateConstantIndexOp(rewriter, loc, boundarySizes[idx]); + offset = rewriter.createOrFold(loc, offset, zeroVal); + offset = rewriter.createOrFold( + loc, rewriter.getIndexType(), offset); + OpFoldResult ofr; + if (auto constOp = offset.getDefiningOp()) { + ofr = constOp.getValue(); + } else { + ofr = offset; + } + ofr = minOpFoldResult(ofr, size, loc, rewriter); + boundarySizes[idx] = subOpFoldResult(size, ofr, loc, rewriter); + dstOffsets.push_back(ofr); + } + } else { + dstOffsets = srcOffsets; + } + if (padding.has_value()) { + TypedAttr padAttr = rewriter.getZeroAttr(memRefElementType); + // triton already ensure only NAN and ZERO are passed in + if (padding.value() == triton::PaddingOption::PAD_NAN) { + // FIXME: Why NaN requires elemTy to be non-int or non-index? + assert(!memRefElementType.isIntOrIndex()); + auto apNaN = llvm::APFloat::getNaN( + cast(padAttr).getValue().getSemantics()); + padAttr = rewriter.getFloatAttr(memRefElementType, apNaN); + } + auto padVal = rewriter.create(loc, padAttr); + + fillTensorWithOtherForMaskScenario(padVal, allocOp, boundarySizes, + rewriter); + } + auto srcSubView = mlir::ConverterUtils::makeSubViewOp( + ptr, srcOffsets, boundarySizes, loc, rewriter); + auto dstSubview = mlir::ConverterUtils::makeSubViewOp( + allocOp, dstOffsets, boundarySizes, loc, rewriter); + auto copyOp = rewriter.create(loc, srcSubView, dstSubview); + propagateWasBoolToInt8Attr(op.getOperation(), copyOp.getOperation(), + rewriter); + if (mayImplicitTransposeWithLastAxis) { + auto markOp = rewriter.create(loc, dstSubview); + markOp->setAttr(MayImplicitTransposeWithLastAxisTAG, + UnitAttr::get(rewriter.getContext())); + } + return this->toTensorAndReplace(op, tensorType, allocOp, + mayImplicitTransposeWithLastAxis, loc, + rewriter); + } + + if (!mask) { + assert(!other && "can not input 'other' when 'mask' is not set"); + if (auto unrealizedCastOp = + ptr.getDefiningOp()) { + // TODO : not support handle associate with "module" + // hint : can be handled in Linearize + op->emitError("meeting unexpected UCC in LoadConverter!"); + return failure(); + } else { + // If last dimension stride equals 2, try deinterleave optimization. + auto [ptrStrides, ptrOffsets] = memRefType.getStridesAndOffset(); + if (ptrStrides.back() == 2 && (memRefShape.back() % 2 == 0) && + !skipNonComplementaryStaticDeinterleave(op) && + mlir::triton::DeinterleaveStatusOptimization(op, adaptor, rewriter) + .succeeded()) { + return success(); + } + auto copyOp = rewriter.create(loc, ptr, allocOp); + propagateWasBoolToInt8Attr(op.getOperation(), copyOp.getOperation(), + rewriter); + if (mayImplicitTransposeWithLastAxis && + allocOp.getDefiningOp()) { + auto markOp = rewriter.create(loc, allocOp); + markOp->setAttr(MayImplicitTransposeWithLastAxisTAG, + UnitAttr::get(rewriter.getContext())); + } else if (mayImplicitTransposeWithLastAxis && + allocOp.getDefiningOp()) { + auto markOp = rewriter.create(loc, allocOpTmp); + markOp->setAttr(MayImplicitTransposeWithLastAxisTAG, + UnitAttr::get(rewriter.getContext())); + } + } + + return this->toTensorAndReplace(op, tensorType, allocOp, + mayImplicitTransposeWithLastAxis, loc, + rewriter); + } + + MaskState mstate; + auto isContMask = mstate.parse(mask, loc, rewriter); + if (isContMask.failed()) { + return rewriter.notifyMatchFailure( + op, "can not lower uncontinuout masked loads"); + } + + Value tensorOtherBase; + if (other) { + auto scalarOther = + mlir::ConverterUtils::getScalarValue(other, loc, rewriter); + if (scalarOther) { + fillTensorWithOtherForMaskScenario(scalarOther, allocOp, mstate.dims, + rewriter); + } else if (isa(other.getType())) { + tensorOtherBase = other; + } else { + return rewriter.notifyMatchFailure( + op, "other value used in masked load produced by unsupported " + "instruction"); + } + } + + // To enable deinterleave optimization with mask load, mask state along last + // dimension couldn't be split, which means `dims.back()` must be equal to + // origin type last dimension constant size and `offsets.back()` must be 0. + // + // The basis is that last dimension range comparison would generate + // unaccepted discontinuous mask. + if (!tensorOtherBase && mstate.getRank() == memRefType.getRank() && + isConstantIntValue(mstate.offsets.back(), 0) && + isConstantIntValue(mstate.dims.back(), memRefType.getShape().back())) { + auto [ptrStrides, ptrOffsets] = memRefType.getStridesAndOffset(); + if (ptrStrides.back() == 2 && (memRefType.getShape().back() % 2 == 0) && + DeinterleaveStatusWithMaskOptimization(op, adaptor, rewriter, mstate, + allocOp) + .succeeded()) { + return success(); + } + } + + if (auto unrealizedCastOp = ptr.getDefiningOp()) { + // TODO : not support handle associate with "module" + // hint : can be handled in Linearize + op->emitError("meeting unexpected UCC in LoadConverter!"); + return failure(); + } else { + if (mstate.isMemrefSubviewValid(ptr, rewriter)) { + memref::SubViewOp srcSubView = mstate.getSubview(ptr, loc, rewriter); + memref::SubViewOp dstSubView = mstate.getSubview(allocOp, loc, rewriter); + MemRefType dstSubViewType = mlir::cast(dstSubView.getType()); + + auto [srcStrides, srcOffset] = dstSubViewType.getStridesAndOffset(); + MemRefType castType = MemRefType::get( + dstSubViewType.getShape(), dstSubViewType.getElementType(), + makeStridedLinearLayoutMap(srcStrides, srcOffset, + rewriter.getContext())); + auto castOp = rewriter.create(loc, castType, dstSubView); + auto copyOp = rewriter.create(loc, srcSubView, castOp); + propagateWasBoolToInt8Attr(op.getOperation(), copyOp.getOperation(), + rewriter); + } + + if (mayImplicitTransposeWithLastAxis && + allocOp.getDefiningOp()) { + auto markOp = rewriter.create(loc, allocOp); + markOp->setAttr(MayImplicitTransposeWithLastAxisTAG, + UnitAttr::get(rewriter.getContext())); + } else if (mayImplicitTransposeWithLastAxis && + allocOp.getDefiningOp()) { + auto markOp = rewriter.create(loc, allocOpTmp); + markOp->setAttr(MayImplicitTransposeWithLastAxisTAG, + UnitAttr::get(rewriter.getContext())); + } + } + + if (!tensorOtherBase) { + return this->toTensorAndReplace(op, tensorType, allocOp, + mayImplicitTransposeWithLastAxis, loc, + rewriter); + } + + return replaceMaskedLoadWithTensorOther( + op, allocOp, mayImplicitTransposeWithLastAxis, rewriter); +} + +AtomicRMWConverter::AtomicRMWConverter(MLIRContext *context) + : OpConversionPattern(context) {} + +// lowering tt.atomicRMW to linalg.generic +// If atomic op's return value is used by other op as it's the old value stored +// at the ptrwe will use tt.load to get it +// +// example: +// input: +// %return_value = tt.atomic_rmw fadd, acq_rel, gpu, +// %output_memref, %input_tensor, %mask : +// (tensor<256x!tt.ptr>, tensor<256xf32>, tensor<256xi1>) +// -> tensor<256xf32> +// +// output: +// memref.copy %output_memref, %ub_buf : memref to memref +// %17 = bufferization.to_tensor %alloc_3 restrict writable : memref<256xf32> +// linalg.generic +// {indexing_maps = [#map, #map, #map], iterator_types = ["parallel"]} +// ins(%output_memref, %masked_input_memref : memref, memref) +// outs(%subview_2 : memref) +// attrs = {GenericAtomicRMW = "fadd", MemSemantic = "acq_rel", +// MemSyncScope = "gpu"} { +// ^bb0(%in: f32, %in_9: f32, %out: f32): +// %25 = arith.addf %in, %in_9 : f32 +// linalg.yield %25 : f32 +// } +LogicalResult +AtomicRMWConverter::matchAndRewrite(triton::AtomicRMWOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto ptr = adaptor.getPtr(); + auto val = op.getVal(); + auto loc = op.getLoc(); + auto mask = op.getMask(); + auto rmwOp = op.getAtomicRmwOp(); + auto resType = dyn_cast(op.getResult().getType()); + insertDebugNop(loc, rewriter); + insertDebugNopForMask(mask, rewriter); + + FailureOr resolvedPtr = + resolveMemoryPointer(op.getPtr(), ptr, rewriter); + if (failed(resolvedPtr)) + return rewriter.notifyMatchFailure( + op, "unable to materialize the atomic RMW pointer as a memref"); + ptr = *resolvedPtr; + auto ptrType = dyn_cast(ptr.getType()); + if (!resType) + return rewriter.notifyMatchFailure( + op, "atomicRMWConverter: scalar will be handled by " + "ScalarAtomicRMWCanonicalizer"); + if (!ptrType) + return rewriter.notifyMatchFailure( + op, "AtomicRMWOp expects a memref, not a memref of pointers"); + + const std::map atomicKindMap = { + {RMWOp::ADD, hivm::AtomicKind::ADD}, + {RMWOp::FADD, hivm::AtomicKind::ADD}, + {RMWOp::OR, hivm::AtomicKind::OR}, + {RMWOp::XOR, hivm::AtomicKind::XOR}, + {RMWOp::AND, hivm::AtomicKind::AND}, + {RMWOp::MIN, hivm::AtomicKind::MIN}, + {RMWOp::UMIN, hivm::AtomicKind::UMIN}, + {RMWOp::MAX, hivm::AtomicKind::MAX}, + {RMWOp::UMAX, hivm::AtomicKind::UMAX}, + {RMWOp::XCHG, hivm::AtomicKind::XCHG}, + }; + const std::map hfusionAtomicKindMap = { + {RMWOp::ADD, hfusion::AtomicKind::ADD}, + {RMWOp::FADD, hfusion::AtomicKind::ADD}, + {RMWOp::OR, hfusion::AtomicKind::OR}, + {RMWOp::XOR, hfusion::AtomicKind::XOR}, + {RMWOp::AND, hfusion::AtomicKind::AND}, + {RMWOp::MIN, hfusion::AtomicKind::MIN}, + {RMWOp::UMIN, hfusion::AtomicKind::UMIN}, + {RMWOp::MAX, hfusion::AtomicKind::MAX}, + {RMWOp::UMAX, hfusion::AtomicKind::UMAX}, + {RMWOp::XCHG, hfusion::AtomicKind::XCHG}, + }; + assert(atomicKindMap.find(rmwOp) != atomicKindMap.end()); + auto atomicKind = + hivm::AtomicKindAttr::get(rewriter.getContext(), atomicKindMap.at(rmwOp)); + assert(hfusionAtomicKindMap.find(rmwOp) != hfusionAtomicKindMap.end()); + auto hfusionAtomicKind = hfusion::AtomicKindAttr::get( + rewriter.getContext(), hfusionAtomicKindMap.at(rmwOp)); + + auto dstMemref = ptr; + Value inputVal = val; + + // Lazily materialize a memref view only when we truly need buffer + // semantics (e.g., mask subview or XCHG lowering). Otherwise keep tensor + // inputs to avoid redundant to_memref conversions before hivm.store. + auto getInputMemref = [&]() -> Value { + if (isa(inputVal.getType())) + return inputVal; + return rewriter.create(loc, ptrType, inputVal); + }; + auto inputMemref = getInputMemref(); + auto inputMemrefType = cast(inputMemref.getType()); + auto elementType = inputMemrefType.getElementType(); + auto isHardwareSupported = + (rmwOp == RMWOp::ADD || rmwOp == RMWOp::FADD || rmwOp == RMWOp::MAX || + rmwOp == RMWOp::MIN) && + (elementType.isF16() || elementType.isBF16() || elementType.isF32() || + elementType.isInteger(8) || elementType.isInteger(16) || + elementType.isInteger(32)); + + bool isDiscreteMask = false; + bool hasContinuousMaskSubview = false; + bool hasUsedReturn = !op.getResult().use_empty(); + MaskState mstate; + if (mask) { + auto constantMask = mask.getDefiningOp(); + if (constantMask && !isConstantMaskTrue(mask)) { + rewriter.eraseOp(op); + return success(); + } + isDiscreteMask = mstate.parse(mask, loc, rewriter).failed(); + if (!constantMask && !isDiscreteMask) { + // For dstMemref (store output), use subview to maintain reference to + // original memref. For inputVal (store input), use tensor.extract_slice + // to keep tensor semantics. + dstMemref = mstate.getSubview(ptr, loc, rewriter); + hasContinuousMaskSubview = true; + if (isHardwareSupported) { + auto inputTensorType = RankedTensorType::get( + inputMemrefType.getShape(), inputMemrefType.getElementType()); + if (!isa(inputVal.getType())) + inputVal = rewriter.create( + loc, inputTensorType, inputMemref, true, true); + inputVal = mstate.getExtractSlice(inputVal, loc, rewriter); + } else { + inputMemref = mstate.getSubview(inputMemref, loc, rewriter); + } + } + } + + bool needsReturnValueLock = + hasUsedReturn && hasContinuousMaskSubview && isHardwareSupported; + Value returnValueLock; + if (hasUsedReturn) { + auto tensorType = + RankedTensorType::get(ptrType.getShape(), ptrType.getElementType()); + auto alloc = rewriter.create( + loc, MemRefType::get(ptrType.getShape(), ptrType.getElementType())); + Value copySrc = ptr; + Value copyDst = alloc; + if (hasContinuousMaskSubview) { + // Masked-off atomic results are undefined. Copy only the active region + // so the old-value read and the atomic store use the same address view. + copySrc = dstMemref; + copyDst = mstate.getSubview(alloc, loc, rewriter); + } + if (needsReturnValueLock) { + auto lockType = MemRefType::get({1}, rewriter.getI64Type()); + auto lockVar = + rewriter.create(loc, lockType, Value()); + returnValueLock = lockVar.getResult(); + rewriter.create(loc, returnValueLock); + } + rewriter.create(loc, copySrc, copyDst); + Value tensorToReplace = rewriter.create( + loc, tensorType, alloc, true /* restrict */, true /* writable */); + rewriter.replaceOp(op, tensorToReplace); + } + + if (isDiscreteMask) { + if (rmwOp != RMWOp::XCHG) { + return op.emitError( + "Discrete mask is only expected for XCHG; other atomics " + "should be lowered without discrete masks"); + } + Value memrefMask = mask; + if (auto maskTypeT = dyn_cast(mask.getType())) { + MemRefType maskTypeM = + MemRefType::get(maskTypeT.getShape(), maskTypeT.getElementType()); + memrefMask = + rewriter.create(loc, maskTypeM, mask); + } + rewriter.create(op.getLoc(), TypeRange(), + inputMemref, dstMemref, memrefMask); + } else { + if (isHardwareSupported) { + auto storeOp = rewriter.create( + op.getLoc(), TypeRange{}, inputVal, dstMemref, atomicKind); + if (needsReturnValueLock) { + // The return-value copy and the atomic update must be one critical + // section. Mark the store so HIVM does not add another lock. + storeOp->setAttr(kAlreadySyncAttr, rewriter.getUnitAttr()); + rewriter.create(loc, returnValueLock); + } + } else if (rmwOp == RMWOp::XCHG) + rewriter.create(op.getLoc(), TypeRange(), + inputMemref, dstMemref); + else { + if (rmwOp == RMWOp::OR || rmwOp == RMWOp::XOR || rmwOp == RMWOp::AND) { + if (!elementType.isSignlessIntOrIndex()) { + return op->emitOpError() + << "must be signless-integer-like, but got " << elementType; + } + } + // Currently, for atomic kind and element type that is not supported by + // the hardware, we use software to simulate the computation. However, + // decompose now happens in both HFusion and HIVM, and is not consistent + // for 910B and 91095. Therefore, we convert to different atomic/store ops + // for now. This should be unified and refactored later. + if (compileOn91095Flag) { + rewriter.create( + op.getLoc(), TypeRange{}, ValueRange{inputMemref}, + ValueRange{dstMemref}, hfusionAtomicKind, + ArrayRef{}); + } else { + rewriter.create(op.getLoc(), TypeRange(), + inputMemref, dstMemref, + hfusionAtomicKind); + } + } + } + + if (op.getResult().use_empty()) { + rewriter.eraseOp(op); + } + return success(); +} + +LogicalResult +AtomicCASConverter::matchAndRewrite(triton::AtomicCASOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + // If the result of AtomicCASOp is not used, we don't need to load the old + // data stored at the ptr + auto ptr = adaptor.getPtr(); + auto cmp = op.getCmp(); + auto val = op.getVal(); + auto loc = op.getLoc(); + + FailureOr resolvedPtr = + resolveMemoryPointer(op.getPtr(), ptr, rewriter); + if (failed(resolvedPtr)) + return rewriter.notifyMatchFailure( + op, "unable to materialize the atomic CAS pointer as a memref"); + ptr = *resolvedPtr; + auto resType = dyn_cast(op.getResult().getType()); + if (!resType) { + return rewriter.notifyMatchFailure( + op, "atomicCASConverter: scalar will be handled by " + "ScalarAtomicCASCanonicalizer"); + } + + // 1. Simple case where no mask is used. + auto type = dyn_cast(ptr.getType()); + if (!type) { + // Seen when implicit broadcasting is done late in a chain of + // operations. The workaround is to broadcast the pointers early in the + // address calculation. A proper fix is complicated, but at least we can + // provide a better error message. + return rewriter.notifyMatchFailure( + op, "AtomicCASOp expects a memref, not a memref of pointers"); + } + + auto dstMemref = ptr; + // Well, linalg structure op wouldn't support mixed tensor/buffer semantics + // any more in latest LLVM(triton LLVM dependency has involed this), so we + // need to convert tensor to buffer early. + auto dstOriType = cast(dstMemref.getType()); + MemRefType dstType = + MemRefType::get(dstOriType.getShape(), dstOriType.getElementType()); + Value inputMemref = + rewriter.create(loc, dstType, val); + + Value cmpMemref = + rewriter.create(loc, dstType, cmp); + + // create element-wise map + int64_t rank = type.getRank(); + SmallVector inputDims; + auto context = rewriter.getContext(); + + for (int i = 0; i < rank; i++) { + inputDims.push_back(getAffineDimExpr(i, context)); + } + + SmallVector indexingMaps; + // As mask has been erased for now + // the number of input must be 2 + // the input memref is also the output memref + // Thus, there are a total of four inputs and outputs. + // so here we have 4 map to create + for (int i = 0; i < 4; i++) { // 4: 3 input and 1 output + indexingMaps.push_back(AffineMap::get(rank, 0, inputDims, context)); + } + + if (!op.getResult().use_empty()) { + auto tensorType = + RankedTensorType::get(type.getShape(), type.getElementType()); + auto alloc = rewriter.create( + loc, MemRefType::get(type.getShape(), type.getElementType())); + + // For the return value, don't need to care about mask for now + // this op don't support other, so we best not fill it + rewriter.create(loc, ptr, alloc); + Value tensor = rewriter.create( + loc, tensorType, alloc, true /* restrict */, true /* writable */); + rewriter.replaceOp(op, tensor); + } + + auto linalgOp = rewriter.create( + loc, ValueRange{dstMemref, cmpMemref, inputMemref}, + mlir::ValueRange{dstMemref}, indexingMaps, + mlir::ConverterUtils::getNParallelLoopsAttrs(rank), + [&](OpBuilder &nestedBuilder, Location nestedLoc, ValueRange blockArgs) { + Value lhs = blockArgs[0]; + Value rhs = blockArgs[1]; + Value setValue = blockArgs[2]; + Value cond; + if (mlir::isa(lhs.getType())) { + cond = nestedBuilder.create( + nestedLoc, arith::CmpFPredicate::UEQ, lhs, rhs); + } else { + cond = nestedBuilder.create( + nestedLoc, arith::CmpIPredicate::eq, lhs, rhs); + } + auto ifOp = nestedBuilder.create( + nestedLoc, TypeRange{setValue.getType()}, cond, true); + { + OpBuilder::InsertionGuard guard(nestedBuilder); + nestedBuilder.setInsertionPointToEnd(&ifOp.getThenRegion().front()); + nestedBuilder.create(nestedLoc, setValue); + } + { + OpBuilder::InsertionGuard guard(nestedBuilder); + nestedBuilder.setInsertionPointToEnd(&ifOp.getElseRegion().front()); + nestedBuilder.create(nestedLoc, lhs); + } + nestedBuilder.setInsertionPointToEnd(nestedBuilder.getBlock()); + nestedBuilder.create(nestedLoc, + ifOp.getResult(0)); + }); + + const StringRef genericAtomicRMW = "GenericAtomicRMW"; + const StringRef memSemantic = "MemSemantic"; + const StringRef memSyncScope = "MemSyncScope"; + auto attr = mlir::StringAttr::get(context, "cas"); + + linalgOp->setAttr(genericAtomicRMW, attr); + linalgOp->setAttr(memSemantic, + rewriter.getStringAttr(stringifyEnum(op.getSem()))); + linalgOp->setAttr(memSyncScope, + rewriter.getStringAttr(stringifyEnum(op.getScope()))); + + linalgOp->setAttr("Software", rewriter.getUnitAttr()); + + // tt.atomicRMW op has two part of feature + // 1. load the old data at the ptr + // 2. atomically store the data on ub to the ptr + // at the same time it perform the action it has been assigned + // So we lower this op to load + atomically store + // + // The first part is not necessary when the returned value of atomic op + // is not used, it will be deleted cause it's meaningless + // Here, we preemptively determine whether it will be used + // and decide whether it is necessary to create the load process based on + // this assessment. + // + // logic of handling is copied + if (op.getResult().use_empty()) { + rewriter.eraseOp(op); + } + return success(); +} + +LogicalResult +ScalarStoreCanonicalizer::matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const { + if (!op.getValue().getType().isIntOrIndexOrFloat()) { + return rewriter.notifyMatchFailure( + op, "ScalarStoreCanonicalizer handles scalar store scene!"); + } + auto ptr = op.getPtr(); + auto mask = op.getMask(); + auto value = op.getValue(); + if (mask) { + rewriter.replaceOpWithNewOp( + op, mask, [&](OpBuilder &b, Location loc) { + b.create(loc, ptr, value, op.getCache(), + op.getEvict()); + b.create(loc); + }); + return success(); + } + + auto ptrTy = RankedTensorType::get({(int64_t)1}, ptr.getType()); + auto ptrSplat = rewriter.create(op.getLoc(), ptrTy, ptr); + auto valTy = RankedTensorType::get({(int64_t)1}, value.getType()); + auto valSplat = rewriter.create(op.getLoc(), valTy, value); + auto newStoreOp = rewriter.create( + op.getLoc(), ptrSplat, valSplat, op.getCache(), op.getEvict()); + rewriter.replaceOp(op, newStoreOp); + return success(); +} + +LogicalResult +ScalarAtomicRMWCanonicalizer::matchAndRewrite(triton::AtomicRMWOp op, + PatternRewriter &rewriter) const { + if (!op.getVal().getType().isIntOrIndexOrFloat()) { + return rewriter.notifyMatchFailure( + op, "ScalarAtomicRMWCanonicalizer handles scalar atomic rmw op scene!"); + } + + auto ptr = op.getPtr(); + auto ptrTy = RankedTensorType::get({(int64_t)1}, ptr.getType()); + auto ptrSplat = rewriter.create(op.getLoc(), ptrTy, ptr); + auto valTy = RankedTensorType::get({(int64_t)1}, op.getVal().getType()); + auto valSplat = + rewriter.create(op.getLoc(), valTy, op.getVal()); + auto maskTy = RankedTensorType::get({(int64_t)1}, op.getMask().getType()); + auto maskSplat = + rewriter.create(op.getLoc(), maskTy, op.getMask()); + + auto newAtomicOp = rewriter.create( + op.getLoc(), valTy, op.getAtomicRmwOp(), ptrSplat, valSplat, maskSplat, + op.getSem(), op.getScope()); + auto idxZero = + rewriter.create(op.getLoc(), rewriter.getIndexAttr(0)); + rewriter.replaceOpWithNewOp(op, newAtomicOp, + ValueRange({idxZero})); + return success(); +} + +LogicalResult +ScalarAtomicCASCanonicalizer::matchAndRewrite(triton::AtomicCASOp op, + PatternRewriter &rewriter) const { + if (!op.getVal().getType().isIntOrIndexOrFloat() && + !op.getCmp().getType().isIntOrIndexOrFloat()) { + return rewriter.notifyMatchFailure( + op, "ScalarAtomicCASCanonicalizer handles scalar atomic cas op scene!"); + } + + auto ptr = op.getPtr(); + auto ptrTy = RankedTensorType::get({(int64_t)1}, ptr.getType()); + auto ptrSplat = rewriter.create(op.getLoc(), ptrTy, ptr); + auto cmpTy = RankedTensorType::get({(int64_t)1}, op.getCmp().getType()); + auto cmpSplat = + rewriter.create(op.getLoc(), cmpTy, op.getCmp()); + auto valTy = RankedTensorType::get({(int64_t)1}, op.getVal().getType()); + auto valSplat = + rewriter.create(op.getLoc(), valTy, op.getVal()); + + auto newAtomicOp = rewriter.create( + op.getLoc(), valTy, ptrSplat, cmpSplat, valSplat, op.getSem(), + op.getScope()); + auto idxZero = + rewriter.create(op.getLoc(), rewriter.getIndexAttr(0)); + rewriter.replaceOpWithNewOp(op, newAtomicOp, + ValueRange({idxZero})); + return success(); +} + +// The atomic max op with float input will be devided into +// two atomic max ops with integer input +// One handles the part of the tensor greater than zero +// the other deals with the part less than zero +// It will lead to maskAnalysis failure +// So here we need to revert the procedures in semantics.py +// The triton IR is like +// +// %cst_0 = arith.constant dense<0.000000e+00> : tensor<1x256xf32> +// %1 = tt.bitcast %value : tensor<1x256xf32> -> tensor<1x256xi32> +// %2 = tt.bitcast %ptr : tensor<1x256x!tt.ptr> -> +// tensor<1x256x!tt.ptr> %3 = arith.cmpf oge, %1, %cst_0 %4 = arith.cmpf +// olt, %1, %cst_0 %5 = arith.andi %8, %3 %6 = tt.atomic_rmw max, acq_rel, gpu, +// %2, %1, %5 : +// (tensor<1x256x!tt.ptr>, tensor<1x256xi32>, tensor<1x256xi1>) -> +// tensor<1x256xi32> +// %7 = arith.andi %8, %4 +// %8 = tt.atomic_rmw umin, acq_rel, gpu, %2, %1, %7 : +// (tensor<1x256x!tt.ptr>, tensor<1x256xi32>, tensor<1x256xi1>) -> +// tensor<1x256xi32> +// +// it's hard to handle and meaningless complicated for our device +// so we revert it to +// %0 = tt.atomic_rmw max, acq_rel, gpu, %23, %21, %8 : +// (tensor<1x256x!tt.ptr>, tensor<1x256xf32>, tensor<1x256xi1>) -> +// tensor<1x256xf32> +LogicalResult +AtomicMaxMinCanonicalizer::matchAndRewrite(triton::AtomicRMWOp op, + PatternRewriter &rewriter) const { + // Revert the op to its original form + auto ptrBitcastOp = op.getPtr().getDefiningOp(); + triton::SplatOp ptrSplatOp; + if (!ptrBitcastOp) { + // For tensor atomics on a scalar base pointer, semantic.py emits + // splat(bitcast(ptr -> ptr)) + // instead of + // bitcast(tensor> -> tensor>). + // Accept both forms so the expanded integer atomics can be fused before + // discrete-mask conversion. + ptrSplatOp = op.getPtr().getDefiningOp(); + if (ptrSplatOp) + ptrBitcastOp = ptrSplatOp.getSrc().getDefiningOp(); + } + auto valueBitcastOp = op.getVal().getDefiningOp(); + if (!ptrBitcastOp || !valueBitcastOp) { + return failure(); + } + + auto eraseDeadPointerCasts = [&]() { + if (ptrSplatOp && ptrSplatOp->use_empty()) + rewriter.eraseOp(ptrSplatOp); + if (ptrBitcastOp->use_empty()) + rewriter.eraseOp(ptrBitcastOp); + }; + + // We only need to handle the op when the element type is float + auto elementType = getElementTypeOrSelf(valueBitcastOp.getSrc().getType()); + if (!isa(elementType)) { + return failure(); + } + + auto rmwOp = op.getAtomicRmwOp(); + // here we know that atomic UMAX/UMIN + // is created by special logic of triton right now + // so we can simply delete it + if (rmwOp == triton::RMWOp::UMAX || rmwOp == triton::RMWOp::UMIN) { + // if the return value of op is used, we can't simply erase it + if (op.getResult().use_empty()) { + rewriter.eraseOp(op); + eraseDeadPointerCasts(); + return success(); + } + return failure(); + } + + if (rmwOp != triton::RMWOp::MAX && rmwOp != triton::RMWOp::MIN) { + return failure(); + } + + // 1. Though semantic interpreter will generate full true tensor as original + // mask if atomicrmwOp don't have it, above float devision process will also + // generate positive and negative comparison mask, which will cause to fold + // true mask. + // 2. While if atomicrmwOp has original mask, there exists andiop between + // original mask and positive/negative comparison mask + // + // Here wanna extract original mask + Value originalMask = op.getMask(); + auto createAllTrueMask = [&]() -> Value { + Type maskType = op.getMask().getType(); + if (auto shapedMaskType = dyn_cast(maskType)) { + return rewriter.create( + op->getLoc(), DenseElementsAttr::get(shapedMaskType, true)); + } + if (maskType.isInteger(1)) { + return rewriter.create(op->getLoc(), + rewriter.getBoolAttr(true)); + } + return {}; + }; + + if (auto andOp = originalMask.getDefiningOp()) + // LHS is convention in semantic interpreter + originalMask = andOp.getLhs(); + else if (auto xorOp = originalMask.getDefiningOp()) { + // Current f32 atomic_min uses !signbit as the positive mask: + // shrui(value_bits, 31) -> cmpi ne 0 -> xori true. + if ((rmwOp != triton::RMWOp::MIN && rmwOp != triton::RMWOp::MAX) || + (!elementType.isF32() && !elementType.isF64()) || + !matchPattern(xorOp.getRhs(), m_One())) { + return failure(); + } + + auto cmpOp = xorOp.getLhs().getDefiningOp(); + if (!cmpOp || cmpOp.getPredicate() != arith::CmpIPredicate::ne || + !matchPattern(cmpOp.getRhs(), m_Zero())) + return op->emitError("Illegal mask for atomicrmwOp of float type"); + + auto shiftOp = cmpOp.getLhs().getDefiningOp(); + if (!shiftOp || shiftOp.getLhs() != valueBitcastOp.getResult()) + return op->emitError("Illegal mask for atomicrmwOp of float type"); + + // Restore the implicit all-true mask. + originalMask = createAllTrueMask(); + if (!originalMask) + return failure(); + } else if (auto cmpOp = originalMask.getDefiningOp()) { + // Scalar floating-point atomic max/min represents !signbit as: + // shrui(value_bits, 31/63) -> cmpi eq 0. + if (cmpOp.getPredicate() != arith::CmpIPredicate::eq || + !matchPattern(cmpOp.getRhs(), m_Zero())) + return failure(); + + auto shiftOp = cmpOp.getLhs().getDefiningOp(); + if (!shiftOp || shiftOp.getLhs() != valueBitcastOp.getResult()) + return failure(); + + originalMask = createAllTrueMask(); + if (!originalMask) + return failure(); + } else if (auto cmpOp = originalMask.getDefiningOp()) { + if (cmpOp.getPredicate() != mlir::arith::CmpFPredicate::OGE || + !matchPattern(cmpOp.getRhs(), + /*positive float zero matcher*/ m_PosZeroFloat())) + // Here recheck frontend interpreter generation in no manual mask state + return op->emitError("Illegal mask for atomicrmwOp of float type"); + // Restore original true mask + originalMask = createAllTrueMask(); + if (!originalMask) + return failure(); + } else + return op->emitError("Illegal mask for atomicrmwOp of float type"); + + Value originalPtr = ptrBitcastOp.getSrc(); + if (ptrSplatOp) { + auto ptrTensorType = dyn_cast(op.getPtr().getType()); + if (!ptrTensorType) + return failure(); + auto originalPtrType = RankedTensorType::get( + ptrTensorType.getShape(), ptrBitcastOp.getSrc().getType(), + ptrTensorType.getEncoding()); + originalPtr = rewriter.create(op.getLoc(), originalPtrType, + ptrBitcastOp.getSrc()); + } + + auto originAtomicOp = rewriter.create( + op.getLoc(), valueBitcastOp.getSrc().getType(), op.getAtomicRmwOp(), + originalPtr, valueBitcastOp.getSrc(), originalMask, op.getSem(), + op.getScope()); + + // if the return value of op is used + // we need to handle its usage + // In semantic.py, if the atomic Max/Min with float input is used + // It will use select + bitcast to get float value + // so here we need to revert it too + // + // For example: + // %0 = tt.atomic_rmw max, acq_rel, gpu, %gm, %input, %mask1 : + // (tensor<32x!tt.ptr>... %1 = tt.atomic_rmw umin, acq_rel, gpu, %gm, + // %input, %mask2 : (tensor<32x!tt.ptr>... %2 = arith.select + // %devidedMask, %0, %1 : tensor<32xi1>, tensor<32xi32> %3 = tt.bitcast %2 : + // tensor<32xi32> -> tensor<32xf32> tt.store %outputMemref, %3 : + // tensor<32x!tt.ptr> + // + // will be revert to: + // %0 = tt.atomic_rmw max, acq_rel, gpu, %gm, %input, %mask : + // (tensor<32x!tt.ptr>... tt.store %outputMemref, %0 : + // tensor<32x!tt.ptr> + // + if (!op.getResult().use_empty()) { + for (OpOperand &use : op->getUses()) { + auto selectOp = dyn_cast(use.getOwner()); + if (!selectOp) + continue; + + for (OpOperand &selectUse : selectOp->getUses()) { + if (auto bitcastOp = + dyn_cast(selectUse.getOwner())) { + bitcastOp.getResult().replaceAllUsesWith(originAtomicOp); + } + } + } + rewriter.replaceOp(op, originAtomicOp); + } else { + rewriter.eraseOp(op); + } + + // The restored atomic uses the original floating-point GM pointer. Remove + // the expanded pointer splat/bitcast once the paired integer atomic is gone. + eraseDeadPointerCasts(); + + return success(); +} + +StoreConverter::StoreConverter(MLIRContext *context) + : OpConversionPattern(context) {} + +LogicalResult +StoreConverter::matchAndRewrite(triton::StoreOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + + // triton store op basic + auto mask = op.getMask(); + auto loc = op.getLoc(); + insertDebugNopForMask(mask, rewriter); + auto ptr = adaptor.getPtr(); + auto val = adaptor.getValue(); + + FailureOr resolvedPtr = + resolveMemoryPointer(op.getPtr(), ptr, rewriter); + if (failed(resolvedPtr)) + return rewriter.notifyMatchFailure( + op, "unable to materialize the store pointer as a memref"); + ptr = *resolvedPtr; + // 1. boundary size check + auto boundaryCheck = op.getBoundaryCheck(); + if (!boundaryCheck.empty()) { + auto makeTensorPtrOp = op.getPtr().getDefiningOp(); + auto boundarySizes = mlir::ConverterUtils::getBoundarySizes( + boundaryCheck, /*remapped*/ ptr, loc, rewriter); + SmallVector srcOffsets; + SmallVector dstOffsets(boundarySizes.size(), + rewriter.getIndexAttr(0)); + if (makeTensorPtrOp) { + auto zeroVal = rewriter.createOrFold( + loc, rewriter.getI32IntegerAttr(0)); + for (auto [idx, offVal] : llvm::enumerate(makeTensorPtrOp.getOffsets())) { + if (llvm::find(boundaryCheck, idx) == boundaryCheck.end()) { + srcOffsets.push_back(dstOffsets[idx]); + continue; + } + Value offset = + rewriter.createOrFold(loc, zeroVal, offVal); + Value size = + getValueOrCreateConstantIndexOp(rewriter, loc, boundarySizes[idx]); + offset = rewriter.createOrFold(loc, offset, zeroVal); + offset = rewriter.createOrFold( + loc, rewriter.getIndexType(), offset); + OpFoldResult ofr; + if (auto constOp = offset.getDefiningOp()) { + ofr = constOp.getValue(); + } else { + ofr = offset; + } + ofr = minOpFoldResult(ofr, size, loc, rewriter); + boundarySizes[idx] = subOpFoldResult(size, ofr, loc, rewriter); + srcOffsets.push_back(ofr); + } + } else { + srcOffsets = dstOffsets; + } + auto srcSlice = mlir::ConverterUtils::makeExtractSliceOp( + val, srcOffsets, boundarySizes, loc, rewriter); + auto dstSubview = mlir::ConverterUtils::makeSubViewOp( + ptr, dstOffsets, boundarySizes, loc, rewriter); + auto storeOp = rewriter.create( + loc, srcSlice, dstSubview); + storeOp.setWritable(true); + rewriter.eraseOp(op); + return success(); + } + + // 2. Simple load with no mask + if (!mask) { + auto storeOp = rewriter.create( + loc, val, ptr); + storeOp.setWritable(true); + rewriter.eraseOp(op); + return success(); + } + + // 3. Continuous masked stores. + // Analyze the mask operand to determine at runtime the size of the data we + // are moving. + MaskState mstate; + auto isContMask = mstate.parse(mask, loc, rewriter); + + if (isContMask.failed()) { + return failure(); + } + LLVM_DEBUG({ llvm::dbgs() << *getModuleOpFromOperation(op) << "\n"; }); + auto srcSlice = mstate.getExtractSlice(val, loc, rewriter); + auto dstSubview = mstate.getSubview(ptr, loc, rewriter); + auto storeOp = rewriter.create( + loc, srcSlice, dstSubview); + storeOp.setWritable(true); + rewriter.eraseOp(op); + return success(); +} + +bool ReinterpretCastStrideCanonicalizer::hasFixableZeroStride( + memref::ReinterpretCastOp op) { + auto staticSizes = op.getStaticSizes(); + auto staticStrides = op.getStaticStrides(); + auto dynamicStrides = op.getStrides(); + + if (staticSizes.size() != staticStrides.size()) + return false; + + // now handle: size all static + if (llvm::any_of(staticSizes, ShapedType::isDynamic)) + return false; + + unsigned dynStrideIdx = 0; + for (unsigned i = 0; i < staticStrides.size(); ++i) { + // now handle: dynamic stride 0 with static size 1 + if (!ShapedType::isDynamic(staticStrides[i])) + continue; + + if (dynStrideIdx >= dynamicStrides.size()) + return false; + + Value st = dynamicStrides[dynStrideIdx]; + dynStrideIdx++; + if (staticSizes[i] == 1 && mlir::isZero(OpFoldResult(st))) + return true; + } + return false; +} + +LogicalResult ReinterpretCastStrideCanonicalizer::matchAndRewrite( + memref::ReinterpretCastOp op, PatternRewriter &rewriter) const { + if (!hasFixableZeroStride(op)) + return failure(); + + auto staticSizes = op.getStaticSizes(); + auto staticStrides = op.getStaticStrides(); + auto dynamicStrides = op.getStrides(); + + SmallVector newDynamicStrides; + newDynamicStrides.reserve(dynamicStrides.size()); + + unsigned dynStrideIdx = 0; + bool changed = false; + Value c1 = + rewriter.create(op.getLoc(), rewriter.getIndexAttr(1)); + + for (unsigned i = 0, e = staticStrides.size(); i < e; ++i) { + if (!ShapedType::isDynamic(staticStrides[i])) + continue; + + if (dynStrideIdx >= dynamicStrides.size()) + return failure(); + + Value oldStride = dynamicStrides[dynStrideIdx]; + dynStrideIdx++; + if (staticSizes[i] == 1 && mlir::isZero(OpFoldResult(oldStride))) { + newDynamicStrides.push_back(c1); + changed = true; + } else { + newDynamicStrides.push_back(oldStride); + } + } + + // all dynStride should be visited, and at least one should be changed + if (dynStrideIdx != dynamicStrides.size()) + return failure(); + if (!changed) + return failure(); + + auto newReinterpretCast = rewriter.create( + op.getLoc(), cast(op.getResult().getType()), op.getSource(), + op.getOffsets(), op.getSizes(), newDynamicStrides, op.getStaticOffsets(), + op.getStaticSizes(), op.getStaticStrides()); + + rewriter.replaceOp(op, newReinterpretCast.getResult()); + + return success(); +} + +} // namespace LoadStoreConverter diff --git a/third_party/ascend/lib/TritonToLinalg/MarkTensorKindPass.cpp b/third_party/ascend/lib/TritonToLinalg/MarkTensorKindPass.cpp new file mode 100644 index 0000000000..59858b80f2 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/MarkTensorKindPass.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/MarkTensorKindPass.h" +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" + +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "mark-tensor-kind" + +using namespace mlir; +using namespace triton; +const unsigned INT_BIT_WIDTH = 32; +const unsigned SET_INIT_SIZE = 16; + +template struct has_getPtr : std::false_type {}; +template +struct has_getPtr().getPtr())>> + : std::true_type {}; + +template struct has_getSrc : std::false_type {}; +template +struct has_getSrc().getSrc())>> + : std::true_type {}; + +template struct has_getDst : std::false_type {}; +template +struct has_getDst().getDst())>> + : std::true_type {}; + +template struct has_getBase : std::false_type {}; +template +struct has_getBase().getBase())>> + : std::true_type {}; + +template static Value extractPointer(OpTy op) { + if constexpr (has_getPtr::value) + return op.getPtr(); + else if constexpr (has_getDst::value) + return op.getDst(); + else if constexpr (has_getSrc::value) + return op.getSrc(); + else if constexpr (has_getBase::value) + return op.getBase(); + else { + Operation *raw = op.getOperation(); + if (!raw || raw->getNumOperands() == 0) + return Value(); + return raw->getOperand(0); + } +} + +static void setBlockArgumentAttr(BlockArgument blockArg, triton::FuncOp func, + TensorKind tensorKind) { + unsigned argIdx = blockArg.getArgNumber(); + auto existingAttr = + func.getArgAttrOfType(argIdx, "tt.tensor_kind"); + TensorKind oldVal = existingAttr + ? static_cast(existingAttr.getInt()) + : TensorKind::NONE; + + TensorKind finalVal = tensorKind; + if ((oldVal == TensorKind::INPUT && tensorKind == TensorKind::OUTPUT) || + (oldVal == TensorKind::OUTPUT && tensorKind == TensorKind::INPUT)) { + finalVal = TensorKind::INPUT_OUTPUT; + } else if (oldVal == TensorKind::INPUT_OUTPUT) { + finalVal = oldVal; + } + + LLVM_DEBUG(llvm::dbgs() << "Setting tensor_kind for argument " << argIdx + << ": " << finalVal << "\n";); + + func.setArgAttr( + argIdx, "tt.tensor_kind", + IntegerAttr::get(IntegerType::get(func.getContext(), INT_BIT_WIDTH), + static_cast(finalVal))); +} + +template +static void addTensorKindToArguments(OpTy op, TensorKind tensorKind) { + Value ptr = extractPointer(op); + if (!ptr) + return; + + LLVM_DEBUG(llvm::dbgs() << "Processing op: " << *op.getOperation() << "\n";); + + Value cur = ptr; + llvm::SmallPtrSet visited; + while (visited.insert(cur).second) { + if (auto blockArg = dyn_cast(cur)) { + if (auto func = dyn_cast_or_null( + blockArg.getOwner()->getParentOp())) { + if (blockArg.getOwner() == &func.getBody().front() && + isa(blockArg.getType())) { + setBlockArgumentAttr(blockArg, func, tensorKind); + break; + } + } + } + + Operation *defOp = cur.getDefiningOp(); + if (!defOp || defOp->getNumOperands() == 0) + break; + cur = defOp->getOperand(0); + } +} + +template +struct MarkTensorKindPattern : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(OpTy op, + PatternRewriter &rewriter) const override { + addTensorKindToArguments(op, Kind); + return success(); + } +}; + +void MarkTensorKindPass::runOnOperation() { + RewritePatternSet patterns(&getContext()); + + // INPUT tensors + patterns.add< + MarkTensorKindPattern, + MarkTensorKindPattern, + MarkTensorKindPattern, + MarkTensorKindPattern, + MarkTensorKindPattern>( + &getContext()); + + // OUTPUT tensors + patterns.add< + MarkTensorKindPattern, + MarkTensorKindPattern, + MarkTensorKindPattern, + MarkTensorKindPattern, + MarkTensorKindPattern>(&getContext()); + + // INPUT_OUTPUT tensors + patterns.add< + MarkTensorKindPattern, + MarkTensorKindPattern>( + &getContext()); + + (void)applyPatternsGreedily(getOperation(), std::move(patterns)); +} + +std::unique_ptr> triton::createMarkTensorKindPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToLinalg/TritonOpConverter.cpp b/third_party/ascend/lib/TritonToLinalg/TritonOpConverter.cpp new file mode 100644 index 0000000000..f1d7516e5b --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/TritonOpConverter.cpp @@ -0,0 +1,4435 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/TritonOpConverter.h" +#include "ascend/include/TritonToLinalg/BlockPtrAnalysis.h" +#include "ascend/include/TritonToLinalg/MaskAnalysis.h" +#include "ascend/include/TritonToLinalg/TritonToLinalgPass.h" +#include "ascend/include/Utils/DebugUtils.h" +#include "ascend/include/Utils/Utils.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/MemRef/Transforms/Passes.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/Interfaces/CallInterfaces.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HFusion/IR/HFusion.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" + +namespace TTOpConverters { +using namespace mlir; +using namespace triton; + +/** + * Retrieves a boolean environment variable. + * @param envVar The name of the environment variable. + * @param defaultValue The default value to return if the variable is not set + * or cannot be parsed. + * @return true if the environment variable exists and its value is parsed as + * "true", otherwise returns defaultValue. Parsing rules (case-insensitive): + * "true" values: any non-empty string not equal to "0", "false", "no", "off" + * is considered true. "false" values: an empty string or a string equal to + * any of the false literals is considered false. + */ +bool getEnvBool(const char *envVar, bool defaultValue) { + const char *val = std::getenv(envVar); + if (val == nullptr) { + return defaultValue; // variable not set + } + + std::string s(val); + // Convert to lowercase for easier comparison + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return std::tolower(c); }); + + // Common false literals + if (s.empty() || s == "0" || s == "false" || s == "no" || s == "off") { + return false; + } + // All other cases (including "1", "true", "yes", "on", etc.) are considered + // true + return true; +} + +static llvm::SmallString +generateUniqueFuncName(ModuleOp moduleOp, llvm::StringRef funcNameBase) { + llvm::SmallString funcName = funcNameBase; + int uniqueId = 0; + while (SymbolTable::lookupSymbolIn(moduleOp, funcName)) { + funcName = funcNameBase; + funcName += ("_" + std::to_string(uniqueId++)); + } + return funcName; +} + +// Indirect load/store conversion normally consumes the already-converted +// memref adaptor directly. That is correct for ordinary pointer bases, but it +// skips BlockDataParser::materializePointer for an int_to_ptr base and loses +// the required one-element identity view. Only scalar pointer-preserving +// producers are followed here; tensor-of-pointers and arbitrary pointer +// arithmetic retain their established conversion path. +static bool isIntToPtrBasedScalarPointer(Value value) { + auto pointerType = dyn_cast(value.getType()); + if (!pointerType || isa(pointerType.getPointeeType())) + return false; + + SmallPtrSet visited; + while (value && visited.insert(value).second) { + if (value.getDefiningOp()) + return true; + + Operation *producer = value.getDefiningOp(); + if (!producer) + return false; + if (auto addPtr = dyn_cast(producer)) { + value = addPtr.getPtr(); + continue; + } + if (auto bitcast = dyn_cast(producer)) { + value = bitcast.getSrc(); + continue; + } + return false; + } + return false; +} + +// A volatile indirect load whose scalar source is obtained through a Triton +// pointer bitcast needs the same one-element identity descriptor as the +// scalar-pointer path. Restrict this provenance check to scalar pointers and +// a pointer-preserving bitcast/addptr chain so ordinary dynamic memrefs and +// tensor-of-pointers keep their established ABI. +static bool isVolatileBitcastScalarPointer(Value value) { + auto pointerType = dyn_cast(value.getType()); + if (!pointerType || isa(pointerType.getPointeeType())) + return false; + + SmallPtrSet visited; + bool sawBitcast = false; + while (value && visited.insert(value).second) { + if (auto bitcast = value.getDefiningOp()) { + sawBitcast = true; + value = bitcast.getSrc(); + continue; + } + if (auto addPtr = value.getDefiningOp()) { + value = addPtr.getPtr(); + continue; + } + break; + } + return sawBitcast; +} + +LogicalResult +BitcastConverter::matchAndRewrite(triton::BitcastOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Value result; + auto loc = op.getLoc(); + + if (auto dstPtrTy = dyn_cast(op.getType())) { + auto srcPtrTy = cast(op.getSrc().getType()); + auto resType = + MemRefType::get({ShapedType::kDynamic}, dstPtrTy.getPointeeType()); + + auto i1Ty = rewriter.getIntegerType(1); + auto i8Ty = rewriter.getIntegerType(8); + bool isI1toI8 = (srcPtrTy.getPointeeType() == i1Ty) && + (dstPtrTy.getPointeeType() == i8Ty); + // handling special case: ptr -> ptr, directly forward without + // arith.bitcast + if (isI1toI8) { + // TypeConverter has already converted i1 to i8 memref, + LLVM_DEBUG({ + llvm::dbgs() + << "[BitcastConverter] Special i1->i8 pointer bitcast. Forward " + "without arith.bitcast. srcConvertedTy=" + << adaptor.getSrc().getType() << "\n"; + }); + rewriter.replaceOp(op, adaptor.getSrc()); + return success(); + } + result = rewriter.create(loc, resType, adaptor.getSrc()); + } else { + // handling normal case: bitcast between tensors/memrefs + result = + rewriter.create(loc, op.getType(), adaptor.getSrc()); + } + rewriter.replaceOp(op, result); + return success(); +} + +LogicalResult +TransposeConverter::matchAndRewrite(triton::TransOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + insertDebugNop(op.getLoc(), rewriter); + auto src = adaptor.getSrc(); + auto res = ConverterUtils::getTransposedValue(src, op.getLoc(), rewriter, + op.getOrder()); + rewriter.replaceOp(op, res); + return success(); +} + +bool hasScalarPointerResult(scf::IfOp op) { + bool hasPointerResult = false; + for (Type resultType : op.getResultTypes()) { + auto pointerType = dyn_cast(resultType); + if (!pointerType) + continue; + if (isa(pointerType.getPointeeType())) + return false; + hasPointerResult = true; + } + return hasPointerResult; +} + +bool isScalarPointerSelect(arith::SelectOp op) { + auto pointerType = dyn_cast(op.getType()); + return pointerType && !isa(pointerType.getPointeeType()); +} + +// Convert a scalar Triton pointer type to the canonical memref descriptor used +// by TritonTypeConverter. It is reconstructed only after a complete byte +// address has crossed control flow, so it needs no dynamic layout. +static FailureOr +getScalarPointerCarrierType(Type originalType, + const TypeConverter &typeConverter) { + auto pointerType = dyn_cast(originalType); + if (!pointerType || isa(pointerType.getPointeeType())) + return failure(); + + Type convertedType = typeConverter.convertType(originalType); + if (!convertedType) + return failure(); + auto memrefType = dyn_cast(convertedType); + if (!memrefType) + return failure(); + + return memrefType; +} + +static Type getIfResultCarrierType(Type originalType) { + if (isa(originalType)) + return IntegerType::get(originalType.getContext(), 64); + + // This converter exists only to carry scalar pointers as integer addresses. + // Sibling results already have valid SSA types and must stay unchanged; using + // the global type converter here would turn a tensor mask into a memref even + // though its existing users still consume the tensor value. + return originalType; +} + +// Materialize a no-op-compatible memref cast to the common carrier. Returning +// failure for non-memref or incompatible values prevents the pointer transport +// pattern from silently changing element, shape, rank, or memory-space types. +static FailureOr +castToMemRefCarrier(Value value, MemRefType carrierType, Location loc, + ConversionPatternRewriter &rewriter) { + if (value.getType() == carrierType) + return value; + + auto sourceType = dyn_cast(value.getType()); + if (!sourceType || + !memref::CastOp::areCastCompatible(sourceType, carrierType)) + return failure(); + + return rewriter.create(loc, carrierType, value).getResult(); +} + +// Dialect conversion may adapt a lane-local pointer descriptor such as +// `memref<1xT, strided<[1], offset: ?>>` to the canonical scalar-pointer +// carrier `memref`. Address materialization must inspect the original +// descriptor: its dynamic layout offset is part of the represented address and +// cannot be recovered from the shape-only carrier type. +// +// Only unwrap a one-to-one memref materialization that preserves rank, element +// type, and memory space. Other unrealized casts may represent a real element +// or address-space conversion and must remain visible to their converters. +static Value unwrapPointerDescriptorMaterialization(Value value) { + auto materialization = value.getDefiningOp(); + if (!materialization || materialization.getInputs().size() != 1 || + materialization.getOutputs().size() != 1) + return value; + + Value source = materialization.getInputs().front(); + auto sourceType = dyn_cast(source.getType()); + auto targetType = dyn_cast(value.getType()); + if (!sourceType || !targetType || + sourceType.getRank() != targetType.getRank() || + sourceType.getElementType() != targetType.getElementType() || + sourceType.getMemorySpace() != targetType.getMemorySpace()) + return value; + + return source; +} + +// Converts a memref descriptor into the complete byte address represented by +// that descriptor. extract_aligned_pointer_as_index yields the aligned buffer +// pointer; the descriptor's element offset must therefore be converted to +// bytes and added explicitly before the address crosses control flow. +static FailureOr +materializePointerAddress(Value value, Location loc, + ConversionPatternRewriter &rewriter) { + value = unwrapPointerDescriptorMaterialization(value); + auto memrefType = dyn_cast(value.getType()); + if (!memrefType) + return failure(); + Type elementType = memrefType.getElementType(); + if (!elementType.isIntOrFloat()) + return failure(); + + // A canonical scalar PointerCast already stores the complete byte address + // that IntToPtr received. Extracting its aligned pointer immediately and + // casting it back to i64 is an identity round trip. In current failing + // kernels this chain also survives into InjectSync, so folding it keeps a + // redundant region-local form out of the backend. Only fold the canonical + // one-dimensional carrier; descriptors with a non-zero offset or a non-unit + // stride still require the general address materialization below. + if (auto pointerCast = value.getDefiningOp(); + pointerCast && pointerCast->hasAttr(kScalarPointerCarrierAttr)) { + auto [strides, offset] = memrefType.getStridesAndOffset(); + if (pointerCast.getAddrs().size() == 1 && memrefType.getRank() == 1 && + offset == 0 && strides.size() == 1 && strides.front() == 1) { + Value address = pointerCast.getAddrs().front(); + if (address.getType().isInteger(64)) + return address; + if (address.getType().isIndex()) + return rewriter + .create(loc, rewriter.getI64Type(), address) + .getResult(); + } + } + + Value address = + rewriter.create(loc, value); + int64_t staticOffset = memrefType.getStridesAndOffset().second; + if (staticOffset != 0) { + Value elementOffset; + if (ShapedType::isDynamic(staticOffset)) { + elementOffset = + rewriter.create(loc, value) + .getOffset(); + } else { + elementOffset = + rewriter.create(loc, staticOffset); + } + + int64_t elementBytes = (elementType.getIntOrFloatBitWidth() + 7) / 8; + if (elementBytes != 1) { + Value scale = rewriter.create(loc, elementBytes); + elementOffset = rewriter.create(loc, elementOffset, scale); + } + address = rewriter.create(loc, address, elementOffset); + } + return rewriter + .create(loc, rewriter.getI64Type(), address) + .getResult(); +} + +LogicalResult +IfConverter::matchAndRewrite(scf::IfOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + const TypeConverter *typeConverter = getTypeConverter(); + if (!typeConverter) + return rewriter.notifyMatchFailure(op, "requires a type converter"); + if (!hasScalarPointerResult(op)) + return failure(); + if (!op->hasAttr(kScalarPointerCarrierBoundaryAttr)) + return rewriter.notifyMatchFailure( + op, "scalar-pointer if is missing its carrier boundary marker"); + + SmallVector convertedResultTypes; + convertedResultTypes.reserve(op.getNumResults()); + for (Type resultType : op.getResultTypes()) { + convertedResultTypes.push_back(getIfResultCarrierType(resultType)); + } + + auto newIfOp = rewriter.create(op.getLoc(), convertedResultTypes, + adaptor.getCondition(), + /*withElseRegion=*/true); + newIfOp->setAttrs(op->getAttrs()); + newIfOp->setAttr(kScalarPointerCarrierBoundaryAttr, + UnitAttr::get(rewriter.getContext())); + + // Move the original regions instead of cloning them. Besides preserving + // side effects, this keeps nested operations in the conversion driver's + // worklist so their operands are remapped normally. + rewriter.eraseBlock(newIfOp.thenBlock()); + rewriter.eraseBlock(newIfOp.elseBlock()); + rewriter.inlineRegionBefore(op.getThenRegion(), newIfOp.getThenRegion(), + newIfOp.getThenRegion().end()); + rewriter.inlineRegionBefore(op.getElseRegion(), newIfOp.getElseRegion(), + newIfOp.getElseRegion().end()); + + SmallVector replacementResults; + replacementResults.reserve(op.getNumResults()); + rewriter.setInsertionPointAfter(newIfOp); + for (auto [originalResultType, newResult] : + llvm::zip(op.getResultTypes(), newIfOp.getResults())) { + if (!isa(originalResultType)) { + replacementResults.push_back(newResult); + continue; + } + FailureOr resultType = + getScalarPointerCarrierType(originalResultType, *typeConverter); + if (failed(resultType)) + return rewriter.notifyMatchFailure( + op, "could not reconstruct a scalar pointer result"); + replacementResults.push_back( + createScalarPointerCast(rewriter, op.getLoc(), *resultType, newResult) + .getResult()); + } + rewriter.replaceOp(op, replacementResults); + return success(); +} + +LogicalResult PointerSelectConverter::matchAndRewrite( + arith::SelectOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + if (!isScalarPointerSelect(op)) + return failure(); + + const TypeConverter *typeConverter = getTypeConverter(); + if (!typeConverter) + return rewriter.notifyMatchFailure(op, "requires a type converter"); + + FailureOr resultType = + getScalarPointerCarrierType(op.getType(), *typeConverter); + if (failed(resultType)) + return rewriter.notifyMatchFailure( + op, "could not build a scalar pointer memref carrier"); + + FailureOr trueAddress = + materializePointerAddress(adaptor.getTrueValue(), op.getLoc(), rewriter); + FailureOr falseAddress = + materializePointerAddress(adaptor.getFalseValue(), op.getLoc(), rewriter); + if (failed(trueAddress) || failed(falseAddress)) + return rewriter.notifyMatchFailure( + op, "selected pointer values have no complete integer address"); + + auto selectedAddress = rewriter.create( + op.getLoc(), rewriter.getI64Type(), adaptor.getCondition(), *trueAddress, + *falseAddress); + selectedAddress->setAttrs(op->getAttrs()); + auto pointerCast = createScalarPointerCast(rewriter, op.getLoc(), *resultType, + selectedAddress.getResult()); + rewriter.replaceOp(op, pointerCast.getResult()); + return success(); +} + +LogicalResult +YieldConverter::matchAndRewrite(scf::YieldOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto parentIf = dyn_cast(op->getParentOp()); + if (!parentIf || !parentIf->hasAttr(kScalarPointerCarrierBoundaryAttr)) + return failure(); + + SmallVector convertedOperands(adaptor.getOperands()); + + if (parentIf.getNumResults() != convertedOperands.size()) + return rewriter.notifyMatchFailure(op, "yield/result arity does not match"); + + for (auto [index, targetType] : llvm::enumerate(parentIf.getResultTypes())) { + Value &operand = convertedOperands[index]; + if (operand.getType() == targetType) + continue; + + if (targetType.isInteger(64) && isa(operand.getType())) { + FailureOr address = + materializePointerAddress(operand, op.getLoc(), rewriter); + if (failed(address)) + return rewriter.notifyMatchFailure( + op, "could not materialize a yielded pointer address"); + operand = *address; + continue; + } + auto targetMemrefType = dyn_cast(targetType); + if (!targetMemrefType) + return rewriter.notifyMatchFailure( + op, "converted yield operand is incompatible with if result"); + + FailureOr casted = + castToMemRefCarrier(operand, targetMemrefType, op.getLoc(), rewriter); + if (failed(casted)) + return rewriter.notifyMatchFailure( + op, "converted yield operand is incompatible with if result"); + operand = *casted; + } + + rewriter.replaceOpWithNewOp(op, convertedOperands); + return success(); +} + +LogicalResult +AdvanceConverter::matchAndRewrite(triton::AdvanceOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + llvm::SmallDenseMap known; + return BlockDataParser::rewriteAdvanceOp(op, rewriter, known); +} + +// ToDo: +// 1. Refactor MakeTensorPtrConverter and AdvanceConverter with +// memref::ReinterpretCastOp and memref::SubViewOp. +// Use recast to describe full shape of tensor, and use subview to represent +// current block tensor. +LogicalResult MakeTensorPtrConverter::matchAndRewrite( + triton::MakeTensorPtrOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + llvm::SmallDenseMap known; + return BlockDataParser::rewriteMakeTensorPtrOp(op, adaptor.getBase(), + rewriter, known); +} + +LogicalResult PreciseDivConverter::matchAndRewrite( + triton::PreciseDivFOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Value opa = adaptor.getX(); + Value opb = adaptor.getY(); + auto loc = op.getLoc(); + + if (opa.getType() != opb.getType()) + return rewriter.notifyMatchFailure(op, "operands must have the same type"); + + // Let DivFOp infer its result type from converted operands. PreciseDivFOp + // is valid for both scalar and tensor floating-point values; casting the + // original result to RankedTensorType made scalar divisions produce a null + // result type during partial conversion. + auto divOp = rewriter.create(loc, opa, opb); + rewriter.replaceOp(op, divOp.getResult()); + return success(); +} + +LogicalResult +SelectCanonicalizer::matchAndRewrite(arith::SelectOp op, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + // 0. Shortcut for scalars and bool type + auto type = dyn_cast(op.getResult().getType()); + if (!type) { + // do nothing non-tensor select + return failure(); + } + auto elementType = type.getElementType(); + if (elementType.isInteger(1)) { + // do nothing with bool type + return failure(); + } + auto tensorShape = type.getShape(); + auto mask = op.getCondition(); + if (!isa(mask.getType())) { + // do nothing for scalar mask + return failure(); + } + + // 1. Check for continuous masked loads. + // Analyze the mask operand to determine at runtime the size of the data we + // are moving. + MaskState mstate; + auto isContMask = mstate.parse(mask, loc, rewriter); + + if (isContMask.failed()) { + mstate.eraseInsertedOps(op, rewriter); + return rewriter.notifyMatchFailure( + op, "Cannot lower continuous masked selects"); + } + + // 2. Get mask position + MaskPosition maskPos = mstate.getMaskPosition(tensorShape); + LLVM_DEBUG({ + llvm::dbgs() << "[SelectAnalysis] MaskPosition detected: " + << (maskPos == MaskPosition::Head ? "Head" + : maskPos == MaskPosition::Tail ? "Tail" + : maskPos == MaskPosition::Middle ? "Middle" + : "Unknown") + << "\n"; + }); + + if (maskPos == MaskPosition::Unknown) { + mstate.eraseInsertedOps(op, rewriter); + return failure(); + } + auto trueTensor = op.getTrueValue(); + auto falseTensor = op.getFalseValue(); + + // 3. Slice and insert out the masked part + if (maskPos == MaskPosition::Head) { + // Slice out the masked part of true tensor + auto extractSliceOp = mstate.getExtractSlice(trueTensor, loc, rewriter); + + // Insert out the sliced true tensor into false tensor + auto insertSliceOp = + mstate.getInsertSlice(extractSliceOp, falseTensor, loc, rewriter); + + LLVM_DEBUG({ + llvm::dbgs() << " -> Created ExtractSlice: " + << *extractSliceOp.getOperation() << "\n" + << " -> Created InsertSlice: " + << *insertSliceOp.getOperation() << "\n"; + }); + rewriter.replaceOp(op, insertSliceOp); + return success(); + } + + // For Tail or Middle positions, we need to compute inverted dimensions + // to handle the masking logic + SmallVector invertOffsets; + SmallVector invertFalseDims; + SmallVector invertTrueDims; + OpFoldResult falseDimOp; + OpFoldResult trueDimOp; + int valDim = -1; + for (int i = 0; i < mstate.getRank(); ++i) { + const auto &offVal = mstate.offsets[i]; + const auto &dimVal = mstate.dims[i]; + auto constOffVal = getConstantIntValue(offVal); + invertOffsets.push_back(rewriter.getIndexAttr(0)); + if (constOffVal.has_value() && constOffVal.value() == 0) { + invertFalseDims.push_back(dimVal); + invertTrueDims.push_back(dimVal); + } else { + assert(valDim == -1 && + "The offset in only one dimension can be not zero."); + if (!constOffVal.has_value()) { + valDim = i; + falseDimOp = offVal; + } + + invertFalseDims.push_back(offVal); + trueDimOp = addOpFoldResult(offVal, dimVal, loc, rewriter, + rewriter.getIndexType()); + invertTrueDims.push_back(trueDimOp); + } + } + + // Slice out the invert first masked part of false tensor + auto falseExtractSliceOp = mstate.getExtractSlice( + falseTensor, loc, rewriter, invertOffsets, invertFalseDims); + // Insert out the sliced false tensor into true tensor + auto trueInsertSliceOp = + mstate.getInsertSlice(falseExtractSliceOp, trueTensor, loc, rewriter, + invertOffsets, invertFalseDims); + // Slice out the invert first masked and masked part of inserted true tensor + auto extractSliceOp = mstate.getExtractSlice(trueInsertSliceOp, loc, rewriter, + invertOffsets, invertTrueDims); + // Insert out the sliced true tensor into false tensor + auto insertSliceOp = + mstate.getInsertSlice(extractSliceOp, falseTensor, loc, rewriter, + invertOffsets, invertTrueDims); + if (valDim != -1) { + rewriter.setInsertionPointAfter(trueInsertSliceOp); + assert(isa(falseDimOp) && + "Expected to be a runtime Value for dynamic dimension check."); + Value zeroIndex = rewriter.create(loc, 0); + Value isNegative = rewriter.create( + loc, arith::CmpIPredicate::slt, cast(falseDimOp), zeroIndex); + + Value sizeIndex = + rewriter.create(loc, tensorShape[valDim]); + Value isOutOfRange = rewriter.create( + loc, arith::CmpIPredicate::sge, cast(falseDimOp), sizeIndex); + auto orOp = rewriter.create(loc, isNegative, isOutOfRange); + auto ifOp = rewriter.create(loc, TypeRange{type}, + orOp.getResult(), true, true); + + Block *thenBlock = &ifOp.getThenRegion().front(); + rewriter.setInsertionPointToStart(thenBlock); + rewriter.create(loc, ValueRange{falseTensor}); + + Block *elseBlock = &ifOp.getElseRegion().front(); + rewriter.setInsertionPointToStart(elseBlock); + falseExtractSliceOp->moveBefore(elseBlock, elseBlock->begin()); + trueInsertSliceOp->moveAfter(falseExtractSliceOp); + extractSliceOp->moveAfter(trueInsertSliceOp); + insertSliceOp->moveAfter(extractSliceOp); + + rewriter.setInsertionPointAfter(insertSliceOp); + rewriter.create(loc, ValueRange{insertSliceOp.getResult()}); + rewriter.replaceOp(op, ifOp); + } else { // static offsets + rewriter.replaceOp(op, insertSliceOp); + } + LLVM_DEBUG({ + llvm::dbgs() << " -> [invert] Created false tensor extractSlice: " + << *falseExtractSliceOp.getOperation() << "\n" + << " -> [invert] Created true tensor insertSlice: " + << *trueInsertSliceOp.getOperation() << "\n" + << " -> [invert] Created ExtractSlice: " + << *extractSliceOp.getOperation() << "\n" + << " -> [invert] Created InsertSlice: " + << *insertSliceOp.getOperation() << "\n"; + }); + return success(); +} + +/* + * Move tt.bitcast to a previous location if tt.bitcast is not directly + * applied on function arguments + */ +LogicalResult +BitcastCanonicalizer::matchAndRewrite(triton::BitcastOp bitcastOp, + PatternRewriter &rewriter) const { + Value castSrc = bitcastOp.getSrc(); + Value castRes = bitcastOp.getResult(); + Type castSrcTy = castSrc.getType(); + Type castSrcPtrTy = isa(castSrcTy) + ? cast(castSrcTy).getElementType() + : castSrcTy; + if (!isa(castSrcPtrTy)) + return failure(); + + auto origBitwidth = getPointeeBitWidth(castSrc.getType()); + auto castBitwidth = getPointeeBitWidth(castRes.getType()); + + if (origBitwidth == 1) + origBitwidth = 8; + if (castBitwidth == 1) + castBitwidth = 8; + if (origBitwidth != castBitwidth) { + bitcastOp.emitError() << "Casting pointers with unmatched bitwidth!\n"; + return failure(); + } + + Operation *beforeCastOp = castSrc.getDefiningOp(); + if (beforeCastOp == nullptr) { + return failure(); + } + + auto newRes = + TypeSwitch>(beforeCastOp) + // before: addptr - bitcast - load/store + // after: bitcast - addptr - load/store + .Case([&](triton::AddPtrOp addptrOp) { + auto newCastOp = rewriter.create( + bitcastOp.getLoc(), castRes.getType(), addptrOp.getPtr()); + return rewriter.create( + bitcastOp.getLoc(), castRes.getType(), newCastOp.getResult(), + addptrOp.getOffset()); + }) + .Case([&](triton::SplatOp splatOp) { + Type newCastSrcTy = + cast(castRes.getType()).getElementType(); + + Value splatSrc = splatOp.getSrc(); + Type splatSrcTy = splatSrc.getType(); + if (auto splatSrcTensorTy = dyn_cast(splatSrcTy)) + newCastSrcTy = + splatSrcTensorTy.cloneWith(std::nullopt, newCastSrcTy); + auto newCastOp = rewriter.create( + bitcastOp.getLoc(), newCastSrcTy, splatSrc); + return rewriter.create( + bitcastOp.getLoc(), castRes.getType(), newCastOp); + }) + // before: bitcast - bitcast + // after(fusion optimization): bitcast + .Case([&](triton::BitcastOp prevCastOp) { + return rewriter.create( + bitcastOp.getLoc(), castRes.getType(), prevCastOp.getSrc()); + }) + .Default([&](Operation *op) { + return rewriter.notifyMatchFailure(bitcastOp, + "Unknown bitcast pattern"); + }); + if (succeeded(newRes)) { + // Preserve debug location in case beforeCastOp is erased below. + insertDebugNop(beforeCastOp->getLoc(), rewriter); + rewriter.replaceOp(bitcastOp, newRes.value()); + if (beforeCastOp->use_empty()) { + rewriter.eraseOp(beforeCastOp); + } + return success(); + } + return failure(); +} + +LogicalResult +FpToFpCanonicalizer::matchAndRewrite(triton::FpToFpOp op, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + Value input = op.getSrc(); + auto resultType = op.getResult().getType(); + + // Check if rounding mode is specified + auto roundingMode = op.getRounding(); + if (roundingMode.has_value() && + roundingMode.value() != triton::RoundingMode::RTNE) { + // Non-RTNE rounding modes (e.g., RTZ) should be handled by + // TritonToHFusion pass Return failure here so this pattern doesn't match + return failure(); + } + + // Handle RTNE (default) rounding mode with arith.truncf/extf. This can be + // either a scalar conversion or a ranked tensor conversion. + auto srcElemType = getElementTypeOrSelf(input.getType()); + auto dstElemType = getElementTypeOrSelf(resultType); + if (!isa(srcElemType) || !isa(dstElemType)) { + return op.emitError("FpToFp expects floating point types"); + } + + unsigned srcBitwidth = srcElemType.getIntOrFloatBitWidth(); + unsigned dstBitwidth = dstElemType.getIntOrFloatBitWidth(); + + // Create round_mode attribute (RINT for RTNE) + auto roundModeAttr = hfusion::RoundModeAttr::get(rewriter.getContext(), + hfusion::RoundMode::RINT); + + // A no-op conversion is valid only when the complete MLIR type is identical. + // Equal element bitwidth alone is insufficient: FP8 formats such as + // f8E4M3FN and f8E5M2 have different numerical semantics. + if (input.getType() == resultType) { + rewriter.replaceOp(op, input); + return success(); + } + + if (srcBitwidth == dstBitwidth) { + if (srcElemType == dstElemType) { + return op.emitError( + "fp_to_fp with identical element types has incompatible " + "container or layout types"); + } + + // arith.extf/truncf require a strict bitwidth change. Materialize an f32 + // intermediate so the conversion remains a numerical cast in TTAdapter + // IR and can follow the normal Bisheng lowering path. + Type f32Type; + if (auto tensorType = dyn_cast(input.getType())) { + f32Type = + RankedTensorType::get(tensorType.getShape(), rewriter.getF32Type(), + tensorType.getEncoding()); + } else if (isa(input.getType())) { + f32Type = rewriter.getF32Type(); + } else { + return op.emitError("FpToFp expects a scalar or ranked tensor type"); + } + + auto extOp = rewriter.create(loc, f32Type, input); + extOp->setAttr("round_mode", roundModeAttr); + auto truncOp = + rewriter.create(loc, resultType, extOp.getResult()); + truncOp->setAttr("round_mode", roundModeAttr); + rewriter.replaceOp(op, truncOp.getResult()); + } else if (srcBitwidth > dstBitwidth) { + // Downcast: use arith.truncf with round_mode=rint + auto truncOp = rewriter.create(loc, resultType, input); + truncOp->setAttr("round_mode", roundModeAttr); + rewriter.replaceOp(op, truncOp.getResult()); + } else { + // Upcast: use arith.extf with round_mode=rint + auto extOp = rewriter.create(loc, resultType, input); + extOp->setAttr("round_mode", roundModeAttr); + rewriter.replaceOp(op, extOp.getResult()); + } + + return success(); +} + +void rewriteUserWithNewOrder( + mlir::OpOperand *use, PatternRewriter &rewriter, + llvm::SmallVector &blkShapeI64, // 8: container size + mlir::Location &loc, llvm::ArrayRef &order, size_t &orderSize) { + Operation *user = use->getOwner(); + rewriter.setInsertionPointAfter(user); + if (auto loadOp = dyn_cast(user)) { + auto loadResTy = loadOp.getResult().getType(); + auto loadResShapedTy = cast(loadResTy); + auto newLoadTy = loadResShapedTy.cloneWith( + blkShapeI64, loadResShapedTy.getElementType()); + auto newLoadOp = rewriter.create( + loc, newLoadTy, loadOp->getOperands(), loadOp->getAttrs()); + newLoadOp->setAttr(ConverterUtils::GeneratedByMakeTensorPtrTAG, + UnitAttr::get(rewriter.getContext())); + rewriter.replaceOp(loadOp, newLoadOp); + // load contiguous data then permute. thus the permute order is as + // follows. + SmallVector permuteOrder; // 8: container size + for (auto [i, v] : llvm::enumerate(order)) { + permuteOrder.push_back(orderSize - 1 - order[i]); + } + auto permuteOp = rewriter.create( + loc, newLoadOp.getResult(), + DenseI32ArrayAttr::get(loadOp.getContext(), permuteOrder)); + newLoadOp.getResult().replaceAllUsesExcept(permuteOp.getResult(), + permuteOp); + } else if (auto storeOp = dyn_cast(user)) { + // permute to contiguous then store. thus the permute order is as follows. + SmallVector permuteOrder; // 8: container size + for (auto [i, v] : llvm::enumerate(order)) { + permuteOrder.push_back(order[orderSize - 1 - i]); + } + auto permuteOp = rewriter.create( + loc, storeOp.getValue(), + DenseI32ArrayAttr::get(storeOp.getContext(), permuteOrder)); + storeOp.getValue().replaceAllUsesExcept(permuteOp.getResult(), permuteOp); + auto newStoreOp = rewriter.create( + loc, storeOp.getPtr(), storeOp.getValue(), storeOp.getMask(), + storeOp.getBoundaryCheck(), storeOp.getCache(), storeOp.getEvict()); + rewriter.replaceOp(storeOp, newStoreOp); + } else if (auto advanceOp = dyn_cast(user)) { + auto advanceResPtrTy = + cast(advanceOp.getResult().getType()); + auto advanceResShapedTy = + cast(advanceResPtrTy.getPointeeType()); + auto newAdvanceResShapedTy = advanceResShapedTy.cloneWith( + blkShapeI64, advanceResShapedTy.getElementType()); + auto newAdvanceResPtrTy = triton::PointerType::get( + newAdvanceResShapedTy, advanceResPtrTy.getAddressSpace()); + auto advanceOffsets = advanceOp.getOffsets(); + llvm::SmallVector newAdvanceOffsets; // 8: container size + for (int i = orderSize - 1; i >= 0; i--) { + newAdvanceOffsets.push_back(advanceOffsets[order[i]]); + } + SmallVector resUses; + for (auto &use : advanceOp->getUses()) + resUses.push_back(&use); + auto newAdvanceOp = rewriter.create( + loc, newAdvanceResPtrTy, advanceOp.getPtr(), newAdvanceOffsets); + rewriter.replaceOp(advanceOp, newAdvanceOp); + for (auto resUse : resUses) + rewriteUserWithNewOrder(resUse, rewriter, blkShapeI64, loc, order, + orderSize); + } else if (auto loopOp = dyn_cast(user)) { + auto initArg = use->get(); + auto iterArg = loopOp.getTiedLoopRegionIterArg(use); + auto resultValue = loopOp.getTiedLoopResult(use); + iterArg.setType(initArg.getType()); + resultValue.setType(initArg.getType()); + for (auto &argUse : iterArg.getUses()) + rewriteUserWithNewOrder(&argUse, rewriter, blkShapeI64, loc, order, + orderSize); + for (auto &resUse : resultValue.getUses()) + rewriteUserWithNewOrder(&resUse, rewriter, blkShapeI64, loc, order, + orderSize); + } else if (isa(user)) { + return; + } else { + llvm_unreachable( + "[MakeTensorPtrCanonicalizer] tt.make_tensor_ptr's result is " + "not used by load/store/advance op"); + } +} + +void markLoadUsers(mlir::OpOperand *use, PatternRewriter &rewriter) { + Operation *user = use->getOwner(); + if (auto loadOp = dyn_cast(user)) { + loadOp->setAttr(ConverterUtils::GeneratedByMakeTensorPtrTAG, + UnitAttr::get(rewriter.getContext())); + } else if (auto storeOp = dyn_cast(user)) { + return; + } else if (auto advanceOp = dyn_cast(user)) { + SmallVector resUses; + for (auto &use : advanceOp->getUses()) + resUses.push_back(&use); + for (auto resUse : resUses) + markLoadUsers(resUse, rewriter); + } else if (auto loopOp = dyn_cast(user)) { + auto initArg = use->get(); + auto iterArg = loopOp.getTiedLoopRegionIterArg(use); + auto resultValue = loopOp.getTiedLoopResult(use); + iterArg.setType(initArg.getType()); + resultValue.setType(initArg.getType()); + for (auto &argUse : iterArg.getUses()) + markLoadUsers(&argUse, rewriter); + for (auto &resUse : resultValue.getUses()) + markLoadUsers(&resUse, rewriter); + } else if (isa(user)) { + return; + } else { + llvm_unreachable( + "[MakeTensorPtrCanonicalizer] tt.make_tensor_ptr's result is " + "not used by load/store/advance op"); + } +} + +LogicalResult +MakeTensorPtrCanonicalizer::matchAndRewrite(triton::MakeTensorPtrOp op, + PatternRewriter &rewriter) const { + auto order = op.getOrder(); + auto orderSize = order.size(); + if (orderSize == 1) { + return rewriter.notifyMatchFailure( + op, "make_tensor_ptr's order has single value."); + } + + bool isPermuted = false; + for (auto [first, second] : llvm::zip(order.slice(0, orderSize - 1), + order.slice(1, orderSize - 1))) { + if (first != second + 1) { + isPermuted = true; + break; + } + } + + auto loc = op.getLoc(); + auto base = op.getBase(); + auto shape = op.getShape(); + auto strides = op.getStrides(); + auto offsets = op.getOffsets(); + auto result = op.getResult(); + SmallVector opUses; + + for (auto &use : result.getUses()) + opUses.push_back(&use); + for (auto use : opUses) + markLoadUsers(use, rewriter); + + if (!isPermuted) { + return rewriter.notifyMatchFailure( + op, "make_tensor_ptr's order is contiguous."); + } + + llvm::SmallVector blkShapeI32; + llvm::SmallVector blkShapeI64; + auto resPtrType = cast(result.getType()); + if (auto resShapedTy = dyn_cast(resPtrType.getPointeeType())) { + auto resBlkShape = resShapedTy.getShape(); + for (auto [i, v] : llvm::enumerate(resBlkShape)) { + auto reverseI = orderSize - 1 - i; + blkShapeI32.push_back(resBlkShape[order[reverseI]]); + blkShapeI64.push_back(resBlkShape[order[reverseI]]); + } + } + + llvm::SmallVector newShape; + llvm::SmallVector newStrides; + llvm::SmallVector newOffsets; + for (int i = orderSize - 1; i >= 0; i--) { + newShape.push_back(shape[order[i]]); + newStrides.push_back(strides[order[i]]); + newOffsets.push_back(offsets[order[i]]); + } + + llvm::SmallVector contiguousOrder; + for (int i = orderSize - 1; i >= 0; i--) + contiguousOrder.push_back(i); + + rewriter.setInsertionPoint(op); + auto newMakeTensorPtrOp = rewriter.create( + loc, base, ValueRange(newShape), ValueRange(newStrides), + ValueRange(newOffsets), blkShapeI32, contiguousOrder); + rewriter.replaceOp(op, newMakeTensorPtrOp); + for (auto use : opUses) + rewriteUserWithNewOrder(use, rewriter, blkShapeI64, loc, order, orderSize); + return success(); +} + +LogicalResult +ReduceSingleCanonicalizer::matchAndRewrite(triton::ReduceOp reduceOp, + PatternRewriter &rewriter) const { + // This canonicalization only handles value reductions and value/index + // reductions. Multi-input reductions, such as Welford's + // (mean, count, m2) reduction, must fall through to ReduceConverter's + // extended lowering instead of terminating the compiler here. + if (reduceOp.getSrcs().size() > 2) + return rewriter.notifyMatchFailure( + reduceOp, "only canonicalizes value and value/index reductions"); + auto src = reduceOp.getSrcs()[0]; + auto srcType = cast(src.getType()); + auto srcShape = srcType.getShape(); + if (llvm::any_of(srcShape, [](auto s) { return s != 1; })) + return rewriter.notifyMatchFailure( + reduceOp, "reduce's srcs are not all with single element"); + auto loc = reduceOp->getLoc(); + + // Handle Reduce Value + auto res = reduceOp.getResult()[0]; + Value extracted; + if (srcType.getRank() == 1) { + auto zero = + rewriter.create(loc, rewriter.getIndexAttr(0)); + extracted = rewriter.create(loc, src, zero.getResult()) + .getResult(); + } else { + auto resShape = cast(res.getType()).getShape(); + auto collapseReassociationIndicesOptional = + getReassociationIndicesForCollapse(srcShape, resShape); + if (!collapseReassociationIndicesOptional.has_value()) { + return rewriter.notifyMatchFailure( + reduceOp, "Failure with getReassociationIndicesForCollapse call"); + } + auto collapseReassociationIndices = + collapseReassociationIndicesOptional.value(); + extracted = rewriter + .create( + loc, src, collapseReassociationIndices) + .getResult(); + } + res.replaceAllUsesWith(extracted); + + // Handle Reduce Index + if (reduceOp.getSrcs().size() == 1) + return success(); + + auto resIdx = reduceOp.getResult()[1]; + auto zeroI32 = + rewriter.create(loc, rewriter.getI32IntegerAttr(0)); + if (srcType.getRank() == 1) { + resIdx.replaceAllUsesWith(zeroI32); + } else { + auto resIdxShape = cast(resIdx.getType()).getShape(); + auto initTensor = rewriter.create(loc, resIdxShape, + rewriter.getI32Type()); + auto fillOp = rewriter.create(loc, ValueRange{zeroI32}, + ValueRange{initTensor}); + resIdx.replaceAllUsesWith(fillOp.getResult(0)); + } + + return success(); +} + +LogicalResult DenseConstantConverter::matchAndRewrite( + arith::ConstantOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto denseAttr = cast(op.getValue()); + auto loc = op.getLoc(); + auto constSplatOp = arith::ConstantOp::materialize( + rewriter, denseAttr.getSplatValue(), + denseAttr.getElementType(), loc); + auto emptyOp = rewriter.create( + loc, cast(op.getResult().getType()).getShape(), + denseAttr.getElementType()); + + rewriter.replaceOpWithNewOp(op, ValueRange{constSplatOp}, + ValueRange{emptyOp}); + + return success(); +} + +LogicalResult +MakeRangeConverter::matchAndRewrite(triton::MakeRangeOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + insertDebugNop(loc, rewriter); + auto type = cast(op.getResult().getType()); + auto shape = type.getShape(); + auto elementType = type.getElementType(); + auto context = op.getContext(); + + assert(type.getShape().size() == 1 && + isa(type.getElementType()) && + type.getElementType().getIntOrFloatBitWidth() == 32 && + "make range can only return 1D int32 tensor"); + + SmallVector indexingMaps{AffineMap::get( + /* dimCount */ 1, /* symbolCount */ 0, + {mlir::getAffineDimExpr(0, context)}, context)}; + + auto init = rewriter.create(loc, shape, elementType); + + auto nestedBody = [&](OpBuilder &nestedBuilder, Location nestedLoc, + ValueRange blockArgs) { + Value index = nestedBuilder.create(loc, 0); + Value res = + nestedBuilder.create(loc, elementType, index); + nestedBuilder.create(loc, res); + }; + + auto linalgOp = rewriter.create( + loc, op->getResultTypes(), /* operands */ ValueRange{}, ValueRange{init}, + indexingMaps, ConverterUtils::getNParallelLoopsAttrs(1), nestedBody); + + linalgOp->setAttr("tt.from_make_range", mlir::UnitAttr::get(context)); + linalgOp->setAttr("tt.make_range_offset", + mlir::IntegerAttr::get(mlir::IndexType::get(context), 0)); + linalgOp->setAttr( + "tt.make_range_size", + mlir::IntegerAttr::get(mlir::IndexType::get(context), shape[0])); + + int32_t startVal = op.getStartAttr().getInt(); + if (startVal == 0) { + rewriter.replaceOp(op, linalgOp->getResults()); + return success(); + } + + // Apply start offset + Value startScaler = rewriter.create( + loc, rewriter.getI32IntegerAttr(static_cast(startVal))); + auto startInit = rewriter.create(loc, shape, elementType); + Value startTensor = rewriter + .create(loc, ValueRange{startScaler}, + ValueRange{startInit}) + .getResult(0); + auto addOp = + rewriter.create(loc, linalgOp->getResult(0), startTensor); + rewriter.replaceOp(op, addOp); + return success(); +} + +LogicalResult +SplatConverter::matchAndRewrite(triton::SplatOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + // These NOPs are transient debug anchors: DeduplicateDebugNopsPass collapses + // them to one per source line before codegen. + insertDebugNopForAllLines(loc, rewriter); + auto shape = op.getType().getShape(); + auto init = rewriter.create(loc, shape, + op.getType().getElementType()); + if (llvm::all_of(shape, [](int64_t dim) { return dim == 1; })) { + SmallVector idx(shape.size(), rewriter.create( + loc, rewriter.getIndexAttr(0))); + rewriter.replaceOpWithNewOp(op, adaptor.getSrc(), init, + idx); + } else { + rewriter.replaceOpWithNewOp( + op, ValueRange{adaptor.getSrc()}, ValueRange{init}); + } + return success(); +} + +LogicalResult +UnsplatConverter::matchAndRewrite(triton::UnsplatOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto src = adaptor.getSrc(); + auto srcType = cast(src.getType()); + auto shape = srcType.getShape(); + + // Create index constants for all dimensions (all zeros since we're + // extracting the single element) + SmallVector indices; + for (int64_t dim : shape) { + indices.push_back( + rewriter.create(loc, rewriter.getIndexAttr(0))); + } + + // Extract the scalar element from the tensor + auto elementType = srcType.getElementType(); + auto extractOp = + rewriter.create(loc, elementType, src, indices); + rewriter.replaceOp(op, extractOp.getResult()); + return success(); +} + +LogicalResult +ReshapeConverter::matchAndRewrite(triton::ReshapeOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + insertDebugNop(loc, rewriter); + auto src = op.getSrc(); + auto dst = op.getResult(); + Value shape = rewriter.create( + loc, + rewriter.getI64TensorAttr(cast(dst.getType()).getShape())); + auto reshapeOp = + rewriter.create(loc, dst.getType(), src, shape); + rewriter.replaceOp(op, reshapeOp.getResult()); + return success(); +} + +LogicalResult ExpandDimsConverter::matchAndRewrite( + triton::ExpandDimsOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + insertDebugNop(loc, rewriter); + auto src = op.getSrc(); + auto resShape = cast(op.getResult().getType()).getShape(); + auto axis = op.getAxis(); + + SmallVector reassociation; + + auto src_last_dim = resShape.size() - 2; + auto map_func = [&](unsigned i) -> ReassociationIndices { + if (i < axis) { + return i == src_last_dim ? ReassociationIndices{i, i + 1} + : ReassociationIndices{i}; + } + return i == axis ? ReassociationIndices{i, i + 1} + : ReassociationIndices{i + 1}; + }; + + reassociation = llvm::to_vector( + llvm::map_range(llvm::seq(0, src_last_dim + 1), map_func)); + + auto expandShapeOp = rewriter.create( + op.getLoc(), op.getResult().getType(), src, reassociation); + rewriter.replaceOp(op, expandShapeOp.getResult()); + return success(); +} + +LogicalResult +ClampFConverter::matchAndRewrite(triton::ClampFOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto input = adaptor.getX(); + auto min_para = adaptor.getMin(); + auto max_para = adaptor.getMax(); + auto propagateNan_para = adaptor.getPropagateNan(); + + if (auto input_type = dyn_cast(input.getType())) { + if (isa(min_para.getType())) { + auto minEmptyTensor = rewriter.create( + loc, input_type.getShape(), input_type.getElementType()); + min_para = rewriter + .create(loc, ValueRange{min_para}, + ValueRange{minEmptyTensor}) + .result(); + } + if (isa(max_para.getType())) { + auto maxEmptyTensor = rewriter.create( + loc, input_type.getShape(), input_type.getElementType()); + max_para = rewriter + .create(loc, ValueRange{max_para}, + ValueRange{maxEmptyTensor}) + .result(); + } + } + + if (propagateNan_para == PropagateNan::NONE) { + auto minOp = rewriter.create(loc, input, max_para); + auto maxOp = rewriter.create(loc, min_para, minOp); + rewriter.replaceOp(op, ValueRange{maxOp}); + } else if (propagateNan_para == PropagateNan::ALL) { + auto minOp = rewriter.create(loc, input, max_para); + auto maxOp = rewriter.create(loc, min_para, minOp); + rewriter.replaceOp(op, ValueRange{maxOp}); + } else { + return failure(); + } + + return success(); +} + +// Here convert tt.broadcast to linalg.broadcast +// +// before +// %out = tt.broadcast %in : tensor<1x4x8xf32> -> tensor<128x4x8xf32> +// +// after +// %collpased = tensor.collapse_shape %in [[0, 1], [2]] : +// tensor<1x4x8xf32> into tensor<4x8xf32> +// %out = linalg.broadcast ins(%collpased : tensor<4x8xf32>) +// outs(%empty : tensor<128x4x8xf32>) dimensions = [0] +LogicalResult +BroadcastConverter::matchAndRewrite(triton::BroadcastOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + assert(op->getNumResults() == 1 && "BroadcastOp assumes single result"); + + RankedTensorType sourceType = + cast(adaptor.getSrc().getType()); + RankedTensorType resultType = cast(op.getType()); + auto elementType = resultType.getElementType(); + auto loc = op.getLoc(); + insertDebugNopForAllLines(loc, rewriter); + auto initEmpty = + rewriter.create(loc, resultType.getShape(), elementType); + + SmallVector broadcastDims = + ConverterUtils::getBroadcastDims(sourceType, resultType); + SmallVector unbroadcastDims = + ConverterUtils::getUnbroadcastDims(sourceType, resultType); + + SmallVector collapseReassociationIndices; + auto collapseReassociationIndicesOptional = + getReassociationIndicesForCollapse(sourceType.getShape(), + unbroadcastDims); + if (!collapseReassociationIndicesOptional.has_value()) { + return rewriter.notifyMatchFailure( + op, "Failure with getReassociationIndicesForCollapse call"); + } + collapseReassociationIndices = collapseReassociationIndicesOptional.value(); + + RankedTensorType collapseResultType = + RankedTensorType::get(unbroadcastDims, sourceType.getElementType()); + + auto collpasedOp = rewriter.create( + loc, collapseResultType, adaptor.getSrc(), collapseReassociationIndices); + + auto broadcastOp = rewriter.create( + loc, collpasedOp, initEmpty, + rewriter.getDenseI64ArrayAttr(broadcastDims)); + + rewriter.replaceOp(op, broadcastOp.getResults()); + return success(); +} + +// Reduce Converter +bool ReduceConverter::isReductionOpSupported(Operation *redOp) const { + return isa(redOp); +} + +bool ReduceConverter::isMultiReductionOpSupported(Operation *redOp) { + return isa( + redOp); +} + +Value ReduceConverter::cloneReduceOps(OpBuilder &builder, Value in, Value out, + Value opIns, Value opOuts, + triton::ReduceOp op) const { + auto ® = op->getRegion(0); + assert(reg.getBlocks().size() == 1); + auto &body = reg.getBlocks().front(); + auto numArguments = 2; + assert(body.getNumArguments() == numArguments); + + Value ttIn = body.getArgument(0); + Value ttOut = body.getArgument(1); + + IRMapping mapping; + mapping.map(ttIn, in); + mapping.map(ttOut, out); + + for (auto &op : body.without_terminator()) { + builder.clone(op, mapping); + } + auto yield = cast(body.getTerminator()); + return mapping.lookup(yield->getOperand(0)); +} + +void ReduceConverter::checkIsNotCallOp( + const llvm::SmallVector &reductionOps) const { + llvm::for_each(reductionOps, [](Operation *op) { + assert(!isa(op) && "tt.call ops expected to be inlined in " + "tt.reduce body in ttir building stage"); + }); +} + +bool ReduceConverter::isSCFOpReduce( + const llvm::SmallVector &reductionOps) const { + return (reductionOps.size() == 1 && + reductionOps.front()->getDialect()->getNamespace() == + scf::SCFDialect::getDialectNamespace()); +} + +bool ReduceConverter::isMultiOpReduce( + const llvm::SmallVector &reductionOps) const { + this->checkIsNotCallOp(reductionOps); + + return (reductionOps.size() > 1) || + (reductionOps.size() == 1 && + this->isMultiReductionOpSupported(reductionOps.front())) || + this->isSCFOpReduce(reductionOps); +} + +Value ReduceConverter::computeReduceResultWithCompileFlag( + OpBuilder &opBuilder, Location loc, Value lhs, Value rhs, Value source, + Value initTensor, triton::ReduceOp op, bool compileOn91095Flag) const { + // Original operation list (including all operations) + auto originalReductionOps = this->getReductionOps(op); + auto realReductionOps = this->getRealReductionOps(op); + + // If the size of the original operation list is greater than 1, + // there are additional operations such as type conversion, and these + // operations must be cloned. + bool needClone = compileOn91095Flag || originalReductionOps.size() > 1; + if (needClone) { + return this->cloneReduceOps(opBuilder, lhs, rhs, source, initTensor, op); + } else { + assert(realReductionOps.size() == 1); + auto rop = realReductionOps.front(); + return this->getReductionElement(lhs, rhs, loc, rop, opBuilder, false); + } +} + +LogicalResult +ReduceConverter::convertToTargetOp(triton::ReduceOp op, + typename triton::ReduceOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto source = adaptor.getOperands().front(); + auto sourceType = cast(source.getType()); + auto elemType = sourceType.getElementType(); + auto resType = op.getResult().front().getType(); + auto loc = op.getLoc(); + + // Actual operation list (filtering type conversion operations, leaving only + // actual reduce operations) + auto realReductionOps = this->getRealReductionOps(op); + + bool multiOpReduce = this->isMultiOpReduce(realReductionOps); + // Reduction of arbitrary operations isn't supported because using the first + // element across the reduction dimension requires us to iterate over a + // subview that skips over each first element. + if (!multiOpReduce && + !this->isReductionOpSupported(realReductionOps.front())) { + if (compileOn91095Flag) { + llvm_unreachable("All reduction cases expected to be covered"); + } + return rewriter.notifyMatchFailure( + op, "Only support lowering reduction with single op and limited types " + "of reduction"); + } + + auto rop = realReductionOps.front(); + auto ropLoc = rop->getLoc(); + auto axis = op.getAxis(); + auto isVectorReduce = sourceType.getRank() == 1; + + auto constantType = elemType; + + auto accBaseConstOp = + multiOpReduce + ? this->getMultiOpReductionBaseConstOp(rewriter, op, ropLoc, + constantType) + : this->getReductionBaseConstOp(rewriter, rop, constantType); + + Value initTensor; + if (isVectorReduce) { + auto holder = rewriter.create( + loc, RankedTensorType::get({}, constantType), ValueRange{}); + initTensor = rewriter + .create(loc, accBaseConstOp.getResult(), + holder.getResult()) + .getResult(0); + } else { + Value init = rewriter.create( + loc, cast(resType).getShape(), constantType); + initTensor = + rewriter.create(loc, accBaseConstOp.getResult(), init) + .getResult(0); + } + + Value finalResult = + rewriter + .create( + loc, ValueRange{source}, ValueRange{initTensor}, + SmallVector{axis}, + [&](OpBuilder &opBuilder, Location loc, ValueRange inputs) { + assert(inputs.size() == 2); + Value result = this->computeReduceResultWithCompileFlag( + opBuilder, loc, inputs[0], inputs[1], source, initTensor, + op, compileOn91095Flag); + opBuilder.create(loc, result); + }) + .getResult(0); + + if (sourceType.getRank() == 1) { + finalResult = + rewriter.create(loc, constantType, finalResult); + } + insertDebugNop(loc, rewriter); + rewriter.replaceOp(op, finalResult); + return success(); +} + +LogicalResult ReduceConverter::convertToTargetOpExtended( + triton::ReduceOp op, typename triton::ReduceOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto operands = adaptor.getOperands(); + + // BiShengIR's VReduce lowering only supports a single value input (or a + // value/index pair). A multi-input `tt.reduce`, such as Welford's + // (mean, count, m2) reduction, cannot be represented by that VReduceOp. + // Keep the current reduction body, but lower the static rank-1 scalar case + // to an explicit scalar loop so it never reaches the variadic VReduce path. + if (operands.size() > 2) { + auto inputType = dyn_cast(operands.front().getType()); + if (!inputType || inputType.getRank() != 1 || adaptor.getAxis() != 0 || + ShapedType::isDynamic(inputType.getShape()[0]) || + inputType.getShape()[0] < 1) { + return rewriter.notifyMatchFailure( + op, "multi-input reduce fallback requires static rank-1 axis-0 " + "inputs"); + } + if (op.getResult().size() != operands.size()) { + return rewriter.notifyMatchFailure( + op, "multi-input reduce results do not match input count"); + } + + for (auto [i, operand] : llvm::enumerate(operands)) { + auto operandType = dyn_cast(operand.getType()); + if (!operandType || operandType.getShape() != inputType.getShape() || + op.getResult()[i].getType() != operandType.getElementType()) { + return rewriter.notifyMatchFailure( + op, "multi-input reduce fallback requires matching scalar " + "results"); + } + } + + auto reduceBlock = op.getBody(); + if (reduceBlock->getNumArguments() != 2 * operands.size() || + reduceBlock->getTerminator()->getNumOperands() != operands.size()) { + return rewriter.notifyMatchFailure( + op, "unexpected multi-input reduce combine region"); + } + + Value zero = rewriter.create(loc, 0); + Value one = rewriter.create(loc, 1); + Value upper = + rewriter.create(loc, inputType.getShape()[0]); + SmallVector initialValues; + initialValues.reserve(operands.size()); + for (Value operand : operands) + initialValues.push_back( + rewriter.create(loc, operand, zero)); + + auto loop = rewriter.create( + loc, one, upper, one, initialValues, + [&](OpBuilder &builder, Location loopLoc, Value inductionVar, + ValueRange iterArgs) { + IRMapping mapping; + for (auto [i, operand] : llvm::enumerate(operands)) { + Value current = builder.create(loopLoc, operand, + inductionVar); + mapping.map(reduceBlock->getArgument(i), current); + mapping.map(reduceBlock->getArgument(i + operands.size()), + iterArgs[i]); + } + for (Operation &innerOp : reduceBlock->without_terminator()) + builder.clone(innerOp, mapping); + + SmallVector yielded; + yielded.reserve(operands.size()); + for (Value value : reduceBlock->getTerminator()->getOperands()) + yielded.push_back(mapping.lookup(value)); + builder.create(loopLoc, yielded); + }); + rewriter.replaceOp(op, loop.getResults()); + return success(); + } + + auto elemTypes = op.getElementTypes(); + + auto valueResultType = dyn_cast(op.getType(0)); + const auto isScalarReduce = valueResultType == nullptr; + + SmallVector outputs; + for (auto i = 0; i < op.getResult().size() && i < elemTypes.size(); i++) { + auto result = dyn_cast(op.getType(i)); + SmallVector resultShape{ + isScalarReduce ? SmallVector{} + : SmallVector(result.getShape())}; + outputs.push_back( + rewriter.create(loc, resultShape, elemTypes[i])); + } + + auto linalgOp = rewriter.create( + loc, adaptor.getOperands(), outputs, + SmallVector{adaptor.getAxis()}, + [&](OpBuilder &b, Location loc, ValueRange inputs) { + auto tritonReduceBlock = op.getBody(); + IRMapping mapping; + mapping.map(tritonReduceBlock->getArguments(), inputs); + + for (auto &op : tritonReduceBlock->without_terminator()) { + b.clone(op, mapping); + } + + auto tritonYield = tritonReduceBlock->getTerminator(); + auto results = + llvm::map_to_vector(tritonYield->getOperands(), + [&](Value val) { return mapping.lookup(val); }); + b.create(loc, results); + }); + + auto params = getReduceWithIndexParams(op); + if (failed(params)) { + return rewriter.notifyMatchFailure(op, "meaningless reduce operation"); + } else if (params->withIndexType != ReduceWithIndexType::None) { + addReduceWithIndexAttr(*params, rewriter, linalgOp); + } + + insertDebugNop(loc, rewriter); + if (isScalarReduce) { + SmallVector reduceResults; + for (auto i = 0; i < linalgOp.getResults().size() && i < elemTypes.size(); + i++) { + reduceResults.push_back(rewriter.create( + loc, elemTypes[i], linalgOp.getResults()[i], ValueRange{})); + } + rewriter.replaceOp(op, reduceResults); + } else { + rewriter.replaceOp(op, linalgOp); + } + return success(); +} + +bool ScanConverter::isReductionOpSupported(Operation *reductionOp) const { + if (isa( + reductionOp)) { + return true; + } + if (compileOn91095Flag && + isa(reductionOp)) { + return true; + } + return false; +} + +LogicalResult +ScanConverter::convertToTargetOp(triton::ScanOp op, + typename triton::ScanOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto reductionOps = this->getRealReductionOps(op); + if (reductionOps.empty()) { + return rewriter.notifyMatchFailure(op, + "No reduction op found in scan body"); + } + + llvm::SmallString<64> funcName; + auto rop = reductionOps.front(); + if (this->isReductionOpSupported(reductionOps.front())) { + bool propagateNan = true; + bool isMinMax = false; + if (isa(rop)) { + funcName = "triton_cumsum"; + } else if (isa(rop)) { + funcName = "triton_cumprod"; + } else if (isa(rop)) { + funcName = "triton_cummax"; + propagateNan = isa(rop); + isMinMax = true; + } else if (isa(rop)) { + funcName = "triton_cummin"; + propagateNan = isa(rop); + isMinMax = true; + } + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + auto loc = op.getLoc(); + auto src = adaptor.getOperands().front(); + auto resTy = op.getResult().front().getType(); + // cummax/cummin take a trailing propagateNan flag; cumsum/cumprod do not. + SmallVector argTypes{src.getType(), rewriter.getI32Type(), + rewriter.getI1Type()}; + if (isMinMax) { + argTypes.push_back(rewriter.getI1Type()); + } + auto libFnType = rewriter.getFunctionType(argTypes, {resTy}); + // Unique the name before creating the op: building a SymbolTable over a + // module that already holds a symbol of this name -- a kernel named + // "triton_cumsum", say -- trips SymbolTable's "uniquely named symbol + // operations" assertion before any renaming can happen. + auto uniqueName = generateUniqueFuncName(moduleOp, funcName); + auto funcOp = + rewriter.create(loc, uniqueName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + + rewriter.setInsertionPoint(op); + auto scanAxis = op.getAxis(); + auto scanReverse = op.getReverse(); + Value axis = rewriter.create(loc, scanAxis, 32); + Value reverseVal = + rewriter.create(loc, scanReverse, 1); + SmallVector callOperands{src, axis, reverseVal}; + if (isMinMax) { + callOperands.push_back( + rewriter.create(loc, propagateNan, 1)); + } + auto callOp = rewriter.create( + loc, funcOp.getSymNameAttr(), TypeRange({resTy}), callOperands); + + rewriter.replaceOp(op, callOp); + + return success(); + } else { + // This branch is the associative_scan op. + bool reverse = op.getReverse(); + + auto loc = op.getLoc(); + + Value scanInput = op.getOperand(0); + + auto srcType = mlir::dyn_cast(scanInput.getType()); + if (!srcType) { + return rewriter.notifyMatchFailure( + op, "Expected RankedTensorType input for associative_scan"); + } + + auto elementType = srcType.getElementType(); + auto shape = srcType.getShape(); + int rank = shape.size(); + int axis = op.getAxis(); + + if (axis < 0 || axis >= rank) { + return rewriter.notifyMatchFailure(op, "Invalid scan axis: " + + std::to_string(axis)); + } + + if (op->getNumRegions() < 1 || op->getRegion(0).empty()) { + return rewriter.notifyMatchFailure(op, "Missing combine region"); + } + + OpBuilder::InsertionGuard guard(rewriter); + + auto memrefType = MemRefType::get(shape, elementType); + Value inputMemRef = + rewriter.create(loc, memrefType, scanInput); + + // Wrap scan logic in a scope with UB address space for the output buffer. + auto tensorResultType = RankedTensorType::get(shape, elementType); + auto scopeOp = + rewriter.create(loc, TypeRange{tensorResultType}); + scopeOp.getBodyRegion().emplaceBlock(); + rewriter.setInsertionPointToEnd(&scopeOp.getBodyRegion().front()); + + auto ubMemRefType = MemRefType::get( + shape, elementType, nullptr, + rewriter.getAttr(hivm::AddressSpace::UB)); + Value outputMemRef = rewriter.create(loc, ubMemRefType); + + auto processDimension = [&](ArrayRef baseIdxsArray) { + auto startInd = rewriter.create(op.getLoc(), 0); + if (reverse) { + startInd = rewriter.create(op.getLoc(), + shape[axis] - 1); + } + llvm::SmallVector baseIdxs(baseIdxsArray.begin(), + baseIdxsArray.end()); + llvm::SmallVector firstIdx = baseIdxs; + if (axis <= firstIdx.size()) { + firstIdx.insert(firstIdx.begin() + axis, startInd); + } else { + firstIdx.push_back(startInd); + } + + Value firstVal = + rewriter.create(loc, inputMemRef, firstIdx); + rewriter.create(loc, firstVal, outputMemRef, firstIdx); + + Value axisSize = + rewriter.create(loc, inputMemRef, axis).getResult(); + Value one = rewriter.create(loc, 1); + + Value cmp = rewriter.create(loc, arith::CmpIPredicate::sgt, + axisSize, one); + auto ifOp = rewriter.create(loc, cmp, false); + + // Create a loop only when the axis size is greater than 1. + rewriter.setInsertionPointToStart(ifOp.thenBlock()); + + auto forOp = rewriter.create(loc, one, axisSize, one); + rewriter.setInsertionPointToStart(forOp.getBody()); + + Value k = forOp.getInductionVar(); + if (reverse) { + llvm::SmallVector fixInd; + fixInd.push_back( + rewriter + .create(op.getLoc(), shape[axis] - 1) + .getResult()); + fixInd.push_back(k); + auto fixIndVal = rewriter.create(op.getLoc(), fixInd); + k = fixIndVal.getResult(); + } + llvm::SmallVector currIdx = baseIdxs; + if (axis <= currIdx.size()) { + currIdx.insert(currIdx.begin() + axis, k); + } else { + currIdx.push_back(k); + } + + Value km1 = rewriter.create(loc, k, one); + if (reverse) { + km1 = rewriter.create(loc, k, one); + } + llvm::SmallVector prevIdx = baseIdxs; + if (axis <= prevIdx.size()) { + prevIdx.insert(prevIdx.begin() + axis, km1); + } else { + prevIdx.push_back(km1); + } + + Value currentVal = + rewriter.create(loc, inputMemRef, currIdx); + Value prevResult = + rewriter.create(loc, outputMemRef, prevIdx); + + Region &combineRegion = op->getRegion(0); + Block &combineBlock = combineRegion.front(); + IRMapping mapping; + mapping.map(combineBlock.getArgument(0), prevResult); + mapping.map(combineBlock.getArgument(1), currentVal); + + for (Operation &innerOp : combineBlock.without_terminator()) { + rewriter.clone(innerOp, mapping); + } + + Operation *yieldOp = combineBlock.getTerminator(); + Value resultVal = mapping.lookup(yieldOp->getOperand(0)); + + rewriter.create(loc, resultVal, outputMemRef, currIdx); + + rewriter.setInsertionPointAfter(ifOp); + }; + + // Constructing loops for non-scanning dimensions + llvm::SmallVector nonScanDims; + for (int i = 0; i < rank; ++i) { + if (i != axis) + nonScanDims.push_back(i); + } + + createSimpleNestedLoops(rewriter, loc, outputMemRef, nonScanDims, + processDimension); + + mlir::Type resultType = mlir::memref::getTensorTypeFromMemRefType( + dyn_cast(outputMemRef.getType())); + Value outputTensor = rewriter.create( + loc, resultType, outputMemRef, true); + rewriter.create(loc, ValueRange{outputTensor}); + + scopeOp->setAttr(hivm::TCoreTypeAttr::name, + hivm::TCoreTypeAttr::get(rewriter.getContext(), + hivm::TCoreType::VECTOR)); + + rewriter.replaceOp(op, scopeOp.getResult(0)); + return success(); + } +} + +LogicalResult ScanConverter::convertToTargetOpExtended( + triton::ScanOp op, typename triton::ScanOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + bool reverse = op.getReverse(); + + // 1. Extract all input tensors (supports multiple inputs) + auto operands = op->getOperands(); + if (operands.empty()) { + return rewriter.notifyMatchFailure(op, + "No input operands for extended scan"); + } + + // 2. Validate all inputs are of RankedTensorType + llvm::SmallVector inputTensTypes; + for (auto operand : operands) { + auto tensorTy = dyn_cast(operand.getType()); + if (!tensorTy) { + return rewriter.notifyMatchFailure(op, + "All inputs must be RankedTensorType"); + } + inputTensTypes.push_back(tensorTy); + } + + // 3. Validate all input tensors have the same shape (scan operation + // requires matching input dimensions) + auto baseShape = inputTensTypes[0].getShape(); + int rank = baseShape.size(); + int axis = op.getAxis(); + if (axis < 0 || axis >= rank) { + return rewriter.notifyMatchFailure(op, "Invalid scan axis: " + + std::to_string(axis)); + } + for (size_t i = 1; i < inputTensTypes.size(); ++i) { + if (inputTensTypes[i].getShape() != baseShape) { + return rewriter.notifyMatchFailure(op, + "All inputs must have the same shape"); + } + } + + // 4. Prepare MemRefs for multiple inputs/outputs + llvm::SmallVector inputMemRefs; + llvm::SmallVector outputMemRefs; + llvm::SmallVector memRefTypes; + for (size_t i = 0; i < inputTensTypes.size(); ++i) { + auto &tensorTy = inputTensTypes[i]; + auto memRefTy = + MemRefType::get(tensorTy.getShape(), tensorTy.getElementType()); + memRefTypes.push_back(memRefTy); + // Convert input tensors to MemRefs + inputMemRefs.push_back( + rewriter.create(loc, memRefTy, operands[i])); + // Allocate MemRefs for outputs + outputMemRefs.push_back(rewriter.create(loc, memRefTy)); + } + + // 5. Define scanning logic for multiple inputs/outputs + LogicalResult loopResult = success(); + auto processDimension = [&](ArrayRef baseIdxsArray) { + llvm::SmallVector baseIdxs(baseIdxsArray.begin(), + baseIdxsArray.end()); + + auto startInd = rewriter.create(op.getLoc(), 0); + if (reverse) { + startInd = rewriter.create(op.getLoc(), + baseShape[axis] - 1); + } + + llvm::SmallVector firstIdx = baseIdxs; + if (axis <= firstIdx.size()) { + firstIdx.insert(firstIdx.begin() + axis, startInd); + } else { + firstIdx.push_back(startInd); + } + + // 5.1 Process the first element: directly copy multiple inputs to + // multiple outputs (initialize cumulative results) + for (size_t i = 0; i < inputMemRefs.size(); ++i) { + Value firstVal = + rewriter.create(loc, inputMemRefs[i], firstIdx); + rewriter.create(loc, firstVal, outputMemRefs[i], + firstIdx); + } + + Value axisSize = + rewriter.create(loc, baseShape[axis]); + Value one = rewriter.create(loc, 1); + + Value cmp = rewriter.create(loc, arith::CmpIPredicate::sgt, + axisSize, one); + auto ifOp = rewriter.create(loc, cmp, false); + + // Create a loop only when the axis size is greater than 1. + rewriter.setInsertionPointToStart(ifOp.thenBlock()); + + // Use a forward loop, but handle reverse indexing inside the loop. + auto forOp = rewriter.create(loc, one, axisSize, one); + rewriter.setInsertionPointToStart(forOp.getBody()); + + Value k = forOp.getInductionVar(); + + if (reverse) { + // Reverse scanning: Convert the forward loop index to the actual + // reverse index. (axis_size - 1) - k + Value axisSizeVal = + rewriter.create(loc, baseShape[axis]); + Value axisSizeMinusOne = + rewriter.create(loc, axisSizeVal, one); + k = rewriter.create(loc, axisSizeMinusOne, k); + } + + llvm::SmallVector currIdx = baseIdxs; + if (axis <= currIdx.size()) { + currIdx.insert(currIdx.begin() + axis, k); + } else { + currIdx.push_back(k); + } + + Value prevIndex; + if (reverse) { + prevIndex = rewriter.create(loc, k, one); + } else { + prevIndex = rewriter.create(loc, k, one); + } + + llvm::SmallVector prevIdx = baseIdxs; + if (axis <= prevIdx.size()) { + prevIdx.insert(prevIdx.begin() + axis, prevIndex); + } else { + prevIdx.push_back(prevIndex); + } + + // 5.4 Load current elements and previous cumulative results + llvm::SmallVector currentVals; + llvm::SmallVector prevResults; + for (size_t i = 0; i < inputMemRefs.size(); ++i) { + currentVals.push_back( + rewriter.create(loc, inputMemRefs[i], currIdx)); + prevResults.push_back( + rewriter.create(loc, outputMemRefs[i], prevIdx)); + } + + // 5.5 Bind parameters for custom reduction logic + Region &combineRegion = op->getRegion(0); + if (combineRegion.empty()) { + op->emitError("Missing combine region in extended scan"); + loopResult = failure(); + return; + } + Block &combineBlock = combineRegion.front(); + // Validate that the number of reduction region arguments matches (number + // of previous results + number of current elements) + if (combineBlock.getNumArguments() != 2 * inputMemRefs.size()) { + op->emitError("Combine region arguments mismatch with input count"); + loopResult = failure(); + return; + } + IRMapping mapping; + for (size_t i = 0; i < inputMemRefs.size(); ++i) { + // Bind previous results (previous value of the i-th output) to the i-th + // argument of the reduction region + mapping.map(combineBlock.getArgument(i), prevResults[i]); + // Bind current elements (current value of the i-th input) to the i+N-th + // argument of the reduction region (N is the number of inputs) + mapping.map(combineBlock.getArgument(i + inputMemRefs.size()), + currentVals[i]); + } + + // 5.6 Clone all operations within the reduction region + for (Operation &innerOp : combineBlock.without_terminator()) { + rewriter.clone(innerOp, mapping); + } + + // 5.7 Extract reduction results and store them in outputMemRef + Operation *yieldOp = combineBlock.getTerminator(); + if (yieldOp->getNumOperands() != outputMemRefs.size()) { + op->emitError("Combine region returns mismatch with output count"); + loopResult = failure(); + return; + } + for (size_t i = 0; i < outputMemRefs.size(); ++i) { + Value resultVal = mapping.lookup(yieldOp->getOperand(i)); + rewriter.create(loc, resultVal, outputMemRefs[i], + currIdx); + } + + rewriter.setInsertionPointAfter(ifOp); + }; + + // 6. Generate nested loops for non-scan dimensions + llvm::SmallVector nonScanDims; + for (int i = 0; i < rank; ++i) { + if (i != axis) + nonScanDims.push_back(i); + } + createSimpleNestedLoops(rewriter, loc, outputMemRefs[0], nonScanDims, + processDimension); + + if (failed(loopResult)) { + return failure(); + } + + // 7. Convert multiple output MemRefs back to tensors and replace the + // original tt.scan operation + llvm::SmallVector outputTensors; + for (auto outputMemRef : outputMemRefs) { + mlir::Type resultType = mlir::memref::getTensorTypeFromMemRefType( + dyn_cast(outputMemRef.getType())); + outputTensors.push_back(rewriter.create( + loc, resultType, outputMemRef, true)); + } + rewriter.replaceOp(op, outputTensors); + + return success(); +} + +LogicalResult ExternElementwiseClOpConverter::matchAndRewrite( + triton::ExternElementwiseOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + if (!op.getPure()) { + op->emitWarning() << "impure elementwise op!"; + return failure(); + } + if (op.getSymbol().contains("__hmf_")) { + Type dstTy = op.getResult().getType(); + bool isDstScalar = !isa(dstTy); + Type dstElemTy = + isDstScalar ? dstTy : cast(dstTy).getElementType(); + SmallVector srcElemTys; + SmallVector srcs; + for (auto src : op.getSrcs()) { + if (!isa(src.getType())) { + src = rewriter.create( + op.getLoc(), RankedTensorType::get({int64_t{1}}, src.getType()), + src); + } + srcs.push_back(src); + srcElemTys.push_back( + cast(src.getType()).getElementType()); + } + + // extern libdevice ops -> hivm.hir.custom + static constexpr llvm::StringLiteral libdeviceSuffixes[] = { + "_fp32", "_fp16", "_bf16", "_i32", "_i64", "_u32", "_u64"}; + bool isLibdeviceOp = + llvm::any_of(libdeviceSuffixes, [&](llvm::StringRef suffix) { + return op.getSymbol().ends_with(suffix); + }); + if (isLibdeviceOp) { + auto originalTensorType = isDstScalar + ? RankedTensorType::get({1}, dstElemTy) + : cast(dstTy); + auto originalShape = llvm::to_vector(originalTensorType.getShape()); + bool needsExpand = originalTensorType.getRank() != 1; + bool isI1Result = dstElemTy.isInteger(1); + + Type customElemType = dstElemTy; + if (isI1Result) + customElemType = rewriter.getI32Type(); + + // Flatten inputs to 1D + SmallVector collapsedInputs; + + for (Value operand : srcs) { + RankedTensorType tensorTy = cast(operand.getType()); + if (tensorTy.getRank() <= 1) { + collapsedInputs.push_back(operand); + continue; + } + + int64_t totalSize = 1; + for (int64_t dim : tensorTy.getShape()) { + if (ShapedType::isDynamic(dim)) { + totalSize = ShapedType::kDynamic; + break; + } + totalSize *= dim; + } + + ReassociationIndices indices; + for (int i = 0; i < tensorTy.getRank(); ++i) + indices.push_back(i); + + RankedTensorType collapse1DTy = + RankedTensorType::get({totalSize}, tensorTy.getElementType()); + Value collapsedVal = rewriter.create( + op.getLoc(), collapse1DTy, operand, indices); + collapsedInputs.push_back(collapsedVal); + } + + int64_t outputSize = 1; + for (int64_t dim : originalTensorType.getShape()) { + if (ShapedType::isDynamic(dim)) { + outputSize = ShapedType::kDynamic; + break; + } + outputSize *= dim; + } + auto customOutputType = + RankedTensorType::get({outputSize}, customElemType); + + SmallVector dynamicSizes; + if (ShapedType::isDynamic(outputSize)) { + for (Value collapsedInput : collapsedInputs) { + auto collapsedInputType = + dyn_cast(collapsedInput.getType()); + if (!collapsedInputType || collapsedInputType.getRank() != 1 || + !collapsedInputType.isDynamicDim(0)) + continue; + + dynamicSizes.push_back( + rewriter.create(op.getLoc(), collapsedInput, 0)); + break; + } + + if (dynamicSizes.empty()) + return rewriter.notifyMatchFailure( + op, "cannot determine the runtime size of a dynamic " + "libdevice output"); + } + + auto outputMemRefType = + MemRefType::get(customOutputType.getShape(), customElemType); + Value outputMemRef = rewriter.create( + op.getLoc(), outputMemRefType, dynamicSizes); + Value outputTensor = rewriter.create( + op.getLoc(), customOutputType, outputMemRef, true, true); + + ReassociationIndices outputReassociation; + if (needsExpand) { + for (int i = 0; i < originalTensorType.getRank(); ++i) + outputReassociation.push_back(i); + } + + std::string sym = + llvm::join(llvm::split(op.getSymbol().str(), "__hmf_"), ""); + auto customOp = rewriter.create( + op.getLoc(), sym, ValueRange{collapsedInputs}, + ValueRange{outputTensor}); + + auto argAttrsArray = mlir::ArrayAttr::get(customOp->getContext(), {}); + auto pipeAttr = + hivm::PipeAttr::get(customOp->getContext(), hivm::PIPE::PIPE_V); + auto tcoreTypeAttr = hivm::TCoreTypeAttr::get(customOp->getContext(), + hivm::TCoreType::VECTOR); + auto vfModeAttr = + hivm::VFModeAttr::get(customOp->getContext(), hivm::VFMode::SIMD); + + customOp->setAttr("bitcode", + mlir::StringAttr::get(customOp->getContext(), "")); + customOp->setAttr("hivm.pipe", pipeAttr); + customOp->setAttr("hivm.tcore_type", tcoreTypeAttr); + customOp->setAttr("hivm.vf_mode", vfModeAttr); + customOp->setAttr("symbol", + mlir::StringAttr::get(customOp->getContext(), sym)); + customOp->setAttr("arg_attrs", argAttrsArray); + customOp.setInlineMode(hivm::InlineMode::AlwaysInline); + + // Restore the result's shape and element type + Value finalResult = customOp.getResults().front(); + if (needsExpand) { + auto expandedType = + RankedTensorType::get(originalShape, customElemType); + finalResult = rewriter.create( + op->getLoc(), expandedType, finalResult, outputReassociation); + } + + if (isI1Result) { + auto resultTensorType = cast(finalResult.getType()); + auto zeroAttr = DenseElementsAttr::get( + RankedTensorType::get(resultTensorType.getShape(), + rewriter.getI32Type()), + 0); + Value zeroTensor = + rewriter.create(op->getLoc(), zeroAttr); + finalResult = rewriter.create( + op->getLoc(), arith::CmpIPredicate::ne, finalResult, zeroTensor); + } + + if (isDstScalar) { + Value zero = rewriter.create( + op->getLoc(), rewriter.getIndexAttr(0)); + finalResult = + rewriter.create(op->getLoc(), finalResult, zero); + } + + rewriter.replaceOp(op, finalResult); + return success(); + } + + // 1. get or create the declaration of external elementwise function + FunctionType elemFuncType = + FunctionType::get(rewriter.getContext(), srcElemTys, {dstElemTy}); + auto mod = SymbolTable::getNearestSymbolTable(op); + auto extFunc = dyn_cast_or_null( + SymbolTable::lookupSymbolIn(mod, op.getSymbol())); + // std::string symbol = op.getSymbol().str(); + if (!extFunc) { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToStart(&mod->getRegion(0).front()); + extFunc = rewriter.create(rewriter.getUnknownLoc(), + op.getSymbol(), elemFuncType); + extFunc.setPrivate(); + extFunc->setAttr(LLVM::LLVMDialect::getReadnoneAttrName(), + UnitAttr::get(rewriter.getContext())); + // set coreType for external func, otherwise InferFuncCoreTypePass will + // fail + } + assert(isa( + SymbolTable::lookupSymbolIn(mod, op.getSymbol()))); + // 2. prepare the output tensor + Value output; + if (isDstScalar) { + dstTy = RankedTensorType::get({(int64_t)1}, dstElemTy); + } + bool found = false; + for (Value v : srcs) { + if (v.getType() == dstTy) { + found = true; + output = v; + break; + } + } + if (!found) { + output = rewriter.create( + op.getLoc(), cast(dstTy).getShape(), dstElemTy); + } + // 3. create the linalg.map op + auto mapOp = rewriter.create( + loc, + /*inputs=*/srcs, + /*init=*/output, + /*bodyBuilder=*/ + [&](OpBuilder &builder, Location loc, ValueRange regionArgs) { + auto elemOp = builder.create(loc, + /*name=*/op.getSymbol(), + /*resultType=*/dstElemTy, + /*operands=*/regionArgs); + builder.create(loc, elemOp->getResults()); + }); + if (isDstScalar) { + // need to convert tensor back to scalar + auto indexType = rewriter.getIndexType(); + Value zeroConstant = rewriter.create( + loc, indexType, rewriter.getIntegerAttr(indexType, 0)); + auto extractOp = rewriter.create( + loc, mapOp.getResults()[0], zeroConstant); + rewriter.replaceOp(op, extractOp); + } else { + rewriter.replaceOp(op, mapOp); + } + return success(); + } + return failure(); +} + +LogicalResult UnrealizedCastConverter::matchAndRewrite( + UnrealizedConversionCastOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + rewriter.eraseOp(op); + return success(); +} + +LogicalResult +JoinConverter::matchAndRewrite(triton::JoinOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Value opa = op.getLhs(); + Value opb = op.getRhs(); + auto loc = op.getLoc(); + + auto resType = dyn_cast(op.getResult().getType()); + Value emptyOp = rewriter.create(loc, resType.getShape(), + resType.getElementType()); + + auto shape = dyn_cast(opa.getType()).getShape(); + auto sizes = llvm::map_to_vector(shape, [&](int64_t t) { + return OpFoldResult(rewriter.getI64IntegerAttr(t)); + }); + sizes.push_back(rewriter.getI64IntegerAttr(1)); + + int64_t rank = resType.getRank(); + + // Set last dimension stride to 2 in layout + // As last dimension size is always 1, last dimension stride here could be + // either 1 or 2, while stride `2` could carry interleave trait and it's + // convenient for next lower. + SmallVector strides(rank, rewriter.getIndexAttr(1)); + strides.back() = rewriter.getIndexAttr(2); + + SmallVector offsets(rank, rewriter.getIndexAttr(0)); + + auto insert0 = rewriter.create( + loc, opa, emptyOp, offsets, sizes, strides); + + offsets.back() = rewriter.getIndexAttr(1); + auto insert1 = rewriter.create( + loc, opb, insert0, offsets, sizes, strides); + rewriter.replaceOp(op, insert1); + return success(); +} + +LogicalResult +CatConverter::matchAndRewrite(triton::CatOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Value opa = op.getLhs(); + Value opb = op.getRhs(); + auto loc = op.getLoc(); + + auto resType = dyn_cast(op.getResult().getType()); + if (!resType || resType.getRank() != 1) { + return rewriter.notifyMatchFailure(op, "only support 1D cat"); + } + + auto inputTypeA = dyn_cast(opa.getType()); + auto inputTypeB = dyn_cast(opb.getType()); + if (!inputTypeA || !inputTypeB || inputTypeA.getRank() != 1 || + inputTypeB.getRank() != 1) { + return rewriter.notifyMatchFailure(op, "inputs must be 1D tensors"); + } + + int64_t sizeA = inputTypeA.getShape()[0]; + int64_t sizeB = inputTypeB.getShape()[0]; + + // Only handle the case where both inputs have size 1 (i.e., scalar-like) + if (sizeA == 1 && sizeB == 1) { + // Use scalar extract + insert + auto emptyOp = rewriter.create(loc, resType.getShape(), + resType.getElementType()); + + Value zero = rewriter.create(loc, 0); + Value one = rewriter.create(loc, 1); + + Value scalarA = rewriter.create(loc, opa, zero); + Value scalarB = rewriter.create(loc, opb, zero); + + Value inserted0 = + rewriter.create(loc, scalarA, emptyOp, zero); + Value inserted1 = + rewriter.create(loc, scalarB, inserted0, one); + + rewriter.replaceOp(op, inserted1); + return success(); + } + + // General case: use tensor.insert_slice + auto emptyOp = rewriter.create(loc, resType.getShape(), + resType.getElementType()); + + auto rank = resType.getRank(); + SmallVector offsets(rank, rewriter.getIndexAttr(0)); + SmallVector strides(rank, rewriter.getIndexAttr(1)); + + auto inputType = dyn_cast(opa.getType()); + + SmallVector sizes = + llvm::map_to_vector(inputType.getShape(), [&](int64_t t) { + return OpFoldResult(rewriter.getI64IntegerAttr(t)); + }); + + auto insert0 = rewriter.create( + loc, opa, emptyOp, offsets, sizes, strides); + + offsets[0] = + rewriter.getIndexAttr(inputType.getRank() ? inputType.getShape()[0] : 1); + auto insert1 = rewriter.create( + loc, opb, insert0, offsets, sizes, strides); + + rewriter.replaceOp(op, insert1); + return success(); +} + +/// @brief Convert tt.gather to func.call. BiShengIR captures the func +/// with assumed semantics. +/// @param op The `triton::GatherOp` operation to be rewritten. +/// @param adaptor An adaptor for the operation's operands. +/// @param rewriter A pattern rewriter used to modify the IR. +/// @return A `LogicalResult` indicating whether the rewrite was successful. +LogicalResult +GatherConverter::matchAndRewrite(triton::GatherOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + Value src = adaptor.getSrc(); + Value idx = adaptor.getIndices(); + Value res = op.getResult(); + auto gatherAxis = op.getAxis(); + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + llvm::SmallString funcName = gatherFuncNameBase; + int uniqueId = 0; + while (SymbolTable::lookupSymbolIn(moduleOp, funcName)) { + funcName = gatherFuncNameBase; + funcName += ("_" + std::to_string(uniqueId++)); + } + + auto resTy = res.getType(); + auto libFnType = rewriter.getFunctionType( + {src.getType(), idx.getType(), rewriter.getI32Type()}, {resTy}); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + + rewriter.setInsertionPoint(op); + Value axis = rewriter.create(loc, gatherAxis, 32); + auto callOp = rewriter.create(loc, funcOp.getSymNameAttr(), + TypeRange({resTy}), + ValueRange({src, idx, axis})); + + rewriter.replaceOp(op, callOp); + + return success(); +} + +LogicalResult +SplitConverter::matchAndRewrite(triton::SplitOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Value input = op.getSrc(); + auto loc = op.getLoc(); + auto inputType = cast(input.getType()); + + int64_t rank = inputType.getRank(); + SmallVector offsets(rank, rewriter.getIndexAttr(0)); + // Similar to JoinConverter, here adjust last dimension stride + SmallVector strides(rank, rewriter.getIndexAttr(1)); + strides.back() = rewriter.getIndexAttr(2); + + auto outType = dyn_cast(op.getOutLHS().getType()); + auto sizes = llvm::map_to_vector(outType.getShape(), [&](int64_t t) { + return OpFoldResult(rewriter.getIndexAttr(t)); + }); + sizes.push_back(rewriter.getIndexAttr(1)); + + auto slice0 = rewriter.create( + loc, outType, input, offsets, sizes, strides); + + offsets.back() = rewriter.getIndexAttr(1); + auto slice1 = rewriter.create( + loc, outType, input, offsets, sizes, strides); + + SmallVector slices = {slice0.getResult(), slice1.getResult()}; + rewriter.replaceOp(op, ValueRange(slices)); + return success(); +} + +/* +the element-wise most significant N bits of the 2N-bit product of x and y +%x:2 = arith.mulsi_extended %y, %z : tensor<4x?xi32> +*/ +LogicalResult TritonMulhiuiConverter::matchAndRewrite( + triton::MulhiUIOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + Value opl = op.getX(); + Value opr = op.getY(); + Value res = op.getResult(); + auto newMulOp = rewriter.create( + loc, res.getType(), res.getType(), opl, opr); + // triton only need the high value + rewriter.replaceOp(op, ValueRange{newMulOp.getHigh()}); + return success(); +} + +LogicalResult TritonPreciseSqrtConverter::matchAndRewrite( + triton::PreciseSqrtOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + rewriter.replaceOpWithNewOp(op, adaptor.getOperands()); + return success(); +} + +LogicalResult DevicePrintConverter::matchAndRewrite( + triton::PrintOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + SmallVector inputTypes; + for (auto arg : op.getArgs()) { + inputTypes.push_back(arg.getType()); + } + auto libFnType = rewriter.getFunctionType(inputTypes, {}); + // See ScanConverter: the name is uniqued before the op is created so that a + // kernel named "triton_print" cannot make SymbolTable assert. + auto funcName = generateUniqueFuncName(moduleOp, printFuncNameBase); + auto funcOp = + rewriter.create(op.getLoc(), funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + auto prefixAttr = op.getPrefixAttr(); + funcOp->setAttr(prefixAttrName, prefixAttr); + auto hexAttr = op.getHexAttr(); + funcOp->setAttr(hexAttrName, hexAttr); + + rewriter.setInsertionPoint(op); + rewriter.create(op.getLoc(), funcOp, op.getArgs()); + + rewriter.eraseOp(op); + return success(); +} + +LogicalResult DeviceAssertConverter::matchAndRewrite( + triton::AssertOp op, OpAdaptor adaptor, + mlir::ConversionPatternRewriter &rewriter) const { + auto msgAttr = op.getMessageAttr(); + // The frontend marks only sanitize_overflow assertions. A user may choose + // the same text for tl.device_assert, so message matching is not a safe + // provenance test here or in the graph rewrite. + if (op->hasAttr("tt.auto_overflow_assert")) { + rewriter.eraseOp(op); + return success(); + } + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + auto conditionType = op.getCondition().getType(); + + auto libFnType = rewriter.getFunctionType({conditionType}, {}); + // See ScanConverter: the name is uniqued before the op is created so that a + // kernel named "triton_assert" cannot make SymbolTable assert. + auto funcName = generateUniqueFuncName(moduleOp, printFuncNameBase); + auto funcOp = + rewriter.create(op.getLoc(), funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + funcOp->setAttr(msgAttrName, msgAttr); + + rewriter.setInsertionPoint(op); + rewriter.create(op.getLoc(), funcOp, + ValueRange{op.getCondition()}); + + rewriter.eraseOp(op); + return success(); +} + +LogicalResult +MatmulConverter::matchAndRewrite(triton::DotOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto opa = adaptor.getA(); + auto opb = adaptor.getB(); + auto opc = adaptor.getC(); + auto dstType = cast(op.getType()); + auto elemTy = dstType.getElementType(); + auto inputPrec = op.getInputPrecision(); + // Ascend does not support tf32; map it to hf32 which provides similar + // functionality.HF32 is only valid for fp32 x fp32 inputs; for other + // dtypes, fall back to ieee. + if (inputPrec == InputPrecision::TF32) { + op->emitWarning("Ascend does not support tf32; map it to hf32."); + inputPrec = InputPrecision::HF32; + } + if (inputPrec == InputPrecision::HF32) { + auto opaElemTy = cast(opa.getType()).getElementType(); + auto opbElemTy = cast(opb.getType()).getElementType(); + if (!opaElemTy.isF32() || !opbElemTy.isF32()) { + inputPrec = InputPrecision::IEEE; + } + } + auto createOp = [&](auto &&rewriter, ValueRange operands, + ValueRange results) -> Operation * { + if (dstType.getRank() == 2) + return rewriter.template create(op.getLoc(), operands, + results); + else if (dstType.getRank() == 3) + return rewriter.template create(op.getLoc(), + operands, results); + llvm_unreachable("Datatype of DotOp operands could only be 2D or 3D"); + }; + + auto replaceOp = [&](auto &&rewriter, ValueRange operands, + ValueRange results) -> Operation * { + if (dstType.getRank() == 2) + return rewriter.template replaceOpWithNewOp( + op, operands, results); + else if (dstType.getRank() == 3) + return rewriter.template replaceOpWithNewOp( + op, operands, results); + llvm_unreachable("Datatype of DotOp operands could only be 2D or 3D"); + }; + + Operation *matmulOp; + if (mlir::isa(elemTy) && !elemTy.isF32()) { + RankedTensorType opcFp32Ty = + RankedTensorType::get(dstType.getShape(), rewriter.getF32Type()); + Value opcFp32 = rewriter.create(op.getLoc(), opcFp32Ty, opc); + matmulOp = createOp(rewriter, ValueRange{opa, opb}, ValueRange{opcFp32}); + auto roundModeAttr = hfusion::RoundModeAttr::get(rewriter.getContext(), + hfusion::RoundMode::RINT); + auto truncOp = rewriter.replaceOpWithNewOp( + op, dstType, matmulOp->getResult(0)); + truncOp->setAttr("round_mode", roundModeAttr); + } else { + matmulOp = replaceOp(rewriter, ValueRange{opa, opb}, ValueRange{opc}); + } + matmulOp->setAttr("input_precision", + rewriter.getStringAttr(stringifyInputPrecision(inputPrec))); + return success(); +} + +// Fractal (zN) block geometry for ascend.dot lowering comes from Utils.h, +// shared with DotOp::inferReturnTypes. + +// zN block column count b = 32 bytes / element bytes. +static int64_t nzBlockCol(Type elemTy) { + return triton::ascend::kBytesPerFractalCol / + (elemTy.getIntOrFloatBitWidth() / triton::ascend::kBitsPerByte); +} + +static hivm::DataLayoutAttr dotNdLayout(MLIRContext *ctx) { + return hivm::DataLayoutAttr::get(ctx, hivm::DataLayout::ND); +} + +static hivm::DataLayoutAttr dotFractalLayout(MLIRContext *ctx, int64_t blockRow, + int64_t blockCol) { + return hivm::DataLayoutAttr::get( + ctx, hivm::DataLayout::Fractal, /*transpose=*/nullptr, + DenseI64ArrayAttr::get(ctx, {blockRow, blockCol})); +} + +// A 4D fractal dot operand -> 2D ND (no-op if already ND). Both operands are zN +// (non-transpose): lhs [K1,M1,16,b] -> [M,K] and rhs [N1,K1,16,b] -> [K,N], +// each = [d1*d2, d0*d3]. +static Value dotFractalToND(ConversionPatternRewriter &rewriter, Location loc, + Value operand, bool isLhs) { + auto operandTy = cast(operand.getType()); + if (operandTy.getRank() != 4) + return operand; + auto blockedShape = operandTy.getShape(); + SmallVector ndShape{blockedShape[1] * blockedShape[2], + blockedShape[0] * blockedShape[3]}; + MLIRContext *ctx = rewriter.getContext(); + return rewriter.create( + loc, RankedTensorType::get(ndShape, operandTy.getElementType()), operand, + /*srcLayout=*/dotFractalLayout(ctx, blockedShape[2], blockedShape[3]), + /*dstLayout=*/dotNdLayout(ctx), DenseI64ArrayAttr::get(ctx, ndShape), + /*output_shape=*/ValueRange{}); +} + +// If `v` is a freshly-loaded buffer used only here -- to_tensor of an alloc +// with a single writer copy (the load) -- return that load copy so its DMA can +// be retargeted into the padded buffer (one copy, not load-then-copy); else +// null. +static memref::CopyOp findRedirectableLoad(Value v) { + auto toTensor = v.getDefiningOp(); + if (!toTensor || v.hasNUsesOrMore(2)) + return nullptr; + Value srcBuf = toTensor.getBuffer(); + if (!srcBuf.getDefiningOp()) + return nullptr; + memref::CopyOp loadCopy = nullptr; + for (Operation *user : srcBuf.getUsers()) { + if (user == toTensor.getOperation()) + continue; + // Bail on a non-copy user, a source-use of srcBuf, or a second writer. + auto copyOp = dyn_cast(user); + if (!copyOp || copyOp.getTarget() != srcBuf || loadCopy) + return nullptr; + loadCopy = copyOp; + } + return loadCopy; +} + +// Zero-pad `v`'s dimension `dim` up to `target` (no-op if already that size). +// Uses memref.alloc + fill(0) + subview + copy, NOT tensor.insert_slice: the +// latter lowers (in AscendNPU-IR AutoVectorizeV2) to a scalar copy loop that +// blows up cbuf. +static Value zeroPadDimTo(Value v, int64_t dim, int64_t target, + ConversionPatternRewriter &rewriter, Location loc) { + auto t = cast(v.getType()); + if (t.getShape()[dim] == target) + return v; + Type elemTy = t.getElementType(); + + // Push the pad through a transpose (pad the mapped input dim, re-transpose). + if (auto trans = v.getDefiningOp()) { + ArrayRef perm = trans.getPermutation(); + Value paddedInput = + zeroPadDimTo(trans.getInput(), perm[dim], target, rewriter, loc); + SmallVector outShape(t.getShape().begin(), t.getShape().end()); + outShape[dim] = target; + Value init = rewriter.create(loc, outShape, elemTy); + return rewriter.create(loc, paddedInput, init, perm) + .getResults()[0]; + } + + SmallVector paddedShape(t.getShape().begin(), t.getShape().end()); + paddedShape[dim] = target; + + // Allocate the padded buffer and zero-fill it. + auto paddedMemTy = MemRefType::get(paddedShape, elemTy); + Value padded = rewriter.create(loc, paddedMemTy); + Value zero = + rewriter.create(loc, rewriter.getZeroAttr(elemTy)); + rewriter.create(loc, ValueRange{zero}, ValueRange{padded}); + + // Subview covering the real (unpadded) region, then fill it with one copy. + int64_t rank = t.getRank(); + SmallVector offsets(rank, rewriter.getIndexAttr(0)); + SmallVector strides(rank, rewriter.getIndexAttr(1)); + SmallVector sizes; + for (int64_t d : t.getShape()) + sizes.push_back(rewriter.getIndexAttr(d)); + auto subviewTy = + memref::SubViewOp::inferResultType(paddedMemTy, offsets, sizes, strides); + Value subview = rewriter.create( + loc, cast(subviewTy), padded, offsets, sizes, strides); + + if (memref::CopyOp loadCopy = findRedirectableLoad(v)) { + rewriter.create(loc, loadCopy.getSource(), subview); + rewriter.eraseOp(loadCopy); + } else { + auto srcBufferTy = MemRefType::get(t.getShape(), elemTy); + Value srcBuffer = + rewriter.create(loc, srcBufferTy, v); + rewriter.create(loc, srcBuffer, subview); + } + + return rewriter.create( + loc, RankedTensorType::get(paddedShape, elemTy), padded, + /*restrict=*/true, /*writable=*/true); +} + +// Reconcile the ND operands' contraction (K) dim (a/b mutated in place). When +// exactly one operand is fractal, its K is block-rounded while the ND side may +// be shorter; zero-padding the ND side to the block-aligned K is safe (the +// extra K products are * 0). Only an in-one-block gap is padding; else error. +static LogicalResult +reconcileDotContractionK(triton::ascend::DotOp op, Value &a, Value &b, + Type elemTy, ConversionPatternRewriter &rewriter, + Location loc) { + if (op.getFractalA() == op.getFractalB()) + return success(); + int64_t kA = cast(a.getType()).getShape()[1]; // A_nd [M,kA] + int64_t kB = cast(b.getType()).getShape()[0]; // B_nd [kB,N] + if (kA == kB) + return success(); + int64_t paddedK = std::max(kA, kB), shortK = std::min(kA, kB); + int64_t blockK = nzBlockCol(elemTy); + if (paddedK % blockK != 0 || paddedK - shortK >= blockK) + return op.emitError() + << "dot: contraction dim mismatch (A K=" << kA << ", B K=" << kB + << ") is larger than fractal padding; pad the operand " + "explicitly"; + op.emitWarning() << "dot: auto zero-padding the non-fractal operand's K from " + << shortK << " to " << paddedK + << " to match the fractal operand's block-aligned K"; + a = zeroPadDimTo(a, /*dim=*/1, paddedK, rewriter, loc); // A_nd [M,K] + b = zeroPadDimTo(b, /*dim=*/0, paddedK, rewriter, loc); // B_nd [K,N] + return success(); +} + +// Decompose `ascend.dot` (D = A * B): fractal inputs -> ND, a plain +// linalg.matmul, then (if fractal_c) the ND result back to fractal. +LogicalResult +DotConverter::matchAndRewrite(triton::ascend::DotOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Location loc = op.getLoc(); + MLIRContext *ctx = rewriter.getContext(); + auto resTy = cast(op.getType()); + // The result carries the cube accumulator dtype (f32/i32); the matmul inputs + // and K-padding use the operands' own (input) dtype. + Type accTy = resTy.getElementType(); + + Value a = op.getFractalA() + ? dotFractalToND(rewriter, loc, adaptor.getA(), /*isLhs=*/true) + : adaptor.getA(); + Value b = op.getFractalB() + ? dotFractalToND(rewriter, loc, adaptor.getB(), /*isLhs=*/false) + : adaptor.getB(); + Type inElemTy = cast(a.getType()).getElementType(); + if (failed(reconcileDotContractionK(op, a, b, inElemTy, rewriter, loc))) + return failure(); + + // Uninitialized accumulator (accTy): mmadL1 (accumulate=false) overwrites it. + int64_t numRows = cast(a.getType()).getShape()[0]; + int64_t numCols = cast(b.getType()).getShape()[1]; + auto ndResultTy = RankedTensorType::get({numRows, numCols}, accTy); + Value acc = rewriter.create( + loc, ArrayRef{numRows, numCols}, accTy); + auto matmul = rewriter.create( + loc, TypeRange{ndResultTy}, ValueRange{a, b}, ValueRange{acc}); + matmul->setAttr("input_precision", rewriter.getStringAttr("ieee")); + Value ndResult = matmul->getResult(0); + + if (!op.getFractalC()) { + rewriter.replaceOp(op, ndResult); + return success(); + } + // fractal_c: ND result -> the L0C accumulator fractal (block [16, 16]). + Value fractalResult = rewriter.create( + loc, resTy, ndResult, /*srcLayout=*/dotNdLayout(ctx), + /*dstLayout=*/ + dotFractalLayout(ctx, triton::ascend::kFractalBlock, + triton::ascend::kFractalBlock), + DenseI64ArrayAttr::get(ctx, resTy.getShape()), /*output_shape=*/ + ValueRange{}); + rewriter.replaceOp(op, fractalResult); + return success(); +} + +LogicalResult +FlipOpConverter::matchAndRewrite(triton::ascend::FlipOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Value src = adaptor.getSrc(); + auto rankedSrcTy = cast(src.getType()); + + MLIRContext *ctx = rewriter.getContext(); + + Type valuesTy = src.getType(); + Location loc = op.getLoc(); + + auto dimAttr = op->getAttrOfType("dim"); + if (!dimAttr) { + op->emitError("missing 'dim' attribute"); + return failure(); + } + + auto moduleOp = op->getParentOfType(); + if (!moduleOp) { + op->emitError("must be inside a module"); + return failure(); + } + + // Unique callee name: triton_flip, triton_flip_1, … + std::string funcName = baseFuncName.str(); + int uniqueId = 0; + while (SymbolTable::lookupSymbolIn(moduleOp, funcName)) + funcName = (baseFuncName + Twine("_") + Twine(uniqueId++)).str(); + + auto i64Ty = IntegerType::get(ctx, 64); + auto libFnType = + rewriter.getFunctionType({rankedSrcTy, i64Ty}, {rankedSrcTy}); + + // Declare the callee + auto moduleIP = rewriter.saveInsertionPoint(); + rewriter.setInsertionPointToEnd(moduleOp.getBody()); + auto funcOp = rewriter.create(loc, funcName, libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + rewriter.restoreInsertionPoint(moduleIP); + + // dim constant + Value dimVal = + rewriter.create(loc, dimAttr.getInt(), 64); + + // Call the backend function + auto callee = SymbolRefAttr::get(ctx, funcOp.getSymName()); + auto callOp = rewriter.create( + loc, TypeRange({rankedSrcTy}), callee, ValueRange({src, dimVal})); + + Value finalValues = callOp.getResult(0); + + rewriter.replaceOp(op, {finalValues}); + return success(); +} + +LogicalResult +SortOpConverter::matchAndRewrite(triton::ascend::SortOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Value src = adaptor.getSrc(); + auto rankedSrcTy = cast(src.getType()); + auto srcElemTy = rankedSrcTy.getElementType(); + auto srcShape = rankedSrcTy.getShape(); + auto srcEnc = rankedSrcTy.getEncoding(); + + MLIRContext *ctx = rewriter.getContext(); + + Type backendElemTy = srcElemTy; + if (srcElemTy.isInteger(8)) { + backendElemTy = Float16Type::get(ctx); // i8 -> f16 + } else if (srcElemTy.isInteger(16)) { + backendElemTy = Float32Type::get(ctx); // i16 -> f32 + } + Type backendTensorTy = RankedTensorType::get(srcShape, backendElemTy, srcEnc); + + Type valuesTy = src.getType(); + + Location loc = op.getLoc(); + auto dimAttr = op->getAttrOfType("dim"); + auto descAttr = op->getAttrOfType("descending"); + if (!dimAttr || !descAttr) { + op->emitError("missing 'dim' or 'descending' attribute"); + return failure(); + } + + auto moduleOp = op->getParentOfType(); + if (!moduleOp) { + op->emitError("must be inside a module"); + return failure(); + } + + llvm::SmallString<64> baseName("triton_sort"); + llvm::SmallString<64> funcName = baseName; + int uniqueId = 0; + while (SymbolTable::lookupSymbolIn(moduleOp, funcName)) { + funcName = baseName; + funcName += ("_" + std::to_string(uniqueId++)); + } + + auto i64Ty = IntegerType::get(ctx, 64); + auto i1Ty = IntegerType::get(ctx, 1); + auto libFnType = rewriter.getFunctionType({backendTensorTy, i64Ty, i1Ty}, + {backendTensorTy}); + + auto moduleIP = rewriter.saveInsertionPoint(); + rewriter.setInsertionPointToEnd(moduleOp.getBody()); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + rewriter.restoreInsertionPoint(moduleIP); + + Value srcForCall = src; + if (backendElemTy != srcElemTy) { + srcForCall = rewriter.create(loc, backendTensorTy, src); + } + + Value dimVal = + rewriter.create(loc, dimAttr.getInt(), 64); + Value descVal = rewriter.create( + loc, descAttr.getValue() ? 1 : 0, 1); + + auto callee = SymbolRefAttr::get(ctx, funcOp.getSymName()); + auto callOp = + rewriter.create(loc, TypeRange({backendTensorTy}), callee, + ValueRange({srcForCall, dimVal, descVal})); + + Value valuesFloat = callOp.getResult(0); // tensor + + Value finalValues = valuesFloat; + if (backendElemTy != srcElemTy) { + finalValues = rewriter.create(loc, valuesTy, valuesFloat); + } + + rewriter.replaceOp(op, {finalValues}); + + return success(); +} + +LogicalResult +DotScaledConverter::matchAndRewrite(triton::DotScaledOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Location loc = op.getLoc(); + + Value lhs = adaptor.getA(); + Value rhs = adaptor.getB(); + Value c = adaptor.getC(); + Value lhsScale = adaptor.getAScale(); + Value rhsScale = adaptor.getBScale(); + RankedTensorType dstType = cast(op.getType()); + + auto lhsElemType = op.getAElemType(); + auto rhsElemType = op.getBElemType(); + + bool isFP8Input = (lhsElemType == triton::ScaleDotElemType::E4M3 || + lhsElemType == triton::ScaleDotElemType::E5M2) && + (rhsElemType == triton::ScaleDotElemType::E4M3 || + rhsElemType == triton::ScaleDotElemType::E5M2); + bool isFP4Input = (lhsElemType == triton::ScaleDotElemType::E2M1) && + (rhsElemType == triton::ScaleDotElemType::E2M1); + if ((isFP8Input || isFP4Input) && !lhsScale) { + auto lhsTy = cast(lhs.getType()); + int64_t k = lhsTy.getShape().back(); + if (lhsElemType == triton::ScaleDotElemType::E2M1 && op.getLhsKPack()) + k *= 2; + auto defaultScaleTy = RankedTensorType::get( + {dstType.getDimSize(dstType.getRank() - 2), (k + 31) / 32}, + rewriter.getI8Type()); + // The native MX path uses E8M0, whose encoding of one is 127. + Value defaultScaleVal = + rewriter.create(loc, rewriter.getI8IntegerAttr(127)); + Value defaultScaleEmpty = rewriter.create( + loc, defaultScaleTy.getShape(), defaultScaleTy.getElementType()); + lhsScale = rewriter + .create(loc, ValueRange{defaultScaleVal}, + ValueRange{defaultScaleEmpty}) + .getResult(0); + } + if (isFP8Input || isFP4Input) { + if (!rhsScale) { + auto rhsTy = cast(rhs.getType()); + int64_t k = rhsTy.getDimSize(rhsTy.getRank() - 2); + if (rhsElemType == triton::ScaleDotElemType::E2M1 && op.getRhsKPack()) + k *= 2; + auto defaultScaleTy = RankedTensorType::get( + {dstType.getShape().back(), (k + 31) / 32}, rewriter.getI8Type()); + // E8M0 encodes a scale factor of one with exponent bias 127. + Value defaultScaleVal = rewriter.create( + loc, rewriter.getI8IntegerAttr(127)); + Value defaultScaleEmpty = rewriter.create( + loc, defaultScaleTy.getShape(), defaultScaleTy.getElementType()); + rhsScale = rewriter + .create(loc, ValueRange{defaultScaleVal}, + ValueRange{defaultScaleEmpty}) + .getResult(0); + } + Value acc = c ? c + : rewriter.create(loc, dstType.getShape(), + dstType.getElementType()); + + // Get the ScaleDotElemType enum + // Helper to convert + auto convertFormat = + [&](triton::ScaleDotElemType ty) -> mlir::hfusion::DataformatAttr { + auto ctx = rewriter.getContext(); + switch (ty) { + case triton::ScaleDotElemType::E2M1: + return mlir::hfusion::DataformatAttr::get( + ctx, mlir::hfusion::Dataformat::FP4E2M1_T); + case triton::ScaleDotElemType::E4M3: + return mlir::hfusion::DataformatAttr::get( + ctx, mlir::hfusion::Dataformat::FP8E4M3_T); + case triton::ScaleDotElemType::E5M2: + return mlir::hfusion::DataformatAttr::get( + ctx, mlir::hfusion::Dataformat::FP8E5M2_T); + default: + llvm_unreachable("unsupported ScaleDotElemType"); + } + }; + + Value matmulMxResult = rewriter.create( + loc, dstType, lhs, rhs, lhsScale, rhsScale, acc, + /*lhsFormat(optional)*/ convertFormat(lhsElemType), + /*rhsFormat(optional)*/ convertFormat(rhsElemType)); + + Value finalResult = matmulMxResult; + if (dstType.getElementType().isBF16()) { + finalResult = + rewriter.create(loc, dstType, matmulMxResult); + } + rewriter.replaceOp(op, finalResult); + return success(); + } + + if (!lhsScale) { + return op.emitError("lhsScale is required for non-FP8 input"); + } + + RankedTensorType lhsTy = cast(lhs.getType()); + RankedTensorType lhsScaleTy = cast(lhsScale.getType()); + RankedTensorType rhsScaleTy = + rhsScale ? cast(rhsScale.getType()) : nullptr; + RankedTensorType rhsTy = cast(rhs.getType()); + + Value lhsScaleOut; + Value rhsScaleOut; + Value c127 = rewriter.create( + op.getLoc(), rewriter.getI16Type(), rewriter.getI16IntegerAttr(127)); + Value c7 = rewriter.create( + op.getLoc(), rewriter.getI16Type(), rewriter.getI16IntegerAttr(7)); + Type i16Ty = rewriter.getI16Type(); + Type bf16Ty = rewriter.getBF16Type(); + Type fp16Ty = rewriter.getF16Type(); + Type fp32Ty = rewriter.getF32Type(); + bool fastMath = op.getFastMath(); + + auto createNanSplat = [&](RankedTensorType tensorTy) -> Value { + auto floatTy = cast(tensorTy.getElementType()); + auto nanAttr = rewriter.getFloatAttr( + floatTy, APFloat::getNaN(floatTy.getFloatSemantics())); + Value empty = rewriter.create(loc, tensorTy.getShape(), + tensorTy.getElementType()); + return rewriter + .create( + loc, ValueRange{rewriter.create(loc, nanAttr)}, + ValueRange{empty}) + .getResult(0); + }; + + auto createNaNMask = [&](Value scaleTensor, + RankedTensorType scaleTy) -> Value { + if (scaleTy.getElementType().isIntOrIndex()) { + auto bitWidth = scaleTy.getElementTypeBitWidth(); + auto allOnes = APInt::getAllOnes(bitWidth); + auto sentinel = rewriter.create( + loc, scaleTy, DenseElementsAttr::get(scaleTy, allOnes)); + return rewriter + .create(loc, arith::CmpIPredicate::eq, scaleTensor, + sentinel) + .getResult(); + } + + return rewriter + .create(loc, arith::CmpFPredicate::UNO, scaleTensor, + scaleTensor) + .getResult(); + }; + + auto applyNaNMask = [&](Value valueTensor, Value maskTensor) -> Value { + auto valueTy = cast(valueTensor.getType()); + Value nanTensor = createNanSplat(valueTy); + return rewriter + .create(loc, maskTensor, nanTensor, valueTensor) + .getResult(); + }; + + if (lhsScaleTy.getElementType().isIntOrIndex()) { + RankedTensorType lhsScaleI16Ty = + RankedTensorType::get(lhsScaleTy.getShape(), i16Ty); + Value lhsScaleI16; + lhsScaleI16 = + rewriter.create(op.getLoc(), lhsScaleI16Ty, lhsScale); + + Value lhsShift7Empty = rewriter.create( + op.getLoc(), lhsScaleI16Ty.getShape(), i16Ty); + Value lhsShift7 = rewriter + .create(op.getLoc(), ValueRange{c7}, + ValueRange{lhsShift7Empty}) + .getResult(0); + Value lhsScaleI16Shifted = + rewriter.create(op.getLoc(), lhsScaleI16, lhsShift7); + + RankedTensorType lhsScaleBF16Ty = + RankedTensorType::get(lhsScaleTy.getShape(), bf16Ty); + Value lhsScaleBF16 = rewriter.create( + op.getLoc(), lhsScaleBF16Ty, lhsScaleI16Shifted); + if (lhsTy.getElementType() == fp16Ty) { + RankedTensorType lhsScaleFp32Ty = + RankedTensorType::get(lhsScaleTy.getShape(), fp32Ty); + Value lhsScaleFp32 = rewriter.create( + op.getLoc(), lhsScaleFp32Ty, lhsScaleBF16); + RankedTensorType lhsScaleFp16Ty = + RankedTensorType::get(lhsScaleTy.getShape(), fp16Ty); + lhsScaleOut = rewriter.create( + op.getLoc(), lhsScaleFp16Ty, lhsScaleFp32); + } else { + lhsScaleOut = lhsScaleBF16; + } + } else { + lhsScaleOut = + rewriter + .create( + op.getLoc(), + RankedTensorType::get(lhsScaleTy.getShape(), fp32Ty), lhsScale) + .getResult(); + } + + if (rhsScale && rhsScaleTy.getElementType().isIntOrIndex()) { + if (rhsScaleTy.getRank() != 2) { + return op.emitError("rhsScale must be 2D for transpose"); + } + + SmallVector transposedShape = {rhsScaleTy.getShape()[1], + rhsScaleTy.getShape()[0]}; + RankedTensorType transposedRhsScaleTy = + RankedTensorType::get(transposedShape, rhsScaleTy.getElementType()); + + Value transposedRhsScale = rewriter.create( + op.getLoc(), transposedRhsScaleTy, rhsScale, + DenseI32ArrayAttr::get(rewriter.getContext(), ArrayRef{1, 0})); + RankedTensorType rhsScaleI16Ty = + RankedTensorType::get(transposedShape, i16Ty); + Value rhsScaleI16; + rhsScaleI16 = rewriter.create(op.getLoc(), rhsScaleI16Ty, + transposedRhsScale); + + Value rhsShift7Empty = rewriter.create( + op.getLoc(), rhsScaleI16Ty.getShape(), i16Ty); + Value rhsShift7 = rewriter + .create(op.getLoc(), ValueRange{c7}, + ValueRange{rhsShift7Empty}) + .getResult(0); + Value rhsScaleI16Shifted = + rewriter.create(op.getLoc(), rhsScaleI16, rhsShift7); + + RankedTensorType rhsScaleBF16Ty = + RankedTensorType::get(transposedShape, bf16Ty); + Value rhsScaleBF16 = rewriter.create( + op.getLoc(), rhsScaleBF16Ty, rhsScaleI16Shifted); + + if (rhsTy.getElementType() == fp16Ty) { + RankedTensorType rhsScaleFp32Ty = + RankedTensorType::get(transposedShape, fp32Ty); + Value rhsScaleFp32 = rewriter.create( + op.getLoc(), rhsScaleFp32Ty, rhsScaleBF16); + RankedTensorType rhsScaleFp16Ty = + RankedTensorType::get(transposedShape, fp16Ty); + rhsScaleOut = rewriter.create( + op.getLoc(), rhsScaleFp16Ty, rhsScaleFp32); + } else { + rhsScaleOut = rhsScaleBF16; + } + int64_t rhsD0 = rhsScaleTy.getShape()[1]; + int64_t rhsD1 = rhsScaleTy.getShape()[0]; + SmallVector rhsExpandedShape1 = {rhsD0, rhsD1, 1}; + RankedTensorType rhsExpandedTy1 = + RankedTensorType::get(rhsExpandedShape1, rhsTy.getElementType()); + Value rhsExpanded1 = rewriter + .create( + op.getLoc(), rhsExpandedTy1, rhsScaleOut, + rewriter.getI32IntegerAttr(2)) + .getResult(); + + int64_t rhsDim1 = rhsTy.getShape()[0]; + if (rhsDim1 % rhsD0 != 0) { + return op.emitError( + "rhs dim0 must be an integer multiple of rhsScale dim0"); + } + int64_t rhsD2 = rhsDim1 / rhsD0; + SmallVector rhsBroadcastShape = {rhsD0, rhsD1, rhsD2}; + RankedTensorType rhsBroadcastTy = + RankedTensorType::get(rhsBroadcastShape, rhsTy.getElementType()); + Value rhsBroadcasted = rewriter + .create( + op.getLoc(), rhsBroadcastTy, rhsExpanded1) + .getResult(); + + SmallVector transposeOrder = {0, 2, 1}; + Value transposedBroadcasted = rewriter.create( + op.getLoc(), + RankedTensorType::get({rhsD0, rhsD2, rhsD1}, rhsTy.getElementType()), + rhsBroadcasted, + DenseI32ArrayAttr::get(rewriter.getContext(), transposeOrder)); + SmallVector rhsReassociation; + rhsReassociation.push_back({0, 1}); + rhsReassociation.push_back({2}); + + Value scaledRhs = rewriter + .create( + op.getLoc(), + RankedTensorType::get({rhsD0 * rhsD2, rhsD1}, + rhsTy.getElementType()), + transposedBroadcasted, rhsReassociation) + .getResult(); + + rhs = + rewriter.create(op.getLoc(), rhs, scaledRhs).getResult(); + + if (!fastMath) { + Value rhsScaleNaNMask = + createNaNMask(transposedRhsScale, transposedRhsScaleTy); + Value rhsExpandedMask = + rewriter + .create( + op.getLoc(), + RankedTensorType::get(rhsExpandedShape1, + rewriter.getI1Type()), + rhsScaleNaNMask, rewriter.getI32IntegerAttr(2)) + .getResult(); + Value rhsBroadcastMask = + rewriter + .create( + op.getLoc(), + RankedTensorType::get(rhsBroadcastShape, + rewriter.getI1Type()), + rhsExpandedMask) + .getResult(); + Value transposedBroadcastMask = + rewriter + .create( + op.getLoc(), + RankedTensorType::get({rhsD0, rhsD2, rhsD1}, + rewriter.getI1Type()), + rhsBroadcastMask, + DenseI32ArrayAttr::get(rewriter.getContext(), transposeOrder)) + .getResult(); + Value collapsedRhsMask = + rewriter + .create( + op.getLoc(), + RankedTensorType::get({rhsD0 * rhsD2, rhsD1}, + rewriter.getI1Type()), + transposedBroadcastMask, rhsReassociation) + .getResult(); + rhs = applyNaNMask(rhs, collapsedRhsMask); + } + } + + int64_t D0 = lhsScaleTy.getShape()[0]; + int64_t D1 = lhsScaleTy.getShape()[1]; + SmallVector expandedShape1 = {D0, D1, 1}; + RankedTensorType expandedTy1 = + RankedTensorType::get(expandedShape1, lhsTy.getElementType()); + Value expanded1 = + rewriter + .create(op.getLoc(), expandedTy1, lhsScaleOut, + rewriter.getI32IntegerAttr(2)) + .getResult(); + + int64_t lhsDim1 = lhsTy.getShape()[1]; + if (lhsDim1 % D1 != 0) { + return op.emitError( + "lhs dim1 must be an integer multiple of lhsScale dim1"); + } + int64_t D2 = lhsDim1 / D1; + SmallVector broadcastShape = {D0, D1, D2}; + RankedTensorType broadcastTy = + RankedTensorType::get(broadcastShape, lhsTy.getElementType()); + Value broadcasted = + rewriter.create(op.getLoc(), broadcastTy, expanded1) + .getResult(); + + SmallVector reassociation; + reassociation.push_back({0}); + reassociation.push_back({1, 2}); + + Value scaledLhs = + rewriter + .create( + op.getLoc(), + RankedTensorType::get({D0, D1 * D2}, lhsTy.getElementType()), + broadcasted, reassociation) + .getResult(); + + Value scaledLhsFinal = + rewriter.create(op.getLoc(), lhs, scaledLhs).getResult(); + + if (!fastMath) { + Value lhsScaleNaNMask = createNaNMask(lhsScale, lhsScaleTy); + Value lhsExpandedMask = + rewriter + .create( + op.getLoc(), + RankedTensorType::get(expandedShape1, rewriter.getI1Type()), + lhsScaleNaNMask, rewriter.getI32IntegerAttr(2)) + .getResult(); + Value lhsBroadcastMask = + rewriter + .create( + op.getLoc(), + RankedTensorType::get(broadcastShape, rewriter.getI1Type()), + lhsExpandedMask) + .getResult(); + Value collapsedLhsMask = + rewriter + .create( + op.getLoc(), + RankedTensorType::get({D0, D1 * D2}, rewriter.getI1Type()), + lhsBroadcastMask, reassociation) + .getResult(); + scaledLhsFinal = applyNaNMask(scaledLhsFinal, collapsedLhsMask); + } + + Operation *matmulOp; + if (dstType.getRank() == 2) { + matmulOp = rewriter.create( + op.getLoc(), ValueRange{scaledLhsFinal, rhs}, ValueRange{c}); + } else if (dstType.getRank() == 3) { + matmulOp = rewriter.create( + op.getLoc(), ValueRange{scaledLhsFinal, rhs}, ValueRange{c}); + } else { + return op.emitError("DotScaledOp only support 2D or 3D tensor"); + } + + rewriter.replaceOp(op, matmulOp->getResults()); + return success(); +} + +LogicalResult +IntToPtrConverter::matchAndRewrite(triton::IntToPtrOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto pointerType = dyn_cast(op.getType()); + if (!pointerType || isa(pointerType.getPointeeType())) + return rewriter.notifyMatchFailure( + op, "only scalar pointer reconstruction is supported"); + + const TypeConverter *typeConverter = getTypeConverter(); + if (!typeConverter) + return rewriter.notifyMatchFailure(op, "requires a type converter"); + auto resultType = + dyn_cast(typeConverter->convertType(op.getType())); + if (!resultType) + return rewriter.notifyMatchFailure( + op, "pointer result did not convert to a memref type"); + + // Rebuild the memref only after the integer address has crossed control + // flow. Selecting an i64 address is a pure SSA operation and avoids the + // backend interpreting a memref-valued merge as a GM-to-UB copy. + auto pointerCast = createScalarPointerCast(rewriter, op.getLoc(), resultType, + adaptor.getSrc()); + rewriter.replaceOp(op, pointerCast.getResult()); + return success(); +} + +LogicalResult +PtrToIntConverter::matchAndRewrite(triton::PtrToIntOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + FailureOr address = + materializePointerAddress(adaptor.getSrc(), op.getLoc(), rewriter); + if (failed(address)) { + return rewriter.notifyMatchFailure(op, "input is not a memref type"); + } + + rewriter.replaceOp(op, *address); + return success(); +} + +LogicalResult +IndexPutConverter::matchAndRewrite(triton::ascend::IndexPutOp op, + OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + auto funcName = generateUniqueFuncName(moduleOp, funcNameBase); + + auto ptr = adaptor.getPtr(); + auto index = op.getIndex(); + auto value = op.getValue(); + auto dim = op.getDim(); + auto indexBoundary = op.getIndexBoundary(); + auto endOffset = op.getEndOffset(); + auto startOffset = op.getStartOffset(); + auto dstStride = adaptor.getDstStride(); + + // convert !tt.ptr to memref + auto ptrTy = dyn_cast(ptr.getType()); + if (!ptrTy) { + return rewriter.notifyMatchFailure(op, "expected MemRefType for ptr"); + } + SmallVector inputTypes({ptrTy, index.getType(), value.getType(), + dim.getType(), indexBoundary.getType()}); + inputTypes.append(endOffset.getTypes().begin(), endOffset.getTypes().end()); + inputTypes.append(startOffset.getTypes().begin(), + startOffset.getTypes().end()); + inputTypes.append(dstStride.getTypes().begin(), dstStride.getTypes().end()); + auto libFnType = rewriter.getFunctionType(inputTypes, {}); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + + rewriter.setInsertionPoint(op); + SmallVector inputVals({ptr, index, value, dim, indexBoundary}); + inputVals.append(endOffset.begin(), endOffset.end()); + inputVals.append(startOffset.begin(), startOffset.end()); + inputVals.append(dstStride.begin(), dstStride.end()); + rewriter.create(loc, funcOp.getSymNameAttr(), TypeRange({}), + inputVals); + rewriter.eraseOp(op); + return success(); +} + +LogicalResult GatherOutToUbConverter::matchAndRewrite( + triton::ascend::GatherOutToUbOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + auto funcName = generateUniqueFuncName(moduleOp, funcNameBase); + + auto src = adaptor.getSrc(); + auto index = op.getIndex(); + auto indexBoundary = op.getIndexBoundary(); + auto dim = op.getDim(); + auto srcStride = op.getSrcStride(); + auto endOffset = op.getEndOffset(); + auto startOffset = op.getStartOffset(); + auto other = op.getOther(); + + auto res = op.getResult(); + auto resTy = res.getType(); + + // convert !tt.ptr to memref + auto srcTy = dyn_cast(src.getType()); + if (!srcTy) { + return rewriter.notifyMatchFailure(op, "expected MemRefType for src"); + } + + SmallVector inputTypes( + {srcTy, index.getType(), indexBoundary.getType(), dim.getType()}); + inputTypes.append(srcStride.getTypes().begin(), srcStride.getTypes().end()); + inputTypes.append(endOffset.getTypes().begin(), endOffset.getTypes().end()); + inputTypes.append(startOffset.getTypes().begin(), + startOffset.getTypes().end()); + if (other) + inputTypes.push_back(other.getType()); + + auto libFnType = rewriter.getFunctionType(inputTypes, {resTy}); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + + rewriter.setInsertionPoint(op); + SmallVector inputVals({src, index, indexBoundary, dim}); + inputVals.append(srcStride.begin(), srcStride.end()); + inputVals.append(endOffset.begin(), endOffset.end()); + inputVals.append(startOffset.begin(), startOffset.end()); + if (other) + inputVals.push_back(other); + auto callOp = rewriter.create(loc, funcOp.getSymNameAttr(), + TypeRange({resTy}), inputVals); + rewriter.replaceOp(op, callOp); + return success(); +} + +LogicalResult ScatterUbToOutConverter::matchAndRewrite( + triton::ascend::ScatterUbToOutOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + auto funcName = generateUniqueFuncName(moduleOp, funcNameBase); + + auto ptr = adaptor.getPtr(); + auto value = op.getValue(); + auto index = op.getIndex(); + auto indexBoundary = op.getIndexBoundary(); + auto dim = op.getDim(); + auto dstStride = op.getDstStride(); + auto endOffset = op.getEndOffset(); + auto startOffset = op.getStartOffset(); + + // convert !tt.ptr to memref + auto ptrTy = dyn_cast(ptr.getType()); + if (!ptrTy) { + return rewriter.notifyMatchFailure(op, "expected MemRefType for ptr"); + } + + SmallVector inputTypes({ptrTy, value.getType(), index.getType(), + indexBoundary.getType(), dim.getType()}); + inputTypes.append(dstStride.getTypes().begin(), dstStride.getTypes().end()); + inputTypes.append(endOffset.getTypes().begin(), endOffset.getTypes().end()); + inputTypes.append(startOffset.getTypes().begin(), + startOffset.getTypes().end()); + + auto libFnType = rewriter.getFunctionType(inputTypes, {}); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + + rewriter.setInsertionPoint(op); + SmallVector inputVals({ptr, value, index, indexBoundary, dim}); + inputVals.append(dstStride.begin(), dstStride.end()); + inputVals.append(endOffset.begin(), endOffset.end()); + inputVals.append(startOffset.begin(), startOffset.end()); + rewriter.create(loc, funcOp.getSymNameAttr(), TypeRange({}), + inputVals); + rewriter.eraseOp(op); + return success(); +} + +LogicalResult IndirectLoadConverter::matchAndRewrite( + triton::ascend::IndirectLoadOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + auto funcName = generateUniqueFuncName(moduleOp, funcNameBase); + + auto src = adaptor.getSrc(); + auto offsets = op.getOffsets(); + auto mask = op.getMask(); + auto other = op.getOther(); + auto res = op.getResult(); + auto resTy = res.getType(); + + // The adaptor may already expose the dynamic carrier created by + // IntToPtrConverter. Re-materialize that narrow provenance before the + // MemRefType fast path, otherwise the indirect helper receives memref + // instead of the required one-element identity descriptor. + if (isIntToPtrBasedScalarPointer(op.getSrc()) || + (op.getIsVolatile() && isVolatileBitcastScalarPointer(op.getSrc()))) { + llvm::SmallDenseMap known; + FailureOr materialized = + BlockDataParser::materializePointer(op.getSrc(), rewriter, known); + if (failed(materialized)) + return rewriter.notifyMatchFailure( + op, "unable to materialize int_to_ptr indirect-load source"); + src = *materialized; + } else if (!isa(src.getType())) { + llvm::SmallDenseMap known; + FailureOr materialized = + BlockDataParser::materializePointer(op.getSrc(), rewriter, known); + if (failed(materialized)) + return rewriter.notifyMatchFailure( + op, "unable to materialize indirect-load source as a memref"); + src = *materialized; + } + // convert !tt.ptr to memref + auto srcTy = dyn_cast(src.getType()); + if (!srcTy) { + return rewriter.notifyMatchFailure(op, "expected MemRefType for src"); + } + SmallVector inputTypes({srcTy, offsets.getType()}); + if (mask) + inputTypes.push_back(mask.getType()); + if (other) + inputTypes.push_back(other.getType()); + auto libFnType = rewriter.getFunctionType(inputTypes, {resTy}); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + auto isVolatileAttr = rewriter.getBoolAttr(op.getIsVolatile()); + funcOp->setAttr("isVolatile", isVolatileAttr); + + rewriter.setInsertionPoint(op); + SmallVector inputVals({src, offsets}); + if (mask) + inputVals.push_back(mask); + if (other) + inputVals.push_back(other); + auto callOp = rewriter.create(loc, funcOp.getSymNameAttr(), + TypeRange({resTy}), inputVals); + callOp->setAttr("isVolatile", isVolatileAttr); + rewriter.replaceOp(op, callOp); + return success(); +} + +LogicalResult StrideLoadConverter::matchAndRewrite( + triton::ascend::StrideLoadOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + auto funcName = generateUniqueFuncName(moduleOp, funcNameBase); + + auto src = adaptor.getSrc(); + auto offset = adaptor.getOffset(); + auto other = adaptor.getOther(); + auto strides = adaptor.getStride(); + auto numels = adaptor.getNumel(); + auto resTy = op.getResult().getType(); + + auto srcTy = dyn_cast(src.getType()); + if (!srcTy) { + return rewriter.notifyMatchFailure(op, "expected MemRefType for src"); + } + + SmallVector inputTypes({srcTy}); + inputTypes.push_back(offset.getType()); + inputTypes.push_back(other.getType()); + for (Value stride : strides) + inputTypes.push_back(stride.getType()); + for (Value numel : numels) + inputTypes.push_back(numel.getType()); + auto libFnType = rewriter.getFunctionType(inputTypes, {resTy}); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + + SmallVector inputVals({src}); + inputVals.push_back(offset); + inputVals.push_back(other); + inputVals.append(strides.begin(), strides.end()); + inputVals.append(numels.begin(), numels.end()); + + rewriter.setInsertionPoint(op); + auto callOp = rewriter.create(loc, funcOp.getSymNameAttr(), + TypeRange({resTy}), inputVals); + rewriter.replaceOp(op, callOp); + return success(); +} + +LogicalResult StrideStoreConverter::matchAndRewrite( + triton::ascend::StrideStoreOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + auto funcName = generateUniqueFuncName(moduleOp, funcNameBase); + + auto dst = adaptor.getDst(); + auto src = adaptor.getSrc(); + auto offset = adaptor.getOffset(); + auto strides = adaptor.getStride(); + auto numels = adaptor.getNumel(); + + auto dstTy = dyn_cast(dst.getType()); + if (!dstTy) { + return rewriter.notifyMatchFailure(op, "expected MemRefType for dst"); + } + + SmallVector inputTypes({dstTy, src.getType(), offset.getType()}); + for (Value stride : strides) + inputTypes.push_back(stride.getType()); + for (Value numel : numels) + inputTypes.push_back(numel.getType()); + auto libFnType = rewriter.getFunctionType(inputTypes, {}); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + + SmallVector inputVals({dst, src, offset}); + inputVals.append(strides.begin(), strides.end()); + inputVals.append(numels.begin(), numels.end()); + + rewriter.setInsertionPoint(op); + rewriter.create(loc, funcOp.getSymNameAttr(), TypeRange({}), + inputVals); + rewriter.eraseOp(op); + return success(); +} + +LogicalResult IndirectStoreConverter::matchAndRewrite( + triton::ascend::IndirectStoreOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + auto moduleOp = op->getParentOfType(); + rewriter.setInsertionPoint(moduleOp.getBody(), + std::prev(moduleOp.getBody()->end())); + + auto funcName = generateUniqueFuncName(moduleOp, funcNameBase); + + auto src = adaptor.getSrc(); + auto offsets = op.getOffsets(); + auto value = op.getValue(); + auto mask = op.getMask(); + + // Keep the indirect-store ABI consistent with indirect-load for a scalar + // address reconstructed by tt.int_to_ptr. Ordinary dynamic memrefs retain + // the existing direct path. + if (isIntToPtrBasedScalarPointer(op.getSrc())) { + llvm::SmallDenseMap known; + FailureOr materialized = + BlockDataParser::materializePointer(op.getSrc(), rewriter, known); + if (failed(materialized)) + return rewriter.notifyMatchFailure( + op, "unable to materialize int_to_ptr indirect-store source"); + src = *materialized; + } else if (!isa(src.getType())) { + llvm::SmallDenseMap known; + FailureOr materialized = + BlockDataParser::materializePointer(op.getSrc(), rewriter, known); + if (failed(materialized)) + return rewriter.notifyMatchFailure( + op, "unable to materialize indirect-store source as a memref"); + src = *materialized; + } + // convert !tt.ptr to memref + auto srcTy = dyn_cast(src.getType()); + if (!srcTy) { + return rewriter.notifyMatchFailure(op, "expected MemRefType for src"); + } + SmallVector inputTypes({srcTy, offsets.getType(), value.getType()}); + if (mask) + inputTypes.push_back(mask.getType()); + + auto libFnType = rewriter.getFunctionType(inputTypes, {}); + auto funcOp = rewriter.create(loc, funcName.str(), libFnType); + SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private); + + rewriter.setInsertionPoint(op); + SmallVector inputVals({src, offsets, value}); + if (mask) + inputVals.push_back(mask); + rewriter.create(loc, funcOp.getSymNameAttr(), TypeRange({}), + inputVals); + rewriter.eraseOp(op); + return success(); +} + +IndexSelectSimdConverter::IndexSelectSimdConverter(MLIRContext *context) + : OpConversionPattern(context) {} + +LogicalResult IndexSelectSimdConverter::matchAndRewrite( + triton::ascend::IndexSelectSimdOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + // Get converted operands + Value src = adaptor.getSrc(); + Value indexTensor = adaptor.getIndex(); + auto srcShapeVals = adaptor.getSrcShape(); + auto srcOffsetVals = adaptor.getSrcOffset(); + auto readShapeAttr = op.getReadShape(); + int32_t dim = op.getDim(); + + // Get result type + auto resultTensorType = cast(op.getResult().getType()); + static constexpr llvm::StringLiteral wasBoolToInt8AttrName = + "was_bool_to_int8"; + bool wasBoolToInt8 = op->hasAttr(wasBoolToInt8AttrName); + + auto elemType = resultTensorType.getElementType(); + auto resultShape = resultTensorType.getShape(); + + // Convert src (tt.ptr -> memref) to the correct memref shape + // src is now memref after type conversion, need to reinterpret to full + // shape + auto srcMemRefType = cast(src.getType()); + + // DenseI32ArrayAttr can be implicitly converted to ArrayRef + ArrayRef readShape = readShapeAttr; + + // Helper lambda to convert Value to Index type if needed + auto toIndexValue = [&](Value val) -> Value { + if (!val.getType().isIndex()) { + return rewriter.create(loc, rewriter.getIndexType(), + val); + } + return val; + }; + + // Build multi-dimensional memref type and determine static sizes + // Merge two passes: build fullSrcShape and staticSizes in one loop + SmallVector fullSrcShape; + SmallVector staticSizes; + SmallVector sizes; + + for (size_t i = 0; i < srcShapeVals.size(); ++i) { + bool isDynamicDim = (i == static_cast(dim) && readShape[i] == -1); + int64_t staticSize; + + if (isDynamicDim) { + // Dynamic dimension: readShape[i] == -1 indicates dynamic + staticSize = ShapedType::kDynamic; + fullSrcShape.push_back(ShapedType::kDynamic); + sizes.push_back(toIndexValue(srcShapeVals[i])); + } else if (auto constOp = + srcShapeVals[i].getDefiningOp()) { + // Static dimension: use constant value + staticSize = constOp.value(); + fullSrcShape.push_back(staticSize); + } else { + // Runtime value: must use dynamic + staticSize = ShapedType::kDynamic; + fullSrcShape.push_back(ShapedType::kDynamic); + sizes.push_back(toIndexValue(srcShapeVals[i])); + } + staticSizes.push_back(staticSize); + } + auto fullSrcMemRefType = MemRefType::get(fullSrcShape, elemType); + + // Build static offsets, sizes, and strides for ReinterpretCastOp + SmallVector offsets, strides; + SmallVector staticOffsets, staticStrides; + staticOffsets.push_back(0); // offsets are 0 + + // Calculate static strides: stride[i] = product of all dimensions after i + for (size_t i = 0; i < srcShapeVals.size(); ++i) { + int64_t staticStride = 1; + bool isDynamic = false; + + // Check if stride needs to be dynamic (any dimension after i is dynamic) + for (size_t j = i + 1; j < srcShapeVals.size(); ++j) { + if (staticSizes[j] == ShapedType::kDynamic) { + isDynamic = true; + break; + } + staticStride *= staticSizes[j]; + } + + if (isDynamic) { + staticStride = ShapedType::kDynamic; + // Compute stride dynamically: stride[i] = product of sizes after i + Value strideVal = rewriter.create(loc, 1); + for (size_t j = i + 1; j < srcShapeVals.size(); ++j) { + if (staticSizes[j] != ShapedType::kDynamic) { + strideVal = rewriter.create( + loc, strideVal, + rewriter.create(loc, staticSizes[j])); + } else { + // Dynamic dimension: use runtime value + strideVal = rewriter.create( + loc, strideVal, toIndexValue(srcShapeVals[j])); + } + } + strides.push_back(strideVal); + } + staticStrides.push_back(staticStride); + } + + auto srcMemRef = rewriter.create( + loc, fullSrcMemRefType, src, offsets, sizes, strides, staticOffsets, + staticSizes, staticStrides); + + // Allocate output buffer + auto resultMemRefType = MemRefType::get(resultShape, elemType); + auto outputBuffer = rewriter.create(loc, resultMemRefType); + + // Get indices tensor type for extracting + auto indicesTensorType = cast(indexTensor.getType()); + int64_t numIndices = indicesTensorType.getShape()[0]; + + // Create for loop + auto zeroIdx = rewriter.create(loc, 0); + auto numIndicesVal = rewriter.create(loc, numIndices); + auto stepOne = rewriter.create(loc, 1); + auto forOp = + rewriter.create(loc, zeroIdx, numIndicesVal, stepOne); + + // Mark as parallel loop + forOp->setAttr("hivm.parallel_loop", rewriter.getUnitAttr()); + + // Build loop body + Block *loopBody = forOp.getBody(); + auto savedInsertionPoint = rewriter.saveInsertionPoint(); + rewriter.setInsertionPointToStart(loopBody); + + // Remove the terminator temporarily + Operation *terminator = &loopBody->back(); + rewriter.setInsertionPoint(terminator); + + Value iv = forOp.getInductionVar(); + + // Extract index from indices tensor + Value selectedIdx = + rewriter.create(loc, indexTensor, ValueRange{iv}); + Value selectedIdxAsIndex = rewriter.create( + loc, rewriter.getIndexType(), selectedIdx); + + // Build source subview offsets/sizes/strides + SmallVector srcSubviewOffsets, srcSubviewSizes, + srcSubviewStrides; + + for (size_t i = 0; i < srcOffsetVals.size(); ++i) { + if (i == static_cast(dim)) { + // Use the selected index for this dimension + srcSubviewOffsets.push_back(selectedIdxAsIndex); + srcSubviewSizes.push_back(rewriter.getIndexAttr(1)); + } else { + // Use provided offset and read size for other dimensions + Value offsetVal = srcOffsetVals[i]; + if (!offsetVal.getType().isIndex()) { + offsetVal = rewriter.create( + loc, rewriter.getIndexType(), offsetVal); + } + srcSubviewOffsets.push_back(offsetVal); + srcSubviewSizes.push_back(rewriter.getIndexAttr(readShape[i])); + } + srcSubviewStrides.push_back(rewriter.getIndexAttr(1)); + } + + auto srcSubview = rewriter.create( + loc, srcMemRef, srcSubviewOffsets, srcSubviewSizes, srcSubviewStrides); + + // Build destination subview + SmallVector dstSubviewOffsets, dstSubviewSizes, + dstSubviewStrides; + for (size_t i = 0; i < resultShape.size(); ++i) { + if (i == static_cast(dim)) { + dstSubviewOffsets.push_back(iv); + dstSubviewSizes.push_back(rewriter.getIndexAttr(1)); + } else { + dstSubviewOffsets.push_back(rewriter.getIndexAttr(0)); + dstSubviewSizes.push_back(rewriter.getIndexAttr(readShape[i])); + } + dstSubviewStrides.push_back(rewriter.getIndexAttr(1)); + } + + auto dstSubview = rewriter.create( + loc, outputBuffer, dstSubviewOffsets, dstSubviewSizes, dstSubviewStrides); + + // Check if index_select is on the trailing axis (last dimension) + if (static_cast(dim) == fullSrcShape.size() - 1) { + // For index_select on the trailing axis, mark as discrete memory access + // This degrades to scalar read/write handling to avoid alignment issues + auto copyOp = rewriter.create(loc, srcSubview, dstSubview); + if (wasBoolToInt8) + copyOp->setAttr(wasBoolToInt8AttrName, rewriter.getBoolAttr(true)); + copyOp->setAttr(ConverterUtils::discreteAttrName, rewriter.getUnitAttr()); + } else { + // For index_select on non-trailing axes, add stride alignment annotation + // This tells the backend to handle address alignment for DMA operations + auto dstMarkOp = rewriter.create(loc, dstSubview); + dstMarkOp->setAttr( + "hfusion.stride_align_dims", + rewriter.getDenseI32ArrayAttr({static_cast(dim)})); + dstMarkOp->setAttr("hfusion.stride_align_value_in_byte", + rewriter.getDenseI32ArrayAttr({32})); + + // Copy from source to destination + auto copyOp = rewriter.create(loc, srcSubview, dstSubview); + if (wasBoolToInt8) + copyOp->setAttr(wasBoolToInt8AttrName, rewriter.getBoolAttr(true)); + } + + // Restore insertion point + rewriter.restoreInsertionPoint(savedInsertionPoint); + + // Convert memref to tensor + auto resultTensor = rewriter.create( + loc, resultTensorType, outputBuffer, true, true); + if (wasBoolToInt8) + resultTensor->setAttr(wasBoolToInt8AttrName, rewriter.getBoolAttr(true)); + + // Mark as index_select_simd + resultTensor->setAttr("index_select_simd", rewriter.getUnitAttr()); + + // Replace the original op + rewriter.replaceOp(op, resultTensor); + + return success(); +} + +LogicalResult +HistogramConverter::matchAndRewrite(triton::HistogramOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + auto loc = op.getLoc(); + Value input = adaptor.getSrc(); + Value mask = adaptor.getMask(); + auto resultType = dyn_cast(op.getResult().getType()); + if (!resultType || !resultType.hasStaticShape()) { + return rewriter.notifyMatchFailure(op, + "expected static shaped tensor result"); + } + + int64_t numBins = resultType.getDimSize(0); + + auto outputTensor = rewriter.create( + loc, resultType.getShape(), resultType.getElementType()); + + auto zeroVal = rewriter.create( + loc, rewriter.getIntegerAttr(resultType.getElementType(), 0)); + auto fillOp = rewriter.create(loc, ValueRange{zeroVal}, + ValueRange{outputTensor}); + + Value numBinsVal = rewriter.create(loc, numBins, 64); + + SmallVector inputs = {input, numBinsVal}; + if (mask) { + inputs.push_back(mask); + } + + auto customOp = rewriter.create( + loc, TypeRange{resultType}, "__builtin_histogram", ValueRange{inputs}, + ValueRange{fillOp.getResult(0)}, ValueRange{}); + + customOp->setAttr("symbol", rewriter.getStringAttr("__builtin_histogram")); + customOp->setAttr( + "hivm.tcore_type", + hivm::TCoreTypeAttr::get(rewriter.getContext(), hivm::TCoreType::VECTOR)); + customOp->setAttr("hivm.vf_mode", hivm::VFModeAttr::get(rewriter.getContext(), + hivm::VFMode::SIMT)); + customOp->setAttr("hivm.pipe", hivm::PipeAttr::get(rewriter.getContext(), + hivm::PIPE::PIPE_V)); + customOp->setAttr("gm_addr_args_indices", + DenseI32ArrayAttr::get(rewriter.getContext(), {})); + + rewriter.replaceOp(op, customOp->getResult(0)); + return success(); +} +// ===----------------------------------------------------------------------=== +// MapElementwiseDecomposeConverter +// ===----------------------------------------------------------------------=== + +// Promote a single scalar op to tensor-level. Returns the tensor result +// values after recording them in valueMap. +SmallVector MapElementwiseDecomposeConverter::promoteOp( + Operation *op, llvm::DenseMap &valueMap, OpBuilder &builder, + Location loc, ArrayRef tensorShape) const { + + // ---- helpers ---- + auto getTensorType = [&](Type scalarTy) -> RankedTensorType { + return RankedTensorType::get(tensorShape, scalarTy); + }; + + auto createInit = [&](Type scalarTy) -> Value { + return builder.create(loc, tensorShape, scalarTy); + }; + + auto getOperand = [&](Value v) -> Value { + auto it = valueMap.find(v); + if (it != valueMap.end()) + return it->second; + // External constant defined outside the region — promote on first use + if (auto cst = v.getDefiningOp()) { + auto scalarTy = cst.getResult().getType(); + Value init = createInit(scalarTy); + auto fill = builder.create(loc, cst.getResult(), init); + valueMap[v] = fill.getResult(0); + return fill.getResult(0); + } + llvm_unreachable("operand not in valueMap and not a constant"); + }; + + auto record = [&](Value oldVal, Value newVal) -> SmallVector { + valueMap[oldVal] = newVal; + return {newVal}; + }; + + // ---- binary arith: Op(lhs, rhs) → tensor Op(lhs, rhs) ---- +#define PROMOTE_BINARY(OpTy) \ + if (auto a = dyn_cast(op)) { \ + auto r = builder.create(loc, getOperand(a.getLhs()), \ + getOperand(a.getRhs())); \ + return record(a.getResult(), r.getResult()); \ + } + + PROMOTE_BINARY(arith::AddIOp) + PROMOTE_BINARY(arith::SubIOp) + PROMOTE_BINARY(arith::MulIOp) + PROMOTE_BINARY(arith::DivSIOp) + PROMOTE_BINARY(arith::DivUIOp) + PROMOTE_BINARY(arith::RemSIOp) + PROMOTE_BINARY(arith::RemUIOp) + PROMOTE_BINARY(arith::MaxSIOp) + PROMOTE_BINARY(arith::MinSIOp) + PROMOTE_BINARY(arith::MaxUIOp) + PROMOTE_BINARY(arith::MinUIOp) + PROMOTE_BINARY(arith::AddFOp) + PROMOTE_BINARY(arith::SubFOp) + PROMOTE_BINARY(arith::MulFOp) + PROMOTE_BINARY(arith::DivFOp) + PROMOTE_BINARY(arith::RemFOp) + PROMOTE_BINARY(arith::MaxNumFOp) + PROMOTE_BINARY(arith::MinNumFOp) + PROMOTE_BINARY(arith::MaximumFOp) + PROMOTE_BINARY(arith::MinimumFOp) + PROMOTE_BINARY(arith::AndIOp) + PROMOTE_BINARY(arith::OrIOp) + PROMOTE_BINARY(arith::XOrIOp) + PROMOTE_BINARY(arith::ShLIOp) + PROMOTE_BINARY(arith::ShRSIOp) + PROMOTE_BINARY(arith::ShRUIOp) +#undef PROMOTE_BINARY + + // ---- unary arith: Op(operand) → tensor Op(operand) ---- + if (auto a = dyn_cast(op)) { + auto r = builder.create(loc, getOperand(a.getOperand())); + return record(a.getResult(), r.getResult()); + } + + // ---- comparisons: preserve predicate ---- + if (auto a = dyn_cast(op)) { + auto r = builder.create( + loc, a.getPredicate(), getOperand(a.getLhs()), getOperand(a.getRhs())); + return record(a.getResult(), r.getResult()); + } + if (auto a = dyn_cast(op)) { + auto r = builder.create( + loc, a.getPredicate(), getOperand(a.getLhs()), getOperand(a.getRhs())); + return record(a.getResult(), r.getResult()); + } + + // ---- select ---- + if (auto a = dyn_cast(op)) { + auto r = builder.create(loc, getOperand(a.getCondition()), + getOperand(a.getTrueValue()), + getOperand(a.getFalseValue())); + return record(a.getResult(), r.getResult()); + } + + // ---- type casts: Op(in) → Op(in) with tensor result type ---- +#define PROMOTE_CAST(OpTy) \ + if (auto a = dyn_cast(op)) { \ + auto r = builder.create(loc, getTensorType(a.getOut().getType()), \ + getOperand(a.getIn())); \ + return record(a.getResult(), r.getResult()); \ + } + + PROMOTE_CAST(arith::SIToFPOp) + PROMOTE_CAST(arith::UIToFPOp) + PROMOTE_CAST(arith::FPToSIOp) + PROMOTE_CAST(arith::FPToUIOp) + PROMOTE_CAST(arith::ExtSIOp) + PROMOTE_CAST(arith::ExtUIOp) + PROMOTE_CAST(arith::ExtFOp) + PROMOTE_CAST(arith::TruncIOp) + PROMOTE_CAST(arith::TruncFOp) +#undef PROMOTE_CAST + + // ---- constant: splat via linalg.fill ---- + if (auto a = dyn_cast(op)) { + auto scalarTy = a.getResult().getType(); + Value init = createInit(scalarTy); + auto fill = builder.create(loc, a.getResult(), init); + return record(a.getResult(), fill.getResult(0)); + } + + // ---- math ops: directly on tensors (same as PROMOTE_BINARY) ---- +#define PROMOTE_MATH(OpTy) \ + if (auto a = dyn_cast(op)) { \ + auto r = builder.create(loc, getOperand(a.getOperand())); \ + return record(a.getResult(), r.getResult()); \ + } + + PROMOTE_MATH(math::ExpOp) + PROMOTE_MATH(math::Exp2Op) + PROMOTE_MATH(math::SqrtOp) + PROMOTE_MATH(math::RsqrtOp) + PROMOTE_MATH(math::LogOp) + PROMOTE_MATH(math::Log2Op) + PROMOTE_MATH(math::SinOp) + PROMOTE_MATH(math::CosOp) + PROMOTE_MATH(math::ErfOp) + PROMOTE_MATH(math::CeilOp) + PROMOTE_MATH(math::FloorOp) + PROMOTE_MATH(math::AbsFOp) + PROMOTE_MATH(math::AbsIOp) +#undef PROMOTE_MATH + + // ---- scf.if → arith.select ---- + if (auto a = dyn_cast(op)) { + Value cond = getOperand(a.getCondition()); + // Process then/else regions with inherited valueMap + SmallVector thenResults = promoteRegionBody( + a.getThenRegion(), valueMap, builder, loc, tensorShape); + SmallVector elseResults = promoteRegionBody( + a.getElseRegion(), valueMap, builder, loc, tensorShape); + for (unsigned i = 0; i < thenResults.size(); i++) { + auto sel = builder.create(loc, cond, thenResults[i], + elseResults[i]); + valueMap[a.getResult(i)] = sel.getResult(); + } + SmallVector results; + for (unsigned i = 0; i < thenResults.size(); i++) + results.push_back(valueMap[a.getResult(i)]); + return results; + } + + // ---- scf.for ---- + if (auto a = dyn_cast(op)) { + SmallVector tensorInits; + for (Value v : a.getInitArgs()) + tensorInits.push_back(getOperand(v)); + auto newFor = builder.create( + loc, a.getLowerBound(), a.getUpperBound(), a.getStep(), tensorInits, + [&](OpBuilder &b, Location l, Value iv, ValueRange iterArgs) { + llvm::DenseMap loopMap = valueMap; + loopMap[a.getInductionVar()] = iv; + for (auto [oldArg, newArg] : + llvm::zip(a.getRegionIterArgs(), iterArgs)) + loopMap[oldArg] = newArg; + SmallVector yielded = + promoteRegionBody(a.getRegion(), loopMap, b, l, tensorShape); + b.create(l, yielded); + }); + for (auto [oldR, newR] : llvm::zip(a.getResults(), newFor.getResults())) + valueMap[oldR] = newR; + SmallVector results; + for (auto r : newFor.getResults()) + results.push_back(r); + return results; + } + + if (isa(op)) { + op->emitError( + "scf.while is not supported inside map_elementwise " + "(scf.condition requires scalar i1, cannot promote to tensor)"); + llvm_unreachable(""); + } + op->emitError("MapElementwiseDecomposeConverter: unsupported op"); + llvm_unreachable("unhandled op in map_elementwise region"); + llvm_unreachable("unhandled op in map_elementwise region"); +} + +SmallVector MapElementwiseDecomposeConverter::promoteRegionBody( + Region ®ion, llvm::DenseMap &valueMap, OpBuilder &builder, + Location loc, ArrayRef tensorShape) const { + llvm::DenseMap localMap = valueMap; + Block &block = region.front(); + for (Operation &op : block.without_terminator()) + promoteOp(&op, localMap, builder, loc, tensorShape); + + // Helper to get-or-create a tensor version of any value (external + // constants are promoted on first use). + auto getTensorValue = [&](Value v) -> Value { + auto it = localMap.find(v); + if (it != localMap.end()) + return it->second; + // External constant — promote to tensor + if (auto cst = v.getDefiningOp()) { + auto scalarTy = cst.getResult().getType(); + Value init = builder.create(loc, tensorShape, scalarTy); + auto fill = builder.create(loc, cst.getResult(), init); + localMap[v] = fill.getResult(0); + return fill.getResult(0); + } + // External but not a constant — use as-is (e.g., SSA values from + // enclosing scope that are already tensors or valid in this context). + return v; + }; + + Operation *term = block.getTerminator(); + SmallVector results; + for (Value v : term->getOperands()) + results.push_back(getTensorValue(v)); + return results; +} + +LogicalResult MapElementwiseDecomposeConverter::matchAndRewrite( + triton::MapElementwiseOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + Location loc = op.getLoc(); + + auto tensorType = cast(op.getOperand(0).getType()); + auto shape = tensorType.getShape(); + int64_t rank = shape.size(); + int64_t pack = op.getPack(); + int64_t numInputs = op.getNumOperands(); + int64_t numOutputs = op.getNumResults(); + + assert(rank > 0 && "map_elementwise requires ranked tensor inputs"); + + Region ®ion = op.getRegion(); + assert(region.hasOneBlock() && "expected single-block region"); + Block &body = region.getBlocks().front(); + unsigned numRegionArgs = body.getNumArguments(); + + // Build initial ValueMap: block_arg[i] → input_tensor[i/pack] + llvm::DenseMap valueMap; + for (unsigned i = 0; i < numRegionArgs; i++) { + unsigned inputIdx = i / pack; + valueMap[body.getArgument(i)] = adaptor.getOperands()[inputIdx]; + } + + // Promote all non-terminator ops + for (Operation &innerOp : body.without_terminator()) + promoteOp(&innerOp, valueMap, rewriter, loc, shape); + + // Terminator → results + Operation *terminator = body.getTerminator(); + SmallVector results; + for (unsigned i = 0; i < numOutputs; i++) + results.push_back(valueMap[terminator->getOperand(i * pack)]); + + rewriter.replaceOp(op, results); + return success(); +} + +} // namespace TTOpConverters diff --git a/third_party/ascend/lib/TritonToLinalg/TritonToLinalgPass.cpp b/third_party/ascend/lib/TritonToLinalg/TritonToLinalgPass.cpp new file mode 100644 index 0000000000..7ed436f1ce --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/TritonToLinalgPass.cpp @@ -0,0 +1,2471 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "TritonControlFlowOpt/ControlFlowRewrite.h" +#include "TritonToGraph/LayoutMemoryOptimization.h" +#include "TritonToLinalg/BlockPtrAnalysis.h" +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "ascend/include/TritonToLinalg/ArgMinMaxConverter.h" +#include "ascend/include/TritonToLinalg/CanonicalizeDebugLocationsPass.h" +#include "ascend/include/TritonToLinalg/DeduplicateDebugNopsPass.h" +#include "ascend/include/TritonToLinalg/DescriptorConverter.h" +#include "ascend/include/TritonToLinalg/DevicePrintOffsetRewrite.h" +#include "ascend/include/TritonToLinalg/FunctionConverter.h" +#include "ascend/include/TritonToLinalg/HoistBroadcast.h" +#include "ascend/include/TritonToLinalg/ImplicitPermute.h" +#include "ascend/include/TritonToLinalg/LoadStoreConverter.h" +#include "ascend/include/TritonToLinalg/MarkTensorKindPass.h" +#include "ascend/include/TritonToLinalg/TritonOpConverter.h" +#include "ascend/include/TritonToLinalg/TritonToLinalgPass.h" +#include "ascend/include/TritonToLinalg/UseAnalysis.h" +#include "ascend/include/TritonToStructured/CannonicalizerConverter.h" +#include "ascend/include/TritonToUnstructure/UnstructureConversionPass.h" +#include "ascend/include/Utils/InterleaveOptimization.h" + +#include "bishengir/Dialect/HFusion/IR/HFusion.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/TypeRange.h" +#include "mlir/IR/TypeUtilities.h" +#include "mlir/IR/Types.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" + +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Transforms/Transforms.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Visitors.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" + +#include "llvm/ADT/BitVector.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/LogicalResult.h" + +#include +#include +#include +#include + +#define DEBUG_TYPE "triton-to-linalg" + +using namespace mlir; +using namespace triton; + +int nd2nzFlag = 0; +bool compileOn91095Flag = false; +bool existDotFlag = false; +triton::ascend::CompileMode compileModeFlag = triton::ascend::CompileMode::Simd; + +static bool containsTritonPointer(Type type) { + if (isa(type)) + return true; + auto shapedType = dyn_cast(type); + return shapedType && isa(shapedType.getElementType()); +} + +static bool hasPointerFreeTypes(TypeRange types) { + return llvm::none_of(types, containsTritonPointer); +} + +template static bool hasPointerFreeValues(RangeT values) { + return llvm::none_of(values, [](auto value) { + return containsTritonPointer(value.getType()); + }); +} + +// Checks every structural edge of a marker loop. Looking only at the loop +// operation's operands/results misses region arguments and terminator operands, +// which can otherwise leave a half-converted pointer boundary. +static bool hasPointerFreeControlFlowBoundary(LoopLikeOpInterface loopOp) { + Operation *loop = loopOp.getOperation(); + if (auto forOp = dyn_cast(loop)) { + return hasPointerFreeValues(forOp.getInitArgs()) && + hasPointerFreeValues(forOp.getRegionIterArgs()) && + hasPointerFreeValues(forOp.getYieldedValues()) && + hasPointerFreeTypes(forOp.getResultTypes()); + } + if (auto whileOp = dyn_cast(loop)) { + return hasPointerFreeValues(whileOp.getInits()) && + hasPointerFreeValues(whileOp.getBeforeArguments()) && + hasPointerFreeValues(whileOp.getConditionOp().getArgs()) && + hasPointerFreeValues(whileOp.getAfterArguments()) && + hasPointerFreeTypes(whileOp.getYieldOp().getOperandTypes()) && + hasPointerFreeTypes(whileOp.getResultTypes()); + } + return false; +} + +// Adds only the values that can produce one SSA result. Structured control +// flow is followed by result index so preserving one descriptor slot does not +// retain unrelated loop accumulators or sibling results. +static LogicalResult +appendProducerOperands(Value value, SmallVectorImpl &producerWorklist) { + if (auto argument = dyn_cast(value)) { + Operation *parent = argument.getOwner()->getParentOp(); + unsigned argumentIndex = argument.getArgNumber(); + if (auto forOp = dyn_cast_or_null(parent)) { + if (argumentIndex == 0) + return success(); + unsigned iterIndex = argumentIndex - 1; + if (iterIndex >= forOp.getInitArgs().size()) + return failure(); + producerWorklist.push_back(forOp.getInitArgs()[iterIndex]); + producerWorklist.push_back(forOp.getYieldedValues()[iterIndex]); + return success(); + } + if (auto whileOp = dyn_cast_or_null(parent)) { + if (argumentIndex >= whileOp.getInits().size()) + return failure(); + if (argument.getOwner() == whileOp.getBeforeBody()) { + producerWorklist.push_back(whileOp.getInits()[argumentIndex]); + producerWorklist.push_back( + whileOp.getYieldOp().getOperand(argumentIndex)); + return success(); + } + if (argument.getOwner() == whileOp.getAfterBody()) { + producerWorklist.push_back( + whileOp.getConditionOp().getArgs()[argumentIndex]); + return success(); + } + } + return success(); + } + + Operation *producer = value.getDefiningOp(); + if (!producer) + return success(); + + auto result = dyn_cast(value); + if (auto forOp = dyn_cast(producer)) { + if (!result || result.getResultNumber() >= forOp.getNumResults()) + return failure(); + unsigned index = result.getResultNumber(); + producerWorklist.push_back(forOp.getInitArgs()[index]); + producerWorklist.push_back(forOp.getYieldedValues()[index]); + return success(); + } + if (auto whileOp = dyn_cast(producer)) { + if (!result || result.getResultNumber() >= whileOp.getNumResults()) + return failure(); + unsigned index = result.getResultNumber(); + producerWorklist.push_back(whileOp.getInits()[index]); + producerWorklist.push_back(whileOp.getConditionOp().getArgs()[index]); + producerWorklist.push_back(whileOp.getYieldOp().getOperand(index)); + return success(); + } + if (auto ifOp = dyn_cast(producer)) { + if (!result || result.getResultNumber() >= ifOp.getNumResults() || + !ifOp.elseBlock()) + return failure(); + unsigned index = result.getResultNumber(); + producerWorklist.push_back(ifOp.thenYield().getOperand(index)); + producerWorklist.push_back(ifOp.elseYield().getOperand(index)); + return success(); + } + + producerWorklist.append(producer->operand_begin(), producer->operand_end()); + return success(); +} + +// Visits every structural value represented by one dynamic descriptor slot. +// The result type is the slot contract; init values, region arguments and +// terminator operands must all agree with it exactly. Keeping this traversal +// shared prevents entry validation and later MetaUse preservation from +// interpreting PointerDescriptorBoundary differently. +template +static LogicalResult +visitPointerDescriptorBoundarySlotValues(Operation *loop, int32_t slot, + VisitorT &&visit) { + if (!isa(loop) || slot < 0 || + static_cast(slot) >= loop->getNumResults()) + return failure(); + + Type expectedType = loop->getResult(slot).getType(); + if (containsTritonPointer(expectedType)) + return failure(); + + auto visitSlot = [&](auto values) { + if (static_cast(slot) >= values.size() || + values[slot].getType() != expectedType) + return failure(); + visit(values[slot]); + return success(); + }; + + visit(loop->getResult(slot)); + if (auto forOp = dyn_cast(loop)) { + return success(succeeded(visitSlot(forOp.getInitArgs())) && + succeeded(visitSlot(forOp.getRegionIterArgs())) && + succeeded(visitSlot(forOp.getYieldedValues()))); + } + + auto whileOp = cast(loop); + return success(succeeded(visitSlot(whileOp.getInits())) && + succeeded(visitSlot(whileOp.getBeforeArguments())) && + succeeded(visitSlot(whileOp.getConditionOp().getArgs())) && + succeeded(visitSlot(whileOp.getAfterArguments())) && + succeeded(visitSlot(whileOp.getYieldOp().getOperands()))); +} + +// Validates one CFO-owned loop boundary without changing the IR. An empty +// DenseI32ArrayAttr is valid and means every descriptor component is invariant +// and therefore absent from the loop signature. +static LogicalResult validatePointerDescriptorBoundary(Operation *loop) { + if (!isa(loop)) + return failure(); + + Attribute marker = loop->getAttr(controlflow::kPointerDescriptorBoundaryAttr); + if (!marker) + return failure(); + auto descriptorSlots = dyn_cast(marker); + if (!descriptorSlots || + !hasPointerFreeControlFlowBoundary(cast(loop))) + return failure(); + + llvm::SmallDenseSet seenSlots; + for (int32_t slot : descriptorSlots.asArrayRef()) { + if (!seenSlots.insert(slot).second || + failed( + visitPointerDescriptorBoundarySlotValues(loop, slot, [](Value) {}))) + return failure(); + } + return success(); +} + +// Validates a complete descriptor reconstruction root without changing the +// IR. Rebuild roots need operands even when the corresponding loop marker is +// empty because those operands preserve the invariant descriptor components. +static LogicalResult validatePointerDescriptorRebuild(Operation *op) { + Attribute marker = op->getAttr(controlflow::kPointerDescriptorRebuildAttr); + if (!marker || !isa(marker) || op->getNumOperands() == 0 || + !llvm::any_of(op->getResultTypes(), containsTritonPointer)) + return failure(); + + Attribute offsetForm = + op->getAttr(controlflow::kPointerDescriptorOffsetFormAttr); + if (!offsetForm) + return success(); + auto form = dyn_cast(offsetForm); + auto addPtr = dyn_cast(op); + if (!form || form.getValue() != controlflow::kStrided1DOffsetForm || !addPtr) + return failure(); + + auto resultType = dyn_cast(addPtr.getType()); + auto offsetType = dyn_cast(addPtr.getOffset().getType()); + auto offsetElementType = + offsetType ? dyn_cast(offsetType.getElementType()) + : IntegerType(); + auto baseSplat = addPtr.getPtr().getDefiningOp(); + return success(resultType && resultType.getRank() == 1 && offsetType && + isa(resultType.getElementType()) && + offsetElementType && offsetElementType.getWidth() <= 64 && + resultType.getShape() == offsetType.getShape() && + resultType.getEncoding() == offsetType.getEncoding() && + baseSplat && + isa(baseSplat.getSrc().getType())); +} + +// Returns true when a pointer consumer is reached through ordinary addptr +// operations from a CFO descriptor reconstruction root. The walk is purposely +// limited to addptr: following arbitrary pointer-producing operations would +// make the handoff contract retain computations it does not own. +static bool hasPointerDescriptorRebuildProvenance(Value pointer) { + llvm::DenseSet visited; + while (pointer && visited.insert(pointer).second) { + Operation *producer = pointer.getDefiningOp(); + if (!producer) + return false; + if (producer->hasAttr(controlflow::kPointerDescriptorRebuildAttr)) + return true; + auto addPtr = dyn_cast(producer); + if (!addPtr) + return false; + pointer = addPtr.getPtr(); + } + return false; +} + +// Checks the complete CFO-to-TritonToLinalg handoff before any rewrite can +// fold away a malformed marker. This function is deliberately read-only so it +// can run at pass entry, before UseAnalysis has produced MetaUse attributes. +static LogicalResult +validatePointerDescriptorHandoffMetadata(ModuleOp moduleOp) { + bool valid = true; + moduleOp.walk([&](Operation *op) { + if (op->hasAttr(controlflow::kPointerDescriptorBoundaryAttr) && + failed(validatePointerDescriptorBoundary(op))) + valid = false; + if (op->hasAttr(controlflow::kPointerDescriptorRebuildAttr) && + failed(validatePointerDescriptorRebuild(op))) + valid = false; + if (op->hasAttr(controlflow::kPointerDescriptorOffsetFormAttr) && + !op->hasAttr(controlflow::kPointerDescriptorRebuildAttr)) + valid = false; + }); + return success(valid); +} + +// PointerDescriptorBoundary identifies the dynamic loop slots used to rebuild +// pointers. PointerDescriptorRebuild operands are the complete descriptor +// roots, including invariant components omitted from an empty or minimal slot +// list. After UseAnalysis, preserve exactly those producer chains so +// MetaUseEraser cannot discard values required by conversion. +static LogicalResult preservePointerDescriptorComputations(ModuleOp moduleOp) { + if (failed(validatePointerDescriptorHandoffMetadata(moduleOp))) + return failure(); + + bool valid = true; + SmallVector producerWorklist; + moduleOp.walk([&](Operation *loop) { + Attribute marker = + loop->getAttr(controlflow::kPointerDescriptorBoundaryAttr); + if (!marker) + return; + auto descriptorSlots = cast(marker); + + loop->removeAttr("MetaUse"); + for (int32_t slot : descriptorSlots.asArrayRef()) { + if (failed(visitPointerDescriptorBoundarySlotValues( + loop, slot, + [&](Value value) { producerWorklist.push_back(value); }))) + valid = false; + } + }); + + moduleOp.walk([&](Operation *op) { + if (!op->hasAttr(controlflow::kPointerDescriptorRebuildAttr)) + return; + + // The rebuild operation itself and each of its operands are exact live + // conversion roots. This includes an invariant opaque tensor base as well + // as a scalar address behind int_to_ptr+splat. UseAnalysis has already + // propagated MetaUse by this point; preserving only the offset would leave + // a live ptr operand's producer eligible for erasure. This operand-local + // closure is still narrower than retaining all loop operands/terminators. + op->removeAttr("MetaUse"); + producerWorklist.append(op->operand_begin(), op->operand_end()); + }); + + // UseAnalysis classifies load/store masks and load fallback values as + // pointer metadata. A descriptor rebuild, however, is converted to a memref + // while its consumer still needs those tensor values to form subviews and + // padding. Preserve these exact consumer operands when the pointer is owned + // by the CFO handoff; otherwise MetaUseEraser can leave an unconvertible + // memref-to-pointer materialization at the live memory operation. + moduleOp.walk([&](triton::LoadOp load) { + if (!hasPointerDescriptorRebuildProvenance(load.getPtr())) + return; + if (Value mask = load.getMask()) + producerWorklist.push_back(mask); + if (Value other = load.getOther()) + producerWorklist.push_back(other); + }); + moduleOp.walk([&](triton::StoreOp store) { + if (hasPointerDescriptorRebuildProvenance(store.getPtr()) && + store.getMask()) + producerWorklist.push_back(store.getMask()); + }); + moduleOp.walk([&](triton::AtomicRMWOp atomic) { + if (hasPointerDescriptorRebuildProvenance(atomic.getPtr()) && + atomic.getMask()) + producerWorklist.push_back(atomic.getMask()); + }); + + llvm::DenseSet visitedValues; + while (!producerWorklist.empty()) { + Value value = producerWorklist.pop_back_val(); + if (!value || !visitedValues.insert(value).second) + continue; + if (Operation *producer = value.getDefiningOp()) + producer->removeAttr("MetaUse"); + if (failed(appendProducerOperands(value, producerWorklist))) + valid = false; + } + return success(valid); +} + +// The generic canonicalizer may remove forwarded scf.for/scf.while iter-args, +// which changes a marker loop's positional signature without updating its +// external descriptor-slot metadata. Marker-bearing modules therefore use this +// narrow pre-clean: CSE remains enabled, and only constant scf.if regions are +// inlined so use analysis does not visit unreachable branches. +class FoldConstantIfBeforeUseAnalysis final + : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(scf::IfOp ifOp, + PatternRewriter &rewriter) const override { + auto condition = ifOp.getCondition().getDefiningOp(); + if (!condition) + return failure(); + auto conditionValue = dyn_cast(condition.getValue()); + if (!conditionValue || !conditionValue.getType().isInteger(1)) + return failure(); + + Block *selectedBlock = + conditionValue.getValue().isOne() ? ifOp.thenBlock() : ifOp.elseBlock(); + if (!selectedBlock) { + if (ifOp.getNumResults() != 0) + return failure(); + rewriter.eraseOp(ifOp); + return success(); + } + + auto yield = cast(selectedBlock->getTerminator()); + SmallVector replacements(yield.getOperands()); + rewriter.inlineBlockBefore(selectedBlock, ifOp->getBlock(), + ifOp->getIterator()); + rewriter.eraseOp(yield); + rewriter.replaceOp(ifOp, replacements); + return success(); + } +}; + +// Inspect Operation directly so discovery cannot silently miss an SCF marker +// or an if-only rebuild root. Any surviving handoff attribute disables generic +// pre-clean canonicalization because its validated slot positions and rebuild +// operands must remain stable until MetaUse preservation. +static bool containsPointerDescriptorHandoff(ModuleOp moduleOp) { + bool found = false; + moduleOp.walk([&](Operation *op) { + if (op->hasAttr(controlflow::kPointerDescriptorBoundaryAttr) || + op->hasAttr(controlflow::kPointerDescriptorRebuildAttr) || + op->hasAttr(controlflow::kPointerDescriptorOffsetFormAttr)) + found = true; + }); + return found; +} + +// A converted make_range may leave a tensor-valued SCF state behind even +// though no loop result or loop-body operation observes it. Keep this cleanup +// deliberately narrow: it recognizes only statically shaped integer tensors +// whose entire loop-carried use chain consists of uniform integer updates. +static bool isDeadRangeUpdate(Value value, scf::ForOp loop, unsigned slot, + llvm::SmallPtrSetImpl &visited) { + if (!visited.insert(value).second) + return false; + + auto yield = cast(loop.getBody()->getTerminator()); + bool sawYield = false; + for (OpOperand &use : value.getUses()) { + Operation *user = use.getOwner(); + if (user == yield) { + if (use.getOperandNumber() != slot) + return false; + sawYield = true; + continue; + } + + Value updateResult; + Value lhs; + Value rhs; + if (auto add = dyn_cast(user)) { + updateResult = add.getResult(); + lhs = add.getLhs(); + rhs = add.getRhs(); + } else if (auto sub = dyn_cast(user)) { + updateResult = sub.getResult(); + lhs = sub.getLhs(); + rhs = sub.getRhs(); + } else { + return false; + } + if (updateResult.getType() != value.getType()) + return false; + + bool usesValueAsLhs = lhs == value; + bool usesValueAsRhs = rhs == value; + if (usesValueAsLhs == usesValueAsRhs) + return false; + Value delta = usesValueAsLhs ? rhs : lhs; + auto deltaType = dyn_cast(delta.getType()); + if (!deltaType || deltaType != value.getType()) + return false; + + Operation *deltaProducer = delta.getDefiningOp(); + bool isUniformDelta = false; + if (auto constant = dyn_cast_or_null(deltaProducer)) { + if (auto dense = dyn_cast(constant.getValue())) + isUniformDelta = dense.isSplat(); + } + if (auto fill = dyn_cast_or_null(deltaProducer)) + isUniformDelta = fill.getInputs().size() == 1 && + fill.getOutputs().size() == 1 && + fill.getOutputs().front().getType() == value.getType(); + if (!isUniformDelta) + return false; + + if (!isDeadRangeUpdate(updateResult, loop, slot, visited)) + return false; + } + return sawYield; +} + +static bool isDeadRangeCarrier(scf::ForOp loop, unsigned slot) { + if (slot >= loop.getInitArgs().size() || + slot >= loop.getRegionIterArgs().size() || + slot >= loop.getResults().size() || + slot >= loop.getYieldedValues().size() || + !loop.getResult(slot).use_empty()) + return false; + + auto type = dyn_cast(loop.getInitArgs()[slot].getType()); + auto iterType = + dyn_cast(loop.getRegionIterArgs()[slot].getType()); + if (!type || !iterType || type != iterType || !type.hasStaticShape() || + !isa(type.getElementType())) + return false; + + Operation *producer = loop.getInitArgs()[slot].getDefiningOp(); + if (!producer || + (!isa(producer) && + !producer->hasAttr("tt.from_make_range") && + !producer->hasAttr("tt.make_range_offset") && + !producer->hasAttr("tt.make_range_size"))) + return false; + + llvm::SmallPtrSet visited; + return isDeadRangeUpdate(loop.getRegionIterArgs()[slot], loop, slot, visited); +} + +// Remove only fully dead range-like SCF state. Body arguments and yield +// operands are removed first, then the loop is rebuilt with surviving values. +static void eraseDeadRangeCarriers(ModuleOp moduleOp) { + SmallVector loops; + moduleOp.walk([&](scf::ForOp loop) { loops.push_back(loop); }); + + for (scf::ForOp loop : loops) { + if (!loop || loop->getParentOp() == nullptr) + continue; + llvm::BitVector dead(loop.getInitArgs().size()); + for (unsigned i = 0; i < dead.size(); ++i) { + if (isDeadRangeCarrier(loop, i)) + dead.set(i); + } + if (dead.none()) + continue; + + auto yield = cast(loop.getBody()->getTerminator()); + yield->eraseOperands(dead); + loop.getBody()->eraseArguments([&](BlockArgument arg) { + unsigned argNumber = arg.getArgNumber(); + return argNumber != 0 && dead.test(argNumber - 1); + }); + + llvm::BitVector operandIndices(loop->getNumOperands()); + for (auto [i, init] : llvm::enumerate(loop.getInitArgsMutable())) { + if (dead.test(i)) + operandIndices.set(init.getOperandNumber()); + } + loop->eraseOperands(operandIndices); + + OperationState state(loop.getLoc(), loop->getName(), loop->getOperands(), + loop.getInitArgs().getTypes(), loop->getAttrs()); + state.addRegion()->takeBody(loop.getBodyRegion()); + OpBuilder builder(loop); + auto newLoop = cast(builder.create(state)); + + unsigned newResultIndex = 0; + for (auto [i, result] : llvm::enumerate(loop.getResults())) { + if (dead.test(i)) { + assert(result.use_empty() && "dead range result still has uses"); + continue; + } + result.replaceAllUsesWith(newLoop.getResult(newResultIndex++)); + } + loop.erase(); + } +} + +static LogicalResult preCleanBeforeUseAnalysis(ModuleOp moduleOp) { + bool hasPointerDescriptorHandoff = containsPointerDescriptorHandoff(moduleOp); + + PassManager csePipeline(moduleOp.getContext(), moduleOp.getOperationName()); + csePipeline.addPass(createCSEPass()); + if (!hasPointerDescriptorHandoff) + csePipeline.addPass(createCanonicalizerPass()); + if (failed(csePipeline.run(moduleOp))) + return failure(); + + if (!hasPointerDescriptorHandoff) + return success(); + RewritePatternSet patterns(moduleOp.getContext()); + patterns.add(moduleOp.getContext()); + return applyPatternsGreedily(moduleOp, std::move(patterns)); +} + +// Recomputes the result layouts of subviews whose source descriptor has been +// rebased. A memref.subview result layout is derived from both its mixed +// offsets/strides and its source layout. For example, changing the source from +// `memref<32xf32, strided<[1], offset: ?>>` to the equivalent rebased +// `memref<32xf32, strided<[1]>>` changes a zero-offset subview result from a +// dynamic offset to offset zero. Merely changing the source SSA value leaves +// the old result type behind and makes SubViewOp verification fail. +// +// Subviews may be chained, so every updated result becomes a new worklist +// source. Rank-reduced subviews retain their existing result shape while their +// layout is inferred again from the rebased source. +static LogicalResult propagateRebasedSubviewTypes(Value rebasedSource, + IRRewriter &rewriter) { + SmallVector sources{rebasedSource}; + llvm::SmallPtrSet visited; + + while (!sources.empty()) { + Value source = sources.pop_back_val(); + for (Operation *user : source.getUsers()) { + auto subview = dyn_cast(user); + if (!subview || !visited.insert(user).second) + continue; + + auto sourceType = dyn_cast(subview.getSource().getType()); + auto oldResultType = dyn_cast(subview.getResult().getType()); + if (!sourceType || !oldResultType) + return failure(); + + Type inferredType; + if (sourceType.getRank() == oldResultType.getRank()) { + inferredType = memref::SubViewOp::inferResultType( + sourceType, subview.getMixedOffsets(), subview.getMixedSizes(), + subview.getMixedStrides()); + } else { + inferredType = memref::SubViewOp::inferRankReducedResultType( + oldResultType.getShape(), sourceType, subview.getMixedOffsets(), + subview.getMixedSizes(), subview.getMixedStrides()); + } + + auto inferredMemRefType = dyn_cast(inferredType); + if (!inferredMemRefType) + return failure(); + if (inferredMemRefType != oldResultType) { + rewriter.modifyOpInPlace( + subview, [&] { subview.getResult().setType(inferredMemRefType); }); + } + sources.push_back(subview.getResult()); + } + } + return success(); +} + +// Returns true when rebasing a descriptor would change a type owned by a +// different operation. A subview chain ending in direct memref loads/stores is +// local to this rewrite. Calls, returns, SCF/CFG boundaries, and every other +// user retain their existing descriptor layout and therefore stop rebasing. +static bool reachesLayoutSensitiveBoundary(Value root) { + SmallVector worklist{root}; + llvm::DenseSet visited; + while (!worklist.empty()) { + Value value = worklist.pop_back_val(); + if (!visited.insert(value).second) + continue; + for (Operation *user : value.getUsers()) { + if (auto subview = dyn_cast(user)) { + if (subview.getSource() == value) { + worklist.push_back(subview.getResult()); + continue; + } + } + if (auto load = dyn_cast(user)) { + if (load.getMemRef() == value) + continue; + } + if (auto store = dyn_cast(user)) { + if (store.getMemRef() == value) + continue; + } + return true; + } + } + return false; +} + +// Convert structured custom ops after operand type converted, +// for example tt.ptr converted to memref. +template +class StructuredCustomOpConverter : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(CustomOpT op, typename CustomOpT::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const override { + BlockDataParser::rewriteStructuredCustomOp(op, adaptor, rewriter); + return success(); + } +}; + +// Collect the ops in a scan body that actually contribute to the yielded +// result: compute the backward slice from the terminator operands, then drop +// pure type-cast ops. Mirrors ReductionOpBaseConverter::getRealReductionOps so +// that auxiliary ops (e.g. the overflow asserts injected by TRITON_DEBUG) +// never feed the yielded result and are ignored here as well. +static llvm::SmallVector getRealScanBodyOps(triton::ScanOp op) { + Block *body = op.getBody(); + Operation *terminator = body->getTerminator(); + + llvm::DenseSet liveOps; + llvm::SmallVector worklist(terminator->getOperands()); + while (!worklist.empty()) { + Value val = worklist.pop_back_val(); + if (auto *defOp = val.getDefiningOp()) { + if (defOp->getBlock() == body && liveOps.insert(defOp).second) { + for (auto operand : defOp->getOperands()) + worklist.push_back(operand); + } + } + } + + llvm::SmallVector realOps; + for (Operation &bodyOp : body->without_terminator()) { + if (!liveOps.contains(&bodyOp)) + continue; + if (isa(&bodyOp)) + continue; + realOps.push_back(&bodyOp); + } + return realOps; +} + +// A tt.scan that is (1) a plain cumsum (combine body is a single add, matching +// ScanConverter's triton_cumsum selection) and (2) collapses to a 1-D scan +// after backend lowering, i.e. every dim except the scan axis has extent 1 +// (e.g. [1,1,128,1] with axis=2). Only this case is routed to the SIMT +// (Sklansky) cumsum template; cumprod / generic scans and multi-dim cumsum stay +// on SIMD. +static bool isSimt1DCumsum(triton::ScanOp op) { + // (1) Must be a single-add combine body (only ops that feed the yielded + // result count, mirroring getRealReductionOps). + llvm::SmallVector realOps = getRealScanBodyOps(op); + if (realOps.size() != 1 || + !isa(realOps.front())) + return false; + + // (2) Must be the 1-D scenario: all non-scan dims are unit-sized. + auto srcTy = dyn_cast(op.getOperand(0).getType()); + if (!srcTy || !srcTy.hasRank()) + return false; + int64_t axis = op.getAxis(); + ArrayRef shape = srcTy.getShape(); + if (axis < 0 || axis >= static_cast(shape.size())) + return false; + for (int64_t i = 0; i < static_cast(shape.size()); ++i) { + if (i != axis && shape[i] != 1) + return false; + } + return true; +} + +static bool isCustomOpOperandTypesLegal(TypeRange types) { + return llvm::all_of(types, [](Type t) { + if (isa(t)) { + return false; + } + if (auto shapedType = dyn_cast(t)) { + return !isa(shapedType.getElementType()); + } + return true; + }); +} + +static bool isSIMTOp(Operation *op) { + if (auto custom_op = dyn_cast(op)) { + return custom_op.getCoreType() == hivm::TCoreType::VECTOR && + custom_op.getVFMode() == hivm::VFMode::SIMT; + } + + if (isa(op) && compileOn91095Flag) { + return true; + } + + if (isa(op) && compileOn91095Flag) { + return true; + } + + // tt.scan: only a 1-D cumsum is treated as a SIMT op (drives the kernel + // parallel_mode -> mix_simd_simt -> enable_simt). Everything else stays SIMD. + if (compileOn91095Flag) { + if (auto scan = dyn_cast(op)) { + return isSimt1DCumsum(scan); + } + } + + // math.sin / math.cos on f16/f32 inputs: downstream (Ascend 950 RegBase + // normalize, enable-high-precision defaults to true) rewrites them into a + // Payne-Hanek range reduction that looks up a 320xi32 2/pi limbs table with + // two hfusion.gather ops per collapsed region. Match that scenario here and + // route it to the SIMT template so the table gathers run in SIMT. + if (compileOn91095Flag && (isa(op) || isa(op))) { + Type inElem = getElementTypeOrSelf(op->getOperand(0).getType()); + if (inElem.isF16() || inElem.isF32()) { + return true; + } + } + + return isa(op); +} + +TritonTypeConverter::TritonTypeConverter() { + addConversion([](Type type) { return type; }); + + addConversion([](triton::PointerType ptrType) { + Type elem = ptrType.getPointeeType(); + // Handling special case: ptr -> memref + if (auto it = dyn_cast(elem); it && it.getWidth() == 1) { + elem = IntegerType::get(ptrType.getContext(), 8); + LLVM_DEBUG({ + llvm::dbgs() << "[TritonTypeConverter] Normalize i1 pointer to i8 " + "memref. elemType=" + << elem << "\n"; + }); + } + return MemRefType::get({ShapedType::kDynamic}, elem); + }); + + addConversion([](TensorType tensorType) -> Type { + auto elemType = tensorType.getElementType(); + if (auto ptrType = dyn_cast(elemType)) { + elemType = ptrType.getPointeeType(); + } + // Handling special case: tensor -> memref + if (auto it = dyn_cast(elemType); it && it.getWidth() == 1) { + elemType = IntegerType::get(tensorType.getContext(), 8); + LLVM_DEBUG({ + llvm::dbgs() << "[TritonTypeConverter] Normalize i1 tensor to i8 " + "memref. elemType=" + << elemType << "\n"; + }); + } + return MemRefType::get(tensorType.getShape(), elemType); + }); + + // A pointer-descriptor boundary can intentionally remain a legal SCF op + // carrying pointer-free tensor values while function signature conversion + // changes the corresponding argument to a memref. Materialize only the + // canonical numerical memref-to-ranked-tensor pair produced by the + // conversion above. The exact type check deliberately excludes pointer + // tensors, encoded tensors, i1-to-i8 normalization, non-identity layouts, + // and non-default memory spaces, so this does not become a general SCF + // type-conversion path. + addSourceMaterialization([](OpBuilder &builder, RankedTensorType resultType, + ValueRange inputs, Location loc) -> Value { + if (inputs.size() != 1 || resultType.getEncoding() || + isa(resultType.getElementType())) + return nullptr; + + auto inputType = dyn_cast(inputs.front().getType()); + if (!inputType || !inputType.getLayout().isIdentity() || + inputType.getMemorySpace()) + return nullptr; + + auto expectedInputType = + MemRefType::get(resultType.getShape(), resultType.getElementType()); + if (inputType != expectedInputType) + return nullptr; + + return builder.create( + loc, resultType, inputs.front(), /*restrict=*/false, + /*writable=*/false); + }); +} + +void TritonToLinalgPass::addProgramInfo(triton::FuncOp func, + bool globalKernel) { + OpBuilder b(func); + + auto origFuncType = func.getFunctionType(); + auto origInputTypes = origFuncType.getInputs(); + SmallVector newInputTypes(origInputTypes); + newInputTypes.append(TRITON_PROGRAM_INFO_ARG_COUNT, b.getI32Type()); + + auto newFuncType = + b.getFunctionType(newInputTypes, origFuncType.getResults()); + + func.setFunctionType(newFuncType); + + // If argument attributes exist, extend attribute list. + if (func.getAllArgAttrs()) { + SmallVector newArgAttrs; + func.getAllArgAttrs(newArgAttrs); + newArgAttrs.append(TRITON_PROGRAM_INFO_ARG_COUNT, DictionaryAttr()); + func.setAllArgAttrs(newArgAttrs); + } + + // Append the arguments to the entry block. + for (unsigned i = 0; i < TRITON_PROGRAM_INFO_ARG_COUNT; i++) { + func.getBody().front().addArgument(b.getI32Type(), func.getLoc()); + } + + if (globalKernel) { + func->setAttr(globalKernelAttr, b.getStringAttr("")); + } else { + func->setAttr(globalKernelAttr, b.getStringAttr("local")); + } +} + +LogicalResult +TritonToLinalgPass::convertMultipleBlockControlFlow(Operation *funcOp, + OpBuilder &builder) { + if (!isa(funcOp)) { + funcOp->emitError( + "convertMultipleBlockControlFlow can only process func::FuncOp!"); + return failure(); + } + + SmallVector candidate; + SmallVector eraseBlocks; + for (Block &block : dyn_cast(funcOp).getBody()) { + auto curTerminator = block.getTerminator(); + if (isa(curTerminator)) { + candidate.push_back(curTerminator); + } else if (isa(curTerminator)) { + if (candidate.empty()) { + curTerminator->emitError( + "funcOp has more than one Block but got an early 'tt.return' Op."); + return failure(); + } + } else if (!isa(curTerminator)) { + funcOp->emitError( + "funcOp has more than one Block but found unsupported Terminator: ") + << *curTerminator; + return failure(); + } + + if (!block.isEntryBlock()) + eraseBlocks.push_back(&block); + } + + LLVM_DEBUG({ + llvm::dbgs() << "Found " << candidate.size() + << " candidate cond_branch operations to convert.\n"; + }); + + if (candidate.empty()) { + funcOp->emitError("funcOp has more than one Block but no candidate " + "Terminator was found!"); + return failure(); + } + + llvm::BitVector visitFlag(candidate.size(), false); + + // Recursive function to convert all cf::CondBranchOp to scf::IfOp + std::function convertToSCF = + [&](Operation *op, Operation *insertPosOp) -> void { + auto condBranchOp = dyn_cast_if_present(op); + auto iter = llvm::find(candidate, condBranchOp); + if (!(condBranchOp && iter != candidate.end())) { + op->emitError( + "convertToSCF must process with condBranchOp in candidates!"); + return; + } + visitFlag.set(iter - candidate.begin()); + + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointAfter(insertPosOp); + + // Well, here force to destory original control flow + builder.create( + condBranchOp->getLoc(), condBranchOp.getCondition(), + /*thenBuilder=*/ + [&](OpBuilder &builder, Location loc) { + SmallVector movedOps = llvm::map_to_vector( + condBranchOp.getTrueDest()->without_terminator(), + [](Operation &op) { return &op; }); + for (auto *innerOp : movedOps) { + innerOp->moveBefore(builder.getInsertionBlock(), + builder.getInsertionPoint()); + } + + auto blockTerm = condBranchOp.getTrueDest()->getTerminator(); + if (auto nextCond = dyn_cast(blockTerm)) { + if (movedOps.empty()) { + blockTerm->emitError("movedOps can not be empty before entering " + "convertToSCF (then)!"); + return; + } + convertToSCF(nextCond, movedOps.back()); + } else if (!isa(blockTerm)) { + blockTerm->emitError( + "Unsupported terminator in then branch after structuring"); + } + + builder.create(loc); + }, + /*elseBuilder=*/ + [&](OpBuilder &builder, Location loc) { + SmallVector movedOps = llvm::map_to_vector( + condBranchOp.getFalseDest()->without_terminator(), + [](Operation &op) { return &op; }); + for (auto *innerOp : movedOps) { + innerOp->moveBefore(builder.getInsertionBlock(), + builder.getInsertionPoint()); + } + + auto blockTerm = condBranchOp.getFalseDest()->getTerminator(); + if (auto nextCond = dyn_cast(blockTerm)) { + if (movedOps.empty()) { + blockTerm->emitError("movedOps can not be empty before entering " + "convertToSCF (else)!"); + return; + } + convertToSCF(nextCond, movedOps.back()); + } else if (!isa(blockTerm)) { + blockTerm->emitError( + "Unsupported terminator in else branch after structuring"); + } + builder.create(loc); + }); + }; + + Block::iterator insertOp(candidate.front()); + if (insertOp == candidate.front()->getBlock()->begin()) { + // if the first operation is a cond_branch, we need to insert before it + convertToSCF(candidate.front(), candidate.front()); + } else { + --insertOp; + convertToSCF(candidate.front(), &(*insertOp)); + } + + if (!visitFlag.all()) { + funcOp->emitError("Not all cf.cond_br converted!"); + return failure(); + } + + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(candidate.front()); + builder.create(candidate.front()->getLoc()); + + for (Operation *eachTerm : candidate) + eachTerm->erase(); + for (Block *block : llvm::reverse(eraseBlocks)) + block->erase(); + + return success(); +} + +void TritonToLinalgPass::convertTTFunc(triton::FuncOp func, const bool existDot, + const bool existSIMTOp) { + OpBuilder builder(func); + + auto name = func.getName(); + auto type = func.getFunctionType(); + + SmallVector argAttrs, resAttrs; + func.getAllArgAttrs(argAttrs); + func.getAllResultAttrs(resAttrs); + + // Special handling for bit-casted tt.ptr arguments + SmallVector inputTypes{type.getInputs()}; + SmallVector retTypes{type.getResults()}; + if (func.getSymVisibility() == "public" && !func.isDeclaration()) { + for (size_t i = 0; i < func.getNumArguments(); ++i) { + auto arg = func.getArgument(i); + // Special method for i1 arg + if (!isa(arg.getType()) || + dyn_cast(arg.getType()).getElementTypeBitWidth() != + 1) { + continue; + } + + SmallVector argVaildUser{arg.getUsers()}; + llvm::erase_if(argVaildUser, [](Operation *op) -> bool { + return isOpTriviallyDead(op); + }); + + if (!argVaildUser.empty()) { + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << arg << " has users:\n"; + int cnt = 0; + for (auto it : argVaildUser) { + os << "users[" << cnt++ << "] = " << *it; + } + }); + if (llvm::all_of(argVaildUser, [](Operation *userOp) { + return isa(userOp); + })) { + auto castOp = cast(*argVaildUser.begin()); + if (castOp.getInputs().size() == 1 && + castOp.getOutputs().size() == 1) { + arg.setType(castOp.getOutputs()[0].getType()); + inputTypes[i] = arg.getType(); + } + } else { + func->emitError(Twine("Unsupported use of func arg at index ") + + Twine(i)); + } + } else { + // Process unused bool ptr type specially, which guarantees bool pointer + // argument's type is realistic and don't mislead backend compiler. + // realistic memory layout of bool pointer is 8 bit width + auto memType = dyn_cast(arg.getType()) + .cloneWith(std::nullopt, builder.getI8Type()); + arg.setType(memType); + inputTypes[i] = arg.getType(); + } + } + } + auto castType = FunctionType::get(func.getContext(), inputTypes, retTypes); + + auto funcFunc = builder.create(func.getLoc(), name, castType); + funcFunc.setAllArgAttrs(argAttrs); + funcFunc.setAllResultAttrs(resAttrs); + auto kernelAttr = func->getAttr(globalKernelAttr); + if (kernelAttr) { + funcFunc->setAttr(globalKernelAttr, kernelAttr); + } + std::string kernelMixMode = "aiv"; + if (existDot) { + // mix also works for pure cube kernel by using the same MAGIC_ELF keyword + kernelMixMode = "mix"; + } + // Set mix_mode in the func attrs so that the backend could know + // the mix_mode by parse the func attrs. + // The backend needs to know the mix_mode because the host wrapper + // needs to set the devbin.magic. Check npu_utils.cpp. + funcFunc->setAttr(kernelMixModeName, builder.getStringAttr(kernelMixMode)); + + std::string parallelMode = "simd"; + if (existSIMTOp) { + parallelMode = "mix_simd_simt"; + } + funcFunc->setAttr(kernelParallelModeName, + builder.getStringAttr(parallelMode)); + + auto autoBlockifyAttr = func->getAttr("auto_blockify_size"); + if (autoBlockifyAttr) + funcFunc->setAttr("auto_blockify_size", autoBlockifyAttr); + + auto &funcFuncBody = funcFunc.getBody(); + auto &funcBody = func.getBody(); + + IRMapping map; + funcBody.cloneInto(&funcFuncBody, map); + + if (!funcFuncBody.hasOneBlock()) { + if (failed(convertMultipleBlockControlFlow(funcFunc, builder))) { + llvm_unreachable("Encounter unsupported control flow"); + } + } + + for (Block &block : funcFuncBody.getBlocks()) { + auto term = block.getTerminator(); + builder.setInsertionPoint(term); + builder.create(func.getLoc(), term->getOperands()); + term->erase(); + } + func.erase(); +} + +void TritonToLinalgPass::addDynamicLegal( + ConversionTarget &target, TritonTypeConverter &tritonTypeConverter) { + target.addLegalDialect< + func::FuncDialect, arith::ArithDialect, math::MathDialect, + linalg::LinalgDialect, affine::AffineDialect, scf::SCFDialect, + cf::ControlFlowDialect, tensor::TensorDialect, LLVM::LLVMDialect, + bufferization::BufferizationDialect, memref::MemRefDialect, + annotation::AnnotationDialect, hivm::HIVMDialect, hfusion::HFusionDialect, + scope::ScopeDialect>(); + + // add legal dialect on condition + target.addLegalOp(); + + // decide which ops need conversion based on uses + target.addDynamicallyLegalOp( + [](mlir::Operation *op) { + if (op->use_empty()) { + return false; + } else { + return true; + } + }); + + target.addDynamicallyLegalOp([&](triton::FuncOp op) { + return tritonTypeConverter.isSignatureLegal(op.getFunctionType()); + }); + + // For CustomOp/CustomMacroOp, tt.ptr should be converted to memref. + target.addDynamicallyLegalOp([&](hivm::CustomOp op) { + return isCustomOpOperandTypesLegal(op->getOperandTypes()); + }); + target.addDynamicallyLegalOp( + [&](hivm::CustomMacroOp op) { + return isCustomOpOperandTypesLegal(op->getOperandTypes()); + }); + + target.addDynamicallyLegalOp([](arith::ConstantOp op) { + auto res = op.getResult(); + if (!isa(res.getType())) { + return true; + } + + if (auto denseAttr = dyn_cast(op.getValue())) { + if (!denseAttr.isSplat() || + !isa(denseAttr.getElementType())) { + return true; + } + if (res.hasOneUse() && isa(*res.user_begin())) { + return true; + } + return false; + } + return true; + }); + + target.addDynamicallyLegalOp( + [](scf::IfOp op) { return !TTOpConverters::hasScalarPointerResult(op); }); + + auto controlFlowTerminatorLegal = [](Operation *op) { + Operation *parent = op->getParentOp(); + if (parent && + parent->hasAttr(TTOpConverters::kScalarPointerCarrierBoundaryAttr)) { + if (auto parentIf = dyn_cast(parent)) + return llvm::equal(op->getOperandTypes(), parentIf.getResultTypes()); + } + + if (parent && parent->hasAttr(controlflow::kPointerDescriptorBoundaryAttr)) + return hasPointerFreeControlFlowBoundary( + cast(parent)); + + return llvm::all_of(op->getOperandTypes(), [](Type t) { + if (isa(t)) + return false; + if (auto shapedType = dyn_cast(t)) + return shapedType.getElementType().isIntOrFloat(); + assert(t.isIntOrIndexOrFloat()); + return true; + }); + }; + + target.addDynamicallyLegalOp( + controlFlowTerminatorLegal); + + auto isArithOrMathOpLegal = [this](Operation *op) { + if (op->hasAttr("MetaUse")) + return false; + + if (isa(op)) + return true; + + bool operateOnTensors = llvm::all_of(op->getOperandTypes(), [](Type type) { + return isa(type); + }); + + return this->namedOps || !operateOnTensors; + }; + + // Numeric selects retain the existing Arith legality. Every scalar-pointer + // select uses the integer-address converter so no memref object crosses it. + target.addDynamicallyLegalOp( + [isArithOrMathOpLegal](arith::SelectOp op) { + if (TTOpConverters::isScalarPointerSelect(op)) + return false; + return isArithOrMathOpLegal(op); + }); + + target.addDynamicallyLegalDialect( + isArithOrMathOpLegal); +} + +namespace { + +/// Route the specific `splat(base) -> addptr(scan) -> addptr(constant) -> +/// store` form through the existing indirect-store operation before use +/// analysis. Lowering either addptr independently would materialize a memref +/// while the remaining operation still expects tensor. +class FoldScanOffsetAddPtrChain : public OpRewritePattern { +public: + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(triton::AddPtrOp op, + PatternRewriter &rewriter) const override { + if (!op->hasOneUse() || !op.getOffset().getDefiningOp()) + return failure(); + auto nextAddPtr = dyn_cast(*op->user_begin()); + if (!nextAddPtr || + !nextAddPtr.getOffset().getDefiningOp() || + op.getOffset().getType() != nextAddPtr.getOffset().getType() || + !nextAddPtr->hasOneUse()) + return failure(); + auto store = dyn_cast(*nextAddPtr->user_begin()); + auto baseSplat = op.getPtr().getDefiningOp(); + if (!store || !baseSplat || + !isa(baseSplat.getSrc().getType())) + return failure(); + + rewriter.setInsertionPoint(nextAddPtr); + Value combinedOffset = rewriter.create( + nextAddPtr.getLoc(), op.getOffset(), nextAddPtr.getOffset()); + rewriter.create( + store.getLoc(), baseSplat.getSrc(), combinedOffset, store.getValue(), + store.getMask()); + rewriter.eraseOp(store); + rewriter.eraseOp(nextAddPtr); + rewriter.eraseOp(op); + return success(); + } +}; + +} // namespace + +// Version only the small, bufferized row-copy loops emitted for mixed-axis +// loads. The column extent is shared by every row: checking it once outside +// the loop lets full tiles recover a static copy while tails keep their mask. +// Run after memory conversion so both branches use the same outer allocation. +static void specializeFullRowCopies(ModuleOp moduleOp) { + SmallVector loops; + moduleOp.walk([&](scf::ForOp loop) { + if (loop->hasAttr("ExtractedLoadOrStore") && + loop->hasAttr("hivm.parallel_loop") && loop.getNumResults() == 0) + loops.push_back(loop); + }); + IRRewriter rewriter(moduleOp.getContext()); + for (scf::ForOp loop : loops) { + // Do not duplicate nested control flow, allocations, or other accesses. + memref::CopyOp copy; + bool supported = true; + unsigned numOps = 0; + for (Operation &op : loop.getBody()->without_terminator()) { + if (++numOps > 32 || op.getNumRegions() != 0) { + supported = false; + break; + } + if (auto candidate = dyn_cast(op)) { + if (copy) { + supported = false; + break; + } + copy = candidate; + } else if (!isMemoryEffectFree(&op)) { + supported = false; + break; + } + } + if (!supported || !copy) + continue; + + auto src = copy.getSource().getDefiningOp(); + auto dst = copy.getTarget().getDefiningOp(); + if (!src || !dst || src.getSourceType().getRank() != 2 || + dst.getSourceType().getRank() != 2 || src.getType().getRank() != 2 || + dst.getType().getRank() != 2) + continue; + auto shape = src.getSourceType().getShape(); + if (shape[0] != 1 || ShapedType::isDynamic(shape[1]) || shape[1] <= 1 || + dst.getSourceType().getShape() != shape) + continue; + int64_t width = shape[1]; + auto isZero = [](OpFoldResult v) { return isConstantIntValue(v, 0); }; + auto isOne = [](OpFoldResult v) { return isConstantIntValue(v, 1); }; + auto isRowPrefix = [&](memref::SubViewOp view) { + auto [strides, offset] = view.getSourceType().getStridesAndOffset(); + return strides.back() == 1 && + llvm::all_of(view.getMixedOffsets(), isZero) && + llvm::all_of(view.getMixedStrides(), isOne) && + isOne(view.getMixedSizes()[0]); + }; + if (!isRowPrefix(src) || !isRowPrefix(dst) || + src.getMixedSizes()[1] != dst.getMixedSizes()[1]) + continue; + auto extent = dyn_cast(src.getMixedSizes()[1]); + if (!extent || !loop.isDefinedOutsideOfLoop(extent)) + continue; + + // Require the existing load buffer and its exact row slice. This excludes + // stores and avoids moving/duplicating a per-row allocation into a branch. + auto row = dst.getSource().getDefiningOp(); + auto source = src.getSource().getDefiningOp(); + if (!row || !source || row.getSourceType().getRank() != 2 || + !row.getSource().getDefiningOp() || + !loop.isDefinedOutsideOfLoop(row.getSource()) || + !loop.isDefinedOutsideOfLoop(source.getSource()) || + row.getMixedOffsets()[0] != OpFoldResult(loop.getInductionVar()) || + !isZero(row.getMixedOffsets()[1]) || + !llvm::all_of(row.getMixedStrides(), isOne) || + !isOne(row.getMixedSizes()[0]) || + !isConstantIntValue(row.getMixedSizes()[1], width)) + continue; + + rewriter.setInsertionPoint(loop); + Location loc = copy.getLoc(); + Value fullWidth = rewriter.create(loc, width); + Value full = rewriter.create(loc, arith::CmpIPredicate::eq, + extent, fullWidth); + auto branch = + rewriter.create(loc, full, /*withElseRegion=*/true); + rewriter.setInsertionPointToStart(branch.thenBlock()); + IRMapping mapping; + auto fastLoop = cast(rewriter.clone(*loop, mapping)); + auto fastCopy = *fastLoop.getBody()->getOps().begin(); + // Under extent == width the two zero-offset subviews are the complete + // static rows. Bypass them explicitly; no branch-sensitive folding needed. + fastCopy->setOperand(0, mapping.lookupOrDefault(src.getSource())); + fastCopy->setOperand(1, mapping.lookupOrDefault(dst.getSource())); + loop->moveBefore(branch.elseBlock()->getTerminator()); + // The A5 backend must be able to specialize the full-width library call + // independently of the dynamic tail call that shares its callee. + moduleOp->setAttr("tt.full_row_copy", rewriter.getUnitAttr()); + } +} + +void TritonToLinalgPass::populateTritonToLinalgCanonicalizationPatterns( + RewritePatternSet &patterns) { + patterns.add, + LoadStoreConverter::LoadStoreCanonicalizer, + LoadStoreConverter::LoadStoreCanonicalizer, + LoadStoreConverter::LoadStoreCanonicalizer>( + patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add< + TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer< + arith:: + MulFOp>, // TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer, + TTOpConverters::ScalarMathCanonicalizer + // By test, the following ops do not need canonicalization. + // TTOpConverters::ScalarMathCanonicalizer + // TTOpConverters::ScalarMathCanonicalizer + // TTOpConverters::ScalarMathCanonicalizer + >(patterns.getContext()); + patterns.add( + patterns.getContext()); + if (this->enableSelectAnalysis) { + patterns.add(patterns.getContext()); + } +} + +void TritonToLinalgPass::populateTritonToLinalgConversionPatterns( + TypeConverter &typeConverter, RewritePatternSet &patterns, + unsigned int launchGridRank) { + nd2nzFlag = this->enableNd2nzOnVector; + populateFunctionOpInterfaceTypeConversionPattern( + patterns, typeConverter); + + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns + .add, + LoadStoreConverter::MemoryPointerConverter, + LoadStoreConverter::MemoryPointerConverter, + LoadStoreConverter::MemoryPointerConverter>( + patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + // reduce converters + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + + patterns.add(patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add>(patterns.getContext(), + PatternBenefit(2)); + patterns.add>( + patterns.getContext(), PatternBenefit(2)); + patterns.add(patterns.getContext(), + PatternBenefit(2)); + patterns.add( + typeConverter, patterns.getContext(), PatternBenefit(2)); + patterns.add(typeConverter, + patterns.getContext()); + + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(typeConverter, + patterns.getContext()); + + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + // On 950 (910B4/91095), histogram is lowered via hivm.custom builtin + // template. On other targets, histogram is handled by TritonToHFusion pass + // instead. + if (compileOn91095Flag) { + patterns.add(patterns.getContext()); + } + + // Add convert pattern for structured custom ops. + patterns.add, + StructuredCustomOpConverter>( + patterns.getContext()); + + if (!this->namedOps) { + linalg::populateElementwiseToLinalgConversionPatterns(patterns); + } +} + +void TritonToLinalgPass::getDependentDialects(DialectRegistry ®istry) const { + registry + .insert(); +} + +LogicalResult +TritonToLinalgPass::processDescriptorOperations(ModuleOp moduleOp) { + // --- ConversionTarget: dynamic legality checks --- + mlir::ConversionTarget target(getContext()); + target.addLegalDialect(); + + // Dialect-level dynamic legality: ops are legal if none of their + // operands/results use TensorDescType. + target.addDynamicallyLegalDialect< + mlir::arith::ArithDialect, mlir::scf::SCFDialect, triton::TritonDialect>( + [](mlir::Operation *op) { + return !DescriptorConverter::hasATensorDescriptorType( + op->getOperandTypes()) && + !DescriptorConverter::hasATensorDescriptorType( + op->getResultTypes()); + }); + // Function signature legality: Triton FuncOp is legal if its inputs/outputs + // contain no TensorDescType. + target.addDynamicallyLegalOp([](triton::FuncOp funcOp) { + return !DescriptorConverter::hasATensorDescriptorType( + funcOp.getFunctionType().getInputs()) && + !DescriptorConverter::hasATensorDescriptorType( + funcOp.getFunctionType().getResults()); + }); + target.addLegalOp(); + target.addIllegalOp(); + + // --- Patterns --- + mlir::RewritePatternSet patterns(&getContext()); + patterns.add( + patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add( + patterns.getContext()); + + mlir::ConversionConfig config; + config.buildMaterializations = true; + if (failed(applyPartialConversion(moduleOp, target, std::move(patterns), + config))) { + moduleOp->emitError("failed to convert tensor descriptor operations"); + return failure(); + } + + return success(); +} + +LogicalResult +TritonToLinalgPass::processPtrBroadcastOperations(ModuleOp moduleOp) { + // --- ConversionTarget: dynamic legality checks --- + mlir::ConversionTarget target(getContext()); + target.addLegalOp(); + target.addLegalOp(); + target.addDynamicallyLegalOp([](triton::BroadcastOp op) { + if (op->hasAttr("MetaUse")) { + return true; + } + auto resultType = dyn_cast(op.getType()); + HoistBroadcast::BroadcastHoister hoister(op); + return !(isa(resultType.getElementType()) && + hoister.canBroadcast()); + }); + + // --- Patterns --- + mlir::RewritePatternSet patterns(&getContext()); + patterns.add(patterns.getContext()); + + if (failed(applyPartialConversion(moduleOp, target, std::move(patterns)))) { + moduleOp->emitError("failed to convert ptr broadcast operations"); + return failure(); + } + + return success(); +} + +LogicalResult +TritonToLinalgPass::processImplicitPermuteOperations(ModuleOp moduleOp) { + mlir::RewritePatternSet patterns(&getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add(patterns.getContext()); + patterns.add( + patterns.getContext()); + + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + LLVM_DEBUG({ llvm::dbgs() << "ImplicitPermute: rewrite MemOp failed\n"; }); + } + + mlir::PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + return runPipeline(pm, getOperation()); +} + +LogicalResult TritonToLinalgPass::processStridedLoadStoreRewriteOperations( + ModuleOp moduleOp) { + // The strided-axis rewrites below only apply in 950 SIMT mode. On other + // targets we leave strided loads to the legacy strided DMA lowering. + if (!(compileOn91095Flag && + triton::ascend::isSimtTemplateMode(compileModeFlag))) { + return success(); + } + + auto runLayoutMemoryPhase = + [&](cfg::LayoutMemoryCompatibilityPhase phase) -> LogicalResult { + mlir::PassManager phasePm(&getContext(), moduleOp.getOperationName()); + phasePm.addPass(cfg::createLayoutMemoryCompatibilityPass(phase)); + return runPipeline(phasePm, getOperation()); + }; + + // Keep the original insertion point after ImplicitPermute. Axis remains in + // the pre-Diagonal slot; the current target has no Diagonal migration, so + // the two compatibility phases run adjacently. + if (failed(runLayoutMemoryPhase( + cfg::LayoutMemoryCompatibilityPhase::BeforeDiagonal))) { + return failure(); + } + + if (failed(runLayoutMemoryPhase( + cfg::LayoutMemoryCompatibilityPhase::AfterDiagonal))) { + return failure(); + } + + // Mirror processImplicitPermuteOperations: clean up dead IR left behind by + // PtrAnalysis when the pattern decided not to rewrite (e.g. stride==1 case + // returns failure() but PtrAnalysis has already inserted helper ExtSI ops). + // Without this, downstream passes may trip on stale uses. + mlir::PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + return runPipeline(pm, getOperation()); +} + +LogicalResult +TritonToLinalgPass::processLegalStrideOperations(ModuleOp moduleOp) { + mlir::ConversionTarget target(getContext()); + target.addLegalOp(); + target.addDynamicallyLegalOp( + [](memref::ReinterpretCastOp op) { + return !LoadStoreConverter::ReinterpretCastStrideCanonicalizer:: + hasFixableZeroStride(op); + }); + + mlir::RewritePatternSet patterns(&getContext()); + patterns.add( + patterns.getContext()); + + if (failed(applyPartialConversion(moduleOp, target, std::move(patterns)))) { + moduleOp->emitError( + "failed to legalize reinterpret_cast dynamic stride(0) with size(1)"); + return failure(); + } + + return success(); +} + +void TritonToLinalgPass::runOnOperation() { + compileOn91095Flag = this->compileOn91095; + auto compileMode = triton::ascend::parseCompileMode(this->compileMode); + if (!compileMode) { + getOperation().emitError() + << "triton-to-linalg compile-mode is invalid: " << this->compileMode; + signalPassFailure(); + return; + } + compileModeFlag = *compileMode; + + auto moduleOp = getOperation(); + + // Validate the CFO handoff before descriptor conversion, canonicalization, + // or any other IR mutation can erase malformed metadata with dead code. + if (failed(validatePointerDescriptorHandoffMetadata(moduleOp))) { + moduleOp->emitError("invalid pointer descriptor handoff metadata"); + signalPassFailure(); + return; + } + + // Check if the kernel contains a cube op: tl.dot / tl.dot_scaled / al.dot + // decompose into a cube linalg.matmul, and conv1d/conv2d run on the cube + // unit. Without any of them the kernel would be tagged as a pure AIV kernel; + // with them it must be tagged mix mode, otherwise the cube tile-and-slice + // fails (cbuf overflow). + bool existDot = false; + moduleOp.walk([&](hivm::CustomOp customOp) { + if (customOp.getCoreType() == hivm::TCoreType::CUBE_AND_VECTOR || + customOp.getCoreType() == hivm::TCoreType::CUBE) { + existDot = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + moduleOp.walk([&](hivm::CustomMacroOp customMacroOp) { + if (customMacroOp.getCoreType() == hivm::TCoreType::CUBE_AND_VECTOR || + customMacroOp.getCoreType() == hivm::TCoreType::CUBE) { + existDot = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + moduleOp.walk([&](Operation *op) { + if (isa(op)) { + existDot = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + existDotFlag = existDot; + + // NOTE: existSIMTOp is intentionally computed AFTER + // processStridedLoadStoreRewriteOperations below, because that step + // materializes triton::ascend::IndirectLoadOp/IndirectStoreOp (which + // isSIMTOp() counts). Walking here (before the rewrite) would miss them and + // mislabel the kernel parallel_mode as "simd" instead of "mix_simd_simt"; + // then enable_simt would be false and the launch would not reserve + // localMemorySize for the SIMT templates -> VEC UB out-of-bounds (error 341) + // at runtime on mix-CV kernels. + bool existSIMTOp = false; + + // Execute tensor descriptor operations conversion + if (failed(processDescriptorOperations(moduleOp))) { + signalPassFailure(); + } + + // Execute implicit permute + if (failed(processImplicitPermuteOperations(moduleOp))) { + LLVM_DEBUG( + { llvm::dbgs() << "Failed to process implicit permute operations\n"; }); + signalPassFailure(); + } + + // SIMT IndirectLoad fast-path rewrite (runs after ImplicitPermute so the + // permuted access patterns have already been absorbed; this step only + // catches non-permuted last-axis stride > 1 loads). + if (failed(processStridedLoadStoreRewriteOperations(moduleOp))) { + LLVM_DEBUG({ + llvm::dbgs() << "Failed to process indirect-load rewrite operations\n"; + }); + signalPassFailure(); + } + + // Detect SIMT ops AFTER the indirect-load rewrite so the freshly materialized + // IndirectLoadOp/IndirectStoreOp are counted (drives parallel_mode -> + // "mix_simd_simt" -> enable_simt -> launch reserves localMemorySize). + moduleOp.walk([&](Operation *op) { + if (isSIMTOp(op)) { + existSIMTOp = true; + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Found SIMT op in function: "; + os << op->getName(); + os << "\n"; + }); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + // 0. Annotate Memory-Related Triton FuncOps with tensor_kind (used by + // profiling). + { + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(triton::createMarkTensorKindPass()); + if (failed(runPipeline(pm, moduleOp))) { + moduleOp->emitError("failed to run LoopCanonicalizerPass"); + signalPassFailure(); + return; + } + } + + RewritePatternSet canonicalizerPatterns(&getContext()); + // 1. Canonicalize load/store related patterns. + // The currently registered patterns rewrite pointer consumers but do not + // replace tt.make_tensor_ptr/tt.addptr descriptor rebuild roots. A future + // pattern that rewrites those producers must atomically transfer + // PointerDescriptorRebuild to its replacement; entry validation alone + // cannot preserve metadata across a producer rewrite. + this->populateTritonToLinalgCanonicalizationPatterns(canonicalizerPatterns); + if (failed( + applyPatternsGreedily(moduleOp, std::move(canonicalizerPatterns)))) { + moduleOp->emitError("failed to apply Canonicalizer Patterns"); + signalPassFailure(); + return; + } + + // Stage-1 may legitimately delete an unused, already validated boundary. + // Revalidate every surviving handoff so later phases never consume a marker + // whose structural edges were changed without updating its slot contract. + if (failed(validatePointerDescriptorHandoffMetadata(moduleOp))) { + moduleOp->emitError("invalid pointer descriptor handoff metadata"); + signalPassFailure(); + return; + } + + // 2.1 Pre-clean dead control flow before use analysis. Descriptor handoff + // attributes require stable loop positions and rebuild operands, so marked + // modules use a restricted cleanup that cannot rewrite either contract. + if (failed(preCleanBeforeUseAnalysis(moduleOp))) { + moduleOp->emitError( + "failed to pre-clean dead control-flow before use analysis"); + signalPassFailure(); + return; + } + + // 2. Perform use analysis on FuncOp. + moduleOp.walk([this](triton::FuncOp op) { + if (failed(runUseAnalysis(op))) { + signalPassFailure(); + } + }); + + if (failed(preservePointerDescriptorComputations(moduleOp))) { + moduleOp->emitError("invalid pointer descriptor handoff metadata"); + signalPassFailure(); + return; + } + + RewritePatternSet patterns(&getContext()); + ConversionTarget target(getContext()); + TritonTypeConverter tritonTypeConverter{}; + + // 3. Mark legal dialects and operations. + this->addDynamicLegal(target, tritonTypeConverter); + + // 4. Mark ops that must be converted explicitly (e.g. tt.scan). + auto loopOpLegalFn = [](LoopLikeOpInterface loopOp) { + Operation *op = loopOp.getOperation(); + if (op->hasAttr(controlflow::kPointerDescriptorBoundaryAttr)) { + return hasPointerFreeControlFlowBoundary(loopOp); + } + return !op->hasAttr("UnhandledLoopOp"); + }; + + target.addIllegalOp(); + target.addIllegalOp(); + target.addDynamicallyLegalOp(loopOpLegalFn); + target.addDynamicallyLegalOp(loopOpLegalFn); + + // 5. Register converters for all illegal Triton ops. + // Execute ptr broadcast operations conversion + if (failed(processPtrBroadcastOperations(moduleOp))) { + signalPassFailure(); + } + this->populateTritonToLinalgConversionPatterns(tritonTypeConverter, patterns, + LAUNCH_GRID_RANK); + + // 6. Inject program id / number of programs arguments into each Triton kernel + // function. + for (auto func : getOperation().getOps()) { + addProgramInfo(func, globalKernel); + } + + moduleOp.walk([this](LoopLikeOpInterface loopOp) { + auto *op = loopOp.getOperation(); + // CFO-expanded pointer loops already carry policy-owned descriptor + // components. They require ordinary nested-op conversion, not the legacy + // pointer-loop decomposition a second time. Unmarked loops are delegated + // only when their original boundary still carries Triton pointer/address + // state. A fixed-layout memref is already a complete SCF value; the fact + // that its init is produced by reinterpret_cast does not make it BlockData. + bool hasExpandedPointerDescriptor = + op->hasAttr(mlir::triton::controlflow::kPointerDescriptorBoundaryAttr); + if (!op->hasAttr("ExtractedLoadOrStore") && + needsLegacyBlockDataLoopRewrite(loopOp)) + op->setAttr("UnhandledLoopOp", UnitAttr::get(op->getContext())); + + if (hasExpandedPointerDescriptor) + return; + + for (auto res : loopOp->getResults()) { + if (auto tensorType = dyn_cast(res.getType()); + tensorType && + !isa(tensorType.getElementType())) { + IRRewriter rewriter(op->getContext()); + rewriter.setInsertionPointAfter(op); + auto newVal = + rewriter.create(op->getLoc(), res.getType(), res); + rewriter.replaceAllUsesExcept(res, newVal, newVal); + } + } + }); + + // 7. Convert ops. + LogicalResult conversionResult = + applyPartialConversion(moduleOp, target, std::move(patterns)); + + if (failed(conversionResult)) { + moduleOp->emitError("failed to apply Conversion Patterns"); + signalPassFailure(); + return; + } + + // These markers are contracts with this conversion pass. Remove them only + // after successful conversion so failure reproducers retain ownership, + // exact dynamic descriptor slots, and complete rebuild roots. + moduleOp.walk([](LoopLikeOpInterface loopOp) { + loopOp->removeAttr(controlflow::kPointerDescriptorBoundaryAttr); + }); + moduleOp.walk([](Operation *op) { + op->removeAttr(controlflow::kPointerDescriptorRebuildAttr); + op->removeAttr(controlflow::kPointerDescriptorOffsetFormAttr); + op->removeAttr(controlflow::kPointerDescriptorStructuredAxesAttr); + }); + moduleOp.walk([](Operation *op) { + op->removeAttr(TTOpConverters::kScalarPointerCarrierBoundaryAttr); + }); + + // Conversion can expose a make_range carrier whose loop result is already + // dead. Remove only the proven dead range/update chain before generic + // canonicalization; live tensor carriers and scalar address state remain. + eraseDeadRangeCarriers(moduleOp); + + // 7.1 Workaround: fold duplicated one-hot reconstruction emitted after + // ArgMax lowering. The issue is not in triton::ReduceOp semantics themselves; + // redundant value reconstruction is materialized later and can lower to + // incorrect code on Ascend, so this is fixed post-conversion on + // linalg::ReduceOp. + { + RewritePatternSet foldPatterns(&getContext()); + TTOpConverters::populatePostConversionCanonicalizationPatterns( + foldPatterns); + + if (failed(applyPatternsGreedily(moduleOp, std::move(foldPatterns)))) { + moduleOp->emitError("failed to fold one-hot gather after max_with_index"); + signalPassFailure(); + return; + } + } + + // Execute legal stride operations conversion + if (failed(processLegalStrideOperations(moduleOp))) { + signalPassFailure(); + } + + // 8. Convert function prologue/epilogue. + moduleOp.walk([&](triton::FuncOp func) { + this->convertTTFunc(func, existDot, existSIMTOp); + }); + + rewriteDevicePrintOffsets(moduleOp); + + // 9. Clean up dead code and simplify IR. + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + if (failed(runPipeline(pm, getOperation()))) { + signalPassFailure(); + } + + specializeFullRowCopies(moduleOp); + + // 10. Collapses call-site locations whose callee is an inlined Triton stdlib + // helper (under site-packages) down to their caller (user-file) frame + // Opt-in via LLVM_EXTRACT_DI_LOCAL_VARIABLES=1. + { + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(triton::createCanonicalizeDebugLocationsPass()); + if (failed(runPipeline(pm, moduleOp))) { + moduleOp->emitWarning("CanonicalizeDebugLocationsPass pass failed"); + } + } + + // 11. Deduplicate debug NOPs inserted by converters. + // Opt-in via LLVM_EXTRACT_DI_LOCAL_VARIABLES=1. + { + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(triton::createDeduplicateDebugNopsPass()); + if (failed(runPipeline(pm, moduleOp))) { + moduleOp->emitWarning("DeduplicateDebugNops pass failed"); + // Non-fatal: dedup is a quality improvement, not a correctness pass. + } + } + + // Calculate size of PointerCastOp precisely + SmallVector castOps; + + moduleOp.walk([&](hivm::PointerCastOp op) { castOps.push_back(op); }); + + for (auto op : castOps) { + SmallVector reinterpretCastOps; + for (Operation *user : op->getUsers()) { + if (auto reinterpretCast = dyn_cast(user)) + reinterpretCastOps.push_back(reinterpretCast); + } + if (reinterpretCastOps.empty()) + continue; + + bool isScalarPointerCarrier = op->hasAttr(kScalarPointerCarrierAttr); + IRRewriter rewriter(&getContext()); + rewriter.setInsertionPointAfter(op); + Value addr = op.getAddrs()[0]; + auto elementType = + cast(op.getResult().getType()).getElementType(); + + for (memref::ReinterpretCastOp reinterpretCastOp : reinterpretCastOps) { + auto sizes = reinterpretCastOp.getStaticSizes(); + auto staticStrides = reinterpretCastOp.getStaticStrides(); + auto strides = reinterpretCastOp.getStrides(); + if (reinterpretCastOp.getStaticOffsets().size() != 1) { + reinterpretCastOp->emitError( + "IntToPtrOp must converted to PointerCastOp of " + "memref type"); + signalPassFailure(); + return; + } + int64_t castOpSize = 0; + SmallVector dynamicSizes; + for (const auto &[size, stride] : llvm::zip_equal(sizes, staticStrides)) { + assert(!ShapedType::isDynamic(size)); + if (ShapedType::isDynamic(stride)) + dynamicSizes.push_back(size); + else + castOpSize = size * stride; + } + rewriter.setInsertionPoint(reinterpretCastOp); + Value dynamicSize = rewriter.create( + op.getLoc(), rewriter.getIndexAttr(castOpSize)); + for (const auto &[size, stride] : + llvm::zip_equal(dynamicSizes, strides)) { + Value axisSize = rewriter.create( + op.getLoc(), rewriter.getIndexAttr(size)); + axisSize = + rewriter.create(op.getLoc(), stride, axisSize); + dynamicSize = + rewriter.create(op.getLoc(), dynamicSize, axisSize); + } + auto staticOffset = reinterpretCastOp.getStaticOffsets()[0]; + auto materializeOffset = [&](Type targetType) -> Value { + if (ShapedType::isDynamic(staticOffset)) { + Value offset = reinterpretCastOp.getOffsets()[0]; + if (offset.getType() != targetType) + offset = rewriter.create(op.getLoc(), + targetType, offset); + return offset; + } + if (targetType.isIndex()) + return rewriter.create(op.getLoc(), + staticOffset); + return rewriter.create( + op.getLoc(), rewriter.getIntegerAttr(targetType, staticOffset)); + }; + + auto memrefType = MemRefType::get({ShapedType::kDynamic}, elementType); + auto createPointerCast = [&](Value address, Value capacity) { + auto cast = rewriter.create( + op.getLoc(), memrefType, address, capacity); + auto mark = + rewriter.create(op.getLoc(), cast.getResult()); + mark->setAttr(hivm::AddressSpaceAttr::getMnemonic(), + {hivm::AddressSpaceAttr::get(rewriter.getContext(), + hivm::AddressSpace::GM)}); + return cast; + }; + + if (!isScalarPointerCarrier) { + // Preserve main-dev behavior for ordinary PointerCast operations. The + // provenance-gated branches below exist specifically for scalar + // pointer carriers introduced by this conversion pipeline. + Value offsetValue = materializeOffset(addr.getType()); + Value elementTypeSize; + if (auto intType = dyn_cast(elementType)) { + elementTypeSize = rewriter.create( + op.getLoc(), + rewriter.getIntegerAttr(addr.getType(), intType.getWidth() / 8)); + } else if (auto floatType = dyn_cast(elementType)) { + elementTypeSize = rewriter.create( + op.getLoc(), rewriter.getIntegerAttr(addr.getType(), + floatType.getWidth() / 8)); + } else { + llvm_unreachable("Cannot get memory size"); + } + offsetValue = rewriter.create(op.getLoc(), offsetValue, + elementTypeSize); + Value realAddr = + rewriter.create(op.getLoc(), addr, offsetValue); + auto newCastOp = createPointerCast(realAddr, dynamicSize); + + auto oldResultType = + cast(reinterpretCastOp.getResult().getType()); + MemRefType newResultType = oldResultType; + if (auto stridedLayout = + dyn_cast(oldResultType.getLayout())) { + if (!ShapedType::isDynamic(stridedLayout.getOffset())) { + auto newLayout = + StridedLayoutAttr::get(rewriter.getContext(), /*offset=*/0, + stridedLayout.getStrides()); + newResultType = MemRefType::get( + oldResultType.getShape(), oldResultType.getElementType(), + newLayout, oldResultType.getMemorySpace()); + } + } + rewriter.replaceOpWithNewOp( + reinterpretCastOp, newResultType, newCastOp, ValueRange({}), + reinterpretCastOp.getSizes(), reinterpretCastOp.getStrides(), + SmallVector({0}), reinterpretCastOp.getStaticSizes(), + reinterpretCastOp.getStaticStrides()); + continue; + } + + if (reachesLayoutSensitiveBoundary(reinterpretCastOp.getResult())) { + // Keep the original offset and result type at externally typed + // boundaries. The replacement PointerCast still starts at the old + // address, so its capacity must include the leading view displacement. + Value pointerCapacity = dynamicSize; + Value offsetElements = materializeOffset(pointerCapacity.getType()); + Value leadingExtent = offsetElements; + if (ShapedType::isDynamic(staticOffset)) { + Value zero = rewriter.create(op.getLoc(), 0); + leadingExtent = rewriter.create(op.getLoc(), + offsetElements, zero); + } + if (ShapedType::isDynamic(staticOffset) || staticOffset > 0) + pointerCapacity = rewriter.create( + op.getLoc(), pointerCapacity, leadingExtent); + auto newCastOp = createPointerCast(addr, pointerCapacity); + rewriter.modifyOpInPlace(reinterpretCastOp, [&] { + reinterpretCastOp.getSourceMutable().assign(newCastOp.getResult()); + }); + continue; + } + + Value offsetValue = materializeOffset(addr.getType()); + Value elementTypeSize; + if (auto intType = dyn_cast(elementType)) { + elementTypeSize = rewriter.create( + op.getLoc(), + rewriter.getIntegerAttr(addr.getType(), intType.getWidth() / 8)); + } else if (auto floatType = dyn_cast(elementType)) { + elementTypeSize = rewriter.create( + op.getLoc(), + rewriter.getIntegerAttr(addr.getType(), floatType.getWidth() / 8)); + } else { + llvm_unreachable("Cannot get memory size"); + } + offsetValue = rewriter.create(op.getLoc(), offsetValue, + elementTypeSize); + Value realAddr = + rewriter.create(op.getLoc(), addr, offsetValue); + auto newCastOp = createPointerCast(realAddr, dynamicSize); + // realAddr already includes the old reinterpret-cast offset in bytes. + // The replacement view therefore starts at offset zero, and its result + // type must describe the same rebased layout. Reusing the old type here + // would combine static_offsets=[0] with (for example) a type-level + // offset of 1, which is rejected by the ReinterpretCast verifier. + auto oldResultType = + cast(reinterpretCastOp.getResult().getType()); + SmallVector rebasedStrides( + oldResultType.getStridesAndOffset().first); + auto rebasedResultType = MemRefType::get( + oldResultType.getShape(), oldResultType.getElementType(), + StridedLayoutAttr::get(oldResultType.getContext(), /*offset=*/0, + rebasedStrides), + oldResultType.getMemorySpace()); + + // Keep the old result and replacement types equal while RAUW updates all + // users to the rebased descriptor type. + rewriter.modifyOpInPlace(reinterpretCastOp, [&] { + reinterpretCastOp.getResult().setType(rebasedResultType); + }); + auto rebasedReinterpretCast = + rewriter.replaceOpWithNewOp( + reinterpretCastOp, rebasedResultType, newCastOp, ValueRange({}), + reinterpretCastOp.getSizes(), reinterpretCastOp.getStrides(), + SmallVector({0}), reinterpretCastOp.getStaticSizes(), + reinterpretCastOp.getStaticStrides()); + if (failed(propagateRebasedSubviewTypes( + rebasedReinterpretCast.getResult(), rewriter))) { + rebasedReinterpretCast.emitError( + "failed to propagate rebased layout through subview users"); + signalPassFailure(); + return; + } + } + if (op->use_empty()) + rewriter.eraseOp(op); + } + + // ScalarPointerCarrier is a pass-local provenance marker. Keep it through + // PointerCast post-processing so only known scalar-address carriers use the + // new layout path. A carrier that is consumed directly (without an + // intermediate reinterpret_cast) is not rebuilt by the loop above, so give + // that remaining GM pointer the same address-space annotation before the + // provenance marker is removed. + moduleOp.walk([](hivm::PointerCastOp pointerCast) { + if (!pointerCast->hasAttr(kScalarPointerCarrierAttr)) + return; + OpBuilder builder(pointerCast); + builder.setInsertionPointAfter(pointerCast); + auto mark = builder.create(pointerCast.getLoc(), + pointerCast.getResult()); + mark->setAttr(hivm::AddressSpaceAttr::getMnemonic(), + {hivm::AddressSpaceAttr::get(pointerCast.getContext(), + hivm::AddressSpace::GM)}); + pointerCast->removeAttr(kScalarPointerCarrierAttr); + }); + + // Try interleave optimization + llvm::DenseMap> interleaveCandidate; + llvm::DenseMap> + interleaveCandidateWithMask; + moduleOp.walk([&](bufferization::MaterializeInDestinationOp materializeOp) { + if (auto reinterpretCastOp = + materializeOp.getDest() + .getDefiningOp()) { + if (llvm::isa(reinterpretCastOp.getSource()) && + reinterpretCastOp.getStaticStrides().back() == 2) { + interleaveCandidate[llvm::cast( + reinterpretCastOp.getSource())] + .push_back(materializeOp); + } + } + + // Difference is that converted op chain of store with mask has + // `memref::SubViewOp` + if (auto subviewOp = + materializeOp.getDest().getDefiningOp()) { + if (!llvm::isa( + materializeOp.getSource().getDefiningOp())) + return WalkResult::advance(); + + if (auto reinterpretCastOp = + subviewOp.getSource() + .getDefiningOp()) { + if (llvm::isa(reinterpretCastOp.getSource()) && + reinterpretCastOp.getStaticStrides().back() == 2) { + interleaveCandidateWithMask[llvm::cast( + reinterpretCastOp.getSource())] + .push_back(materializeOp); + } + } + } + + return WalkResult::advance(); + }); + + for (auto [blockArg, materializeVec] : interleaveCandidate) { + // Just enable optimization where exists double materializeOp with same + // block argument destination. + if (materializeVec.size() != 2) + continue; + auto result = InterleaveStatusOptimization(materializeVec); + } + + for (auto [blockArg, materializeVec] : interleaveCandidateWithMask) { + if (materializeVec.size() != 2) + continue; + auto result = InterleaveStatusWithMaskOptimization(materializeVec); + } + + // Force to add an argument at the beginning of function arguments, which + // represents stub arg for workspace. Default type is memref + for (auto func : getOperation().getOps()) { + if (!func->hasAttr("global_kernel")) + continue; + + auto context = func.getContext(); + constexpr int64_t syncBlockLockArgIdx = 0; + NamedAttribute syncBlockLockArgAttr( + StringAttr::get(context, "syncBlockLock"), UnitAttr::get(context)); + MemRefType syncBlockLockArgType = + MemRefType::get(SmallVector(1, ShapedType::kDynamic), + IntegerType::get(context, 8)); + llvm::LogicalResult syncBlockLockArg = + func.insertArgument(syncBlockLockArgIdx, // argIndex + syncBlockLockArgType, // argType + nullptr, func->getLoc()); // dicAttr + func->setAttr("SyncBlockLockArgIdx", + IntegerAttr::get(IntegerType::get(&getContext(), 64), + 0)); // 64: 64位整型 + + constexpr int64_t workspaceArgIdx = 1; + MemRefType workspaceArgType = + MemRefType::get(SmallVector(1, ShapedType::kDynamic), + IntegerType::get(context, 8)); + NamedAttribute workspaceArgAttr(StringAttr::get(context, "workspace"), + UnitAttr::get(context)); + + llvm::LogicalResult workspaceArg = + func.insertArgument(/*argIndex*/ workspaceArgIdx, + /*argType*/ workspaceArgType, + /*dicAttr*/ nullptr, func->getLoc()); + func->setAttr("WorkspaceArgIdx", + IntegerAttr::get(IntegerType::get(&getContext(), 64), + 1)); // 64: 64位整型 + } + + // Fix the Location info + moduleOp.walk([&](Operation *op) { + auto loc = op->getLoc(); + if (isa(loc)) { + llvm::SmallPtrSet stopOps; + traverseForwardUpdateUserChainIf( + op, + /*conditionFn*/ + [](Operation *curOp) { return false; }, + /*stopFn*/ + [](Operation *curOp) { return !isa(curOp->getLoc()); }, + /*actionFn*/ + nullptr, stopOps); + if (stopOps.empty()) { + op->emitWarning() << *op << " and its users all have no location!"; + } else { + Operation *goodOp = *stopOps.begin(); + op->setLoc(goodOp->getLoc()); + } + } + return WalkResult::advance(); + }); +} + +std::unique_ptr> +triton::createTritonToLinalgPass(bool globalKernel, bool namedOps, + bool enableNd2nzOnVector, + bool enableSelectAnalysis, bool compileOn91095, + const std::string &compileMode) { + return std::make_unique( + globalKernel, namedOps, enableNd2nzOnVector, enableSelectAnalysis, + compileOn91095, compileMode); +} + +std::unique_ptr> triton::createTritonToLinalgPass() { + return std::make_unique(); +} diff --git a/third_party/ascend/lib/TritonToLinalg/UseAnalysis.cpp b/third_party/ascend/lib/TritonToLinalg/UseAnalysis.cpp new file mode 100644 index 0000000000..1dfacff1b1 --- /dev/null +++ b/third_party/ascend/lib/TritonToLinalg/UseAnalysis.cpp @@ -0,0 +1,636 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/TritonToLinalg/UseAnalysis.h" +#include "ascend/include/Utils/Utils.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "bishengir/Dialect/Scope/IR/Scope.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "mlir/Analysis/DataFlow/ConstantPropagationAnalysis.h" +#include "mlir/Analysis/DataFlow/DeadCodeAnalysis.h" + +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Debug.h" + +using namespace mlir; +using namespace triton; +using namespace dataflow; + +#define DEBUG_TYPE "triton-use-analysis" + +std::string stringifyUseType(UseType useTy) { + std::string ret; + if (useTy == UseType::MetaUse) { + ret = "MetaUse"; + } else if (useTy == UseType::DataUse) { + ret = "DataUse"; + } else if (useTy == UseType::MixUse) { + ret = "MixUse"; + } else if (useTy == UseType::Undefined) { + ret = "Undefined"; + } + return ret; +} + +#if LLVM_VERSION_MAJOR >= 20 +LogicalResult +triton::UseAnalysis::visitOperation(Operation *op, ArrayRef operands, + ArrayRef results) { +#else +void triton::UseAnalysis::visitOperation(Operation *op, + ArrayRef operands, + ArrayRef results) { +#endif + + if (op->getResults().size() == 1) { + auto resultType = dyn_cast(op->getResult(0).getType()); + if (resultType && isa(resultType.getElementType())) { + for (auto opnd : operands) { + propagateUse(opnd, UseType::MetaUse); + } + } + } + + TypeSwitch(op) + .Case([&](auto load) { + propagateUse(operands[0], UseType::MetaUse); + auto mask = load.getMask(); + auto other = load.getOther(); + if (mask) { + assert(mask != other && "mask and other cannot be the same"); + propagateUse(operands[1], UseType::MetaUse); + } + if (other) { + propagateUse(operands[2], UseType::MetaUse); + } + }) + .Case([&](auto print) { + for (auto operand : operands) + propagateUse(operand, UseType::DataUse); + }) + .Case( + [&](auto assert) { propagateUse(operands[0], UseType::DataUse); }) + .Case([&](auto store) { + propagateUse(operands[0], UseType::MetaUse); + propagateUse(operands[1], UseType::DataUse); + auto value = store.getValue(); + auto mask = store.getMask(); + if (mask) { + assert(mask != value && "mask and data cannot be the same"); + propagateUse(operands[2], UseType::MetaUse); + } + }) + .Case([&](auto store) { + propagateUse(operands[0], UseType::MetaUse); + propagateUse(operands[1], UseType::DataUse); + propagateUse(operands[2], UseType::DataUse); + auto mask = store.getMask(); + if (mask) { + propagateUse(operands[3], UseType::DataUse); + } + }) + // Consider triton::AtomicRMWOp as store operation + .Case([&](auto atomicOp) { + propagateUse(operands[0], UseType::MixUse); + propagateUse(operands[1], UseType::DataUse); + auto value = atomicOp.getVal(); + auto mask = atomicOp.getMask(); + if (mask) { + assert(mask != value && "mask and data cannot be the same"); + propagateUse(operands[2], UseType::MetaUse); + } + }) + .Case([&](auto atomicOp) { + propagateUse(operands[0], UseType::MetaUse); + propagateUse(operands[1], UseType::DataUse); + propagateUse(operands[2], UseType::DataUse); + auto value = atomicOp.getVal(); + }) + .Case([&](auto dot) { + propagateResults(operands[0], results); + propagateResults(operands[1], results); + + auto opc = dot.getC(); + triton::SplatOp splat; + if (opc) { + splat = opc.template getDefiningOp(); + } + + if (opc && splat && splat.getSrc().getDefiningOp()) { + propagateUse(operands[2], UseType::MetaUse); + } else { + propagateUse(operands[2], UseType::DataUse); + } + }) + .Case([&](auto loopOp) { + for (const auto &[yield, init, result] : llvm::zip_equal( + loopOp.getYieldedValues(), loopOp.getInits(), results)) { + propagateResults(getLatticeElement(yield), {result}); + propagateResults(getLatticeElement(init), {result}); + } + }) + .Case([&](auto reduceOp) { + for (auto operand : operands) { + propagateUse(operand, UseType::DataUse); + } + }) + .Case([&](auto extractOp) { + for (auto operand : operands) { + propagateUse(operand, UseType::DataUse); + } + }) + .Case( + [&](auto fixpipeOp) { propagateUse(operands[0], UseType::DataUse); }) + .Case( + [&](auto copyOp) { propagateUse(operands[0], UseType::DataUse); }) + .Case([&](auto customOp) { + for (auto operand : operands) { + propagateUse(operand, UseType::MixUse); + } + }) + .Default([&](Operation *op) { + // this condition account for tt.addptr + for (auto operand : operands) { + propagateResults(operand, results); + } + }); +#if LLVM_VERSION_MAJOR >= 20 + return success(); +#endif +} + +void setMixUseRecursively(Operation *rootOp, bool applyRoot = true) { + traverseBackwardUpdateOperandChainIf( + rootOp, + // ConditionFn + [rootOp, applyRoot](Operation *curOp) { + for (auto res : curOp->getResults()) { + auto tensorType = dyn_cast(res.getType()); + if (tensorType && + isa(tensorType.getElementType())) + return false; + } + return isMetaUse(curOp) && (curOp != rootOp || applyRoot); + }, + // StopFn + [rootOp](Operation *curOp) { + return isa(curOp) && curOp != rootOp; + }, + // ActionFn + [](OpBuilder &b, Operation *op) { + LLVM_DEBUG({ op->setAttr("MixUse", UnitAttr::get(b.getContext())); }); + op->removeAttr("MetaUse"); + }); +} + +static void setMixUseFromValue(Value v) { + if (auto *defOp = v.getDefiningOp()) { + setMixUseRecursively(defOp); + return; + } + + auto blockArg = dyn_cast(v); + if (!blockArg) { + return; + } + + auto *parentOp = blockArg.getOwner()->getParentOp(); + auto loopLikeOp = dyn_cast_or_null(parentOp); + if (!loopLikeOp) { + return; + } + + if (OpOperand *init = loopLikeOp.getTiedLoopInit(blockArg)) { + if (auto *initDefOp = init->get().getDefiningOp()) + setMixUseRecursively(initDefOp); + } + + if (OpOperand *yielded = loopLikeOp.getTiedLoopYieldedValue(blockArg)) { + if (auto *yieldDefOp = yielded->get().getDefiningOp()) + setMixUseRecursively(yieldDefOp); + } +} + +std::optional isIterArgMixUse(Value v, Value target, + const DataFlowSolver &solver) { + auto defOp = v.getDefiningOp(); + auto *use = solver.lookupState(v); + if ((use && use->type == UseType::DataUse) || + isa_and_nonnull(defOp)) + return true; + if (v == target) + return false; + if (!defOp) + return std::nullopt; + for (auto oper : defOp->getOperands()) { + auto res = isIterArgMixUse(oper, target, solver); + if (res.has_value()) + return res.value() || !isMetaUse(defOp); + } + return std::nullopt; +} + +void postProcessWhileOp(scf::WhileOp op, const DataFlowSolver &solver) { + for (const auto &[res, arg] : + llvm::zip_equal(op->getResults(), op.getConditionOp().getArgs())) { + auto *defOp = arg.getDefiningOp(); + if (!defOp) + continue; + auto *use = solver.lookupState(res); + if (use && use->type == UseType::DataUse) + setMixUseRecursively(defOp); + } + for (const auto &[yield, regionArg] : llvm::zip_equal( + op.getYieldOp().getOperands(), op.getBeforeArguments())) { + auto *defOp = yield.getDefiningOp(); + if (!defOp) + continue; + if (isIterArgMixUse(yield, regionArg, solver).value_or(false)) + setMixUseRecursively(defOp); + } +} + +void postProcessLoopOp(LoopLikeOpInterface loopOp, + const DataFlowSolver &solver) { + if (auto whileOp = dyn_cast(loopOp.getOperation())) { + postProcessWhileOp(whileOp, solver); + return; + } + for (const auto &[res, yield, regionArg] : + llvm::zip_equal(loopOp->getResults(), loopOp.getYieldedValues(), + loopOp.getRegionIterArgs())) { + auto *defOp = yield.getDefiningOp(); + if (!defOp) + continue; + auto *use = solver.lookupState(res); + if ((use && use->type == UseType::DataUse) || + isIterArgMixUse(yield, regionArg, solver).value_or(false)) + setMixUseRecursively(defOp); + } +} + +LogicalResult triton::runUseAnalysis(triton::FuncOp &funcOp) { + MLIRContext *context = funcOp.getContext(); + SymbolTableCollection symbolTable; + + DataFlowSolver solver; + solver.load(); + solver.load(); + solver.load(symbolTable); + if (failed(solver.initializeAndRun(funcOp))) { + return failure(); + } + auto &os = llvm::dbgs(); + // Walk the func op, convert tags on operands to tags on operations + funcOp.walk([&](Operation *op) { + LLVM_DEBUG({ os << "[UseAnalysis] op is " << *op << "\n"; }); + UseType useType = UseType::Undefined; + for (auto result : op->getResults()) { + LLVM_DEBUG({ os << "[UseAnalysis] ===> result is " << result << "\n"; }); + auto use = solver.lookupState(result); + assert(use && "Lattice value not found"); + auto thisUseType = use->type; + LLVM_DEBUG({ + os << "[UseAnalysis] ==========> useType is " + << stringifyUseType(thisUseType) << "\n"; + }); + if (thisUseType == UseType::Undefined) { + continue; + } + if (useType == UseType::Undefined) { + useType = thisUseType; + } + if (thisUseType == UseType::MixUse || thisUseType != useType) { + useType = UseType::MixUse; + break; + } + } + + if (useType == UseType::Undefined) { + LLVM_DEBUG({ op->setAttr("Undefined", UnitAttr::get(context)); }); + return; + } else if (useType == UseType::MetaUse) { + auto memEffect = dyn_cast(op); + if (memEffect) { + if (isa(op)) { + LLVM_DEBUG( + { os << "force protecting side-effect op:" << *op << "\n"; }); + op->setAttr("DataUse", UnitAttr::get(context)); + return; + } + } + if (!isa(op)) { + assert(op->getNumResults() == 1 && + "Ops used for meta computation are expected to have one result"); + } + for (auto it = 0; it < op->getNumResults(); ++it) { + // Only set the tag if the operation uses tensors + if (isa(op->getResult(it).getType()) || + (isa(op) && + op->hasAttr(ConverterUtils::discreteAttrName)) || + (isa(op) && + isa(op->getResult(it).getType()))) { + // Setting tag for erasing op later + op->setAttr("MetaUse", UnitAttr::get(context)); + } + } + return; + } else if (useType == UseType::DataUse) { + LLVM_DEBUG({ op->setAttr("DataUse", UnitAttr::get(context)); }); + return; + } + + assert(useType == UseType::MixUse); + + // If the operation only produces scalars, no need to clone it + bool shapedResult = true; + for (auto result : op->getResults()) + shapedResult &= isa(result.getType()); + if (!shapedResult || + isa(op)) { + LLVM_DEBUG({ op->setAttr("MixUse", UnitAttr::get(context)); }); + return; + } + llvm::SetVector metaUsers; + for (auto result : op->getResults()) { + for (auto user : result.getUsers()) { + TypeSwitch(user) + .Case([&](auto load) { + auto ptr = load.getPtr(); + auto mask = load.getMask(); + auto other = load.getOther(); + if (result == ptr || result == mask || result == other) { + metaUsers.insert(user); + } + }) + .Case([&](auto store) { + auto ptr = store.getPtr(); + auto mask = store.getMask(); + if (result == ptr || result == mask) { + metaUsers.insert(user); + } + }) + .Case([&](auto indirectstore) { + auto src = indirectstore.getSrc(); + // Only src is MetaUse (see visitOperation). offsets/value/mask + // are DataUse + if (result == src) { + metaUsers.insert(user); + } + }) + .Case([&](auto atomicOp) { + auto ptr = atomicOp.getPtr(); + auto mask = atomicOp.getMask(); + if (result == ptr || result == mask) + metaUsers.insert(user); + }) + .Case([&](auto atomicOp) { + auto ptr = atomicOp.getPtr(); + if (result == ptr) + metaUsers.insert(user); + }) + .Case([&](auto dot) { + auto opc = dot.getC(); + triton::SplatOp splat; + if (opc) { + splat = opc.template getDefiningOp(); + } + + if (opc && splat && + splat.getSrc().getDefiningOp()) { + metaUsers.insert(user); + } + }) + .Case([&](auto print) {}) + .Case([](auto) { + // Custom operands are MixUse in visitOperation, even when all + // results are used only as memory addresses. Keep the call on + // the materialized value, not a MetaUse clone that is erased + // before the custom operation's operands are converted. + }) + .Default([&](Operation *op) { + bool allMeta = true; + for (auto res : op->getResults()) { + auto resUse = solver.lookupState(res); + if (resUse->type != UseType::MetaUse) { + allMeta = false; + break; + } + } + if (allMeta) { + metaUsers.insert(user); + } + }); + } + } + + // If the operation doesn't have direct meta users, no need to clone it + if (metaUsers.empty()) { + LLVM_DEBUG({ op->setAttr("MixUse", UnitAttr::get(context)); }); + return; + } + + if (isa(op)) + return; + + if (isa(op)) + return; + + // Clone the operation; switch all meta users to use the clone + OpBuilder builder(op); + auto clone = builder.clone(*op); + LLVM_DEBUG({ op->setAttr("MixUse", UnitAttr::get(context)); }); + + // Setting tag for erasing op later + clone->setAttr("MetaUse", UnitAttr::get(context)); + + for (auto [res_i, result] : llvm::enumerate(op->getResults())) { + for (auto user : metaUsers) { + for (auto &operand : user->getOpOperands()) { + if (operand.get() == result) { + operand.set(clone->getResult(res_i)); + } + } + } + } + }); + LLVM_DEBUG({ + os << "[UseAnalysis] Before post-process, funcOp is " << *funcOp << "\n"; + }); + // Post-process + funcOp.walk([&](Operation *op) { + // Handle indirect load and store case. + // For example, load(1st) -> computeOp -> load(2nd), + // or load -> computeOp -> store + // The first load is IndirectLoadInterfaceOp. + // Do not inplace replace MetaUse by MixUse. Because the condition checking + // depends on that the op has the attr of MetaUse. + // Handle the indirect load interface op + // We first trace from the 1st load to the 2nd load with the ops between + // them marked as MixUse. Then we traceback from the 2nd load to mark defs + // MixUse. + if (opIsIndirectLoad(op) || opIsIndirectCalc(op)) { + LLVM_DEBUG({ + os << "[UseAnalysis] Found indirect load interface op: " << *op << "\n"; + }); + llvm::SmallPtrSet stopOps; + // Modify the users of this op's result. + traverseForwardUpdateUserChainIf( + op, + /*conditionFn*/ + [op](Operation *curOp) { return isMetaUse(curOp) && curOp != op; }, + /*stopFn*/ + [&](Operation *curOp) { + // triton::LoadOp or triton::StoreOp without MetaUse means + // it is an indirect load or store + // instead of the load providing the offset. + // The pattern is as follows, + // load -> ops -> load + // load -> ops -> store + // We need to ensure the intermediate ops are marked MixUse + // so that they will be replaced instead of be erased without + // conversion. + return (isa(curOp)) && + !isMetaUse(curOp); + }, + /*actionFn*/ + [](OpBuilder &b, Operation *op) { setMixUseRecursively(op); }, + stopOps); + LLVM_DEBUG({ + os << "[UseAnalysis] stopOps are \n"; + for (auto [idx, stopOp] : llvm::enumerate(stopOps)) + os << idx << ": " << *stopOp << "\n"; + }); + LLVM_DEBUG({ + os << "[UseAnalysis] After trace, funcOp is " << *funcOp << "\n"; + }); + for (auto *stopOp : stopOps) + setMixUseRecursively(stopOp, /*applyRoot=*/false); + LLVM_DEBUG({ + os << "[UseAnalysis] After traceback of stopOp, funcOp is " << *funcOp + << "\n"; + }); + // Modify this op. + LLVM_DEBUG({ op->setAttr("MixUse", UnitAttr::get(context)); }); + op->removeAttr("MetaUse"); + } + if (op->hasAttr(ConverterUtils::discreteAttrName)) + setMixUseRecursively(op); + if (auto loopOp = dyn_cast(op)) { + postProcessLoopOp(loopOp, solver); + } else if (auto ifOp = dyn_cast(op)) { + SmallVector yields(ifOp.thenYield().getOperands()); + if (!ifOp.getElseRegion().empty()) + yields.append(llvm::to_vector(ifOp.elseYield().getOperands())); + for (auto yield : yields) { + setMixUseFromValue(yield); + } + } else if (auto atomicRmwOp = dyn_cast(op)) { + auto mask = atomicRmwOp.getMask(); + if (mask && op->hasAttr(ConverterUtils::discreteMaskAttrName)) + setMixUseRecursively(mask.getDefiningOp()); + } + }); + // Remove MetaUse in case of MixUse existing in the op + funcOp.walk([&](Operation *op) { + if (isMetaUse(op) && isMixUse(op)) { + op->removeAttr("MetaUse"); + } + }); + // Masked load with non-scalar tensor `other` lowers as + // arith.select(mask, loaded, other) so the mask SSA must stay live. + funcOp.walk([&](triton::LoadOp load) { + Value mask = load.getMask(); + Value other = load.getOther(); + if (!mask || !other) + return; + if (other.getDefiningOp()) + return; + if (auto c = other.getDefiningOp()) { + if (auto dense = dyn_cast(c.getValue())) { + if (dense.isSplat()) + return; + } + } + if (auto *def = mask.getDefiningOp()) + setMixUseRecursively(def); + }); + // hivm.custom present library call, shouldn't be metause + funcOp.walk([&](hivm::CustomOp op) { + if (isMetaUse(op)) { + op->removeAttr("MetaUse"); + } + }); + funcOp.walk([&](hivm::CustomMacroOp op) { + if (isMetaUse(op)) { + op->removeAttr("MetaUse"); + } + }); + LLVM_DEBUG({ + os << "[UseAnalysis] After post-process, funcOp is " << *funcOp << "\n"; + }); + return success(); +} + +static bool isScalarI1ToI8PointerBitcast(Operation *op) { + auto bitcast = dyn_cast(op); + if (!bitcast) + return false; + + auto sourceType = dyn_cast(bitcast.getSrc().getType()); + auto resultType = dyn_cast(bitcast.getType()); + if (!sourceType || !resultType) + return false; + + return sourceType.getPointeeType().isInteger(1) && + resultType.getPointeeType().isInteger(8); +} + +MetaUseEraser::MetaUseEraser(MLIRContext *context) + : RewritePattern(MatchAnyOpTypeTag(), /*benefit=*/10, context) {} + +LogicalResult MetaUseEraser::matchAndRewrite(Operation *op, + PatternRewriter &rewriter) const { + LLVM_DEBUG({ + int64_t count = 0; + for (auto result : op->getResults()) { + count += std::distance(result.use_begin(), result.use_end()); + } + llvm::dbgs() << "Number of user: " << count << "\n"; + }); + if (isa(op)) { + return rewriter.notifyMatchFailure(op, + "AddPtrOp will be handled separately"); + } + if (isScalarI1ToI8PointerBitcast(op)) + return rewriter.notifyMatchFailure( + op, "scalar i1-to-i8 pointer bitcast requires conversion"); + if (isMetaUse(op)) { + rewriter.eraseOp(op); + return success(); + } + return rewriter.notifyMatchFailure(op, "requires meta ops"); +} diff --git a/third_party/ascend/lib/TritonToStructured/CMakeLists.txt b/third_party/ascend/lib/TritonToStructured/CMakeLists.txt new file mode 100644 index 0000000000..7c2def9208 --- /dev/null +++ b/third_party/ascend/lib/TritonToStructured/CMakeLists.txt @@ -0,0 +1,27 @@ +add_triton_library(TritonToStructured + TritonToStructuredPass.cpp + PtrAnalysis.cpp + CannonicalizerConverter.cpp + MemOpConverter.cpp + MaskAnalysis.cpp + + DEPENDS + TritonToStructuredConversionPassIncGen + + LINK_LIBS PUBLIC + MLIRArithDialect + MLIRDialectUtils + MLIRIR + MLIRMathDialect + MLIRPass + MLIRTensorDialect + MLIRTransforms + MLIRSupport + TritonIR + TritonTransforms + TritonAnalysis + MLIRTritonNPUUtils + MLIRSCFTransforms + MLIRLinalgTransforms + BiShengIRHIVMDialect +) diff --git a/third_party/ascend/lib/TritonToStructured/CannonicalizerConverter.cpp b/third_party/ascend/lib/TritonToStructured/CannonicalizerConverter.cpp new file mode 100644 index 0000000000..58229ed06b --- /dev/null +++ b/third_party/ascend/lib/TritonToStructured/CannonicalizerConverter.cpp @@ -0,0 +1,2800 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToStructured/CannonicalizerConverter.h" + +#include +#include +#include + +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Passes.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LLVM.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/MathExtras.h" + +#include "llvm/Support/Debug.h" + +#include "TritonToStructured/PtrAnalysis.h" +#include "TritonToStructured/TritonToStructuredPass.h" +#include "Utils/InterleaveOptimization.h" +#include "Utils/Utils.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" + +#define DEBUG_TYPE "triton-cannonicalizer-converter" + +namespace CannonicalizerConverter { +using namespace mlir; +using namespace triton; +constexpr int INT_TYPE_BIT_WIDTH = 32; + +// Match and rewrite pattern for optimizing cmp.ne (select(cond, 1, 0), 0) -> +// cond This pattern transforms: +// %select = arith.select %cond, %true_val, %false_val +// %cmp = arith.cmpi ne, %select, %zero +// Where: +// - %true_val is a constant splat tensor of 1s +// - %false_val is a constant splat tensor of 0s +// - %zero is a constant splat tensor of 0s +// Into: +// %cond (directly replace the cmp with the select condition) +// +// This optimization is valid because: +// select(cond, 1, 0) != 0 +// is equivalent to: cond != 0 +// Since the result of select is either 1 or 0, the only way it's not equal to +// 0 is when it's 1, which happens exactly when cond is true. +// +// Example: +// Input IR: +// %39 = arith.cmpi slt, %15, %cst_14 : tensor<128xi32> +// %40 = arith.select %39, %cst_13, %cst_12 : tensor<128xi1>, +// tensor<128xi32> %41 = arith.cmpi ne, %40, %cst_12 : tensor<128xi32> +// Where cst_13 is constant dense<1> and cst_12 is constant dense<0> +// Output IR: +// %39 = arith.cmpi slt, %15, %cst_14 : tensor<128xi32> +LogicalResult CmpConverter::matchAndRewrite(arith::CmpIOp cmpOp, + PatternRewriter &rewriter) const { + // Only handle "not equal" comparison + auto cmpType = cmpOp.getPredicate(); + if (cmpType != arith::CmpIPredicate::ne) { + return failure(); + } + + Value rhs = cmpOp.getRhs(); + Value lhs = cmpOp.getLhs(); + + // 1. Check if RHS is a constant zero + APInt rhsValue; + if (!matchPattern(rhs, m_ConstantInt(&rhsValue))) { + return failure(); // RHS is not a constant + } + + if (!rhsValue.isZero()) { + return failure(); // RHS is not zero + } + + // 2. Check if LHS is defined by a select operation + auto selectOp = lhs.getDefiningOp(); + if (!selectOp) { + return failure(); + } + + // 3. Check if select's true and false values are constants + DenseElementsAttr trueAttr; + DenseElementsAttr falseAttr; + if (!matchPattern(selectOp.getTrueValue(), m_Constant(&trueAttr)) || + !matchPattern(selectOp.getFalseValue(), m_Constant(&falseAttr))) { + return failure(); // Either true or false value is not constant + } + + // 4. Check if true value is all 1s and false value is all 0s + if (!trueAttr.isSplat() || !trueAttr.getSplatValue().isOne() || + !falseAttr.isSplat() || !falseAttr.getSplatValue().isZero()) { + return failure(); + } + + // 5. Optimization matched, replace cmp with select's condition + rewriter.replaceOp(cmpOp, selectOp.getCondition()); + return success(); +} + +// Detect when both operands of the cmpOp are triton::SplatOp. If so, +// replace the original comparison by comparing the underlying scalar values +// and then splatting (broadcasting) the scalar comparison result back to the +// original tensor shape. +// Example: +// Input IR: +// %splat_lhs = tt.splat %val1 : tensor<128xi32> +// %splat_rhs = tt.splat %val2 : tensor<128xi32> +// %cmp = arith.cmpi slt, %splat_lhs, %splat_rhs : tensor<128xi32> +// Output IR: +// %cmp_scalar = arith.cmpi slt, %val1, %val2 +// %splat_cmp = tt.splat %cmp_scalar : tensor<128xi1> +LogicalResult +SplatCmpConverter::matchAndRewrite(arith::CmpIOp cmpOp, + PatternRewriter &rewriter) const { + auto lhs = cmpOp.getLhs(); + auto rhs = cmpOp.getRhs(); + auto lhsSplatOp = lhs.getDefiningOp(); + auto rhsSplatOp = rhs.getDefiningOp(); + if (!lhsSplatOp || !rhsSplatOp) { + return failure(); + } + auto lhsSrc = lhsSplatOp.getSrc(); + auto rhsSrc = rhsSplatOp.getSrc(); + auto newCmpOp = rewriter.create( + cmpOp.getLoc(), cmpOp.getPredicate(), lhsSrc, rhsSrc); + auto cmpType = dyn_cast(cmpOp.getType()); + if (!cmpType) { + return failure(); + } + auto splatType = + RankedTensorType::get(cmpType.getShape(), newCmpOp.getType()); + auto splatOp = rewriter.create(cmpOp.getLoc(), splatType, + newCmpOp.getResult()); + rewriter.replaceOp(cmpOp, splatOp.getResult()); + return success(); +} + +// Convert AddPtr when ptr is produced by tt.splat and offset is produced by +// tt.broadcast of a smaller-shaped value. Transformation: +// %ptr_splat = tt.splat %ptr_src : tensor> +// %offset_b = tt.broadcast %offset_src : tensor +// %res = tt.addptr %ptr_splat, %offset_b +// => +// %ptr_small_splat = tt.splat %ptr_src : tensor> +// %add_small = tt.addptr %ptr_small_splat, %offset_src +// %res = tt.broadcast %add_small : tensor> +LogicalResult +AddPtrSplatConverter::matchAndRewrite(triton::AddPtrOp addPtrOp, + PatternRewriter &rewriter) const { + // Match ptr produced by splat + auto ptr = addPtrOp.getPtr(); + auto ptrSplatOp = ptr.getDefiningOp(); + if (!ptrSplatOp) + return failure(); + + // Match offset produced by broadcast + auto offset = addPtrOp.getOffset(); + auto offsetBroadcastOp = offset.getDefiningOp(); + if (!offsetBroadcastOp) + return failure(); + + // Extract sources + auto ptrSrc = ptrSplatOp.getSrc(); + auto offsetSrc = offsetBroadcastOp.getSrc(); + + // Types must be ranked to reason about shapes + auto resultType = dyn_cast(addPtrOp.getResult().getType()); + auto offsetSrcType = dyn_cast(offsetSrc.getType()); + if (!resultType || !offsetSrcType) + return failure(); + + // If offset source already has same shape as result, nothing to do + if (resultType.getShape() == offsetSrcType.getShape()) + return failure(); + + auto loc = addPtrOp.getLoc(); + + // Build pointer tensor type corresponding to offset's (smaller) shape + // ptrSrc should be a pointer type (element type) + auto ptrElementType = dyn_cast(ptrSrc.getType()); + auto smallPtrTensorType = + RankedTensorType::get(offsetSrcType.getShape(), ptrElementType); + + // Create splat of ptrSrc to the smaller shape + auto smallPtrSplat = + rewriter.create(loc, smallPtrTensorType, ptrSrc); + + // Create addptr on the smaller shape + auto smallAdd = rewriter.create( + loc, smallPtrTensorType, smallPtrSplat.getResult(), offsetSrc); + + // Broadcast the small add result back to the original (larger) shape + auto broadcasted = rewriter.create(loc, resultType, + smallAdd.getResult()); + + rewriter.replaceOp(addPtrOp, broadcasted.getResult()); + return success(); +} + +// Move load before broadcast when possible: +// If load.ptr is a triton::BroadcastOp and +// - load.mask is null; or +// - load.mask is a triton::BroadcastOp, +// then rewrite the load in one of two ways depending on whether the ptr src +// and mask src shapes match: +// +// Case 1 (shapes match, original fast path): +// %ptr_b = tt.broadcast %ptr_src +// %mask_b = tt.broadcast %mask_src (src shapes identical) +// %v = tt.load %ptr_b, %mask_b, %other_splat +// becomes: +// %v_small = tt.load %ptr_src, %mask_src, %other_small +// %v = tt.broadcast %v_small +// +// Case 2 (shapes differ, e.g. ptr and mask broadcast on different axes): +// %ptr_b = tt.broadcast %ptr_src // e.g. [1,64] -> [256,64] +// %mask_b = tt.broadcast %mask_src // e.g. [256,1] -> [256,64] +// %v = tt.load %ptr_b, %mask_b, %other +// becomes: +// %v_small = tt.load %ptr_src (no mask, no other — safe) +// %v_bc = tt.broadcast %v_small +// %v = arith.select %mask_b, %v_bc, %other +// The full mask is applied via select after broadcast, which downstream +// select analysis lowers to extract_slice / insert_slice. +LogicalResult +LoadBroadcastConverter::matchAndRewrite(triton::LoadOp loadOp, + PatternRewriter &rewriter) const { + // Match when ptr is defined by BroadcastOp + Value ptr = loadOp.getPtr(); + auto ptrBroadcast = ptr.getDefiningOp(); + if (!ptrBroadcast) + return failure(); + + auto ptrSrcType = dyn_cast(ptrBroadcast.getSrc().getType()); + if (!ptrSrcType) + return failure(); + + // mask can be null + Value mask = loadOp.getMask(); + triton::BroadcastOp maskBroadcast = nullptr; + Value maskSrc = nullptr; + RankedTensorType maskSrcType = nullptr; + if (mask) { + maskBroadcast = mask.getDefiningOp(); + if (!maskBroadcast) + return failure(); + maskSrc = maskBroadcast.getSrc(); + maskSrcType = dyn_cast(maskSrc.getType()); + if (!maskSrcType) + return failure(); + } + + Location loc = loadOp.getLoc(); + Value smallPtr = ptrBroadcast.getSrc(); + + // Preserve other operands of load (like 'other', cache, evict, isVolatile) + Value other = loadOp.getOther(); + auto cache = loadOp.getCache(); + auto evict = loadOp.getEvict(); + auto isVolatile = loadOp.getIsVolatile(); + auto resultType = dyn_cast(loadOp.getResult().getType()); + if (!resultType) + return failure(); + + bool shapesMatch = !mask || (ptrSrcType.getShape() == maskSrcType.getShape()); + + // no mask, or ptr/mask src shapes match + if (shapesMatch) { + Value newOther = other; + if (other) { + // Build a small-shaped 'other' constant matching ptr src shape. + // Only splat dense constants are supported here (same as before). + Attribute otherAttr; + if (!matchPattern(other, m_Constant(&otherAttr))) + return failure(); + auto denseOther = dyn_cast(otherAttr); + if (!denseOther) + return failure(); + + auto elemType = denseOther.getType().getElementType(); + if (!elemType) + return failure(); + auto smallType = RankedTensorType::get(ptrSrcType.getShape(), elemType); + + DenseElementsAttr newDense; + if (denseOther.isSplat()) { + newDense = DenseElementsAttr::get( + smallType, denseOther.getSplatValue()); + } else { + return failure(); + } + newOther = rewriter.create(loc, newDense).getResult(); + } + + auto newLoad = rewriter.create( + loc, smallPtr, maskSrc, newOther, cache, evict, isVolatile); + auto broadcasted = rewriter.create( + loc, resultType, newLoad.getResult()); + rewriter.replaceOp(loadOp, broadcasted.getResult()); + return success(); + } + + auto newLoad = rewriter.create( + loc, smallPtr, /*mask=*/nullptr, /*other=*/nullptr, cache, evict, + isVolatile); + auto broadcasted = rewriter.create(loc, resultType, + newLoad.getResult()); + Value result = broadcasted.getResult(); + + if (!other) { + // default for masked-off elements is 0 + auto zeroAttr = DenseElementsAttr::get( + resultType, rewriter.getZeroAttr(resultType.getElementType())); + other = rewriter.create(loc, zeroAttr).getResult(); + } + auto selectOp = rewriter.create(loc, mask, result, other); + rewriter.replaceOp(loadOp, selectOp.getResult()); + return success(); +} + +// Move store before broadcast when possible: +// If store.ptr is defined by a triton::BroadcastOp, the broadcast axes refer +// to the same memory locations (every pointer along a broadcast axis points +// to the same address), so storing once at the first index of each broadcast +// axis is sufficient. Rewrite +// %ptr = tt.broadcast %ptr_src // e.g. [32,1] -> [32,32] +// %mask = tt.broadcast %mask_src? // optional, same expansion +// tt.store %ptr, %val, %mask? +// with +// %val_small = tensor.extract_slice %val[..0..] // first index per +// // broadcast axis +// %mask_small = %mask_src | tensor.extract_slice %mask +// tt.store %ptr_src, %val_small, %mask_small? +LogicalResult +StoreBroadcastConverter::matchAndRewrite(triton::StoreOp storeOp, + PatternRewriter &rewriter) const { + // Match when ptr is defined by BroadcastOp + Value ptr = storeOp.getPtr(); + auto ptrBroadcast = ptr.getDefiningOp(); + if (!ptrBroadcast) + return failure(); + + auto ptrSrcType = dyn_cast(ptrBroadcast.getSrc().getType()); + auto resultType = dyn_cast(ptr.getType()); + if (!ptrSrcType || !resultType) + return failure(); + + // Find the axes expanded by broadcast (src size 1 -> full size) + auto srcShape = ptrSrcType.getShape(); + auto resultShape = resultType.getShape(); + SmallVector broadcastAxes; + for (size_t i = 0; i < srcShape.size(); ++i) { + if (srcShape[i] == 1 && resultShape[i] != 1) + broadcastAxes.push_back(i); + } + if (broadcastAxes.empty()) + return failure(); + + Location loc = storeOp.getLoc(); + SmallVector offsets, sizes, strides; + for (size_t i = 0; i < resultShape.size(); ++i) { + offsets.push_back(rewriter.getIndexAttr(0)); + strides.push_back(rewriter.getIndexAttr(1)); + sizes.push_back(rewriter.getIndexAttr(resultShape[i])); + } + for (auto axis : broadcastAxes) + sizes[axis] = rewriter.getIndexAttr(1); + + // Slice value along the broadcast axes (first index) if it has the full + // broadcast shape; otherwise it must already match the small ptr shape. + Value newValue = storeOp.getValue(); + if (auto valueType = dyn_cast(newValue.getType())) { + if (valueType.getShape() == resultShape) { + newValue = rewriter.create(loc, newValue, offsets, + sizes, strides); + } else if (valueType.getShape() != srcShape) { + return failure(); + } + } + + // Slice mask the same way; reuse mask's broadcast source when its shape + // matches the small ptr shape, avoiding a redundant extract_slice. + Value newMask; + if (Value mask = storeOp.getMask()) { + auto maskType = dyn_cast(mask.getType()); + if (!maskType) + return failure(); + if (maskType.getShape() == resultShape) { + Value maskSrc = nullptr; + if (auto maskBroadcast = mask.getDefiningOp()) { + auto maskSrcType = + dyn_cast(maskBroadcast.getSrc().getType()); + if (maskSrcType && maskSrcType.getShape() == srcShape) + maskSrc = maskBroadcast.getSrc(); + } + newMask = + maskSrc ? maskSrc + : static_cast(rewriter.create( + loc, mask, offsets, sizes, strides)); + } else if (maskType.getShape() != srcShape) { + return failure(); + } else { + newMask = mask; + } + } + + // Store the reduced slice; broadcast dims all alias the same address. + if (newMask) { + rewriter.create(loc, ptrBroadcast.getSrc(), newValue, + newMask, storeOp.getBoundaryCheck(), + storeOp.getCache(), storeOp.getEvict()); + } else { + rewriter.create(loc, ptrBroadcast.getSrc(), newValue, + storeOp.getBoundaryCheck(), + storeOp.getCache(), storeOp.getEvict()); + } + rewriter.eraseOp(storeOp); + return success(); +} + +LogicalResult ZeroStrideMakeTensorPtrConverter::matchAndRewrite( + triton::MakeTensorPtrOp op, PatternRewriter &rewriter) const { + // Only rewrite when every stride is statically 0; any non-zero stride + // would mean the dimension actually varies in memory and the broadcast + // pattern doesn't apply. + if (!llvm::all_of(op.getStrides(), [](Value stride) { + Attribute attr; + if (!matchPattern(stride, m_Constant(&attr))) + return false; + if (auto intAttr = dyn_cast(attr)) + return intAttr.getValue() == 0; + return false; + })) { + return failure(); + } + + auto isAllowedUser = [](Operation *user) { + // FIXME: temporary solution + // return isa(user); + return isa(user); + }; + if (!llvm::all_of(op->getUsers(), isAllowedUser)) + return failure(); + + Location loc = op.getLoc(); + Value base = op.getBase(); + + auto ptrType = cast(op.getResult().getType()); + auto pointeeTensorTy = cast(ptrType.getPointeeType()); + + SmallVector loadUsers; + SmallVector storeUsers; + SmallVector advanceUsers; + for (Operation *user : op->getUsers()) { + if (auto loadOp = dyn_cast(user)) + loadUsers.push_back(loadOp); + else if (auto storeOp = dyn_cast(user)) + storeUsers.push_back(storeOp); + else if (auto advanceOp = dyn_cast(user)) + advanceUsers.push_back(advanceOp); + } + + // Rewrite loads: load a scalar from `base` directly (tt.load accepts a + // pointer-to-scalar type) and splat it to the block shape. The + // "splat" Triton op is the intended way to broadcast a scalar value into + // a tensor of arbitrary shape — equivalent in semantics to what + // `tt.broadcast(0-D-tensor -> shape)` does, but without the intermediate + // 0-D tensorization step. + auto rewriteLoad = [&](triton::LoadOp loadOp) { + rewriter.setInsertionPoint(loadOp); + auto scalarLoad = rewriter.create( + loc, base, /*mask=*/Value(), /*other=*/Value(), loadOp.getCache(), + loadOp.getEvict(), loadOp.getIsVolatile()); + + auto broadcasted = rewriter.create(loc, pointeeTensorTy, + scalarLoad.getResult()); + rewriter.replaceOp(loadOp, broadcasted.getResult()); + }; + + for (triton::LoadOp loadOp : loadUsers) + rewriteLoad(loadOp); + + rewriter.eraseOp(op); + return success(); +} + +LogicalResult PromotePointerIterArgsPattern::matchAndRewrite( + scf::ForOp forOp, PatternRewriter &rewriter) const { + // 1. Check if the loop meets transformation conditions + if (failed(matchLoop(forOp))) { + return failure(); + } + + // 2. Collect pointer iteration arguments to be processed + if (!failed(matchAndRewriteForAddPtr(forOp, rewriter))) { + return success(); + } + return matchAndRewriteAdvancePtr(forOp, rewriter); +} + +// Transform a for loop that uses pointer iteration arguments into one that uses +// integer offsets instead. This pattern handles the specific case where: +// 1. The loop has pointer iteration arguments of type like +// tensor<1024x!tt.ptr> +// 2. Each pointer is used in a load/store operation and then incremented by +// a constant offset via tt.addptr +// 3. The updated pointer (from addptr) is yielded back as the next iteration +// value +// +// The transformation converts: +// scf.for iter_args(%ptr = %base_ptr) { +// %val = tt.load %ptr +// tt.store %other_ptr, %val +// %new_ptr = tt.addptr %ptr, %offset +// scf.yield %new_ptr +// } +// +// Into: +// scf.for iter_args(%offset_int = 0) { +// %splat_offset = tt.splat %offset_int +// %current_ptr = tt.addptr %base_ptr, %splat_offset +// %val = tt.load %current_ptr +// tt.store %other_ptr, %val +// %new_offset = arith.addi %offset_int, %const_offset +// scf.yield %new_offset +// } +// +LogicalResult PromotePointerIterArgsPattern::matchAndRewriteForAddPtr( + scf::ForOp forOp, PatternRewriter &rewriter) const { + // 1. Collect pointer iteration arguments to be processed + auto pointerArgsInfo = collectPointerIterArgs(forOp); + if (pointerArgsInfo.empty()) { + return failure(); + } + // 2. Create new iteration argument types and initial values + auto [newInitArgs, newIterArgTypes, indexMap] = + createNewIterArgs(forOp, pointerArgsInfo, rewriter); + // 3. Create the new for loop + auto newForOp = + createNewForLoop(forOp, newInitArgs, newIterArgTypes, rewriter); + // 4. Rewrite the loop body + if (failed(rewriteLoopBody(forOp, newForOp, pointerArgsInfo, indexMap, + rewriter))) { + rewriter.eraseOp(newForOp); + return failure(); + } + // 5. Replace original loop results + if (failed(replaceResults(forOp, newForOp, pointerArgsInfo, indexMap, + rewriter))) { + rewriter.eraseOp(newForOp); + return failure(); + } + return success(); +} + +// Transform a for loop that uses pointer iteration arguments into one that uses +// integer offsets instead. This pattern handles the specific case where: +// 1. The loop has pointer iteration arguments of type like +// !tt.ptr> +// 2. Each pointer is used in a load/store operation and then incremented by +// a constant offset via tt.advanceptr +// 3. The updated pointer (from advanceptr) is yielded back as the next +// iteration value +// +// The transformation converts: +// scf.for iter_args(%ptr1 = %base_ptr1, %ptr2 = %base_ptr2 ) { +// %val = tt.load %ptr1 +// tt.store %base_ptr2, %val +// %new_ptr1 = tt.adcanceptr %ptr1, %offset +// %new_ptr2 = tt.adcanceptr %ptr2, %offset +// scf.yield %new_ptr1,%new_ptr2 +// } +// +// Into: +// The transformation converts: +// scf.for iter_args(%offset_int1 = 0,%offset_int2 = 0) { +// %new_ptr1 = tt.adcanceptr %ptr1, %offset_int1 +// %new_ptr2 = tt.adcanceptr %ptr2, %offset_int1 +// %val = tt.load %new_ptr1 +// tt.store %new_ptr2, %val +// %new_offset1 = arith.addi %offset_int, %const_offset +// %new_offset2 = arith.addi %offset_int, %const_offset +// scf.yield %new_offset1,%new_offset2 +// } +// +LogicalResult PromotePointerIterArgsPattern::matchAndRewriteAdvancePtr( + scf::ForOp forOp, PatternRewriter &rewriter) const { + // 1. Check if the loop meets transformation conditions + if (failed(matchLoop(forOp))) { + return failure(); + } + // 2. Collect pointer iteration arguments to be processed + auto pointerArgsInfo = collectPointerIterArgsForAdvancePtr(forOp); + if (pointerArgsInfo.size() == 0) { + return failure(); + } + // 3. Create new iteration argument types and initial values + auto [newInitArgs, newIterArgTypes, indexMap] = + createNewIterArgsForAdvancePtr(forOp, pointerArgsInfo, rewriter); + + // 4. Create the new for loop + auto newForOp = + createNewForLoop(forOp, newInitArgs, newIterArgTypes, rewriter); + // 5. Rewrite the loop body + if (failed(rewriteLoopBodyForAdvancePtr(forOp, newForOp, pointerArgsInfo, + indexMap, rewriter))) { + rewriter.eraseOp(newForOp); + return failure(); + } + rewriter.replaceOp(forOp, newForOp); + return success(); +} + +LogicalResult PromotePointerIterArgsPattern::matchLoop(scf::ForOp forOp) const { + auto lowerBound = forOp.getLowerBound(); + auto upperBound = forOp.getUpperBound(); + auto step = forOp.getStep(); + if (!matchPattern(lowerBound, m_Constant()) || + !matchPattern(upperBound, m_Constant()) || + !matchPattern(step, m_Constant())) { + return failure(); + } + return success(); +} + +SmallVector +PromotePointerIterArgsPattern::collectPointerIterArgs(scf::ForOp forOp) const { + SmallVector result; + auto &loopBody = *forOp.getBody(); + for (auto [idx, iterArg] : llvm::enumerate(forOp.getRegionIterArgs())) { + if (isPointerIterArg(iterArg)) { + auto info = analyzePointerIterArg(iterArg, loopBody); + if (info.has_value()) { + info->oldIndex = static_cast(idx), + info->basePointer = forOp.getInitArgs()[idx], + result.push_back(info.value()); + } + } + } + return result; +} + +SmallVector +PromotePointerIterArgsPattern::collectPointerIterArgsForAdvancePtr( + scf::ForOp forOp) const { + SmallVector result; + auto &loopBody = *forOp.getBody(); + + for (auto [idx, iterArg] : llvm::enumerate(forOp.getRegionIterArgs())) { + if (isa(iterArg.getType())) { + auto info = analyzePointerIterArgForAdvancePtr(iterArg, loopBody); + if (info.has_value()) { + info->oldIndex = static_cast(idx); + info->basePointer = forOp.getInitArgs()[idx]; + result.push_back(info.value()); + } + } + } + return result; +} + +bool PromotePointerIterArgsPattern::isPointerIterArg(Value iterArg) const { + auto ptrType = dyn_cast(iterArg.getType()); + return ptrType && isa(ptrType.getElementType()); +} + +std::optional +PromotePointerIterArgsPattern::analyzePointerIterArg(Value iterArg, + Block &loopBody) const { + int memCount = + 0; // Count of memory operations (load/store) using this pointer + int addPtrCount = 0; // Count of addptr operations on this pointer + Value addPtrResult = nullptr; // Result of the addptr operation + Value offset = nullptr; // Offset value used in addptr + Value addPtrValue = nullptr; // The addptr operation result value + + for (auto &op : loopBody) { + TypeSwitch(&op) + .Case([&](auto memoryOp) { + // Check if this memory operation uses the pointer we're analyzing + if (memoryOp.getPtr() == iterArg) + ++memCount; + }) + .Case([&](auto addPtrOp) { + // Check if this addptr operation updates the pointer we're analyzing + if (addPtrOp.getPtr() == iterArg) { + ++addPtrCount; + addPtrResult = addPtrOp.getResult(); + offset = addPtrOp.getOffset(); + addPtrValue = addPtrOp.getResult(); + } + }) + .Default([](auto) {}); // Ignore other operations + } + + // Check the terminator to see if the addptr result is yielded + auto yieldOp = dyn_cast(loopBody.getTerminator()); + if (!yieldOp) + return std::nullopt; + + bool isYielded = false; + for (auto operand : yieldOp.getOperands()) { + if (operand == addPtrResult) { + isYielded = true; + break; + } + } + + // Pattern matched if: + // 1. Exactly one addptr operation on this pointer + // 2. At least one memory operation using this pointer + // 3. The addptr result is yielded + if (addPtrCount == 1 && memCount >= 1 && isYielded) { + return PointerArgInfo{ + .oldIndex = 0, + .basePointer = nullptr, // Will be set in collectPointerIterArgs + .offsetValue = offset, + .newIterArg = nullptr, // Will be set in createNewIterArgs + .addPtrValue = addPtrValue}; + } + return std::nullopt; +} + +std::optional +PromotePointerIterArgsPattern::analyzePointerIterArgForAdvancePtr( + Value iterArg, Block &loopBody) const { + int memCount = + 0; // Count of memory operations (load/store) using this pointer + int advancePtrCount = 0; // Count of advanceptr operations on this pointer + Value advancePtrResult = nullptr; // Result of the addptr operation + SmallVector offsetValues; + Value advancePtrValue = nullptr; // The addptr operation result value + int nonZeroConstant = 0; // Number of non-zero constants + for (auto &op : loopBody) { + TypeSwitch(&op) + .Case([&](auto memoryOp) { + // Check if this memory operation uses the pointer we're analyzing + if (memoryOp.getPtr() == iterArg) + ++memCount; + }) + .Case([&](triton::AdvanceOp advancePtrOp) { + // Check if this addptr operation updates the pointer we're analyzing + if (advancePtrOp.getPtr() == iterArg) { + ++advancePtrCount; + advancePtrResult = advancePtrOp.getResult(); + for (auto offsetVal : advancePtrOp.getOffsets()) { + if (auto offsetInt = getConstantIntValue(offsetVal)) { + if (*offsetInt != 0) { + ++nonZeroConstant; + } + offsetValues.push_back(offsetVal); + } + } + advancePtrValue = advancePtrOp.getResult(); + } + }) + .Default([](auto) {}); // Ignore other operations + } + // Check the terminator to see if the addptr result is yielded + auto yieldOp = dyn_cast(loopBody.getTerminator()); + if (!yieldOp) + return std::nullopt; + bool isYielded = false; + for (auto operand : yieldOp.getOperands()) { + if (operand == advancePtrResult) { + isYielded = true; + break; + } + } + // Pattern matched if: + // 1. Exactly one addptr operation on this pointer + // 2. At least one memory operation using this pointer + // 3. The addptr result is yielded + if (advancePtrCount == 1 && nonZeroConstant == 1 && memCount >= 1 && + isYielded) { + return PointerArgInfo{ + .oldIndex = 0, + .basePointer = nullptr, // Will be set in collectPointerIterArgs + .offsetValue = nullptr, + .newIterArg = nullptr, // Will be set in createNewIterArgs + .addPtrValue = advancePtrValue, + .offsetValues = offsetValues}; + } + return std::nullopt; +} + +std::tuple, SmallVector, DenseMap> +PromotePointerIterArgsPattern::createNewIterArgs( + scf::ForOp forOp, ArrayRef pointerArgs, + PatternRewriter &rewriter) const { + SmallVector newInitArgs; + SmallVector newIterArgTypes; + DenseMap indexMap; + + for (unsigned i = 0; i < forOp.getInitArgs().size(); ++i) { + if (isPointerArgIndex(pointerArgs, i)) { + // Replace pointer with integer offset (initialized to 0) + Value zero = rewriter.create(forOp.getLoc(), 0, + INT_TYPE_BIT_WIDTH); + newInitArgs.push_back(zero); + newIterArgTypes.push_back(rewriter.getIntegerType(INT_TYPE_BIT_WIDTH)); + } else { + // Preserve original argument unchanged + newInitArgs.push_back(forOp.getInitArgs()[i]); + newIterArgTypes.push_back(forOp.getInitArgs()[i].getType()); + } + // Identity mapping: argument count and order unchanged, + // may change in future + indexMap[i] = i; + } + + return {newInitArgs, newIterArgTypes, indexMap}; +} + +std::tuple, SmallVector, DenseMap> +PromotePointerIterArgsPattern::createNewIterArgsForAdvancePtr( + scf::ForOp forOp, SmallVector &pointerArgs, + PatternRewriter &rewriter) const { + DenseMap indexMap; + SmallVector newInitArgs; + SmallVector newIterArgTypes; + SmallVector newInitArgsTemp; + SmallVector newIterArgTypesTemp; + + for (unsigned i = 0; i < forOp.getInitArgs().size(); ++i) { + PointerArgInfo *infoTemp = nullptr; + bool isMatch = false; + + // find the matching pointerArg + for (size_t k = 0; k < pointerArgs.size(); ++k) { + auto &info = pointerArgs[k]; + if (info.oldIndex == i) { + infoTemp = &info; + isMatch = true; + break; + } + } + + if (!isMatch) { + // Preserve original argument unchanged + newInitArgsTemp.push_back(forOp.getInitArgs()[i]); + newIterArgTypesTemp.push_back(forOp.getInitArgs()[i].getType()); + } else if (infoTemp != nullptr) { + // Replace pointer with integer offset (initialized to 0) + SmallVector newInitArgs; + SmallVector newIterArgTypes; + + if (infoTemp->offsetValues.empty()) { + Value zero = + rewriter.create(forOp.getLoc(), 0, 32); + newInitArgs.push_back(zero); + newIterArgTypes.push_back(rewriter.getIntegerType(INT_TYPE_BIT_WIDTH)); + newInitArgsTemp.push_back(zero); + newIterArgTypesTemp.push_back( + rewriter.getIntegerType(INT_TYPE_BIT_WIDTH)); + } else { + Value zero = + rewriter.create(forOp.getLoc(), 0, 32); + for (size_t j = 0; j < infoTemp->offsetValues.size(); ++j) { + Value offset = infoTemp->offsetValues[j]; + if (auto maskInt = getConstantIntValue(offset)) { + newInitArgs.push_back(zero); + newIterArgTypes.push_back( + rewriter.getIntegerType(INT_TYPE_BIT_WIDTH)); + if (*maskInt == 0) { + continue; + } + newInitArgsTemp.push_back(zero); + newIterArgTypesTemp.push_back( + rewriter.getIntegerType(INT_TYPE_BIT_WIDTH)); + } + } + } + + infoTemp->newInitArgs = newInitArgs; + infoTemp->newIterArgTypes = newIterArgTypes; + } else { + newInitArgsTemp.push_back(forOp.getInitArgs()[i]); + newIterArgTypesTemp.push_back(forOp.getInitArgs()[i].getType()); + } + // may change in future + indexMap[i] = i; + } + return {newInitArgsTemp, newIterArgTypesTemp, indexMap}; +} + +scf::ForOp PromotePointerIterArgsPattern::createNewForLoop( + scf::ForOp forOp, ArrayRef newInitArgs, + ArrayRef newIterArgTypes, PatternRewriter &rewriter) const { + return rewriter.create(forOp.getLoc(), forOp.getLowerBound(), + forOp.getUpperBound(), forOp.getStep(), + newInitArgs); +} + +LogicalResult PromotePointerIterArgsPattern::rewriteLoopBody( + scf::ForOp oldForOp, scf::ForOp newForOp, + SmallVector &pointerArgs, + DenseMap &indexMap, PatternRewriter &rewriter) const { + Block &oldBody = *oldForOp.getBody(); + Block &newBody = *newForOp.getBody(); + + rewriter.setInsertionPointToStart(&newBody); + + // Create IR mapping that maps original values to their transformed + // equivalents + IRMapping mapping = + createIRMapping(oldForOp, newForOp, pointerArgs, indexMap, rewriter); + // Clone instructions from original loop body, applying the mapping + return cloneInstructions(oldBody, newBody, pointerArgs, indexMap, mapping, + rewriter); +} + +LogicalResult PromotePointerIterArgsPattern::rewriteLoopBodyForAdvancePtr( + scf::ForOp oldForOp, scf::ForOp newForOp, + SmallVector &pointerArgs, + DenseMap &indexMap, PatternRewriter &rewriter) const { + Block &oldBody = *oldForOp.getBody(); + Block &newBody = *newForOp.getBody(); + rewriter.setInsertionPointToStart(&newBody); + // Create IR mapping that maps original values to their transformed + // equivalents + IRMapping mapping = createIRMappingForAdvancePtr( + oldForOp, newForOp, pointerArgs, indexMap, rewriter); + // Clone instructions from original loop body, applying the mapping + return cloneInstructionsForAdvancePtr(oldBody, newBody, pointerArgs, indexMap, + mapping, rewriter); +} + +IRMapping PromotePointerIterArgsPattern::createIRMapping( + scf::ForOp oldForOp, scf::ForOp newForOp, + SmallVector &pointerArgs, + DenseMap &indexMap, PatternRewriter &rewriter) const { + IRMapping mapping; + mapping.map(oldForOp.getInductionVar(), newForOp.getInductionVar()); + + // Process iteration arguments + for (unsigned i = 0; i < oldForOp.getRegionIterArgs().size(); ++i) { + Value oldIterArg = oldForOp.getRegionIterArgs()[i]; + Value newIterArg = newForOp.getRegionIterArgs()[indexMap[i]]; + + if (isPointerArgIndex(pointerArgs, i)) { + // Update the PointerArgInfo with the new integer iteration argument + for (auto &info : pointerArgs) { + if (info.oldIndex == i) { + info.newIterArg = newIterArg; + break; + } + } + + // Map original pointer argument to a reconstructed pointer + mapping.map(oldIterArg, + rebuildPointer(oldForOp, pointerArgs, i, rewriter)); + } else { + // Direct mapping for non-pointer arguments + mapping.map(oldIterArg, newIterArg); + } + } + return mapping; +} + +IRMapping PromotePointerIterArgsPattern::createIRMappingForAdvancePtr( + scf::ForOp oldForOp, scf::ForOp newForOp, + SmallVector &pointerArgs, + DenseMap &indexMap, PatternRewriter &rewriter) const { + IRMapping mapping; + mapping.map(oldForOp.getInductionVar(), newForOp.getInductionVar()); + // Process iteration arguments + for (unsigned i = 0; i < oldForOp.getRegionIterArgs().size(); ++i) { + Value oldIterArg = oldForOp.getRegionIterArgs()[i]; + Value newIterArg = newForOp.getRegionIterArgs()[indexMap[i]]; + if (isPointerArgIndex(pointerArgs, i)) { + // Update the PointerArgInfo with the new integer iteration argument + for (auto &info : pointerArgs) { + if (info.oldIndex == i) { + info.newIterArg = newIterArg; + break; + } + } + Value newIterArgTemp = + rebuildPointerForAdvancePtr(oldForOp, pointerArgs, i, rewriter); + // Map original pointer argument to a reconstructed pointer + mapping.map(oldIterArg, newIterArgTemp); + } else { + // Direct mapping for non-pointer arguments + mapping.map(oldIterArg, newIterArg); + } + } + return mapping; +} + +bool PromotePointerIterArgsPattern::isPointerArgIndex( + ArrayRef pointerArgs, unsigned idx) const { + for (auto &info : pointerArgs) { + if (info.oldIndex == idx) + return true; + } + return false; +} + +Value PromotePointerIterArgsPattern::rebuildPointer( + scf::ForOp forOp, ArrayRef pointerArgs, unsigned idx, + PatternRewriter &rewriter) const { + const PointerArgInfo *info = nullptr; + for (auto &argInfo : pointerArgs) { + if (argInfo.oldIndex == idx) { + info = &argInfo; + break; + } + } + if (!info) + return nullptr; + // Create splat operation to broadcast integer offset to tensor shape + auto baseType = info->basePointer.getType(); + Value splatOffset = nullptr; + if (auto rankedType = dyn_cast(baseType)) { + // Get the shape of the original tensor + auto shape = rankedType.getShape(); + + splatOffset = rewriter.create( + forOp.getLoc(), RankedTensorType::get(shape, rewriter.getI32Type()), + info->newIterArg); + } else { + return nullptr; + } + // Create addptr operation: base pointer + splatted offset + return rewriter.create(forOp.getLoc(), + info->basePointer.getType(), + info->basePointer, splatOffset); +} + +Value PromotePointerIterArgsPattern::rebuildPointerForAdvancePtr( + scf::ForOp forOp, ArrayRef pointerArgs, unsigned idx, + PatternRewriter &rewriter) const { + const PointerArgInfo *info = nullptr; + for (auto &argInfo : pointerArgs) { + if (argInfo.oldIndex == idx) { + info = &argInfo; + break; + } + } + if (!info) { + return nullptr; + } + SmallVector advanceOpOld; + advanceOpOld.push_back(info->basePointer); + for (size_t i = 0; i < info->offsetValues.size(); ++i) { + if (auto maskInt = getConstantIntValue(info->offsetValues[i])) { + if (*maskInt != 0) { + advanceOpOld.push_back(info->newIterArg); + continue; + } + } + advanceOpOld.push_back(info->offsetValues[i]); + } + auto advanceOp = rewriter.create( + forOp.getLoc(), info->basePointer.getType(), advanceOpOld); + return advanceOp; +} + +LogicalResult PromotePointerIterArgsPattern::cloneInstructions( + Block &oldBody, Block &newBody, ArrayRef pointerArgs, + DenseMap &indexMap, IRMapping &mapping, + PatternRewriter &rewriter) const { + // Collect all operations from the old loop body except the terminator + SmallVector toClone; + for (auto &op : oldBody.without_terminator()) { + toClone.push_back(&op); + } + // Build a set of addptr operations to skip (those that update pointer + // iteration arguments) + DenseSet addPtrOpsToSkip; + for (const auto &info : pointerArgs) { + if (info.addPtrValue) { + addPtrOpsToSkip.insert(info.addPtrValue); + } + } + // Clone all operations except the skipped addptr operations + for (auto *op : toClone) { + // Only skip addptr operations that are updating pointer iteration arguments + if (auto addPtrOp = dyn_cast(op)) { + if (addPtrOpsToSkip.contains(addPtrOp.getResult())) { + continue; + } + } + rewriter.clone(*op, mapping); + } + // Handle the yield terminator separately + auto yieldOp = dyn_cast(oldBody.getTerminator()); + if (!yieldOp) { + return failure(); + } + return cloneYieldOp(yieldOp, pointerArgs, indexMap, mapping, rewriter); +} + +LogicalResult PromotePointerIterArgsPattern::cloneInstructionsForAdvancePtr( + Block &oldBody, Block &newBody, ArrayRef pointerArgs, + DenseMap &indexMap, IRMapping &mapping, + PatternRewriter &rewriter) const { + // Collect all operations from the old loop body except the terminator + SmallVector toClone; + for (auto &op : oldBody.without_terminator()) { + toClone.push_back(&op); + } + // Build a set of addptr operations to skip (those that update pointer + // iteration arguments) + DenseSet advancePtrOpsToSkip; + for (const auto &info : pointerArgs) { + if (info.addPtrValue) { + advancePtrOpsToSkip.insert(info.addPtrValue); + } + } + // Clone all operations except the skipped addptr operations + for (auto *op : toClone) { + if (auto advancePtrOp = dyn_cast(op)) { + if (advancePtrOpsToSkip.contains(advancePtrOp.getResult())) { + continue; + } + } + auto clonedOp = rewriter.clone(*op, mapping); + } + // Handle the yield terminator separately + auto yieldOp = dyn_cast(oldBody.getTerminator()); + if (!yieldOp) { + return failure(); + } + return cloneYieldOpForAdvancePtr(yieldOp, pointerArgs, indexMap, mapping, + rewriter); +} + +LogicalResult PromotePointerIterArgsPattern::cloneYieldOpForAdvancePtr( + scf::YieldOp yieldOp, ArrayRef pointerArgs, + DenseMap &indexMap, IRMapping &mapping, + PatternRewriter &rewriter) const { + SmallVector newOperands; + // Process each operand of the original yield operation + for (unsigned i = 0; i < yieldOp.getNumOperands(); ++i) { + auto operand = yieldOp.getOperand(i); + if (isPointerArgIndex(pointerArgs, i)) { + // For pointer arguments being promoted: create integer addition + SmallVector intResult = + createOffsetsForAdvancePtr(i, pointerArgs, indexMap, rewriter); + for (size_t i = 0; i < intResult.size(); ++i) { + newOperands.push_back(intResult[i]); + } + } else { + // For other arguments: use the value from the IR mapping + Value mappedValue = mapping.lookupOrDefault(operand); + newOperands.push_back(mappedValue); + } + } + // Validate that all new operands are non-null + for (size_t i = 0; i < newOperands.size(); ++i) { + auto v = newOperands[i]; + if (!v) { + return failure(); + } + } + // Create the new yield operation in the transformed loop + auto newYieldOp = + rewriter.create(yieldOp.getLoc(), newOperands); + return success(); +} + +LogicalResult PromotePointerIterArgsPattern::cloneYieldOp( + scf::YieldOp yieldOp, ArrayRef pointerArgs, + DenseMap &indexMap, IRMapping &mapping, + PatternRewriter &rewriter) const { + SmallVector newOperands; + // Process each operand of the original yield operation + for (unsigned i = 0; i < yieldOp.getNumOperands(); ++i) { + if (isPointerArgIndex(pointerArgs, i)) { + // For pointer arguments being promoted: create integer addition + Value intResult = createIntegerAdd(i, pointerArgs, indexMap, rewriter); + newOperands.push_back(intResult); + } else { + // For other arguments: use the value from the IR mapping + newOperands.push_back(mapping.lookupOrDefault(yieldOp.getOperand(i))); + } + } + // Validate that all new operands are non-null + for (auto v : newOperands) { + if (!v) { + return failure(); + } + } + // Create the new yield operation in the transformed loop + rewriter.create(yieldOp.getLoc(), newOperands); + return success(); +} + +SmallVector PromotePointerIterArgsPattern::createOffsetsForAdvancePtr( + unsigned idx, ArrayRef pointerArgs, + DenseMap &indexMap, PatternRewriter &rewriter) const { + const PointerArgInfo *info = nullptr; + for (auto &argInfo : pointerArgs) { + if (argInfo.oldIndex == idx) { + info = &argInfo; + break; + } + } + if (!info) { + return SmallVector(); + } + + if (info->offsetValues.empty()) { + return SmallVector(); + } + SmallVector offsets; + // Process all offset values in the collection + for (size_t i = 0; i < info->offsetValues.size(); ++i) { + Value offset = info->offsetValues[i]; + Value newArg = info->newInitArgs[i]; + // Get a location for creating constants + Location loc = offset.getLoc(); + // Check if this offset is a constant + Attribute offsetAttr; + if (matchPattern(offset, m_Constant(&offsetAttr))) { + // Case 1: Integer attribute (scalar constant) + if (auto intAttr = dyn_cast(offsetAttr)) { + if (intAttr.getInt() == 0) { + continue; + } + + Value result = + rewriter.create(loc, info->newIterArg, offset); + offsets.push_back(result); + } + } + } + return offsets; +} + +Value PromotePointerIterArgsPattern::createIntegerAdd( + unsigned idx, ArrayRef pointerArgs, + DenseMap &indexMap, PatternRewriter &rewriter) const { + const PointerArgInfo *info = nullptr; + for (auto &argInfo : pointerArgs) { + if (argInfo.oldIndex == idx) { + info = &argInfo; + break; + } + } + if (!info) + return nullptr; + // Try to extract constant offset value + Attribute offsetAttr; + if (matchPattern(info->offsetValue, m_Constant(&offsetAttr))) { + Location loc = info->offsetValue.getLoc(); + // Case 1: Integer attribute (scalar constant) + if (auto intAttr = dyn_cast(offsetAttr)) { + Value constOffset = rewriter.create( + loc, intAttr.getInt(), INT_TYPE_BIT_WIDTH); + return rewriter.create(loc, info->newIterArg, constOffset); + } + // Case 2: DenseElementsAttr (tensor constant) + if (auto denseAttr = dyn_cast(offsetAttr)) { + // Check if it's a splat (all elements are the same) + if (denseAttr.isSplat()) { + // For integer-type DenseElementsAttr + if (denseAttr.getElementType().isInteger(INT_TYPE_BIT_WIDTH)) { + auto splatValue = denseAttr.getSplatValue(); + Value constOffset = rewriter.create( + loc, splatValue.getZExtValue(), INT_TYPE_BIT_WIDTH); + return rewriter.create(loc, info->newIterArg, + constOffset); + } + } else { + // If not a splat, but has only one element, we can still handle it + if (denseAttr.getNumElements() == 1) { + auto firstElement = *denseAttr.getValues().begin(); + Value constOffset = rewriter.create( + loc, firstElement.getZExtValue(), INT_TYPE_BIT_WIDTH); + return rewriter.create(loc, info->newIterArg, + constOffset); + } + } + } + } + + // Return nullptr if offset is not a constant (pattern only handles constant + // offsets) + return nullptr; +} + +LogicalResult PromotePointerIterArgsPattern::replaceResults( + scf::ForOp oldForOp, scf::ForOp newForOp, + ArrayRef pointerArgs, + DenseMap &indexMap, PatternRewriter &rewriter) const { + SmallVector newResults; + + for (unsigned i = 0; i < oldForOp.getNumResults(); ++i) { + if (isPointerArgIndex(pointerArgs, i)) { + Value ptrResult = reconstructPointer( + oldForOp, i, newForOp.getResult(indexMap[i]), pointerArgs, rewriter); + newResults.push_back(ptrResult); + } else { + newResults.push_back(newForOp.getResult(indexMap[i])); + } + } + + for (auto v : newResults) { + if (!v) { + return failure(); + } + } + rewriter.replaceOp(oldForOp, newResults); + return success(); +} + +SmallVector PromotePointerIterArgsPattern::reconstructPointerForAdvance( + scf::ForOp forOp, unsigned idx, Value intResult, + ArrayRef pointerArgs, PatternRewriter &rewriter) const { + const PointerArgInfo *info = nullptr; + for (auto &argInfo : pointerArgs) { + if (argInfo.oldIndex == idx) { + info = &argInfo; + break; + } + } + if (!info) { + return SmallVector(); + } + // Create splat operation to broadcast integer result to tensor shape + auto baseType = info->basePointer.getType(); + SmallVector addPtr; + // Create a tensor with the same shape, where all elements are the integer + // result + SmallVector newInitArgs = info->newInitArgs; + for (size_t i = 0; i < newInitArgs.size(); ++i) { + if (auto maskInt = getConstantIntValue(newInitArgs[i])) { + if (*maskInt == 0) { + continue; + } + Value constOffset = rewriter.create( + forOp.getLoc(), *maskInt, INT_TYPE_BIT_WIDTH); + Value addPtrResult = rewriter.create( + forOp.getLoc(), newInitArgs[i], constOffset); + addPtr.push_back(addPtrResult); + } + } + return addPtr; +} + +Value PromotePointerIterArgsPattern::reconstructPointer( + scf::ForOp forOp, unsigned idx, Value intResult, + ArrayRef pointerArgs, PatternRewriter &rewriter) const { + const PointerArgInfo *info = nullptr; + for (auto &argInfo : pointerArgs) { + if (argInfo.oldIndex == idx) { + info = &argInfo; + break; + } + } + if (!info) + return nullptr; + + // Create splat operation to broadcast integer result to tensor shape + auto baseType = info->basePointer.getType(); + Value splatOffset = nullptr; + if (auto rankedType = dyn_cast(baseType)) { + // Get the shape of the original tensor + auto shape = rankedType.getShape(); + + splatOffset = rewriter.create( + forOp.getLoc(), RankedTensorType::get(shape, rewriter.getI32Type()), + intResult); + } else { + return nullptr; + } + + // Create a tensor with the same shape, where all elements are the integer + // result + return rewriter.create(forOp.getLoc(), + info->basePointer.getType(), + info->basePointer, splatOffset); +} + +void SimplifyTensorIterArgsPattern::ShapeChainInfo::dump() const { + LLVM_DEBUG({ + llvm::dbgs() << "ShapeChainInfo:\n"; + llvm::dbgs() << " base: " << base << "\n"; + llvm::dbgs() << " chain:\n"; + for (Operation *op : chain) { + llvm::dbgs() << " " << *op << "\n"; + } + }); +} + +void SimplifyTensorIterArgsPattern::CandidateInfo::dump() const { + LLVM_DEBUG({ + llvm::dbgs() << "CandidateInfo:\n"; + llvm::dbgs() << " idx: " << idx << "\n"; + llvm::dbgs() << " ShapeInfo: \n"; + shapeInfo.dump(); + for (Operation *op : arithOps) { + llvm::dbgs() << " ArithOp: " << *op << "\n"; + } + }); +} + +Value SimplifyTensorIterArgsPattern::cloneShapeChain( + Location loc, Value base, ArrayRef chain, + PatternRewriter &rewriter) const { + Value cur = base; + for (Operation *op : chain) { + if (auto splat = dyn_cast(op)) { + auto dstTy = cast(splat.getType()); + cur = rewriter.create(loc, dstTy, cur); + continue; + } + if (auto bcast = dyn_cast(op)) { + auto dstTy = cast(bcast.getType()); + cur = rewriter.create(loc, dstTy, cur); + continue; + } + if (auto expand = dyn_cast(op)) { + auto dstTy = cast(expand.getType()); + cur = rewriter.create(loc, dstTy, cur, + expand.getAxis()); + continue; + } + return Value(); + } + return cur; +} + +bool SimplifyTensorIterArgsPattern::isBlockArgumentFromAnotherForLoop( + Value v) const { + auto barg = dyn_cast(v); + if (!barg) { + return false; + } + + Block *owner = barg.getOwner(); + if (!owner) { + return false; + } + + auto parentFor = dyn_cast_or_null(owner->getParentOp()); + if (!parentFor) { + return false; + } + + // Only handle block args that belong to the body block of scf.for. + if (&parentFor.getRegion().front() != owner) { + return false; + } + + // scf.for body block args layout: + // arg#0 : induction variable + // arg#1..arg#N : region iter args + unsigned argNo = barg.getArgNumber(); + if (argNo == 0) { + // IV: not an iter arg init source. + return false; + } + + unsigned iterIdx = argNo - 1; + if (iterIdx >= parentFor.getInitArgs().size()) { + return false; + } + + return true; +} + +Value SimplifyTensorIterArgsPattern::normalizeInitArgForShapePeel( + Value v) const { + if (!isBlockArgumentFromAnotherForLoop(v)) { + // Not a block argument from another for loop, return as is. + return v; + } + auto barg = dyn_cast(v); + if (!barg) { + return v; + } + + Block *owner = barg.getOwner(); + if (!owner) { + return v; + } + + auto parentFor = dyn_cast_or_null(owner->getParentOp()); + if (!parentFor) { + return v; + } + + unsigned argNo = barg.getArgNumber(); + if (argNo == 0) { + return v; + } + + unsigned iterIdx = argNo - 1; + if (iterIdx >= parentFor.getInitArgs().size()) { + return v; + } + // For the simple nested relay case: + // inner.initArg = outer.regionIterArg ==> resolve to outer.initArg + return parentFor.getInitArgs()[iterIdx]; +} + +std::optional +SimplifyTensorIterArgsPattern::getRelayMapM1(scf::ForOp innerFor, + scf::ForOp outerFor, + unsigned innerIdx) const { + if (innerIdx >= innerFor.getInitArgs().size() || + innerIdx >= innerFor.getNumResults()) { + return std::nullopt; + } + + // 1) inner initArg -> outer iterArg idx + Value innerInit = innerFor.getInitArgs()[innerIdx]; + if (!isBlockArgumentFromAnotherForLoop(innerInit)) { + return std::nullopt; + } + + auto barg = dyn_cast(innerInit); + if (!barg) { + return std::nullopt; + } + + unsigned outerInitIdx = barg.getArgNumber() - 1; + if (outerInitIdx >= outerFor.getRegionIterArgs().size()) { + return std::nullopt; + } + + // NEW: ensure outer iterArg lane is used only as the mapped inner initArg. + // i.e. no extra users in outer body. + Value outerIterArg = outerFor.getRegionIterArgs()[outerInitIdx]; + if (!outerIterArg.hasOneUse()) { + return std::nullopt; + } + OpOperand &onlyUse = *outerIterArg.getUses().begin(); + if (onlyUse.getOwner() != innerFor.getOperation() || + onlyUse.get() != innerFor.getInitArgs()[innerIdx]) { + return std::nullopt; + } + + // 2) inner result -> outer yield slot + if (!outerFor.getBody() || !outerFor.getBody()->mightHaveTerminator()) { + return std::nullopt; + } + auto outerYield = dyn_cast(outerFor.getBody()->getTerminator()); + if (!outerYield) { + return std::nullopt; + } + + // only handle the case where inner result directly feeds into outer yield + // operand, without any intermediate use/def. + Value innerRes = innerFor.getResult(innerIdx); + std::optional outerYieldIdx; + for (unsigned k = 0; k < outerYield.getNumOperands(); ++k) { + if (outerYield.getOperand(k) == innerRes) { + outerYieldIdx = k; + break; + } + } + if (!outerYieldIdx.has_value()) { + return std::nullopt; + } + + if (outerInitIdx != *outerYieldIdx) { + // The position of iterArg and yield operand should be the same in this + // simple relay case. + return std::nullopt; + } + + return RelayMapM1{ + .innerIdx = innerIdx, + .outerInitIdx = outerInitIdx, + .outerYieldIdx = *outerYieldIdx, + }; +} + +void SimplifyTensorIterArgsPattern::splitCandidatesByRelay( + SmallVector all, + SmallVector &locals, + SmallVector &relays) const { + for (const auto &c : all) { + if (c.relayMap.has_value()) { + relays.push_back(c); + } else { + locals.push_back(c); + } + } +} + +std::optional +SimplifyTensorIterArgsPattern::peelShapeChain(Value v) const { + ShapeChainInfo info; + Value cur = v; + SmallVector rev; + while (Operation *def = cur.getDefiningOp()) { + if (isa(def)) { + rev.push_back(def); + cur = def->getOperand(0); + continue; + } + break; + } + if (rev.empty()) { + return std::nullopt; + } + std::reverse(rev.begin(), rev.end()); + info.base = cur; + info.chain = std::move(rev); + return info; +} + +bool SimplifyTensorIterArgsPattern::isArithWithConst(Operation *op, + Value curVal, + Value &nextVal, + Value &constVal) const { + nextVal = Value(); + constVal = Value(); + + Value lhs; + Value rhs; + if (!extractBinaryArithOperands(op, lhs, rhs)) { + return false; + } + + bool lhsIsConst = matchPattern(lhs, m_Constant()); + bool rhsIsConst = matchPattern(rhs, m_Constant()); + if (lhsIsConst == rhsIsConst) { + return false; + } + + if (lhs == curVal && rhsIsConst) { + nextVal = op->getResult(0); + constVal = rhs; + return true; + } + if (rhs == curVal && lhsIsConst) { + nextVal = op->getResult(0); + constVal = lhs; + return true; + } + + return false; +} + +Value SimplifyTensorIterArgsPattern::getNewConstLikeOperand( + Value cst, Type targetTy, PatternRewriter &rewriter) const { + Attribute attr; + if (!matchPattern(cst, m_Constant(&attr))) { + return Value(); + } + auto tensorTy = dyn_cast(targetTy); + if (!tensorTy) { + return Value(); + } + // Only support arith.constant dense right now. + auto dense = dyn_cast(attr); + if (!dense || !dense.isSplat()) { + return Value(); + } + auto splatAttr = + DenseElementsAttr::get(tensorTy, dense.getSplatValue()); + return rewriter.create(cst.getLoc(), tensorTy, splatAttr); +} + +bool SimplifyTensorIterArgsPattern::canBuildConstLikeOperand( + Value cst, Type targetTy) const { + Attribute attr; + if (!matchPattern(cst, m_Constant(&attr))) { + return false; + } + auto tensorTy = dyn_cast(targetTy); + if (!tensorTy) { + return false; + } + auto dense = dyn_cast(attr); + return dense && dense.isSplat(); +} + +LogicalResult SimplifyTensorIterArgsPattern::collectReverseLinearYieldPath( + Value yielded, Value iterArg, + SmallVectorImpl &opsInExecOrder) const { + SmallVector revOps; + Value cur = yielded; + + while (cur != iterArg) { + Operation *def = cur.getDefiningOp(); + if (!def || isa(def)) { + return failure(); + } + + Value lhs; + Value rhs; + if (!extractBinaryArithOperands(def, lhs, rhs)) { + return failure(); + } + + bool lhsIsConst = matchPattern(lhs, m_Constant()); + bool rhsIsConst = matchPattern(rhs, m_Constant()); + if (lhsIsConst == rhsIsConst) { + return failure(); + } + + Value upstream = lhsIsConst ? rhs : lhs; + if (!upstream) { + return failure(); + } + + // Safety: only accept a strict linear chain. + // The current def result must be consumed exclusively by `cur`. + // If there is any extra user, rewriting this lane may break semantics. + if (def->getNumResults() != 1) { + return failure(); + } + Value defRes = def->getResult(0); + if (!defRes.hasOneUse()) { + return failure(); + } + OpOperand &onlyUse = *defRes.getUses().begin(); + if (onlyUse.get() != cur) { + return failure(); + } + + revOps.push_back(def); + cur = upstream; + } + + opsInExecOrder.assign(revOps.rbegin(), revOps.rend()); + return success(); +} + +LogicalResult SimplifyTensorIterArgsPattern::matchAndRewrite( + scf::ForOp forOp, PatternRewriter &rewriter) const { + LLVM_DEBUG({ + llvm::dbgs() << "Now Handling For Op: \n"; + forOp.dump(); + }); + + // if you want only simplify iter args once to avoid infinite pattern + // application, return failure when meeting done label + if (forOp->hasAttr(kSimplifiedAttr)) { + LLVM_DEBUG({ + llvm::dbgs() << "This For Op has been simplified before.\n"; + forOp.dump(); + }); + } + // If the forOp has been marked as failed before, it means we attempted to + // simplify it but couldn't, so we should not try again. + if (forOp->hasAttr(kFailedAttr) || forOp->hasAttr(kIncompleteAttr)) { + return failure(); + } + + Block &oldBody = *forOp.getBody(); + if (!oldBody.mightHaveTerminator()) { + return failure(); + } + auto oldYield = dyn_cast(oldBody.getTerminator()); + if (!oldYield) { + return failure(); + } + + SmallVector candidates; + auto regionIterArgs = forOp.getRegionIterArgs(); + auto initArgs = forOp.getInitArgs(); + + for (unsigned i = 0; i < regionIterArgs.size(); ++i) { + Value iterArg = regionIterArgs[i]; + Value initArg = initArgs[i]; + Value yielded = oldYield.getOperand(i); + + auto iterTy = dyn_cast(iterArg.getType()); + if (!iterTy || !iterTy.hasStaticShape()) { + continue; + } + + // if initArg comes from another iterArg of an outer loop, get the ultimate + // source initArg. This handles the common nested relay pattern where inner + // loop iter arg is directly yielded from outer loop iter arg without + // modification. multiple for-loop levels of relay may require more complex + // data flow analysis to resolve the ultimate source initArg. + Value normalizedInitArg = normalizeInitArgForShapePeel(initArg); + + auto shapeInfoOpt = peelShapeChain(normalizedInitArg); + if (!shapeInfoOpt.has_value()) { + continue; + } + auto shapeInfo = *shapeInfoOpt; + + SmallVector chainOps; + if (failed(collectReverseLinearYieldPath(yielded, iterArg, chainOps))) { + continue; + } + + std::optional relayMap; + if (auto outerFor = dyn_cast_or_null(forOp->getParentOp())) { + relayMap = getRelayMapM1(forOp, outerFor, i); + } + + if (!relayMap.has_value() && isBlockArgumentFromAnotherForLoop(initArg)) { + LLVM_DEBUG({ + llvm::dbgs() + << "Init arg is a block argument from another for loop, but does " + "not form a simple relay pattern. Skipping candidate.\n"; + }); + continue; + } + + candidates.push_back(CandidateInfo{ + .idx = i, + .shapeInfo = std::move(shapeInfo), + .arithOps = std::move(chainOps), + .relayMap = relayMap, + }); + } + + if (candidates.empty()) { + return failure(); + } + + // Pre-validate all candidate chains before mutating IR. + // This avoids creating partial new IR and then returning failure(). + for (auto &c : candidates) { + auto baseTensorTy = dyn_cast(c.shapeInfo.base.getType()); + if (!baseTensorTy) { + return failure(); + } + + Value iterCur = regionIterArgs[c.idx]; + for (Operation *oldOp : c.arithOps) { + Value nextVal, oldConst; + if (!isArithWithConst(oldOp, iterCur, nextVal, oldConst)) { + return failure(); + } + if (!canBuildConstLikeOperand(oldConst, baseTensorTy)) { + return failure(); + } + iterCur = nextVal; + } + } + + if (!isSafeToRewriteLanesByResultUses(forOp, candidates)) { + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "Now Simplify For Op: \n"; + forOp.dump(); + llvm::dbgs() << "Found " << candidates.size() + << " candidate iter args to simplify in for loop at " + << forOp.getLoc() << "\n"; + for (auto &c : candidates) { + c.dump(); + } + }); + + SmallVector localCandidates; + SmallVector relayCandidates; + splitCandidatesByRelay(candidates, localCandidates, relayCandidates); + + FailureOr newForRes = rewriteForWithLocalCandidates( + forOp, localCandidates, /*outerCaptureMap=*/nullptr, rewriter); + if (failed(newForRes)) { + return failure(); + } + auto newFor = *newForRes; + + // case A: has relay -> continue relay pipeline + // If there are relay candidates, we need to rewrite the innerFor and outerFor + // together to maintain the relay relationship. The innerFor needs to be + // rewritten because the iterArg shape is changed after peeling, and the + // outerFor needs to be rewritten together to maintain the relay relationship + // between inner and outer iter args. + if (!relayCandidates.empty()) { + LLVM_DEBUG({ + llvm::dbgs() << "Nested For-loop args need to be rewritten to maintain " + "relay relationship\n"; + }); + scf::ForOp oldFor = forOp; + if (failed(rewriteForWithRelayCandidates(newFor, oldFor, relayCandidates, + rewriter))) { + rewriter.eraseOp(newFor); + return failure(); + } + return success(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "Only local candidates found, committing local rewrite\n"; + }); + // case B: no relay -> commit local rewrite now + if (newFor->hasAttr(kIncompleteAttr)) { + newFor->removeAttr(kIncompleteAttr); + } + newFor->setAttr(kSimplifiedAttr, rewriter.getUnitAttr()); + LLVM_DEBUG({ + llvm::dbgs() << "Successfully rewrote ForOp to: \n"; + newFor.dump(); + }); + rewriter.replaceOp(forOp, newFor.getResults()); + return success(); +} + +bool SimplifyTensorIterArgsPattern::extractBinaryArithOperands( + Operation *op, Value &lhs, Value &rhs) const { + if (auto v = dyn_cast(op)) { + lhs = v.getLhs(); + rhs = v.getRhs(); + return true; + } + if (auto v = dyn_cast(op)) { + lhs = v.getLhs(); + rhs = v.getRhs(); + return true; + } + if (auto v = dyn_cast(op)) { + lhs = v.getLhs(); + rhs = v.getRhs(); + return true; + } + if (auto v = dyn_cast(op)) { + lhs = v.getLhs(); + rhs = v.getRhs(); + return true; + } + + if (auto v = dyn_cast(op)) { + lhs = v.getLhs(); + rhs = v.getRhs(); + return true; + } + if (auto v = dyn_cast(op)) { + lhs = v.getLhs(); + rhs = v.getRhs(); + return true; + } + if (auto v = dyn_cast(op)) { + lhs = v.getLhs(); + rhs = v.getRhs(); + return true; + } + if (auto v = dyn_cast(op)) { + lhs = v.getLhs(); + rhs = v.getRhs(); + return true; + } + + return false; +} + +Value SimplifyTensorIterArgsPattern::createSameBinaryArithOp( + Operation *oldOp, Location loc, Value lhs, Value rhs, + PatternRewriter &rewriter) const { + if (isa(oldOp)) + return rewriter.create(loc, lhs, rhs).getResult(); + if (isa(oldOp)) + return rewriter.create(loc, lhs, rhs).getResult(); + if (isa(oldOp)) + return rewriter.create(loc, lhs, rhs).getResult(); + if (isa(oldOp)) + return rewriter.create(loc, lhs, rhs).getResult(); + + if (isa(oldOp)) + return rewriter.create(loc, lhs, rhs).getResult(); + if (isa(oldOp)) + return rewriter.create(loc, lhs, rhs).getResult(); + if (isa(oldOp)) + return rewriter.create(loc, lhs, rhs).getResult(); + if (isa(oldOp)) + return rewriter.create(loc, lhs, rhs).getResult(); + + return Value(); +} + +FailureOr +SimplifyTensorIterArgsPattern::rewriteForWithLocalCandidates( + scf::ForOp forOp, + ArrayRef candidates, + const IRMapping *outerCaptureMap, PatternRewriter &rewriter) const { + // This function is used to rewrite the forOp with local candidate. + // The caller should have already validated that the candidate's shape chain + // can be peeled and the arithmetic chain can be rebuilt with constants. + if (candidates.empty()) { + return forOp; + } + + Block &oldBody = *forOp.getBody(); + if (!oldBody.mightHaveTerminator()) { + return failure(); + } + auto oldYield = dyn_cast(oldBody.getTerminator()); + if (!oldYield) { + return failure(); + } + + auto regionIterArgs = forOp.getRegionIterArgs(); + auto initArgs = forOp.getInitArgs(); + + DenseMap candMap; + for (auto &c : candidates) { + candMap[c.idx] = &c; + } + + DenseMap arithOpToCandIdx; + DenseSet candidateArithSet; + for (auto &c : candidates) { + for (Operation *op : c.arithOps) { + candidateArithSet.insert(op); + arithOpToCandIdx[op] = c.idx; + } + } + + // Build new init args (candidate iter arg uses base) + SmallVector newInitArgs; + newInitArgs.reserve(initArgs.size()); + for (unsigned i = 0; i < initArgs.size(); ++i) { + if (auto it = candMap.find(i); it != candMap.end()) { + newInitArgs.push_back(it->second->shapeInfo.base); + continue; + } + Value v = initArgs[i]; + if (outerCaptureMap) { + if (Value mapped = outerCaptureMap->lookupOrNull(v)) { + v = mapped; + } + } + newInitArgs.push_back(v); + } + + auto newFor = rewriter.create( + forOp.getLoc(), forOp.getLowerBound(), forOp.getUpperBound(), + forOp.getStep(), newInitArgs); + newFor->setAttr(kIncompleteAttr, rewriter.getUnitAttr()); + + auto failAfterCreate = [&]() -> LogicalResult { + rewriter.eraseOp(newFor); + return failure(); + }; + + IRMapping mapping; + mapping.map(forOp.getInductionVar(), newFor.getInductionVar()); + + // map region args + for (unsigned i = 0; i < regionIterArgs.size(); ++i) { + Value oldArg = regionIterArgs[i]; + Value newArg = newFor.getRegionIterArgs()[i]; + mapping.map(oldArg, newArg); + } + + rewriter.setInsertionPointToStart(newFor.getBody()); + + // Materialize candidate iter-shape values and remap old iterArg semantic + // values. + for (auto &c : candidates) { + Value baseArg = newFor.getRegionIterArgs()[c.idx]; + Value mat = + cloneShapeChain(forOp.getLoc(), baseArg, c.shapeInfo.chain, rewriter); + if (!mat) { + return failAfterCreate(); + } + mapping.map(regionIterArgs[c.idx], mat); + } + + // Track current base-domain running value for each candidate lane. + DenseMap baseCurByCand; + for (auto &c : candidates) { + baseCurByCand[c.idx] = newFor.getRegionIterArgs()[c.idx]; + } + + // Replay old body in-order: + // - candidate arith op: rebuild now and map old result -> new result + // - other op: clone with mapping + for (Operation &op : oldBody.without_terminator()) { + if (candidateArithSet.contains(&op)) { + auto itIdx = arithOpToCandIdx.find(&op); + if (itIdx == arithOpToCandIdx.end()) { + return failAfterCreate(); + } + unsigned candIdx = itIdx->second; + + Value baseCur = baseCurByCand[candIdx]; + auto baseTensorTy = dyn_cast(baseCur.getType()); + if (!baseTensorTy) { + return failAfterCreate(); + } + + Value lhs; + Value rhs; + if (!extractBinaryArithOperands(&op, lhs, rhs)) { + return failAfterCreate(); + } + + bool lhsIsCst = matchPattern(lhs, m_Constant()); + bool rhsIsCst = matchPattern(rhs, m_Constant()); + if (lhsIsCst == rhsIsCst) { + return failAfterCreate(); + } + + Value oldConst = lhsIsCst ? lhs : rhs; + Value baseConst = + getNewConstLikeOperand(oldConst, baseTensorTy, rewriter); + if (!baseConst) { + return failAfterCreate(); + } + + Value newRes = createSameBinaryArithOp(&op, op.getLoc(), baseCur, + baseConst, rewriter); + if (!newRes) { + return failAfterCreate(); + } + + mapping.map(op.getResult(0), newRes); + baseCurByCand[candIdx] = newRes; + continue; + } + + // Non-candidate op: clone as-is with mapping. + if (outerCaptureMap) { + for (Value operand : op.getOperands()) { + if (mapping.lookupOrNull(operand)) { + continue; // already mapped (iv/iterArg/local rebuilt values) + } + if (Value mappedOuter = outerCaptureMap->lookupOrNull(operand)) { + mapping.map(operand, mappedOuter); + } + } + } + rewriter.clone(op, mapping); + } + + // Rebuild yield. + SmallVector newYieldOperands; + newYieldOperands.reserve(oldYield.getNumOperands()); + for (unsigned i = 0; i < oldYield.getNumOperands(); ++i) { + if (candMap.count(i)) { + auto it = baseCurByCand.find(i); + if (it == baseCurByCand.end() || !it->second) { + return failAfterCreate(); + } + newYieldOperands.push_back(it->second); + } else { + Value mapped = mapping.lookupOrDefault(oldYield.getOperand(i)); + if (!mapped) { + return failAfterCreate(); + } + newYieldOperands.push_back(mapped); + } + } + rewriter.create(oldYield.getLoc(), newYieldOperands); + if (newFor->hasAttr(kIncompleteAttr)) { + newFor->removeAttr(kIncompleteAttr); + } + newFor->setAttr(kSimplifiedAttr, rewriter.getUnitAttr()); + return newFor; +} + +bool SimplifyTensorIterArgsPattern::isSafeToRewriteLanesByResultUses( + scf::ForOp forOp, + ArrayRef candidates) const { + if (candidates.empty()) { + return false; + } + + SmallVector laneIdxs; + laneIdxs.reserve(candidates.size()); + for (const auto &c : candidates) { + laneIdxs.push_back(c.idx); + } + + auto parentOuter = dyn_cast_or_null(forOp->getParentOp()); + if (!parentOuter || !parentOuter.getBody() || + !parentOuter.getBody()->mightHaveTerminator()) { + // No parent loop or parent loop body is malformed, be conservative and + // return false to avoid unsafe rewrite. + return false; + } + auto parentOuterYield = + parentOuter + ? dyn_cast(parentOuter.getBody()->getTerminator()) + : nullptr; + + for (const auto &c : candidates) { + unsigned idx = c.idx; + if (idx >= forOp.getNumResults()) { + return false; + } + + Value r = forOp.getResult(idx); + if (r.use_empty()) { + continue; + } + + // relay case: the rewritten lane can only be used by the corresponding + // outer yield operand in the parent loop, and cannot have any other uses. + if (c.relayMap.has_value()) { + if (!parentOuter || !parentOuterYield) { + return false; + } + unsigned outerYieldIdx = c.relayMap->outerYieldIdx; + if (outerYieldIdx >= parentOuterYield.getNumOperands()) { + return false; + } + + for (OpOperand &use : r.getUses()) { + auto y = dyn_cast(use.getOwner()); + if (!y || y != parentOuterYield) { + return false; + } + if (use.getOperandNumber() != outerYieldIdx) { + return false; + } + } + + // outer result cannot have any uses outside of the parent loop's yield + // operand + unsigned outerLane = c.relayMap->outerYieldIdx; + if (outerLane >= parentOuter.getNumResults()) { + return false; + } + if (!parentOuter.getResult(outerLane).use_empty()) { + return false; + } + + continue; + } + + // local case: the rewritten lane cannot have any uses outside of the forOp + // results + return false; + } + + return true; +} + +LogicalResult SimplifyTensorIterArgsPattern::precheckRelayCandidates( + scf::ForOp innerFor, + ArrayRef relayCandidates, + scf::ForOp &outerForOut) const { + if (relayCandidates.empty()) { + return failure(); + } + + auto outerFor = dyn_cast_or_null(innerFor->getParentOp()); + if (!outerFor) { + return failure(); + } + + DenseSet usedOuterInitIdx; + DenseSet usedOuterYieldIdx; + if (!outerFor.getBody() || !outerFor.getBody()->mightHaveTerminator()) { + return failure(); + } + auto outerYield = dyn_cast(outerFor.getBody()->getTerminator()); + if (!outerYield) { + return failure(); + } + + for (const auto &c : relayCandidates) { + if (!c.relayMap.has_value()) { + return failure(); + } + const auto &m = *c.relayMap; + + // must be the same level mapping for the current innerFor + if (m.innerIdx != c.idx) { + return failure(); + } + + // M1 strict lane + if (m.outerInitIdx != m.outerYieldIdx) { + return failure(); + } + + if (m.outerInitIdx >= outerFor.getInitArgs().size()) { + return failure(); + } + if (m.outerYieldIdx >= outerYield.getNumOperands()) { + return failure(); + } + + // Conflict check: outer lane cannot be occupied multiple times + // different inner iter args mapped to the same outer iter arg or yield + // operand is not supported in this simple relay case, as it may require + // more complex data flow analysis to resolve the ultimate source init arg + // and final yield operand. + if (!usedOuterInitIdx.insert(m.outerInitIdx).second) { + return failure(); + } + if (!usedOuterYieldIdx.insert(m.outerYieldIdx).second) { + return failure(); + } + } + + outerForOut = outerFor; + return success(); +} + +FailureOr +SimplifyTensorIterArgsPattern::rewriteInnerForWithRelayCandidates( + scf::ForOp innerFor, + ArrayRef relayCandidates, + const IRMapping *outerCaptureMap, PatternRewriter &rewriter) const { + if (!innerFor || relayCandidates.empty()) { + return failure(); + } + + // Reuse local-like rebuild on the already-correct inner init args. + // IMPORTANT: For relay mode, caller must ensure innerFor init args on relay + // lanes are already wired to new outer iter args. + return rewriteForWithLocalCandidates(innerFor, relayCandidates, + outerCaptureMap, rewriter); +} + +FailureOr +SimplifyTensorIterArgsPattern::rewriteOuterForWithRelayCandidates( + scf::ForOp innerFor, scf::ForOp oldInnerFor, scf::ForOp outerFor, + ArrayRef relayCandidates, + PatternRewriter &rewriter) const { + if (!outerFor || !innerFor || relayCandidates.empty()) { + return failure(); + } + + Block &oldOuterBody = *outerFor.getBody(); + if (!oldOuterBody.mightHaveTerminator()) { + return failure(); + } + auto oldOuterYield = dyn_cast(oldOuterBody.getTerminator()); + if (!oldOuterYield) { + return failure(); + } + + // Build new outer init args: relay lanes switch to base. + SmallVector newOuterInitArgs(outerFor.getInitArgs().begin(), + outerFor.getInitArgs().end()); + for (const auto &c : relayCandidates) { + if (!c.relayMap.has_value()) { + return failure(); + } + const auto &m = *c.relayMap; + if (m.outerInitIdx >= newOuterInitArgs.size()) { + return failure(); + } + newOuterInitArgs[m.outerInitIdx] = c.shapeInfo.base; + } + + rewriter.setInsertionPoint(outerFor); + auto newOuterFor = rewriter.create( + outerFor.getLoc(), outerFor.getLowerBound(), outerFor.getUpperBound(), + outerFor.getStep(), newOuterInitArgs); + newOuterFor->setAttr(kIncompleteAttr, rewriter.getUnitAttr()); + + auto failAfterCreate = [&]() -> FailureOr { + rewriter.eraseOp(newOuterFor); + return failure(); + }; + + IRMapping outerMap; + outerMap.map(outerFor.getInductionVar(), newOuterFor.getInductionVar()); + for (unsigned i = 0; i < outerFor.getRegionIterArgs().size(); ++i) { + outerMap.map(outerFor.getRegionIterArgs()[i], + newOuterFor.getRegionIterArgs()[i]); + } + + rewriter.setInsertionPointToStart(newOuterFor.getBody()); + + scf::ForOp rewrittenInnerInNewOuter; + + // Clone outer body with special handling for old inner. + for (Operation &op : oldOuterBody.without_terminator()) { + // Fast path: not the anchor inner op we want to rebuild. + if (&op != innerFor.getOperation()) { + if (&op == oldInnerFor.getOperation()) { + // Drop old inner only when it is different from anchor inner. + continue; + } + rewriter.clone(op, outerMap); + continue; + } + + // Build relay-wired inner init args: + // relay lane: inner init <- new outer iterArg[outerInitIdx] + // non-relay lane: mapped old inner init + SmallVector relayWiredInnerInitArgs; + relayWiredInnerInitArgs.reserve(innerFor.getInitArgs().size()); + + DenseMap innerIdxToOuterInitIdx; + for (const auto &c : relayCandidates) { + const auto &m = *c.relayMap; + innerIdxToOuterInitIdx[m.innerIdx] = m.outerInitIdx; + } + + for (unsigned i = 0; i < innerFor.getInitArgs().size(); ++i) { + auto it = innerIdxToOuterInitIdx.find(i); + if (it != innerIdxToOuterInitIdx.end()) { + unsigned outerInitIdx = it->second; + if (outerInitIdx >= newOuterFor.getRegionIterArgs().size()) { + return failAfterCreate(); + } + relayWiredInnerInitArgs.push_back( + newOuterFor.getRegionIterArgs()[outerInitIdx]); + } else { + relayWiredInnerInitArgs.push_back( + outerMap.lookupOrDefault(innerFor.getInitArgs()[i])); + } + } + + // Build inner relay candidates whose base comes from new outer iter args. + SmallVector relayCandidatesForInner; + relayCandidatesForInner.reserve(relayCandidates.size()); + for (const auto &c : relayCandidates) { + CandidateInfo cc = c; // copy + + if (!cc.relayMap.has_value()) { + return failAfterCreate(); + } + unsigned outerInitIdx = cc.relayMap->outerInitIdx; + if (outerInitIdx >= newOuterFor.getRegionIterArgs().size()) { + return failAfterCreate(); + } + + // relay semantics: inner base is new outer iter arg on mapped lane + cc.shapeInfo.base = newOuterFor.getRegionIterArgs()[outerInitIdx]; + relayCandidatesForInner.push_back(std::move(cc)); + } + + FailureOr rewrittenInnerRes = failure(); + { + PatternRewriter::InsertionGuard guard(rewriter); + rewrittenInnerRes = rewriteInnerForWithRelayCandidates( + innerFor, relayCandidatesForInner, &outerMap, rewriter); + } + if (failed(rewrittenInnerRes)) { + return failAfterCreate(); + } + rewrittenInnerInNewOuter = *rewrittenInnerRes; + + // Map old inner results to rewritten inner results for outer cloning/yield + // mapping. + if (innerFor.getNumResults() != rewrittenInnerInNewOuter.getNumResults()) { + return failAfterCreate(); + } + for (unsigned r = 0; r < innerFor.getNumResults(); ++r) { + outerMap.map(innerFor.getResult(r), + rewrittenInnerInNewOuter.getResult(r)); + } + } + + if (!rewrittenInnerInNewOuter) { + return failAfterCreate(); + } + + // Rebuild outer yield; relay lanes forced from rewritten inner results. + SmallVector newOuterYieldOps; + newOuterYieldOps.reserve(oldOuterYield.getNumOperands()); + DenseMap outerYieldToInnerIdx; + for (const auto &c : relayCandidates) { + const auto &m = *c.relayMap; + outerYieldToInnerIdx[m.outerYieldIdx] = m.innerIdx; + } + + for (unsigned i = 0; i < oldOuterYield.getNumOperands(); ++i) { + auto it = outerYieldToInnerIdx.find(i); + if (it != outerYieldToInnerIdx.end()) { + unsigned innerIdx = it->second; + if (innerIdx >= rewrittenInnerInNewOuter.getNumResults()) { + return failAfterCreate(); + } + newOuterYieldOps.push_back(rewrittenInnerInNewOuter.getResult(innerIdx)); + } else { + newOuterYieldOps.push_back( + outerMap.lookupOrDefault(oldOuterYield.getOperand(i))); + } + } + + rewriter.setInsertionPointToEnd(newOuterFor.getBody()); + rewriter.create(oldOuterYield.getLoc(), newOuterYieldOps); + + if (newOuterFor->hasAttr(kIncompleteAttr)) { + newOuterFor->removeAttr(kIncompleteAttr); + } + newOuterFor->setAttr(kSimplifiedAttr, rewriter.getUnitAttr()); + return newOuterFor; +} + +LogicalResult SimplifyTensorIterArgsPattern::rewriteForWithRelayCandidates( + scf::ForOp newfor, scf::ForOp oldFor, + ArrayRef relayCandidates, PatternRewriter &rewriter) const { + // forOp is innerFor (already local-rewritten or to-be-relay-rewritten) + scf::ForOp innerFor = newfor; + scf::ForOp outerFor; + if (failed(precheckRelayCandidates(innerFor, relayCandidates, outerFor))) { + return failure(); + } + + FailureOr newOuterRes = rewriteOuterForWithRelayCandidates( + innerFor, oldFor, outerFor, relayCandidates, rewriter); + if (failed(newOuterRes)) { + return failure(); + } + + scf::ForOp newOuterFor = *newOuterRes; + LLVM_DEBUG({ + llvm::dbgs() << "Successfully rewrote outer ForOp to: \n"; + newOuterFor.dump(); + }); + + // Only replace outer; old inner is nested under old outer and will be removed + // together. + rewriter.replaceOp(outerFor, newOuterFor.getResults()); + return success(); +} + +bool IfYieldAddHoistConverter::isSupportedTensorResultType(Type type) const { + auto tensorType = dyn_cast(type); + return tensorType && !isa(tensorType.getElementType()); +} + +bool IfYieldAddHoistConverter::isDefinedOutsideIf(Value value, + scf::IfOp ifOp) const { + if (auto blockArg = dyn_cast(value)) { + Block *owner = blockArg.getOwner(); + return owner && owner->getParentOp() != ifOp.getOperation(); + } + + Operation *defOp = value.getDefiningOp(); + return defOp && !ifOp->isAncestor(defOp); +} + +bool IfYieldAddHoistConverter::extractAddendFromAddExpr( + Value maybeAddExpr, Value baseValue, Value &addendOut) const { + if (auto addi = maybeAddExpr.getDefiningOp()) { + if (addi.getLhs() == baseValue) { + addendOut = addi.getRhs(); + return true; + } + if (addi.getRhs() == baseValue) { + addendOut = addi.getLhs(); + return true; + } + return false; + } + + if (auto addf = maybeAddExpr.getDefiningOp()) { + if (addf.getLhs() == baseValue) { + addendOut = addf.getRhs(); + return true; + } + if (addf.getRhs() == baseValue) { + addendOut = addf.getLhs(); + return true; + } + return false; + } + + return false; +} + +Value IfYieldAddHoistConverter::buildZeroTensorLikeType( + Type laneType, Location loc, PatternRewriter &rewriter) const { + auto tensorType = dyn_cast(laneType); + if (!tensorType) + return Value(); + + Type elemType = tensorType.getElementType(); + Attribute zeroElemAttr; + if (isa(elemType)) { + zeroElemAttr = rewriter.getFloatAttr(elemType, 0.0); + } else if (elemType.isIntOrIndex()) { + zeroElemAttr = rewriter.getIntegerAttr(elemType, 0); + } else { + return Value(); + } + + auto zeroTensorAttr = DenseElementsAttr::get(tensorType, zeroElemAttr); + return rewriter.create(loc, laneType, zeroTensorAttr); +} + +bool IfYieldAddHoistConverter::tryRewriteSingleLane( + unsigned laneIdx, Value baseBranchYield, Value addExprBranchYield, + bool baseInThenBranch, Type laneType, scf::IfOp ifOp, + PatternRewriter &rewriter, SmallVectorImpl &updatedThenYieldOperands, + SmallVectorImpl &updatedElseYieldOperands, + SmallVectorImpl &hoistedBasePerLane, + SmallVectorImpl &laneRewrittenFlags) const { + if (!isDefinedOutsideIf(baseBranchYield, ifOp)) + return false; + + Value addendValue; + if (!extractAddendFromAddExpr(addExprBranchYield, baseBranchYield, + addendValue)) + return false; + if (!addendValue || addendValue.getType() != laneType) + return false; + + Value zeroTensor = buildZeroTensorLikeType(laneType, ifOp.getLoc(), rewriter); + if (!zeroTensor) + return false; + + if (baseInThenBranch) { + updatedThenYieldOperands[laneIdx] = zeroTensor; + updatedElseYieldOperands[laneIdx] = addendValue; + } else { + updatedThenYieldOperands[laneIdx] = addendValue; + updatedElseYieldOperands[laneIdx] = zeroTensor; + } + + hoistedBasePerLane[laneIdx] = baseBranchYield; + laneRewrittenFlags[laneIdx] = true; + return true; +} + +// scf.if lane rewrite: +// one branch yields A, the other yields A + B +// => if yields 0 / B, then outside do A + if_result +LogicalResult +IfYieldAddHoistConverter::matchAndRewrite(scf::IfOp ifOp, + PatternRewriter &rewriter) const { + if (ifOp.getNumResults() == 0) { + return failure(); + } + + // Robust guard for malformed / emptied regions. + Block *thenBlockPtr = ifOp.thenBlock(); + Block *elseBlockPtr = ifOp.elseBlock(); + if (!thenBlockPtr || !elseBlockPtr) { + return failure(); + } + if (!thenBlockPtr->mightHaveTerminator() || + !elseBlockPtr->mightHaveTerminator()) { + return failure(); + } + + auto thenYieldOp = dyn_cast(ifOp.thenBlock()->getTerminator()); + auto elseYieldOp = dyn_cast(ifOp.elseBlock()->getTerminator()); + if (!thenYieldOp || !elseYieldOp) { + return failure(); + } + + Location loc = ifOp.getLoc(); + bool anyLaneRewritten = false; + + SmallVector updatedThenYieldOperands(thenYieldOp.getOperands().begin(), + thenYieldOp.getOperands().end()); + SmallVector updatedElseYieldOperands(elseYieldOp.getOperands().begin(), + elseYieldOp.getOperands().end()); + + // for each lane, record whether we need post-if add with baseYield + SmallVector hoistedBasePerLane(ifOp.getNumResults(), Value()); + SmallVector laneRewrittenFlags(ifOp.getNumResults(), false); + + for (unsigned laneIdx = 0; laneIdx < ifOp.getNumResults(); ++laneIdx) { + Type laneType = ifOp.getResultTypes()[laneIdx]; + if (!isSupportedTensorResultType(laneType)) { + continue; + } + + Value thenYieldVal = thenYieldOp.getOperand(laneIdx); + Value elseYieldVal = elseYieldOp.getOperand(laneIdx); + + // Assume that only one of the two branches can have the add pattern, and + // try both ways to find a rewrite opportunity. + bool rewritten = + tryRewriteSingleLane( + laneIdx, thenYieldVal, elseYieldVal, /* baseInThenBranch= */ true, + laneType, ifOp, rewriter, updatedThenYieldOperands, + updatedElseYieldOperands, hoistedBasePerLane, laneRewrittenFlags) || + tryRewriteSingleLane( + laneIdx, elseYieldVal, thenYieldVal, /* baseInThenBranch= */ false, + laneType, ifOp, rewriter, updatedThenYieldOperands, + updatedElseYieldOperands, hoistedBasePerLane, laneRewrittenFlags); + + anyLaneRewritten |= rewritten; + } + + if (!anyLaneRewritten) { + return failure(); + } + + rewriter.setInsertionPoint(ifOp); + auto rewrittenIfOp = rewriter.create(loc, ifOp.getResultTypes(), + ifOp.getCondition(), + /* withElseRegion= */ true); + + { + IRMapping thenMapping; + Block *oldThenBlock = ifOp.thenBlock(); + Block *newThenBlock = rewrittenIfOp.thenBlock(); + + rewriter.setInsertionPointToStart(newThenBlock); + for (Operation &op : oldThenBlock->without_terminator()) { + rewriter.clone(op, thenMapping); + } + + SmallVector mappedThenYieldOperands; + mappedThenYieldOperands.reserve(updatedThenYieldOperands.size()); + for (Value v : updatedThenYieldOperands) { + mappedThenYieldOperands.push_back(thenMapping.lookupOrDefault(v)); + } + + rewriter.create(loc, mappedThenYieldOperands); + } + + { + IRMapping elseMapping; + Block *oldElseBlock = ifOp.elseBlock(); + Block *newElseBlock = rewrittenIfOp.elseBlock(); + + rewriter.setInsertionPointToStart(newElseBlock); + for (Operation &op : oldElseBlock->without_terminator()) { + rewriter.clone(op, elseMapping); + } + + SmallVector mappedElseYieldOperands; + mappedElseYieldOperands.reserve(updatedElseYieldOperands.size()); + for (Value v : updatedElseYieldOperands) { + mappedElseYieldOperands.push_back(elseMapping.lookupOrDefault(v)); + } + + rewriter.create(loc, mappedElseYieldOperands); + } + + rewriter.setInsertionPointAfter(rewrittenIfOp); + SmallVector finalResults; + finalResults.reserve(ifOp.getNumResults()); + + for (unsigned laneIdx = 0; laneIdx < ifOp.getNumResults(); ++laneIdx) { + if (!laneRewrittenFlags[laneIdx]) { + finalResults.push_back(rewrittenIfOp.getResult(laneIdx)); + continue; + } + + Value hoistedBase = hoistedBasePerLane[laneIdx]; + Value laneDelta = rewrittenIfOp.getResult(laneIdx); + + auto laneTensorType = dyn_cast(laneDelta.getType()); + if (!laneTensorType) + return failure(); + + Type elemType = laneTensorType.getElementType(); + Value reconstructedLane; + if (isa(elemType)) { + reconstructedLane = + rewriter.create(loc, hoistedBase, laneDelta); + } else if (elemType.isIntOrIndex()) { + reconstructedLane = + rewriter.create(loc, hoistedBase, laneDelta); + } else { + return failure(); + } + + finalResults.push_back(reconstructedLane); + } + + rewriter.replaceOp(ifOp, finalResults); + return success(); +} + +} // namespace CannonicalizerConverter diff --git a/third_party/ascend/lib/TritonToStructured/MaskAnalysis.cpp b/third_party/ascend/lib/TritonToStructured/MaskAnalysis.cpp new file mode 100644 index 0000000000..e638e1994c --- /dev/null +++ b/third_party/ascend/lib/TritonToStructured/MaskAnalysis.cpp @@ -0,0 +1,1066 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToStructured/MaskAnalysis.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/Value.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/IR/Visitors.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/DialectConversion.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" + +#include "TritonToStructured/PtrAnalysis.h" +#include "Utils/Utils.h" + +#define DEBUG_TYPE "triton-to-structured-mask-analysis" + +namespace TritonToStructured { +using namespace mlir; +using namespace triton; + +bool dimInfo::setType(arith::CmpIPredicate Type) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Setting compare type for dimIndex " << dimIndex << "\n"; + llvm::dbgs() << "Type: " << Type << "\n"; + llvm::dbgs() << "----------------------------------------------\n"; + }); + + switch (Type) { + case arith::CmpIPredicate::slt: + this->currentType = dimInfo::CompareType::slt; + break; + case arith::CmpIPredicate::ult: + this->currentType = dimInfo::CompareType::ult; + break; + case arith::CmpIPredicate::sge: + this->currentType = dimInfo::CompareType::sge; + break; + case arith::CmpIPredicate::uge: + this->currentType = dimInfo::CompareType::uge; + break; + default: + return false; + } + return true; +} + +bool dimInfo::compareTypeIsLess() const { + return this->currentType == dimInfo::CompareType::slt || + this->currentType == dimInfo::CompareType::ult; +} + +void dimInfo::dump() const { + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskDimInfo: \n"; + llvm::dbgs() << "offset = " << offset << "\n"; + llvm::dbgs() << "shape = " << shape << "\n"; + llvm::dbgs() << "rhs = " << rhs << "\n"; + llvm::dbgs() << "isLessMode = " << compareTypeIsLess() << "\n"; + llvm::dbgs() << "hasBroadCast = " << hasBroadCast << "\n"; + llvm::dbgs() << "----------------------------------------------\n"; +}; + +void MaskState::dump() const { + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskState :\n"; + llvm::dbgs() << "scalar = " << scalar << "\n"; + llvm::dbgs() << "stateInfo.size = " << stateInfo.size() << "\n"; + for (auto info : stateInfo) + info.dump(); + llvm::dbgs() << "----------------------------------------------\n"; +}; + +LogicalResult MaskState::parse(Value operand, const Location loc, + OpBuilder &builder) { + if (isa(operand.getType()) && + operand.getType().getIntOrFloatBitWidth() != 1) { + return this->parseIntScalar(operand, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseConstant(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseAdd(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseSub(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseAnd(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseCmp(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseMakeRange(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseBroadcast(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseSplat(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseExpandDims(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseExtSI(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseRem(op, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return this->parseDiv(op, loc, builder); + } + LLVM_DEBUG({ + InFlightDiagnostic diag = emitWarning(loc) + << "MaskAnalysis: compare operand produced by an " + "unsupported operation\n"; + }); + return failure(); +} + +LogicalResult MaskState::parseConstant(arith::ConstantOp constOp, + const Location loc, OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + constOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting constant"); + }); + return failure(); + } + + if (auto intType = dyn_cast(constOp.getType())) { + if (intType.getWidth() == 1) { + LLVM_DEBUG({ + constOp.emitWarning("MaskAnalysis: Unsupported constant for int1"); + }); + return failure(); + } + } + + if (isa(constOp.getValue())) { + auto attr = cast(constOp.getValue()); + auto elementType = attr.getElementType(); + if (!attr.isSplat() || !isa(elementType)) { + LLVM_DEBUG({ + constOp.emitError("MaskAnalysis: only support splat integer constant"); + }); + return failure(); + } + auto values = attr.getValues(); + auto value = values[0].getValue(); + auto constAttr = builder.getIndexAttr(value.getSExtValue()); + auto op = arith::ConstantOp::materialize(builder, constAttr, + builder.getIndexType(), loc); + this->scalar = op.getValue(); + } else { + auto value = cast(constOp.getValue()).getInt(); + this->scalar = builder.getIndexAttr(value); + } + return success(); +} + +LogicalResult MaskState::parseIntScalar(Value scalar, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitError(loc) << "MaskAnalysis: MaskState should be empty when " + "visiting integer scalar"; + }); + return failure(); + } + auto castOp = + builder.create(loc, builder.getIndexType(), scalar); + this->scalar = castOp.getResult(); + return success(); +} + +LogicalResult MaskState::parseMakeRange(triton::MakeRangeOp rangeOp, + const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + rangeOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting make_range"); + }); + return failure(); + } + + auto shape = cast(rangeOp.getType()).getShape(); + auto start = rangeOp.getStart(); + auto end = rangeOp.getEnd(); + auto stride = (end - start + shape[0] - 1) / shape[0]; + + if (stride != 1) { + LLVM_DEBUG( + { + InFlightDiagnostic diag = + emitError(loc) + << "stride must be 1 for make_range whose result is used " + "as load or store masks"; + }); + return failure(); + } + + stateInfo.emplace_back(builder.getIndexAttr(start), + builder.getIndexAttr(shape[0])); + return success(); +} + +LogicalResult MaskState::parseExtSI(arith::ExtSIOp op, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + op->emitError( + "MaskAnalysis: MaskState should be empty when visiting extsi"); + }); + return failure(); + } + return parse(op.getIn(), loc, builder); +} + +LogicalResult MaskState::parseSplat(triton::SplatOp splatOp, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + splatOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting splat"); + }); + return failure(); + } + + auto src = splatOp.getSrc(); + auto dst = splatOp.getResult(); + auto dstShape = cast(dst.getType()).getShape(); + + if (!isa(src.getType())) { + LLVM_DEBUG( + { + splatOp.emitError() + << "splat source must be an integer scalar for load/store masks"; + }); + return failure(); + } + + if (failed(this->parse(src, loc, builder))) + return failure(); + + if (stateInfo.size() > 1 || + (stateInfo.size() == 1 && !isOne(stateInfo.back().shape))) { + LLVM_DEBUG({ + splatOp.emitError() << "splat from a non-scalar source is not supported, " + "unless it's state size and shape are 1"; + }); + return failure(); + } + + auto zeroAttr = builder.getIndexAttr(0); + SmallVector newStateInfo; + for (auto [i, shape] : llvm::enumerate(dstShape)) { + auto shapeAttr = builder.getIndexAttr(shape); + newStateInfo.emplace_back(zeroAttr, shapeAttr, i, true); + if (!stateInfo.empty() && shape == 1) { + newStateInfo.back() = stateInfo.back(); + newStateInfo.back().dimIndex = i; + } + } + this->stateInfo = newStateInfo; + return success(); +} + +LogicalResult MaskState::parseExpandDims(triton::ExpandDimsOp expandDimsOp, + const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + expandDimsOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting expand_dims"); + }); + return failure(); + } + + auto zeroAttr = builder.getIndexAttr(0); + auto defaultShape = builder.getIndexAttr(1); + + if (failed(this->parse(expandDimsOp.getSrc(), loc, builder))) + return failure(); + + auto dstShape = + cast(expandDimsOp.getResult().getType()).getShape(); + auto axis = expandDimsOp.getAxis(); + if (dstShape[axis] != 1) { + LLVM_DEBUG({ + expandDimsOp.emitError( + "MaskAnalysis: unexpected dimension size in expand_dims"); + }); + return failure(); + } + + size_t insertPos = 0; + for (auto &info : stateInfo) { + if (info.dimIndex >= axis) + ++info.dimIndex; + if (info.dimIndex < axis) + ++insertPos; + } + + dimInfo insertInfo(zeroAttr, defaultShape, axis, true); + stateInfo.insert(stateInfo.begin() + insertPos, insertInfo); + + return success(); +} + +LogicalResult MaskState::parseAdd(arith::AddIOp addOp, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + addOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting add"); + }); + return failure(); + } + + MaskState lhsState; + if (failed(lhsState.parse(addOp.getLhs(), loc, builder))) + return failure(); + + MaskState rhsState; + if (failed(rhsState.parse(addOp.getRhs(), loc, builder))) + return failure(); + + return this->addStates(lhsState, rhsState, loc, builder); +} + +LogicalResult MaskState::parseSub(arith::SubIOp subOp, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + subOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting sub"); + }); + return failure(); + } + + MaskState lhsState; + if (failed(lhsState.parse(subOp.getLhs(), loc, builder))) + return failure(); + + MaskState rhsState; + if (failed(rhsState.parse(subOp.getRhs(), loc, builder))) + return failure(); + + return this->subStates(lhsState, rhsState, loc, builder); +} + +LogicalResult MaskState::addStates(const MaskState &lhsState, + const MaskState &rhsState, Location loc, + OpBuilder &builder) { + if (lhsState.scalar && rhsState.scalar) { + LLVM_DEBUG( + { + InFlightDiagnostic diag = + emitWarning(loc) + << "Unexpected case where both lhs and rhs are scalars"; + }); + return failure(); + } + + if (!lhsState.scalar && !rhsState.scalar) { + LLVM_DEBUG( + { + InFlightDiagnostic diag = + emitWarning(loc) + << "Unsupported scenario where neither lhs nor rhs is a scalar"; + }); + return failure(); + } + + if (lhsState.scalar) + return addStateScalar(rhsState, lhsState.scalar, loc, builder); + else + return addStateScalar(lhsState, rhsState.scalar, loc, builder); +} + +LogicalResult MaskState::addStateScalar(const MaskState &state, + const OpFoldResult scalar, Location loc, + OpBuilder &builder) { + for (auto info : state.stateInfo) { + info.offset = addOpFoldResult(info.offset, scalar, loc, builder, + builder.getIndexType()); + this->stateInfo.emplace_back(info); + } + return success(); +} + +LogicalResult MaskState::subStates(const MaskState &lhsState, + const MaskState &rhsState, Location loc, + OpBuilder &builder) { + // Only (range - scalar) keeps a contiguous increasing index range. + if (!lhsState.scalar && rhsState.scalar) + return subStateScalar(lhsState, rhsState.scalar, loc, builder); + + LLVM_DEBUG( + { + InFlightDiagnostic diag = + emitWarning(loc) + << "Unsupported subi for continuous mask: only (range - scalar) is " + "supported"; + }); + return failure(); +} + +LogicalResult MaskState::subStateScalar(const MaskState &state, + const OpFoldResult scalar, Location loc, + OpBuilder &builder) { + for (auto info : state.stateInfo) { + info.offset = subOpFoldResult(info.offset, scalar, loc, builder); + if (!info.offset) + return failure(); + this->stateInfo.emplace_back(info); + } + return success(); +} + +LogicalResult MaskState::parseBroadcast(triton::BroadcastOp broadcastOp, + const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + broadcastOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting broadcast"); + }); + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Parsing BROADCAST operation: " << broadcastOp << "\n"; + }); + + auto src = broadcastOp.getSrc(); + auto dst = broadcastOp.getResult(); + if (!isa(dst.getType())) { + LLVM_DEBUG({ + broadcastOp.emitError( + "MaskAnalysis: broadcast dst should be a shaped type"); + }); + return failure(); + } + + auto srcShape = cast(src.getType()).getShape(); + auto dstShape = cast(dst.getType()).getShape(); + if (srcShape.size() != dstShape.size()) { + LLVM_DEBUG({ + broadcastOp.emitError( + "MaskAnalysis: broadcast src and dst should have the same rank"); + }); + return failure(); + } + + if (failed(parse(src, loc, builder))) + return failure(); + + LLVM_DEBUG({ + llvm::dbgs() << "Before BROADCAST MaskState: \n"; + this->dump(); + }); + + for (size_t i = 0; i < srcShape.size(); ++i) { + if (srcShape[i] == dstShape[i]) + continue; + else if (srcShape[i] < dstShape[i] && srcShape[i] == 1) { + for (auto &info : stateInfo) { + if (info.dimIndex != i) + continue; + info.shape = builder.getIndexAttr(dstShape[i]); + info.hasBroadCast = true; + } + } else { + LLVM_DEBUG({ + broadcastOp.emitError( + "MaskAnalysis: unexpected dimensions used in broadcast"); + }); + return failure(); + } + } + + LLVM_DEBUG({ + llvm::dbgs() << "After BROADCAST MaskState: \n"; + this->dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + + return success(); +} + +LogicalResult MaskState::parseCmp(arith::CmpIOp cmpOp, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + cmpOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting cmpi"); + }); + return failure(); + } + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Parsing CMP operation: " << cmpOp << "\n"; + }); + + if (isa(cmpOp.getLhs().getType()) && + (cmpOp.getLhs().getDefiningOp() || + cmpOp.getRhs().getDefiningOp())) { + LLVM_DEBUG({ + cmpOp.emitWarning( + "MaskAnalysis: Unsupported nested cmpi scenario for int1"); + }); + return failure(); + } + + MaskState lhsState; + if (failed(lhsState.parse(cmpOp.getLhs(), loc, builder))) + return failure(); + + MaskState rhsState; + if (failed(rhsState.parse(cmpOp.getRhs(), loc, builder))) + return failure(); + + if (lhsState.scalar) { + if (lhsState.stateInfo.empty()) { + lhsState.stateInfo.emplace_back(builder.getIndexAttr(0), + builder.getIndexAttr(1)); + } + for (auto &info : lhsState.stateInfo) { + if (isOne(info.shape)) { + info.offset = lhsState.scalar; + } + } + } + + // lhs must be a Value and rhs must be scalar + if (!rhsState.scalar) { + LLVM_DEBUG( + { cmpOp.emitWarning("MaskAnalysis: Unsupported cmpi scenario"); }); + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "LHS MaskState: \n"; + lhsState.dump(); + llvm::dbgs() << "RHS MaskState: \n"; + rhsState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + + // In the case where the values we are loading are entirely masked off like + // the following: + // + // ---|-------|-----------| + // ^ ^ ^ + // scalar start end + // + // newEnd = min(end, scalar) = scalar + // Now scalar < start, so simply doing dim = newEnd - start is incorrect. + // + // The correct formula is to optionally move `newDim` back to `start` using + // max(newEnd, start). + auto cmpType = cmpOp.getPredicate(); + for (auto &info : lhsState.stateInfo) { + if (info.hasBroadCast) + continue; + if (!info.setType(cmpType)) { + LLVM_DEBUG({ cmpOp.emitWarning("MaskAnalysis: Unsupported cmpi type"); }); + return failure(); + } + info.rhs = rhsState.scalar; + } + this->stateInfo = lhsState.stateInfo; + + LLVM_DEBUG({ + llvm::dbgs() << "After CMP MaskState: \n"; + this->dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return success(); +} + +LogicalResult MaskState::parseRem(arith::RemSIOp remOp, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + remOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting REMSI"); + }); + return failure(); + } + + MaskState lhsState; + if (failed(lhsState.parse(remOp.getLhs(), loc, builder))) + return failure(); + + MaskState rhsState; + if (failed(rhsState.parse(remOp.getRhs(), loc, builder))) + return failure(); + + if (lhsState.scalar || !rhsState.scalar) { + LLVM_DEBUG( + { remOp.emitRemark("MaskAnalysis: Unsupported REMSI scenario"); }); + return failure(); + } + + auto divisorAttr = rhsState.scalar; + + if (!getIntAttr(divisorAttr).has_value()) { + LLVM_DEBUG({ + remOp.emitError("MaskAnalysis: do not support dynamic divisor in REMSI."); + }); + return failure(); + } + + SmallVector newStateInfo; + auto zeroAttr = builder.getIndexAttr(0); + for (auto info : lhsState.stateInfo) { + if (info.hasBroadCast) { + newStateInfo.emplace_back(info); + continue; + } + if (!isMultiple(divisorAttr, info.shape) && + !isMultiple(info.shape, divisorAttr)) { + LLVM_DEBUG({ + remOp.emitError( + "MaskAnalysis: do not support dynamic stride before REMSI."); + }); + return failure(); + } + + auto contiguousSize = + minOpFoldResult(divisorAttr, info.shape, loc, builder); + auto nonContiguousSize = + divOpFoldResult(info.shape, contiguousSize, loc, builder); + + auto staticNonContiguousSize = getIntAttr(nonContiguousSize); + if (!staticNonContiguousSize.has_value()) { + LLVM_DEBUG({ + remOp.emitError( + "MaskAnalysis: do not support dynamic size before REMSI."); + }); + return failure(); + } + + if (staticNonContiguousSize.value() != 0) + newStateInfo.emplace_back(zeroAttr, nonContiguousSize, info.dimIndex, + true); + + auto newOffset = remOpFoldResult(info.offset, divisorAttr, loc, builder); + newStateInfo.emplace_back(newOffset, nonContiguousSize, info.dimIndex); + } + + this->stateInfo = newStateInfo; + + return success(); +} + +LogicalResult MaskState::parseDiv(arith::DivSIOp divOp, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + divOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting DIVSI"); + }); + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Parsing DIV operation: " << divOp << "\n"; + }); + + MaskState lhsState; + if (failed(lhsState.parse(divOp.getLhs(), loc, builder))) + return failure(); + + MaskState rhsState; + if (failed(rhsState.parse(divOp.getRhs(), loc, builder))) + return failure(); + + LLVM_DEBUG({ + llvm::dbgs() << "LHS MaskState: \n"; + lhsState.dump(); + llvm::dbgs() << "RHS MaskState: \n"; + rhsState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + + if (lhsState.scalar || !rhsState.scalar) { + LLVM_DEBUG( + { divOp.emitRemark("MaskAnalysis: Unsupported DIVSI scenario"); }); + return failure(); + } + + auto divisorAttr = rhsState.scalar; + + if (!getIntAttr(divisorAttr).has_value()) { + LLVM_DEBUG({ + divOp.emitError("MaskAnalysis: do not support dynamix divisor in DIVSI."); + }); + return failure(); + } + + SmallVector newStateInfo; + auto zeroAttr = builder.getIndexAttr(0); + for (auto info : lhsState.stateInfo) { + if (info.hasBroadCast) { + newStateInfo.emplace_back(info); + continue; + } + if (!isMultiple(divisorAttr, info.shape) && + !isMultiple(info.shape, divisorAttr)) { + LLVM_DEBUG({ + divOp.emitError( + "MaskAnalysis: do not support dynamix stride before DIVSI."); + }); + return failure(); + } + + auto nonContiguousSize = + minOpFoldResult(divisorAttr, info.shape, loc, builder); + auto contiguousSize = + divOpFoldResult(info.shape, nonContiguousSize, loc, builder); + + auto staticContiguousSize = getIntAttr(contiguousSize); + if (!staticContiguousSize.has_value()) { + LLVM_DEBUG({ + divOp.emitError( + "MaskAnalysis: do not support dynamix size before DIVSI."); + }); + return failure(); + } + + if (staticContiguousSize.value() != 0) { + auto newOffset = divOpFoldResult(info.offset, divisorAttr, loc, builder); + newStateInfo.emplace_back(newOffset, contiguousSize, info.dimIndex); + } + + newStateInfo.emplace_back(zeroAttr, nonContiguousSize, info.dimIndex, true); + } + + this->stateInfo = newStateInfo; + + LLVM_DEBUG({ + llvm::dbgs() << "After DIV MaskState: \n"; + this->dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return success(); +} + +LogicalResult MaskState::parseAnd(arith::AndIOp andOp, const Location loc, + OpBuilder &builder) { + if (!this->isEmpty()) { + LLVM_DEBUG({ + andOp.emitError( + "MaskAnalysis: MaskState should be empty when visiting and"); + }); + return failure(); + } + auto zeroAttr = builder.getIndexAttr(0); + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Parsing AND operation: " << andOp << "\n"; + }); + + if (isa(andOp.getLhs().getType())) { + LLVM_DEBUG({ + andOp.emitWarning("MaskAnalysis: Unsupported andi scenario for int1"); + }); + return failure(); + } + + MaskState lhsState; + if (failed(lhsState.parse(andOp.getLhs(), loc, builder))) + return failure(); + MaskState rhsState; + if (failed(rhsState.parse(andOp.getRhs(), loc, builder))) + return failure(); + + LLVM_DEBUG({ + llvm::dbgs() << "LHS MaskState: \n"; + lhsState.dump(); + llvm::dbgs() << "RHS MaskState: \n"; + rhsState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + + SmallVector newStateInfo; + auto lIt = lhsState.stateInfo.begin(); + auto rIt = rhsState.stateInfo.begin(); + + while (lIt != lhsState.stateInfo.end() && rIt != rhsState.stateInfo.end()) { + if (lIt->dimIndex != rIt->dimIndex) { + auto newInfo = lIt->dimIndex < rIt->dimIndex ? *lIt++ : *rIt++; + newStateInfo.emplace_back(newInfo); + continue; + } + + if (!isMultiple(lIt->shape, rIt->shape) && + !isMultiple(rIt->shape, lIt->shape)) { + LLVM_DEBUG({ + llvm::dbgs() << "LHS MaskState: \n"; + lhsState.dump(); + llvm::dbgs() << "RHS MaskState: \n"; + rhsState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + LLVM_DEBUG({ + andOp.emitError( + "MaskAnalysis: the add operation have incompatible sizes"); + }); + return failure(); + } + + dimInfo newInfo; + newInfo.dimIndex = lIt->dimIndex; + newInfo.shape = minOpFoldResult(lIt->shape, rIt->shape, loc, builder); + if ((isLess(newInfo.shape, lIt->shape) && !lIt->hasBroadCast || + isLess(newInfo.shape, rIt->shape) && !rIt->hasBroadCast)) { + LLVM_DEBUG({ + llvm::dbgs() << "LHS MaskState: \n"; + lhsState.dump(); + llvm::dbgs() << "RHS MaskState: \n"; + rhsState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + LLVM_DEBUG({ + andOp.emitError( + "MaskAnalysis: the add operation have incompatible sizes." + "Valid dimensions are split."); + }); + return failure(); + } + newInfo.currentType = + lIt->hasBroadCast ? rIt->currentType : lIt->currentType; + if (lIt->currentType != dimInfo::CompareType::deafaultType && + rIt->currentType != dimInfo::CompareType::deafaultType && + lIt->currentType != rIt->currentType) { + LLVM_DEBUG({ + andOp.emitError( + "MaskAnalysis: do not suppport different compare mode within" + "the same dimension."); + }); + return failure(); + } + + if (lIt->hasBroadCast) { + newInfo.offset = rIt->offset; + newInfo.rhs = rIt->rhs; + newInfo.hasBroadCast = rIt->hasBroadCast; + } else if (rIt->hasBroadCast) { + newInfo.offset = lIt->offset; + newInfo.rhs = lIt->rhs; + newInfo.hasBroadCast = lIt->hasBroadCast; + } else { + LLVM_DEBUG({ + andOp.emitError("MaskAnalysis: do not suppport " + "and in the same dimension."); + }); + return failure(); + } + + newStateInfo.emplace_back(newInfo); + + if (isEqual(lIt->shape, newInfo.shape)) + ++lIt; + else + lIt->shape = divOpFoldResult(lIt->shape, newInfo.shape, loc, builder); + if (isEqual(rIt->shape, newInfo.shape)) + ++rIt; + else + rIt->shape = divOpFoldResult(rIt->shape, newInfo.shape, loc, builder); + } + + while (rIt != rhsState.stateInfo.end()) { + newStateInfo.push_back(*rIt++); + } + while (lIt != lhsState.stateInfo.end()) { + newStateInfo.push_back(*lIt++); + } + + this->stateInfo = newStateInfo; + + LLVM_DEBUG({ + llvm::dbgs() << "After AND MaskState: \n"; + this->dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return success(); +} + +LogicalResult MaskState::analysisMask(Value operand) { + auto op = operand.getDefiningOp(); + if (!op) { + return failure(); + } + auto loc = op->getLoc(); + OpBuilder builder(op); + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Analyzing mask: " << operand << "\n"; + }); + + if (this->parse(operand, loc, builder).failed() || this->isEmpty()) { + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "Mask analysis result: \n"; + this->dump(); + llvm::dbgs() << "MaskAnalysis: successfully analyzed mask.\n"; + llvm::dbgs() << "----------------------------------------------\n"; + }); + return success(); +} + +Value MaskState::createNewMask(const Location loc, OpBuilder &builder) { + if (isEmpty()) + return nullptr; + + SmallVector shape; + for (auto info : stateInfo) { + auto staticShape = getIntAttr(info.shape); + if (!staticShape.has_value()) { + LLVM_DEBUG( + { + InFlightDiagnostic diag = + emitError(loc) + << "MaskAnalysis: dynamic shape is not supported in mask " + "generation\n"; + }); + return nullptr; + } + shape.emplace_back(staticShape.value()); + } + SmallVector cacheResults; + auto maskShape = RankedTensorType::get(shape, builder.getI1Type()); + + auto createRhsValue = [&](OpFoldResult rhs) -> Value { + if (auto rhsInt = getIntAttr(rhs)) { + auto rhsAttr = + builder.getI32IntegerAttr(static_cast(rhsInt.value())); + return builder.create(loc, rhsAttr).getResult(); + } + Value rhsValue = dyn_cast(rhs); + if (rhsValue.getType().isIndex()) { + rhsValue = builder.create(loc, builder.getI32Type(), + rhsValue); + } + return rhsValue; + }; + for (size_t i = 0; i < stateInfo.size(); ++i) { + auto info = stateInfo[i]; + if (info.hasBroadCast) { + continue; + } + auto indexI32RowType = + RankedTensorType::get(shape[i], builder.getI32Type()); + Value newMask = + builder.create(loc, indexI32RowType, 0, shape[i]); + + Value newOffset = createRhsValue(info.offset); + if (newOffset.getType().isIndex()) { + newOffset = builder.create(loc, builder.getI32Type(), + newOffset); + } + Value splatRhs = + builder.create(loc, indexI32RowType, newOffset); + newMask = builder.create(loc, newMask, splatRhs); + + auto rhsValue = createRhsValue(info.rhs); + auto splatOp = + builder.create(loc, indexI32RowType, rhsValue); + + if (info.currentType == dimInfo::CompareType::deafaultType) { + LLVM_DEBUG({ + InFlightDiagnostic diag = emitError(loc) + << "MaskAnalysis: cannot generate mask when " + "compare type is not set\n"; + }); + return nullptr; + } + auto cmpOp = builder.create(loc, + info.compareTypeIsLess() + ? arith::CmpIPredicate::slt + : arith::CmpIPredicate::sge, + newMask, splatOp.getResult()); + + auto expandValue = cmpOp.getResult(); + for (size_t j = 0; j < stateInfo.size(); ++j) { + if (j == i) + continue; + expandValue = builder.create(loc, expandValue, j); + } + + auto broadcastValue = + builder.create(loc, maskShape, expandValue); + + cacheResults.push_back(broadcastValue); + } + + if (cacheResults.empty()) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) << "MaskAnalysis: cannot generate mask when all " + "dimensions are broadcasted"; + }); + return nullptr; + } + newMask = cacheResults[0]; + for (size_t i = 1; i < cacheResults.size(); ++i) { + newMask = builder.create(loc, newMask, cacheResults[i]); + } + return newMask; +} + +} // namespace TritonToStructured diff --git a/third_party/ascend/lib/TritonToStructured/MemOpConverter.cpp b/third_party/ascend/lib/TritonToStructured/MemOpConverter.cpp new file mode 100644 index 0000000000..ea9a29427c --- /dev/null +++ b/third_party/ascend/lib/TritonToStructured/MemOpConverter.cpp @@ -0,0 +1,797 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToStructured/MemOpConverter.h" + +#include +#include +#include + +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Arith/Utils/Utils.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Passes.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/Dialect/Utils/ReshapeOpsUtils.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Value.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/MathExtras.h" + +#include "llvm/Support/Debug.h" + +#include "TritonToStructured/CannonicalizerConverter.h" +#include "TritonToStructured/MaskAnalysis.h" +#include "TritonToStructured/PtrAnalysis.h" +#include "TritonToStructured/TritonToStructuredPass.h" +#include "Utils/InterleaveOptimization.h" +#include "Utils/Utils.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/Utils/Utils.h" + +#define DEBUG_TYPE "triton-mem-op-converter" + +namespace MemOpConverter { +using namespace mlir; +using namespace triton; +using namespace TritonToStructured; + +namespace { + +struct CatPointerShape { + int64_t lhsSize; + int64_t rhsSize; + + int64_t totalSize() const { return lhsSize + rhsSize; } +}; + +struct SplitValues { + Value lhs; + Value rhs; +}; + +FailureOr getCatPointerShape(triton::CatOp catOp) { + auto lhsType = dyn_cast(catOp.getLhs().getType()); + auto rhsType = dyn_cast(catOp.getRhs().getType()); + auto resultType = dyn_cast(catOp.getType()); + if (!lhsType || !rhsType || !resultType || lhsType.getRank() != 1 || + rhsType.getRank() != 1 || resultType.getRank() != 1 || + !lhsType.hasStaticShape() || !rhsType.hasStaticShape() || + !resultType.hasStaticShape() || + !isa(lhsType.getElementType()) || + lhsType.getElementType() != rhsType.getElementType() || + lhsType.getElementType() != resultType.getElementType()) { + return failure(); + } + + CatPointerShape shape{lhsType.getDimSize(0), rhsType.getDimSize(0)}; + if (shape.lhsSize != shape.rhsSize || + resultType.getDimSize(0) != shape.totalSize()) + return failure(); + return shape; +} + +bool isCatAlignedValue(Value value, int64_t totalSize) { + if (!value) + return true; + auto type = dyn_cast(value.getType()); + return type && type.getRank() == 1 && type.hasStaticShape() && + type.getDimSize(0) == totalSize; +} + +SplitValues splitCatAlignedValue(Value value, const CatPointerShape &shape, + Location loc, PatternRewriter &rewriter) { + if (!value) + return {}; + + auto valueType = cast(value.getType()); + Type elementType = valueType.getElementType(); + // Ascend's downstream tensor layout requires byte-aligned mask slices. + // Convert back to i1 only after selecting the contiguous halves. + if (elementType.isInteger(1)) { + auto byteType = valueType.clone(rewriter.getI8Type()); + Value byteValue = rewriter.create(loc, byteType, value); + auto byteHalfType = + RankedTensorType::get({shape.lhsSize}, rewriter.getI8Type()); + SmallVector offsets{rewriter.getIndexAttr(0)}; + SmallVector sizes{rewriter.getIndexAttr(shape.lhsSize)}; + SmallVector strides{rewriter.getIndexAttr(1)}; + Value lhsByte = rewriter.create( + loc, byteHalfType, byteValue, offsets, sizes, strides); + offsets[0] = rewriter.getIndexAttr(shape.lhsSize); + Value rhsByte = rewriter.create( + loc, byteHalfType, byteValue, offsets, sizes, strides); + auto maskHalfType = + RankedTensorType::get({shape.lhsSize}, rewriter.getI1Type()); + Value lhs = rewriter.create(loc, maskHalfType, lhsByte); + Value rhs = rewriter.create(loc, maskHalfType, rhsByte); + return {lhs, rhs}; + } + auto reshapedType = RankedTensorType::get({2, shape.lhsSize}, elementType); + auto transposedType = RankedTensorType::get({shape.lhsSize, 2}, elementType); + auto halfType = RankedTensorType::get({shape.lhsSize}, elementType); + + Value reshaped = rewriter.create(loc, reshapedType, value); + SmallVector order{1, 0}; + Value transposed = + rewriter.create(loc, transposedType, reshaped, order); + auto split = + rewriter.create(loc, halfType, halfType, transposed); + return {split.getOutLHS(), split.getOutRHS()}; +} + +void copyMissingAttrs(Operation *source, Operation *target) { + for (NamedAttribute attr : source->getAttrs()) { + if (!target->hasAttr(attr.getName())) + target->setAttr(attr.getName(), attr.getValue()); + } +} + +LogicalResult rewriteCatPointerLoad(triton::LoadOp op, triton::CatOp catOp, + PatternRewriter &rewriter) { + FailureOr shape = getCatPointerShape(catOp); + if (failed(shape) || !isCatAlignedValue(op.getMask(), shape->totalSize()) || + !isCatAlignedValue(op.getOther(), shape->totalSize())) { + return failure(); + } + + Location loc = op.getLoc(); + SplitValues masks = splitCatAlignedValue(op.getMask(), *shape, loc, rewriter); + SplitValues others = + splitCatAlignedValue(op.getOther(), *shape, loc, rewriter); + auto lhsLoad = rewriter.create( + loc, catOp.getLhs(), masks.lhs, others.lhs, op.getBoundaryCheck(), + op.getPadding(), op.getCache(), op.getEvict(), op.getIsVolatile()); + auto rhsLoad = rewriter.create( + loc, catOp.getRhs(), masks.rhs, others.rhs, op.getBoundaryCheck(), + op.getPadding(), op.getCache(), op.getEvict(), op.getIsVolatile()); + copyMissingAttrs(op.getOperation(), lhsLoad.getOperation()); + copyMissingAttrs(op.getOperation(), rhsLoad.getOperation()); + + auto result = rewriter.create( + loc, op.getType(), ValueRange{lhsLoad.getResult(), rhsLoad.getResult()}); + copyMissingAttrs(catOp.getOperation(), result.getOperation()); + rewriter.replaceOp(op, result.getResult()); + if (catOp.getResult().use_empty()) + rewriter.eraseOp(catOp); + return success(); +} + +LogicalResult rewriteCatPointerStore(triton::StoreOp op, triton::CatOp catOp, + PatternRewriter &rewriter) { + FailureOr shape = getCatPointerShape(catOp); + if (failed(shape) || !isCatAlignedValue(op.getValue(), shape->totalSize()) || + !isCatAlignedValue(op.getMask(), shape->totalSize())) { + return failure(); + } + + Location loc = op.getLoc(); + SplitValues values = + splitCatAlignedValue(op.getValue(), *shape, loc, rewriter); + SplitValues masks = splitCatAlignedValue(op.getMask(), *shape, loc, rewriter); + auto lhsStore = rewriter.create( + loc, catOp.getLhs(), values.lhs, masks.lhs, op.getBoundaryCheck(), + op.getCache(), op.getEvict()); + auto rhsStore = rewriter.create( + loc, catOp.getRhs(), values.rhs, masks.rhs, op.getBoundaryCheck(), + op.getCache(), op.getEvict()); + copyMissingAttrs(op.getOperation(), lhsStore.getOperation()); + copyMissingAttrs(op.getOperation(), rhsStore.getOperation()); + + rewriter.eraseOp(op); + if (catOp.getResult().use_empty()) + rewriter.eraseOp(catOp); + return success(); +} + +// TTIR requires a store value to have the same shape as its pointer. A DSL +// scalar is consequently represented either by tt.splat of a scalar or by a +// folded dense splat constant. +bool isUniformScalarStoreValue(Value value) { + if (auto splat = value.getDefiningOp()) + return !isa(splat.getSrc().getType()); + + if (auto constant = value.getDefiningOp()) { + if (auto dense = dyn_cast(constant.getValue())) + return dense.isSplat(); + } + return false; +} + +// Return true only for the store form that must be left to the later pointer +// broadcast lowering. A non-singleton zero-stride pointer dimension aliases +// lanes, so deferring is sound only when the DSL supplied one scalar value. +bool shouldDeferScalarPointerBroadcastStore(Value ptr, Value value) { + if (!isUniformScalarStoreValue(value)) + return false; + + auto broadcast = ptr.getDefiningOp(); + if (!broadcast) + return false; + + auto resultType = dyn_cast(broadcast.getType()); + auto sourceType = dyn_cast(broadcast.getSrc().getType()); + if (!resultType || !sourceType || + !isa(resultType.getElementType()) || + resultType.getRank() != sourceType.getRank()) { + return false; + } + + int broadcastedDims = 0; + for (size_t i = 0; i < resultType.getShape().size(); ++i) { + const int64_t srcDim = sourceType.getShape()[i]; + const int64_t resultDim = resultType.getShape()[i]; + if (srcDim == 1 && resultDim != 1) { + ++broadcastedDims; + continue; + } + if (srcDim != resultDim) + return false; + } + if (broadcastedDims != 1) + return false; + + Value source = broadcast.getSrc(); + while (auto addPtr = source.getDefiningOp()) + source = addPtr.getPtr(); + auto splat = source.getDefiningOp(); + return splat && isa(splat.getSrc().getType()); +} + +} // namespace + +LogicalResult LoadConverter::matchAndRewrite(triton::LoadOp op, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto oldPtr = op.getPtr(); + auto oldMask = op.getMask(); + auto oldOther = op.getOther(); + + if (auto catOp = oldPtr.getDefiningOp()) { + if (succeeded(rewriteCatPointerLoad(op, catOp, rewriter))) + return success(); + } + + MemOpTransformer tf(MemOpTransformer::MemType::load, optimizeDynamicOffset); + + auto newPtr = tf.createNewPtr(oldPtr, loc, rewriter); + auto newMask = tf.createNewMask(oldMask, loc, rewriter); + auto newOther = tf.createNewOther(oldOther, loc, rewriter); + + if (!tf.ptrState.shouldLinearize) { + // no need to rewrite + return failure(); + } + + if (!newPtr) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) << "PtrAnalysis: failed to analyze load pointer."; + }); + return failure(); + } + + if (!enableMaskFallbackConversion && oldMask && !newMask) { + LLVM_DEBUG({ + InFlightDiagnostic diag = emitWarning(loc) + << "MaskAnalysis: failed to analyze load mask."; + }); + return failure(); + } + + auto loadOp = rewriter.create(loc, newPtr, newMask, newOther, + op.getCache(), op.getEvict(), + op.getIsVolatile()); + + // insert implicit ops + auto broadCastResult = + tf.materializeImplicitBroadcast(loadOp.getResult(), loc, rewriter); + auto permuteResult = + tf.materializeImplicitPermute(broadCastResult, loc, rewriter); + auto reshapeResult = + tf.materializeImplicitReshape(permuteResult, loc, rewriter); + auto selectResult = tf.materializeImplicitSelect(reshapeResult, oldMask, + oldOther, loc, rewriter); + + rewriter.replaceOp(op, selectResult); + return success(); +} + +LogicalResult StoreConverter::matchAndRewrite(triton::StoreOp op, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + auto oldPtr = op.getPtr(); + auto oldMask = op.getMask(); + auto oldValue = op.getValue(); + + if (auto catOp = oldPtr.getDefiningOp()) { + if (succeeded(rewriteCatPointerStore(op, catOp, rewriter))) + return success(); + } + + // AddPtrSplatConverter can preserve a non-singleton zero-stride dimension + // as a pointer broadcast. Rebuilding the scalar store here drops that + // dimension from the pointer and mask while the store value stays full-rank. + if (shouldDeferScalarPointerBroadcastStore(oldPtr, oldValue)) { + return rewriter.notifyMatchFailure( + op, "defer scalar pointer broadcast store to TritonToLinalg"); + } + + MemOpTransformer tf(MemOpTransformer::MemType::store, optimizeDynamicOffset); + + auto newPtr = tf.createNewPtr(oldPtr, loc, rewriter); + auto newMask = tf.createNewMask(oldMask, loc, rewriter); + + if (!tf.ptrState.shouldLinearize) { + // no need to rewrite + return failure(); + } + + if (!newPtr) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) << "PtrAnalysis: failed to analyze store pointer."; + }); + return failure(); + } + + if (!enableMaskFallbackConversion && oldMask && !newMask) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) << "MaskAnalysis: failed to analyze store mask."; + }); + return failure(); + } + + // insert sync_block_lock + auto lockVar = createSyncBlockLockVar(rewriter, loc); + if (oldMask && !newMask) { + rewriter.create(loc, lockVar); + } + + auto selectResult = + tf.materializeImplicitSelect(oldValue, oldMask, oldPtr, loc, rewriter); + auto reshapeResult = + tf.materializeImplicitReshape(selectResult, loc, rewriter); + auto permuteResult = + tf.materializeImplicitPermute(reshapeResult, loc, rewriter); + + auto storeOp = rewriter.create( + loc, newPtr, permuteResult, newMask, op.getBoundaryCheck(), op.getCache(), + op.getEvict()); + + // insert sync_block_unlock + if (oldMask && !newMask) { + rewriter.create(loc, lockVar); + } + rewriter.eraseOp(op); + return success(); +} + +Value MemOpTransformer::materializeImplicitBroadcast( + Value srcTensor, const Location loc, PatternRewriter &rewriter) { + SmallVector broadCastIndex; + SmallVector broadCastShape; + for (auto [i, info] : llvm::enumerate(ptrState.stateInfo)) { + if (isZero(info.stride)) { + broadCastIndex.emplace_back(i); + } + auto staticShape = getIntAttr(info.shape); + if (!staticShape.has_value()) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) + << "PtrAnalysis: dynamic shape is not supported in broadcast\n"; + }); + return srcTensor; + } + broadCastShape.emplace_back(staticShape.value()); + } + + if (broadCastIndex.empty()) + return srcTensor; + + // when load is a scalar, we need to use splat to broadcast + auto srcType = srcTensor.getType(); + if (srcType.isIntOrFloat()) { + auto broadCastType = RankedTensorType::get(broadCastShape, srcType); + auto splatOp = + rewriter.create(loc, broadCastType, srcTensor); + return splatOp.getResult(); + } + + auto init = rewriter.create( + loc, broadCastShape, + cast(srcTensor.getType()).getElementType()); + + auto broadCastOp = rewriter.create(loc, srcTensor, init, + broadCastIndex); + + return broadCastOp->getResult(0); +} + +Value MemOpTransformer::materializeImplicitReshape(Value srcTensor, + const Location loc, + PatternRewriter &rewriter) { + if (ptrState.sizes.size() == ptrState.stateInfo.size()) + return srcTensor; + SmallVector targetShape; + if (currentType == MemType::load) { + for (auto size : ptrState.sizes) { + auto staticShape = getIntAttr(size); + if (!staticShape.has_value()) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) + << "PtrAnalysis: dynamic shape is not supported in reshape\n"; + }); + return srcTensor; + } + targetShape.emplace_back(staticShape.value()); + } + } else { + for (auto info : ptrState.stateInfo) { + auto staticShape = getIntAttr(info.shape); + if (!staticShape.has_value()) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) + << "PtrAnalysis: dynamic shape is not supported in reshape\n"; + }); + return srcTensor; + } + targetShape.emplace_back(staticShape.value()); + } + } + + auto targetShapeAttr = DenseIntElementsAttr::get( + RankedTensorType::get({static_cast(targetShape.size())}, + rewriter.getI64Type()), + targetShape); + auto targetShapeType = RankedTensorType::get( + targetShape, cast(srcTensor.getType()).getElementType()); + auto targetShapeValue = + rewriter.create(loc, targetShapeAttr); + auto reshapeOp = rewriter.create( + loc, targetShapeType, srcTensor, targetShapeValue); + return reshapeOp.getResult(); +} + +Value MemOpTransformer::materializeImplicitSelect(Value srcTensor, Value mask, + Value other, + const Location loc, + PatternRewriter &rewriter) { + if (!mask || maskState.newMask) + return srcTensor; + auto TensorType = cast(srcTensor.getType()); + if (cast(mask.getType()).getShape() != TensorType.getShape()) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) << "MaskAnalysis: mask shape is not same as Value"; + }); + return srcTensor; + } + + if (currentType == MemType::store) { + auto loadOp = rewriter.create(loc, other, nullptr, nullptr, + ArrayRef(), nullptr); + other = loadOp.getResult(); + } + + if (!other) { + auto elementType = TensorType.getElementType(); + auto emptyOp = rewriter.create(loc, TensorType.getShape(), + elementType); + other = emptyOp.getResult(); + } + auto selectOp = rewriter.create(loc, mask, srcTensor, other); + return selectOp->getResult(0); +} + +Value MemOpTransformer::materializeImplicitPermute(Value srcTensor, + const Location loc, + PatternRewriter &rewriter) { + auto inTy = dyn_cast(srcTensor.getType()); + if (!inTy || !ptrState.isPermuted) + return srcTensor; + + auto inShape = inTy.getShape(); + SmallVector order(ptrState.permuteIds.size()); + for (size_t i = 0; i < ptrState.permuteIds.size(); ++i) { + if (currentType == MemType::load) { + order[ptrState.permuteIds[i]] = i; + } else { + order[i] = ptrState.permuteIds[i]; + } + } + SmallVector outShape(order.size()); + if (inShape.size() != outShape.size()) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) << "PtrAnalysis: incompatible shape for permute"; + }); + return srcTensor; + } + + for (size_t i = 0; i < outShape.size(); ++i) { + outShape[i] = inShape[order[i]]; + } + + auto outTy = RankedTensorType::get(outShape, inTy.getElementType()); + auto transOp = rewriter.create(loc, outTy, srcTensor, order); + return transOp.getResult(); +} + +Value MemOpTransformer::createNewPtr(Value oldPtr, const Location loc, + PatternRewriter &rewriter) { + TritonToStructured::PtrAnalysis ptrAnalysis(optimizeDynamicOffset); + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "PtrAnalysis: analyzing load/store's ptr.\n"; + }); + + if (ptrAnalysis.visitOperand(oldPtr, ptrState, loc, rewriter).failed()) { + ptrState.shouldLinearize = false; + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) << "PtranAlysis: failed to analyze load/store ptr."; + }); + return oldPtr; + } + + // compute missing strides + // if stateinfo.shape is 1 and sizes[dimIndex] is 1, + // then the stride is the accumulated size of all dimensions on the right side + // ie. for shape [1, 128], sizes [1, 128], originally stride is [0, 1], + // after normalization, stride is [128, 1] + OpFoldResult maxStride = rewriter.getIndexAttr(1); + for (auto it = ptrState.stateInfo.rbegin(); it != ptrState.stateInfo.rend(); + ++it) { + if (TritonToStructured::isOne(it->shape) && isZero(it->stride)) { + it->stride = maxStride; + } + maxStride = maxOpFoldResult(maxStride, it->stride, loc, rewriter); + } + + for (auto it = ptrState.stateInfo.rbegin(); it != ptrState.stateInfo.rend(); + ++it) { + if (isZero(it->stride)) { + ptrState.shouldLinearize = true; + } + } + + ptrState.generateOriginPermuteIds(); + + return ptrState.createAddPtrOp(rewriter, loc); +} + +Value MemOpTransformer::createNewMask(Value oldMask, const Location loc, + PatternRewriter &rewriter) { + if (!oldMask) + return nullptr; + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskAnalysis: analyzing load/store mask.\n"; + }); + + if (!oldMask || maskState.analysisMask(oldMask).failed()) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskAnalysis: no mask or failed to analyze mask.\n"; + llvm::dbgs() << "oldMask:" << oldMask << "\n"; + maskState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + LLVM_DEBUG( + { + InFlightDiagnostic diag = + emitWarning(loc) + << "MaskAnalysis: failed to analyze load/store mask."; + }); + return nullptr; + } + + SmallVector newMaskInfo; + auto itPtr = ptrState.stateInfo.begin(); + auto itMask = maskState.stateInfo.begin(); + + // match and create new mask info + while (itPtr != ptrState.stateInfo.end() && + itMask != maskState.stateInfo.end()) { + // ptr'shape must be multiple of mask'shape or vice versa + if (!isMultiple(itMask->shape, itPtr->shape)) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) + << "MaskAnalysis: incompatible shapes between ptr and mask."; + llvm::dbgs() << "----------------------------------------------\n"; + ptrState.dump(); + llvm::dbgs() << "oldMask:" << oldMask << "\n"; + maskState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return nullptr; + } + + auto newShape = minOpFoldResult(itMask->shape, itPtr->shape, loc, rewriter); + if (isLess(newShape, itMask->shape) && !itMask->hasBroadCast) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) + << "MaskAnalysis: the mask shape is incompatible with ptr shape."; + }); + return nullptr; + } + + TritonToStructured::dimInfo newInfo(itMask->offset, newShape, + itMask->dimIndex, itMask->hasBroadCast, + itMask->currentType, itMask->rhs); + + if (!isZero(itPtr->stride)) { + newMaskInfo.emplace_back(newInfo); + } + + ++itPtr; + if (isEqual(itMask->shape, newShape)) { + ++itMask; + } + } + + if (itPtr != ptrState.stateInfo.end() || + itMask != maskState.stateInfo.end()) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskAnalysis: failed to apply permute on mask.\n"; + ptrState.dump(); + llvm::dbgs() << "oldMask:" << oldMask << "\n"; + maskState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + LLVM_DEBUG({ + InFlightDiagnostic diag = emitWarning(loc) + << "MaskAnalysis: incompatible number of " + "dimensions between ptr and mask."; + }); + return nullptr; + } + + maskState.stateInfo = newMaskInfo; + + if (ptrState.isPermuted && !applyPermuteOnMask()) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "MaskAnalysis: failed to apply permute on mask.\n"; + ptrState.dump(); + llvm::dbgs() << "oldMask:" << oldMask << "\n"; + maskState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + InFlightDiagnostic diag = + emitWarning(loc) << "MaskAnalysis: failed to apply permute on mask."; + }); + return nullptr; + } + + LLVM_DEBUG({ + llvm::dbgs() << "After matching MaskState: \n"; + for (auto info : newMaskInfo) { + info.dump(); + } + llvm::dbgs() << "----------------------------------------------\n"; + }); + + auto newMask = maskState.createNewMask(loc, rewriter); + return newMask; +} + +Value MemOpTransformer::createNewOther(Value oldOther, const Location loc, + PatternRewriter &rewriter) { + if (!oldOther || !maskState.newMask) + return nullptr; + + auto ptrType = dyn_cast(ptrState.source.getType()); + if (!ptrType) { + LLVM_DEBUG( + { + InFlightDiagnostic diag = + emitWarning(loc) + << "PtrAnalysis: source of ptrState is not a pointer type."; + }); + return nullptr; + } + Type elementType = ptrType.getPointeeType(); + + SmallVector targetShape; + for (auto info : maskState.stateInfo) { + auto staticShape = getIntAttr(info.shape); + if (!staticShape.has_value()) { + LLVM_DEBUG({ + InFlightDiagnostic diag = + emitWarning(loc) + << "MaskAnalysis: dynamic shape is not supported in reshape\n"; + }); + return oldOther; + } + targetShape.emplace_back(staticShape.value()); + } + auto targetShapeAttr = DenseIntElementsAttr::get( + RankedTensorType::get({static_cast(targetShape.size())}, + rewriter.getI64Type()), + targetShape); + auto targetShapeType = RankedTensorType::get(targetShape, elementType); + auto targetShapeValue = + rewriter.create(loc, targetShapeAttr); + + auto reshapeOp = rewriter.create( + loc, targetShapeType, oldOther, targetShapeValue); + + return reshapeOp.getResult(); +} + +bool MemOpTransformer::applyPermuteOnMask() { + if (!ptrState.isPermuted || maskState.isEmpty()) { + return true; + } + if (ptrState.permuteIds.size() != maskState.stateInfo.size()) { + return false; + } + SmallVector newMaskInfo; + for (auto id : ptrState.permuteIds) { + newMaskInfo.push_back(maskState.stateInfo[id]); + } + maskState.stateInfo = newMaskInfo; + return true; +} + +hivm::CreateSyncBlockLockOp createSyncBlockLockVar(OpBuilder &builder, + Location loc) { + SmallVector shape = {1}; + auto elementType = builder.getI64Type(); + Type memrefType = MemRefType::get(shape, elementType); + + auto createSyncBlockLockOp = + builder.create(loc, memrefType, Value()); + return createSyncBlockLockOp; +} +} // namespace MemOpConverter diff --git a/third_party/ascend/lib/TritonToStructured/PtrAnalysis.cpp b/third_party/ascend/lib/TritonToStructured/PtrAnalysis.cpp new file mode 100644 index 0000000000..ed5acbf6fc --- /dev/null +++ b/third_party/ascend/lib/TritonToStructured/PtrAnalysis.cpp @@ -0,0 +1,1580 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToStructured/PtrAnalysis.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Value.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/IR/Visitors.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" + +#include "mlir/IR/IRMapping.h" +#include "mlir/Transforms/DialectConversion.h" + +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/LogicalResult.h" + +#include "Utils/Utils.h" + +#define DEBUG_TYPE "triton-to-structured-ptr-analysis" + +namespace TritonToStructured { +using namespace mlir; +using namespace triton; + +bool isMultiple(const OpFoldResult ÷nd, const OpFoldResult &divisor) { + auto staticDividend = getIntAttr(dividend); + auto staticDivisor = getIntAttr(divisor); + if (!staticDividend || !staticDivisor || staticDivisor.value() == 0) { + return false; + } + return staticDividend.value() % staticDivisor.value() == 0; +} + +bool isEqual(const OpFoldResult &ofr1, const OpFoldResult &ofr2) { + auto staticOfr1 = getIntAttr(ofr1); + auto staticOfr2 = getIntAttr(ofr2); + return staticOfr1 == staticOfr2; +} + +bool isLess(const OpFoldResult &ofr1, const OpFoldResult &ofr2) { + auto staticOfr1 = getIntAttr(ofr1); + auto staticOfr2 = getIntAttr(ofr2); + // When sorting for permute, the value determined at runtime + // is greater than the value determined at compile time. + if (!staticOfr1 && !staticOfr2) { + return false; // keep relative order (stable_sort) + } + if (!staticOfr1 && staticOfr2) { + return false; // dynamic > static + } + if (staticOfr1 && !staticOfr2) { + return true; // static < dynamic + } + return staticOfr1.value() < staticOfr2.value(); +} + +bool isGreater(const OpFoldResult &ofr1, const OpFoldResult &ofr2) { + auto staticOfr1 = getIntAttr(ofr1); + auto staticOfr2 = getIntAttr(ofr2); + // When sorting for permute, the value determined at runtime + // is greater than the value determined at compile time. + if (!staticOfr1 && !staticOfr2) { + return false; // keep relative order (stable_sort) + } + if (!staticOfr1 && staticOfr2) { + return true; // dynamic > static + } + if (staticOfr1 && !staticOfr2) { + return false; // static < dynamic + } + return staticOfr1.value() > staticOfr2.value(); +} + +void StateInfo::dump() const { + llvm::dbgs() << "StateInfo: \n"; + llvm::dbgs() << "dimIndex = " << dimIndex << "\n"; + llvm::dbgs() << "shape = " << shape << "\n"; + llvm::dbgs() << "stride = " << stride << "\n"; +} + +void PtrState::dump() const { + llvm::dbgs() << "PtrState: \n"; + llvm::dbgs() << "source:" << source << "\n"; + llvm::dbgs() << "scalar:" << offset << "\n"; + llvm::dbgs() << "size: ["; + for (auto size : sizes) + llvm::dbgs() << size << ", "; + llvm::dbgs() << "]\n"; + llvm::dbgs() << "shouldLinearize: " << shouldLinearize << "\n"; + llvm::dbgs() << "isPermuted: " << isPermuted << "\n"; + llvm::dbgs() << "isBlockPtr: " << isBlockPtr() << "\n"; + + llvm::dbgs() << "permuteIds: ["; + for (auto id : permuteIds) + llvm::dbgs() << id << ", "; + llvm::dbgs() << "]\n"; + llvm::dbgs() << "order: ["; + for (auto id : order) + llvm::dbgs() << id << ", "; + llvm::dbgs() << "]\n"; + + llvm::dbgs() << "stateInfo:\n"; + llvm::dbgs() << "\n"; + for (auto info : stateInfo) { + llvm::dbgs() << "-----------------------------------------\n"; + info.dump(); + llvm::dbgs() << "-----------------------------------------\n"; + } +} + +bool PtrState::isEmpty() const { + return (stateInfo.empty() && !source && !offset); +} + +bool PtrState::isScalar() const { + bool scalar = true; + for (auto info : stateInfo) { + auto staticStride = getIntAttr(info.stride); + if (!staticStride.has_value() || staticStride.value() != 0) + scalar = false; + } + return scalar && (offset || source); +} + +bool PtrState::hasSource() const { return source != nullptr; } + +bool PtrState::isBlockPtr() const { return !order.empty(); } + +bool PtrState::isSameSizeAs(const PtrState &x) const { + if (this->sizes.size() != x.sizes.size()) + return false; + + for (size_t i = 0; i < this->sizes.size(); ++i) { + if (this->sizes[i] != x.sizes[i]) + return false; + } + return true; +} + +void PtrState::updatePtrState(SmallVector stateInfo, + SmallVector sizes, Value source, + OpFoldResult offset, const Location loc, + OpBuilder &builder, bool shouldLinearize) { + this->stateInfo = stateInfo; + this->sizes = sizes; + this->source = source; + this->offset = offset; + this->shouldLinearize = shouldLinearize; + this->normalizeState(loc, builder); +} + +void PtrState::normalizeState(const Location loc, OpBuilder &builder) { + SmallVector newStateInfo; + auto zeroAttr = builder.getIndexAttr(0); + + // merge continuous zero strides + // e.g., stride [0, 0, 1] shape [4, 32, 16] --> stride [0, 1] shape [128, 16] + for (auto it = this->stateInfo.begin(); it != this->stateInfo.end(); ++it) { + while (it != this->stateInfo.end() && isZero(it->stride)) { + auto newShape = it->shape; + auto dimIndex = it->dimIndex; + for (++it; it != this->stateInfo.end() && isZero(it->stride) && + it->dimIndex == dimIndex; + ++it) { + newShape = mulOpFoldResult(newShape, it->shape, loc, builder, + builder.getIndexType()); + } + newStateInfo.emplace_back(zeroAttr, newShape, dimIndex); + } + if (it == this->stateInfo.end()) + break; + // if the info is the only one with oriSize 1 in this dimension, skip it + // e.g., stride [0, 1] shape [1, 128] sizes [1, 128] do not delete the first + // info + if (isOne(it->shape) && !isOne(sizes[it->dimIndex])) + continue; + newStateInfo.emplace_back(*it); + } + + this->stateInfo = newStateInfo; +} + +LogicalResult PtrAnalysis::visitOperandAddptr(triton::AddPtrOp addptrOp, + PtrState &state, + const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + addptrOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting addptr"); + }); + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Visit addptr operation: " << addptrOp << "\n"; + }); + + PtrState ptrState; + if (visitOperand(addptrOp.getPtr(), ptrState, addptrOp.getLoc(), builder) + .failed()) { + return failure(); + } + + PtrState offsetState; + if (visitOperand(addptrOp.getOffset(), offsetState, addptrOp.getLoc(), + builder) + .failed()) { + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Before visiting addptr operands: \n"; + llvm::dbgs() << "PtrState: \n"; + ptrState.dump(); + llvm::dbgs() << "OffsetState: \n"; + offsetState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + + if (!ptrState.source) { + LLVM_DEBUG({ + addptrOp.emitError("ptr field should provide source / base pointer"); + }); + return failure(); + } + return state.addState(ptrState, offsetState, addptrOp, builder); +} + +LogicalResult +PtrAnalysis::visitOperandMakeTensorPtr(triton::MakeTensorPtrOp makeTPtrOp, + PtrState &state, const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + makeTPtrOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting make_tensor_ptr"); + return failure(); + } + if (makeTPtrOp.getOrder().empty()) { + LLVM_DEBUG(makeTPtrOp->emitRemark( + "PtrAnalysis: expect tt.make_tensor_ptr to have order field set")); + return failure(); + } + + // Build: + // - stateInfo: per-dimension (stride, shape, dimIndex) of the parent tensor + // - sizes: original tensor shape of the block + // - dimOffsets: the offset to the block in the parent tensor + state.source = makeTPtrOp.getBase(); + state.dimOffsets = makeTPtrOp.getOffsets(); + state.order = SmallVector(makeTPtrOp.getOrder()); + + auto resType = cast(makeTPtrOp.getResult().getType()); + auto pointeeType = cast(resType.getPointeeType()); + auto pointeeShape = pointeeType.getShape(); + const int64_t rank = pointeeType.getRank(); + + SmallVector newStateInfo; + for (int64_t i = 0; i < rank; i++) { + state.sizes.push_back(builder.getIndexAttr(pointeeShape[i])); + newStateInfo.emplace_back(makeTPtrOp.getStrides()[i], + makeTPtrOp.getShape()[i], i); + } + state.stateInfo = newStateInfo; + + assert(state.isBlockPtr() && + "tt.make_tensor_ptr pointer state should describe a block pointer"); + + return success(); +} + +bool PtrAnalysis::operandIsScalar(Value operand) { + auto tensorType = dyn_cast(operand.getType()); + auto elementType = + tensorType ? tensorType.getElementType() : operand.getType(); + bool isScalar = true; + if (tensorType) { + for (size_t i = 0; i < tensorType.getRank() && isScalar; ++i) { + isScalar = tensorType.getDimSize(i) == 1; + } + } + return isScalar && + (isa(elementType) || isa(elementType)); +} + +LogicalResult PtrAnalysis::initStateByScalar(Value operand, PtrState &state, + const Location loc, + OpBuilder &builder) { + OpFoldResult newOffset; + SmallVector newSizes; + SmallVector newStateInfo; + if (isa(operand.getType())) { + OpBuilder::InsertionGuard guard(builder); + if (!isa(operand) && operand.getDefiningOp()) { + builder.setInsertionPointAfter(operand.getDefiningOp()); + } + auto castOp = builder.create( + loc, builder.getIndexType(), operand); + newOffset = castOp.getResult(); + } else if (isa(operand.getType())) { + newOffset = operand; + } else { + auto tensorType = dyn_cast(operand.getType()); + auto index = builder.create(loc, 0); + auto zeroAttr = builder.getIndexAttr(0); + auto oneAttr = builder.getIndexAttr(1); + SmallVector indices; + for (size_t i = 0; i < tensorType.getRank(); ++i) { + indices.push_back(index); + newSizes.emplace_back(oneAttr); + newStateInfo.emplace_back(zeroAttr, oneAttr, i); + } + auto extractedElement = + builder.create(loc, operand, indices); + newOffset = extractedElement.getResult(); + } + state.updatePtrState(newStateInfo, newSizes, nullptr, newOffset, loc, + builder); + return success(); +} + +LogicalResult PtrAnalysis::initStateByPointer(Value operand, PtrState &state, + const Location loc, + OpBuilder &builder) { + Value newSource; + SmallVector newSizes; + SmallVector newStateInfo; + + if (auto op = operand.getDefiningOp()) { + if (auto addPtrOp = dyn_cast(op)) { + return visitOperandAddptr(cast(op), state, loc, + builder); + } else if (auto bitCastOp = dyn_cast(op)) { + newSource = operand; + } else if (auto makeTensorOp = dyn_cast(op)) { + LLVM_DEBUG({ + op->emitWarning("Unexpected operand defining operation tts.make_tptr."); + }); + return failure(); + } else if (auto intToPtrOp = dyn_cast(op)) { + newSource = operand; + } else { + LLVM_DEBUG({ op->emitWarning("PtrAnalysis: Unexpected operand."); }); + return failure(); + } + } else { + newSource = operand; + } + auto newOffset = builder.getIndexAttr(0); + state.updatePtrState(newStateInfo, newSizes, newSource, newOffset, loc, + builder); + return success(); +} + +LogicalResult PtrState::mulState(const PtrState &lhsState, + const PtrState &rhsState, Operation *op, + OpBuilder &builder) { + auto loc = op->getLoc(); + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "mulState: " << op << "\n"; + }); + + if (!isEmpty()) { + LLVM_DEBUG({ + op->emitError("PtrAnalysis: PtrState should be empty when multiplying"); + }); + return failure(); + } + + // neither lhs nor rhs should have source, since multiplying base pointer + // does not make sense + if (lhsState.hasSource() || rhsState.hasSource()) { + LLVM_DEBUG({ + op->emitError("PtrAnalysis: do not support base inters in multiplying"); + }); + return failure(); + } else if (!lhsState.isScalar() && !rhsState.isScalar()) { + // do not support both tensors are effectively non-scalar + LLVM_DEBUG({ + op->emitError( + "PtrAnalysis: only support multiplying pointer states when one of " + "them represent a scalar"); + }); + return failure(); + } + + PtrState const *lhs = &lhsState; + PtrState const *rhs = &rhsState; + + if (!rhs->isScalar() && lhs->isScalar()) { + std::swap(lhs, rhs); + } + + SmallVector newStateInfo; + for (auto info : lhs->stateInfo) { + OpFoldResult newStride = mulOpFoldResult(info.stride, rhs->offset, loc, + builder, builder.getIndexType()); + newStateInfo.emplace_back(newStride, info.shape, info.dimIndex); + } + + auto newOffset = mulOpFoldResult(lhsState.offset, rhsState.offset, loc, + builder, builder.getIndexType()); + updatePtrState(newStateInfo, lhs->sizes, lhs->source, newOffset, loc, builder, + lhs->shouldLinearize); + + LLVM_DEBUG({ + llvm::dbgs() << "After mulState: \n"; + this->dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + + return success(); +} + +LogicalResult PtrState::subState(const PtrState &lhsState, + const PtrState &rhsState, Operation *op, + OpBuilder &builder) { + auto loc = op->getLoc(); + if (!isEmpty()) { + LLVM_DEBUG({ + op->emitError("PtrAnalysis: PtrState should be empty when subtracting"); + }); + return failure(); + } + + if (lhsState.hasSource() && rhsState.hasSource()) { + LLVM_DEBUG({ + op->emitError( + "PtrAnalysis: do not support both sides have base pointers in sub"); + }); + return failure(); + } + + if (!rhsState.isScalar()) { + LLVM_DEBUG({ + op->emitError("PtrAnalysis: only support sub when one of " + "them represents a scalar"); + }); + return failure(); + } + + auto newOffset = + subOpFoldResult(lhsState.offset, rhsState.offset, loc, builder); + updatePtrState(lhsState.stateInfo, lhsState.sizes, lhsState.source, newOffset, + loc, builder, lhsState.shouldLinearize); + + return success(); +} + +LogicalResult PtrState::addState(PtrState &lhsState, PtrState &rhsState, + Operation *op, OpBuilder &builder) { + auto loc = op->getLoc(); + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "addState: " << op << "\n"; + }); + + if (!isEmpty()) { + LLVM_DEBUG({ + op->emitError("PtrAnalysis: PtrState should be empty when adding"); + }); + return failure(); + } + if (!lhsState.isSameSizeAs(rhsState)) { + LLVM_DEBUG({ + op->emitError( + "PtrAnalysis: The original size of the addition should be the same"); + }); + return failure(); + } + + SmallVector newStateInfo; + auto lIt = lhsState.stateInfo.begin(); + auto rIt = rhsState.stateInfo.begin(); + while (lIt != lhsState.stateInfo.end() && rIt != rhsState.stateInfo.end()) { + if (lIt->dimIndex != rIt->dimIndex) { + auto newInfo = lIt->dimIndex < rIt->dimIndex ? *lIt++ : *rIt++; + newStateInfo.emplace_back(newInfo); + continue; + } + if (!isMultiple(lIt->shape, rIt->shape) && + !isMultiple(rIt->shape, lIt->shape)) { + LLVM_DEBUG({ + llvm::dbgs() << "LHS PtrState: \n"; + lhsState.dump(); + llvm::dbgs() << "RHS PtrState: \n"; + rhsState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + LLVM_DEBUG({ + op->emitError("PtrAnalysis: the add operation have incompatible sizes"); + }); + return failure(); + } + + auto newShape = minOpFoldResult(lIt->shape, rIt->shape, loc, builder); + if ((isLess(newShape, lIt->shape) && !isZero(lIt->stride) || + isLess(newShape, rIt->shape) && !isZero(rIt->stride))) { + LLVM_DEBUG({ + llvm::dbgs() << "LHS PtrState: \n"; + lhsState.dump(); + llvm::dbgs() << "RHS PtrState: \n"; + rhsState.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + LLVM_DEBUG({ + op->emitError("PtrAnalysis: the add operation have incompatible sizes." + "Valid dimensions are split."); + }); + return failure(); + } + + auto newStride = addOpFoldResult(lIt->stride, rIt->stride, loc, builder, + builder.getIndexType()); + newStateInfo.emplace_back(newStride, newShape, lIt->dimIndex); + + if (isEqual(lIt->shape, newShape)) + ++lIt; + else + lIt->shape = divOpFoldResult(lIt->shape, newShape, loc, builder); + if (isEqual(rIt->shape, newShape)) + ++rIt; + else + rIt->shape = divOpFoldResult(rIt->shape, newShape, loc, builder); + } + + while (rIt != rhsState.stateInfo.end()) { + newStateInfo.push_back(*rIt++); + } + while (lIt != lhsState.stateInfo.end()) { + newStateInfo.push_back(*lIt++); + } + + auto newSource = source = lhsState.source ? lhsState.source : rhsState.source; + auto newOffset = addOpFoldResult(lhsState.offset, rhsState.offset, loc, + builder, builder.getIndexType()); + auto newShouldLinearize = + lhsState.shouldLinearize || rhsState.shouldLinearize; + auto newSizes = lhsState.sizes; + + updatePtrState(newStateInfo, newSizes, newSource, newOffset, loc, builder, + newShouldLinearize); + + LLVM_DEBUG({ + llvm::dbgs() << "After addState: \n"; + this->dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + + return success(); +} + +triton::AddPtrOp PtrState::createAddPtrOp(OpBuilder &builder, Location loc) { + SmallVector tensorSizes; + SmallVector tensorStrides; + + auto zeroAttr = builder.getIndexAttr(0); + auto oneAttr = builder.getIndexAttr(1); + + for (auto id : permuteIds) { + auto info = stateInfo[id]; + if (isZero(info.stride)) + continue; + tensorStrides.emplace_back(info.stride); + tensorSizes.emplace_back(getIntAttr(info.shape).value()); + } + + // load a scalar pointer + if (tensorSizes.empty()) { + Value offsetValue = materializeValue(builder, loc, offset); + if (offsetValue.getType().isIndex()) { + offsetValue = builder.create( + loc, builder.getI32Type(), offsetValue); + } + auto addptrOp = builder.create(loc, source.getType(), + source, offsetValue); + return addptrOp; + } + + SmallVector cachedRange; + auto ptrType = cast(source.getType()); + auto ptrTensorType = RankedTensorType::get({tensorSizes}, ptrType); + auto broadCastType = + RankedTensorType::get({tensorSizes}, builder.getI32Type()); + + if (tensorSizes.size() != tensorStrides.size()) { + LLVM_DEBUG( + { + InFlightDiagnostic diag = + emitError(loc) + << "PtrAnalysis: inconsistent tensor sizes and strides"; + }); + return nullptr; + } + for (size_t i = 0; i < tensorSizes.size(); ++i) { + // make range + auto indexI32RowType = + RankedTensorType::get({tensorSizes[i]}, builder.getI32Type()); + Value makeRangeOp = builder.create( + loc, indexI32RowType, 0, tensorSizes[i]); + + // multiply stride + Value strideValue = materializeValue(builder, loc, tensorStrides[i]); + if (strideValue.getType().isIndex()) { + strideValue = builder.create( + loc, builder.getI32Type(), strideValue); + } + Value splatStride = + builder.create(loc, indexI32RowType, strideValue); + auto rangeAfterMul = + builder.create(loc, makeRangeOp, splatStride); + + // reshape + Value expandedValue = rangeAfterMul; + for (size_t j = 0; j < tensorSizes.size(); ++j) { + if (j == i) + continue; + expandedValue = + builder.create(loc, expandedValue, j); + } + + // broadcast + auto broadcastValue = + builder.create(loc, broadCastType, expandedValue); + cachedRange.push_back(broadcastValue); + } + + // combine the cachedRange + Value rangeAfterCombine = cachedRange[0]; + for (size_t i = 1; i < cachedRange.size(); ++i) { + rangeAfterCombine = + builder.create(loc, rangeAfterCombine, cachedRange[i]); + } + + // addOffset + Value addValue = materializeValue(builder, loc, offset); + if (addValue.getType().isIndex()) { + addValue = + builder.create(loc, builder.getI32Type(), addValue); + } + Value splatOffset = + builder.create(loc, broadCastType, addValue); + auto rangeAfterAdd = + builder.create(loc, rangeAfterCombine, splatOffset); + + // addPtr + Value splatPtr = builder.create(loc, ptrTensorType, source); + auto addptrOp = builder.create(loc, ptrTensorType, splatPtr, + rangeAfterAdd); + return addptrOp; +} + +triton::MakeTensorPtrOp PtrState::createMakeTensorPtrOp(OpBuilder &builder, + Location loc) { + SmallVector newShape; + SmallVector newStrides; + SmallVector newOffsets; + SmallVector newBlkShape; + SmallVector newOrder; // must be int32_t for MakeTensorPtrOp builder + + const size_t rank = order.size(); + if (rank == 0) { + emitError(loc) << "PtrAnalysis: empty order in createMakeTensorPtrOp"; + return nullptr; + } + + // iterate reversed safely: i = rank-1, ..., 0 + for (size_t i = rank; i-- > 0;) { + size_t dim = order[i]; + + if (dim >= stateInfo.size() || dim >= dimOffsets.size() || + dim >= sizes.size()) { + emitError(loc) + << "PtrAnalysis: invalid dim index in createMakeTensorPtrOp"; + return nullptr; + } + + auto info = stateInfo[dim]; + + newShape.push_back(materializeValue(builder, loc, info.shape)); + newStrides.push_back(materializeValue(builder, loc, info.stride)); + newOffsets.push_back(materializeValue(builder, loc, dimOffsets[dim])); + + auto blkSzOpt = getIntAttr(sizes[dim]); + if (!blkSzOpt.has_value()) { + emitError(loc) << "PtrAnalysis: dynamic block_shape is not supported for " + "tt.make_tensor_ptr"; + return nullptr; + } + newBlkShape.push_back(static_cast(blkSzOpt.value())); + newOrder.push_back(static_cast(i)); + } + + return builder.create( + loc, source, ValueRange(newShape), ValueRange(newStrides), + ValueRange(newOffsets), newBlkShape, newOrder); +} + +LogicalResult PtrAnalysis::visitOperandMul(arith::MulIOp mulOp, PtrState &state, + const Location loc, + OpBuilder &builder) { + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Visit Mul operation: " << mulOp << "\n"; + }); + + PtrState lhsState; + if (visitOperand(mulOp.getLhs(), lhsState, loc, builder).failed()) { + return failure(); + } + + PtrState rhsState; + if (visitOperand(mulOp.getRhs(), rhsState, loc, builder).failed()) { + return failure(); + } + + return state.mulState(lhsState, rhsState, mulOp, builder); +} + +LogicalResult PtrAnalysis::visitOperandSub(arith::SubIOp subOp, PtrState &state, + const Location loc, + OpBuilder &builder) { + PtrState lhsState; + if (visitOperand(subOp.getLhs(), lhsState, loc, builder).failed()) { + return failure(); + } + + PtrState rhsState; + if (visitOperand(subOp.getRhs(), rhsState, loc, builder).failed()) { + return failure(); + } + + return state.subState(lhsState, rhsState, subOp, builder); +} + +LogicalResult PtrAnalysis::visitOperandMakeRange(triton::MakeRangeOp rangeOp, + PtrState &state, Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + rangeOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting make_range"); + }); + return failure(); + } + + auto shape = cast(rangeOp.getType()).getShape(); + + auto start = rangeOp.getStart(); + auto end = rangeOp.getEnd(); + auto stride = (end - start + shape[0] - 1) / shape[0]; + if (stride != 1) { + LLVM_DEBUG({ + rangeOp.emitError( + "PtrAnalysis: make_range op with stride != 1 is not supported"); + }); + return failure(); + } + + auto infoStride = builder.getIndexAttr(stride); + auto size = builder.getIndexAttr(shape[0]); + auto offset = builder.getIndexAttr(start); + + SmallVector stateInfo; + SmallVector sizes; + stateInfo.emplace_back(infoStride, size); + sizes.emplace_back(size); + + state.updatePtrState(stateInfo, sizes, nullptr, offset, loc, builder); + return success(); +} + +LogicalResult +PtrAnalysis::visitOperandBroadcast(triton::BroadcastOp broadcastOp, + PtrState &state, const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + broadcastOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting broadcast"); + }); + return failure(); + } + + auto src = broadcastOp.getSrc(); + auto dst = broadcastOp.getResult(); + if (!isa(dst.getType())) { + LLVM_DEBUG({ + broadcastOp.emitRemark( + "PtrAnalysis: broadcast dst should be a shaped type"); + }); + return failure(); + } + + auto srcShape = cast(src.getType()).getShape(); + auto dstShape = cast(dst.getType()).getShape(); + if (srcShape.size() != dstShape.size()) { + LLVM_DEBUG({ + broadcastOp.emitRemark( + "PtrAnalysis: broadcast src and dst should have the same rank"); + }); + return failure(); + } + if (visitOperand(src, state, loc, builder).failed()) { + return failure(); + } + + if (state.sizes.size() != dstShape.size()) { + llvm::dbgs() << broadcastOp << "\n"; + state.dump(); + llvm::dbgs() << "dst.size = " << dstShape.size() << "\n"; + for (auto x : dstShape) + llvm::dbgs() << x << ", "; + llvm::dbgs() << "\n"; + } + + SmallVector newStateInfo(state.stateInfo); + SmallVector newSizes; + if (srcShape.size() != dstShape.size()) { + LLVM_DEBUG({ + broadcastOp.emitRemark( + "PtrAnalysis: unexpected state info size in broadcast"); + }); + return failure(); + } + for (size_t i = 0; i < dstShape.size(); ++i) { + newSizes.emplace_back(builder.getIndexAttr(dstShape[i])); + if (srcShape[i] == dstShape[i]) { + continue; + } else if (srcShape[i] < dstShape[i] && srcShape[i] == 1) { + for (auto &info : newStateInfo) { + if (info.dimIndex != i) + continue; + info.shape = builder.getIndexAttr(dstShape[i]); + } + } else { + LLVM_DEBUG({ + broadcastOp.emitRemark("unexpected dimensions used in broadcast"); + }); + return failure(); + } + } + state.updatePtrState(newStateInfo, newSizes, state.source, state.offset, loc, + builder, state.shouldLinearize); + return success(); +} + +LogicalResult PtrAnalysis::visitOperandSplat(triton::SplatOp splatOp, + PtrState &state, + const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + splatOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting splat"); + }); + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Visit SPLAT operation: " << splatOp << "\n"; + }); + + auto src = splatOp.getSrc(); + auto dst = splatOp.getResult(); + auto dstShape = cast(dst.getType()).getShape(); + + if (visitOperand(src, state, loc, builder).failed()) { + return failure(); + } + + LLVM_DEBUG({ + llvm::dbgs() << "splat ptrState: \n"; + state.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + + if (!state.isScalar()) { + LLVM_DEBUG( + { splatOp.emitRemark("PtrAnalysis: splat source should be scalar"); }); + return failure(); + } + + SmallVector newStateInfo; + SmallVector newSizes; + auto zeroAttr = builder.getIndexAttr(0); + if (isa(src.getType())) { + for (size_t i = 0; i < dstShape.size(); ++i) { + auto currentSize = builder.getIndexAttr(dstShape[i]); + newSizes.emplace_back(currentSize); + newStateInfo.emplace_back(zeroAttr, currentSize, i); + } + } else { + LLVM_DEBUG( + { splatOp.emitRemark("PtrAnalysis: unsupported splat pattern"); }); + return failure(); + } + state.updatePtrState(newStateInfo, newSizes, state.source, state.offset, loc, + builder, state.shouldLinearize); + + LLVM_DEBUG({ + llvm::dbgs() << "After SPLAT ptrState: \n"; + state.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return success(); +} + +LogicalResult +PtrAnalysis::visitOperandExpandDims(triton::ExpandDimsOp expandDimsOp, + PtrState &state, const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + expandDimsOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting expand_dims"); + }); + return failure(); + } + + if (visitOperand(expandDimsOp.getSrc(), state, loc, builder).failed()) { + return failure(); + } + + auto dstShape = + cast(expandDimsOp.getResult().getType()).getShape(); + auto axis = expandDimsOp.getAxis(); + + SmallVector newStateInfo(state.stateInfo); + SmallVector newSizes(state.sizes); + size_t insertPos = 0; + for (auto &info : newStateInfo) { + if (info.dimIndex >= axis) + ++info.dimIndex; + if (info.dimIndex < axis) + ++insertPos; + } + auto zeroAttr = builder.getIndexAttr(0); + auto oneAttr = builder.getIndexAttr(1); + StateInfo insertInfo(zeroAttr, oneAttr, axis); + + newStateInfo.insert(newStateInfo.begin() + insertPos, insertInfo); + newSizes.insert(newSizes.begin() + axis, oneAttr); + + state.updatePtrState(newStateInfo, newSizes, state.source, state.offset, loc, + builder, state.shouldLinearize); + return success(); +} + +LogicalResult PtrAnalysis::visitOperandConstSplat(arith::ConstantOp op, + PtrState &state, + const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + op->emitError( + "PtrAnalysis: PtrState should be empty when visiting const_splat"); + }); + return failure(); + } + + auto attr = cast(op.getValue()); + auto elementType = attr.getElementType(); + if (!attr.isSplat() || !isa(elementType)) { + LLVM_DEBUG( + { op->emitError("PtrAnalysis: only support splat integer constant"); }); + return failure(); + } + + auto value = attr.getValues()[0].getValue(); + auto constAttr = builder.getIndexAttr(value.getSExtValue()); + + auto resultShape = cast(op.getResult().getType()).getShape(); + + SmallVector sizes; + SmallVector stateInfo; + auto defaultAttr = builder.getIndexAttr(0); + + for (auto [i, shape] : llvm::enumerate(resultShape)) { + auto shapeAttr = builder.getIndexAttr(shape); + sizes.emplace_back(shapeAttr); + stateInfo.emplace_back(defaultAttr, shapeAttr, i); + } + + state.updatePtrState(stateInfo, sizes, nullptr, constAttr, loc, builder); + return success(); +} + +LogicalResult PtrAnalysis::visitOperandExtSI(arith::ExtSIOp extOp, + PtrState &state, + const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + extOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting extsi"); + }); + return failure(); + } + + if (visitOperand(extOp.getIn(), state, loc, builder).failed()) { + return failure(); + } + + return success(); +} + +LogicalResult PtrAnalysis::visitOperandRem(arith::RemSIOp remOp, + PtrState &state, const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + remOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting remsi"); + }); + return failure(); + } + LLVM_DEBUG({ + llvm::dbgs() << "before VisitRemOperands \n"; + state.dump(); + }); + + PtrState rhsState; + if (visitOperand(remOp.getRhs(), rhsState, loc, builder).failed()) { + return failure(); + } + + if (!rhsState.isScalar() || rhsState.hasSource()) { + LLVM_DEBUG({ + remOp.emitRemark("PtrAnalysis: only support cases when rhs of remainder " + "contains scalar"); + }); + return failure(); + } + + if (visitOperand(remOp.getLhs(), state, loc, builder).failed()) { + return failure(); + } + + bool hasAnnotation = optimizeDynamicOffset; + + auto zeroAttr = builder.getIndexAttr(0); + auto oneAttr = builder.getIndexAttr(1); + auto divisorAttr = rhsState.offset; + + auto staticOffset = getIntAttr(state.offset); + if ((!staticOffset.has_value() || !isMultiple(state.offset, divisorAttr)) && + !hasAnnotation) { + LLVM_DEBUG({ + remOp.emitRemark( + "PtrAnalysis: dynamic offset before REMSI, adding annotation"); + }); + return failure(); + } + + if (!getIntAttr(divisorAttr).has_value()) { + LLVM_DEBUG({ + remOp.emitError("PtrAnalysis: do not support dynamix divisor in REMSI."); + }); + return failure(); + } + + SmallVector newStateInfo; + for (auto info : state.stateInfo) { + if (!getIntAttr(info.stride).has_value()) { + LLVM_DEBUG({ + remOp.emitError( + "PtrAnalysis: do not support dynamix stride before REMSI."); + }); + return failure(); + } + + if (!isMultiple(divisorAttr, info.shape) && + !isMultiple(info.shape, divisorAttr)) { + LLVM_DEBUG({ + remOp.emitError( + "PtrAnalysis: do not support dynamix stride before REMSI."); + }); + } + + if (isMultiple(info.stride, divisorAttr)) { + newStateInfo.emplace_back(zeroAttr, info.shape, info.dimIndex); + } else if (isMultiple(divisorAttr, info.stride)) { + auto contiguousSize = + divOpFoldResult(divisorAttr, info.stride, loc, builder); + contiguousSize = + minOpFoldResult(contiguousSize, info.shape, loc, builder); + auto nonContiguousSize = + divOpFoldResult(info.shape, contiguousSize, loc, builder); + + auto staticNonContiguousSize = getIntAttr(nonContiguousSize); + if (!staticNonContiguousSize.has_value()) { + LLVM_DEBUG({ + remOp.emitError( + "PtrAnalysis: do not support dynamix size before REMSI."); + }); + return failure(); + } + + if (staticNonContiguousSize.value() > 1) + newStateInfo.emplace_back(zeroAttr, nonContiguousSize, info.dimIndex); + + newStateInfo.emplace_back(info.stride, contiguousSize, info.dimIndex); + } else { + LLVM_DEBUG({ + remOp.emitError("PtrAnalysis: stride that are not divisible by REMSI " + "are not allowed " + "to precede REMSI"); + }); + return failure(); + } + } + + auto newOffset = remOpFoldResult(state.offset, divisorAttr, loc, builder); + state.updatePtrState(newStateInfo, state.sizes, state.source, newOffset, loc, + builder, true); + + LLVM_DEBUG({ + llvm::dbgs() << "after VisitRemOperands \n"; + state.dump(); + }); + + return success(); +} + +LogicalResult PtrAnalysis::visitOperandDiv(arith::DivSIOp divOp, + PtrState &state, const Location loc, + OpBuilder &builder) { + if (!state.isEmpty()) { + LLVM_DEBUG({ + divOp.emitError( + "PtrAnalysis: PtrState should be empty when visiting divsi"); + }); + return failure(); + } + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "Visit DIVSI operation: " << divOp << "\n"; + }); + + PtrState rhsState; + if (visitOperand(divOp.getRhs(), rhsState, loc, builder).failed()) { + return failure(); + } + + if (!rhsState.isScalar() || rhsState.hasSource()) { + LLVM_DEBUG({ + divOp.emitRemark("PtrAnalysis: only support cases when rhs of remainder " + "contains scalar"); + }); + return failure(); + } + + if (visitOperand(divOp.getLhs(), state, loc, builder).failed()) { + return failure(); + } + + bool hasAnnotation = optimizeDynamicOffset; + + auto staticMultipleOf = extractDivisibilityFromOpFoldResult(state.offset); + if (!hasAnnotation && staticMultipleOf.has_value()) { + auto attr = builder.getIndexAttr(staticMultipleOf.value()); + hasAnnotation = isMultiple(attr, rhsState.offset); + } + + // add divState + auto zeroAttr = builder.getIndexAttr(0); + auto oneAttr = builder.getIndexAttr(1); + auto divisorAttr = rhsState.offset; + + auto staticOffset = getIntAttr(state.offset); + if ((!staticOffset.has_value() || !isMultiple(state.offset, divisorAttr)) && + !hasAnnotation) { + LLVM_DEBUG({ + divOp.emitRemark( + "PtrAnalysis: dynamic offset before DIVSI, adding annotation"); + }); + return failure(); + } + + if (!getIntAttr(divisorAttr).has_value()) { + LLVM_DEBUG({ + divOp.emitError("PtrAnalysis: do not support dynamix divisor in DIVSI."); + }); + return failure(); + } + + SmallVector newStateInfo; + for (auto info : state.stateInfo) { + auto staticStride = getIntAttr(info.stride); + if (!staticStride.has_value()) { + LLVM_DEBUG({ + divOp.emitError( + "PtrAnalysis: do not support dynamix stride before DIVSI."); + }); + return failure(); + } + + if (!isMultiple(divisorAttr, info.shape) && + !isMultiple(info.shape, divisorAttr)) { + LLVM_DEBUG({ + divOp.emitError( + "PtrAnalysis: do not support dynamix stride before DivSI."); + }); + } + + if (isMultiple(info.stride, divisorAttr)) { + auto newStride = divOpFoldResult(info.stride, divisorAttr, loc, builder); + newStateInfo.emplace_back(newStride, info.shape, info.dimIndex); + } else if (isMultiple(divisorAttr, info.stride)) { + auto nonContiguousSize = + divOpFoldResult(divisorAttr, info.stride, loc, builder); + nonContiguousSize = + minOpFoldResult(nonContiguousSize, info.shape, loc, builder); + auto contiguousSize = + divOpFoldResult(info.shape, nonContiguousSize, loc, builder); + + auto staticContiguousSize = getIntAttr(contiguousSize); + if (!staticContiguousSize.has_value()) { + LLVM_DEBUG({ + divOp.emitError( + "PtrAnalysis: do not support dynamix size before DIVSI."); + }); + return failure(); + } + + if (staticContiguousSize.value() != 0) + newStateInfo.emplace_back(oneAttr, contiguousSize, info.dimIndex); + + newStateInfo.emplace_back(zeroAttr, nonContiguousSize, info.dimIndex); + } else { + LLVM_DEBUG({ + divOp.emitError("PtrAnalysis: stride that are not divisible by DIVSI " + "are not allowed " + "to precede DIVSI"); + }); + return failure(); + } + } + + auto newOffset = divOpFoldResult(state.offset, divisorAttr, loc, builder); + state.updatePtrState(newStateInfo, state.sizes, state.source, newOffset, loc, + builder, true); + + LLVM_DEBUG({ + llvm::dbgs() << "after VisitDivOperands \n"; + state.dump(); + }); + return success(); +} + +LogicalResult PtrAnalysis::visitOperandAdd(arith::AddIOp addOp, PtrState &state, + const Location loc, + OpBuilder &builder) { + PtrState lhsState; + if (visitOperand(addOp.getLhs(), lhsState, loc, builder).failed()) { + return failure(); + } + + PtrState rhsState; + if (visitOperand(addOp.getRhs(), rhsState, loc, builder).failed()) + return failure(); + return state.addState(lhsState, rhsState, addOp, builder); +} + +LogicalResult PtrAnalysis::visitOperand(Value operand, PtrState &state, + const Location loc, + OpBuilder &builder) { + if (knownPtrs.find(operand) != knownPtrs.end()) { + state = knownPtrs.lookup(operand); + return success(); + } + + if (operandIsScalar(operand)) { + return initStateByScalar(operand, state, loc, builder); + } + + if (isa(operand.getType())) { + return initStateByPointer(operand, state, loc, builder); + } + + if (auto op = operand.getDefiningOp()) { + return visitOperandAdd(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandMul(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandSub(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandMakeRange(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandBroadcast(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandSplat(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandExpandDims(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandAddptr(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandConstSplat(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandRem(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandDiv(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + return visitOperandExtSI(op, state, loc, builder); + } else if (auto op = operand.getDefiningOp()) { + LLVM_DEBUG({ + op.emitRemark("TritonToStructured: Invalid dynamic offset" + "The load operation's offset cannot be derived from " + "another load result."); + }); + return failure(); + } else if (auto op = operand.getDefiningOp()) { + LLVM_DEBUG({ + op.emitWarning("IllegalTypeConversionInAddressCalculation" + "float-to-int precision conversion is not supported " + "during address computation."); + llvm::dbgs() << "Operand: \n"; + operand.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return failure(); + } else if (!operand.getDefiningOp()) { + if (!knownPtrs.contains(operand)) { + LLVM_DEBUG({ + llvm::dbgs() << "TritonToStructured: Pointer analysis is not supported " + "for input parameters\n"; + }); + return failure(); + } + + // This operand must be an iter-arg of an inner-loop in a multiple-level + // nested loop, which means its PtrState must have already been populated + // during rewriteForOp of the parent loop. + state = knownPtrs[operand]; + return success(); + } else { + auto op = operand.getDefiningOp(); + LLVM_DEBUG({ + op->emitWarning("TritonToStructured: encountered addptr operand produced " + "by an unsupported operation"); + llvm::dbgs() << "Operand: \n"; + operand.dump(); + llvm::dbgs() << "----------------------------------------------\n"; + }); + return failure(); + } + return success(); +} + +LogicalResult PtrAnalysis::rewriteAddptrOp(triton::AddPtrOp op) { + OpBuilder builder(op); + auto loc = op.getLoc(); + + PtrState state; + if (visitOperandAddptr(op, state, op.getLoc(), builder).failed()) { + return failure(); + } + + auto maketptrOp = state.createAddPtrOp(builder, op.getLoc()); + knownPtrs[op.getResult()] = state; + ptrMap.map(op.getResult(), maketptrOp.getResult()); + + return success(); +} + +void PtrState::analyzePermute() { + const size_t n = stateInfo.size(); + generateOriginPermuteIds(); + + if (n <= 1) + return; + + // ============================================================ + // === 1. make_tensor_ptr (block ptr): order-based === + // Rule: permute if order is NOT canonical (i.e. strictly decreasing, + // representing inner-to-outer memory layout priority, e.g. [n-1, ..., 0]) + // + // NOTE: order describes memory layout priority, not axis permutation. + // Do NOT translate order into permuteIds. + // ============================================================ + if (isBlockPtr()) { + for (size_t i = 0; i + 1 < n; ++i) { + if (order[i] <= order[i + 1]) { + isPermuted = true; + break; + } + } + return; + } + + // ============================================================ + // === 2. addptr: stride-based permuteIds + contiguous-axis === + // Rule: permute if contiguous axes increased + // + // analyze constraints: must have at least one static stride + // ============================================================ + bool hasStatic = false; + for (auto &s : stateInfo) { + if (getIntAttr(s.stride).has_value()) { + hasStatic = true; + break; + } + } + if (!hasStatic) { + return; + } + auto isIdentity = [&](ArrayRef perm) -> bool { + for (size_t i = 0; i < perm.size(); ++i) + if (perm[i] != i) + return false; + return true; + }; + + std::stable_sort(permuteIds.begin(), permuteIds.end(), + [&](size_t a, size_t b) { + return isGreater(stateInfo[a].stride, stateInfo[b].stride); + }); + + // If already in canonical axis order, do not permute. + if (isIdentity(permuteIds)) { + return; + } + + // Tail axis must be physically contiguous (stride == 1), + // otherwise addptr-based permutation is invalid. + auto tailStride = getIntAttr(stateInfo[permuteIds.back()].stride); + if (!tailStride.has_value() || tailStride.value() != 1) { + generateOriginPermuteIds(); + return; + } + + // Compute new contiguous axes count using the permuted stateInfo. + SmallVector newStateInfo; + newStateInfo.reserve(n); + for (size_t id : permuteIds) { + newStateInfo.push_back(stateInfo[id]); + } + const size_t oldContig = countContiguousAxes(stateInfo); + const size_t newContig = countContiguousAxes(newStateInfo); + LLVM_DEBUG({ + llvm::dbgs() << "----------------------------------------------\n"; + llvm::dbgs() << "after analyzePermute:\n" + << "oldContig: " << oldContig << "\n" + << "newContig: " << newContig << "\n"; + dump(); + }); + // only permute if contiguous axes increased + if (newContig > oldContig) { + isPermuted = true; + return; + } + + // otherwise: no permute + generateOriginPermuteIds(); +} + +std::optional +extractDivisibilityFromOpFoldResult(mlir::OpFoldResult ofr) { + auto value = dyn_cast(ofr); + if (!value) { + return std::nullopt; + } + auto defOp = value.getDefiningOp(); + if (!defOp) { + return std::nullopt; + } + + auto divisibilityAttr = defOp->getAttr("tt.divisibility"); + if (!divisibilityAttr) { + return std::nullopt; + } + + auto denseAttr = dyn_cast(divisibilityAttr); + if (!denseAttr || denseAttr.empty()) { + return std::nullopt; + } + + return denseAttr.getValues()[0]; +} + +void PtrState::generateOriginPermuteIds() { + permuteIds.clear(); + isPermuted = false; + for (size_t i = 0; i < stateInfo.size(); ++i) { + permuteIds.emplace_back(i); + } + return; +} + +// Formula of "contiguous axes" +// - axis i is contiguous if stride[i] == product(shape[0..i-1]). +// We only prove contiguity from the rightmost axis outward. +// Rules implemented +// - Start from the rightmost axis. It is contiguous only if stride == 1 +// (static). +// - Then move left; expectedStride multiplies by the static shape of the axis +// to the right. +// - If we encounter any dynamic stride or dynamic shape needed for +// expectedStride, +// stop (cannot prove further) and return the count accumulated so far. +// Examples +// - shape=[2,3,4,5], stride=[A,B,2,C] => rightmost stride is dynamic => +// count=0 +// - shape=[2,3,4,5], stride=[A,B,C,1] => rightmost stride=1 => count=1 +// - shape=[2,3,4,5], stride=[60,20,5,1] => count=4 +size_t PtrState::countContiguousAxes(SmallVector stateInfo) const { + if (stateInfo.empty()) + return 0; + int64_t expected = 1; + size_t cnt = 0; + // iterate reversed safely: i = stateInfo.size()-1, ..., 0 + for (size_t i = stateInfo.size(); i-- > 0;) { + auto stride = getIntAttr(stateInfo[i].stride); + if (!stride.has_value()) + break; + if (stride.value() != expected) + break; + ++cnt; + // Update expected for the next (outer) axis: expected *= shape[i] + auto shape = getIntAttr(stateInfo[i].shape); + if (!shape.has_value()) + break; + expected *= shape.value(); + } + return cnt; +} +} // namespace TritonToStructured diff --git a/third_party/ascend/lib/TritonToStructured/TritonToStructuredPass.cpp b/third_party/ascend/lib/TritonToStructured/TritonToStructuredPass.cpp new file mode 100644 index 0000000000..5446efdffa --- /dev/null +++ b/third_party/ascend/lib/TritonToStructured/TritonToStructuredPass.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToStructured/TritonToStructuredPass.h" + +#include +#include +#include + +#include "Utils/InterleaveOptimization.h" +#include "Utils/Utils.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/Operation.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Transforms/Transforms.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Visitors.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" + +#include "TritonToStructured/CannonicalizerConverter.h" +#include "TritonToStructured/MemOpConverter.h" +#include "TritonToStructured/PtrAnalysis.h" +#include "llvm/ADT/BitVector.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/LogicalResult.h" + +#define DEBUG_TYPE "triton-to-structured" + +using namespace mlir; +using namespace triton; + +void TritonToStructuredPass::getDependentDialects( + DialectRegistry ®istry) const { + registry.insert(); +} + +void TritonToStructuredPass::populateTritonToStructuredCanonicalizationPatterns( + RewritePatternSet &patterns) { + // TODO enable this optimization after fixing the bisheng bug it causes in + // current version + // patterns.add(patterns.getContext()); + patterns.add( + patterns.getContext()); + patterns.add( + patterns.getContext()); + // Add addptr splat->broadcast hoisting converter + patterns.add( + patterns.getContext()); + // Move loads before broadcasts when safe + patterns.add( + patterns.getContext()); + // Move stores before broadcasts when safe + patterns.add( + patterns.getContext()); + // Lower `make_tensor_ptr` with all-zero strides to scalar load + broadcast. + patterns.add( + patterns.getContext()); +} + +void TritonToStructuredPass::populateTritonToStructuredPatterns( + RewritePatternSet &patterns, bool optimizeDynamicOffset, + bool enableMaskFallbackConversion) { + patterns.add(patterns.getContext(), + optimizeDynamicOffset, + enableMaskFallbackConversion); + patterns.add(patterns.getContext(), + optimizeDynamicOffset, + enableMaskFallbackConversion); +} + +LogicalResult +TritonToStructuredPass::processSplatBinaryOperations(ModuleOp moduleOp) { + mlir::RewritePatternSet patterns(&getContext()); + patterns.add( + patterns.getContext()); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + moduleOp.emitWarning("Splat binary op processing failed"); + return failure(); + } + return success(); +} + +void TritonToStructuredPass::runOnOperation() { + auto moduleOp = getOperation(); + ConversionTarget target(getContext()); + RewritePatternSet canonicalizerPatterns(&getContext()); + + this->populateTritonToStructuredCanonicalizationPatterns( + canonicalizerPatterns); + if (failed( + applyPatternsGreedily(moduleOp, std::move(canonicalizerPatterns)))) { + moduleOp.emitWarning("Canonicalize failed"); + } + + RewritePatternSet tritonToStructuredPatterns(&getContext()); + populateTritonToStructuredPatterns(tritonToStructuredPatterns, + optimizeDynamicOffset, + enableMaskFallbackConversion); + + if (failed(applyPatternsGreedily(moduleOp, + std::move(tritonToStructuredPatterns)))) { + LLVM_DEBUG({ moduleOp->emitRemark("PtrAnalysis: rewrite MemOp failed"); }); + } + + if (failed(processSplatBinaryOperations(moduleOp))) { + moduleOp.emitWarning("Splat binary op processing failed"); + } + + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + if (failed(runPipeline(pm, getOperation()))) { + moduleOp->emitWarning("Canonicalize failed"); + } +} + +std::unique_ptr> +triton::createTritonToStructuredPass() { + return std::make_unique(); +} + +std::unique_ptr> +triton::createTritonToStructuredPass(bool enableMaskFallbackConversion, + bool optimizeDynamicOffset) { + return std::make_unique(enableMaskFallbackConversion, + optimizeDynamicOffset); +} diff --git a/third_party/ascend/lib/TritonToUnstructure/BubbleUpOperation.cpp b/third_party/ascend/lib/TritonToUnstructure/BubbleUpOperation.cpp new file mode 100644 index 0000000000..c1efb5990a --- /dev/null +++ b/third_party/ascend/lib/TritonToUnstructure/BubbleUpOperation.cpp @@ -0,0 +1,564 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToUnstructure/BubbleUpOperation.h" +#include "Utils/Utils.h" + +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" + +#define DEBUG_TYPE "triton-bubble-up-operation" + +template +BubbleUpExtract::BubbleUpExtract(MLIRContext *context, + bool enableAggressiveMode) + : OpRewritePattern(context), + enableAggressiveMode(enableAggressiveMode) {} + +template +LogicalResult +BubbleUpExtract::matchAndRewrite(ExtractOpTy op, + PatternRewriter &rewriter) const { + Value tensorValue; + if constexpr (std::is_same_v) { + tensorValue = op.getTensor(); + } else if constexpr (std::is_same_v) { + tensorValue = op.getSource(); + if (tensorValue.getType() == op.getResult().getType()) { + rewriter.replaceAllUsesWith(op.getResult(), tensorValue); + rewriter.eraseOp(op); + return success(); + } + } else { + llvm_unreachable("Unhandled case"); + } + auto funcOp = op->template getParentOfType(); + auto parentOp = tensorValue.getDefiningOp(); + auto loc = op.getLoc(); + + if (!parentOp || (!enableAggressiveMode && !parentOp->hasOneUse())) { + return failure(); + } + + // Keep the vector mask attached to discrete memory accesses. Depending on + // which mask conversion produced the select, the marker is either on the + // select itself or on the generated load loop that supplies its value. + if (auto selectOp = dyn_cast(parentOp); + selectOp && isa(selectOp.getCondition().getType())) { + auto isDiscreteLoadResult = [](Value value) { + auto forOp = value.getDefiningOp(); + return forOp && forOp->hasAttr("ExtractedLoadOrStore"); + }; + if (selectOp->hasAttr(ConverterUtils::discreteAttrName) || + isDiscreteLoadResult(selectOp.getTrueValue()) || + isDiscreteLoadResult(selectOp.getFalseValue())) + return failure(); + } + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Before bubble up\n" << op << '\n' << funcOp << "\n"; + }); + + if (auto extsiOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, extsiOp, loc, rewriter); + } else if (auto addIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, addIOp, loc, rewriter); + } else if (auto subIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, subIOp, loc, rewriter); + } else if (auto mulIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, mulIOp, loc, rewriter); + } else if (auto divSIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, divSIOp, loc, rewriter); + } else if (auto remSIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, remSIOp, loc, rewriter); + } else if (auto maxSIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, maxSIOp, loc, rewriter); + } else if (auto minSIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, minSIOp, loc, rewriter); + } else if (auto andIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, andIOp, loc, rewriter); + } else if (auto orIOp = dyn_cast(parentOp)) { + bubbleUpIntBinaryOp(op, orIOp, loc, rewriter); + } else if (auto cmpIOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, cmpIOp, loc, rewriter); + } else if (auto selectOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, selectOp, loc, rewriter); + } else if (auto truncFOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, truncFOp, loc, rewriter); + } else if (auto extFOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, extFOp, loc, rewriter); + } else if (auto fpTosiOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, fpTosiOp, loc, rewriter); + } else if (auto siTofpOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, siTofpOp, loc, rewriter); + } else if (auto clampFOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, clampFOp, loc, rewriter); + } else if (auto addFOp = dyn_cast(parentOp)) { + bubbleUpFloatBinaryOp(op, addFOp, loc, rewriter); + } else if (auto subFOp = dyn_cast(parentOp)) { + bubbleUpFloatBinaryOp(op, subFOp, loc, rewriter); + } else if (auto mulFOp = dyn_cast(parentOp)) { + bubbleUpFloatBinaryOp(op, mulFOp, loc, rewriter); + } else if (auto divFOp = dyn_cast(parentOp)) { + bubbleUpFloatBinaryOp(op, divFOp, loc, rewriter); + } else if (auto minNumFOp = dyn_cast(parentOp)) { + bubbleUpFloatBinaryOp(op, minNumFOp, loc, rewriter); + } else if (auto maxNumFOp = dyn_cast(parentOp)) { + bubbleUpFloatBinaryOp(op, maxNumFOp, loc, rewriter); + } else if (auto cmpFOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, cmpFOp, loc, rewriter); + } else if (auto broadCastOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, broadCastOp, loc, rewriter); + } else if (auto expandDimsOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, expandDimsOp, loc, rewriter); + } else if (auto splatOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, splatOp, loc, rewriter); + } else if (auto makeRangeOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, makeRangeOp, loc, rewriter); + } else if (auto addPtrOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, addPtrOp, loc, rewriter); + } else if (auto floorOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, floorOp, loc, rewriter); + } else if (auto ceilOp = dyn_cast(parentOp)) { + bubbleUpOperation(op, ceilOp, loc, rewriter); + } else if (auto extractSliceOp = dyn_cast(parentOp)) { + if constexpr (std::is_same_v) { + bubbleUpOperation(op, extractSliceOp, loc, rewriter); + } else { + return failure(); + } + } else { + return failure(); + } + if (parentOp->use_empty()) + rewriter.eraseOp(parentOp); + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "After bubble up\n" << funcOp << '\n'; + }); + + return success(); +} + +template +Value BubbleUpExtract::createExtractOp( + ExtractOpTy op, Value value, Location loc, + PatternRewriter &rewriter) const { + llvm_unreachable("Unhandled extract operation"); +} + +template <> +Value BubbleUpExtract::createExtractOp( + tensor::ExtractOp op, Value value, Location loc, + PatternRewriter &rewriter) const { + auto extractedOp = + rewriter.create(loc, value, op.getIndices()); + extractedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + return extractedOp; +} + +template <> +Value BubbleUpExtract::createExtractOp( + tensor::ExtractSliceOp op, Value value, Location loc, + PatternRewriter &rewriter) const { + auto extractedType = getExtractSlicedType( + op.getMixedSizes(), op.getDroppedDims(), getElementTypeOrSelf(value)); + auto extractedOp = rewriter.create( + loc, extractedType, value, op.getMixedOffsets(), op.getMixedSizes(), + op.getMixedStrides()); + extractedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + return extractedOp; +} + +template +template +void BubbleUpExtract::bubbleUpIntBinaryOp( + ExtractOpTy op, BinOpTy binOp, Location loc, + PatternRewriter &rewriter) const { + auto lhs = createExtractOp(op, binOp.getLhs(), loc, rewriter); + auto rhs = createExtractOp(op, binOp.getRhs(), loc, rewriter); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Binary\n" << *op << '\n' << binOp << '\n'; + }); + rewriter.replaceOpWithNewOp(op, lhs, rhs); +} + +template +template +void BubbleUpExtract::bubbleUpFloatBinaryOp( + ExtractOpTy op, BinOpTy binOp, Location loc, + PatternRewriter &rewriter) const { + auto lhs = createExtractOp(op, binOp.getLhs(), loc, rewriter); + auto rhs = createExtractOp(op, binOp.getRhs(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, lhs, rhs); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, arith::ExtSIOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto in = createExtractOp(op, parentOp.getIn(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, op.getResult().getType(), in); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, arith::CmpIOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto lhs = createExtractOp(op, parentOp.getLhs(), loc, rewriter); + auto rhs = createExtractOp(op, parentOp.getRhs(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, parentOp.getPredicateAttr(), + lhs, rhs); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, arith::SelectOp parentOp, Location loc, + PatternRewriter &rewriter) const { + Value condition = parentOp.getCondition(); + if (isa(condition.getType())) + condition = createExtractOp(op, condition, loc, rewriter); + Value trueValue = createExtractOp(op, parentOp.getTrueValue(), loc, rewriter); + Value falseValue = + createExtractOp(op, parentOp.getFalseValue(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, condition, trueValue, + falseValue); +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractOp op, triton::BroadcastOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto src = parentOp.getSrc(); + auto srcShape = cast(src.getType()).getShape(); + SmallVector newIndices; + for (const auto &[index, shape] : + llvm::zip_equal(op.getIndices(), srcShape)) { + if (shape == 1) { + newIndices.push_back( + rewriter.create(loc, rewriter.getIndexAttr(0))); + } else { + newIndices.push_back(index); + } + } + auto extractedOp = rewriter.create(loc, src, newIndices); + extractedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + rewriter.replaceOp(op, extractedOp); +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractSliceOp op, triton::BroadcastOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto src = parentOp.getSrc(); + auto srcShape = cast(src.getType()).getShape(); + SmallVector newOffsets; + SmallVector newSizes; + bool isScalarLikeSrc = true; + for (const auto &[offset, size, shape] : + llvm::zip_equal(op.getMixedOffsets(), op.getMixedSizes(), srcShape)) { + if (shape == 1) { + newOffsets.push_back(rewriter.getIndexAttr(0)); + newSizes.push_back(rewriter.getIndexAttr(1)); + } else { + newOffsets.push_back(offset); + newSizes.push_back(size); + } + if (getConstantIntValue(newSizes.back()).value_or(-1) != 1) + isScalarLikeSrc = false; + } + auto extractedType = getExtractSlicedType(newSizes, op.getDroppedDims(), + getElementTypeOrSelf(src)); + auto extractedOp = rewriter.create( + loc, extractedType, src, newOffsets, newSizes, op.getMixedStrides()); + extractedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + if (isScalarLikeSrc) { + SmallVector indices( + extractedType.getRank(), + rewriter.create(loc, rewriter.getIndexAttr(0))); + auto extractedValue = + rewriter.create(loc, extractedOp, indices); + rewriter.replaceOpWithNewOp(op, op.getResult().getType(), + extractedValue); + } else { + rewriter.replaceOpWithNewOp( + op, op.getResult().getType(), extractedOp); + } +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractOp op, triton::ExpandDimsOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto src = parentOp.getSrc(); + SmallVector newIndices; + for (const auto index : llvm::enumerate(op.getIndices())) { + if (index.index() != parentOp.getAxis()) + newIndices.push_back(index.value()); + } + auto extractedOp = rewriter.create(loc, src, newIndices); + extractedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + rewriter.replaceOp(op, extractedOp); +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractSliceOp op, triton::ExpandDimsOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto src = parentOp.getSrc(); + auto srcShape = cast(src.getType()).getShape(); + auto offsets = op.getMixedOffsets(); + auto sizes = op.getMixedSizes(); + auto strides = op.getMixedStrides(); + auto axis = parentOp.getAxis(); + int64_t axisOffset = 0; + SmallVector newOffsets; + SmallVector newSizes; + SmallVector newStrides; + auto droppedDim = op.getDroppedDims(); + llvm::SmallBitVector srcDroppedDims; + for (size_t i = 0; i <= srcShape.size(); i++) { + if (i != axis) { + newOffsets.push_back(offsets[i]); + newSizes.push_back(sizes[i]); + newStrides.push_back(strides[i]); + srcDroppedDims.push_back(droppedDim[i]); + if (i < axis && droppedDim[i]) + axisOffset++; + } + } + auto extractedType = + getExtractSlicedType(newSizes, srcDroppedDims, getElementTypeOrSelf(src)); + if (extractedType == src.getType()) { + rewriter.replaceOp(op, src); + } else { + auto extractedOp = rewriter.create( + loc, extractedType, src, newOffsets, newSizes, newStrides); + extractedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + rewriter.replaceOpWithNewOp(op, extractedOp, + axis - axisOffset); + } +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractOp op, triton::SplatOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto src = parentOp.getSrc(); + rewriter.replaceOp(op, src); +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractSliceOp op, triton::SplatOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto src = parentOp.getSrc(); + rewriter.replaceOpWithNewOp( + op, cast(op.getResult().getType()), src); +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractOp op, triton::MakeRangeOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto resultType = cast(parentOp.getResult().getType()); + int32_t start = parentOp.getStart(); + Value idx = op.getIndices()[0]; + Value result = rewriter.create( + op.getLoc(), resultType.getElementType(), idx); + if (start != 0) { + Value startVal = rewriter.create( + op.getLoc(), + rewriter.getIntegerAttr(resultType.getElementType(), start)); + result = rewriter.create(op.getLoc(), result, startVal); + } + rewriter.replaceOp(op, result); +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractSliceOp op, triton::MakeRangeOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto resultType = cast(parentOp.getResult().getType()); + auto idxOfr = op.getMixedOffsets()[0]; + Value idx = getValueOrCreateConstantIndexOp(rewriter, op.getLoc(), idxOfr); + idx = rewriter.create(op.getLoc(), + resultType.getElementType(), idx); + rewriter.replaceOpWithNewOp(op, op.getResult().getType(), + idx); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, triton::AddPtrOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto ptr = createExtractOp(op, parentOp.getPtr(), loc, rewriter); + auto offset = createExtractOp(op, parentOp.getOffset(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, ptr.getType(), ptr, offset); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, arith::TruncFOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto in = createExtractOp(op, parentOp.getIn(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, op.getResult().getType(), + in); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, arith::ExtFOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto in = createExtractOp(op, parentOp.getIn(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, op.getResult().getType(), in); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, arith::FPToSIOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto in = createExtractOp(op, parentOp.getIn(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, op.getResult().getType(), + in); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, arith::SIToFPOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto in = createExtractOp(op, parentOp.getIn(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, op.getResult().getType(), + in); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, triton::ClampFOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto x = createExtractOp(op, parentOp.getX(), loc, rewriter); + auto min = createExtractOp(op, parentOp.getMin(), loc, rewriter); + auto max = createExtractOp(op, parentOp.getMax(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, x, min, max, + parentOp.getPropagateNan()); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, arith::CmpFOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto lhs = createExtractOp(op, parentOp.getLhs(), loc, rewriter); + auto rhs = createExtractOp(op, parentOp.getRhs(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, parentOp.getPredicateAttr(), + lhs, rhs); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, math::FloorOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto operand = createExtractOp(op, parentOp.getOperand(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, operand, + parentOp.getFastmath()); +} + +template +void BubbleUpExtract::bubbleUpOperation( + ExtractOpTy op, math::CeilOp parentOp, Location loc, + PatternRewriter &rewriter) const { + auto operand = createExtractOp(op, parentOp.getOperand(), loc, rewriter); + rewriter.replaceOpWithNewOp(op, operand, + parentOp.getFastmath()); +} + +template <> +void BubbleUpExtract::bubbleUpOperation( + tensor::ExtractOp op, tensor::ExtractSliceOp parentOp, Location loc, + PatternRewriter &rewriter) const { + SmallVector indices; + SmallVector newIndices; + auto indiceIter = op.getIndices().begin(); + auto droppedDims = parentOp.getDroppedDims(); + for (auto [idx, offset] : llvm::enumerate(parentOp.getMixedOffsets())) { + if (droppedDims[idx]) { + auto zeroIdx = rewriter.create( + op.getLoc(), rewriter.getIndexAttr(0)); + indices.push_back(zeroIdx); + } else { + indices.push_back(*indiceIter); + ++indiceIter; + } + } + for (const auto &[offset, index] : + llvm::zip_equal(parentOp.getMixedOffsets(), indices)) { + Value offsetVal = + getValueOrCreateConstantIndexOp(rewriter, op.getLoc(), offset); + newIndices.push_back( + rewriter.create(op.getLoc(), offsetVal, index)); + } + rewriter + .replaceOpWithNewOp(op, parentOp.getSource(), + newIndices) + ->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); +} + +BubbleUpOperationPass::BubbleUpOperationPass( + const BubbleUpOperationOptions &options) + : BubbleUpOperationBase(options) {} + +void BubbleUpOperationPass::runOnOperation() { + ModuleOp moduleOp = getOperation(); + MLIRContext *ctx = &getContext(); + + RewritePatternSet patterns(ctx); + patterns.add, + BubbleUpExtract>(ctx, + enableAggressiveMode); + + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + moduleOp->emitError("failed to apply Patterns"); + signalPassFailure(); + } + + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + if (failed(runPipeline(pm, getOperation()))) { + signalPassFailure(); + } +} + +std::unique_ptr> +triton::createBubbleUpOperationPass(const BubbleUpOperationOptions &options) { + return std::make_unique(options); +} diff --git a/third_party/ascend/lib/TritonToUnstructure/CMakeLists.txt b/third_party/ascend/lib/TritonToUnstructure/CMakeLists.txt new file mode 100644 index 0000000000..e167c44619 --- /dev/null +++ b/third_party/ascend/lib/TritonToUnstructure/CMakeLists.txt @@ -0,0 +1,23 @@ +add_triton_library(TritonToUnstructure + IndirectAtomicUtils.cpp + UnstructureConversionPass.cpp + OffsetAnalysis.cpp + BubbleUpOperation.cpp + ReplaceArguments.cpp + + DEPENDS + TritonToUnstructureConversionPassIncGen + + LINK_LIBS PUBLIC + MLIRArithDialect + MLIRDialectUtils + MLIRIR + MLIRPass + MLIRTensorDialect + MLIRTransforms + MLIRSupport + TritonIR + TritonAnalysis + TritonMemoryAccessAnalysis + MLIRSCFTransforms +) diff --git a/third_party/ascend/lib/TritonToUnstructure/IndirectAtomicUtils.cpp b/third_party/ascend/lib/TritonToUnstructure/IndirectAtomicUtils.cpp new file mode 100644 index 0000000000..767024f360 --- /dev/null +++ b/third_party/ascend/lib/TritonToUnstructure/IndirectAtomicUtils.cpp @@ -0,0 +1,394 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToUnstructure/IndirectAtomicUtils.h" + +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include + +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "triton-indirect-atomic-utils" + +using namespace mlir; +using namespace triton; + +namespace { + +constexpr llvm::StringLiteral kIndirectAtomicBuiltin = + "__builtin_indirect_atomic"; +constexpr unsigned kInt8BitWidth = 8; +constexpr unsigned kInt16BitWidth = 16; + +bool hasStaticShape(RankedTensorType tensorType) { + return tensorType && tensorType.hasStaticShape(); +} + +bool hasStaticOffsetShape(Value offsetValue) { + if (auto offsetTensorType = dyn_cast(offsetValue.getType())) + return offsetTensorType.hasStaticShape(); + return offsetValue.getType().isIntOrIndex(); +} + +bool isUnsupportedCasOrXchgElementType(Type elementType) { + return elementType.isF16() || elementType.isBF16(); +} + +bool canUseIndirectAtomicFastPathForElementType(Type elementType) { + if (auto intType = dyn_cast(elementType)) + return intType.getWidth() != kInt8BitWidth && + intType.getWidth() != kInt16BitWidth; + return true; +} + +bool canUseIndirectAtomicFastPathForOffset(Value offsetValue) { + if (!offsetValue) { + return true; + } + return hasStaticOffsetShape(offsetValue); +} + +int64_t getNumElements(ArrayRef tensorShape) { + return std::accumulate(tensorShape.begin(), tensorShape.end(), int64_t{1}, + std::multiplies()); +} + +FailureOr getZeroAttr(Type elementType, PatternRewriter &rewriter) { + if (auto floatType = dyn_cast(elementType)) + return rewriter.getFloatAttr(floatType, 0.0); + if (auto intType = dyn_cast(elementType)) + return rewriter.getIntegerAttr(intType, 0); + return failure(); +} + +FailureOr createSplatConstantTensor(Location loc, + RankedTensorType tensorType, + Attribute splatAttr, + PatternRewriter &rewriter) { + if (!tensorType.hasStaticShape()) { + return failure(); + } + auto constant = rewriter.create( + loc, tensorType, DenseElementsAttr::get(tensorType, splatAttr)); + return constant.getResult(); +} + +FailureOr createZeroTensor(Location loc, RankedTensorType tensorType, + PatternRewriter &rewriter) { + auto zeroAttr = getZeroAttr(tensorType.getElementType(), rewriter); + if (failed(zeroAttr)) { + return failure(); + } + return createSplatConstantTensor(loc, tensorType, *zeroAttr, rewriter); +} + +// Convert i1 mask to i8 before reshape to avoid hardware alignment issues. +// Directly reshaping 2x3xi1 to 6xi1 without casting leads to incorrect memory +// layout: Expected: 1 1 1 1 1 1 0 0 Actual (due to i8 alignment padding): 1 1 1 +// 0 0 0 0 0, 1 1 1 0 0 0 0 0 This layout mismatch breaks mask matching and +// causes accuracy errors. Casting to i8 first ensures proper element packing +// without alignment padding. +Value castMaskToI8(Location loc, Value maskValue, PatternRewriter &rewriter) { + Type maskType = maskValue.getType(); + if (auto tensorType = dyn_cast(maskType)) { + auto targetType = + RankedTensorType::get(tensorType.getShape(), rewriter.getI8Type()); + return rewriter.create(loc, targetType, maskValue); + } + return rewriter.create(loc, rewriter.getI8Type(), maskValue); +} + +// Converts inputValue into a 1D tensor of type flatTensorType. +// This 1D flattening is mandatory: the low-level SIMT execution model only +// supports 1D tensor. If inputValue is scalar-like, it is first splatted to +// expandedTensorShape and then reshaped into the flattened 1D tensor type. +FailureOr createFlattenedTensorValue( + Location loc, Value inputValue, ArrayRef expandedTensorShape, + RankedTensorType flatTensorType, PatternRewriter &rewriter) { + Value flattenedValue = inputValue; + if (auto inputTensorType = dyn_cast(inputValue.getType())) { + if (!inputTensorType.hasStaticShape()) { + return failure(); + } + if (inputTensorType != flatTensorType) { + if (inputTensorType.getNumElements() != flatTensorType.getNumElements()) { + return failure(); + } + flattenedValue = + rewriter.create(loc, flatTensorType, inputValue); + } + return flattenedValue; + } + + auto expandedTensorType = RankedTensorType::get( + expandedTensorShape, flatTensorType.getElementType()); + Value splatValue = + rewriter.create(loc, expandedTensorType, inputValue); + if (expandedTensorType == flatTensorType) { + return splatValue; + } + auto reshape = + rewriter.create(loc, flatTensorType, splatValue); + return reshape.getResult(); +} + +// Restores a flattened 1D tensor result to targetTensorType, which is the +// destination result tensor type expected by the original Triton op. +Value restoreFlattenedTensorShape(Location loc, Value flatTensorValue, + RankedTensorType targetTensorType, + PatternRewriter &rewriter) { + if (cast(flatTensorValue.getType()) == targetTensorType) { + return flatTensorValue; + } + return rewriter.create(loc, targetTensorType, + flatTensorValue); +} + +std::optional normalizeRmwOperate(RMWOp op) { + switch (op) { + case RMWOp::ADD: + case RMWOp::FADD: + return llvm::StringRef("add"); + case RMWOp::MAX: + case RMWOp::UMAX: + return llvm::StringRef("max"); + case RMWOp::MIN: + case RMWOp::UMIN: + return llvm::StringRef("min"); + case RMWOp::XCHG: + return llvm::StringRef("xchg"); + case RMWOp::AND: + return llvm::StringRef("and"); + case RMWOp::OR: + return llvm::StringRef("or"); + case RMWOp::XOR: + return llvm::StringRef("xor"); + default: + return std::nullopt; + } +} + +Value createIndirectAtomicCustom(Location loc, + RankedTensorType customResultType, + ValueRange inputs, ValueRange outputs, + llvm::StringRef operate, + PatternRewriter &rewriter) { + auto custom = rewriter.create( + loc, TypeRange{customResultType}, kIndirectAtomicBuiltin, inputs, outputs, + ValueRange{}); + custom->setAttr("symbol", rewriter.getStringAttr(kIndirectAtomicBuiltin)); + custom->setAttr("extra_attr", + rewriter.getStringAttr(("operate=" + operate).str())); + custom->setAttr("hivm.pipe", hivm::PipeAttr::get(rewriter.getContext(), + hivm::PIPE::PIPE_V)); + custom->setAttr( + "hivm.tcore_type", + hivm::TCoreTypeAttr::get(rewriter.getContext(), hivm::TCoreType::VECTOR)); + custom->setAttr("hivm.vf_mode", hivm::VFModeAttr::get(rewriter.getContext(), + hivm::VFMode::SIMT)); + return custom->getResult(0); +} + +} // namespace + +namespace IndirectAtomicUtils { + +bool canUseIndirectAtomicFastPath(triton::AtomicRMWOp op, Value offsetValue) { + auto resultTensorType = dyn_cast(op.getResult().getType()); + if (!hasStaticShape(resultTensorType)) { + LLVM_DEBUG({ + llvm::dbgs() << "Indirect atomic RMW falls back to legacy path: result " + "shape is not static.\n"; + }); + return false; + } + Type elementType = resultTensorType.getElementType(); + if (!canUseIndirectAtomicFastPathForElementType(elementType)) { + LLVM_DEBUG({ + llvm::dbgs() + << "Indirect atomic RMW falls back to legacy path: element type " + << elementType << " is not supported by SIMT fast path.\n"; + }); + return false; + } + if (op.getAtomicRmwOp() == RMWOp::XCHG && + isUnsupportedCasOrXchgElementType(elementType)) { + LLVM_DEBUG({ + llvm::dbgs() << "Indirect atomic RMW falls back to legacy path: XCHG " + "with element type " + << elementType << " is not supported by SIMT fast path.\n"; + }); + return false; + } + if (!canUseIndirectAtomicFastPathForOffset(offsetValue)) { + LLVM_DEBUG({ + llvm::dbgs() << "Indirect atomic RMW falls back to legacy path: offset " + "shape is not static.\n"; + }); + return false; + } + return true; +} + +bool canUseIndirectAtomicFastPath(triton::AtomicCASOp op, Value offsetValue) { + auto resultTensorType = dyn_cast(op.getResult().getType()); + if (!hasStaticShape(resultTensorType)) { + LLVM_DEBUG({ + llvm::dbgs() << "Indirect atomic CAS falls back to legacy path: result " + "shape is not static.\n"; + }); + return false; + } + Type elementType = resultTensorType.getElementType(); + if (!canUseIndirectAtomicFastPathForElementType(elementType)) { + LLVM_DEBUG({ + llvm::dbgs() + << "Indirect atomic CAS falls back to legacy path: element type " + << elementType << " is not supported by SIMT fast path.\n"; + }); + return false; + } + if (isUnsupportedCasOrXchgElementType(elementType)) { + LLVM_DEBUG({ + llvm::dbgs() + << "Indirect atomic CAS falls back to legacy path: element type " + << elementType << " is not supported by SIMT fast path.\n"; + }); + return false; + } + if (!canUseIndirectAtomicFastPathForOffset(offsetValue)) { + LLVM_DEBUG({ + llvm::dbgs() << "Indirect atomic CAS falls back to legacy path: offset " + "shape is not static.\n"; + }); + return false; + } + return true; +} + +FailureOr +tryConvertAtomicRmwToIndirectCustom(triton::AtomicRMWOp op, Value srcPtr, + Value offsetValue, + PatternRewriter &rewriter) { + auto resultTensorType = dyn_cast(op.getResult().getType()); + if (!hasStaticShape(resultTensorType)) { + return failure(); + } + + auto operate = normalizeRmwOperate(op.getAtomicRmwOp()); + if (!operate) { + return failure(); + } + + auto resultTensorShape = resultTensorType.getShape(); + int64_t flatNumel = getNumElements(resultTensorShape); + auto flatValueTensorType = + RankedTensorType::get({flatNumel}, resultTensorType.getElementType()); + auto flatOffsetTensorType = + RankedTensorType::get({flatNumel}, rewriter.getI64Type()); + + auto flatOffsetValue = + createFlattenedTensorValue(op.getLoc(), offsetValue, resultTensorShape, + flatOffsetTensorType, rewriter); + auto flatUpdateValue = + createFlattenedTensorValue(op.getLoc(), op.getVal(), resultTensorShape, + flatValueTensorType, rewriter); + if (failed(flatOffsetValue) || failed(flatUpdateValue)) { + return failure(); + } + + SmallVector inputs{srcPtr, *flatOffsetValue, *flatUpdateValue}; + if (Value maskValue = op.getMask()) { + Value maskValueI8 = castMaskToI8(op.getLoc(), maskValue, rewriter); + auto flatMaskTensorType = + RankedTensorType::get({flatNumel}, rewriter.getI8Type()); + auto flatMaskValue = + createFlattenedTensorValue(op.getLoc(), maskValueI8, resultTensorShape, + flatMaskTensorType, rewriter); + if (failed(flatMaskValue)) { + return failure(); + } + inputs.push_back(*flatMaskValue); + } + + auto initOutFlatValue = + createZeroTensor(op.getLoc(), flatValueTensorType, rewriter); + if (failed(initOutFlatValue)) { + return failure(); + } + + Value flatOldValue = createIndirectAtomicCustom( + op.getLoc(), flatValueTensorType, inputs, ValueRange{*initOutFlatValue}, + *operate, rewriter); + return restoreFlattenedTensorShape(op.getLoc(), flatOldValue, + resultTensorType, rewriter); +} + +FailureOr +tryConvertAtomicCasToIndirectCustom(triton::AtomicCASOp op, Value srcPtr, + Value offsetValue, + PatternRewriter &rewriter) { + auto resultTensorType = dyn_cast(op.getResult().getType()); + if (!hasStaticShape(resultTensorType)) { + return failure(); + } + + auto resultTensorShape = resultTensorType.getShape(); + int64_t flatNumel = getNumElements(resultTensorShape); + auto flatValueTensorType = + RankedTensorType::get({flatNumel}, resultTensorType.getElementType()); + auto flatOffsetTensorType = + RankedTensorType::get({flatNumel}, rewriter.getI64Type()); + + auto flatOffsetValue = + createFlattenedTensorValue(op.getLoc(), offsetValue, resultTensorShape, + flatOffsetTensorType, rewriter); + auto flatCompareValue = + createFlattenedTensorValue(op.getLoc(), op.getCmp(), resultTensorShape, + flatValueTensorType, rewriter); + auto flatUpdateValue = + createFlattenedTensorValue(op.getLoc(), op.getVal(), resultTensorShape, + flatValueTensorType, rewriter); + if (failed(flatOffsetValue) || failed(flatCompareValue) || + failed(flatUpdateValue)) { + return failure(); + } + + auto initOutFlatValue = + createZeroTensor(op.getLoc(), flatValueTensorType, rewriter); + if (failed(initOutFlatValue)) { + return failure(); + } + + Value flatOldValue = createIndirectAtomicCustom( + op.getLoc(), flatValueTensorType, + ValueRange{srcPtr, *flatOffsetValue, *flatCompareValue, *flatUpdateValue}, + ValueRange{*initOutFlatValue}, "cas", rewriter); + return restoreFlattenedTensorShape(op.getLoc(), flatOldValue, + resultTensorType, rewriter); +} + +} // namespace IndirectAtomicUtils diff --git a/third_party/ascend/lib/TritonToUnstructure/OffsetAnalysis.cpp b/third_party/ascend/lib/TritonToUnstructure/OffsetAnalysis.cpp new file mode 100644 index 0000000000..0bae477acc --- /dev/null +++ b/third_party/ascend/lib/TritonToUnstructure/OffsetAnalysis.cpp @@ -0,0 +1,1719 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToUnstructure/OffsetAnalysis.h" +#include "TritonControlFlowOpt/ControlFlowRewrite.h" +#include "Utils/Utils.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "triton/Dialect/Triton/IR/Types.h" + +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "triton-offset-analysis" + +namespace mlir { +namespace triton { + +PtrOffsetInfo::PtrOffsetInfo() : ptr(nullptr), offset(nullptr) {} + +PtrOffsetInfo::PtrOffsetInfo(const PtrOffsetInfo &other) { *this = other; } + +PtrOffsetInfo::PtrOffsetInfo(const Value &ptr) : ptr(ptr) { setZeroOffset(); } + +PtrOffsetInfo::PtrOffsetInfo(ArrayRef structured) + : ptr(nullptr), offset(nullptr) { + setStructured(structured); +} + +PtrOffsetInfo::PtrOffsetInfo(const Value &ptr, AxisInfo structured) : ptr(ptr) { + setZeroOffset(); + if (auto tensorType = dyn_cast(ptr.getType())) + this->structured.resize(tensorType.getRank(), structured); +} + +PtrOffsetInfo::PtrOffsetInfo(const Value &ptr, ArrayRef structured) + : ptr(ptr) { + setStructured(structured); +} + +PtrOffsetInfo::PtrOffsetInfo(const Value &ptr, const Value &offset, + AxisInfo structured) + : ptr(ptr), offset(offset) { + if (auto tensorType = dyn_cast(ptr.getType())) + this->structured.resize(tensorType.getRank(), structured); +} + +PtrOffsetInfo::PtrOffsetInfo(const Value &ptr, const Value &offset, + ArrayRef structured) + : ptr(ptr), offset(offset) { + setStructured(structured); +} + +PtrOffsetInfo &PtrOffsetInfo::operator=(const PtrOffsetInfo &other) { + setPtr(other.getPtr()); + setOffset(other.getOffset()); + setOffsets(other.getOffsets()); + setStructured(other.getStructured()); + setScalarLike(other.isScalarLike()); + setPointerDescriptorOwned(other.isPointerDescriptorOwned()); + return *this; +} + +Value PtrOffsetInfo::getPtr() const { return this->ptr; } +Value PtrOffsetInfo::getOffset() const { return this->offset; } +SmallVector PtrOffsetInfo::getOffsets() const { + return this->tptOffsets; +} +SmallVector &PtrOffsetInfo::getOffsetsRef() { return this->tptOffsets; } + +bool PtrOffsetInfo::isScalarLike() const { return this->scalarLike; } +bool PtrOffsetInfo::isPointerDescriptorOwned() const { + return this->pointerDescriptorOwned; +} + +SmallVector &PtrOffsetInfo::getStructuredRef() { + return this->structured; +} +const SmallVector & +PtrOffsetInfo::getStructured() const { + return this->structured; +} + +int PtrOffsetInfo::getRank() const { return structured.size(); } + +void PtrOffsetInfo::setPtr(const Value &ptr) { this->ptr = ptr; } +void PtrOffsetInfo::setOffset(const Value &offset) { this->offset = offset; } + +void PtrOffsetInfo::setOffsets(ValueRange offsets) { + tptOffsets.clear(); + for (auto offset : offsets) + tptOffsets.push_back(offset); +} + +void PtrOffsetInfo::setStructured() { + assert(ptr && "ptr Should be to infer rank"); + this->structured.clear(); + if (auto tensorType = dyn_cast(ptr.getType())) + this->structured.resize(tensorType.getRank(), AxisInfo::structured); +} + +void PtrOffsetInfo::setStructured(int rank) { + this->structured.clear(); + this->structured.resize(rank, AxisInfo::structured); +} + +void PtrOffsetInfo::setStructured(int rank, AxisInfo info) { + this->structured.clear(); + this->structured.resize(rank, info); +} + +void PtrOffsetInfo::setUnstructured() { + assert(ptr && "ptr Should be to infer rank"); + this->structured.clear(); + if (auto tensorType = dyn_cast(ptr.getType())) + this->structured.resize(tensorType.getRank(), AxisInfo::unstructured); +} + +void PtrOffsetInfo::setUnstructured(int rank) { + this->structured.clear(); + this->structured.resize(rank, AxisInfo::unstructured); +} + +void PtrOffsetInfo::setStructured(ArrayRef structured) { + this->structured.resize(structured.size()); + for (size_t i = 0; i < structured.size(); i++) + this->structured[i] = structured[i]; +} + +void PtrOffsetInfo::setStructured(const PtrOffsetInfo &other) { + this->setStructured(other.getStructured()); +} + +void PtrOffsetInfo::setScalarLike(bool scalarLike) { + this->scalarLike = scalarLike; +} + +void PtrOffsetInfo::setPointerDescriptorOwned(bool pointerDescriptorOwned) { + this->pointerDescriptorOwned = pointerDescriptorOwned; +} + +bool PtrOffsetInfo::isStructured(int dim) const { + return this->scalarLike || structured[dim] == AxisInfo::structured || + structured[dim] == AxisInfo::scalar; +} + +bool PtrOffsetInfo::isStructured() const { + return this->scalarLike || llvm::all_of(structured, [](auto dim) { + return dim == AxisInfo::structured || dim == AxisInfo::scalar; + }); +} + +bool PtrOffsetInfo::isUnstructured() const { + return llvm::all_of(structured, + [](auto dim) { return dim == AxisInfo::unstructured; }); +} + +bool PtrOffsetInfo::isUnstructuredOrScalarlike() const { + return llvm::all_of(structured, [](auto dim) { + return dim == AxisInfo::unstructured || dim == AxisInfo::scalarlike || + dim == AxisInfo::scalar; + }); +} + +void PtrOffsetInfo::setZeroOffset() { + if (!ptr) + return; + Value offset; + OpBuilder builder(ptr.getContext()); + builder.setInsertionPointToStart(ptr.getParentBlock()); + if (auto tensorType = dyn_cast(ptr.getType())) { + offset = builder.create( + ptr.getLoc(), DenseElementsAttr::get( + RankedTensorType::get(tensorType.getShape(), + builder.getIntegerType(64), + tensorType.getEncoding()), + builder.getZeroAttr(builder.getIntegerType(64)))); + } else { + offset = builder.create(ptr.getLoc(), + builder.getI64IntegerAttr(0)); + } + setOffset(offset); +} + +PtrOffsetInfo combineInfo(const PtrOffsetInfo &lhs, const PtrOffsetInfo &rhs) { + PtrOffsetInfo info; + assert(lhs.getRank() == rhs.getRank() && "Rank must be same to be combined"); + + info.setScalarLike(lhs.isScalarLike() && rhs.isScalarLike()); + auto &structuredRef = info.getStructuredRef(); + auto lhsStructured = lhs.getStructured(); + auto rhsStructured = rhs.getStructured(); + structuredRef.resize(lhs.getRank()); + for (size_t i = 0; i < structuredRef.size(); i++) + structuredRef[i] = std::min(lhsStructured[i], rhsStructured[i]); + return info; +} + +namespace { + +bool isScalarPointer(Value value) { + auto pointerType = dyn_cast(value.getType()); + return pointerType && !isa(pointerType.getPointeeType()); +} + +bool isTensorPointer(Value value) { + auto tensorType = dyn_cast(value.getType()); + return tensorType && isa(tensorType.getElementType()); +} + +// A scalar pointer selected or carried by structured control flow is already a +// complete runtime address. Recording the SSA value itself as the source avoids +// choosing one incoming source and losing the other branch or loop iteration. +// Later addptr users can still accumulate a separate offset from this address. +void recordOpaqueScalarPointer( + Value pointer, llvm::DenseMap &offsetMap) { + offsetMap[pointer] = PtrOffsetInfo(); + offsetMap[pointer].setPtr(pointer); + offsetMap[pointer].setZeroOffset(); + offsetMap[pointer].setScalarLike(true); +} + +// A tensor pointer without a proven common scalar base may choose a different +// source for every element. This includes opaque function arguments, +// per-lane selects, and loop phi values whose incoming provenance has not been +// merged. Preserve the pointer tensor itself as the complete opaque base and +// attach a zero displacement. Later addptr parsing can accumulate an additional +// offset without discarding the runtime lane-wise source choice. +void recordOpaqueTensorPointer( + Value pointerTensor, llvm::DenseMap &offsetMap) { + auto tensorType = cast(pointerTensor.getType()); + offsetMap[pointerTensor] = PtrOffsetInfo(); + offsetMap[pointerTensor].setPtr(pointerTensor); + offsetMap[pointerTensor].setZeroOffset(); + offsetMap[pointerTensor].setUnstructured(tensorType.getRank()); +} + +// PointerDescriptorBoundary stores loop-carried slot indices in the common +// init/region-argument/result/terminator coordinate space. Only those slots +// belong to the CFO descriptor protocol; unmarked tensor-pointer loops retain +// the established main-dev provenance analysis. +bool isPointerDescriptorBoundarySlot(Operation *loop, unsigned slot) { + auto slots = dyn_cast_or_null( + loop->getAttr(controlflow::kPointerDescriptorBoundaryAttr)); + if (!slots) + return false; + return llvm::is_contained(slots.asArrayRef(), static_cast(slot)); +} + +bool isScalarPointerOffsetBoundarySlot(Operation *loop, unsigned slot) { + auto slots = dyn_cast_or_null( + loop->getAttr(kScalarPointerOffsetBoundaryAttr)); + return slots && + llvm::is_contained(slots.asArrayRef(), static_cast(slot)); +} + +bool isScalarPointerOffsetBoundaryRegionArgument(LoopLikeOpInterface loopOp, + BlockArgument regionIterArg) { + if (auto whileOp = dyn_cast(loopOp.getOperation())) { + if (regionIterArg.getOwner() != whileOp.getBeforeBody() && + regionIterArg.getOwner() != whileOp.getAfterBody()) + return false; + return isScalarPointerOffsetBoundarySlot(loopOp.getOperation(), + regionIterArg.getArgNumber()); + } + + if (auto forOp = dyn_cast(loopOp.getOperation())) { + if (regionIterArg.getOwner() != forOp.getBody() || + regionIterArg.getArgNumber() == 0) + return false; + return isScalarPointerOffsetBoundarySlot(loopOp.getOperation(), + regionIterArg.getArgNumber() - 1); + } + return false; +} + +bool isPointerDescriptorBoundaryRegionArgument(LoopLikeOpInterface loopOp, + BlockArgument regionIterArg) { + if (auto whileOp = dyn_cast(loopOp.getOperation())) { + if (regionIterArg.getOwner() != whileOp.getBeforeBody() && + regionIterArg.getOwner() != whileOp.getAfterBody()) + return false; + return isPointerDescriptorBoundarySlot(whileOp, + regionIterArg.getArgNumber()); + } + + for (auto [slot, argument] : llvm::enumerate(loopOp.getRegionIterArgs())) { + if (argument == regionIterArg) + return isPointerDescriptorBoundarySlot(loopOp.getOperation(), slot); + } + return false; +} + +bool isPointerDescriptorBoundaryResult(LoopLikeOpInterface loopOp, + Value result) { + auto opResult = dyn_cast(result); + return opResult && opResult.getOwner() == loopOp.getOperation() && + isPointerDescriptorBoundarySlot(loopOp.getOperation(), + opResult.getResultNumber()); +} + +// Materialize the current value of an ordinary tensor offset carried beside a +// CFO pointer descriptor. T2L cannot recover layout from an scf.for block +// argument, but it can analyze the equivalent affine expression. Keep this +// deliberately narrow: the backedge must be an additive, constant-splat +// recurrence, and the carrier slot itself must not belong to the descriptor. +Value materializeAffineForOffsetCarrier(Value value, triton::AddPtrOp addPtr, + RewriterBase &rewriter) { + auto blockArg = dyn_cast(value); + if (!blockArg || blockArg.getArgNumber() == 0) + return nullptr; + auto forOp = dyn_cast_or_null(blockArg.getOwner()->getParentOp()); + if (!forOp || blockArg.getOwner() != forOp.getBody()) + return nullptr; + + unsigned slot = blockArg.getArgNumber() - 1; + if (!forOp->hasAttr(controlflow::kPointerDescriptorBoundaryAttr) || + isPointerDescriptorBoundarySlot(forOp, slot) || + slot >= forOp.getInitArgs().size() || + slot >= forOp.getYieldedValues().size()) + return nullptr; + + auto carrierType = dyn_cast(value.getType()); + auto elementType = carrierType + ? dyn_cast(carrierType.getElementType()) + : IntegerType(); + if (!carrierType || !elementType || elementType.getWidth() > 64 || + forOp.getInitArgs()[slot].getType() != carrierType) + return nullptr; + + auto backedgeAdd = + forOp.getYieldedValues()[slot].getDefiningOp(); + if (!backedgeAdd) + return nullptr; + Value carrierStep; + if (backedgeAdd.getLhs() == value) + carrierStep = backedgeAdd.getRhs(); + else if (backedgeAdd.getRhs() == value) + carrierStep = backedgeAdd.getLhs(); + else + return nullptr; + + auto constant = carrierStep.getDefiningOp(); + auto elements = constant ? dyn_cast(constant.getValue()) + : DenseElementsAttr(); + if (!elements || !elements.isSplat() || + !isa(elements.getElementType()) || + carrierStep.getType() != carrierType) + return nullptr; + + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(addPtr); + Value iterationDistance = rewriter.create( + addPtr.getLoc(), forOp.getInductionVar(), forOp.getLowerBound()); + Value iteration = rewriter.create( + addPtr.getLoc(), iterationDistance, forOp.getStep()); + Value typedIteration = rewriter.create( + addPtr.getLoc(), elementType, iteration); + Value splatIteration = rewriter.create( + addPtr.getLoc(), carrierType, typedIteration); + Value materializedStep = + rewriter.create(addPtr.getLoc(), elements); + Value displacement = rewriter.create( + addPtr.getLoc(), splatIteration, materializedStep); + return rewriter.create( + addPtr.getLoc(), forOp.getInitArgs()[slot], displacement); +} + +// Recover structured axes only when the carrier's own provenance proves that +// every lane is either affine, uniform, or a singleton axis. Complete +// carriers without that proof remain opaque and keep the existing indirect +// access fallback. +bool getRecoverableCarrierAxes(const PtrOffsetInfo &carrierInfo, unsigned rank, + SmallVectorImpl &axes) { + if (carrierInfo.getRank() != static_cast(rank) || + !carrierInfo.isStructured()) + return false; + + axes.clear(); + axes.reserve(rank); + for (PtrOffsetInfo::AxisInfo axis : carrierInfo.getStructured()) { + if (axis == PtrOffsetInfo::AxisInfo::unstructured) + return false; + axes.push_back(axis == PtrOffsetInfo::AxisInfo::scalarlike + ? PtrOffsetInfo::AxisInfo::structured + : axis); + } + return true; +} + +} // namespace + +void parse(Value operand, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + if (offsetMap.contains(operand)) { + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "found\n" << operand << '\n'; + }); + return; + } + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "parse\n" << operand << '\n'; + }); + + if (auto *defOp = operand.getDefiningOp()) { + if (isa(defOp->getDialect())) { + parseArithOp(defOp, loc, rewriter, offsetMap); + } else if (isa(defOp->getDialect())) { + parseTritonOp(defOp, loc, rewriter, offsetMap); + } else { + if (auto ifOp = dyn_cast(defOp)) { + parseIf(ifOp, loc, rewriter, offsetMap, operand); + } else if (auto yieldOp = dyn_cast(defOp)) { + parseYield(yieldOp, loc, rewriter, offsetMap); + } else if (auto loopOp = dyn_cast(defOp)) { + parseLoopOp(loopOp, loc, rewriter, offsetMap, operand); + } else if (auto extractOp = dyn_cast(defOp)) { + parseExtract(extractOp, loc, rewriter, offsetMap); + } else if (auto insertOp = dyn_cast(defOp)) { + parseInsert(insertOp, loc, rewriter, offsetMap); + } else if (auto extractSliceOp = + dyn_cast(defOp)) { + parseExtractSlice(extractSliceOp, loc, rewriter, offsetMap); + } else if (auto insertSliceOp = dyn_cast(defOp)) { + parseInsertSlice(insertSliceOp, loc, rewriter, offsetMap); + } else if (isDistributedTypeCustomOp(defOp)) { + auto opResult = dyn_cast(operand); + assert(opResult && "Expected operand to be an OpResult"); + parseStructuredCustomOp(defOp, loc, rewriter, offsetMap, + opResult.getResultNumber()); + } + } + } else if (auto blockArgument = dyn_cast(operand)) { + auto parentOp = blockArgument.getOwner()->getParentOp(); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Handling block argument\n" << *blockArgument.getOwner() << '\n'; + }); + if (isa(parentOp)) { + if (auto ptrType = dyn_cast(operand.getType())) { + offsetMap[operand] = + PtrOffsetInfo(operand, PtrOffsetInfo::AxisInfo::scalar); + } else if (isTensorPointer(operand)) { + // CFO may rebuild a tensor pointer from one invariant opaque function + // argument plus a separately carried displacement. The argument itself + // is the complete per-lane base: it has no common scalar provenance and + // must remain unstructured even when the added displacement is affine. + recordOpaqueTensorPointer(operand, offsetMap); + } else { + offsetMap[operand] = PtrOffsetInfo(); + if (auto tensorType = dyn_cast(operand.getType())) + offsetMap[operand].setUnstructured(tensorType.getRank()); + } + } else if (auto loopOp = dyn_cast(parentOp)) { + parseLoopRegionIterArg(loopOp, loc, rewriter, offsetMap, blockArgument); + } + } else { + llvm_unreachable("Unreachable"); + } + + if (!offsetMap.contains(operand)) { + offsetMap[operand] = PtrOffsetInfo(); + if (auto tensorType = dyn_cast(operand.getType())) + offsetMap[operand].setUnstructured(tensorType.getRank()); + } + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "finish parse\n" << operand << '\n'; + auto data = offsetMap.at(operand); + for (auto s : data.getStructuredRef()) + os << static_cast(s); + os << "\n"; + }); + + if (auto tensorType = dyn_cast(operand.getType()); + tensorType && isa(tensorType.getElementType())) { + auto data = offsetMap.at(operand); + assert(data.getPtr() && "pointer type should be parsed"); + } +} + +void parseLoopRegionIterArg(LoopLikeOpInterface loopOp, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap, + BlockArgument regionIterArg) { + // This argument is a dynamic relative offset created by T2U. Do not copy + // the loop init provenance here: doing so makes every backedge restart from + // the initial offset and drops the accumulated delta. Record the argument + // itself as the offset identity so every later addptr keeps the live carrier + // (`offset(region_arg) = region_arg`) when adding its backedge delta. + if (isScalarPointerOffsetBoundaryRegionArgument(loopOp, regionIterArg)) { + offsetMap.erase(regionIterArg); + offsetMap[regionIterArg] = PtrOffsetInfo(); + offsetMap[regionIterArg].setOffset(regionIterArg); + offsetMap[regionIterArg].setScalarLike(true); + return; + } + + if (isScalarPointer(regionIterArg) && + isPointerDescriptorBoundaryRegionArgument(loopOp, regionIterArg)) { + recordOpaqueScalarPointer(regionIterArg, offsetMap); + return; + } + + // CFO descriptor slots may merge different incoming bases. Without a fixed + // point, preserve the runtime phi itself rather than choosing one edge. + // Ordinary unmarked loops stay on the established init/condition analysis. + if (isTensorPointer(regionIterArg) && + isPointerDescriptorBoundaryRegionArgument(loopOp, regionIterArg)) { + recordOpaqueTensorPointer(regionIterArg, offsetMap); + return; + } + + if (auto whileOp = dyn_cast(loopOp.getOperation()); + whileOp && whileOp.getAfterBody() == regionIterArg.getOwner()) { + auto argNum = regionIterArg.getArgNumber(); + auto conditionArg = whileOp.getConditionOp().getArgs()[argNum]; + parse(conditionArg, loc, rewriter, offsetMap); + auto tmp = offsetMap[conditionArg]; + offsetMap[regionIterArg] = tmp; + return; + } + OpOperand *initArgOperand = loopOp.getTiedLoopInit(regionIterArg); + if (!initArgOperand) + return; + Value initArg = initArgOperand->get(); + parse(initArg, loc, rewriter, offsetMap); + auto tmp = offsetMap[initArg]; + offsetMap[regionIterArg] = tmp; +} + +void parseArithOp(Operation *arithOp, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + assert(isa(arithOp->getDialect())); + if (auto addIOp = dyn_cast(arithOp)) { + parseAddI(addIOp, loc, rewriter, offsetMap); + } else if (auto subIOp = dyn_cast(arithOp)) { + parseSubI(subIOp, loc, rewriter, offsetMap); + } else if (auto indexCastOp = dyn_cast(arithOp)) { + parseIndexCast(indexCastOp, loc, rewriter, offsetMap); + } else if (auto constantFloatOp = dyn_cast(arithOp)) { + parseConstantOp(constantFloatOp, loc, rewriter, offsetMap); + } else if (auto constantIntOp = dyn_cast(arithOp)) { + parseConstantOp(constantIntOp, loc, rewriter, offsetMap); + } else if (auto constantOp = dyn_cast(arithOp)) { + parseConstantOp(constantOp, loc, rewriter, offsetMap); + } else if (auto extSIOp = dyn_cast(arithOp)) { + parseExtSI(extSIOp, loc, rewriter, offsetMap); + } else if (auto mulIOp = dyn_cast(arithOp)) { + parseMulI(mulIOp, loc, rewriter, offsetMap); + } else if (auto remSIOp = dyn_cast(arithOp)) { + parseBinaryOp(remSIOp, loc, rewriter, offsetMap); + } else if (auto divSIOp = dyn_cast(arithOp)) { + parseBinaryOp(divSIOp, loc, rewriter, offsetMap); + } else if (auto selectOp = dyn_cast(arithOp)) { + parseSelect(selectOp, loc, rewriter, offsetMap); + } else if (auto fPToSIOp = dyn_cast(arithOp)) { + parseFPToSI(fPToSIOp, loc, rewriter, offsetMap); + } else if (auto sIToFPOp = dyn_cast(arithOp)) { + parseSIToFP(sIToFPOp, loc, rewriter, offsetMap); + } else if (auto mulFOp = dyn_cast(arithOp)) { + parseBinaryOp(mulFOp, loc, rewriter, offsetMap); + } else if (auto divFOp = dyn_cast(arithOp)) { + parseBinaryOp(divFOp, loc, rewriter, offsetMap); + } else if (auto addFOp = dyn_cast(arithOp)) { + parseBinaryOp(addFOp, loc, rewriter, offsetMap); + } else if (auto subFOp = dyn_cast(arithOp)) { + parseBinaryOp(subFOp, loc, rewriter, offsetMap); + } else if (auto minNumFOp = dyn_cast(arithOp)) { + parseBinaryOp(minNumFOp, loc, rewriter, offsetMap); + } else if (auto maxNumFOp = dyn_cast(arithOp)) { + parseBinaryOp(maxNumFOp, loc, rewriter, offsetMap); + } else if (auto maxSIOp = dyn_cast(arithOp)) { + parseBinaryOp(maxSIOp, loc, rewriter, offsetMap); + } else if (auto minSIOp = dyn_cast(arithOp)) { + parseBinaryOp(minSIOp, loc, rewriter, offsetMap); + } else if (auto cmpIOp = dyn_cast(arithOp)) { + parseBinaryOp(cmpIOp, loc, rewriter, offsetMap); + } else if (auto andIOp = dyn_cast(arithOp)) { + parseBinaryOp(andIOp, loc, rewriter, offsetMap); + } else if (auto orIOp = dyn_cast(arithOp)) { + parseBinaryOp(orIOp, loc, rewriter, offsetMap); + } +} + +void parseTritonOp(Operation *tritonOp, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + assert(isa(tritonOp->getDialect())); + if (auto addPtrOp = dyn_cast(tritonOp)) { + parseAddPtr(addPtrOp, loc, rewriter, offsetMap); + } else if (auto splatOp = dyn_cast(tritonOp)) { + parseSplat(splatOp, loc, rewriter, offsetMap); + } else if (auto getProgramIdOp = dyn_cast(tritonOp)) { + parseConstantOp(getProgramIdOp, loc, rewriter, offsetMap); + } else if (auto getNumProgramsOp = + dyn_cast(tritonOp)) { + parseConstantOp(getNumProgramsOp, loc, rewriter, offsetMap); + } else if (auto makeRangeOp = dyn_cast(tritonOp)) { + parseMakeRange(makeRangeOp, loc, rewriter, offsetMap); + } else if (auto bitcastOp = dyn_cast(tritonOp)) { + parseBitcast(bitcastOp, loc, rewriter, offsetMap); + } else if (auto loadOp = dyn_cast(tritonOp)) { + parseLoad(loadOp, loc, rewriter, offsetMap); + } else if (auto broadcastOp = dyn_cast(tritonOp)) { + parseBroadcast(broadcastOp, loc, rewriter, offsetMap); + } else if (auto expandDimsOp = dyn_cast(tritonOp)) { + parseExpandDims(expandDimsOp, loc, rewriter, offsetMap); + } else if (auto reshapeOp = dyn_cast(tritonOp)) { + parseReshape(reshapeOp, loc, rewriter, offsetMap); + } else if (auto clampFOp = dyn_cast(tritonOp)) { + parseClampF(clampFOp, loc, rewriter, offsetMap); + } + // FIXME:Z|wait triton version upgrade to 3.4 + // else if (auto makeTensorDescOp = + // dyn_cast(tritonOp)) { + // parseMakeTensorDesc(makeTensorDescOp, loc, rewriter, offsetMap); + // } + else if (auto makeTensorPtrOp = dyn_cast(tritonOp)) { + parseMakeTensorPtr(makeTensorPtrOp, loc, rewriter, offsetMap); + } else if (auto reduceOp = dyn_cast(tritonOp)) { + parseReduce(reduceOp, loc, rewriter, offsetMap); + } else if (auto reduceReturnOp = dyn_cast(tritonOp)) { + parseReduceReturn(reduceReturnOp, loc, rewriter, offsetMap); + } else if (auto advanceOp = dyn_cast(tritonOp)) { + parseAdvance(advanceOp, loc, rewriter, offsetMap); + } else if (auto intToPtrOp = dyn_cast(tritonOp)) { + parseIntToPtr(intToPtrOp, loc, rewriter, offsetMap); + } +} + +void parseAddPtr(triton::AddPtrOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get addPtr base_ptr + Value ptr = op.getPtr(); + parse(ptr, op.getLoc(), rewriter, offsetMap); + auto ptrInfo = offsetMap.find(ptr); + if (ptrInfo == offsetMap.end()) { + op.emitOpError("could not analyze the pointer base"); + return; + } + // Get addPtr offset + Value offsetValue = op.getOffset(); + PtrOffsetInfo ptrOffsetInfo = ptrInfo->second; + bool isRebuild = op->hasAttr(controlflow::kPointerDescriptorRebuildAttr); + Attribute offsetForm = + op->getAttr(controlflow::kPointerDescriptorOffsetFormAttr); + auto structuredAxes = dyn_cast_or_null( + op->getAttr(controlflow::kPointerDescriptorStructuredAxesAttr)); + auto resultType = dyn_cast(op.getType()); + auto baseSplat = ptr.getDefiningOp(); + bool hasScalarPointerSplatBase = + baseSplat && isa(baseSplat.getSrc().getType()); + SmallVector descriptorAxes; + if (structuredAxes) { + if (!isRebuild || !resultType || + structuredAxes.asArrayRef().size() != + static_cast(resultType.getRank())) { + op.emitOpError("invalid pointer descriptor structured-axis metadata"); + return; + } + for (int32_t axis : structuredAxes.asArrayRef()) { + if (axis == 1) + descriptorAxes.push_back(PtrOffsetInfo::AxisInfo::structured); + else if (axis == 0) + descriptorAxes.push_back(PtrOffsetInfo::AxisInfo::unstructured); + else { + op.emitOpError( + "pointer descriptor structured axes must contain only 0 or 1"); + return; + } + } + } + bool isStridedRankOne = false; + if (offsetForm) { + auto form = dyn_cast(offsetForm); + if (!isRebuild || !form || + form.getValue() != controlflow::kStrided1DOffsetForm) { + op.emitOpError("invalid pointer descriptor offset form"); + return; + } + auto offsetType = dyn_cast(offsetValue.getType()); + auto offsetElementType = + offsetType ? dyn_cast(offsetType.getElementType()) + : IntegerType(); + auto baseSplat = ptr.getDefiningOp(); + if (!resultType || resultType.getRank() != 1 || !offsetType || + !isa(resultType.getElementType()) || + !offsetElementType || offsetElementType.getWidth() > 64 || + resultType.getShape() != offsetType.getShape() || + resultType.getEncoding() != offsetType.getEncoding() || !baseSplat || + !isa(baseSplat.getSrc().getType())) { + op.emitOpError( + "expected strided_1d on a rank-1 tensor pointer rebuilt from a " + "scalar pointer base and a compatible ranked integer offset"); + return; + } + if (descriptorAxes.size() != 1 || + descriptorAxes.front() != PtrOffsetInfo::AxisInfo::structured) { + op.emitOpError( + "strided_1d requires PointerDescriptorStructuredAxes = [1]"); + return; + } + isStridedRankOne = true; + } + bool isCompleteOffsetCarrier = isRebuild && !isStridedRankOne; + bool isDescriptorOwned = + isRebuild || ptrOffsetInfo.isPointerDescriptorOwned(); + bool descriptorIsOpaque = + !descriptorAxes.empty() && + llvm::all_of(descriptorAxes, [](PtrOffsetInfo::AxisInfo axis) { + return axis == PtrOffsetInfo::AxisInfo::unstructured; + }); + + if (isCompleteOffsetCarrier) { + // The carrier is complete relative to the descriptor base, but parsing + // that base may expose an additional displacement from its scalar source. + // Preserve both parts when reconstructing the complete pointer offset. + auto offsetType = dyn_cast(offsetValue.getType()); + auto offsetElementType = + offsetType ? dyn_cast(offsetType.getElementType()) + : IntegerType(); + if (!resultType || !offsetType || !offsetElementType || + resultType.getShape() != offsetType.getShape() || + resultType.getEncoding() != offsetType.getEncoding()) { + op.emitOpError("expected a shape- and encoding-compatible ranked integer " + "complete-offset carrier"); + return; + } + if (offsetElementType.getWidth() > 64) { + op.emitOpError("complete-offset carrier wider than i64 is unsupported"); + return; + } + + SmallVector recoveredAxes; + Value materializedOffset; + if (descriptorIsOpaque && hasScalarPointerSplatBase) { + parse(offsetValue, op.getLoc(), rewriter, offsetMap); + auto carrierInfo = offsetMap.find(offsetValue); + if (carrierInfo != offsetMap.end() && + getRecoverableCarrierAxes(carrierInfo->second, resultType.getRank(), + recoveredAxes)) { + materializedOffset = + materializeAffineForOffsetCarrier(offsetValue, op, rewriter); + if (!materializedOffset) + recoveredAxes.clear(); + } + } + + if (materializedOffset) { + offsetValue = materializedOffset; + op->setOperand(1, offsetValue); + SmallVector structuredAxes(recoveredAxes.size(), 1); + op->setAttr(controlflow::kPointerDescriptorStructuredAxesAttr, + rewriter.getDenseI32ArrayAttr(structuredAxes)); + } + + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + if (offsetElementType.getWidth() != 64) { + auto carrierType = + RankedTensorType::get(offsetType.getShape(), rewriter.getI64Type(), + offsetType.getEncoding()); + offsetValue = rewriter.create(op.getLoc(), carrierType, + offsetValue); + } + Value baseDisplacement = ptrOffsetInfo.getOffset(); + if (!baseDisplacement || + baseDisplacement.getType() != offsetValue.getType()) { + op.emitOpError( + "expected pointer-base displacement compatible with complete " + "carrier"); + return; + } + Value completeOffset = rewriter.create( + op.getLoc(), baseDisplacement, offsetValue); + ptrOffsetInfo.setOffset(completeOffset); + // setUnstructured() updates only the per-axis classification; it does not + // clear the independent scalar-like property inherited from a splatted + // base. A complete offset carrier is intentionally opaque here, so there + // is no proof that all lanes address the same element. Keep this state + // conservative and force the lane-wise memory-access path. + ptrOffsetInfo.setScalarLike(false); + if (!recoveredAxes.empty()) + ptrOffsetInfo.setStructured(recoveredAxes); + else if (!descriptorAxes.empty()) + ptrOffsetInfo.setStructured(descriptorAxes); + else + ptrOffsetInfo.setUnstructured(resultType.getRank()); + ptrOffsetInfo.setPointerDescriptorOwned(true); + offsetMap[op.getResult()] = ptrOffsetInfo; + return; + } + + parse(offsetValue, op.getLoc(), rewriter, offsetMap); + // Modify IR + + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + if (auto offsetType = dyn_cast(offsetValue.getType())) { + auto offsetElementType = cast(offsetType.getElementType()); + if (offsetElementType.getWidth() != 64) { + auto newOffsetType = RankedTensorType::get(offsetType.getShape(), + rewriter.getIntegerType(64), + offsetType.getEncoding()); + offsetValue = rewriter.create(op.getLoc(), newOffsetType, + offsetValue); + } + } else { + auto offsetIntType = cast(offsetValue.getType()); + if (offsetIntType.getWidth() != 64) { + offsetValue = rewriter.create( + op.getLoc(), rewriter.getIntegerType(64), offsetValue); + } + } + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "[parseAddPtr] Adding offset\n"; + os << ptrOffsetInfo.getOffset() << '\n' << offsetValue << '\n'; + }); + Value offset = rewriter.create( + op.getLoc(), ptrOffsetInfo.getOffset(), offsetValue); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "[parseAddPtr] offset is\n" << offset << '\n'; + }); + // Set addPtr offset map + auto dst = op.getResult(); + PtrOffsetInfo offsetOffsetInfo = offsetMap.at(op.getOffset()); + auto dstOffsetInfo = combineInfo(ptrOffsetInfo, offsetOffsetInfo); + dstOffsetInfo.setPtr(ptrOffsetInfo.getPtr()); + dstOffsetInfo.setOffset(offset); + dstOffsetInfo.setPointerDescriptorOwned(isDescriptorOwned); + // CFO's strided_1d descriptor is an explicit proof that a scalar pointer + // base plus this rank-1 lane offset remains a contiguous SIMD access. The + // generic offset join above cannot retain that proof when the lane offset + // was built through several arithmetic operations, so restore the marker's + // classification after the join. Complete opaque carriers intentionally do + // not enter this branch and keep their conservative unstructured state. + if (isStridedRankOne) { + dstOffsetInfo.setStructured(descriptorAxes); + // The descriptor describes a lane-varying, structured offset. It is + // contiguous, but it is not a scalar-like pointer: scalarLike would + // route the load/store converter through splatAndLoadScenario(), which + // loads lane zero once and fills every lane with that value. + dstOffsetInfo.setScalarLike(false); + } + offsetMap[dst] = dstOffsetInfo; + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + auto &ptrStructured = ptrOffsetInfo.getStructuredRef(); + auto &offsetStructured = offsetOffsetInfo.getStructuredRef(); + os << "[parseAddPtr] ptrStructured: "; + for (size_t i = 0; i < ptrStructured.size(); i++) + os << static_cast(ptrStructured[i]); + os << "\n"; + os << "[parseAddPtr] offsetStructured: "; + for (size_t i = 0; i < offsetStructured.size(); i++) + os << static_cast(offsetStructured[i]); + os << "\n"; + }); +} + +void parseSplat(triton::SplatOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get splat src + auto src = op.getSrc(); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + auto dst = op.getResult(); + auto dstType = cast(dst.getType()); + PtrOffsetInfo dstOffsetInfo(srcOffsetInfo.getPtr()); + // Modify IR + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "[parseSplat] dst is\n" << dst << '\n'; + }); + if (isa(dstType.getElementType())) { + RewriterBase::InsertionGuard guard(rewriter); + auto dstShape = dstType.getShape(); + rewriter.setInsertionPoint(op); + Value valueOffset = srcOffsetInfo.getOffset(); + Value offset = rewriter.create( + loc, + RankedTensorType::get(dstShape, rewriter.getIntegerType(64), + dstType.getEncoding()), + valueOffset); + dstOffsetInfo.setOffset(offset); + } + // Set addPtr offset map + auto &dstStructured = dstOffsetInfo.getStructuredRef(); + for (auto dim : dstType.getShape()) + dstStructured.push_back(dim == 1 ? PtrOffsetInfo::AxisInfo::scalar + : PtrOffsetInfo::AxisInfo::scalarlike); + dstOffsetInfo.setScalarLike(true); + dstOffsetInfo.setPointerDescriptorOwned( + srcOffsetInfo.isPointerDescriptorOwned()); + offsetMap[dst] = dstOffsetInfo; +} + +template +void parseBinaryOp(BinOpTy op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + auto lhs = op.getLhs(); + parse(lhs, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo lhsOffsetInfo = offsetMap.at(lhs); + auto &lhsStructured = lhsOffsetInfo.getStructuredRef(); + auto rhs = op.getRhs(); + parse(rhs, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo rhsOffsetInfo = offsetMap.at(rhs); + auto &rhsStructured = rhsOffsetInfo.getStructuredRef(); + auto dst = op->getResult(0); + PtrOffsetInfo dstOffsetInfo; + dstOffsetInfo.setScalarLike(lhsOffsetInfo.isScalarLike() && + rhsOffsetInfo.isScalarLike()); + if (dstOffsetInfo.isScalarLike()) + dstOffsetInfo.setStructured(lhsStructured.size(), + PtrOffsetInfo::AxisInfo::scalarlike); + else + dstOffsetInfo.setUnstructured(lhsStructured.size()); + offsetMap[dst] = dstOffsetInfo; +} + +void parseAddI(arith::AddIOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get addi lhs + auto lhs = op.getLhs(); + parse(lhs, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo lhsOffsetInfo = offsetMap.at(lhs); + // Get addi rhs + auto rhs = op.getRhs(); + parse(rhs, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo rhsOffsetInfo = offsetMap.at(rhs); + // Set addi offset map + auto dst = op.getResult(); + offsetMap[dst] = combineInfo(lhsOffsetInfo, rhsOffsetInfo); +} + +void parseSubI(arith::SubIOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get addi lhs + auto lhs = op.getLhs(); + parse(lhs, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo lhsOffsetInfo = offsetMap.at(lhs); + // Get addi rhs + auto rhs = op.getRhs(); + parse(rhs, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo rhsOffsetInfo = offsetMap.at(rhs); + // Set addi offset map + auto dst = op.getResult(); + offsetMap[dst] = combineInfo(lhsOffsetInfo, rhsOffsetInfo); + if (!(lhsOffsetInfo.isStructured() && rhsOffsetInfo.isScalarLike())) { + offsetMap[dst].setUnstructured(offsetMap[dst].getRank()); + } +} + +void parseIndexCast(arith::IndexCastOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get indexCast input + auto src = op.getIn(); + parse(src, op.getLoc(), rewriter, offsetMap); + // Set indexCast offset map + auto dst = op.getOut(); + auto srcOffsetInfo = offsetMap.at(src); + offsetMap[dst] = srcOffsetInfo; +} + +template +void parseConstantOp(ConstOpTy dst, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Set constant offset map + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setScalarLike(true); + if (auto tensorType = + dyn_cast(dst->getResult(0).getType())) { + auto &dstStructured = offsetMap[dst].getStructuredRef(); + for (auto dim : tensorType.getShape()) + dstStructured.push_back(dim == 1 ? PtrOffsetInfo::AxisInfo::scalar + : PtrOffsetInfo::AxisInfo::scalarlike); + } +} + +void parseMakeRange(triton::MakeRangeOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Set makeRange offset map + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setStructured(1); +} + +void parseExtSI(arith::ExtSIOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get extSI input + auto src = op.getIn(); + parse(src, op.getLoc(), rewriter, offsetMap); + // Set extSI offset map + auto dst = op.getOut(); + auto srcOffsetInfo = offsetMap.at(src); + offsetMap[dst] = srcOffsetInfo; +} + +void parseBitcast(triton::BitcastOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get bitcast src + auto src = op.getSrc(); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + auto &srcStructured = srcOffsetInfo.getStructuredRef(); + // Set extSI offset map + auto dst = op.getResult(); + if (auto ptr = srcOffsetInfo.getPtr()) { + Type ptrType = dst.getType(); + if (auto tensorType = dyn_cast(ptrType)) + ptrType = tensorType.getElementType(); + rewriter.setInsertionPoint(op); + ptr = rewriter.create(loc, ptrType, ptr); + offsetMap[dst] = + PtrOffsetInfo(ptr, srcOffsetInfo.getOffset(), srcStructured); + } else { + offsetMap[dst] = PtrOffsetInfo(srcStructured); + } + offsetMap[dst].setScalarLike(srcOffsetInfo.isScalarLike()); + offsetMap[dst].setPointerDescriptorOwned( + srcOffsetInfo.isPointerDescriptorOwned()); +} + +void parseLoad(triton::LoadOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get load ptr + auto ptr = op.getPtr(); + parse(ptr, op.getLoc(), rewriter, offsetMap); + // Set load offset map + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setScalarLike(offsetMap[ptr].isScalarLike()); + auto tensorType = dyn_cast(dst.getType()); + if (!tensorType) + return; + offsetMap[dst].setUnstructured(tensorType.getRank()); +} + +void parseMulI(arith::MulIOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get muli lhs + auto lhs = op.getLhs(); + parse(lhs, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo lhsOffsetInfo = offsetMap.at(lhs); + auto &lhsStructured = lhsOffsetInfo.getStructuredRef(); + bool lhsScalarLike = lhsOffsetInfo.isScalarLike(); + // Get muli rhs + auto rhs = op.getRhs(); + parse(rhs, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo rhsOffsetInfo = offsetMap.at(rhs); + auto &rhsStructured = rhsOffsetInfo.getStructuredRef(); + bool rhsScalarLike = rhsOffsetInfo.isScalarLike(); + // Set muli offset map + size_t maxSize = std::max(lhsStructured.size(), rhsStructured.size()); + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setScalarLike(lhsScalarLike && rhsScalarLike); + auto &dstStructured = offsetMap[dst].getStructuredRef(); + dstStructured.resize(maxSize); + for (size_t i = 0; i < maxSize; i++) + if (lhsScalarLike) + dstStructured[i] = rhsStructured[i]; + else if (rhsScalarLike) + dstStructured[i] = lhsStructured[i]; + else + dstStructured[i] = PtrOffsetInfo::AxisInfo::unstructured; +} + +void parseBroadcast(triton::BroadcastOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get broadcast src + auto src = op.getSrcMutable().get(); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + auto &srcStructured = srcOffsetInfo.getStructuredRef(); + // Get broadcast dim + auto dst = op.getResult(); + assert(isa(src.getType()) && + "tt.broadcast's input should be a tensor"); + auto srcType = cast(src.getType()); + auto dstType = cast(dst.getType()); + assert(srcType.getRank() == dstType.getRank() && + "rank of source shoule be equal to destnation"); + auto broadcastDim = ConverterUtils::getBroadcastDims(srcType, dstType); + // Set broadcast offset map + offsetMap[dst] = PtrOffsetInfo(srcOffsetInfo.getPtr()); + offsetMap[dst].setScalarLike(srcOffsetInfo.isScalarLike()); + offsetMap[dst].setPointerDescriptorOwned( + srcOffsetInfo.isPointerDescriptorOwned()); + + if (srcOffsetInfo.getPtr()) { + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + Value valueOffset = srcOffsetInfo.getOffset(); + Value offset = rewriter.create( + loc, + RankedTensorType::get(dstType.getShape(), rewriter.getIntegerType(64), + dstType.getEncoding()), + valueOffset); + + offsetMap[dst].setOffset(offset); + } + + auto &dstStructured = offsetMap[dst].getStructuredRef(); + auto dstShape = dstType.getShape(); + dstStructured.resize(srcStructured.size()); + for (size_t i = 0; i < dstStructured.size(); i++) + if (llvm::find(broadcastDim, i) != broadcastDim.end() && dstShape[i] != 1) { + dstStructured[i] = PtrOffsetInfo::AxisInfo::scalarlike; + } else { + dstStructured[i] = srcStructured[i]; + } +} + +void parseExpandDims(triton::ExpandDimsOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get expandDims src + auto src = op.getSrc(); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + auto &srcStructured = srcOffsetInfo.getStructuredRef(); + // Set expandDims offset map + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(srcOffsetInfo.getPtr()); + offsetMap[dst].setScalarLike(srcOffsetInfo.isScalarLike()); + offsetMap[dst].setPointerDescriptorOwned( + srcOffsetInfo.isPointerDescriptorOwned()); + if (srcOffsetInfo.getPtr()) { + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + Value valueOffset = srcOffsetInfo.getOffset(); + Value offset = + rewriter.create(loc, valueOffset, op.getAxis()); + + offsetMap[dst].setOffset(offset); + } + auto &dstStructured = offsetMap[dst].getStructuredRef(); + dstStructured.resize(srcStructured.size() + 1); + size_t j = 0; + for (size_t i = 0; i < dstStructured.size(); i++) + if (i == op.getAxis()) { + dstStructured[i] = PtrOffsetInfo::AxisInfo::scalar; + } else { + dstStructured[i] = srcStructured[j]; + j++; + } +} + +void parseReshape(triton::ReshapeOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + auto dst = op.getResult(); + auto dstType = cast(dst.getType()); + // Numeric reshapes retain the existing conservative classification. + if (!isa(dstType.getElementType())) + return; + + parse(op.getSrc(), op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo info = offsetMap.at(op.getSrc()); + if (!info.getPtr() || !isScalarPointer(info.getPtr()) || + (op.getAllowReorder() && dstType.getEncoding())) { + // Without a common scalar base (or a fixed lane mapping), keep the + // actual result pointers. Never choose one lane's base for every lane. + recordOpaqueTensorPointer(dst, offsetMap); + return; + } + + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + if (op.getAllowReorder()) { + // Choose the order-preserving realization permitted by allow_reorder. + // Remaining pointer users and the derived offset must share this mapping. + rewriter.modifyOpInPlace(op, [&] { op->removeAttr("allow_reorder"); }); + } + auto offsetType = cast(info.getOffset().getType()); + auto reshapedOffsetType = RankedTensorType::get( + dstType.getShape(), offsetType.getElementType(), dstType.getEncoding()); + Value offset = rewriter.create( + op.getLoc(), reshapedOffsetType, info.getOffset(), + /*allowReorder=*/false, op.getEfficientLayout()); + info.setOffset(offset); + // Reshaping complete offsets preserves addresses, but the old per-axis + // structure does not describe the new shape. Scalar-like state must also + // be cleared: setUnstructured alone does not force the lane-wise path. + info.setUnstructured(dstType.getRank()); + info.setScalarLike(false); + offsetMap[dst] = info; +} + +void parseClampF(triton::ClampFOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get clampF src + auto src = op.getX(); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + // Get clampF min + auto clampMin = op.getMin(); + parse(clampMin, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo minOffsetInfo = offsetMap.at(clampMin); + // Get clampF max + auto clampMax = op.getMax(); + parse(clampMax, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo maxOffsetInfo = offsetMap.at(clampMax); + // Set clampF offset map + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setScalarLike(srcOffsetInfo.isScalarLike() && + minOffsetInfo.isScalarLike() && + maxOffsetInfo.isScalarLike()); + auto dstType = dyn_cast(dst.getType()); + if (!dstType) + return; + offsetMap[dst].setUnstructured(dstType.getRank()); +} + +void parseSelect(arith::SelectOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + if (isScalarPointer(op.getResult())) { + recordOpaqueScalarPointer(op.getResult(), offsetMap); + return; + } + + if (isTensorPointer(op.getResult())) { + recordOpaqueTensorPointer(op.getResult(), offsetMap); + return; + } + + // Get select condition + auto condition = op.getCondition(); + parse(condition, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo conditionOffsetInfo = offsetMap.at(condition); + bool conditionScalarLike = conditionOffsetInfo.isScalarLike(); + // Get select trueValue + auto trueValue = op.getTrueValue(); + parse(trueValue, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo trueValueOffsetInfo = offsetMap.at(trueValue); + auto &trueValueStructured = trueValueOffsetInfo.getStructuredRef(); + bool trueValueScalarLike = trueValueOffsetInfo.isScalarLike(); + // Get select falseValue + auto falseValue = op.getFalseValue(); + parse(falseValue, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo falseValueOffsetInfo = offsetMap.at(falseValue); + auto &falseValueStructured = falseValueOffsetInfo.getStructuredRef(); + bool falseValueScalarLike = falseValueOffsetInfo.isScalarLike(); + // Set select offset map + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(); + auto dstType = dyn_cast(dst.getType()); + if (!dstType) + return; + + auto dstIsScalar = + trueValueScalarLike && falseValueScalarLike && conditionScalarLike; + offsetMap[dst].setScalarLike(dstIsScalar); + + auto &dstStructured = offsetMap[dst].getStructuredRef(); + dstStructured.resize(trueValueStructured.size()); + for (size_t i = 0; i < dstStructured.size(); i++) + dstStructured[i] = (dstIsScalar) ? PtrOffsetInfo::AxisInfo::scalarlike + : PtrOffsetInfo::AxisInfo::unstructured; +} + +void parseFPToSI(arith::FPToSIOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get FPToSI src + auto src = op.getIn(); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + // Set FPToSI offset map + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setScalarLike(srcOffsetInfo.isScalarLike()); + auto dstType = dyn_cast(dst.getType()); + if (!dstType) + return; + if (offsetMap[dst].isScalarLike()) + offsetMap[dst].setStructured(dstType.getRank(), + PtrOffsetInfo::AxisInfo::scalarlike); + else + offsetMap[dst].setUnstructured(dstType.getRank()); +} + +void parseSIToFP(arith::SIToFPOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get SIToFP src + auto src = op.getIn(); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + // Set SIToFP offset map + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setScalarLike(srcOffsetInfo.isScalarLike()); + auto dstType = dyn_cast(dst.getType()); + if (!dstType) + return; + if (offsetMap[dst].isScalarLike()) + offsetMap[dst].setStructured(dstType.getRank(), + PtrOffsetInfo::AxisInfo::scalarlike); + else + offsetMap[dst].setUnstructured(dstType.getRank()); +} + +// FIXME:Z|wait triton version upgrade to 3.4 +// void parseMakeTensorDesc(triton::MakeTensorDescOp op, const Location &loc, +// RewriterBase &rewriter, +// llvm::DenseMap &offsetMap) { +// // Set MakeTensorDesc offset map +// auto dst = op.getResult(); +// offsetMap[dst] = PtrOffsetInfo(); +// auto dstType = dyn_cast(dst.getType()); +// if (!dstType) +// return; +// offsetMap[dst].setStructured(dstType.getRank()); +// } + +void parseMakeTensorPtr(triton::MakeTensorPtrOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Set MakeTensorPtr offset map + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(dst); + auto dstType = dyn_cast( + cast(dst.getType()).getPointeeType()); + if (!dstType) + return; + offsetMap[dst].setStructured(dstType.getRank()); + offsetMap[dst].setOffsets(op.getOffsets()); +} + +void parseAdvance(triton::AdvanceOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Set Advance offset map + auto ptr = op.getPtr(); + parse(ptr, op.getLoc(), rewriter, offsetMap); + auto dst = op.getResult(); + auto ptrOffsetInfo = offsetMap.at(ptr); + offsetMap[dst] = ptrOffsetInfo; + auto dstType = dyn_cast( + cast(dst.getType()).getPointeeType()); + if (!dstType) + return; + offsetMap[dst].setStructured(dstType.getRank()); + auto &offsets = offsetMap[dst].getOffsetsRef(); + + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + for (auto [curOffset, opOffset] : llvm::zip(offsets, op.getOffsets())) { + curOffset = + rewriter.create(op.getLoc(), curOffset, opOffset); + } +} + +void parseReduce(triton::ReduceOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get reduce src + Value src = op->getOperand(0); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + auto &srcStructured = srcOffsetInfo.getStructuredRef(); + // Set reduce offset map + Value dst = op->getResult(0); + auto dstType = dyn_cast(dst.getType()); + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setScalarLike(srcOffsetInfo.isScalarLike()); + if (!dstType) + return; + auto &dstStructured = offsetMap[dst].getStructuredRef(); + auto dstShape = dstType.getShape(); + dstStructured.resize(dstShape.size()); + for (size_t i = 0; i < dstStructured.size(); i++) + if (dstShape[i] == 1) + dstStructured[i] = PtrOffsetInfo::AxisInfo::scalar; + else + dstStructured[i] = srcStructured[i]; +} + +void parseReduceReturn(triton::ReduceReturnOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get reduce src + Value src = op->getOperand(0); + parse(src, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo srcOffsetInfo = offsetMap.at(src); + auto &srcStructured = srcOffsetInfo.getStructuredRef(); + // Set reduce offset map + Value dst = op->getResult(0); + auto dstType = dyn_cast(dst.getType()); + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setScalarLike(srcOffsetInfo.isScalarLike()); + if (!dstType) + return; + auto &dstStructured = offsetMap[dst].getStructuredRef(); + auto dstShape = dstType.getShape(); + dstStructured.resize(dstShape.size()); + for (size_t i = 0; i < dstStructured.size(); i++) + if (dstShape[i] == 1) + dstStructured[i] = PtrOffsetInfo::AxisInfo::scalar; + else + dstStructured[i] = srcStructured[i]; +} + +void parseIf(scf::IfOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap, Value dst) { + if (isScalarPointer(dst)) { + recordOpaqueScalarPointer(dst, offsetMap); + return; + } + + const unsigned int index = cast(dst).getResultNumber(); + // Get if then region + Block &thenBlock = op.getThenRegion().front(); + Value thenYieldedValue = thenBlock.getTerminator()->getOperand(index); + parse(thenYieldedValue, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo thenOffsetInfo = offsetMap.at(thenYieldedValue); + auto &thenStructured = thenOffsetInfo.getStructuredRef(); + auto thenSrcPtr = thenOffsetInfo.getPtr(); + // Get if else region + bool dstIsScalar = thenOffsetInfo.isScalarLike(); + bool descriptorOwned = thenOffsetInfo.isPointerDescriptorOwned(); + SmallVector elseStructured; + if (op.elseBlock()) { + Block &elseBlock = op.getElseRegion().front(); + Value elseYieldedValue = elseBlock.getTerminator()->getOperand(index); + parse(elseYieldedValue, op.getLoc(), rewriter, offsetMap); + PtrOffsetInfo elseOffsetInfo = offsetMap.at(elseYieldedValue); + elseStructured = elseOffsetInfo.getStructuredRef(); + dstIsScalar = dstIsScalar && elseOffsetInfo.isScalarLike(); + descriptorOwned = + descriptorOwned && elseOffsetInfo.isPointerDescriptorOwned(); + if (thenSrcPtr != elseOffsetInfo.getPtr()) { + emitError(loc) + << "Currently ptr type from different source not supported"; + } + } + + // Set if offset map + offsetMap[dst] = PtrOffsetInfo(); + offsetMap[dst].setPtr(thenSrcPtr); + offsetMap[dst].setScalarLike(dstIsScalar); + offsetMap[dst].setPointerDescriptorOwned(descriptorOwned); + auto &dstStructured = offsetMap[dst].getStructuredRef(); + dstStructured.resize(thenStructured.size()); + for (size_t i = 0; i < dstStructured.size(); i++) + if (op.elseBlock()) + dstStructured[i] = (dstIsScalar) ? PtrOffsetInfo::AxisInfo::scalarlike + : PtrOffsetInfo::AxisInfo::unstructured; + else + dstStructured[i] = thenStructured[i]; + SmallVector dstOffsets(thenOffsetInfo.getOffsetsRef().size()); + if (!dstOffsets.empty()) { + // Assumes ifOp is already rewritten + for (size_t i = 0; i < dstOffsets.size(); i++) + dstOffsets[i] = op->getResult(index + i); + offsetMap[dst].setOffsets(dstOffsets); + } +} + +void parseYield(scf::YieldOp op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get yield src + for (auto src : op->getOperands()) + parse(src, op.getLoc(), rewriter, offsetMap); +} + +void parseLoopOp(LoopLikeOpInterface op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap, Value dst) { + if (isScalarPointer(dst) && isPointerDescriptorBoundaryResult(op, dst)) { + recordOpaqueScalarPointer(dst, offsetMap); + return; + } + + // Apply the same boundary-slot rule to results so entry, region arguments, + // backedges and zero-trip results cannot disagree about the representation. + if (isTensorPointer(dst) && isPointerDescriptorBoundaryResult(op, dst)) { + recordOpaqueTensorPointer(dst, offsetMap); + return; + } + + auto resNum = cast(dst).getResultNumber(); + Value yieldedValue = nullptr; + if (auto whileOp = dyn_cast(op.getOperation())) { + yieldedValue = whileOp.getConditionOp().getArgs()[resNum]; + } else { + yieldedValue = op.getYieldedValues()[resNum]; + } + parse(yieldedValue, op.getLoc(), rewriter, offsetMap); + auto yieldOffsetInfo = offsetMap.at(yieldedValue); + offsetMap[dst] = yieldOffsetInfo; +} + +void parseExtractSlice(tensor::ExtractSliceOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get extractSlice src + auto src = op.getSource(); + parse(src, op.getLoc(), rewriter, offsetMap); + // Set extractSlice offset map + auto dst = op.getResult(); + auto srcPtrInfo = offsetMap.at(src); + auto srcPtr = srcPtrInfo.getPtr(); + auto srcOffset = srcPtrInfo.getOffset(); + auto srcStructured = srcPtrInfo.getStructured(); + auto droppedDims = op.getDroppedDims(); + if (srcOffset) { + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + auto offsetType = getExtractSlicedType(op.getMixedSizes(), droppedDims, + getElementTypeOrSelf(srcOffset)); + srcOffset = rewriter.create( + op.getLoc(), offsetType, srcOffset, op.getMixedOffsets(), + op.getMixedSizes(), op.getMixedStrides()); + } + SmallVector dstStructured; + for (size_t i = 0; i < srcStructured.size(); i++) { + if (!droppedDims[i]) + dstStructured.push_back(srcStructured[i]); + } + offsetMap[dst] = PtrOffsetInfo(srcPtr, srcOffset, dstStructured); + offsetMap[dst].setPointerDescriptorOwned( + srcPtrInfo.isPointerDescriptorOwned()); +} + +void parseInsertSlice(tensor::InsertSliceOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + // Get insertSlice src and dst + auto src = op.getSource(); + parse(src, op.getLoc(), rewriter, offsetMap); + auto dst = op.getDest(); + parse(dst, op.getLoc(), rewriter, offsetMap); + // Set insertSlice offset map + auto res = op.getResult(); + auto srcPtrInfo = offsetMap.at(src); + auto dstPtrInfo = offsetMap.at(dst); + PtrOffsetInfo resPtrInfo; + if (auto srcOffset = srcPtrInfo.getOffset()) { + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + auto resOffset = rewriter.create( + op.getLoc(), srcOffset, dstPtrInfo.getOffset(), op.getMixedOffsets(), + op.getMixedSizes(), op.getMixedStrides()); + auto srcPtr = srcPtrInfo.getPtr(); + auto dstPtr = dstPtrInfo.getPtr(); + assert(srcPtr == dstPtr && "ptrInfo for insert slice should be consistent"); + resPtrInfo.setPtr(srcPtr); + resPtrInfo.setOffset(resOffset); + } + auto droppedDims = op.getDroppedDims(); + auto srcStructuredIter = srcPtrInfo.getStructured().begin(); + SmallVector resStructured; + auto srcShape = op.getStaticSizes(); + auto dstShape = cast(dst.getType()).getShape(); + for (size_t i = 0; i < dstShape.size(); i++) { + if (!ShapedType::isDynamic(srcShape[i]) && srcShape[i] == dstShape[i]) { + resStructured.push_back(*srcStructuredIter); + } else { + resStructured.push_back(PtrOffsetInfo::AxisInfo::unstructured); + } + if (!droppedDims[i]) + ++srcStructuredIter; + } + resPtrInfo.setStructured(resStructured); + resPtrInfo.setPointerDescriptorOwned(srcPtrInfo.isPointerDescriptorOwned() && + dstPtrInfo.isPointerDescriptorOwned()); + offsetMap[res] = resPtrInfo; +} + +void parseExtract(tensor::ExtractOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + auto parentValue = op.getTensor(); + parse(parentValue, op.getLoc(), rewriter, offsetMap); + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(); + if (isa(dst.getType())) { + offsetMap[dst].setPtr(dst); + offsetMap[dst].setZeroOffset(); + } + offsetMap[dst].setScalarLike(true); +} + +void parseInsert(tensor::InsertOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + auto src = op.getScalar(); + parse(src, op.getLoc(), rewriter, offsetMap); + auto dst = op.getDest(); + parse(dst, op.getLoc(), rewriter, offsetMap); + + auto res = op.getResult(); + auto srcPtrInfo = offsetMap.at(src); + auto dstPtrInfo = offsetMap.at(dst); + + PtrOffsetInfo resPtrInfo; + if (auto srcOffset = srcPtrInfo.getOffset()) { + RewriterBase::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op); + auto resOffset = rewriter.create( + op.getLoc(), srcOffset, dstPtrInfo.getOffset(), op.getIndices()); + auto srcPtr = srcPtrInfo.getPtr(); + auto dstPtr = dstPtrInfo.getPtr(); + resPtrInfo.setPtr(srcPtr); + resPtrInfo.setOffset(resOffset); + } + resPtrInfo.setUnstructured(dstPtrInfo.getRank()); + resPtrInfo.setPointerDescriptorOwned(srcPtrInfo.isPointerDescriptorOwned() && + dstPtrInfo.isPointerDescriptorOwned()); + offsetMap[res] = resPtrInfo; +} + +void parseIntToPtr(triton::IntToPtrOp op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + auto dst = op.getResult(); + offsetMap[dst] = PtrOffsetInfo(dst); + offsetMap[dst].setScalarLike(true); +} + +namespace { +template +void parseStructuredCustomOpImpl( + CustomOpT op, const Location &loc, RewriterBase &rewriter, + llvm::DenseMap &offsetMap, unsigned int resultIdx) { + for (auto operand : op.getInputs()) { + parse(operand, op->getLoc(), rewriter, offsetMap); + } + auto dst = op->getResult(resultIdx); + offsetMap[dst] = PtrOffsetInfo(); + auto tensorType = dyn_cast(dst.getType()); + if (!tensorType) { + if (isa(dst.getType())) { + offsetMap[dst].setPtr(dst); + offsetMap[dst].setZeroOffset(); + } else if (isa(dst.getType())) { + offsetMap[dst].setOffset(dst); + } else { + emitError(loc) << "Unsupported return type for hivm custom op: " + << dst.getType(); + } + return; + } + if (llvm::isa(tensorType.getElementType())) { + if (checkStructureAnnotated(op, rewriter)) { + auto srcValArrayAttr = op->template getAttrOfType( + ConverterUtils::customSrcPtrIndexAttrName); + assert(srcValArrayAttr && + "structure hivm custom op should present src tensor"); + auto srcValArray = srcValArrayAttr.asArrayRef(); + assert(srcValArray[resultIdx] != -1 && + "tensor result should map to src tensor"); + auto srcOffsetInfo = offsetMap[op->getOperand(srcValArray[resultIdx])]; + offsetMap[dst] = srcOffsetInfo; + return; + } + emitError(loc) << "Unsupported return unstructure RankedTensor of tt.ptr " + "for hivm custom op: " + << dst; + } + offsetMap[dst].setUnstructured(tensorType.getRank()); +} +} // namespace + +void parseStructuredCustomOp(Operation *op, const Location &loc, + RewriterBase &rewriter, + llvm::DenseMap &offsetMap, + unsigned int resultIdx) { + if (auto customOp = dyn_cast(op)) { + parseStructuredCustomOpImpl(customOp, loc, rewriter, offsetMap, resultIdx); + } else if (auto macroOp = dyn_cast(op)) { + parseStructuredCustomOpImpl(macroOp, loc, rewriter, offsetMap, resultIdx); + } else { + llvm_unreachable("expected hivm custom op"); + } +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/TritonToUnstructure/ReplaceArguments.cpp b/third_party/ascend/lib/TritonToUnstructure/ReplaceArguments.cpp new file mode 100644 index 0000000000..76ec6825a6 --- /dev/null +++ b/third_party/ascend/lib/TritonToUnstructure/ReplaceArguments.cpp @@ -0,0 +1,733 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonControlFlowOpt/ControlFlowRewrite.h" +#include "TritonToUnstructure/UnstructureConversionPass.h" +#include "Utils/Utils.h" + +#include "mlir/IR/Block.h" + +#include "llvm/Support/Debug.h" + +#define DEBUG_TYPE "triton-replace-arguments" + +using namespace mlir; +using namespace triton; + +namespace { + +inline constexpr llvm::StringLiteral kScalarPointerCarrierBoundaryAttr = + "ScalarPointerCarrierBoundary"; + +static bool isScalarPointerType(Type type) { + auto pointerType = dyn_cast(type); + return pointerType && !isa(pointerType.getPointeeType()); +} + +static bool isPointerDescriptorBoundarySlot(Operation *loop, unsigned slot) { + if (!loop) + return false; + auto slots = dyn_cast_or_null( + loop->getAttr(controlflow::kPointerDescriptorBoundaryAttr)); + return slots && + llvm::is_contained(slots.asArrayRef(), static_cast(slot)); +} + +// Only scalar pointers owned by the CFO descriptor schema cross a loop as a +// complete i64 address. Ordinary scalar pointers retain the established +// base-plus-relative-offset representation. +static bool useCompleteScalarAddress(Operation *loop, unsigned slot, + Type type) { + return loop && isa(loop) && + isScalarPointerType(type) && + (isPointerDescriptorBoundarySlot(loop, slot) || + loop->hasAttr(kScalarPointerCarrierBoundaryAttr)); +} + +// Record the exact loop slot whose block argument is changed from a scalar +// pointer to an i64 relative offset. The marker is deliberately attached to +// the cloned SCF op rather than inferred later from type alone: ordinary i64 +// loop-carried values must retain their established analysis. +static void markScalarPointerOffsetSlot(Value value) { + auto blockArg = dyn_cast(value); + if (!blockArg) + return; + Operation *owner = blockArg.getOwner()->getParentOp(); + if (!owner) + return; + + unsigned slot = 0; + if (auto forOp = dyn_cast(owner)) { + if (blockArg.getOwner() != forOp.getBody() || blockArg.getArgNumber() == 0) + return; + slot = blockArg.getArgNumber() - 1; + } else if (auto whileOp = dyn_cast(owner)) { + if (blockArg.getOwner() != whileOp.getBeforeBody() && + blockArg.getOwner() != whileOp.getAfterBody()) + return; + slot = blockArg.getArgNumber(); + } else { + return; + } + + SmallVector slots; + if (auto existing = dyn_cast_or_null( + owner->getAttr(kScalarPointerOffsetBoundaryAttr))) + slots.append(existing.asArrayRef().begin(), existing.asArrayRef().end()); + if (!llvm::is_contained(slots, static_cast(slot))) + slots.push_back(static_cast(slot)); + owner->setAttr(kScalarPointerOffsetBoundaryAttr, + DenseI32ArrayAttr::get(owner->getContext(), slots)); +} + +// A scalar-pointer SCF slot is represented by one complete i64 address on all +// structural edges. Region-local pointer users are rebuilt immediately, so no +// pointer type crosses the control-flow boundary. +static Value rebuildScalarPointer(Value address, Type pointerType, + OpBuilder &builder, Location loc) { + return builder.create(loc, pointerType, address); +} + +static Value materializeScalarPointerAddress(Value pointer, OpBuilder &builder, + Location loc) { + return builder.create(loc, builder.getI64Type(), pointer); +} + +static bool hasScalarPointerBase(const PtrOffsetInfo &info) { + return info.getPtr() && isScalarPointerType(info.getPtr().getType()); +} + +// A function argument is a stable pointer base: it dominates the enclosing +// loop and carries no control-flow-selected address. Offset analysis records +// this base with `ptr == value` and a zero displacement, so that identity is +// valid for a loop init but must not be confused with a loop phi or an +// int_to_ptr result elsewhere. +static bool isStableFunctionScalarPointerBase(Value value) { + auto blockArg = dyn_cast(value); + if (!blockArg || !isScalarPointerType(value.getType())) + return false; + Operation *owner = blockArg.getOwner()->getParentOp(); + return owner && isa(owner); +} + +// Offset analysis deliberately represents a scalar pointer selected by +// scf.if as an opaque complete address. If such a value is a loop backedge, +// the loop must choose the complete-address protocol before any edge is +// rewritten; otherwise the init/region argument can become i64 while the +// yield remains a pointer. +static bool isOpaqueScalarPointerIfResult(Value value) { + return isScalarPointerType(value.getType()) && + value.getDefiningOp(); +} + +// A scalar pointer can use the established T2U offset representation only if +// analysis found both a source pointer and a displacement. An entry whose +// source is the value itself is normally an opaque complete address (for +// example a control-flow phi). A function argument is the one exception when +// it is examined as the init anchor of an ordinary loop; the loop conversion +// must then materialize a zero offset for that anchor. +static bool +canRewriteScalarPointer(Value value, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + if (!isScalarPointerType(value.getType())) + return true; + + parse(value, value.getLoc(), rewriter, offsetMap); + auto it = offsetMap.find(value); + if (it == offsetMap.end()) + return false; + const PtrOffsetInfo &info = it->second; + return info.getPtr() && info.getOffset() && info.getPtr() != value; +} + +// Decide the representation once for the complete SCF boundary. The caller +// passes this same decision to every init, region argument, yield, condition +// operand, and result rewrite. Thus a failed proof falls back atomically to +// the pointer representation instead of producing a mixed-type boundary. +static bool +shouldPreserveScalarPointers(Operation *op, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + auto canRewriteBoundary = [&](Value value, bool allowStableBase) { + if (!isScalarPointerType(value.getType())) + return true; + if (allowStableBase && isStableFunctionScalarPointerBase(value)) + return true; + return canRewriteScalarPointer(value, rewriter, offsetMap); + }; + + if (auto whileOp = dyn_cast(op)) { + for (Value init : whileOp.getInits()) + if (!canRewriteBoundary(init, /*allowStableBase=*/true)) + return true; + for (Value arg : whileOp.getBeforeArguments()) + if (!canRewriteBoundary(arg, /*allowStableBase=*/false)) + return true; + for (Value arg : whileOp.getAfterArguments()) + if (!canRewriteBoundary(arg, /*allowStableBase=*/false)) + return true; + for (Value value : whileOp.getYieldOp().getOperands()) + if (isOpaqueScalarPointerIfResult(value)) + return true; + return false; + } else if (auto loopOp = dyn_cast(op)) { + for (Value init : loopOp.getInits()) + if (!canRewriteBoundary(init, /*allowStableBase=*/true)) + return true; + for (Value arg : loopOp.getRegionIterArgs()) + if (!canRewriteBoundary(arg, /*allowStableBase=*/false)) + return true; + for (Value value : loopOp.getYieldedValues()) + if (isOpaqueScalarPointerIfResult(value)) + return true; + return false; + } else if (auto ifOp = dyn_cast(op)) { + for (Value value : ifOp->getResults()) + if (!canRewriteBoundary(value, /*allowStableBase=*/false)) + return true; + for (Value value : ifOp.thenYield().getResults()) + if (!canRewriteBoundary(value, /*allowStableBase=*/false)) + return true; + if (ifOp.elseBlock()) + for (Value value : ifOp.elseYield().getResults()) + if (!canRewriteBoundary(value, /*allowStableBase=*/false)) + return true; + } else { + return false; + } + return false; +} + +static bool hasScalarPointerResult(scf::IfOp op) { + return llvm::any_of(op->getResultTypes(), + [](Type type) { return isScalarPointerType(type); }); +} + +static bool hasScalarPointerBoundary(Operation *op) { + SmallVector values; + if (auto whileOp = dyn_cast(op)) { + values.append(whileOp.getInits().begin(), whileOp.getInits().end()); + values.append(whileOp.getBeforeArguments().begin(), + whileOp.getBeforeArguments().end()); + values.append(whileOp.getAfterArguments().begin(), + whileOp.getAfterArguments().end()); + values.append(whileOp->getResults().begin(), whileOp->getResults().end()); + values.append(whileOp.getConditionOp().getArgs().begin(), + whileOp.getConditionOp().getArgs().end()); + values.append(whileOp.getYieldOp()->getOperands().begin(), + whileOp.getYieldOp()->getOperands().end()); + } else if (auto forOp = dyn_cast(op)) { + values.append(forOp.getInitArgs().begin(), forOp.getInitArgs().end()); + values.append(forOp.getRegionIterArgs().begin(), + forOp.getRegionIterArgs().end()); + values.append(forOp->getResults().begin(), forOp->getResults().end()); + values.append(forOp.getYieldedValues().begin(), + forOp.getYieldedValues().end()); + } else { + return false; + } + return llvm::any_of( + values, [](Value value) { return isScalarPointerType(value.getType()); }); +} + +static void markOpaqueScalarPointerBoundary(Operation *op) { + if (hasScalarPointerBoundary(op)) + op->setAttr(kScalarPointerCarrierBoundaryAttr, + UnitAttr::get(op->getContext())); +} + +// `shouldPreserveScalarPointers` analyzes every value on the SCF boundary to +// choose one representation. When the choice is relative offsets, those +// cached entries describe the old pointer-typed boundary and must be removed +// before replaceArgs/replaceOperands rebuild the boundary. Keeping even one +// stale yield or result entry is enough to bypass the live backedge analysis. +static void invalidateScalarPointerBoundaryAnalysis( + Operation *op, llvm::DenseMap &offsetMap) { + auto eraseScalarPointers = [&](ValueRange values) { + for (Value value : values) + if (isScalarPointerType(value.getType())) + offsetMap.erase(value); + }; + + if (auto whileOp = dyn_cast(op)) { + eraseScalarPointers(whileOp.getInits()); + eraseScalarPointers(whileOp.getBeforeArguments()); + eraseScalarPointers(whileOp.getAfterArguments()); + eraseScalarPointers(whileOp->getResults()); + eraseScalarPointers(whileOp.getConditionOp().getArgs()); + eraseScalarPointers(whileOp.getYieldOp()->getOperands()); + return; + } + if (auto loopOp = dyn_cast(op)) { + eraseScalarPointers(loopOp.getInits()); + eraseScalarPointers(loopOp.getRegionIterArgs()); + eraseScalarPointers(loopOp->getResults()); + eraseScalarPointers(loopOp.getYieldedValues()); + return; + } + if (auto ifOp = dyn_cast(op)) { + eraseScalarPointers(ifOp->getResults()); + eraseScalarPointers(ifOp.thenYield().getResults()); + if (ifOp.elseBlock()) + eraseScalarPointers(ifOp.elseYield().getResults()); + } +} + +} // namespace + +void replaceOperands(MutableArrayRef oprs, RewriterBase &rewriter, + llvm::DenseMap &offsetMap, + bool preserveScalarPointers, + bool allowStableBaseInit = false) { + for (auto it = oprs.begin(); it != oprs.end(); ++it) { + auto &opr = *it; + auto operand = opr.get(); + if (auto tensorType = dyn_cast(operand.getType()); + tensorType && isa(tensorType.getElementType())) { + parse(operand, operand.getLoc(), rewriter, offsetMap); + const PtrOffsetInfo &info = offsetMap.at(operand); + // A complete opaque tensor base cannot be represented as an offset from + // one scalar source. Keep this structural edge unchanged instead of + // creating a type-mismatched loop signature. + if (hasScalarPointerBase(info)) + opr.set(info.getOffset()); + } else if (auto ptrType = + dyn_cast(operand.getType())) { + // An unmarked scalar pointer has no complete structural carrier schema. + // Keep it as a pointer on every SCF edge instead of changing only this + // operand to a relative offset and invalidating the parent operation. + if (preserveScalarPointers && isScalarPointerType(operand.getType())) + continue; + parse(operand, operand.getLoc(), rewriter, offsetMap); + if (allowStableBaseInit && isStableFunctionScalarPointerBase(operand)) { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(opr.getOwner()); + Value zero = + rewriter.create(operand.getLoc(), 0, 64); + opr.set(zero); + continue; + } + if (auto tensorType = + dyn_cast(ptrType.getPointeeType())) { + for (auto offset : offsetMap.at(operand).getOffsets()) { + it->set(offset); + ++it; + } + --it; + } else { + // source == operand marks a complete scalar address whose source may be + // selected or loop-carried at runtime. Keep it as a pointer instead of + // replacing it with an offset relative to one statically chosen base. + if (offsetMap.at(operand).getPtr() == operand) + continue; + opr.set(offsetMap.at(operand).getOffset()); + } + } + } +} + +void replaceArgs(ValueRange args, RewriterBase &rewriter, + llvm::DenseMap &offsetMap, + bool preserveScalarPointers) { + for (auto it = args.begin(); it != args.end(); ++it) { + auto arg = *it; + if (auto tensorType = dyn_cast(arg.getType()); + tensorType && isa(tensorType.getElementType())) { + parse(arg, arg.getLoc(), rewriter, offsetMap); + const PtrOffsetInfo &info = offsetMap.at(arg); + // The relative-offset representation is legal only with one scalar + // element-pointer base. Opaque tensor bases remain pointer values and + // are handled lane-wise (or rejected later by the boundary validator). + if (!hasScalarPointerBase(info)) + continue; + + RewriterBase::InsertionGuard guard(rewriter); + if (auto blockArg = dyn_cast(arg)) { + rewriter.setInsertionPointToStart(blockArg.getOwner()); + } else { + rewriter.setInsertionPointAfterValue(arg); + } + auto tempVar = rewriter + .create( + arg.getLoc(), arg.getType(), ValueRange({})) + ->getResult(0); + Value src = info.getPtr(); + rewriter.replaceAllUsesWith(arg, tempVar); + arg.setType(RankedTensorType::get(tensorType.getShape(), + rewriter.getIntegerType(64))); + src = rewriter.create(arg.getLoc(), tempVar.getType(), + src); + rewriter.replaceOpWithNewOp( + tempVar.getDefiningOp(), tempVar.getType(), src, arg); + } else if (auto ptrType = dyn_cast(arg.getType())) { + // Region arguments and results must use the same representation as the + // corresponding init/condition/yield operands. + if (preserveScalarPointers && isScalarPointerType(arg.getType())) + continue; + parse(arg, arg.getLoc(), rewriter, offsetMap); + if (!isa(ptrType.getPointeeType()) && + offsetMap.at(arg).getPtr() == arg) + continue; + + RewriterBase::InsertionGuard guard(rewriter); + if (auto blockArg = dyn_cast(arg)) { + rewriter.setInsertionPointToStart(blockArg.getOwner()); + } else { + rewriter.setInsertionPointAfterValue(arg); + } + auto tempVar = rewriter + .create( + arg.getLoc(), arg.getType(), ValueRange({})) + ->getResult(0); + rewriter.replaceAllUsesWith(arg, tempVar); + if (auto tensorType = + dyn_cast(ptrType.getPointeeType())) { + auto srcOp = + offsetMap.at(arg).getPtr().getDefiningOp(); + arg.setType(rewriter.getIntegerType(32)); + SmallVector newOffsets; + for (auto offset : offsetMap.at(arg).getOffsets()) { + newOffsets.push_back(*it); + ++it; + } + --it; + rewriter.replaceOpWithNewOp( + tempVar.getDefiningOp(), tempVar.getType(), srcOp.getBase(), + srcOp.getShape(), srcOp.getStrides(), newOffsets, srcOp.getOrder()); + } else { + auto src = offsetMap.at(arg).getPtr(); + offsetMap.erase(arg); + arg.setType(rewriter.getIntegerType(64)); + rewriter.replaceOpWithNewOp( + tempVar.getDefiningOp(), tempVar.getType(), src, arg); + } + } + } +} + +void convertTensorPtrPre(Operation *op, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "[convertTensorPtr]: Preorder start\n" << *op << "\n"; + }); + bool preserveScalarPointers = + shouldPreserveScalarPointers(op, rewriter, offsetMap); + if (!preserveScalarPointers) + invalidateScalarPointerBoundaryAnalysis(op, offsetMap); + SmallVector scalarPointerOffsetArgs; + if (auto whileOp = dyn_cast(op)) { + if (!preserveScalarPointers) + llvm::copy_if(whileOp.getBeforeArguments(), + std::back_inserter(scalarPointerOffsetArgs), [](Value arg) { + return isScalarPointerType(arg.getType()); + }); + replaceArgs(whileOp.getBeforeArguments(), rewriter, offsetMap, + preserveScalarPointers); + replaceOperands(whileOp.getInitsMutable(), rewriter, offsetMap, + preserveScalarPointers, + /*allowStableBaseInit=*/true); + replaceArgs(whileOp.getAfterArguments(), rewriter, offsetMap, + preserveScalarPointers); + replaceArgs(whileOp->getResults(), rewriter, offsetMap, + preserveScalarPointers); + replaceOperands(whileOp.getConditionOp().getArgsMutable(), rewriter, + offsetMap, preserveScalarPointers); + } else if (auto loopOp = dyn_cast(op)) { + if (!preserveScalarPointers) + llvm::copy_if(loopOp.getRegionIterArgs(), + std::back_inserter(scalarPointerOffsetArgs), [](Value arg) { + return isScalarPointerType(arg.getType()); + }); + replaceArgs(loopOp.getRegionIterArgs(), rewriter, offsetMap, + preserveScalarPointers); + replaceOperands(loopOp.getInitsMutable(), rewriter, offsetMap, + preserveScalarPointers, + /*allowStableBaseInit=*/true); + } else if (auto ifOp = dyn_cast(op)) { + if (preserveScalarPointers && hasScalarPointerResult(ifOp)) + ifOp->setAttr(kScalarPointerCarrierBoundaryAttr, + UnitAttr::get(ifOp.getContext())); + replaceArgs(ifOp->getResults(), rewriter, offsetMap, + preserveScalarPointers); + replaceOperands(ifOp.thenYield().getResultsMutable(), rewriter, offsetMap, + preserveScalarPointers); + replaceOperands(ifOp.elseYield().getResultsMutable(), rewriter, offsetMap, + preserveScalarPointers); + } + // Publish the offset-carrier schema only after every structural edge has + // been rewritten. In particular, a While before argument and its paired + // after argument must both retain their original base provenance while they + // are converted. Publishing after the first region would make parsing the + // second region treat its still-pointer-typed argument as a base-less i64 + // offset and attempt to rebuild tt.addptr from a null source. + for (Value arg : scalarPointerOffsetArgs) { + markScalarPointerOffsetSlot(arg); + // replaceArgs() may have cached the old pointer provenance before the + // marker was published. Drop only the marked region arguments so the + // subsequent body walk re-parses them as live scalar offset carriers. + offsetMap.erase(arg); + } + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "[convertTensorPtr]: Preorder end\n" << *op << "\n"; + }); +} + +void convertTensorPtrPost(Operation *op, RewriterBase &rewriter, + llvm::DenseMap &offsetMap) { + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "[convertTensorPtr]: Postorder start\n" << *op << "\n"; + }); + bool preserveScalarPointers = + shouldPreserveScalarPointers(op, rewriter, offsetMap); + if (auto whileOp = dyn_cast(op)) { + replaceOperands(whileOp.getYieldOp()->getOpOperands(), rewriter, offsetMap, + preserveScalarPointers); + } else if (auto loopOp = dyn_cast(op)) { + replaceArgs(loopOp->getResults(), rewriter, offsetMap, + preserveScalarPointers); + replaceOperands(*loopOp.getYieldedValuesMutable(), rewriter, offsetMap, + preserveScalarPointers); + } + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "[convertTensorPtr]: Postorder end\n" << *op << "\n"; + }); +} + +int getPtrTensorRank(Type type) { + if (auto ptrType = dyn_cast(type)) { + if (auto tensorType = + dyn_cast(ptrType.getPointeeType())) { + return tensorType.getRank(); + } + } + return 0; +} + +SmallVector constructOperands(ValueRange operands, Value tempVar, + IRMapping mapping, OpBuilder &builder, + Operation *loop) { + SmallVector newOperands; + for (auto [slot, originalOperand] : llvm::enumerate(operands)) { + Value mappedOperand = mapping.lookupOrDefault(originalOperand); + if (useCompleteScalarAddress(loop, slot, originalOperand.getType())) + mappedOperand = materializeScalarPointerAddress(mappedOperand, builder, + originalOperand.getLoc()); + newOperands.push_back(mappedOperand); + auto numAppend = getPtrTensorRank(originalOperand.getType()) - 1; + if (numAppend > 0) + newOperands.append(numAppend, tempVar); + } + return newOperands; +} + +SmallVector constructTypes(TypeRange types, Operation *loop) { + SmallVector newTypes; + for (auto [slot, type] : llvm::enumerate(types)) { + newTypes.push_back(useCompleteScalarAddress(loop, slot, type) + ? IntegerType::get(type.getContext(), 64) + : type); + if (auto ptrType = dyn_cast(type)) { + if (auto tensorType = + dyn_cast(ptrType.getPointeeType())) { + if (tensorType.getRank() > 0) + newTypes.append(tensorType.getRank() - 1, + IntegerType::get(type.getContext(), 32)); + } + } + } + return newTypes; +} + +void replacePtrArguments(triton::FuncOp funcOp, + llvm::DenseMap &offsetMap) { + // Keep replaced values alive until the analysis cache is no longer used. + // This detached block owns the retired operations without revisiting them. + Block retiredOps; + IRRewriter rewriter(funcOp.getContext()); + rewriter.setInsertionPointToStart(&funcOp.getBody().front()); + Value tempVar = rewriter + .create( + funcOp.getLoc(), rewriter.getI32Type(), ValueRange{}) + ->getResult(0); + std::function convertTensorPtr = [&](Operation *op) { + IRMapping mapping; + Operation *newOp = nullptr; + // Address carriers used by the replacement op must dominate it. Build the + // replacement immediately before the old op and unlink the old op last. + rewriter.setInsertionPoint(op); + if (auto forOp = dyn_cast(op)) { + // An opaque scalar-pointer boundary cannot be represented by the + // relative-offset protocol: one edge may come from an if/select and + // therefore has no single static base. Convert every scalar pointer + // slot to a complete i64 address before constructing the replacement + // loop, so the loop signature is pointer-free and T2L never has to + // materialize a pointer from an unresolved SCF value. + if (shouldPreserveScalarPointers(forOp.getOperation(), rewriter, + offsetMap)) + markOpaqueScalarPointerBoundary(forOp.getOperation()); + SmallVector newInitArgs = constructOperands( + forOp.getInitArgs(), tempVar, mapping, rewriter, forOp); + newOp = rewriter.create( + forOp.getLoc(), forOp.getLowerBound(), forOp.getUpperBound(), + forOp.getStep(), newInitArgs, + [&](OpBuilder &b, Location loc, Value iv, ValueRange args) { + mapping.map(forOp.getInductionVar(), iv); + auto newArgIter = args.begin(); + for (auto [slot, oldArg] : + llvm::enumerate(forOp.getRegionIterArgs())) { + Value mappedArg = *newArgIter; + if (useCompleteScalarAddress(forOp, slot, oldArg.getType())) + mappedArg = + rebuildScalarPointer(mappedArg, oldArg.getType(), b, loc); + mapping.map(oldArg, mappedArg); + std::advance(newArgIter, + std::max(getPtrTensorRank(oldArg.getType()), 1)); + } + for (auto &bodyOp : forOp.getBody()->without_terminator()) { + b.clone(bodyOp, mapping); + } + auto yieldOp = cast(forOp.getBody()->getTerminator()); + b.create(yieldOp.getLoc(), + constructOperands(yieldOp.getOperands(), + tempVar, mapping, b, + forOp)); + }); + } else if (auto whileOp = dyn_cast(op)) { + // Keep analyzable scalar pointers on the established relative-offset + // path. If any boundary edge has no stable base-plus-offset form, switch + // all scalar pointer edges atomically to complete i64 addresses so the + // before/after regions cannot acquire a mixed pointer/integer contract. + if (shouldPreserveScalarPointers(whileOp.getOperation(), rewriter, + offsetMap)) + markOpaqueScalarPointerBoundary(whileOp.getOperation()); + SmallVector newInits = constructOperands( + whileOp.getInits(), tempVar, mapping, rewriter, whileOp); + newOp = rewriter.create( + whileOp.getLoc(), constructTypes(whileOp->getResultTypes(), whileOp), + newInits, + [&](OpBuilder &b, Location loc, ValueRange args) { + IRMapping beforeMapping; + auto newArgIter = args.begin(); + for (auto [slot, oldArg] : + llvm::enumerate(whileOp.getBeforeArguments())) { + Value mappedArg = *newArgIter; + if (useCompleteScalarAddress(whileOp, slot, oldArg.getType())) + mappedArg = + rebuildScalarPointer(mappedArg, oldArg.getType(), b, loc); + beforeMapping.map(oldArg, mappedArg); + std::advance(newArgIter, + std::max(getPtrTensorRank(oldArg.getType()), 1)); + } + for (auto &bodyOp : whileOp.getBeforeBody()->without_terminator()) { + b.clone(bodyOp, beforeMapping); + } + auto conditionOp = whileOp.getConditionOp(); + b.create( + conditionOp.getLoc(), + beforeMapping.lookup(conditionOp.getCondition()), + constructOperands(conditionOp.getArgs(), tempVar, beforeMapping, + b, whileOp)); + }, + [&](OpBuilder &b, Location loc, ValueRange args) { + IRMapping afterMapping; + auto newArgIter = args.begin(); + for (auto [slot, oldArg] : + llvm::enumerate(whileOp.getAfterArguments())) { + Value mappedArg = *newArgIter; + if (useCompleteScalarAddress(whileOp, slot, oldArg.getType())) + mappedArg = + rebuildScalarPointer(mappedArg, oldArg.getType(), b, loc); + afterMapping.map(oldArg, mappedArg); + std::advance(newArgIter, + std::max(getPtrTensorRank(oldArg.getType()), 1)); + } + for (auto &bodyOp : whileOp.getAfterBody()->without_terminator()) { + b.clone(bodyOp, afterMapping); + } + auto yieldOp = whileOp.getYieldOp(); + b.create(yieldOp.getLoc(), + constructOperands(yieldOp.getOperands(), + tempVar, afterMapping, b, + whileOp)); + }); + } else if (auto ifOp = dyn_cast(op); + ifOp && ifOp->getNumResults() > 0) { + newOp = rewriter.create( + ifOp.getLoc(), ifOp.getCondition(), + [&](OpBuilder &b, Location loc) { + for (auto &bodyOp : ifOp.thenBlock()->without_terminator()) { + b.clone(bodyOp, mapping); + } + auto yieldOp = ifOp.thenYield(); + b.create(yieldOp.getLoc(), + constructOperands(yieldOp.getOperands(), + tempVar, mapping, b, + /*loop=*/nullptr)); + }, + [&](OpBuilder &b, Location loc) { + for (auto &bodyOp : ifOp.elseBlock()->without_terminator()) { + b.clone(bodyOp, mapping); + } + auto yieldOp = ifOp.elseYield(); + b.create(yieldOp.getLoc(), + constructOperands(yieldOp.getOperands(), + tempVar, mapping, b, + /*loop=*/nullptr)); + }); + } else if (auto loopOp = dyn_cast(op)) { + llvm_unreachable("Unsupported loop op"); + } + if (newOp) { + newOp->setAttrs(op->getAttrs()); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Converting\n" << *op << "\nto\n" << *newOp << "\n"; + }); + auto resIter = newOp->result_begin(); + for (auto [slot, res] : llvm::enumerate(op->getResults())) { + Value replacement = *resIter; + if (useCompleteScalarAddress(op, slot, res.getType())) { + rewriter.setInsertionPointAfter(newOp); + replacement = rebuildScalarPointer(replacement, res.getType(), + rewriter, res.getLoc()); + } + rewriter.replaceAllUsesWith(res, replacement); + std::advance(resIter, std::max(getPtrTensorRank(res.getType()), 1)); + } + rewriter.moveOpBefore(op, &retiredOps, retiredOps.end()); + op = newOp; + convertTensorPtrPre(op, rewriter, offsetMap); + for (auto ®ion : op->getRegions()) + region.walk(convertTensorPtr); + convertTensorPtrPost(op, rewriter, offsetMap); + return WalkResult::skip(); + } + return WalkResult::advance(); + }; + + funcOp->walk(convertTensorPtr); + // Drop all cached references before retiredOps destroys the old IR. + offsetMap.clear(); +} diff --git a/third_party/ascend/lib/TritonToUnstructure/UnstructureConversionPass.cpp b/third_party/ascend/lib/TritonToUnstructure/UnstructureConversionPass.cpp new file mode 100644 index 0000000000..e5eeac0bfa --- /dev/null +++ b/third_party/ascend/lib/TritonToUnstructure/UnstructureConversionPass.cpp @@ -0,0 +1,1382 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToUnstructure/UnstructureConversionPass.h" +#include "TritonToLinalg/MaskAnalysis.h" +#include "TritonToStructured/CannonicalizerConverter.h" +#include "TritonToUnstructure/IndirectAtomicUtils.h" +#include "Utils/Utils.h" +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" + +#include "llvm/ADT/STLExtras.h" + +#include + +#define DEBUG_TYPE "triton-unstructure-converter" + +using namespace mlir; +using namespace triton; + +#include "llvm/Support/Debug.h" + +static triton::ascend::CompileMode unstructureCompileMode = + triton::ascend::CompileMode::Simd; + +namespace { + +static bool isTensorOfPointers(Type type) { + auto tensorType = dyn_cast(type); + return tensorType && isa(tensorType.getElementType()); +} + +constexpr int64_t kBitsPerByte = 8; + +static triton::PointerType getScalarPointerType(Type type) { + if (auto tensorType = dyn_cast(type)) + type = tensorType.getElementType(); + return dyn_cast(type); +} + +// Keep this deliberately narrow. These are the byte-buffer views used by the +// paged-attention and KV-cache kernels covered by this canonicalization. +static std::optional +getSupportedPointerBitcastScale(triton::BitcastOp op) { + auto srcType = getScalarPointerType(op.getSrc().getType()); + auto dstType = getScalarPointerType(op.getType()); + if (!srcType || !dstType || + srcType.getAddressSpace() != dstType.getAddressSpace() || + !srcType.getPointeeType().isInteger(8)) + return std::nullopt; + + Type dstPointeeType = dstType.getPointeeType(); + if (dstPointeeType.isF16() || dstPointeeType.isBF16()) + return 2; + if (dstPointeeType.isF32() || dstPointeeType.isInteger(32)) + return 4; + return std::nullopt; +} + +static FailureOr scaleTensorPointerOffset(Value offset, int64_t divisor, + IRRewriter &rewriter) { + auto offsetType = dyn_cast(offset.getType()); + if (!offsetType || !offsetType.hasStaticShape()) + return failure(); + auto elementType = dyn_cast(offsetType.getElementType()); + if (!elementType || + (elementType.getWidth() != 32 && elementType.getWidth() != 64)) + return failure(); + auto divisorAttr = rewriter.getIntegerAttr(elementType, divisor); + Value divisorValue = rewriter.create( + offset.getLoc(), DenseElementsAttr::get(offsetType, divisorAttr)); + return rewriter.create(offset.getLoc(), offset, divisorValue) + .getResult(); +} + +static bool isPublicKernelArgument(Value value) { + auto blockArgument = dyn_cast(value); + if (!blockArgument) + return false; + auto funcOp = + dyn_cast(blockArgument.getOwner()->getParentOp()); + return funcOp && funcOp.getVisibility() == SymbolTable::Visibility::Public; +} + +static FailureOr materializeScalarByteAddress(Value pointer, + Location loc, + IRRewriter &rewriter) { + SmallVector byteOffsets; + Value root = pointer; + while (auto addPtrOp = root.getDefiningOp()) { + auto offsetType = dyn_cast(addPtrOp.getOffset().getType()); + if (isa(addPtrOp.getType()) || !offsetType || + (offsetType.getWidth() != 32 && offsetType.getWidth() != 64)) + return failure(); + byteOffsets.push_back(addPtrOp.getOffset()); + root = addPtrOp.getPtr(); + } + if (!isPublicKernelArgument(root)) + return failure(); + + Value address = + rewriter.create(loc, rewriter.getI64Type(), root); + for (Value offset : llvm::reverse(byteOffsets)) { + auto offsetType = cast(offset.getType()); + if (offsetType.getWidth() < 64) + offset = + rewriter.create(loc, rewriter.getI64Type(), offset); + address = rewriter.create(loc, address, offset); + } + return address; +} + +static bool hasOnlySupportedPointerUses(Value pointer) { + if (pointer.use_empty()) + return false; + for (Operation *user : pointer.getUsers()) { + if (auto addPtrOp = dyn_cast(user)) { + if (addPtrOp.getPtr() != pointer || + !hasOnlySupportedPointerUses(addPtrOp.getResult())) + return false; + continue; + } + if (auto splatOp = dyn_cast(user)) { + if (splatOp.getSrc() != pointer || + !hasOnlySupportedPointerUses(splatOp.getResult())) + return false; + continue; + } + if (auto expandDimsOp = dyn_cast(user)) { + if (expandDimsOp.getSrc() != pointer || + !hasOnlySupportedPointerUses(expandDimsOp.getResult())) + return false; + continue; + } + if (auto broadcastOp = dyn_cast(user)) { + if (broadcastOp.getSrc() != pointer || + !hasOnlySupportedPointerUses(broadcastOp.getResult())) + return false; + continue; + } + if (auto loadOp = dyn_cast(user)) { + if (loadOp.getPtr() != pointer) + return false; + continue; + } + if (auto storeOp = dyn_cast(user)) { + if (storeOp.getPtr() != pointer) + return false; + continue; + } + return false; + } + return true; +} + +static bool canRewriteSupportedPointerBitcast(triton::BitcastOp op) { + if (!getSupportedPointerBitcastScale(op) || + !hasOnlySupportedPointerUses(op.getResult())) + return false; + + Value current = op.getSrc(); + while (Operation *producer = current.getDefiningOp()) { + if (auto addPtrOp = dyn_cast(producer)) { + Type offsetType = addPtrOp.getOffset().getType(); + if (isa(current.getType())) { + auto pointerType = cast(current.getType()); + auto tensorType = dyn_cast(offsetType); + if (!pointerType.hasStaticShape() || !tensorType || + !tensorType.hasStaticShape() || + pointerType.getShape() != tensorType.getShape()) + return false; + auto elementType = dyn_cast(tensorType.getElementType()); + if (!elementType || + (elementType.getWidth() != 32 && elementType.getWidth() != 64)) + return false; + } else if (auto intType = dyn_cast(offsetType); + !intType || + (intType.getWidth() != 32 && intType.getWidth() != 64)) { + return false; + } + current = addPtrOp.getPtr(); + continue; + } + if (auto splatOp = dyn_cast(producer)) { + if (isa(splatOp.getSrc().getType())) + return false; + current = splatOp.getSrc(); + continue; + } + return false; + } + return !isa(current.getType()) && + isPublicKernelArgument(current); +} + +// Rewrite only the supported byte-buffer views into ordinary target-element +// addptr operations. All later pointer and control-flow handling remains on the +// existing main-dev paths. +static LogicalResult +rewriteSupportedPointerBitcast(triton::BitcastOp op, IRRewriter &rewriter, + triton::BitcastOp &nextBitcast) { + auto divisor = getSupportedPointerBitcastScale(op); + if (!divisor) + return failure(); + + Value src = op.getSrc(); + rewriter.setInsertionPoint(op); + if (!isa(src.getType())) { + auto address = materializeScalarByteAddress(src, op.getLoc(), rewriter); + if (failed(address)) + return failure(); + auto targetPtr = rewriter.create( + op.getLoc(), op.getType(), *address); + rewriter.replaceOp(op, targetPtr.getResult()); + nextBitcast = nullptr; + return success(); + } + + if (src.getDefiningOp()) { + Value base = src; + Value byteOffset; + while (auto currentAddPtr = base.getDefiningOp()) { + Value currentOffset = currentAddPtr.getOffset(); + auto offsetType = cast(currentOffset.getType()); + auto elementType = cast(offsetType.getElementType()); + if (elementType.getWidth() < 64) { + auto i64OffsetType = + RankedTensorType::get(offsetType.getShape(), rewriter.getI64Type()); + currentOffset = rewriter.create( + op.getLoc(), i64OffsetType, currentOffset); + } + if (byteOffset) + byteOffset = rewriter.create(op.getLoc(), byteOffset, + currentOffset); + else + byteOffset = currentOffset; + base = currentAddPtr.getPtr(); + } + auto scaledOffset = + scaleTensorPointerOffset(byteOffset, *divisor, rewriter); + if (failed(scaledOffset)) + return failure(); + nextBitcast = + rewriter.create(op.getLoc(), op.getType(), base); + auto newAddPtr = rewriter.create( + op.getLoc(), op.getType(), nextBitcast, *scaledOffset); + rewriter.replaceOp(op, newAddPtr.getResult()); + return success(); + } + + if (auto splatOp = src.getDefiningOp()) { + auto resultType = dyn_cast(op.getType()); + if (!resultType) + return failure(); + nextBitcast = rewriter.create( + op.getLoc(), resultType.getElementType(), splatOp.getSrc()); + auto newSplat = rewriter.create(op.getLoc(), resultType, + nextBitcast.getResult()); + rewriter.replaceOp(op, newSplat.getResult()); + return success(); + } + + return failure(); +} + +static void canonicalizeSupportedPointerBitcasts(ModuleOp moduleOp) { + SmallVector bitcasts; + moduleOp.walk([&](triton::BitcastOp op) { + if (canRewriteSupportedPointerBitcast(op)) + bitcasts.push_back(op); + }); + + IRRewriter rewriter(moduleOp.getContext()); + for (triton::BitcastOp bitcast : bitcasts) { + triton::BitcastOp current = bitcast; + while (current) { + triton::BitcastOp next; + if (failed(rewriteSupportedPointerBitcast(current, rewriter, next))) + break; + current = next; + } + } +} + +static RankedTensorType resolvePtrTensorType(Value ptr) { + auto ptrType = dyn_cast(ptr.getType()); + if (auto ptrPtrType = dyn_cast(ptr.getType())) { + if (auto ptrTensorType = + dyn_cast_or_null(ptrPtrType.getPointeeType())) { + ptrType = ptrTensorType; + } + } + return ptrType; +} + +static Type getResultElementType(RankedTensorType ptrType) { + auto resultElementType = ptrType.getElementType(); + if (auto pointerType = + dyn_cast(ptrType.getElementType())) { + resultElementType = pointerType.getPointeeType(); + } + return resultElementType; +} + +static int64_t getTypeSizeInByte(Type type) { + if (auto intType = dyn_cast(type)) { + return intType.getWidth() / kBitsPerByte; + } + if (auto floatType = dyn_cast(type)) { + return floatType.getWidth() / kBitsPerByte; + } + llvm_unreachable("Unhandled element type of tensor"); +} + +template +void normalizeDiscreteMaskAccessForFallback(MemAccOpTy &op, + PtrOffsetInfo &ptrOffsetInfo, + PatternRewriter &rewriter) { + if (!op->hasAttr(ConverterUtils::discreteMaskAttrName)) { + return; + } + + if constexpr (std::is_same_v) { + auto selectOp = op.getValue().template getDefiningOp(); + op = rewriter.replaceOpWithNewOp( + op, op.getPtr(), selectOp.getTrueValue(), selectOp.getCondition(), + op.getCache(), op.getEvict()); + } else if constexpr (std::is_same_v) { + if (auto selectOp = op.getVal().template getDefiningOp()) { + op = rewriter.replaceOpWithNewOp( + op, op.getType(), op.getAtomicRmwOp(), op.getPtr(), + selectOp.getTrueValue(), selectOp.getCondition(), op.getSem(), + op.getScope()); + } + } + + rewriter.setInsertionPoint(op); + ptrOffsetInfo.setUnstructured(ptrOffsetInfo.getRank()); +} + +// ======================== 950 template-SIMT indirect fast-path lowering +// ======================== +// Load/store uses the established indirect-template ABI when its rank and +// pointer requirements are met. Atomic operations use the same gate with their +// indirect custom-op lowering. Other cases fall back to scalar-loop lowering. +// ====================================================================================== +static bool canUseIndirectFastPath(Value srcPtr, Value ptrOffset) { + if (!srcPtr || !ptrOffset) + return false; + auto ptrTy = dyn_cast(srcPtr.getType()); + if (!ptrTy || isa(ptrTy.getPointeeType())) + return false; + return isa(ptrOffset.getType()); +} + +template +LogicalResult tryRewriteIndirectFastPath(MemAccOpTy op, Location loc, + Value srcPtr, Value ptrOffset, + ArrayRef resultShape, + PatternRewriter &rewriter) { + // Indirect backend operations accept one scalar base plus lane offsets. An + // opaque tensor base can contain a different pointer in every lane, so it + // must be handled by the scalar-loop fallback below. + if (!isa(srcPtr.getType())) + return failure(); + + bool rankWithinIndirectLoadStoreFastPathLimit = resultShape.size() <= 5; + + if (!canUseIndirectFastPath(srcPtr, ptrOffset)) { + LLVM_DEBUG({ + llvm::dbgs() + << "Skip SIMT indirect fast path: src must be scalar elem ptr and " + "offset must be an int tensor (reject block_ptr)\n"; + }); + return failure(); + } + + if constexpr (std::is_same_v) { + if (!rankWithinIndirectLoadStoreFastPathLimit) { + return failure(); + } + + assert(isa(srcPtr.getType()) && + "src must be ptr type"); + Value mask = op.getMask(); + Value other = op.getOther(); + auto resultType = op.getType(); + auto indirect = rewriter.create( + loc, resultType, srcPtr, ptrOffset, mask, other, + ConverterUtils::requiresVolatileIndirectLoad(op.getPtr(), op)); + rewriter.replaceOp(op, indirect.getResult()); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Rewriting tt.load to tt.indirect_load\n"; + os << indirect << "\n"; + }); + return success(); + } else if constexpr (std::is_same_v) { + if (!rankWithinIndirectLoadStoreFastPathLimit) { + return failure(); + } + + assert(isa(srcPtr.getType()) && + "src must be ptr type"); + Value value = op.getValue(); + Value mask = op.getMask(); + + // For bool store, unwrap ptr -> ptr bitcast before creating + // indirect_store. Keep ptr so TypeConverter can map it to memref. + if (auto bitcastOp = srcPtr.getDefiningOp()) { + auto srcPtrTy = + dyn_cast(bitcastOp.getSrc().getType()); + auto dstPtrTy = dyn_cast(bitcastOp.getType()); + + if (srcPtrTy && dstPtrTy && srcPtrTy.getPointeeType().isInteger(1) && + dstPtrTy.getPointeeType().isInteger(8)) { + srcPtr = bitcastOp.getSrc(); + } + } + auto indirect = rewriter.create( + loc, srcPtr, ptrOffset, value, mask); + rewriter.eraseOp(op); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Rewriting tt.store to tt.indirect_store\n"; + os << indirect << "\n"; + }); + return success(); + } else if constexpr (std::is_same_v) { + assert(isa(srcPtr.getType()) && + "src must be ptr type"); + if (!IndirectAtomicUtils::canUseIndirectAtomicFastPath(op, ptrOffset)) { + return failure(); + } + + auto customResult = + IndirectAtomicUtils::tryConvertAtomicRmwToIndirectCustom( + op, srcPtr, ptrOffset, rewriter); + if (failed(customResult)) { + return failure(); + } + + rewriter.replaceOp(op, *customResult); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Rewriting tt.atomic_rmw to hivm.hir.custom indirect atomic\n"; + }); + return success(); + } else if constexpr (std::is_same_v) { + assert(isa(srcPtr.getType()) && + "src must be ptr type"); + if (!IndirectAtomicUtils::canUseIndirectAtomicFastPath(op, ptrOffset)) { + return failure(); + } + + auto customResult = + IndirectAtomicUtils::tryConvertAtomicCasToIndirectCustom( + op, srcPtr, ptrOffset, rewriter); + if (failed(customResult)) { + return failure(); + } + + rewriter.replaceOp(op, *customResult); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Rewriting tt.atomic_cas to hivm.hir.custom indirect atomic\n"; + }); + return success(); + } +} + +} // namespace + +template +bool UnstructuredMemAccessConverter::checkUnstructureAnnotated( + MemAccOpTy op, PatternRewriter &rewriter) const { + return llvm::any_of(op->getUsers(), [&rewriter](Operation *user) { + auto annotationOp = dyn_cast(user); + if (annotationOp && annotationOp->hasAttr("mayDiscretememaccess")) { + rewriter.eraseOp(annotationOp); + return true; + } + return false; + }); +} + +template <> +bool UnstructuredMemAccessConverter::checkUnstructureAnnotated( + triton::StoreOp op, PatternRewriter &rewriter) const { + return llvm::any_of(op.getValue().getUsers(), [&rewriter](Operation *user) { + auto annotationOp = dyn_cast(user); + if (annotationOp && annotationOp->hasAttr("mayDiscretememaccess")) { + rewriter.eraseOp(annotationOp); + return true; + } + return false; + }); +} + +// A null result denotes a predicate already enforced by the generated loops. +// Keep this limited to conjunctions and broadcast upper bounds. In particular, +// do not remove scalar gates or predicates with nonzero/unknown loop starts. +static Value getResidualLoadMask(Value mask, ArrayRef boundedLoopAxes, + PatternRewriter &rewriter) { + if (auto andOp = mask.getDefiningOp()) { + Value lhs = getResidualLoadMask(andOp.getLhs(), boundedLoopAxes, rewriter); + Value rhs = getResidualLoadMask(andOp.getRhs(), boundedLoopAxes, rewriter); + if (!lhs) + return rhs; + if (!rhs) + return lhs; + if (lhs == andOp.getLhs() && rhs == andOp.getRhs()) + return mask; + return rewriter.create(andOp.getLoc(), lhs, rhs); + } + + auto broadcast = mask.getDefiningOp(); + if (!broadcast) + return mask; + auto sourceType = cast(broadcast.getSrc().getType()); + if (sourceType.getShape().size() != boundedLoopAxes.size()) + return mask; + bool hasLoopAxis = false; + for (auto [axis, size] : llvm::enumerate(sourceType.getShape())) { + if (size == 1) + continue; + if (!boundedLoopAxes[axis]) + return mask; + hasLoopAxis = true; + } + if (!hasLoopAxis) + return mask; + + Value predicate = broadcast.getSrc(); + while (auto expand = predicate.getDefiningOp()) + predicate = expand.getSrc(); + auto cmp = predicate.getDefiningOp(); + if (!cmp || cmp.getPredicate() != arith::CmpIPredicate::slt) + return mask; + return {}; +} + +template +Value UnstructuredMemAccessConverter::createExtractOp( + Location loc, Value value, PatternRewriter &rewriter, + ArrayRef iterIdx) const { + if (!value) + return value; + SmallVector indices; + for (auto idxOfr : iterIdx) { + auto idx = getValueOrCreateConstantIndexOp(rewriter, loc, idxOfr); + indices.push_back(idx); + } + auto extractedOp = rewriter.create(loc, value, indices); + extractedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + return extractedOp; +} + +template +Value UnstructuredMemAccessConverter::createExtractOp( + Location loc, Value value, PatternRewriter &rewriter, + ArrayRef offsets, ArrayRef sizes, + ArrayRef strides) const { + if (!value) + return value; + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Extracting\n"; + os << value << "\n"; + }); + auto extractedOp = rewriter.create( + loc, value, offsets, sizes, strides); + extractedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + return extractedOp; +} + +template <> +template +triton::LoadOp UnstructuredMemAccessConverter::createMemAccOp( + triton::LoadOp op, Value ptrToAccess, Location loc, + PatternRewriter &rewriter, Args &&...args) const { + return rewriter.create(loc, ptrToAccess, op.getCache(), + op.getEvict(), op.getIsVolatile()); +} + +template <> +template +triton::AtomicRMWOp +UnstructuredMemAccessConverter::createMemAccOp( + triton::AtomicRMWOp op, Value ptrToAccess, Location loc, + PatternRewriter &rewriter, Args &&...args) const { + auto extractedValue = + createExtractOp(loc, op.getVal(), rewriter, std::forward(args)...); + auto extractedMask = + createExtractOp(loc, op.getMask(), rewriter, std::forward(args)...); + Type targetType = ptrToAccess.getType(); + if (auto tensorType = dyn_cast(targetType)) { + auto ptrType = cast(tensorType.getElementType()); + targetType = + RankedTensorType::get(tensorType.getShape(), ptrType.getPointeeType()); + } else { + auto resultType = cast(op.getResult().getType()); + SmallVector scalarLikeShape(resultType.getRank(), 1); + targetType = + RankedTensorType::get(scalarLikeShape, resultType.getElementType()); + ptrToAccess = rewriter.create( + loc, RankedTensorType::get(scalarLikeShape, ptrToAccess.getType()), + ptrToAccess); + extractedValue = rewriter.create( + loc, RankedTensorType::get(scalarLikeShape, extractedValue.getType()), + extractedValue); + if (extractedMask) { + extractedMask = rewriter.create( + loc, RankedTensorType::get(scalarLikeShape, extractedMask.getType()), + extractedMask); + } + } + return rewriter.create( + loc, targetType, op.getAtomicRmwOpAttr(), ptrToAccess, extractedValue, + extractedMask, op.getSemAttr(), op.getScopeAttr()); +} + +template <> +template +triton::AtomicCASOp +UnstructuredMemAccessConverter::createMemAccOp( + triton::AtomicCASOp op, Value ptrToAccess, Location loc, + PatternRewriter &rewriter, Args &&...args) const { + auto extractedCmp = + createExtractOp(loc, op.getCmp(), rewriter, std::forward(args)...); + auto extractedValue = + createExtractOp(loc, op.getVal(), rewriter, std::forward(args)...); + Type targetType = ptrToAccess.getType(); + if (auto tensorType = dyn_cast(targetType)) { + auto ptrType = cast(tensorType.getElementType()); + targetType = + RankedTensorType::get(tensorType.getShape(), ptrType.getPointeeType()); + } else { + auto resultType = cast(op.getResult().getType()); + SmallVector scalarLikeShape(resultType.getRank(), 1); + targetType = + RankedTensorType::get(scalarLikeShape, resultType.getElementType()); + ptrToAccess = rewriter.create( + loc, RankedTensorType::get(scalarLikeShape, ptrToAccess.getType()), + ptrToAccess); + extractedCmp = rewriter.create( + loc, RankedTensorType::get(scalarLikeShape, extractedCmp.getType()), + extractedCmp); + extractedValue = rewriter.create( + loc, RankedTensorType::get(scalarLikeShape, extractedValue.getType()), + extractedValue); + } + return rewriter.create( + loc, targetType, ptrToAccess, extractedCmp, extractedValue, + op.getSemAttr(), op.getScopeAttr()); +} + +template <> +template +triton::StoreOp UnstructuredMemAccessConverter::createMemAccOp( + triton::StoreOp op, Value ptrToAccess, Location loc, + PatternRewriter &rewriter, Args &&...args) const { + auto extractedValue = createExtractOp(loc, op.getValue(), rewriter, + std::forward(args)...); + auto extractedMask = + createExtractOp(loc, op.getMask(), rewriter, std::forward(args)...); + return rewriter.create(loc, ptrToAccess, extractedValue, + extractedMask); +} + +template <> +template <> +LogicalResult +UnstructuredMemAccessConverter::splatAndLoadScenario< + triton::LoadOp>(triton::LoadOp op, const PtrOffsetInfo &ptrOffsetInfo, + PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + if (!ptrOffsetInfo.isPointerDescriptorOwned()) { + // Preserve the legacy scalar-like path for unmarked pointer tensors. The + // analyzed-base reconstruction below is a descriptor handoff contract. + // Provenance is carried by PtrOffsetInfo so supported pointer-preserving + // operations, such as an ordinary addptr after the rebuild, do not lose + // ownership; unrelated pointer tensors still keep this legacy path. + SmallVector indices(ptrOffsetInfo.getRank(), + rewriter.getIndexAttr(0)); + Value extractedPtr = createExtractOp(loc, op.getPtr(), rewriter, indices); + Value mask = op.getMask(); + Value other = op.getOther(); + Value loadedValue = rewriter.create( + loc, extractedPtr, /*mask=*/nullptr, /*other=*/nullptr, + /*boundaryCheck=*/ArrayRef(), + /*PaddingOptionAttr=*/nullptr); + loadedValue = rewriter.create( + loc, op.getResult().getType(), loadedValue); + if (mask) + rewriter.replaceOpWithNewOp(op, mask, loadedValue, + other); + else + rewriter.replaceOp(op, loadedValue); + return success(); + } + + Value scalarBase = ptrOffsetInfo.getPtr(); + Value scalarOffset = ptrOffsetInfo.getOffset(); + if (!scalarBase || !isa(scalarBase.getType()) || + !scalarOffset) + return failure(); + + if (auto offsetType = dyn_cast(scalarOffset.getType())) { + if (offsetType.getRank() != ptrOffsetInfo.getRank() || + !isa(offsetType.getElementType())) + return failure(); + SmallVector indices(ptrOffsetInfo.getRank(), + rewriter.getIndexAttr(0)); + scalarOffset = createExtractOp(loc, scalarOffset, rewriter, indices); + } else if (!scalarOffset.getType().isIndex() && + !isa(scalarOffset.getType())) { + return failure(); + } + + // PtrOffsetInfo already represents the analyzed address as scalarBase plus + // scalarOffset. Rebuild that scalar pointer directly instead of extracting + // a pointer lane from the original tensor-of-pointers value. This keeps the + // pointer producer in the T2L-supported addptr family; only the numerical + // offset is extracted from a tensor when necessary. + Value ptrToLoad = rewriter.create(loc, scalarBase.getType(), + scalarBase, scalarOffset); + Value mask = op.getMask(); + Value other = op.getOther(); + Value loadedValue = rewriter.create( + loc, ptrToLoad, /*mask=*/nullptr, /*other=*/nullptr, + /*boundaryCheck=*/ArrayRef(), + /*PaddingOptionAttr=*/nullptr); + loadedValue = rewriter.create(loc, op.getResult().getType(), + loadedValue); + if (mask) + rewriter.replaceOpWithNewOp(op, mask, loadedValue, other); + else + rewriter.replaceOp(op, loadedValue); + return success(); +} + +template +UnstructuredMemAccessConverter::UnstructuredMemAccessConverter( + MLIRContext *context, bool forceScalarizeMode, + const llvm::DenseMap &offsetMap, + const llvm::SmallDenseMap &fromTensorArg) + : OpRewritePattern(context), + forceScalarizeMode(forceScalarizeMode), offsetMap(offsetMap), + fromTensorArg(fromTensorArg) {} + +template +LogicalResult UnstructuredMemAccessConverter::matchAndRewrite( + MemAccOpTy op, PatternRewriter &rewriter) const { + auto loc = op.getLoc(); + + auto ptr = op.getPtr(); + auto ptrType = resolvePtrTensorType(ptr); + auto mixCompileDiscreteMask = + op->hasAttr(ConverterUtils::mixCompileDiscreteMaskAttrName); + bool runtimeLoopMask = op->hasAttr(ConverterUtils::runtimeLoopMaskAttrName); + + if (!ptrType || op->hasAttr(ConverterUtils::discreteAttrName)) + return failure(); + if (!offsetMap.contains(ptr)) + return op.emitError() << "PtrOffsetInfo should be computed\n" << ptr; + + auto ptrOffsetInfo = offsetMap.at(ptr); + + // Handle these accesses before any speculative fallback IR is created. + // A failed match must leave the original operation intact for the final + // unsupported-mask diagnostic, without repeatedly inserting dead helpers. + if (runtimeLoopMask) { + if constexpr (std::is_same_v || + std::is_same_v) { + // A one-element tensor needs only a scalar predicate, including in SIMD + // mode. Use explicit control flow: the legacy scalar masked-load + // converter otherwise loads unconditionally and selects afterwards. + if (ptrType.getNumElements() == 1 && + getResultElementType(ptrType).isIntOrFloat() && + canUseIndirectFastPath(ptrOffsetInfo.getPtr(), + ptrOffsetInfo.getOffset())) { + SmallVector indices(ptrType.getRank(), + rewriter.getIndexAttr(0)); + Value offset = + createExtractOp(loc, ptrOffsetInfo.getOffset(), rewriter, indices); + Value base = ptrOffsetInfo.getPtr(); + Value scalarPtr = rewriter.create(loc, base.getType(), + base, offset); + Value mask = createExtractOp(loc, op.getMask(), rewriter, indices); + if constexpr (std::is_same_v) { + Value other = createExtractOp(loc, op.getOther(), rewriter, indices); + if (!other) + other = rewriter.create( + loc, rewriter.getZeroAttr(getResultElementType(ptrType))); + auto guardedLoad = rewriter.create( + loc, mask, + [&](OpBuilder &b, Location loc) { + auto load = + b.create(loc, scalarPtr, op.getCache(), + op.getEvict(), op.getIsVolatile()); + b.create(loc, load.getResult()); + }, + [&](OpBuilder &b, Location loc) { + b.create(loc, other); + }); + rewriter.replaceOpWithNewOp( + op, op.getType(), guardedLoad.getResult(0)); + } else { + Value value = createExtractOp(loc, op.getValue(), rewriter, indices); + rewriter.create( + loc, mask, [&](OpBuilder &b, Location loc) { + b.create(loc, scalarPtr, value, op.getCache(), + op.getEvict()); + b.create(loc); + }); + rewriter.eraseOp(op); + } + return success(); + } + } + if (compileOn91095Flag && + triton::ascend::isSimtTemplateMode(unstructureCompileMode)) + return tryRewriteIndirectFastPath(op, loc, ptrOffsetInfo.getPtr(), + ptrOffsetInfo.getOffset(), + ptrType.getShape(), rewriter); + return rewriter.notifyMatchFailure( + op, "loop mask requires a supported masked indirect access"); + } + + if (checkUnstructureAnnotated(op, rewriter)) + ptrOffsetInfo.setUnstructured(ptrOffsetInfo.getRank()); + + if (ptrOffsetInfo.isStructured() && !mixCompileDiscreteMask && + (!ptrOffsetInfo.isScalarLike() || + llvm::all_of(ptrType.getShape(), [](int64_t dim) { return dim == 1; }))) + return failure(); + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Converting " << op->getName() << "\n"; + os << op << "\n"; + for (auto structured : ptrOffsetInfo.getStructuredRef()) + os << static_cast(structured); + os << "\n"; + os << ptrOffsetInfo.isScalarLike() << "\n"; + }); + + if constexpr (std::is_same_v) { + if (ptrOffsetInfo.isScalarLike() && + succeeded(splatAndLoadScenario(op, ptrOffsetInfo, rewriter))) { + return success(); + } + } + + std::optional mstate = runMaskAnalysis(op, rewriter); + + normalizeDiscreteMaskAccessForFallback(op, ptrOffsetInfo, rewriter); + + if (forceScalarizeMode || ptrOffsetInfo.isScalarLike() || + fromTensorArg.at(ptr)) { + ptrOffsetInfo.setUnstructured(ptrOffsetInfo.getRank()); + } + + auto srcPtr = ptrOffsetInfo.getPtr(); + auto ptrOffset = ptrOffsetInfo.getOffset(); + if (!isa(srcPtr.getType()) && + !isTensorOfPointers(srcPtr.getType())) + return rewriter.notifyMatchFailure( + op, "expected a scalar pointer or a tensor of scalar pointers"); + + // LoadLike is operation with result + bool isLoadLike = !op->use_empty(); + + Value zeroIdx = + rewriter.create(loc, rewriter.getIndexAttr(0)); + Value oneIdx = + rewriter.create(loc, rewriter.getIndexAttr(1)); + auto resultShape = ptrType.getShape(); + auto resultElementType = getResultElementType(ptrType); + int64_t sizeInByte = getTypeSizeInByte(resultElementType); + + for (int i = ptrOffsetInfo.getRank() - 1; i >= 0; i--) { + if (!ptrOffsetInfo.isStructured(i)) + break; + sizeInByte *= resultShape[i]; + } + + // Force scalarize if memory is not aligned + if (sizeInByte % 32 != 0) { + ptrOffsetInfo.setUnstructured(ptrOffsetInfo.getRank()); + } + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "UnStructured Flag check:\n"; + os << "ptrOffsetInfo.isStructured: " << ptrOffsetInfo.isStructured() + << "\n"; + os << "compileOn91095Flag: " << compileOn91095Flag << "\n"; + os << "compileMode: " << static_cast(unstructureCompileMode) << "\n"; + }); + + bool templateIndirectFastPathEnabled = + compileOn91095Flag && + triton::ascend::isSimtTemplateMode(unstructureCompileMode) && + ((!ptrOffsetInfo.isStructured() && sizeInByte < 64) || + mixCompileDiscreteMask); + bool rankWithinIndirectLoadStoreFastPathLimit = resultShape.size() <= 5; + if (templateIndirectFastPathEnabled && + succeeded(tryRewriteIndirectFastPath(op, loc, srcPtr, ptrOffset, + resultShape, rewriter))) { + return success(); + } + + LLVM_DEBUG({ + if (triton::ascend::isSimtTemplateMode(unstructureCompileMode) && + sizeInByte >= 64) { + auto &os = llvm::dbgs(); + os << "Skip template-SIMT indirect fast path because continuous shape " + "product is " + << sizeInByte << " (>=64)\n"; + } + if constexpr (std::is_same_v || + std::is_same_v) { + if (templateIndirectFastPathEnabled && + !rankWithinIndirectLoadStoreFastPathLimit) { + auto &os = llvm::dbgs(); + os << "Skip tt.indirect_load/store fast path because rank is " + << resultShape.size() << " (>5), falling back to scalar loop path\n"; + } + } + }); + + Value iterArg = nullptr; + + // Only load case + if (isLoadLike) { + iterArg = + rewriter.create(loc, resultShape, resultElementType); + } + Value newOpResult = nullptr; + + auto insertPoint = rewriter.saveInsertionPoint(); + + SmallVector offsets; + SmallVector sizes; + SmallVector strides; + SmallVector extractedShape; + SmallVector boundedLoopAxes(resultShape.size(), false); + + for (size_t i = 0; i < resultShape.size(); i++) { + auto size = resultShape[i]; + auto structured = ptrOffsetInfo.getStructuredRef()[i] == + PtrOffsetInfo::AxisInfo::structured; + // handle indirect dimension + strides.push_back(rewriter.getIndexAttr(1)); + Value sizeVal = + rewriter.create(loc, rewriter.getIndexAttr(size)); + if (structured) { + offsets.push_back(rewriter.getIndexAttr(0)); + sizes.push_back(rewriter.getIndexAttr(size)); + extractedShape.push_back(size); + } else { + scf::ForOp forOp; + if (auto mtptOp = + srcPtr.template getDefiningOp()) { + auto tptShape = mtptOp.getShape()[i]; + if (tptShape.getType() != rewriter.getIndexType()) { + tptShape = rewriter.create( + loc, rewriter.getIndexType(), tptShape); + } + sizeVal = rewriter.create(loc, sizeVal, tptShape); + } + + Value loopLower = zeroIdx; + Value loopUpper = sizeVal; + if (mstate && i < mstate->dims.size() && i < mstate->offsets.size()) { + Value maskOffset = + getValueOrCreateConstantIndexOp(rewriter, loc, mstate->offsets[i]); + maskOffset = rewriter.create(loc, maskOffset, zeroIdx); + maskOffset = rewriter.create(loc, maskOffset, sizeVal); + loopLower = maskOffset; + + Value maskDim = + getValueOrCreateConstantIndexOp(rewriter, loc, mstate->dims[i]); + maskDim = rewriter.create(loc, maskOffset, maskDim); + maskDim = rewriter.create(loc, maskDim, sizeVal); + loopUpper = maskDim; + // With a zero mask offset, [0, loopUpper) is contained in the + // analyzed interval. Do not infer this for a clipped negative start: + // clamping the start can otherwise shift a nonempty interval. + boundedLoopAxes[i] = mstate->isMask() && + mstate->dims.size() == resultShape.size() && + isConstantIntValue(mstate->offsets[i], 0); + } + + if (isLoadLike) { + forOp = rewriter.create(loc, loopLower, loopUpper, oneIdx, + ValueRange({iterArg})); + if (!newOpResult) { + newOpResult = forOp->getResult(0); + } else { + rewriter.create(loc, forOp->getResult(0)); + } + iterArg = forOp.getRegionIterArg(0); + } else { + forOp = rewriter.create(loc, loopLower, loopUpper, oneIdx); + } + sizes.push_back(rewriter.getIndexAttr(1)); + offsets.push_back(forOp.getInductionVar()); + extractedShape.push_back(1); + forOp->setAttr("ExtractedLoadOrStore", + UnitAttr::get(rewriter.getContext())); + rewriter.setInsertionPointToStart(forOp.getBody()); + } + } + + bool fullyUnstructured = ptrOffsetInfo.isUnstructuredOrScalarlike(); + auto extractedType = RankedTensorType::get(extractedShape, resultElementType); + + Value extractedOffset; + if (fullyUnstructured) { + if (auto mtptOp = + srcPtr.template getDefiningOp()) { + auto I64Type = rewriter.getIntegerType(64); + srcPtr = mtptOp.getBase(); + extractedOffset = rewriter.create(loc, 0, 64); + for (auto [indVar, offset, stride] : llvm::zip_equal( + offsets, ptrOffsetInfo.getOffsets(), mtptOp.getStrides())) { + Value inductionVar = rewriter.create( + loc, I64Type, cast(indVar)); + Value tptOffset = rewriter.create(loc, I64Type, offset); + Value tptStride = rewriter.create(loc, I64Type, stride); + tptOffset = rewriter.create(loc, tptStride, tptOffset); + tptStride = + rewriter.create(loc, tptStride, inductionVar); + extractedOffset = + rewriter.create(loc, extractedOffset, tptOffset); + extractedOffset = + rewriter.create(loc, extractedOffset, tptStride); + } + } else { + extractedOffset = createExtractOp(loc, ptrOffset, rewriter, offsets); + } + } else { + extractedOffset = + createExtractOp(loc, ptrOffset, rewriter, offsets, sizes, strides); + } + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Extracted offset\n"; + os << extractedOffset << "\n"; + }); + + // A tensor-of-pointers base is opaque: each lane may select a different + // allocation. Extract the base with the same scalar or slice coordinates as + // the offset, then form the access pointer lane by lane. + if (isTensorOfPointers(srcPtr.getType())) { + if (fullyUnstructured) + srcPtr = createExtractOp(loc, srcPtr, rewriter, offsets); + else + srcPtr = createExtractOp(loc, srcPtr, rewriter, offsets, sizes, strides); + } + + assert((isa(srcPtr.getType()) || + isTensorOfPointers(srcPtr.getType())) && + "src must be a scalar pointer or tensor of pointers"); + if (!fullyUnstructured && isa(srcPtr.getType())) { + srcPtr = rewriter.create( + loc, RankedTensorType::get(extractedShape, srcPtr.getType()), srcPtr); + } + Value ptrToAccess = rewriter.create( + loc, srcPtr.getType(), srcPtr, extractedOffset); + + MemAccOpTy accessedOp; + if (fullyUnstructured) { + accessedOp = createMemAccOp(op, ptrToAccess, loc, rewriter, offsets); + } else { + accessedOp = + createMemAccOp(op, ptrToAccess, loc, rewriter, offsets, sizes, strides); + } + if constexpr (std::is_same_v) { + // Preserve the mask on retained dimensions, just as stores and atomics do. + // BubbleUpOperation exposes the sliced mask for structured lowering. + if (mstate && !fullyUnstructured) { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(accessedOp); + Value residualMask = + getResidualLoadMask(op.getMask(), boundedLoopAxes, rewriter); + if (residualMask) + accessedOp.getMaskMutable().assign(createExtractOp( + loc, residualMask, rewriter, offsets, sizes, strides)); + } + } + + accessedOp->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + + if (isLoadLike) { + assert(iterArg && "Load case must have iterArg in for loop"); + + Value value = accessedOp->getResult(0); + Value result; + if (!isa(value.getType()) && + (std::is_same_v || + std::is_same_v)) { + value = rewriter.create(loc, extractedType, value); + } + if (!isa(value.getType())) { + SmallVector indices; + for (auto idxOfr : offsets) { + auto idx = getValueOrCreateConstantIndexOp(rewriter, loc, idxOfr); + indices.push_back(idx); + } + result = rewriter.create(loc, value, iterArg, indices); + } else { + result = rewriter.create(loc, value, iterArg, + offsets, sizes, strides); + } + rewriter.create(loc, result) + ->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + rewriter.restoreInsertionPoint(insertPoint); + if constexpr (std::is_same_v) { + if (op.getMask() && op.getOther()) { + rewriter + .replaceOpWithNewOp(op, op.getMask(), newOpResult, + op.getOther()) + ->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + } else { + rewriter.replaceOp(op, newOpResult); + } + } else { + rewriter.replaceOp(op, newOpResult); + } + } else { + if constexpr (std::is_same_v) { + if (fullyUnstructured && accessedOp.getMask()) { + auto mask = createExtractOp( + loc, accessedOp.getMask(), rewriter, + SmallVector(ptrOffsetInfo.getRank(), + rewriter.getIndexAttr(0))); + rewriter.create(loc, mask, [&](OpBuilder &b, Location loc) { + b.create( + loc, accessedOp.getType(), accessedOp.getAtomicRmwOp(), + accessedOp.getPtr(), accessedOp.getVal(), nullptr, + accessedOp.getSem(), accessedOp.getScope()) + ->setAttr(ConverterUtils::discreteAttrName, + UnitAttr::get(rewriter.getContext())); + b.create(loc); + }); + rewriter.eraseOp(accessedOp); + } + } + rewriter.eraseOp(op); + } + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "After conversion\n" + << ptrToAccess.getDefiningOp() + ->template getParentOfType() + << "\n"; + }); + return success(); +} + +void TritonToUnstructurePass::runPreparse(LoopLikeOpInterface op) { + IRRewriter rewriter(&getContext()); + auto loc = op.getLoc(); + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Pre-parsing " << op->getName() << "\n" << op << "\n"; + }); + + Block::BlockArgListType args; + ValueRange yields; + if (auto whileOp = dyn_cast(op.getOperation())) { + args = whileOp.getBeforeArguments(); + yields = whileOp.getYieldOp().getOperands(); + } else { + args = op.getRegionIterArgs(); + yields = op.getYieldedValues(); + } + + for (auto [arg, yield] : llvm::zip_equal(args, yields)) { + if (auto tensorType = dyn_cast(yield.getType())) { + parse(yield, loc, rewriter, offsetMapForLoopArgs); + offsetMap[arg] = offsetMapForLoopArgs.at(yield); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Pre-parsing result of\n" << arg << "\nis "; + for (auto structured : offsetMap[arg].getStructuredRef()) + os << static_cast(structured); + os << '\n'; + }); + } + } +} + +static bool isFromTensorArg(Value v, + llvm::SmallDenseMap &fromTensorArg) { + if (fromTensorArg.contains(v)) + return fromTensorArg.at(v); + auto *defOp = v.getDefiningOp(); + if (!defOp) { + fromTensorArg[v] = isa(v.getType()); + return isa(v.getType()); + } + for (auto opr : defOp->getOperands()) { + if (isFromTensorArg(opr, fromTensorArg)) { + fromTensorArg[v] = true; + return true; + } + } + fromTensorArg[v] = false; + return false; +} + +template +void TritonToUnstructurePass::runParse(MemAccOpTy op) { + IRRewriter rewriter(&getContext()); + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Parsing " << op->getName() << "\n" << op << "\n"; + }); + parse(op.getPtr(), op.getLoc(), rewriter, offsetMap); + isFromTensorArg(op.getPtr(), fromTensorArg); +} + +LogicalResult +TritonToUnstructurePass::processIfYieldAddHoistOperations(ModuleOp moduleOp) { + mlir::RewritePatternSet patterns(&getContext()); + patterns.add( + patterns.getContext()); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + moduleOp.emitWarning("IfYieldAddHoist processing failed"); + return failure(); + } + return success(); +} + +TritonToUnstructurePass::TritonToUnstructurePass( + const TritonToUnstructureOptions &options) + : TritonToUnstructureBase(options) {} + +void TritonToUnstructurePass::runOnOperation() { + compileOn91095Flag = this->compileOn91095; + auto compileMode = triton::ascend::parseCompileMode(this->compileMode); + if (!compileMode) { + getOperation().emitError() + << "triton-to-unstructure compile-mode is invalid: " + << this->compileMode; + signalPassFailure(); + return; + } + unstructureCompileMode = *compileMode; + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "TritonToUnstructurePass started with options:\n"; + os << " compileOn91095: " << compileOn91095Flag << "\n"; + os << " compileMode: " << this->compileMode << "\n"; + }); + + ModuleOp moduleOp = getOperation(); + MLIRContext *ctx = &getContext(); + + canonicalizeSupportedPointerBitcasts(moduleOp); + + moduleOp->walk([this](triton::FuncOp funcOp) { + replacePtrArguments(funcOp, offsetMapForLoopArgs); + }); + offsetMapForLoopArgs.clear(); + + if (failed(processIfYieldAddHoistOperations(moduleOp))) { + moduleOp.emitWarning("Failed to process IfYieldAddHoist operations"); + } + + moduleOp->walk([this](LoopLikeOpInterface op) { runPreparse(op); }); + moduleOp->walk([this](Operation *op) { + if (auto loadOp = dyn_cast(op)) { + runParse(loadOp); + } else if (auto storeOp = dyn_cast(op)) { + runParse(storeOp); + } else if (auto atomicRMWOp = dyn_cast(op)) { + runParse(atomicRMWOp); + } else if (auto atomicCASOp = dyn_cast(op)) { + runParse(atomicCASOp); + } + }); + + RewritePatternSet patterns(ctx); + + patterns.add, + UnstructuredMemAccessConverter, + UnstructuredMemAccessConverter, + UnstructuredMemAccessConverter>( + ctx, forceScalarizeMode, offsetMap, fromTensorArg); + + LLVM_DEBUG({ + auto &os = llvm::dbgs(); + os << "Parsing done\n"; + }); + + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + moduleOp->emitError("failed to apply Patterns"); + signalPassFailure(); + return; + } + + bool unsupportedLoopMask = false; + moduleOp.walk([&](Operation *op) { + if (op->hasAttr(ConverterUtils::runtimeLoopMaskAttrName)) { + op->emitError("cannot lower this loop-carried mask with a masked " + "indirect access for the selected target and compile mode"); + unsupportedLoopMask = true; + } + }); + if (unsupportedLoopMask) { + signalPassFailure(); + return; + } + + // The offset-boundary marker is only an intra-pass analysis handoff. Do + // not expose it to T2L or let it inhibit later canonicalization. + moduleOp->walk( + [](Operation *op) { op->removeAttr(kScalarPointerOffsetBoundaryAttr); }); + + PassManager pm(&getContext(), moduleOp.getOperationName()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + if (failed(runPipeline(pm, getOperation()))) { + signalPassFailure(); + } +} + +void TritonToUnstructurePass::getDependentDialects( + DialectRegistry ®istry) const { + registry.insert(); +} + +std::unique_ptr> triton::createTritonToUnstructurePass( + const TritonToUnstructureOptions &options) { + return std::make_unique(options); +} diff --git a/third_party/ascend/lib/Utils/CMakeLists.txt b/third_party/ascend/lib/Utils/CMakeLists.txt new file mode 100644 index 0000000000..2421454029 --- /dev/null +++ b/third_party/ascend/lib/Utils/CMakeLists.txt @@ -0,0 +1,12 @@ +add_triton_library(MLIRTritonNPUUtils + Utils.cpp + InterleaveOptimization.cpp + DebugUtils.cpp + + LINK_LIBS PUBLIC + MLIRIR + MLIRLLVMDialect + TritonIR + TritonMemoryAccessAnalysis + TritonMemoryAccessCore +) diff --git a/third_party/ascend/lib/Utils/DebugUtils.cpp b/third_party/ascend/lib/Utils/DebugUtils.cpp new file mode 100644 index 0000000000..4272a79ef2 --- /dev/null +++ b/third_party/ascend/lib/Utils/DebugUtils.cpp @@ -0,0 +1,264 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +//===----------------------------------------------------------------------===// +// DebugUtils.cpp +// +// Implementations for DebugUtils.h: +// * NOP insertion helpers, runtime-gated via the +// LLVM_EXTRACT_DI_LOCAL_VARIABLES environment variable, and +// * the shared location analysis / rewrite helpers used by +// CanonicalizeDebugLocationsPass and DeduplicateDebugNopsPass. +// +// All definitions live in mlir::triton::debug; DebugUtils.h re-exports the NOP +// helpers at global scope via using declarations. +//===----------------------------------------------------------------------===// + +#include "Utils/DebugUtils.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace mlir; + +namespace mlir { +namespace triton { +namespace debug { + +// Guard against pathologically deep / cyclic location trees. +static constexpr unsigned kMaxLocDepth = 16; + +//===----------------------------------------------------------------------===// +// NOP insertion helpers. +//===----------------------------------------------------------------------===// + +static bool moduleContainsScope(PatternRewriter &rewriter) { + Block *insertionBlock = rewriter.getInsertionBlock(); + if (!insertionBlock) + return false; + + Operation *parentOp = insertionBlock->getParentOp(); + if (!parentOp) + return false; + + ModuleOp module = dyn_cast(parentOp); + if (!module) + module = parentOp->getParentOfType(); + if (!module) + return false; + + bool found = false; + module.walk([&](Operation *op) { + if (op->getName().getStringRef() != "scope.scope") + return WalkResult::advance(); + found = true; + return WalkResult::interrupt(); + }); + return found; +} + +Location unwrapFusedLocForDebug(Location loc, unsigned depth) { + if (depth > kMaxLocDepth) + return loc; + if (auto cs = dyn_cast(loc)) + return unwrapFusedLocForDebug(cs.getCaller(), depth + 1); + if (auto fused = dyn_cast(loc)) { + for (auto inner : llvm::reverse(fused.getLocations())) { + if (!isa(inner)) + return unwrapFusedLocForDebug(inner, depth + 1); + } + } + return loc; +} + +void insertDebugNop(Location loc, PatternRewriter &rewriter) { + if (!isDebugNopEnabled() || moduleContainsScope(rewriter)) + return; + auto unwrapped = unwrapFusedLocForDebug(loc); + + auto ctx = rewriter.getContext(); + rewriter.create( + unwrapped, + /*resultTypes=*/TypeRange(), + /*operands=*/ValueRange(), + /*asm_string=*/"nop", + /*constraints=*/"", + /*has_side_effects=*/true, + /*is_align_stack=*/false, LLVM::tailcallkind::TailCallKind::None, + LLVM::AsmDialectAttr::get(ctx, LLVM::AsmDialect::AD_ATT), ArrayAttr()); +} + +void insertDebugNopForMask(Value mask, PatternRewriter &rewriter) { + if (!mask) + return; + if (Operation *def = mask.getDefiningOp()) + insertDebugNop(def->getLoc(), rewriter); +} + +// Collect every distinct user FileLineColLoc reachable through NameLoc / +// CallSiteLoc / (nested) FusedLoc wrappers. Locations are context-uniqued, so +// the Location handle itself is an exact dedup key (file, line, column). +static void collectUserLineLocs(Location loc, + llvm::SmallDenseSet &seen, + llvm::SmallVectorImpl &out, + unsigned depth = 0) { + if (depth > kMaxLocDepth) + return; + if (auto cs = dyn_cast(loc)) { + collectUserLineLocs(cs.getCaller(), seen, out, depth + 1); + return; + } + if (auto named = dyn_cast(loc)) { + collectUserLineLocs(named.getChildLoc(), seen, out, depth + 1); + return; + } + if (auto fused = dyn_cast(loc)) { + for (Location inner : fused.getLocations()) + collectUserLineLocs(inner, seen, out, depth + 1); + return; + } + if (auto flc = dyn_cast(loc)) + if (seen.insert(flc).second) + out.push_back(loc); +} + +void insertDebugNopForAllLines(Location loc, PatternRewriter &rewriter) { + if (!isDebugNopEnabled() || moduleContainsScope(rewriter)) + return; + + llvm::SmallDenseSet seen; + llvm::SmallVector lineLocs; + collectUserLineLocs(loc, seen, lineLocs); + if (lineLocs.empty()) { + insertDebugNop(loc, rewriter); + return; + } + for (Location l : lineLocs) + insertDebugNop(l, rewriter); +} + +//===----------------------------------------------------------------------===// +// Shared location analysis / rewrite helpers (no self-gating; passes gate). +//===----------------------------------------------------------------------===// + +bool isForeignFile(llvm::StringRef filename) { + // A library/stdlib file inlined into the kernel (e.g. triton/language/ + // standard.py). Heuristic: lives under a site-packages or dist-packages tree + return filename.contains("/site-packages/") || + filename.contains("/dist-packages/"); +} + +bool isDebugNop(Operation *op) { + auto asmOp = dyn_cast(op); + if (!asmOp) + return false; + if (!asmOp.getHasSideEffects()) + return false; + if (asmOp.getAsmString() != "nop") + return false; + // NOPs have no results and no operands. + if (asmOp->getNumResults() != 0 || asmOp->getNumOperands() != 0) + return false; + return true; +} + +FileLineColLoc unwrapToUserFileLineCol(Location loc) { + // Reuse the existing caller-preferring unwrapper, then descend NameLoc to the + // underlying FileLineColLoc. callsite(stdlib at user) -> the user's line. + Location l = unwrapFusedLocForDebug(loc); + while (auto named = dyn_cast(l)) + l = named.getChildLoc(); + return dyn_cast(l); +} + +Location collapseForeignCallsites(Location loc, unsigned depth) { + if (depth > kMaxLocDepth) + return loc; + + if (auto named = dyn_cast(loc)) { + Location child = collapseForeignCallsites(named.getChildLoc(), depth + 1); + return child == named.getChildLoc() + ? loc + : Location(NameLoc::get(named.getName(), child)); + } + + if (auto cs = dyn_cast(loc)) { + Location caller = collapseForeignCallsites(cs.getCaller(), depth + 1); + // The callee is the inlined helper frame; resolve its own file. (The + // callee is not itself a call site, so caller-preference is moot here.) + if (FileLineColLoc calleeFlc = unwrapToUserFileLineCol(cs.getCallee())) + if (isForeignFile(calleeFlc.getFilename().getValue())) + return caller; + Location callee = collapseForeignCallsites(cs.getCallee(), depth + 1); + if (callee == cs.getCallee() && caller == cs.getCaller()) + return loc; + return Location(CallSiteLoc::get(callee, caller)); + } + + if (auto fused = dyn_cast(loc)) { + llvm::SmallVector newLocs; + bool changed = false; + for (Location sub : fused.getLocations()) { + Location c = collapseForeignCallsites(sub, depth + 1); + changed |= (c != sub); + newLocs.push_back(c); + } + return changed ? Location(FusedLoc::get(loc.getContext(), newLocs, + fused.getMetadata())) + : loc; + } + + return loc; +} + +bool isDebugNopEnabled() { + // Environment variables do not change mid-process; read once. Function-local + // static initialisation is thread-safe since C++11. + static const bool enabled = [] { + const char *s = std::getenv("LLVM_EXTRACT_DI_LOCAL_VARIABLES"); + if (!s) + return false; + std::string v(s); + std::transform(v.begin(), v.end(), v.begin(), + [](unsigned char c) { return std::tolower(c); }); + return v == "1" || v == "true" || v == "on"; + }(); + return enabled; +} + +} // namespace debug +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/Utils/InterleaveOptimization.cpp b/third_party/ascend/lib/Utils/InterleaveOptimization.cpp new file mode 100644 index 0000000000..1637d67673 --- /dev/null +++ b/third_party/ascend/lib/Utils/InterleaveOptimization.cpp @@ -0,0 +1,715 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/Utils/InterleaveOptimization.h" +#include "ascend/include/Utils/Utils.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/Interfaces/ViewLikeInterface.h" +#include "mlir/Support/LogicalResult.h" + +#include "mlir/IR/Operation.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include +#include +#include +#include + +namespace mlir { +namespace triton { +// For origin MemRefType of ReinterpretCastOp under interleave state, here wanna +// adjust its shape info by expanding last dimension double. +static MemRefType expandInterleaveMemRefType( + MemRefType originType, + std::optional castOffset = std::nullopt) { + // Double the last dimension shape + SmallVector shape(originType.getShape()); + shape.back() = shape.back() * 2; + + // Adjuest layout attribute + StridedLayoutAttr originLayout = + llvm::dyn_cast(originType.getLayout()); + // If offset is static, just reset it to 0. + auto offset = originLayout.getOffset() == ShapedType::kDynamic + ? originLayout.getOffset() + : 0; + // A reinterpret_cast result type must agree with its offset operand. The + // original interleaved view can carry layout offset 0 while the normalized + // base is a non-zero static offset, so use the new cast offset whenever it + // is available. + if (castOffset) { + if (auto staticOffset = getConstantIntValue(*castOffset)) + offset = staticOffset.value(); + else + offset = ShapedType::kDynamic; + } + // Set last dimension stride to 1 + SmallVector stride(originLayout.getStrides()); + stride.back() = 1; + + return MemRefType::get( + shape, originType.getElementType(), + StridedLayoutAttr::get(originType.getContext(), offset, stride)); +} + +// The last dimension of an interleaved view has stride 2. The address can be +// expressed as an arbitrary base plus a lane selector (0 for even, 1 for odd): +// +// base + 2 * lane + {0, 1} +// +// The base is allowed to contain static and dynamic offsets. In particular, +// a constant such as the RoPE region offset (+64) is part of the base, not an +// odd-lane marker. Keep the analysis side-effect free so callers can safely +// return failure and use the normal lowering path. +struct DeinterleaveOffsetInfo { + SmallVector baseTerms; + int64_t baseConstant = 0; + IndexMode indexMode = IndexMode::EVEN_MODE; + // Reuse the original SSA base whenever the outermost +1 unambiguously + // selects the odd lane. This preserves the existing offset def-use chain + // instead of recreating an equivalent arithmetic expression during type + // conversion. + std::optional materializedBaseOffset; +}; + +static bool addStaticOffset(int64_t offset, int64_t &sum) { + if ((offset > 0 && sum > std::numeric_limits::max() - offset) || + (offset < 0 && sum < std::numeric_limits::min() - offset)) + return false; + sum += offset; + return true; +} + +static bool collectDeinterleaveOffsetTerms(OpFoldResult offset, + SmallVectorImpl &baseTerms, + int64_t &staticOffset) { + if (auto constant = getConstantIntValue(offset)) + return addStaticOffset(constant.value(), staticOffset); + + auto value = llvm::dyn_cast(offset); + if (!value) + return false; + + if (auto addOp = value.getDefiningOp()) { + return collectDeinterleaveOffsetTerms(addOp.getLhs(), baseTerms, + staticOffset) && + collectDeinterleaveOffsetTerms(addOp.getRhs(), baseTerms, + staticOffset); + } + + // The expression below this value is an opaque base term. Do not recurse + // into it: a constant in a multiply/addptr/base-address subgraph is not a + // lane selector. + baseTerms.push_back(value); + return true; +} + +static FailureOr +normalizeDeinterleaveOffset(OpFoldResult originOffset) { + DeinterleaveOffsetInfo result; + int64_t staticOffset = 0; + if (!collectDeinterleaveOffsetTerms(originOffset, result.baseTerms, + staticOffset)) + return failure(); + + for (Value term : result.baseTerms) { + if (!isa(term.getType())) + return failure(); + } + + // Keep the base pair-aligned relative to the original view. This also + // supports folded constants, e.g. +65 becomes base +64 and ODD_MODE. + int64_t laneOffset = staticOffset % 2; + if (laneOffset < 0) + laneOffset += 2; + result.baseConstant = staticOffset - laneOffset; + result.indexMode = + laneOffset == 0 ? IndexMode::EVEN_MODE : IndexMode::ODD_MODE; + + if (auto value = llvm::dyn_cast(originOffset)) { + Value base = value; + if (result.indexMode == IndexMode::ODD_MODE) { + auto addOp = value.getDefiningOp(); + if (addOp) { + if (auto lhsConstant = getConstantIntValue(addOp.getLhs()); + lhsConstant && lhsConstant.value() == 1) { + base = addOp.getRhs(); + result.materializedBaseOffset = base; + } else if (auto rhsConstant = getConstantIntValue(addOp.getRhs()); + rhsConstant && rhsConstant.value() == 1) { + base = addOp.getLhs(); + result.materializedBaseOffset = base; + } + } + } else { + result.materializedBaseOffset = base; + } + } + return result; +} + +static bool hasEquivalentDeinterleaveBase(const DeinterleaveOffsetInfo &lhs, + const DeinterleaveOffsetInfo &rhs) { + if (lhs.baseConstant != rhs.baseConstant || + lhs.baseTerms.size() != rhs.baseTerms.size()) + return false; + + // Addition is commutative. Compare the opaque terms as a multiset so that + // equivalent add trees with a different operand order remain optimizable. + SmallVector matched(rhs.baseTerms.size(), false); + for (Value lhsTerm : lhs.baseTerms) { + bool found = false; + for (auto [index, rhsTerm] : llvm::enumerate(rhs.baseTerms)) { + if (!matched[index] && lhsTerm == rhsTerm) { + matched[index] = true; + found = true; + break; + } + } + if (!found) + return false; + } + return true; +} + +static OpFoldResult +materializeDeinterleaveBaseOffset(const DeinterleaveOffsetInfo &offsetInfo, + OpBuilder &builder, Location loc) { + if (offsetInfo.materializedBaseOffset) + return *offsetInfo.materializedBaseOffset; + if (offsetInfo.baseTerms.empty()) + return builder.getIndexAttr(offsetInfo.baseConstant); + + Value base = offsetInfo.baseTerms.front(); + for (Value term : llvm::drop_begin(offsetInfo.baseTerms)) + base = builder.create(loc, base, term); + + if (offsetInfo.baseConstant != 0) { + Value constant = builder.create( + loc, builder.getIndexAttr(offsetInfo.baseConstant)); + base = builder.create(loc, base, constant); + } + return base; +} + +LogicalResult +DeinterleaveStatusOptimization(triton::LoadOp op, + triton::LoadOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter) { + auto ptr = adaptor.getPtr(); + if (auto reinterpretCast = ptr.getDefiningOp()) { + auto loc = op.getLoc(); + + // 1. Normalize the original view offset, then create the expanded cast. + auto originCastOffset = reinterpretCast.getConstifiedMixedOffset(); + auto castSize = reinterpretCast.getConstifiedMixedSizes(); + auto castStride = reinterpretCast.getConstifiedMixedStrides(); + // Actually, `castSize` is always constant value as `MemRefType` result + if (auto lastDimSize = getConstantIntValue(castSize.back())) { + castSize.back() = rewriter.getIndexAttr(lastDimSize.value() * 2); + } else { + return failure(); + } + // Last element of castStride is also constant value as prerequisite + // is that last dimension stride of casted memref type is always 2. + castStride.back() = rewriter.getIndexAttr(1); + auto offsetInfo = normalizeDeinterleaveOffset(originCastOffset); + if (failed(offsetInfo)) + return failure(); + OpFoldResult castOffset = + materializeDeinterleaveBaseOffset(*offsetInfo, rewriter, loc); + auto srcType = + expandInterleaveMemRefType(reinterpretCast.getType(), castOffset); + auto newCastOp = rewriter.create( + loc, srcType, reinterpretCast.getViewSource(), castOffset, castSize, + castStride); + + // 3. Create new memref allocOp + auto newAllocOp = rewriter.create( + loc, MemRefType::get(srcType.getShape(), srcType.getElementType())); + + // 4. Implement memref copy and bufferization back to tensor + rewriter.create(loc, newCastOp.getResult(), newAllocOp); + Value newTensor = rewriter.create( + loc, + RankedTensorType::get(srcType.getShape(), srcType.getElementType()), + newAllocOp, true /* restrict */, true /* writable */); + + // 5. Implement tensor extract_slice to represent deinterleave + // Here use `castOffset` to determine whether even index deinterleave or + // odd index. + SmallVector extractOffsets(srcType.getRank(), + rewriter.getIndexAttr(0)); + SmallVector extractStrides(srcType.getRank(), + rewriter.getIndexAttr(1)); + SmallVector extractSizes = llvm::to_vector( + llvm::map_range(srcType.getShape(), [&](int64_t dim) -> OpFoldResult { + return rewriter.getIndexAttr(dim); + })); + + // Adjust extract_slice shape + switch (offsetInfo->indexMode) { + case IndexMode::EVEN_MODE: + extractOffsets.back() = rewriter.getIndexAttr(0); + break; + case IndexMode::ODD_MODE: + extractOffsets.back() = rewriter.getIndexAttr(1); + break; + } + extractStrides.back() = rewriter.getIndexAttr(2); + extractSizes.back() = rewriter.getIndexAttr(srcType.getShape().back() / 2); + + Value deinterleaveSlice = rewriter.create( + loc, newTensor, extractOffsets, extractSizes, extractStrides); + + rewriter.replaceOp(op, deinterleaveSlice); + return success(); + } + + return failure(); +} + +LogicalResult DeinterleaveStatusWithMaskOptimization( + triton::LoadOp op, triton::LoadOp::Adaptor adaptor, + ConversionPatternRewriter &rewriter, MaskState &mstate, Value localMem) { + auto ptr = adaptor.getPtr(); + if (auto reinterpretCast = ptr.getDefiningOp()) { + auto loc = op.getLoc(); + + // 1. Normalize the original view offset, then create the expanded cast. + auto originCastOffset = reinterpretCast.getConstifiedMixedOffset(); + auto castSize = reinterpretCast.getConstifiedMixedSizes(); + auto castStride = reinterpretCast.getConstifiedMixedStrides(); + + if (auto lastDimSize = getConstantIntValue(castSize.back())) { + castSize.back() = rewriter.getIndexAttr(lastDimSize.value() * 2); + } else { + return failure(); + } + castStride.back() = rewriter.getIndexAttr(1); + auto offsetInfo = normalizeDeinterleaveOffset(originCastOffset); + if (failed(offsetInfo)) + return failure(); + OpFoldResult castOffset = + materializeDeinterleaveBaseOffset(*offsetInfo, rewriter, loc); + auto srcType = + expandInterleaveMemRefType(reinterpretCast.getType(), castOffset); + + auto newCastOp = rewriter.create( + loc, srcType, reinterpretCast.getViewSource(), castOffset, castSize, + castStride); + + // 3. Create new memref allocOp + // To reuse existing linalg::fill, here need to change insertion point + auto savedInsertPoint = rewriter.saveInsertionPoint(); + rewriter.setInsertionPointAfterValue(localMem); + auto newAllocOp = rewriter.create( + loc, MemRefType::get(srcType.getShape(), srcType.getElementType())); + rewriter.restoreInsertionPoint(savedInsertPoint); + + // 4. Broadcast other value by linalg.fill if necessary + auto other = op.getOther(); + // While deinterleave optimization will just adjust last dimension info + // and origin mask state wouldn't involve last dimension. Therefore in + // current `scf.if + linalg.fill` combination, condition of `if` could be + // kept and just replace linalg.fill' + if (other) { + assert(localMem.hasOneUse() && + llvm::isa(*(localMem.getUsers().begin()))); + auto originFillOp = + llvm::dyn_cast(*(localMem.getUsers().begin())); + + assert(llvm::isa(originFillOp->getParentOp())); + auto ifOp = llvm::dyn_cast(originFillOp->getParentOp()); + + auto newFillOp = ifOp.getThenBodyBuilder().create( + originFillOp.getLoc(), originFillOp.getInputs(), + ValueRange{newAllocOp}); + rewriter.replaceOp(originFillOp, newFillOp); + } + + // 5. Implement new subview, memref copy and bufferization back to tensor + SmallVector subviewStrides(srcType.getRank(), + rewriter.getIndexAttr(1)); + SmallVector subviewOffsets = mstate.offsets; + SmallVector subviewSizes = mstate.dims; + // Just adjust last dimension size to double + std::optional originSubviewLastDim = + getConstantIntValue(subviewSizes.back()); + assert(originSubviewLastDim.has_value()); + subviewSizes.back() = + rewriter.getIndexAttr(originSubviewLastDim.value() * 2); + + auto argSubviewType = memref::SubViewOp::inferResultType( + srcType, subviewOffsets, subviewSizes, subviewStrides); + // alloca subview type doesn't carry layout attribute + auto allocSubviewType = memref::SubViewOp::inferResultType( + newAllocOp.getType(), subviewOffsets, subviewSizes, subviewStrides); + + memref::SubViewOp srcSubview = rewriter.create( + loc, llvm::cast(argSubviewType), newCastOp, subviewOffsets, + subviewSizes, subviewStrides); + memref::SubViewOp dstSubview = rewriter.create( + loc, llvm::cast(allocSubviewType), newAllocOp, + subviewOffsets, subviewSizes, subviewStrides); + rewriter.create(loc, srcSubview, dstSubview); + Value newTensor = rewriter.create( + loc, + RankedTensorType::get(srcType.getShape(), srcType.getElementType()), + newAllocOp, true /* restrict */, true /* writable */); + + // 6. Implement tensor extract_slice to represent deinterleave + // Here use `castOffset` to determine whether even index deinterleave or + // odd index. + SmallVector extractOffsets(srcType.getRank(), + rewriter.getIndexAttr(0)); + SmallVector extractStrides(srcType.getRank(), + rewriter.getIndexAttr(1)); + SmallVector extractSizes = llvm::to_vector( + llvm::map_range(srcType.getShape(), [&](int64_t dim) -> OpFoldResult { + return rewriter.getIndexAttr(dim); + })); + + switch (offsetInfo->indexMode) { + case IndexMode::EVEN_MODE: + extractOffsets.back() = rewriter.getIndexAttr(0); + break; + case IndexMode::ODD_MODE: + extractOffsets.back() = rewriter.getIndexAttr(1); + break; + } + extractStrides.back() = rewriter.getIndexAttr(2); + extractSizes.back() = rewriter.getIndexAttr(srcType.getShape().back() / 2); + + Value deinterleaveSlice = rewriter.create( + loc, newTensor, extractOffsets, extractSizes, extractStrides); + + rewriter.replaceOp(op, deinterleaveSlice); + return success(); + } + return failure(); +} + +LogicalResult +InterleaveStatusOptimization(SmallVector materializeVec) { + OpBuilder builder(materializeVec[1]); + auto loc = materializeVec[1]->getLoc(); + + auto firstReinterpretCastOp = + llvm::dyn_cast( + materializeVec[0]) + .getDest() + .getDefiningOp(); + auto secondReinterpretCastOp = + llvm::dyn_cast( + materializeVec[1]) + .getDest() + .getDefiningOp(); + + assert(firstReinterpretCastOp && secondReinterpretCastOp); + // Judge whether two `ReinterpretCastOp` shape satisfy interleave state + // a. both size are equal + if (!isEqualConstantIntOrValueArray( + firstReinterpretCastOp.getConstifiedMixedSizes(), + secondReinterpretCastOp.getConstifiedMixedSizes())) { + return failure(); + } + // b. both strides are equal + if (!isEqualConstantIntOrValueArray( + firstReinterpretCastOp.getConstifiedMixedStrides(), + secondReinterpretCastOp.getConstifiedMixedStrides())) { + return failure(); + } + // c. both offsets should satisfy tricky rule + auto firstOriginCastOffset = + firstReinterpretCastOp.getConstifiedMixedOffset(); + auto secondOriginCastOffset = + secondReinterpretCastOp.getConstifiedMixedOffset(); + auto firstOffsetInfo = normalizeDeinterleaveOffset(firstOriginCastOffset); + auto secondOffsetInfo = normalizeDeinterleaveOffset(secondOriginCastOffset); + if (failed(firstOffsetInfo) || failed(secondOffsetInfo) || + !hasEquivalentDeinterleaveBase(*firstOffsetInfo, *secondOffsetInfo) || + firstOffsetInfo->indexMode == secondOffsetInfo->indexMode) + return failure(); + + std::pair indexModeRecord = { + firstOffsetInfo->indexMode, secondOffsetInfo->indexMode}; + OpFoldResult newCastOffset = + materializeDeinterleaveBaseOffset(*firstOffsetInfo, builder, loc); + + // Create new op + // 1. Get new destination memref type + auto dstType = expandInterleaveMemRefType(firstReinterpretCastOp.getType(), + newCastOffset); + + // 2. New tensor::EmptyOp + auto emptyTensor = builder.create(loc, dstType.getShape(), + dstType.getElementType()); + + // 3. New insert_slice from materialization source into new empty tensor + SmallVector insertOffsets(dstType.getRank(), + builder.getIndexAttr(0)); + SmallVector insertStrides(dstType.getRank(), + builder.getIndexAttr(1)); + SmallVector insertSizes = llvm::to_vector( + llvm::map_range(dstType.getShape(), [&](int64_t dim) -> OpFoldResult { + return builder.getIndexAttr(dim); + })); + insertStrides.back() = builder.getIndexAttr(2); + insertSizes.back() = builder.getIndexAttr(dstType.getShape().back() / 2); + if (indexModeRecord.first == IndexMode::ODD_MODE) { + insertOffsets.back() = builder.getIndexAttr(1); + } else { + insertOffsets.back() = builder.getIndexAttr(0); + } + auto insertFirst = builder.create( + loc, + llvm::dyn_cast( + materializeVec[0]) + .getSource(), + emptyTensor.getResult(), insertOffsets, insertSizes, insertStrides); + + if (indexModeRecord.second == IndexMode::ODD_MODE) { + insertOffsets.back() = builder.getIndexAttr(1); + } else { + insertOffsets.back() = builder.getIndexAttr(0); + } + auto insertSecond = builder.create( + loc, + llvm::dyn_cast( + materializeVec[1]) + .getSource(), + insertFirst.getResult(), insertOffsets, insertSizes, insertStrides); + + // 4. Reinterpret_cast block arg + auto newCastSize = firstReinterpretCastOp.getConstifiedMixedSizes(); + auto newCastStride = firstReinterpretCastOp.getConstifiedMixedStrides(); + newCastSize.back() = builder.getIndexAttr(dstType.getShape().back()); + newCastStride.back() = builder.getIndexAttr(1); + auto newCastOp = builder.create( + loc, dstType, firstReinterpretCastOp.getViewSource(), newCastOffset, + newCastSize, newCastStride); + + // 5. Create new bufferization::MaterializeInDestinationOp + auto newStoreOp = builder.create( + loc, insertSecond.getResult(), newCastOp.getResult()); + // Setting writable is necessary as dst is memref type + newStoreOp.setWritable(true); + + // 6. Erase origin materialization + materializeVec[0]->erase(); + materializeVec[1]->erase(); + + return success(); +} + +LogicalResult +InterleaveStatusWithMaskOptimization(SmallVector materializeVec) { + OpBuilder builder(materializeVec[1]); + + auto firstSubviewOpOfReCast = + llvm::dyn_cast( + materializeVec[0]) + .getDest() + .getDefiningOp(); + auto firstSrcExtractSlice = + llvm::dyn_cast( + materializeVec[0]) + .getSource() + .getDefiningOp(); + auto firstReinterpretCastOp = firstSubviewOpOfReCast.getSource() + .getDefiningOp(); + + auto secondSubviewOpOfReCast = + llvm::dyn_cast( + materializeVec[1]) + .getDest() + .getDefiningOp(); + auto secondSrcExtractSlice = + llvm::dyn_cast( + materializeVec[1]) + .getSource() + .getDefiningOp(); + auto secondReinterpretCastOp = + secondSubviewOpOfReCast.getSource() + .getDefiningOp(); + + // 1. Both source shapes of subview and extract_slice are equal + if (firstSubviewOpOfReCast.getSourceType().getShape() != + firstSrcExtractSlice.getSourceType().getShape()) + return failure(); + if (secondSubviewOpOfReCast.getSourceType().getShape() != + secondSrcExtractSlice.getSourceType().getShape()) + return failure(); + if (firstSubviewOpOfReCast.getSourceType().getShape() != + secondSubviewOpOfReCast.getSourceType().getShape()) + return failure(); + + // 2. both mask state are equal + std::function cmpFunc = + mlir::isEqualConstantIntOrValue; + if (!mlir::detail::sameOffsetsSizesAndStrides(firstSubviewOpOfReCast, + firstSrcExtractSlice, cmpFunc)) + return failure(); + if (!mlir::detail::sameOffsetsSizesAndStrides(secondSubviewOpOfReCast, + secondSrcExtractSlice, cmpFunc)) + return failure(); + if (!mlir::detail::sameOffsetsSizesAndStrides( + firstSubviewOpOfReCast, secondSubviewOpOfReCast, cmpFunc)) + return failure(); + + // 3. Still judge whether two `ReinterpretCastOp` shape satisfy request + // a. both size are equal + if (!isEqualConstantIntOrValueArray( + firstReinterpretCastOp.getConstifiedMixedSizes(), + secondReinterpretCastOp.getConstifiedMixedSizes())) + return failure(); + // b. both strides are equal + if (!isEqualConstantIntOrValueArray( + firstReinterpretCastOp.getConstifiedMixedStrides(), + secondReinterpretCastOp.getConstifiedMixedStrides())) + return failure(); + // c. both offsets should satisfy tricky rule + auto firstOriginCastOffset = + firstReinterpretCastOp.getConstifiedMixedOffset(); + auto secondOriginCastOffset = + secondReinterpretCastOp.getConstifiedMixedOffset(); + auto firstOffsetInfo = normalizeDeinterleaveOffset(firstOriginCastOffset); + auto secondOffsetInfo = normalizeDeinterleaveOffset(secondOriginCastOffset); + if (failed(firstOffsetInfo) || failed(secondOffsetInfo) || + !hasEquivalentDeinterleaveBase(*firstOffsetInfo, *secondOffsetInfo) || + firstOffsetInfo->indexMode == secondOffsetInfo->indexMode) + return failure(); + + std::pair indexModeRecord = { + firstOffsetInfo->indexMode, secondOffsetInfo->indexMode}; + OpFoldResult newCastOffset = materializeDeinterleaveBaseOffset( + *firstOffsetInfo, builder, materializeVec[1]->getLoc()); + auto loc = materializeVec[1]->getLoc(); + + // Create new op + // 1. Get new destination memref type + auto dstType = expandInterleaveMemRefType(firstReinterpretCastOp.getType(), + newCastOffset); + + // 2. New tensor::EmptyOp + auto emptyTensor = builder.create(loc, dstType.getShape(), + dstType.getElementType()); + + // 3. New insert_slice from extract_slice source into new empty tensor + SmallVector insertOffsets(dstType.getRank(), + builder.getIndexAttr(0)); + SmallVector insertStrides(dstType.getRank(), + builder.getIndexAttr(1)); + SmallVector insertSizes = llvm::to_vector( + llvm::map_range(dstType.getShape(), [&](int64_t dim) -> OpFoldResult { + return builder.getIndexAttr(dim); + })); + insertStrides.back() = builder.getIndexAttr(2); + insertSizes.back() = builder.getIndexAttr(dstType.getShape().back() / 2); + if (indexModeRecord.first == IndexMode::ODD_MODE) { + insertOffsets.back() = builder.getIndexAttr(1); + } else { + insertOffsets.back() = builder.getIndexAttr(0); + } + auto insertFirst = builder.create( + loc, firstSrcExtractSlice.getSource(), emptyTensor.getResult(), + insertOffsets, insertSizes, insertStrides); + + if (indexModeRecord.second == IndexMode::ODD_MODE) { + insertOffsets.back() = builder.getIndexAttr(1); + } else { + insertOffsets.back() = builder.getIndexAttr(0); + } + auto insertSecond = builder.create( + loc, secondSrcExtractSlice.getSource(), insertFirst.getResult(), + insertOffsets, insertSizes, insertStrides); + + // 4. To enable store with mask, create new extract_slice + SmallVector extractOffsets = + firstSrcExtractSlice.getMixedOffsets(); + SmallVector extractStrides = + firstSrcExtractSlice.getMixedStrides(); + SmallVector extractSizes = firstSrcExtractSlice.getMixedSizes(); + if (!llvm::isa(extractSizes.back())) { + return failure(); + } + extractSizes.back() = builder.getIndexAttr( + getConstantIntValue(extractSizes.back()).value() * 2); + auto newSrcExtractSlice = builder.create( + loc, insertSecond.getResult(), extractOffsets, extractSizes, + extractStrides); + + // 5. Reinterpret_cast block arg + auto newCastSize = firstReinterpretCastOp.getConstifiedMixedSizes(); + auto newCastStride = firstReinterpretCastOp.getConstifiedMixedStrides(); + newCastSize.back() = builder.getIndexAttr(dstType.getShape().back()); + newCastStride.back() = builder.getIndexAttr(1); + auto newCastOp = builder.create( + loc, dstType, firstReinterpretCastOp.getViewSource(), newCastOffset, + newCastSize, newCastStride); + + // 6. Create new memref::SubViewOp of above new reinterpret_cast + // Here could reuse shape info of new extract_slice + auto dstSubviewType = memref::SubViewOp::inferResultType( + dstType, extractOffsets, extractSizes, extractStrides); + auto newSubviewOpOfReCast = builder.create( + loc, llvm::cast(dstSubviewType), newCastOp, extractOffsets, + extractSizes, extractStrides); + + // 7. Create new bufferization::MaterializeInDestinationOp + auto newStoreOp = builder.create( + loc, newSrcExtractSlice.getResult(), newSubviewOpOfReCast.getResult()); + // Setting writable is necessary as dst is memref type + newStoreOp.setWritable(true); + + // 8. Erase origin operation + materializeVec[0]->erase(); + materializeVec[1]->erase(); + if (firstSubviewOpOfReCast->use_empty()) { + firstSubviewOpOfReCast->erase(); + } + if (firstSrcExtractSlice->use_empty()) { + firstSrcExtractSlice->erase(); + } + if (secondSubviewOpOfReCast->use_empty()) { + secondSubviewOpOfReCast->erase(); + } + if (secondSrcExtractSlice->use_empty()) { + secondSrcExtractSlice->erase(); + } + + return success(); +} + +} // namespace triton +} // namespace mlir diff --git a/third_party/ascend/lib/Utils/Utils.cpp b/third_party/ascend/lib/Utils/Utils.cpp new file mode 100644 index 0000000000..ebe9bc7e23 --- /dev/null +++ b/third_party/ascend/lib/Utils/Utils.cpp @@ -0,0 +1,1337 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ascend/include/Utils/Utils.h" + +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "bishengir/Dialect/Annotation/IR/Annotation.h" +#include "bishengir/Dialect/HIVM/IR/HIVM.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Dialect/Utils/StaticValueUtils.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypeInterfaces.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/Operation.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Value.h" +#include "mlir/Interfaces/FunctionInterfaces.h" +#include "mlir/Interfaces/LoopLikeInterface.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Transforms/DialectConversion.h" + +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/SmallVectorExtras.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/LogicalResult.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "TritonNPU-Utils" + +namespace mlir { + +namespace ConverterUtils { + +std::optional +getLastStrideOfReinterpretCastOp(memref::ReinterpretCastOp op) { + SmallVector mixedStrides = op.getMixedStrides(); + if (mixedStrides.empty()) { + op->emitError("ReinterpretCastOp has no strides"); + return std::nullopt; + } + + OpFoldResult lastStride = mixedStrides.back(); + + if (op.getStaticStrides().back() != ShapedType::kDynamic) { + return op.getStaticStrides().back(); + } else if (isa(op.getStrides().back())) { + auto u = op.getStrides().back(); + while (auto blkArg = dyn_cast(u)) { + if (auto forOp = dyn_cast(blkArg.getOwner()->getParentOp())) { + auto prt = forOp->getOperand(3 + blkArg.getArgNumber() - 1); + u = prt; + } else { + u = nullptr; + break; + } + } + if (!u) + return std::nullopt; + lastStride = u; + } + + if (auto attr = lastStride.dyn_cast()) { + return getConstantOfAttr(lastStride); + } else if (auto value = lastStride.dyn_cast()) { + auto defOp = value.getDefiningOp(); + if (auto constIndexOp = dyn_cast(defOp)) { + int64_t constValue = constIndexOp.value(); + return constValue; + } else if (auto constIntOp = dyn_cast(defOp)) { + int64_t constValue = constIntOp.value(); + return constValue; + } + } + return std::nullopt; +} + +bool isaPermutedMemRefType(MemRefType memRefType) { + auto [ptrStrides, ptrOffsets] = memRefType.getStridesAndOffset(); + LLVM_DEBUG({ + llvm::dbgs() << "---------- [BEG] ptrStrides ----------\n"; + for (auto stride : ptrStrides) + llvm::dbgs() << stride << " "; + llvm::dbgs() << "\n"; + llvm::dbgs() << "---------- [END] ptrStrides ----------\n"; + }); + + switch (ptrStrides.size()) { + case 0: + return false; + case 1: + return false; + default: { + return ptrStrides[ptrStrides.size() - 1] != 1; + } + } +} + +Value getTransposedValue(Value source, const Location loc, + ConversionPatternRewriter &rewriter, + llvm::ArrayRef order) { + auto sourceType = cast(source.getType()); + auto sourceRank = sourceType.getRank(); + + SmallVector perm(order); + SmallVector originalShape(sourceType.getShape()); + SmallVector transposedShape(sourceRank); + for (size_t i = 0; i < sourceRank; i++) { + transposedShape[i] = originalShape[perm[i]]; + } + + Value transposeInit = rewriter.create( + loc, transposedShape, sourceType.getElementType()); + + Value transpose = + rewriter.create(loc, source, transposeInit, perm) + .getResults()[0]; + + return transpose; +} + +SmallVector getNParallelLoopsAttrs(unsigned n) { + return SmallVector(n, utils::IteratorType::parallel); +} + +Value getScalarValue(Value operand, Location loc, + ConversionPatternRewriter &rewriter) { + // Peel splat / cast / select chains down to a scalar; nullptr if non-splat + // tensor. + SmallVector ops; + auto reconstructScalarValue = [&](Value src) { + for (auto op = ops.rbegin(); op != ops.rend(); ++op) { + src = mlir::TypeSwitch(*op) + .Case([&](Operation *op) { + auto resType = op->getResults()[0].getType(); + if (auto shapedType = dyn_cast(resType)) { + resType = shapedType.getElementType(); + } + return rewriter.create(loc, resType, src); + }) + .Case([&](Operation *op) { + auto resType = op->getResults()[0].getType(); + if (auto shapedType = dyn_cast(resType)) { + resType = shapedType.getElementType(); + } + return rewriter.create(loc, resType, src); + }) + .Case([&](Operation *op) { + auto resType = op->getResults()[0].getType(); + if (auto shapedType = dyn_cast(resType)) { + resType = shapedType.getElementType(); + } + return rewriter.create(loc, resType, src); + }) + .Default([](Operation *op) { + llvm_unreachable("unsupported op in generating "); + return nullptr; + }); + } + return src; + }; + + while (true) { + if (!dyn_cast(operand.getType())) { + return reconstructScalarValue(operand); + } else if (auto op = operand.getDefiningOp()) { + if (auto attr = dyn_cast(op.getValue())) { + if (!attr.isSplat()) + return nullptr; + auto elemValue = attr.getSplatValue(); + auto constOp = arith::ConstantOp::materialize( + rewriter, elemValue, attr.getElementType(), op.getLoc()); + return reconstructScalarValue(constOp.getResult()); + } + return nullptr; + } else if (auto op = operand.getDefiningOp()) { + operand = op.getSrc(); + } else if (auto op = operand.getDefiningOp()) { + // Descriptor padding: select(i1, splat_nan, splat_zero) -> scalar select. + Value trueVal = getScalarValue(op.getTrueValue(), loc, rewriter); + Value falseVal = getScalarValue(op.getFalseValue(), loc, rewriter); + if (!trueVal || !falseVal) + return nullptr; + Value cond = op.getCondition(); + if (isa(cond.getType())) { + if (auto splat = cond.getDefiningOp()) + cond = splat.getSrc(); + else if (auto c = cond.getDefiningOp()) { + if (auto attr = dyn_cast(c.getValue()); + attr && attr.isSplat()) { + cond = arith::ConstantOp::materialize( + rewriter, attr.getSplatValue(), + attr.getElementType(), c.getLoc()); + } else { + return nullptr; + } + } else { + return nullptr; + } + } + if (isa(cond.getType())) + return nullptr; + Value scalarSelect = rewriter.create( + loc, trueVal.getType(), cond, trueVal, falseVal); + return reconstructScalarValue(scalarSelect); + } else if (auto op = operand.getDefiningOp()) { + ops.push_back(op.getOperation()); + operand = op.getIn(); + } else if (auto op = operand.getDefiningOp()) { + ops.push_back(op.getOperation()); + operand = op.getIn(); + } else if (auto op = operand.getDefiningOp()) { + ops.push_back(op.getOperation()); + operand = op.getIn(); + } else { + return nullptr; + } + } + return nullptr; +} + +memref::SubViewOp makeSubViewOp(Value src, + const llvm::SmallVector &offsets, + const llvm::SmallVector &sizes, + const Location &loc, + ConversionPatternRewriter &rewriter) { + auto srcType = cast(src.getType()); + SmallVector strides(srcType.getRank(), + rewriter.getIndexAttr(1)); + auto dstType = + memref::SubViewOp::inferResultType(srcType, offsets, sizes, strides); + return rewriter.create(loc, dyn_cast(dstType), + src, offsets, sizes, strides); +} + +tensor::ExtractSliceOp +makeExtractSliceOp(Value src, const llvm::SmallVector &offsets, + const llvm::SmallVector &sizes, + const Location &loc, ConversionPatternRewriter &rewriter) { + auto srcType = cast(src.getType()); + SmallVector strides(srcType.getRank(), + rewriter.getIndexAttr(1)); + auto dstType = + tensor::ExtractSliceOp::inferResultType(srcType, offsets, sizes, strides); + return rewriter.create(loc, dstType, src, offsets, + sizes, strides); +} + +std::optional getFullShapeOp(Value val, + ConversionPatternRewriter &rewriter) { + while (true) { + if (isa(val)) { + auto blockArg = dyn_cast(val); + Operation *parentOp = blockArg.getOwner()->getParentOp(); + + // When BlockArgument is from the scf.for loop, trace its initial value + if (auto forOp = dyn_cast_or_null(parentOp)) { + auto init = forOp.getTiedLoopInit(blockArg); + if (!init) + return std::nullopt; + val = init->get(); + continue; + } + + // When BlockArgument is not scf::ForOp but FuncOp, it means that shape + // information can no longer be tracked. In this case, std::nullopt is + // returned, and getBoundarySizes() is called to return the current shape + // as the boundary. + if (isa(parentOp)) { + return std::nullopt; + } + + emitWarning(val.getLoc()) + << "getFullShapeOp() only support ReinterpretCastOp, " + "UnrealizedConversionCastOp " + "and scf.for's block argument, but got : " + << val << "\n"; + return std::nullopt; + } + + Operation *defOp = val.getDefiningOp(); + if (!defOp) + return std::nullopt; + + if (auto castOp = dyn_cast(defOp)) { + if (castOp.getInputs().size() == 1) { + val = castOp.getInputs()[0]; + continue; + } + return std::nullopt; + } + + if (!isa(val.getType())) + return std::nullopt; + + if (auto reCastOp = dyn_cast(defOp)) { + if (reCastOp->hasAttr("tensor_ptr_full_shape")) + return reCastOp; + val = reCastOp.getSource(); + continue; + } + + emitWarning(val.getLoc()) + << "getFullShapeOp() only support ReinterpretCastOp, " + "UnrealizedConversionCastOp " + "and scf.for's block argument, but got : " + << val << "\n"; + return std::nullopt; + } +} + +SmallVector +getBoundarySizes(llvm::ArrayRef boundaryCheck, Value ptr, + const Location &loc, ConversionPatternRewriter &rewriter) { + if (isa(ptr.getType())) { + ptr = rewriter.getRemappedValue(ptr); + } + + auto shapedType = dyn_cast_if_present(ptr.getType()); + if (!shapedType) { + LLVM_DEBUG(llvm::dbgs() << "ptr is not a ShapedType.\n";); + return {}; + } + + if (!shapedType.hasStaticShape()) { + LLVM_DEBUG(llvm::dbgs() << "shapedType does not have a static shape\n";); + return {}; + } + + auto fullShapeOp = getFullShapeOp(ptr, rewriter); + if (!fullShapeOp.has_value()) { + // If fullShapeOp has no value, the current shape is returned as the + // boundary. + SmallVector boundarySize = + getAsIndexOpFoldResult(rewriter.getContext(), shapedType.getShape()); + + return boundarySize; + } + + SmallVector boundarySize = + getAsIndexOpFoldResult(rewriter.getContext(), shapedType.getShape()); + + auto fullShapeReCast = + dyn_cast(fullShapeOp.value()); + if (!fullShapeReCast) { + return getAsIndexOpFoldResult(rewriter.getContext(), shapedType.getShape()); + } + + OpFoldResult curPtrOffset; + if (auto curReCast = ptr.getDefiningOp()) { + curPtrOffset = curReCast.getConstifiedMixedOffset(); + } else if (isa(ptr) && + isa(ptr.getParentBlock()->getParentOp())) { + // Here's to process loop state where ptr is just from loop interator. + // Following assertion corresponds to conversion result from `rewriteFor` + auto blockArg = dyn_cast(ptr); + auto forOp = dyn_cast(ptr.getParentBlock()->getParentOp()); + auto initReCastOfLoop = forOp.getTiedLoopInit(blockArg) + ->get() + .getDefiningOp(); + assert(initReCastOfLoop && initReCastOfLoop.getOffsets().size() == 1); + Value initReCastOffset = initReCastOfLoop.getOffsets()[0]; + + for (OpOperand &use : initReCastOffset.getUses()) { + if (use.getOwner() == initReCastOfLoop) + continue; + else if (isa(use.getOwner())) + continue; + else if (use.getOwner() == forOp) + curPtrOffset = OpFoldResult(forOp.getTiedLoopRegionIterArg(&use)); + else + llvm_unreachable("Illegal interation offset after rewriteFor"); + } + } else { + llvm_unreachable("Unsupported state when check tensor_ptr boundary"); + } + + assert(curPtrOffset); + + OpFoldResult offsetShift = subOpFoldResult( + curPtrOffset, fullShapeReCast.getConstifiedMixedOffset(), loc, rewriter); + + auto strides = fullShapeReCast.getConstifiedMixedStrides(); + SmallVector axisOrder; + axisOrder.reserve(shapedType.getRank()); + for (int i = 0; i < shapedType.getRank(); ++i) + axisOrder.push_back(i); + + bool allStridesConstant = llvm::all_of(strides, [](OpFoldResult stride) { + return getConstantIntValue(stride).has_value(); + }); + if (allStridesConstant) { + llvm::stable_sort(axisOrder, [&strides](int lhs, int rhs) { + return std::abs(*getConstantIntValue(strides[lhs])) > + std::abs(*getConstantIntValue(strides[rhs])); + }); + } + + for (int i : axisOrder) { + OpFoldResult curStride = strides[i]; + if (llvm::find(boundaryCheck, i) != boundaryCheck.end()) { + if (isZero(curStride)) { + emitWarning(loc) + << "getBoundarySizes() cannot reconstruct boundary on checked " + "zero-stride axis " + << i << "; keep current block size for this axis"; + continue; + } + + OpFoldResult curOffset = + divOpFoldResult(offsetShift, curStride, loc, rewriter); + auto fullShape = fullShapeReCast.getConstifiedMixedSizes()[i]; + OpFoldResult curLeftSize = + maxOpFoldResult(subOpFoldResult(fullShape, curOffset, loc, rewriter), + rewriter.getIndexAttr(0), loc, rewriter); + + boundarySize[i] = + minOpFoldResult(boundarySize[i], curLeftSize, loc, rewriter); + } + + if (!isZero(curStride)) { + offsetShift = remOpFoldResult(offsetShift, curStride, loc, rewriter); + } + } + + return boundarySize; +} + +static std::optional getRootPointer(Value ptr) { + llvm::SmallPtrSet visited; + while (true) { + if (!visited.insert(ptr).second) { + return std::nullopt; + } + + if (auto blockArg = dyn_cast(ptr)) { + // A loop-carried pointer block argument is still rooted at its init + // value unless the loop updates it through an unsupported cycle. + Operation *parentOp = blockArg.getOwner()->getParentOp(); + if (auto whileOp = dyn_cast_or_null(parentOp); + whileOp && whileOp.getAfterBody() == blockArg.getOwner()) { + auto argNum = blockArg.getArgNumber(); + auto conditionArgs = whileOp.getConditionOp().getArgs(); + if (argNum < conditionArgs.size()) { + ptr = conditionArgs[argNum]; + continue; + } + } + + if (auto loopOp = dyn_cast_or_null(parentOp)) { + if (OpOperand *initArgOperand = loopOp.getTiedLoopInit(blockArg)) { + ptr = initArgOperand->get(); + continue; + } + } + + return ptr; + } + + auto *defOp = ptr.getDefiningOp(); + if (!defOp) { + return ptr; + } + + auto nextPtr = + llvm::TypeSwitch>(defOp) + .Case( + [](auto op) -> std::optional { return op.getPtr(); }) + .Case( + [](auto op) -> std::optional { return op.getSrc(); }) + .Case( + [](auto op) -> std::optional { return op.getSrc(); }) + .Case( + [](auto op) -> std::optional { return op.getSrc(); }) + .Case( + [](auto op) -> std::optional { return op.getBase(); }) + .Case( + [](auto op) -> std::optional { return op.getPtr(); }) + .Case( + [](auto op) -> std::optional { return op.getSource(); }) + .Case( + [](auto op) -> std::optional { return op.getSource(); }) + .Case( + [](auto op) -> std::optional { return op.getSource(); }) + .Case( + [](auto op) -> std::optional { + if (op.getInputs().size() != 1) { + return std::nullopt; + } + return op.getInputs().front(); + }) + .Default([](Operation *) -> std::optional { + return std::nullopt; + }); + if (!nextPtr) { + return std::nullopt; + } + ptr = *nextPtr; + } +} + +struct WritePointerInfo { + bool isWrite = false; + std::optional rootPtr; +}; + +static WritePointerInfo getKnownWritePointer(Operation *op) { + return llvm::TypeSwitch(op) + .Case([](auto storeOp) { + return WritePointerInfo{true, getRootPointer(storeOp.getPtr())}; + }) + .Case([](auto atomicOp) { + return WritePointerInfo{true, getRootPointer(atomicOp.getPtr())}; + }) + .Case([](auto atomicOp) { + return WritePointerInfo{true, getRootPointer(atomicOp.getPtr())}; + }) + .Case([](auto storeOp) { + return WritePointerInfo{true, getRootPointer(storeOp.getSrc())}; + }) + .Default([](Operation *) { return WritePointerInfo{}; }); +} + +static bool isLocalMemRef(Value value) { + while (auto *defOp = value.getDefiningOp()) { + if (isa(defOp)) { + return true; + } + auto source = + llvm::TypeSwitch(defOp) + .Case( + [](auto op) { return op.getSource(); }) + .Case([](auto op) { return op.getSource(); }) + .Case([](auto op) { return op.getSource(); }) + .Default([](Operation *) { return Value(); }); + if (!source) { + return false; + } + value = source; + } + return false; +} + +static bool mayWriteRoot(Operation *op, Value loadRootPtr) { + auto effectOp = dyn_cast(op); + if (!effectOp) { + return !mlir::isMemoryEffectFree(op); + } + + SmallVector> effects; + effectOp.getEffects(effects); + for (auto &effect : effects) { + if (!isa(effect.getEffect())) { + continue; + } + + Value value = effect.getValue(); + if (!value) { + return true; + } + if (!isa(value.getType())) { + return true; + } + + auto rootPtr = getRootPointer(value); + if (rootPtr) { + if (*rootPtr == loadRootPtr) { + return true; + } + continue; + } + if (!isLocalMemRef(value)) { + return true; + } + } + return false; +} + +// Keep an indirect load volatile unless its root pointer is proven to have no +// possible prior write. The analysis scans ordered predecessors in the same +// and enclosing blocks, nested writes in predecessor structured-control-flow +// ops, and loop-carried writes where a later store in the loop body may feed a +// following iteration. +bool requiresVolatileIndirectLoad(Value srcPtr, Operation *loadOp) { + auto loadRootPtr = getRootPointer(srcPtr); + if (!loadRootPtr) { + return true; + } + + auto opMayWriteRoot = [&](Operation *op) { + // Device-side diagnostics are modeled as GlobalMemory writes in Triton, + // but they do not write through user GM pointers and must not affect the + // indirect-load source/root analysis. + if (isa(op)) { + return false; + } + + auto memRefWriteTarget = llvm::TypeSwitch(op) + .Case([](auto copyOp) { + return copyOp.getTarget(); + }) + .Case([](auto storeOp) { + return storeOp.getMemRef(); + }) + .Default([](Operation *) { return Value(); }); + if (memRefWriteTarget) { + auto rootPtr = getRootPointer(memRefWriteTarget); + if (rootPtr) { + return *rootPtr == *loadRootPtr; + } + return !isLocalMemRef(memRefWriteTarget); + } + + auto writePtr = getKnownWritePointer(op); + if (writePtr.isWrite) { + return !writePtr.rootPtr || *writePtr.rootPtr == *loadRootPtr; + } + + // For structured control flow, look through regions instead of using the + // op-level MemoryEffect summary, otherwise unrelated nested writes would + // make this load volatile. + if (isa(op) || isa(op)) { + return false; + } + + // Unknown side-effecting ops are conservative: they may write through an + // alias that is no longer recoverable as a Triton root. + return mayWriteRoot(op, *loadRootPtr); + }; + + auto nestedMayWriteRoot = [&](Operation *container) { + bool found = false; + container->walk([&](Operation *nested) { + if (nested == container) { + return WalkResult::advance(); + } + if (opMayWriteRoot(nested)) { + found = true; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return found; + }; + + Operation *current = loadOp; + + while (current) { + Block *block = current->getBlock(); + if (!block) { + return true; + } + + // First scan operations that are ordered before the current op in this + // block. When such an operation owns regions, its nested writes are also + // considered ordered before this load. + for (auto it = Block::iterator(current); it != block->begin();) { + --it; + Operation *op = &*it; + if ((op->getNumRegions() > 0 && nestedMayWriteRoot(op)) || + opMayWriteRoot(op)) { + return true; + } + } + + Operation *parentOp = block->getParentOp(); + if (!parentOp || isa(parentOp) || + isa(parentOp)) { + return false; + } + + // If the load is inside a loop, a store that is textually after the load + // may still execute before the load in a later iteration. Scan the whole + // loop body before climbing further out. + if (isa(parentOp) && nestedMayWriteRoot(parentOp)) { + return true; + } + + current = parentOp; + } + + return false; +} + +SmallVector getBroadcastDims(RankedTensorType src, + RankedTensorType dst) { + SmallVector broadcastDims; + auto srcShape = src.getShape(); + auto dstShape = dst.getShape(); + + for (size_t i = 0; i < srcShape.size(); ++i) { + if (dstShape[i] != srcShape[i]) { + assert(srcShape[i] == 1 && + "Size of source broadcast dimension must be 1"); + broadcastDims.push_back(i); + } + } + assert(!broadcastDims.empty() && "Cannot identify broadcast dimension"); + return broadcastDims; +} + +// Dimensions of collapesd tensor is all unbroadcast dims +SmallVector getUnbroadcastDims(RankedTensorType src, + RankedTensorType dst) { + SmallVector unbroadcastDims; + auto srcShape = src.getShape(); + auto dstShape = dst.getShape(); + + for (size_t i = 0; i < srcShape.size(); ++i) { + if (dstShape[i] == srcShape[i]) { + unbroadcastDims.emplace_back(srcShape[i]); + } + } + return unbroadcastDims; +} + +} // namespace ConverterUtils + +namespace triton { + +mlir::Operation * +findFirstMatchingOperandDef(mlir::Operation *rootOp, + const std::function &condFn) { + LLVM_DEBUG(llvm::dbgs() << "[findFirstMatchingOperandDef] Current op: " + << *rootOp << "\n"); + mlir::Value lhs = nullptr; + mlir::Value rhs = nullptr; + if (auto op = dyn_cast(rootOp)) { + lhs = op.getPtr(); + rhs = op.getOffset(); + } else if (auto op = dyn_cast(rootOp)) { + lhs = op.getLhs(); + rhs = op.getRhs(); + } else if (auto op = dyn_cast(rootOp)) { + lhs = op.getLhs(); + rhs = op.getRhs(); + } else if (auto op = dyn_cast(rootOp)) { + lhs = op.getLhs(); + rhs = op.getRhs(); + } else if (auto op = dyn_cast(rootOp)) { + lhs = op.getLhs(); + rhs = op.getRhs(); + } else if (auto op = dyn_cast(rootOp)) { + lhs = op.getLhs(); + rhs = op.getRhs(); + } else if (auto op = dyn_cast(rootOp)) { + lhs = op.getSrc(); + } else if (auto op = dyn_cast(rootOp)) { + } else { + rootOp->emitRemark("Backtracing encounters unsupported Operation"); + return nullptr; + } + // Backtrace operands + if (!lhs) { + return nullptr; + } + auto lhsDef = lhs.getDefiningOp(); + mlir::Operation *targetOp; + if (lhsDef) { + if (condFn(lhsDef)) { + targetOp = lhsDef; + } else { + targetOp = findFirstMatchingOperandDef(lhsDef, condFn); + } + if (targetOp) { + return targetOp; + } + } + if (!rhs) { + return nullptr; + } + auto rhsDef = rhs.getDefiningOp(); + if (rhsDef) { + if (condFn(rhsDef)) { + targetOp = rhsDef; + } else { + targetOp = findFirstMatchingOperandDef(rhsDef, condFn); + } + if (targetOp) { + return targetOp; + } + } + return nullptr; +} + +void traverseBackwardUpdateOperandChainIf( + Operation *op, std::function conditionFn, + std::function stopFn, + std::function actionFn, OpBuilder &builder, + DenseSet &handledOperation) { + + if (!op || handledOperation.contains(op)) + return; + + handledOperation.insert(op); + + if (stopFn(op)) + return; + + if (conditionFn(op)) + actionFn(builder, op); + + DenseSet handledOperand; + + std::function handler = [&](Value operand) { + if (handledOperand.contains(operand)) + return; + handledOperand.insert(operand); + if (Operation *defOp = operand.getDefiningOp()) { + traverseBackwardUpdateOperandChainIf(defOp, conditionFn, stopFn, actionFn, + builder, handledOperation); + } else { + auto blockArgument = cast(operand); + auto parentOp = blockArgument.getOwner()->getParentOp(); + if (auto whileOp = dyn_cast(parentOp); + whileOp && whileOp.getAfterBody() == blockArgument.getOwner()) { + auto argNum = blockArgument.getArgNumber(); + auto conditionArg = whileOp.getConditionOp().getArgs()[argNum]; + handler(conditionArg); + } else if (auto loopOp = dyn_cast(parentOp)) { + OpOperand *initArgOperand = loopOp.getTiedLoopInit(blockArgument); + if (!initArgOperand) + return; + Value initArg = initArgOperand->get(); + handler(initArg); + Value yieldedValue = + loopOp.getTiedLoopYieldedValue(blockArgument)->get(); + if (yieldedValue != blockArgument) + handler(yieldedValue); + } + } + }; + + for (Value operand : op->getOperands()) { + handler(operand); + } + + if (auto loopOp = dyn_cast(op)) { + for (auto yieldedValue : loopOp.getYieldedValues()) + handler(yieldedValue); + } +} + +// Note: rootOp will also be processed. +void traverseBackwardUpdateOperandChainIf( + Operation *rootOp, std::function conditionFn, + std::function stopFn, + std::function actionFn) { + + OpBuilder builder(rootOp->getContext()); + DenseSet handledOperation; + + traverseBackwardUpdateOperandChainIf(rootOp, conditionFn, stopFn, actionFn, + builder, handledOperation); +} + +void traverseForwardUpdateUserChainIf( + Operation *op, std::function conditionFn, + std::function stopFn, + std::function actionFn, OpBuilder &builder, + llvm::SmallPtrSet &stopOps) { + + if (!op) { + return; + } + + if (stopFn(op)) { + stopOps.insert(op); + return; + } + + if (conditionFn(op)) { + actionFn(builder, op); + } + + for (auto res : op->getResults()) { + for (auto userOp : res.getUsers()) { + traverseForwardUpdateUserChainIf(userOp, conditionFn, stopFn, actionFn, + builder, stopOps); + } + } +} + +// Note: rootOp will also be processed. +void traverseForwardUpdateUserChainIf( + Operation *rootOp, std::function conditionFn, + std::function stopFn, + std::function actionFn, + llvm::SmallPtrSet &stopOps) { + + OpBuilder builder(rootOp->getContext()); + + traverseForwardUpdateUserChainIf(rootOp, conditionFn, stopFn, actionFn, + builder, stopOps); +} + +bool isMetaUse(Operation *op) { return op->hasAttr("MetaUse"); } + +bool isMixUse(Operation *op) { return op->hasAttr("MixUse"); } + +IndirectLoadInterfaceOpType getIndirectLoadInterfaceOpType(Operation *op) { + auto ty = IndirectLoadInterfaceOpType::Undefined; + if (isMetaUse(op)) { + if (isa(op)) { + ty = IndirectLoadInterfaceOpType::Load; + } else if (isa(op)) { + ty = IndirectLoadInterfaceOpType::Calc; + } + } + return ty; +} + +bool opIsIndirectLoad(Operation *op) { + auto opType = getIndirectLoadInterfaceOpType(op); + return opType == IndirectLoadInterfaceOpType::Load; +} + +bool opIsIndirectCalc(Operation *op) { + auto opType = getIndirectLoadInterfaceOpType(op); + return opType == IndirectLoadInterfaceOpType::Calc; +} + +scf::ForOp createNestedLoops( + OpBuilder &builder, Location loc, unsigned currentDim, unsigned totalDims, + ValueRange LBs, ValueRange UBs, ValueRange steps, SmallVector &ivs, + ValueRange initArgs, + function_ref &, ValueRange)> + bodyBuilder) { + + if (currentDim >= totalDims) { + bodyBuilder(builder, loc, ivs, initArgs); + return nullptr; + } + + auto loop = builder.create( + loc, LBs[currentDim], UBs[currentDim], steps[currentDim], initArgs, + [&](OpBuilder &nestedBuilder, Location nestedLoc, Value iv, + ValueRange iterArgs) { + ivs.push_back(iv); + auto innerLoop = createNestedLoops(nestedBuilder, nestedLoc, + currentDim + 1, totalDims, LBs, UBs, + steps, ivs, iterArgs, bodyBuilder); + if (innerLoop) { + nestedBuilder.create(loc, innerLoop.getResults()); + } + }); + + return loop; +} + +ModuleOp getModuleOpFromOperation(Operation *op) { + Operation *parent = op; + while (parent != nullptr && !isa(parent)) { + parent = parent->getParentOp(); // 向上查找 + } + return cast(parent); // 如果没找到会抛出异常 +} + +bool isTensorPtrType(Type type) { + auto ptrType = dyn_cast(type); + if (!ptrType) + return false; + return isa(ptrType.getPointeeType()); +} + +} // namespace triton + +bool haveSameTypes(TypeRange lhs, TypeRange rhs) { + return llvm::equal(lhs, rhs); +} + +void addReduceWithIndexAttr(ReduceWithIndexParams params, + ConversionPatternRewriter &rewriter, + linalg::ReduceOp reduceOp) { + const StringRef reduceRef = "reduce_mode"; + const StringRef tieBreakLeftRef = "tie_break_left"; + const StringRef unsignedSrcRef = "unsigned_src"; + + const StringRef tieBreakStr = + params.tieBreakType == TieBreakType::LEFT ? "true" : "false"; + const StringRef withIndexStr = + params.withIndexType == ReduceWithIndexType::MAX ? "max_with_index" + : "min_with_index"; + const StringRef unsignedSrcStr = params.isUnsignedSrc ? "true" : "false"; + + reduceOp->setAttr(reduceRef, rewriter.getStringAttr(withIndexStr)); + reduceOp->setAttr(tieBreakLeftRef, rewriter.getStringAttr(tieBreakStr)); + reduceOp->setAttr(unsignedSrcRef, rewriter.getStringAttr(unsignedSrcStr)); +} + +llvm::FailureOr +getReduceWithIndexParams(triton::ReduceOp op) { + auto tritonReduceBlock = op.getBody(); + auto *tritonYield = tritonReduceBlock->getTerminator(); + auto yieldValues = tritonYield->getOperands(); + constexpr int yieldValuesNum = 2; + if (yieldValues.empty()) { + return llvm::failure(); + } + if (yieldValues.size() != yieldValuesNum) { + return ReduceWithIndexParams{}; + } + + // Unify signed/unsigned and int/float predicate + enum class Predicate { Undefined = 0, lt = 1, gt = 2, eq = 3 }; + enum class Signedness { NotApplicable = 0, Signed = 1, Unsigned = 2 }; + auto unifyPredicateI = + [](arith::CmpIPredicate p) -> std::pair { + switch (p) { + case arith::CmpIPredicate::slt: + return {Predicate::lt, Signedness::Signed}; + case arith::CmpIPredicate::ult: + return {Predicate::lt, Signedness::Unsigned}; + case arith::CmpIPredicate::sgt: + return {Predicate::gt, Signedness::Signed}; + case arith::CmpIPredicate::ugt: + return {Predicate::gt, Signedness::Unsigned}; + case arith::CmpIPredicate::eq: + return {Predicate::eq, Signedness::NotApplicable}; + default: + return {Predicate::Undefined, Signedness::NotApplicable}; + } + }; + auto unifyPredicateF = + [](arith::CmpFPredicate p) -> std::pair { + switch (p) { + case arith::CmpFPredicate::OLT: + return {Predicate::lt, Signedness::Signed}; + case arith::CmpFPredicate::ULT: + return {Predicate::lt, Signedness::Unsigned}; + case arith::CmpFPredicate::OGT: + return {Predicate::gt, Signedness::Signed}; + case arith::CmpFPredicate::UGT: + return {Predicate::gt, Signedness::Unsigned}; + case arith::CmpFPredicate::OEQ: + return {Predicate::eq, Signedness::Signed}; + case arith::CmpFPredicate::UEQ: + return {Predicate::eq, Signedness::Unsigned}; + default: + return {Predicate::Undefined, Signedness::NotApplicable}; + } + }; + + // Composite predicate to pick index of min (or max) element have to be + // written in following form: (v means value and i means index) + // For leftmost element: + // (new_v == old_v and new_i < old_i) or new_v < old_v + // new_v < old_v or (new_v == old_v and new_i < old_i) + // new_v < old_v // python3.11 ttir + // (new_v == old_v and new_i < old_i) or new_v > old_v + // new_v > old_v or (new_v == old_v and new_i < old_i) + // new_v > old_v // python3.11 ttir + // For rightmost element: + // (new_v == old_v and new_i > old_i) or new_v < old_v + // new_v < old_v or (new_v == old_v and new_i > old_i) + // (new_v == old_v and new_i > old_i) or new_v > old_v + // new_v > old_v or (new_v == old_v and new_i > old_i) + + std::map, std::pair> + m{ + // leftmost + {{Predicate::eq, Predicate::lt, Predicate::lt}, + {ReduceWithIndexType::MIN, TieBreakType::LEFT}}, + {{Predicate::lt, Predicate::eq, Predicate::lt}, + {ReduceWithIndexType::MIN, TieBreakType::LEFT}}, + {{Predicate::lt}, {ReduceWithIndexType::MIN, TieBreakType::LEFT}}, + {{Predicate::eq, Predicate::lt, Predicate::gt}, + {ReduceWithIndexType::MAX, TieBreakType::LEFT}}, + {{Predicate::gt, Predicate::eq, Predicate::lt}, + {ReduceWithIndexType::MAX, TieBreakType::LEFT}}, + {{Predicate::gt}, {ReduceWithIndexType::MAX, TieBreakType::LEFT}}, + // rightmost + {{Predicate::eq, Predicate::gt, Predicate::lt}, + {ReduceWithIndexType::MIN, TieBreakType::RIGHT}}, + {{Predicate::lt, Predicate::eq, Predicate::gt}, + {ReduceWithIndexType::MIN, TieBreakType::RIGHT}}, + {{Predicate::eq, Predicate::gt, Predicate::gt}, + {ReduceWithIndexType::MAX, TieBreakType::RIGHT}}, + {{Predicate::gt, Predicate::eq, Predicate::gt}, + {ReduceWithIndexType::MAX, TieBreakType::RIGHT}}, + }; + + std::vector preds; + std::vector signednesses; + // A better way is to trace the arith.select + // Checking the operations one by one is hacky :( + for (auto &op : tritonReduceBlock->without_terminator()) { + Predicate pred = Predicate::Undefined; + Signedness signedness = Signedness::NotApplicable; + if (auto cmpiOp = dyn_cast(op)) { + auto predi = cmpiOp.getPredicate(); + std::tie(pred, signedness) = unifyPredicateI(predi); + } + if (auto cmpfOp = dyn_cast(op)) { + auto predf = cmpfOp.getPredicate(); + std::tie(pred, signedness) = unifyPredicateF(predf); + } + if (pred != Predicate::Undefined) { + preds.push_back(pred); + signednesses.push_back(signedness); + } + } + + // check if sequence of predicates matches any sequence for min/max + // leftmost/rightmost + if (m.find(preds) == m.end()) { + return llvm::failure(); + } + + assert(!signednesses.empty()); + const bool isUnsignedSrc = + signednesses[0] == Signedness::Unsigned || + signednesses[signednesses.size() - 1] == Signedness::Unsigned; + return ReduceWithIndexParams{.withIndexType = m.at(preds).first, + .tieBreakType = m.at(preds).second, + .isUnsignedSrc = isUnsignedSrc}; +} + +// Specialize the Typeless Value (Zero, Min, Max) into a mlir TypedAttr +FailureOr specializeTypelessValueToAttr(TypelessValue value, + Type type, OpBuilder &b) { + mlir::Type f16Ty = Float16Type::get(b.getContext()); + mlir::Type bf16Ty = BFloat16Type::get(b.getContext()); + mlir::Type f32Ty = Float32Type::get(b.getContext()); + mlir::Type i8TySL = IntegerType::get( + b.getContext(), 8, IntegerType::SignednessSemantics::Signless); + mlir::Type i8TyS = IntegerType::get(b.getContext(), 8, + IntegerType::SignednessSemantics::Signed); + mlir::Type i8TyU = IntegerType::get( + b.getContext(), 8, IntegerType::SignednessSemantics::Unsigned); + mlir::Type i16TySL = IntegerType::get( + b.getContext(), 16, IntegerType::SignednessSemantics::Signless); + mlir::Type i16TyS = IntegerType::get( + b.getContext(), 16, IntegerType::SignednessSemantics::Signed); + mlir::Type i16TyU = IntegerType::get( + b.getContext(), 16, IntegerType::SignednessSemantics::Unsigned); + mlir::Type i32TySL = IntegerType::get( + b.getContext(), 32, IntegerType::SignednessSemantics::Signless); + mlir::Type i32TyS = IntegerType::get( + b.getContext(), 32, IntegerType::SignednessSemantics::Signed); + mlir::Type i32TyU = IntegerType::get( + b.getContext(), 32, IntegerType::SignednessSemantics::Unsigned); + mlir::Type i64TySL = IntegerType::get( + b.getContext(), 64, IntegerType::SignednessSemantics::Signless); + mlir::Type i64TyS = IntegerType::get( + b.getContext(), 64, IntegerType::SignednessSemantics::Signed); + mlir::Type i64TyU = IntegerType::get( + b.getContext(), 64, IntegerType::SignednessSemantics::Unsigned); + llvm::APFloat halfZero = llvm::APFloat::getZero(llvm::APFloat::IEEEhalf()); + llvm::APFloat halfOne(llvm::APFloat::IEEEhalf(), 1); + llvm::APFloat halfMax = llvm::APFloat::getInf(llvm::APFloat::IEEEhalf()); + llvm::APFloat halfMin = + llvm::APFloat::getInf(llvm::APFloat::IEEEhalf(), true); + llvm::APFloat bfloatZero = llvm::APFloat::getZero(llvm::APFloat::BFloat()); + llvm::APFloat bfloatOne(llvm::APFloat::BFloat(), 1); + llvm::APFloat bfloatMax = llvm::APFloat::getInf(llvm::APFloat::BFloat()); + llvm::APFloat bfloatMin = + llvm::APFloat::getInf(llvm::APFloat::BFloat(), true); + llvm::APFloat floatZero = llvm::APFloat::getZero(llvm::APFloat::IEEEsingle()); + llvm::APFloat floatOne(llvm::APFloat::IEEEsingle(), 1); + llvm::APFloat floatMax = llvm::APFloat::getInf(llvm::APFloat::IEEEsingle()); + llvm::APFloat floatMin = + llvm::APFloat::getInf(llvm::APFloat::IEEEsingle(), true); + auto toPtr = [](mlir::Type ty) { return ty.getAsOpaquePointer(); }; + + std::map, + std::variant> + initMap = { + {{TypelessValue::Zero, toPtr(f16Ty)}, halfZero}, + {{TypelessValue::Zero, toPtr(bf16Ty)}, bfloatZero}, + {{TypelessValue::Zero, toPtr(f32Ty)}, floatZero}, + {{TypelessValue::Zero, toPtr(i8TySL)}, llvm::APInt(8, 0, true)}, + {{TypelessValue::Zero, toPtr(i8TyS)}, llvm::APInt(8, 0, true)}, + {{TypelessValue::Zero, toPtr(i8TyU)}, llvm::APInt(8, 0, false)}, + {{TypelessValue::Zero, toPtr(i16TySL)}, llvm::APInt(16, 0, true)}, + {{TypelessValue::Zero, toPtr(i16TyS)}, llvm::APInt(16, 0, true)}, + {{TypelessValue::Zero, toPtr(i16TyU)}, llvm::APInt(16, 0, false)}, + {{TypelessValue::Zero, toPtr(i32TySL)}, llvm::APInt(32, 0, true)}, + {{TypelessValue::Zero, toPtr(i32TyS)}, llvm::APInt(32, 0, true)}, + {{TypelessValue::Zero, toPtr(i32TyU)}, llvm::APInt(32, 0, false)}, + {{TypelessValue::Zero, toPtr(i64TySL)}, llvm::APInt(64, 0, true)}, + {{TypelessValue::Zero, toPtr(i64TyS)}, llvm::APInt(64, 0, true)}, + {{TypelessValue::Zero, toPtr(i64TyU)}, llvm::APInt(64, 0, false)}, + {{TypelessValue::Min, toPtr(f16Ty)}, halfMin}, + {{TypelessValue::Min, toPtr(bf16Ty)}, bfloatMin}, + {{TypelessValue::Min, toPtr(f32Ty)}, floatMin}, + {{TypelessValue::Min, toPtr(i8TySL)}, llvm::APInt(8, -128, true)}, + {{TypelessValue::Min, toPtr(i8TyS)}, llvm::APInt(8, -128, true)}, + {{TypelessValue::Min, toPtr(i8TyU)}, llvm::APInt(8, 0, false)}, + {{TypelessValue::Min, toPtr(i16TySL)}, llvm::APInt(16, -32768, true)}, + {{TypelessValue::Min, toPtr(i16TyS)}, llvm::APInt(16, -32768, true)}, + {{TypelessValue::Min, toPtr(i16TyU)}, llvm::APInt(16, 0, false)}, + {{TypelessValue::Min, toPtr(i32TySL)}, + llvm::APInt(32, std::numeric_limits::min(), true)}, + {{TypelessValue::Min, toPtr(i32TyS)}, + llvm::APInt(32, std::numeric_limits::min(), true)}, + {{TypelessValue::Min, toPtr(i32TyU)}, llvm::APInt(32, 0, false)}, + {{TypelessValue::Min, toPtr(i64TySL)}, + llvm::APInt(64, std::numeric_limits::min(), true)}, + {{TypelessValue::Min, toPtr(i64TyS)}, + llvm::APInt(64, std::numeric_limits::min(), true)}, + {{TypelessValue::Min, toPtr(i64TyU)}, llvm::APInt(64, 0, false)}, + {{TypelessValue::Max, toPtr(f16Ty)}, halfMax}, + {{TypelessValue::Max, toPtr(bf16Ty)}, bfloatMax}, + {{TypelessValue::Max, toPtr(f32Ty)}, floatMax}, + {{TypelessValue::Max, toPtr(i8TySL)}, llvm::APInt(8, 127, true)}, + {{TypelessValue::Max, toPtr(i8TyS)}, llvm::APInt(8, 127, true)}, + {{TypelessValue::Max, toPtr(i8TyU)}, llvm::APInt::getAllOnes(8)}, + {{TypelessValue::Max, toPtr(i16TySL)}, llvm::APInt(16, 32767, true)}, + {{TypelessValue::Max, toPtr(i16TyS)}, llvm::APInt(16, 32767, true)}, + {{TypelessValue::Max, toPtr(i16TyU)}, llvm::APInt::getAllOnes(16)}, + {{TypelessValue::Max, toPtr(i32TySL)}, + llvm::APInt(32, std::numeric_limits::max(), true)}, + {{TypelessValue::Max, toPtr(i32TyS)}, + llvm::APInt(32, std::numeric_limits::max(), true)}, + {{TypelessValue::Max, toPtr(i32TyU)}, llvm::APInt::getAllOnes(32)}, + {{TypelessValue::Max, toPtr(i64TySL)}, + llvm::APInt(64, std::numeric_limits::max(), true)}, + {{TypelessValue::Max, toPtr(i64TyS)}, + llvm::APInt(64, std::numeric_limits::max(), true)}, + {{TypelessValue::Max, toPtr(i64TyU)}, llvm::APInt::getAllOnes(64)}, + }; + + std::pair key = + std::make_pair(value, toPtr(type)); + if (initMap.find(key) == initMap.end()) + return failure(); + if (auto intType = dyn_cast(type)) + return success( + IntegerAttr::get(intType, std::get(initMap.at(key)))); + if (isa(type)) + return success( + FloatAttr::get(f16Ty, std::get(initMap.at(key)))); + if (isa(type)) + return success( + FloatAttr::get(bf16Ty, std::get(initMap.at(key)))); + if (isa(type)) + return success( + FloatAttr::get(f32Ty, std::get(initMap.at(key)))); + return failure(); +} + +// Specialize the Typeless Value (Zero, Min, Max) into a mlir constant value +FailureOr specializeTypelessValueToConstant(TypelessValue value, + Type type, Location loc, + OpBuilder &b) { + std::function getElemType = [&](mlir::Type ty) { + if (auto ptrType = dyn_cast(getElementTypeOrSelf(ty))) + return getElemType(ptrType.getPointeeType()); + if (auto tensorType = mlir::dyn_cast(ty)) + return getElemType(tensorType.getElementType()); + return ty; + }; + + if (value == TypelessValue::Undefined) + return failure(); + if (auto tensorType = mlir::dyn_cast(type)) { + auto elemType = getElemType(tensorType); + FailureOr typedAttr = + specializeTypelessValueToAttr(value, elemType, b); + if (failed(typedAttr)) + return failure(); + auto otherTensorType = + RankedTensorType::get(tensorType.getShape(), elemType); + auto denseAttr = DenseElementsAttr::get(otherTensorType, *typedAttr); + return b.create(loc, denseAttr).getResult(); + } + if (mlir::isa(type) || mlir::isa(type)) { + FailureOr typedAttr = + specializeTypelessValueToAttr(value, type, b); + if (failed(typedAttr)) + return failure(); + return b.create(loc, *typedAttr).getResult(); + } + return failure(); +} + +bool checkStructureAnnotated(Operation *op, RewriterBase &rewriter) { + return llvm::any_of(op->getUsers(), [&rewriter](Operation *user) { + auto annotationOp = dyn_cast(user); + if (annotationOp && + annotationOp->hasAttr(ConverterUtils::continuousAttrName)) { + rewriter.eraseOp(annotationOp); + return true; + } + return false; + }); +} + +bool isDistributedTypeCustomOp(Operation *op) { + return op->hasAttr("hivm.is_distributed") && + (llvm::isa(op) || llvm::isa(op)); +} +} // namespace mlir diff --git a/third_party/ascend/patch/llvm_patch_f6ded0b.patch b/third_party/ascend/patch/llvm_patch_f6ded0b.patch new file mode 100644 index 0000000000..2dff011cb8 --- /dev/null +++ b/third_party/ascend/patch/llvm_patch_f6ded0b.patch @@ -0,0 +1,662 @@ +diff --git a/mlir/include/mlir/Dialect/Arith/IR/ArithOps.td b/mlir/include/mlir/Dialect/Arith/IR/ArithOps.td +index ef9ccb7e8794..83d85f173802 100644 +--- a/mlir/include/mlir/Dialect/Arith/IR/ArithOps.td ++++ b/mlir/include/mlir/Dialect/Arith/IR/ArithOps.td +@@ -1317,6 +1317,7 @@ def Arith_TruncIOp : Arith_Op<"trunci", + let hasFolder = 1; + let hasCanonicalizer = 1; + let hasVerifier = 1; ++ let useCustomPropertiesEncoding = 1; + } + + //===----------------------------------------------------------------------===// +diff --git a/mlir/include/mlir/Dialect/ControlFlow/IR/ControlFlowOps.td b/mlir/include/mlir/Dialect/ControlFlow/IR/ControlFlowOps.td +index a441fd82546e..b7569ef0fbc7 100644 +--- a/mlir/include/mlir/Dialect/ControlFlow/IR/ControlFlowOps.td ++++ b/mlir/include/mlir/Dialect/ControlFlow/IR/ControlFlowOps.td +@@ -233,6 +233,7 @@ def CondBranchOp + $falseDest (`(` $falseDestOperands^ `:` type($falseDestOperands) `)`)? + attr-dict + }]; ++ let useCustomPropertiesEncoding = 1; + } + + //===----------------------------------------------------------------------===// +diff --git a/mlir/include/mlir/Dialect/Func/IR/FuncOps.td b/mlir/include/mlir/Dialect/Func/IR/FuncOps.td +index 06ce4f16c867..905efc7e4d30 100644 +--- a/mlir/include/mlir/Dialect/Func/IR/FuncOps.td ++++ b/mlir/include/mlir/Dialect/Func/IR/FuncOps.td +@@ -122,6 +122,8 @@ def CallOp : Func_Op<"call", + let assemblyFormat = [{ + $callee `(` $operands `)` attr-dict `:` functional-type($operands, results) + }]; ++ ++ let useCustomPropertiesEncoding = 1; + } + + //===----------------------------------------------------------------------===// +@@ -357,6 +359,7 @@ def FuncOp : Func_Op<"func", [ + bool isDeclaration() { return isExternal(); } + }]; + let hasCustomAssemblyFormat = 1; ++ let useCustomPropertiesEncoding = 1; + } + + //===----------------------------------------------------------------------===// +diff --git a/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td b/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td +index 5e2461b4508e..d3c398ab12c9 100644 +--- a/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td ++++ b/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td +@@ -554,7 +554,7 @@ def LLVM_AssumeOp + $op_bundle_tags)^ )? + `:` type($cond) attr-dict + }]; +- ++ let useCustomPropertiesEncoding = 1; + let builders = [ + OpBuilder<(ins "Value":$cond)>, + OpBuilder<(ins "Value":$cond, "llvm::StringRef":$tag, "ValueRange":$args)>, +diff --git a/mlir/include/mlir/Dialect/Linalg/IR/LinalgStructuredOps.td b/mlir/include/mlir/Dialect/Linalg/IR/LinalgStructuredOps.td +index f3674c3eecfe..312ef5d211af 100644 +--- a/mlir/include/mlir/Dialect/Linalg/IR/LinalgStructuredOps.td ++++ b/mlir/include/mlir/Dialect/Linalg/IR/LinalgStructuredOps.td +@@ -775,6 +775,7 @@ def MatmulOp : LinalgStructuredBase_Op<"matmul", [ + + ]; + let hasCustomAssemblyFormat = 1; ++ let useCustomPropertiesEncoding = 1; + let hasFolder = 1; + let hasVerifier = 1; + +@@ -1045,6 +1046,7 @@ def BatchMatmulOp : LinalgStructuredBase_Op<"batch_matmul", !listconcat([AttrSiz + + ]; + let hasCustomAssemblyFormat = 1; ++ let useCustomPropertiesEncoding = 1; + let hasFolder = 1; + let hasVerifier = 1; + +diff --git a/mlir/include/mlir/Dialect/SCF/IR/SCFOps.td b/mlir/include/mlir/Dialect/SCF/IR/SCFOps.td +index 88df54174da2..01c8ea9b61c0 100644 +--- a/mlir/include/mlir/Dialect/SCF/IR/SCFOps.td ++++ b/mlir/include/mlir/Dialect/SCF/IR/SCFOps.td +@@ -309,6 +309,7 @@ def ForOp : SCF_Op<"for", + + let hasCanonicalizer = 1; + let hasCustomAssemblyFormat = 1; ++ let useCustomPropertiesEncoding = 1; + let hasVerifier = 1; + let hasRegionVerifier = 1; + } +diff --git a/mlir/lib/Dialect/Arith/IR/ArithOps.cpp b/mlir/lib/Dialect/Arith/IR/ArithOps.cpp +index 7d4d818ee448..3fdc036abf14 100644 +--- a/mlir/lib/Dialect/Arith/IR/ArithOps.cpp ++++ b/mlir/lib/Dialect/Arith/IR/ArithOps.cpp +@@ -1560,6 +1560,13 @@ LogicalResult arith::ScalingExtFOp::verify() { + //===----------------------------------------------------------------------===// + // TruncIOp + //===----------------------------------------------------------------------===// ++::llvm::LogicalResult TruncIOp::readProperties(::mlir::DialectBytecodeReader &reader, ::mlir::OperationState &state) { ++ return ::mlir::success(); ++} ++ ++void TruncIOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ return; ++} + + OpFoldResult arith::TruncIOp::fold(FoldAdaptor adaptor) { + if (matchPattern(getOperand(), m_Op()) || +diff --git a/mlir/lib/Dialect/Bufferization/IR/BufferizationDialect.cpp b/mlir/lib/Dialect/Bufferization/IR/BufferizationDialect.cpp +index 6c08cdfb669f..f4442c9d0cd6 100644 +--- a/mlir/lib/Dialect/Bufferization/IR/BufferizationDialect.cpp ++++ b/mlir/lib/Dialect/Bufferization/IR/BufferizationDialect.cpp +@@ -98,6 +98,25 @@ struct BuiltinMemRefExternalModel + //===----------------------------------------------------------------------===// + // Bufferization Dialect + //===----------------------------------------------------------------------===// ++struct BufferizationDialectVersion : public DialectVersion { ++ BufferizationDialectVersion(uint64_t version) : version(version) {} ++ uint64_t version; ++}; ++ ++struct BufferizationBytecodeInterface : public BytecodeDialectInterface { ++ BufferizationBytecodeInterface(Dialect *dialect) : BytecodeDialectInterface(dialect) {} ++ ++ void writeVersion(DialectBytecodeWriter &writer) const override { ++ writer.writeVarInt(351); ++ return; ++ } ++ ++ std::unique_ptr readVersion(DialectBytecodeReader &reader) const override { ++ uint64_t version; ++ if (failed(reader.readVarInt(version))) return nullptr; ++ return std::make_unique(version); ++ } ++}; + + void mlir::bufferization::BufferizationDialect::initialize() { + addOperations< +@@ -119,6 +138,7 @@ void mlir::bufferization::BufferizationDialect::initialize() { + *getContext()); + UnrankedMemRefType::attachInterface< + BuiltinMemRefExternalModel>(*getContext()); ++ addInterfaces(); + } + + LogicalResult BufferizationDialect::verifyRegionArgAttribute( +diff --git a/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp b/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp +index 582593adfa5c..48e0cf29bc60 100644 +--- a/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp ++++ b/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp +@@ -458,6 +458,57 @@ Block *CondBranchOp::getSuccessorForOperands(ArrayRef operands) { + return nullptr; + } + ++::llvm::LogicalResult CondBranchOp::readProperties(::mlir::DialectBytecodeReader &reader, ::mlir::OperationState &state) { ++ auto &prop = state.getOrAddProperties(); (void)prop; ++ if (::mlir::failed(reader.readOptionalAttribute(prop.branch_weights))) ++ return ::mlir::failure(); ++ ++ if (reader.getBytecodeVersion() < /*kNativePropertiesODSSegmentSize=*/6) { ++ auto &propStorage = prop.operandSegmentSizes; ++ ::mlir::DenseI32ArrayAttr attr; ++ if (::mlir::failed(reader.readAttribute(attr))) return ::mlir::failure(); ++ if (attr.size() > static_cast(sizeof(propStorage) / sizeof(int32_t))) { ++ reader.emitError("size mismatch for operand/result_segment_size"); ++ return ::mlir::failure(); ++ } ++ ::llvm::copy(::llvm::ArrayRef(attr), propStorage.begin()); ++ } ++ ++ { ++ auto &propStorage = prop.operandSegmentSizes; ++ auto readProp = [&]() { ++ ++ if (reader.getBytecodeVersion() >= /*kNativePropertiesODSSegmentSize=*/6) ++ return reader.readSparseArray(::llvm::MutableArrayRef(propStorage)); ++; ++ return ::mlir::success(); ++ }; ++ if (::mlir::failed(readProp())) ++ return ::mlir::failure(); ++ } ++ return ::mlir::success(); ++} ++ ++void CondBranchOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ auto &prop = getProperties(); (void)prop; ++ ++ // writer.writeOptionalAttribute(prop.branch_weights); ++ ++if (writer.getBytecodeVersion() < /*kNativePropertiesODSSegmentSize=*/6) { ++ auto &propStorage = prop.operandSegmentSizes; ++ writer.writeAttribute(::mlir::DenseI32ArrayAttr::get(this->getContext(), propStorage)); ++} ++ ++ { ++ auto &propStorage = prop.operandSegmentSizes; ++ ++ if (writer.getBytecodeVersion() >= /*kNativePropertiesODSSegmentSize=*/6) ++ writer.writeSparseArray(::llvm::ArrayRef(propStorage)); ++; ++ } ++} ++ ++ + //===----------------------------------------------------------------------===// + // SwitchOp + //===----------------------------------------------------------------------===// +diff --git a/mlir/lib/Dialect/Func/IR/FuncOps.cpp b/mlir/lib/Dialect/Func/IR/FuncOps.cpp +index 3c09a2124bd7..3b41d9a18efb 100644 +--- a/mlir/lib/Dialect/Func/IR/FuncOps.cpp ++++ b/mlir/lib/Dialect/Func/IR/FuncOps.cpp +@@ -33,6 +33,26 @@ using namespace mlir::func; + // FuncDialect + //===----------------------------------------------------------------------===// + ++struct FuncDialectVersion : public DialectVersion { ++ FuncDialectVersion(uint64_t version) : version(version) {} ++ uint64_t version; ++}; ++ ++struct FuncBytecodeInterface : public BytecodeDialectInterface { ++ FuncBytecodeInterface(Dialect *dialect) : BytecodeDialectInterface(dialect) {} ++ ++ void writeVersion(DialectBytecodeWriter &writer) const override { ++ writer.writeVarInt(351); ++ return; ++ } ++ ++ std::unique_ptr readVersion(DialectBytecodeReader &reader) const override { ++ uint64_t version; ++ if (failed(reader.readVarInt(version))) return nullptr; ++ return std::make_unique(version); ++ } ++}; ++ + void FuncDialect::initialize() { + addOperations< + #define GET_OP_LIST +@@ -43,6 +63,7 @@ void FuncDialect::initialize() { + declarePromisedInterface(); + declarePromisedInterfaces(); ++ addInterfaces(); + } + + /// Materialize a single constant operation from a given attribute value with +@@ -279,6 +300,89 @@ FuncOp FuncOp::clone() { + return clone(mapper); + } + ++::llvm::LogicalResult FuncOp::readProperties(DialectBytecodeReader &reader, ++ OperationState &state) { ++ auto &prop = state.getOrAddProperties(); (void)prop; ++ ++ // 1. arg_attrs ++ if (failed(reader.readOptionalAttribute(prop.arg_attrs))) ++ return failure(); ++ ++ // 2. function_type ++ if (failed(reader.readAttribute(prop.function_type))) ++ return failure(); ++ ++ // --- 关键修改:版本感知的 no_inline 读取 --- ++ // 1. 移除 auto 后面的星号,因为返回的是 FailureOr 包装类 ++ auto versionOr = reader.getDialectVersion(); ++ ++ uint64_t version = 0; ++ // 2. 检查是否读取成功,并且返回的指针不为空 ++ if (succeeded(versionOr) && *versionOr) { ++ // 3. 使用 *versionOr 获取内部的 const DialectVersion*,再进行类型转换 ++ version = static_cast(*versionOr)->version; ++ } ++ ++ if (version >= 1) { ++ // 只有新版文件才读取这 1 字节 ++ if (failed(reader.readOptionalAttribute(prop.no_inline))) ++ return failure(); ++ } else { ++ // 旧版文件没有这个属性,设为默认值(false),不移动指针 ++ prop.no_inline = nullptr; ++ } ++ ++ // 3. res_attrs (字母 r) ++ if (failed(reader.readOptionalAttribute(prop.res_attrs))) ++ return failure(); ++ ++ // 4. sym_name (字母 s) ++ if (failed(reader.readAttribute(prop.sym_name))) ++ return failure(); ++ ++ // 5. sym_visibility ++ if (failed(reader.readOptionalAttribute(prop.sym_visibility))) ++ return failure(); ++ ++ return success(); ++} ++ ++ ++void FuncOp::writeProperties(DialectBytecodeWriter &writer) { ++ auto &prop = getProperties(); (void)prop; ++ ++ writer.writeOptionalAttribute(prop.arg_attrs); ++ writer.writeAttribute(prop.function_type); ++ ++ writer.writeOptionalAttribute(prop.res_attrs); ++ writer.writeAttribute(prop.sym_name); ++ writer.writeOptionalAttribute(prop.sym_visibility); ++} ++ ++::llvm::LogicalResult CallOp::readProperties(DialectBytecodeReader &reader, OperationState &state) { ++ auto &prop = state.getOrAddProperties(); (void)prop; ++ ++ // 读取必需的 "callee" 属性 ++ if (failed(reader.readAttribute(prop.callee))) ++ return failure(); ++ ++ if (failed(reader.readOptionalAttribute(prop.arg_attrs))) ++ return failure(); ++ ++ if (failed(reader.readOptionalAttribute(prop.res_attrs))) ++ return failure(); ++ ++ return success(); ++} ++ ++void CallOp::writeProperties(DialectBytecodeWriter &writer) { ++ auto &prop = getProperties(); (void)prop; ++ ++ writer.writeAttribute(prop.callee); ++ ++ // writer.writeOptionalAttribute(prop.arg_attrs); ++ // writer.writeOptionalAttribute(prop.res_attrs); ++} + //===----------------------------------------------------------------------===// + // ReturnOp + //===----------------------------------------------------------------------===// +diff --git a/mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp b/mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp +index a6e89f63c822..21e863025215 100644 +--- a/mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp ++++ b/mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp +@@ -4114,6 +4114,14 @@ void LLVM::AssumeOp::build(OpBuilder &builder, OperationState &state, + + LogicalResult LLVM::AssumeOp::verify() { return verifyOperandBundles(*this); } + ++::llvm::LogicalResult AssumeOp::readProperties(::mlir::DialectBytecodeReader &reader, ::mlir::OperationState &state) { ++ return ::mlir::success(); ++} ++ ++void AssumeOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ return; ++} ++ + //===----------------------------------------------------------------------===// + // masked_gather (intrinsic) + //===----------------------------------------------------------------------===// +diff --git a/mlir/lib/Dialect/Linalg/IR/LinalgOps.cpp b/mlir/lib/Dialect/Linalg/IR/LinalgOps.cpp +index e0e371617729..da3ab7590e54 100644 +--- a/mlir/lib/Dialect/Linalg/IR/LinalgOps.cpp ++++ b/mlir/lib/Dialect/Linalg/IR/LinalgOps.cpp +@@ -203,6 +203,110 @@ static void buildMatmulOp(OpBuilder &b, OperationState &state, + attributes, regionBuilder); + } + ++::llvm::LogicalResult MatmulOp::readProperties(::mlir::DialectBytecodeReader &reader, ::mlir::OperationState &state) { ++ auto &prop = state.getOrAddProperties(); (void)prop; ++ if (::mlir::failed(reader.readOptionalAttribute(prop.cast))) ++ return ::mlir::failure(); ++ ++ if (::mlir::failed(reader.readOptionalAttribute(prop.indexing_maps))) ++ return ::mlir::failure(); ++ ++ if (reader.getBytecodeVersion() < /*kNativePropertiesODSSegmentSize=*/6) { ++ auto &propStorage = prop.operandSegmentSizes; ++ ::mlir::DenseI32ArrayAttr attr; ++ if (::mlir::failed(reader.readAttribute(attr))) return ::mlir::failure(); ++ if (attr.size() > static_cast(sizeof(propStorage) / sizeof(int32_t))) { ++ reader.emitError("size mismatch for operand/result_segment_size"); ++ return ::mlir::failure(); ++ } ++ ::llvm::copy(::llvm::ArrayRef(attr), propStorage.begin()); ++ } ++ ++ { ++ auto &propStorage = prop.operandSegmentSizes; ++ auto readProp = [&]() { ++ ++ if (reader.getBytecodeVersion() >= /*kNativePropertiesODSSegmentSize=*/6) ++ return reader.readSparseArray(::llvm::MutableArrayRef(propStorage)); ++; ++ return ::mlir::success(); ++ }; ++ if (::mlir::failed(readProp())) ++ return ::mlir::failure(); ++ } ++ return ::mlir::success(); ++} ++ ++void MatmulOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ auto &prop = getProperties(); (void)prop; ++ ++ writer.writeOptionalAttribute(prop.cast); ++ ++if (writer.getBytecodeVersion() < /*kNativePropertiesODSSegmentSize=*/6) { ++ auto &propStorage = prop.operandSegmentSizes; ++ writer.writeAttribute(::mlir::DenseI32ArrayAttr::get(this->getContext(), propStorage)); ++} ++ ++ { ++ auto &propStorage = prop.operandSegmentSizes; ++ ++ if (writer.getBytecodeVersion() >= /*kNativePropertiesODSSegmentSize=*/6) ++ writer.writeSparseArray(::llvm::ArrayRef(propStorage)); ++; ++ } ++} ++ ++::llvm::LogicalResult BatchMatmulOp::readProperties(::mlir::DialectBytecodeReader &reader, ::mlir::OperationState &state) { ++ auto &prop = state.getOrAddProperties(); (void)prop; ++ if (::mlir::failed(reader.readOptionalAttribute(prop.cast))) ++ return ::mlir::failure(); ++ ++ if (::mlir::failed(reader.readOptionalAttribute(prop.indexing_maps))) ++ return ::mlir::failure(); ++ ++ if (reader.getBytecodeVersion() < /*kNativePropertiesODSSegmentSize=*/6) { ++ auto &propStorage = prop.operandSegmentSizes; ++ ::mlir::DenseI32ArrayAttr attr; ++ if (::mlir::failed(reader.readAttribute(attr))) return ::mlir::failure(); ++ if (attr.size() > static_cast(sizeof(propStorage) / sizeof(int32_t))) { ++ reader.emitError("size mismatch for operand/result_segment_size"); ++ return ::mlir::failure(); ++ } ++ ::llvm::copy(::llvm::ArrayRef(attr), propStorage.begin()); ++ } ++ ++ { ++ auto &propStorage = prop.operandSegmentSizes; ++ auto readProp = [&]() { ++ ++ if (reader.getBytecodeVersion() >= /*kNativePropertiesODSSegmentSize=*/6) ++ return reader.readSparseArray(::llvm::MutableArrayRef(propStorage)); ++; ++ return ::mlir::success(); ++ }; ++ if (::mlir::failed(readProp())) ++ return ::mlir::failure(); ++ } ++ return ::mlir::success(); ++} ++ ++void BatchMatmulOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ auto &prop = getProperties(); (void)prop; ++ ++if (writer.getBytecodeVersion() < /*kNativePropertiesODSSegmentSize=*/6) { ++ auto &propStorage = prop.operandSegmentSizes; ++ writer.writeAttribute(::mlir::DenseI32ArrayAttr::get(this->getContext(), propStorage)); ++} ++ ++ { ++ auto &propStorage = prop.operandSegmentSizes; ++ ++ if (writer.getBytecodeVersion() >= /*kNativePropertiesODSSegmentSize=*/6) ++ writer.writeSparseArray(::llvm::ArrayRef(propStorage)); ++; ++ } ++} ++ + static void buildBatchMatmulOp(OpBuilder &b, OperationState &state, + std::optional resultTensorTypes, + ValueRange inputs, ValueRange outputs, +diff --git a/mlir/lib/Dialect/SCF/IR/SCF.cpp b/mlir/lib/Dialect/SCF/IR/SCF.cpp +index 0dbc041d231a..9d1960a65a74 100644 +--- a/mlir/lib/Dialect/SCF/IR/SCF.cpp ++++ b/mlir/lib/Dialect/SCF/IR/SCF.cpp +@@ -315,6 +315,18 @@ void ConditionOp::getSuccessorRegions( + //===----------------------------------------------------------------------===// + // ForOp + //===----------------------------------------------------------------------===// ++::llvm::LogicalResult ForOp::readProperties(::mlir::DialectBytecodeReader &reader, ::mlir::OperationState &state) { ++ // auto &prop = state.getOrAddProperties(); (void)prop; ++ // if (::mlir::failed(reader.readOptionalAttribute(prop.unsignedCmp))) ++ // return ::mlir::failure(); ++ return ::mlir::success(); ++} ++ ++void ForOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ // auto &prop = getProperties(); (void)prop; ++ ++ // writer.writeOptionalAttribute(prop.unsignedCmp); ++} + + void ForOp::build(OpBuilder &builder, OperationState &result, Value lb, + Value ub, Value step, ValueRange initArgs, +diff --git a/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp b/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp +--- a/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp ++++ b/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp +@@ -1025,11 +1025,21 @@ LogicalResult BytecodeWriter::writeOp(EncodingEmitter &emitter, Operation *op) { + } + + // Emit the operands of the operation. +- if (unsigned numOperands = op->getNumOperands()) { ++ unsigned numOperands = op->getNumOperands(); ++ bool dropAssumeOperandBundles = ++ op->getName().getStringRef() == "llvm.intr.assume" && numOperands > 1; ++ if (dropAssumeOperandBundles) ++ numOperands = 1; ++ if (numOperands) { + opEncodingMask |= bytecode::OpEncodingMask::kHasOperands; + emitter.emitVarInt(numOperands, "op operands count"); +- for (Value operand : op->getOperands()) +- emitter.emitVarInt(numberingState.getNumber(operand), "op operand types"); ++ if (dropAssumeOperandBundles) { ++ emitter.emitVarInt(numberingState.getNumber(op->getOperand(0)), ++ "op operand types"); ++ } else { ++ for (Value operand : op->getOperands()) ++ emitter.emitVarInt(numberingState.getNumber(operand), "op operand types"); ++ } + } + + // Emit the successors of the operation. +diff --git a/mlir/include/mlir/Dialect/LLVMIR/LLVMOps.td b/mlir/include/mlir/Dialect/LLVMIR/LLVMOps.td +--- a/mlir/include/mlir/Dialect/LLVMIR/LLVMOps.td ++++ b/mlir/include/mlir/Dialect/LLVMIR/LLVMOps.td +@@ -456,6 +456,7 @@ def LLVM_LoadOp : LLVM_MemAccessOpBase<"load", + CArg<"StringRef", "StringRef()">:$syncscope)> + ]; + let hasVerifier = 1; ++ let useCustomPropertiesEncoding = 1; + } + + def LLVM_StoreOp : LLVM_MemAccessOpBase<"store", +@@ -621,8 +621,10 @@ + let hasFolder = 1; + } + def LLVM_IntToPtrOp : LLVM_DereferenceableCastOp<"inttoptr", "IntToPtr", +- LLVM_ScalarOrVectorOf, +- LLVM_ScalarOrVectorOf>; ++ LLVM_ScalarOrVectorOf, ++ LLVM_ScalarOrVectorOf> { ++ let useCustomPropertiesEncoding = 1; ++} + def LLVM_PtrToIntOp : LLVM_CastOp<"ptrtoint", "PtrToInt", + LLVM_ScalarOrVectorOf, + LLVM_ScalarOrVectorOf>; +@@ -2409,7 +2411,8 @@ def LLVM_InlineAsmOp : LLVM_Op<"inline_asm", [DeclareOpInterfaceMethods(); (void)prop; ++ if (::mlir::failed(reader.readOptionalAttribute(prop.access_groups))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.alias_scopes))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.alignment))) ++ return ::mlir::failure(); ++ ::mlir::Attribute dereferenceable; ++ if (::mlir::failed(reader.readOptionalAttribute(dereferenceable))) ++ return ::mlir::failure(); ++ prop.dereferenceable = nullptr; ++ if (::mlir::failed(reader.readOptionalAttribute(prop.invariant))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.invariantGroup))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.noalias_scopes))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.nontemporal))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.ordering))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.syncscope))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.tbaa))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.volatile_))) ++ return ::mlir::failure(); ++ return ::mlir::success(); ++} ++ ++void LoadOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ auto &prop = getProperties(); (void)prop; ++ writer.writeOptionalAttribute(prop.access_groups); ++ writer.writeOptionalAttribute(prop.alias_scopes); ++ writer.writeOptionalAttribute(prop.alignment); ++ // LLVM 19/20 does not know the dereferenceable load property. ++ writer.writeOptionalAttribute(::mlir::Attribute()); ++ writer.writeOptionalAttribute(prop.invariant); ++ writer.writeOptionalAttribute(prop.invariantGroup); ++ writer.writeOptionalAttribute(prop.noalias_scopes); ++ writer.writeOptionalAttribute(prop.nontemporal); ++ writer.writeOptionalAttribute(prop.ordering); ++ writer.writeOptionalAttribute(prop.syncscope); ++ writer.writeOptionalAttribute(prop.tbaa); ++ writer.writeOptionalAttribute(prop.volatile_); ++} ++ ++::llvm::LogicalResult IntToPtrOp::readProperties( ++ ::mlir::DialectBytecodeReader &reader, ::mlir::OperationState &state) { ++ auto &prop = state.getOrAddProperties(); (void)prop; ++ prop.dereferenceable = nullptr; ++ return ::mlir::success(); ++} ++ ++void IntToPtrOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ return; ++} ++ + //===----------------------------------------------------------------------===// + // CallOp + //===----------------------------------------------------------------------===// +@@ -4197,6 +4208,37 @@ void LLVM::masked_compressstore::build(OpBuilder &builder, + // InlineAsmOp + //===----------------------------------------------------------------------===// +- ++::llvm::LogicalResult InlineAsmOp::readProperties( ++ ::mlir::DialectBytecodeReader &reader, ::mlir::OperationState &state) { ++ auto &prop = state.getOrAddProperties(); (void)prop; ++ if (::mlir::failed(reader.readOptionalAttribute(prop.asm_dialect))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readAttribute(prop.asm_string))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readAttribute(prop.constraints))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.has_side_effects))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.is_align_stack))) ++ return ::mlir::failure(); ++ if (::mlir::failed(reader.readOptionalAttribute(prop.operand_attrs))) ++ return ::mlir::failure(); ++ // LLVM 20 does not know the tail_call_kind inline asm property. ++ prop.tail_call_kind = ++ TailCallKindAttr::get(state.getContext(), TailCallKind::None); ++ return ::mlir::success(); ++} ++ ++void InlineAsmOp::writeProperties(::mlir::DialectBytecodeWriter &writer) { ++ auto &prop = getProperties(); (void)prop; ++ writer.writeOptionalAttribute(prop.asm_dialect); ++ writer.writeAttribute(prop.asm_string); ++ writer.writeAttribute(prop.constraints); ++ writer.writeOptionalAttribute(prop.has_side_effects); ++ writer.writeOptionalAttribute(prop.is_align_stack); ++ writer.writeOptionalAttribute(prop.operand_attrs); ++ // LLVM 20 does not know the tail_call_kind inline asm property. ++} ++ + LogicalResult InlineAsmOp::verify() { + if (!getTailCallKindAttr()) + return success(); diff --git a/third_party/ascend/patch/triton-ascend-3.6.0.patch b/third_party/ascend/patch/triton-ascend-3.6.0.patch new file mode 100644 index 0000000000..781c4a18fb --- /dev/null +++ b/third_party/ascend/patch/triton-ascend-3.6.0.patch @@ -0,0 +1,1214 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index cab58801f..406ed4634 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -15,13 +15,22 @@ include(CTest) + + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + ++# Security compilation options settings ++include(${CMAKE_CURRENT_SOURCE_DIR}/safe_compile.cmake) ++ + # Options + option(TRITON_BUILD_PYTHON_MODULE "Build Python Triton bindings" OFF) + option(TRITON_BUILD_PROTON "Build the Triton Proton profiler" ON) ++option(TRITON_BUILD_TD "Build Triton Distributed" OFF) + option(TRITON_BUILD_UT "Build C++ Triton Unit Tests" ON) + option(TRITON_BUILD_WITH_CCACHE "Build with ccache (if available)" ON) + set(TRITON_CODEGEN_BACKENDS "" CACHE STRING "Enable different codegen backends") + ++if(TRITON_BUILD_TD) ++ set(TRITON_USE_ASCEND ON) ++ add_compile_definitions(TRITON_USE_ASCEND=1) ++endif() ++ + if(TRITON_BUILD_WITH_CCACHE) + find_program(CCACHE_PROGRAM ccache) + if(CCACHE_PROGRAM) +@@ -129,6 +138,21 @@ function(add_triton_library name) + set_property(GLOBAL APPEND PROPERTY TRITON_LIBS ${name}) + add_triton_object(${name} ${ARGN}) + llvm_update_compile_flags(${name}) ++ ++ # To enable llvm-cov coverage tool, you need to pass the argument TRITON_APPEND_CMAKE_ARGS="-DTRITON_ENABLE_COVERAGE_LLVM_COV=ON" during compilation. ++ if(TRITON_ENABLE_COVERAGE_LLVM_COV) ++ if("${CMAKE_CURRENT_SOURCE_DIR}" MATCHES ".*/third_party/ascend/.*" AND NOT "${CMAKE_CURRENT_SOURCE_DIR}" MATCHES ".*/AscendNPU-IR/.*") ++ message(STATUS "Enabling coverage for target ${name} (in Ascend plugin)") ++ target_compile_options(${name} PRIVATE ++ -fprofile-arcs ++ -ftest-coverage ++ -O0 ++ -fprofile-update=atomic ++ -g ++ ) ++ target_link_options(${name} PUBLIC --coverage) ++ endif() ++ endif() + endfunction() + + set_property(GLOBAL PROPERTY TRITON_PLUGINS "") +@@ -164,6 +188,21 @@ if (NOT WIN32 AND NOT APPLE AND NOT BSD) + link_libraries(stdc++fs) + endif() + ++# Adapter LLVM VERSION For AscendNPU IR ++option(LLVM_MAJOR_VERSION_21_COMPATIBLE "NPUIR build with LLVM 21 or later" OFF) ++if(LLVM_MAJOR_VERSION_21_COMPATIBLE) ++ message(STATUS "[TA]Building AscendNPU IR with LLVM 21 or later") ++ add_definitions(-D__LLVM_MAJOR_VERSION_21_COMPATIBLE__) ++endif() ++ ++# Some compatibility changes for LLVM 21 are also needed on LLVM 22. ++# Therefore __LLVM_MAJOR_VERSION_21_COMPATIBLE__ would be enabled too. ++option(LLVM_MAJOR_VERSION_22_COMPATIBLE "NPUIR build with LLVM 22" OFF) ++if(LLVM_MAJOR_VERSION_22_COMPATIBLE) ++ message(STATUS "[TA]Building AscendNPU IR with LLVM 22 or later") ++ add_definitions(-D__LLVM_MAJOR_VERSION_21_COMPATIBLE__) ++ add_definitions(-D__LLVM_MAJOR_VERSION_22_COMPATIBLE__) ++endif() + + # ----- + +@@ -176,11 +215,21 @@ if(TRITON_BUILD_PYTHON_MODULE) + # Python Interpreter is used to run lit tests + find_package(Python3 REQUIRED COMPONENTS Development.Module Interpreter) + find_package(pybind11 CONFIG REQUIRED HINTS "${Python3_SITELIB}") ++ # need to include pybind11 ++ include_directories(${Python3_INCLUDE_DIRS}) ++ include_directories(${pybind11_INCLUDE_DIR}) ++ link_directories(${Python3_LIBRARY_DIRS}) ++ link_libraries(${Python3_LIBRARIES}) ++ add_link_options(${Python3_LINK_OPTIONS}) + + foreach(CODEGEN_BACKEND ${TRITON_CODEGEN_BACKENDS}) + add_subdirectory(third_party/${CODEGEN_BACKEND}) + endforeach() + ++ if(TRITON_BUILD_TD) ++ list(APPEND TRITON_PLUGIN_NAMES "distributed") ++ endif() ++ + if (TRITON_BUILD_PROTON) + add_subdirectory(third_party/proton) + endif() +@@ -306,11 +355,6 @@ if(TRITON_BUILD_PYTHON_MODULE) + message(FATAL_ERROR "TRITON_WHEEL_DIR must be set.") + endif() + +- configure_file( +- "${LLVM_SYSPATH}/bin/FileCheck" +- "${TRITON_WHEEL_DIR}/FileCheck" +- COPYONLY) +- + endif() + + if (UNIX AND NOT APPLE) +@@ -338,6 +387,12 @@ endif() + find_package(Threads REQUIRED) + + add_subdirectory(third_party/f2reduce) ++ ++# To enable the hitest coverage tool, you need to set the environment variable TRITON_ENABLE_COVERAGE_HITEST=1 ++if(TRITON_ENABLE_COVERAGE_HITEST) ++ set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK hitestwrapper) ++endif() ++ + add_subdirectory(bin) + add_subdirectory(test) + +diff --git a/bin/CMakeLists.txt b/bin/CMakeLists.txt +index cc0b4fa15..82f52f87e 100644 +--- a/bin/CMakeLists.txt ++++ b/bin/CMakeLists.txt +@@ -1,11 +1,38 @@ + get_property(triton_libs GLOBAL PROPERTY TRITON_LIBS) + ++include_directories( ++ ${PROJECT_SOURCE_DIR}/third_party/ascend/include ++ ${PROJECT_SOURCE_DIR}/third_party/ascend/AscendNPU-IR/bishengir/include ++ ${PROJECT_BINARY_DIR}/third_party/ascend/AscendNPU-IR/bishengir/include ++ ) ++ + add_llvm_executable(triton-opt triton-opt.cpp PARTIAL_SOURCES_INTENDED) ++target_include_directories(triton-opt PRIVATE ++ "${PROJECT_SOURCE_DIR}/third_party/ascend/include") + + # TODO: what's this? + llvm_update_compile_flags(triton-opt) + target_link_libraries(triton-opt PRIVATE + ${triton_libs} ++ # Ascend backend libraries ++ DiscreteMaskAccessConversion ++ TritonToAnnotation ++ TritonToHFusion ++ TritonToHIVM ++ TritonToLinalg ++ TritonToLLVM ++ TritonToStructured ++ TritonToUnstructure ++ # BiShengIR dialects ++ BiShengIRAnnotationDialect ++ BiShengIRHACCDialect ++ BiShengIRHFusionDialect ++ BiShengIRHIVMDialect ++ BiShengIRMathExtDialect ++ BiShengIRMemRefExtDialect ++ BiShengIRScopeDialect ++ BiShengIRSymbolDialect ++ BiShengIRTensorDialect + # tests + TritonTestAnalysis + TritonTestDialect +@@ -27,6 +54,25 @@ mlir_check_all_link_libraries(triton-reduce) + llvm_update_compile_flags(triton-reduce) + target_link_libraries(triton-reduce PRIVATE + ${triton_libs} ++ # Ascend backend libraries ++ DiscreteMaskAccessConversion ++ TritonToAnnotation ++ TritonToHFusion ++ TritonToHIVM ++ TritonToLinalg ++ TritonToLLVM ++ TritonToStructured ++ TritonToUnstructure ++ # BiShengIR dialects ++ BiShengIRAnnotationDialect ++ BiShengIRHACCDialect ++ BiShengIRHFusionDialect ++ BiShengIRHIVMDialect ++ BiShengIRMathExtDialect ++ BiShengIRMemRefExtDialect ++ BiShengIRScopeDialect ++ BiShengIRSymbolDialect ++ BiShengIRTensorDialect + # tests + TritonTestAnalysis + TritonTestDialect +@@ -47,6 +93,25 @@ add_llvm_executable(triton-lsp triton-lsp.cpp PARTIAL_SOURCES_INTENDED) + llvm_update_compile_flags(triton-lsp) + target_link_libraries(triton-lsp PRIVATE + ${triton_libs} ++ # Ascend backend libraries ++ DiscreteMaskAccessConversion ++ TritonToAnnotation ++ TritonToHFusion ++ TritonToHIVM ++ TritonToLinalg ++ TritonToLLVM ++ TritonToStructured ++ TritonToUnstructure ++ # BiShengIR dialects ++ BiShengIRAnnotationDialect ++ BiShengIRHACCDialect ++ BiShengIRHFusionDialect ++ BiShengIRHIVMDialect ++ BiShengIRMathExtDialect ++ BiShengIRMemRefExtDialect ++ BiShengIRScopeDialect ++ BiShengIRSymbolDialect ++ BiShengIRTensorDialect + # tests + TritonTestAnalysis + TritonTestDialect +@@ -86,6 +151,26 @@ export_executable_symbols_for_plugins(triton-llvm-opt) + add_llvm_executable(triton-tensor-layout triton-tensor-layout.cpp PARTIAL_SOURCES_INTENDED) + target_link_libraries(triton-tensor-layout PRIVATE + ${triton_libs} ++ # Ascend backend libraries ++ DiscreteMaskAccessConversion ++ TritonToAnnotation ++ TritonToHFusion ++ TritonToHIVM ++ TritonToLinalg ++ TritonToLLVM ++ TritonToStructured ++ TritonToUnstructure ++ # BiShengIR dialects ++ BiShengIRAnnotationDialect ++ BiShengIRHACCDialect ++ BiShengIRHFusionDialect ++ BiShengIRHIVMDialect ++ BiShengIRMathExtDialect ++ BiShengIRMemRefExtDialect ++ BiShengIRScopeDialect ++ BiShengIRSymbolDialect ++ BiShengIRTensorDialect ++ # tests + TritonTestAnalysis + TritonTestDialect + TritonTestProton +diff --git a/bin/RegisterTritonDialects.h b/bin/RegisterTritonDialects.h +index 48049f026..e96284bc2 100644 +--- a/bin/RegisterTritonDialects.h ++++ b/bin/RegisterTritonDialects.h +@@ -1,6 +1,36 @@ + #pragma once ++#include "ascend/include/AutoBlockify/Passes.h" ++#include "ascend/include/DiscreteMaskAccessConversion/Passes.h" ++#include "ascend/include/DynamicCVPipeline/AddControlFlowCondition.h" ++#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.h" ++#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferOuterScope.h" ++#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" ++#include "ascend/include/DynamicCVPipeline/ComputeBlockOptPass.h" ++#include "ascend/include/DynamicCVPipeline/Passes.h" ++#include "ascend/include/DynamicCVPipeline/RemoveAttributes.h" ++#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromCompute/UBOverflowChecker.h" ++#include "ascend/include/DynamicCVPipeline/SeparateMemoryFromComputePass.h" ++#include "ascend/include/DynamicCVPipeline/SplitDataflow/RefineArgsBlockId.h" ++#include "ascend/include/DynamicCVPipeline/StandardizeOp.h" ++#include "ascend/include/TritonControlFlowOpt/Passes.h" ++#include "ascend/include/TritonToAnnotation/Passes.h" ++#include "ascend/include/TritonToGraph/Passes.h" ++#include "ascend/include/TritonToHFusion/Passes.h" ++#include "ascend/include/TritonToHIVM/Passes.h" ++#include "ascend/include/TritonToLLVM/Passes.h" ++#include "ascend/include/TritonToLinalg/Passes.h" ++#include "ascend/include/TritonToStructured/Passes.h" ++#include "ascend/include/TritonToUnstructure/Passes.h" ++ ++#include "bishengir/Dialect/Annotation/IR/Annotation.h" ++#include "bishengir/Dialect/HACC/IR/HACC.h" ++#include "bishengir/Dialect/HFusion/IR/HFusion.h" ++#include "bishengir/Dialect/HIVM/IR/HIVM.h" ++#include "bishengir/Dialect/Scope/IR/Scope.h" ++ + #include "amd/include/Dialect/TritonAMDGPU/IR/Dialect.h" + #include "amd/include/TritonAMDGPUTransforms/Passes.h" ++#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" + #include "nvidia/include/Dialect/NVGPU/IR/Dialect.h" + #include "nvidia/include/Dialect/NVWS/IR/Dialect.h" + #include "proton/Dialect/include/Conversion/ProtonGPUToLLVM/Passes.h" +@@ -85,6 +114,19 @@ inline void registerTritonDialects(mlir::DialectRegistry ®istry) { + mlir::triton::registerConvertTritonGPUToLLVMPass(); + mlir::triton::registerConvertNVGPUToLLVMPass(); + mlir::triton::registerAllocateSharedMemoryNvPass(); ++ mlir::triton::registerTritonToLinalgPasses(); ++ mlir::triton::registerTritonControlFlowOptPasses(); ++ mlir::triton::registerDiscreteMaskAccessConversion(); ++ mlir::triton::registerTritonToStructuredPasses(); ++ mlir::triton::registerTritonToAnnotationPasses(); ++ mlir::triton::registerTritonToUnstructurePasses(); ++ mlir::triton::registerTritonToHIVMPasses(); ++ mlir::triton::registerTritonToHFusionPasses(); ++ mlir::triton::registerTritonToLLVMPasses(); ++ mlir::triton::registerAutoBlockifyPasses(); ++ mlir::triton::cfg::registerTritonToGraphPasses(); ++ mlir::triton::registerBubbleUpOperationPass(); ++ + mlir::registerLLVMDIScope(); + mlir::LLVM::registerInlinerInterface(registry); + mlir::NVVM::registerInlinerInterface(registry); +@@ -136,16 +178,39 @@ inline void registerTritonDialects(mlir::DialectRegistry ®istry) { + mlir::triton::proton::gpu::registerScheduleBufferStorePass(); + mlir::triton::proton::gpu::registerAddSchedBarriersPass(); + ++ // DynamicCVPipeline passes ++ mlir::triton::registerAddDynamicCVPipelinePasses(); ++ mlir::triton::registerPreCheckAvailablePasses(); ++ mlir::triton::registerStandardizeOpPasses(); ++ mlir::triton::registerAddControlFlowConditionPasses(); ++ mlir::triton::registerAddMultiBufferOuterScopePasses(); ++ mlir::triton::registerAddMultiBufferInnerScopePasses(); ++ mlir::triton::registerUBOverflowCheckerPasses(); ++ mlir::triton::registerRemoveSsbufAttrPasses(); ++ mlir::triton::registerAnalyzeDataFlowPasses(); ++ mlir::triton::registerComputeBlockOptPasses(); ++ mlir::triton::registerPlanComputeBlockPasses(); ++ mlir::triton::registerOpClassifierPass(); ++ mlir::triton::registerRefineArgsBlockIdPasses(); ++ mlir::triton::registerSeparateMemoryFromComputePasses(); ++ + registry.insert< + mlir::triton::TritonDialect, mlir::cf::ControlFlowDialect, + mlir::triton::nvidia_gpu::TritonNvidiaGPUDialect, + mlir::triton::gpu::TritonGPUDialect, + mlir::triton::instrument::TritonInstrumentDialect, + mlir::math::MathDialect, mlir::arith::ArithDialect, mlir::scf::SCFDialect, +- mlir::gpu::GPUDialect, mlir::LLVM::LLVMDialect, mlir::NVVM::NVVMDialect, ++ mlir::tensor::TensorDialect, mlir::gpu::GPUDialect, ++ mlir::LLVM::LLVMDialect, mlir::NVVM::NVVMDialect, + mlir::triton::nvgpu::NVGPUDialect, mlir::triton::nvws::NVWSDialect, + mlir::triton::amdgpu::TritonAMDGPUDialect, + mlir::triton::proton::ProtonDialect, + mlir::triton::proton::gpu::ProtonGPUDialect, mlir::ROCDL::ROCDLDialect, +- mlir::triton::gluon::GluonDialect>(); ++ mlir::triton::gluon::GluonDialect, ++ mlir::triton::ascend::TritonAscendDialect, mlir::hivm::HIVMDialect, ++ mlir::scope::ScopeDialect, mlir::hacc::HACCDialect, ++ mlir::annotation::AnnotationDialect, mlir::hfusion::HFusionDialect, ++ mlir::tensor::TensorDialect, mlir::linalg::LinalgDialect, ++ mlir::memref::MemRefDialect, mlir::bufferization::BufferizationDialect, ++ mlir::func::FuncDialect>(); + } +diff --git a/bin/triton-opt.cpp b/bin/triton-opt.cpp +index 2d2570771..91ed8b79a 100644 +--- a/bin/triton-opt.cpp ++++ b/bin/triton-opt.cpp +@@ -1,4 +1,5 @@ + #include "./RegisterTritonDialects.h" ++#include "ascend/include/DynamicCVPipeline/AllocMultiCache/AddMultiBufferInnerScope.h" + + #include "mlir/Tools/mlir-opt/MlirOptMain.h" + +@@ -6,6 +7,9 @@ int main(int argc, char **argv) { + mlir::DialectRegistry registry; + registerTritonDialects(registry); + ++ // Register AddMultiBufferInnerScope pass ++ mlir::triton::registerAddMultiBufferInnerScopePasses(); ++ + return mlir::asMainReturnCode(mlir::MlirOptMain( + argc, argv, "Triton (GPU) optimizer driver\n", registry)); + } +diff --git a/include/triton/Dialect/Triton/IR/TritonAttrDefs.td b/include/triton/Dialect/Triton/IR/TritonAttrDefs.td +index 5a76a1d7b..4114df60c 100644 +--- a/include/triton/Dialect/Triton/IR/TritonAttrDefs.td ++++ b/include/triton/Dialect/Triton/IR/TritonAttrDefs.td +@@ -130,8 +130,9 @@ def TT_InputPrecisionAttr : I32EnumAttr< + I32EnumAttrCase<"TF32", 0, "tf32">, + I32EnumAttrCase<"TF32x3", 1, "tf32x3">, + I32EnumAttrCase<"IEEE", 2, "ieee">, +- I32EnumAttrCase<"BF16x3", 3, "bf16x3">, +- I32EnumAttrCase<"BF16x6", 4, "bf16x6"> ++ I32EnumAttrCase<"HF32", 3, "hf32">, ++ I32EnumAttrCase<"BF16x3", 4, "bf16x3">, ++ I32EnumAttrCase<"BF16x6", 5, "bf16x6"> + ]>{ + let cppNamespace = "::mlir::triton"; + } +diff --git a/lib/Dialect/Triton/IR/Traits.cpp b/lib/Dialect/Triton/IR/Traits.cpp +index 1b72c1376..ee5defb6e 100644 +--- a/lib/Dialect/Triton/IR/Traits.cpp ++++ b/lib/Dialect/Triton/IR/Traits.cpp +@@ -126,10 +126,11 @@ LogicalResult OpTrait::impl::verifyTensorSize(Operation *op) { + return op->emitError("Maximum allowed number of elements is ") + << maxTensorNumElements << ", but " << *op + << " has more than that"; +- if ((numElements & (numElements - 1)) != 0) +- return op->emitError("Number of elements must be power-of-two, but ") +- << *op << " doesn't follow the rule (" << numElements << ")" +- << " elements"; ++ // FIXME:patched triton community ++ // if ((numElements & (numElements - 1)) != 0) ++ // return op->emitError("Number of elements must be power-of-two, but ") ++ // << *op << " doesn't follow the rule (" << numElements << ")" ++ // << " elements"; + } + } + for (auto opType : op->getResultTypes()) { +@@ -141,10 +142,11 @@ LogicalResult OpTrait::impl::verifyTensorSize(Operation *op) { + return op->emitError("Maximum allowed number of elements is ") + << maxTensorNumElements << ", but " << *op + << " has more than that"; +- if ((numElements & (numElements - 1)) != 0) +- return op->emitError("Number of elements must be power-of-two, but ") +- << *op << " doesn't follow the rule (" << numElements << ")" +- << " elements"; ++ // FIXME:patched triton community ++ // if ((numElements & (numElements - 1)) != 0) ++ // return op->emitError("Number of elements must be power-of-two, but ") ++ // << *op << " doesn't follow the rule (" << numElements << ")" ++ // << " elements"; + } + } + return success(); +diff --git a/python/src/ir.cc b/python/src/ir.cc +index 5af645bc0..011f82d60 100644 +--- a/python/src/ir.cc ++++ b/python/src/ir.cc +@@ -332,6 +332,7 @@ void init_triton_ir(py::module &&m) { + .value("TF32", InputPrecision::TF32) + .value("TF32x3", InputPrecision::TF32x3) + .value("IEEE", InputPrecision::IEEE) ++ .value("HF32", InputPrecision::HF32) + .value("BF16x3", InputPrecision::BF16x3) + .value("BF16x6", InputPrecision::BF16x6) + .export_values(); +@@ -1790,6 +1791,18 @@ void init_triton_ir(py::module &&m) { + llvm::StringRef(message)); + self.create(condition, messageAttr); + }) ++ // This is intentionally separate from create_assert: user-authored ++ // device_assert operations must not acquire the internal provenance ++ // marker merely because they happen to use an overflow-like message. ++ .def("create_auto_overflow_assert", ++ [](TritonOpBuilder &self, Value &condition, ++ const std::string &message) -> void { ++ auto messageAttr = StringAttr::get(self.getBuilder().getContext(), ++ llvm::StringRef(message)); ++ auto assertOp = self.create(condition, messageAttr); ++ assertOp->setAttr("tt.auto_overflow_assert", ++ self.getBuilder().getUnitAttr()); ++ }) + .def("create_assume", + [](TritonOpBuilder &self, Value &condition) { + self.create(condition); +diff --git a/python/triton/_utils.py b/python/triton/_utils.py +index a3cf0ff0c..28ba11c92 100644 +--- a/python/triton/_utils.py ++++ b/python/triton/_utils.py +@@ -50,8 +50,9 @@ def validate_block_shape(shape: List[int]): + for i, d in enumerate(shape): + if not isinstance(d, int): + raise TypeError(f"Shape element {i} must have type `constexpr[int]`, got `constexpr[{type(d)}]") +- if not is_power_of_two(d): +- raise ValueError(f"Shape element {i} must be a power of 2") ++ # FIXME:patched triton community ++ # if not is_power_of_two(d): ++ # raise ValueError(f"Shape element {i} must be a power of 2") + numel *= d + + if numel > TRITON_MAX_TENSOR_NUMEL: +diff --git a/python/triton/compiler/code_generator.py b/python/triton/compiler/code_generator.py +index c572f81e3..6e06b8477 100644 +--- a/python/triton/compiler/code_generator.py ++++ b/python/triton/compiler/code_generator.py +@@ -10,8 +10,16 @@ from dataclasses import dataclass + from types import ModuleType + from typing import Any, Callable, Dict, Optional, Tuple, Type, Union, Iterable, List + ++# Import and register Ascend extension dispatch handlers ++import triton.language.extra.cann.extension as extension ++from triton.language.extra.cann.extension.dispatch import ASCEND_WITH_DISPATCH ++from triton.language.extra.cann.extension.builder import setup_unified_builder ++ ++from triton.extension.buffer.language.builder import setup_unified_builder_with_buffer_builder ++ + from .. import knobs, language +-from .._C.libtriton import ir, gluon_ir ++from .._C.libtriton import ir, gluon_ir, buffer_ir ++from .._C.libtriton.ascend import ir as ascend_ir + from ..language import constexpr, str_to_ty, tensor, tuple as tl_tuple + from ..language.core import _unwrap_if_constexpr, base_value, base_type + # ideally we wouldn't need any runtime component +@@ -20,6 +28,10 @@ from .._utils import find_paths_if, get_iterable_path, set_iterable_path, is_nam + + from .errors import (CompilationError, CompileTimeAssertionFailure, UnsupportedLanguageConstruct) + ++# Central registry for all 'with' statement handlers ++WITH_DISPATCH = {} ++WITH_DISPATCH.update(ASCEND_WITH_DISPATCH) ++ + + def check_identifier_legality(name, type): + pattern = r'^[a-zA-Z_][a-zA-Z0-9_]*$' +@@ -300,7 +312,14 @@ class CodeGenerator(ast.NodeVisitor): + self.semantic = GluonSemantic(self.builder) + else: + from triton.language.semantic import TritonSemantic +- self.builder = ir.builder(context) ++ try: ++ from .._C.libtriton import distributed ++ except ImportError: ++ distributed = None ++ if distributed is not None: ++ self.builder = distributed.ir.DistributedOpBuilder(context) ++ else: ++ self.builder = ir.builder(context) + self.semantic = TritonSemantic(self.builder) + + self.name_loc_as_prefix = None +@@ -309,6 +328,17 @@ class CodeGenerator(ast.NodeVisitor): + self.begin_line = begin_line - 1 + self.builder.set_loc(file_name, begin_line, 0) + self.builder.options = options ++ ++ # Set up unified builder interface with methods from specialized builders ++ compile_mode = "simt" if getattr(options, "compile_mode", "simd") == "simt_only" else "simd" ++ self.ascend_builder = ascend_ir.ascendnpu_ir_builder(context, getattr(options, "arch", ""), ++ compile_mode=compile_mode) ++ self.ascend_builder.set_loc(file_name, begin_line, 0) ++ setup_unified_builder(self.builder, self.ascend_builder) ++ self.buffer_builder = buffer_ir.buffer_builder(context) ++ self.buffer_builder.set_loc(file_name, begin_line, 0) ++ setup_unified_builder_with_buffer_builder(self.builder, self.buffer_builder) ++ + # dict of functions provided by the backend. Below are the list of possible functions: + # Convert custom types not natively supported on HW. + # convert_custom_types(input_tensor, dtype, fp_downcast_rounding=None, _builder=None) +@@ -451,17 +481,19 @@ class CodeGenerator(ast.NodeVisitor): + self.lscope[name] = value + self.local_defs[name] = value + +- def _get_insertion_point_and_loc(self): ++ def _get_insertion_point_and_loc(self, builder=None): + # XXX: this is a hack to get the location of the insertion point. + # The insertion point's location could be invalid sometimes, + # so we need to explicitly set the location +- loc = self.builder.get_loc() +- ip = self.builder.get_insertion_point() ++ _builder = self.builder if not builder else builder ++ loc = _builder.get_loc() ++ ip = _builder.get_insertion_point() + return ip, loc + +- def _set_insertion_point_and_loc(self, ip, loc): +- self.builder.restore_insertion_point(ip) +- self.builder.set_loc(loc) ++ def _set_insertion_point_and_loc(self, ip, loc, builder=None): ++ _builder = self.builder if not builder else builder ++ _builder.restore_insertion_point(ip) ++ _builder.set_loc(loc) + + def _find_carries(self, node, liveins, ignore: set[str] = set()): + # create loop body block +@@ -980,6 +1012,26 @@ class CodeGenerator(ast.NodeVisitor): + return self.visit(node.orelse) + + def visit_With(self, node): ++ """ ++ Handle 'with' statements with dispatch pattern for Ascend extensions, ++ falling back to standard context manager protocol for general cases. ++ ++ This implementation: ++ 1. First tries dispatch mechanism for Ascend-specific context managers (e.g., scope) ++ 2. Falls back to standard Python context manager protocol for general cases ++ """ ++ # Try dispatch mechanism for Ascend-specific context managers ++ # Only attempt dispatch for single context manager with Call expression ++ if len(node.items) == 1: ++ context = node.items[0].context_expr ++ if isinstance(context, ast.Call): ++ withitemClass = self.visit(context.func) ++ handler = WITH_DISPATCH.get(withitemClass) ++ if handler: ++ # Dispatch to registered handler (e.g., handle_scope_with) ++ return handler(self, node) ++ ++ # Fall back to standard context manager protocol (community logic) + # Lower `with` statements by constructing context managers and calling their enter/exit hooks + # Instantiate each context manager with builder injection + cm_list = [] +@@ -1147,7 +1199,7 @@ class CodeGenerator(ast.NodeVisitor): + flatten = False + warp_specialize = False + disable_licm = False +- if IteratorClass is language.range: ++ if IteratorClass in [language.range, extension.parallel]: + iterator = IteratorClass(*iter_args, **iter_kwargs) + # visit iterator arguments + # note: only `range` iterator is supported now +@@ -1224,7 +1276,9 @@ class CodeGenerator(ast.NodeVisitor): + if warp_specialize: + for_op.set_attr("tt.warp_specialize", self.builder.get_unit_attr()) + if disable_licm: +- for_op.set_attr("llvm.loop_annotation", self.builder.get_disable_loop_licm_attr()) ++ for_op.set_attr("tt.disable_licm", self.builder.get_unit_attr()) ++ if (IteratorClass is extension.parallel): ++ for_op.set_attr("hivm.parallel_loop", self.builder.get_unit_attr()) + + self.scf_stack.append(node) + for_op_body = for_op.get_body(0) +@@ -1286,7 +1340,7 @@ class CodeGenerator(ast.NodeVisitor): + args = inspect.getcallargs(fn.fn, *args, **kwargs) + args = [args[name] for name in fn.arg_names] + for i, arg in enumerate(args): +- if isinstance(arg, (language.dtype, float, int, bool, JITFunction)): ++ if isinstance(arg, (language.dtype, float, int, bool, JITFunction, language.PropagateNan)): + args[i] = language.core.constexpr(arg) + args_cst = find_paths_if(args, lambda _, x: _is_constexpr(x)) + args_cst = {path: get_iterable_path(args, path) for path in args_cst} +@@ -1340,6 +1394,11 @@ class CodeGenerator(ast.NodeVisitor): + return self.call_JitFunction(fn, args, kws) + if (hasattr(fn, '__self__') and _is_triton_value(fn.__self__)) or language.core.is_builtin(fn) or isinstance( + fn, ConstexprFunction): ++ # Copy builder's location and insertion point. ++ ip, last_loc = self._get_insertion_point_and_loc() ++ # Use ascend_builder if this function is a builtin extension operation. ++ _builder = self.ascend_builder if extension.is_builtin(fn) else self.builder ++ self._set_insertion_point_and_loc(ip, last_loc, _builder) + extra_kwargs = dict() + + if isinstance(fn, ConstexprFunction): +@@ -1355,6 +1414,9 @@ class CodeGenerator(ast.NodeVisitor): + # builtin functions return plain tuples for readability + if isinstance(ret, tuple): + ret = language.tuple(ret) ++ # Sync the builder's location before return. ++ ip, last_loc = self._get_insertion_point_and_loc(_builder) ++ self._set_insertion_point_and_loc(ip, last_loc) + return ret + except Exception as e: + if knobs.compilation.front_end_debugging: +@@ -1594,6 +1656,7 @@ class CodeGenerator(ast.NodeVisitor): + ttgl.static_print: static_executor(print), + int: static_executor(int), + len: static_executor(len), ++ extension.int64: static_executor(extension.int64), + } + + +diff --git a/python/triton/compiler/compiler.py b/python/triton/compiler/compiler.py +index d4872a463..51347e144 100644 +--- a/python/triton/compiler/compiler.py ++++ b/python/triton/compiler/compiler.py +@@ -10,6 +10,7 @@ from ..runtime.autotuner import OutOfResources + from ..runtime.cache import get_cache_manager, get_dump_manager, get_override_manager, get_cache_key + from ..runtime.driver import driver + from ..tools.disasm import get_sass ++from .errors import MLIRCompilationError + from pathlib import Path + import re + import functools +@@ -321,7 +322,29 @@ def compile(src, target=None, options=None, _env_vars=None): + if compilation_listener: + timer.finished_ir_initialization() + for ext, compile_ir in list(stages.items())[first_stage:]: +- next_module = compile_ir(module, metadata) ++ try: ++ next_module = compile_ir(module, metadata) ++ except Exception as e: ++ if (ext == "ttadapter"): ++ stage_name = "ConvertTritonIRToLinalgIR" ++ elif (ext == "npubin"): ++ stage_name = "ConvertLinalgIRToBinary" ++ elif (ext == "bcmlir"): ++ stage_name = "BytecodeToLinalgIRByBishengirOpt" ++ elif (ext == "mlirbc"): ++ stage_name = "LinalgIRToBytecodeByTritonMLIROpt" ++ else: ++ stage_name = "MLIRCompile" ++ if hasattr(e, 'stderr') and e.stderr: ++ error_detail = e.stderr.decode('utf-8') if isinstance(e.stderr, bytes) else e.stderr ++ else: ++ error_detail = str(e) ++ from ..runtime.cache import FileCacheManager ++ if isinstance(fn_cache_manager, FileCacheManager): ++ error_detail += f"\n\n[INFO]: The compiled kernel cache is in {fn_cache_manager.cache_dir}\n\n" ++ else: ++ error_detail += f"\n\n[INFO]: The compiled kernel cache is {file_name}.{ext}\n\n" ++ raise MLIRCompilationError(stage_name, error_detail) from e + ir_filename = f"{file_name}.{ext}" + if fn_override_manager is None: + # Users can override kernels at scale by setting `ir_override` in autotune config +@@ -420,8 +443,10 @@ class CompiledKernel: + # stores the text of each level of IR that was generated during compilation + asm_files = [Path(p) for c, p in metadata_group.items() if not c.endswith(".json")] + binary_ext = backend.binary_ext ++ binary_extensions = getattr(backend, 'binary_extensions', {binary_ext}) + self.asm = AsmDict({ +- file.suffix[1:]: file.read_bytes() if file.suffix[1:] == binary_ext else file.read_text() ++ file.suffix[1:]: ++ file.read_bytes() if file.suffix[1:] in binary_extensions else file.read_text() + for file in asm_files + }) + self.metadata_group = metadata_group +@@ -463,7 +488,7 @@ class CompiledKernel: + knobs.runtime.kernel_load_start_hook(self.module, self.function, self.name, self.metadata_group, self.hash) + # TODO: n_regs, n_spills should be metadata generated when calling `ptxas` + self.module, self.function, self.n_regs, self.n_spills, self.n_max_threads = driver.active.utils.load_binary( +- self.name, self.kernel, self.metadata.shared, device) ++ self.metadata.kernel_name, self.kernel, self.metadata.shared, device, self.metadata.mix_mode) + warp_size = driver.active.get_current_target().warp_size + if self.metadata.num_warps * warp_size > self.n_max_threads: + raise_(OutOfResources(self.metadata.num_warps * warp_size, self.n_max_threads, "threads")) +diff --git a/python/triton/compiler/errors.py b/python/triton/compiler/errors.py +index 39e6c4dfb..c72ca65c2 100644 +--- a/python/triton/compiler/errors.py ++++ b/python/triton/compiler/errors.py +@@ -49,3 +49,24 @@ class CompileTimeAssertionFailure(CompilationError): + + class UnsupportedLanguageConstruct(CompilationError): + pass ++ ++ ++class MLIRCompilationError(TritonError): ++ ++ def __init__(self, stage_name: Optional[str], message: Optional[str] = None): ++ self.stage_name = stage_name ++ self.message = f"\n" \ ++ f"{self.format_line_delim('[ERROR][Triton][BEG]')}" \ ++ f"[{self.stage_name}] encounters error:\n" \ ++ f"{self.filter_message(message)}" \ ++ f"{self.format_line_delim('[ERROR][Triton][END]')}" ++ ++ def __str__(self): ++ return self.message ++ ++ def filter_message(self, message): ++ # Content starting from "Stack dump without symbol names" means nothing to the users ++ return message.split("Stack dump without symbol names")[0] ++ ++ def format_line_delim(self, keyword): ++ return f"///------------------{keyword}------------------\n" +diff --git a/python/triton/language/math.py b/python/triton/language/math.py +index 582cd876cb..13ff1cef09 100644 +--- a/python/triton/language/math.py ++++ b/python/triton/language/math.py +@@ -21,8 +21,12 @@ def _check_dtype(dtypes: List[str]) -> T: + # concatenate args and kwargs + all_args = list(args) + list(kwargs.values()) + for arg in [a for a in all_args if isinstance(a, core.tensor)]: +- if arg.type.scalar.name not in dtypes: +- raise ValueError(f"Expected dtype {dtypes} but got {arg.type.scalar.name}") ++ arg_type = arg.type.scalar.name ++ if hasattr(arg, 'was_bool_to_int8') and arg.was_bool_to_int8: ++ # In Triton, int1 maps to the boolean type ++ arg_type = 'int1' ++ if arg_type not in dtypes: ++ raise ValueError(f"Expected dtype {dtypes} but got {arg_type}") + return fn(*args, **kwargs) + + return check +@@ -92,7 +96,7 @@ def umulhi(x, y, _semantic=None): + + + @core.builtin +-@_check_dtype(dtypes=["fp32", "fp64"]) ++@_check_dtype(dtypes=["fp16", "fp32"]) + @_add_math_1arg_docstr("exponential") + @core._tensor_member_fn + def exp(x, _semantic=None): +diff --git a/python/triton/language/semantic.py b/python/triton/language/semantic.py +index 52192cf3e..241ba3378 100644 +--- a/python/triton/language/semantic.py ++++ b/python/triton/language/semantic.py +@@ -219,7 +219,7 @@ class TritonSemantic(Generic[TensorTy]): + min_value = self.scalar_constant(min_value, tl.int64) + cond = self.and_(self.less_equal(ret, max_value), self.greater_equal(ret, min_value)) + msg = f"int{lhs_sca_ty.int_bitwidth} overflow detected for operation {binary_op.__name__}" +- self.device_assert(cond, msg, None) ++ self.device_assert(cond, msg, None, auto_overflow=True) + + def add(self, input: TensorTy | numbers.Number, other: TensorTy | numbers.Number, + sanitize_overflow: bool) -> TensorTy: +@@ -344,6 +344,9 @@ class TritonSemantic(Generic[TensorTy]): + raise TypeError("Cannot mod " + scalar_ty.__repr__() + " by " + other_scalar_ty.__repr__() + " " + "because they have different signedness;" + "this is unlikely to result in a useful answer. Cast them to the same signedness.") ++ if hasattr(input, 'was_bool_to_int8'): ++ false_val = self.builder.get_int1(False) ++ return self.tensor(false_val, tl.int1) + if scalar_ty.is_int_signed(): + return self.tensor(self.builder.create_srem(input.handle, other.handle), input.type) + else: +@@ -429,22 +432,61 @@ class TritonSemantic(Generic[TensorTy]): + return self.tensor(self.builder.create_xor(input.handle, other.handle), input.type) + + def logical_and(self, input: TensorTy, other: TensorTy) -> TensorTy: ++ dst_sca_ty = tl.dtype("int1") ++ dst_bits = dst_sca_ty.primitive_bitwidth ++ if hasattr(input, 'was_bool_to_int8'): ++ assert input.type.scalar.is_int8(), "input wat bool to int8. However, input.type is not int8." ++ input = self.cast(input, tl.int1) + if not input.type.is_int1(): +- input = self.bitcast(input, tl.int1) ++ src_sca_ty = input.type.scalar ++ src_bits = src_sca_ty.primitive_bitwidth ++ if src_bits == dst_bits or src_sca_ty.is_ptr() or dst_sca_ty.is_ptr(): ++ input = self.bitcast(input, tl.int1) ++ else: ++ input = self.not_equal(input, 0) ++ if hasattr(other, 'was_bool_to_int8'): ++ assert other.type.scalar.is_int8(), "Other input wat bool to int8. However, other input.type is not int8." ++ other = self.cast(other, tl.int1) + if not other.type.is_int1(): +- other = self.bitcast(other, tl.int1) ++ src_sca_ty = other.type.scalar ++ src_bits = src_sca_ty.primitive_bitwidth ++ if src_bits == dst_bits or src_sca_ty.is_ptr() or dst_sca_ty.is_ptr(): ++ other = self.bitcast(other, tl.int1) ++ else: ++ other = self.not_equal(other, 0) + return self.and_(input, other) + + def logical_or(self, input: TensorTy, other: TensorTy) -> TensorTy: ++ dst_sca_ty = tl.dtype("int1") ++ dst_bits = dst_sca_ty.primitive_bitwidth ++ if hasattr(input, 'was_bool_to_int8'): ++ assert input.type.scalar.is_int8(), "input wat bool to int8. However, input.type is not int8." ++ input = self.cast(input, tl.int1) + if not input.type.is_int1(): +- input = self.bitcast(input, tl.int1) ++ src_sca_ty = input.type.scalar ++ src_bits = src_sca_ty.primitive_bitwidth ++ if src_bits == dst_bits or src_sca_ty.is_ptr() or dst_sca_ty.is_ptr(): ++ input = self.bitcast(input, tl.int1) ++ else: ++ input = self.not_equal(input, 0) ++ if hasattr(other, 'was_bool_to_int8'): ++ assert other.type.scalar.is_int8(), "Other wat bool to int8. However, other.type is not int8." ++ other = self.cast(other, tl.int1) + if not other.type.is_int1(): +- other = self.bitcast(other, tl.int1) ++ src_sca_ty = other.type.scalar ++ src_bits = src_sca_ty.primitive_bitwidth ++ if src_bits == dst_bits or src_sca_ty.is_ptr() or dst_sca_ty.is_ptr(): ++ other = self.bitcast(other, tl.int1) ++ else: ++ other = self.not_equal(other, 0) + return self.or_(input, other) + + def not_(self, input: TensorTy): +- if not input.type.is_int1(): +- input = self.bitcast(input, tl.int1) ++ if hasattr(input, 'was_bool_to_int8'): ++ assert input.type.scalar.is_int8(), "input wat bool to int8. However, input.type is not int8." ++ input = self.cast(input, tl.int1) ++ if input.type.scalar.is_floating(): ++ raise TypeError(f"unexpected type {input.type.scalar}") + return self.invert(input) + + def lshr(self, input: TensorTy, other: TensorTy) -> TensorTy: +@@ -468,14 +510,22 @@ class TritonSemantic(Generic[TensorTy]): + + def minus(self, input: TensorTy) -> TensorTy: + input_sca_ty = input.type.scalar ++ if hasattr(input, 'was_bool_to_int8'): ++ if input.type.scalar.is_int8(): ++ raise TypeError("unexpected type bool") + if input_sca_ty.is_ptr(): + raise ValueError("wrong type argument to unary minus (" + input_sca_ty.__repr__() + ")") + _0 = self.tensor(self.builder.get_null_value(input_sca_ty.to_ir(self.builder)), input_sca_ty) + return self.sub(_0, input, True) + + def invert(self, input: TensorTy) -> TensorTy: ++ if hasattr(input, 'was_bool_to_int8'): ++ assert input.type.scalar.is_int8(), "input wat bool to int8. However, input.type is not int8." ++ input = self.cast(input, tl.int1) + input_sca_ty = input.type.scalar +- if input_sca_ty.is_ptr() or input_sca_ty.is_floating(): ++ if input_sca_ty.is_floating(): ++ raise TypeError(f"unexpected type {input_sca_ty}") ++ if input_sca_ty.is_ptr(): + raise ValueError("wrong type argument to unary invert (" + input_sca_ty.__repr__() + ")") + _1 = self.tensor(self.builder.get_all_ones_value(input_sca_ty.to_ir(self.builder)), input_sca_ty) + return self.xor_(input, _1) +@@ -579,8 +629,11 @@ class TritonSemantic(Generic[TensorTy]): + if end <= start: + raise ValueError("arange's end argument must be greater than the start argument") + range = end - start +- if (range & (range - 1)) != 0: +- raise ValueError("arange's range must be a power of 2") ++ # Check if compile_mode is simt, then range must be a power of 2 ++ if self.builder.is_simt_mode(): ++ # Check if range is a power of 2 ++ if (range & (range - 1)) != 0: ++ raise ValueError("arange's range must be a power of 2") + shape = [range] + if ret_ty is None: + ret_ty = tl.block_type(tl.int32, shape) +@@ -1009,6 +1062,8 @@ class TritonSemantic(Generic[TensorTy]): + # Check `boundary_check` argument + boundary_check = self._canonicalize_boundary_check(boundary_check, dst_ty.get_block_shapes()) + ++ if boundary_check and padding is None: ++ padding = ir.PADDING_OPTION.PAD_ZERO + # Build IR + return self.tensor( + self.builder.create_tensor_pointer_load(ptr.handle, boundary_check, padding, cache, eviction, is_volatile), +@@ -1026,7 +1081,12 @@ class TritonSemantic(Generic[TensorTy]): + raise ValueError("`padding_option` or `boundary_check` argument is not supported for loading a tensor of" + "pointers or loading a scalar. Because the compiler does not know the boundary; please " + "use block pointers (defined by `make_block_ptr`) instead") +- ++ if mask is not None and other is None: ++ # Get element type to determine default padding value ++ elt_ty = ptr.type.scalar.element_ty ++ # Use 0.0 for floating point types, 0 for integer types ++ default_value = 0.0 if elt_ty.is_floating() else 0 ++ other = self.to_tensor(default_value) + # For a pointer of scalar, check the type of `mask` and `other` + if not ptr.type.is_block(): + if mask and mask.type.is_block(): +@@ -1065,13 +1125,19 @@ class TritonSemantic(Generic[TensorTy]): + + # Build IR + if mask is None: +- ret = self.tensor(self.builder.create_load(ptr.handle, cache, eviction, is_volatile), dst_ty) ++ load_handle = self.builder.create_load(ptr.handle, cache, eviction, is_volatile) + else: +- ret = self.tensor( +- self.builder.create_masked_load(ptr.handle, mask.handle, other.handle if other else None, cache, +- eviction, is_volatile), dst_ty) ++ load_handle = self.builder.create_masked_load(ptr.handle, mask.handle, other.handle if other else None, ++ cache, eviction, is_volatile) ++ ++ if is_bool: ++ load_handle.set_attr("was_bool_to_int8", self.builder.get_bool_attr(True)) ++ ++ ret = self.tensor(load_handle, dst_ty) ++ # Do not cast back to int1 when is_bool=true. We directly use the int8 tensor given by tl.load + if is_bool: +- ret = self.cast(ret, tl.int1) ++ ret.was_bool_to_int8 = True ++ + return ret + + def load(self, ptr: TensorTy, mask: Optional[TensorTy], other: Optional[TensorTy], boundary_check: Tuple, +@@ -1799,12 +1865,17 @@ class TritonSemantic(Generic[TensorTy]): + is_signed = [arg.dtype.is_int_signed() for arg in args] + return self.tensor(self.builder.create_print(prefix, hex, new_args, is_signed), tl.void) + +- def device_assert(self, cond: TensorTy, msg: str, mask: Optional[TensorTy]) -> TensorTy: ++ def device_assert(self, cond: TensorTy, msg: str, mask: Optional[TensorTy], ++ auto_overflow: bool = False) -> TensorTy: + if not self.builder.options.debug: + return + if mask is not None: + cond = self.or_(cond, self.not_(mask)) +- return self.tensor(self.builder.create_assert(cond.handle, msg), tl.void) ++ if auto_overflow: ++ self.builder.create_auto_overflow_assert(cond.handle, msg) ++ else: ++ self.builder.create_assert(cond.handle, msg) ++ return self.tensor(None, tl.void) + + def assume(self, cond) -> TensorTy: + return self.tensor(self.builder.create_assume(cond.handle), tl.void) +diff --git a/python/triton/language/standard.py b/python/triton/language/standard.py +index b1dd327bb..c49330d40 100644 +--- a/python/triton/language/standard.py ++++ b/python/triton/language/standard.py +@@ -170,11 +170,17 @@ def _elementwise_max(a, b): + return core.maximum(a, b) + + ++@jit ++def _elementwise_max_propagate_nan(a, b): ++ return core.maximum(a, b, propagate_nan=core.PropagateNan.ALL) ++ ++ + @core._tensor_member_fn + @jit + @core._add_reduction_docstr("maximum", return_indices_arg="return_indices", + tie_break_arg="return_indices_tie_break_left") +-def max(input, axis=None, return_indices=False, return_indices_tie_break_left=True, keep_dims=False): ++def max(input, axis=None, return_indices=False, return_indices_tie_break_left=True, keep_dims=False, ++ propagate_nan: core.constexpr = core.PropagateNan.NONE): + input = core._promote_bfloat16_to_float32(input) + if return_indices: + if return_indices_tie_break_left: +@@ -188,7 +194,10 @@ def max(input, axis=None, return_indices=False, return_indices_tie_break_left=Tr + else: + assert input.dtype.is_int(), "Expecting input to be integer type" + input = input.to(core.int32) +- return core.reduce(input, axis, _elementwise_max, keep_dims=keep_dims) ++ if propagate_nan == core.PropagateNan.NONE: ++ return core.reduce(input, axis, _elementwise_max, keep_dims=keep_dims) ++ else: ++ return core.reduce(input, axis, _elementwise_max_propagate_nan, keep_dims=keep_dims) + + + @core._tensor_member_fn +diff --git a/python/triton/runtime/interpreter.py b/python/triton/runtime/interpreter.py +index cd871cb2e..82eb6960d 100644 +--- a/python/triton/runtime/interpreter.py ++++ b/python/triton/runtime/interpreter.py +@@ -22,6 +22,25 @@ from .._C.libtriton import ir as _ir + + T = TypeVar("T") + ++# Import Ascend-specific interpreter builder (with deferred import to avoid circular dependency) ++_has_ascend_support = False ++AscendInterpreterBuilder = None ++ ++ ++def _try_import_ascend(): ++ global _has_ascend_support, AscendInterpreterBuilder ++ try: ++ from triton.backends.ascend import ascend_interpreter ++ AscendInterpreterBuilder = ascend_interpreter.AscendInterpreterBuilder ++ _has_ascend_support = True ++ except ImportError: ++ _has_ascend_support = False ++ AscendInterpreterBuilder = None ++ except Exception: ++ # Catch other exceptions (like circular import) and log them ++ _has_ascend_support = False ++ AscendInterpreterBuilder = None ++ + + @dataclass + class TensorHandle: +@@ -134,7 +153,7 @@ class InterpreterOptions: + supported_fp8_dtypes: Tuple[str] = ("fp8e5", "fp8e5b16", "fp8e4nv", "fp8e4b8", "fp8e4b15") + deprecated_fp8_dot_operand_dtypes: Tuple[str] = () + default_dot_input_precision: str = "tf32" +- allowed_dot_input_precisions: Tuple[str] = ("tf32", "tf32x3", "ieee") ++ allowed_dot_input_precisions: Tuple[str] = ("tf32", "tf32x3", "ieee", "hf32") + max_num_imprecise_acc_default: int = 0 + backend_name: str = "interpreter" + +@@ -210,6 +229,8 @@ def _convert_float(input, input_dtype, output_dtype, rounding_mode): + bias_input = input_dtype.exponent_bias + bias_output = output_dtype.exponent_bias + exponent = ((input_bin >> input_dtype.fp_mantissa_width) & ((1 << input_exponent_width) - 1)).astype(np.int32) ++ # mark NAN value ++ input_nan_index = (exponent == (1 << input_exponent_width) - 1) & (significand != 0) + subnormal_index = exponent == 0 + if np.any(subnormal_index): + # Credit to Phil: phil@openai.com +@@ -229,8 +250,13 @@ def _convert_float(input, input_dtype, output_dtype, rounding_mode): + significand[subnormal_index] = (significand[subnormal_index] << bit_pos[subnormal_index]) & ( + (1 << input_dtype.fp_mantissa_width) - 1) + # Prevent overflow and underflow +- exponent_output = np.maximum(0, np.minimum((exponent - bias_input + bias_output), (1 << output_exponent_width) - 1)) ++ exponent_unclamped = exponent - bias_input + bias_output ++ output_max_exponent = (1 << output_exponent_width) - 1 ++ exponent_output = np.maximum(0, np.minimum(exponent_unclamped, output_max_exponent)) + exponent_output = exponent_output.astype(output_unint_dtype) ++ # mark overflow index ++ overflow_index = exponent_unclamped > output_max_exponent - 1 ++ + sign_output = sign.astype(output_unint_dtype) + if input_dtype.primitive_bitwidth > output_dtype.primitive_bitwidth: # Downcast + significand_output = (significand >> (input_dtype.fp_mantissa_width - output_dtype.fp_mantissa_width)) & ( +@@ -258,6 +284,8 @@ def _convert_float(input, input_dtype, output_dtype, rounding_mode): + shift[subnormal_index] = (1 - bias_output) - (exponent[subnormal_index] - bias_input) + significand_output[subnormal_index] = (significand_output[subnormal_index] >> shift[subnormal_index]) | ( + 1 << (output_dtype.fp_mantissa_width - shift[subnormal_index])) ++ # covert overflow value to inf ++ significand_output[overflow_index & ~input_nan_index] = 0 + output = (sign_output << (output_dtype.primitive_bitwidth - 1)) | ( + exponent_output << output_dtype.fp_mantissa_width) | significand_output + return output.reshape(input.shape) +@@ -312,6 +340,8 @@ class InterpreterBuilder: + self.options = InterpreterOptions() + self.codegen_fns = {} + self.codegen_fns["convert_custom_types"] = ExtraFunctions._convert_custom_types ++ # For interpreter mode, don't enforce GPU hardware shape constraints ++ # NumPy matmul works with any size, including small matrices + self.codegen_fns["min_dot_size"] = lambda lhsType, rhsType: (1, 1, 1) + + def set_grid_idx(self, x, y, z): +@@ -755,6 +785,9 @@ class InterpreterBuilder: + # Interpreter's device_assert function has a different format than Triton's device_assert + assert condition, f"{message}" + ++ def create_auto_overflow_assert(self, condition, message): ++ self.create_assert(condition, message) ++ + def create_assume(self, condition): + assert condition, "Assume failed" + +@@ -1190,6 +1223,11 @@ def _patch_lang(fn): + _patch_lang_tensor(lang.tensor, scope) + _patch_lang_core(lang, scope) + _patch_builtin(tl.core.tensor_descriptor_base, interpreter_builder, scope) ++ ++ # Patch Ascend extensions if using AscendInterpreterBuilder ++ if hasattr(interpreter_builder, 'patch_extensions'): ++ interpreter_builder.patch_extensions(fn, scope) ++ + return scope + + +@@ -1237,7 +1275,12 @@ def _implicit_cvt(arg): + return arg + + +-interpreter_builder = InterpreterBuilder() ++# Use AscendInterpreterBuilder if available, otherwise fall back to base InterpreterBuilder ++_try_import_ascend() ++if _has_ascend_support and AscendInterpreterBuilder is not None: ++ interpreter_builder = AscendInterpreterBuilder() ++else: ++ interpreter_builder = InterpreterBuilder() + interpreter_semantic = TritonSemantic(interpreter_builder) + + +@@ -1350,11 +1393,14 @@ class GridExecutor: + grid = grid + (1, ) * (3 - len(grid)) + interpreter_builder.set_grid_dim(*grid) + try: +- for x in range(grid[0]): +- for y in range(grid[1]): +- for z in range(grid[2]): +- interpreter_builder.set_grid_idx(x, y, z) +- self.fn(**args) ++ if hasattr(interpreter_builder, 'execute_with_sub_vec_simulation'): ++ interpreter_builder.execute_with_sub_vec_simulation(self.fn, args, grid) ++ else: ++ for x in range(grid[0]): ++ for y in range(grid[1]): ++ for z in range(grid[2]): ++ interpreter_builder.set_grid_idx(x, y, z) ++ self.fn(**args) + except Exception as e: + if triton.knobs.compilation.front_end_debugging: + raise +diff --git a/python/triton/runtime/jit.py b/python/triton/runtime/jit.py +index 0f506818f..43742c328 100644 +--- a/python/triton/runtime/jit.py ++++ b/python/triton/runtime/jit.py +@@ -398,14 +398,19 @@ def create_function_from_signature(sig, kparams, backend): + assert len(sig.parameters) == len(kparams) + # Create the function argument list and the dict entries for the return statement + specialization = [] ++ uses_alignment_specialization = False ++ alignment_specialization_name = "__triton_alignment_specialization" ++ integer_annotation_types = {"i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64"} + # signature + for name, kp in zip(sig.parameters.keys(), kparams): + if kp.is_constexpr: + specialization.append(f'("constexpr", {name})') + else: +- is_const = 'True' if kp.is_const else 'False' +- specialize = 'False' if kp.do_not_specialize else 'True' +- align = 'False' if kp.do_not_specialize_on_alignment else 'True' +- ret = f"specialize_impl(backend, {name}, {is_const}, {specialize}, {align})" ++ is_const = kp.is_const ++ specialize = not kp.do_not_specialize ++ align = not kp.do_not_specialize_on_alignment ++ effective_align = alignment_specialization_name if align and specialize else "False" ++ ret = (f"specialize_impl(backend, {name}, {is_const}, {specialize}, " ++ f"{effective_align})") + if kp.annotation_type: + if isinstance(kp.annotation_type, str): +@@ -413,7 +418,14 @@ def create_function_from_signature(sig, kparams, backend): + # we do not specialize non-constexpr floats and bools: + specialize = False + if specialize: +- specialization.append(f'("{kp.annotation_type}",) + {ret}[1:]') ++ uses_alignment_specialization = uses_alignment_specialization or align ++ if kp.annotation_type in integer_annotation_types: ++ # An integer annotation constrains the type of runtime values, but must not discard a ++ # constexpr classification already produced by the value specializer (for example, 1). ++ specialization.append(f'(lambda ret: ret if ret[0] == "constexpr" ' ++ f'else ("{kp.annotation_type}",) + ret[1:])({ret})') ++ else: ++ specialization.append(f'("{kp.annotation_type}",) + {ret}[1:]') + else: + # skip runtime specialization: + specialization.append(f'("{kp.annotation_type}", None)') +@@ -420,2 +432,3 @@ def create_function_from_signature(sig, kparams, backend): + else: ++ uses_alignment_specialization = uses_alignment_specialization or (align and specialize) + specialization.append(f"{ret}") +@@ -423,8 +436,11 @@ def create_function_from_signature(sig, kparams, backend): + # compute argument string for a given parameter + arg = lambda x: x[0] if x[1].default is inspect.Parameter.empty else f"{x[0]}=default_{x[0]}" ++ alignment_specialization_init = (f" {alignment_specialization_name} = " ++ "backend.use_alignment_specialization(options)\n" ++ if uses_alignment_specialization else "") + func_body = f""" + def dynamic_func({", ".join(list(map(arg, sig.parameters.items())) + ["**options"])}): +- params = {{{', '.join([f"'{name}': {name}" for name in sig.parameters.keys()])}}} ++{alignment_specialization_init} params = {{{', '.join([f"'{name}': {name}" for name in sig.parameters.keys()])}}} + specialization = [{','.join(specialization)}] + return params, specialization, options + """ +diff --git a/python/triton/runtime/autotuner.py b/python/triton/runtime/autotuner.py +index 0c4d710496..d98afb86f9 100644 +--- a/python/triton/runtime/autotuner.py ++++ b/python/triton/runtime/autotuner.py +@@ -126,7 +126,7 @@ class Autotuner(KernelInterface): + return self._do_bench + + def _bench(self, *args, config, **meta): +- from ..compiler.errors import CompileTimeAssertionFailure ++ from ..compiler.errors import CompileTimeAssertionFailure, MLIRCompilationError + + verbose = knobs.autotuning.print + if verbose: +@@ -162,7 +162,7 @@ class Autotuner(KernelInterface): + + try: + return self.do_bench(kernel_call, quantiles=(0.5, 0.2, 0.8)) +- except (OutOfResources, CompileTimeAssertionFailure, PTXASError) as e: ++ except (OutOfResources, CompileTimeAssertionFailure, PTXASError, MLIRCompilationError) as e: + if verbose: + print(f"Autotuning failed with {e}") + return [float("inf"), float("inf"), float("inf")] diff --git a/third_party/ascend/python/triton/extension/buffer/src/buffer_ir.cc b/third_party/ascend/python/triton/extension/buffer/src/buffer_ir.cc new file mode 100644 index 0000000000..9ff1e4df97 --- /dev/null +++ b/third_party/ascend/python/triton/extension/buffer/src/buffer_ir.cc @@ -0,0 +1,183 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +#include "ir.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Types.h" + +using namespace mlir; +namespace py = pybind11; + +constexpr unsigned kIntegerAttrBitWidth = 64; + +struct BufferOpBuilder : public TritonOpBuilder {}; + +void init_buffer_ir(py::module &&m) { + m.def("load_dialects", [](MLIRContext &context) { + DialectRegistry registry; + registry.insert(); + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + }); + + py::class_( + m, "buffer_builder", py::module_local(), py::dynamic_attr()) + .def(py::init()) + .def("get_null_attr", [](BufferOpBuilder &self) { return Attribute(); }) + .def("get_str_array_attr", + [](BufferOpBuilder &self, + const std::vector &array) -> ArrayAttr { + auto strRefVec = to_vector(llvm::map_range( + array, [](const auto &s) { return llvm::StringRef(s); })); + return self.getBuilder().getStrArrayAttr( + llvm::ArrayRef{strRefVec}); + }) + .def("alloc", + [](BufferOpBuilder &self, Type memrefType) -> Value { + return self.create( + mlir::cast(memrefType)); + }) + .def("to_buffer", + [](BufferOpBuilder &self, Value &src, + const Attribute &addressSpace) -> Value { + auto tensorType = dyn_cast(src.getType()); + if (!tensorType) { + llvm::report_fatal_error("to_buffer: src must be tensor type"); + } + auto memrefType = MemRefType::get(tensorType.getShape(), + tensorType.getElementType(), + MemRefLayoutAttrInterface{}); + // TODO: We need to add a pass before OneShotBufferize to generate + // MemorySpaceCastOp + Operation *memref = + self.create(memrefType, src); + if (addressSpace) { + memref = self.create( + MemRefType::get(memrefType.getShape(), + memrefType.getElementType(), + memrefType.getLayout(), addressSpace), + memref->getResult(0)); + } + return memref->getResult(0); + }) + .def("to_tensor", + [](BufferOpBuilder &self, Value &src, bool writable) -> Value { + const auto &memrefType = mlir::cast(src.getType()); + auto tensorType = mlir::RankedTensorType::get( + memrefType.getShape(), memrefType.getElementType()); + auto hasAddressSpace = memrefType.getMemorySpace(); + if (hasAddressSpace) { + MemRefType targetType = MemRefType::get( + memrefType.getShape(), memrefType.getElementType(), + memrefType.getLayout()); + return self.create( + tensorType, + self.create(targetType, src), + true ? mlir::UnitAttr::get(self.getContext()) : nullptr, + writable ? mlir::UnitAttr::get(self.getContext()) : nullptr); + } + return self.create( + tensorType, src, + true ? mlir::UnitAttr::get(self.getContext()) : nullptr, + writable ? mlir::UnitAttr::get(self.getContext()) : nullptr); + }) + .def("subview", + [](BufferOpBuilder &self, Value source, std::vector &offsets, + const std::vector &sizes, + const std::vector &strides) -> Value { + SmallVector mixedOffsets; + auto *context = self.getBuilder().getContext(); + auto &builder = self.getBuilder(); + + // Get source memref type for validation + auto sourceType = mlir::cast(source.getType()); + int64_t rank = sourceType.getRank(); + // Verify the number of parameters + if (offsets.size() != rank || sizes.size() != rank || + strides.size() != rank) { + throw std::runtime_error("Number of offsets, sizes, and strides " + "must match memref rank"); + } + + for (const auto &offset : offsets) { + auto indexType = builder.getIndexType(); + if (offset.getType() != indexType) { + Value offset_val = + self.create(indexType, offset); + mixedOffsets.push_back(offset_val); + } else { + mixedOffsets.push_back(offset); + } + } + + SmallVector mixedSizes; + SmallVector mixedStrides; + for (int64_t i = 0; i < rank; ++i) { + int64_t size = sizes[i]; + int64_t stride = strides[i]; + int64_t srcDim = sourceType.getDimSize(i); + + // verify sizes cannot be negative or zero + if (size <= 0) { + throw std::runtime_error("Expected sizes to be positive"); + } + + // verify strides cannot be negative or zero + if (stride <= 0) { + throw std::runtime_error("Expected strides to be positive"); + } + + // getDimSize() returns -1 (ShapedType::kDynamic) for dynamic + // dimensions + if (!ShapedType::isDynamic(srcDim)) { + // verify the subview size does not exceed the source dimension + if (size > srcDim) { + throw std::runtime_error( + "Subview size cannot exceed source dimension size"); + } + + // verify strides cannot exceed the source dimension size + if (stride > srcDim) { + throw std::runtime_error( + "Stride cannot exceed source dimension size"); + } + } + + mixedSizes.push_back(IntegerAttr::get( + IntegerType::get(context, kIntegerAttrBitWidth), size)); + mixedStrides.push_back(IntegerAttr::get( + IntegerType::get(context, kIntegerAttrBitWidth), stride)); + } + + return self.create(source, mixedOffsets, + mixedSizes, mixedStrides); + }); +} diff --git a/third_party/ascend/spec/triton/extension/buffer/__init__.py b/third_party/ascend/spec/triton/extension/buffer/__init__.py new file mode 100644 index 0000000000..3c2c15fac8 --- /dev/null +++ b/third_party/ascend/spec/triton/extension/buffer/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. diff --git a/third_party/ascend/spec/triton/extension/buffer/language/__init__.py b/third_party/ascend/spec/triton/extension/buffer/language/__init__.py new file mode 100644 index 0000000000..86af01bdb8 --- /dev/null +++ b/third_party/ascend/spec/triton/extension/buffer/language/__init__.py @@ -0,0 +1,44 @@ +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +__all__ = [ + # core + "builtin", + "is_builtin", + + # buffer + "buffer", + + # base address space + "address_space", + + # alloc + "alloc", + + # to_buffer + "to_buffer", + + # to_tensor + "to_tensor", + "subview", +] + +from .core import builtin, is_builtin, address_space, buffer, alloc, to_buffer, to_tensor, subview diff --git a/third_party/ascend/spec/triton/extension/buffer/language/builder.py b/third_party/ascend/spec/triton/extension/buffer/language/builder.py new file mode 100644 index 0000000000..2257080bbd --- /dev/null +++ b/third_party/ascend/spec/triton/extension/buffer/language/builder.py @@ -0,0 +1,73 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +buffer-specific builder utilities for code generation. +""" + +__all__ = [ + "create_builder_method_wrapper_with_buffer_builder", + "attach_builder_methods_with_buffer_builder", + "setup_unified_builder_with_buffer_builder", +] + + +def create_builder_method_wrapper_with_buffer_builder(main_builder, delegate_builder, method_name): + """ + Create a wrapper that delegates a method call to another builder while + synchronizing insertion points and locations. + """ + delegate_method = getattr(delegate_builder, method_name) + + def wrapper(*args, **kwargs): + saved_ip = main_builder.get_insertion_point() + saved_loc = main_builder.get_loc() + delegate_builder.restore_insertion_point(saved_ip) + if saved_loc: + delegate_builder.set_loc(saved_loc) + result = delegate_method(*args, **kwargs) + main_builder.restore_insertion_point(saved_ip) + if saved_loc: + main_builder.set_loc(saved_loc) + return result + + wrapper.__name__ = method_name + wrapper.__doc__ = getattr(delegate_method, '__doc__', None) + return wrapper + + +def attach_builder_methods_with_buffer_builder(main_builder, delegate_builder, method_names): + """Attach multiple methods from a delegate builder to the main builder.""" + for method_name in method_names: + wrapper = create_builder_method_wrapper_with_buffer_builder(main_builder, delegate_builder, method_name) + setattr(main_builder, method_name, wrapper) + + +def setup_unified_builder_with_buffer_builder(main_builder, buffer_builder): + """Set up a unified builder interface by attaching methods from specialized builders.""" + main_builder._buffer_builder = buffer_builder + buffer_methods = [ + 'get_null_attr', + 'get_str_array_attr', + 'alloc', + 'to_buffer', + 'to_tensor', + 'subview', + ] + attach_builder_methods_with_buffer_builder(main_builder, buffer_builder, buffer_methods) diff --git a/third_party/ascend/spec/triton/extension/buffer/language/core.py b/third_party/ascend/spec/triton/extension/buffer/language/core.py new file mode 100644 index 0000000000..d170561813 --- /dev/null +++ b/third_party/ascend/spec/triton/extension/buffer/language/core.py @@ -0,0 +1,341 @@ +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +__all__ = [ + "address_space", + "buffer_type", + "subview", + "alloc", + "buffer", + "to_buffer", + "to_tensor", +] + +import importlib +from typing import TypeVar, List +from functools import wraps + +from triton._C.libtriton import ir +import triton.language.core as tl + +T = TypeVar("T") + +TRITON_BUILTIN = "__triton_builtin__" +BUFFER_BUILTIN = "__buffer_builtin__" + + +def builtin(fn: T) -> T: + """Mark a function as a buffer language builtin.""" + assert callable(fn) + + @wraps(fn) + def wrapper(*args, **kwargs): + if "_semantic" not in kwargs or kwargs["_semantic"] is None: + raise ValueError("Did you forget to add @triton.jit ? " + "(`_semantic` argument must be provided outside of JIT functions.)") + return fn(*args, **kwargs) + + # also set triton_builtin to true so that CodeGenerator will recognize this function + setattr(wrapper, TRITON_BUILTIN, True) + setattr(wrapper, BUFFER_BUILTIN, True) + + return wrapper + + +def is_builtin(fn) -> bool: + """Is this a registered buffer language builtin function?""" + return getattr(fn, BUFFER_BUILTIN, False) + + +class address_space: + """Represents a buffer's address space. + + The :code:`address_space` of a buffer is a target-specific attribute. + """ + + def to_ir(self, builder: ir.builder) -> ir.type: + raise NotImplementedError("Abstract address_space cannot be converted to ir") + + +class buffer_type(tl.dtype): + + def __init__(self, element_ty: tl.dtype, shape: List, space: address_space = None, strides: List = None): + self.element_ty = element_ty + self.shape = shape if isinstance(shape, list) else list(shape) + self.space = space + self.strides = strides if strides is not None else [] + self.name = self._make_name() + + def _make_name(self): + res = '' + + def to_ir(self, builder: ir.builder) -> ir.type: + element_ty_ir = self.element_ty.to_ir(builder) + addr_space_attr = self.space.to_ir(builder) if self.space else builder.get_null_attr() + + # use the method with strides if strides is not empty + if self.strides: + return builder.get_buffer_ty_with_strides(self.shape, element_ty_ir, self.strides, addr_space_attr) + else: + return builder.get_buffer_ty(self.shape, element_ty_ir, addr_space_attr) + + def __str__(self): + return self.name + + def __repr__(self): + return self.__str__() + + def __eq__(self, other) -> bool: + if not isinstance(other, buffer_type): + return False + return (self.element_ty == other.element_ty and self.shape == other.shape and self.space == other.space + and self.strides == other.strides) + + def __ne__(self, other) -> bool: + return not self.__eq__(other) + + @property + def scalar(self): + return self.element_ty + + def mangle(self) -> str: + elt = self.element_ty.mangle() + shape = "_".join(map(str, self.shape)) + return f"B{elt}S{shape}S" + + def _unflatten_ir(self, handles: List[ir.value], cursor: int): + return buffer(handles[cursor], self), cursor + 1 + + +# ----------------------- +# buffer +# ----------------------- + + +class buffer(tl.base_value): + """Represents a region of memory. + + :code:`buffer` is the fundamental data structure for Triton programs using + the buffer language extension. Most functions in + :py:mod:`triton.extension.buffer.language` operate on and return buffers. + + Most of the named member functions here are duplicates of the free functions + in :code:`triton.language`. For example, :code:`triton.language.sqrt(x)` is + equivalent to :code:`x.sqrt()`. + + .. rubric:: Constructors + .. + For some reason Sphinx includes __init__ before printing the full table + of methods. Not what I want, but I can't figure out how to fix it. Give + it its own section so it looks intentional. :) + """ + + def __init__(self, handle, buffer_ty: buffer_type): + """Not called by user code.""" + super().__init__() + self.handle = handle + self.type = buffer_ty + self.dtype = buffer_ty.element_ty.scalar + self.shape = buffer_ty.shape + self.space = buffer_ty.space + self.strides = buffer_ty.strides + + def _flatten_ir(self, handles: List[ir.value]) -> None: + handles.append(self.handle) + + def __str__(self) -> str: + # ex. "<16x32xfloat32, address_space>" + res = '<' + 'x'.join(str(s) for s in self.shape) + 'x' + str(self.dtype) + if self.space: + res += ', ' + str(self.space) + return res + '>' + + @builtin + def subview(self, offsets: List[tl.constexpr], sizes: List[tl.constexpr], strides: List[tl.constexpr], + _semantic=None) -> 'buffer': + return subview(self, offsets, sizes, strides, _semantic=_semantic) + + @builtin + def to_tensor(self, writable=True, target_shape=None, _semantic=None): + """Convert this buffer to a tl.tensor""" + return to_tensor(self, writable=writable, target_shape=target_shape, _semantic=_semantic) + + +semantic = importlib.import_module(".semantic", package=__package__) + + +@builtin +def alloc(etype: tl.dtype, shape: List[tl.constexpr], _address_space: address_space = None, is_mem_unique: bool = False, + _semantic=None) -> buffer: + """ + Allocates a region of local memory with the specified shape and type. + + :param etype: the element type of the buffer. + :type etype: tl.dtype + :param shape: A list of non-negative integers representing the shape of the buffer. + :type shape: List[tl.constexpr] + :param _address_space: (Optional) backend-specific local memory address space + :type _address_space: bl.address_space + """ + return semantic.alloc(etype, shape, _address_space, is_mem_unique, _semantic.builder) + + +@builtin +def to_buffer(tensor: tl.tensor, space: address_space = None, bind_buffer: buffer = None, _semantic=None) -> buffer: + """ + Convert a tensor to a buffer. + + :param tensor: the tensor to convert. + :type tensor: tl.tensor + :param space: the address space for the buffer (optional). + :type space: address_space + """ + return semantic.to_buffer(tensor, space, bind_buffer, _semantic.builder) + + +@builtin +def to_tensor(memref: buffer, writable: bool = True, target_shape=None, _semantic=None) -> tl.tensor: + """ + Create a tl.tensor from a bl.buffer. + + :param memref: the input bl.buffer object. + :memref type: bl.buffer + :param writable: If set true, the resultant tensor is considered "writable" during bufferization. + :type writable: bool + """ + return semantic.to_tensor(memref, writable, _semantic.builder, target_shape=target_shape) + + +def check_subview(src, offsets, sizes, strides): + """ + Check data of subview methods which the data length and the offset value must be 32-byte aligned. + + The conditions for checking data are as follows: + 1. offset value must be 32-bytes aligned. + 2. all strides must be 1. + 3. the first point's offset in the second row of the last dimension must be 32-bytes aligned. + + For instance, the following example fails to satisfy the specified criteria. + %subview = memref.subview %arg0[1, 1][4, 4][2, 2] + : memref<8x8xf32, strided<[8, 1], offset: 0>> to + memref<4x4xf32, strided<[16, 2], offset: 9>> + offsets = [8, 8] | sizes = [4, 4] | strides = [2, 2] + result_offset = 9 + second_row_start_offset = 25 + The scene will be go wrong because the follow conditions are not meet. + 1) result_offset is not 32-bytes aligned. + 2) strides = [2, 2], not all strides are equal to 1. + 3) second_row_start_offset are not 32-bytes aligned. + """ + bytes_per_block = 32 + bits_per_byte = 8 + base_byte = bytes_per_block // (src.dtype.primitive_bitwidth // bits_per_byte) + result_strides = [] + result_offset = 0 + second_row_start_offset = 0 + length = len(strides) + src_strides = [1] * length + if length == 1: + if offset[0] % base_byte != 0: # noqa: F821 + raise TypeError("all strides should be 1 and the offset value should be 32-bytes aligned.") + return + for i in range(length - 2, -1, -1): + src_strides[i] = src_strides[i + 1] * src.shape[i + 1] + for i in range(0, length): + if isinstance(offsets[i], tl.tensor): + return + result_strides.append(src_strides[i] * strides[i]) + result_offset = result_offset + offsets[i] * src_strides[i] + second_row_start_offset = result_offset + src_strides[-2] * strides[-2] + is_unaligned = False + if sizes[1] > 1: + is_unaligned = second_row_start_offset % base_byte != 0 + stride_1 = all(s == 1 for s in strides) + is_unaligned = result_offset % base_byte != 0 or is_unaligned or not stride_1 + if is_unaligned: + raise TypeError("all strides should be 1 and the offset value should be 32-bytes aligned.") + + +@builtin +def subview(src: buffer, offsets: List[tl.constexpr], sizes: List[tl.constexpr], strides: List[tl.constexpr], + _semantic=None) -> buffer: + ''' + Creates a subview of the source buffer with the specified offsets, sizes, and strides. + + :param src: The source buffer to create a subview from. + :type src: buffer + :param offsets: A list of non-negative integers representing the offsets in each dimension. + :type offsets: List[tl.constexpr] + :param sizes: A list of non-negative integers representing the sizes in each dimension. + :type sizes: List[tl.constexpr] + :param strides: A list of non-negative integers representing the strides in each dimension. + :type strides: List[tl.constexpr] + :return: A new buffer representing the subview of the source buffer. + :rtype: buffer + ''' + # Validate that sizes and strides contain only constexpr values + new_sizes = [] + for i, size in enumerate(sizes): + if isinstance(size, int): + # Convert regular integers to constexpr + new_sizes.append(tl.constexpr(size)) + elif isinstance(size, tl.constexpr): + new_sizes.append(size) + else: + raise TypeError(f"sizes[{i}] must be constexpr, got {type(size).__name__}") + + new_strides = [] + for i, stride in enumerate(strides): + if isinstance(stride, int): + # Convert regular integers to constexpr + new_strides.append(tl.constexpr(stride)) + elif isinstance(stride, tl.constexpr): + new_strides.append(stride) + else: + raise TypeError(f"strides[{i}] must be constexpr, got {type(stride).__name__}") + + check_offsets = [] + new_offsets = [] + for offset in offsets: + if isinstance(offset, tl.constexpr): + # Check that constexpr offset values cannot be negative + if offset < 0: + raise ValueError(f"Offset value must be non-negative, got {offset}") + new_offsets.append(_semantic.to_tensor(offset)) + check_offsets.append(offset) + elif isinstance(offset, int): + # Convert regular integers to constexpr and then to tensor + if offset < 0: + raise ValueError(f"Offset value must be non-negative, got {offset}") + new_offsets.append(_semantic.to_tensor(tl.constexpr(offset))) + check_offsets.append(tl.constexpr(offset)) + else: + # Assume it's already a tensor + new_offsets.append(offset) + check_offsets.append(offset) + + check_subview(src, check_offsets, new_sizes, new_strides) + return semantic.subview(src, new_offsets, new_sizes, new_strides, _semantic.builder) diff --git a/third_party/ascend/spec/triton/extension/buffer/language/semantic.py b/third_party/ascend/spec/triton/extension/buffer/language/semantic.py new file mode 100644 index 0000000000..dcbabb709e --- /dev/null +++ b/third_party/ascend/spec/triton/extension/buffer/language/semantic.py @@ -0,0 +1,128 @@ +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from typing import (TypeVar, List) + +from triton._C.libtriton import ir +import triton.language.core as tl + +from . import core as bl + +T = TypeVar('T') + + +def alloc(etype: tl.dtype, shape: List[tl.constexpr], address_space: bl.address_space, is_mem_unique, + builder: ir.builder) -> bl.buffer: + shape = tl._unwrap_shape(shape) + if etype == tl.int1: + raise TypeError("Unsupported alloc int1 type") + if not isinstance(shape, (tl.tuple, list)): + raise TypeError("shape must be list/tuple") + etype = tl._unwrap_if_constexpr(etype) + address_space = tl._unwrap_if_constexpr(address_space) + element_ty_ir = etype.to_ir(builder) + addr_space_attr = (address_space.to_ir(builder) if address_space else builder.get_null_attr()) + memref_ty = builder.get_buffer_ty(shape, element_ty_ir, addr_space_attr) + handle = builder.alloc(memref_ty) + if is_mem_unique: + builder.create_annotation_mark(handle, "mem_unique", builder.get_unit_attr()) + builder.create_annotation_mark(handle, "effects", builder.get_str_array_attr(["write", "read"])) + + buffer_ty = bl.buffer_type(element_ty=etype, shape=shape, space=address_space) + return bl.buffer(handle, buffer_ty) + + +def to_buffer( + tensor: tl.tensor, + address_space: bl.address_space, + bind_buffer: bl.buffer, + builder: ir.builder, +) -> bl.buffer: + if not isinstance(tensor.shape, (tl.tuple, list)) or not tensor.shape: + raise TypeError("scalar type cannot be converted to buffer") + if isinstance(bind_buffer, bl.buffer): + builder.create_bind_buffer(tensor.handle, bind_buffer.handle) + return bind_buffer + if bind_buffer is not None: + raise ValueError("bind_buffer must be a buffer or None") + address_space = tl._unwrap_if_constexpr(address_space) + addr_space_attr = (address_space.to_ir(builder) if address_space else builder.get_null_attr()) + handle = builder.to_buffer(tensor.handle, addr_space_attr) + buffer_ty = bl.buffer_type(element_ty=tensor.dtype, shape=tensor.shape, space=address_space) + return bl.buffer(handle, buffer_ty) + + +def to_tensor(memref: bl.buffer, writable: bool, builder: ir.builder, target_shape=None) -> tl.tensor: + if not isinstance(memref, bl.buffer): + raise TypeError("memref must be bl.buffer") + + need_convert_layout = False + shape = memref.shape + if target_shape: + need_convert_layout = True + shape = tl._unwrap_shape(target_shape) + assert shape != memref.shape, "target shape is the same as source shape" + if not isinstance(shape, (tl.tuple, list)): + raise TypeError("shape must be list/tuple") + tensor_type = tl.block_type(memref.dtype, shape) + + memref_value = memref.handle + if need_convert_layout: + buffer_ty = bl.buffer_type( + element_ty=memref.dtype, + shape=shape, + space=memref.space, + ) + memref_value = builder.create_convert_layout(memref_value, buffer_ty.to_ir(builder)) + + return tl.tensor(builder.to_tensor(memref_value, writable), tensor_type) + + +def subview(src: bl.buffer, offsets: List[tl.tensor], sizes: List[tl.constexpr], strides: List[tl.constexpr], + builder: ir.builder) -> bl.buffer: + + new_offsets = [offset.handle for offset in offsets] + sizes_int = tl._unwrap_shape(sizes) + strides_int = tl._unwrap_shape(strides) + + result_handle = builder.subview(src.handle, new_offsets, sizes_int, strides_int) + + # calculate the memory layout strides of the source buffer + if src.strides: + # use the strides of the source buffer + src_memory_strides = src.strides + else: + # calculate the default row-major strides + src_memory_strides = [] + stride = 1 + for dim_size in reversed(src.shape): + if dim_size < 0: + raise ValueError("Cannot compute strides for buffer with dynamic dimensions") + src_memory_strides.insert(0, stride) + stride *= dim_size + + result_memory_strides = [] + for src_stride, subview_stride in zip(src_memory_strides, strides_int): + result_memory_strides.append(src_stride * subview_stride) + + # create buffer_type with strides + buffer_ty = bl.buffer_type(element_ty=src.dtype, shape=sizes_int, space=src.space, strides=result_memory_strides) + return bl.buffer(result_handle, buffer_ty) diff --git a/third_party/ascend/spec/triton/runtime/code_cache.py b/third_party/ascend/spec/triton/runtime/code_cache.py new file mode 100644 index 0000000000..563d46c8af --- /dev/null +++ b/third_party/ascend/spec/triton/runtime/code_cache.py @@ -0,0 +1,68 @@ +# Copyright (c) FlagOpen contributors +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# Copyright © 2024 BAAI. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Modifications: +# - 2025-06-03: +# - init version: e9c7aa71832eb2f897a49ce787e42d5377404a72 +# + +import functools +import os +import shutil +from pathlib import Path + + +@functools.lru_cache(maxsize=None) # this is the same as functools.cache in Python 3.9+ +def cache_dir_path() -> Path: + """Return the cache directory for generated files in flaggems.""" + _cache_dir = os.environ.get("FLAGGEMS_CACHE_DIR") + if _cache_dir is None: + _cache_dir = Path.home() / ".flaggems" + else: + _cache_dir = Path(_cache_dir) + return _cache_dir + + +def cache_dir() -> Path: + """Return cache directory for generated files in flaggems. Create it if it does not exist.""" + _cache_dir = cache_dir_path() + os.makedirs(_cache_dir, exist_ok=True) + return _cache_dir + + +def code_cache_dir() -> Path: + _code_cache_dir = cache_dir() / "code_cache" + os.makedirs(_code_cache_dir, exist_ok=True) + return _code_cache_dir + + +def config_cache_dir() -> Path: + _config_cache_dir = cache_dir() / "config_cache" + os.makedirs(_config_cache_dir, exist_ok=True) + return _config_cache_dir + + +def clear_cache(): + """Clear the cache directory for code cache.""" + _cache_dir = cache_dir_path() + shutil.rmtree(_cache_dir) diff --git a/third_party/ascend/spec/triton/runtime/libentry.py b/third_party/ascend/spec/triton/runtime/libentry.py new file mode 100644 index 0000000000..db0a36f679 --- /dev/null +++ b/third_party/ascend/spec/triton/runtime/libentry.py @@ -0,0 +1,386 @@ +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# Copyright © 2024 BAAI. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Modifications: +# - 2025-06-03: +# - init version: e9c7aa71832eb2f897a49ce787e42d5377404a72 +# - adapt torch_device_fn to ascend +# + +import inspect +import sqlite3 +import threading +import weakref +import ast +from collections import OrderedDict +from typing import ( + Dict, + Optional, +) + +import triton +import torch +from .code_cache import config_cache_dir + +torch_device_fn = torch.npu +DEVICE_COUNT = torch_device_fn.device_count() +version = triton.__version__.split(".") +major_version, minor_version = eval(version[0]), eval(version[1]) + + +def quote_identifier(name: str) -> str: + if not name: + raise ValueError("empty identifier") + allowed = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_") + if not (name[0].isalpha() or name[0] == "_"): + raise ValueError("identifier must start with letter or _") + if not all(ch in allowed for ch in name): + raise ValueError("identifier contains illegal char") + return '"' + name.replace('"', '""') + '"' + + +class LibTuner(triton.runtime.Autotuner): + + def __init__( + self, + fn, + arg_names, + configs, + key, + reset_to_zero, + restore_value, + pre_hook=None, + post_hook=None, + prune_configs_by: Optional[Dict] = None, + warmup=None, + rep=None, + use_cuda_graph=False, + ): + if major_version == 2 or (major_version == 3 and minor_version <= 1): + if warmup is None: + warmup = 25 + if rep is None: + rep = 100 + if major_version == 2: + super().__init__( + fn, + arg_names, + configs, + key, + reset_to_zero, + restore_value, + prune_configs_by, + warmup, + rep, + ) + self.base_fn = fn + while not inspect.isfunction(self.base_fn): + self.base_fn = self.base_fn.fn + else: + super().__init__( + fn, + arg_names, + configs, + key, + reset_to_zero, + restore_value, + pre_hook, + post_hook, + prune_configs_by, + warmup, + rep, + use_cuda_graph, + ) + self.__name__ = self.base_fn.__name__ + self.table_name = quote_identifier(self.__name__) + self.cache_path = config_cache_dir() / "TunedConfig.db" + self.preload() + weakref.finalize(self, self.store) + + def preload(self): + connect = sqlite3.connect(self.cache_path) + c = connect.cursor() + c.execute(f"CREATE TABLE IF NOT EXISTS {self.table_name} (key TEXT PRIMARY KEY, config TEXT)") + cursor = c.execute(f"SELECT key, config from {self.table_name}") + + for row in cursor: + key_str, config_str = row + key = [ast.literal_eval(k) for k in key_str[1:-1].split(", ")] + + cfg_ls = [item.split(": ") for item in config_str.split(", ")] + config = triton.Config({}) + attrs = -5 if major_version == 2 else -4 + for k, v in cfg_ls[:attrs]: + config.kwargs[k] = ast.literal_eval(v) + config.num_warps = ast.literal_eval(cfg_ls[attrs][1]) + config.num_ctas = ast.literal_eval(cfg_ls[attrs + 1][1]) + config.num_stages = ast.literal_eval(cfg_ls[attrs + 2][1]) + if major_version == 2: + config.enable_warp_specialization = ast.literal_eval(cfg_ls[attrs + 3][1]) + config.enable_persistent = ast.literal_eval(cfg_ls[attrs + 4][1]) + else: + config.maxnreg = ast.literal_eval(cfg_ls[attrs + 3][1]) + + self.cache[tuple(key)] = config + + connect.close() + self.volumn = len(self.cache) + + def store(self): + if len(self.cache) == self.volumn: + return + connect = sqlite3.connect(self.cache_path) + c = connect.cursor() + c.execute(f"CREATE TABLE IF NOT EXISTS {self.table_name} (key TEXT PRIMARY KEY, config TEXT)") + for key, config in self.cache.items(): + c.execute( + f"INSERT OR IGNORE INTO {self.table_name} (key, config) VALUES (?, ?)", + (str(key), config.__str__()), + ) + + connect.commit() + connect.close() + + +def libtuner( + configs, + key, + prune_configs_by=None, + reset_to_zero=None, + restore_value=None, + pre_hook=None, + post_hook=None, + warmup=25, + rep=100, + use_cuda_graph=False, +): + """ + Decorator for triton library autotuner. + """ + + def decorator(fn): + return LibTuner( + fn, + fn.arg_names, + configs, + key, + reset_to_zero, + restore_value, + pre_hook=pre_hook, + post_hook=post_hook, + prune_configs_by=prune_configs_by, + warmup=warmup, + rep=rep, + use_cuda_graph=use_cuda_graph, + ) + + return decorator + + +class LibEntry(triton.KernelInterface): + + def __init__( + self, + fn, + ): + self.fn = fn + self.arg_names = fn.arg_names + self.divisibility = 16 + self.kernel_cache = tuple(dict() for _ in range(DEVICE_COUNT)) + + while not isinstance(fn, triton.runtime.JITFunction): + fn = fn.fn + self.jit_function: triton.runtime.JITFunction = fn + self.specialize_indices = [ + p.num for p in self.jit_function.params if not p.is_constexpr and not p.do_not_specialize + ] + self.do_not_specialize_indices = [ + p.num for p in self.jit_function.params if not p.is_constexpr and p.do_not_specialize + ] + self.lock = threading.Lock() + self.signature = fn.signature + + def key(self, spec_args, dns_args, const_args): + + def spec_arg(arg): + if hasattr(arg, "data_ptr"): + return (arg.dtype, arg.data_ptr() % self.divisibility == 0) + return (type(arg), arg) + + def dns_arg(arg): + if hasattr(arg, "data_ptr"): + return arg.dtype + if not isinstance(arg, int): + return type(arg) + if -(2**31) <= arg and arg <= 2**31 - 1: + return "i32" + if 2**63 <= arg and arg <= 2**64 - 1: + return "u64" + return "i64" + + spec_key = [spec_arg(arg) for arg in spec_args] + dns_key = [dns_arg(arg) for arg in dns_args] + # const args passed by position + return tuple(spec_key + dns_key + const_args) + + def run(self, *args, **kwargs): + grid = kwargs["grid"] + + # collect all the arguments + spec_args = [] # specialize arguments + dns_args = [] # do not specialize arguments + const_args = [] # constexpr arguments + k_args = OrderedDict() + param_names = list(self.signature.parameters.keys()) + for i, arg in enumerate(args): + hashable_arg = arg + if (hasattr(arg, "__class__") and arg.__class__.__name__ == "TensorDescriptor"): + # Create a hashable representation of TensorDescriptor + hashable_arg = ( + "TensorDescriptor", + tuple(arg.shape) if hasattr(arg, "shape") else None, + tuple(arg.strides) if hasattr(arg, "strides") else None, + tuple(arg.block_shape) if hasattr(arg, "block_shape") else None, + arg.padding if hasattr(arg, "padding") else None, + # Add other relevant attributes + ) + if i in self.specialize_indices: + k_args[param_names[i]] = arg + spec_args.append(hashable_arg) + elif i in self.do_not_specialize_indices: + k_args[param_names[i]] = arg + dns_args.append(hashable_arg) + else: + if major_version == 3 and 3 <= minor_version <= 6: + k_args[param_names[i]] = arg + const_args.append(hashable_arg) + for p in self.jit_function.params[len(args):]: + if p.name in kwargs: + val = kwargs[p.name] + elif p.default is inspect._empty: + continue + else: + val = p.default + + if p.is_constexpr: + const_args.append(val) + if major_version == 3 and 3 <= minor_version <= 6: + k_args[p.name] = val + elif p.do_not_specialize: + dns_args.append(val) + k_args[p.name] = val + else: + spec_args.append(val) + k_args[p.name] = val + + entry_key = self.key(spec_args, dns_args, const_args) + device = torch_device_fn.current_device() + cache = self.kernel_cache[device] + while entry_key not in cache: + # NOTE: we serialize the first run of a jit function regardless of which device to run on + # because Triton runtime is currently not threadsafe. + with self.lock: + if entry_key in cache: + break + kernel = self.fn.run(*args, **kwargs) + fn = self.fn + # collect constexpr arguments for grid computation + constexprs = {} + tune_constexprs = {} + heur_constexprs = {} + while not isinstance(fn, triton.runtime.JITFunction): + if isinstance(fn, triton.runtime.Autotuner): + config = fn.best_config + constexprs["num_warps"] = config.num_warps + constexprs["num_stages"] = config.num_stages + constexprs["num_ctas"] = config.num_ctas + constexprs = {**constexprs, **config.kwargs} + tune_constexprs = {**tune_constexprs, **config.kwargs} + elif isinstance(fn, triton.runtime.Heuristics): + for v, heur in fn.values.items(): + heur_constexprs[v] = heur({ + **dict(zip(fn.arg_names, args)), + **kwargs, + **constexprs, + }) + constexprs[v] = heur_constexprs[v] + else: + raise RuntimeError("Invalid Runtime Function") + fn = fn.fn + for p in self.jit_function.params: + if (p.is_constexpr and p.name not in constexprs and (p.default is not inspect._empty)): + constexprs[p.name] = p.default + cache[entry_key] = ( + kernel, + constexprs, + tune_constexprs, + heur_constexprs, + ) + return kernel, constexprs + + kernel, constexprs, tune_constexprs, heur_constexprs = cache[entry_key] + + if callable(grid): + # collect all arguments to the grid fn,ie: + # 1. args, + # 2. kwargs, + # 3. all all other captured arguments in CompiledKernel from Autotunner & Heuristics + # when kwargs & captured args conflict, captured args have higher priority + meta = {**dict(zip(self.arg_names, args)), **kwargs, **constexprs} + grid = grid(meta) + grid = grid + (1, 1) + + if major_version == 3 and 3 <= minor_version <= 6: + all_args = [] + missing_keys = [] + for key in list(self.signature.parameters.keys()): + if key in k_args: + all_args.append(k_args[key]) + elif key in tune_constexprs: + all_args.append(tune_constexprs[key]) + elif key in heur_constexprs: + all_args.append(heur_constexprs[key]) + elif key in constexprs: + all_args.append(constexprs[key]) + else: + missing_keys.append(key) + if len(missing_keys): + raise RuntimeError( + f"[libentry]: probably a bug, the following kernel params where not captured: {missing_keys}") + kernel[grid[0:3]](*all_args) + else: + kernel[grid[0:3]](*k_args.values()) + return kernel, constexprs + + +def libentry(): + """ + Decorator for triton library entries. + """ + + def decorator(fn): + from triton.runtime.interpreter import InterpretedFunction + if isinstance(fn, InterpretedFunction): + return fn + return LibEntry(fn) + + return decorator diff --git a/third_party/ascend/spec/triton/tools/get_ascend_devices.py b/third_party/ascend/spec/triton/tools/get_ascend_devices.py new file mode 100644 index 0000000000..f6ba1c9c01 --- /dev/null +++ b/third_party/ascend/spec/triton/tools/get_ascend_devices.py @@ -0,0 +1,49 @@ +import os +import glob +import logging +import subprocess + +logger = logging.getLogger(__name__) + + +def get_ascend_devices(): + devices = [] + pci_path = '/sys/bus/pci/devices/*' + + for dev in glob.glob(pci_path): + try: + vendor_path = os.path.join(dev, 'vendor') + device_path = os.path.join(dev, 'device') + + if os.path.exists(vendor_path): + with open(vendor_path, 'r') as f: + vendor = f.read().strip() + + if vendor == "0x19e5" and os.path.exists(device_path): + with open(device_path, 'r') as f: + device = f.read().strip() + devices.append(device) + except (IOError, OSError) as e: + logger.warning(f"can not fetch device {dev}: {e}") + continue + + return devices + + +def check_npu_smi_device(): + try: + result = subprocess.run(["npu-smi", "info"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + shell=False, timeout=100) + if result.returncode == 0: + output = result.stdout.lower() + return "ascend910_95" in output or "ascend950" in output or "910_958b" in output + return False + except Exception: + logger.warning("can not use command: npu-smi info") + return False + + +ascend_devices = get_ascend_devices() +pci_condition = any("0xd806" in dev for dev in ascend_devices) +npu_smi_condition = check_npu_smi_device() +is_compile_on_910_95 = pci_condition or npu_smi_condition diff --git a/third_party/ascend/spec_cpp/include/triton/Dialect/Triton/IR/TritonAttrDefs.td b/third_party/ascend/spec_cpp/include/triton/Dialect/Triton/IR/TritonAttrDefs.td new file mode 100644 index 0000000000..51a6008f20 --- /dev/null +++ b/third_party/ascend/spec_cpp/include/triton/Dialect/Triton/IR/TritonAttrDefs.td @@ -0,0 +1,180 @@ +/* + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * Copyright 2025- FlagOS Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef TRITON_ATTR_DEFS +#define TRITON_ATTR_DEFS + +include "mlir/IR/EnumAttr.td" + +// Attributes for LoadOp and StoreOp +def TT_CacheModifierAttr : I32EnumAttr< + "CacheModifier", "", + [ + I32EnumAttrCase<"NONE", 1, "none">, + I32EnumAttrCase<"CA", 2, "ca">, + I32EnumAttrCase<"CG", 3, "cg">, + I32EnumAttrCase<"WB", 4, "wb">, + I32EnumAttrCase<"CS", 5, "cs">, + I32EnumAttrCase<"WT", 6, "wt">, + I32EnumAttrCase<"CV", 7, "cv">, + ]> { + let cppNamespace = "::mlir::triton"; +} + +def TT_MemSemanticAttr : I32EnumAttr< + "MemSemantic", "", + [ + I32EnumAttrCase<"RELAXED", 1, "relaxed">, + I32EnumAttrCase<"ACQUIRE", 2, "acquire">, + I32EnumAttrCase<"RELEASE", 3, "release">, + I32EnumAttrCase<"ACQUIRE_RELEASE", 4, "acq_rel">, + ]> { + let cppNamespace = "::mlir::triton"; +} + +def TT_EvictionPolicyAttr : I32EnumAttr< + "EvictionPolicy", "", + [ + I32EnumAttrCase<"NORMAL", 1, "evict_normal">, + I32EnumAttrCase<"EVICT_FIRST", 2, "evict_first">, + I32EnumAttrCase<"EVICT_LAST", 3, "evict_last"> + ]> { + let cppNamespace = "::mlir::triton"; +} + +def TT_PaddingOptionAttr : I32EnumAttr< + "PaddingOption", "", + [ + I32EnumAttrCase<"PAD_ZERO", 1, "zero">, + // We can not set the string value to "NAN" because it is a keyword in C++ + I32EnumAttrCase<"PAD_NAN", 2, "nan"> + ]> { + let cppNamespace = "::mlir::triton"; +} + +// atomic +def TT_AtomicRMWAttr : I32EnumAttr< + "RMWOp", "", + [ + I32EnumAttrCase<"AND", 1, "and">, + I32EnumAttrCase<"OR", 2, "or">, + I32EnumAttrCase<"XOR", 3, "xor">, + I32EnumAttrCase<"ADD", 4, "add">, + I32EnumAttrCase<"FADD", 5, "fadd">, + I32EnumAttrCase<"MAX", 6, "max">, + I32EnumAttrCase<"MIN", 7, "min">, + I32EnumAttrCase<"UMAX", 8, "umax">, + I32EnumAttrCase<"UMIN", 9, "umin">, + I32EnumAttrCase<"XCHG", 10, "exch"> + ]> { + let cppNamespace = "::mlir::triton"; +} + +def TT_DescriptorReduceKindAttr : I32EnumAttr< + "DescriptorReduceKind", "", + [ + I32EnumAttrCase<"ADD", 1, "add">, + I32EnumAttrCase<"MIN", 2, "min">, + I32EnumAttrCase<"MAX", 3, "max">, + I32EnumAttrCase<"INC", 4, "inc">, + I32EnumAttrCase<"DEC", 5, "dec">, + I32EnumAttrCase<"AND", 6, "and">, + I32EnumAttrCase<"OR", 7, "or">, + I32EnumAttrCase<"XOR", 8, "xor">, + ]> { + let cppNamespace = "::mlir::triton"; +} + +def TT_MemSyncScopeAttr : I32EnumAttr< + "MemSyncScope", "", + [ + I32EnumAttrCase<"GPU", 1, "gpu">, + I32EnumAttrCase<"CTA", 2, "cta">, + I32EnumAttrCase<"SYSTEM", 3, "sys">, + ]> { + let cppNamespace = "::mlir::triton"; +} + +// Program ID dimensions. +def TT_ProgramDim : I32EnumAttr< + "ProgramIDDim", "", + [ + I32EnumAttrCase<"X", 0, "x">, + I32EnumAttrCase<"Y", 1, "y">, + I32EnumAttrCase<"Z", 2, "z">, + ]> { + let cppNamespace = "::mlir::triton"; +} + +// Rounding mode. +def TT_RoundingModeAttr : I32EnumAttr< + "RoundingMode", "", + [ + I32EnumAttrCase<"RTZ", 0, "rtz">, + I32EnumAttrCase<"RTNE", 1, "rtne">, + ]> { + let cppNamespace = "::mlir::triton"; +} + +// PropagateNan. +def TT_PropagateNanAttr : I32EnumAttr< + "PropagateNan", "", + [ + I32EnumAttrCase<"NONE", 0, "none">, + I32EnumAttrCase<"ALL", 0xFFFF, "all">, + ]> { + let cppNamespace = "::mlir::triton"; +} + +// InputPrecision +def TT_InputPrecisionAttr : I32EnumAttr< + "InputPrecision", "", + [ + I32EnumAttrCase<"TF32", 0, "tf32">, + I32EnumAttrCase<"TF32x3", 1, "tf32x3">, + I32EnumAttrCase<"IEEE", 2, "ieee">, + I32EnumAttrCase<"HF32", 3, "hf32">, + I32EnumAttrCase<"BF16x3", 4, "bf16x3">, + I32EnumAttrCase<"BF16x6", 5, "bf16x6"> + ]>{ + let cppNamespace = "::mlir::triton"; +} + +// Type for ScaleDotElemType kind of floats. +def TT_ScaleDotElemTypeAttr : I32EnumAttr< + "ScaleDotElemType", "", + [ + I32EnumAttrCase<"E4M3", 0, "e4m3">, + I32EnumAttrCase<"E5M2", 1, "e5m2">, + I32EnumAttrCase<"E2M3", 2, "e2m3">, + I32EnumAttrCase<"E3M2", 3, "e3m2">, + I32EnumAttrCase<"E2M1", 4, "e2m1">, + I32EnumAttrCase<"BF16", 5, "bf16">, + I32EnumAttrCase<"FP16", 6, "fp16"> + ]>{ + let cppNamespace = "::mlir::triton"; +} + +#endif diff --git a/third_party/ascend/spec_cpp/lib/Dialect/Triton/IR/Traits.cpp b/third_party/ascend/spec_cpp/lib/Dialect/Triton/IR/Traits.cpp new file mode 100644 index 0000000000..81d4ffcdbc --- /dev/null +++ b/third_party/ascend/spec_cpp/lib/Dialect/Triton/IR/Traits.cpp @@ -0,0 +1,276 @@ +/* + * Copyright 2018-2020 Philippe Tillet + * Copyright 2020-2022 OpenAI + * Copyright 2025- FlagOS Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "triton/Dialect/Triton/IR/Traits.h" + +#include + +#include "mlir/IR/TypeUtilities.h" +#include "triton/Dialect/Triton/IR/Dialect.h" +#include "triton/Dialect/Triton/IR/Types.h" +#include "triton/Dialect/Triton/IR/Utility.h" +#include "triton/Dialect/TritonGPU/IR/Types.h" +#include "llvm/Support/ErrorHandling.h" + +using namespace mlir; +using namespace mlir::triton::gpu; + +LogicalResult OpTrait::impl::verifyEquivalentType(Type typeA, Type typeB) { + auto memdescA = dyn_cast(typeA); + auto memdescB = dyn_cast(typeB); + if (memdescA || memdescB) { + if (!memdescA || !memdescB) + return failure(); + if (memdescA.getShape() != memdescB.getShape()) + return failure(); + if (memdescA.getAllocShape() != memdescB.getAllocShape()) + return failure(); + if (memdescA.getElementType() != memdescB.getElementType()) + return failure(); + if (memdescA.getMemorySpace() != memdescB.getMemorySpace()) + return failure(); + if (memdescA.getMutableMemory() != memdescB.getMutableMemory()) + return failure(); + + Attribute encodingA = memdescA.getEncoding(); + Attribute encodingB = memdescB.getEncoding(); + if (encodingA == encodingB) + return success(); + if (static_cast(encodingA) != static_cast(encodingB)) + return failure(); + + auto layoutInterface = + cast(&encodingA.getDialect()); + return layoutInterface->verifyLayoutsAreEqual(memdescA.getShape(), + encodingA, encodingB, {}); + } + auto tensorTypeA = dyn_cast(typeA); + auto tensorTypeB = dyn_cast(typeB); + if (!(bool(tensorTypeA) && bool(tensorTypeB))) + return typeA == typeB ? success() : failure(); + auto encodingA = tensorTypeA.getEncoding(); + auto encodingB = tensorTypeB.getEncoding(); + auto shapeA = tensorTypeA.getShape(); + auto shapeB = tensorTypeB.getShape(); + if (shapeA != shapeB) + return failure(); + if (tensorTypeA.getElementType() != tensorTypeB.getElementType()) + return failure(); + // If there's no encoding or the encodings are the same + if (encodingA == encodingB) + return success(); + if (bool(encodingA) != bool(encodingB)) + return failure(); + + return cast(&encodingA.getDialect()) + ->verifyLayoutsAreEqual(shapeA, encodingA, encodingB, {}); +} + +static LogicalResult verifySameEncoding(Type typeA, Type typeB, + bool allowTensorPointerType) { + // TODO(Keren): the allowTensorPointerType argument is a hack to allow. + // The type checking code is kind of a mess with the current design. + auto getEncoding = [=](Type type) -> Attribute { + Attribute ret; + if (auto tensorType = dyn_cast(type)) { + ret = tensorType.getEncoding(); + } + if (!allowTensorPointerType) { + assert(!triton::isTensorPointerType(type)); + } + return ret; + }; + auto encodingA = getEncoding(typeA); + auto encodingB = getEncoding(typeB); + if (!encodingA || !encodingB) + return success(); + return encodingA == encodingB ? success() : failure(); +} + +LogicalResult +OpTrait::impl::verifySameOperandsEncoding(Operation *op, + bool allowTensorPointerType) { + if (failed(verifyAtLeastNOperands(op, 1))) + return failure(); + + auto type = op->getOperand(0).getType(); + for (auto opType : llvm::drop_begin(op->getOperandTypes(), 1)) + if (failed(verifySameEncoding(opType, type, allowTensorPointerType))) + return op->emitOpError() << "requires the same encoding for all operands"; + + return success(); +} + +LogicalResult OpTrait::impl::verifySameOperandsAndResultEncoding( + Operation *op, bool allowTensorPointerType) { + if (op->getNumOperands() == 0) + return success(); + + if (failed(verifyAtLeastNOperands(op, 1)) || + failed(verifyAtLeastNResults(op, 1))) + return failure(); + + auto type = op->getOperand(0).getType(); + for (auto resultType : op->getResultTypes()) + if (failed(verifySameEncoding(resultType, type, allowTensorPointerType))) + return op->emitOpError() + << "requires the same encoding for all operands and results"; + + return verifySameOperandsEncoding(op, allowTensorPointerType); +} + +LogicalResult OpTrait::impl::verifyTensorSize(Operation *op) { + for (auto opType : op->getOperandTypes()) { + if (auto tensorType = dyn_cast(opType)) { + int64_t numElements = 1; + for (int64_t s : tensorType.getShape()) + numElements *= s; + if (numElements > maxTensorNumElements) + return op->emitError("Maximum allowed number of elements is ") + << maxTensorNumElements << ", but " << *op + << " has more than that"; + // FIXME:patched triton community + // if ((numElements & (numElements - 1)) != 0) + // return op->emitError("Number of elements must be power-of-two, but ") + // << *op << " doesn't follow the rule (" << numElements << ")" + // << " elements"; + } + } + for (auto opType : op->getResultTypes()) { + if (auto tensorType = dyn_cast(opType)) { + int64_t numElements = 1; + for (int64_t s : tensorType.getShape()) + numElements *= s; + if (numElements > maxTensorNumElements) + return op->emitError("Maximum allowed number of elements is ") + << maxTensorNumElements << ", but " << *op + << " has more than that"; + // FIXME:patched triton community + // if ((numElements & (numElements - 1)) != 0) + // return op->emitError("Number of elements must be power-of-two, but ") + // << *op << " doesn't follow the rule (" << numElements << ")" + // << " elements"; + } + } + return success(); +} + +// Check that the Triton layouts on op's operands and return types are valid. +// For example, we check that the number of warps per block in a Triton GPU +// blocked layout matches that of its module. +// +// It's a little weird to check these properties of a layout only when the +// layout is used in an op, since most of the properties don't actually depend +// on the op. They do depend on the *module*, though, and a layout is attached +// to a module only by virtue of being used in one of the module's ops. +LogicalResult OpTrait::impl::verifyTensorLayouts(Operation *op) { + auto checkLayout = [&](Value val, auto makeErr) -> LogicalResult { + // Only ranked tensors can have layouts. + auto rankedTy = dyn_cast(val.getType()); + if (!rankedTy) + return success(); + + mlir::Attribute layout = rankedTy.getEncoding(); + if (!layout) + return success(); + + Dialect &dialect = layout.getDialect(); + auto verifyLayoutInterface = + dyn_cast(&dialect); + if (verifyLayoutInterface) { + return verifyLayoutInterface->verifyTensorLayout(layout, rankedTy, op, + makeErr); + } + + return success(); + }; + + for (size_t i = 0; i < op->getNumOperands(); i++) { + auto operand = op->getOperand(i); + auto err = checkLayout(operand, [&]() { + // Stringify the operand using `printAsOperand`. This prints e.g. "%42" + // rather than the full definition. + std::string operandStr; + llvm::raw_string_ostream os(operandStr); + // If we don't assume verified, dump() will recursively call this + // function! + operand.printAsOperand(os, OpPrintingFlags().assumeVerified()); + + return op->emitError("Operand ") + << i << " (" << operand << ") has an invalid layout: "; + }); + if (!err.succeeded()) + return err; + } + + for (size_t i = 0; i < op->getNumResults(); i++) { + auto result = op->getResult(i); + auto err = checkLayout(result, [&]() { + if (op->getNumResults() == 1) { + return op->emitError("Result has an invalid layout: "); + } else { + return op->emitError("Result ") << i << " has an invalid layout: "; + } + }); + if (!err.succeeded()) + return err; + } + + return success(); +} + +static ArrayRef getTypeShape(Type type) { + auto rankedType = dyn_cast(type); + if (auto ptrType = dyn_cast(type)) + rankedType = dyn_cast(ptrType.getPointeeType()); + return rankedType ? rankedType.getShape() : ArrayRef(); +} + +LogicalResult OpTrait::impl::verifySameLoadStoreOperandsShape(Operation *op) { + if (failed(verifyAtLeastNOperands(op, 1))) + return failure(); + + auto firstOperandShape = getTypeShape(op->getOperand(0).getType()); + for (auto type : llvm::drop_begin(op->getOperandTypes(), 1)) + if (failed(verifyCompatibleShape(getTypeShape(type), firstOperandShape))) + return op->emitOpError() << "requires the same shape for all operands"; + + return success(); +} + +LogicalResult +OpTrait::impl::verifySameLoadStoreOperandsAndResultShape(Operation *op) { + if (failed(verifyAtLeastNOperands(op, 1)) || + failed(verifyAtLeastNResults(op, 1))) + return failure(); + + auto firstOperandShape = getTypeShape(op->getOperand(0).getType()); + for (auto type : op->getResultTypes()) + if (failed(verifyCompatibleShape(getTypeShape(type), firstOperandShape))) + return op->emitOpError() + << "requires the same shape for all operands and results"; + + return verifySameLoadStoreOperandsShape(op); +} diff --git a/third_party/ascend/triton_ascend.cc b/third_party/ascend/triton_ascend.cc new file mode 100644 index 0000000000..a08a3a0b43 --- /dev/null +++ b/third_party/ascend/triton_ascend.cc @@ -0,0 +1,519 @@ +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/GPU/IR/GPUDialect.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/Dialect/Linalg/IR/Linalg.h" +#include "mlir/Dialect/Linalg/Passes.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Transforms/Passes.h" // createInlinerPass + +#include "ascend/include/AutoBlockify/Passes.h" +#include "ascend/include/Dialect/TritonAscend/IR/TritonAscendDialect.h" +#include "ascend/include/DiscreteMaskAccessConversion/Passes.h" +#include "ascend/include/TritonControlFlowOpt/Passes.h" +#include "ascend/include/TritonToAnnotation/Passes.h" +#include "ascend/include/TritonToGraph/GraphOptimization.h" +#include "ascend/include/TritonToGraph/LayoutMemoryOptimization.h" +#include "ascend/include/TritonToGraph/Passes.h" +#include "ascend/include/TritonToHFusion/Passes.h" +#include "ascend/include/TritonToHIVM/Passes.h" +#include "ascend/include/TritonToLLVM/Passes.h" +#include "ascend/include/TritonToLinalg/Passes.h" +#include "ascend/include/TritonToStructured/Passes.h" +#include "ascend/include/TritonToUnstructure/Passes.h" + +#include "ascend/include/DynamicCVPipeline/AnalyzeDataFlow.h" +#include "ascend/include/DynamicCVPipeline/Common/BufferCountManager.h" +#include "ascend/include/DynamicCVPipeline/Common/Utils.h" +#include "ascend/include/DynamicCVPipeline/Passes.h" + +#include "ascend/include/CommonIRToHIVM/Passes.h" +#include "ascend/include/CommonIRToHIVM/ReduceSumStrengthPass.h" +#include "mlir-ext/Dialect/CommonIR/IR/CommonIRDialect.h" +#include "mlir-ext/Dialect/CommonIR/Transforms/Passes.h" +#include "mlir/Dialect/Func/Extensions/InlinerExtension.h" + +#include "ir.h" // TritonOpBuilder +#include "triton/Dialect/Triton/IR/Dialect.h" + +#include +#include + +#include +#include +#include + +#if TRITON_ASCEND_HAS_INPROC_COSTMODEL +#include "AscendModel/IR/AscendModelDialect.h" +#include "AscendModel/Transforms/Passes.h" + +#include "mlir/Dialect/Affine/IR/AffineOps.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Math/IR/Math.h" +#include "mlir/Dialect/SCF/IR/SCF.h" + +#include +#include +#include +#include +#include +#include +#endif + +namespace py = pybind11; +using namespace mlir; + +void init_triton_ascend_passes_ttir(py::module &&m) { + m.def("add_lane_vectorize", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::tile::createTritonLaneVectorize()); + }); + + m.def("add_commonir_to_hivm", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::createCommonIRToHIVMPass()); + }); + + m.def("add_reduce_sum_strength", [](mlir::PassManager &pm, bool enable, + int32_t splitFactor) { + pm.addPass(mlir::triton::createReduceSumStrengthPass(enable, splitFactor)); + }); + + m.def("add_auto_blockify", [](mlir::PassManager &pm, int autoBlockifySize) { + AutoBlockifyOptions opts; + opts.autoBlockifySize = autoBlockifySize; + pm.addPass(mlir::triton::createAutoBlockifyPass(opts)); + }); + + m.def("add_triton_to_structure", + [](mlir::PassManager &pm, bool enableMaskFallbackConversion, + bool optimizeDynamicOffset) { + pm.addPass(mlir::triton::createTritonToStructuredPass( + enableMaskFallbackConversion, optimizeDynamicOffset)); + }); + + m.def("add_triton_control_flow_opt", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::createTritonControlFlowOptPass()); + }); + + m.def("add_triton_to_annotation", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::createTritonToAnnotationPass()); + }); + + m.def("add_triton_to_linalg", + [](mlir::PassManager &pm, bool globalKernel, bool namedOps, + bool enableNd2nzOnVector, bool enableSelectAnalysis, + bool compileOn91095, const std::string &compileMode) { + pm.addPass(mlir::triton::createTritonToLinalgPass( + globalKernel, namedOps, enableNd2nzOnVector, enableSelectAnalysis, + compileOn91095, compileMode)); + }); + + m.def("add_merge_concat_load_buffer", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::cfg::createMergeConcatLoadBufferPass()); + }); + + m.def("add_triton_to_unstructure", + [](mlir::PassManager &pm, bool compileOn91095, + const std::string &compileMode) { + TritonToUnstructureOptions opts; + opts.compileOn91095 = compileOn91095; + opts.compileMode = compileMode; + pm.addPass(mlir::triton::createTritonToUnstructurePass(opts)); + }); + + m.def("add_triton_to_hfusion", + [](mlir::PassManager &pm, bool compileOn91095) { + pm.addPass(mlir::triton::createTritonToHFusionPass(compileOn91095)); + }); + + m.def("add_discrete_mask_access_conversion", + [](mlir::PassManager &pm, bool compileOn91095, + const std::string &compileMode) { + DiscreteMaskAccessConversionOptions opts; + opts.compileOn91095 = compileOn91095; + opts.compileMode = compileMode; + pm.addPass( + mlir::triton::createDiscreteMaskAccessConversionPass(opts)); + }); + + m.def("add_triton_to_hivm", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::createTritonToHIVMPass()); + }); + + m.def("add_triton_to_llvm", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::createTritonToLLVMPass()); + }); + + m.def("add_normalize_debug_line_locations", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::createNormalizeDebugLineLocationsPass()); + }); + + m.def("add_bubble_up_operation", [](mlir::PassManager &pm) { + pm.addPass(mlir::triton::createBubbleUpOperationPass()); + }); + + m.def("add_dynamic_cv_pipeline", + [](mlir::PassManager &pm, bool compileOn91095) { + AddDynamicCVPipelineOptions opts; + opts.compileOn91095 = compileOn91095; + pm.addPass(mlir::triton::createAddDynamicCVPipelinePass(opts)); + }); + + m.def( + "add_graph_optimize", + [](mlir::PassManager &pm, std::uint64_t ruleMask, + std::uint64_t maxRewritesPerFunction, std::uint64_t ubCapacityBytes, + std::uint64_t mappingUBCapacityBytes, + std::uint64_t storeCoalescingUBBudgetBytes, + const std::string &compileMode, std::uint64_t ubSafetyPercent, + std::uint64_t reservedUBBytes) { + if (ruleMask > std::numeric_limits::max()) + throw py::value_error("rule_mask must fit in uint32_t"); + const auto graphRuleMask = + static_cast(ruleMask); + if (!mlir::triton::cfg::isValidGraphOptimizationRuleMask( + graphRuleMask)) { + throw py::value_error("rule_mask contains unknown graph rule bits"); + } + if (maxRewritesPerFunction > std::numeric_limits::max()) + throw py::value_error( + "max_rewrites_per_function must fit in unsigned"); + if (ubCapacityBytes > std::numeric_limits::max()) + throw py::value_error("ub_capacity_bytes must fit in unsigned"); + if (mappingUBCapacityBytes > std::numeric_limits::max()) + throw py::value_error( + "mapping_ub_capacity_bytes must fit in unsigned"); + if (storeCoalescingUBBudgetBytes > std::numeric_limits::max()) + throw py::value_error( + "store_coalescing_ub_budget_bytes must fit in unsigned"); + if (ubSafetyPercent == 0 || ubSafetyPercent > 100 || + ubSafetyPercent > std::numeric_limits::max()) + throw py::value_error( + "ub_safety_percent must be an unsigned value in [1, 100]"); + const std::uint64_t effectiveMappingUBCapacity = + mappingUBCapacityBytes ? mappingUBCapacityBytes : ubCapacityBytes; + if (reservedUBBytes > std::numeric_limits::max() || + reservedUBBytes > effectiveMappingUBCapacity) + throw py::value_error("reserved_ub_bytes must fit in unsigned and " + "not exceed mapping UB"); + + mlir::triton::cfg::GraphOptimizationOptions options; + options.enabledRuleMask = graphRuleMask; + options.maxRewritesPerFunction = + static_cast(maxRewritesPerFunction); + options.ubCapacityBytes = static_cast(ubCapacityBytes); + options.mappingUBCapacityBytes = + static_cast(mappingUBCapacityBytes); + options.storeCoalescingUBBudgetBytes = + static_cast(storeCoalescingUBBudgetBytes); + options.ubSafetyPercent = static_cast(ubSafetyPercent); + options.reservedUBBytes = static_cast(reservedUBBytes); + options.compileMode = compileMode; + pm.addPass(mlir::triton::cfg::createGraphOptimizePass(options)); + }, + py::arg("pm"), + py::arg("rule_mask") = static_cast( + mlir::triton::cfg::kDefaultGraphOptimizationRuleMask), + py::arg("max_rewrites_per_function") = 64, + py::arg("ub_capacity_bytes") = 0, + py::arg("mapping_ub_capacity_bytes") = 0, + py::arg("store_coalescing_ub_budget_bytes") = 0, + py::arg("compile_mode") = "simd_simt_template", + py::arg("ub_safety_percent") = 80, py::arg("reserved_ub_bytes") = 0); + + m.def("set_buffer_count", [](mlir::ModuleOp &module, const std::string &type, + int count) { + mlir::triton::BufferCountManager mgr(module); + if (type == "INTRA") { + mgr.setBufferCount(mlir::triton::BufferCountManager::DepType::IntraCore, + count); + } else if (type == "INTER") { + mgr.setBufferCount(mlir::triton::BufferCountManager::DepType::InterCore, + count); + } else if (type == "LOAD") { + mgr.setBufferCount(mlir::triton::BufferCountManager::DepType::LoadStore, + count); + } + }); + + m.def("set_enable_ub_refine_opt", + [](mlir::ModuleOp &moduleop, bool enable) { return; }); + m.def("set_enable_buffer_insert_optimization", [](mlir::ModuleOp &moduleop) { + OpBuilder builder(moduleop.getContext()); + moduleop->setAttr(CVPipeline::kInsertionOptimization, + builder.getUnitAttr()); + }); +} + +#if TRITON_ASCEND_HAS_INPROC_COSTMODEL +namespace { +struct AscendCostModelRuntimeOptions { + bool allowUnregisteredDialect = false; + bool enableCostModelPipeline = false; + std::string argBindings; + std::string hardwareConfig; +}; + +static void +parseAscendCostModelOptionString(const std::string &option, + AscendCostModelRuntimeOptions &opts) { + // Parse known keys while allowing commas inside values, e.g. + // arg-bindings=arg8=256,arg9=80,arg10=32,pid_x=0 + auto startsWith = [](const std::string &s, const std::string &p) { + return s.rfind(p, 0) == 0; + }; + + auto trim = [](std::string v) { + while (!v.empty() && (v.back() == ' ' || v.back() == ',')) { + v.pop_back(); + } + size_t i = 0; + while (i < v.size() && v[i] == ' ') { + ++i; + } + return v.substr(i); + }; + + auto extractValue = [&](const std::string &key) -> std::string { + const std::string needle = key + "="; + size_t keyPos = option.find(needle); + if (keyPos == std::string::npos) { + return ""; + } + + size_t valueStart = keyPos + needle.size(); + size_t valueEnd = option.size(); + + const std::string keys[] = {"arg-bindings", "hardware-config"}; + for (const auto &otherKey : keys) { + if (otherKey == key) { + continue; + } + const std::string otherNeedle = otherKey + "="; + const std::string commaBoundary = "," + otherNeedle; + const std::string spaceBoundary = " " + otherNeedle; + + size_t p1 = option.find(commaBoundary, valueStart); + if (p1 != std::string::npos) { + valueEnd = std::min(valueEnd, p1); + } + size_t p2 = option.find(spaceBoundary, valueStart); + if (p2 != std::string::npos) { + valueEnd = std::min(valueEnd, p2); + } + } + + return trim(option.substr(valueStart, valueEnd - valueStart)); + }; + + // Fast path for single-key payloads passed as next argv token. + if (startsWith(option, "arg-bindings=")) { + opts.argBindings = trim(option.substr(std::string("arg-bindings=").size())); + return; + } + if (startsWith(option, "hardware-config=")) { + opts.hardwareConfig = + trim(option.substr(std::string("hardware-config=").size())); + return; + } + + auto argBindings = extractValue("arg-bindings"); + if (!argBindings.empty()) { + opts.argBindings = argBindings; + } + auto hardwareConfig = extractValue("hardware-config"); + if (!hardwareConfig.empty()) { + opts.hardwareConfig = hardwareConfig; + } +} + +static AscendCostModelRuntimeOptions +parseAscendCostmodelArgs(const std::vector &extraArgs) { + AscendCostModelRuntimeOptions opts; + for (size_t i = 0; i < extraArgs.size(); ++i) { + const auto &arg = extraArgs[i]; + if (arg.empty()) { + continue; + } + if (arg == "-allow-unregistered-dialect") { + opts.allowUnregisteredDialect = true; + continue; + } + if (arg == "-ascend-perf-model" || arg == "--ascend-perf-model") { + opts.enableCostModelPipeline = true; + if (i + 1 < extraArgs.size() && !extraArgs[i + 1].empty() && + extraArgs[i + 1][0] != '-') { + parseAscendCostModelOptionString(extraArgs[++i], opts); + } + continue; + } + constexpr const char *kShortPrefix = "-ascend-perf-model="; + constexpr const char *kLongPrefix = "--ascend-perf-model="; + if (arg.rfind(kShortPrefix, 0) == 0) { + opts.enableCostModelPipeline = true; + parseAscendCostModelOptionString(arg.substr(std::strlen(kShortPrefix)), + opts); + continue; + } + if (arg.rfind(kLongPrefix, 0) == 0) { + opts.enableCostModelPipeline = true; + parseAscendCostModelOptionString(arg.substr(std::strlen(kLongPrefix)), + opts); + continue; + } + } + return opts; +} + +static double extractEstimatedTimeUs(mlir::ModuleOp module) { + constexpr double CYCLES_PER_US = 1850.0; + + auto getCycleAttr = [&](llvm::StringRef name) -> std::optional { + if (auto attr = module->getAttrOfType(name)) + return attr.getInt(); + return std::nullopt; + }; + + // PipelineAnalysisPass computes loop-multiplied roofline cycles. Older + // callers looked for ascend.scheduled_cycles, so keep that first for + // compatibility, then fall back to the weighted roofline summary. + if (auto cycles = getCycleAttr("ascend.scheduled_cycles")) + return static_cast(*cycles) / CYCLES_PER_US; + if (auto cycles = getCycleAttr("ascend.roofline_cycles")) + return static_cast(*cycles) / CYCLES_PER_US; + if (auto cycles = getCycleAttr("ascend.simple_sum_cycles")) + return static_cast(*cycles) / CYCLES_PER_US; + + int64_t fallbackCycles = 0; + module.walk([&](mlir::Operation *op) { + if (auto cyclesAttr = + op->getAttrOfType("estimated_cycles")) { + int64_t multiplier = 1; + if (auto multiplierAttr = + op->getAttrOfType("loop_multiplier")) + multiplier = std::max(1, multiplierAttr.getInt()); + fallbackCycles += cyclesAttr.getInt() * multiplier; + } + }); + return static_cast(fallbackCycles) / CYCLES_PER_US; +} +} // namespace + +static std::string +runAscendCostModelInProcess(const std::string &mlirText, + const std::vector &extraArgs) { + mlir::DialectRegistry registry; + registry.insert(); + registry.insert(); + registry.insert(); + registry.insert(); + registry.insert(); + registry.insert(); + registry.insert(); + registry.insert(); + registry.insert(); + + auto opts = parseAscendCostmodelArgs(extraArgs); + if (!opts.enableCostModelPipeline) { + throw std::runtime_error( + "in-process costmodel requires '-ascend-perf-model'"); + } + + mlir::MLIRContext context(registry); + if (opts.allowUnregisteredDialect) { + context.allowUnregisteredDialects(true); + } + + auto module = mlir::parseSourceString(mlirText, &context); + if (!module) { + throw std::runtime_error( + "in-process costmodel failed to parse input MLIR module"); + } + + mlir::PassManager pm(&context); + // Inline tt.call helpers (e.g. softmax's @max/@sum reduce helpers) into the + // calling kernel BEFORE conversion/estimation. Without this, ops in a helper + // invoked from inside a loop are estimated at the helper's top level (outside + // the loop) and get loopMultiplier=1 instead of the caller's trip count, so + // per-iteration ops like reductions are under-counted. TritonDialect + // registers TritonInlinerInterface, so the generic MLIR inliner handles + // tt.call (CallOpInterface). SymbolDCE then erases the now-dead private + // helpers so their ops are not double-counted alongside the inlined copies. + pm.addPass(mlir::createInlinerPass()); + pm.addPass(mlir::createSymbolDCEPass()); + pm.addPass(mlir::ascend::createConvertTritonToAscendPass()); + pm.addPass(mlir::ascend::createInsertDataTransfersPass()); + pm.addPass(mlir::ascend::createAssignOpIDsPass()); + { + mlir::ascend::EstimateCyclesPassOptions estimateOpts; + estimateOpts.argBindingsStr = opts.argBindings; + estimateOpts.hardwareConfigPath = opts.hardwareConfig; + pm.addPass(mlir::ascend::createEstimateCyclesPass(estimateOpts)); + } + { + mlir::ascend::PipelineAnalysisPassOptions pipelineOpts; + pipelineOpts.argBindingsStr = opts.argBindings; + pipelineOpts.hardwareConfigPath = opts.hardwareConfig; + pm.addPass(mlir::ascend::createPipelineAnalysisPass(pipelineOpts)); + } + + if (mlir::failed(pm.run(*module))) { + throw std::runtime_error("in-process costmodel pass pipeline failed"); + } + + const double timeUs = extractEstimatedTimeUs(*module); + std::ostringstream os; + os.setf(std::ios::fixed); + os.precision(3); + os << "Estimated Time: " << timeUs << " us\n"; + return os.str(); +} +#endif + +// Forward declaration for ascend_ir bindings (defined in ascend_ir.cc) +void init_ascend_ir(py::module &&m); +void init_buffer_ir(py::module &&m); + +void init_triton_ascend(py::module &&m) { + py::module_ libtriton = py::module_::import("triton._C.libtriton"); + init_buffer_ir(libtriton.def_submodule("buffer_ir")); + + auto passes = m.def_submodule("passes"); + // load dialects + m.def("load_dialects", [](mlir::MLIRContext &context) { + mlir::DialectRegistry registry; + registry.insert(); + // commonir-to-hivm is followed by the Inliner, which aborts unless the func + // dialect's inliner interface is registered. + mlir::func::registerInlinerExtension(registry); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + }); + + init_triton_ascend_passes_ttir(passes.def_submodule("ttir")); + +#if TRITON_ASCEND_HAS_INPROC_COSTMODEL + m.def( + "run_costmodel_inproc", + [](const std::string &mlirText, + const std::vector &extraArgs) { + py::gil_scoped_release release; + return runAscendCostModelInProcess(mlirText, extraArgs); + }, + py::arg("mlir_text"), py::arg("extra_args") = std::vector{}); +#else + m.def( + "run_costmodel_inproc", + [](const std::string &, const std::vector &) { + throw std::runtime_error( + "in-process costmodel bridge is not enabled in this build"); + return std::string(); + }, + py::arg("mlir_text"), py::arg("extra_args") = std::vector{}); +#endif + + // Initialize ascend IR bindings (ascendnpu_ir_builder, scope/hivm dialects) + init_ascend_ir(m.def_submodule("ir")); +} diff --git a/third_party/ascend/tutorials/01-vector-add.py b/third_party/ascend/tutorials/01-vector-add.py new file mode 100644 index 0000000000..038a625e28 --- /dev/null +++ b/third_party/ascend/tutorials/01-vector-add.py @@ -0,0 +1,110 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Vector Addition +=============== + +In this tutorial, you will write a simple vector addition using Triton. + +In doing so, you will learn about: + +* The basic programming model of Triton. + +* The `triton.jit` decorator, which is used to define Triton kernels. + +* The best practices for validating and benchmarking your custom ops against native reference implementations. + +""" + +# %% +# Compute Kernel +# -------------- + +import torch +import torch_npu + +import triton +import triton.language as tl + + +@triton.jit +def add_kernel(x_ptr, # *Pointer* to first input vector. + y_ptr, # *Pointer* to second input vector. + output_ptr, # *Pointer* to output vector. + n_elements, # Size of the vector. + BLOCK_SIZE: tl.constexpr, # Number of elements each program should process. + # NOTE: `constexpr` so it can be used as a shape value. + ): + # There are multiple 'programs' processing different data. We identify which program + # we are here: + pid = tl.program_id(axis=0) # We use a 1D launch grid so axis is 0. + # This program will process inputs that are offset from the initial data. + # For instance, if you had a vector of length 256 and block_size of 64, the programs + # would each access the elements [0:64, 64:128, 128:192, 192:256]. + # Note that offsets is a list of pointers: + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + # Create a mask to guard memory operations against out-of-bounds accesses. + mask = offsets < n_elements + # Load x and y from DRAM, masking out any extra elements in case the input is not a + # multiple of the block size. + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + # Write x + y back to DRAM. + tl.store(output_ptr + offsets, output, mask=mask) + + +# %% +# Let's also declare a helper function to (1) allocate the `z` tensor +# and (2) enqueue the above kernel with appropriate grid/block sizes: + + +def add(x: torch.Tensor, y: torch.Tensor): + output = torch.empty_like(x) + n_elements = output.numel() + grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + add_kernel[grid](x, y, output, n_elements, BLOCK_SIZE=1024) + return output + + +# %% +# We can now use the above function to compute the element-wise sum of two `torch.tensor` objects and test its correctness: + + +def test_vector_addition(): + torch.manual_seed(0) + size = 98432 + x = torch.rand(size, device='npu') + y = torch.rand(size, device='npu') + output_torch = x + y + output_triton = add(x, y) + torch.testing.assert_close(output_triton, output_torch) + print("output_torch:", output_torch) + print("output_triton:", output_triton) + print(f'The maximum difference between torch and triton is ' + f'{torch.max(torch.abs(output_torch - output_triton))}') + + +if __name__ == "__main__": + test_vector_addition() + print("======Vector Add Test Passed!======") diff --git a/third_party/ascend/tutorials/02-fused-softmax.py b/third_party/ascend/tutorials/02-fused-softmax.py new file mode 100644 index 0000000000..aef6ceea44 --- /dev/null +++ b/third_party/ascend/tutorials/02-fused-softmax.py @@ -0,0 +1,135 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Fused Softmax +============= +""" + +import torch +import torch_npu +import triton +import triton.language as tl +from triton.runtime import driver + + +def naive_softmax(x): + """Compute row-wise softmax of X using native pytorch + + We subtract the maximum element in order to avoid overflows. Softmax is invariant to + this shift. + """ + # read MN elements ; write M elements + x_max = x.max(dim=1)[0] + # read MN + M elements ; write MN elements + z = x - x_max[:, None] + # read MN elements ; write MN elements + numerator = torch.exp(z) + # read MN elements ; write M elements + denominator = numerator.sum(dim=1) + # read MN + M elements ; write MN elements + ret = numerator / denominator[:, None] + # in total: read 5MN + 2M elements ; wrote 3MN + 2M elements + return ret + + +@triton.jit +def softmax_kernel(output_ptr, input_ptr, input_row_stride, output_row_stride, n_rows, n_cols, + BLOCK_SIZE: tl.constexpr): + # starting row of the program + row_start = tl.program_id(0) + row_step = tl.num_programs(0) + for row_idx in tl.range(row_start, n_rows, row_step): + # The stride represents how much we need to increase the pointer to advance 1 row + row_start_ptr = input_ptr + row_idx * input_row_stride + # The block size is the next power of two greater than n_cols, so we can fit each + # row in a single block + col_offsets = tl.arange(0, BLOCK_SIZE) + input_ptrs = row_start_ptr + col_offsets + # Load the row into SRAM, using a mask since BLOCK_SIZE may be > than n_cols + mask = col_offsets < n_cols + row = tl.load(input_ptrs, mask=mask, other=-float('inf')) + # Subtract maximum for numerical stability + row_minus_max = row - tl.max(row, axis=0) + + numerator = tl.exp(row_minus_max) + denominator = tl.sum(numerator, axis=0) + softmax_output = numerator / denominator + # Write back output to DRAM + output_row_start_ptr = output_ptr + row_idx * output_row_stride + output_ptrs = output_row_start_ptr + col_offsets + tl.store(output_ptrs, softmax_output, mask=mask) + + +target = triton.runtime.driver.active.get_current_target() +kernels = {} + + +def softmax(x, stream=None): + n_rows, n_cols = x.shape + + # The block size of each loop iteration is the smallest power of two greater than the number of columns in `x` + BLOCK_SIZE = triton.next_power_of_2(n_cols) + + # Allocate output + y = torch.empty_like(x) + + if stream is None: + device = torch.npu.current_device() + stream = torch.npu.current_stream(device).npu_stream + + # pre-compile kernel to get register usage and compute thread occupancy. + kernel, num_programs = kernels.get(BLOCK_SIZE, (None, 0)) + if kernel is None: + num_programs = 32 + kernel = softmax_kernel + kernels[BLOCK_SIZE] = (kernel, num_programs) + + num_programs = min(num_programs, n_rows) + + # Create a number of persistent programs. + kernel[(num_programs, 1, 1)](y, x, x.stride(0), y.stride(0), n_rows, n_cols, BLOCK_SIZE) + return y + + +# %% +# Unit Test +# --------- + + +def test_fused_softmax(): + shape = (1823, 781) + torch.manual_seed(0) + x = torch.randn(shape, dtype=torch.float16, device="npu") + y_triton = softmax(x) + y_torch = torch.softmax(x, axis=1) + torch.testing.assert_close(y_triton, y_torch, atol=1e-4, rtol=1e-4) + # Demo-style print output for the irregular shape + print(y_triton) + print(y_torch) + print(f'The maximum difference between torch and triton is ' + f'{torch.max(torch.abs(y_triton-y_torch))}') + + +# %% +# We make sure that we test our kernel on a matrix with an irregular number of rows and columns. +# This will allow us to verify that our padding mechanism works. +if __name__ == "__main__": + test_fused_softmax() + print("======Fused Softmax Test Passed!======") diff --git a/third_party/ascend/tutorials/03-matrix-multiplication.py b/third_party/ascend/tutorials/03-matrix-multiplication.py new file mode 100644 index 0000000000..ee00de6788 --- /dev/null +++ b/third_party/ascend/tutorials/03-matrix-multiplication.py @@ -0,0 +1,203 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Matrix Multiplication +=============== +""" + +import triton +import triton.language as tl +import torch +import torch_npu +import triton.language.extra.cann.extension as extension + +DEV = "npu" + + +def get_autotune_config(): + return [ + triton.Config({"BLOCK_SIZE_M": 128, "BLOCK_SIZE_N": 128, "BLOCK_SIZE_K": 128}), + triton.Config({"BLOCK_SIZE_M": 64, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 64}), + ] + + +@triton.autotune( + configs=get_autotune_config(), + key=["M", "N", "K"], +) +@triton.jit +def matmul_kernel( + # Pointers to matrices + a_ptr, b_ptr, c_ptr, + # Matrix dimensions + M, N, K, + # The stride variables represent how much to increase the ptr by when moving by 1 + # element in a particular dimension. E.g. `stride_am` is how much to increase `a_ptr` + # by to get the element one row down (A has M rows). + stride_am, stride_ak, # + stride_bk, stride_bn, # + stride_cm, stride_cn, + # Meta-parameters + BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, # + ACTIVATION: tl.constexpr, # +): + """Kernel for computing the matmul C = A x B. + A has shape (M, K), B has shape (K, N) and C has shape (M, N) + """ + GROUP_SIZE_M: tl.constexpr = 1 + # ----------------------------------------------------------- + # Map program ids `pid` to the block of C it should compute. + # This is done in a grouped ordering to promote L2 data reuse. + # See above `L2 Cache Optimizations` section for details. + pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + # ---------------------------------------------------------- + # Create pointers for the first blocks of A and B. + # We will advance this pointer as we move in the K direction + # and accumulate + # `a_ptrs` is a block of [BLOCK_SIZE_M, BLOCK_SIZE_K] pointers + # `b_ptrs` is a block of [BLOCK_SIZE_K, BLOCK_SIZE_N] pointers + # See above `Pointer Arithmetic` section for details + offs_am = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + offs_k = tl.arange(0, BLOCK_SIZE_K) + a_ptrs_base = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) + b_ptrs_base = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) + msk_m = offs_am < M + msk_n = offs_bn < N + # ----------------------------------------------------------- + # Iterate to compute a block of the C matrix. + # We accumulate into a `[BLOCK_SIZE_M, BLOCK_SIZE_N]` block + # of fp32 values for higher accuracy. + # `accumulator` will be converted back to fp16 after the loop. + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)): + # Load the next block of A and B, generate a mask by checking the K dimension. + # If it is out of bounds, set it to 0. + a_ptrs = a_ptrs_base + k * BLOCK_SIZE_K * stride_ak + b_ptrs = b_ptrs_base + k * BLOCK_SIZE_K * stride_bk + a = tl.load( + a_ptrs, + mask=msk_m[:, None] and (offs_k[None, :] < K - k * BLOCK_SIZE_K), + other=0.0, + ) + b = tl.load( + b_ptrs, + mask=msk_n[None, :] and (offs_k[:, None] < K - k * BLOCK_SIZE_K), + other=0.0, + ) + # We accumulate along the K dimension. + accumulator = tl.dot(a, b, accumulator) + # You can fuse arbitrary activation functions here + # while the accumulator is still in FP32! + # Original vector operations + # # ----------------------------------------------------------- + # # Write back the block of the output matrix C with masks. + # Comment out the following lines to enable split the workload to two vector cores + SUB_BLK_M: tl.constexpr = BLOCK_SIZE_M // 2 + for s in range(0, 2): + vec_sub_blk = extension.extract_slice(accumulator, (s * SUB_BLK_M, 0), (SUB_BLK_M, BLOCK_SIZE_N), (1, 1)) + if ACTIVATION == "leaky_relu_custom": + vec_sub_blk = leaky_relu_custom(vec_sub_blk) + c_sub_blk = vec_sub_blk.to(tl.float16) + # ----------------------------------------------------------- + # Write back the block of the output matrix C with masks. + offs_cm = pid_m * BLOCK_SIZE_M + s * SUB_BLK_M + tl.arange(0, SUB_BLK_M) + offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] + c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N) + tl.store(c_ptrs, c_sub_blk, mask=c_mask) + + +# We can fuse `leaky_relu_custom` by providing it as an `ACTIVATION` meta-parameter in `matmul_kernel`. +@triton.jit +def leaky_relu_custom(x): + return tl.where(x >= 0, x, 0.01 * x) + 1.0 + + +def torch_matmul(a, b, activation=""): + c = torch.matmul(a, b) + if activation == "leaky_relu_custom": + c = torch.where(c >= 0, c, 0.01 * c) + 1.0 + return c + + +# %% +# We can now create a convenience wrapper function that only takes two input tensors, +# and (1) checks any shape constraint; (2) allocates the output; (3) launches the above kernel. + + +def matmul(a, b, activation=""): + # Check constraints. + assert a.shape[1] == b.shape[0], "Incompatible dimensions" + assert a.is_contiguous(), "Matrix A must be contiguous" + M, K = a.shape + K, N = b.shape + # Allocates output. + c = torch.empty((M, N), device=a.device, dtype=torch.float16) + + # 1D launch kernel where each block gets its own program. + + def grid(META): + return (triton.cdiv(M, META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]), ) + + matmul_kernel[grid]( + a, b, c, # + M, N, K, # + a.stride(0), a.stride(1), # + b.stride(0), b.stride(1), # + c.stride(0), c.stride(1), # + ACTIVATION=activation, # + ) + return c + + +# %% +# Unit Test +# --------- +# +# We can test our custom matrix multiplication operation against a native torch implementation (i.e., cuBLAS). + + +def test_matrix_multiplication(): + activation = "" + shape = (512, 512, 512) + m, k, n = shape + torch.manual_seed(0) + a = torch.randn((m, k), device=DEV, dtype=torch.float16) + b = torch.randn((k, n), device=DEV, dtype=torch.float16) + triton_output = matmul(a, b, activation) + torch_output = torch_matmul(a, b, activation) + print(f"triton_output(activation={activation}, shape={shape})={triton_output}") + print(f"torch_output(activation={activation}, shape={shape})={torch_output}") + torch.testing.assert_close(triton_output, torch_output, atol=1e-3, rtol=1e-3) + + +if __name__ == "__main__": + test_matrix_multiplication() + print("======Matrix Multiplication Test Passed!======") diff --git a/third_party/ascend/tutorials/04-low-memory-dropout.py b/third_party/ascend/tutorials/04-low-memory-dropout.py new file mode 100644 index 0000000000..9cbd5db876 --- /dev/null +++ b/third_party/ascend/tutorials/04-low-memory-dropout.py @@ -0,0 +1,129 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Low-Memory Dropout +================== +""" + +import tabulate +import torch +import torch_npu + +import triton +import triton.language as tl + +DEV = "npu" + + +@triton.jit +def _dropout( + x_ptr, # pointer to the input + x_keep_ptr, # pointer to a mask of 0s and 1s + output_ptr, # pointer to the output + n_elements, # number of elements in the `x` tensor + p, # probability that an element of `x` is changed to zero + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + # Load data + x = tl.load(x_ptr + offsets, mask=mask) + x_keep = tl.load(x_keep_ptr + offsets, mask=mask) + # The line below is the crucial part, described in the paragraph above! + output = tl.where(x_keep != 0, x / (1 - p), 0.0) + # Write-back output + tl.store(output_ptr + offsets, output, mask=mask) + + +def dropout(x, x_keep, p): + output = torch.empty_like(x) + assert x.is_contiguous() + n_elements = x.numel() + + def grid(meta): + return (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + _dropout[grid](x, x_keep, output, n_elements, p, BLOCK_SIZE=1024) + return output + + +@triton.jit +def _seeded_dropout( + x_ptr, + output_ptr, + n_elements, + p, + seed, + BLOCK_SIZE: tl.constexpr, +): + # compute memory offsets of elements handled by this instance + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + # load data from x + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + # randomly prune it + random = tl.rand(seed, offsets) + x_keep = random > p + # write-back + output = tl.where(x_keep, x / (1 - p), 0.0) + tl.store(output_ptr + offsets, output, mask=mask) + + +def seeded_dropout(x, p, seed): + output = torch.empty_like(x) + assert x.is_contiguous() + n_elements = x.numel() + + def grid(meta): + return (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + _seeded_dropout[grid](x, output, n_elements, p, seed, BLOCK_SIZE=1024) + return output + + +def test_dropout_matches_reference(): + shape, p = (256, ), 0.5 + torch.manual_seed(0) + x = torch.randn(size=shape, device=DEV, dtype=torch.float32) + x_keep = (torch.rand(size=shape, device=DEV) > p).to(torch.int32) + output = dropout(x, x_keep=x_keep, p=p) + expected = torch.where(x_keep != 0, x / (1 - p), torch.zeros_like(x)) + torch.testing.assert_close(output, expected, atol=1e-6, rtol=0) + # Demo-style print for dropout + x_demo = torch.randn(size=(10, ), device=DEV) + x_keep_demo = (torch.rand(size=(10, ), device=DEV) > 0.5).to(torch.int32) + output_demo = dropout(x_demo, x_keep=x_keep_demo, p=0.5) + print( + tabulate.tabulate([ + ["input"] + x_demo.tolist(), + ["keep mask"] + x_keep_demo.tolist(), + ["output"] + output_demo.tolist(), + ])) + + +if __name__ == "__main__": + test_dropout_matches_reference() + print("======Low Memory Dropout Test Passed!======") diff --git a/third_party/ascend/tutorials/05-layer-norm.py b/third_party/ascend/tutorials/05-layer-norm.py new file mode 100644 index 0000000000..515df84a43 --- /dev/null +++ b/third_party/ascend/tutorials/05-layer-norm.py @@ -0,0 +1,122 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Layer Normalization +============= +""" + +import torch +import triton +import triton.language as tl +import torch_npu + + +@triton.jit +def _layer_norm_fwd_fused( + X, # pointer to the input + Y, # pointer to the output + W, # pointer to the weights + B, # pointer to the biases + Mean, # pointer to the mean + Rstd, # pointer to the 1/std + stride, # how much to increase the pointer when moving by 1 row + N, # number of columns in X + eps, # epsilon to avoid division by zero + BLOCK_SIZE: tl.constexpr, +): + # Map the program id to the row of X and Y it should compute. + row = tl.program_id(0) + Y += row * stride + X += row * stride + # Compute mean + mean = 0 + _mean = tl.zeros([BLOCK_SIZE], dtype=tl.float32) + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + a = tl.load(X + cols, mask=cols < N, other=0.).to(tl.float32) + _mean += a + mean = tl.sum(_mean, axis=0) / N + # Compute variance + _var = tl.zeros([BLOCK_SIZE], dtype=tl.float32) + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + x = tl.load(X + cols, mask=cols < N, other=0.).to(tl.float32) + x = tl.where(cols < N, x - mean, 0.) + _var += x * x + var = tl.sum(_var, axis=0) / N + rstd = 1 / tl.sqrt(var + eps) + # Write mean / rstd + tl.store(Mean + row, mean) + tl.store(Rstd + row, rstd) + # Normalize and apply linear transformation + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + mask = cols < N + w = tl.load(W + cols, mask=mask) + b = tl.load(B + cols, mask=mask) + x = tl.load(X + cols, mask=mask, other=0.).to(tl.float32) + x_hat = (x - mean) * rstd + y = x_hat * w + b + # Write output + tl.store(Y + cols, y, mask=mask) + + +@torch.inference_mode() +def layer_norm(x, normalized_shape, weight, bias, eps=1e-5): + # allocate output + y = torch.empty_like(x) + # reshape input data into 2D tensor + x_arg = x.reshape(-1, x.shape[-1]) + M, N = x_arg.shape + mean = torch.empty((M, ), dtype=torch.float32, device=x.device) + rstd = torch.empty((M, ), dtype=torch.float32, device=x.device) + # Less than 64KB per feature: enqueue fused kernel + MAX_FUSED_SIZE = 65536 // x.element_size() + BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) + # heuristics for number of warps + num_warps = min(max(BLOCK_SIZE // 256, 1), 8) + # enqueue kernel + kernel = _layer_norm_fwd_fused[(M, )]( # + x_arg, y, weight, bias, mean, rstd, # + x_arg.stride(0), N, eps, # + BLOCK_SIZE=BLOCK_SIZE, num_warps=num_warps, num_ctas=1) + return y + + +def test_layer_norm(): + dtype = torch.float16 + M, N = 128, 128 + eps = 1e-5 + device = 'npu' + x_shape = (M, N) + w_shape = (x_shape[-1], ) + weight = torch.rand(w_shape, dtype=dtype, device=device) + bias = torch.rand(w_shape, dtype=dtype, device=device) + x = -2.3 + 0.5 * torch.randn(x_shape, dtype=dtype, device=device) + y_tri = layer_norm(x, w_shape, weight, bias, eps) + y_ref = torch.nn.functional.layer_norm(x, w_shape, weight, bias, eps).to(dtype) + assert torch.allclose(y_tri, y_ref, atol=1e-2, rtol=0) + print(f"y_tri: {y_tri}") + print(f"y_ref: {y_ref}") + + +if __name__ == "__main__": + test_layer_norm() + print("======Layer Normalization Test Passed!======") diff --git a/third_party/ascend/tutorials/06-fused-attention.py b/third_party/ascend/tutorials/06-fused-attention.py new file mode 100644 index 0000000000..c9645cc596 --- /dev/null +++ b/third_party/ascend/tutorials/06-fused-attention.py @@ -0,0 +1,319 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Fused Attention +=============== + +This is a Triton implementation of the Flash Attention v2 algorithm from Tri Dao (https://tridao.me/publications/flash2/flash2.pdf) + +Credits: OpenAI kernel team + +Extra Credits: + +* Original flash attention paper (https://arxiv.org/abs/2205.14135) +* Rabe and Staats (https://arxiv.org/pdf/2112.05682v2.pdf) + +""" + +import torch +import torch_npu +import triton +import triton.language as tl +import triton.language.extra.cann.extension as extension + +DEVICE = "npu" + + +@triton.jit +def _attn_fwd_inner(acc_ptr, l_i, m_i, q, # Accumulator, local l, local m, query vector + K_block_ptr, V_block_ptr, # Key and value block pointers for current stage + start_m, qk_scale, # Starting position of current query block, qk scale factor + BLOCK_M: tl.constexpr, HEAD_DIM: tl.constexpr, BLOCK_N: tl.constexpr, # Block size constants + STAGE: tl.constexpr, offs_m: tl.constexpr, + offs_n: tl.constexpr, # Current stage flag, m and n offset indices + N_CTX: tl.constexpr, + fp8_v: tl.constexpr): # Total context length, whether to enable FP8 for value precision + # Set the processing range [lo, hi) for the current stage (in column block units) + # Causal attention, as the name implies, restricts the flow of information during computation, + # only allowing the model to see the current and previous positions. + # In other words, the output at the current position can only depend on the input at or before this position, + # and cannot access information from future positions. + # Causal attention ensures sequential order and prevents "leakage of future information." + # But the following logic will also be triggered + if STAGE == 1: + # Stage 1: process all tokens before the query block + tl.static_assert(BLOCK_M >= BLOCK_N) + lo, hi = 0, start_m * BLOCK_M + elif STAGE == 2: + # Stage 2: process the current query block + tl.static_assert(BLOCK_M >= BLOCK_N) + lo, hi = start_m * BLOCK_M, (start_m + 1) * BLOCK_M + lo = tl.multiple_of(lo, BLOCK_M) # Align starting position + # causal = False (no need for masking) + else: + lo, hi = 0, N_CTX # Process the entire context + + # Adjust K and V block pointers to the starting position `lo` + K_block_ptr = tl.advance(K_block_ptr, (lo, 0)) # K is [HEAD_DIM, N_CTX], shift along the second dim by lo + V_block_ptr = tl.advance(V_block_ptr, (lo, 0)) # V is [N_CTX, HEAD_DIM], shift along the first dim by lo + + # Index mapping for the accumulator , used for slicing when HEAD_DIM >= 256 + row = tl.arange(0, BLOCK_M)[:, None] + col_head_dim = tl.arange(0, HEAD_DIM)[None, :] + block2d_acc = row * HEAD_DIM + col_head_dim + + # Iterate over all k, v blocks in the current stage and accumulate the output + for start_n in range(lo, hi, BLOCK_N): # Process BLOCK_N columns at a time + start_n = tl.multiple_of(start_n, BLOCK_N) # Align column start position + # -- Compute qk ---- + k = tl.load(K_block_ptr) + # Modify K + trans_k = tl.trans(k) + qk = tl.dot(q, trans_k) + # Apply causal mask for STAGE 2 + if STAGE == 2: + mask = offs_m[:, None] >= (start_n + offs_n[None, :]) # Construct upper triangular mask + qk = qk * qk_scale + tl.where(mask, 0, -1.0e6) # Set invalid positions to -∞ + m_ij = tl.maximum(m_i, tl.max(qk, 1)) # Update m_ij = max(m_i, max(qk)) + qk -= m_ij[:, None] # Subtract max for softmax stability + else: + qk = qk * qk_scale + m_ij = tl.maximum(m_i, tl.max(qk, 1)) # Scaled max + qk = qk - m_ij[:, None] # Stabilize + + # Softmax weights p = exp(qk) + p = tl.math.exp(qk) + + # Convert softmax weight type depending on FP8 usage + if fp8_v: + p_cast = p.to(tl.float8e5) # Convert to FP8 format (save memory) + else: + p_cast = p.to(k.dtype) + + v = tl.load(V_block_ptr) # Load corresponding V block + pv = tl.dot(p_cast, v) + l_ij = tl.sum(p, 1) # Softmax denominator (sum of each row) + # -- Update m_i and l_i + alpha = tl.math.exp(m_i - m_ij) # Update factor: exp difference between old and new max + l_i = l_i * alpha + l_ij # Update softmax denominator + # -- Update output accumulator -- + if HEAD_DIM < 256: + acc_ptr = acc_ptr * alpha[:, None] + acc_ptr = tl.dot(p_cast, v, acc_ptr) + else: + # 1. Load current slice of accumulator + acc = tl.load(acc_ptr + block2d_acc) + # 2. Update in slices (split by 1/4 of BLOCK_M to avoid ub overflow) + for i in range(4): + # Calculate start/end rows for current slice + offset = i * (BLOCK_M // 4) + # Extract slice data + acc_i = extension.extract_slice(acc, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + alpha_i = extension.extract_slice(alpha, [offset], [BLOCK_M // 4], [1]) + pv_i = extension.extract_slice(pv, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + # Incrementally update slice: acc = acc * alpha + pv + acc_i = acc_i * alpha_i[:, None] + pv_i + # Write updated slice back to accumulator + acc = extension.insert_slice(acc, acc_i, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + # 3. updated accumulator + tl.store(acc_ptr + block2d_acc, acc) + + m_i = m_ij # Update current block max + # Advance V and K block pointers to next BLOCK_N range + V_block_ptr = tl.advance(V_block_ptr, (BLOCK_N, 0)) + K_block_ptr = tl.advance(K_block_ptr, (BLOCK_N, 0)) + # Return accumulated output acc_ptr, softmax denominator l_i, and max value m_i + return acc_ptr, l_i, m_i + + +@triton.jit +def _attn_fwd(Q, K, V, M, Out, acc, sm_scale, stride_qz: tl.constexpr, stride_qh: tl.constexpr, stride_qm: tl.constexpr, + stride_qk: tl.constexpr, stride_kz: tl.constexpr, stride_kh: tl.constexpr, stride_kn: tl.constexpr, + stride_kk: tl.constexpr, stride_vz: tl.constexpr, stride_vh: tl.constexpr, stride_vn: tl.constexpr, + stride_vk: tl.constexpr, stride_oz: tl.constexpr, stride_oh: tl.constexpr, stride_om: tl.constexpr, + stride_on: tl.constexpr, Z: tl.constexpr, H: tl.constexpr, N_CTX: tl.constexpr, HEAD_DIM: tl.constexpr, + BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, STAGE: tl.constexpr): + # Total number of blocks in sequence dimension (M) + NUM_BLOCKS_M = N_CTX // BLOCK_M + # Total tasks = number of sequence blocks × batch size (Z) × number of attention heads (H) + NUM_BLOCKS = NUM_BLOCKS_M * Z * H + + # Current M-dimension block index + pid = tl.program_id(0) + + for block_idx in range(pid, NUM_BLOCKS, 20): + task_hz_idx = block_idx // NUM_BLOCKS_M + task_m_idx = block_idx % NUM_BLOCKS_M + off_z = task_hz_idx // H + off_h = task_hz_idx % H + qvk_offset = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh + # Create block pointers for Q, K, V, Output + Q_block_ptr = tl.make_block_ptr( + base=Q + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_qm, stride_qk), + offsets=(task_m_idx * BLOCK_M, 0), + block_shape=(BLOCK_M, HEAD_DIM), + order=(1, 0), + ) + V_block_ptr = tl.make_block_ptr( + base=V + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_vn, stride_vk), + offsets=(0, 0), + block_shape=(BLOCK_N, HEAD_DIM), + order=(1, 0), + ) + K_block_ptr = tl.make_block_ptr( + base=K + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_kn, stride_kk), + offsets=(0, 0), + block_shape=(BLOCK_N, HEAD_DIM), + order=(1, 0), + ) + O_block_ptr = tl.make_block_ptr( + base=Out + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_om, stride_on), + offsets=(task_m_idx * BLOCK_M, 0), + block_shape=(BLOCK_M, HEAD_DIM), + order=(1, 0), + ) + # Initialize offsets + offs_m = task_m_idx * BLOCK_M + tl.arange(0, BLOCK_M) + offs_n = tl.arange(0, BLOCK_N) + + m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf") + l_i = tl.zeros([BLOCK_M], dtype=tl.float32) + 1.0 + + # Initialize accumulator + if HEAD_DIM < 256: + acc_ptr = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32) + else: + acc_offset = (off_z.to(tl.int64) * stride_qz // stride_qm * HEAD_DIM + + off_h.to(tl.int64) * stride_qh // stride_qm * HEAD_DIM + task_m_idx * BLOCK_M * HEAD_DIM) + acc_ptr = acc + acc_offset + + # load q: it will stay in SRAM throughout + q = tl.load(Q_block_ptr) + + # stage 1: off-band + # For causal = True, STAGE = 3 and _attn_fwd_inner gets 1 as its STAGE + # For causal = False, STAGE = 1, and _attn_fwd_inner gets 3 as its STAGE + if STAGE & 1: + acc_ptr, l_i, m_i = _attn_fwd_inner(acc_ptr, l_i, m_i, q, K_block_ptr, V_block_ptr, # + task_m_idx, sm_scale, # + BLOCK_M, HEAD_DIM, BLOCK_N, # + 4 - STAGE, offs_m, offs_n, N_CTX, V.dtype.element_ty == tl.float8e5 # + ) + # stage 2: on-band + if STAGE & 2: + # barrier makes it easier for compielr to schedule the + # two loops independently + acc_ptr, l_i, m_i = _attn_fwd_inner(acc_ptr, l_i, m_i, q, K_block_ptr, V_block_ptr, # + task_m_idx, sm_scale, # + BLOCK_M, HEAD_DIM, BLOCK_N, # + 2, offs_m, offs_n, N_CTX, V.dtype.element_ty == tl.float8e5 # + ) + + m_i += tl.math.log(l_i) + if HEAD_DIM < 256: + accumulator = acc_ptr / l_i[:, None] + else: + row = tl.arange(0, BLOCK_M)[:, None] + col_head_dim = tl.arange(0, HEAD_DIM)[None, :] + block2d_acc = row * HEAD_DIM + col_head_dim + accumulator = tl.load(acc_ptr + block2d_acc) + accumulator = accumulator / l_i[:, None] + + m_ptrs = M + task_hz_idx * N_CTX + offs_m + + tl.store(m_ptrs, m_i) + tl.store(O_block_ptr, accumulator.to(Out.type.element_ty)) + + +class _attention(torch.autograd.Function): + + @staticmethod + def forward(ctx, q, k, v, causal, sm_scale, BM, BN): + """ + Forward computation interface: + Args: + ctx: Context object + q: Query tensor (Q), shape [Z, H, N_CTX, HEAD_DIM] + k: Key tensor (K), shape [Z, H, N_CTX, HEAD_DIM] + v: Value tensor (V), shape [Z, H, N_CTX, HEAD_DIM] + causal: Whether to enable causal attention + sm_scale: Scaling factor for QK product + BM: Q block size (BLOCK_M) + BN: K/V block size (BLOCK_N) + Returns: + out: Attention output tensor, shape [Z, H, N_CTX, HEAD_DIM] + """ + # shape constraints + HEAD_DIM_Q, HEAD_DIM_K = q.shape[-1], k.shape[-1] + # when v is in float8_e5m2 it is transposed. + HEAD_DIM_V = v.shape[-1] + assert HEAD_DIM_Q == HEAD_DIM_K and HEAD_DIM_K == HEAD_DIM_V + assert HEAD_DIM_K in {16, 32, 64, 128, 256} + + out = torch.empty_like(q) + stage = 3 if causal else 1 + extra_kern_args = {} + + # Number of NPU cores (adjust based on hardware) + num_cores = 20 + acc = torch.zeros((q.shape[0], q.shape[1], q.shape[2], HEAD_DIM_K), dtype=torch.float32, device=q.device) + M = torch.empty((q.shape[0], q.shape[1], q.shape[2]), device=q.device, dtype=torch.float32) + + _attn_fwd[(num_cores, )](q, k, v, M, out, acc, sm_scale, q.stride(0), q.stride(1), q.stride(2), q.stride(3), + k.stride(0), k.stride(1), k.stride(2), k.stride(3), v.stride(0), v.stride(1), + v.stride(2), v.stride(3), out.stride(0), out.stride(1), out.stride(2), out.stride(3), + q.shape[0], q.shape[1], N_CTX=q.shape[2], HEAD_DIM=HEAD_DIM_K, BLOCK_M=BM, BLOCK_N=BN, + STAGE=stage, **extra_kern_args) + + ctx.save_for_backward(q, k, v, out, M) + ctx.sm_scale = sm_scale + ctx.HEAD_DIM = HEAD_DIM_K + ctx.causal = causal + return out + + +attention = _attention.apply + + +def test_attention_fused(): + Z, H, N_CTX, HEAD_DIM, causal, dtype, BM, BN = 4, 32, 1024, 64, False, torch.bfloat16, 64, 128 + torch.manual_seed(20) + q = torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_() + k = torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_() + v = torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_() + sm_scale = 0.5 + tri_out = attention(q, k, v, causal, sm_scale, BM, BN) + ref_out = torch_npu.npu_fusion_attention(q, k, v, H, padding_mask=None, atten_mask=None, scale=sm_scale, + keep_prob=1.0, input_layout="BNSD", pre_tockens=65535, next_tockens=65535, + sparse_mode=0)[0] + torch.testing.assert_close(ref_out, tri_out, atol=1e-2, rtol=1e-2, equal_nan=True) + + +if __name__ == "__main__": + test_attention_fused() + print("======Fused Attention Test Passed!======") diff --git a/third_party/ascend/tutorials/07-extern-functions.py b/third_party/ascend/tutorials/07-extern-functions.py new file mode 100644 index 0000000000..af0c767658 --- /dev/null +++ b/third_party/ascend/tutorials/07-extern-functions.py @@ -0,0 +1,84 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Libdevice (`tl.extra.libdevice`) function +============================== +""" +import torch +import torch_npu + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice + +DEV = "npu" + + +@triton.jit +def asin_kernel( + x_ptr, + y_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + x = libdevice.asin(x) + tl.store(y_ptr + offsets, x, mask=mask) + + +def test_asin_kernel_matches_torch(): + size = 98432 + torch.manual_seed(0) + x = torch.rand(size, device=DEV) + output_triton = torch.empty_like(x) + output_torch = torch.asin(x) + n_elements = output_torch.numel() + + def grid(meta): + return (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + asin_kernel[grid](x, output_triton, n_elements, BLOCK_SIZE=1024) + torch.testing.assert_close(output_torch, output_triton, rtol=1e-4, atol=1e-4) + # Demo-style print + torch.manual_seed(0) + x_demo = torch.rand(98432, device=DEV) + output_triton_demo = torch.empty_like(x_demo) + output_torch_demo = torch.asin(x_demo) + n_elements = output_torch_demo.numel() + + def grid(meta): + return (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + asin_kernel[grid](x_demo, output_triton_demo, n_elements, BLOCK_SIZE=1024) + print(output_torch_demo) + print(output_triton_demo) + print(f'The maximum difference between torch and triton is ' + f'{torch.max(torch.abs(output_torch_demo - output_triton_demo))}') + + +if __name__ == "__main__": + test_asin_kernel_matches_torch() + print("======Extern Functions Test Passed!======") diff --git a/third_party/ascend/tutorials/08-grouped-gemm.py b/third_party/ascend/tutorials/08-grouped-gemm.py new file mode 100644 index 0000000000..7e63e31d58 --- /dev/null +++ b/third_party/ascend/tutorials/08-grouped-gemm.py @@ -0,0 +1,278 @@ +# Copyright (c) 2023 NVIDIA Corporation & Affiliates. All rights reserved. +# Copyright (c) Huawei Technologies Co., Ltd. 2025. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Group GEMM +============================ +""" + +import torch +import torch_npu + +import triton +import triton.language as tl +import triton.runtime.driver as driver + +DEV = "npu" + + +def get_npu_properties(): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device) + + +NUM_CORES = get_npu_properties()["num_aicore"] + + +@triton.autotune( + configs=[ + triton.Config({ + 'BLOCK_SIZE_M': 128, + 'BLOCK_SIZE_N': 128, + 'BLOCK_SIZE_K': 32, + 'NUM_SM': NUM_CORES, + }), + triton.Config({ + 'BLOCK_SIZE_M': 128, + 'BLOCK_SIZE_N': 128, + 'BLOCK_SIZE_K': 32, + 'NUM_SM': NUM_CORES, + }), + triton.Config({ + 'BLOCK_SIZE_M': 64, + 'BLOCK_SIZE_N': 64, + 'BLOCK_SIZE_K': 32, + 'NUM_SM': NUM_CORES, + }), + triton.Config({ + 'BLOCK_SIZE_M': 64, + 'BLOCK_SIZE_N': 64, + 'BLOCK_SIZE_K': 32, + 'NUM_SM': NUM_CORES, + }), + ], + key=['group_size'], +) +@triton.jit +def grouped_matmul_kernel( + group_a_ptrs, + group_b_ptrs, + group_c_ptrs, + group_gemm_sizes, + g_lds, + group_size, + NUM_SM: tl.constexpr, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + tile_idx = tl.program_id(0) + last_problem_end = 0 + for g in range(group_size): + gm = tl.load(group_gemm_sizes + g * 3) + gn = tl.load(group_gemm_sizes + g * 3 + 1) + gk = tl.load(group_gemm_sizes + g * 3 + 2) + num_m_tiles = tl.cdiv(gm, BLOCK_SIZE_M) + num_n_tiles = tl.cdiv(gn, BLOCK_SIZE_N) + num_tiles = num_m_tiles * num_n_tiles + while (tile_idx >= last_problem_end and tile_idx < last_problem_end + num_tiles): + k = gk + lda = tl.load(g_lds + g * 3) + ldb = tl.load(g_lds + g * 3 + 1) + ldc = tl.load(g_lds + g * 3 + 2) + a_ptr = tl.load(group_a_ptrs + g).to(tl.pointer_type(tl.float16)) + b_ptr = tl.load(group_b_ptrs + g).to(tl.pointer_type(tl.float16)) + c_ptr = tl.load(group_c_ptrs + g).to(tl.pointer_type(tl.float16)) + tile_idx_in_gemm = tile_idx - last_problem_end + tile_m_idx = tile_idx_in_gemm // num_n_tiles + tile_n_idx = tile_idx_in_gemm % num_n_tiles + + offs_am = tile_m_idx * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_bn = tile_n_idx * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + offs_k = tl.arange(0, BLOCK_SIZE_K) + a_ptrs = a_ptr + offs_am[:, None] * lda + offs_k[None, :] + b_ptrs = b_ptr + offs_k[:, None] * ldb + offs_bn[None, :] + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + for _ in range(0, tl.cdiv(k, BLOCK_SIZE_K)): + tl.multiple_of(a_ptrs, [16, 16]) + tl.multiple_of(b_ptrs, [16, 16]) + a = tl.load(a_ptrs) + b = tl.load(b_ptrs) + accumulator += tl.dot(a, b) + a_ptrs += BLOCK_SIZE_K + b_ptrs += BLOCK_SIZE_K * ldb + c = accumulator.to(tl.float16) + + offs_cm = tile_m_idx * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_cn = tile_n_idx * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + c_ptrs = c_ptr + ldc * offs_cm[:, None] + offs_cn[None, :] + + tl.store(c_ptrs, c) + tile_idx += NUM_SM + + last_problem_end = last_problem_end + num_tiles + + +def group_gemm_fn(group_A, group_B): + device = torch.device(DEV) + assert len(group_A) == len(group_B) + group_size = len(group_A) + + A_addrs = [] + B_addrs = [] + C_addrs = [] + g_sizes = [] + g_lds = [] + group_C = [] + for i in range(group_size): + A = group_A[i] + B = group_B[i] + assert A.shape[1] == B.shape[0] + M, K = A.shape + K, N = B.shape + C = torch.empty((M, N), device=device, dtype=A.dtype) + group_C.append(C) + A_addrs.append(A.data_ptr()) + B_addrs.append(B.data_ptr()) + C_addrs.append(C.data_ptr()) + g_sizes += [M, N, K] + g_lds += [A.stride(0), B.stride(0), C.stride(0)] + + d_a_ptrs = torch.tensor(A_addrs, device=device) + d_b_ptrs = torch.tensor(B_addrs, device=device) + d_c_ptrs = torch.tensor(C_addrs, device=device) + d_g_sizes = torch.tensor(g_sizes, dtype=torch.int32, device=device) + d_g_lds = torch.tensor(g_lds, dtype=torch.int32, device=device) + + def grid(meta): + return (meta['NUM_SM'], ) + + grouped_matmul_kernel[grid]( + d_a_ptrs, + d_b_ptrs, + d_c_ptrs, + d_g_sizes, + d_g_lds, + group_size, + ) + + return group_C + + +def triton_perf_fn(a_ptrs, b_ptrs, c_ptrs, sizes, lds, group_size): + + def grid(meta): + return (meta['NUM_SM'], ) + + grouped_matmul_kernel[grid](a_ptrs, b_ptrs, c_ptrs, sizes, lds, group_size) + + +def torch_perf_fn(group_A, group_B): + for a, b in zip(group_A, group_B): + torch.matmul(a, b) + + +def run_benchmark_case(N, provider): + group_size = 4 + group_A = [] + group_B = [] + group_C = [] + A_addrs = [] + B_addrs = [] + C_addrs = [] + g_sizes = [] + g_lds = [] + for _ in range(group_size): + A = torch.rand((N, N), device=DEV, dtype=torch.float16) + B = torch.rand((N, N), device=DEV, dtype=torch.float16) + C = torch.empty((N, N), device=DEV, dtype=torch.float16) + group_A.append(A) + group_B.append(B) + group_C.append(C) + A_addrs.append(A.data_ptr()) + B_addrs.append(B.data_ptr()) + C_addrs.append(C.data_ptr()) + g_sizes += [N, N, N] + g_lds += [N, N, N] + + d_a_ptrs = torch.tensor(A_addrs, device=DEV) + d_b_ptrs = torch.tensor(B_addrs, device=DEV) + d_c_ptrs = torch.tensor(C_addrs, device=DEV) + d_g_sizes = torch.tensor(g_sizes, dtype=torch.int32, device=DEV) + d_g_lds = torch.tensor(g_lds, dtype=torch.int32, device=DEV) + + quantiles = [0.5, 0.2, 0.8] + + def bench_torch(): + torch_perf_fn(group_A, group_B) + + def bench_triton(): + triton_perf_fn(d_a_ptrs, d_b_ptrs, d_c_ptrs, d_g_sizes, d_g_lds, group_size) + + if provider == 'torch': + ms, min_ms, max_ms = triton.testing.do_bench(bench_torch, quantiles=quantiles) + if provider == 'triton': + ms, min_ms, max_ms = triton.testing.do_bench(bench_triton, quantiles=quantiles) + assert ms >= 0 + assert min_ms >= 0 + assert max_ms >= 0 + + +def build_group_inputs(group_m, group_n, group_k): + assert len(group_m) == len(group_n) + assert len(group_n) == len(group_k) + + group_A = [] + group_B = [] + for m, n, k in zip(group_m, group_n, group_k): + group_A.append(torch.rand((m, k), device=DEV, dtype=torch.float16)) + group_B.append(torch.rand((k, n), device=DEV, dtype=torch.float16)) + return group_A, group_B + + +def run_group_gemm_case(group_m, group_n, group_k): + group_A, group_B = build_group_inputs(group_m, group_n, group_k) + + tri_out = group_gemm_fn(group_A, group_B) + ref_out = [torch.matmul(a, b) for a, b in zip(group_A, group_B)] + + assert len(tri_out) == len(ref_out) + for tri_tensor, ref_tensor, m, n in zip(tri_out, ref_out, group_m, group_n): + assert tri_tensor.shape == (m, n) + assert tri_tensor.dtype == torch.float16 + torch.testing.assert_close(ref_tensor, tri_tensor, atol=1e-2, rtol=1e-3) + + +def test_grouped_gemm_tutorial_example(): + group_m = [1024, 512, 256, 128] + group_n = [1024, 512, 256, 128] + group_k = [1024, 512, 256, 128] + group_A, group_B = build_group_inputs(group_m, group_n, group_k) + tri_out = group_gemm_fn(group_A, group_B) + ref_out = [torch.matmul(a, b) for a, b in zip(group_A, group_B)] + for tri_tensor, ref_tensor, m, n in zip(tri_out, ref_out, group_m, group_n): + assert tri_tensor.shape == (m, n) + assert tri_tensor.dtype == torch.float16 + torch.testing.assert_close(ref_tensor, tri_tensor, atol=1e-3, rtol=1e-3) + + +if __name__ == "__main__": + test_grouped_gemm_tutorial_example() + print("======Grouped GEMM Test Passed!======") diff --git a/third_party/ascend/tutorials/09-persistent-matmul.py b/third_party/ascend/tutorials/09-persistent-matmul.py new file mode 100644 index 0000000000..78fba87a75 --- /dev/null +++ b/third_party/ascend/tutorials/09-persistent-matmul.py @@ -0,0 +1,329 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Persistent Matmul +===================== +""" + +import argparse +import time + +import torch +import torch_npu +import triton +import triton.language as tl +import triton.runtime.driver as driver + +DEV = "npu" +DTYPE = torch.float16 + + +def get_npu_properties(): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device) + + +def get_num_compute_cores(): + return get_npu_properties()["num_aicore"] + + +def _matmul_launch_metadata(grid, kernel, args): + ret = {} + M, N, K = args["M"], args["N"], args["K"] + bytes_per_elem = args["c_ptr"].element_size() + ret["name"] = f"{kernel.name} [M={M}, N={N}, K={K}]" + ret[f"flops{bytes_per_elem * 8}"] = 2.0 * M * N * K + ret["bytes"] = bytes_per_elem * (M * K + N * K + M * N) + return ret + + +@triton.jit(launch_metadata=_matmul_launch_metadata) +def matmul_kernel( + a_ptr, + b_ptr, + c_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_cn, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + GROUP_SIZE_M: tl.constexpr, +): + pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + (pid % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + start_m = pid_m * BLOCK_SIZE_M + start_n = pid_n * BLOCK_SIZE_N + + offs_am = start_m + tl.arange(0, BLOCK_SIZE_M) + offs_bn = start_n + tl.arange(0, BLOCK_SIZE_N) + offs_am = tl.where(offs_am < M, offs_am, 0) + offs_bn = tl.where(offs_bn < N, offs_bn, 0) + + offs_am = tl.max_contiguous(tl.multiple_of(offs_am, BLOCK_SIZE_M), BLOCK_SIZE_M) + offs_bn = tl.max_contiguous(tl.multiple_of(offs_bn, BLOCK_SIZE_N), BLOCK_SIZE_N) + offs_k = tl.arange(0, BLOCK_SIZE_K) + a_ptrs = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) + b_ptrs = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) + + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + + for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)): + a = tl.load(a_ptrs, mask=offs_k[None, :] < K - k * BLOCK_SIZE_K, other=0.0) + b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_SIZE_K, other=0.0) + accumulator = tl.dot(a, b, accumulator) + a_ptrs += BLOCK_SIZE_K * stride_ak + b_ptrs += BLOCK_SIZE_K * stride_bk + + c = accumulator.to(tl.float16) + offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] + c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N) + tl.store(c_ptrs, c, mask=c_mask) + + +@triton.jit(launch_metadata=_matmul_launch_metadata) +def matmul_kernel_persistent( + a_ptr, + b_ptr, + c_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_cn, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + GROUP_SIZE_M: tl.constexpr, + NUM_SMS: tl.constexpr, +): + start_pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + k_tiles = tl.cdiv(K, BLOCK_SIZE_K) + num_tiles = num_pid_m * num_pid_n + + tiles_per_sm = num_tiles // NUM_SMS + if start_pid < num_tiles % NUM_SMS: + tiles_per_sm += 1 + + tile_id = start_pid - NUM_SMS + ki = -1 + offs_k_for_mask = tl.arange(0, BLOCK_SIZE_K) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + + pid_m = 0 + pid_n = 0 + offs_am = tl.arange(0, BLOCK_SIZE_M) + offs_bn = tl.arange(0, BLOCK_SIZE_N) + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + + for _ in range(0, k_tiles * tiles_per_sm): + ki = tl.where(ki == k_tiles - 1, 0, ki + 1) + if ki == 0: + tile_id += NUM_SMS + group_id = tile_id // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + (tile_id % group_size_m) + pid_n = (tile_id % num_pid_in_group) // group_size_m + + start_m = pid_m * BLOCK_SIZE_M + start_n = pid_n * BLOCK_SIZE_N + offs_am = start_m + tl.arange(0, BLOCK_SIZE_M) + offs_bn = start_n + tl.arange(0, BLOCK_SIZE_N) + offs_am = tl.where(offs_am < M, offs_am, 0) + offs_bn = tl.where(offs_bn < N, offs_bn, 0) + offs_am = tl.max_contiguous(tl.multiple_of(offs_am, BLOCK_SIZE_M), BLOCK_SIZE_M) + offs_bn = tl.max_contiguous(tl.multiple_of(offs_bn, BLOCK_SIZE_N), BLOCK_SIZE_N) + + offs_k = ki * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K) + a_ptrs = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) + b_ptrs = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) + + a = tl.load(a_ptrs, mask=offs_k_for_mask[None, :] < K - ki * BLOCK_SIZE_K, other=0.0) + b = tl.load(b_ptrs, mask=offs_k_for_mask[:, None] < K - ki * BLOCK_SIZE_K, other=0.0) + accumulator = tl.dot(a, b, accumulator) + + if ki == k_tiles - 1: + offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] + c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N) + c = accumulator.to(tl.float16) + tl.store(c_ptrs, c, mask=c_mask) + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + + +def get_configs(dtype): + return { + torch.float16: { + "BLOCK_SIZE_M": 64, + "BLOCK_SIZE_N": 128, + "BLOCK_SIZE_K": 32, + "GROUP_SIZE_M": 4, + "num_stages": 2, + "num_warps": 8, + } + }[dtype] + + +def matmul(a, b): + assert a.shape[1] == b.shape[0], "Incompatible dimensions" + assert a.dtype == b.dtype, "Incompatible dtypes" + M, K = a.shape + _, N = b.shape + configs = get_configs(a.dtype) + c = torch.empty((M, N), device=a.device, dtype=a.dtype) + + def grid(meta): + return (triton.cdiv(M, meta["BLOCK_SIZE_M"]) * triton.cdiv(N, meta["BLOCK_SIZE_N"]), ) + + matmul_kernel[grid]( + a, + b, + c, + M, + N, + K, + a.stride(0), + a.stride(1), + b.stride(0), + b.stride(1), + c.stride(0), + c.stride(1), + BLOCK_SIZE_M=configs["BLOCK_SIZE_M"], + BLOCK_SIZE_N=configs["BLOCK_SIZE_N"], + BLOCK_SIZE_K=configs["BLOCK_SIZE_K"], + GROUP_SIZE_M=configs["GROUP_SIZE_M"], + num_stages=configs["num_stages"], + num_warps=configs["num_warps"], + ) + return c + + +def matmul_persistent(a, b): + assert a.shape[1] == b.shape[0], "Incompatible dimensions" + assert a.dtype == b.dtype, "Incompatible dtypes" + num_sms = get_num_compute_cores() + M, K = a.shape + _, N = b.shape + configs = get_configs(a.dtype) + c = torch.empty((M, N), device=a.device, dtype=a.dtype) + + def grid(meta): + return (min(num_sms, triton.cdiv(M, meta["BLOCK_SIZE_M"]) * triton.cdiv(N, meta["BLOCK_SIZE_N"])), ) + + matmul_kernel_persistent[grid]( + a, + b, + c, + M, + N, + K, + a.stride(0), + a.stride(1), + b.stride(0), + b.stride(1), + c.stride(0), + c.stride(1), + BLOCK_SIZE_M=configs["BLOCK_SIZE_M"], + BLOCK_SIZE_N=configs["BLOCK_SIZE_N"], + BLOCK_SIZE_K=configs["BLOCK_SIZE_K"], + GROUP_SIZE_M=configs["GROUP_SIZE_M"], + NUM_SMS=num_sms, + num_stages=configs["num_stages"], + num_warps=configs["num_warps"], + ) + return c + + +def torch_matmul(a, b): + return torch.matmul(a, b) + + +def bench(K, reps=10): + M = 8192 + N = 8192 + a = torch.randn((M, K), device=DEV, dtype=DTYPE) + b = torch.randn((K, N), device=DEV, dtype=DTYPE) + + for _ in range(reps): + _ = torch_matmul(a, b) + time.sleep(0.01) + for _ in range(reps): + _ = matmul(a, b) + time.sleep(0.01) + for _ in range(reps): + _ = matmul_persistent(a, b) + time.sleep(0.01) + + +def validate(M, N, K): + a = torch.randn((M, K), device=DEV, dtype=DTYPE) + b = torch.randn((K, N), device=DEV, dtype=DTYPE) + + torch_result = torch_matmul(a, b) + naive_result = matmul(a, b) + persistent_result = matmul_persistent(a, b) + + naive_vs_torch = "✅" if torch.allclose(naive_result, torch_result, atol=1.0) else "❌" + persistent_vs_torch = "✅" if torch.allclose(persistent_result, torch_result, atol=1.0) else "❌" + naive_vs_persistent = "✅" if torch.allclose(naive_result, persistent_result, atol=1.0) else "❌" + + print(f"M={M}, N={N}, K={K} verification naive vs torch: {naive_vs_torch} " + f"persistent vs torch: {persistent_vs_torch} naive vs persistent: {naive_vs_persistent}") + return torch_result, naive_result, persistent_result + + +def test_persistent_matmul_validate_cases(): + M, N, K = 32, 32, 32 + torch.manual_seed(0) + torch_result, naive_result, persistent_result = validate(M, N, K) + torch.testing.assert_close(naive_result, torch_result, atol=1.0, rtol=0) + torch.testing.assert_close(persistent_result, torch_result, atol=1.0, rtol=0) + torch.testing.assert_close(naive_result, persistent_result, atol=1.0, rtol=0) + + +if __name__ == "__main__": + test_persistent_matmul_validate_cases() + print("======Persistent MatMul Test Passed!======") diff --git a/third_party/ascend/tutorials/10-gather-2d-simd.py b/third_party/ascend/tutorials/10-gather-2d-simd.py new file mode 100644 index 0000000000..b29ddde7c1 --- /dev/null +++ b/third_party/ascend/tutorials/10-gather-2d-simd.py @@ -0,0 +1,108 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Gather kernel optimized for Ascend NPU. +""" + +import triton +import triton.language as tl +import torch + + +@triton.jit +def gather_2d_simd(src_ptr, idx_ptr, out_ptr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, XBLOCK: tl.constexpr, + XBLOCK_SUB: tl.constexpr): + """ + 2D gather kernel for axis=1 (tail axis) with SIMD-style vectorization. + + This kernel is optimized for Ascend NPU architecture with focus on: + - Vectorized memory access using XBLOCK_SUB + - Efficient global memory (GM) access patterns + - Suitable for cases where N and K are not extremely large + + Args: + src_ptr: [M, N] source tensor in GM (Global Memory) + idx_ptr: [M, K] indices tensor in GM + out_ptr: [M, K] output tensor in GM + M: batch dimension size + N: source dimension size (gather from this dimension) + K: output dimension size (number of indices per batch) + XBLOCK: outer block size for M dimension (for program distribution) + XBLOCK_SUB: inner block size for M dimension (for SIMD vectorization) + + Example: + import torch + import triton + from third_party.ascend.language.kernels import gather_2d_simd + + M, N, K = 128, 256, 64 + src = torch.randn(M, N, device='npu') + indices = torch.randint(0, N, (M, K), dtype=torch.int32, device='npu') + output = torch.empty((M, K), dtype=src.dtype, device='npu') + + grid = (triton.cdiv(M, 32),) + gather_2d_simd[grid](src, indices, output, M, N, K, + XBLOCK=32, XBLOCK_SUB=4) + """ + pid = tl.program_id(0) + m_start = pid * XBLOCK + m_end = min(m_start + XBLOCK, M) + m_base = tl.arange(0, XBLOCK_SUB) + + # Process multiple rows at once using XBLOCK_SUB for vectorization + for m_tile_start in range(m_start, m_end, XBLOCK_SUB): + # M dimension offsets: [XBLOCK_SUB] + m_offs = m_tile_start + m_base + m_mask = m_offs < M + + # Load indices: [XBLOCK_SUB, K] + k_offs = tl.arange(0, K) + idx_tile = tl.load(idx_ptr + m_offs[:, None] * K + k_offs[None, :]) + + # Load source data: [XBLOCK_SUB, N] + n_offs = tl.arange(0, N) + src_tile = tl.load(src_ptr + m_offs[:, None] * N + n_offs[None, :]) + + # Gather operation along axis=1 + gathered_values = tl.gather(src_tile, idx_tile, axis=1) + + # Store results + tl.store(out_ptr + m_offs[:, None] * K + k_offs[None, :], gathered_values, mask=m_mask[:, None]) + + +def test_gather_2d_simd(M, N, K): + """Test gather_2d_simd with various tensor sizes.""" + src = torch.randn(M, N, dtype=torch.float32, device='npu') + indices = torch.randint(0, N, (M, K), dtype=torch.int32, device='npu') + output = torch.empty((M, K), dtype=src.dtype, device='npu') + + grid = (triton.cdiv(M, 32), ) + gather_2d_simd[grid](src, indices, output, M, N, K, XBLOCK=32, XBLOCK_SUB=4) + + ref = torch.gather(src, 1, indices.long()) + assert torch.allclose(output, ref, rtol=1e-5, atol=1e-5) + + +if __name__ == "__main__": + M = 32 + N = 128 + K = 64 + test_gather_2d_simd(M, N, K) + print("======Persistent Gather Test Passed!======") diff --git a/third_party/ascend/tutorials/conftest.py b/third_party/ascend/tutorials/conftest.py new file mode 100644 index 0000000000..58db5ff914 --- /dev/null +++ b/third_party/ascend/tutorials/conftest.py @@ -0,0 +1,9 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. + +import pytest + + +def pytest_collect_file(parent, file_path): + if file_path.suffix == ".py" and file_path.name[0].isdigit() and file_path.name != "__init__.py": + return pytest.Module.from_parent(parent, path=file_path) + return None diff --git a/third_party/ascend/unittest/Analysis/CMakeLists.txt b/third_party/ascend/unittest/Analysis/CMakeLists.txt new file mode 100644 index 0000000000..51c49b2231 --- /dev/null +++ b/third_party/ascend/unittest/Analysis/CMakeLists.txt @@ -0,0 +1,30 @@ +include(${CMAKE_SOURCE_DIR}/cmake/AddTritonUnitTest.cmake) + +# Link the compiler objects into this executable so ELF wrapping observes +# their deallocations; calls inside a shared compiler library cannot be wrapped. +get_property(offset_cache_test_libs GLOBAL PROPERTY TRITON_LIBS) + +add_triton_ut( + NAME TestTritonOffsetCacheLifetime + SRCS OffsetCacheLifetimeTest.cpp + LIBS + ${offset_cache_test_libs} + TritonNVIDIAGPUToLLVM + BiShengIRAnnotationDialect + BiShengIRHACCDialect + BiShengIRHFusionDialect + BiShengIRHIVMDialect + BiShengIRMathExtDialect + BiShengIRMemRefExtDialect + BiShengIRScopeDialect + BiShengIRSymbolDialect + BiShengIRTensorDialect + MLIRParser +) + +# Observe real BlockArgumentImpl deallocation without production hooks. +# Both forms of operator delete are used by supported Linux/ELF toolchains. +target_link_options(TestTritonOffsetCacheLifetime PRIVATE + "LINKER:--wrap=_ZdlPv" + "LINKER:--wrap=_ZdlPvm" +) diff --git a/third_party/ascend/unittest/Analysis/OffsetCacheLifetimeTest.cpp b/third_party/ascend/unittest/Analysis/OffsetCacheLifetimeTest.cpp new file mode 100644 index 0000000000..d6720939bd --- /dev/null +++ b/third_party/ascend/unittest/Analysis/OffsetCacheLifetimeTest.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "TritonToUnstructure/UnstructureConversionPass.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Parser/Parser.h" +#include "llvm/ADT/ScopeExit.h" +#include "gtest/gtest.h" +#include + +using namespace mlir; +using namespace mlir::triton; + +namespace { +struct ReleaseProbe { + Value watched; + llvm::DenseMap *cache; + unsigned releases = 0; + bool staleKey = false; + bool stalePayload = false; +}; +thread_local ReleaseProbe *activeProbe = nullptr; + +// Observe deallocation only in this test executable. Do not allocate, print, +// or dereference the watched Value here: its destructor has already run. +void observeRelease(void *pointer) { + auto *probe = activeProbe; + if (!probe || pointer != probe->watched.getAsOpaquePointer()) + return; + ++probe->releases; + probe->staleKey |= probe->cache->contains(probe->watched); + for (auto &entry : *probe->cache) { + auto &info = entry.second; + probe->stalePayload |= + info.getPtr() == probe->watched || info.getOffset() == probe->watched; + for (Value offset : info.getOffsetsRef()) + probe->stalePayload |= offset == probe->watched; + } +} +} // namespace + +// ELF linker wrapping redirects delete calls from the linked MLIR objects; +// the actual allocator and production implementation remain unchanged. +extern "C" void __real__ZdlPv(void *) noexcept; +extern "C" void __real__ZdlPvm(void *, std::size_t) noexcept; +extern "C" void __wrap__ZdlPv(void *pointer) noexcept { + observeRelease(pointer); + __real__ZdlPv(pointer); +} +extern "C" void __wrap__ZdlPvm(void *pointer, std::size_t size) noexcept { + observeRelease(pointer); + __real__ZdlPvm(pointer, size); +} + +TEST(OffsetCacheLifetime, ReleasesOldValuesAfterCacheInvalidation) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir( + module { + tt.func @cache_lifetime(%base: !tt.ptr, %n: i32) { + %zero = arith.constant 0 : i32 + %one = arith.constant 1 : i32 + %p0 = tt.addptr %base, %n : !tt.ptr, i32 + %result = scf.for %i = %zero to %n step %one + iter_args(%p = %p0) -> (!tt.ptr) : i32 { + %next = tt.addptr %p, %one : !tt.ptr, i32 + scf.yield %next : !tt.ptr + } + tt.return + } + } + )mlir", + &context); + ASSERT_TRUE(module); + auto func = *module->getOps().begin(); + auto loop = *func.getBody().front().getOps().begin(); + Value oldArg = loop.getRegionIterArgs().front(); + llvm::DenseMap cache; + IRRewriter rewriter(&context); + parse(oldArg, oldArg.getLoc(), rewriter, cache); + ASSERT_TRUE(cache.contains(oldArg)); + + ReleaseProbe probe{oldArg, &cache}; + ASSERT_EQ(activeProbe, nullptr); + { + activeProbe = &probe; + auto resetProbe = llvm::make_scope_exit([] { activeProbe = nullptr; }); + replacePtrArguments(func, cache); + } + + // Do not silently pass if the linker or MLIR allocation path stops routing + // the watched deallocation through the wrappers. + EXPECT_EQ(probe.releases, 1u); + EXPECT_FALSE(probe.staleKey); + EXPECT_FALSE(probe.stalePayload); + EXPECT_TRUE(succeeded(verify(*module))); +} diff --git a/third_party/ascend/unittest/CMakeLists.txt b/third_party/ascend/unittest/CMakeLists.txt new file mode 100644 index 0000000000..20ba26b517 --- /dev/null +++ b/third_party/ascend/unittest/CMakeLists.txt @@ -0,0 +1,36 @@ +llvm_canonicalize_cmake_booleans( + MLIR_ENABLE_BINDINGS_PYTHON +) + +configure_lit_site_cfg( + ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in + ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py + MAIN_CONFIG + ${CMAKE_CURRENT_SOURCE_DIR}/lit.cfg.py +) + +set(TRITON_TEST_DEPENDS + triton-opt +) + +set(FILECHECK_PATH "${LLVM_LIBRARY_DIR}/../bin/FileCheck") +set(LIT_ARGS "-Dfilecheck=${FILECHECK_PATH}") + +add_lit_testsuite(check-triton-ascend-lit-tests "Running the triton regression tests" + ${CMAKE_CURRENT_BINARY_DIR} + ARGS ${LIT_ARGS} + DEPENDS ${TRITON_TEST_DEPENDS} + ) + +set_target_properties(check-triton-ascend-lit-tests PROPERTIES FOLDER "Tests") + +add_lit_testsuites(TRITON-LIT-TESTS ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${TRITON_TEST_DEPENDS}) + +if(TRITON_ASCEND_HAS_INPROC_COSTMODEL) + add_subdirectory(costmodel_ut) +endif() + +# The deallocation observer uses ELF linker wrapping. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_subdirectory(Analysis) +endif() diff --git a/third_party/ascend/unittest/Conversion/950PR/DiscreteMaskAccess/indirect_loadstore.mlir b/third_party/ascend/unittest/Conversion/950PR/DiscreteMaskAccess/indirect_loadstore.mlir new file mode 100644 index 0000000000..2972718930 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/950PR/DiscreteMaskAccess/indirect_loadstore.mlir @@ -0,0 +1,39 @@ +// RUN: triton-opt '--discrete-mask-access-conversion=compile-on-910-95=True compile-mode=simd_simt_template' --split-input-file %s | FileCheck %s + +// CHECK-LABEL: tt.func @discrete_load +// CHECK: tt.load {{.*}} {MixCompileDiscreteMask} : tensor<1024x!tt.ptr> +tt.func @discrete_load(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<0> : tensor<1024xi32> + %cst_0 = arith.constant dense<200> : tensor<1024xi32> + %cst_1 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst_0 : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_1 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %5, %3, %cst : tensor<1024x!tt.ptr> + tt.store %7, %8 : tensor<1024x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @discrete_store +// CHECK: tt.store {{.*}} {MixCompileDiscreteMask} : tensor<1024x!tt.ptr> +tt.func @discrete_store(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<0> : tensor<1024xi32> + %cst_0 = arith.constant dense<200> : tensor<1024xi32> + %cst_1 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst_0 : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_1 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %5 : tensor<1024x!tt.ptr> + tt.store %7, %8, %3 : tensor<1024x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/950PR/DiscreteMaskAccess/loop_mask_fallback.mlir b/third_party/ascend/unittest/Conversion/950PR/DiscreteMaskAccess/loop_mask_fallback.mlir new file mode 100644 index 0000000000..f082be9473 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/950PR/DiscreteMaskAccess/loop_mask_fallback.mlir @@ -0,0 +1,82 @@ +// RUN: triton-opt '--discrete-mask-access-conversion=compile-on-910-95=true compile-mode=simd_simt_template' --split-input-file --verify-each %s | FileCheck %s --check-prefix=MASK +// RUN: triton-opt '--discrete-mask-access-conversion=compile-on-910-95=true compile-mode=simd_simt_template' '--triton-to-unstructure=compile-on-910-95=true compile-mode=simd_simt_template' '--triton-to-linalg=compile-on-910-95=true compile-mode=simd_simt_template' --split-input-file --verify-each %s | FileCheck %s --check-prefix=LOWER --implicit-check-not=unrealized_conversion_cast --implicit-check-not=RuntimeLoopMask + +// A suffix is outside the prefix rewrite, but is still an analyzable range. +// Replacing the iter_arg by its init would incorrectly forget earlier bounds. +// MASK-LABEL: tt.func public @loop_suffix +// MASK: scf.for {{.*}} -> (tensor<16xi1>, tensor<16xf32>) +// MASK: %[[NEXT:.*]] = arith.andi +// MASK: tt.load %{{.*}}, %[[NEXT]], %{{.*}} {MixCompileDiscreteMask, RuntimeLoopMask} +// LOWER-LABEL: func.func @loop_suffix +// LOWER: scf.for {{.*}} -> (tensor<16xi1>, tensor<16xf32>) +// LOWER: %[[NEXT:.*]] = arith.andi {{.*}} : tensor<16xi1> +// LOWER: func.call @triton_indirect_load({{.*}}%[[NEXT]], +// LOWER: scf.yield %[[NEXT]], +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @loop_suffix(%x: !tt.ptr, %out: !tt.ptr, %steps: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c12 = arith.constant 12 : i32 + %init = arith.constant dense : tensor<16xi1> + %zeros = arith.constant dense<0.0> : tensor<16xf32> + %lane = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %base = tt.splat %x : !tt.ptr -> tensor<16x!tt.ptr> + %ptrs = tt.addptr %base, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %result:2 = scf.for %i = %c0 to %steps step %c1 iter_args(%mask = %init, %acc = %zeros) -> (tensor<16xi1>, tensor<16xf32>) : i32 { + %limit = arith.subi %c12, %i : i32 + %bound = tt.splat %limit : i32 -> tensor<16xi32> + %current = arith.cmpi sge, %lane, %bound : tensor<16xi32> + %next = arith.andi %mask, %current : tensor<16xi1> + %value = tt.load %ptrs, %next, %zeros : tensor<16x!tt.ptr> + %sum = arith.addf %acc, %value : tensor<16xf32> + scf.yield %next, %sum : tensor<16xi1>, tensor<16xf32> + } + %output = tt.splat %out : !tt.ptr -> tensor<16x!tt.ptr> + %output_ptrs = tt.addptr %output, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %output_ptrs, %result#1 : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- + +// While before/after slots have different order. A changing mask must not be +// considered invariant just because the condition forwards it unchanged. +// MASK-LABEL: tt.func public @while_permuted_mask +// MASK: scf.while +// MASK: %[[NEXT:.*]] = arith.andi +// MASK: tt.load %{{.*}}, %[[NEXT]], %{{.*}} {MixCompileDiscreteMask, RuntimeLoopMask} +// LOWER-LABEL: func.func @while_permuted_mask +// LOWER: scf.while +// LOWER: %[[NEXT:.*]] = arith.andi {{.*}} : tensor<16xi1> +// LOWER: func.call @triton_indirect_load({{.*}}%[[NEXT]], +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @while_permuted_mask(%x: !tt.ptr, %out: !tt.ptr, %steps: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c12 = arith.constant 12 : i32 + %init = arith.constant dense : tensor<16xi1> + %zeros = arith.constant dense<0.0> : tensor<16xf32> + %lane = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %base = tt.splat %x : !tt.ptr -> tensor<16x!tt.ptr> + %ptrs = tt.addptr %base, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %result:3 = scf.while (%i = %c0, %mask = %init, %acc = %zeros) : (i32, tensor<16xi1>, tensor<16xf32>) -> (tensor<16xi1>, i32, tensor<16xf32>) { + %keep = arith.cmpi slt, %i, %steps : i32 + scf.condition(%keep) %mask, %i, %acc : tensor<16xi1>, i32, tensor<16xf32> + } do { + ^bb0(%mask: tensor<16xi1>, %i: i32, %acc: tensor<16xf32>): + %limit = arith.subi %c12, %i : i32 + %bound = tt.splat %limit : i32 -> tensor<16xi32> + %current = arith.cmpi sge, %lane, %bound : tensor<16xi32> + %next = arith.andi %mask, %current : tensor<16xi1> + %value = tt.load %ptrs, %next, %zeros : tensor<16x!tt.ptr> + %sum = arith.addf %acc, %value : tensor<16xf32> + %inc = arith.addi %i, %c1 : i32 + scf.yield %inc, %next, %sum : i32, tensor<16xi1>, tensor<16xf32> + } + %output = tt.splat %out : !tt.ptr -> tensor<16x!tt.ptr> + %output_ptrs = tt.addptr %output, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %output_ptrs, %result#2 : tensor<16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/chunk_coalescing.mlir b/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/chunk_coalescing.mlir new file mode 100644 index 0000000000..3fb7a2ca1e --- /dev/null +++ b/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/chunk_coalescing.mlir @@ -0,0 +1,536 @@ +// RUN: triton-opt %s --triton-to-unstructure='compile-on-910-95=true compile-mode=simd_simt_template' \ +// RUN: --triton-to-linalg='compile-on-910-95=true compile-mode=simd_simt_template' --split-input-file \ +// RUN: | FileCheck %s + +// ----- +// Adjacent 16-f32 tiles form a small contiguous DMA. The pass should merge +// 16 tiles per program, drop the all-true tile mask, and record the launch-grid +// shrink metadata on the tile program-id axis. +// CHECK-LABEL: module attributes {hacc.coalesce_axis = 0 : i32, hacc.coalesce_factor = 16 : i32 +// CHECK-LABEL: func.func @chunk_coalesce_simple +// CHECK: memref.reinterpret_cast +// CHECK-SAME: sizes: [16, 16] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @chunk_coalesce_simple(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %c256 = arith.constant dense<256> : tensor<16xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<16xf32> + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %mask = arith.cmpi slt, %offs, %c256 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val, %mask : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// Unmasked kernels do not carry a static tile count in the IR. The pass cannot +// prove runtime grid[axis] is >= H and divisible by H, so it must leave the +// kernel uncoalesced. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_skip_unmasked_unknown_grid +// CHECK-NOT: sizes: [16, 16] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_skip_unmasked_unknown_grid(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// Unmasked kernels with hacc.grid_num_tiles can coalesce when the hint gives +// the exact compile-time tile count. +// CHECK-LABEL: module attributes {hacc.coalesce_axis = 0 : i32, hacc.coalesce_factor = 16 : i32 +// CHECK-NOT: hacc.grid_num_tiles +// CHECK-LABEL: func.func @tile_chunk_unmasked_with_grid_hint +// CHECK: memref.reinterpret_cast +// CHECK-SAME: sizes: [16, 16] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 128 : i32} { + tt.func public @tile_chunk_unmasked_with_grid_hint(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// A one-tile mask has BOUND == tileLen. It is provably all true for the only +// tile, but the static tile count is one, so there is nothing to coalesce. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_single_full_tile +// CHECK-NOT: sizes: [16, 16] +// CHECK: sizes: [16] +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_single_full_tile(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %c16_tensor = arith.constant dense<16> : tensor<16xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<16xf32> + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %mask = arith.cmpi slt, %offs, %c16_tensor : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val, %mask : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// Reading num_programs on the coalesced axis is unsafe because the host launcher +// divides that grid dimension by H. The pass must leave the kernel uncoalesced. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_reads_num_programs +// CHECK-NOT: sizes: [16, 16] +// CHECK: sizes: [16] +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_reads_num_programs(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %num = tt.get_num_programs x : i32 + %c16 = arith.constant 16 : i32 + %c512 = arith.constant dense<512> : tensor<16xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<16xf32> + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %num_splat = tt.splat %num : i32 -> tensor<16xi32> + %guard_offs = arith.addi %offs, %num_splat : tensor<16xi32> + %mask = arith.cmpi slt, %guard_offs, %c512 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val, %mask : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// Partial tail masks are not separable after prepending the H lane, so the pass +// must keep the original one-tile program shape. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_partial_tail +// CHECK-NOT: sizes: [16, 16] +// CHECK: sizes: [16] +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_partial_tail(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %c250 = arith.constant dense<250> : tensor<16xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<16xf32> + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %mask = arith.cmpi slt, %offs, %c250 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val, %mask : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// A 2-D masked block may contain the tile-index signature on the outer grid +// axis, but a dynamic boundary mask on that axis means grid/H is not provably +// exact and the lifted mask is not a single structured slice. Keep the original +// program shape instead. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_skip_2d_dynamic_boundary +// CHECK-NOT: tensor<2x256x256 +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_skip_2d_dynamic_boundary(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %m: i32, %n: i32) { + %pid_m = tt.get_program_id x : i32 + %pid_n = tt.get_program_id y : i32 + %c256 = arith.constant 256 : i32 + %zero = arith.constant dense<0.000000e+00> : tensor<256x256xf32> + + %row_blk = arith.muli %pid_m, %c256 : i32 + %row_range = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %row_splat = tt.splat %row_blk : i32 -> tensor<256xi32> + %row = arith.addi %row_splat, %row_range : tensor<256xi32> + %row_2d = tt.expand_dims %row {axis = 1 : i32} : tensor<256xi32> -> tensor<256x1xi32> + + %col_blk = arith.muli %pid_n, %c256 : i32 + %col_range = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %col_splat = tt.splat %col_blk : i32 -> tensor<256xi32> + %col = arith.addi %col_splat, %col_range : tensor<256xi32> + %col_2d = tt.expand_dims %col {axis = 0 : i32} : tensor<256xi32> -> tensor<1x256xi32> + + %n_row = tt.splat %n : i32 -> tensor<256x1xi32> + %row_offset = arith.muli %row_2d, %n_row : tensor<256x1xi32> + %row_offset_bc = tt.broadcast %row_offset : tensor<256x1xi32> -> tensor<256x256xi32> + %col_bc = tt.broadcast %col_2d : tensor<1x256xi32> -> tensor<256x256xi32> + %offsets = arith.addi %row_offset_bc, %col_bc : tensor<256x256xi32> + + %m_bound = tt.splat %m : i32 -> tensor<256x1xi32> + %row_mask = arith.cmpi slt, %row_2d, %m_bound : tensor<256x1xi32> + %row_mask_bc = tt.broadcast %row_mask : tensor<256x1xi1> -> tensor<256x256xi1> + %n_bound = tt.splat %n : i32 -> tensor<1x256xi32> + %col_mask = arith.cmpi slt, %col_2d, %n_bound : tensor<1x256xi32> + %col_mask_bc = tt.broadcast %col_mask : tensor<1x256xi1> -> tensor<256x256xi1> + %mask = arith.andi %row_mask_bc, %col_mask_bc : tensor<256x256xi1> + + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<256x256x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offsets : tensor<256x256x!tt.ptr>, tensor<256x256xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<256x256x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<256x256x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offsets : tensor<256x256x!tt.ptr>, tensor<256x256xi32> + tt.store %dst_ptr, %val, %mask : tensor<256x256x!tt.ptr> + tt.return + } +} + +// ----- +// BUG REPRO: Unmasked kernel with grid_num_tiles=1 — only one tile, nothing to +// coalesce. The pass must NOT rewrite (H would be 0 or 1). Previously this +// could crash or produce dead code. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-NOT: hacc.grid_num_tiles +// CHECK-LABEL: func.func @tile_chunk_grid_hint_one_tile +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 1 : i32} { + tt.func public @tile_chunk_grid_hint_one_tile(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// BUG REPRO: grid_num_tiles is a prime number (e.g. 127) that is not divisible +// by any H in [hMin, maxH]. chooseH must return 0 and the pass must bail. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-NOT: hacc.grid_num_tiles +// CHECK-LABEL: func.func @tile_chunk_prime_num_tiles +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 127 : i32} { + tt.func public @tile_chunk_prime_num_tiles(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// BUG REPRO: tileLen=4 with f16 (blockBytes=4*2=8). hMin = ceil(512/8) = 64. +// maxH is capped at 16 < hMin=64, so the pass must bail (cannot reach +// kMinContigBytes within UB budget). +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_tiny_tile_bails +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_tiny_tile_bails(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c4 = arith.constant 4 : i32 + %c1024 = arith.constant dense<1024> : tensor<4xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<4xf16> + %blk = arith.muli %pid, %c4 : i32 + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<4xi32> + %offs = arith.addi %blk_splat, %range : tensor<4xi32> + %mask = arith.cmpi slt, %offs, %c1024 : tensor<4xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<4x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<4x!tt.ptr>, tensor<4xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<4x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<4x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<4x!tt.ptr>, tensor<4xi32> + tt.store %dst_ptr, %val, %mask : tensor<4x!tt.ptr> + tt.return + } +} + +// ----- +// Large footprint forces maxH < 2 due to UB overflow. The pass must bail +// instead of setting maxH=2 and overflowing. Here the kernel has two large +// 256x256xf32 tensors => footprintUnit = 2*256*256*4 = 524288 > kUBBytesBudget. +// maxH = floor(131072/524288) = 0 < 2 → bail. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_ub_overflow_bails +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_ub_overflow_bails(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c256 = arith.constant 256 : i32 + %c4096 = arith.constant dense<4096> : tensor<256xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<256xf32> + %blk = arith.muli %pid, %c256 : i32 + %range = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<256xi32> + %offs = arith.addi %blk_splat, %range : tensor<256xi32> + %mask = arith.cmpi slt, %offs, %c4096 : tensor<256xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<256x!tt.ptr>, tensor<256xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<256x!tt.ptr> + // Heavy compute: expand to NxN to inflate footprint + %exp = tt.expand_dims %val {axis = 1 : i32} : tensor<256xf32> -> tensor<256x1xf32> + %bcast = tt.broadcast %exp : tensor<256x1xf32> -> tensor<256x256xf32> + %red = "tt.reduce"(%bcast) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<256x256xf32>) -> tensor<256xf32> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %dst_ptr, %red, %mask : tensor<256x!tt.ptr> + tt.return + } +} + +// ----- +// The two same-axis program-id values are canonicalized to one launch argument +// by the preceding Unstructure stage. Keep the original Chunk behavior: +// operate on that canonical single seed. +// CHECK-LABEL: module attributes {hacc.coalesce_axis = 0 : i32, hacc.coalesce_factor = 16 : i32 +// CHECK-LABEL: func.func @tile_chunk_duplicate_pid +// CHECK: memref.reinterpret_cast +// CHECK-SAME: sizes: [16, 16] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_duplicate_pid(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid1 = tt.get_program_id x : i32 + %pid2 = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %c256 = arith.constant dense<256> : tensor<16xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<16xf32> + %blk = arith.muli %pid1, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %mask = arith.cmpi slt, %offs, %c256 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<16x!tt.ptr> + // pid2 used for a different base offset (not matching the seed pattern) + %off2 = arith.muli %pid2, %c16 : i32 + %off2_splat = tt.splat %off2 : i32 -> tensor<16xi32> + %dst_offs = arith.addi %off2_splat, %range : tensor<16xi32> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %dst_offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val, %mask : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// Pid-derived offset goes through arith.remsi (unsafe op). The pass must bail. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_unsafe_remsi +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_unsafe_remsi(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %c8 = arith.constant 8 : i32 + %c256 = arith.constant dense<256> : tensor<16xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<16xf32> + %rem_pid = arith.remsi %pid, %c8 : i32 + %blk = arith.muli %rem_pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %mask = arith.cmpi slt, %offs, %c256 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val, %mask : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// Mask is used by operations OTHER than load/store (e.g. arith.select). The +// pass must bail because dropping the mask would be incorrect. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @tile_chunk_mask_escapes_to_where +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_mask_escapes_to_where(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %c256 = arith.constant dense<256> : tensor<16xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<16xf32> + %one = arith.constant dense<1.000000e+00> : tensor<16xf32> + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %mask = arith.cmpi slt, %offs, %c256 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<16x!tt.ptr> + // mask used by select (non load/store use) -> must bail + %selected = arith.select %mask, %val, %one : tensor<16xi1>, tensor<16xf32> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %selected, %mask : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// Coalescing with scan and reduce in the region — must succeed (these ops are +// liftable). tileLen=16, BOUND=256 => numTiles=16, H should be chosen. +// CHECK-LABEL: module attributes {hacc.coalesce_axis = 0 : i32 +// CHECK-LABEL: func.func @tile_chunk_with_scan_reduce +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @tile_chunk_with_scan_reduce(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %c256 = arith.constant dense<256> : tensor<16xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<16xf32> + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %mask = arith.cmpi slt, %offs, %c256 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<16x!tt.ptr> + %scanned = "tt.scan"(%val) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %scanned, %mask : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// grid_num_tiles=0 (invalid) — must bail, not crash. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-NOT: hacc.grid_num_tiles +// CHECK-LABEL: func.func @tile_chunk_grid_hint_zero +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 0 : i32} { + tt.func public @tile_chunk_grid_hint_zero(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// grid_num_tiles negative (invalid) — must bail, not crash. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-NOT: hacc.grid_num_tiles +// CHECK-LABEL: func.func @tile_chunk_grid_hint_negative +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = -1 : i32} { + tt.func public @tile_chunk_grid_hint_negative(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %pid = tt.get_program_id x : i32 + %c16 = arith.constant 16 : i32 + %blk = arith.muli %pid, %c16 : i32 + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %src_ptr : tensor<16x!tt.ptr> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %val : tensor<16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/indirect_load_rewrite.mlir b/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/indirect_load_rewrite.mlir new file mode 100644 index 0000000000..1430910ec4 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/indirect_load_rewrite.mlir @@ -0,0 +1,1061 @@ +// RUN: triton-opt %s --triton-to-unstructure='compile-on-910-95=true compile-mode=simd_simt_template' \ +// RUN: --triton-to-linalg='compile-on-910-95=true compile-mode=simd_simt_template' --split-input-file \ +// RUN: | FileCheck %s +// RUN: triton-opt %s --mlir-print-ir-after-all \ +// RUN: --triton-to-unstructure='compile-on-910-95=true compile-mode=simd_simt_template' \ +// RUN: --triton-to-linalg='compile-on-910-95=true compile-mode=simd_simt_template' --split-input-file 2>&1 \ +// RUN: | FileCheck %s --check-prefix=SLS-INSPECT + +// The static stride-4 AddPtr below is deliberately inspected but cannot be +// converted to an indirect load. The trace locks down the original greedy +// protocol: mark it after the Chunk/SLS stage, retain the mark through T2L's +// in-place CSE/Canonicalizer cleanup, then lower it through memref.copy. +// SLS-INSPECT: IR Dump After {{.*}}LayoutMemoryCompatibilityPass{{.*}} +// SLS-INSPECT: tt.func public @addptr_stride4_1d +// SLS-INSPECT: tt.load {{.*}} {InspectedByStridedLoadStoreRewrite} +// SLS-INSPECT: IR Dump After CSE +// SLS-INSPECT: tt.func public @addptr_stride4_1d +// SLS-INSPECT: tt.load {{.*}} {InspectedByStridedLoadStoreRewrite} +// SLS-INSPECT: IR Dump After Canonicalizer +// SLS-INSPECT: tt.func public @addptr_stride4_1d +// SLS-INSPECT: tt.load {{.*}} {InspectedByStridedLoadStoreRewrite} +// SLS-INSPECT: IR Dump After TritonToLinalg +// SLS-INSPECT: func.func @addptr_stride4_1d +// SLS-INSPECT-NOT: call @triton_indirect_load +// SLS-INSPECT: memref.copy + +// The masked stride-4 variant follows the indirect branch. Keep this trace +// separate from the inspect/no-rewrite case above so both greedy outcomes are +// covered at the Graph compatibility boundary. +// SLS-INSPECT: IR Dump After {{.*}}LayoutMemoryCompatibilityPass{{.*}} +// SLS-INSPECT: tt.func public @addptr_stride4_masked_single_tile +// SLS-INSPECT: ascend.indirect_load {{.*}} {RewrittenByStridedLoadStoreRewrite, isVolatile = false} +// SLS-INSPECT: IR Dump After TritonToLinalg +// SLS-INSPECT: func.func @addptr_stride4_masked_single_tile +// SLS-INSPECT: call @triton_indirect_load + +// ----- +// V1 rank-1 miss (AddPtr, static power-of-two stride 4): +// tt.addptr(splat ptr, arange*4) -> should stay on the reinterpret_cast/memref.copy path. +// CHECK-LABEL: func.func @addptr_stride4_1d +// CHECK-NOT: call @triton_indirect_load +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_stride4_1d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c4 = arith.constant dense<4> : tensor<256xi32> + %0 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %1 = arith.muli %0, %c4 : tensor<256xi32> + %2 = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %3 = tt.addptr %2, %1 : tensor<256x!tt.ptr>, tensor<256xi32> + %4 = tt.load %3 : tensor<256x!tt.ptr> + %5 = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %6 = tt.addptr %5, %0 : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %6, %4 : tensor<256x!tt.ptr> + tt.return + } +} + +// ----- +// V1 rank-1 hit (AddPtr, static power-of-two stride 4, masked single tile): +// The structured strided-copy route would create a dynamic boundary size that +// can become zero for over-launched programs. Route to indirect_load instead. +// CHECK-LABEL: func.func @addptr_stride4_masked_single_tile +// CHECK-SAME: parallel_mode = "mix_simd_simt" +// CHECK: call @triton_indirect_load(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = false} : (memref, tensor<1024xi64>, tensor<1024xi1>, tensor<1024xf16>) -> tensor<1024xf16> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_stride4_masked_single_tile(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %zero = arith.constant dense<0.000000e+00> : tensor<1024xf16> + %one = arith.constant dense<1.000000e+00> : tensor<1024xf16> + %c4 = arith.constant dense<4> : tensor<1024xi32> + %bound = arith.constant dense<1024> : tensor<1024xi32> + %c1024_i32 = arith.constant 1024 : i32 + %pid = tt.get_program_id x : i32 + %tile_base = arith.muli %pid, %c1024_i32 : i32 + %range = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %base_splat = tt.splat %tile_base : i32 -> tensor<1024xi32> + %idx = arith.addi %base_splat, %range : tensor<1024xi32> + %mask = arith.cmpi slt, %idx, %bound : tensor<1024xi32> + %src_offsets = arith.muli %idx, %c4 : tensor<1024xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %src_ptr = tt.addptr %src_base, %src_offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + %val = tt.load %src_ptr, %mask, %zero : tensor<1024x!tt.ptr> + %out = arith.addf %val, %one : tensor<1024xf16> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %idx : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %dst_ptr, %out, %mask : tensor<1024x!tt.ptr> + tt.return + } +} + +// ----- +// V1 guard (AddPtr, static power-of-two stride 4, masked single tile): +// This otherwise indirect-eligible load was already handled by +// ImplicitPermute. SLS must leave it on the normal memref.copy lowering path. +// CHECK-LABEL: func.func @addptr_stride4_masked_implicit_permute_handled +// CHECK-NOT: call @triton_indirect_load +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_stride4_masked_implicit_permute_handled(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %zero = arith.constant dense<0.000000e+00> : tensor<1024xf16> + %one = arith.constant dense<1.000000e+00> : tensor<1024xf16> + %c4 = arith.constant dense<4> : tensor<1024xi32> + %bound = arith.constant dense<1024> : tensor<1024xi32> + %c1024_i32 = arith.constant 1024 : i32 + %pid = tt.get_program_id x : i32 + %tile_base = arith.muli %pid, %c1024_i32 : i32 + %range = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %base_splat = tt.splat %tile_base : i32 -> tensor<1024xi32> + %idx = arith.addi %base_splat, %range : tensor<1024xi32> + %mask = arith.cmpi slt, %idx, %bound : tensor<1024xi32> + %src_offsets = arith.muli %idx, %c4 : tensor<1024xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %src_ptr = tt.addptr %src_base, %src_offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + %val = tt.load %src_ptr, %mask, %zero {ImplicitPermuteHandled} : tensor<1024x!tt.ptr> + %out = arith.addf %val, %one : tensor<1024xf16> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %idx : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %dst_ptr, %out, %mask : tensor<1024x!tt.ptr> + tt.return + } +} + +// ----- +// V1 rank-1 hit (AddPtr, static non-power-of-two stride 3): +// tt.addptr(splat ptr, arange*3) -> should become triton_stride_load call. +// CHECK-LABEL: func.func @addptr_stride3_1d +// CHECK-NOT: call @triton_indirect_load +// CHECK: call @triton_stride_load +// CHECK-SAME: : (memref, i64, f32, i64, i64) -> tensor<256xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_stride3_1d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c3 = arith.constant dense<3> : tensor<256xi32> + %0 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %1 = arith.muli %0, %c3 : tensor<256xi32> + %2 = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %3 = tt.addptr %2, %1 : tensor<256x!tt.ptr>, tensor<256xi32> + %4 = tt.load %3 : tensor<256x!tt.ptr> + %5 = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %6 = tt.addptr %5, %0 : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %6, %4 : tensor<256x!tt.ptr> + tt.return + } +} + +// ----- +// V1 rank-1 miss (AddPtr, dynamic stride): +// Runtime stride may be 1 or power-of-two, so keep the structured SIMD path. +// CHECK-LABEL: func.func @addptr_dynamic_stride_1d +// CHECK-NOT: call @triton_indirect_load +// CHECK-NOT: call @triton_stride_load +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_dynamic_stride_1d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %stride: i32) { + %0 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %stride_splat = tt.splat %stride : i32 -> tensor<256xi32> + %1 = arith.muli %0, %stride_splat : tensor<256xi32> + %2 = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %3 = tt.addptr %2, %1 : tensor<256x!tt.ptr>, tensor<256xi32> + %4 = tt.load %3 : tensor<256x!tt.ptr> + %5 = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %6 = tt.addptr %5, %0 : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %6, %4 : tensor<256x!tt.ptr> + tt.return + } +} + +// ----- +// V2 rank-1 miss (AddPtr store, scalar base AddPtr + dynamic stride): +// Runtime stride may be 1 or power-of-two, so keep the structured SIMD path. +// CHECK-LABEL: func.func @addptr_dynamic_stride_store_scalar_base +// CHECK-NOT: call @triton_indirect_store +// CHECK-NOT: call @triton_stride_store +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_dynamic_stride_store_scalar_base(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %base_offset: i64, + %stride: i64) { + %cst = arith.constant dense<1.000000e+00> : tensor<32xf32> + %range_i32 = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %range_i64 = arith.extsi %range_i32 : tensor<32xi32> to tensor<32xi64> + %stride_splat = tt.splat %stride : i64 -> tensor<32xi64> + %offsets = arith.muli %range_i64, %stride_splat : tensor<32xi64> + %scalar_base = tt.addptr %arg0, %base_offset : !tt.ptr, i64 + %base = tt.splat %scalar_base : !tt.ptr -> tensor<32x!tt.ptr> + %ptr = tt.addptr %base, %offsets : tensor<32x!tt.ptr>, tensor<32xi64> + tt.store %ptr, %cst : tensor<32x!tt.ptr> + tt.return + } +} + +// ----- +// V1 rank-2 hit (AddPtr, static non-power-of-two stride 3): +// CHECK-LABEL: func.func @addptr_stride3_2d +// CHECK-NOT: call @triton_indirect_load +// CHECK: call @triton_stride_load +// CHECK-SAME: : (memref, i64, f32, i64, i64, i64, i64) -> tensor<4x8xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_stride3_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %m = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %m_exp = tt.expand_dims %m {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %m_b = tt.broadcast %m_exp : tensor<4x1xi32> -> tensor<4x8xi32> + %n = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %n_exp = tt.expand_dims %n {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %n_b = tt.broadcast %n_exp : tensor<1x8xi32> -> tensor<4x8xi32> + %c96 = arith.constant dense<96> : tensor<4x8xi32> + %c3 = arith.constant dense<3> : tensor<4x8xi32> + %row = arith.muli %m_b, %c96 : tensor<4x8xi32> + %col = arith.muli %n_b, %c3 : tensor<4x8xi32> + %offset = arith.addi %row, %col : tensor<4x8xi32> + %src = tt.splat %arg0 : !tt.ptr -> tensor<4x8x!tt.ptr> + %ptr = tt.addptr %src, %offset : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + %val = tt.load %ptr : tensor<4x8x!tt.ptr> + %c8 = arith.constant dense<8> : tensor<4x8xi32> + %out_row = arith.muli %m_b, %c8 : tensor<4x8xi32> + %out_offset = arith.addi %out_row, %n_b : tensor<4x8xi32> + %dst = tt.splat %arg1 : !tt.ptr -> tensor<4x8x!tt.ptr> + %out_ptr = tt.addptr %dst, %out_offset : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + tt.store %out_ptr, %val : tensor<4x8x!tt.ptr> + tt.return + } +} + +// ----- +// V1 miss (AddPtr, 1D, stride == 1): +// arange + splat (no muli > 1) -> NOT rewritten, normal strided memref.copy +// CHECK-LABEL: func.func @addptr_stride1_1d +// CHECK-NOT: call @triton_indirect_load +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_stride1_1d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %0 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %1 = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<256x!tt.ptr>, tensor<256xi32> + %3 = tt.load %2 : tensor<256x!tt.ptr> + %4 = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %5, %3 : tensor<256x!tt.ptr> + tt.return + } +} + +// ----- +// V3 hit (make_tensor_ptr, rank-2): non-power-of-two low-dim stride uses +// triton_stride_load. +// CHECK-LABEL: func.func @mtpt_low_stride5 +// CHECK-NOT: call @triton_indirect_load +// CHECK: call @triton_stride_load +// CHECK-SAME: : (memref, i64, f32, i64, i64, i64, i64) -> tensor<4x8xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_low_stride5(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %size_m = arith.constant 64 : i64 + %size_n = arith.constant 256 : i64 + %stride_m = arith.constant 256 : i64 + %stride_n = arith.constant 5 : i64 + %0 = tt.make_tensor_ptr %arg0, [%size_m, %size_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %sz1 = arith.constant 1 : i64 + %sz_out_m = arith.constant 4 : i64 + %sz_out_n = arith.constant 8 : i64 + %2 = tt.make_tensor_ptr %arg1, [%sz_out_m, %sz_out_n], [%sz_out_n, %sz1], [%c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V3 hit (make_tensor_ptr, rank-3): non-power-of-two low-dim stride uses +// triton_stride_load with one base offset, 3 strides and 3 numels. +// CHECK-LABEL: func.func @mtpt_3d_low_stride3 +// CHECK-NOT: call @triton_indirect_load +// CHECK: call @triton_stride_load +// CHECK-SAME: : (memref, i64, f32, i64, i64, i64, i64, i64, i64) -> tensor<2x4x8xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_3d_low_stride3(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %size_b = arith.constant 16 : i64 + %size_m = arith.constant 64 : i64 + %size_n = arith.constant 256 : i64 + %stride_b = arith.constant 16384 : i64 + %stride_m = arith.constant 256 : i64 + %stride_n = arith.constant 3 : i64 + %0 = tt.make_tensor_ptr %arg0, [%size_b, %size_m, %size_n], [%stride_b, %stride_m, %stride_n], [%c0, %c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %sz_b = arith.constant 2 : i64 + %sz_m = arith.constant 4 : i64 + %sz_n = arith.constant 8 : i64 + %out_stride_b = arith.constant 32 : i64 + %out_stride_m = arith.constant 8 : i64 + %out_stride_n = arith.constant 1 : i64 + %2 = tt.make_tensor_ptr %arg1, [%sz_b, %sz_m, %sz_n], [%out_stride_b, %out_stride_m, %out_stride_n], [%c0, %c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1 rank-4 fallback (make_tensor_ptr): stride_load only supports 1-3D, so +// rank-4 static non-power-of-two stride still uses triton_indirect_load. +// CHECK-LABEL: func.func @mtpt_4d_low_stride3_indirect +// CHECK-NOT: call @triton_stride_load +// CHECK: call @triton_indirect_load +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_4d_low_stride3_indirect(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %size_b = arith.constant 8 : i64 + %size_h = arith.constant 16 : i64 + %size_m = arith.constant 64 : i64 + %size_n = arith.constant 256 : i64 + %stride_b = arith.constant 262144 : i64 + %stride_h = arith.constant 16384 : i64 + %stride_m = arith.constant 256 : i64 + %stride_n = arith.constant 3 : i64 + %0 = tt.make_tensor_ptr %arg0, [%size_b, %size_h, %size_m, %size_n], [%stride_b, %stride_h, %stride_m, %stride_n], [%c0, %c0, %c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %sz_b = arith.constant 2 : i64 + %sz_h = arith.constant 3 : i64 + %sz_m = arith.constant 4 : i64 + %sz_n = arith.constant 8 : i64 + %out_stride_b = arith.constant 96 : i64 + %out_stride_h = arith.constant 32 : i64 + %out_stride_m = arith.constant 8 : i64 + %out_stride_n = arith.constant 1 : i64 + %2 = tt.make_tensor_ptr %arg1, [%sz_b, %sz_h, %sz_m, %sz_n], [%out_stride_b, %out_stride_h, %out_stride_m, %out_stride_n], [%c0, %c0, %c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1 miss (make_tensor_ptr, non-permuted, low-dim stride 1): +// strides=[8, 1] = contiguous -> last-dim stride 1, V1 not hit +// CHECK-LABEL: func.func @mtpt_low_stride1 +// CHECK-NOT: call @triton_indirect_load +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_low_stride1(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %size_m = arith.constant 4 : i64 + %size_n = arith.constant 8 : i64 + %stride_m = arith.constant 8 : i64 + %stride_n = arith.constant 1 : i64 + %0 = tt.make_tensor_ptr %arg0, [%size_m, %size_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %2 = tt.make_tensor_ptr %arg1, [%size_m, %size_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1 rank-1 miss (make_tensor_ptr, static power-of-two stride 8): +// CHECK-LABEL: func.func @mtpt_1d_stride8_pow2 +// CHECK-NOT: call @triton_indirect_load +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_1d_stride8_pow2(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %t = arith.constant 2048 : i64 + %stride = arith.constant 8 : i64 + %0 = tt.make_tensor_ptr %arg0, [%t], [%stride], [%c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %sz1 = arith.constant 1 : i64 + %2 = tt.make_tensor_ptr %arg1, [%t], [%sz1], [%c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1 rank-1 hit (make_tensor_ptr, static non-power-of-two stride 7): +// CHECK-LABEL: func.func @mtpt_1d_stride7 +// CHECK-NOT: call @triton_indirect_load +// CHECK: call @triton_stride_load +// CHECK-SAME: : (memref, i64, f32, i64, i64) -> tensor<128xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_1d_stride7(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %t = arith.constant 2048 : i64 + %stride = arith.constant 7 : i64 + %0 = tt.make_tensor_ptr %arg0, [%t], [%stride], [%c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %sz1 = arith.constant 1 : i64 + %2 = tt.make_tensor_ptr %arg1, [%t], [%sz1], [%c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1 rank-1 miss (make_tensor_ptr, dynamic stride): +// Runtime stride may be 1 or power-of-two, so keep the structured SIMD path. +// CHECK-LABEL: func.func @mtpt_1d_dynamic_stride +// CHECK-NOT: call @triton_indirect_load +// CHECK-NOT: call @triton_stride_load +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_1d_dynamic_stride(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %stride: i64) { + %c0 = arith.constant 0 : i32 + %t = arith.constant 2048 : i64 + %0 = tt.make_tensor_ptr %arg0, [%t], [%stride], [%c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %sz1 = arith.constant 1 : i64 + %2 = tt.make_tensor_ptr %arg1, [%t], [%sz1], [%c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1 miss (make_tensor_ptr, permuted - ImplicitPermute should handle, V1 must stay out): +// strides=[1, 8], order=[0,1] -> permuted; ImplicitPermute rewrites with tt.trans, +// V1 sees ImplicitPermuteHandledTAG and bails. +// CHECK-LABEL: func.func @mtpt_permuted +// CHECK-NOT: call @triton_indirect_load +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_permuted(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %size_m = arith.constant 4 : i64 + %size_n = arith.constant 8 : i64 + %stride_m = arith.constant 1 : i64 + %stride_n = arith.constant 8 : i64 + %0 = tt.make_tensor_ptr %arg0, [%size_m, %size_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %out_stride_m = arith.constant 8 : i64 + %out_stride_n = arith.constant 1 : i64 + %2 = tt.make_tensor_ptr %arg1, [%size_m, %size_n], [%out_stride_m, %out_stride_n], [%c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V2 hit (AddPtr Store, 1D, static non-power-of-two stride 3): +// tt.store(tt.addptr(splat ptr, arange*3), value) -> tt.stride_store -> call @triton_stride_store +// CHECK-LABEL: func.func @addptr_store_stride3_1d +// CHECK-NOT: call @triton_indirect_store +// CHECK: call @triton_stride_store(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) : (memref, tensor<256xf32>, i64, i64, i64) -> () +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_store_stride3_1d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c3 = arith.constant dense<3> : tensor<256xi32> + %0 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %1 = arith.muli %0, %c3 : tensor<256xi32> + %2 = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %3 = tt.addptr %2, %0 : tensor<256x!tt.ptr>, tensor<256xi32> + %4 = tt.load %3 : tensor<256x!tt.ptr> + %5 = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %6 = tt.addptr %5, %1 : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %6, %4 : tensor<256x!tt.ptr> + tt.return + } +} + +// ----- +// V2 miss (AddPtr Store, stride == 1): +// CHECK-LABEL: func.func @addptr_store_stride1_1d +// CHECK-NOT: call @triton_indirect_store +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @addptr_store_stride1_1d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %0 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %1 = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<256x!tt.ptr>, tensor<256xi32> + %3 = tt.load %2 : tensor<256x!tt.ptr> + %4 = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %5, %3 : tensor<256x!tt.ptr> + tt.return + } +} + +// ----- +// V2 hit (make_tensor_ptr Store, non-permuted, low-dim non-power-of-two stride 5): +// CHECK-LABEL: func.func @mtpt_store_low_stride5 +// CHECK-NOT: call @triton_indirect_store +// CHECK: call @triton_stride_store(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) : (memref, tensor<4x8xf32>, i64, i64, i64, i64, i64) -> () +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_store_low_stride5(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %size_m = arith.constant 64 : i64 + %size_n = arith.constant 256 : i64 + %stride_m = arith.constant 256 : i64 + %stride_n = arith.constant 5 : i64 + %sz1 = arith.constant 1 : i64 + %sz_in_m = arith.constant 4 : i64 + %sz_in_n = arith.constant 8 : i64 + %0 = tt.make_tensor_ptr %arg0, [%sz_in_m, %sz_in_n], [%sz_in_n, %sz1], [%c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %2 = tt.make_tensor_ptr %arg1, [%size_m, %size_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V2 rank-4 fallback (make_tensor_ptr Store): stride_store only supports 1-3D, +// so rank-4 static non-power-of-two stride still uses triton_indirect_store. +// CHECK-LABEL: func.func @mtpt_store_4d_low_stride5_indirect +// CHECK-NOT: call @triton_stride_store +// CHECK: call @triton_indirect_store +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_store_4d_low_stride5_indirect(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %size_b = arith.constant 8 : i64 + %size_h = arith.constant 16 : i64 + %size_m = arith.constant 64 : i64 + %size_n = arith.constant 256 : i64 + %stride_b = arith.constant 327680 : i64 + %stride_h = arith.constant 20480 : i64 + %stride_m = arith.constant 256 : i64 + %stride_n = arith.constant 5 : i64 + %sz_b = arith.constant 2 : i64 + %sz_h = arith.constant 3 : i64 + %sz_m = arith.constant 4 : i64 + %sz_n = arith.constant 8 : i64 + %in_stride_b = arith.constant 96 : i64 + %in_stride_h = arith.constant 32 : i64 + %in_stride_m = arith.constant 8 : i64 + %in_stride_n = arith.constant 1 : i64 + %0 = tt.make_tensor_ptr %arg0, [%sz_b, %sz_h, %sz_m, %sz_n], [%in_stride_b, %in_stride_h, %in_stride_m, %in_stride_n], [%c0, %c0, %c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %2 = tt.make_tensor_ptr %arg1, [%size_b, %size_h, %size_m, %size_n], [%stride_b, %stride_h, %stride_m, %stride_n], [%c0, %c0, %c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V2 miss (make_tensor_ptr Store, dynamic stride): +// Runtime stride may be 1 or power-of-two, so keep the structured SIMD path. +// CHECK-LABEL: func.func @mtpt_store_dynamic_stride +// CHECK-NOT: call @triton_indirect_store +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_store_dynamic_stride(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %stride: i64) { + %c0 = arith.constant 0 : i32 + %sz1 = arith.constant 1 : i64 + %t = arith.constant 2048 : i64 + %0 = tt.make_tensor_ptr %arg0, [%t], [%sz1], [%c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %2 = tt.make_tensor_ptr %arg1, [%t], [%stride], [%c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V2 miss (make_tensor_ptr Store, contiguous): +// CHECK-LABEL: func.func @mtpt_store_low_stride1 +// CHECK-NOT: call @triton_indirect_store +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_store_low_stride1(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %size_m = arith.constant 4 : i64 + %size_n = arith.constant 8 : i64 + %stride_m = arith.constant 8 : i64 + %stride_n = arith.constant 1 : i64 + %0 = tt.make_tensor_ptr %arg0, [%size_m, %size_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %2 = tt.make_tensor_ptr %arg1, [%size_m, %size_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V3 hit (make_tensor_ptr Load + boundary_check + PAD_ZERO, rank-2): +// zero padding is represented by per-dimension numel operands. +// CHECK-LABEL: func.func @mtpt_load_boundary_pad_zero +// CHECK-NOT: call @triton_indirect_load +// CHECK: call @triton_stride_load +// CHECK-SAME: : (memref, i64, f32, i64, i64, i64, i64) -> tensor<8x8xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_load_boundary_pad_zero(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %parent_m = arith.constant 7 : i64 + %parent_n = arith.constant 5 : i64 + %stride_m = arith.constant 20 : i64 + %stride_n = arith.constant 5 : i64 + %0 = tt.make_tensor_ptr %arg0, [%parent_m, %parent_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + %1 = tt.load %0 {boundaryCheck = array, padding = 1 : i32} + : !tt.ptr> + %out_stride_m = arith.constant 8 : i64 + %out_stride_n = arith.constant 1 : i64 + %sz8 = arith.constant 8 : i64 + %2 = tt.make_tensor_ptr %arg1, [%sz8, %sz8], [%out_stride_m, %out_stride_n], [%c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1.5 hit (make_tensor_ptr Load + boundary_check + PAD_NAN, low-dim non-power-of-two stride 5): +// CHECK-LABEL: func.func @mtpt_load_boundary_pad_nan +// CHECK-NOT: call @triton_indirect_load +// CHECK: call @triton_stride_load +// CHECK-SAME: : (memref, i64, f32, i64, i64, i64, i64) -> tensor<8x8xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_load_boundary_pad_nan(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %parent_m = arith.constant 7 : i64 + %parent_n = arith.constant 5 : i64 + %stride_m = arith.constant 20 : i64 + %stride_n = arith.constant 5 : i64 + %0 = tt.make_tensor_ptr %arg0, [%parent_m, %parent_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + %1 = tt.load %0 {boundaryCheck = array, padding = 2 : i32} + : !tt.ptr> + %out_stride_m = arith.constant 8 : i64 + %out_stride_n = arith.constant 1 : i64 + %sz8 = arith.constant 8 : i64 + %2 = tt.make_tensor_ptr %arg1, [%sz8, %sz8], [%out_stride_m, %out_stride_n], [%c0, %c0] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1.5 hit (make_tensor_ptr Load + boundary_check + SCALAR-AddPtr base): +// kernel pattern `tl.make_block_ptr(s + bos*H + i_h, ...)` -- the base is a +// chain of scalar AddPtr ops, NOT the raw function-arg ptr. V1.5 must unwrap +// the chain so the lowered call's src is `memref` (full underlying), +// NOT `memref<1xf32, strided<[1], offset: ?>>` (size-1 view that would make +// per-element offsets index out of bounds). +// +// Regression test for the chunk_local_cumsum precision bug fixed 2026-05-28. +// CHECK-LABEL: func.func @mtpt_load_scalar_addptr_base +// CHECK-NOT: call @triton_indirect_load +// CHECK-NOT: memref<1xf32 +// CHECK: call @triton_stride_load +// CHECK-SAME: : (memref, i64, f32, i64, i64) -> tensor<128xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_load_scalar_addptr_base(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %i_b: i32, %i_h: i32, %i_t: i32) { + // base = arg0 + i_b*T*H + i_h, where T=2048, H=7 statically + %c0 = arith.constant 0 : i32 + %c7 = arith.constant 7 : i32 + %c2048 = arith.constant 2048 : i32 + %bos = arith.muli %i_b, %c2048 : i32 + %bosH = arith.muli %bos, %c7 : i32 + %off1 = arith.addi %bosH, %i_h : i32 + %base = tt.addptr %arg0, %off1 : !tt.ptr, i32 + %t = arith.constant 2048 : i64 + %h = arith.constant 7 : i64 + %off_t = arith.muli %i_t, %c7 : i32 + %0 = tt.make_tensor_ptr %base, [%t], [%h], [%off_t] + {order = array} : > + %1 = tt.load %0 {boundaryCheck = array, padding = 1 : i32} + : !tt.ptr> + %sz1 = arith.constant 1 : i64 + %2 = tt.make_tensor_ptr %arg1, [%t], [%sz1], [%off_t] + {order = array} : > + tt.store %2, %1 : !tt.ptr> + tt.return + } +} + +// ----- +// V1.5 hit (make_tensor_ptr Store + boundary_check, low-dim non-power-of-two stride 5): +// Store boundary is represented by per-dimension numel operands. +// CHECK-LABEL: func.func @mtpt_store_boundary +// CHECK-NOT: call @triton_indirect_store +// CHECK: call @triton_stride_store(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) : (memref, tensor<8x8xf32>, i64, i64, i64, i64, i64) -> () +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_store_boundary(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %sz8 = arith.constant 8 : i64 + %sz1 = arith.constant 1 : i64 + %in_stride_m = arith.constant 8 : i64 + %0 = tt.make_tensor_ptr %arg0, [%sz8, %sz8], [%in_stride_m, %sz1], [%c0, %c0] + {order = array} : > + %1 = tt.load %0 : !tt.ptr> + %parent_m = arith.constant 7 : i64 + %parent_n = arith.constant 5 : i64 + %stride_m = arith.constant 20 : i64 + %stride_n = arith.constant 5 : i64 + %2 = tt.make_tensor_ptr %arg1, [%parent_m, %parent_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + tt.store %2, %1 {boundaryCheck = array} : !tt.ptr> + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_without_prior_same_ptr_store +// CHECK: call @triton_indirect_load(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = false} : (memref, tensor<128xi64>, tensor<128xi1>, tensor<128xi32>) -> tensor<128xi32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_without_prior_same_ptr_store(%arg0: !tt.ptr, + %arg1: !tt.ptr) { + %zero = arith.constant dense<0> : tensor<128xi32> + %range = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %mask = arith.cmpi sge, %range, %zero : tensor<128xi32> + %src_splat = tt.splat %arg0 : !tt.ptr -> tensor<128x!tt.ptr> + %src_ptr = tt.addptr %src_splat, %range : tensor<128x!tt.ptr>, tensor<128xi32> + %dst_splat = tt.splat %arg1 : !tt.ptr -> tensor<128x!tt.ptr> + %dst_ptr = tt.addptr %dst_splat, %range : tensor<128x!tt.ptr>, tensor<128xi32> + %value = tt.load %src_ptr, %mask, %zero {MixCompileDiscreteMask} : tensor<128x!tt.ptr> + tt.store %dst_ptr, %value, %mask {MixCompileDiscreteMask} : tensor<128x!tt.ptr> + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_after_prior_same_ptr_store +// CHECK: call @triton_indirect_load(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = true} : (memref, tensor<128xi64>, tensor<128xi1>, tensor<128xi32>) -> tensor<128xi32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_after_prior_same_ptr_store(%arg0: !tt.ptr, + %arg1: !tt.ptr) { + %one = arith.constant dense<1> : tensor<128xi32> + %zero = arith.constant dense<0> : tensor<128xi32> + %range = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %mask = arith.cmpi sge, %range, %zero : tensor<128xi32> + %src_splat = tt.splat %arg0 : !tt.ptr -> tensor<128x!tt.ptr> + %src_ptr = tt.addptr %src_splat, %range : tensor<128x!tt.ptr>, tensor<128xi32> + %dst_splat = tt.splat %arg1 : !tt.ptr -> tensor<128x!tt.ptr> + %dst_ptr = tt.addptr %dst_splat, %range : tensor<128x!tt.ptr>, tensor<128xi32> + tt.store %src_ptr, %one, %mask {MixCompileDiscreteMask} : tensor<128x!tt.ptr> + %value = tt.load %src_ptr, %mask, %zero {MixCompileDiscreteMask} : tensor<128x!tt.ptr> + tt.store %dst_ptr, %value, %mask {MixCompileDiscreteMask} : tensor<128x!tt.ptr> + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_unknown_root_int_to_ptr +// CHECK: call @triton_indirect_load(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = true} : (memref<1xi32, strided<[1]>>, tensor<128xi64>, tensor<128xi1>, tensor<128xi32>) -> tensor<128xi32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_unknown_root_int_to_ptr(%arg0: !tt.ptr) { + %base_i64 = arith.constant 1024 : i64 + %base = tt.int_to_ptr %base_i64 : i64 -> !tt.ptr + %zero = arith.constant dense<0> : tensor<128xi32> + %range = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %mask = arith.cmpi sge, %range, %zero : tensor<128xi32> + %src_splat = tt.splat %base : !tt.ptr -> tensor<128x!tt.ptr> + %src_ptr = tt.addptr %src_splat, %range : tensor<128x!tt.ptr>, tensor<128xi32> + %dst_splat = tt.splat %arg0 : !tt.ptr -> tensor<128x!tt.ptr> + %dst_ptr = tt.addptr %dst_splat, %range : tensor<128x!tt.ptr>, tensor<128xi32> + %value = tt.load %src_ptr, %mask, %zero {MixCompileDiscreteMask} : tensor<128x!tt.ptr> + tt.store %dst_ptr, %value, %mask {MixCompileDiscreteMask} : tensor<128x!tt.ptr> + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_store_in_preceding_scf_if +// CHECK: call @triton_indirect_load(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = true} : (memref, tensor<16xi64>, tensor<16xi1>, tensor<16xi32>) -> tensor<16xi32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_store_in_preceding_scf_if(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %cond1: i1, + %cond2: i1) { + %zero = arith.constant dense<0> : tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %c3 = arith.constant dense<3> : tensor<16xi32> + %offsets = arith.muli %range, %c3 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offsets : tensor<16x!tt.ptr>, tensor<16xi32> + %mask = arith.cmpi sge, %range, %zero : tensor<16xi32> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + scf.if %cond1 { + %one = arith.constant dense<1> : tensor<16xi32> + tt.store %src_ptr, %one, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + } + %value = scf.if %cond2 -> tensor<16xi32> { + %loaded = tt.load %src_ptr, %mask, %zero {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + scf.yield %loaded : tensor<16xi32> + } else { + scf.yield %zero : tensor<16xi32> + } + tt.store %dst_ptr, %value, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_no_store_in_preceding_scf_if +// CHECK: call @triton_indirect_load(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = false} : (memref, tensor<16xi64>, tensor<16xi1>, tensor<16xi32>) -> tensor<16xi32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_no_store_in_preceding_scf_if(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %cond1: i1, + %cond2: i1) { + %zero = arith.constant dense<0> : tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %c3 = arith.constant dense<3> : tensor<16xi32> + %offsets = arith.muli %range, %c3 : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offsets : tensor<16x!tt.ptr>, tensor<16xi32> + %mask = arith.cmpi sge, %range, %zero : tensor<16xi32> + %other_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %other_ptr = tt.addptr %other_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + scf.if %cond1 { + %one = arith.constant dense<1> : tensor<16xi32> + tt.store %other_ptr, %one, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + } + %value = scf.if %cond2 -> tensor<16xi32> { + %loaded = tt.load %src_ptr, %mask, %zero {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + scf.yield %loaded : tensor<16xi32> + } else { + scf.yield %zero : tensor<16xi32> + } + tt.store %dst_ptr, %value, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_store_in_preceding_scf_for +// CHECK: call @triton_indirect_load(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = true} : (memref, tensor<16xi64>, tensor<16xi1>, tensor<16xi32>) -> tensor<16xi32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_store_in_preceding_scf_for(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %trip: i32) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %zero = arith.constant dense<0> : tensor<16xi32> + %one = arith.constant dense<1> : tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %mask = arith.cmpi sge, %range, %zero : tensor<16xi32> + scf.for %i = %c0_i32 to %trip step %c1_i32 : i32 { + tt.store %src_ptr, %one, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + } + scf.for %j = %c0_i32 to %trip step %c1_i32 : i32 { + %loaded = tt.load %src_ptr, %mask, %zero {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + tt.store %dst_ptr, %loaded, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + } + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_nested_loop_carried_store +// CHECK: call @triton_indirect_load(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = true} : (memref, tensor<16xi64>, tensor<16xi1>, tensor<16xi32>) -> tensor<16xi32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_nested_loop_carried_store(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %outer: i32, + %inner: i32) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %zero = arith.constant dense<0> : tensor<16xi32> + %one = arith.constant dense<1> : tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %mask = arith.cmpi sge, %range, %zero : tensor<16xi32> + scf.for %i = %c0_i32 to %outer step %c1_i32 : i32 { + scf.for %j = %c0_i32 to %inner step %c1_i32 : i32 { + %loaded = tt.load %src_ptr, %mask, %zero {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + tt.store %src_ptr, %one, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + tt.store %dst_ptr, %loaded, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + } + } + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_ignores_loop_store_to_different_memref_root +// CHECK: call @triton_indirect_load_0(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = false} : (memref, tensor<16xi64>, tensor<16xi1>, tensor<16xf32>) -> tensor<16xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_ignores_loop_store_to_different_memref_root(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %arg2: !tt.ptr {tt.divisibility = 16 : i32}, + %trip: i32) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %zero = arith.constant dense<0> : tensor<16xi32> + %zero_f = arith.constant dense<0.000000e+00> : tensor<16xf32> + %c3 = arith.constant dense<3> : tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %mask = arith.cmpi sge, %range, %zero : tensor<16xi32> + %gm_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %gm_ptr = tt.addptr %gm_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %src_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %src_offsets = arith.muli %range, %c3 : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %src_offsets : tensor<16x!tt.ptr>, tensor<16xi32> + %dst_base = tt.splat %arg2 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + scf.for %i = %c0_i32 to %trip step %c1_i32 : i32 { + %local = tt.load %gm_ptr, %mask, %zero_f {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + tt.assert %mask, "mask must be true" : tensor<16xi1> + %indirect = tt.load %src_ptr, %mask, %zero_f {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + %sum = arith.addf %local, %indirect : tensor<16xf32> + tt.store %gm_ptr, %sum, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + tt.store %dst_ptr, %indirect, %mask {MixCompileDiscreteMask} : tensor<16x!tt.ptr> + } + tt.return + } +} + +// ----- +// CHECK-LABEL: func.func @indirect_load_broadcast_loop_carried_different_root +// CHECK: call @triton_indirect_load{{.*}}(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) {isVolatile = false} : (memref, tensor<4x8xi64>, tensor<4x8xi1>, tensor<4x8xf32>) -> tensor<4x8xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @indirect_load_broadcast_loop_carried_different_root(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %trip: i32) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %row_stride = arith.constant dense<8> : tensor<4x1xi32> + %advance = arith.constant dense<32> : tensor<4x8xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<4x8xf32> + %mask = arith.constant dense : tensor<4x8xi1> + %rows = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %rows_2d = tt.expand_dims %rows {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %row_offsets = arith.muli %rows_2d, %row_stride : tensor<4x1xi32> + %cols = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %cols_2d = tt.expand_dims %cols {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %col_offsets = tt.broadcast %cols_2d : tensor<1x8xi32> -> tensor<4x8xi32> + %src_base_1d = tt.splat %arg0 : !tt.ptr -> tensor<4x!tt.ptr> + %src_base = tt.expand_dims %src_base_1d {axis = 1 : i32} : tensor<4x!tt.ptr> -> tensor<4x1x!tt.ptr> + %src_rows = tt.addptr %src_base, %row_offsets : tensor<4x1x!tt.ptr>, tensor<4x1xi32> + %src_broadcast = tt.broadcast %src_rows : tensor<4x1x!tt.ptr> -> tensor<4x8x!tt.ptr> + %src_ptr = tt.addptr %src_broadcast, %col_offsets : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + %dst_base_1d = tt.splat %arg1 : !tt.ptr -> tensor<4x!tt.ptr> + %dst_base = tt.expand_dims %dst_base_1d {axis = 1 : i32} : tensor<4x!tt.ptr> -> tensor<4x1x!tt.ptr> + %dst_rows = tt.addptr %dst_base, %row_offsets : tensor<4x1x!tt.ptr>, tensor<4x1xi32> + %dst_broadcast = tt.broadcast %dst_rows : tensor<4x1x!tt.ptr> -> tensor<4x8x!tt.ptr> + %dst_ptr = tt.addptr %dst_broadcast, %col_offsets : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + %result:2 = scf.for %i = %c0_i32 to %trip step %c1_i32 + iter_args(%src_iter = %src_ptr, %dst_iter = %dst_ptr) + -> (tensor<4x8x!tt.ptr>, tensor<4x8x!tt.ptr>) : i32 { + %value = tt.load %src_iter, %mask, %zero {MixCompileDiscreteMask} : tensor<4x8x!tt.ptr> + tt.store %dst_iter, %value, %mask : tensor<4x8x!tt.ptr> + %src_next = tt.addptr %src_iter, %advance : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + %dst_next = tt.addptr %dst_iter, %advance : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + scf.yield %src_next, %dst_next : tensor<4x8x!tt.ptr>, tensor<4x8x!tt.ptr> + } + tt.return + } +} + +// ----- +// V1 guard (make_tensor_ptr Load): only one tt.advance layer is supported. +// A nested advance must stay on the legacy block-pointer lowering path. +// CHECK-LABEL: func.func @mtpt_nested_advance_bails +// CHECK-NOT: call @triton_indirect_load +// CHECK-NOT: call @triton_indirect_store +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_nested_advance_bails(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %size_m = arith.constant 64 : i64 + %size_n = arith.constant 256 : i64 + %stride_m = arith.constant 256 : i64 + %stride_n = arith.constant 3 : i64 + %src = tt.make_tensor_ptr %arg0, [%size_m, %size_n], [%stride_m, %stride_n], [%c0, %c0] + {order = array} : > + %adv0 = tt.advance %src, [%c1, %c0] : !tt.ptr> + %adv1 = tt.advance %adv0, [%c0, %c1] : !tt.ptr> + %value = tt.load %adv1 : !tt.ptr> + %out_stride_m = arith.constant 8 : i64 + %out_stride_n = arith.constant 1 : i64 + %dst = tt.make_tensor_ptr %arg1, [%size_m, %size_n], [%out_stride_m, %out_stride_n], [%c0, %c0] + {order = array} : > + tt.store %dst, %value : !tt.ptr> + tt.return + } +} + +// ----- +// V1/V2 rank-5 hit (make_tensor_ptr + one-level tt.advance): ranks 4-5 use +// indirect memory ops, and the advance offsets must be folded into the +// effective block-pointer offsets for both the load and store paths. +// CHECK-LABEL: func.func @mtpt_advance_5d_indirect_load_store +// CHECK: call @triton_indirect_load +// CHECK: tensor<2x2x2x2x4xi64> +// CHECK: call @triton_indirect_store +// CHECK: tensor<2x2x2x2x4xi64> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @mtpt_advance_5d_indirect_load_store(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %c3 = arith.constant 3 : i32 + %size_0 = arith.constant 4 : i64 + %size_1 = arith.constant 4 : i64 + %size_2 = arith.constant 4 : i64 + %size_3 = arith.constant 4 : i64 + %size_4 = arith.constant 64 : i64 + %src_stride_0 = arith.constant 4096 : i64 + %src_stride_1 = arith.constant 1024 : i64 + %src_stride_2 = arith.constant 256 : i64 + %src_stride_3 = arith.constant 64 : i64 + %src_stride_4 = arith.constant 3 : i64 + %src = tt.make_tensor_ptr %arg0, [%size_0, %size_1, %size_2, %size_3, %size_4], [%src_stride_0, %src_stride_1, %src_stride_2, %src_stride_3, %src_stride_4], [%c0, %c0, %c0, %c0, %c0] + {order = array} : > + %src_adv = tt.advance %src, [%c1, %c0, %c2, %c0, %c3] : !tt.ptr> + %value = tt.load %src_adv : !tt.ptr> + %dst_stride_0 = arith.constant 5120 : i64 + %dst_stride_1 = arith.constant 1280 : i64 + %dst_stride_2 = arith.constant 320 : i64 + %dst_stride_3 = arith.constant 80 : i64 + %dst_stride_4 = arith.constant 5 : i64 + %dst = tt.make_tensor_ptr %arg1, [%size_0, %size_1, %size_2, %size_3, %size_4], [%dst_stride_0, %dst_stride_1, %dst_stride_2, %dst_stride_3, %dst_stride_4], [%c0, %c0, %c0, %c0, %c0] + {order = array} : > + %dst_adv = tt.advance %dst, [%c0, %c3, %c0, %c1, %c2] : !tt.ptr> + tt.store %dst_adv, %value : !tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/sin_cos_simt_route.mlir b/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/sin_cos_simt_route.mlir new file mode 100644 index 0000000000..bbc54f74cd --- /dev/null +++ b/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/sin_cos_simt_route.mlir @@ -0,0 +1,113 @@ +// RUN: triton-opt %s --triton-to-linalg='compile-on-910-95=true' --split-input-file | FileCheck %s --check-prefixes=ON-CHECK-LABEL,ON-CHECK-SAME +// RUN: triton-opt %s --triton-to-linalg='compile-on-910-95=false' --split-input-file | FileCheck %s --check-prefixes=OFF-CHECK-LABEL,OFF-CHECK-SAME + +// isSIMTOp() routes math.sin / math.cos on f16/f32 tensor inputs to the SIMT +// template. Downstream justification (Ascend 950 RegBase normalize, enable-high- +// precision defaults to true): these ops are rewritten into a Payne-Hanek +// range reduction that looks up a 320xi32 2/pi limbs table with two +// hfusion.gather ops, so the kernel must be launched as mix_simd_simt to +// reserve localMemorySize for the SIMT template (otherwise VEC UB +// out-of-bounds, error 341). Observable signal: the converted func.func +// carries parallel_mode = "mix_simd_simt". bf16 is the negative control: +// the downstream high-precision trait only accepts f16/f32, so bf16 sin +// stays SIMD and produces no table gather. + +// ON-CHECK-LABEL: func.func @sin_f32 +// ON-CHECK-SAME: parallel_mode = "mix_simd_simt" +// OFF-CHECK-LABEL: func.func @sin_f32 +// OFF-CHECK-SAME: parallel_mode = "simd" +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @sin_f32(%arg0: !tt.ptr, %arg1: !tt.ptr) attributes {noinline = false} { + %offsets = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %in_ptrs = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %in_addrs = tt.addptr %in_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + %input = tt.load %in_addrs : tensor<1024x!tt.ptr> + %result = math.sin %input : tensor<1024xf32> + %out_ptrs = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %out_addrs = tt.addptr %out_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %out_addrs, %result : tensor<1024x!tt.ptr> + tt.return + } +} + +// ----- + +// ON-CHECK-LABEL: func.func @cos_f32 +// ON-CHECK-SAME: parallel_mode = "mix_simd_simt" +// OFF-CHECK-LABEL: func.func @cos_f32 +// OFF-CHECK-SAME: parallel_mode = "simd" +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @cos_f32(%arg0: !tt.ptr, %arg1: !tt.ptr) attributes {noinline = false} { + %offsets = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %in_ptrs = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %in_addrs = tt.addptr %in_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + %input = tt.load %in_addrs : tensor<1024x!tt.ptr> + %result = math.cos %input : tensor<1024xf32> + %out_ptrs = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %out_addrs = tt.addptr %out_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %out_addrs, %result : tensor<1024x!tt.ptr> + tt.return + } +} + +// ----- + +// ON-CHECK-LABEL: func.func @sin_f16 +// ON-CHECK-SAME: parallel_mode = "mix_simd_simt" +// OFF-CHECK-LABEL: func.func @sin_f16 +// OFF-CHECK-SAME: parallel_mode = "simd" +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @sin_f16(%arg0: !tt.ptr, %arg1: !tt.ptr) attributes {noinline = false} { + %offsets = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %in_ptrs = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %in_addrs = tt.addptr %in_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + %input = tt.load %in_addrs : tensor<1024x!tt.ptr> + %result = math.sin %input : tensor<1024xf16> + %out_ptrs = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %out_addrs = tt.addptr %out_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %out_addrs, %result : tensor<1024x!tt.ptr> + tt.return + } +} + +// ----- + +// ON-CHECK-LABEL: func.func @cos_f16 +// ON-CHECK-SAME: parallel_mode = "mix_simd_simt" +// OFF-CHECK-LABEL: func.func @cos_f16 +// OFF-CHECK-SAME: parallel_mode = "simd" +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @cos_f16(%arg0: !tt.ptr, %arg1: !tt.ptr) attributes {noinline = false} { + %offsets = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %in_ptrs = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %in_addrs = tt.addptr %in_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + %input = tt.load %in_addrs : tensor<1024x!tt.ptr> + %result = math.cos %input : tensor<1024xf16> + %out_ptrs = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %out_addrs = tt.addptr %out_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %out_addrs, %result : tensor<1024x!tt.ptr> + tt.return + } +} + +// ----- + +// Negative control: bf16 input does not match the f16/f32 filter, so sin +// stays SIMD (no downstream high-precision table gather for bf16). +// ON-CHECK-LABEL: func.func @sin_bf16 +// ON-CHECK-SAME: parallel_mode = "simd" +// OFF-CHECK-LABEL: func.func @sin_bf16 +// OFF-CHECK-SAME: parallel_mode = "simd" +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @sin_bf16(%arg0: !tt.ptr, %arg1: !tt.ptr) attributes {noinline = false} { + %offsets = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %in_ptrs = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %in_addrs = tt.addptr %in_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + %input = tt.load %in_addrs : tensor<1024x!tt.ptr> + %result = math.sin %input : tensor<1024xbf16> + %out_ptrs = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %out_addrs = tt.addptr %out_ptrs, %offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %out_addrs, %result : tensor<1024x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/strided_axis_coalescing.mlir b/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/strided_axis_coalescing.mlir new file mode 100644 index 0000000000..ea352fa6cd --- /dev/null +++ b/third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/strided_axis_coalescing.mlir @@ -0,0 +1,307 @@ +// RUN: triton-opt %s --triton-to-unstructure='compile-on-910-95=true compile-mode=simd_simt_template' \ +// RUN: --triton-to-linalg='compile-on-910-95=true compile-mode=simd_simt_template' --split-input-file \ +// RUN: | FileCheck %s + +// ----- +// The `pid % S` strided block-pointer shape is owned by StridedAxisCoalescing, +// before Chunk and StridedLoadStoreRewrite. It folds the S heads into the +// inner tensor dimension and records the launch-axis shrink metadata. +// CHECK-LABEL: module attributes { +// CHECK: hacc.coalesce_axis = 0 : i32 +// CHECK: hacc.coalesce_factor = 4 : i32 +// CHECK-LABEL: func.func @strided_axis_coalesce +// CHECK: memref.reinterpret_cast +// CHECK-SAME: sizes: [16, 4] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_coalesce(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c4_i32 = arith.constant 4 : i32 + %c16_i64 = arith.constant 16 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id x : i32 + %head = arith.remsi %pid, %c4_i32 : i32 + %src_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src = tt.make_tensor_ptr %src_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value = tt.load %src : !tt.ptr> + %twice = arith.addf %value, %value : tensor<16xf32> + %dst_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %dst = tt.make_tensor_ptr %dst_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst, %twice : !tt.ptr> + tt.return + } +} + +// ----- +// Reading num_programs on the coalesced grid axis changes its visible value +// after launch-grid division. Keep the original shape and do not claim the +// coalesce metadata. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @strided_axis_reads_num_programs +// CHECK-NOT: sizes: [16, 4] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_reads_num_programs(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c4_i32 = arith.constant 4 : i32 + %c16_i64 = arith.constant 16 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id x : i32 + %num = tt.get_num_programs x : i32 + %head = arith.remsi %pid, %c4_i32 : i32 + %src_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src = tt.make_tensor_ptr %src_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value = tt.load %src : !tt.ptr> + %num_splat = tt.splat %num : i32 -> tensor<16xi32> + %num_f = arith.sitofp %num_splat : tensor<16xi32> to tensor<16xf32> + %sum = arith.addf %value, %num_f : tensor<16xf32> + %dst_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %dst = tt.make_tensor_ptr %dst_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst, %sum : !tt.ptr> + tt.return + } +} + +// ----- +// The folded head lane may be driven by any launch axis. Keep the axis value +// in the module metadata so the launcher shrinks grid[1], not grid[0]. +// CHECK-LABEL: module attributes { +// CHECK: hacc.coalesce_axis = 1 : i32 +// CHECK: hacc.coalesce_factor = 4 : i32 +// CHECK-LABEL: func.func @strided_axis_coalesce_axis1 +// CHECK: memref.reinterpret_cast +// CHECK-SAME: sizes: [16, 4] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_coalesce_axis1(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c4_i32 = arith.constant 4 : i32 + %c16_i64 = arith.constant 16 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id y : i32 + %head = arith.remsi %pid, %c4_i32 : i32 + %src_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src = tt.make_tensor_ptr %src_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value = tt.load %src : !tt.ptr> + %twice = arith.addf %value, %value : tensor<16xf32> + %dst_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %dst = tt.make_tensor_ptr %dst_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst, %twice : !tt.ptr> + tt.return + } +} + +// ----- +// A scalar per-head load is only lane-liftable when it is data. If it feeds +// an address (an indirect gather), lane expansion would change the pointer +// relation, so the entire strided-axis rewrite must bail. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-LABEL: func.func @strided_axis_per_head_indirect_gather_bails +// CHECK-NOT: sizes: [16, 4] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_per_head_indirect_gather_bails(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %indices: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c4_i32 = arith.constant 4 : i32 + %c16_i64 = arith.constant 16 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id x : i32 + %head = arith.remsi %pid, %c4_i32 : i32 + %src_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src = tt.make_tensor_ptr %src_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value = tt.load %src : !tt.ptr> + %head_index_ptr = tt.addptr %indices, %head : !tt.ptr, i32 + %head_index = tt.load %head_index_ptr : !tt.ptr + %gather_ptr = tt.addptr %arg0, %head_index : !tt.ptr, i32 + %gather = tt.load %gather_ptr : !tt.ptr + %gather_splat = tt.splat %gather : f32 -> tensor<16xf32> + %sum = arith.addf %value, %gather_splat : tensor<16xf32> + %dst_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %dst = tt.make_tensor_ptr %dst_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst, %sum : !tt.ptr> + tt.return + } +} + +// ----- +// Multiple block-pointer loads with the same S are collected as one region and +// must be lifted together before their elementwise merge and matching store. +// CHECK-LABEL: module attributes { +// CHECK: hacc.coalesce_axis = 0 : i32 +// CHECK: hacc.coalesce_factor = 4 : i32 +// CHECK-LABEL: func.func @strided_axis_multiple_same_s_seeds +// Two source block-pointer loads and the destination must all become 2-D. +// CHECK-COUNT-3: memref.reinterpret_cast {{.*}}sizes: [16, 4] +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_multiple_same_s_seeds(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %arg2: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c4_i32 = arith.constant 4 : i32 + %c16_i64 = arith.constant 16 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id x : i32 + %head = arith.remsi %pid, %c4_i32 : i32 + %src0_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src0 = tt.make_tensor_ptr %src0_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %lhs = tt.load %src0 : !tt.ptr> + %src1_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %src1 = tt.make_tensor_ptr %src1_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %rhs = tt.load %src1 : !tt.ptr> + %sum = arith.addf %lhs, %rhs : tensor<16xf32> + %dst_base = tt.addptr %arg2, %head : !tt.ptr, i32 + %dst = tt.make_tensor_ptr %dst_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst, %sum : !tt.ptr> + tt.return + } +} + +// ----- +// Scan along the original T axis remains independent across the newly folded +// S lanes. The original 1-D scan therefore becomes a [BT, S] scan on axis 0. +// CHECK-LABEL: module attributes { +// CHECK: hacc.coalesce_axis = 0 : i32 +// CHECK: hacc.coalesce_factor = 4 : i32 +// CHECK: func.func private @[[$SCAN:triton_cumsum[_0-9]*]](tensor<16x4xf32>, i32, i1) -> tensor<16x4xf32> +// CHECK-LABEL: func.func @strided_axis_scan_axis0 +// CHECK: memref.reinterpret_cast +// CHECK-SAME: sizes: [16, 4] +// CHECK: call @[[$SCAN]]({{.*}}) : (tensor<16x4xf32>, i32, i1) -> tensor<16x4xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_scan_axis0(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c4_i32 = arith.constant 4 : i32 + %c16_i64 = arith.constant 16 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id x : i32 + %head = arith.remsi %pid, %c4_i32 : i32 + %src_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src = tt.make_tensor_ptr %src_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value = tt.load %src : !tt.ptr> + %scanned = "tt.scan"(%value) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %sum = arith.addf %a, %b : f32 + tt.scan.return %sum : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + %dst_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %dst = tt.make_tensor_ptr %dst_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst, %scanned : !tt.ptr> + tt.return + } +} + +// ----- +// A T-axis reduction becomes one independent reduction per folded S lane. A +// following scalar splat must be rebuilt as an S-lane broadcast, not collapsed. +// CHECK-LABEL: module attributes { +// CHECK: hacc.coalesce_axis = 0 : i32 +// CHECK: hacc.coalesce_factor = 4 : i32 +// CHECK-LABEL: func.func @strided_axis_reduce_axis0 +// CHECK: memref.reinterpret_cast +// CHECK-SAME: sizes: [16, 4] +// CHECK: linalg.reduce ins({{.*}} : tensor<16x4xf32>) outs({{.*}} : tensor<4xf32>) dimensions = [0] +// CHECK: linalg.broadcast ins({{.*}} : tensor<4xf32>) outs({{.*}} : tensor<16x4xf32>) dimensions = [0] +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_reduce_axis0(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c4_i32 = arith.constant 4 : i32 + %c16_i64 = arith.constant 16 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id x : i32 + %head = arith.remsi %pid, %c4_i32 : i32 + %src_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src = tt.make_tensor_ptr %src_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value = tt.load %src : !tt.ptr> + %reduced = "tt.reduce"(%value) <{axis = 0 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %sum = arith.addf %a, %b : f32 + tt.reduce.return %sum : f32 + }) : (tensor<16xf32>) -> f32 + %splat = tt.splat %reduced : f32 -> tensor<16xf32> + %dst_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %dst = tt.make_tensor_ptr %dst_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst, %splat : !tt.ptr> + tt.return + } +} + +// ----- +// A candidate is valid only when the stride agrees with the ih split. Here +// the block pointer has stride 4 but its base uses `pid % 2`, so Axis must not +// claim it or write coalescing metadata. +// CHECK-LABEL: module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +// CHECK-NOT: hacc.coalesce_factor +// CHECK-NOT: hacc.coalesce_axis +// CHECK-LABEL: func.func @strided_axis_stride_ih_mismatch_bails +// CHECK-NOT: sizes: [16, 4] +// CHECK: memref.reinterpret_cast +// CHECK-SAME: sizes: [16], strides: [4] +// CHECK-NOT: sizes: [16, 4] +// CHECK: return +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_stride_ih_mismatch_bails(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c2_i32 = arith.constant 2 : i32 + %c16_i64 = arith.constant 16 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id x : i32 + %head = arith.remsi %pid, %c2_i32 : i32 + %src_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src = tt.make_tensor_ptr %src_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value = tt.load %src : !tt.ptr> + %dst_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %dst = tt.make_tensor_ptr %dst_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst, %value : !tt.ptr> + tt.return + } +} + +// ----- +// Preserve the historical first-seed behavior: S and BT are selected from the +// first seed. A later same-S seed with BT=8 is still lifted using the first +// seed's BT=16 backing tile and is sliced back to its original [8, 4] region. +// CHECK-LABEL: module attributes { +// CHECK: hacc.coalesce_axis = 0 : i32 +// CHECK: hacc.coalesce_factor = 4 : i32 +// CHECK-LABEL: func.func @strided_axis_same_s_first_seed_bt +// CHECK-COUNT-4: memref.reinterpret_cast {{.*}}sizes: [16, 4] +// CHECK: tensor.extract_slice {{.*}}[8, 4] {{.*}}tensor<16x4xf32> to tensor<8x4xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @strided_axis_same_s_first_seed_bt(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %arg2: !tt.ptr {tt.divisibility = 16 : i32}, + %arg3: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_i32 = arith.constant 0 : i32 + %c4_i32 = arith.constant 4 : i32 + %c16_i64 = arith.constant 16 : i64 + %c8_i64 = arith.constant 8 : i64 + %c4_i64 = arith.constant 4 : i64 + %pid = tt.get_program_id x : i32 + %head = arith.remsi %pid, %c4_i32 : i32 + + %src0_base = tt.addptr %arg0, %head : !tt.ptr, i32 + %src0 = tt.make_tensor_ptr %src0_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value0 = tt.load %src0 : !tt.ptr> + %dst0_base = tt.addptr %arg1, %head : !tt.ptr, i32 + %dst0 = tt.make_tensor_ptr %dst0_base, [%c16_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst0, %value0 : !tt.ptr> + + %src1_base = tt.addptr %arg2, %head : !tt.ptr, i32 + %src1 = tt.make_tensor_ptr %src1_base, [%c8_i64], [%c4_i64], [%c0_i32] {order = array} : > + %value1 = tt.load %src1 : !tt.ptr> + %dst1_base = tt.addptr %arg3, %head : !tt.ptr, i32 + %dst1 = tt.make_tensor_ptr %dst1_base, [%c8_i64], [%c4_i64], [%c0_i32] {order = array} : > + tt.store %dst1, %value1 : !tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/950PR/TritonToUnstructure/indirect_mem_access.mlir b/third_party/ascend/unittest/Conversion/950PR/TritonToUnstructure/indirect_mem_access.mlir new file mode 100644 index 0000000000..b268cab496 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/950PR/TritonToUnstructure/indirect_mem_access.mlir @@ -0,0 +1,219 @@ +// RUN: triton-opt %s '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' | FileCheck %s + +// CHECK-LABEL: tt.func @triton_indirect_load +// CHECK: ascend.indirect_load {{.*}} -> tensor<1024xi32> +tt.func @triton_indirect_load(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<0> : tensor<1024xi32> + %cst_0 = arith.constant dense<200> : tensor<1024xi32> + %cst_1 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst_0 : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_1 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %5, %3, %cst {MixCompileDiscreteMask} : tensor<1024x!tt.ptr> + tt.store %7, %8 : tensor<1024x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @triton_indirect_store +// CHECK: ascend.indirect_store {{.*}} +tt.func @triton_indirect_store(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst_0 = arith.constant dense<200> : tensor<1024xi32> + %cst_1 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst_0 : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_1 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %5 : tensor<1024x!tt.ptr> + tt.store %7, %8, %3 {MixCompileDiscreteMask} : tensor<1024x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @discrete_highrank_and_structured_lowrank_loadstore_2d +// CHECK: ascend.indirect_load {{.*}} -> tensor<2x16xbf16> +// CHECK: ascend.indirect_store {{.*}} +tt.func @discrete_highrank_and_structured_lowrank_loadstore_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<2.000000e+00> : tensor<2x16xbf16> + %cst_0 = arith.constant dense<16> : tensor<2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %3, %cst_0 : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = arith.addf %12, %cst : tensor<2x16xbf16> + tt.store %11, %13 : tensor<2x16x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @discrete_highrank_and_structured_lowrank_loadstore_3d +// CHECK: ascend.indirect_load {{.*}} -> tensor<2x2x8xbf16> +// CHECK: ascend.indirect_store {{.*}} +tt.func @discrete_highrank_and_structured_lowrank_loadstore_3d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<3.000000e+00> : tensor<2x2x8xbf16> + %cst_0 = arith.constant dense<8> : tensor<2x2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.expand_dims %0 {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> + %5 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %6 = tt.expand_dims %5 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %7 = tt.expand_dims %6 {axis = 1 : i32} : tensor<1x8xi32> -> tensor<1x1x8xi32> + %8 = arith.muli %3, %cst_1 : tensor<2x1xi32> + %9 = tt.broadcast %8 : tensor<2x1xi32> -> tensor<2x2xi32> + %10 = tt.broadcast %4 : tensor<1x2xi32> -> tensor<2x2xi32> + %11 = arith.addi %9, %10 : tensor<2x2xi32> + %12 = tt.expand_dims %11 {axis = 2 : i32} : tensor<2x2xi32> -> tensor<2x2x1xi32> + %13 = arith.muli %12, %cst_0 : tensor<2x2x1xi32> + %14 = tt.broadcast %13 : tensor<2x2x1xi32> -> tensor<2x2x8xi32> + %15 = tt.broadcast %7 : tensor<1x1x8xi32> -> tensor<2x2x8xi32> + %16 = arith.addi %14, %15 : tensor<2x2x8xi32> + %17 = tt.splat %arg0 : !tt.ptr -> tensor<2x2x8x!tt.ptr> + %18 = tt.addptr %17, %16 : tensor<2x2x8x!tt.ptr>, tensor<2x2x8xi32> + %19 = tt.load %18 : tensor<2x2x8x!tt.ptr> + %20 = arith.addf %19, %cst : tensor<2x2x8xbf16> + tt.store %18, %20 : tensor<2x2x8x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @discrete_highrank_and_structured_lowrank_loadstore_4d +// CHECK: %[[VAL_0:.*]] = scf.for %[[VAL_1:.*]] = %{{.*}} to %{{.*}} step %{{.*}} iter_args(%[[VAL_2:.*]] = %{{.*}}) -> (tensor<2x2x2x8xbf16>) { +// CHECK: %[[VAL_3:.*]] = tensor.extract_slice %{{.*}}{{\[}}%[[VAL_1]], 0, 0, 0] [1, 2, 2, 8] [1, 1, 1, 1] {DiscreteMemAccess} : tensor<2x2x2x8xi64> to tensor<1x2x2x8xi64> +// CHECK: %[[VAL_4:.*]] = tt.splat %{{.*}} : !tt.ptr -> tensor<1x2x2x8x!tt.ptr> +// CHECK: %[[VAL_5:.*]] = tt.addptr %[[VAL_4]], %[[VAL_3]] : tensor<1x2x2x8x!tt.ptr>, tensor<1x2x2x8xi64> +// CHECK: %[[VAL_6:.*]] = tt.load %[[VAL_5]] {DiscreteMemAccess} : tensor<1x2x2x8x!tt.ptr> +// CHECK: %[[VAL_7:.*]] = tensor.insert_slice %[[VAL_6]] into %[[VAL_2]]{{\[}}%[[VAL_1]], 0, 0, 0] [1, 2, 2, 8] [1, 1, 1, 1] : tensor<1x2x2x8xbf16> into tensor<2x2x2x8xbf16> +// CHECK: scf.yield {DiscreteMemAccess} %[[VAL_7]] : tensor<2x2x2x8xbf16> +// CHECK: } {ExtractedLoadOrStore} +// CHECK: %[[VAL_8:.*]] = arith.addf %[[VAL_0]], %{{.*}} : tensor<2x2x2x8xbf16> +// CHECK: scf.for %[[VAL_9:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: %[[VAL_10:.*]] = tensor.extract_slice %{{.*}}{{\[}}%[[VAL_9]], 0, 0, 0] [1, 2, 2, 8] [1, 1, 1, 1] {DiscreteMemAccess} : tensor<2x2x2x8xi64> to tensor<1x2x2x8xi64> +// CHECK: %[[VAL_11:.*]] = tt.splat %{{.*}} : !tt.ptr -> tensor<1x2x2x8x!tt.ptr> +// CHECK: %[[VAL_12:.*]] = tt.addptr %[[VAL_11]], %[[VAL_10]] : tensor<1x2x2x8x!tt.ptr>, tensor<1x2x2x8xi64> +// CHECK: %[[VAL_13:.*]] = tensor.extract_slice %[[VAL_8]]{{\[}}%[[VAL_9]], 0, 0, 0] [1, 2, 2, 8] [1, 1, 1, 1] {DiscreteMemAccess} : tensor<2x2x2x8xbf16> to tensor<1x2x2x8xbf16> +// CHECK: tt.store %[[VAL_12]], %[[VAL_13]] {DiscreteMemAccess} : tensor<1x2x2x8x!tt.ptr> +// CHECK: } {ExtractedLoadOrStore} +tt.func @discrete_highrank_and_structured_lowrank_loadstore_4d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<4.000000e+00> : tensor<2x2x2x8xbf16> + %cst_0 = arith.constant dense<8> : tensor<2x2x2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x2x1xi32> + %cst_2 = arith.constant dense<2> : tensor<2x1x1xi32> + %cst_3 = arith.constant dense<1> : tensor<2x1x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = tt.expand_dims %1 {axis = 2 : i32} : tensor<2x1xi32> -> tensor<2x1x1xi32> + %3 = arith.addi %2, %cst_3 : tensor<2x1x1xi32> + %4 = arith.remsi %3, %cst_2 : tensor<2x1x1xi32> + %5 = tt.expand_dims %0 {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> + %6 = tt.expand_dims %5 {axis = 2 : i32} : tensor<1x2xi32> -> tensor<1x2x1xi32> + %7 = tt.expand_dims %5 {axis = 1 : i32} : tensor<1x2xi32> -> tensor<1x1x2xi32> + %8 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %9 = tt.expand_dims %8 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %10 = tt.expand_dims %9 {axis = 1 : i32} : tensor<1x8xi32> -> tensor<1x1x8xi32> + %11 = tt.expand_dims %10 {axis = 2 : i32} : tensor<1x1x8xi32> -> tensor<1x1x1x8xi32> + %12 = arith.muli %4, %cst_2 : tensor<2x1x1xi32> + %13 = tt.broadcast %12 : tensor<2x1x1xi32> -> tensor<2x2x1xi32> + %14 = tt.broadcast %6 : tensor<1x2x1xi32> -> tensor<2x2x1xi32> + %15 = arith.addi %13, %14 : tensor<2x2x1xi32> + %16 = arith.muli %15, %cst_1 : tensor<2x2x1xi32> + %17 = tt.broadcast %16 : tensor<2x2x1xi32> -> tensor<2x2x2xi32> + %18 = tt.broadcast %7 : tensor<1x1x2xi32> -> tensor<2x2x2xi32> + %19 = arith.addi %17, %18 : tensor<2x2x2xi32> + %20 = tt.expand_dims %19 {axis = 3 : i32} : tensor<2x2x2xi32> -> tensor<2x2x2x1xi32> + %21 = arith.muli %20, %cst_0 : tensor<2x2x2x1xi32> + %22 = tt.broadcast %21 : tensor<2x2x2x1xi32> -> tensor<2x2x2x8xi32> + %23 = tt.broadcast %11 : tensor<1x1x1x8xi32> -> tensor<2x2x2x8xi32> + %24 = arith.addi %22, %23 : tensor<2x2x2x8xi32> + %25 = tt.splat %arg0 : !tt.ptr -> tensor<2x2x2x8x!tt.ptr> + %26 = tt.addptr %25, %24 : tensor<2x2x2x8x!tt.ptr>, tensor<2x2x2x8xi32> + %27 = tt.load %26 : tensor<2x2x2x8x!tt.ptr> + %28 = arith.addf %27, %cst : tensor<2x2x2x8xbf16> + tt.store %26, %28 : tensor<2x2x2x8x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @discrete_highrank_and_structured_lowrank_loadstore_5d +// CHECK: %[[VAL_0:.*]] = scf.for %[[VAL_1:.*]] = %{{.*}} to %{{.*}} step %{{.*}} iter_args(%[[VAL_2:.*]] = %{{.*}}) -> (tensor<2x2x2x2x8xbf16>) { +// CHECK: %[[VAL_3:.*]] = tensor.extract_slice %{{.*}}{{\[}}%[[VAL_1]], 0, 0, 0, 0] [1, 2, 2, 2, 8] [1, 1, 1, 1, 1] {DiscreteMemAccess} : tensor<2x2x2x2x8xi64> to tensor<1x2x2x2x8xi64> +// CHECK: %[[VAL_4:.*]] = tt.splat %{{.*}} : !tt.ptr -> tensor<1x2x2x2x8x!tt.ptr> +// CHECK: %[[VAL_5:.*]] = tt.addptr %[[VAL_4]], %[[VAL_3]] : tensor<1x2x2x2x8x!tt.ptr>, tensor<1x2x2x2x8xi64> +// CHECK: %[[VAL_6:.*]] = tt.load %[[VAL_5]] {DiscreteMemAccess} : tensor<1x2x2x2x8x!tt.ptr> +// CHECK: %[[VAL_7:.*]] = tensor.insert_slice %[[VAL_6]] into %[[VAL_2]]{{\[}}%[[VAL_1]], 0, 0, 0, 0] [1, 2, 2, 2, 8] [1, 1, 1, 1, 1] : tensor<1x2x2x2x8xbf16> into tensor<2x2x2x2x8xbf16> +// CHECK: scf.yield {DiscreteMemAccess} %[[VAL_7]] : tensor<2x2x2x2x8xbf16> +// CHECK: } {ExtractedLoadOrStore} +// CHECK: %[[VAL_8:.*]] = arith.addf %[[VAL_0]], %{{.*}} : tensor<2x2x2x2x8xbf16> +// CHECK: scf.for %[[VAL_9:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: %[[VAL_10:.*]] = tensor.extract_slice %{{.*}}{{\[}}%[[VAL_9]], 0, 0, 0, 0] [1, 2, 2, 2, 8] [1, 1, 1, 1, 1] {DiscreteMemAccess} : tensor<2x2x2x2x8xi64> to tensor<1x2x2x2x8xi64> +// CHECK: %[[VAL_11:.*]] = tt.splat %{{.*}} : !tt.ptr -> tensor<1x2x2x2x8x!tt.ptr> +// CHECK: %[[VAL_12:.*]] = tt.addptr %[[VAL_11]], %[[VAL_10]] : tensor<1x2x2x2x8x!tt.ptr>, tensor<1x2x2x2x8xi64> +// CHECK: %[[VAL_13:.*]] = tensor.extract_slice %[[VAL_8]]{{\[}}%[[VAL_9]], 0, 0, 0, 0] [1, 2, 2, 2, 8] [1, 1, 1, 1, 1] {DiscreteMemAccess} : tensor<2x2x2x2x8xbf16> to tensor<1x2x2x2x8xbf16> +// CHECK: tt.store %[[VAL_12]], %[[VAL_13]] {DiscreteMemAccess} : tensor<1x2x2x2x8x!tt.ptr> +// CHECK: } {ExtractedLoadOrStore} +tt.func @discrete_highrank_and_structured_lowrank_loadstore_5d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<5.000000e+00> : tensor<2x2x2x2x8xbf16> + %cst_0 = arith.constant dense<8> : tensor<2x2x2x2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x2x2x1xi32> + %cst_2 = arith.constant dense<2> : tensor<2x2x1x1xi32> + %cst_3 = arith.constant dense<2> : tensor<2x1x1x1xi32> + %cst_4 = arith.constant dense<1> : tensor<2x1x1x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = tt.expand_dims %1 {axis = 2 : i32} : tensor<2x1xi32> -> tensor<2x1x1xi32> + %3 = tt.expand_dims %2 {axis = 3 : i32} : tensor<2x1x1xi32> -> tensor<2x1x1x1xi32> + %4 = arith.addi %3, %cst_4 : tensor<2x1x1x1xi32> + %5 = arith.remsi %4, %cst_3 : tensor<2x1x1x1xi32> + %6 = tt.expand_dims %0 {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> + %7 = tt.expand_dims %6 {axis = 2 : i32} : tensor<1x2xi32> -> tensor<1x2x1xi32> + %8 = tt.expand_dims %7 {axis = 3 : i32} : tensor<1x2x1xi32> -> tensor<1x2x1x1xi32> + %9 = tt.expand_dims %6 {axis = 1 : i32} : tensor<1x2xi32> -> tensor<1x1x2xi32> + %10 = tt.expand_dims %9 {axis = 3 : i32} : tensor<1x1x2xi32> -> tensor<1x1x2x1xi32> + %11 = tt.expand_dims %9 {axis = 2 : i32} : tensor<1x1x2xi32> -> tensor<1x1x1x2xi32> + %12 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %13 = tt.expand_dims %12 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %14 = tt.expand_dims %13 {axis = 1 : i32} : tensor<1x8xi32> -> tensor<1x1x8xi32> + %15 = tt.expand_dims %14 {axis = 2 : i32} : tensor<1x1x8xi32> -> tensor<1x1x1x8xi32> + %16 = tt.expand_dims %15 {axis = 3 : i32} : tensor<1x1x1x8xi32> -> tensor<1x1x1x1x8xi32> + %17 = arith.muli %5, %cst_3 : tensor<2x1x1x1xi32> + %18 = tt.broadcast %17 : tensor<2x1x1x1xi32> -> tensor<2x2x1x1xi32> + %19 = tt.broadcast %8 : tensor<1x2x1x1xi32> -> tensor<2x2x1x1xi32> + %20 = arith.addi %18, %19 : tensor<2x2x1x1xi32> + %21 = arith.muli %20, %cst_2 : tensor<2x2x1x1xi32> + %22 = tt.broadcast %21 : tensor<2x2x1x1xi32> -> tensor<2x2x2x1xi32> + %23 = tt.broadcast %10 : tensor<1x1x2x1xi32> -> tensor<2x2x2x1xi32> + %24 = arith.addi %22, %23 : tensor<2x2x2x1xi32> + %25 = arith.muli %24, %cst_1 : tensor<2x2x2x1xi32> + %26 = tt.broadcast %25 : tensor<2x2x2x1xi32> -> tensor<2x2x2x2xi32> + %27 = tt.broadcast %11 : tensor<1x1x1x2xi32> -> tensor<2x2x2x2xi32> + %28 = arith.addi %26, %27 : tensor<2x2x2x2xi32> + %29 = tt.expand_dims %28 {axis = 4 : i32} : tensor<2x2x2x2xi32> -> tensor<2x2x2x2x1xi32> + %30 = arith.muli %29, %cst_0 : tensor<2x2x2x2x1xi32> + %31 = tt.broadcast %30 : tensor<2x2x2x2x1xi32> -> tensor<2x2x2x2x8xi32> + %32 = tt.broadcast %16 : tensor<1x1x1x1x8xi32> -> tensor<2x2x2x2x8xi32> + %33 = arith.addi %31, %32 : tensor<2x2x2x2x8xi32> + %34 = tt.splat %arg0 : !tt.ptr -> tensor<2x2x2x2x8x!tt.ptr> + %35 = tt.addptr %34, %33 : tensor<2x2x2x2x8x!tt.ptr>, tensor<2x2x2x2x8xi32> + %36 = tt.load %35 : tensor<2x2x2x2x8x!tt.ptr> + %37 = arith.addf %36, %cst : tensor<2x2x2x2x8xbf16> + tt.store %35, %37 : tensor<2x2x2x2x8x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/AutoBlockify/auto_blockify.mlir b/third_party/ascend/unittest/Conversion/General/AutoBlockify/auto_blockify.mlir new file mode 100644 index 0000000000..c9a1dcb409 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/AutoBlockify/auto_blockify.mlir @@ -0,0 +1,134 @@ +// RUN: triton-opt --auto-blockify="auto-blockify-size=5" --split-input-file %s | FileCheck %s + +// ----- + +// CHECK-LABEL: tt.func @kernel( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr) attributes {auto_blockify_size = 5 : i32} { +// CHECK: %[[VAL_1:.*]] = arith.constant dense<0.000000e+00> : tensor<5x8xf32> +// CHECK: %[[VAL_2:.*]] = arith.constant dense<8> : tensor<5xi32> +// CHECK: %[[VAL_3:.*]] = arith.constant dense<0> : tensor<5xi32> +// CHECK: %[[VAL_4:.*]] = tt.get_num_programs x : i32 +// CHECK: %[[VAL_5:.*]] = tt.get_num_programs y : i32 +// CHECK: %[[VAL_6:.*]] = tt.get_num_programs z : i32 +// CHECK: %[[VAL_7:.*]] = arith.muli %[[VAL_5]], %[[VAL_6]] : i32 +// CHECK: %[[VAL_8:.*]] = arith.muli %[[VAL_7]], %[[VAL_4]] : i32 +// CHECK: %[[VAL_9:.*]] = tt.get_program_id x {logical_block_id} : i32 +// CHECK: %[[VAL_10:.*]] = tt.get_program_id y {logical_block_id} : i32 +// CHECK: %[[VAL_11:.*]] = tt.get_program_id z {logical_block_id} : i32 +// CHECK: %[[VAL_12:.*]] = arith.muli %[[VAL_9]], %[[VAL_7]] : i32 +// CHECK: %[[VAL_13:.*]] = arith.muli %[[VAL_10]], %[[VAL_6]] : i32 +// CHECK: %[[VAL_14:.*]] = arith.addi %[[VAL_12]], %[[VAL_13]] : i32 +// CHECK: %[[VAL_15:.*]] = arith.addi %[[VAL_14]], %[[VAL_11]] : i32 +// CHECK: %[[VAL_16:.*]] = tt.make_range {end = 5 : i32, start = 0 : i32} : tensor<5xi32> +// CHECK: %[[VAL_17:.*]] = tt.splat %[[VAL_15]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_18:.*]] = arith.addi %[[VAL_17]], %[[VAL_16]] : tensor<5xi32> +// CHECK: %[[VAL_19:.*]] = tt.splat %[[VAL_8]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_20:.*]] = arith.cmpi slt, %[[VAL_18]], %[[VAL_19]] : tensor<5xi32> +// CHECK: %[[VAL_21:.*]] = arith.cmpi sge, %[[VAL_18]], %[[VAL_3]] : tensor<5xi32> +// CHECK: %[[VAL_22:.*]] = arith.ori %[[VAL_20]], %[[VAL_21]] : tensor<5xi1> +// CHECK: %[[VAL_23:.*]] = tt.splat %[[VAL_7]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_24:.*]] = arith.divsi %[[VAL_18]], %[[VAL_23]] : tensor<5xi32> +// CHECK: %[[VAL_25:.*]] = tt.splat %[[VAL_4]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_26:.*]] = arith.remsi %[[VAL_24]], %[[VAL_25]] : tensor<5xi32> +// CHECK: %[[VAL_27:.*]] = arith.muli %[[VAL_26]], %[[VAL_2]] : tensor<5xi32> +// CHECK: %[[VAL_28:.*]] = tt.expand_dims %[[VAL_27]] {axis = 1 : i32} : tensor<5xi32> -> tensor<5x1xi32> +// CHECK: %[[VAL_29:.*]] = tt.broadcast %[[VAL_28]] : tensor<5x1xi32> -> tensor<5x8xi32> +// CHECK: %[[VAL_30:.*]] = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> +// CHECK: %[[VAL_31:.*]] = tt.expand_dims %[[VAL_30]] {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> +// CHECK: %[[VAL_32:.*]] = tt.broadcast %[[VAL_31]] : tensor<1x8xi32> -> tensor<5x8xi32> +// CHECK: %[[VAL_33:.*]] = arith.addi %[[VAL_29]], %[[VAL_32]] : tensor<5x8xi32> +// CHECK: %[[VAL_34:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<5x8x!tt.ptr> +// CHECK: %[[VAL_35:.*]] = tt.addptr %[[VAL_34]], %[[VAL_33]] : tensor<5x8x!tt.ptr>, tensor<5x8xi32> +// CHECK: %[[VAL_36:.*]] = tt.expand_dims %[[VAL_22]] {axis = 1 : i32} : tensor<5xi1> -> tensor<5x1xi1> +// CHECK: %[[VAL_37:.*]] = tt.broadcast %[[VAL_36]] : tensor<5x1xi1> -> tensor<5x8xi1> +// CHECK: tt.store %[[VAL_35]], %[[VAL_1]], %[[VAL_37]] : tensor<5x8x!tt.ptr> +// CHECK: tt.return +// CHECK: } +tt.func @kernel(%arg0: !tt.ptr) { + %cst = arith.constant dense<0.000000e+00> : tensor<8xf32> + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c8_i32 : i32 + %2 = tt.splat %1 : i32 -> tensor<8xi32> + %3 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %4 = arith.addi %2, %3 : tensor<8xi32> + %5 = tt.splat %arg0 : !tt.ptr -> tensor<8x!tt.ptr> + %6 = tt.addptr %5, %4 : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %6, %cst : tensor<8x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @kernel2( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr) attributes {auto_blockify_size = 5 : i32} { +// CHECK: %[[VAL_1:.*]] = arith.constant dense<8> : tensor<5xi32> +// CHECK: %[[VAL_2:.*]] = arith.constant 0 : i32 +// CHECK: %[[VAL_3:.*]] = arith.constant 5 : index +// CHECK: %[[VAL_4:.*]] = arith.constant 1 : index +// CHECK: %[[VAL_5:.*]] = arith.constant 0 : index +// CHECK: %[[VAL_6:.*]] = arith.constant dense<0.000000e+00> : tensor<8xf32> +// CHECK: %[[VAL_7:.*]] = tt.get_num_programs x : i32 +// CHECK: %[[VAL_8:.*]] = tt.get_num_programs y : i32 +// CHECK: %[[VAL_9:.*]] = tt.get_num_programs z : i32 +// CHECK: %[[VAL_10:.*]] = arith.muli %[[VAL_8]], %[[VAL_9]] : i32 +// CHECK: %[[VAL_11:.*]] = arith.muli %[[VAL_10]], %[[VAL_7]] : i32 +// CHECK: %[[VAL_12:.*]] = tt.get_program_id x {logical_block_id} : i32 +// CHECK: %[[VAL_13:.*]] = tt.get_program_id y {logical_block_id} : i32 +// CHECK: %[[VAL_14:.*]] = tt.get_program_id z {logical_block_id} : i32 +// CHECK: %[[VAL_15:.*]] = arith.muli %[[VAL_12]], %[[VAL_10]] : i32 +// CHECK: %[[VAL_16:.*]] = arith.muli %[[VAL_13]], %[[VAL_9]] : i32 +// CHECK: %[[VAL_17:.*]] = arith.addi %[[VAL_15]], %[[VAL_16]] : i32 +// CHECK: %[[VAL_18:.*]] = arith.addi %[[VAL_17]], %[[VAL_14]] : i32 +// CHECK: %[[VAL_19:.*]] = tt.make_range {end = 5 : i32, start = 0 : i32} : tensor<5xi32> +// CHECK: %[[VAL_20:.*]] = tt.splat %[[VAL_18]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_21:.*]] = arith.addi %[[VAL_20]], %[[VAL_19]] : tensor<5xi32> +// CHECK: %[[VAL_22:.*]] = tt.splat %[[VAL_10]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_23:.*]] = arith.divsi %[[VAL_21]], %[[VAL_22]] : tensor<5xi32> +// CHECK: %[[VAL_24:.*]] = tt.splat %[[VAL_7]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_25:.*]] = arith.remsi %[[VAL_23]], %[[VAL_24]] : tensor<5xi32> +// CHECK: %[[VAL_26:.*]] = tt.splat %[[VAL_9]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_27:.*]] = arith.divsi %[[VAL_21]], %[[VAL_26]] : tensor<5xi32> +// CHECK: %[[VAL_28:.*]] = tt.splat %[[VAL_8]] : i32 -> tensor<5xi32> +// CHECK: %[[VAL_29:.*]] = arith.remsi %[[VAL_27]], %[[VAL_28]] : tensor<5xi32> +// CHECK: %[[VAL_30:.*]] = arith.cmpi slt, %[[VAL_29]], %[[VAL_1]] : tensor<5xi32> +// CHECK: %[[VAL_31:.*]] = arith.muli %[[VAL_25]], %[[VAL_1]] : tensor<5xi32> +// CHECK: %[[VAL_32:.*]] = tt.expand_dims %[[VAL_31]] {axis = 1 : i32} : tensor<5xi32> -> tensor<5x1xi32> +// CHECK: %[[VAL_33:.*]] = tt.broadcast %[[VAL_32]] : tensor<5x1xi32> -> tensor<5x8xi32> +// CHECK: %[[VAL_34:.*]] = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> +// CHECK: %[[VAL_35:.*]] = tt.expand_dims %[[VAL_34]] {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> +// CHECK: %[[VAL_36:.*]] = tt.broadcast %[[VAL_35]] : tensor<1x8xi32> -> tensor<5x8xi32> +// CHECK: %[[VAL_37:.*]] = arith.addi %[[VAL_33]], %[[VAL_36]] : tensor<5x8xi32> +// CHECK: %[[VAL_38:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<5x8x!tt.ptr> +// CHECK: %[[VAL_39:.*]] = tt.addptr %[[VAL_38]], %[[VAL_37]] : tensor<5x8x!tt.ptr>, tensor<5x8xi32> +// CHECK: %[[VAL_40:.*]] = arith.subi %[[VAL_11]], %[[VAL_18]] : i32 +// CHECK: %[[VAL_41:.*]] = arith.maxsi %[[VAL_40]], %[[VAL_2]] : i32 +// CHECK: %[[VAL_42:.*]] = arith.index_cast %[[VAL_41]] : i32 to index +// CHECK: %[[VAL_43:.*]] = arith.minsi %[[VAL_42]], %[[VAL_3]] : index +// CHECK: scf.for %[[VAL_44:.*]] = %[[VAL_5]] to %[[VAL_43]] step %[[VAL_4]] { +// CHECK: %[[VAL_45:.*]] = tensor.extract %[[VAL_30]]{{\[}}%[[VAL_44]]] : tensor<5xi1> +// CHECK: scf.if %[[VAL_45]] { +// CHECK: %[[VAL_46:.*]] = tensor.extract_slice %[[VAL_39]]{{\[}}%[[VAL_44]], 0] [1, 8] [1, 1] : tensor<5x8x!tt.ptr> to tensor<8x!tt.ptr> +// CHECK: tt.store %[[VAL_46]], %[[VAL_6]] : tensor<8x!tt.ptr> +// CHECK: } +// CHECK: } {auto_blockify_loop} +// CHECK: tt.return +// CHECK: } +tt.func @kernel2(%arg0: !tt.ptr) { + %cst = arith.constant dense<0.000000e+00> : tensor<8xf32> + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %a = tt.get_program_id y : i32 + %b = arith.cmpi slt, %a, %c8_i32 : i32 + %1 = arith.muli %0, %c8_i32 : i32 + %2 = tt.splat %1 : i32 -> tensor<8xi32> + %3 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %4 = arith.addi %2, %3 : tensor<8xi32> + %5 = tt.splat %arg0 : !tt.ptr -> tensor<8x!tt.ptr> + %6 = tt.addptr %5, %4 : tensor<8x!tt.ptr>, tensor<8xi32> + scf.if %b { + tt.store %6, %cst : tensor<8x!tt.ptr> + scf.yield + } + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/atomic.mlir b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/atomic.mlir new file mode 100644 index 0000000000..5c6ebdbf54 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/atomic.mlir @@ -0,0 +1,282 @@ +// RUN: triton-opt --discrete-mask-access-conversion --split-input-file %s | FileCheck %s + +// CHECK-LABEL: tt.func @atomic_add_i32 +// CHECK: %[[default:.*]] = arith.constant dense<0> : tensor<1024xi32> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw add, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_add_i32(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw add, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xi32>, tensor<1024xi1>) -> tensor<1024xi32> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_fadd_f32 +// CHECK: %[[default:.*]] = arith.constant dense<0.000000e+00> : tensor<1024xf32> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw fadd, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_fadd_f32(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw fadd, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xf32>, tensor<1024xi1>) -> tensor<1024xf32> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_max_i32 +// CHECK: %[[default:.*]] = arith.constant dense<-2147483648> : tensor<1024xi32> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw max, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_max_i32(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw max, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xi32>, tensor<1024xi1>) -> tensor<1024xi32> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_umax_i32 +// CHECK: %[[default:.*]] = arith.constant dense<0> : tensor<1024xi32> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw umax, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_umax_i32(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw umax, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xi32>, tensor<1024xi1>) -> tensor<1024xi32> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_min_i32 +// CHECK: %[[default:.*]] = arith.constant dense<2147483647> : tensor<1024xi32> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw min, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_min_i32(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw min, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xi32>, tensor<1024xi1>) -> tensor<1024xi32> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_umin_i32 +// CHECK: %[[default:.*]] = arith.constant dense<2147483647> : tensor<1024xi32> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw umin, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_umin_i32(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw umin, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xi32>, tensor<1024xi1>) -> tensor<1024xi32> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_and_i32 +// CHECK: %[[default:.*]] = arith.constant dense<2147483647> : tensor<1024xi32> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw and, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_and_i32(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw and, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xi32>, tensor<1024xi1>) -> tensor<1024xi32> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_or_i32 +// CHECK: %[[default:.*]] = arith.constant dense<0> : tensor<1024xi32> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw or, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_or_i32(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw or, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xi32>, tensor<1024xi1>) -> tensor<1024xi32> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_max_i16 +// CHECK: %[[default:.*]] = arith.constant dense<-32768> : tensor<1024xi16> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw max, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_max_i16(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw max, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xi16>, tensor<1024xi1>) -> tensor<1024xi16> + tt.return +} + +// CHECK-LABEL: tt.func @atomic_max_f16 +// CHECK: %[[default:.*]] = arith.constant dense<0xFC00> : tensor<1024xf16> +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[origin:.*]], %[[default]] +// CHECK: %[[result:.*]] = tt.atomic_rmw max, acq_rel, gpu, %[[ptr:.*]], %[[value]] +tt.func @atomic_max_f16(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<200> : tensor<1024xi32> + %cst_0 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_0 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %7 : tensor<1024x!tt.ptr> + %9 = tt.atomic_rmw max, acq_rel, gpu, %5, %8, %3 : (tensor<1024x!tt.ptr>, tensor<1024xf16>, tensor<1024xi1>) -> tensor<1024xf16> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @atomic_add_cont_disc_i32 +// CHECK: %[[DEFAULT:.*]] = arith.constant dense<0> : tensor<8x8xi32> +// CHECK: %[[CONT_MASK:.*]] = tt.broadcast {{.*}} : tensor<8x1xi1> -> tensor<8x8xi1> +// CHECK: %[[DISC_MASK:.*]] = tt.broadcast {{.*}} : tensor<1x8xi1> -> tensor<8x8xi1> +// CHECK: %[[VALUE:.*]] = arith.select %[[DISC_MASK]], %{{.*}}, %[[DEFAULT]] : tensor<8x8xi1>, tensor<8x8xi32> +// CHECK: tt.atomic_rmw add, acq_rel, gpu, %{{.*}}, %[[VALUE]], %[[CONT_MASK]] : (tensor<8x8x!tt.ptr>, tensor<8x8xi32>, tensor<8x8xi1>) -> tensor<8x8xi32> +tt.func @atomic_add_cont_disc_i32(%arg0: !tt.ptr, %arg1: i32) { + %cst_0 = arith.constant dense<8> : tensor<8x1xi32> + %cst_1 = arith.constant dense<2> : tensor<1x8xi32> + %cst_2 = arith.constant dense<8> : tensor<1x8xi32> + %cst_3 = arith.constant dense<1> : tensor<8x8xi32> + %0 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %3 = tt.splat %arg1 : i32 -> tensor<8x1xi32> + %4 = arith.cmpi slt, %1, %3 : tensor<8x1xi32> + %5 = tt.broadcast %4 : tensor<8x1xi1> -> tensor<8x8xi1> + %6 = arith.muli %2, %cst_1 : tensor<1x8xi32> + %7 = arith.cmpi slt, %6, %cst_2 : tensor<1x8xi32> + %8 = tt.broadcast %7 : tensor<1x8xi1> -> tensor<8x8xi1> + %9 = arith.andi %5, %8 : tensor<8x8xi1> + %10 = arith.muli %1, %cst_0 : tensor<8x1xi32> + %11 = tt.broadcast %10 : tensor<8x1xi32> -> tensor<8x8xi32> + %12 = tt.broadcast %2 : tensor<1x8xi32> -> tensor<8x8xi32> + %13 = arith.addi %11, %12 : tensor<8x8xi32> + %14 = tt.splat %arg0 : !tt.ptr -> tensor<8x8x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<8x8x!tt.ptr>, tensor<8x8xi32> + %16 = tt.atomic_rmw add, acq_rel, gpu, %15, %cst_3, %9 : (tensor<8x8x!tt.ptr>, tensor<8x8xi32>, tensor<8x8xi1>) -> tensor<8x8xi32> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @atomic_min_f32_splat_bitcast +// CHECK-NOT: tt.addptr %arg0 +// CHECK: %[[VALUE_PTRS:.*]] = tt.splat %arg0 : !tt.ptr -> tensor<1x1x1x1x!tt.ptr> +// CHECK: %[[VALUE:.*]] = tt.load %[[VALUE_PTRS]] : tensor<1x1x1x1x!tt.ptr> +// CHECK-NOT: tt.addptr %arg1 +// CHECK: %[[OUTPUT_PTRS:.*]] = tt.splat %arg1 : !tt.ptr -> tensor<1x1x1x1x!tt.ptr> +// CHECK: tt.atomic_rmw min, acq_rel, gpu, %[[OUTPUT_PTRS]], %[[VALUE]], %{{.*}} : (tensor<1x1x1x1x!tt.ptr>, tensor<1x1x1x1xf32>, tensor<1x1x1x1xi1>) -> tensor<1x1x1x1xf32> +// CHECK-NOT: tt.atomic_rmw umax +tt.func @atomic_min_f32_splat_bitcast(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %c0_i32 = arith.constant 0 : i32 + %cst = arith.constant dense<0> : tensor<1x1x1x1xi32> + %cst_0 = arith.constant dense<31> : tensor<1x1x1x1xi32> + %cst_1 = arith.constant dense : tensor<1x1x1x1xi1> + %0 = tt.addptr %arg0, %c0_i32 : !tt.ptr, i32 + %1 = tt.splat %0 : !tt.ptr -> tensor<1x1x1x1x!tt.ptr> + %2 = tt.load %1 : tensor<1x1x1x1x!tt.ptr> + %3 = tt.addptr %arg1, %c0_i32 : !tt.ptr, i32 + %4 = tt.bitcast %2 : tensor<1x1x1x1xf32> -> tensor<1x1x1x1xi32> + %5 = tt.bitcast %3 : !tt.ptr -> !tt.ptr + %6 = tt.splat %5 : !tt.ptr -> tensor<1x1x1x1x!tt.ptr> + %7 = arith.shrui %4, %cst_0 : tensor<1x1x1x1xi32> + %8 = arith.cmpi ne, %7, %cst : tensor<1x1x1x1xi32> + %9 = arith.xori %8, %cst_1 : tensor<1x1x1x1xi1> + %10 = tt.atomic_rmw min, acq_rel, gpu, %6, %4, %9 : (tensor<1x1x1x1x!tt.ptr>, tensor<1x1x1x1xi32>, tensor<1x1x1x1xi1>) -> tensor<1x1x1x1xi32> + %11 = tt.atomic_rmw umax, acq_rel, gpu, %6, %4, %8 : (tensor<1x1x1x1x!tt.ptr>, tensor<1x1x1x1xi32>, tensor<1x1x1x1xi1>) -> tensor<1x1x1x1xi32> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @atomic_max_f32_scalar +// CHECK: tt.atomic_rmw max, acq_rel, gpu, %arg1, %arg0, %{{.*}} : (!tt.ptr, f32, i1) -> f32 +// CHECK-NOT: tt.atomic_rmw umin +tt.func @atomic_max_f32_scalar(%arg0: f32, %arg1: !tt.ptr) { + %c31_i32 = arith.constant 31 : i32 + %c0_i32 = arith.constant 0 : i32 + %0 = tt.bitcast %arg0 : f32 -> i32 + %1 = tt.bitcast %arg1 : !tt.ptr -> !tt.ptr + %2 = arith.shrui %0, %c31_i32 : i32 + %3 = arith.cmpi eq, %2, %c0_i32 : i32 + %4 = arith.cmpi ne, %2, %c0_i32 : i32 + %5 = tt.atomic_rmw max, acq_rel, gpu, %1, %0, %3 : (!tt.ptr, i32, i1) -> i32 + %6 = tt.atomic_rmw umin, acq_rel, gpu, %1, %0, %4 : (!tt.ptr, i32, i1) -> i32 + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/continuous_mask_subi.mlir b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/continuous_mask_subi.mlir new file mode 100644 index 0000000000..69cf70edd2 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/continuous_mask_subi.mlir @@ -0,0 +1,25 @@ +// RUN: triton-opt --discrete-mask-access-conversion --split-input-file %s | FileCheck %s + +// Continuous head mask written as (arange - C) >= 0 must be recognized by +// MaskState (via arith.subi). discrete-mask-access-conversion must NOT strip +// the mask into a full load + select. + +// CHECK-LABEL: tt.func @continuous_head_mask_subi +// CHECK-NOT: arith.select +// CHECK: tt.load {{.*}}, %[[MASK:.*]] +// CHECK-NOT: arith.select +tt.func @continuous_head_mask_subi(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %c0 = arith.constant dense<0> : tensor<256xi32> + %c2 = arith.constant dense<2> : tensor<256xi32> + %other = arith.constant dense<0.000000e+00> : tensor<256xf32> + %offs = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %idx = arith.subi %offs, %c2 : tensor<256xi32> + %mask = arith.cmpi sge, %idx, %c0 : tensor<256xi32> + %x_splat = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %x_ptrs = tt.addptr %x_splat, %idx : tensor<256x!tt.ptr>, tensor<256xi32> + %o_splat = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %o_ptrs = tt.addptr %o_splat, %offs : tensor<256x!tt.ptr>, tensor<256xi32> + %v = tt.load %x_ptrs, %mask, %other : tensor<256x!tt.ptr> + tt.store %o_ptrs, %v : tensor<256x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/indirect_atomic.mlir b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/indirect_atomic.mlir new file mode 100644 index 0000000000..2dc780c261 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/indirect_atomic.mlir @@ -0,0 +1,215 @@ +// RUN: triton-opt '--discrete-mask-access-conversion=compile-on-910-95=True compile-mode=simd_simt_template' --split-input-file %s | FileCheck %s + +// CHECK-LABEL: tt.func @structured_disc_mask_atomic_add_2d +// CHECK: arith.select {{.*}} : tensor<4x4xi1>, tensor<4x4xi32> +// CHECK: tt.atomic_rmw add, acq_rel, gpu, {{.*}} : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>) -> tensor<4x4xi32> +// CHECK: tt.store {{.*}} {MixCompileDiscreteMask} : tensor<4x4x!tt.ptr> +tt.func @structured_disc_mask_atomic_add_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense<16> : tensor<4x4xi32> + %cst_0 = arith.constant dense<2> : tensor<4x4xi32> + %cst_1 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_1 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = arith.muli %6, %cst_0 : tensor<4x4xi32> + %8 = arith.cmpi slt, %7, %cst : tensor<4x4xi32> + %9 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %10 = tt.addptr %9, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %11 = tt.load %10 : tensor<4x4x!tt.ptr> + %12 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %13 = tt.addptr %12, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %14 = tt.atomic_rmw add, acq_rel, gpu, %13, %11, %8 : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %15 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %16 = tt.addptr %15, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %16, %14, %8 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @structured_disc_mask_atomic_and_2d +// CHECK: arith.select {{.*}} : tensor<4x4xi1>, tensor<4x4xi32> +// CHECK: tt.atomic_rmw and, acq_rel, gpu, {{.*}} : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>) -> tensor<4x4xi32> +// CHECK: tt.store {{.*}} {MixCompileDiscreteMask} : tensor<4x4x!tt.ptr> +tt.func @structured_disc_mask_atomic_and_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense<16> : tensor<4x4xi32> + %cst_0 = arith.constant dense<2> : tensor<4x4xi32> + %cst_1 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_1 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = arith.muli %6, %cst_0 : tensor<4x4xi32> + %8 = arith.cmpi slt, %7, %cst : tensor<4x4xi32> + %9 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %10 = tt.addptr %9, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %11 = tt.load %10 : tensor<4x4x!tt.ptr> + %12 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %13 = tt.addptr %12, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %14 = tt.atomic_rmw and, acq_rel, gpu, %13, %11, %8 : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %15 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %16 = tt.addptr %15, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %16, %14, %8 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @structured_disc_mask_atomic_or_2d +// CHECK: arith.select {{.*}} : tensor<4x4xi1>, tensor<4x4xi32> +// CHECK: tt.atomic_rmw or, acq_rel, gpu, {{.*}} : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>) -> tensor<4x4xi32> +// CHECK: tt.store {{.*}} {MixCompileDiscreteMask} : tensor<4x4x!tt.ptr> +tt.func @structured_disc_mask_atomic_or_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense<16> : tensor<4x4xi32> + %cst_0 = arith.constant dense<2> : tensor<4x4xi32> + %cst_1 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_1 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = arith.muli %6, %cst_0 : tensor<4x4xi32> + %8 = arith.cmpi slt, %7, %cst : tensor<4x4xi32> + %9 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %10 = tt.addptr %9, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %11 = tt.load %10 : tensor<4x4x!tt.ptr> + %12 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %13 = tt.addptr %12, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %14 = tt.atomic_rmw or, acq_rel, gpu, %13, %11, %8 : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %15 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %16 = tt.addptr %15, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %16, %14, %8 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @structured_disc_mask_atomic_xor_2d +// CHECK: arith.select {{.*}} : tensor<4x4xi1>, tensor<4x4xi32> +// CHECK: tt.atomic_rmw xor, acq_rel, gpu, {{.*}} : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>) -> tensor<4x4xi32> +// CHECK: tt.store {{.*}} {MixCompileDiscreteMask} : tensor<4x4x!tt.ptr> +tt.func @structured_disc_mask_atomic_xor_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense<16> : tensor<4x4xi32> + %cst_0 = arith.constant dense<2> : tensor<4x4xi32> + %cst_1 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_1 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = arith.muli %6, %cst_0 : tensor<4x4xi32> + %8 = arith.cmpi slt, %7, %cst : tensor<4x4xi32> + %9 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %10 = tt.addptr %9, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %11 = tt.load %10 : tensor<4x4x!tt.ptr> + %12 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %13 = tt.addptr %12, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %14 = tt.atomic_rmw xor, acq_rel, gpu, %13, %11, %8 : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %15 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %16 = tt.addptr %15, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %16, %14, %8 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @structured_disc_mask_atomic_xchg_2d +// CHECK: tt.atomic_rmw exch, acq_rel, gpu, {{.*}} {DiscreteMask, ExplicitRouteToSIMD = true} : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> +// CHECK: tt.store {{.*}} {MixCompileDiscreteMask} : tensor<4x4x!tt.ptr> +tt.func @structured_disc_mask_atomic_xchg_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense<16> : tensor<4x4xi32> + %cst_0 = arith.constant dense<2> : tensor<4x4xi32> + %cst_1 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_1 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = arith.muli %6, %cst_0 : tensor<4x4xi32> + %8 = arith.cmpi slt, %7, %cst : tensor<4x4xi32> + %9 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %10 = tt.addptr %9, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %11 = tt.load %10 : tensor<4x4x!tt.ptr> + %12 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %13 = tt.addptr %12, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %14 = tt.atomic_rmw exch, acq_rel, gpu, %13, %11, %8 : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %15 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %16 = tt.addptr %15, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %16, %14, %8 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @structured_disc_mask_atomic_max_2d +// CHECK: arith.select {{.*}} : tensor<4x4xi1>, tensor<4x4xi32> +// CHECK: tt.atomic_rmw max, acq_rel, gpu, {{.*}} : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>) -> tensor<4x4xi32> +// CHECK: tt.store {{.*}} {MixCompileDiscreteMask} : tensor<4x4x!tt.ptr> +tt.func @structured_disc_mask_atomic_max_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense<16> : tensor<4x4xi32> + %cst_0 = arith.constant dense<2> : tensor<4x4xi32> + %cst_1 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_1 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = arith.muli %6, %cst_0 : tensor<4x4xi32> + %8 = arith.cmpi slt, %7, %cst : tensor<4x4xi32> + %9 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %10 = tt.addptr %9, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %11 = tt.load %10 : tensor<4x4x!tt.ptr> + %12 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %13 = tt.addptr %12, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %14 = tt.atomic_rmw max, acq_rel, gpu, %13, %11, %8 : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %15 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %16 = tt.addptr %15, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %16, %14, %8 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func @structured_disc_mask_atomic_min_2d +// CHECK: arith.select {{.*}} : tensor<4x4xi1>, tensor<4x4xi32> +// CHECK: tt.atomic_rmw min, acq_rel, gpu, {{.*}} : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>) -> tensor<4x4xi32> +// CHECK: tt.store {{.*}} {MixCompileDiscreteMask} : tensor<4x4x!tt.ptr> +tt.func @structured_disc_mask_atomic_min_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense<16> : tensor<4x4xi32> + %cst_0 = arith.constant dense<2> : tensor<4x4xi32> + %cst_1 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_1 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = arith.muli %6, %cst_0 : tensor<4x4xi32> + %8 = arith.cmpi slt, %7, %cst : tensor<4x4xi32> + %9 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %10 = tt.addptr %9, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %11 = tt.load %10 : tensor<4x4x!tt.ptr> + %12 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %13 = tt.addptr %12, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %14 = tt.atomic_rmw min, acq_rel, gpu, %13, %11, %8 : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %15 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %16 = tt.addptr %15, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %16, %14, %8 : tensor<4x4x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loadstore.mlir b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loadstore.mlir new file mode 100644 index 0000000000..88a26cade3 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loadstore.mlir @@ -0,0 +1,66 @@ +// RUN: triton-opt --discrete-mask-access-conversion --split-input-file %s | FileCheck %s + +// CHECK-LABEL: tt.func @discrete_load +// CHECK: %[[loaded_value:.*]] = tt.load %[[load_ptr:.*]] +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[loaded_value]], %[[other:.*]] +// CHECK: tt.store %[[store_ptr:.*]], %[[value]] +tt.func @discrete_load(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<0> : tensor<1024xi32> + %cst_0 = arith.constant dense<200> : tensor<1024xi32> + %cst_1 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst_0 : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_1 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %5, %3, %cst : tensor<1024x!tt.ptr> + tt.store %7, %8 : tensor<1024x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @discrete_load_without_other +// CHECK: %[[other:.*]] = arith.constant dense<0> +// CHECK: %[[loaded_value:.*]] = tt.load %[[load_ptr:.*]] +// CHECK: %[[value:.*]] = arith.select %[[mask:.*]], %[[loaded_value]], %[[other]] +// CHECK: tt.store %[[store_ptr:.*]], %[[value]] +tt.func @discrete_load_without_other(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<0> : tensor<1024xi32> + %cst_0 = arith.constant dense<200> : tensor<1024xi32> + %cst_1 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst_0 : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_1 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %5, %3 : tensor<1024x!tt.ptr> + tt.store %7, %8 : tensor<1024x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @discrete_store +// CHECK: %[[loaded_value:.*]] = tt.load %[[load_ptr:.*]] : tensor<1024x!tt.ptr> +// CHECK: %[[origin_value:.*]] = tt.load %[[store_ptr:.*]] : tensor<1024x!tt.ptr> +// CHECK: %[[store_value:.*]] = arith.select %[[mask:.*]], %[[loaded_value]], %[[origin_value]] +// CHECK: tt.store %[[store_ptr]], %[[store_value]] +tt.func @discrete_store(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<0> : tensor<1024xi32> + %cst_0 = arith.constant dense<200> : tensor<1024xi32> + %cst_1 = arith.constant dense<400> : tensor<1024xi32> + %0 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %1 = arith.cmpi slt, %0, %cst_0 : tensor<1024xi32> + %2 = arith.cmpi sgt, %0, %cst_1 : tensor<1024xi32> + %3 = arith.ori %1, %2 : tensor<1024xi1> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %8 = tt.load %5 : tensor<1024x!tt.ptr> + tt.store %7, %8, %3 : tensor<1024x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loop_mask_unsupported.mlir b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loop_mask_unsupported.mlir new file mode 100644 index 0000000000..1f2330830f --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loop_mask_unsupported.mlir @@ -0,0 +1,28 @@ +// RUN: not triton-opt --discrete-mask-access-conversion --triton-to-unstructure %s 2>&1 | FileCheck %s +// RUN: not triton-opt '--discrete-mask-access-conversion=compile-mode=simd_simt_template' '--triton-to-unstructure=compile-mode=simd_simt_template' %s 2>&1 | FileCheck %s + +// A changing non-prefix mask cannot use SIMD's unguarded full-load/select. +// CHECK: error: cannot lower this loop-carried mask with a masked indirect access for the selected target and compile mode +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @unsupported_loop_mask(%x: !tt.ptr, %out: !tt.ptr, %steps: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %init = arith.constant dense : tensor<16xi1> + %zeros = arith.constant dense<0.0> : tensor<16xf32> + %lane = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %base = tt.splat %x : !tt.ptr -> tensor<16x!tt.ptr> + %ptrs = tt.addptr %base, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %result:2 = scf.for %i = %c0 to %steps step %c1 iter_args(%mask = %init, %acc = %zeros) -> (tensor<16xi1>, tensor<16xf32>) : i32 { + %bound = tt.splat %i : i32 -> tensor<16xi32> + %current = arith.cmpi sge, %lane, %bound : tensor<16xi32> + %next = arith.andi %mask, %current : tensor<16xi1> + %value = tt.load %ptrs, %next, %zeros : tensor<16x!tt.ptr> + %sum = arith.addf %acc, %value : tensor<16xf32> + scf.yield %next, %sum : tensor<16xi1>, tensor<16xf32> + } + %output = tt.splat %out : !tt.ptr -> tensor<16x!tt.ptr> + %output_ptrs = tt.addptr %output, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %output_ptrs, %result#1 : tensor<16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loop_prefix_mask.mlir b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loop_prefix_mask.mlir new file mode 100644 index 0000000000..f94d6dc1f9 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DiscreteMaskAccess/loop_prefix_mask.mlir @@ -0,0 +1,139 @@ +// RUN: triton-opt --discrete-mask-access-conversion --split-input-file --verify-each %s | FileCheck %s --check-prefix=PREFIX +// RUN: triton-opt --triton-control-flow-opt --discrete-mask-access-conversion --triton-to-unstructure --triton-to-linalg --split-input-file --verify-each %s | FileCheck %s --check-prefix=LINALG --implicit-check-not=unrealized_conversion_cast --implicit-check-not=indirect_load + +// Growing limits must not reopen lanes closed by a preceding iteration. +// PREFIX-LABEL: tt.func public @prefix_history +// PREFIX: scf.for {{.*}} iter_args(%[[BOUND:.*]] = %{{.*}}, {{.*}}) -> (i32, tensor<16xf32>) +// PREFIX: %[[LIMIT:.*]] = arith.select +// PREFIX: %[[NEXT:.*]] = arith.minsi %[[BOUND]], %[[LIMIT]] : i32 +// PREFIX: %[[SPLAT:.*]] = tt.splat %[[NEXT]] : i32 -> tensor<16xi32> +// PREFIX: %[[MASK:.*]] = arith.cmpi slt, %{{.*}}, %[[SPLAT]] : tensor<16xi32> +// PREFIX: tt.load %{{.*}}, %[[MASK]], +// PREFIX: scf.yield %[[NEXT]], {{.*}} : i32, tensor<16xf32> +// LINALG-LABEL: func.func @prefix_history +// LINALG: scf.for {{.*}} iter_args(%[[BOUND:.*]] = %{{.*}}, {{.*}}) -> (i32, tensor<16xf32>) +// LINALG: %[[NEXT:.*]] = arith.minsi %[[BOUND]], %{{.*}} : i32 +// LINALG: %[[INDEX:.*]] = arith.index_cast %[[NEXT]] : i32 to index +// LINALG: %[[NONNEG:.*]] = arith.maxsi %[[INDEX]], %{{.*}} : index +// LINALG: %[[LENGTH:.*]] = arith.minsi %[[NONNEG]], %{{.*}} : index +// LINALG: memref.subview {{.*}}[0] [%[[LENGTH]]] [1] +// LINALG: memref.copy +// LINALG: scf.yield %[[NEXT]], {{.*}} : i32, tensor<16xf32> +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @prefix_history(%x: !tt.ptr, %out: !tt.ptr, + %steps: i32, %initial: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c4 = arith.constant 4 : i32 + %c12 = arith.constant 12 : i32 + %zeros = arith.constant dense<0.0> : tensor<16xf32> + %lane = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %init_bound = tt.splat %initial : i32 -> tensor<16xi32> + %init = arith.cmpi slt, %lane, %init_bound : tensor<16xi32> + %base = tt.splat %x : !tt.ptr -> tensor<16x!tt.ptr> + %ptrs = tt.addptr %base, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %result:2 = scf.for %i = %c0 to %steps step %c1 iter_args(%mask = %init, %acc = %zeros) -> (tensor<16xi1>, tensor<16xf32>) : i32 { + %odd = arith.andi %i, %c1 : i32 + %even = arith.cmpi eq, %odd, %c0 : i32 + %limit = arith.select %even, %c4, %c12 : i32 + %bound = tt.splat %limit : i32 -> tensor<16xi32> + %current = arith.cmpi slt, %lane, %bound : tensor<16xi32> + %next = arith.andi %current, %mask : tensor<16xi1> + %value = tt.load %ptrs, %next, %zeros : tensor<16x!tt.ptr> + %sum = arith.addf %acc, %value : tensor<16xf32> + scf.yield %next, %sum : tensor<16xi1>, tensor<16xf32> + } + %output = tt.splat %out : !tt.ptr -> tensor<16x!tt.ptr> + %output_ptrs = tt.addptr %output, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %output_ptrs, %result#1 : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- + +// An invariant mask can still use its initial continuous range even when a +// while permutes the before/after slots. +// PREFIX-LABEL: tt.func public @invariant_while_mask +// PREFIX-NOT: RuntimeLoopMask +// PREFIX: tt.load +// PREFIX-NOT: RuntimeLoopMask +// PREFIX: tt.return +// LINALG-LABEL: func.func @invariant_while_mask +// LINALG: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @invariant_while_mask(%x: !tt.ptr, %out: !tt.ptr, %steps: i32, %limit: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %zeros = arith.constant dense<0.0> : tensor<16xf32> + %lane = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %bound = tt.splat %limit : i32 -> tensor<16xi32> + %init = arith.cmpi slt, %lane, %bound : tensor<16xi32> + %base = tt.splat %x : !tt.ptr -> tensor<16x!tt.ptr> + %ptrs = tt.addptr %base, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %result:3 = scf.while (%i = %c0, %mask = %init, %acc = %zeros) : (i32, tensor<16xi1>, tensor<16xf32>) -> (tensor<16xi1>, i32, tensor<16xf32>) { + %keep = arith.cmpi slt, %i, %steps : i32 + scf.condition(%keep) %mask, %i, %acc : tensor<16xi1>, i32, tensor<16xf32> + } do { + ^bb0(%mask: tensor<16xi1>, %i: i32, %acc: tensor<16xf32>): + %value = tt.load %ptrs, %mask, %zeros : tensor<16x!tt.ptr> + %sum = arith.addf %acc, %value : tensor<16xf32> + %inc = arith.addi %i, %c1 : i32 + scf.yield %inc, %mask, %sum : i32, tensor<16xi1>, tensor<16xf32> + } + %output = tt.splat %out : !tt.ptr -> tensor<16x!tt.ptr> + %output_ptrs = tt.addptr %output, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %output_ptrs, %result#2 : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- + +// Keep pre-update users and the loop result tied to their own boundaries. +// This also exercises an all-true initial mask and zero-trip result semantics. +// PREFIX-LABEL: tt.func public @prefix_before_and_after +// PREFIX: %[[LOOP:.*]]:2 = scf.for {{.*}} iter_args(%[[BOUND:.*]] = %{{.*}}, {{.*}}) -> (i32, tensor<16xf32>) +// PREFIX: %[[OLD_SPLAT:.*]] = tt.splat %[[BOUND]] +// PREFIX: %[[OLD_MASK:.*]] = arith.cmpi slt, %{{.*}}, %[[OLD_SPLAT]] +// PREFIX: tt.load %{{.*}}, %[[OLD_MASK]], +// PREFIX: %[[NEXT:.*]] = arith.minsi %[[BOUND]], %{{.*}} : i32 +// PREFIX: scf.yield %[[NEXT]], {{.*}} : i32, tensor<16xf32> +// PREFIX: %[[FINAL_BOUND:.*]] = tt.splat %[[LOOP]]#0 +// PREFIX: %[[FINAL_MASK:.*]] = arith.cmpi slt, %{{.*}}, %[[FINAL_BOUND]] +// PREFIX: arith.extui %[[FINAL_MASK]] : tensor<16xi1> to tensor<16xi32> +// LINALG-LABEL: func.func @prefix_before_and_after +// LINALG: scf.for {{.*}} -> (i32, tensor<16xf32>) +// LINALG: memref.copy +// LINALG: arith.minsi {{.*}} : i32 +// LINALG: scf.yield {{.*}} : i32, tensor<16xf32> +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @prefix_before_and_after(%x: !tt.ptr, %out: !tt.ptr, + %mask_out: !tt.ptr, %steps: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c15 = arith.constant 15 : i32 + %init = arith.constant dense : tensor<16xi1> + %zeros = arith.constant dense<0.0> : tensor<16xf32> + %lane = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %base = tt.splat %x : !tt.ptr -> tensor<16x!tt.ptr> + %ptrs = tt.addptr %base, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %result:2 = scf.for %i = %c0 to %steps step %c1 iter_args(%mask = %init, %acc = %zeros) -> (tensor<16xi1>, tensor<16xf32>) : i32 { + %value = tt.load %ptrs, %mask, %zeros : tensor<16x!tt.ptr> + %sum = arith.addf %acc, %value : tensor<16xf32> + %limit = arith.subi %c15, %i : i32 + %bound = tt.splat %limit : i32 -> tensor<16xi32> + %current = arith.cmpi slt, %lane, %bound : tensor<16xi32> + %next = arith.andi %mask, %current : tensor<16xi1> + scf.yield %next, %sum : tensor<16xi1>, tensor<16xf32> + } + %output = tt.splat %out : !tt.ptr -> tensor<16x!tt.ptr> + %output_ptrs = tt.addptr %output, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %output_ptrs, %result#1 : tensor<16x!tt.ptr> + %mask_base = tt.splat %mask_out : !tt.ptr -> tensor<16x!tt.ptr> + %mask_ptrs = tt.addptr %mask_base, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %final = arith.extui %result#0 : tensor<16xi1> to tensor<16xi32> + tt.store %mask_ptrs, %final : tensor<16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/check-clone-tensor-empty-ops.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/check-clone-tensor-empty-ops.mlir new file mode 100644 index 0000000000..e0088480b8 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/check-clone-tensor-empty-ops.mlir @@ -0,0 +1,40 @@ +// RUN: triton-opt --clone-ops --allow-unregistered-dialect %s | FileCheck %s + +// Unit test for CloneOps pass: `tensor.empty` is a pure metadata op that +// allocates a tensor shape but holds no data. When it is cloned into a +// later block within a VECTOR main_loop, the cloned `tensor.empty` must be +// allowed (the validator skips it when checking for tensor-typed results). +// Without this carve-out, `tensor.empty` would be flagged as a tensor-typed +// result and the pass would signal a failure. + +// CHECK-LABEL: func.func @test_vector_clone_tensor_op +// The cloned `tensor.empty` (clone of the block-11 `tensor.empty`) must be +// present in block 12 with the `clone = 11` attribute. If the validator +// incorrectly rejected it, the IR would either be missing this op or the +// pass would signal a failure. +// CHECK: tensor.empty() {ssbuffer.block_id = 12 : i32, ssbuffer.clone = 11 : i32} : tensor<256x64xf32> +// The pre-existing block-12 tensor-typed ops (which are NOT clones) must +// remain untouched and have no `clone` attribute. +// CHECK: arith.addf {{%[0-9]+, %[0-9]+}} {ssbuffer.block_id = 12 : i32} : tensor<256x64xf32> +// CHECK-NOT: arith.addf {{.*}}ssbuffer.clone +// CHECK: linalg.fill {ssbuffer.block_id = 12 : i32} + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_vector_clone_tensor_op(%arg0: memref, %arg1: memref) attributes {global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i64 = arith.constant 0 : i64 + %c1_i64 = arith.constant 1 : i64 + %c10_i64 = arith.constant 10 : i64 + %c1_f32 = arith.constant 1.000000e+00 : f32 + %t1 = tensor.empty() : tensor<256x64xf32> + scope.scope : () -> () { + %0 = tensor.empty() : tensor<256x64xf32> + scf.for %arg2 = %c0_i64 to %c10_i64 step %c1_i64 : i64 { + %1 = tensor.empty() {ssbuffer.block_id = 11 : i32} : tensor<256x64xf32> + %2 = arith.addf %1, %t1 {ssbuffer.block_id = 12 : i32} : tensor<256x64xf32> + %3 = linalg.fill {ssbuffer.block_id = 12 : i32} ins(%c1_f32 : f32) outs(%2 : tensor<256x64xf32>) -> tensor<256x64xf32> + } {ssbuffer.block_id = 10 : i32, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return {ssbuffer.core_type = "VECTOR"} + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/check-clone-tensor-memref-ops.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/check-clone-tensor-memref-ops.mlir new file mode 100644 index 0000000000..c41aa5e132 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/check-clone-tensor-memref-ops.mlir @@ -0,0 +1,21 @@ +// RUN: triton-opt --clone-ops --verify-diagnostics %s --allow-unregistered-dialect + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_vector_clone_tensor_op(%arg0: memref, %arg1: memref) attributes {global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i64 = arith.constant 0 : i64 + %c1_i64 = arith.constant 1 : i64 + %c10_i64 = arith.constant 10 : i64 + %c1_f32 = arith.constant 1.000000e+00 : f32 + %t1 = tensor.empty() : tensor<256x64xf32> + scope.scope : () -> () { + %0 = tensor.empty() : tensor<256x64xf32> + scf.for %arg2 = %c0_i64 to %c10_i64 step %c1_i64 : i64 { + %1 = linalg.fill {ssbuffer.block_id = 11 : i32} ins(%c1_f32 : f32) outs(%0 : tensor<256x64xf32>) -> tensor<256x64xf32> + %2 = arith.addf %1, %t1 {ssbuffer.block_id = 12 : i32} : tensor<256x64xf32> + %3 = linalg.fill {ssbuffer.block_id = 12 : i32} ins(%c1_f32 : f32) outs(%2 : tensor<256x64xf32>) -> tensor<256x64xf32> + } {ssbuffer.block_id = 10 : i32, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return {ssbuffer.core_type = "VECTOR"} + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/clone-ops.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/clone-ops.mlir new file mode 100644 index 0000000000..292d7d45a3 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/clone-ops.mlir @@ -0,0 +1,129 @@ +// RUN: triton-opt --clone-ops %s --allow-unregistered-dialect | FileCheck %s + +// CHECK: func.func @test_clone_ops +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_clone_ops(%arg0: memref, %arg1: memref, %arg2: memref, %arg3: memref, %arg4: memref, %arg5: memref, %arg6: memref, %arg7: memref, %arg8: memref, %arg9: memref, %arg10: f32, %arg11: f32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32, %arg17: i32, %arg18: i32, %arg19: i32, %arg20: i32) attributes {global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i64 = arith.constant {ssbuffer.block_id = 17 : i32} 0 : i64 + %c1_i64 = arith.constant {ssbuffer.block_id = 17 : i32} 1 : i64 + %c8_i64 = arith.constant {ssbuffer.block_id = 18 : i32} 8 : i64 + %c63_i32 = arith.constant {ssbuffer.block_id = 17 : i32} 63 : i32 + %c64_i32 = arith.constant {ssbuffer.block_id = 17 : i32} 64 : i32 + %c256_i32 = arith.constant {ssbuffer.block_id = 17 : i32} 256 : i32 + %c256_i64 = arith.constant {ssbuffer.block_id = 17 : i32} 256 : i64 + %c0_i32 = arith.constant {ssbuffer.block_id = 17 : i32} 0 : i32 + %c3_i32 = arith.constant {ssbuffer.block_id = 17 : i32} 3 : i32 + %c1_i32 = arith.constant {ssbuffer.block_id = 17 : i32} 1 : i32 + %c2_i32 = arith.constant {ssbuffer.block_id = 17 : i32} 2 : i32 + %c0 = arith.constant {ssbuffer.block_id = 17 : i32} 0 : index + %c1 = arith.constant {ssbuffer.block_id = 17 : i32} 1 : index + %c256 = arith.constant {ssbuffer.block_id = 17 : i32} 256 : index + scope.scope : () -> () { + %0 = tensor.empty() {ssbuffer.block_id = 17 : i32} : tensor<256x64xf32> + %1 = linalg.fill {ssbuffer.block_id = 17 : i32} ins(%arg11 : f32) outs(%0 : tensor<256x64xf32>) -> tensor<256x64xf32> + %2 = linalg.fill {ssbuffer.block_id = 17 : i32} ins(%arg11 : f32) outs(%0 : tensor<256x64xf32>) -> tensor<256x64xf32> + %3 = arith.cmpi sle, %arg12, %c256_i32 {ssbuffer.block_id = 17 : i32} : i32 + %4 = scf.if %3 -> (i64) { + %ext = arith.extsi %c2_i32 {ssbuffer.block_id = 17 : i32} : i32 to i64 + scf.yield %ext : i64 + } else { + %reinterpret_cast = memref.reinterpret_cast %arg7 to offset: [2], sizes: [1], strides: [1] {ssbuffer.block_id = 7 : i32} : memref to memref<1xi32, strided<[1], offset: 2>> + %24 = memref.load %reinterpret_cast[%c0] {ssbuffer.block_id = 7 : i32} : memref<1xi32, strided<[1], offset: 2>> + %25 = arith.extsi %24 {ssbuffer.block_id = 7 : i32} : i32 to i64 + scf.yield %25 : i64 + } {ssbuffer.block_id = 19 : i32} + %5 = arith.muli %4, %c8_i64 {ssbuffer.block_id = 18 : i32} : i64 + %6 = arith.extsi %arg15 {ssbuffer.block_id = 18 : i32} : i32 to i64 + %7 = arith.minsi %6, %5 {ssbuffer.block_id = 18 : i32} : i64 + %8 = arith.divsi %5, %7 {ssbuffer.block_id = 18 : i32} : i64 + %9 = arith.addi %8, %c1_i64 {ssbuffer.block_id = 18 : i32} : i64 + %10 = arith.remsi %5, %7 {ssbuffer.block_id = 18 : i32} : i64 + %11 = arith.extsi %arg18 {ssbuffer.block_id = 18 : i32} : i32 to i64 + %12 = arith.cmpi slt, %11, %7 {ssbuffer.block_id = 18 : i32} : i64 + %13 = arith.cmpi slt, %11, %10 {ssbuffer.block_id = 18 : i32} : i64 + %14 = arith.muli %11, %9 {ssbuffer.block_id = 18 : i32} : i64 + %15 = arith.muli %10, %9 {ssbuffer.block_id = 18 : i32} : i64 + %16 = arith.subi %11, %10 {ssbuffer.block_id = 18 : i32} : i64 + %17 = arith.muli %16, %8 {ssbuffer.block_id = 18 : i32} : i64 + %18 = arith.addi %15, %17 {ssbuffer.block_id = 18 : i32} : i64 + %19 = arith.select %13, %14, %18 {ssbuffer.block_id = 18 : i32} : i64 + %20 = arith.select %12, %19, %c0_i64 {ssbuffer.block_id = 18 : i32} : i64 + %21 = arith.select %13, %9, %8 {ssbuffer.block_id = 18 : i32} : i64 + %22 = arith.select %12, %21, %c0_i64 {ssbuffer.block_id = 18 : i32} : i64 + %23 = arith.cmpi sge, %11, %7 {ssbuffer.block_id = 18 : i32} : i64 + scf.if %23 { + } else { + %25 = arith.addi %arg13, %c63_i32 {ssbuffer.block_id = 15 : i32} : i32 + %26 = arith.divsi %25, %c64_i32 {ssbuffer.block_id = 15 : i32} : i32 + %27 = arith.extsi %26 {ssbuffer.block_id = 15 : i32} : i32 to i64 + %28 = arith.muli %22, %27 {ssbuffer.block_id = 15 : i32} : i64 + %29 = linalg.fill {ssbuffer.block_id = 15 : i32} ins(%arg10 : f32) outs(%0 : tensor<256x64xf32>) -> tensor<256x64xf32> + %30 = linalg.fill {ssbuffer.block_id = 15 : i32} ins(%arg11 : f32) outs(%0 : tensor<256x64xf32>) -> tensor<256x64xf32> + %alloc = memref.alloc() {ssbuffer.block_id = 26 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x16x16x16xf16, #hivm.address_space> + %alloc_4 = memref.alloc() {ssbuffer.block_id = 26 : i32, ssbuffer.transfer_id = 1 : i32} : memref<256x64xf32, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 26 : i32, ssbuffer.transfer_id = 2 : i32} : memref<256x64xf32, #hivm.address_space> + scf.for %arg21 = %c0_i64 to %28 step %c1_i64 : i64 { + %31 = arith.divsi %arg21, %27 {ssbuffer.block_id = 11 : i32, ssbuffer.dep_mark = [14 : i32]} : i64 + %32 = arith.addi %20, %31 {ssbuffer.block_id = 11 : i32, ssbuffer.dep_mark = [5 : i32]} : i64 + %33 = arith.remsi %32, %4 {ssbuffer.block_id = 11 : i32, ssbuffer.dep_mark = [3 : i32, 4 : i32]} : i64 + // Test: op within the region depends on the preceding op in another block + // CHECK: arith.divsi {{.*}} {ssbuffer.block_id = 22 : i32, ssbuffer.clone = 11 : i32, ssbuffer.dep_mark = [14 : i32]} + // CHECK: arith.addi {{.*}} {ssbuffer.block_id = 22 : i32, ssbuffer.clone = 11 : i32, ssbuffer.dep_mark = [5 : i32]} + // CHECK: arith.remsi {{.*}} {ssbuffer.block_id = 22 : i32, ssbuffer.clone = 11 : i32, ssbuffer.dep_mark = [3 : i32, 4 : i32]} + %34:2 = scf.if %3 -> (i64, i64) { + scf.yield %33, %c0_i64 : i64, i64 + } else { + %66:2 = scf.for %arg22 = %c0_i32 to %c2_i32 step %c1_i32 iter_args(%arg23 = %c0_i32, %arg24 = %c3_i32) -> (i32, i32) : i32 { + %73 = arith.addi %arg23, %arg24 {ssbuffer.block_id = 9 : i32} : i32 + %74 = arith.divsi %73, %c2_i32 {ssbuffer.block_id = 9 : i32} : i32 + %75 = arith.index_cast %74 {ssbuffer.block_id = 9 : i32} : i32 to index + %reinterpret_cast_11 = memref.reinterpret_cast %arg7 to offset: [%75], sizes: [1], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %76 = memref.load %reinterpret_cast_11[%c0] {ssbuffer.block_id = 9 : i32} : memref<1xi32, strided<[1], offset: ?>> + %77 = arith.extsi %76 {ssbuffer.block_id = 9 : i32} : i32 to i64 + %78 = arith.cmpi sle, %77, %33 {ssbuffer.block_id = 9 : i32, ssbuffer.dep_mark = [3 : i32, 4 : i32]} : i64 + %79 = arith.select %78, %arg24, %74 {ssbuffer.block_id = 9 : i32} : i32 + %80 = scf.if %78 -> (i32) { + %81 = arith.addi %74, %c1_i32 {ssbuffer.block_id = 8 : i32} : i32 + scf.yield %81 : i32 + } else { + scf.yield %arg23 : i32 + } {ssbuffer.block_id = 20 : i32} + scf.yield %80, %79 : i32, i32 + } {ssbuffer.block_id = 21 : i32} + %67 = arith.subi %66#0, %c1_i32 {ssbuffer.block_id = 10 : i32} : i32 + %68 = arith.extsi %67 {ssbuffer.block_id = 10 : i32} : i32 to i64 + %69 = arith.index_cast %67 {ssbuffer.block_id = 10 : i32} : i32 to index + %reinterpret_cast_10 = memref.reinterpret_cast %arg7 to offset: [%69], sizes: [1], strides: [1] {ssbuffer.block_id = 10 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %70 = memref.load %reinterpret_cast_10[%c0] {ssbuffer.block_id = 10 : i32} : memref<1xi32, strided<[1], offset: ?>> + %71 = arith.extsi %70 {ssbuffer.block_id = 10 : i32} : i32 to i64 + %72 = arith.subi %33, %71 {ssbuffer.block_id = 10 : i32, ssbuffer.dep_mark = [3 : i32, 4 : i32]} : i64 + scf.yield %68, %72 : i64, i64 + } {ssbuffer.block_id = 22 : i32, ssbuffer.dep_mark = [1 : i32, 2 : i32]} + // Test: op depends on the preceding region op in another block + // CHECK: arith.divsi {{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.clone = 11 : i32, ssbuffer.dep_mark = [14 : i32]} + // CHECK: arith.addi {{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.clone = 11 : i32, ssbuffer.dep_mark = [5 : i32]} + // CHECK: arith.remsi {{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.clone = 11 : i32, ssbuffer.dep_mark = [3 : i32, 4 : i32]} + // CHECK: } {ssbuffer.block_id = 12 : i32, ssbuffer.clone = 22 : i32, ssbuffer.dep_mark = [1 : i32, 2 : i32]} + %35 = arith.index_cast %34#0 {ssbuffer.block_id = 12 : i32, ssbuffer.dep_mark = [1 : i32]} : i64 to index + %reinterpret_cast = memref.reinterpret_cast %arg5 to offset: [%35], sizes: [1], strides: [1] {ssbuffer.block_id = 12 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %36 = memref.load %reinterpret_cast[%c0] {ssbuffer.block_id = 12 : i32, ssbuffer.dep_mark = [6 : i32]} : memref<1xi64, strided<[1], offset: ?>> + %37 = arith.addi %35, %c1 {ssbuffer.block_id = 12 : i32} : index + %reinterpret_cast_6 = memref.reinterpret_cast %arg5 to offset: [%37], sizes: [1], strides: [1] {ssbuffer.block_id = 12 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %38 = memref.load %reinterpret_cast_6[%c0] {ssbuffer.block_id = 12 : i32} : memref<1xi64, strided<[1], offset: ?>> + %39 = arith.subi %38, %36 {ssbuffer.block_id = 12 : i32} : i64 + %40 = arith.muli %34#1, %c256_i64 {ssbuffer.block_id = 12 : i32, ssbuffer.dep_mark = [2 : i32]} : i64 + %41 = arith.index_cast %40 {ssbuffer.block_id = 12 : i32, ssbuffer.dep_mark = [7 : i32]} : i64 to index + %42 = arith.addi %41, %c256 {ssbuffer.block_id = 12 : i32} : index + %43 = arith.index_cast %39 {ssbuffer.block_id = 12 : i32} : i64 to index + %44 = arith.maxsi %41, %43 {ssbuffer.block_id = 12 : i32} : index + %45 = arith.minsi %42, %44 {ssbuffer.block_id = 12 : i32} : index + %46 = arith.subi %45, %41 {ssbuffer.block_id = 12 : i32, ssbuffer.dep_mark = [8 : i32]} : index + // Test: op depends on the preceding op in another block + // CHECK: arith.divsi {{.*}} {ssbuffer.block_id = 13 : i32, ssbuffer.clone = 11 : i32, ssbuffer.dep_mark = [14 : i32]} + %47 = arith.addi %20, %31 {ssbuffer.block_id = 13 : i32, ssbuffer.dep_mark = [14 : i32]} : i64 + } {ssbuffer.block_id = 26 : i32, ssbuffer.main_loop = 0 : i32} + } {ssbuffer.block_id = 27 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return {ssbuffer.core_type = "VECTOR"} + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/clone-wrapped-sync.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/clone-wrapped-sync.mlir new file mode 100644 index 0000000000..b2ee7bca84 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/clone-wrapped-sync.mlir @@ -0,0 +1,50 @@ +// RUN: triton-opt --clone-ops --verify-each %s | FileCheck %s +// RUN: triton-opt --clone-ops --verify-each %s | FileCheck %s --check-prefix=CLONED + +// Inter-core multi-buffering wraps the readiness notification in an scf.if +// picking the flag for this iteration's buffer half. CloneOps replays block 1 +// into block 3 to resolve the memref dependency on %cast, which is what it is +// there for, but the wrapper must not be replayed with it: one produced tile +// would then be announced twice, from two pipeline stages whose iteration +// counters are their own. +// +// test-clone-ops-if-only-sync.mlir covers a cloned wrapper the exec-after rule +// erases. Here the consumer reads the same buffer, so the memory dependences +// keep the clone alive and only matching the wrapped form by type erases it. +// +// Exactly one notification survives: the original scf.if, whose two branches +// account for both occurrences below. + +// CHECK-LABEL: func.func @cloned_wrapped_sync +// CHECK-COUNT-2: hivm.hir.sync_block_set +// CHECK-NOT: hivm.hir.sync_block_set +// CHECK: return + +// The dependency itself must still be resolved by duplication, or the check +// above passes for the wrong reason. +// CLONED-LABEL: func.func @cloned_wrapped_sync +// CLONED: ssbuffer.clone + +func.func @cloned_wrapped_sync(%out: memref<16x16xf16>, %upper: index) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %zero = arith.constant 0.0 : f16 + scope.scope : () -> () { + %buffer = memref.alloc() : memref<1x1x16x16xf16, #hivm.address_space> + scf.for %iv = %c0 to %upper step %c1 { + linalg.fill {ssbuffer.block_id = 1 : i32} ins(%zero : f16) outs(%buffer : memref<1x1x16x16xf16, #hivm.address_space>) + %layout = hivm.hir.convert_layout %buffer output_shape [16, 16] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32} : (memref<1x1x16x16xf16, #hivm.address_space>) -> memref<16x16xf16, #hivm.address_space> + %cast = memref.memory_space_cast %layout {ssbuffer.block_id = 1 : i32} : memref<16x16xf16, #hivm.address_space> to memref<16x16xf16> + %condition = arith.cmpi eq, %iv, %c0 {ssbuffer.block_id = 1 : i32} : index + scf.if %condition { + hivm.hir.sync_block_set {ssbuffer.block_id = 1 : i32}[, , ] flag = 2 + } else { + hivm.hir.sync_block_set {ssbuffer.block_id = 1 : i32}[, , ] flag = 6 + } {ssbuffer.block_id = 1 : i32} + %tensor = bufferization.to_tensor %cast restrict writable {ssbuffer.block_id = 3 : i32} : memref<16x16xf16> to tensor<16x16xf16> + hivm.hir.copy ins(%tensor : tensor<16x16xf16>) outs(%out : memref<16x16xf16>) {ssbuffer.block_id = 3 : i32} + } {ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/create-if-ops.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/create-if-ops.mlir new file mode 100644 index 0000000000..b3034a8842 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/create-if-ops.mlir @@ -0,0 +1,146 @@ +// RUN: triton-opt --create-if-ops %s --allow-unregistered-dialect | FileCheck %s + +// Test create-if-ops pass wraps operations by block_id with scf.if %true +// CHECK: func.func @test_create_if_ops +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_create_if_ops(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: memref {tt.divisibility = 16 : i32}, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 5 : i32} dense<[8, 8, 16, 16]> : tensor<4xi64> + %cst_0 = arith.constant {ssbuffer.block_id = 5 : i32} dense<[128, 8, 16]> : tensor<3xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 8 : i32} 0.000000e+00 : f32 + %c28_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 28 : i32 + %c65536_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 65536 : i32 + %c8192_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 8192 : i32 + %c128_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 128 : i32 + %c1048576_i64 = arith.constant {ssbuffer.block_id = 8 : i32} 1048576 : i64 + %c8388608_i64 = arith.constant {ssbuffer.block_id = 8 : i32} 8388608 : i64 + %c8_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 8 : i32 + %c64_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 64 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 0 : i32 + %cst_2 = arith.constant {ssbuffer.block_id = 8 : i32} 5.000000e-01 : f32 + %cst_3 = arith.constant {ssbuffer.block_id = 8 : i32} 0xFF800000 : f32 + %cst_4 = arith.constant {ssbuffer.block_id = 8 : i32} 1.000000e+00 : f32 + %c128 = arith.constant {ssbuffer.block_id = 8 : i32} 128 : index + scope.scope : () -> () { + %0 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<128x128xf32> + %1 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<128x128xf32> + %2 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_1 : f32) outs(%0 : tensor<128x128xf32>) -> tensor<128x128xf32> + %3 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_2 : f32) outs(%1 : tensor<128x128xf32>) -> tensor<128x128xf32> + %4 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<128xf32> + %5 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_3 : f32) outs(%4 : tensor<128xf32>) -> tensor<128xf32> + %6 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_4 : f32) outs(%4 : tensor<128xf32>) -> tensor<128xf32> + scf.for %arg16 = %arg13 to %c65536_i32 step %c28_i32 : i32 { + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.crossDeps = [0 : i32, 1 : i32], ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %alloc_6 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.crossDeps = [1 : i32, 1 : i32], ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %alloc_7 = memref.alloc() : memref<128xf32, #hivm.address_space> + %memspacecast = memref.memory_space_cast %alloc_7 {ssbuffer.intraDeps = [0 : i32, 1 : i32]} : memref<128xf32, #hivm.address_space> to memref<128xf32> + %7 = bufferization.to_tensor %memspacecast writable : memref<128xf32> to tensor<128xf32> + %alloc_8 = memref.alloc() : memref<128xf32, #hivm.address_space> + %memspacecast_9 = memref.memory_space_cast %alloc_8 {ssbuffer.intraDeps = [0 : i32, 1 : i32]} : memref<128xf32, #hivm.address_space> to memref<128xf32> + %8 = bufferization.to_tensor %memspacecast_9 writable : memref<128xf32> to tensor<128xf32> + %9:3 = scf.for %arg17 = %c0_i32 to %c8192_i32 step %c128_i32 iter_args(%arg18 = %6, %arg19 = %2, %arg20 = %5) -> (tensor<128xf32>, tensor<128x128xf32>, tensor<128xf32>) : i32 { + // CHECK: %true = arith.constant true + // CHECK: %{{.*}}:2 = scf.if %true -> (tensor<128xf32>, tensor<128xf32>) { + // CHECK: hivm.hir.sync_block_wait {ssbuffer.block_id = 5 : i32 + // CHECK: %13 = arith.mulf %12, %3 {ssbuffer.block_id = 5 : i32} + // CHECK: %16 = math.exp %15 {ssbuffer.block_id = 5 : i32} + // CHECK: %22 = arith.divui %arg17, %c128_i32 {ssbuffer.block_id = 5 : i32} + // CHECK: %c0_i32_14 = arith.constant {ssbuffer.block_id = 5 : i32} 0 : i32 + // CHECK: %c2_i32 = arith.constant {ssbuffer.block_id = 5 : i32} 2 : i32 + // CHECK: %23 = arith.remsi %22, %c2_i32 {ssbuffer.block_id = 5 : i32} + // CHECK: %24 = arith.cmpi eq, %23, %c0_i32_14 {ssbuffer.block_id = 5 : i32} + // CHECK: %25 = arith.mulf %arg18, %21 {ssbuffer.block_id = 5 : i32} + // CHECK: %26 = arith.addf %25, %reduced_13 {ssbuffer.block_id = 5 : i32} + // CHECK: hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + // CHECK: hivm.hir.sync_block_wait {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + // CHECK: hivm.hir.copy ins(%reshape_12 : tensor<8x8x16x16xf16>) outs(%alloc : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32} + // CHECK: } {{.*}}ssbuffer.if = 5 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %memspacecast_11 = memref.memory_space_cast %alloc_5 {ssbuffer.block_id = 5 : i32, ssbuffer.crossDeps = [0 : i32, 0 : i32], ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %33 = bufferization.to_tensor %memspacecast_11 restrict writable {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32> to tensor<128x128xf32> + %34 = arith.mulf %33, %3 {ssbuffer.block_id = 5 : i32} : tensor<128x128xf32> + %reduced = linalg.reduce ins(%34 : tensor<128x128xf32>) outs(%5 : tensor<128xf32>) dimensions = [1] {ssbuffer.block_id = 5 : i32} + (%in: f32, %init: f32) { + %55 = arith.maximumf %in, %init : f32 + linalg.yield %55 : f32 + } + %35 = arith.maximumf %arg20, %reduced {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + %broadcasted_12 = linalg.broadcast ins(%35 : tensor<128xf32>) outs(%1 : tensor<128x128xf32>) dimensions = [1] {ssbuffer.block_id = 5 : i32} + %36 = arith.subf %34, %broadcasted_12 {ssbuffer.block_id = 5 : i32} : tensor<128x128xf32> + %37 = math.exp %36 {ssbuffer.block_id = 5 : i32} : tensor<128x128xf32> + %38 = arith.truncf %37 {ssbuffer.block_id = 5 : i32} : tensor<128x128xf32> to tensor<128x128xf16> + %reshape = tensor.reshape %38(%cst_0) {ssbuffer.block_id = 5 : i32} : (tensor<128x128xf16>, tensor<3xi64>) -> tensor<128x8x16xf16> + annotation.mark %reshape {ssbuffer.block_id = 5 : i32, tiling_dim_mapping = {"1" = 1 : index}} : tensor<128x8x16xf16> + %39 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x128x16xf16> + %transposed = linalg.transpose ins(%reshape : tensor<128x8x16xf16>) outs(%39 : tensor<8x128x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 5 : i32} + %reshape_13 = tensor.reshape %transposed(%cst) {ssbuffer.block_id = 5 : i32} : (tensor<8x128x16xf16>, tensor<4xi64>) -> tensor<8x8x16x16xf16> + annotation.mark %reshape_13 {ssbuffer.block_id = 5 : i32, tiling_dim_mapping = {"1" = 1 : index}} : tensor<8x8x16x16xf16> + %40 = linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst_1 : f32) outs(%4 : tensor<128xf32>) -> tensor<128xf32> + %reduced_14 = linalg.reduce ins(%37 : tensor<128x128xf32>) outs(%40 : tensor<128xf32>) dimensions = [1] {ssbuffer.block_id = 5 : i32} + (%in: f32, %init: f32) { + %55 = arith.addf %in, %init : f32 + linalg.yield %55 : f32 + } + %41 = arith.subf %arg20, %35 {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + %42 = math.exp %41 {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + %43 = arith.divui %arg17, %c128_i32 {ssbuffer.block_id = 5 : i32} : i32 + %c0_i32_15 = arith.constant {ssbuffer.block_id = 5 : i32} 0 : i32 + %c2_i32 = arith.constant {ssbuffer.block_id = 5 : i32} 2 : i32 + %44 = arith.remsi %43, %c2_i32 {ssbuffer.block_id = 5 : i32} : i32 + %45 = arith.cmpi eq, %44, %c0_i32_15 {ssbuffer.block_id = 5 : i32} : i32 + scf.if %45 { + bufferization.materialize_in_destination %42 in restrict writable %memspacecast {ssbuffer.block_id = 5 : i32} : (tensor<128xf32>, memref<128xf32>) -> () + } else { + bufferization.materialize_in_destination %42 in restrict writable %memspacecast_9 {ssbuffer.block_id = 5 : i32} : (tensor<128xf32>, memref<128xf32>) -> () + } {ssbuffer.block_id = 5 : i32} + %46 = arith.mulf %arg18, %42 {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + %47 = arith.addf %46, %reduced_14 {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.copy ins(%reshape_13 : tensor<8x8x16x16xf16>) outs(%alloc : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + // CHECK: %true_10 = arith.constant true + // CHECK: %11 = scf.if %true_10 -> (tensor<128x128xf32>) { + // CHECK: hivm.hir.sync_block_wait {ssbuffer.block_id = 6 : i32 + // CHECK: %13 = arith.divui %arg17, %c128_i32 {ssbuffer.block_id = 6 : i32} + // CHECK: %c2_i32 = arith.constant {ssbuffer.block_id = 6 : i32} 2 : i32 + // CHECK: %14 = arith.remsi %13, %c2_i32 {ssbuffer.block_id = 6 : i32} + // CHECK: %c0_i32_12 = arith.constant {ssbuffer.block_id = 6 : i32} 0 : i32 + // CHECK: %15 = arith.cmpi eq, %14, %c0_i32_12 {ssbuffer.block_id = 6 : i32} + // CHECK: linalg.broadcast ins(%16 : tensor<128xf32>) outs(%1 : tensor<128x128xf32>) dimensions = [1] {ssbuffer.block_id = 6 : i32} + // CHECK: %17 = arith.mulf %arg19, %broadcasted {ssbuffer.block_id = 6 : i32} + // CHECK: %18 = arith.addf %12, %17 {ssbuffer.block_id = 6 : i32} + // CHECK: hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + // CHECK: } {{{.*}}ssbuffer.if = 6 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %memspacecast_16 = memref.memory_space_cast %alloc_6 {ssbuffer.block_id = 6 : i32, ssbuffer.crossDeps = [1, 0], ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %48 = bufferization.to_tensor %memspacecast_16 restrict writable {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32> to tensor<128x128xf32> + %49 = arith.divui %arg17, %c128_i32 {ssbuffer.block_id = 6 : i32} : i32 + %c2_i32_17 = arith.constant {ssbuffer.block_id = 6 : i32} 2 : i32 + %50 = arith.remsi %49, %c2_i32_17 {ssbuffer.block_id = 6 : i32} : i32 + %c0_i32_18 = arith.constant {ssbuffer.block_id = 6 : i32} 0 : i32 + %51 = arith.cmpi eq, %50, %c0_i32_18 {ssbuffer.block_id = 6 : i32} : i32 + %52 = scf.if %51 -> (tensor<128xf32>) { + %55 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 6 : i32} : memref<128xf32> to tensor<128xf32> + scf.yield %55 : tensor<128xf32> + } else { + %55 = bufferization.to_tensor %memspacecast_9 restrict writable {ssbuffer.block_id = 6 : i32} : memref<128xf32> to tensor<128xf32> + scf.yield %55 : tensor<128xf32> + } {ssbuffer.block_id = 6 : i32, ssbuffer.intraDeps = [0 : i32, 0 : i32]} + %broadcasted_19 = linalg.broadcast ins(%52 : tensor<128xf32>) outs(%1 : tensor<128x128xf32>) dimensions = [1] {ssbuffer.block_id = 6 : i32} + %53 = arith.mulf %arg19, %broadcasted_19 {ssbuffer.block_id = 6 : i32} : tensor<128x128xf32> + %54 = arith.addf %48, %53 {ssbuffer.block_id = 6 : i32} : tensor<128x128xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + scf.yield %47, %54, %35 : tensor<128xf32>, tensor<128x128xf32>, tensor<128xf32> + } {ssbuffer.block_id = 9 : i32, ssbuffer.main_loop = 0 : i64} + } {ssbuffer.block_id = 10 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return {ssbuffer.core_type = "VECTOR"} + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/intraDep_flow_opt_test.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/intraDep_flow_opt_test.mlir new file mode 100644 index 0000000000..860f5b0c5d --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/intraDep_flow_opt_test.mlir @@ -0,0 +1,91 @@ +// RUN: triton-opt --add-control-flow-condition %s | FileCheck %s + +// Unit Tests for AddControlFlowCondition Pass - Cross Core Dependencies Test +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // ============================================ + // Test Case 2: @test_data_flow_with_intra_dependent + // ============================================ + // C1 -> V1 + // V1 -> V2 + // V2 -> C2 + // C2 -> V3 + // flowopt condition will be added on C3 block + func.func @test_data_flow_with_intra_dependent(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: memref {tt.divisibility = 16 : i32}, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd", ssbuffer.inter_core_buf_count = 2 : i32, ssbuffer.intra_buf_count = 3 : i32} { + %c8192_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 8192 : i32 + %c128_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 128 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 0 : i32 + scope.scope : () -> () { + // Vector scope only process each vector core's ssbuffer ptr + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc0 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc0 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_5a = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_5a {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_6 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + %30:2 = scf.for %arg16 = %c0_i32 to %c8192_i32 step %c128_i32 iter_args(%arg17 = %c0_i32, %arg18 = %c0_i32) -> (i32, i32) : i32 { + + hivm.hir.sync_block_set {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 3 : i32, ssbuffer.intraDeps = [0 : i32, 1 : i32]}[, , ] flag = 4 + %memspacecast_11 = memref.memory_space_cast %alloc_5 {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 0 : i32], ssbuffer.intraDeps = [0 : i32, 1 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %reshape_13a = tensor.empty() {ssbuffer.block_id = 4 : i32, ssbuffer.intraDeps = [0 : i32, 1 : i32]} : tensor<8x8x16x16xf16> + %reshape_14a = tensor.empty() {ssbuffer.block_id = 4 : i32, ssbuffer.intraDeps = [1 : i32, 1 : i32]} : tensor<8x8x16x16xf16> + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 4 : i32} + + %reshape_13 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x8x16x16xf16> + %reshape_14 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x8x16x16xf16> + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + hivm.hir.copy ins(%reshape_13 : tensor<8x8x16x16xf16>) outs(%alloc : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 1 : i32], ssbuffer.intraDeps = [0 : i32, 0 : i32]} + hivm.hir.copy ins(%reshape_14 : tensor<8x8x16x16xf16>) outs(%alloc0 : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 1 : i32], ssbuffer.intraDeps = [1 : i32, 0 : i32]} + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 5 : i32} + + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %memspacecast_16 = memref.memory_space_cast %alloc_6 {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossCoreDeps = [1, 0]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 6 : i32} + scf.yield %arg17, %arg18 : i32, i32 + } {ssbuffer.block_id = 9 : i32, ssbuffer.main_loop = 0 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + scope.scope : () -> () { + %alloc_4 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_6 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_7 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_7 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + %20:2 = scf.for %arg16 = %c0_i32 to %c8192_i32 step %c128_i32 iter_args(%arg17 = %c0_i32, %arg18 = %c0_i32) -> (i32, i32) : i32 { + + hivm.hir.sync_block_wait {ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %30 = tensor.empty() {ssbuffer.block_id = 0 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 1 : i32]} ins(%30 : tensor<128x128xf32>) outs(%alloc_6 : memref<128x128xf32, #hivm.address_space>) + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 0 : i32} + + // CHECK: memref.load + // CHECK: arith.cmpi sgt + // CHECK: memref.load + // CHECK: arith.cmpi sgt + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: arith.cmpi sge + // CHECK: arith.cmpi sge + // CHECK: arith.ori + // CHECK: scf.if + hivm.hir.sync_block_wait {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %32 = hivm.hir.convert_layout %alloc_4 output_shape [128, 128] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32} : (memref<8x8x16x16xf16, #hivm.address_space>) -> memref<128x128xf16, #hivm.address_space> + %321 = hivm.hir.convert_layout %alloc_5 output_shape [128, 128] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 0 : i32]} : (memref<8x8x16x16xf16, #hivm.address_space>) -> memref<128x128xf16, #hivm.address_space> + %40 = tensor.empty() {ssbuffer.block_id = 1 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossCoreDeps = [1 : i32, 1 : i32]} ins(%40 : tensor<128x128xf32>) outs(%alloc_7 : memref<128x128xf32, #hivm.address_space>) + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 1 : i32} + scf.yield %arg17, %arg18 : i32, i32 + } {ssbuffer.block_id = 9 : i32, ssbuffer.main_loop = 0 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return {ssbuffer.core_type = "VECTOR"} + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/memref-copy-clone.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/memref-copy-clone.mlir new file mode 100644 index 0000000000..3baa5babc6 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/memref-copy-clone.mlir @@ -0,0 +1,188 @@ +// RUN: triton-opt --clone-ops %s --allow-unregistered-dialect | FileCheck %s + +// CHECK: func.func @test_memref_copy_clone +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_memref_copy_clone(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: f32, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 13 : i32} dense<[2, 2, 16, 16]> : tensor<4xi64> + %cst_0 = arith.constant {ssbuffer.block_id = 13 : i32} dense<[32, 2, 16]> : tensor<3xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 12 : i32} 0.000000e+00 : f32 + %c64_i32 = arith.constant {ssbuffer.block_id = 12 : i32} 64 : i32 + %c2_i32 = arith.constant {ssbuffer.block_id = 12 : i32} 2 : i32 + %c4_i32 = arith.constant {ssbuffer.block_id = 12 : i32} 4 : i32 + %c32_i32 = arith.constant {MixUse, ssbuffer.block_id = 12 : i32} 32 : i32 + %c8_i32 = arith.constant {ssbuffer.block_id = 12 : i32} 8 : i32 + %c31_i32 = arith.constant {ssbuffer.block_id = 12 : i32} 31 : i32 + %c4096_i64 = arith.constant {ssbuffer.block_id = 12 : i32} 4096 : i64 + %c0_i32 = arith.constant {Undefined, ssbuffer.block_id = 12 : i32} 0 : i32 + %c1_i32 = arith.constant {Undefined, ssbuffer.block_id = 12 : i32} 1 : i32 + %c256 = arith.constant {ssbuffer.block_id = 12 : i32} 256 : index + %c64 = arith.constant {ssbuffer.block_id = 12 : i32} 64 : index + %c0 = arith.constant {ssbuffer.block_id = 12 : i32} 0 : index + %c32 = arith.constant {ssbuffer.block_id = 12 : i32} 32 : index + %c8 = arith.constant {ssbuffer.block_id = 12 : i32} 8 : index + %c512 = arith.constant {ssbuffer.block_id = 12 : i32} 512 : index + scope.scope : () -> () { + %0 = arith.divsi %arg15, %c8_i32 {ssbuffer.block_id = 8 : i32} : i32 + %1 = arith.remsi %arg15, %c8_i32 {ssbuffer.block_id = 8 : i32} : i32 + %2 = arith.addi %arg9, %c31_i32 {ssbuffer.block_id = 8 : i32} : i32 + %3 = arith.divsi %2, %c32_i32 {ssbuffer.block_id = 8 : i32} : i32 + %4 = arith.muli %0, %3 {ssbuffer.block_id = 8 : i32} : i32 + %5 = arith.addi %4, %arg14 {ssbuffer.block_id = 8 : i32} : i32 + %6 = arith.muli %0, %arg9 {ssbuffer.block_id = 8 : i32} : i32 + %7 = arith.muli %6, %c4_i32 {ssbuffer.block_id = 8 : i32} : i32 + %8 = arith.divsi %1, %c2_i32 {ssbuffer.block_id = 8 : i32} : i32 + %9 = arith.addi %7, %8 {ssbuffer.block_id = 8 : i32} : i32 + %10 = arith.muli %9, %c64_i32 {ssbuffer.block_id = 8 : i32} : i32 + %11 = arith.index_cast %10 {ssbuffer.block_id = 8 : i32} : i32 to index + %12 = arith.muli %5, %c8_i32 {ssbuffer.block_id = 8 : i32} : i32 + %13 = arith.addi %12, %1 {ssbuffer.block_id = 8 : i32} : i32 + %14 = arith.extsi %13 {ssbuffer.block_id = 8 : i32} : i32 to i64 + %15 = arith.muli %14, %c4096_i64 {ssbuffer.block_id = 8 : i32} : i64 + %16 = arith.index_cast %15 {ssbuffer.block_id = 8 : i32} : i64 to index + %17 = arith.muli %arg14, %c32_i32 {MixUse, ssbuffer.block_id = 8 : i32} : i32 + %18 = arith.muli %arg13, %c32_i32 {ssbuffer.block_id = 8 : i32} : i32 + %alloc = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32x32xf16> + %alloc_2 = memref.alloc() {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 2 : i32} : memref<32x32xf32, #hivm.address_space> + %alloc_3 = memref.alloc() {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 2 : i32} : memref<32x32xf32, #hivm.address_space> + annotation.mark %alloc_3 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 2 : i32} : memref<32x32xf32, #hivm.address_space> + annotation.mark %alloc_2 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 2 : i32} : memref<32x32xf32, #hivm.address_space> + %alloc_4 = memref.alloc() {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 3 : i32} : memref<32x32xf32, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 3 : i32} : memref<32x32xf32, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<5>, ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 3 : i32} : memref<32x32xf32, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 3 : i32} : memref<32x32xf32, #hivm.address_space> + scf.for %arg16 = %c0_i32 to %c2_i32 step %c1_i32 : i32 { + %alloc_9 = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<32x32xf16> + %alloc_10 = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<32x32xf16> + %alloc_11 = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<32x32xf16> + %56 = arith.muli %arg16, %c32_i32 {ssbuffer.block_id = 3 : i32} : i32 + %57 = arith.maxsi %17, %c0_i32 {ssbuffer.block_id = 3 : i32} : i32 + %58 = arith.index_cast %57 {ssbuffer.block_id = 3 : i32} : i32 to index + %59 = arith.maxsi %56, %c0_i32 {ssbuffer.block_id = 3 : i32} : i32 + %60 = arith.index_cast %59 {ssbuffer.block_id = 3 : i32} : i32 to index + %61 = arith.muli %58, %c256 {ssbuffer.block_id = 3 : i32} : index + %62 = arith.addi %61, %11 {ssbuffer.block_id = 3 : i32} : index + %63 = arith.index_cast %arg9 {ssbuffer.block_id = 3 : i32} : i32 to index + %64 = arith.addi %62, %60 {ssbuffer.block_id = 3 : i32} : index + %reinterpret_cast_12 = memref.reinterpret_cast %arg2 to offset: [%64], sizes: [32, 32], strides: [256, 1] {ssbuffer.block_id = 3 : i32} : memref to memref<32x32xf16, strided<[256, 1], offset: ?>> + %65 = arith.maxsi %18, %c0_i32 {ssbuffer.block_id = 3 : i32} : i32 + %66 = arith.index_cast %65 {ssbuffer.block_id = 3 : i32} : i32 to index + %67 = arith.muli %60, %c64 {ssbuffer.block_id = 3 : i32} : index + %68 = arith.addi %67, %16 {ssbuffer.block_id = 3 : i32} : index + %69 = arith.addi %68, %66 {ssbuffer.block_id = 3 : i32} : index + %reinterpret_cast_13 = memref.reinterpret_cast %arg5 to offset: [%69], sizes: [32, 32], strides: [64, 1] {ssbuffer.block_id = 3 : i32} : memref to memref<32x32xf16, strided<[64, 1], offset: ?>> + %70 = arith.subi %64, %11 {ssbuffer.block_id = 3 : i32} : index + %71 = arith.divsi %70, %c256 {ssbuffer.block_id = 3 : i32} : index + %72 = arith.subi %63, %71 {ssbuffer.block_id = 3 : i32} : index + %73 = arith.maxsi %72, %c0 {ssbuffer.block_id = 3 : i32} : index + %74 = arith.minsi %73, %c32 {ssbuffer.block_id = 3 : i32} : index + %75 = arith.remsi %70, %c256 {ssbuffer.block_id = 3 : i32} : index + %76 = arith.subi %c64, %75 {ssbuffer.block_id = 3 : i32} : index + %77 = arith.maxsi %76, %c0 {ssbuffer.block_id = 3 : i32} : index + %78 = arith.minsi %77, %c32 {ssbuffer.block_id = 3 : i32} : index + %79 = arith.subi %c0_i32, %17 {ssbuffer.block_id = 3 : i32} : i32 + %80 = arith.maxsi %79, %c0_i32 {ssbuffer.block_id = 3 : i32} : i32 + %81 = arith.index_cast %80 {ssbuffer.block_id = 3 : i32} : i32 to index + %82 = arith.minsi %81, %74 {ssbuffer.block_id = 3 : i32} : index + %83 = arith.subi %74, %82 {ssbuffer.block_id = 3 : i32} : index + %84 = arith.subi %c0_i32, %56 {ssbuffer.block_id = 3 : i32} : i32 + %85 = arith.maxsi %84, %c0_i32 {ssbuffer.block_id = 3 : i32} : i32 + %86 = arith.index_cast %85 {ssbuffer.block_id = 3 : i32} : i32 to index + %87 = arith.minsi %86, %78 {ssbuffer.block_id = 3 : i32} : index + %88 = arith.subi %78, %87 {ssbuffer.block_id = 3 : i32} : index + %subview_14 = memref.subview %reinterpret_cast_12[0, 0] [%83, %88] [1, 1] {ssbuffer.block_id = 3 : i32} : memref<32x32xf16, strided<[256, 1], offset: ?>> to memref> + %subview_15 = memref.subview %alloc_9[%82, %87] [%83, %88] [1, 1] {ssbuffer.block_id = 3 : i32} : memref<32x32xf16> to memref> + memref.copy %subview_14, %subview_15 {ssbuffer.block_id = 3 : i32} : memref> to memref> + %89 = bufferization.to_tensor %alloc_9 restrict writable {gm_load_bufferable, ssbuffer.block_id = 3 : i32} : memref<32x32xf16> to tensor<32x32xf16> + %90 = arith.subi %69, %16 {ssbuffer.block_id = 3 : i32} : index + %91 = arith.divsi %90, %c64 {ssbuffer.block_id = 3 : i32} : index + %92 = arith.subi %c64, %91 {ssbuffer.block_id = 3 : i32} : index + %93 = arith.maxsi %92, %c0 {ssbuffer.block_id = 3 : i32} : index + %94 = arith.minsi %93, %c32 {ssbuffer.block_id = 3 : i32} : index + %95 = arith.remsi %90, %c64 {ssbuffer.block_id = 3 : i32} : index + %96 = arith.subi %c64, %95 {ssbuffer.block_id = 3 : i32} : index + %97 = arith.maxsi %96, %c0 {ssbuffer.block_id = 3 : i32} : index + %98 = arith.minsi %97, %c32 {ssbuffer.block_id = 3 : i32} : index + %99 = arith.minsi %86, %94 {ssbuffer.block_id = 3 : i32} : index + %100 = arith.subi %94, %99 {ssbuffer.block_id = 3 : i32} : index + %101 = arith.subi %c0_i32, %18 {ssbuffer.block_id = 3 : i32} : i32 + %102 = arith.maxsi %101, %c0_i32 {ssbuffer.block_id = 3 : i32} : i32 + %103 = arith.index_cast %102 {ssbuffer.block_id = 3 : i32} : i32 to index + %104 = arith.minsi %103, %98 {ssbuffer.block_id = 3 : i32} : index + %105 = arith.subi %98, %104 {ssbuffer.block_id = 3 : i32} : index + %subview_16 = memref.subview %reinterpret_cast_13[0, 0] [%100, %105] [1, 1] {ssbuffer.block_id = 3 : i32} : memref<32x32xf16, strided<[64, 1], offset: ?>> to memref> + %subview_17 = memref.subview %alloc_11[%99, %104] [%100, %105] [1, 1] {ssbuffer.block_id = 3 : i32} : memref<32x32xf16> to memref> + memref.copy %subview_16, %subview_17 {ssbuffer.block_id = 3 : i32} : memref> to memref> + %106 = bufferization.to_tensor %alloc_11 restrict writable {gm_load_bufferable, ssbuffer.block_id = 3 : i32} : memref<32x32xf16> to tensor<32x32xf16> + %107 = tensor.empty() {ssbuffer.block_id = 3 : i32} : tensor<32x32xf32> + %108 = linalg.fill {ssbuffer.block_id = 3 : i32} ins(%cst_1 : f32) outs(%107 : tensor<32x32xf32>) -> tensor<32x32xf32> + %109 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 3 : i32} ins(%89, %106 : tensor<32x32xf16>, tensor<32x32xf16>) outs(%108 : tensor<32x32xf32>) -> tensor<32x32xf32> + %110 = arith.divsi %arg16, %c1_i32 {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32} : i32 + %c2_i32_18 = arith.constant {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32} 2 : i32 + %111 = arith.remsi %110, %c2_i32_18 {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32} : i32 + %c0_i32_19 = arith.constant {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32} 0 : i32 + %112 = arith.cmpi eq, %111, %c0_i32_19 {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32} : i32 + scf.if %112 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + } else { + hivm.hir.sync_block_wait {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 8 + } {ssbuffer.block_id = 3 : i32, ssbuffer.cross_buffer = 1 : i32} + scf.if %112 { + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32} ins(%109 : tensor<32x32xf32>) outs(%alloc_4 : memref<32x32xf32, #hivm.address_space>) + } else { + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32} ins(%109 : tensor<32x32xf32>) outs(%alloc_5 : memref<32x32xf32, #hivm.address_space>) + } {ssbuffer.block_id = 3 : i32, ssbuffer.cross_buffer = 1 : i32, ssbuffer.transfer_id = 3 : i32} + scf.if %112 { + hivm.hir.sync_block_set {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + } else { + hivm.hir.sync_block_set {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 8 + } {ssbuffer.block_id = 3 : i32, ssbuffer.cross_buffer = 1 : i32} + // CHECK: %[[ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 4 : i32, + // CHECK-SAME: ssbuffer.clone = 5 : i32} : memref<32x32xf16> + // CHECK: %[[SUBVIEW0:.*]] = memref.subview %{{.*}}[0, 0] [{{.*}}, {{.*}}] [1, 1] + // CHECK-SAME: {ssbuffer.block_id = 4 : i32, ssbuffer.clone = 3 : i32} + // CHECK-SAME: : memref<32x32xf16, strided<[256, 1], offset: ?>> to memref> + // CHECK: %[[SUBVIEW1:.*]] = memref.subview %[[ALLOC]]{{.*}} [{{.*}}, {{.*}}] [1, 1] + // CHECK-SAME: {ssbuffer.block_id = 4 : i32, ssbuffer.clone = 3 : i32} + // CHECK-SAME: : memref<32x32xf16> to memref> + // CHECK: memref.copy %[[SUBVIEW0]], %[[SUBVIEW1]] {ssbuffer.block_id = 4 : i32, + // CHECK-SAME: ssbuffer.clone = 3 : i32} + // CHECK-SAME: : memref> to memref> + // CHECK: %[[TO_TENSOR:.*]] = bufferization.to_tensor %[[ALLOC]] restrict writable + // CHECK-SAME: {gm_load_bufferable, ssbuffer.block_id = 4 : i32, ssbuffer.clone = 3 : i32} + // CHECK-SAME: : memref<32x32xf16> + %reinterpret_cast_20 = memref.reinterpret_cast %arg3 to offset: [%64], sizes: [32, 32], strides: [256, 1] {ssbuffer.block_id = 4 : i32} : memref to memref<32x32xf16, strided<[256, 1], offset: ?>> + %subview_21 = memref.subview %reinterpret_cast_20[0, 0] [%83, %88] [1, 1] {ssbuffer.block_id = 4 : i32} : memref<32x32xf16, strided<[256, 1], offset: ?>> to memref> + %subview_22 = memref.subview %alloc_10[%82, %87] [%83, %88] [1, 1] {ssbuffer.block_id = 4 : i32} : memref<32x32xf16> to memref> + memref.copy %subview_21, %subview_22 {ssbuffer.block_id = 4 : i32} : memref> to memref> + %113 = bufferization.to_tensor %alloc_10 restrict writable {gm_load_bufferable, ssbuffer.block_id = 4 : i32} : memref<32x32xf16> to tensor<32x32xf16> + %114 = tensor.empty() {ssbuffer.block_id = 4 : i32} : tensor<32x32xf16> + %transposed = linalg.transpose ins(%113 : tensor<32x32xf16>) outs(%114 : tensor<32x32xf16>) permutation = [1, 0] {ssbuffer.block_id = 4 : i32} + %115 = tensor.empty() {ssbuffer.block_id = 4 : i32} : tensor<32x32xf32> + %116 = linalg.fill {ssbuffer.block_id = 4 : i32} ins(%cst_1 : f32) outs(%115 : tensor<32x32xf32>) -> tensor<32x32xf32> + %117 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 4 : i32} ins(%89, %transposed : tensor<32x32xf16>, tensor<32x32xf16>) outs(%116 : tensor<32x32xf32>) -> tensor<32x32xf32> + %118 = arith.divsi %arg16, %c1_i32 {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32} : i32 + %c2_i32_23 = arith.constant {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32} 2 : i32 + %119 = arith.remsi %118, %c2_i32_23 {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32} : i32 + %c0_i32_24 = arith.constant {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32} 0 : i32 + %120 = arith.cmpi eq, %119, %c0_i32_24 {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32} : i32 + scf.if %120 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + } else { + hivm.hir.sync_block_wait {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 6 + } {ssbuffer.block_id = 4 : i32, ssbuffer.cross_buffer = 1 : i32} + scf.if %120 { + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32} ins(%117 : tensor<32x32xf32>) outs(%alloc_2 : memref<32x32xf32, #hivm.address_space>) + } else { + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32} ins(%117 : tensor<32x32xf32>) outs(%alloc_3 : memref<32x32xf32, #hivm.address_space>) + } {ssbuffer.block_id = 4 : i32, ssbuffer.cross_buffer = 1 : i32, ssbuffer.transfer_id = 2 : i32} + scf.if %120 { + hivm.hir.sync_block_set {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + } else { + hivm.hir.sync_block_set {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 6 + } {ssbuffer.block_id = 4 : i32, ssbuffer.cross_buffer = 1 : i32} + } {DataUse, ssbuffer.block_id = 19 : i32, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/multi_flow_opt_test.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/multi_flow_opt_test.mlir new file mode 100644 index 0000000000..720941dd55 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/multi_flow_opt_test.mlir @@ -0,0 +1,146 @@ +// RUN: triton-opt --add-control-flow-condition %s | FileCheck %s + +// Unit Tests for AddControlFlowCondition Pass - Cross Core Dependencies Test +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // ============================================ + // Test Case 1: @test_multiple_CV_data_flow + // ============================================ + // C1 C2 -> V1 + // V1 -> C3 + // C3-> V2 + // flowopt condition will be added on C3 block + func.func @test_multiple_CV_data_flow(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: memref {tt.divisibility = 16 : i32}, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd", ssbuffer.inter_core_buf_count = 2 : i32, ssbuffer.intra_buf_count = 3 : i32} { + %c8192_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 8192 : i32 + %c128_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 128 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 0 : i32 + scope.scope : () -> () { + // Vector scope only process each vector core's ssbuffer ptr + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc0 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc0 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_5a = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_5a {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_6 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + %30:2 = scf.for %arg16 = %c0_i32 to %c8192_i32 step %c128_i32 iter_args(%arg17 = %c0_i32, %arg18 = %c0_i32) -> (i32, i32) : i32 { + // CHECK: memref.load + // CHECK: arith.cmpi sgt + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: scf.if + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %memspacecast_11 = memref.memory_space_cast %alloc_5 {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 0 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %memspacecast_13 = memref.memory_space_cast %alloc_5a {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [3 : i32, 0 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %reshape_13 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x8x16x16xf16> + hivm.hir.copy ins(%reshape_13 : tensor<8x8x16x16xf16>) outs(%alloc : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 1 : i32]} + %reshape_14 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x8x16x16xf16> + hivm.hir.copy ins(%reshape_14 : tensor<8x8x16x16xf16>) outs(%alloc0 : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 1 : i32]} + // CHECK: memref.load + // CHECK: arith.subi + // CHECK: memref.store + // CHECK: memref.load + // CHECK: arith.addi + // CHECK: memref.store + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 5 : i32} + + // This block only contain an input + // CHECK: memref.load + // CHECK: arith.cmpi sgt + // CHECK: scf.if + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %memspacecast_16 = memref.memory_space_cast %alloc_6 {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossCoreDeps = [1, 0]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + // CHECK: memref.load + // CHECK: arith.subi + // CHECK: memref.store + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 6 : i32} + scf.yield %arg17, %arg18 : i32, i32 + } {ssbuffer.block_id = 9 : i32, ssbuffer.main_loop = 0 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + scope.scope : () -> () { + %alloc_4 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_6 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_6a = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_6a {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_7 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_7 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + %20:2 = scf.for %arg16 = %c0_i32 to %c8192_i32 step %c128_i32 iter_args(%arg17 = %c0_i32, %arg18 = %c0_i32) -> (i32, i32) : i32 { + // This block contain a output + // in CUBE scope, it need to process two ssbuffer ptr + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: scf.if + hivm.hir.sync_block_wait {ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %30 = tensor.empty() {ssbuffer.block_id = 0 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 1 : i32]} ins(%30 : tensor<128x128xf32>) outs(%alloc_6 : memref<128x128xf32, #hivm.address_space>) + // CHECK: memref.load + // CHECK: arith.addi + // CHECK: memref.store + // CHECK: memref.load + // CHECK: arith.addi + // CHECK: memref.store + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 0 : i32} + + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: scf.if + hivm.hir.sync_block_wait {ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %31 = tensor.empty() {ssbuffer.block_id = 2 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [3 : i32, 1 : i32]} ins(%31 : tensor<128x128xf32>) outs(%alloc_6a : memref<128x128xf32, #hivm.address_space>) + // CHECK: memref.load + // CHECK: arith.addi + // CHECK: memref.store + // CHECK: memref.load + // CHECK: arith.addi + // CHECK: memref.store + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 2 : i32} + + // This block contain an input and a output + // CHECK: memref.load + // CHECK: arith.cmpi sgt + // CHECK: memref.load + // CHECK: arith.cmpi sgt + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: memref.load + // CHECK: arith.cmpi slt + // CHECK: arith.cmpi sge + // CHECK: arith.cmpi sge + // CHECK: arith.ori + // CHECK: scf.if + hivm.hir.sync_block_wait {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %32 = hivm.hir.convert_layout %alloc_4 output_shape [128, 128] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32} : (memref<8x8x16x16xf16, #hivm.address_space>) -> memref<128x128xf16, #hivm.address_space> + %321 = hivm.hir.convert_layout %alloc_5 output_shape [128, 128] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 0 : i32]} : (memref<8x8x16x16xf16, #hivm.address_space>) -> memref<128x128xf16, #hivm.address_space> + %40 = tensor.empty() {ssbuffer.block_id = 1 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossCoreDeps = [1 : i32, 1 : i32]} ins(%40 : tensor<128x128xf32>) outs(%alloc_7 : memref<128x128xf32, #hivm.address_space>) + // CHECK: memref.load + // CHECK: arith.subi + // CHECK: memref.store + // CHECK: memref.load + // CHECK: arith.subi + // CHECK: memref.store + // CHECK: memref.load + // CHECK: arith.addi + // CHECK: memref.store + // CHECK: memref.load + // CHECK: arith.addi + // CHECK: memref.store + // CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 1 : i32} + scf.yield %arg17, %arg18 : i32, i32 + } {ssbuffer.block_id = 9 : i32, ssbuffer.main_loop = 0 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return {ssbuffer.core_type = "VECTOR"} + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/skip-control-flow.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/skip-control-flow.mlir new file mode 100644 index 0000000000..ceb0ab45ac --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/skip-control-flow.mlir @@ -0,0 +1,117 @@ +// RUN: triton-opt --add-control-flow-condition %s --allow-unregistered-dialect | FileCheck %s --implicit-check-not=ssbuffer.clone --implicit-check-not=ssbuffer.if +// CHECK: module attributes +// CHECK: func.func @test_skip_control_flow + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_skip_control_flow(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: memref {tt.divisibility = 16 : i32}, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 5 : i32} dense<[8, 8, 16, 16]> : tensor<4xi64> + %cst_0 = arith.constant {ssbuffer.block_id = 5 : i32} dense<[128, 8, 16]> : tensor<3xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 8 : i32} 0.000000e+00 : f32 + %c28_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 28 : i32 + %c65536_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 65536 : i32 + %c8192_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 8192 : i32 + %c128_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 128 : i32 + %c1048576_i64 = arith.constant {ssbuffer.block_id = 8 : i32} 1048576 : i64 + %c8388608_i64 = arith.constant {ssbuffer.block_id = 8 : i32} 8388608 : i64 + %c8_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 8 : i32 + %c64_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 64 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 0 : i32 + %cst_2 = arith.constant {ssbuffer.block_id = 8 : i32} 5.000000e-01 : f32 + %cst_3 = arith.constant {ssbuffer.block_id = 8 : i32} 0xFF800000 : f32 + %cst_4 = arith.constant {ssbuffer.block_id = 8 : i32} 1.000000e+00 : f32 + %c128 = arith.constant {ssbuffer.block_id = 8 : i32} 128 : index + scope.scope : () -> () { + %0 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<128x128xf32> + %1 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<128x128xf32> + %2 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_1 : f32) outs(%0 : tensor<128x128xf32>) -> tensor<128x128xf32> + %3 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_2 : f32) outs(%1 : tensor<128x128xf32>) -> tensor<128x128xf32> + %4 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<128xf32> + %5 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_3 : f32) outs(%4 : tensor<128xf32>) -> tensor<128xf32> + %6 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_4 : f32) outs(%4 : tensor<128xf32>) -> tensor<128xf32> + scf.for %arg16 = %arg13 to %c65536_i32 step %c28_i32 : i32 { + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossDeps = [0 : i32, 1 : i32]} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %alloc_6 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossDeps = [1 : i32, 1 : i32]} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %alloc_7 = memref.alloc() : memref<128xf32, #hivm.address_space> + %memspacecast = memref.memory_space_cast %alloc_7 {ssbuffer.intraDeps = [0 : i32, 1 : i32]} : memref<128xf32, #hivm.address_space> to memref<128xf32> + %7 = bufferization.to_tensor %memspacecast writable : memref<128xf32> to tensor<128xf32> + %alloc_8 = memref.alloc() : memref<128xf32, #hivm.address_space> + %memspacecast_9 = memref.memory_space_cast %alloc_8 {ssbuffer.intraDeps = [0 : i32, 1 : i32]} : memref<128xf32, #hivm.address_space> to memref<128xf32> + %8 = bufferization.to_tensor %memspacecast_9 writable : memref<128xf32> to tensor<128xf32> + %9:3 = scf.for %arg17 = %c0_i32 to %c8192_i32 step %c128_i32 iter_args(%arg18 = %6, %arg19 = %2, %arg20 = %5) -> (tensor<128xf32>, tensor<128x128xf32>, tensor<128xf32>) : i32 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %memspacecast_11 = memref.memory_space_cast %alloc_5 {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossDeps = [0 : i32, 0 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %33 = bufferization.to_tensor %memspacecast_11 restrict writable {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32> to tensor<128x128xf32> + %34 = arith.mulf %33, %3 {ssbuffer.block_id = 5 : i32} : tensor<128x128xf32> + %reduced = linalg.reduce ins(%34 : tensor<128x128xf32>) outs(%5 : tensor<128xf32>) dimensions = [1] {ssbuffer.block_id = 5 : i32} + (%in: f32, %init: f32) { + %55 = arith.maximumf %in, %init : f32 + linalg.yield %55 : f32 + } + %35 = arith.maximumf %arg20, %reduced {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + %broadcasted_12 = linalg.broadcast ins(%35 : tensor<128xf32>) outs(%1 : tensor<128x128xf32>) dimensions = [1] {ssbuffer.block_id = 5 : i32} + %36 = arith.subf %34, %broadcasted_12 {ssbuffer.block_id = 5 : i32} : tensor<128x128xf32> + %37 = math.exp %36 {ssbuffer.block_id = 5 : i32} : tensor<128x128xf32> + %38 = arith.truncf %37 {ssbuffer.block_id = 5 : i32} : tensor<128x128xf32> to tensor<128x128xf16> + %reshape = tensor.reshape %38(%cst_0) {ssbuffer.block_id = 5 : i32} : (tensor<128x128xf16>, tensor<3xi64>) -> tensor<128x8x16xf16> + annotation.mark %reshape {ssbuffer.block_id = 5 : i32, tiling_dim_mapping = {"1" = 1 : index}} : tensor<128x8x16xf16> + %39 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x128x16xf16> + %transposed = linalg.transpose ins(%reshape : tensor<128x8x16xf16>) outs(%39 : tensor<8x128x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 5 : i32} + %reshape_13 = tensor.reshape %transposed(%cst) {ssbuffer.block_id = 5 : i32} : (tensor<8x128x16xf16>, tensor<4xi64>) -> tensor<8x8x16x16xf16> + annotation.mark %reshape_13 {ssbuffer.block_id = 5 : i32, tiling_dim_mapping = {"1" = 1 : index}} : tensor<8x8x16x16xf16> + %40 = linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst_1 : f32) outs(%4 : tensor<128xf32>) -> tensor<128xf32> + %reduced_14 = linalg.reduce ins(%37 : tensor<128x128xf32>) outs(%40 : tensor<128xf32>) dimensions = [1] {ssbuffer.block_id = 5 : i32} + (%in: f32, %init: f32) { + %55 = arith.addf %in, %init : f32 + linalg.yield %55 : f32 + } + %41 = arith.subf %arg20, %35 {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + %42 = math.exp %41 {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + %43 = arith.divui %arg17, %c128_i32 {ssbuffer.block_id = 5 : i32} : i32 + %c0_i32_15 = arith.constant {ssbuffer.block_id = 5 : i32} 0 : i32 + %c2_i32 = arith.constant {ssbuffer.block_id = 5 : i32} 2 : i32 + %44 = arith.remsi %43, %c2_i32 {ssbuffer.block_id = 5 : i32} : i32 + %45 = arith.cmpi eq, %44, %c0_i32_15 {ssbuffer.block_id = 5 : i32} : i32 + scf.if %45 { + bufferization.materialize_in_destination %42 in restrict writable %memspacecast {ssbuffer.block_id = 5 : i32} : (tensor<128xf32>, memref<128xf32>) -> () + } else { + bufferization.materialize_in_destination %42 in restrict writable %memspacecast_9 {ssbuffer.block_id = 5 : i32} : (tensor<128xf32>, memref<128xf32>) -> () + } {ssbuffer.block_id = 5 : i32} + %46 = arith.mulf %arg18, %42 {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + %47 = arith.addf %46, %reduced_14 {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.copy ins(%reshape_13 : tensor<8x8x16x16xf16>) outs(%alloc : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %memspacecast_16 = memref.memory_space_cast %alloc_6 {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossDeps = [1, 0]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %48 = bufferization.to_tensor %memspacecast_16 restrict writable {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32> to tensor<128x128xf32> + %49 = arith.divui %arg17, %c128_i32 {ssbuffer.block_id = 6 : i32} : i32 + %c2_i32_17 = arith.constant {ssbuffer.block_id = 6 : i32} 2 : i32 + %50 = arith.remsi %49, %c2_i32_17 {ssbuffer.block_id = 6 : i32} : i32 + %c0_i32_18 = arith.constant {ssbuffer.block_id = 6 : i32} 0 : i32 + %51 = arith.cmpi eq, %50, %c0_i32_18 {ssbuffer.block_id = 6 : i32} : i32 + %52 = scf.if %51 -> (tensor<128xf32>) { + %55 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 6 : i32} : memref<128xf32> to tensor<128xf32> + scf.yield %55 : tensor<128xf32> + } else { + %55 = bufferization.to_tensor %memspacecast_9 restrict writable {ssbuffer.block_id = 6 : i32} : memref<128xf32> to tensor<128xf32> + scf.yield %55 : tensor<128xf32> + } {ssbuffer.block_id = 6 : i32, ssbuffer.intraDeps = [0 : i32, 0 : i32]} + %broadcasted_19 = linalg.broadcast ins(%52 : tensor<128xf32>) outs(%1 : tensor<128x128xf32>) dimensions = [1] {ssbuffer.block_id = 6 : i32} + %53 = arith.mulf %arg19, %broadcasted_19 {ssbuffer.block_id = 6 : i32} : tensor<128x128xf32> + %54 = arith.addf %48, %53 {ssbuffer.block_id = 6 : i32} : tensor<128x128xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + scf.yield %47, %54, %35 : tensor<128xf32>, tensor<128x128xf32>, tensor<128xf32> + } {ssbuffer.block_id = 9 : i32, ssbuffer.main_loop = 0 : i64, ssbuffer.skip} + } {ssbuffer.block_id = 10 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type, ssbuffer.skip} + return {ssbuffer.core_type = "VECTOR"} + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-if-only-sync.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-if-only-sync.mlir new file mode 100644 index 0000000000..dcc8fa2294 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-if-only-sync.mlir @@ -0,0 +1,64 @@ +// RUN: triton-opt --clone-ops --allow-unregistered-dialect %s | FileCheck %s + +// Unit test for CloneOps pass: an scf.if whose body only contains +// hivm.hir.sync_block_wait/set ops must be skipped in the +// shouldEraseOpForCube Rule-3 execAfter check, so that earlier cloned +// ops in the same block_id are not blocked by it. +// +// Setup: a CUBE main_loop with two block_ids. Block 10 contains: +// - annotation.mark (op with no results, hits Rule 3) +// - scf.if whose body only has sync_block_wait and sync_block_set +// Block 11 has a sync_block_set (sync op, already skipped by line 232 +// in the existing execAfter lambda). +// +// After --clone-ops, block 11 contains (in source order): +// [cloned_annotation_mark, cloned_ifOp, sync_block_set] +// +// Cleanup processes cloned ops bottom-to-top. For each cloned op, +// shouldEraseOpForCube rebuilds the memGraph and walks the op's +// execAfter. Without the isIfOpWithOnlySyncOps short-circuit, the +// cloned_ifOp would be counted as a same-block_id blocker for the +// cloned_annotation_mark, preventing its erasure. With the fix the +// cloned_ifOp is treated like a sync op (skipped), so the cloned +// annotation_mark is also erased. The cloned_ifOp itself is erased +// in turn because its execAfter contains only the sync_block_set +// (already skipped by line 232). +// +// Expected output: only the original (block_id=10) ops survive in the +// forOp body. Both the cloned annotation.mark and the cloned scf.if +// at block_id=11 are erased. + +// CHECK-LABEL: func.func @test_if_with_only_sync_ops_clone_cleanup +// The original scf.if at block 10 is preserved. +// CHECK: scf.if +// CHECK: hivm.hir.sync_block_wait +// CHECK: hivm.hir.sync_block_set +// The cloned annotation.mark at block 11 must be erased (the fix is +// what allows the cloned_ifOp to be skipped in its execAfter check). +// CHECK-NOT: annotation.mark +// The cloned scf.if at block 11 must also be erased. +// CHECK-NOT: scf.if +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_if_with_only_sync_ops_clone_cleanup(%arg0: i1, %arg1: memref<4x2x16x8xf32, #hivm.address_space>) attributes {global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c10_i32 = arith.constant 10 : i32 + scope.scope : () -> () { + %alloc = memref.alloc() {ssbuffer.block_id = 10 : i32} : memref<4x2x16x8xf32, #hivm.address_space> + scf.for %arg2 = %c0_i32 to %c10_i32 step %c1_i32 : i32 { + // Block 10: annotation.mark (op with no results) + scf.if(only sync ops) + annotation.mark %alloc {effects = ["write", "read"], ssbuffer.block_id = 10 : i32} : memref<4x2x16x8xf32, #hivm.address_space> + scf.if %arg0 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.sync_block_set {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + } {ssbuffer.block_id = 10 : i32} + // Block 11: triggers cloning of block 10's ops into block 11 + hivm.hir.sync_block_set {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 2 + } {ssbuffer.block_id = 10 : i32, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-ssa-chain.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-ssa-chain.mlir new file mode 100644 index 0000000000..2a6827ff0f --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-ssa-chain.mlir @@ -0,0 +1,384 @@ +// RUN: triton-opt --clone-ops %s --allow-unregistered-dialect | FileCheck %s + +// Unit test for CloneOps pass: cloning ops with SSA dependency chains. +// +// Block 10 holds a chain of i32 ops: +// %23 = arith.cmpi eq, %arg24, %20 +// %24 = arith.addi %arg24, %c1_i32 +// %25 = arith.select %23, %c0_i32, %24 +// %28 = arith.cmpi eq, %25, %20 +// Block 12 uses %28 via: +// %54 = arith.select %28, %2, %51 +// +// After --clone-ops, the chain in block 10 must be cloned into block 12 and the +// cloned %28 must reference the cloned %25 (not the original %25). Otherwise +// the precursor clones (%23, %24, %25) become dead and are erased by the +// cleanup pass. Each cloned op must keep the `clone = 10` attribute and its +// operand must be replaced with the cloned value. + +// CHECK-LABEL: func.func @_kernel_matmul_fp8_row_no_fast_acc +// Verify the full cloned SSA chain in block 12 (%23, %24, %25, %28 -> %23', %24', +// %25', %28'). The cloned cmpi (clone of %28) must read the cloned select +// (clone of %25), not the original %25. +// CHECK: arith.cmpi eq, {{.*}}, {{.*}} {MixUse, ssbuffer.block_id = 12 : i32, ssbuffer.clone = 10 : i32} : i32 +// CHECK-NEXT: arith.addi {{.*}} {MixUse, ssbuffer.block_id = 12 : i32, ssbuffer.clone = 10 : i32} : i32 +// CHECK-NEXT: arith.select {{.*}} {MixUse, ssbuffer.block_id = 12 : i32, ssbuffer.clone = 10 : i32} : i32 +// CHECK-NEXT: arith.cmpi eq, {{.*}}, {{.*}} {DataUse, ssbuffer.block_id = 12 : i32, ssbuffer.clone = 10 : i32, ssbuffer.dep_mark = [3 : i32]} : i32 +// The block-12 consumer (clone of %54) must reference the cloned cmpi. +// CHECK: arith.select {{.*}} {DataUse, ssbuffer.block_id = 12 : i32, ssbuffer.dep_mark = [3 : i32]} : tensor<32x32xf32> +// Also verify the same chain is cloned into block 9 (the last block). +// CHECK: arith.cmpi eq, {{.*}}, {{.*}} {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.clone = 10 : i32} : i32 +// CHECK-NEXT: arith.addi {{.*}} {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.clone = 10 : i32} : i32 +// CHECK-NEXT: arith.select {{.*}} {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.clone = 10 : i32} : i32 +// CHECK-NEXT: arith.cmpi eq, {{.*}}, {{.*}} {DataUse, ssbuffer.block_id = 9 : i32, ssbuffer.clone = 10 : i32, ssbuffer.dep_mark = [3 : i32]} : i32 + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func private @triton_indirect_load(memref, tensor<32x32xi64>, tensor<32x32xi1>, tensor<32x32xf32>) -> tensor<32x32xf32> + func.func @_kernel_matmul_fp8_row_no_fast_acc(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32 {tt.divisibility = 16 : i32}, %arg9: i32 {tt.divisibility = 16 : i32}, %arg10: i32 {tt.divisibility = 16 : i32}, %arg11: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg12: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg13: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg14: i32 {tt.divisibility = 16 : i32}, %arg15: i32 {tt.divisibility = 16 : i32}, %arg16: i32 {tt.divisibility = 16 : i32}, %arg17: i32, %arg18: i32, %arg19: i32, %arg20: i32, %arg21: i32, %arg22: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "mix_simd_simt"} { + %c32_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 32 : i32 + %c0_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 0 : i32 + %c-1_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} -1 : i32 + %c1_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 1 : i32 + %c80_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 80 : i32 + %c1 = arith.constant {Undefined, ssbuffer.block_id = 13 : i32} 1 : index + %c0 = arith.constant {DataUse, ssbuffer.block_id = 13 : i32} 0 : index + %c32 = arith.constant {DataUse, ssbuffer.block_id = 13 : i32} 32 : index + %cst = arith.constant {ssbuffer.block_id = 13 : i32} 0.000000e+00 : f32 + %c31_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 31 : i32 + %c4_i32 = arith.constant {MixUse, ssbuffer.block_id = 14 : i32} 4 : i32 + %cst_0 = arith.constant {ssbuffer.block_id = 11 : i32} dense<[4, 2, 16, 8]> : tensor<4xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 11 : i32} dense<[32, 4, 8]> : tensor<3xi64> + scope.scope : () -> () { + %0 = tensor.empty() {ssbuffer.block_id = 13 : i32} : tensor<32xi32> + %1 = tensor.empty() {ssbuffer.block_id = 13 : i32} : tensor<32x32xf32> + %2 = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%cst : f32) outs(%1 : tensor<32x32xf32>) -> tensor<32x32xf32> + %3 = arith.addi %arg5, %c31_i32 {MixUse, ssbuffer.block_id = 13 : i32} : i32 + %4 = arith.divsi %3, %c32_i32 {MixUse, ssbuffer.block_id = 13 : i32} : i32 + %5 = arith.addi %arg6, %c31_i32 {MixUse, ssbuffer.block_id = 13 : i32} : i32 + %6 = arith.divsi %5, %c32_i32 {MixUse, ssbuffer.block_id = 13 : i32} : i32 + %7 = arith.muli %4, %6 {Undefined, ssbuffer.block_id = 13 : i32} : i32 + %8 = arith.divsi %7, %c80_i32 {Undefined, ssbuffer.block_id = 13 : i32} : i32 + %9 = arith.remsi %7, %c80_i32 {Undefined, ssbuffer.block_id = 13 : i32} : i32 + %10 = arith.cmpi slt, %arg20, %9 {Undefined, ssbuffer.block_id = 13 : i32} : i32 + %11 = arith.subi %arg20, %c80_i32 {MixUse, ssbuffer.block_id = 13 : i32} : i32 + %12 = linalg.generic {indexing_maps = [affine_map<(d0) -> (d0)>], iterator_types = ["parallel"]} outs(%0 : tensor<32xi32>) attrs = {ssbuffer.block_id = 13 : i32, tt.from_make_range, tt.make_range_offset = 0 : index, tt.make_range_size = 32 : index} { + ^bb0(%out: i32): + %23 = linalg.index 0 : index + %24 = arith.index_cast %23 : index to i32 + linalg.yield %24 : i32 + } -> tensor<32xi32> + %13 = scf.if %10 -> (i32) { + %23 = arith.addi %8, %c1_i32 {Undefined, ssbuffer.block_id = 13 : i32} : i32 + scf.yield {Undefined} %23 : i32 + } else { + scf.yield {Undefined} %8 : i32 + } {Undefined, ssbuffer.block_id = 13 : i32} + %14 = tensor.empty() {ssbuffer.block_id = 15 : i32} : tensor<1x32xi32> + %15 = linalg.fill {ssbuffer.block_id = 15 : i32} ins(%arg15 : i32) outs(%14 : tensor<1x32xi32>) -> tensor<1x32xi32> + %expanded = tensor.expand_shape %12 [[0, 1]] output_shape [32, 1] {ssbuffer.block_id = 15 : i32} : tensor<32xi32> into tensor<32x1xi32> + %16 = linalg.fill {ssbuffer.block_id = 14 : i32} ins(%c0_i32 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> + %17 = arith.muli %6, %c4_i32 {MixUse, ssbuffer.block_id = 14 : i32} : i32 + %18 = arith.addi %arg7, %c31_i32 {MixUse, ssbuffer.block_id = 16 : i32} : i32 + %19 = arith.divsi %18, %c32_i32 {MixUse, ssbuffer.block_id = 16 : i32} : i32 + %20 = arith.subi %19, %c1_i32 {MixUse, ssbuffer.block_id = 16 : i32} : i32 + %21 = arith.muli %19, %13 {Undefined, ssbuffer.block_id = 17 : i32} : i32 + %alloc = memref.alloc() {ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x2x16x8xf32, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x2x16x8xf32, #hivm.address_space> + %alloc_2 = memref.alloc() {ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x2x16x8xf32, #hivm.address_space> + annotation.mark %alloc_2 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x2x16x8xf32, #hivm.address_space> + %alloc_3 = memref.alloc() {ssbuffer.block_id = 18 : i32, ssbuffer.crossDeps = [2 : i32, 1 : i32], ssbuffer.transfer_id = 2 : i32} : memref<32x32xf32, #hivm.address_space> + annotation.mark %alloc_3 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 2 : i32} : memref<32x32xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %alloc_4 = memref.alloc() : memref<32x32xf32, #hivm.address_space> + %memspacecast = memref.memory_space_cast %alloc_4 {ssbuffer.intraDeps = [0 : i32, 1 : i32]} : memref<32x32xf32, #hivm.address_space> to memref<32x32xf32> + %alloc_5 = memref.alloc() : memref<32x32xf32, #hivm.address_space> + %memspacecast_6 = memref.memory_space_cast %alloc_5 {ssbuffer.intraDeps = [0 : i32, 1 : i32]} : memref<32x32xf32, #hivm.address_space> to memref<32x32xf32> + %alloc_7 = memref.alloc() : memref<32xi32, #hivm.address_space> + %memspacecast_8 = memref.memory_space_cast %alloc_7 {ssbuffer.intraDeps = [1 : i32, 1 : i32]} : memref<32xi32, #hivm.address_space> to memref<32xi32> + %alloc_9 = memref.alloc() : memref<32xi32, #hivm.address_space> + %memspacecast_10 = memref.memory_space_cast %alloc_9 {ssbuffer.intraDeps = [1 : i32, 1 : i32]} : memref<32xi32, #hivm.address_space> to memref<32xi32> + %22:7 = scf.for %arg23 = %c0_i32 to %21 step %c1_i32 iter_args(%arg24 = %c-1_i32, %arg25 = %11, %arg26 = %c0_i32, %arg27 = %c0_i32, %arg28 = %12, %arg29 = %12, %arg30 = %2) -> (i32, i32, i32, i32, tensor<32xi32>, tensor<32xi32>, tensor<32x32xf32>) : i32 { + %23 = arith.cmpi eq, %arg24, %20 {MixUse, ssbuffer.block_id = 10 : i32} : i32 + %24 = arith.addi %arg24, %c1_i32 {MixUse, ssbuffer.block_id = 10 : i32} : i32 + %25 = arith.select %23, %c0_i32, %24 {MixUse, ssbuffer.block_id = 10 : i32} : i32 + %26 = arith.cmpi eq, %25, %c0_i32 {Undefined, ssbuffer.block_id = 10 : i32, ssbuffer.dep_mark = [1 : i32]} : i32 + %27 = arith.muli %25, %c32_i32 {MixUse, ssbuffer.block_id = 10 : i32, ssbuffer.dep_mark = [2 : i32]} : i32 + %28 = arith.cmpi eq, %25, %20 {DataUse, ssbuffer.block_id = 10 : i32, ssbuffer.dep_mark = [3 : i32]} : i32 + %29:5 = scf.if %26 -> (i32, i32, i32, tensor<32xi32>, tensor<32xi32>) { + %65 = arith.addi %arg25, %c80_i32 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %66 = arith.divsi %65, %17 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %67 = arith.muli %66, %c4_i32 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %68 = arith.subi %4, %67 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %69 = arith.minsi %68, %c4_i32 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %70 = arith.remsi %65, %69 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %71 = arith.addi %67, %70 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %72 = arith.remsi %65, %17 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %73 = arith.divsi %72, %69 {MixUse, ssbuffer.block_id = 3 : i32} : i32 + %74 = arith.muli %71, %c32_i32 {DataUse, ssbuffer.block_id = 3 : i32} : i32 + %75 = arith.muli %73, %c32_i32 {DataUse, ssbuffer.block_id = 3 : i32} : i32 + %76 = linalg.fill {ssbuffer.block_id = 3 : i32} ins(%74 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> + %77 = arith.addi %76, %12 {DataUse, ssbuffer.block_id = 3 : i32} : tensor<32xi32> + %78 = linalg.fill {ssbuffer.block_id = 3 : i32} ins(%75 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> + %79 = arith.addi %78, %12 {DataUse, ssbuffer.block_id = 3 : i32} : tensor<32xi32> + %80 = arith.index_cast %74 {DataUse, ssbuffer.block_id = 3 : i32} : i32 to index + %81 = arith.addi %80, %c32 {DataUse, ssbuffer.block_id = 3 : i32} : index + %82 = arith.index_cast %arg5 {DataUse, ssbuffer.block_id = 3 : i32} : i32 to index + %83 = arith.maxsi %80, %82 {DataUse, ssbuffer.block_id = 3 : i32} : index + %84 = arith.minsi %81, %83 {DataUse, ssbuffer.block_id = 3 : i32} : index + %85 = arith.subi %84, %80 {DataUse, ssbuffer.block_id = 3 : i32} : index + %extracted_slice_24 = tensor.extract_slice %77[0] [%85] [1] {DataUse, ssbuffer.block_id = 3 : i32} : tensor<32xi32> to tensor + %inserted_slice_25 = tensor.insert_slice %extracted_slice_24 into %16[0] [%85] [1] {DataUse, ssbuffer.block_id = 3 : i32} : tensor into tensor<32xi32> + %86 = arith.index_cast %75 {DataUse, ssbuffer.block_id = 3 : i32} : i32 to index + %87 = arith.addi %86, %c32 {DataUse, ssbuffer.block_id = 3 : i32} : index + %88 = arith.index_cast %arg6 {DataUse, ssbuffer.block_id = 3 : i32} : i32 to index + %89 = arith.maxsi %86, %88 {DataUse, ssbuffer.block_id = 3 : i32} : index + %90 = arith.minsi %87, %89 {DataUse, ssbuffer.block_id = 3 : i32} : index + %91 = arith.subi %90, %86 {DataUse, ssbuffer.block_id = 3 : i32} : index + %extracted_slice_26 = tensor.extract_slice %79[0] [%91] [1] {DataUse, ssbuffer.block_id = 3 : i32} : tensor<32xi32> to tensor + %inserted_slice_27 = tensor.insert_slice %extracted_slice_26 into %16[0] [%91] [1] {DataUse, ssbuffer.block_id = 3 : i32} : tensor into tensor<32xi32> + scf.yield {Undefined} %65, %71, %73, %inserted_slice_25, %inserted_slice_27 : i32, i32, i32, tensor<32xi32>, tensor<32xi32> + } else { + scf.yield {Undefined} %arg25, %arg26, %arg27, %arg28, %arg29 : i32, i32, i32, tensor<32xi32>, tensor<32xi32> + } {MixUse, ssbuffer.block_id = 3 : i32, ssbuffer.dep_mark = [1 : i32]} + %c2_i32 = arith.constant {ssbuffer.block_id = 3 : i32} 2 : i32 + %30 = arith.remsi %arg23, %c2_i32 {ssbuffer.block_id = 3 : i32} : i32 + %c0_i32_11 = arith.constant {ssbuffer.block_id = 3 : i32} 0 : i32 + %31 = arith.cmpi eq, %30, %c0_i32_11 {ssbuffer.block_id = 3 : i32} : i32 + scf.if %31 { + hivm.hir.copy ins(%29#3 : tensor<32xi32>) outs(%memspacecast_8 : memref<32xi32>) {ssbuffer.block_id = 3 : i32} + } else { + hivm.hir.copy ins(%29#3 : tensor<32xi32>) outs(%memspacecast_10 : memref<32xi32>) {ssbuffer.block_id = 3 : i32} + } {ssbuffer.block_id = 3 : i32, ssbuffer.intra_buffer} + %alloc_12 = memref.alloc() {ssbuffer.block_id = 11 : i32} : memref<32x32xf32> + scf.for %arg31 = %c0 to %c32 step %c1 { + %c2_i32_24 = arith.constant {ssbuffer.block_id = 11 : i32} 2 : i32 + %65 = arith.remsi %arg23, %c2_i32_24 {ssbuffer.block_id = 11 : i32} : i32 + %c0_i32_25 = arith.constant {ssbuffer.block_id = 11 : i32} 0 : i32 + %66 = arith.cmpi eq, %65, %c0_i32_25 {ssbuffer.block_id = 11 : i32} : i32 + %67 = scf.if %66 -> (tensor<32xi32>) { + %72 = bufferization.to_tensor %memspacecast_8 restrict writable : memref<32xi32> to tensor<32xi32> + scf.yield %72 : tensor<32xi32> + } else { + %72 = bufferization.to_tensor %memspacecast_10 restrict writable : memref<32xi32> to tensor<32xi32> + scf.yield %72 : tensor<32xi32> + } {ssbuffer.block_id = 11 : i32, ssbuffer.intraDeps = [1 : i32, 0 : i32], ssbuffer.intra_buffer} + %extracted = tensor.extract %67[%arg31] {DiscreteMemAccess, ssbuffer.block_id = 11 : i32} : tensor<32xi32> + %68 = arith.muli %extracted, %arg14 {ssbuffer.block_id = 11 : i32} : i32 + %69 = arith.index_cast %68 {ssbuffer.block_id = 11 : i32} : i32 to index + %70 = arith.index_cast %27 {ssbuffer.block_id = 11 : i32, ssbuffer.dep_mark = [2 : i32]} : i32 to index + %71 = arith.addi %69, %70 {ssbuffer.block_id = 11 : i32} : index + %reinterpret_cast = memref.reinterpret_cast %arg2 to offset: [%71], sizes: [1, 32], strides: [32, 1] {ssbuffer.block_id = 11 : i32} : memref to memref<1x32xf32, strided<[32, 1], offset: ?>> + %subview = memref.subview %alloc_12[%arg31, 0] [1, 32] [1, 1] {ssbuffer.block_id = 11 : i32} : memref<32x32xf32> to memref<1x32xf32, strided<[32, 1], offset: ?>> + memref.copy %reinterpret_cast, %subview {ssbuffer.block_id = 11 : i32} : memref<1x32xf32, strided<[32, 1], offset: ?>> to memref<1x32xf32, strided<[32, 1], offset: ?>> + } {DataUse, ExtractedLoadOrStore, hivm.parallel_loop, ssbuffer.block_id = 11 : i32} + %32 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.dep_mark = [2 : i32]} ins(%27 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> + %33 = arith.addi %32, %12 {MixUse, ssbuffer.block_id = 11 : i32} : tensor<32xi32> + %expanded_13 = tensor.expand_shape %29#4 [[0, 1]] output_shape [1, 32] {ssbuffer.block_id = 11 : i32} : tensor<32xi32> into tensor<1x32xi32> + %34 = arith.muli %expanded_13, %15 {DataUse, ssbuffer.block_id = 11 : i32} : tensor<1x32xi32> + %35 = tensor.empty() {ssbuffer.block_id = 11 : i32} : tensor<32x32xi32> + %broadcasted = linalg.broadcast ins(%33 : tensor<32xi32>) outs(%35 : tensor<32x32xi32>) dimensions = [1] {ssbuffer.block_id = 11 : i32} + %collapsed = tensor.collapse_shape %34 [[0, 1]] {ssbuffer.block_id = 11 : i32} : tensor<1x32xi32> into tensor<32xi32> + %broadcasted_14 = linalg.broadcast ins(%collapsed : tensor<32xi32>) outs(%35 : tensor<32x32xi32>) dimensions = [0] {ssbuffer.block_id = 11 : i32} + %36 = arith.addi %broadcasted, %broadcasted_14 {DataUse, ssbuffer.block_id = 11 : i32} : tensor<32x32xi32> + %37 = arith.extsi %36 {DataUse, ssbuffer.block_id = 11 : i32} : tensor<32x32xi32> to tensor<32x32xi64> + %38 = arith.subi %arg7, %27 {DataUse, ssbuffer.block_id = 11 : i32, ssbuffer.dep_mark = [2 : i32]} : i32 + %39 = bufferization.to_tensor %alloc_12 restrict writable {ssbuffer.block_id = 11 : i32} : memref<32x32xf32> to tensor<32x32xf32> + %40 = arith.index_cast %38 {DataUse, ssbuffer.block_id = 11 : i32} : i32 to index + %41 = arith.maxsi %40, %c0 {DataUse, ssbuffer.block_id = 11 : i32} : index + %42 = arith.minsi %41, %c32 {DataUse, ssbuffer.block_id = 11 : i32} : index + %extracted_slice = tensor.extract_slice %39[0, 0] [32, %42] [1, 1] {DataUse, ssbuffer.block_id = 11 : i32} : tensor<32x32xf32> to tensor<32x?xf32> + %inserted_slice = tensor.insert_slice %extracted_slice into %2[0, 0] [32, %42] [1, 1] {DataUse, ssbuffer.block_id = 11 : i32} : tensor<32x?xf32> into tensor<32x32xf32> + %43 = tensor.empty() {ssbuffer.block_id = 11 : i32} : tensor<32x1xi32> + %44 = linalg.fill {ssbuffer.block_id = 11 : i32} ins(%38 : i32) outs(%43 : tensor<32x1xi32>) -> tensor<32x1xi32> + %45 = arith.cmpi slt, %expanded, %44 {DataUse, ssbuffer.block_id = 11 : i32} : tensor<32x1xi32> + %46 = tensor.empty() {ssbuffer.block_id = 11 : i32} : tensor<32x32xi1> + %collapsed_15 = tensor.collapse_shape %45 [[0, 1]] {ssbuffer.block_id = 11 : i32} : tensor<32x1xi1> into tensor<32xi1> + %broadcasted_16 = linalg.broadcast ins(%collapsed_15 : tensor<32xi1>) outs(%46 : tensor<32x32xi1>) dimensions = [1] {ssbuffer.block_id = 11 : i32} + %47 = func.call @triton_indirect_load(%arg3, %37, %broadcasted_16, %2) {ssbuffer.block_id = 11 : i32} : (memref, tensor<32x32xi64>, tensor<32x32xi1>, tensor<32x32xf32>) -> tensor<32x32xf32> + %reshape = tensor.reshape %inserted_slice(%cst_1) {ssbuffer.block_id = 11 : i32} : (tensor<32x32xf32>, tensor<3xi64>) -> tensor<32x4x8xf32> + %48 = tensor.empty() {ssbuffer.block_id = 11 : i32} : tensor<4x32x8xf32> + %transposed = linalg.transpose ins(%reshape : tensor<32x4x8xf32>) outs(%48 : tensor<4x32x8xf32>) permutation = [1, 0, 2] {ssbuffer.block_id = 11 : i32} + %reshape_17 = tensor.reshape %transposed(%cst_0) {ssbuffer.block_id = 11 : i32} : (tensor<4x32x8xf32>, tensor<4xi64>) -> tensor<4x2x16x8xf32> + %reshape_18 = tensor.reshape %47(%cst_1) {ssbuffer.block_id = 11 : i32} : (tensor<32x32xf32>, tensor<3xi64>) -> tensor<32x4x8xf32> + %49 = tensor.empty() {ssbuffer.block_id = 11 : i32} : tensor<4x32x8xf32> + %transposed_19 = linalg.transpose ins(%reshape_18 : tensor<32x4x8xf32>) outs(%49 : tensor<4x32x8xf32>) permutation = [1, 0, 2] {ssbuffer.block_id = 11 : i32} + %reshape_20 = tensor.reshape %transposed_19(%cst_0) {ssbuffer.block_id = 11 : i32} : (tensor<4x32x8xf32>, tensor<4xi64>) -> tensor<4x2x16x8xf32> + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.copy ins(%reshape_17 : tensor<4x2x16x8xf32>) outs(%alloc : memref<4x2x16x8xf32, #hivm.address_space>) {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.copy ins(%reshape_20 : tensor<4x2x16x8xf32>) outs(%alloc_2 : memref<4x2x16x8xf32, #hivm.address_space>) {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 1 : i32} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %memspacecast_21 = memref.memory_space_cast %alloc_3 {ssbuffer.block_id = 12 : i32, ssbuffer.crossDeps = [2 : i32, 0 : i32], ssbuffer.transfer_id = 2 : i32} : memref<32x32xf32, #hivm.address_space> to memref<32x32xf32> + %50 = bufferization.to_tensor %memspacecast_21 restrict writable {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 2 : i32} : memref<32x32xf32> to tensor<32x32xf32> + %51 = arith.addf %50, %arg30 {ssbuffer.add_from_matmul, ssbuffer.block_id = 12 : i32} : tensor<32x32xf32> + %c2_i32_22 = arith.constant {ssbuffer.block_id = 12 : i32} 2 : i32 + %52 = arith.remsi %arg23, %c2_i32_22 {ssbuffer.block_id = 12 : i32} : i32 + %c0_i32_23 = arith.constant {ssbuffer.block_id = 12 : i32} 0 : i32 + %53 = arith.cmpi eq, %52, %c0_i32_23 {ssbuffer.block_id = 12 : i32} : i32 + scf.if %53 { + hivm.hir.copy ins(%51 : tensor<32x32xf32>) outs(%memspacecast : memref<32x32xf32>) {ssbuffer.block_id = 12 : i32} + } else { + hivm.hir.copy ins(%51 : tensor<32x32xf32>) outs(%memspacecast_6 : memref<32x32xf32>) {ssbuffer.block_id = 12 : i32} + } {ssbuffer.block_id = 12 : i32, ssbuffer.intra_buffer} + %54 = arith.select %28, %2, %51 {DataUse, ssbuffer.block_id = 12 : i32, ssbuffer.dep_mark = [3 : i32]} : tensor<32x32xf32> + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %55 = arith.cmpi eq, %arg24, %20 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %56 = arith.addi %arg24, %c1_i32 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %57 = arith.select %55, %c0_i32, %56 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %58 = arith.cmpi eq, %57, %c0_i32 {Undefined, ssbuffer.block_id = 9 : i32} : i32 + %59:5 = scf.if %58 -> (i32, i32, i32, tensor<32xi32>, tensor<32xi32>) { + %65 = arith.addi %arg25, %c80_i32 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %66 = arith.divsi %65, %17 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %67 = arith.muli %66, %c4_i32 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %68 = arith.subi %4, %67 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %69 = arith.minsi %68, %c4_i32 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %70 = arith.remsi %65, %69 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %71 = arith.addi %67, %70 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %72 = arith.remsi %65, %17 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %73 = arith.divsi %72, %69 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %74 = arith.muli %71, %c32_i32 {DataUse, ssbuffer.block_id = 9 : i32} : i32 + %75 = arith.muli %73, %c32_i32 {DataUse, ssbuffer.block_id = 9 : i32} : i32 + %76 = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%74 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> + %77 = arith.addi %76, %12 {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32xi32> + %78 = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%75 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> + %79 = arith.addi %78, %12 {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32xi32> + %80 = arith.index_cast %74 {DataUse, ssbuffer.block_id = 9 : i32} : i32 to index + %81 = arith.addi %80, %c32 {DataUse, ssbuffer.block_id = 9 : i32} : index + %82 = arith.index_cast %arg5 {DataUse, ssbuffer.block_id = 9 : i32} : i32 to index + %83 = arith.maxsi %80, %82 {DataUse, ssbuffer.block_id = 9 : i32} : index + %84 = arith.minsi %81, %83 {DataUse, ssbuffer.block_id = 9 : i32} : index + %85 = arith.subi %84, %80 {DataUse, ssbuffer.block_id = 9 : i32} : index + %extracted_slice_24 = tensor.extract_slice %77[0] [%85] [1] {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32xi32> to tensor + %inserted_slice_25 = tensor.insert_slice %extracted_slice_24 into %16[0] [%85] [1] {DataUse, ssbuffer.block_id = 9 : i32} : tensor into tensor<32xi32> + %86 = arith.index_cast %75 {DataUse, ssbuffer.block_id = 9 : i32} : i32 to index + %87 = arith.addi %86, %c32 {DataUse, ssbuffer.block_id = 9 : i32} : index + %88 = arith.index_cast %arg6 {DataUse, ssbuffer.block_id = 9 : i32} : i32 to index + %89 = arith.maxsi %86, %88 {DataUse, ssbuffer.block_id = 9 : i32} : index + %90 = arith.minsi %87, %89 {DataUse, ssbuffer.block_id = 9 : i32} : index + %91 = arith.subi %90, %86 {DataUse, ssbuffer.block_id = 9 : i32} : index + %extracted_slice_26 = tensor.extract_slice %79[0] [%91] [1] {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32xi32> to tensor + %inserted_slice_27 = tensor.insert_slice %extracted_slice_26 into %16[0] [%91] [1] {DataUse, ssbuffer.block_id = 9 : i32} : tensor into tensor<32xi32> + scf.yield {Undefined, ssbuffer.block_id = 9 : i32} %65, %71, %73, %inserted_slice_25, %inserted_slice_27 : i32, i32, i32, tensor<32xi32>, tensor<32xi32> + } else { + scf.yield {Undefined, ssbuffer.block_id = 9 : i32} %arg25, %arg26, %arg27, %arg28, %arg29 : i32, i32, i32, tensor<32xi32>, tensor<32xi32> + } {MixUse, ssbuffer.block_id = 9 : i32} + %60 = arith.cmpi eq, %arg24, %20 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %61 = arith.addi %arg24, %c1_i32 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %62 = arith.select %60, %c0_i32, %61 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %63 = arith.cmpi eq, %62, %c0_i32 {Undefined, ssbuffer.block_id = 9 : i32} : i32 + %64:5 = scf.if %63 -> (i32, i32, i32, tensor<32xi32>, tensor<32xi32>) { + %65 = arith.addi %arg25, %c80_i32 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %66 = arith.divsi %65, %17 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %67 = arith.muli %66, %c4_i32 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %68 = arith.subi %4, %67 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %69 = arith.minsi %68, %c4_i32 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %70 = arith.remsi %65, %69 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %71 = arith.addi %67, %70 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %72 = arith.remsi %65, %17 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %73 = arith.divsi %72, %69 {MixUse, ssbuffer.block_id = 9 : i32} : i32 + %74 = arith.muli %71, %c32_i32 {DataUse, ssbuffer.block_id = 9 : i32} : i32 + %75 = arith.muli %73, %c32_i32 {DataUse, ssbuffer.block_id = 9 : i32} : i32 + %76 = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%74 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> + %77 = arith.addi %76, %12 {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32xi32> + %78 = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%75 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> + %79 = arith.addi %78, %12 {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32xi32> + %80 = arith.index_cast %74 {DataUse, ssbuffer.block_id = 9 : i32} : i32 to index + %81 = arith.addi %80, %c32 {DataUse, ssbuffer.block_id = 9 : i32} : index + %82 = arith.index_cast %arg5 {DataUse, ssbuffer.block_id = 9 : i32} : i32 to index + %83 = arith.maxsi %80, %82 {DataUse, ssbuffer.block_id = 9 : i32} : index + %84 = arith.minsi %81, %83 {DataUse, ssbuffer.block_id = 9 : i32} : index + %85 = arith.subi %84, %80 {DataUse, ssbuffer.block_id = 9 : i32} : index + %extracted_slice_24 = tensor.extract_slice %77[0] [%85] [1] {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32xi32> to tensor + %inserted_slice_25 = tensor.insert_slice %extracted_slice_24 into %16[0] [%85] [1] {DataUse, ssbuffer.block_id = 9 : i32} : tensor into tensor<32xi32> + %86 = arith.index_cast %75 {DataUse, ssbuffer.block_id = 9 : i32} : i32 to index + %87 = arith.addi %86, %c32 {DataUse, ssbuffer.block_id = 9 : i32} : index + %88 = arith.index_cast %arg6 {DataUse, ssbuffer.block_id = 9 : i32} : i32 to index + %89 = arith.maxsi %86, %88 {DataUse, ssbuffer.block_id = 9 : i32} : index + %90 = arith.minsi %87, %89 {DataUse, ssbuffer.block_id = 9 : i32} : index + %91 = arith.subi %90, %86 {DataUse, ssbuffer.block_id = 9 : i32} : index + %extracted_slice_26 = tensor.extract_slice %79[0] [%91] [1] {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32xi32> to tensor + %inserted_slice_27 = tensor.insert_slice %extracted_slice_26 into %16[0] [%91] [1] {DataUse, ssbuffer.block_id = 9 : i32} : tensor into tensor<32xi32> + scf.yield {Undefined, ssbuffer.block_id = 9 : i32} %65, %71, %73, %inserted_slice_25, %inserted_slice_27 : i32, i32, i32, tensor<32xi32>, tensor<32xi32> + } else { + scf.yield {Undefined, ssbuffer.block_id = 9 : i32} %arg25, %arg26, %arg27, %arg28, %arg29 : i32, i32, i32, tensor<32xi32>, tensor<32xi32> + } {MixUse, ssbuffer.block_id = 9 : i32} + scf.if %28 { + %65 = arith.muli %59#1, %c32_i32 {ssbuffer.block_id = 9 : i32} : i32 + %66 = arith.muli %64#2, %c32_i32 {ssbuffer.block_id = 9 : i32} : i32 + %67 = arith.index_cast %65 {ssbuffer.block_id = 9 : i32} : i32 to index + %alloc_24 = memref.alloc() {ssbuffer.block_id = 9 : i32} : memref<32xf32> + %68 = arith.addi %67, %c32 {ssbuffer.block_id = 9 : i32} : index + %69 = arith.index_cast %arg5 {ssbuffer.block_id = 9 : i32} : i32 to index + %70 = arith.maxsi %67, %69 {ssbuffer.block_id = 9 : i32} : index + %71 = arith.minsi %68, %70 {ssbuffer.block_id = 9 : i32} : index + %72 = arith.subi %71, %67 {ssbuffer.block_id = 9 : i32} : index + %73 = arith.cmpi slt, %72, %c32 {ssbuffer.block_id = 9 : i32} : index + %74 = arith.index_cast %66 {ssbuffer.block_id = 9 : i32} : i32 to index + %alloc_25 = memref.alloc() {ssbuffer.block_id = 9 : i32} : memref<32xf32> + %75 = arith.addi %74, %c32 {ssbuffer.block_id = 9 : i32} : index + %76 = arith.index_cast %arg6 {ssbuffer.block_id = 9 : i32} : i32 to index + %77 = arith.maxsi %74, %76 {ssbuffer.block_id = 9 : i32} : index + %78 = arith.minsi %75, %77 {ssbuffer.block_id = 9 : i32} : index + %79 = arith.subi %78, %74 {ssbuffer.block_id = 9 : i32} : index + %80 = arith.cmpi slt, %79, %c32 {ssbuffer.block_id = 9 : i32} : index + %alloc_26 = memref.alloc() {ssbuffer.block_id = 9 : i32} : memref<32xf32> + scf.if %80 { + linalg.fill {ssbuffer.block_id = 9 : i32} ins(%cst : f32) outs(%alloc_26 : memref<32xf32>) + linalg.fill {ssbuffer.block_id = 9 : i32} ins(%cst : f32) outs(%alloc_25 : memref<32xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 9 : i32} + scf.if %73 { + linalg.fill {ssbuffer.block_id = 9 : i32} ins(%cst : f32) outs(%alloc_24 : memref<32xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 9 : i32} + %reinterpret_cast = memref.reinterpret_cast %arg11 to offset: [%67], sizes: [32], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<32xf32, strided<[1], offset: ?>> + %subview = memref.subview %reinterpret_cast[0] [%72] [1] {ssbuffer.block_id = 9 : i32} : memref<32xf32, strided<[1], offset: ?>> to memref> + %subview_27 = memref.subview %alloc_24[0] [%72] [1] {ssbuffer.block_id = 9 : i32} : memref<32xf32> to memref> + memref.copy %subview, %subview_27 {ssbuffer.block_id = 9 : i32} : memref> to memref> + %81 = bufferization.to_tensor %alloc_24 restrict writable {ssbuffer.block_id = 9 : i32} : memref<32xf32> to tensor<32xf32> + %reinterpret_cast_28 = memref.reinterpret_cast %arg12 to offset: [%74], sizes: [32], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<32xf32, strided<[1], offset: ?>> + %subview_29 = memref.subview %reinterpret_cast_28[0] [%79] [1] {ssbuffer.block_id = 9 : i32} : memref<32xf32, strided<[1], offset: ?>> to memref> + %subview_30 = memref.subview %alloc_25[0] [%79] [1] {ssbuffer.block_id = 9 : i32} : memref<32xf32> to memref> + memref.copy %subview_29, %subview_30 {ssbuffer.block_id = 9 : i32} : memref> to memref> + %82 = bufferization.to_tensor %alloc_25 restrict writable {ssbuffer.block_id = 9 : i32} : memref<32xf32> to tensor<32xf32> + %broadcasted_31 = linalg.broadcast ins(%81 : tensor<32xf32>) outs(%1 : tensor<32x32xf32>) dimensions = [1] {ssbuffer.block_id = 9 : i32} + %broadcasted_32 = linalg.broadcast ins(%82 : tensor<32xf32>) outs(%1 : tensor<32x32xf32>) dimensions = [0] {ssbuffer.block_id = 9 : i32} + %83 = arith.mulf %broadcasted_31, %broadcasted_32 {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32x32xf32> + %c2_i32_33 = arith.constant {ssbuffer.block_id = 9 : i32} 2 : i32 + %84 = arith.remsi %arg23, %c2_i32_33 {ssbuffer.block_id = 9 : i32} : i32 + %c0_i32_34 = arith.constant {ssbuffer.block_id = 9 : i32} 0 : i32 + %85 = arith.cmpi eq, %84, %c0_i32_34 {ssbuffer.block_id = 9 : i32} : i32 + %86 = scf.if %85 -> (tensor<32x32xf32>) { + %95 = bufferization.to_tensor %memspacecast restrict writable : memref<32x32xf32> to tensor<32x32xf32> + scf.yield %95 : tensor<32x32xf32> + } else { + %95 = bufferization.to_tensor %memspacecast_6 restrict writable : memref<32x32xf32> to tensor<32x32xf32> + scf.yield %95 : tensor<32x32xf32> + } {ssbuffer.block_id = 9 : i32, ssbuffer.intraDeps = [0 : i32, 0 : i32], ssbuffer.intra_buffer} + %87 = arith.mulf %86, %83 {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32x32xf32> + %reinterpret_cast_35 = memref.reinterpret_cast %arg13 to offset: [%74], sizes: [32], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<32xf32, strided<[1], offset: ?>> + %subview_36 = memref.subview %reinterpret_cast_35[0] [%79] [1] {ssbuffer.block_id = 9 : i32} : memref<32xf32, strided<[1], offset: ?>> to memref> + %subview_37 = memref.subview %alloc_26[0] [%79] [1] {ssbuffer.block_id = 9 : i32} : memref<32xf32> to memref> + memref.copy %subview_36, %subview_37 {ssbuffer.block_id = 9 : i32} : memref> to memref> + %88 = bufferization.to_tensor %alloc_26 restrict writable {ssbuffer.block_id = 9 : i32} : memref<32xf32> to tensor<32xf32> + %broadcasted_38 = linalg.broadcast ins(%88 : tensor<32xf32>) outs(%1 : tensor<32x32xf32>) dimensions = [0] {ssbuffer.block_id = 9 : i32} + %89 = arith.addf %87, %broadcasted_38 {DataUse, ssbuffer.block_id = 9 : i32} : tensor<32x32xf32> + %90 = arith.index_cast %arg16 {ssbuffer.block_id = 9 : i32} : i32 to index + %91 = arith.muli %67, %90 {ssbuffer.block_id = 9 : i32} : index + %92 = arith.addi %91, %74 {ssbuffer.block_id = 9 : i32} : index + %reinterpret_cast_39 = memref.reinterpret_cast %arg4 to offset: [%92], sizes: [32, 32], strides: [%90, 1] {ssbuffer.block_id = 9 : i32} : memref to memref<32x32xf32, strided<[?, 1], offset: ?>> + %93 = arith.minsi %72, %c32 {ssbuffer.block_id = 9 : i32} : index + %94 = arith.minsi %79, %c32 {ssbuffer.block_id = 9 : i32} : index + %extracted_slice_40 = tensor.extract_slice %89[0, 0] [%93, %94] [1, 1] {ssbuffer.block_id = 9 : i32} : tensor<32x32xf32> to tensor + %subview_41 = memref.subview %reinterpret_cast_39[0, 0] [%93, %94] [1, 1] {ssbuffer.block_id = 9 : i32} : memref<32x32xf32, strided<[?, 1], offset: ?>> to memref> + bufferization.materialize_in_destination %extracted_slice_40 in writable %subview_41 {ssbuffer.block_id = 9 : i32} : (tensor, memref>) -> () + } {Undefined, ssbuffer.block_id = 9 : i32} + scf.yield %25, %29#0, %29#1, %29#2, %29#3, %29#4, %54 : i32, i32, i32, i32, tensor<32xi32>, tensor<32xi32>, tensor<32x32xf32> + } {Undefined, ssbuffer.block_id = 18 : i32, ssbuffer.main_loop = 0 : i32} + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + scope.return + } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-sync-wait-if.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-sync-wait-if.mlir new file mode 100644 index 0000000000..da8c624052 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-sync-wait-if.mlir @@ -0,0 +1,302 @@ +// RUN: triton-opt --clone-ops --allow-unregistered-dialect %s | FileCheck %s + +// After --clone-ops, no cloned scf.if whose body contains only +// sync_block_wait/sync_block_set/fixpipe ops should remain. +// Such cloned ifOps are erased by cleanup Rule 3 (via the +// isIfOpWithOnlySyncOps check in shouldEraseOpForCube). +// +// A cloned scf.if carries `ssbuffer.clone` only on its closing +// attribute line — body ops are not cloned individually. We +// therefore distinguish the bad pattern (cloned sync-only-ifOp, +// whose closing attrs include `ssbuffer.cross_buffer = 1 : i32`) +// from the allowed pattern (cloned non-sync-only ifOp, whose +// closing attrs include `hivm.unlikely_condition` instead). + +// CHECK-LABEL: func.func @_swa_paged_prefill_small_kernel +// CHECK-NOT: } {ssbuffer.block_id = {{.+}} : i32, ssbuffer.clone = {{.+}} : i32, ssbuffer.cross_buffer = 1 : i32} +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @_swa_paged_prefill_small_kernel(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg8: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg9: f32, %arg10: i32 {tt.divisibility = 16 : i32}, %arg11: i32 {tt.divisibility = 16 : i32}, %arg12: i32 {tt.divisibility = 16 : i32}, %arg13: i32 {tt.divisibility = 16 : i32}, %arg14: i32 {tt.divisibility = 16 : i32}, %arg15: i32 {tt.divisibility = 16 : i32}, %arg16: i32 {tt.divisibility = 16 : i32}, %arg17: i32 {tt.divisibility = 16 : i32}, %arg18: i32 {tt.divisibility = 16 : i32}, %arg19: i32 {tt.divisibility = 16 : i32}, %arg20: i32, %arg21: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg22: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg23: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg24: i32, %arg25: i32, %arg26: i32, %arg27: i32, %arg28: i32, %arg29: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c4_i64 = arith.constant {ssbuffer.block_id = 19 : i32} 4 : i64 + %c0_i8 = arith.constant {ssbuffer.block_id = 19 : i32} 0 : i8 + %cst = arith.constant {ssbuffer.block_id = 19 : i32} -1.000000e+06 : f32 + %c0_i32 = arith.constant {ssbuffer.block_id = 18 : i32} 0 : i32 + %c128 = arith.constant {ssbuffer.block_id = 18 : i32} 128 : index + %cst_0 = arith.constant {ssbuffer.block_id = 17 : i32} 0.000000e+00 : bf16 + %cst_1 = arith.constant {ssbuffer.block_id = 16 : i32} 0.000000e+00 : f32 + %c32_i64 = arith.constant {ssbuffer.block_id = 16 : i32} 32 : i64 + %c1_i64 = arith.constant {ssbuffer.block_id = 16 : i32} 1 : i64 + %c128_i64 = arith.constant {ssbuffer.block_id = 16 : i32} 128 : i64 + %c5_i64 = arith.constant {ssbuffer.block_id = 16 : i32} 5 : i64 + %c2_i32 = arith.constant {ssbuffer.block_id = 16 : i32} 2 : i32 + %c0_i64 = arith.constant {ssbuffer.block_id = 16 : i32} 0 : i64 + %c31_i64 = arith.constant {Undefined, ssbuffer.block_id = 16 : i32} 31 : i64 + %c-1283_i64 = arith.constant {ssbuffer.block_id = 16 : i32} -1283 : i64 + %c1283_i64 = arith.constant {ssbuffer.block_id = 16 : i32} 1283 : i64 + %cst_2 = arith.constant {ssbuffer.block_id = 16 : i32} 0xFF800000 : f32 + %c0 = arith.constant {ssbuffer.block_id = 16 : i32} 0 : index + %c1 = arith.constant {ssbuffer.block_id = 16 : i32} 1 : index + %c2 = arith.constant {ssbuffer.block_id = 16 : i32} 2 : index + %c3 = arith.constant {ssbuffer.block_id = 16 : i32} 3 : index + %c1539 = arith.constant {ssbuffer.block_id = 16 : i32} 1539 : index + %cst_3 = arith.constant {ssbuffer.block_id = 12 : i32} dense<[2, 8, 16, 16]> : tensor<4xi64> + %cst_4 = arith.constant {ssbuffer.block_id = 12 : i32} dense<[128, 2, 16]> : tensor<3xi64> + %c32 = arith.constant {ssbuffer.block_id = 11 : i32} 32 : index + scope.scope : () -> () { + %0 = arith.muli %arg27, %c2_i32 {ssbuffer.block_id = 16 : i32} : i32 + %1 = arith.index_cast %0 {ssbuffer.block_id = 16 : i32} : i32 to index + %reinterpret_cast = memref.reinterpret_cast %arg23 to offset: [%1], sizes: [1], strides: [1] {ssbuffer.block_id = 16 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %2 = memref.load %reinterpret_cast[%c0] {ssbuffer.block_id = 16 : i32} : memref<1xi64, strided<[1], offset: ?>> + %3 = arith.addi %1, %c1 {ssbuffer.block_id = 16 : i32} : index + %reinterpret_cast_5 = memref.reinterpret_cast %arg23 to offset: [%3], sizes: [1], strides: [1] {ssbuffer.block_id = 16 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %4 = memref.load %reinterpret_cast_5[%c0] {ssbuffer.block_id = 16 : i32} : memref<1xi64, strided<[1], offset: ?>> + %5 = tensor.empty() {ssbuffer.block_id = 11 : i32} : tensor<128x32xf32> + %6 = linalg.fill {ssbuffer.block_id = 11 : i32} ins(%cst_1 : f32) outs(%5 : tensor<128x32xf32>) -> tensor<128x32xf32> + %7 = arith.extsi %arg13 {ssbuffer.block_id = 11 : i32} : i32 to i64 + %8 = arith.extsi %arg20 {ssbuffer.block_id = 11 : i32} : i32 to i64 + %9 = arith.extsi %arg15 {ssbuffer.block_id = 11 : i32} : i32 to i64 + %10 = arith.extsi %arg16 {ssbuffer.block_id = 11 : i32} : i32 to i64 + %11 = arith.extsi %arg18 {ssbuffer.block_id = 11 : i32} : i32 to i64 + %12 = arith.extsi %arg19 {ssbuffer.block_id = 11 : i32} : i32 to i64 + scf.for %arg30 = %2 to %4 step %c1_i64 : i64 { + %13 = arith.muli %arg30, %c5_i64 {ssbuffer.block_id = 14 : i32} : i64 + %14 = arith.index_cast %13 {ssbuffer.block_id = 14 : i32} : i64 to index + %15 = arith.addi %14, %c2 {ssbuffer.block_id = 14 : i32} : index + %reinterpret_cast_6 = memref.reinterpret_cast %arg22 to offset: [%15], sizes: [1], strides: [1] {ssbuffer.block_id = 14 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %16 = memref.load %reinterpret_cast_6[%c0] {ssbuffer.block_id = 14 : i32} : memref<1xi64, strided<[1], offset: ?>> + %17 = arith.addi %14, %c3 {ssbuffer.block_id = 14 : i32} : index + %reinterpret_cast_7 = memref.reinterpret_cast %arg22 to offset: [%17], sizes: [1], strides: [1] {ssbuffer.block_id = 14 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %18 = memref.load %reinterpret_cast_7[%c0] {ssbuffer.block_id = 14 : i32} : memref<1xi64, strided<[1], offset: ?>> + %19 = arith.index_cast %16 {ssbuffer.block_id = 14 : i32} : i64 to index + %reinterpret_cast_8 = memref.reinterpret_cast %arg6 to offset: [%19], sizes: [1], strides: [1] {ssbuffer.block_id = 14 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %20 = memref.load %reinterpret_cast_8[%c0] {ssbuffer.block_id = 14 : i32} : memref<1xi32, strided<[1], offset: ?>> + %21 = arith.addi %19, %c1 {ssbuffer.block_id = 14 : i32} : index + %reinterpret_cast_9 = memref.reinterpret_cast %arg6 to offset: [%21], sizes: [1], strides: [1] {ssbuffer.block_id = 14 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %22 = memref.load %reinterpret_cast_9[%c0] {ssbuffer.block_id = 14 : i32} : memref<1xi32, strided<[1], offset: ?>> + %23 = arith.subi %22, %20 {ssbuffer.block_id = 14 : i32} : i32 + %reinterpret_cast_10 = memref.reinterpret_cast %arg7 to offset: [%19], sizes: [1], strides: [1] {ssbuffer.block_id = 14 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %24 = memref.load %reinterpret_cast_10[%c0] {ssbuffer.block_id = 14 : i32} : memref<1xi32, strided<[1], offset: ?>> + %25 = arith.subi %24, %23 {ssbuffer.block_id = 14 : i32} : i32 + %26 = arith.muli %18, %c128_i64 {ssbuffer.block_id = 14 : i32} : i64 + %27 = arith.addi %26, %c128_i64 {Undefined, ssbuffer.block_id = 14 : i32} : i64 + %28 = arith.extsi %23 {ssbuffer.block_id = 14 : i32} : i32 to i64 + %29 = arith.minsi %27, %28 {Undefined, ssbuffer.block_id = 14 : i32} : i64 + %30 = arith.subi %29, %26 {Undefined, ssbuffer.block_id = 14 : i32} : i64 + %31 = arith.extsi %25 {ssbuffer.block_id = 14 : i32} : i32 to i64 + %32 = arith.addi %26, %31 {ssbuffer.block_id = 14 : i32} : i64 + %33 = arith.addi %32, %30 {Undefined, ssbuffer.block_id = 14 : i32} : i64 + %34 = arith.addi %33, %c31_i64 {Undefined, ssbuffer.block_id = 14 : i32} : i64 + %35 = arith.divsi %34, %c32_i64 {Undefined, ssbuffer.block_id = 14 : i32} : i64 + %36 = arith.muli %arg30, %c5_i64 {ssbuffer.block_id = 9 : i32} : i64 + %37 = arith.index_cast %36 {ssbuffer.block_id = 9 : i32} : i64 to index + %38 = arith.addi %37, %c1 {ssbuffer.block_id = 9 : i32} : index + %reinterpret_cast_11 = memref.reinterpret_cast %arg22 to offset: [%38], sizes: [1], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %39 = memref.load %reinterpret_cast_11[%c0] {ssbuffer.block_id = 9 : i32} : memref<1xi64, strided<[1], offset: ?>> + %40 = arith.addi %37, %c2 {ssbuffer.block_id = 9 : i32} : index + %reinterpret_cast_12 = memref.reinterpret_cast %arg22 to offset: [%40], sizes: [1], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %41 = memref.load %reinterpret_cast_12[%c0] {ssbuffer.block_id = 9 : i32} : memref<1xi64, strided<[1], offset: ?>> + %42 = arith.addi %37, %c3 {ssbuffer.block_id = 9 : i32} : index + %reinterpret_cast_13 = memref.reinterpret_cast %arg22 to offset: [%42], sizes: [1], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<1xi64, strided<[1], offset: ?>> + %43 = memref.load %reinterpret_cast_13[%c0] {ssbuffer.block_id = 9 : i32} : memref<1xi64, strided<[1], offset: ?>> + %44 = arith.index_cast %41 {ssbuffer.block_id = 9 : i32} : i64 to index + %reinterpret_cast_14 = memref.reinterpret_cast %arg6 to offset: [%44], sizes: [1], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %45 = memref.load %reinterpret_cast_14[%c0] {ssbuffer.block_id = 9 : i32} : memref<1xi32, strided<[1], offset: ?>> + %46 = arith.addi %44, %c1 {ssbuffer.block_id = 9 : i32} : index + %reinterpret_cast_15 = memref.reinterpret_cast %arg6 to offset: [%46], sizes: [1], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %47 = memref.load %reinterpret_cast_15[%c0] {ssbuffer.block_id = 9 : i32} : memref<1xi32, strided<[1], offset: ?>> + %48 = arith.subi %47, %45 {ssbuffer.block_id = 9 : i32} : i32 + %reinterpret_cast_16 = memref.reinterpret_cast %arg7 to offset: [%44], sizes: [1], strides: [1] {ssbuffer.block_id = 9 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %49 = memref.load %reinterpret_cast_16[%c0] {ssbuffer.block_id = 9 : i32} : memref<1xi32, strided<[1], offset: ?>> + %50 = arith.divsi %39, %c4_i64 {ssbuffer.block_id = 9 : i32} : i64 + %51 = arith.muli %43, %c128_i64 {ssbuffer.block_id = 9 : i32} : i64 + %52 = arith.muli %45, %arg12 {ssbuffer.block_id = 9 : i32} : i32 + %53 = arith.index_cast %52 {ssbuffer.block_id = 9 : i32} : i32 to index + %54 = arith.muli %39, %7 {ssbuffer.block_id = 9 : i32} : i64 + %55 = arith.index_cast %54 {ssbuffer.block_id = 9 : i32} : i64 to index + %56 = arith.addi %53, %55 {ssbuffer.block_id = 9 : i32} : index + %57 = arith.trunci %51 {ssbuffer.block_id = 9 : i32} : i64 to i32 + %58 = arith.maxsi %57, %c0_i32 {ssbuffer.block_id = 9 : i32} : i32 + %59 = arith.index_cast %58 {ssbuffer.block_id = 9 : i32} : i32 to index + %60 = arith.index_cast %arg12 {ssbuffer.block_id = 9 : i32} : i32 to index + %61 = arith.muli %59, %60 {ssbuffer.block_id = 9 : i32} : index + %62 = arith.addi %61, %56 {ssbuffer.block_id = 9 : i32} : index + %63 = arith.index_cast %48 {ssbuffer.block_id = 9 : i32} : i32 to index + %reinterpret_cast_17 = memref.reinterpret_cast %arg3 to offset: [%62], sizes: [128, 128], strides: [%60, 1] {ssbuffer.block_id = 9 : i32} : memref to memref<128x128xbf16, strided<[?, 1], offset: ?>> + %64 = arith.divsi %61, %60 {ssbuffer.block_id = 9 : i32} : index + %65 = arith.subi %63, %64 {ssbuffer.block_id = 9 : i32} : index + %66 = arith.maxsi %65, %c0 {ssbuffer.block_id = 9 : i32} : index + %67 = arith.minsi %66, %c128 {ssbuffer.block_id = 9 : i32} : index + %68 = arith.subi %c0_i32, %57 {ssbuffer.block_id = 9 : i32} : i32 + %69 = arith.maxsi %68, %c0_i32 {ssbuffer.block_id = 9 : i32} : i32 + %70 = arith.index_cast %69 {ssbuffer.block_id = 9 : i32} : i32 to index + %71 = arith.minsi %70, %67 {ssbuffer.block_id = 9 : i32} : index + %72 = arith.subi %67, %71 {ssbuffer.block_id = 9 : i32} : index + %73 = arith.cmpi slt, %72, %c128 {ssbuffer.block_id = 9 : i32} : index + %subview = memref.subview %reinterpret_cast_17[0, 0] [%72, 128] [1, 1] {ssbuffer.block_id = 9 : i32} : memref<128x128xbf16, strided<[?, 1], offset: ?>> to memref> + %74 = arith.extsi %49 {ssbuffer.block_id = 9 : i32} : i32 to i64 + %75 = arith.muli %41, %8 {ssbuffer.block_id = 9 : i32} : i64 + %76 = arith.muli %50, %9 {ssbuffer.block_id = 9 : i32} : i64 + %77 = arith.muli %50, %11 {ssbuffer.block_id = 9 : i32} : i64 + %alloc = memref.alloc() {ssbuffer.block_id = 10 : i32} : memref<128x128xbf16> + %subview_18 = memref.subview %alloc[%71, 0] [%72, 128] [1, 1] {ssbuffer.block_id = 10 : i32} : memref<128x128xbf16> to memref> + scf.if %73 { + linalg.fill {ssbuffer.block_id = 10 : i32} ins(%cst_0 : bf16) outs(%alloc : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 10 : i32} + memref.copy %subview, %subview_18 {ssbuffer.block_id = 10 : i32} : memref> to memref> + %78 = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 10 : i32} : memref<128x128xbf16> to tensor<128x128xbf16> + %alloc_19 = memref.alloc() {ssbuffer.block_id = 20 : i32, ssbuffer.crossDeps = [0 : i32, 1 : i32], ssbuffer.transfer_id = 0 : i32} : memref<2x8x16x16xbf16, #hivm.address_space> + %alloc_20 = memref.alloc() {ssbuffer.block_id = 20 : i32, ssbuffer.crossDeps = [0 : i32, 1 : i32], ssbuffer.transfer_id = 0 : i32} : memref<2x8x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_20 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 0 : i32} : memref<2x8x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_19 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 0 : i32} : memref<2x8x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 4 + %alloc_21 = memref.alloc() {ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x32xf32, #hivm.address_space> + %alloc_22 = memref.alloc() {ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x32xf32, #hivm.address_space> + annotation.mark %alloc_22 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<5>, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x32xf32, #hivm.address_space> + annotation.mark %alloc_21 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x32xf32, #hivm.address_space> + %alloc_23 = memref.alloc() {ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_24 = memref.alloc() {ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_24 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_23 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + scf.for %arg31 = %c0_i64 to %35 step %c1_i64 : i64 { + %79 = arith.muli %arg31, %c32_i64 {ssbuffer.block_id = 8 : i32} : i64 + %80 = arith.addi %79, %c32_i64 {ssbuffer.block_id = 8 : i32} : i64 + %81 = arith.minsi %80, %74 {ssbuffer.block_id = 8 : i32} : i64 + %82 = arith.subi %81, %79 {ssbuffer.block_id = 8 : i32} : i64 + %83 = arith.index_cast %arg16 {ssbuffer.block_id = 8 : i32} : i32 to index + %84 = arith.index_cast %82 {ssbuffer.block_id = 8 : i32} : i64 to index + %85 = arith.index_cast %arg19 {ssbuffer.block_id = 8 : i32} : i32 to index + %86 = arith.divsi %c0, %83 {ssbuffer.block_id = 8 : i32} : index + %87 = arith.subi %84, %86 {ssbuffer.block_id = 8 : i32} : index + %88 = arith.maxsi %87, %c0 {ssbuffer.block_id = 8 : i32} : index + %89 = arith.minsi %88, %c32 {ssbuffer.block_id = 8 : i32} : index + %90 = arith.minsi %89, %c0 {ssbuffer.block_id = 8 : i32} : index + %91 = arith.subi %89, %90 {ssbuffer.block_id = 8 : i32} : index + %92 = arith.cmpi slt, %91, %c32 {ssbuffer.block_id = 8 : i32} : index + %93 = arith.divsi %c0, %85 {ssbuffer.block_id = 8 : i32} : index + %94 = arith.subi %84, %93 {ssbuffer.block_id = 8 : i32} : index + %95 = arith.maxsi %94, %c0 {ssbuffer.block_id = 8 : i32} : index + %96 = arith.minsi %95, %c32 {ssbuffer.block_id = 8 : i32} : index + %97 = arith.minsi %96, %c0 {ssbuffer.block_id = 8 : i32} : index + %98 = arith.subi %96, %97 {ssbuffer.block_id = 8 : i32} : index + %99 = arith.cmpi slt, %98, %c32 {ssbuffer.block_id = 8 : i32} : index + %alloc_25 = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<32x128xbf16> + scf.if %92 { + linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst_0 : bf16) outs(%alloc_25 : memref<32x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 5 : i32} + %100 = arith.divsi %79, %c32_i64 {ssbuffer.block_id = 5 : i32} : i64 + %101 = arith.remsi %79, %c32_i64 {ssbuffer.block_id = 5 : i32} : i64 + %102 = arith.addi %75, %100 {ssbuffer.block_id = 5 : i32} : i64 + %103 = arith.index_cast %102 {ssbuffer.block_id = 5 : i32} : i64 to index + %reinterpret_cast_26 = memref.reinterpret_cast %arg8 to offset: [%103], sizes: [1], strides: [1] {ssbuffer.block_id = 5 : i32} : memref to memref<1xi32, strided<[1], offset: ?>> + %104 = memref.load %reinterpret_cast_26[%c0] {ssbuffer.block_id = 5 : i32} : memref<1xi32, strided<[1], offset: ?>> + %105 = arith.muli %104, %arg14 {ssbuffer.block_id = 5 : i32} : i32 + %106 = arith.index_cast %105 {ssbuffer.block_id = 5 : i32} : i32 to index + %107 = arith.muli %101, %10 {ssbuffer.block_id = 5 : i32} : i64 + %108 = arith.addi %76, %107 {ssbuffer.block_id = 5 : i32} : i64 + %109 = arith.index_cast %108 {ssbuffer.block_id = 5 : i32} : i64 to index + %110 = arith.addi %106, %109 {ssbuffer.block_id = 5 : i32} : index + %reinterpret_cast_27 = memref.reinterpret_cast %arg4 to offset: [%110], sizes: [32, 128], strides: [%83, 1] {ssbuffer.block_id = 5 : i32} : memref to memref<32x128xbf16, strided<[?, 1], offset: ?>> + %subview_28 = memref.subview %reinterpret_cast_27[0, 0] [%91, 128] [1, 1] {ssbuffer.block_id = 5 : i32} : memref<32x128xbf16, strided<[?, 1], offset: ?>> to memref> + %subview_29 = memref.subview %alloc_25[%90, 0] [%91, 128] [1, 1] {ssbuffer.block_id = 5 : i32} : memref<32x128xbf16> to memref> + memref.copy %subview_28, %subview_29 {ssbuffer.block_id = 5 : i32} : memref> to memref> + %111 = bufferization.to_tensor %alloc_25 restrict writable {ssbuffer.block_id = 5 : i32} : memref<32x128xbf16> to tensor<32x128xbf16> + %112 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<128x32xbf16> + %transposed = linalg.transpose ins(%111 : tensor<32x128xbf16>) outs(%112 : tensor<128x32xbf16>) permutation = [1, 0] {ssbuffer.block_id = 5 : i32} + %113 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32, ssbuffer.loop_carried_l0c} ins(%78, %transposed : tensor<128x128xbf16>, tensor<128x32xbf16>) outs(%6 : tensor<128x32xf32>) -> tensor<128x32xf32> + %114 = arith.divsi %arg31, %c1_i64 {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} : i64 + %c2_i64 = arith.constant {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} 2 : i64 + %115 = arith.remsi %114, %c2_i64 {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} : i64 + %c0_i64_30 = arith.constant {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} 0 : i64 + %116 = arith.cmpi eq, %115, %c0_i64_30 {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} : i64 + scf.if %116 { + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + } else { + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 6 + } {ssbuffer.block_id = 5 : i32, ssbuffer.cross_buffer = 1 : i32} + scf.if %116 { + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} ins(%113 : tensor<128x32xf32>) outs(%alloc_21 : memref<128x32xf32, #hivm.address_space>) + } else { + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} ins(%113 : tensor<128x32xf32>) outs(%alloc_22 : memref<128x32xf32, #hivm.address_space>) + } {ssbuffer.block_id = 5 : i32, ssbuffer.cross_buffer = 1 : i32, ssbuffer.transfer_id = 1 : i32} + scf.if %116 { + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + } else { + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 6 + } {ssbuffer.block_id = 5 : i32, ssbuffer.cross_buffer = 1 : i32} + %alloc_31 = memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<32x128xbf16> + scf.if %99 { + linalg.fill {ssbuffer.block_id = 7 : i32} ins(%cst_0 : bf16) outs(%alloc_31 : memref<32x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 7 : i32} + %117 = arith.muli %104, %arg17 {ssbuffer.block_id = 7 : i32} : i32 + %118 = arith.index_cast %117 {ssbuffer.block_id = 7 : i32} : i32 to index + %119 = arith.muli %101, %12 {ssbuffer.block_id = 7 : i32} : i64 + %120 = arith.addi %77, %119 {ssbuffer.block_id = 7 : i32} : i64 + %121 = arith.index_cast %120 {ssbuffer.block_id = 7 : i32} : i64 to index + %122 = arith.addi %118, %121 {ssbuffer.block_id = 7 : i32} : index + %reinterpret_cast_32 = memref.reinterpret_cast %arg5 to offset: [%122], sizes: [32, 128], strides: [%85, 1] {ssbuffer.block_id = 7 : i32} : memref to memref<32x128xbf16, strided<[?, 1], offset: ?>> + %subview_33 = memref.subview %reinterpret_cast_32[0, 0] [%98, 128] [1, 1] {ssbuffer.block_id = 7 : i32} : memref<32x128xbf16, strided<[?, 1], offset: ?>> to memref> + %subview_34 = memref.subview %alloc_31[%97, 0] [%98, 128] [1, 1] {ssbuffer.block_id = 7 : i32} : memref<32x128xbf16> to memref> + memref.copy %subview_33, %subview_34 {ssbuffer.block_id = 7 : i32} : memref> to memref> + %123 = bufferization.to_tensor %alloc_31 restrict writable {ssbuffer.block_id = 7 : i32} : memref<32x128xbf16> to tensor<32x128xbf16> + %124 = tensor.empty() {ssbuffer.block_id = 7 : i32} : tensor<128x128xf32> + %125 = linalg.fill {ssbuffer.block_id = 7 : i32} ins(%cst_1 : f32) outs(%124 : tensor<128x128xf32>) -> tensor<128x128xf32> + %126 = arith.divsi %arg31, %c1_i64 {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} : i64 + %c2_i64_35 = arith.constant {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} 2 : i64 + %127 = arith.remsi %126, %c2_i64_35 {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} : i64 + %c0_i64_36 = arith.constant {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} 0 : i64 + %128 = arith.cmpi eq, %127, %c0_i64_36 {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} : i64 + scf.if %128 { + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + } else { + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 4 + } {ssbuffer.block_id = 7 : i32, ssbuffer.cross_buffer = 1 : i32} + %129 = scf.if %128 -> (tensor<128x32xbf16>) { + %134 = hivm.hir.convert_layout %alloc_19 output_shape [128, 32] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 7 : i32, ssbuffer.crossDeps = [0 : i32, 0 : i32], ssbuffer.transfer_id = 0 : i32} : (memref<2x8x16x16xbf16, #hivm.address_space>) -> memref<128x32xbf16, #hivm.address_space> + %memspacecast = memref.memory_space_cast %134 {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x32xbf16, #hivm.address_space> to memref<128x32xbf16> + %135 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x32xbf16> to tensor<128x32xbf16> + scf.yield %135 : tensor<128x32xbf16> + } else { + %134 = hivm.hir.convert_layout %alloc_20 output_shape [128, 32] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 7 : i32, ssbuffer.crossDeps = [0 : i32, 0 : i32], ssbuffer.transfer_id = 0 : i32} : (memref<2x8x16x16xbf16, #hivm.address_space>) -> memref<128x32xbf16, #hivm.address_space> + %memspacecast = memref.memory_space_cast %134 {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x32xbf16, #hivm.address_space> to memref<128x32xbf16> + %135 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x32xbf16> to tensor<128x32xbf16> + scf.yield %135 : tensor<128x32xbf16> + } {ssbuffer.block_id = 7 : i32, ssbuffer.crossDeps = [0 : i32, 0 : i32], ssbuffer.cross_buffer = 1 : i32, ssbuffer.transfer_id = 0 : i32} + %130 = linalg.matmul {input_precision = "ieee", ssbuffer.adep, ssbuffer.block_id = 7 : i32, ssbuffer.loop_carried_l0c} ins(%129, %123 : tensor<128x32xbf16>, tensor<32x128xbf16>) outs(%125 : tensor<128x128xf32>) -> tensor<128x128xf32> + scf.if %128 { + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + } else { + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 4 + } {ssbuffer.block_id = 7 : i32, ssbuffer.cross_buffer = 1 : i32} + %131 = arith.divsi %arg31, %c1_i64 {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32} : i64 + %c2_i64_37 = arith.constant {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32} 2 : i64 + %132 = arith.remsi %131, %c2_i64_37 {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32} : i64 + %c0_i64_38 = arith.constant {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32} 0 : i64 + %133 = arith.cmpi eq, %132, %c0_i64_38 {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32} : i64 + scf.if %133 { + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + } else { + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + } {ssbuffer.block_id = 7 : i32, ssbuffer.cross_buffer = 1 : i32} + scf.if %133 { + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32} ins(%130 : tensor<128x128xf32>) outs(%alloc_23 : memref<128x128xf32, #hivm.address_space>) + } else { + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32} ins(%130 : tensor<128x128xf32>) outs(%alloc_24 : memref<128x128xf32, #hivm.address_space>) + } {ssbuffer.block_id = 7 : i32, ssbuffer.cross_buffer = 1 : i32, ssbuffer.transfer_id = 2 : i32} + scf.if %133 { + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + } else { + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + } {ssbuffer.block_id = 7 : i32, ssbuffer.cross_buffer = 1 : i32} + } {DataUse, ssbuffer.block_id = 20 : i32, ssbuffer.flowOpt, ssbuffer.main_loop = 0 : i32} + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 6 + } {Undefined, ssbuffer.block_id = 21 : i32} + scope.return + } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-with-scf-if.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-with-scf-if.mlir new file mode 100644 index 0000000000..408e1060ed --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-clone-ops-with-scf-if.mlir @@ -0,0 +1,46 @@ +// RUN: triton-opt --clone-ops %s --allow-unregistered-dialect | FileCheck %s + +// CHECK: func.func @test_clone_ops +func.func @test_clone_ops(%arg0: index, %arg1: index, %arg2: tensor<32x128xbf16>, %arg3: index, %arg4: index, %arg5: memref) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c32 = arith.constant 32 : index + %cst_7 = arith.constant 7.0 : bf16 + %for_op = scf.for %i = %c0 to %c1 step %c1 iter_args(%iter = %c0_i32) -> i32 { + %cmp = arith.cmpi slt, %arg0, %c32 {ssbuffer.block_id = 7 : i32} : index + %alloc_50 = memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<32x128xbf16> + scf.if %cmp { + linalg.fill {ssbuffer.block_id = 7 : i32} ins(%cst_7 : bf16) outs(%alloc_50 : memref<32x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 7 : i32} + %reinterpret_cast_51 = memref.reinterpret_cast %arg5 to offset: [%arg0], sizes: [32, 128], strides: [640, 1] {ssbuffer.block_id = 7 : i32} : memref to memref<32x128xbf16, strided<[640, 1], offset: ?>> + %subview_52 = memref.subview %reinterpret_cast_51[0, 0] [%arg1, 128] [1, 1] {ssbuffer.block_id = 7 : i32} : memref<32x128xbf16, strided<[640, 1], offset: ?>> to memref> + %subview_53 = memref.subview %alloc_50[0, 0] [%arg1, 128] [1, 1] {ssbuffer.block_id = 7 : i32} : memref<32x128xbf16> to memref> + memref.copy %subview_52, %subview_53 {ssbuffer.block_id = 7 : i32} : memref> to memref> + %92 = bufferization.to_tensor %alloc_50 restrict writable {ssbuffer.block_id = 7 : i32} : memref<32x128xbf16> to tensor<32x128xbf16> + %alloc_60 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32x128xbf16> + scf.if %cmp { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_7 : bf16) outs(%alloc_60 : memref<32x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + %reinterpret_cast_61 = memref.reinterpret_cast %arg5 to offset: [%arg0], sizes: [32, 128], strides: [640, 1] {ssbuffer.block_id = 8 : i32} : memref to memref<32x128xbf16, strided<[640, 1], offset: ?>> + %subview_62 = memref.subview %reinterpret_cast_61[0, 0] [%arg1, 128] [1, 1] {ssbuffer.block_id = 8 : i32} : memref<32x128xbf16, strided<[640, 1], offset: ?>> to memref> + %subview_63 = memref.subview %alloc_60[0, 0] [%arg1, 128] [1, 1] {ssbuffer.block_id = 8 : i32} : memref<32x128xbf16> to memref> + memref.copy %subview_62, %subview_63 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %93 = bufferization.to_tensor %alloc_60 restrict writable {ssbuffer.block_id = 8 : i32} : memref<32x128xbf16> to tensor<32x128xbf16> + %empty_transpose = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<128x32xbf16> + // CHECK: scf.if + // CHECK: } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} + // CHECK: {{.*}} = memref.reinterpret_cast {{.*}}{ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} + // CHECK: {{.*}} = memref.subview {{.*}}{ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} + // CHECK: memref.copy {{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} + // CHECK: {{.*}} = bufferization.to_tensor {{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} + %transposed_54 = linalg.transpose ins(%92 : tensor<32x128xbf16>) outs(%empty_transpose : tensor<128x32xbf16>) permutation = [1, 0] {ssbuffer.block_id = 8 : i32} + %94 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<32x32xf32> + %95 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 8 : i32} ins(%93, %transposed_54 : tensor<32x128xbf16>, tensor<128x32xbf16>) outs(%94 : tensor<32x32xf32>) -> tensor<32x32xf32> + + scf.yield %iter : i32 + } {ssbuffer.main_loop} + + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-iter-args-update.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-iter-args-update.mlir new file mode 100644 index 0000000000..a8f041e1f1 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-iter-args-update.mlir @@ -0,0 +1,85 @@ +// RUN: triton-opt --process-args %s --allow-unregistered-dialect | FileCheck %s + +// Test that ProcessArgs correctly handles shared iter_args by cloning computation +// chains and updating operands to new values. + +// CHECK: func.func @test_iter_args_update +module attributes {hacc.target = #hacc.target<"Ascend910_9589">} { + func.func @test_iter_args_update(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg9: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg10: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg11: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg12: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg13: i32 {tt.divisibility = 16 : i32}, %arg14: i32 {tt.divisibility = 16 : i32}, %arg15: i32 {tt.divisibility = 16 : i32}, %arg16: i32 {tt.divisibility = 16 : i32}, %arg17: i32 {tt.divisibility = 16 : i32}, %arg18: i32 {tt.divisibility = 16 : i32}, %arg19: i32 {tt.divisibility = 16 : i32}, %arg20: i32 {tt.divisibility = 16 : i32}, %arg21: i32, %arg22: i32, %arg23: i32 {tt.divisibility = 16 : i32}, %arg24: i32 {tt.divisibility = 16 : i32}, %arg25: i32 {tt.divisibility = 16 : i32}, %arg26: i32 {tt.divisibility = 16 : i32}, %arg27: i32 {tt.divisibility = 16 : i32}, %arg28: i32 {tt.divisibility = 16 : i32}, %arg29: i32 {tt.divisibility = 16 : i32}, %arg30: i32 {tt.divisibility = 16 : i32}, %arg31: i32, %arg32: i32, %arg33: i32, %arg34: i32, %arg35: i32, %arg36: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst_1 = arith.constant {ssbuffer.block_id = 34 : i32} 0.000000e+00 : f32 + %c32_i32 = arith.constant {MixUse, ssbuffer.block_id = 34 : i32} 32 : i32 + %c0_i32 = arith.constant {MixUse, ssbuffer.block_id = 34 : i32} 0 : i32 + %c128_i32 = arith.constant {MixUse, ssbuffer.block_id = 34 : i32} 128 : i32 + %c8_i32 = arith.constant {ssbuffer.block_id = 34 : i32} 8 : i32 + %c32 = arith.constant {DataUse, ssbuffer.block_id = 34 : i32} 32 : index + %c0 = arith.constant {ssbuffer.block_id = 34 : i32} 0 : index + %c1 = arith.constant {ssbuffer.block_id = 34 : i32} 1 : index + %0 = tensor.empty() {ssbuffer.block_id = 19 : i32} : tensor<32x32xf32> + %1 = arith.divsi %arg35, %c8_i32 {ssbuffer.block_id = 19 : i32} : i32 + %2 = arith.remsi %arg35, %c8_i32 {ssbuffer.block_id = 19 : i32} : i32 + %3 = arith.index_cast %1 {ssbuffer.block_id = 19 : i32} : i32 to index + %4 = arith.muli %2, %arg14 {ssbuffer.block_id = 17 : i32} : i32 + %5 = arith.index_cast %4 {ssbuffer.block_id = 17 : i32} : i32 to index + %6 = arith.index_cast %arg16 {ssbuffer.block_id = 17 : i32} : i32 to index + %7 = arith.muli %5, %6 {ssbuffer.block_id = 17 : i32} : index + %8 = arith.muli %arg14, %c32_i32 {ssbuffer.block_id = 17 : i32} : i32 + %9 = arith.muli %arg30, %c32_i32 {ssbuffer.block_id = 17 : i32} : i32 + %10 = arith.index_cast %arg14 {ssbuffer.block_id = 17 : i32} : i32 to index + %11 = arith.index_cast %arg30 {ssbuffer.block_id = 17 : i32} : i32 to index + %12 = arith.index_cast %8 {ssbuffer.block_id = 17 : i32} : i32 to index + %13 = arith.index_cast %9 {ssbuffer.block_id = 17 : i32} : i32 to index + %14 = arith.minsi %8, %c128_i32 {ssbuffer.block_id = 17 : i32} : i32 + %15 = tensor.empty() {ssbuffer.block_id = 34 : i32} : tensor<32x32xf32> + %16 = linalg.fill {ssbuffer.block_id = 34 : i32} ins(%cst_1 : f32) outs(%15 : tensor<32x32xf32>) -> tensor<32x32xf32> + %alloc = memref.alloc() {ssbuffer.block_id = 18 : i32} : memref<32x32xf32> + %17:2 = scf.for %arg37 = %c0_i32 to %14 step %c32_i32 iter_args(%arg38 = %c0, %arg39 = %c0) -> (index, index) : i32 { + %18 = arith.index_cast %arg37 {ssbuffer.block_id = 4 : i32} : i32 to index + %19 = arith.addi %18, %c32 {ssbuffer.block_id = 4 : i32} : index + %20 = arith.maxsi %18, %12 {ssbuffer.block_id = 4 : i32} : index + %21 = arith.minsi %19, %20 {ssbuffer.block_id = 4 : i32} : index + %22 = arith.subi %21, %18 {ssbuffer.block_id = 4 : i32} : index + %23 = arith.minsi %22, %c32 {ssbuffer.block_id = 4 : i32} : index + %24 = arith.cmpi slt, %23, %c32 {ssbuffer.block_id = 4 : i32} : index + %alloc_1 = memref.alloc() {ssbuffer.block_id = 4 : i32} : memref<32x32xf32> + scf.if %24 { + linalg.fill {ssbuffer.block_id = 4 : i32} ins(%cst_1 : f32) outs(%alloc_1 : memref<32x32xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 4 : i32} + %25 = arith.addi %3, %arg38 {ssbuffer.block_id = 4 : i32} : index + %26 = bufferization.to_tensor %alloc_1 restrict writable {ssbuffer.block_id = 4 : i32} : memref<32x32xf32> to tensor<32x32xf32> + %27 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 4 : i32} ins(%26, %15 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%16 : tensor<32x32xf32>) -> tensor<32x32xf32> + %28 = arith.index_cast %8 {ssbuffer.block_id = 4 : i32} : i32 to index + %29 = arith.addi %arg38, %28 {ssbuffer.block_id = 4 : i32} : index + hivm.hir.sync_block_wait {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 5 : i32} ins(%27 : tensor<32x32xf32>) outs(%alloc : memref<32x32xf32>) + hivm.hir.sync_block_set {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + %30 = arith.index_cast %arg37 {ssbuffer.block_id = 5 : i32, ssbuffer.clone = 4 : i32} : i32 to index + %31 = arith.addi %30, %c32 {ssbuffer.block_id = 5 : i32, ssbuffer.clone = 4 : i32} : index + %32 = arith.maxsi %30, %12 {ssbuffer.block_id = 5 : i32, ssbuffer.clone = 4 : i32} : index + %33 = arith.minsi %31, %32 {ssbuffer.block_id = 5 : i32, ssbuffer.clone = 4 : i32} : index + %34 = arith.subi %33, %30 {ssbuffer.block_id = 5 : i32, ssbuffer.clone = 4 : i32} : index + %35 = arith.minsi %34, %c32 {ssbuffer.block_id = 5 : i32, ssbuffer.clone = 4 : i32} : index + %36 = arith.cmpi slt, %35, %c32 {ssbuffer.block_id = 5 : i32, ssbuffer.clone = 4 : i32} : index + %alloc_2 = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<32x32xf32> + scf.if %36 { + linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst_1 : f32) outs(%alloc_2 : memref<32x32xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 5 : i32} + %37 = arith.addi %7, %arg39 {ssbuffer.block_id = 5 : i32} : index + %transposed_2 = linalg.transpose ins(%15 : tensor<32x32xf32>) outs(%0 : tensor<32x32xf32>) permutation = [1, 0] {ssbuffer.block_id = 5 : i32} + %38 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<32x32xf32> + %39 = linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst_1 : f32) outs(%38 : tensor<32x32xf32>) -> tensor<32x32xf32> + %40 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32} ins(%transposed_2, %15 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%39 : tensor<32x32xf32>) -> tensor<32x32xf32> + %41 = arith.index_cast %9 {ssbuffer.block_id = 5 : i32} : i32 to index + %42 = arith.addi %arg39, %41 {ssbuffer.block_id = 5 : i32} : index + %43 = arith.addi %3, %arg38 {ssbuffer.block_id = 10 : i32} : index + %44 = arith.addi %7, %arg39 {ssbuffer.block_id = 11 : i32} : index + // CHECK: %[[RES0:.*]] = arith.addi %3, %arg40 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 10 : i32} : index + // CHECK: %[[RES1:.*]] = arith.index_cast %8 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 10 : i32} : i32 to index + // CHECK: %[[RES2:.*]] = arith.addi %arg40, %[[RES1]] {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 10 : i32} : index + // CHECK: %[[RES3:.*]] = arith.addi %7, %arg41 {ssbuffer.arg = 1 : i32, ssbuffer.block_id = 11 : i32} : index + // CHECK: %[[RES4:.*]] = arith.index_cast %9 {ssbuffer.arg = 1 : i32, ssbuffer.block_id = 11 : i32} : i32 to index + // CHECK: %[[RES5:.*]] = arith.addi %arg41, %[[RES4]] {ssbuffer.arg = 1 : i32, ssbuffer.block_id = 11 : i32} : index + scf.yield %29, %42 : index, index + } {DataUse, ssbuffer.block_id = 36 : i32, ssbuffer.main_loop = 0 : i32} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-no-process-args.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-no-process-args.mlir new file mode 100644 index 0000000000..74e8fb129a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/test-no-process-args.mlir @@ -0,0 +1,69 @@ +// RUN: triton-opt --process-args %s --allow-unregistered-dialect | FileCheck %s --implicit-check-not=ssbuffer.arg +// CHECK: func.func @test_no_process_args + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func private @triton_indirect_load(memref, tensor<128x64xi64>, tensor<128x64xi1>, tensor<128x64xi8>) -> tensor<128x64xi8> + func.func private @triton_indirect_load_0(memref, tensor<64x256xi64>, tensor<64x256xi1>, tensor<64x256xi8>) -> tensor<64x256xi8> + func.func @test_no_process_args(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32 {tt.divisibility = 16 : i32}, %arg9: i32 {tt.divisibility = 16 : i32}, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "mix_simd_simt"} { + %c2048_i32 = arith.constant {DataUse, ssbuffer.block_id = 8 : i32} 2048 : i32 + %c64_i32 = arith.constant {DataUse, ssbuffer.block_id = 8 : i32} 64 : i32 + %c8_i32 = arith.constant {MixUse, ssbuffer.block_id = 8 : i32} 8 : i32 + %c256_i32 = arith.constant {MixUse, ssbuffer.block_id = 8 : i32} 256 : i32 + %c128_i32 = arith.constant {MixUse, ssbuffer.block_id = 8 : i32} 128 : i32 + %c2_i32 = arith.constant {DataUse, ssbuffer.block_id = 8 : i32} 2 : i32 + %c3_i32 = arith.constant {DataUse, ssbuffer.block_id = 8 : i32} 3 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 8 : i32} 0 : i32 + %c127_i32 = arith.constant {MixUse, ssbuffer.block_id = 8 : i32} 127 : i32 + %c255_i32 = arith.constant {MixUse, ssbuffer.block_id = 8 : i32} 255 : i32 + %c4_i32 = arith.constant {Undefined, ssbuffer.block_id = 8 : i32} 4 : i32 + %c1_i32 = arith.constant {Undefined, ssbuffer.block_id = 8 : i32} 1 : i32 + %c0_i8 = arith.constant {ssbuffer.block_id = 8 : i32} 0 : i8 + %c1_i8 = arith.constant {ssbuffer.block_id = 8 : i32} 1 : i8 + %c128 = arith.constant {ssbuffer.block_id = 8 : i32} 128 : index + %c256 = arith.constant {ssbuffer.block_id = 8 : i32} 256 : index + %cst = arith.constant {ssbuffer.block_id = 2 : i32} dense<[8, 4, 16, 32]> : tensor<4xi64> + %cst_0 = arith.constant {ssbuffer.block_id = 2 : i32} dense<[64, 8, 32]> : tensor<3xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 2 : i32} dense<[2, 8, 16, 32]> : tensor<4xi64> + %cst_2 = arith.constant {ssbuffer.block_id = 2 : i32} dense<[128, 2, 32]> : tensor<3xi64> + scope.scope : () -> () { + %0 = arith.addi %arg5, %c127_i32 {MixUse, ssbuffer.block_id = 8 : i32} : i32 + %1 = arith.divsi %0, %c128_i32 {MixUse, ssbuffer.block_id = 8 : i32} : i32 + %2 = arith.addi %arg6, %c255_i32 {MixUse, ssbuffer.block_id = 8 : i32} : i32 + %3 = arith.divsi %2, %c256_i32 {MixUse, ssbuffer.block_id = 8 : i32} : i32 + %4 = arith.muli %1, %3 {Undefined, ssbuffer.block_id = 8 : i32} : i32 + scf.for %arg17 = %arg14 to %4 step %arg10 : i32 { + scf.for %arg18 = %c0_i32 to %c4_i32 step %c1_i32 : i32 { + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.crossDeps = [0 : i32, 1 : i32], ssbuffer.transfer_id = 0 : i32} : memref<2x8x16x32xi8, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<2x8x16x32xi8, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + %alloc_3 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.crossDeps = [1 : i32, 1 : i32], ssbuffer.transfer_id = 1 : i32} : memref<8x4x16x32xi8, #hivm.address_space> + annotation.mark %alloc_3 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<8x4x16x32xi8, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %alloc_4 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x256xi32, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x256xi32, #hivm.address_space> + scf.for %arg19 = %c0_i32 to %c8_i32 step %c1_i32 : i32 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %5 = hivm.hir.convert_layout %alloc_3 output_shape [64, 256] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.crossDeps = [1 : i32, 0 : i32], ssbuffer.transfer_id = 1 : i32} : (memref<8x4x16x32xi8, #hivm.address_space>) -> memref<64x256xi8, #hivm.address_space> + %memspacecast = memref.memory_space_cast %5 {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32} : memref<64x256xi8, #hivm.address_space> to memref<64x256xi8> + %6 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32} : memref<64x256xi8> to tensor<64x256xi8> + hivm.hir.sync_block_wait {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + %7 = hivm.hir.convert_layout %alloc output_shape [128, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.crossDeps = [0 : i32, 0 : i32], ssbuffer.transfer_id = 0 : i32} : (memref<2x8x16x32xi8, #hivm.address_space>) -> memref<128x64xi8, #hivm.address_space> + %memspacecast_5 = memref.memory_space_cast %7 {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xi8, #hivm.address_space> to memref<128x64xi8> + %8 = bufferization.to_tensor %memspacecast_5 restrict writable {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xi8> to tensor<128x64xi8> + %9 = tensor.empty() {ssbuffer.block_id = 1 : i32} : tensor<128x256xi32> + %10 = linalg.fill {ssbuffer.block_id = 1 : i32} ins(%c0_i32 : i32) outs(%9 : tensor<128x256xi32>) -> tensor<128x256xi32> + %11 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 1 : i32} ins(%8, %6 : tensor<128x64xi8>, tensor<64x256xi8>) outs(%10 : tensor<128x256xi32>) -> tensor<128x256xi32> + hivm.hir.sync_block_set {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.sync_block_set {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 2 : i32} ins(%11 : tensor<128x256xi32>) outs(%alloc_4 : memref<128x256xi32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + } {DataUse, ssbuffer.block_id = 9 : i32, ssbuffer.main_loop = 0 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + } {DataUse, ssbuffer.block_id = 10 : i32} + } {Undefined, ssbuffer.block_id = 11 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-clone-ops.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-clone-ops.mlir new file mode 100644 index 0000000000..136c12f1f5 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-clone-ops.mlir @@ -0,0 +1,110 @@ +// RUN: triton-opt --clone-ops %s --allow-unregistered-dialect | FileCheck %s + +// Test CloneOps handling of an scf.while main loop with a cross-block update +// chain (block 9 owns the addition; block 7 builds on it via muli/addi and +// also adds extra vector-only index ops) and the vector-only index chain in +// block 7 that feeds the cube block (block 8). The pass clones the cross-block +// source into block 7 (`clone = 9`) and clones the vector index ops into block +// 8 (`clone = 7`). + +// CHECK: func.func @pcb12_tc01_while_matmul_fill +// CHECK: scf.while +// The cross-block update chain source in block 9 stays in place. +// CHECK: arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 9 : i32} : i32 +// It is cloned into block 7 carrying ssbuffer.clone = 9, so block 7 can +// build its own muli/addi chain without crossing blocks. +// CHECK: arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.clone = 9 : i32} : i32 +// Block 7's vector-only index ops land in block 8 with clone = 7. +// CHECK: arith.index_cast %arg7 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : i32 to index +// CHECK: arith.maxsi {{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index +// CHECK: arith.minsi {{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index +// CHECK: arith.cmpi slt, {{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index +// CHECK: arith.index_cast %arg6 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : i32 to index +// The terminator yields the chain's top result for the loop's single iter_arg. +// CHECK: scf.yield %{{.*}} : i32 + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @pcb12_tc01_while_matmul_fill(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32 {tt.divisibility = 16 : i32}, %arg9: i32 {tt.divisibility = 16 : i32}, %arg10: i32 {tt.divisibility = 16 : i32}, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 11 : i32} 0.000000e+00 : f16 + %c1_i32 = arith.constant {ssbuffer.block_id = 10 : i32} 1 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 10 : i32} 0 : i32 + %c0 = arith.constant {ssbuffer.block_id = 10 : i32} 0 : index + %c32 = arith.constant {ssbuffer.block_id = 10 : i32} 32 : index + %c64 = arith.constant {ssbuffer.block_id = 10 : i32} 64 : index + %cst_0 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[4, 1, 16, 16]> : tensor<4xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[16, 4, 16]> : tensor<3xi64> + %cst_2 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[1, 2, 16, 16]> : tensor<4xi64> + %cst_3 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[32, 1, 16]> : tensor<3xi64> + scope.scope : () -> () { + %alloc_16 = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} : memref<1x2x16x16xf16, #hivm.address_space> + annotation.mark %alloc_16 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} : memref<1x2x16x16xf16, #hivm.address_space> + %alloc_17 = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x1x16x16xf16, #hivm.address_space> + annotation.mark %alloc_17 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x1x16x16xf16, #hivm.address_space> + + %0 = scf.while (%arg17 = %c0_i32) : (i32) -> i32 { + %1 = arith.cmpi slt, %arg17, %arg7 {Undefined, ssbuffer.block_id = 4 : i32} : i32 + scf.condition(%1) %arg17 : i32 + } do { + ^bb0(%arg17: i32): + %1 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 9 : i32} : i32 + %2 = arith.index_cast %arg7 {ssbuffer.block_id = 7 : i32} : i32 to index + %3 = arith.maxsi %2, %c0 {ssbuffer.block_id = 7 : i32} : index + %4 = arith.minsi %3, %c32 {ssbuffer.block_id = 7 : i32} : index + %5 = arith.cmpi slt, %4, %c32 {ssbuffer.block_id = 7 : i32} : index + %6 = arith.index_cast %arg6 {ssbuffer.block_id = 7 : i32} : i32 to index + %7 = arith.maxsi %6, %c0 {ssbuffer.block_id = 7 : i32} : index + %8 = arith.minsi %7, %c64 {ssbuffer.block_id = 7 : i32} : index + %9 = arith.cmpi slt, %8, %c64 {ssbuffer.block_id = 7 : i32} : index + + %mj_1 = arith.muli %1, %c1_i32 {ssbuffer.block_id = 7 : i32} : i32 + %mj_2 = arith.addi %mj_1, %arg17 {ssbuffer.block_id = 7 : i32} : i32 + + %alloc = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32xf16> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<64xf16> + scf.if %9 { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%alloc_5 : memref<64xf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + scf.if %5 { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%alloc : memref<32xf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + %10 = arith.muli %arg17, %arg8 {ssbuffer.block_id = 8 : i32} : i32 + %11 = arith.index_cast %10 {ssbuffer.block_id = 8 : i32} : i32 to index + %reinterpret_cast = memref.reinterpret_cast %arg2 to offset: [%11], sizes: [32], strides: [1] {ssbuffer.block_id = 8 : i32} : memref to memref<32xf16, strided<[1], offset: ?>> + %subview = memref.subview %reinterpret_cast[0] [%4] [1] {ssbuffer.block_id = 8 : i32} : memref<32xf16, strided<[1], offset: ?>> to memref> + %subview_6 = memref.subview %alloc[0] [%4] [1] {ssbuffer.block_id = 8 : i32} : memref<32xf16> to memref> + memref.copy %subview, %subview_6 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %12 = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 8 : i32} : memref<32xf16> to tensor<32xf16> + %13 = arith.muli %arg17, %arg9 {ssbuffer.block_id = 8 : i32} : i32 + %14 = arith.index_cast %13 {ssbuffer.block_id = 8 : i32} : i32 to index + %reinterpret_cast_7 = memref.reinterpret_cast %arg3 to offset: [%14], sizes: [64], strides: [1] {ssbuffer.block_id = 8 : i32} : memref to memref<64xf16, strided<[1], offset: ?>> + %subview_8 = memref.subview %reinterpret_cast_7[0] [%8] [1] {ssbuffer.block_id = 8 : i32} : memref<64xf16, strided<[1], offset: ?>> to memref> + %subview_9 = memref.subview %alloc_5[0] [%8] [1] {ssbuffer.block_id = 8 : i32} : memref<64xf16> to memref> + memref.copy %subview_8, %subview_9 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %15 = bufferization.to_tensor %alloc_5 restrict writable {ssbuffer.block_id = 8 : i32} : memref<64xf16> to tensor<64xf16> + %expanded = tensor.expand_shape %12 [[0, 1]] output_shape [32, 1] {ssbuffer.block_id = 8 : i32} : tensor<32xf16> into tensor<32x1xf16> + %16 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<32x16xf16> + %17 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%16 : tensor<32x16xf16>) -> tensor<32x16xf16> + %inserted_slice = tensor.insert_slice %expanded into %17[0, 0] [32, 1] [1, 1] {ssbuffer.block_id = 8 : i32} : tensor<32x1xf16> into tensor<32x16xf16> + %expanded_10 = tensor.expand_shape %15 [[0, 1]] output_shape [1, 64] {ssbuffer.block_id = 8 : i32} : tensor<64xf16> into tensor<1x64xf16> + %18 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<16x64xf16> + %19 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%18 : tensor<16x64xf16>) -> tensor<16x64xf16> + %inserted_slice_11 = tensor.insert_slice %expanded_10 into %19[0, 0] [1, 64] [1, 1] {ssbuffer.block_id = 8 : i32} : tensor<1x64xf16> into tensor<16x64xf16> + %reshape = tensor.reshape %inserted_slice(%cst_3) {ssbuffer.block_id = 8 : i32} : (tensor<32x16xf16>, tensor<3xi64>) -> tensor<32x1x16xf16> + %20 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<1x32x16xf16> + %transposed = linalg.transpose ins(%reshape : tensor<32x1x16xf16>) outs(%20 : tensor<1x32x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 8 : i32} + %reshape_12 = tensor.reshape %transposed(%cst_2) {ssbuffer.block_id = 8 : i32} : (tensor<1x32x16xf16>, tensor<4xi64>) -> tensor<1x2x16x16xf16> + %reshape_13 = tensor.reshape %inserted_slice_11(%cst_1) {ssbuffer.block_id = 8 : i32} : (tensor<16x64xf16>, tensor<3xi64>) -> tensor<16x4x16xf16> + %21 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<4x16x16xf16> + %transposed_14 = linalg.transpose ins(%reshape_13 : tensor<16x4x16xf16>) outs(%21 : tensor<4x16x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 8 : i32} + %reshape_15 = tensor.reshape %transposed_14(%cst_0) {ssbuffer.block_id = 8 : i32} : (tensor<4x16x16xf16>, tensor<4xi64>) -> tensor<4x1x16x16xf16> + hivm.hir.copy ins(%reshape_12 : tensor<1x2x16x16xf16>) outs(%alloc_16 : memref<1x2x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.copy ins(%reshape_15 : tensor<4x1x16x16xf16>) outs(%alloc_17 : memref<4x1x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + scf.yield %mj_2 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-create-if-ops.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-create-if-ops.mlir new file mode 100644 index 0000000000..9139025963 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-create-if-ops.mlir @@ -0,0 +1,130 @@ +// RUN: triton-opt --create-if-ops %s --allow-unregistered-dialect | FileCheck %s + +// Test CreateIfOps on an scf.while main loop after ProcessArgs. The loop +// already carries 5 iter_args. Each block in the do region is wrapped in an +// scf.if guarded by a boolean, and the per-block while_arg update chains (an +// addi/muli/addi triple tagged `ssbuffer.while_arg = 0`) are threaded through +// the if results so they still reach scf.yield. + +// CHECK: func.func @pcb12_tc01_while_matmul_fill +// CHECK: scf.while +// CHECK: ^bb0(%arg17: i32, %arg18: i32, %arg19: i32, %arg20: i32, %arg21: i32): +// Block 9 wrapped into an scf.if returning its single while_arg update. +// CHECK: scf.if %{{.*}} -> (i32) { +// CHECK: arith.muli %{{.*}}, %c1_i32 {ssbuffer.block_id = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// CHECK: arith.addi %{{.*}}, %arg19 {ssbuffer.block_id = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 9 : i32} +// Block 7 wrapped into an scf.if returning its condition update and while_arg. +// CHECK: scf.if %{{.*}} -> (i32, i32) { +// CHECK: arith.addi %{{.*}}, %arg20 {ssbuffer.block_id = 7 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 7 : i32} +// Block 8 wrapped into an scf.if returning its arg update and while_arg. +// CHECK: scf.if %{{.*}} -> (i32, i32) { +// CHECK: arith.addi %{{.*}}, %arg21 {ssbuffer.block_id = 8 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// CHECK: } {hivm.matmul_limited_in_cube, ssbuffer.if = 8 : i32} +// The terminator wires the if results back into the 5 yielded values. +// CHECK: scf.yield %{{.*}}#0, %{{.*}}#0, %{{.*}}, %{{.*}}#1, %{{.*}}#1 : i32, i32, i32, i32, i32 + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @pcb12_tc01_while_matmul_fill(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32 {tt.divisibility = 16 : i32}, %arg9: i32 {tt.divisibility = 16 : i32}, %arg10: i32 {tt.divisibility = 16 : i32}, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 11 : i32} 0.000000e+00 : f16 + %c1_i32 = arith.constant {ssbuffer.block_id = 10 : i32} 1 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 10 : i32} 0 : i32 + %c0 = arith.constant {ssbuffer.block_id = 10 : i32} 0 : index + %c32 = arith.constant {ssbuffer.block_id = 10 : i32} 32 : index + %c64 = arith.constant {ssbuffer.block_id = 10 : i32} 64 : index + %cst_0 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[4, 1, 16, 16]> : tensor<4xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[16, 4, 16]> : tensor<3xi64> + %cst_2 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[1, 2, 16, 16]> : tensor<4xi64> + %cst_3 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[32, 1, 16]> : tensor<3xi64> + scope.scope : () -> () { + %alloc = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} : memref<1x2x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} : memref<1x2x16x16xf16, #hivm.address_space> + %alloc_4 = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x1x16x16xf16, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x1x16x16xf16, #hivm.address_space> + %0:5 = scf.while (%arg17 = %c0_i32, %arg18 = %c0_i32, %arg19 = %c0_i32, %arg20 = %c0_i32, %arg21 = %c0_i32) : (i32, i32, i32, i32, i32) -> (i32, i32, i32, i32, i32) { + %1 = arith.cmpi slt, %arg17, %arg7 {Undefined, ssbuffer.block_id = 4 : i32} : i32 + scf.condition(%1) %arg17, %arg18, %arg19, %arg20, %arg21 : i32, i32, i32, i32, i32 + } do { + ^bb0(%arg17: i32, %arg18: i32, %arg19: i32, %arg20: i32, %arg21: i32): + %1 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 9 : i32} : i32 + %2 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 9 : i32, ssbuffer.clone = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %3 = arith.muli %2, %c1_i32 {ssbuffer.block_id = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %4 = arith.addi %3, %arg19 {ssbuffer.block_id = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %5 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.clone = 9 : i32} : i32 + %6 = arith.index_cast %arg7 {ssbuffer.block_id = 7 : i32} : i32 to index + %7 = arith.maxsi %6, %c0 {ssbuffer.block_id = 7 : i32} : index + %8 = arith.minsi %7, %c32 {ssbuffer.block_id = 7 : i32} : index + %9 = arith.cmpi slt, %8, %c32 {ssbuffer.block_id = 7 : i32} : index + %10 = arith.index_cast %arg6 {ssbuffer.block_id = 7 : i32} : i32 to index + %11 = arith.maxsi %10, %c0 {ssbuffer.block_id = 7 : i32} : index + %12 = arith.minsi %11, %c64 {ssbuffer.block_id = 7 : i32} : index + %13 = arith.cmpi slt, %12, %c64 {ssbuffer.block_id = 7 : i32} : index + %14 = arith.muli %5, %c1_i32 {ssbuffer.block_id = 7 : i32} : i32 + %15 = arith.addi %14, %arg17 {ssbuffer.block_id = 7 : i32} : i32 + %16 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.clone = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %17 = arith.muli %16, %c1_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %18 = arith.addi %17, %arg20 {ssbuffer.block_id = 7 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %19 = arith.index_cast %arg7 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : i32 to index + %20 = arith.maxsi %19, %c0 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %21 = arith.minsi %20, %c32 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %22 = arith.cmpi slt, %21, %c32 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %23 = arith.index_cast %arg6 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : i32 to index + %24 = arith.maxsi %23, %c0 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %25 = arith.minsi %24, %c64 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %26 = arith.cmpi slt, %25, %c64 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %alloc_5 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32xf16> + %alloc_6 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<64xf16> + scf.if %26 { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%alloc_6 : memref<64xf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + scf.if %22 { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%alloc_5 : memref<32xf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + %27 = arith.muli %arg18, %arg8 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 + %28 = arith.index_cast %27 {ssbuffer.block_id = 8 : i32} : i32 to index + %reinterpret_cast = memref.reinterpret_cast %arg2 to offset: [%28], sizes: [32], strides: [1] {ssbuffer.block_id = 8 : i32} : memref to memref<32xf16, strided<[1], offset: ?>> + %subview = memref.subview %reinterpret_cast[0] [%21] [1] {ssbuffer.block_id = 8 : i32} : memref<32xf16, strided<[1], offset: ?>> to memref> + %subview_7 = memref.subview %alloc_5[0] [%21] [1] {ssbuffer.block_id = 8 : i32} : memref<32xf16> to memref> + memref.copy %subview, %subview_7 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %29 = bufferization.to_tensor %alloc_5 restrict writable {ssbuffer.block_id = 8 : i32} : memref<32xf16> to tensor<32xf16> + %30 = arith.muli %arg18, %arg9 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 + %31 = arith.index_cast %30 {ssbuffer.block_id = 8 : i32} : i32 to index + %reinterpret_cast_8 = memref.reinterpret_cast %arg3 to offset: [%31], sizes: [64], strides: [1] {ssbuffer.block_id = 8 : i32} : memref to memref<64xf16, strided<[1], offset: ?>> + %subview_9 = memref.subview %reinterpret_cast_8[0] [%25] [1] {ssbuffer.block_id = 8 : i32} : memref<64xf16, strided<[1], offset: ?>> to memref> + %subview_10 = memref.subview %alloc_6[0] [%25] [1] {ssbuffer.block_id = 8 : i32} : memref<64xf16> to memref> + memref.copy %subview_9, %subview_10 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %32 = bufferization.to_tensor %alloc_6 restrict writable {ssbuffer.block_id = 8 : i32} : memref<64xf16> to tensor<64xf16> + %expanded = tensor.expand_shape %29 [[0, 1]] output_shape [32, 1] {ssbuffer.block_id = 8 : i32} : tensor<32xf16> into tensor<32x1xf16> + %33 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<32x16xf16> + %34 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%33 : tensor<32x16xf16>) -> tensor<32x16xf16> + %inserted_slice = tensor.insert_slice %expanded into %34[0, 0] [32, 1] [1, 1] {ssbuffer.block_id = 8 : i32} : tensor<32x1xf16> into tensor<32x16xf16> + %expanded_11 = tensor.expand_shape %32 [[0, 1]] output_shape [1, 64] {ssbuffer.block_id = 8 : i32} : tensor<64xf16> into tensor<1x64xf16> + %35 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<16x64xf16> + %36 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%35 : tensor<16x64xf16>) -> tensor<16x64xf16> + %inserted_slice_12 = tensor.insert_slice %expanded_11 into %36[0, 0] [1, 64] [1, 1] {ssbuffer.block_id = 8 : i32} : tensor<1x64xf16> into tensor<16x64xf16> + %reshape = tensor.reshape %inserted_slice(%cst_3) {ssbuffer.block_id = 8 : i32} : (tensor<32x16xf16>, tensor<3xi64>) -> tensor<32x1x16xf16> + %37 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<1x32x16xf16> + %transposed = linalg.transpose ins(%reshape : tensor<32x1x16xf16>) outs(%37 : tensor<1x32x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 8 : i32} + %reshape_13 = tensor.reshape %transposed(%cst_2) {ssbuffer.block_id = 8 : i32} : (tensor<1x32x16xf16>, tensor<4xi64>) -> tensor<1x2x16x16xf16> + %reshape_14 = tensor.reshape %inserted_slice_12(%cst_1) {ssbuffer.block_id = 8 : i32} : (tensor<16x64xf16>, tensor<3xi64>) -> tensor<16x4x16xf16> + %38 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<4x16x16xf16> + %transposed_15 = linalg.transpose ins(%reshape_14 : tensor<16x4x16xf16>) outs(%38 : tensor<4x16x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 8 : i32} + %reshape_16 = tensor.reshape %transposed_15(%cst_0) {ssbuffer.block_id = 8 : i32} : (tensor<4x16x16xf16>, tensor<4xi64>) -> tensor<4x1x16x16xf16> + hivm.hir.copy ins(%reshape_13 : tensor<1x2x16x16xf16>) outs(%alloc : memref<1x2x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.copy ins(%reshape_16 : tensor<4x1x16x16xf16>) outs(%alloc_4 : memref<4x1x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} + %39 = arith.addi %c1_i32, %c1_i32 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32, ssbuffer.clone = 9 : i32} : i32 + %40 = arith.muli %39, %c1_i32 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 + %41 = arith.addi %40, %arg18 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 + %42 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %43 = arith.muli %42, %c1_i32 {ssbuffer.block_id = 8 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %44 = arith.addi %43, %arg21 {ssbuffer.block_id = 8 : i32, ssbuffer.while_arg = 0 : i32} : i32 + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + scf.yield %15, %41, %4, %18, %44 : i32, i32, i32, i32, i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-process-args.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-process-args.mlir new file mode 100644 index 0000000000..432ddb9cb3 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-process-args.mlir @@ -0,0 +1,117 @@ +// RUN: triton-opt --process-args %s --allow-unregistered-dialect | FileCheck %s + +// Test ProcessArgs adaptation of an scf.while main loop. The iter_arg used by +// scf.condition (index 0) is referenced by three separate blocks, so the pass +// clones its update chain (an addi/muli/addi triple) once per block and grows +// the loop from 1 to 5 iter_args. Each per-block clone is tagged with +// `ssbuffer.while_arg = 0`, and the buffer-offset ops that consume the induction +// arg are rewritten onto the new pipeline arg and tagged with `ssbuffer.arg = 0`. + +// CHECK: func.func @pcb12_tc01_while_matmul_fill +// The while op now carries 5 iter_args and 5 results. +// CHECK: %{{.*}}:5 = scf.while (%arg17 = %c0_i32, %arg18 = %c0_i32, %arg19 = %c0_i32, %arg20 = %c0_i32, %arg21 = %c0_i32) : (i32, i32, i32, i32, i32) -> (i32, i32, i32, i32, i32) +// scf.condition forwards all 5 iter_args. +// CHECK: scf.condition(%{{.*}}) %arg17, %arg18, %arg19, %arg20, %arg21 : i32, i32, i32, i32, i32 +// CHECK: ^bb0(%arg17: i32, %arg18: i32, %arg19: i32, %arg20: i32, %arg21: i32): +// The tail of block 9's per-block clone chain feeds iter_arg %arg18. +// CHECK: arith.addi %{{.*}}, %arg18 {ssbuffer.block_id = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// Block 7's per-block clone chain feeds iter_arg %arg19. +// CHECK: arith.addi %{{.*}}, %arg19 {ssbuffer.block_id = 7 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// The buffer-offset op is rewritten onto pipeline arg %arg21 and tagged ssbuffer.arg. +// CHECK: arith.muli %arg21, %arg8 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 +// Block 8's per-block clone chain feeds iter_arg %arg20. +// CHECK: arith.addi %{{.*}}, %arg20 {ssbuffer.block_id = 8 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// The terminator yields the original update plus the three per-block updates. +// CHECK: scf.yield %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}} : i32, i32, i32, i32, i32 + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @pcb12_tc01_while_matmul_fill(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32 {tt.divisibility = 16 : i32}, %arg9: i32 {tt.divisibility = 16 : i32}, %arg10: i32 {tt.divisibility = 16 : i32}, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 11 : i32} 0.000000e+00 : f16 + %c1_i32 = arith.constant {ssbuffer.block_id = 10 : i32} 1 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 10 : i32} 0 : i32 + %c0 = arith.constant {ssbuffer.block_id = 10 : i32} 0 : index + %c32 = arith.constant {ssbuffer.block_id = 10 : i32} 32 : index + %c64 = arith.constant {ssbuffer.block_id = 10 : i32} 64 : index + %cst_0 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[4, 1, 16, 16]> : tensor<4xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[16, 4, 16]> : tensor<3xi64> + %cst_2 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[1, 2, 16, 16]> : tensor<4xi64> + %cst_3 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[32, 1, 16]> : tensor<3xi64> + scope.scope : () -> () { + %alloc = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} : memref<1x2x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} : memref<1x2x16x16xf16, #hivm.address_space> + %alloc_4 = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x1x16x16xf16, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x1x16x16xf16, #hivm.address_space> + %0 = scf.while (%arg17 = %c0_i32) : (i32) -> i32 { + %1 = arith.cmpi slt, %arg17, %arg7 {Undefined, ssbuffer.block_id = 4 : i32} : i32 + scf.condition(%1) %arg17 : i32 + } do { + ^bb0(%arg17: i32): + %1 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 9 : i32} : i32 + %2 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.clone = 9 : i32} : i32 + %3 = arith.index_cast %arg7 {ssbuffer.block_id = 7 : i32} : i32 to index + %4 = arith.maxsi %3, %c0 {ssbuffer.block_id = 7 : i32} : index + %5 = arith.minsi %4, %c32 {ssbuffer.block_id = 7 : i32} : index + %6 = arith.cmpi slt, %5, %c32 {ssbuffer.block_id = 7 : i32} : index + %7 = arith.index_cast %arg6 {ssbuffer.block_id = 7 : i32} : i32 to index + %8 = arith.maxsi %7, %c0 {ssbuffer.block_id = 7 : i32} : index + %9 = arith.minsi %8, %c64 {ssbuffer.block_id = 7 : i32} : index + %10 = arith.cmpi slt, %9, %c64 {ssbuffer.block_id = 7 : i32} : index + %11 = arith.muli %2, %c1_i32 {ssbuffer.block_id = 7 : i32} : i32 + %12 = arith.addi %11, %arg17 {ssbuffer.block_id = 7 : i32} : i32 + %13 = arith.index_cast %arg7 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : i32 to index + %14 = arith.maxsi %13, %c0 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %15 = arith.minsi %14, %c32 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %16 = arith.cmpi slt, %15, %c32 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %17 = arith.index_cast %arg6 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : i32 to index + %18 = arith.maxsi %17, %c0 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %19 = arith.minsi %18, %c64 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %20 = arith.cmpi slt, %19, %c64 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %alloc_5 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32xf16> + %alloc_6 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<64xf16> + scf.if %20 { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%alloc_6 : memref<64xf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + scf.if %16 { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%alloc_5 : memref<32xf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + %21 = arith.muli %arg17, %arg8 {ssbuffer.block_id = 8 : i32} : i32 + %22 = arith.index_cast %21 {ssbuffer.block_id = 8 : i32} : i32 to index + %reinterpret_cast = memref.reinterpret_cast %arg2 to offset: [%22], sizes: [32], strides: [1] {ssbuffer.block_id = 8 : i32} : memref to memref<32xf16, strided<[1], offset: ?>> + %subview = memref.subview %reinterpret_cast[0] [%15] [1] {ssbuffer.block_id = 8 : i32} : memref<32xf16, strided<[1], offset: ?>> to memref> + %subview_7 = memref.subview %alloc_5[0] [%15] [1] {ssbuffer.block_id = 8 : i32} : memref<32xf16> to memref> + memref.copy %subview, %subview_7 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %23 = bufferization.to_tensor %alloc_5 restrict writable {ssbuffer.block_id = 8 : i32} : memref<32xf16> to tensor<32xf16> + %24 = arith.muli %arg17, %arg9 {ssbuffer.block_id = 8 : i32} : i32 + %25 = arith.index_cast %24 {ssbuffer.block_id = 8 : i32} : i32 to index + %reinterpret_cast_8 = memref.reinterpret_cast %arg3 to offset: [%25], sizes: [64], strides: [1] {ssbuffer.block_id = 8 : i32} : memref to memref<64xf16, strided<[1], offset: ?>> + %subview_9 = memref.subview %reinterpret_cast_8[0] [%19] [1] {ssbuffer.block_id = 8 : i32} : memref<64xf16, strided<[1], offset: ?>> to memref> + %subview_10 = memref.subview %alloc_6[0] [%19] [1] {ssbuffer.block_id = 8 : i32} : memref<64xf16> to memref> + memref.copy %subview_9, %subview_10 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %26 = bufferization.to_tensor %alloc_6 restrict writable {ssbuffer.block_id = 8 : i32} : memref<64xf16> to tensor<64xf16> + %expanded = tensor.expand_shape %23 [[0, 1]] output_shape [32, 1] {ssbuffer.block_id = 8 : i32} : tensor<32xf16> into tensor<32x1xf16> + %27 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<32x16xf16> + %28 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%27 : tensor<32x16xf16>) -> tensor<32x16xf16> + %inserted_slice = tensor.insert_slice %expanded into %28[0, 0] [32, 1] [1, 1] {ssbuffer.block_id = 8 : i32} : tensor<32x1xf16> into tensor<32x16xf16> + %expanded_11 = tensor.expand_shape %26 [[0, 1]] output_shape [1, 64] {ssbuffer.block_id = 8 : i32} : tensor<64xf16> into tensor<1x64xf16> + %29 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<16x64xf16> + %30 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%29 : tensor<16x64xf16>) -> tensor<16x64xf16> + %inserted_slice_12 = tensor.insert_slice %expanded_11 into %30[0, 0] [1, 64] [1, 1] {ssbuffer.block_id = 8 : i32} : tensor<1x64xf16> into tensor<16x64xf16> + %reshape = tensor.reshape %inserted_slice(%cst_3) {ssbuffer.block_id = 8 : i32} : (tensor<32x16xf16>, tensor<3xi64>) -> tensor<32x1x16xf16> + %31 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<1x32x16xf16> + %transposed = linalg.transpose ins(%reshape : tensor<32x1x16xf16>) outs(%31 : tensor<1x32x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 8 : i32} + %reshape_13 = tensor.reshape %transposed(%cst_2) {ssbuffer.block_id = 8 : i32} : (tensor<1x32x16xf16>, tensor<4xi64>) -> tensor<1x2x16x16xf16> + %reshape_14 = tensor.reshape %inserted_slice_12(%cst_1) {ssbuffer.block_id = 8 : i32} : (tensor<16x64xf16>, tensor<3xi64>) -> tensor<16x4x16xf16> + %32 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<4x16x16xf16> + %transposed_15 = linalg.transpose ins(%reshape_14 : tensor<16x4x16xf16>) outs(%32 : tensor<4x16x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 8 : i32} + %reshape_16 = tensor.reshape %transposed_15(%cst_0) {ssbuffer.block_id = 8 : i32} : (tensor<4x16x16xf16>, tensor<4xi64>) -> tensor<4x1x16x16xf16> + hivm.hir.copy ins(%reshape_13 : tensor<1x2x16x16xf16>) outs(%alloc : memref<1x2x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.copy ins(%reshape_16 : tensor<4x1x16x16xf16>) outs(%alloc_4 : memref<4x1x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + scf.yield %12 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-counter.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-counter.mlir new file mode 100644 index 0000000000..4be4179329 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-counter.mlir @@ -0,0 +1,94 @@ +// RUN: triton-opt --add-control-flow-condition %s | FileCheck %s + +// UpdateConditionInfo on scf.while: clone scf.condition(x) def-chain +// (extsi + cmpi) into each ssbuffer.if via whileBlockArgMap. + +// CHECK-LABEL: func.func @while_update_condition_counter + +// While-do remains; before-region still has extsi/cmpi i64 predicates. +// CHECK: scf.while +// CHECK: arith.extsi %{{.*}} : i32 to i64 +// CHECK: arith.cmpi slt, %{{.*}} : i64 +// CHECK: scf.condition + +// Vector ssbuffer.if = 5: cross-core cond and-ed with cloned while counter. +// CHECK: %[[EXT5:.*]] = arith.extsi %{{.*}} : i32 to i64 +// CHECK: %[[CMP5:.*]] = arith.cmpi slt, %[[EXT5]], %{{.*}} : i64 +// CHECK: %[[AND5:.*]] = arith.andi %{{.*}}, %[[CMP5]] +// CHECK: scf.if %[[AND5]] +// CHECK: } {{.*}}ssbuffer.if = 5 + +// Vector ssbuffer.if = 6: same cloned counter shape. +// CHECK: %[[EXT6:.*]] = arith.extsi %{{.*}} : i32 to i64 +// CHECK: %[[CMP6:.*]] = arith.cmpi slt, %[[EXT6]], %{{.*}} : i64 +// CHECK: %[[AND6:.*]] = arith.andi %{{.*}}, %[[CMP6]] +// CHECK: scf.if %[[AND6]] +// CHECK: } {{.*}}ssbuffer.if = 6 + +// CHECK: scf.yield + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @while_update_condition_counter(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant 0 : i32 + %c16_i32 = arith.constant 16 : i32 + %bound = arith.extsi %arg7 : i32 to i64 + + scope.scope : () -> () { + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_ub0 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub0 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_ub1 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub1 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + + %0 = scf.while (%arg14 = %c0_i32) : (i32) -> i32 { + %1 = arith.extsi %arg14 {Undefined, ssbuffer.block_id = 4 : i32} : i32 to i64 + %2 = arith.cmpi slt, %1, %bound {Undefined, ssbuffer.block_id = 4 : i32} : i64 + scf.condition(%2) %arg14 : i32 + } do { + ^bb0(%arg14: i32): + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %memspacecast = memref.memory_space_cast %alloc_ub0 {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 0 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %empty = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x8x16x16xf16> + hivm.hir.copy ins(%empty : tensor<8x8x16x16xf16>) outs(%alloc : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 1 : i32]} + %next5 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 5 : i32} : i32 + + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %memspacecast_1 = memref.memory_space_cast %alloc_ub1 {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossCoreDeps = [1 : i32, 0 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %next6 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 6 : i32} : i32 + scf.yield %next6 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + + scope.scope : () -> () { + %alloc_c = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_c {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_ub2 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub2 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_ub3 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub3 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + + %0 = scf.while (%arg14 = %c0_i32) : (i32) -> i32 { + %1 = arith.extsi %arg14 {Undefined, ssbuffer.block_id = 4 : i32} : i32 to i64 + %2 = arith.cmpi slt, %1, %bound {Undefined, ssbuffer.block_id = 4 : i32} : i64 + scf.condition(%2) %arg14 : i32 + } do { + ^bb0(%arg14: i32): + hivm.hir.sync_block_wait {ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %t0 = tensor.empty() {ssbuffer.block_id = 0 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 1 : i32]} ins(%t0 : tensor<128x128xf32>) outs(%alloc_ub2 : memref<128x128xf32, #hivm.address_space>) + %next0 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 0 : i32} : i32 + + hivm.hir.sync_block_wait {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %conv = hivm.hir.convert_layout %alloc_c output_shape [128, 128] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 0 : i32]} : (memref<8x8x16x16xf16, #hivm.address_space>) -> memref<128x128xf16, #hivm.address_space> + %t1 = tensor.empty() {ssbuffer.block_id = 1 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossCoreDeps = [1 : i32, 1 : i32]} ins(%t1 : tensor<128x128xf32>) outs(%alloc_ub3 : memref<128x128xf32, #hivm.address_space>) + %next1 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 1 : i32} : i32 + scf.yield %next1 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-crosscore.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-crosscore.mlir new file mode 100644 index 0000000000..106f4a17d3 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-crosscore.mlir @@ -0,0 +1,112 @@ +// RUN: triton-opt --add-control-flow-condition %s | FileCheck %s + +// UpdateConditionInfo on scf.while: cross-core (核间) buffer conditions. +// Same dual-scope crossCoreDeps skeleton as while-update-condition-counter; +// CHECKs focus on ssbuffer memref init, memref.load/cmpi before ssbuffer.if, +// and load/addi|subi/store updates inside then (post-#1359 MLIR ssbuffer). + +// CHECK-LABEL: func.func @while_update_condition_crosscore + +// SSBuffer slots initialized to 0 via pointer_cast + memref.store. +// CHECK: hivm.hir.pointer_cast(%{{.*}}) : memref> +// CHECK: memref.store %{{.*}}, %{{.*}}[] : memref> +// CHECK: memref.store %{{.*}}, %{{.*}}[] : memref> + +// CHECK: scf.while + +// Vector if=5: consumer sgt 0 + producer slt limit, then and-ed with while counter. +// CHECK: %[[LD_IN5:.*]] = memref.load %{{.*}}[] : memref> +// CHECK: annotation.mark %[[LD_IN5]] {memref_ext.volatile} +// CHECK: %[[SGT5:.*]] = arith.cmpi sgt, %[[LD_IN5]], %{{.*}} : i32 +// CHECK: %[[LD_OUT5:.*]] = memref.load %{{.*}}[] : memref> +// CHECK: annotation.mark %[[LD_OUT5]] {memref_ext.volatile} +// CHECK: %[[SLT5:.*]] = arith.cmpi slt, %[[LD_OUT5]], %{{.*}} : i32 +// CHECK: %[[CROSS5:.*]] = arith.andi %[[SGT5]], %[[SLT5]] +// CHECK: %[[AND5:.*]] = arith.andi %[[CROSS5]], %{{.*}} +// CHECK: scf.if %[[AND5]] +// CHECK: %[[LD_DEC5:.*]] = memref.load %{{.*}}[] : memref> +// CHECK: %[[SUB5:.*]] = arith.subi %[[LD_DEC5]], %{{.*}} : i32 +// CHECK: memref.store %[[SUB5]], %{{.*}}[] : memref> +// CHECK: %[[LD_INC5:.*]] = memref.load %{{.*}}[] : memref> +// CHECK: %[[ADD5:.*]] = arith.addi %[[LD_INC5]], %{{.*}} : i32 +// CHECK: memref.store %[[ADD5]], %{{.*}}[] : memref> +// CHECK: } {{.*}}ssbuffer.if = 5 + +// Vector if=6: consumer-only sgt 0, then and-ed with while counter. +// CHECK: %[[LD_IN6:.*]] = memref.load %{{.*}}[] : memref> +// CHECK: annotation.mark %[[LD_IN6]] {memref_ext.volatile} +// CHECK: %[[SGT6:.*]] = arith.cmpi sgt, %[[LD_IN6]], %{{.*}} : i32 +// CHECK: %[[AND6:.*]] = arith.andi %[[SGT6]], %{{.*}} +// CHECK: scf.if %[[AND6]] +// CHECK: %[[LD_DEC6:.*]] = memref.load %{{.*}}[] : memref> +// CHECK: %[[SUB6:.*]] = arith.subi %[[LD_DEC6]], %{{.*}} : i32 +// CHECK: memref.store %[[SUB6]], %{{.*}}[] : memref> +// CHECK: } {{.*}}ssbuffer.if = 6 + +// CHECK: scf.yield + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @while_update_condition_crosscore(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant 0 : i32 + %c16_i32 = arith.constant 16 : i32 + %bound = arith.extsi %arg7 : i32 to i64 + + scope.scope : () -> () { + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_ub0 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub0 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_ub1 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub1 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + + %0 = scf.while (%arg14 = %c0_i32) : (i32) -> i32 { + %1 = arith.extsi %arg14 {Undefined, ssbuffer.block_id = 4 : i32} : i32 to i64 + %2 = arith.cmpi slt, %1, %bound {Undefined, ssbuffer.block_id = 4 : i32} : i64 + scf.condition(%2) %arg14 : i32 + } do { + ^bb0(%arg14: i32): + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %memspacecast = memref.memory_space_cast %alloc_ub0 {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 0 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %empty = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x8x16x16xf16> + hivm.hir.copy ins(%empty : tensor<8x8x16x16xf16>) outs(%alloc : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 1 : i32]} + %next5 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 5 : i32} : i32 + + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %memspacecast_1 = memref.memory_space_cast %alloc_ub1 {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossCoreDeps = [1 : i32, 0 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %next6 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 6 : i32} : i32 + scf.yield %next6 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + + scope.scope : () -> () { + %alloc_c = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_c {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_ub2 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub2 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + %alloc_ub3 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub3 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x128xf32, #hivm.address_space> + + %0 = scf.while (%arg14 = %c0_i32) : (i32) -> i32 { + %1 = arith.extsi %arg14 {Undefined, ssbuffer.block_id = 4 : i32} : i32 to i64 + %2 = arith.cmpi slt, %1, %bound {Undefined, ssbuffer.block_id = 4 : i32} : i64 + scf.condition(%2) %arg14 : i32 + } do { + ^bb0(%arg14: i32): + hivm.hir.sync_block_wait {ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %t0 = tensor.empty() {ssbuffer.block_id = 0 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 1 : i32]} ins(%t0 : tensor<128x128xf32>) outs(%alloc_ub2 : memref<128x128xf32, #hivm.address_space>) + %next0 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 0 : i32} : i32 + + hivm.hir.sync_block_wait {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %conv = hivm.hir.convert_layout %alloc_c output_shape [128, 128] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [2 : i32, 0 : i32]} : (memref<8x8x16x16xf16, #hivm.address_space>) -> memref<128x128xf16, #hivm.address_space> + %t1 = tensor.empty() {ssbuffer.block_id = 1 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 2 : i32, ssbuffer.crossCoreDeps = [1 : i32, 1 : i32]} ins(%t1 : tensor<128x128xf32>) outs(%alloc_ub3 : memref<128x128xf32, #hivm.address_space>) + %next1 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 1 : i32} : i32 + scf.yield %next1 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-intracore.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-intracore.mlir new file mode 100644 index 0000000000..61d2ac9469 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-intracore.mlir @@ -0,0 +1,90 @@ +// RUN: triton-opt --add-control-flow-condition %s | FileCheck %s + +// UpdateConditionInfo on scf.while: intra-core (核内) conditions. +// VECTOR while has producer (intraDeps role=1) then consumer (role=0). +// Actual shape (with optional cross gate on the same if): +// producer: cmpi slt control_var, 1 ; then addi control_var, 1 +// consumer: cmpi sgt latest, 0 ; then subi latest, 1 + +// CHECK-LABEL: func.func @while_update_condition_intracore + +// CHECK: scf.while + +// Producer if=5: skip cross memref.load/slt, then intra control_var < 1. +// CHECK: memref.load %{{.*}}[] : memref> +// CHECK: arith.cmpi slt, %{{.*}}, %{{.*}} : i32 +// CHECK: %[[LIM:.*]] = arith.constant 1 : i32 +// CHECK: %[[SLT:.*]] = arith.cmpi slt, %{{.*}}, %[[LIM]] : i32 +// CHECK: %[[AND_P0:.*]] = arith.andi %{{.*}}, %[[SLT]] +// CHECK: %[[COND5:.*]] = arith.andi %[[AND_P0]], %{{.*}} +// CHECK: scf.if %[[COND5]] +// CHECK: %[[ONE_P:.*]] = arith.constant 1 : i32 +// CHECK: %[[INC:.*]] = arith.addi %{{.*}}, %[[ONE_P]] : i32 +// CHECK: scf.yield %{{.*}}, %[[INC]] +// CHECK: } {{.*}}ssbuffer.if = 5 + +// Consumer if=7: uses producer if result (latest), sgt 0 then -1. +// CHECK: %[[SGT:.*]] = arith.cmpi sgt, %{{.*}}, %{{.*}} : i32 +// CHECK: %[[COND7:.*]] = arith.andi %[[SGT]], %{{.*}} +// CHECK: scf.if %[[COND7]] +// CHECK: %[[ONE_C:.*]] = arith.constant 1 : i32 +// CHECK: %[[DEC:.*]] = arith.subi %{{.*}}, %[[ONE_C]] : i32 +// CHECK: } {{.*}}ssbuffer.if = 7 + +// CHECK: scf.yield + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, ssbuffer.intra_buf_count = 2 : i32} { + func.func @while_update_condition_intracore(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant 0 : i32 + %c16_i32 = arith.constant 16 : i32 + %bound = arith.extsi %arg7 : i32 to i64 + + scope.scope : () -> () { + %alloc_ub0 = memref.alloc() : memref<128xf32, #hivm.address_space> + %mem0 = memref.memory_space_cast %alloc_ub0 : memref<128xf32, #hivm.address_space> to memref<128xf32> + %alloc_ub1 = memref.alloc() : memref<128xf32, #hivm.address_space> + %mem1 = memref.memory_space_cast %alloc_ub1 : memref<128xf32, #hivm.address_space> to memref<128xf32> + %alloc_cbuf = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<8x4x16x16xf16, #hivm.address_space> + annotation.mark %alloc_cbuf {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<8x4x16x16xf16, #hivm.address_space> + + %0 = scf.while (%arg14 = %c0_i32) : (i32) -> i32 { + %1 = arith.extsi %arg14 {Undefined, ssbuffer.block_id = 4 : i32} : i32 to i64 + %2 = arith.cmpi slt, %1, %bound {Undefined, ssbuffer.block_id = 4 : i32} : i64 + scf.condition(%2) %arg14 : i32 + } do { + ^bb0(%arg14: i32): + // Producer block (intraDeps role = 1). + %t_prod = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + hivm.hir.copy ins(%t_prod : tensor<128xf32>) outs(%mem0 : memref<128xf32>) {ssbuffer.block_id = 5 : i32, ssbuffer.intraDeps = [0 : i32, 1 : i32]} + %cbuf_t = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<8x4x16x16xf16> + hivm.hir.copy ins(%cbuf_t : tensor<8x4x16x16xf16>) outs(%alloc_cbuf : memref<8x4x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [1 : i32, 1 : i32]} + %next5 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 5 : i32} : i32 + + // Consumer block (intraDeps role = 0). + %t_cons = bufferization.to_tensor %mem0 restrict writable {ssbuffer.block_id = 7 : i32, ssbuffer.intraDeps = [0 : i32, 0 : i32]} : memref<128xf32> to tensor<128xf32> + %next7 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 7 : i32} : i32 + scf.yield %next7 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + + scope.scope : () -> () { + %alloc_cbuf = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<8x4x16x16xf16, #hivm.address_space> + annotation.mark %alloc_cbuf {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<8x4x16x16xf16, #hivm.address_space> + + %0 = scf.while (%arg14 = %c0_i32) : (i32) -> i32 { + %1 = arith.extsi %arg14 {Undefined, ssbuffer.block_id = 4 : i32} : i32 to i64 + %2 = arith.cmpi slt, %1, %bound {Undefined, ssbuffer.block_id = 4 : i32} : i64 + scf.condition(%2) %arg14 : i32 + } do { + ^bb0(%arg14: i32): + hivm.hir.sync_block_wait {ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %conv = hivm.hir.convert_layout %alloc_cbuf output_shape [64, 128] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 2 : i32, ssbuffer.crossCoreDeps = [1 : i32, 0 : i32], ssbuffer.transfer_id = 1 : i32} : (memref<8x4x16x16xf16, #hivm.address_space>) -> memref<64x128xf16, #hivm.address_space> + %next2 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 2 : i32} : i32 + scf.yield %next2 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-tensor-iterarg.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-tensor-iterarg.mlir new file mode 100644 index 0000000000..348e230dea --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-condition-tensor-iterarg.mlir @@ -0,0 +1,106 @@ +// RUN: triton-opt --add-control-flow-condition %s | FileCheck %s + +// UpdateConditionInfo on scf.while: tensor-typed iter_arg control vars. +// while carries (tensor, iv); block 5 consumes the tensor, block 6 produces a +// new tensor for yield. UpdateLoopOps appends i32=1 control args; conditions: +// consumer: control_var == 1, then -1 +// producer: control_var == 0, then +1 +// (same semantics as for-path iter_args_deps_add_conditions) + +// CHECK-LABEL: func.func @while_update_condition_tensor_iterarg + +// Tensor-iter control arg is seeded with 1 and appears on the while. +// CHECK: %[[TINIT:.*]] = arith.constant 1 : i32 +// CHECK: scf.while +// CHECK-SAME: %[[TINIT]] + +// Consumer if=5: after cross sgt, eq 1 is and-ed into cond; then -1 inside if. +// IR order: c1 (cross) ; load ; sgt ; c1 (tensor) ; eq ; andi ; andi ; scf.if +// Bind ONE only after sgt so it is the tensor eq constant, not the cross-core one. +// CHECK: arith.cmpi sgt +// CHECK: %[[ONE:.*]] = arith.constant 1 : i32 +// CHECK: arith.cmpi eq, %{{.*}}, %[[ONE]] : i32 +// CHECK: arith.andi +// CHECK: %[[COND5:.*]] = arith.andi +// CHECK: scf.if %[[COND5]] +// CHECK: arith.subi +// CHECK: } {{.*}}ssbuffer.if = 5 + +// Producer if=6: after cross slt, eq 0 is and-ed into cond; then +1 inside if. +// CHECK: arith.cmpi slt +// CHECK: %[[ZERO:.*]] = arith.constant 0 : i32 +// CHECK: arith.cmpi eq, %{{.*}}, %[[ZERO]] : i32 +// CHECK: arith.andi +// CHECK: %[[COND6:.*]] = arith.andi +// CHECK: scf.if %[[COND6]] +// CHECK: arith.addi +// CHECK: } {{.*}}ssbuffer.if = 6 + +// CHECK: scf.yield + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @while_update_condition_tensor_iterarg(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant 0 : i32 + %c16_i32 = arith.constant 16 : i32 + %cst = arith.constant 0.000000e+00 : f32 + %bound = arith.extsi %arg7 : i32 to i64 + + scope.scope : () -> () { + %empty0 = tensor.empty() : tensor<16x16xf32> + %init_t = linalg.fill ins(%cst : f32) outs(%empty0 : tensor<16x16xf32>) -> tensor<16x16xf32> + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_ub = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + + %0:2 = scf.while (%arg14 = %init_t, %arg15 = %c0_i32) : (tensor<16x16xf32>, i32) -> (tensor<16x16xf32>, i32) { + %1 = arith.extsi %arg15 {Undefined, ssbuffer.block_id = 4 : i32} : i32 to i64 + %2 = arith.cmpi slt, %1, %bound {Undefined, ssbuffer.block_id = 4 : i32} : i64 + scf.condition(%2) %arg14, %arg15 : tensor<16x16xf32>, i32 + } do { + ^bb0(%arg14: tensor<16x16xf32>, %arg15: i32): + // Consumer of tensor iter_arg (non-yield use) + cross-core input. + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %sum = arith.addf %arg14, %arg14 {ssbuffer.block_id = 5 : i32} : tensor<16x16xf32> + %memspacecast = memref.memory_space_cast %alloc_ub {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 0 : i32]} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + + // Producer of tensor iter_arg (loop yield comes from this block) + cross-core output. + // CreateIfOps else-yields %arg14 → analyzeTensorIterArgDependencies marks producer. + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %empty1 = tensor.empty() {ssbuffer.block_id = 6 : i32} : tensor<16x16xf32> + %new_t = linalg.fill {ssbuffer.block_id = 6 : i32} ins(%cst : f32) outs(%empty1 : tensor<16x16xf32>) -> tensor<16x16xf32> + %cbuf_t = tensor.empty() {ssbuffer.block_id = 6 : i32} : tensor<8x8x16x16xf16> + hivm.hir.copy ins(%cbuf_t : tensor<8x8x16x16xf16>) outs(%alloc : memref<8x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [0 : i32, 1 : i32]} + %next6 = arith.addi %arg15, %c16_i32 {ssbuffer.block_id = 6 : i32} : i32 + scf.yield %new_t, %next6 : tensor<16x16xf32>, i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + + scope.scope : () -> () { + %alloc_c = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_c {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 0 : i32} : memref<8x8x16x16xf16, #hivm.address_space> + %alloc_ub = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_ub {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 9 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x128xf32, #hivm.address_space> + + %0 = scf.while (%arg14 = %c0_i32) : (i32) -> i32 { + %1 = arith.extsi %arg14 {Undefined, ssbuffer.block_id = 4 : i32} : i32 to i64 + %2 = arith.cmpi slt, %1, %bound {Undefined, ssbuffer.block_id = 4 : i32} : i64 + scf.condition(%2) %arg14 : i32 + } do { + ^bb0(%arg14: i32): + hivm.hir.sync_block_wait {ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %t0 = tensor.empty() {ssbuffer.block_id = 0 : i32} : tensor<128x128xf32> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 0 : i32, ssbuffer.transfer_id = 1 : i32, ssbuffer.crossCoreDeps = [0 : i32, 1 : i32]} ins(%t0 : tensor<128x128xf32>) outs(%alloc_ub : memref<128x128xf32, #hivm.address_space>) + %next0 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 0 : i32} : i32 + + hivm.hir.sync_block_wait {ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %conv = hivm.hir.convert_layout %alloc_c output_shape [128, 128] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 1 : i32, ssbuffer.transfer_id = 0 : i32, ssbuffer.crossCoreDeps = [0 : i32, 0 : i32]} : (memref<8x8x16x16xf16, #hivm.address_space>) -> memref<128x128xf16, #hivm.address_space> + %next1 = arith.addi %arg14, %c16_i32 {ssbuffer.block_id = 1 : i32} : i32 + scf.yield %next1 : i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-loop-ops.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-loop-ops.mlir new file mode 100644 index 0000000000..5978208e7b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddControlFlowCondition/while-update-loop-ops.mlir @@ -0,0 +1,146 @@ +// RUN: triton-opt --update-loop-ops %s --allow-unregistered-dialect | FileCheck %s + +// Test UpdateLoopOps on an scf.while main loop after CreateIfOps. The loop's do +// region already holds the per-block scf.if wrappers with their while_arg +// update chains. The pass emits a prologue sync_block_set, grows the loop from +// 5 to 8 iter_args (adding 3 pipeline sync args), and brackets the body with a +// sync_block_wait / sync_block_set pair while preserving the guarded ifs. + +// CHECK: func.func @pcb12_tc01_while_matmul_fill +// A prologue sync_block_set is emitted before the loop. +// CHECK: hivm.hir.sync_block_set[, , ] flag = 15 +// The while op grows to 8 iter_args (5 from ProcessArgs + 3 sync args). +// CHECK: %{{.*}}:8 = scf.while (%arg17 = %c0_i32, %arg18 = %c0_i32, %arg19 = %c0_i32, %arg20 = %c0_i32, %arg21 = %c0_i32, %arg22 = %c0_i32_5, %arg23 = %c0_i32_6, %arg24 = %c0_i32_7) : (i32, i32, i32, i32, i32, i32, i32, i32) -> (i32, i32, i32, i32, i32, i32, i32, i32) +// CHECK: scf.condition(%{{.*}}) %arg17, %arg18, %arg19, %arg20, %arg21, %arg22, %arg23, %arg24 +// CHECK: ^bb0(%arg17: i32, %arg18: i32, %arg19: i32, %arg20: i32, %arg21: i32, %arg22: i32, %arg23: i32, %arg24: i32): +// The body opens with a sync_block_wait matching the prologue set. +// CHECK: hivm.hir.sync_block_wait[, , ] flag = 15 +// Per-block while_arg update chains survive inside the guarded ifs. +// CHECK: arith.addi %{{.*}}, %arg19 {ssbuffer.block_id = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// CHECK: arith.addi %{{.*}}, %arg20 {ssbuffer.block_id = 7 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// CHECK: arith.addi %{{.*}}, %arg21 {ssbuffer.block_id = 8 : i32, ssbuffer.while_arg = 0 : i32} : i32 +// A trailing sync_block_set closes the body before the terminator. +// CHECK: hivm.hir.sync_block_set[, , ] flag = 15 +// CHECK: scf.yield %{{.*}}#0, %{{.*}}#0, %{{.*}}, %{{.*}}#1, %{{.*}}#1, %arg22, %arg23, %arg24 : i32, i32, i32, i32, i32, i32, i32, i32 + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @pcb12_tc01_while_matmul_fill(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32 {tt.divisibility = 16 : i32}, %arg8: i32 {tt.divisibility = 16 : i32}, %arg9: i32 {tt.divisibility = 16 : i32}, %arg10: i32 {tt.divisibility = 16 : i32}, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 11 : i32} 0.000000e+00 : f16 + %c1_i32 = arith.constant {ssbuffer.block_id = 10 : i32} 1 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 10 : i32} 0 : i32 + %c0 = arith.constant {ssbuffer.block_id = 10 : i32} 0 : index + %c32 = arith.constant {ssbuffer.block_id = 10 : i32} 32 : index + %c64 = arith.constant {ssbuffer.block_id = 10 : i32} 64 : index + %cst_0 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[4, 1, 16, 16]> : tensor<4xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[16, 4, 16]> : tensor<3xi64> + %cst_2 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[1, 2, 16, 16]> : tensor<4xi64> + %cst_3 = arith.constant {ssbuffer.block_id = 8 : i32} dense<[32, 1, 16]> : tensor<3xi64> + scope.scope : () -> () { + %alloc = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} : memref<1x2x16x16xf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} : memref<1x2x16x16xf16, #hivm.address_space> + %alloc_4 = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x1x16x16xf16, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x1x16x16xf16, #hivm.address_space> + %0:5 = scf.while (%arg17 = %c0_i32, %arg18 = %c0_i32, %arg19 = %c0_i32, %arg20 = %c0_i32, %arg21 = %c0_i32) : (i32, i32, i32, i32, i32) -> (i32, i32, i32, i32, i32) { + %1 = arith.cmpi slt, %arg17, %arg7 {Undefined, ssbuffer.block_id = 4 : i32} : i32 + scf.condition(%1) %arg17, %arg18, %arg19, %arg20, %arg21 : i32, i32, i32, i32, i32 + } do { + ^bb0(%arg17: i32, %arg18: i32, %arg19: i32, %arg20: i32, %arg21: i32): + %true = arith.constant true + %1 = scf.if %true -> (i32) { + %4 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 9 : i32} : i32 + %5 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 9 : i32, ssbuffer.clone = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %6 = arith.muli %5, %c1_i32 {ssbuffer.block_id = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %7 = arith.addi %6, %arg19 {ssbuffer.block_id = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + scf.yield %7 : i32 + } else { + scf.yield %arg19 : i32 + } {hivm.matmul_limited_in_cube, ssbuffer.if = 9 : i32} + %true_5 = arith.constant true + %2:2 = scf.if %true_5 -> (i32, i32) { + %4 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.clone = 9 : i32} : i32 + %5 = arith.index_cast %arg7 {ssbuffer.block_id = 7 : i32} : i32 to index + %6 = arith.maxsi %5, %c0 {ssbuffer.block_id = 7 : i32} : index + %7 = arith.minsi %6, %c32 {ssbuffer.block_id = 7 : i32} : index + %8 = arith.cmpi slt, %7, %c32 {ssbuffer.block_id = 7 : i32} : index + %9 = arith.index_cast %arg6 {ssbuffer.block_id = 7 : i32} : i32 to index + %10 = arith.maxsi %9, %c0 {ssbuffer.block_id = 7 : i32} : index + %11 = arith.minsi %10, %c64 {ssbuffer.block_id = 7 : i32} : index + %12 = arith.cmpi slt, %11, %c64 {ssbuffer.block_id = 7 : i32} : index + %13 = arith.muli %4, %c1_i32 {ssbuffer.block_id = 7 : i32} : i32 + %14 = arith.addi %13, %arg17 {ssbuffer.block_id = 7 : i32} : i32 + %15 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.clone = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %16 = arith.muli %15, %c1_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %17 = arith.addi %16, %arg20 {ssbuffer.block_id = 7 : i32, ssbuffer.while_arg = 0 : i32} : i32 + scf.yield %14, %17 : i32, i32 + } else { + scf.yield %arg17, %arg20 : i32, i32 + } {hivm.matmul_limited_in_cube, ssbuffer.if = 7 : i32} + %true_6 = arith.constant true + %3:2 = scf.if %true_6 -> (i32, i32) { + %4 = arith.index_cast %arg7 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : i32 to index + %5 = arith.maxsi %4, %c0 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %6 = arith.minsi %5, %c32 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %7 = arith.cmpi slt, %6, %c32 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %8 = arith.index_cast %arg6 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : i32 to index + %9 = arith.maxsi %8, %c0 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %10 = arith.minsi %9, %c64 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %11 = arith.cmpi slt, %10, %c64 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 7 : i32} : index + %alloc_7 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32xf16> + %alloc_8 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<64xf16> + scf.if %11 { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%alloc_8 : memref<64xf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + scf.if %7 { + linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%alloc_7 : memref<32xf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + %12 = arith.muli %arg18, %arg8 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 + %13 = arith.index_cast %12 {ssbuffer.block_id = 8 : i32} : i32 to index + %reinterpret_cast = memref.reinterpret_cast %arg2 to offset: [%13], sizes: [32], strides: [1] {ssbuffer.block_id = 8 : i32} : memref to memref<32xf16, strided<[1], offset: ?>> + %subview = memref.subview %reinterpret_cast[0] [%6] [1] {ssbuffer.block_id = 8 : i32} : memref<32xf16, strided<[1], offset: ?>> to memref> + %subview_9 = memref.subview %alloc_7[0] [%6] [1] {ssbuffer.block_id = 8 : i32} : memref<32xf16> to memref> + memref.copy %subview, %subview_9 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %14 = bufferization.to_tensor %alloc_7 restrict writable {ssbuffer.block_id = 8 : i32} : memref<32xf16> to tensor<32xf16> + %15 = arith.muli %arg18, %arg9 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 + %16 = arith.index_cast %15 {ssbuffer.block_id = 8 : i32} : i32 to index + %reinterpret_cast_10 = memref.reinterpret_cast %arg3 to offset: [%16], sizes: [64], strides: [1] {ssbuffer.block_id = 8 : i32} : memref to memref<64xf16, strided<[1], offset: ?>> + %subview_11 = memref.subview %reinterpret_cast_10[0] [%10] [1] {ssbuffer.block_id = 8 : i32} : memref<64xf16, strided<[1], offset: ?>> to memref> + %subview_12 = memref.subview %alloc_8[0] [%10] [1] {ssbuffer.block_id = 8 : i32} : memref<64xf16> to memref> + memref.copy %subview_11, %subview_12 {ssbuffer.block_id = 8 : i32} : memref> to memref> + %17 = bufferization.to_tensor %alloc_8 restrict writable {ssbuffer.block_id = 8 : i32} : memref<64xf16> to tensor<64xf16> + %expanded = tensor.expand_shape %14 [[0, 1]] output_shape [32, 1] {ssbuffer.block_id = 8 : i32} : tensor<32xf16> into tensor<32x1xf16> + %18 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<32x16xf16> + %19 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%18 : tensor<32x16xf16>) -> tensor<32x16xf16> + %inserted_slice = tensor.insert_slice %expanded into %19[0, 0] [32, 1] [1, 1] {ssbuffer.block_id = 8 : i32} : tensor<32x1xf16> into tensor<32x16xf16> + %expanded_13 = tensor.expand_shape %17 [[0, 1]] output_shape [1, 64] {ssbuffer.block_id = 8 : i32} : tensor<64xf16> into tensor<1x64xf16> + %20 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<16x64xf16> + %21 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst : f16) outs(%20 : tensor<16x64xf16>) -> tensor<16x64xf16> + %inserted_slice_14 = tensor.insert_slice %expanded_13 into %21[0, 0] [1, 64] [1, 1] {ssbuffer.block_id = 8 : i32} : tensor<1x64xf16> into tensor<16x64xf16> + %reshape = tensor.reshape %inserted_slice(%cst_3) {ssbuffer.block_id = 8 : i32} : (tensor<32x16xf16>, tensor<3xi64>) -> tensor<32x1x16xf16> + %22 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<1x32x16xf16> + %transposed = linalg.transpose ins(%reshape : tensor<32x1x16xf16>) outs(%22 : tensor<1x32x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 8 : i32} + %reshape_15 = tensor.reshape %transposed(%cst_2) {ssbuffer.block_id = 8 : i32} : (tensor<1x32x16xf16>, tensor<4xi64>) -> tensor<1x2x16x16xf16> + %reshape_16 = tensor.reshape %inserted_slice_14(%cst_1) {ssbuffer.block_id = 8 : i32} : (tensor<16x64xf16>, tensor<3xi64>) -> tensor<16x4x16xf16> + %23 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<4x16x16xf16> + %transposed_17 = linalg.transpose ins(%reshape_16 : tensor<16x4x16xf16>) outs(%23 : tensor<4x16x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 8 : i32} + %reshape_18 = tensor.reshape %transposed_17(%cst_0) {ssbuffer.block_id = 8 : i32} : (tensor<4x16x16xf16>, tensor<4xi64>) -> tensor<4x1x16x16xf16> + hivm.hir.copy ins(%reshape_15 : tensor<1x2x16x16xf16>) outs(%alloc : memref<1x2x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.copy ins(%reshape_18 : tensor<4x1x16x16xf16>) outs(%alloc_4 : memref<4x1x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32} + %24 = arith.addi %c1_i32, %c1_i32 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32, ssbuffer.clone = 9 : i32} : i32 + %25 = arith.muli %24, %c1_i32 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 + %26 = arith.addi %25, %arg18 {ssbuffer.arg = 0 : i32, ssbuffer.block_id = 8 : i32} : i32 + %27 = arith.addi %c1_i32, %c1_i32 {ssbuffer.block_id = 8 : i32, ssbuffer.clone = 9 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %28 = arith.muli %27, %c1_i32 {ssbuffer.block_id = 8 : i32, ssbuffer.while_arg = 0 : i32} : i32 + %29 = arith.addi %28, %arg21 {ssbuffer.block_id = 8 : i32, ssbuffer.while_arg = 0 : i32} : i32 + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + scf.yield %26, %29 : i32, i32 + } else { + scf.yield %arg18, %arg21 : i32, i32 + } {hivm.matmul_limited_in_cube, ssbuffer.if = 8 : i32} + scf.yield %2#0, %3#0, %1, %2#1, %3#1 : i32, i32, i32, i32, i32 + } attributes {Undefined, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/fallback_existing_scope.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/fallback_existing_scope.mlir new file mode 100644 index 0000000000..2e78d8dc3c --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/fallback_existing_scope.mlir @@ -0,0 +1,17 @@ +// RUN: triton-opt '--add_dynamic_cv_pipeline=compile-on-910-95=True' %s | FileCheck %s + +// CHECK-LABEL: module attributes {triton_ascend.dynamic_cv_pipeline.rc = 2 : i32} +module { + // CHECK-LABEL: func.func @fallback_existing_scope + // CHECK: arith.addi + // CHECK: scope.scope + // CHECK: scope.return + // CHECK: return + func.func @fallback_existing_scope(%arg0: i32, %arg1: i32) -> i32 { + %0 = arith.addi %arg0, %arg1 : i32 + scope.scope : () -> () { + scope.return + } + return %0 : i32 + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/fallback_no_matmul.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/fallback_no_matmul.mlir new file mode 100644 index 0000000000..40c9b7364a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/fallback_no_matmul.mlir @@ -0,0 +1,13 @@ +// RUN: triton-opt '--add_dynamic_cv_pipeline=compile-on-910-95=True' --debug-only=add-dynamic-cv-pipeline %s 2>&1 | FileCheck %s --implicit-check-not='note: see current operation:' + +// Test: Module with no linalg.matmul. PreCheckMatmul sets ERRCODE_IGNORED, +// AddDynamicCVPipeline falls back with a "Kernel not applicable..." warning +// that does NOT include the full module IR dump. + +// CHECK: warning: [add-dynamic-cv-pipeline] Kernel not applicable for dynamic CV pipeline +module { + func.func @fallback_no_matmul(%arg0: i32, %arg1: i32) -> i32 { + %0 = arith.addi %arg0, %arg1 : i32 + return %0 : i32 + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/pre_check_available.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/pre_check_available.mlir new file mode 100644 index 0000000000..bdf2518f3b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AddDynamicCVPipeline/pre_check_available.mlir @@ -0,0 +1,16 @@ +// RUN: triton-opt %s --pre-check-dynamic-cv-pipeline-available | FileCheck %s --check-prefix=PASS + +// PASS-LABEL: func.func @pre_check_available_passes( +// PASS: linalg.matmul +// PASS-NOT: triton_ascend.dynamic_cv_pipeline.rc +module { + func.func @pre_check_available_passes( + %arg0: memref<16x16xf16>, + %arg1: memref<16x16xf16>, + %arg2: memref<16x16xf32>) { + linalg.matmul + ins(%arg0, %arg1 : memref<16x16xf16>, memref<16x16xf16>) + outs(%arg2 : memref<16x16xf32>) + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-alloc-tensor-clone.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-alloc-tensor-clone.mlir new file mode 100644 index 0000000000..425c1ccf45 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-alloc-tensor-clone.mlir @@ -0,0 +1,85 @@ +// RUN: triton-opt --add_multi_buffer_inner_scope %s | FileCheck %s + +// T-alloc-tensor-clone: bufferization.alloc_tensor() (no payload) is cloned +// into each cross-block consumer's block instead of being multi-buffered. +// +// Case A (test_alloc_tensor_single_consumer): +// %alloc in block_id = 9 (producer block), consumed by linalg.fill in +// block_id = 10. After the pass: +// - Original %alloc stays at block_id = 9 (orphaned, DCE will clean it). +// - A cloned alloc_tensor appears at block_id = 10 (the consumer block). +// - linalg.fill's outs is rewired to the clone, NOT the original. +// - NO multi-buffer machinery (memref.alloc + hivm.copy + scf.if + +// to_tensor) is generated for this dep. +// +// Case B (test_alloc_tensor_multi_consumer): +// %alloc in block_id = 9, consumed by two ops in two different consumer +// blocks (block_id = 10 and block_id = 11). After the pass, the alloc is +// cloned ONCE PER consumer block — each consumer gets a fresh local alloc +// with the corresponding block_id tag. + +// CHECK-LABEL: func.func @test_alloc_tensor_single_consumer +// Original alloc stays in producer block (orphaned, will be DCE'd). +// CHECK: %[[ORIG:.*]] = bufferization.alloc_tensor() {ssbuffer.block_id = 9 : i32} : tensor +// Cloned alloc appears in consumer block, tagged with consumer's block_id. +// CHECK: %[[CLONE:.*]] = bufferization.alloc_tensor() {ssbuffer.block_id = 10 : i32} : tensor +// linalg.fill's outs is rewired to the clone, NOT the original. +// CHECK: linalg.fill {ssbuffer.block_id = 10 : i32} ins(%{{.+}} : f32) outs(%[[CLONE]] : tensor) -> tensor +// alloc_tensor must NOT go through the multi-buffer path: no UB memref.alloc +// or hivm.hir.copy chain for tensor in this scope. +// CHECK-NOT: memref.alloc() : memref> +// CHECK-NOT: hivm.hir.copy{{.*}}: tensor + +// CHECK-LABEL: func.func @test_alloc_tensor_multi_consumer +// Original alloc stays in producer block (orphaned). +// CHECK: %[[B_ORIG:.*]] = bufferization.alloc_tensor() {ssbuffer.block_id = 9 : i32} : tensor +// Clone for block_id = 10 consumer. +// CHECK: %[[B_CLONE_10:.*]] = bufferization.alloc_tensor() {ssbuffer.block_id = 10 : i32} : tensor +// block_id = 10 fill uses the block 10 clone, NOT the original. +// CHECK: linalg.fill {ssbuffer.block_id = 10 : i32} ins(%{{.+}} : f32) outs(%[[B_CLONE_10]] : tensor) -> tensor +// Clone for block_id = 11 consumer. +// CHECK: %[[B_CLONE_11:.*]] = bufferization.alloc_tensor() {ssbuffer.block_id = 11 : i32} : tensor +// block_id = 11 fill uses the block 11 clone, NOT the original or the block 10 +// clone. +// CHECK: linalg.fill {ssbuffer.block_id = 11 : i32} ins(%{{.+}} : f32) outs(%[[B_CLONE_11]] : tensor) -> tensor + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // Case A: single cross-block consumer + func.func @test_alloc_tensor_single_consumer() { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c100_i32 = arith.constant 100 : i32 + %cst = arith.constant 1.0 : f32 + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + // alloc_tensor in producer block (block_id = 9). + %alloc = bufferization.alloc_tensor() {ssbuffer.block_id = 9 : i32} : tensor + // Single cross-block consumer (block_id = 10). Uses %alloc as outs. + %fill = linalg.fill {ssbuffer.block_id = 10 : i32} ins(%cst : f32) outs(%alloc : tensor) -> tensor + %used = arith.addf %fill, %fill {ssbuffer.block_id = 10 : i32} : tensor + } {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } + + // Case B: two cross-block consumers in different blocks + func.func @test_alloc_tensor_multi_consumer() { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c100_i32 = arith.constant 100 : i32 + %cst = arith.constant 1.0 : f32 + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + %alloc = bufferization.alloc_tensor() {ssbuffer.block_id = 9 : i32} : tensor + // Two cross-block consumers, different block_ids: 10 and 11. + %fill_a = linalg.fill {ssbuffer.block_id = 10 : i32} ins(%cst : f32) outs(%alloc : tensor) -> tensor + %fill_b = linalg.fill {ssbuffer.block_id = 11 : i32} ins(%cst : f32) outs(%alloc : tensor) -> tensor + %used_a = arith.addf %fill_a, %fill_a {ssbuffer.block_id = 10 : i32} : tensor + %used_b = arith.addf %fill_b, %fill_b {ssbuffer.block_id = 11 : i32} : tensor + } {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-alloc-to-tensor-clone.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-alloc-to-tensor-clone.mlir new file mode 100644 index 0000000000..64db68eb9a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-alloc-to-tensor-clone.mlir @@ -0,0 +1,95 @@ +// RUN: triton-opt --add_multi_buffer_inner_scope %s | FileCheck %s + +// T-alloc-to-tensor-clone: memref.alloc + bufferization.to_tensor (no payload) +// is cloned into each cross-block consumer's block instead of being +// multi-buffered. Mirrors the alloc_tensor clone case but for the memref path. +// +// Case A (test_alloc_to_tensor_single_consumer): +// %alloc in block_id = 9 (producer block), wrapped by to_tensor. Consumed +// by linalg.fill in block_id = 10. After the pass: +// - Original alloc + to_tensor stay at block_id = 9 (orphaned, DCE will +// clean them). +// - A cloned alloc + cloned to_tensor appear at block_id = 10 (the +// consumer block), tagged with block_id = 10. +// - linalg.fill's outs is rewired to the cloned to_tensor, NOT the +// original. +// - NO multi-buffer machinery (memref.alloc + hivm.copy + scf.if + +// to_tensor) is generated for this dep. +// +// Case B (test_alloc_to_tensor_multi_consumer): +// Same producer at block_id = 9, two consumers in different blocks +// (block_id = 10 and block_id = 11). After the pass, the alloc + to_tensor +// pair is cloned ONCE PER consumer block — each consumer gets a fresh local +// pair with the corresponding block_id tag. + +// CHECK-LABEL: func.func @test_alloc_to_tensor_single_consumer +// Original alloc + to_tensor stay in producer block (orphaned). +// CHECK: %[[ORIG_ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 9 : i32} : memref +// CHECK: %[[ORIG_TT:.*]] = bufferization.to_tensor %[[ORIG_ALLOC]] restrict writable {ssbuffer.block_id = 9 : i32} : memref to tensor +// Cloned alloc + to_tensor appear in consumer block, tagged with block 10. +// CHECK: %[[CLONE_ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 10 : i32} : memref +// CHECK: %[[CLONE_TT:.*]] = bufferization.to_tensor %[[CLONE_ALLOC]] restrict writable {ssbuffer.block_id = 10 : i32} : memref to tensor +// linalg.fill's outs is rewired to the cloned to_tensor, NOT the original. +// CHECK: linalg.fill {ssbuffer.block_id = 10 : i32} ins(%{{.+}} : f32) outs(%[[CLONE_TT]] : tensor) -> tensor +// alloc + to_tensor must NOT go through the multi-buffer path: no UB +// memref.alloc or hivm.hir.copy chain for tensor in this scope. +// CHECK-NOT: memref.alloc() : memref> +// CHECK-NOT: hivm.hir.copy{{.*}}: tensor + +// CHECK-LABEL: func.func @test_alloc_to_tensor_multi_consumer +// Original alloc + to_tensor stay in producer block (orphaned). +// CHECK: %[[B_ORIG_ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 9 : i32} : memref +// CHECK: %[[B_ORIG_TT:.*]] = bufferization.to_tensor %[[B_ORIG_ALLOC]] restrict writable {ssbuffer.block_id = 9 : i32} : memref to tensor +// Clone for block_id = 10 consumer. +// CHECK: %[[B_CLONE_ALLOC_10:.*]] = memref.alloc() {ssbuffer.block_id = 10 : i32} : memref +// CHECK: %[[B_CLONE_TT_10:.*]] = bufferization.to_tensor %[[B_CLONE_ALLOC_10]] restrict writable {ssbuffer.block_id = 10 : i32} : memref to tensor +// block_id = 10 fill uses the block 10 clone, NOT the original. +// CHECK: linalg.fill {ssbuffer.block_id = 10 : i32} ins(%{{.+}} : f32) outs(%[[B_CLONE_TT_10]] : tensor) -> tensor +// Clone for block_id = 11 consumer. +// CHECK: %[[B_CLONE_ALLOC_11:.*]] = memref.alloc() {ssbuffer.block_id = 11 : i32} : memref +// CHECK: %[[B_CLONE_TT_11:.*]] = bufferization.to_tensor %[[B_CLONE_ALLOC_11]] restrict writable {ssbuffer.block_id = 11 : i32} : memref to tensor +// block_id = 11 fill uses the block 11 clone, NOT the original or block 10 clone. +// CHECK: linalg.fill {ssbuffer.block_id = 11 : i32} ins(%{{.+}} : f32) outs(%[[B_CLONE_TT_11]] : tensor) -> tensor + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // Case A: single cross-block consumer + func.func @test_alloc_to_tensor_single_consumer() { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c100_i32 = arith.constant 100 : i32 + %cst = arith.constant 1.0 : f32 + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + // Producer: memref.alloc + to_tensor in block_id = 9. + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32} : memref + %tt = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 9 : i32} : memref to tensor + // Single cross-block consumer (block_id = 10). Uses %tt as outs. + %fill = linalg.fill {ssbuffer.block_id = 10 : i32} ins(%cst : f32) outs(%tt : tensor) -> tensor + %used = arith.addf %fill, %fill {ssbuffer.block_id = 10 : i32} : tensor + } {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } + + // Case B: two cross-block consumers in different blocks + func.func @test_alloc_to_tensor_multi_consumer() { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c100_i32 = arith.constant 100 : i32 + %cst = arith.constant 1.0 : f32 + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32} : memref + %tt = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 9 : i32} : memref to tensor + // Two cross-block consumers, different block_ids: 10 and 11. + %fill_a = linalg.fill {ssbuffer.block_id = 10 : i32} ins(%cst : f32) outs(%tt : tensor) -> tensor + %fill_b = linalg.fill {ssbuffer.block_id = 11 : i32} ins(%cst : f32) outs(%tt : tensor) -> tensor + %used_a = arith.addf %fill_a, %fill_a {ssbuffer.block_id = 10 : i32} : tensor + %used_b = arith.addf %fill_b, %fill_b {ssbuffer.block_id = 11 : i32} : tensor + } {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-cloned-fill-scalar-dep.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-cloned-fill-scalar-dep.mlir new file mode 100644 index 0000000000..6431fea067 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-cloned-fill-scalar-dep.mlir @@ -0,0 +1,145 @@ +// RUN: triton-opt --add_multi_buffer_inner_scope %s | FileCheck %s + +// T-cloned-fill-scalar-dep: empty+fill is cloned into the consumer block. Three +// cases are checked: +// +// Case A (test_cloned_fill_scalar_dep_same_parent): +// `ins` is %extracted = tensor.extract %reduced[], defined inside the same +// parentOp (the main_loop forOp body) as the tensor.empty. All three ops +// (tensor.empty, tensor.extract, linalg.fill) must be cloned together so the +// cloned fill's `ins` references the cloned tensor.extract, not the +// producer-side %extracted. +// +// Case B (test_cloned_fill_scalar_dep_diff_parent): +// `ins` is %cst = arith.constant, defined OUTSIDE the main_loop (different +// parentOp from tensor.empty). Only tensor.empty + linalg.fill should be +// cloned; the arith.constant is a BlockArgument-like outer-scope value and +// must not be cloned. +// +// Case C (test_clone_introduces_new_tensor_dep): +// Exercises the two-phase dep collection: `ins` is tensor.extract of a +// producer-side tensor (%T = linalg.fill with block_id=13) defined INSIDE +// main_loop. Phase 1 clones empty+fill; the cloned extract then reads %T +// from a consumer block (block_id=9), creating a NEW cross-block tensor +// ref. Phase 2 must re-collect deps and add a multi-buffer for %T. The +// cloned extract must read from the multi-buffer result, not %T directly. + +// CHECK-LABEL: func.func @test_cloned_fill_scalar_dep_same_parent +// CHECK: %[[ORIG_EXTRACT:.*]] = tensor.extract {{.*}} {ssbuffer.block_id = 13 : i32} +// CHECK: %[[ORIG_FILL:.*]] = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%[[ORIG_EXTRACT]] : f32) outs({{.*}}) -> tensor<32x1xf32> +// CHECK: %[[CLONE_EXTRACT:.*]] = tensor.extract {{.*}} {ssbuffer.block_id = 9 : i32} +// CHECK: %[[CLONE_FILL:.*]] = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%[[CLONE_EXTRACT]] : f32) outs({{.*}}) -> tensor<32x1xf32> + +// CHECK-LABEL: func.func @test_cloned_fill_scalar_dep_diff_parent +// CHECK: %[[ORIG_FILL2:.*]] = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%{{.*}} : f32) outs({{.*}}) -> tensor<32x1xf32> +// CHECK: %[[CLONE_FILL2:.*]] = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%{{.*}} : f32) outs({{.*}}) -> tensor<32x1xf32> +// CHECK-NOT: arith.constant {{.*}}{ssbuffer.block_id = 9 : i32} + +// Case C: cloned fill's ins reaches producer-side %T → multi-buffer for %T. +// Multi-buffer allocs at top of main_loop scope (ping/pong memrefs). +// CHECK-LABEL: func.func @test_clone_introduces_new_dep +// CHECK-DAG: memref.alloc() : memref> +// CHECK-DAG: memref.alloc() : memref> +// Producer intraDeps tag moved from memref.memory_space_cast to hivm.hir.copy +// (refactor innerscope): the cast is just a buffer container, the copy is the +// actual producer behavior op. +// CHECK-NOT: memref.memory_space_cast{{.+}}intraDeps +// Producer-side ping-pong into multi-buffer (block_id = 13, intra_buffer). +// Producer scf.if has no result type (consumer does: -> tensor). +// CHECK: scf.if %{{[0-9_a-zA-Z]+}} { +// Producer-side hivm.hir.copy now carries intraDeps = [0, 1] (refactor innerscope). +// CHECK: hivm.hir.copy ins(%{{.+}} : tensor) outs(%{{.+}} : memref) {{.+}}intraDeps = [0 : i32, 1 : i32] +// CHECK: } {ssbuffer.block_id = 13 : i32, ssbuffer.intra_buffer} +// Original empty+fill remains in producer block (block_id = 13). +// CHECK: linalg.fill {ssbuffer.block_id = 13 : i32} ins(%extracted : f32) outs({{.*}} : tensor<32x1xf32>) -> tensor<32x1xf32> +// Consumer-side multi-buffer selection (block_id = 22 = inner loop body). +// CHECK: scf.if {{.*}} -> (tensor) { +// CHECK: bufferization.to_tensor {{.*}} restrict writable : memref +// CHECK: } else { +// CHECK: bufferization.to_tensor {{.*}} restrict writable : memref +// CHECK: } {ssbuffer.block_id = 22 : i32, {{.*}}ssbuffer.intra_buffer} +// Cloned extract reads multi-buffer result (NOT %1 directly). +// CHECK: %[[C_CLONE_EXTRACT:.*]] = tensor.extract %{{.*}}[] {ssbuffer.block_id = 9 : i32} : tensor +// Cloned fill in consumer block: ins references the cloned extract. +// CHECK: linalg.fill {ssbuffer.block_id = 9 : i32} ins(%[[C_CLONE_EXTRACT]] : f32) outs({{.*}} : tensor<32x1xf32>) -> tensor<32x1xf32> + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // Case A: ins defining op (tensor.extract) shares parentOp with tensor.empty + func.func @test_cloned_fill_scalar_dep_same_parent(%arg0: memref) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c100_i32 = arith.constant 100 : i32 + %cst = arith.constant 1.0 : f32 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c32 = arith.constant 32 : index + %reduced_init = tensor.empty() {ssbuffer.block_id = 13 : i32} : tensor + %reduced = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%cst : f32) outs(%reduced_init : tensor) -> tensor + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + %empty = tensor.empty() {ssbuffer.block_id = 13 : i32} : tensor<32x1xf32> + %extracted = tensor.extract %reduced[] {ssbuffer.block_id = 13 : i32} : tensor + %fill = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%extracted : f32) outs(%empty : tensor<32x1xf32>) -> tensor<32x1xf32> + scf.for %j = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + %34 = tensor.empty() {ssbuffer.block_id = 9 : i32} : tensor<32x1xf32> + %35 = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%cst : f32) outs(%34 : tensor<32x1xf32>) -> tensor<32x1xf32> + %36 = arith.mulf %35, %fill {ssbuffer.block_id = 9 : i32} : tensor<32x1xf32> + } {Undefined, ssbuffer.block_id = 22 : i32} + } {Undefined, ssbuffer.block_id = 23 : i32, ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } + + // Case B: ins defining op (arith.constant) is OUTSIDE main_loop, so only + // tensor.empty + linalg.fill are cloned. + func.func @test_cloned_fill_scalar_dep_diff_parent(%arg0: memref) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c100_i32 = arith.constant 100 : i32 + %cst = arith.constant {ssbuffer.block_id = 15 : i32} 1.0 : f32 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c32 = arith.constant 32 : index + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + %empty = tensor.empty() {ssbuffer.block_id = 13 : i32} : tensor<32x1xf32> + %fill = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%cst : f32) outs(%empty : tensor<32x1xf32>) -> tensor<32x1xf32> + scf.for %j = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + %34 = tensor.empty() {ssbuffer.block_id = 9 : i32} : tensor<32x1xf32> + %35 = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%cst : f32) outs(%34 : tensor<32x1xf32>) -> tensor<32x1xf32> + %36 = arith.mulf %35, %fill {ssbuffer.block_id = 9 : i32} : tensor<32x1xf32> + } {Undefined, ssbuffer.block_id = 22 : i32} + } {Undefined, ssbuffer.block_id = 23 : i32, ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } + + // Case C: empty+fill's `ins` is tensor.extract of a producer-side tensor + // defined INSIDE main_loop. Phase 1 clones the empty+fill, the cloned + // extract creates a NEW cross-block ref to %T (block_id=13), and Phase 2 + // must add a multi-buffer for %T. + func.func @test_clone_introduces_new_dep(%arg0: memref) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c100_i32 = arith.constant 100 : i32 + %cst = arith.constant 1.0 : f32 + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + %T_alloc = bufferization.alloc_tensor() {ssbuffer.block_id = 13 : i32} : tensor + %T = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%cst : f32) outs(%T_alloc : tensor) -> tensor + %extracted = tensor.extract %T[] {ssbuffer.block_id = 13 : i32} : tensor + %empty = tensor.empty() {ssbuffer.block_id = 13 : i32} : tensor<32x1xf32> + %fill = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%extracted : f32) outs(%empty : tensor<32x1xf32>) -> tensor<32x1xf32> + scf.for %j = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + %inner_empty = tensor.empty() {ssbuffer.block_id = 9 : i32} : tensor<32x1xf32> + %inner_fill = linalg.fill {ssbuffer.block_id = 9 : i32} ins(%cst : f32) outs(%inner_empty : tensor<32x1xf32>) -> tensor<32x1xf32> + %consumed = arith.mulf %inner_fill, %fill {ssbuffer.block_id = 9 : i32} : tensor<32x1xf32> + } {Undefined, ssbuffer.block_id = 22 : i32} + } {Undefined, ssbuffer.block_id = 23 : i32, ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-memref-dep.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-memref-dep.mlir new file mode 100644 index 0000000000..b819bf9e44 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-memref-dep.mlir @@ -0,0 +1,42 @@ +// RUN: triton-opt --add_multi_buffer_inner_scope %s 2>&1 | FileCheck %s +// Pass signals fallback via triton_ascend.dynamic_cv_pipeline.rc = 2 +// (ERRCODE_IGNORED); the IR is otherwise unchanged. + +// CHECK-LABEL: module attributes +// CHECK-SAME: triton_ascend.dynamic_cv_pipeline.rc = 2 + +// T26: Memref Type Dependency Triggers Fallback +// Test: When memref.alloc with block_id=X produces a memref, and later +// bufferization.to_tensor with block_id=Y uses that memref (X != Y), +// the memref is a cross-block dependency and hasMemrefDepValue returns +// true, causing pass failure. +// Key Check: Pass falls back via the ERRCODE_FAILED attribute. + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_t26_memref_dep_fallback() { + %c0_i32 = arith.constant 0 : i32 + %c100_i32 = arith.constant 100 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst = arith.constant 1.0 : f32 + %empty = tensor.empty() : tensor<128xf32> + scope.scope : () -> () { + // Producer in block_id = 5 + %prod = linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst : f32) outs(%empty : tensor<128xf32>) -> tensor<128xf32> + // Main loop - all operations inside must have proper ssbuffer.block_id + %loop_result = scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 iter_args(%arg = %prod) -> (tensor<128xf32>) : i32 { + // memref.alloc in block_id = 9 (producer block) + %alloc = memref.alloc() {ssbuffer.block_id = 9 : i32} : memref<128xf32> + // bufferization.to_tensor in block_id = 10 (consumer block, different from 9) + // This uses %alloc directly, creating a cross-block memref dependency + %tensor_from_alloc = bufferization.to_tensor %alloc {ssbuffer.block_id = 10 : i32} : memref<128xf32> to tensor<128xf32> + // Use the tensor in block_id = 10 + %consumed = arith.addf %tensor_from_alloc, %tensor_from_alloc {ssbuffer.block_id = 10 : i32} : tensor<128xf32> + // Producer continuation in block_id = 5 + %new_prod = arith.addf %consumed, %arg {ssbuffer.block_id = 5 : i32} : tensor<128xf32> + scf.yield %new_prod : tensor<128xf32> + } {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-whileop.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-whileop.mlir new file mode 100644 index 0000000000..82fc6b0898 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner-scope-whileop.mlir @@ -0,0 +1,383 @@ +// RUN: triton-opt --add_multi_buffer_inner_scope %s | FileCheck %s + +// T-while-A: whileOp as main_loop, INTRA bufNum == 1 (single-buffer scope). +// Verifies: +// - whileOp carrying ssbuffer.main_loop on its terminator IS recognized +// (legacy shape supported by hasMainLoopAttr). +// - setupWhileIterArgCounter is SKIPPED (bufNum==1 → no dead iter_arg). +// - The memref.alloc + bufferization.to_tensor dep is CLONED into the +// consumer block (NOT multi-buffered) — the alloc has no payload on +// entry, so multi-buffering would be a read-before-write copy. +// - NO UB memref.alloc or hivm.hir.copy is emitted for this dep. +// - The original whileOp is preserved verbatim (no extra i32 iter_arg). +// We pin the buffer count to 1 via the module-level +// `ssbuffer.intra_buf_count` attribute so the default of 2 doesn't +// trigger the counter-setup branch. + +// CHECK-LABEL: func.func @test_while_mainloop_bufnum_one +// Original whileOp do-region bb0 has only 2 block-args (no i32 counter). +// CHECK: ^bb0(%{{.*}}: tensor<128xf32>, %{{.*}}: i32): +// Original alloc + to_tensor stay in producer block (orphaned, will be DCE'd). +// CHECK-DAG: memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<128xf32> +// CHECK-DAG: bufferization.to_tensor {{.*}}{ssbuffer.block_id = 7 : i32} : memref<128xf32> to tensor<128xf32> +// Cloned alloc + to_tensor appear in consumer block (block_id = 10). +// CHECK-DAG: memref.alloc() {ssbuffer.block_id = 10 : i32} : memref<128xf32> +// CHECK-DAG: bufferization.to_tensor {{.*}}{ssbuffer.block_id = 10 : i32} : memref<128xf32> to tensor<128xf32> +// Consumer uses the cloned to_tensor at block_id = 10. +// CHECK: arith.addf {{.*}}{ssbuffer.block_id = 10 : i32} : tensor<128xf32> +// NO UB alloc, NO hivm.copy chain — dep was cloned, not multi-buffered. +// CHECK-NOT: memref.alloc() : memref<128xf32, #hivm.address_space> +// CHECK-NOT: hivm.hir.copy{{.*}}: tensor<128xf32> +// whileOp carries main_loop but NOT iterCounter (bufNum == 1). +// CHECK: } {{.*}}ssbuffer.main_loop = 1 : i64 +// CHECK-NOT: } {{.*}}ssbuffer.iterCounter, + +// T-while-B: whileOp as main_loop, INTRA bufNum == 2 (multi-buffer scope). +// Verifies: +// - setupWhileIterArgCounter IS called (bufNum>1) — the whileOp gains +// an extra i32 iter_arg (init=0, yielded as counter+1 at end of +// do-region). +// - The new do-region bb0 has 3 block-args (counter arg appended). +// - The memref.alloc + to_tensor dep is CLONED to the consumer block +// (NOT multi-buffered), so no UB alloc + scf.if dispatch + to_tensor +// readback chain is emitted. +// - The counter add-1 (and its constant 1) is relocated to the +// block_id of the FIRST OP that consumes the counter arg — which is +// now the cloned alloc at block_id = 10 (the alloc+to_tensor clone +// becomes the new "first user" since the multi-buffer producer scf.if +// is gone). + +// CHECK-LABEL: func.func @test_while_mainloop_bufnum_two +// WhileOp's do-region bb0 has 3 block-args now (the new i32 counter is last). +// CHECK: ^bb0(%{{.*}}: tensor<128xf32>, %{{.*}}: i32, %{{.*}}: i32): +// Original alloc + to_tensor stay in producer block (orphaned). +// CHECK-DAG: memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<128xf32> +// CHECK-DAG: bufferization.to_tensor {{.*}}{ssbuffer.block_id = 7 : i32} : memref<128xf32> to tensor<128xf32> +// Cloned alloc + to_tensor appear in consumer block (block_id = 10). +// CHECK-DAG: memref.alloc() {ssbuffer.block_id = 10 : i32} : memref<128xf32> +// CHECK-DAG: bufferization.to_tensor {{.*}}{ssbuffer.block_id = 10 : i32} : memref<128xf32> to tensor<128xf32> +// Consumer uses the cloned to_tensor at block_id = 10. +// CHECK: arith.addf {{.*}}{ssbuffer.block_id = 10 : i32} : tensor<128xf32> +// NO UB alloc, NO scf.if dispatch, NO hivm.copy — dep was cloned, not +// multi-buffered. (With bufNum==2 + non-cloneable dep, multi-buffer would +// have generated the full chain.) +// CHECK-NOT: memref.alloc() : memref<128xf32, #hivm.address_space> +// CHECK-NOT: scf.if +// CHECK-NOT: hivm.hir.copy{{.*}}: tensor<128xf32> +// arith.addi increment for the multi-buffer counter — relocated to the +// first consumer's block_id (= 10, the cloned alloc), NOT 7. +// CHECK: %{{.+}} = arith.addi %{{.+}}, %{{.+}} {ssbuffer.block_id = 10 : i32, ssbuffer.iterCounter} : i32 +// Counter-aware whileOp carries iterCounter alongside main_loop. +// CHECK: } {{.*}}ssbuffer.iterCounter, {{.*}}ssbuffer.main_loop = 1 : i64 + +// T-while-C: whileOp main_loop with scf.if inside the do-region (multi-region +// consumer pattern). The whileOp carries `ssbuffer.main_loop` on itself (NOT +// on the terminator). This exercises `hasMainLoopAttr`'s "op has attr" path, +// in contrast to T-while-A/B which use the terminator-attr legacy shape. +// Verifies the cross-block dep is cloned into BOTH consumer branches. + +// CHECK-LABEL: func.func @test_while_mainloop_attr_on_op +// Original alloc + to_tensor stay in producer block (orphaned). +// CHECK: %[[C_ORIG_ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<64xf16> +// CHECK: %[[C_ORIG_TT:.*]] = bufferization.to_tensor %[[C_ORIG_ALLOC]] {ssbuffer.block_id = 7 : i32} : memref<64xf16> to tensor<64xf16> +// Cloned alloc + to_tensor appear in BOTH consumer blocks (block_id = 11 in +// the if-branch, block_id = 12 in the else-branch). +// CHECK: %[[C_CLONE_ALLOC_11:.*]] = memref.alloc() {ssbuffer.block_id = 11 : i32} : memref<64xf16> +// CHECK: %[[C_CLONE_TT_11:.*]] = bufferization.to_tensor %[[C_CLONE_ALLOC_11]] {ssbuffer.block_id = 11 : i32} : memref<64xf16> to tensor<64xf16> +// CHECK: arith.addf %[[C_CLONE_TT_11]], %[[C_CLONE_TT_11]] {ssbuffer.block_id = 11 : i32} : tensor<64xf16> +// CHECK: %[[C_CLONE_ALLOC_12:.*]] = memref.alloc() {ssbuffer.block_id = 12 : i32} : memref<64xf16> +// CHECK: %[[C_CLONE_TT_12:.*]] = bufferization.to_tensor %[[C_CLONE_ALLOC_12]] {ssbuffer.block_id = 12 : i32} : memref<64xf16> to tensor<64xf16> +// CHECK: arith.mulf %[[C_CLONE_TT_12]], %[[C_CLONE_TT_12]] {ssbuffer.block_id = 12 : i32} : tensor<64xf16> +// NO UB alloc, NO consumer-side to_tensor readback — dep was cloned. +// CHECK-NOT: memref.alloc() : memref<64xf16, #hivm.address_space> +// CHECK-NOT: hivm.hir.copy{{.*}}: tensor<64xf16> +// Counter add-1 relocated to first consumer's block_id (= 12, the cloned +// alloc's location). +// CHECK: %{{.+}} = arith.addi %{{.+}}, %{{.+}} {ssbuffer.block_id = 12 : i32, ssbuffer.iterCounter} : i32 +// Counter-aware whileOp carries iterCounter alongside main_loop. +// CHECK: } {{.*}}ssbuffer.iterCounter, {{.*}}ssbuffer.main_loop = 1 : i64 + +// T-while-D: Regression guard for the getOutermostSsbufferId priority fix. +// Before the fix, walking up from an op inside a whileOp's do-region would +// hit the multi-region check (`numRegions >= 2`) BEFORE the kMainLoop check +// and return the whileOp's own block_id, breaking cross-block dep +// classification. +// Setup: whileOp (block_id = 5) wrapping a producer block_id = 8 inside the +// do-region and a consumer block_id = 12 also inside the do-region. +// Cross-block judgment must use 8 vs 12 (NOT 5 vs 12). After my fix the dep +// is cloned to block_id = 12; if the priority bug were present the producer +// and consumer would both be classified as block_id = 5, the dep would NOT +// be detected as cross-block, and NO clone would be emitted. + +// CHECK-LABEL: func.func @test_while_outermost_id_priority +// Original alloc + to_tensor stay at producer block (block_id = 8). +// CHECK: %[[D_ORIG_ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32xf32> +// CHECK: %[[D_ORIG_TT:.*]] = bufferization.to_tensor %[[D_ORIG_ALLOC]] {ssbuffer.block_id = 8 : i32} : memref<32xf32> to tensor<32xf32> +// Cloned alloc + to_tensor appear at consumer block (block_id = 12). If the +// outermost-id priority bug were present, no clone would be emitted (the dep +// would be misclassified as same-block and silently skipped). +// CHECK: %[[D_CLONE_ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 12 : i32} : memref<32xf32> +// CHECK: %[[D_CLONE_TT:.*]] = bufferization.to_tensor %[[D_CLONE_ALLOC]] {ssbuffer.block_id = 12 : i32} : memref<32xf32> to tensor<32xf32> +// CHECK: arith.addf %[[D_CLONE_TT]], %[[D_CLONE_TT]] {ssbuffer.block_id = 12 : i32} : tensor<32xf32> +// NO UB alloc, NO copy chain. +// CHECK-NOT: memref.alloc() : memref<32xf32, #hivm.address_space> +// CHECK-NOT: hivm.hir.copy{{.*}}: tensor<32xf32> +// Counter add-1 at first consumer's block_id (= 12). +// CHECK: %{{.+}} = arith.addi %{{.+}}, %{{.+}} {ssbuffer.block_id = 12 : i32, ssbuffer.iterCounter} : i32 +// Counter-aware whileOp carries iterCounter alongside main_loop. +// CHECK: } {{.*}}ssbuffer.iterCounter, {{.*}}ssbuffer.main_loop = 1 : i64 + +// T-while-E: whileOp main_loop with a tensor::EmptyOp + linalg::FillOp pattern +// inside the do-region. The empty+fill is cloned into each consumer block. +// Verifies cloneEmptyFillsInBlocks works on a whileOp main_loop (the body +// is `getAfterBody()`, not `getBody()`). +// The cloned fill's ins stays the same (arith.constant, lives outside the +// main_loop) — no clone of the scalar chain is needed. +// NOTE: This case uses tensor.empty + linalg.fill (NOT memref.alloc + +// to_tensor), so the alloc-to-tensor clone logic doesn't apply. The empty+ +// fill clone is exercised here, unchanged. + +// CHECK-LABEL: func.func @test_while_clone_empty_fill +// Original fill at block_id = 8 (producer block) is preserved. +// CHECK: linalg.fill {{.*}}{ssbuffer.block_id = 8 : i32} {{.*}}outs({{.*}} : tensor<32x1xf32>) +// Original fill at block_id = 14 (second producer block) is preserved. +// CHECK: linalg.fill {{.*}}{ssbuffer.block_id = 14 : i32} {{.*}}outs({{.*}} : tensor<32x1xf32>) +// Cloned fills land in block_id = 12 (consumer block) — there are two of them, +// one per original producer. CHECK matches at least once. +// CHECK: linalg.fill {{.*}}{ssbuffer.block_id = 12 : i32} {{.*}}outs({{.*}} : tensor<32x1xf32>) +// arith.addi increment for the multi-buffer counter is present (block_id=12), tagged iterCounter. +// CHECK: %{{.+}} = arith.addi %{{.+}}, %{{.+}} {ssbuffer.block_id = 12 : i32, ssbuffer.iterCounter} : i32 +// Counter-aware whileOp carries ssbuffer.iterCounter alongside main_loop. +// CHECK: } {{.*}}ssbuffer.iterCounter, {{.*}}ssbuffer.main_loop = 1 : i64 + +// T-while-F (TODO): Negative — two whileOps in the same scope, neither is the +// "outer" main_loop; one is nested in the other's do-region. The pass must +// refuse via findNestedMainloop (returns WalkResult::interrupt + fallback). +// Not implemented in this initial drop — to be added when the negative case +// is needed for a regression guard. + +// T-while-G: Regression guard for relocateWhileCounterOps. +// The counter add-1 (and its constant 1) used to be appended at the end of +// the do-region, carrying the whileOp's own block_id (= 99 here, distinct +// from both producer and consumer). The pass now relocates them to the +// block_id of the first op that consumes the counter iter_arg — which is +// now the cloned alloc at block_id = 50 (the alloc+to_tensor clone becomes +// the new "first user" since multi-buffer producer scf.if is gone). +// We assert: +// - The counter add-1 carries block_id = 50 (NOT 99, NOT 5). +// - The constant 1 used by the addi also carries block_id = 50. +// - The scf.yield that consumes the add-1's SSA value still references +// the same %result (SSA is preserved across the move). + +// CHECK-LABEL: func.func @test_while_counter_relocation +// Original alloc + to_tensor stay at producer block (block_id = 5, orphaned). +// CHECK: %[[G_ORIG_ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<16xf32> +// CHECK: %[[G_ORIG_TT:.*]] = bufferization.to_tensor %[[G_ORIG_ALLOC]] {ssbuffer.block_id = 5 : i32} : memref<16xf32> to tensor<16xf32> +// Cloned alloc + to_tensor at consumer block (block_id = 50). +// CHECK: %[[G_CLONE_ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = 50 : i32} : memref<16xf32> +// CHECK: %[[G_CLONE_TT:.*]] = bufferization.to_tensor %[[G_CLONE_ALLOC]] {ssbuffer.block_id = 50 : i32} : memref<16xf32> to tensor<16xf32> +// CHECK: arith.addf %[[G_CLONE_TT]], %[[G_CLONE_TT]] {ssbuffer.block_id = 50 : i32} : tensor<16xf32> +// NO UB alloc, NO copy chain. +// CHECK-NOT: memref.alloc() : memref<16xf32, #hivm.address_space> +// CHECK-NOT: hivm.hir.copy{{.*}}: tensor<16xf32> +// Constant 1 carrying block_id = 50 (relocated with the addi, follows the +// first user which is the clone). +// CHECK: %{{.+}} = arith.constant {ssbuffer.block_id = 50 : i32} 1 : i32 +// Counter add-1 carries block_id = 50 and is tagged iterCounter. +// CHECK: %{{.+}} = arith.addi %{{.+}}, %{{.+}} {ssbuffer.block_id = 50 : i32, ssbuffer.iterCounter} : i32 +// scf.yield forwards the counter (referenced by the addi's %addi-res), proving +// SSA is preserved. +// CHECK: scf.yield %{{.*}}, %{{.*}}, %{{.+}} +// Counter-aware whileOp carries ssbuffer.iterCounter alongside main_loop. +// CHECK: } {{.*}}ssbuffer.iterCounter, {{.*}}ssbuffer.main_loop = 1 : i64 + +// ---- Inputs ---- + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, + ssbuffer.intra_buf_count = 1 : i32} { + // T-while-A: whileOp with main_loop on the terminator, single-buffer. + func.func @test_while_mainloop_bufnum_one() { + %c0_i32 = arith.constant 0 : i32 + %c10_i32 = arith.constant 10 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst_zero = arith.constant 0.0 : f32 + %init = tensor.empty() : tensor<128xf32> + %carry = linalg.fill ins(%cst_zero : f32) outs(%init : tensor<128xf32>) -> tensor<128xf32> + scope.scope : () -> () { + %result:2 = scf.while (%arg0 = %carry, %arg1 = %c0_i32) + : (tensor<128xf32>, i32) -> (tensor<128xf32>, i32) { + %cmp = arith.cmpi slt, %arg1, %c10_i32 {ssbuffer.block_id = 16 : i32} : i32 + scf.condition(%cmp) %arg0, %arg1 : tensor<128xf32>, i32 + } do { + ^bb0(%arg0: tensor<128xf32>, %arg1: i32): + // Producer block_id = 7. + %alloc = memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<128xf32> + %prod = bufferization.to_tensor %alloc {ssbuffer.block_id = 7 : i32} : memref<128xf32> to tensor<128xf32> + // Consumer block_id = 10 (cross-block). + %consumed = arith.addf %prod, %prod {ssbuffer.block_id = 10 : i32} : tensor<128xf32> + %next = arith.addi %arg1, %c1_i32 : i32 + scf.yield %consumed, %next : tensor<128xf32>, i32 + } attributes {ssbuffer.main_loop = 1 : i64, ssbuffer.block_id = 23 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, + ssbuffer.intra_buf_count = 2 : i32} { + // T-while-B: whileOp with main_loop on the terminator, multi-buffer (bufNum=2). + func.func @test_while_mainloop_bufnum_two() { + %c0_i32 = arith.constant 0 : i32 + %c10_i32 = arith.constant 10 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst_zero = arith.constant 0.0 : f32 + %init = tensor.empty() : tensor<128xf32> + %carry = linalg.fill ins(%cst_zero : f32) outs(%init : tensor<128xf32>) -> tensor<128xf32> + scope.scope : () -> () { + %result:2 = scf.while (%arg0 = %carry, %arg1 = %c0_i32) + : (tensor<128xf32>, i32) -> (tensor<128xf32>, i32) { + %cmp = arith.cmpi slt, %arg1, %c10_i32 {ssbuffer.block_id = 16 : i32} : i32 + scf.condition(%cmp) %arg0, %arg1 : tensor<128xf32>, i32 + } do { + ^bb0(%arg0: tensor<128xf32>, %arg1: i32): + %alloc = memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<128xf32> + %prod = bufferization.to_tensor %alloc {ssbuffer.block_id = 7 : i32} : memref<128xf32> to tensor<128xf32> + %consumed = arith.addf %prod, %prod {ssbuffer.block_id = 10 : i32} : tensor<128xf32> + %next = arith.addi %arg1, %c1_i32 : i32 + scf.yield %consumed, %next : tensor<128xf32>, i32 + } attributes {ssbuffer.main_loop = 1 : i64, ssbuffer.block_id = 23 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } + + // T-while-C: whileOp with main_loop on the OP itself (not terminator). + func.func @test_while_mainloop_attr_on_op() { + %c0_i32 = arith.constant 0 : i32 + %c10_i32 = arith.constant 10 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst_one = arith.constant 1.0 : f16 + %init = tensor.empty() : tensor<64xf16> + %carry = linalg.fill ins(%cst_one : f16) outs(%init : tensor<64xf16>) -> tensor<64xf16> + scope.scope : () -> () { + %result:2 = scf.while (%arg0 = %carry, %arg1 = %c0_i32) + : (tensor<64xf16>, i32) -> (tensor<64xf16>, i32) { + %cmp = arith.cmpi slt, %arg1, %c10_i32 {ssbuffer.block_id = 16 : i32} : i32 + scf.condition(%cmp) %arg0, %arg1 : tensor<64xf16>, i32 + } do { + ^bb0(%arg0: tensor<64xf16>, %arg1: i32): + %alloc = memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<64xf16> + %prod = bufferization.to_tensor %alloc {ssbuffer.block_id = 7 : i32} : memref<64xf16> to tensor<64xf16> + // Multi-region consumer (scf.if inside the do-region). + %cnd = arith.cmpi slt, %arg1, %c1_i32 {ssbuffer.block_id = 11 : i32} : i32 + %consumed = scf.if %cnd -> (tensor<64xf16>) { + %a = arith.addf %prod, %prod {ssbuffer.block_id = 11 : i32} : tensor<64xf16> + scf.yield %a : tensor<64xf16> + } else { + %b = arith.mulf %prod, %prod {ssbuffer.block_id = 12 : i32} : tensor<64xf16> + scf.yield %b : tensor<64xf16> + } {ssbuffer.block_id = 12 : i32} + %next = arith.addi %arg1, %c1_i32 : i32 + scf.yield %consumed, %next : tensor<64xf16>, i32 + } attributes {ssbuffer.main_loop = 1 : i64, ssbuffer.block_id = 23 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } + + // T-while-D: whileOp main_loop with cross-block dep that tests + // getOutermostSsbufferId's main_loop-first priority. + // Producer at block_id=8, consumer at block_id=12, both inside do-region. + // whileOp itself is at block_id=5 (so the buggy code would have collapsed + // both to 5, hiding the cross-block dep). + func.func @test_while_outermost_id_priority() { + %c0_i32 = arith.constant 0 : i32 + %c10_i32 = arith.constant 10 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst_zero = arith.constant 0.0 : f32 + %init = tensor.empty() : tensor<32xf32> + %carry = linalg.fill ins(%cst_zero : f32) outs(%init : tensor<32xf32>) -> tensor<32xf32> + scope.scope : () -> () { + %result:2 = scf.while (%arg0 = %carry, %arg1 = %c0_i32) + : (tensor<32xf32>, i32) -> (tensor<32xf32>, i32) { + %cmp = arith.cmpi slt, %arg1, %c10_i32 {ssbuffer.block_id = 16 : i32} : i32 + scf.condition(%cmp) %arg0, %arg1 : tensor<32xf32>, i32 + } do { + ^bb0(%arg0: tensor<32xf32>, %arg1: i32): + %alloc = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32xf32> + %prod = bufferization.to_tensor %alloc {ssbuffer.block_id = 8 : i32} : memref<32xf32> to tensor<32xf32> + %consumed = arith.addf %prod, %prod {ssbuffer.block_id = 12 : i32} : tensor<32xf32> + %next = arith.addi %arg1, %c1_i32 : i32 + scf.yield %consumed, %next : tensor<32xf32>, i32 + } attributes {ssbuffer.main_loop = 1 : i64, ssbuffer.block_id = 5 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } + + // T-while-E: whileOp main_loop with empty+fill pattern. + // Two pairs of empty+fill, both cloned to consumer block 12. + func.func @test_while_clone_empty_fill() { + %c0_i32 = arith.constant 0 : i32 + %c10_i32 = arith.constant 10 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst_one = arith.constant 1.0 : f32 + scope.scope : () -> () { + %result:2 = scf.while (%arg0 = %c0_i32, %arg1 = %cst_one) + : (i32, f32) -> (i32, f32) { + %cmp = arith.cmpi slt, %arg0, %c10_i32 {ssbuffer.block_id = 16 : i32} : i32 + scf.condition(%cmp) %arg0, %arg1 : i32, f32 + } do { + ^bb0(%arg0: i32, %arg1: f32): + // Producer block_id = 8: empty+fill (cloneable). + %empty8 = tensor.empty() {ssbuffer.block_id = 8 : i32} : tensor<32x1xf32> + %fill8 = linalg.fill {ssbuffer.block_id = 8 : i32} ins(%cst_one : f32) + outs(%empty8 : tensor<32x1xf32>) -> tensor<32x1xf32> + // Producer block_id = 14: empty+fill (cloneable). + %empty14 = tensor.empty() {ssbuffer.block_id = 14 : i32} : tensor<32x1xf32> + %fill14 = linalg.fill {ssbuffer.block_id = 14 : i32} ins(%cst_one : f32) + outs(%empty14 : tensor<32x1xf32>) -> tensor<32x1xf32> + // Consumer block_id = 12: uses BOTH fills cross-block. + %consumed = arith.addf %fill8, %fill14 {ssbuffer.block_id = 12 : i32} : tensor<32x1xf32> + %next = arith.addi %arg0, %c1_i32 : i32 + scf.yield %next, %arg1 : i32, f32 + } attributes {ssbuffer.main_loop = 1 : i64, ssbuffer.block_id = 23 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } + + // T-while-G: dedicated regression test for relocateWhileCounterOps. + // Producer at block_id = 5, consumer at block_id = 50, whileOp at block_id = 99. + // Before the fix: counter add-1 would land at block_id = 99 (whileOp's own id). + // After the fix: counter add-1 (and its constant 1) land at block_id = 5 + // (the block_id of the first op that consumes the counter iter_arg). + func.func @test_while_counter_relocation() { + %c0_i32 = arith.constant 0 : i32 + %c10_i32 = arith.constant 10 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst_zero = arith.constant 0.0 : f32 + %init = tensor.empty() : tensor<16xf32> + %carry = linalg.fill ins(%cst_zero : f32) outs(%init : tensor<16xf32>) -> tensor<16xf32> + scope.scope : () -> () { + %result:2 = scf.while (%arg0 = %carry, %arg1 = %c0_i32) + : (tensor<16xf32>, i32) -> (tensor<16xf32>, i32) { + %cmp = arith.cmpi slt, %arg1, %c10_i32 {ssbuffer.block_id = 16 : i32} : i32 + scf.condition(%cmp) %arg0, %arg1 : tensor<16xf32>, i32 + } do { + ^bb0(%arg0: tensor<16xf32>, %arg1: i32): + // Producer block_id = 5. + %alloc = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<16xf32> + %prod = bufferization.to_tensor %alloc {ssbuffer.block_id = 5 : i32} : memref<16xf32> to tensor<16xf32> + // Consumer block_id = 50 (very different from 5, to make the relocation + // verdict unambiguous). + %consumed = arith.addf %prod, %prod {ssbuffer.block_id = 50 : i32} : tensor<16xf32> + %next = arith.addi %arg1, %c1_i32 : i32 + scf.yield %consumed, %next : tensor<16xf32>, i32 + } attributes {ssbuffer.main_loop = 1 : i64, ssbuffer.block_id = 99 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner_scope_i1_test.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner_scope_i1_test.mlir new file mode 100644 index 0000000000..d53359bd9d --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Inner_scope_i1_test.mlir @@ -0,0 +1,34 @@ +// RUN: triton-opt --add_multi_buffer_inner_scope %s | FileCheck %s +// Verifies that the i1 tensor dep triggers the IGNORED (rc=2) fallback path, +// not the FAILED (rc=1) path. i1 cross-block tensor deps are intentionally +// not multi-buffered (the multi-buffer pipeline assumes memref-shaped +// tensors with element types the downstream pipeline can clone), so the +// pass must signal IGNORED (rc=2 = ERRCODE_IGNORED) rather than FAILED +// (rc=1 = ERRCODE_FAILED). + +// CHECK-LABEL: module attributes +// CHECK-SAME: triton_ascend.dynamic_cv_pipeline.rc = 2 : i32 +// Confirm we did NOT clobber the IGNORED code with FAILED. +// CHECK-NOT: triton_ascend.dynamic_cv_pipeline.rc = 1 + +// CHECK-LABEL: func.func @test_i1_tensor_dep_multi_buffer + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_i1_tensor_dep_multi_buffer() { + %c0_i32 = arith.constant 0 : i32 + %c100_i32 = arith.constant 100 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst_true = arith.constant true + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 : i32 { + // Producer: tensor<128xi1> in block 7 + %alloc = memref.alloc() {ssbuffer.block_id = 7 : i32} : memref<128xi1> + %prod = bufferization.to_tensor %alloc {ssbuffer.block_id = 7 : i32} : memref<128xi1> to tensor<128xi1> + // Consumer in block 10 (cross-block) + %consumed = arith.ori %prod, %prod {ssbuffer.block_id = 10 : i32} : tensor<128xi1> + } {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-cross-buffer.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-cross-buffer.mlir new file mode 100644 index 0000000000..3921fddbf1 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-cross-buffer.mlir @@ -0,0 +1,28 @@ +// RUN: triton-opt --add_multi_buffer_outer_scope %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { +func.func @tc_cross_buffer_ctov() { + %c0_i32 = arith.constant 0 : i32 + %c100_i32 = arith.constant 100 : i32 + %c1_i32 = arith.constant 1 : i32 + scope.scope : () -> () { + %buf_ub = memref.alloc() {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128xf16, #hivm.address_space> + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 iter_args() -> () : i32 { + hivm.hir.sync_block_set {ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 3 + scf.for %j = %c0_i32 to %c100_i32 step %c1_i32 iter_args() -> () : i32 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 3 + %buf = memref.memory_space_cast %buf_ub {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128xf16, #hivm.address_space> to memref<128xf16> + %t = bufferization.to_tensor %buf restrict writable : memref<128xf16> to tensor<128xf16> + scf.yield + } {ssbuffer.block_id = 18 : i32, ssbuffer.transfer_id = 1 : i32} + scf.yield + } {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} +} + +// Pass runs without crash, nested-loop case handled gracefully +// CHECK: func.func @tc_cross_buffer_ctov +// CHECK: return diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-flag-budget-fallback.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-flag-budget-fallback.mlir new file mode 100644 index 0000000000..4c004a2c89 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-flag-budget-fallback.mlir @@ -0,0 +1,275 @@ +// RUN: triton-opt --add_multi_buffer_outer_scope %s | FileCheck %s + +// When the input IR already uses flags 1..9 and requests inter-core +// double-buffering (ssbuffer.inter_core_buf_count = 2), the 6 main-loop +// transfer keys need to acquire output flags 10..15, pushing the maximum +// flag id past MAX_FLAG_ID (14). The pass must detect the budget overflow +// and fall back to single-buffer mode, leaving the IR unchanged: +// no new output flags, no polling control flow, no output buffers. + +// The double-buffer request attribute is preserved (downgrade, not removal). +// CHECK: ssbuffer.inter_core_buf_count = 2 +// CHECK-LABEL: func.func @_attn_bwd +// Original flags 1..9 are preserved. +// CHECK: flag = 9 +// No new output flag (10..15) is allocated -> confirms single-buffer fallback. +// CHECK-NOT: flag = {{1[0-5]}} + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, ssbuffer.insertionOptimization, ssbuffer.inter_core_buf_count = 2 : i32, ssbuffer.intra_buf_count = 3 : i32, ssbuffer.load_store_buf_count = 1 : i32} { + func.func @_attn_bwd(%arg0: memref, %arg1: memref, %arg2: memref {tt.tensor_kind = 0 : i32}, %arg3: memref {tt.tensor_kind = 0 : i32}, %arg4: memref {tt.tensor_kind = 0 : i32}, %arg5: memref {tt.tensor_kind = 0 : i32}, %arg6: memref {tt.tensor_kind = 2 : i32}, %arg7: memref {tt.tensor_kind = 1 : i32}, %arg8: memref {tt.tensor_kind = 1 : i32}, %arg9: memref {tt.tensor_kind = 0 : i32}, %arg10: memref {tt.tensor_kind = 0 : i32}, %arg11: f32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32, %arg17: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 19 : i32} dense<[4, 8, 16, 16]> : tensor<4xi64> + %cst_0 = arith.constant {ssbuffer.block_id = 19 : i32} dense<[128, 4, 16]> : tensor<3xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 14 : i32} 0.000000e+00 : f32 + %c64_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 64 : i32 + %c8388608_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 8388608 : i32 + %c1048576_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 1048576 : i32 + %c8_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 8 : i32 + %c8192_i32 = arith.constant {ssbuffer.block_id = 13 : i32} 8192 : i32 + %c128_i32 = arith.constant {MixUse, ssbuffer.block_id = 13 : i32} 128 : i32 + %c131072_i32 = arith.constant {Undefined, ssbuffer.block_id = 13 : i32} 131072 : i32 + %c28_i32 = arith.constant {Undefined, ssbuffer.block_id = 13 : i32} 28 : i32 + %c0_i32 = arith.constant {Undefined, ssbuffer.block_id = 13 : i32} 0 : i32 + %c1_i32 = arith.constant {Undefined, ssbuffer.block_id = 13 : i32} 1 : i32 + %c128 = arith.constant {ssbuffer.block_id = 13 : i32} 128 : index + scope.scope : () -> () { + %0 = tensor.empty() {ssbuffer.block_id = 13 : i32} : tensor<128x64xf32> + %1 = linalg.fill {ssbuffer.block_id = 13 : i32} ins(%arg11 : f32) outs(%0 : tensor<128x64xf32>) -> tensor<128x64xf32> + %alloc = memref.alloc() {ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x128xf32, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<6>, ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x128xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 1 + %alloc_2 = memref.alloc() {ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x128xf32, #hivm.address_space> + annotation.mark %alloc_2 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<7>, ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x128xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 2 + scf.for %arg18 = %arg15 to %c131072_i32 step %c28_i32 : i32 { + %2 = arith.divsi %arg18, %c128_i32 {MixUse, ssbuffer.block_id = 15 : i32} : i32 + %3 = arith.muli %2, %c8192_i32 {ssbuffer.block_id = 15 : i32} : i32 + %4 = arith.remsi %2, %c8_i32 {MixUse, ssbuffer.block_id = 15 : i32} : i32 + %5 = arith.muli %4, %c1048576_i32 {MixUse, ssbuffer.block_id = 15 : i32} : i32 + %6 = arith.divsi %2, %c8_i32 {MixUse, ssbuffer.block_id = 15 : i32} : i32 + %7 = arith.muli %6, %c8388608_i32 {MixUse, ssbuffer.block_id = 15 : i32} : i32 + %8 = arith.addi %5, %7 {MixUse, ssbuffer.block_id = 15 : i32} : i32 + %9 = arith.index_cast %8 {ssbuffer.block_id = 15 : i32} : i32 to index + %alloc_3 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_3 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + %alloc_4 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 2 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 2 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + %alloc_6 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 3 : i32} : memref<128x64xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 3 : i32} : memref<128x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 6 + %alloc_7 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 4 : i32} : memref<128x64xf32, #hivm.address_space> + annotation.mark %alloc_7 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 4 : i32} : memref<128x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 7 + %alloc_8 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 5 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_8 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<5>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 5 : i32} : memref<128x128xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 9 + scf.for %arg19 = %c0_i32 to %c64_i32 step %c1_i32 : i32 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 6 + %memspacecast_11 = memref.memory_space_cast %alloc_6 {ssbuffer.block_id = 19 : i32, ssbuffer.crossCoreDeps = [3 : i32, 0 : i32], ssbuffer.transfer_id = 3 : i32} : memref<128x64xf32, #hivm.address_space> to memref<128x64xf32> + %27 = bufferization.to_tensor %memspacecast_11 restrict writable {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 3 : i32} : memref<128x64xf32> to tensor<128x64xf32> + %28 = arith.muli %arg19, %c128_i32 {MixUse, ssbuffer.block_id = 19 : i32} : i32 + %29 = arith.index_cast %28 {ssbuffer.block_id = 19 : i32} : i32 to index + %30 = arith.index_cast %3 {ssbuffer.block_id = 19 : i32} : i32 to index + %31 = arith.addi %30, %29 {ssbuffer.block_id = 19 : i32} : index + %reinterpret_cast_12 = memref.reinterpret_cast %arg9 to offset: [%31], sizes: [128], strides: [1] {ssbuffer.block_id = 19 : i32} : memref to memref<128xf32, strided<[1], offset: ?>> + %alloc_13 = memref.alloc() {ssbuffer.block_id = 19 : i32} : memref<128xf32> + memref.copy %reinterpret_cast_12, %alloc_13 {ssbuffer.block_id = 19 : i32} : memref<128xf32, strided<[1], offset: ?>> to memref<128xf32> + %32 = bufferization.to_tensor %alloc_13 restrict writable {ssbuffer.block_id = 19 : i32} : memref<128xf32> to tensor<128xf32> + %reinterpret_cast_14 = memref.reinterpret_cast %arg10 to offset: [%31], sizes: [128], strides: [1] {ssbuffer.block_id = 19 : i32} : memref to memref<128xf32, strided<[1], offset: ?>> + %alloc_15 = memref.alloc() {ssbuffer.block_id = 19 : i32} : memref<128xf32> + memref.copy %reinterpret_cast_14, %alloc_15 {ssbuffer.block_id = 19 : i32} : memref<128xf32, strided<[1], offset: ?>> to memref<128xf32> + %33 = bufferization.to_tensor %alloc_15 restrict writable {ssbuffer.block_id = 19 : i32} : memref<128xf32> to tensor<128xf32> + %broadcasted = linalg.broadcast ins(%32 : tensor<128xf32>) outs(%0 : tensor<128x64xf32>) dimensions = [1] {ssbuffer.block_id = 19 : i32} + %34 = arith.mulf %27, %1 {DataUse, ssbuffer.block_id = 19 : i32} : tensor<128x64xf32> + %35 = arith.subf %34, %broadcasted {DataUse, ssbuffer.block_id = 19 : i32} : tensor<128x64xf32> + %36 = math.exp %35 {DataUse, ssbuffer.block_id = 19 : i32} : tensor<128x64xf32> + %37 = arith.truncf %36 {DataUse, ssbuffer.block_id = 19 : i32} : tensor<128x64xf32> to tensor<128x64xf16> + %reshape = tensor.reshape %37(%cst_0) {ssbuffer.block_id = 19 : i32} : (tensor<128x64xf16>, tensor<3xi64>) -> tensor<128x4x16xf16> + %38 = tensor.empty() {ssbuffer.block_id = 19 : i32} : tensor<4x128x16xf16> + %transposed = linalg.transpose ins(%reshape : tensor<128x4x16xf16>) outs(%38 : tensor<4x128x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 19 : i32} + %reshape_16 = tensor.reshape %transposed(%cst) {ssbuffer.block_id = 19 : i32} : (tensor<4x128x16xf16>, tensor<4xi64>) -> tensor<4x8x16x16xf16> + hivm.hir.sync_block_wait {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 3 + hivm.hir.copy ins(%reshape_16 : tensor<4x8x16x16xf16>) outs(%alloc_3 : memref<4x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 19 : i32, ssbuffer.crossCoreDeps = [0 : i32, 1 : i32], ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 3 + hivm.hir.sync_block_set {ssbuffer.block_id = 19 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 6 + hivm.hir.sync_block_wait {ssbuffer.block_id = 21 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 7 + %memspacecast_17 = memref.memory_space_cast %alloc_7 {ssbuffer.block_id = 21 : i32, ssbuffer.crossCoreDeps = [4 : i32, 0 : i32], ssbuffer.transfer_id = 4 : i32} : memref<128x64xf32, #hivm.address_space> to memref<128x64xf32> + %39 = bufferization.to_tensor %memspacecast_17 restrict writable {ssbuffer.block_id = 21 : i32, ssbuffer.transfer_id = 4 : i32} : memref<128x64xf32> to tensor<128x64xf32> + %40 = arith.extf %37 {DataUse, ssbuffer.block_id = 21 : i32} : tensor<128x64xf16> to tensor<128x64xf32> + %broadcasted_18 = linalg.broadcast ins(%33 : tensor<128xf32>) outs(%0 : tensor<128x64xf32>) dimensions = [1] {ssbuffer.block_id = 21 : i32} + %41 = arith.subf %39, %broadcasted_18 {DataUse, ssbuffer.block_id = 21 : i32} : tensor<128x64xf32> + %42 = arith.mulf %40, %41 {DataUse, ssbuffer.block_id = 21 : i32} : tensor<128x64xf32> + %43 = arith.mulf %42, %1 {DataUse, ssbuffer.block_id = 21 : i32} : tensor<128x64xf32> + %44 = arith.truncf %43 {DataUse, ssbuffer.block_id = 21 : i32} : tensor<128x64xf32> to tensor<128x64xf16> + %reshape_19 = tensor.reshape %44(%cst_0) {ssbuffer.block_id = 21 : i32} : (tensor<128x64xf16>, tensor<3xi64>) -> tensor<128x4x16xf16> + %45 = tensor.empty() {ssbuffer.block_id = 21 : i32} : tensor<4x128x16xf16> + %transposed_20 = linalg.transpose ins(%reshape_19 : tensor<128x4x16xf16>) outs(%45 : tensor<4x128x16xf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 21 : i32} + %reshape_21 = tensor.reshape %transposed_20(%cst) {ssbuffer.block_id = 21 : i32} : (tensor<4x128x16xf16>, tensor<4xi64>) -> tensor<4x8x16x16xf16> + hivm.hir.sync_block_wait {ssbuffer.block_id = 21 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 4 + hivm.hir.copy ins(%reshape_21 : tensor<4x8x16x16xf16>) outs(%alloc_4 : memref<4x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 21 : i32, ssbuffer.crossCoreDeps = [1 : i32, 1 : i32], ssbuffer.transfer_id = 1 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 21 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 4 + hivm.hir.sync_block_wait {ssbuffer.block_id = 21 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + hivm.hir.copy ins(%reshape_21 : tensor<4x8x16x16xf16>) outs(%alloc_5 : memref<4x8x16x16xf16, #hivm.address_space>) {ssbuffer.block_id = 21 : i32, ssbuffer.crossCoreDeps = [2 : i32, 1 : i32], ssbuffer.transfer_id = 2 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 21 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + hivm.hir.sync_block_set {ssbuffer.block_id = 21 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 7 + hivm.hir.sync_block_wait {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 9 + %memspacecast_22 = memref.memory_space_cast %alloc_8 {ssbuffer.block_id = 22 : i32, ssbuffer.crossCoreDeps = [5 : i32, 0 : i32], ssbuffer.transfer_id = 5 : i32} : memref<128x128xf32, #hivm.address_space> to memref<128x128xf32> + %46 = bufferization.to_tensor %memspacecast_22 restrict writable {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 5 : i32} : memref<128x128xf32> to tensor<128x128xf32> + %47 = arith.muli %arg19, %c128_i32 {MixUse, ssbuffer.block_id = 22 : i32} : i32 + %48 = arith.index_cast %47 {ssbuffer.block_id = 22 : i32} : i32 to index + %49 = arith.muli %48, %c128 {ssbuffer.block_id = 22 : i32} : index + %50 = arith.addi %9, %49 {ssbuffer.block_id = 22 : i32} : index + %reinterpret_cast_23 = memref.reinterpret_cast %arg6 to offset: [%50], sizes: [128, 128], strides: [128, 1] {ssbuffer.block_id = 22 : i32} : memref to memref<128x128xf16, strided<[128, 1], offset: ?>> + %51 = arith.truncf %46 {DataUse, ssbuffer.block_id = 22 : i32} : tensor<128x128xf32> to tensor<128x128xf16> + hivm.hir.store ins(%51 : tensor<128x128xf16>) outs(%reinterpret_cast_23 : memref<128x128xf16, strided<[128, 1], offset: ?>>) {ssbuffer.block_id = 22 : i32} atomic = + hivm.hir.sync_block_set {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 9 + } {DataUse, ssbuffer.block_id = 23 : i32, ssbuffer.main_loop = 0 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 4 + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 3 + hivm.hir.sync_block_wait {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 2 + %memspacecast = memref.memory_space_cast %alloc_2 {ssbuffer.block_id = 17 : i32, ssbuffer.crossCoreDeps = [7 : i32, 0 : i32], ssbuffer.transfer_id = 7 : i32} : memref<64x128xf32, #hivm.address_space> to memref<64x128xf32> + %10 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x128xf32> to tensor<64x128xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 1 + %memspacecast_9 = memref.memory_space_cast %alloc {ssbuffer.block_id = 17 : i32, ssbuffer.crossCoreDeps = [6 : i32, 0 : i32], ssbuffer.transfer_id = 6 : i32} : memref<64x128xf32, #hivm.address_space> to memref<64x128xf32> + %11 = bufferization.to_tensor %memspacecast_9 restrict writable {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x128xf32> to tensor<64x128xf32> + %12 = arith.divsi %arg18, %c128_i32 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %13 = arith.remsi %12, %c8_i32 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %14 = arith.muli %13, %c1048576_i32 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %15 = arith.divsi %12, %c8_i32 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %16 = arith.muli %15, %c8388608_i32 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %17 = arith.addi %14, %16 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %18 = arith.index_cast %17 {ssbuffer.block_id = 17 : i32} : i32 to index + %19 = arith.muli %12, %c128_i32 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %20 = arith.subi %arg18, %19 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %21 = arith.muli %20, %c64_i32 {MixUse, ssbuffer.block_id = 17 : i32} : i32 + %22 = arith.index_cast %21 {ssbuffer.block_id = 17 : i32} : i32 to index + %23 = arith.muli %22, %c128 {ssbuffer.block_id = 17 : i32} : index + %24 = arith.addi %18, %23 {ssbuffer.block_id = 17 : i32} : index + %reinterpret_cast = memref.reinterpret_cast %arg7 to offset: [%24], sizes: [64, 128], strides: [128, 1] {ssbuffer.block_id = 17 : i32} : memref to memref<64x128xf16, strided<[128, 1], offset: ?>> + %reinterpret_cast_10 = memref.reinterpret_cast %arg8 to offset: [%24], sizes: [64, 128], strides: [128, 1] {ssbuffer.block_id = 17 : i32} : memref to memref<64x128xf16, strided<[128, 1], offset: ?>> + %25 = arith.truncf %11 {DataUse, ssbuffer.block_id = 17 : i32} : tensor<64x128xf32> to tensor<64x128xf16> + %26 = arith.truncf %10 {DataUse, ssbuffer.block_id = 17 : i32} : tensor<64x128xf32> to tensor<64x128xf16> + bufferization.materialize_in_destination %25 in writable %reinterpret_cast {ssbuffer.block_id = 17 : i32} : (tensor<64x128xf16>, memref<64x128xf16, strided<[128, 1], offset: ?>>) -> () + bufferization.materialize_in_destination %26 in writable %reinterpret_cast_10 {ssbuffer.block_id = 17 : i32} : (tensor<64x128xf16>, memref<64x128xf16, strided<[128, 1], offset: ?>>) -> () + hivm.hir.sync_block_set {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 1 + hivm.hir.sync_block_set {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 2 + } {Undefined, ssbuffer.block_id = 24 : i32} + scope.return + } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + scope.scope : () -> () { + %0 = tensor.empty() {ssbuffer.block_id = 1 : i32} : tensor<64x128xf32> + %1 = linalg.fill {ssbuffer.block_id = 1 : i32} ins(%cst_1 : f32) outs(%0 : tensor<64x128xf32>) -> tensor<64x128xf32> + %2 = tensor.empty() {ssbuffer.block_id = 1 : i32} : tensor<128x128xf32> + %3 = linalg.fill {ssbuffer.block_id = 1 : i32} ins(%cst_1 : f32) outs(%2 : tensor<128x128xf32>) -> tensor<128x128xf32> + %4 = tensor.empty() {ssbuffer.block_id = 1 : i32} : tensor<128x64xf32> + %5 = linalg.fill {ssbuffer.block_id = 1 : i32} ins(%cst_1 : f32) outs(%4 : tensor<128x64xf32>) -> tensor<128x64xf32> + %alloc = memref.alloc() {ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x128xf32, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<6>, ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x128xf32, #hivm.address_space> + %alloc_2 = memref.alloc() {ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x128xf32, #hivm.address_space> + annotation.mark %alloc_2 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<7>, ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x128xf32, #hivm.address_space> + scf.for %arg18 = %arg15 to %c131072_i32 step %c28_i32 : i32 { + %6 = arith.divsi %arg18, %c128_i32 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %7 = arith.muli %6, %c128_i32 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %8 = arith.subi %arg18, %7 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %9 = arith.remsi %6, %c8_i32 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %10 = arith.muli %9, %c1048576_i32 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %11 = arith.divsi %6, %c8_i32 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %12 = arith.muli %11, %c8388608_i32 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %13 = arith.addi %10, %12 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %14 = arith.muli %8, %c64_i32 {MixUse, ssbuffer.block_id = 2 : i32} : i32 + %15 = arith.index_cast %13 {ssbuffer.block_id = 2 : i32} : i32 to index + %16 = arith.index_cast %14 {ssbuffer.block_id = 2 : i32} : i32 to index + %17 = arith.muli %16, %c128 {ssbuffer.block_id = 2 : i32} : index + %18 = arith.addi %15, %17 {ssbuffer.block_id = 2 : i32} : index + %reinterpret_cast = memref.reinterpret_cast %arg3 to offset: [%18], sizes: [64, 128], strides: [128, 1] {ssbuffer.block_id = 2 : i32} : memref to memref<64x128xf16, strided<[128, 1], offset: ?>> + %alloc_3 = memref.alloc() {ssbuffer.block_id = 2 : i32} : memref<64x128xf16> + memref.copy %reinterpret_cast, %alloc_3 {ssbuffer.block_id = 2 : i32} : memref<64x128xf16, strided<[128, 1], offset: ?>> to memref<64x128xf16> + %19 = bufferization.to_tensor %alloc_3 restrict writable {ssbuffer.block_id = 2 : i32} : memref<64x128xf16> to tensor<64x128xf16> + %reinterpret_cast_4 = memref.reinterpret_cast %arg4 to offset: [%18], sizes: [64, 128], strides: [128, 1] {ssbuffer.block_id = 2 : i32} : memref to memref<64x128xf16, strided<[128, 1], offset: ?>> + %alloc_5 = memref.alloc() {ssbuffer.block_id = 2 : i32} : memref<64x128xf16> + memref.copy %reinterpret_cast_4, %alloc_5 {ssbuffer.block_id = 2 : i32} : memref<64x128xf16, strided<[128, 1], offset: ?>> to memref<64x128xf16> + %20 = bufferization.to_tensor %alloc_5 restrict writable {ssbuffer.block_id = 2 : i32} : memref<64x128xf16> to tensor<64x128xf16> + %21 = tensor.empty() {ssbuffer.block_id = 2 : i32} : tensor<128x64xf16> + %transposed = linalg.transpose ins(%19 : tensor<64x128xf16>) outs(%21 : tensor<128x64xf16>) permutation = [1, 0] {ssbuffer.block_id = 2 : i32} + %transposed_6 = linalg.transpose ins(%20 : tensor<64x128xf16>) outs(%21 : tensor<128x64xf16>) permutation = [1, 0] {ssbuffer.block_id = 2 : i32} + %alloc_7 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_7 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 3 + %alloc_8 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_8 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 4 + %alloc_9 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 2 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + annotation.mark %alloc_9 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 2 : i32} : memref<4x8x16x16xf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + %alloc_10 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 3 : i32} : memref<128x64xf32, #hivm.address_space> + annotation.mark %alloc_10 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 3 : i32} : memref<128x64xf32, #hivm.address_space> + %alloc_11 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 4 : i32} : memref<128x64xf32, #hivm.address_space> + annotation.mark %alloc_11 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 4 : i32} : memref<128x64xf32, #hivm.address_space> + %alloc_12 = memref.alloc() {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 5 : i32} : memref<128x128xf32, #hivm.address_space> + annotation.mark %alloc_12 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<5>, ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 5 : i32} : memref<128x128xf32, #hivm.address_space> + %22:2 = scf.for %arg19 = %c0_i32 to %c64_i32 step %c1_i32 iter_args(%arg20 = %1, %arg21 = %1) -> (tensor<64x128xf32>, tensor<64x128xf32>) : i32 { + %23 = arith.muli %arg19, %c128_i32 {MixUse, ssbuffer.block_id = 4 : i32} : i32 + %24 = arith.index_cast %23 {ssbuffer.block_id = 4 : i32} : i32 to index + %25 = arith.muli %24, %c128 {ssbuffer.block_id = 4 : i32} : index + %26 = arith.addi %15, %25 {ssbuffer.block_id = 4 : i32} : index + %reinterpret_cast_13 = memref.reinterpret_cast %arg2 to offset: [%26], sizes: [128, 128], strides: [128, 1] {ssbuffer.block_id = 4 : i32} : memref to memref<128x128xf16, strided<[128, 1], offset: ?>> + %alloc_14 = memref.alloc() {ssbuffer.block_id = 4 : i32} : memref<128x128xf16> + memref.copy %reinterpret_cast_13, %alloc_14 {ssbuffer.block_id = 4 : i32} : memref<128x128xf16, strided<[128, 1], offset: ?>> to memref<128x128xf16> + %27 = bufferization.to_tensor %alloc_14 restrict writable {ssbuffer.block_id = 4 : i32} : memref<128x128xf16> to tensor<128x128xf16> + %28 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 4 : i32, ssbuffer.loop_carried_l0c} ins(%27, %transposed : tensor<128x128xf16>, tensor<128x64xf16>) outs(%5 : tensor<128x64xf32>) -> tensor<128x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 6 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 4 : i32, ssbuffer.crossCoreDeps = [3 : i32, 1 : i32], ssbuffer.transfer_id = 3 : i32} ins(%28 : tensor<128x64xf32>) outs(%alloc_10 : memref<128x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 6 + %reinterpret_cast_15 = memref.reinterpret_cast %arg5 to offset: [%26], sizes: [128, 128], strides: [128, 1] {ssbuffer.block_id = 6 : i32} : memref to memref<128x128xf16, strided<[128, 1], offset: ?>> + %alloc_16 = memref.alloc() {ssbuffer.block_id = 6 : i32} : memref<128x128xf16> + memref.copy %reinterpret_cast_15, %alloc_16 {ssbuffer.block_id = 6 : i32} : memref<128x128xf16, strided<[128, 1], offset: ?>> to memref<128x128xf16> + %29 = bufferization.to_tensor %alloc_16 restrict writable {ssbuffer.block_id = 6 : i32} : memref<128x128xf16> to tensor<128x128xf16> + %30 = tensor.empty() {ssbuffer.block_id = 6 : i32} : tensor<64x128xf16> + hivm.hir.sync_block_wait {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 3 + %31 = hivm.hir.convert_layout %alloc_7 output_shape [128, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 6 : i32, ssbuffer.crossCoreDeps = [0 : i32, 0 : i32], ssbuffer.transfer_id = 0 : i32} : (memref<4x8x16x16xf16, #hivm.address_space>) -> memref<128x64xf16, #hivm.address_space> + %memspacecast = memref.memory_space_cast %31 {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf16, #hivm.address_space> to memref<128x64xf16> + %32 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf16> to tensor<128x64xf16> + %transposed_17 = linalg.transpose ins(%32 : tensor<128x64xf16>) outs(%30 : tensor<64x128xf16>) permutation = [1, 0] {ssbuffer.block_id = 6 : i32} + %33 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 6 : i32, ssbuffer.loop_carried_l0c} ins(%transposed_17, %29 : tensor<64x128xf16>, tensor<128x128xf16>) outs(%arg21 : tensor<64x128xf32>) -> tensor<64x128xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 3 + %34 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 8 : i32, ssbuffer.loop_carried_l0c} ins(%29, %transposed_6 : tensor<128x128xf16>, tensor<128x64xf16>) outs(%5 : tensor<128x64xf32>) -> tensor<128x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 7 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 8 : i32, ssbuffer.crossCoreDeps = [4 : i32, 1 : i32], ssbuffer.transfer_id = 4 : i32} ins(%34 : tensor<128x64xf32>) outs(%alloc_11 : memref<128x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 8 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 7 + hivm.hir.sync_block_wait {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 4 + %35 = hivm.hir.convert_layout %alloc_8 output_shape [128, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 10 : i32, ssbuffer.crossCoreDeps = [1 : i32, 0 : i32], ssbuffer.transfer_id = 1 : i32} : (memref<4x8x16x16xf16, #hivm.address_space>) -> memref<128x64xf16, #hivm.address_space> + %memspacecast_18 = memref.memory_space_cast %35 {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x64xf16, #hivm.address_space> to memref<128x64xf16> + %36 = bufferization.to_tensor %memspacecast_18 restrict writable {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x64xf16> to tensor<128x64xf16> + %transposed_19 = linalg.transpose ins(%36 : tensor<128x64xf16>) outs(%30 : tensor<64x128xf16>) permutation = [1, 0] {ssbuffer.block_id = 10 : i32} + %37 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 10 : i32, ssbuffer.loop_carried_l0c} ins(%transposed_19, %27 : tensor<64x128xf16>, tensor<128x128xf16>) outs(%arg20 : tensor<64x128xf32>) -> tensor<64x128xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 4 + hivm.hir.sync_block_wait {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + %38 = hivm.hir.convert_layout %alloc_9 output_shape [128, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 12 : i32, ssbuffer.crossCoreDeps = [2 : i32, 0 : i32], ssbuffer.transfer_id = 2 : i32} : (memref<4x8x16x16xf16, #hivm.address_space>) -> memref<128x64xf16, #hivm.address_space> + %memspacecast_20 = memref.memory_space_cast %38 {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x64xf16, #hivm.address_space> to memref<128x64xf16> + %39 = bufferization.to_tensor %memspacecast_20 restrict writable {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x64xf16> to tensor<128x64xf16> + %40 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 12 : i32, ssbuffer.loop_carried_l0c} ins(%39, %19 : tensor<128x64xf16>, tensor<64x128xf16>) outs(%3 : tensor<128x128xf32>) -> tensor<128x128xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 5 + hivm.hir.sync_block_wait {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 9 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 12 : i32, ssbuffer.crossCoreDeps = [5 : i32, 1 : i32], ssbuffer.transfer_id = 5 : i32} ins(%40 : tensor<128x128xf32>) outs(%alloc_12 : memref<128x128xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 9 + scf.yield {Undefined} %37, %33 : tensor<64x128xf32>, tensor<64x128xf32> + } {DataUse, ssbuffer.block_id = 23 : i32, ssbuffer.main_loop = 0 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 9 + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 7 + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 6 + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 1 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 23 : i32, ssbuffer.crossCoreDeps = [6 : i32, 1 : i32], ssbuffer.transfer_id = 6 : i32} ins(%22#0 : tensor<64x128xf32>) outs(%alloc : memref<64x128xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 2 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 23 : i32, ssbuffer.crossCoreDeps = [7 : i32, 1 : i32], ssbuffer.transfer_id = 7 : i32} ins(%22#1 : tensor<64x128xf32>) outs(%alloc_2 : memref<64x128xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 2 + } {Undefined, ssbuffer.block_id = 24 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.block_id = 24 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 1 + scope.return + } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-whileop.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-whileop.mlir new file mode 100644 index 0000000000..bd7a3825a9 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AllocMultiCache/Outer-scope-whileop.mlir @@ -0,0 +1,162 @@ +// RUN: triton-opt --add_multi_buffer_outer_scope %s | FileCheck %s + +// Test: C→V transfer with scf.while (main_loop) in double-buffer mode. +// - pin inter_core_buf_count=2: polling branch (scf.if dispatch) now +// supported for scf.while, as the polling condition is inserted at +// while-body start (dominance fix); supersedes the pin-to-1 workaround +// - wrong insertion order still rejected by verifier +// (operand-defined-after-use) +// - CHECK locks double-buffer output: two mutually-exclusive transfer +// groups (even/odd flags) + double-buffer selection + +// CHECK-LABEL: func.func @tc_while_ctov_sender +// CHECK: tightly_coupled_buffer +// while gets injected counter iter_arg +// CHECK: scf.while {{.*}} : (i32, i32) -> (i32, i32) +// polling condition computed at body start +// CHECK: arith.remsi +// CHECK: arith.cmpi eq +// two mutually-exclusive transfer groups: even flag=3 / odd flag=7 +// CHECK: scf.if +// CHECK: sync_block_wait {{.*}} flag = 3 +// CHECK: } else { +// CHECK: sync_block_wait {{.*}} flag = 7 +// CHECK: ssbuffer.cross_buffer +// double-buffer selection +// CHECK: memref.memory_space_cast +// CHECK: } else { +// CHECK: memref.memory_space_cast +// CHECK: sync_block_set {{.*}} flag = 3 +// CHECK: } else { +// CHECK: sync_block_set {{.*}} flag = 7 +// CHECK: ssbuffer.iterCounter +// CHECK: ssbuffer.main_loop +// cross-core initial signals for both flags + CUBE-side waits +// CHECK: sync_block_set {{.*}} flag = 3 +// CHECK: sync_block_set {{.*}} flag = 7 +// CHECK: sync_block_wait {{.*}} flag = 3 +// CHECK: sync_block_wait {{.*}} flag = 7 +// CUBE side: fixpipe double-buffer selection +// CHECK: hivm.hir.fixpipe +// CHECK: } else { +// CHECK: hivm.hir.fixpipe + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, ssbuffer.inter_core_buf_count = 2 : i32} { +func.func @tc_while_ctov_sender() { + %c0_i32 = arith.constant 0 : i32 + %c100_i32 = arith.constant 100 : i32 + %c1_i32 = arith.constant 1 : i32 + // --- VECTOR scope (receiver for C→V: memspace_cast reads from ub) --- + scope.scope : () -> () { + %buf_ub = memref.alloc() {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128xf16, #hivm.address_space> + annotation.mark %buf_ub {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128xf16, #hivm.address_space> + scf.while (%iter = %c0_i32) : (i32) -> i32 { + %cond = arith.cmpi slt, %iter, %c100_i32 : i32 + scf.condition(%cond) %iter : i32 + } do { + ^bb0(%iter: i32): + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 3 + %buf = memref.memory_space_cast %buf_ub {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128xf16, #hivm.address_space> to memref<128xf16> + %t = bufferization.to_tensor %buf restrict writable : memref<128xf16> to tensor<128xf16> + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 3 + %next_iter = arith.addi %iter, %c1_i32 : i32 + scf.yield %next_iter : i32 + } attributes {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 3 + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 3 + // --- CUBE scope (sender for C→V: fixpipe writes to ub) --- + scope.scope : () -> () { + %buf_cc = memref.alloc() {ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128xf16, #hivm.address_space> + %buf_ub = memref.alloc() {ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128xf16, #hivm.address_space> + annotation.mark %buf_ub {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128xf16, #hivm.address_space> + scf.for %i = %c0_i32 to %c100_i32 step %c1_i32 iter_args() -> () : i32 { + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 3 + hivm.hir.fixpipe {ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32} ins(%buf_cc : memref<128xf16, #hivm.address_space>) outs(%buf_ub : memref<128xf16, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 20 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 3 + scf.yield + } {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + + +// Test: Both sender and receiver use scf.while with main_loop +// - both whiles get counter injection + two mutually-exclusive groups + +// CHECK-LABEL: func.func @tc_while_both_sides +// CHECK: tightly_coupled_buffer +// VECTOR while: counter injection + two mutually-exclusive groups (flag 6/8) +// CHECK: scf.while {{.*}} : (i32, i32) -> (i32, i32) +// CHECK: arith.remsi +// CHECK: arith.cmpi eq +// CHECK: scf.if +// CHECK: sync_block_wait {{.*}} flag = 6 +// CHECK: } else { +// CHECK: sync_block_wait {{.*}} flag = 8 +// CHECK: ssbuffer.cross_buffer +// CHECK: memref.memory_space_cast +// CHECK: } else { +// CHECK: memref.memory_space_cast +// CHECK: sync_block_set {{.*}} flag = 6 +// CHECK: } else { +// CHECK: sync_block_set {{.*}} flag = 8 +// CHECK: ssbuffer.iterCounter +// CHECK: sync_block_set {{.*}} flag = 6 +// CHECK: sync_block_set {{.*}} flag = 8 +// CHECK: sync_block_wait {{.*}} flag = 6 +// CHECK: sync_block_wait {{.*}} flag = 8 +// CUBE while: same counter injection + fixpipe double-buffer selection +// CHECK: scf.while {{.*}} : (i32, i32) -> (i32, i32) +// CHECK: hivm.hir.fixpipe +// CHECK: } else { +// CHECK: hivm.hir.fixpipe +// CHECK: ssbuffer.iterCounter + +func.func @tc_while_both_sides() { + %c0_i32 = arith.constant 0 : i32 + %c100_i32 = arith.constant 100 : i32 + %c1_i32 = arith.constant 1 : i32 + // --- VECTOR scope --- + scope.scope : () -> () { + %buf_ub = memref.alloc() {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128xf16, #hivm.address_space> + annotation.mark %buf_ub {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128xf16, #hivm.address_space> + scf.while (%iter = %c0_i32) : (i32) -> i32 { + %cond = arith.cmpi slt, %iter, %c100_i32 : i32 + scf.condition(%cond) %iter : i32 + } do { + ^bb0(%iter: i32): + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 6 + %buf = memref.memory_space_cast %buf_ub {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128xf16, #hivm.address_space> to memref<128xf16> + %t = bufferization.to_tensor %buf restrict writable : memref<128xf16> to tensor<128xf16> + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 6 + %next_iter = arith.addi %iter, %c1_i32 : i32 + scf.yield %next_iter : i32 + } attributes {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 6 + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 40 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 6 + // --- CUBE scope --- + scope.scope : () -> () { + %buf_cc = memref.alloc() {ssbuffer.block_id = 40 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128xf16, #hivm.address_space> + %buf_ub = memref.alloc() {ssbuffer.block_id = 40 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128xf16, #hivm.address_space> + annotation.mark %buf_ub {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 40 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128xf16, #hivm.address_space> + scf.while (%iter = %c0_i32) : (i32) -> i32 { + %cond = arith.cmpi slt, %iter, %c100_i32 : i32 + scf.condition(%cond) %iter : i32 + } do { + ^bb0(%iter: i32): + hivm.hir.sync_block_wait {ssbuffer.analyze_flag_id, ssbuffer.block_id = 40 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 6 + hivm.hir.fixpipe {ssbuffer.block_id = 40 : i32, ssbuffer.transfer_id = 2 : i32} ins(%buf_cc : memref<128xf16, #hivm.address_space>) outs(%buf_ub : memref<128xf16, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.analyze_flag_id, ssbuffer.block_id = 40 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 6 + %next_iter = arith.addi %iter, %c1_i32 : i32 + scf.yield %next_iter : i32 + } attributes {ssbuffer.main_loop = 1 : i64} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-add-from-matmul.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-add-from-matmul.mlir new file mode 100644 index 0000000000..02e81739c9 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-add-from-matmul.mlir @@ -0,0 +1,161 @@ +// RUN: triton-opt --analyze-data-flow --verify-diagnostics %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_add_from_matmul(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg9: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg10: i32 {tt.divisibility = 16 : i32}, %arg11: i32 {tt.divisibility = 16 : i32}, %arg12: i32 {tt.divisibility = 16 : i32}, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32, %arg17: i32, %arg18: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c32 = arith.constant {ssbuffer.block_id = 8 : i32} 32 : index + %cst = arith.constant {ssbuffer.block_id = 14 : i32} 0.000000e+00 : f32 + %c1_i32 = arith.constant {Undefined, ssbuffer.block_id = 14 : i32} 1 : i32 + %c0_i32 = arith.constant {Undefined, ssbuffer.block_id = 14 : i32} 0 : i32 + %c63_i32 = arith.constant {ssbuffer.block_id = 14 : i32} 63 : i32 + %c32_i32 = arith.constant {ssbuffer.block_id = 14 : i32} 32 : i32 + %c8_i32 = arith.constant {ssbuffer.block_id = 14 : i32} 8 : i32 + %c64_i32 = arith.constant {ssbuffer.block_id = 14 : i32} 64 : i32 + %cst_0 = arith.constant {ssbuffer.block_id = 14 : i32} 1.000000e+00 : f32 + %c64 = arith.constant {ssbuffer.block_id = 14 : i32} 64 : index + %cst_1 = arith.constant {ssbuffer.block_id = 14 : i32} 0.000000e+00 : bf16 + %c128 = arith.constant {ssbuffer.block_id = 14 : i32} 128 : index + scope.scope : () -> () { + %0 = tensor.empty() {ssbuffer.block_id = 14 : i32} : tensor<128x64xf32> + %1 = linalg.fill {ssbuffer.block_id = 14 : i32} ins(%cst : f32) outs(%0 : tensor<128x64xf32>) -> tensor<128x64xf32> + %2 = linalg.fill {ssbuffer.block_id = 14 : i32} ins(%cst_0 : f32) outs(%0 : tensor<128x64xf32>) -> tensor<128x64xf32> + %alloc = memref.alloc() {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf32, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + %23 = scf.for %arg19 = %c0_i32 to %c32_i32 step %c1_i32 iter_args(%arg20 = %1) -> (tensor<128x64xf32>) : i32 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + %memspacecast_20 = memref.memory_space_cast %alloc {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf32, #hivm.address_space> to memref<128x64xf32> + %60 = bufferization.to_tensor %memspacecast_20 restrict writable {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf32> to tensor<128x64xf32> + %61 = arith.addf %60, %arg20 {ssbuffer.block_id = 11 : i32, ssbuffer.add_from_matmul} : tensor<128x64xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + scf.yield %61 : tensor<128x64xf32> + } {DataUse, ssbuffer.block_id = 15 : i32, ssbuffer.main_loop = 0 : i32} + %alloc_1 = memref.alloc() {ssbuffer.block_id = 16 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf32, #hivm.address_space> + annotation.mark %alloc_1 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 16 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 16 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 2 + %24 = scf.for %arg19 = %c0_i32 to %c32_i32 step %c1_i32 iter_args(%arg20 = %1) -> (tensor<128x64xf32>) : i32 { + %500 = math.exp %2 {ssbuffer.block_id = 12 : i32} : tensor<128x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 13 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 2 + %memspacecast_200 = memref.memory_space_cast %alloc_1 {ssbuffer.block_id = 13 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x64xf32, #hivm.address_space> to memref<128x64xf32> + %600 = bufferization.to_tensor %memspacecast_200 restrict writable {ssbuffer.block_id = 13 : i32, ssbuffer.transfer_id = 2 : i32} : memref<128x64xf32> to tensor<128x64xf32> + %610 = arith.addf %600, %arg20 {ssbuffer.block_id = 13 : i32, ssbuffer.add_from_matmul} : tensor<128x64xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 13 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 2 + scf.yield %610 : tensor<128x64xf32> + } {DataUse, ssbuffer.block_id = 16 : i32, ssbuffer.main_loop = 1 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + scope.scope : () -> () { + %20 = arith.addi %arg11, %c63_i32 {ssbuffer.block_id = 8 : i32} : i32 + %21 = arith.divsi %20, %c64_i32 {ssbuffer.block_id = 8 : i32} : i32 + %22 = arith.muli %21, %c8_i32 {ssbuffer.block_id = 8 : i32} : i32 + %23 = arith.divsi %arg16, %22 {ssbuffer.block_id = 8 : i32} : i32 + %24 = arith.muli %23, %c8_i32 {ssbuffer.block_id = 8 : i32} : i32 + %35 = arith.index_cast %arg11 {ssbuffer.block_id = 7 : i32} : i32 to index + %36 = arith.maxsi %35, %35 {ssbuffer.block_id = 7 : i32} : index + %38 = arith.subi %36, %35 {ssbuffer.block_id = 7 : i32} : index + %alloc = memref.alloc() {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf32, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 0 : i32} : memref<128x64xf32, #hivm.address_space> + scf.for %arg19 = %c0_i32 to %c32_i32 step %c1_i32 : i32 { + %41 = arith.muli %arg19, %c32_i32 {ssbuffer.block_id = 6 : i32} : i32 + %42 = arith.index_cast %24 {ssbuffer.block_id = 6 : i32} : i32 to index + %43 = arith.index_cast %arg12 {ssbuffer.block_id = 6 : i32} : i32 to index + %44 = arith.index_cast %41 {ssbuffer.block_id = 6 : i32} : i32 to index + %45 = arith.addi %44, %c128 {ssbuffer.block_id = 6 : i32} : index + %46 = arith.index_cast %arg10 {ssbuffer.block_id = 6 : i32} : i32 to index + %47 = arith.maxsi %44, %46 {ssbuffer.block_id = 6 : i32} : index + %48 = arith.minsi %45, %47 {ssbuffer.block_id = 6 : i32} : index + %49 = arith.subi %48, %44 {ssbuffer.block_id = 6 : i32} : index + %50 = arith.addi %44, %c32 {ssbuffer.block_id = 6 : i32} : index + %51 = arith.maxsi %44, %43 {ssbuffer.block_id = 6 : i32} : index + %52 = arith.minsi %50, %51 {ssbuffer.block_id = 6 : i32} : index + %53 = arith.subi %52, %44 {ssbuffer.block_id = 6 : i32} : index + %54 = arith.minsi %49, %c128 {ssbuffer.block_id = 6 : i32} : index + %55 = arith.minsi %53, %c32 {ssbuffer.block_id = 6 : i32} : index + %59 = arith.minsi %38, %c64 {ssbuffer.block_id = 6 : i32} : index + %60 = arith.cmpi slt, %59, %c64 {ssbuffer.block_id = 6 : i32} : index + %61 = arith.ori %60, %60 {ssbuffer.block_id = 6 : i32} : i1 + %alloc_3 = memref.alloc() {ssbuffer.block_id = 4 : i32} : memref<128x32xbf16> + %alloc_4 = memref.alloc() {ssbuffer.block_id = 4 : i32} : memref<32x64xbf16> + scf.if %61 { + linalg.fill {ssbuffer.block_id = 4 : i32} ins(%cst_1 : bf16) outs(%alloc_4 : memref<32x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 4 : i32} + scf.if %61 { + linalg.fill {ssbuffer.block_id = 4 : i32} ins(%cst_1 : bf16) outs(%alloc_3 : memref<128x32xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 4 : i32} + %62 = arith.muli %42, %43 {ssbuffer.block_id = 4 : i32} : index + %63 = arith.addi %62, %44 {ssbuffer.block_id = 4 : i32} : index + %reinterpret_cast = memref.reinterpret_cast %arg2 to offset: [%63], sizes: [128, 32], strides: [%43, 1] {ssbuffer.block_id = 4 : i32} : memref to memref<128x32xbf16, strided<[?, 1], offset: ?>> + %subview = memref.subview %reinterpret_cast[0, 0] [%54, %55] [1, 1] {ssbuffer.block_id = 4 : i32} : memref<128x32xbf16, strided<[?, 1], offset: ?>> to memref> + %subview_5 = memref.subview %alloc_3[0, 0] [%54, %55] [1, 1] {ssbuffer.block_id = 4 : i32} : memref<128x32xbf16> to memref> + memref.copy %subview, %subview_5 {ssbuffer.block_id = 4 : i32} : memref> to memref> + %64 = bufferization.to_tensor %alloc_3 restrict writable {ssbuffer.block_id = 4 : i32} : memref<128x32xbf16> to tensor<128x32xbf16> + %65 = arith.muli %44, %35 {ssbuffer.block_id = 4 : i32} : index + %66 = arith.addi %65, %35 {ssbuffer.block_id = 4 : i32} : index + %reinterpret_cast_6 = memref.reinterpret_cast %arg3 to offset: [%66], sizes: [32, 64], strides: [%35, 1] {ssbuffer.block_id = 4 : i32} : memref to memref<32x64xbf16, strided<[?, 1], offset: ?>> + %subview_7 = memref.subview %reinterpret_cast_6[0, 0] [%55, %59] [1, 1] {ssbuffer.block_id = 4 : i32} : memref<32x64xbf16, strided<[?, 1], offset: ?>> to memref> + %subview_8 = memref.subview %alloc_4[0, 0] [%55, %59] [1, 1] {ssbuffer.block_id = 4 : i32} : memref<32x64xbf16> to memref> + memref.copy %subview_7, %subview_8 {ssbuffer.block_id = 4 : i32} : memref> to memref> + %67 = bufferization.to_tensor %alloc_4 restrict writable {ssbuffer.block_id = 4 : i32} : memref<32x64xbf16> to tensor<32x64xbf16> + %68 = tensor.empty() {ssbuffer.block_id = 4 : i32} : tensor<128x64xf32> + %69 = linalg.fill {ssbuffer.block_id = 4 : i32} ins(%cst : f32) outs(%68 : tensor<128x64xf32>) -> tensor<128x64xf32> + %70 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 4 : i32} ins(%64, %67 : tensor<128x32xbf16>, tensor<32x64xbf16>) outs(%69 : tensor<128x64xf32>) -> tensor<128x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 0 : i32} ins(%70 : tensor<128x64xf32>) outs(%alloc : memref<128x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 4 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + } {DataUse, ssbuffer.block_id = 15 : i32, ssbuffer.main_loop = 0 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + %alloc_1 = memref.alloc() {ssbuffer.block_id = 16 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x64xf32, #hivm.address_space> + annotation.mark %alloc_1 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 16 : i32, ssbuffer.transfer_id = 1 : i32} : memref<128x64xf32, #hivm.address_space> + scf.for %arg19 = %c0_i32 to %c32_i32 step %c1_i32 : i32 { + %410 = arith.muli %arg19, %c32_i32 {ssbuffer.block_id = 7 : i32} : i32 + %420 = arith.index_cast %24 {ssbuffer.block_id = 7 : i32} : i32 to index + %430 = arith.index_cast %arg12 {ssbuffer.block_id = 7 : i32} : i32 to index + %440 = arith.index_cast %410 {ssbuffer.block_id = 7 : i32} : i32 to index + %450 = arith.addi %440, %c128 {ssbuffer.block_id = 7 : i32} : index + %460 = arith.index_cast %arg10 {ssbuffer.block_id = 7 : i32} : i32 to index + %470 = arith.maxsi %440, %460 {ssbuffer.block_id = 7 : i32} : index + %480 = arith.minsi %450, %470 {ssbuffer.block_id = 7 : i32} : index + %490 = arith.subi %480, %440 {ssbuffer.block_id = 7 : i32} : index + %500 = arith.addi %440, %c32 {ssbuffer.block_id = 7 : i32} : index + %510 = arith.maxsi %440, %430 {ssbuffer.block_id = 7 : i32} : index + %520 = arith.minsi %500, %510 {ssbuffer.block_id = 7 : i32} : index + %530 = arith.subi %520, %440 {ssbuffer.block_id = 7 : i32} : index + %540 = arith.minsi %490, %c128 {ssbuffer.block_id = 7 : i32} : index + %550 = arith.minsi %530, %c32 {ssbuffer.block_id = 7 : i32} : index + %590 = arith.minsi %38, %c64 {ssbuffer.block_id = 7 : i32} : index + %600 = arith.cmpi slt, %590, %c64 {ssbuffer.block_id = 7 : i32} : index + %610 = arith.ori %600, %600 {ssbuffer.block_id = 7 : i32} : i1 + %alloc_30 = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<128x32xbf16> + %alloc_40 = memref.alloc() {ssbuffer.block_id = 5 : i32} : memref<32x64xbf16> + scf.if %610 { + linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst_1 : bf16) outs(%alloc_40 : memref<32x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 5 : i32} + scf.if %610 { + linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst_1 : bf16) outs(%alloc_30 : memref<128x32xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 5 : i32} + %620 = arith.muli %420, %430 {ssbuffer.block_id = 5 : i32} : index + %630 = arith.addi %620, %440 {ssbuffer.block_id = 5 : i32} : index + %reinterpret_cast0 = memref.reinterpret_cast %arg2 to offset: [%630], sizes: [128, 32], strides: [%430, 1] {ssbuffer.block_id = 5 : i32} : memref to memref<128x32xbf16, strided<[?, 1], offset: ?>> + %subview0 = memref.subview %reinterpret_cast0[0, 0] [%540, %550] [1, 1] {ssbuffer.block_id = 5 : i32} : memref<128x32xbf16, strided<[?, 1], offset: ?>> to memref> + %subview_50 = memref.subview %alloc_30[0, 0] [%540, %550] [1, 1] {ssbuffer.block_id = 5 : i32} : memref<128x32xbf16> to memref> + memref.copy %subview0, %subview_50 {ssbuffer.block_id = 5 : i32} : memref> to memref> + %640 = bufferization.to_tensor %alloc_30 restrict writable {ssbuffer.block_id = 5 : i32} : memref<128x32xbf16> to tensor<128x32xbf16> + %650 = arith.muli %440, %35 {ssbuffer.block_id = 5 : i32} : index + %660 = arith.addi %650, %35 {ssbuffer.block_id = 5 : i32} : index + %reinterpret_cast_60 = memref.reinterpret_cast %arg3 to offset: [%660], sizes: [32, 64], strides: [%35, 1] {ssbuffer.block_id = 5 : i32} : memref to memref<32x64xbf16, strided<[?, 1], offset: ?>> + %subview_70 = memref.subview %reinterpret_cast_60[0, 0] [%550, %590] [1, 1] {ssbuffer.block_id = 5 : i32} : memref<32x64xbf16, strided<[?, 1], offset: ?>> to memref> + %subview_80 = memref.subview %alloc_40[0, 0] [%550, %590] [1, 1] {ssbuffer.block_id = 5 : i32} : memref<32x64xbf16> to memref> + memref.copy %subview_70, %subview_80 {ssbuffer.block_id = 5 : i32} : memref> to memref> + %670 = bufferization.to_tensor %alloc_40 restrict writable {ssbuffer.block_id = 5 : i32} : memref<32x64xbf16> to tensor<32x64xbf16> + %680 = tensor.empty() {ssbuffer.block_id = 5 : i32} : tensor<128x64xf32> + %690 = linalg.fill {ssbuffer.block_id = 5 : i32} ins(%cst : f32) outs(%680 : tensor<128x64xf32>) -> tensor<128x64xf32> + %700 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32} ins(%640, %670 : tensor<128x32xbf16>, tensor<32x64xbf16>) outs(%690 : tensor<128x64xf32>) -> tensor<128x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32} ins(%700 : tensor<128x64xf32>) outs(%alloc_1 : memref<128x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 5 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + } {DataUse, ssbuffer.block_id = 16 : i32, ssbuffer.main_loop = 1 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 16 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-flag-id.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-flag-id.mlir new file mode 100644 index 0000000000..af13c14223 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-flag-id.mlir @@ -0,0 +1,858 @@ +// RUN: triton-opt --analyze-data-flow --verify-diagnostics %s + +#map = affine_map<(d0) -> (d0)> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @prepare_wy_repr_bwd_kernel(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg8: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg9: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 2 : i32}, %arg10: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg11: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg12: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32, %arg17: i32, %arg18: i32, %arg19: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 22 : i32} dense<[4, 4, 16, 16]> : tensor<4xi64> + %cst_0 = arith.constant {ssbuffer.block_id = 22 : i32} dense<[64, 4, 16]> : tensor<3xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 31 : i32} 0.000000e+00 : f32 + %c128_i32 = arith.constant {ssbuffer.block_id = 31 : i32} 128 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 31 : i32} 0 : i32 + %c64_i32 = arith.constant {ssbuffer.block_id = 31 : i32} 64 : i32 + %c8_i32 = arith.constant {ssbuffer.block_id = 31 : i32} 8 : i32 + %c2_i32 = arith.constant {ssbuffer.block_id = 31 : i32} 2 : i32 + %c1_i32 = arith.constant {ssbuffer.block_id = 31 : i32} 1 : i32 + %c8 = arith.constant {ssbuffer.block_id = 31 : i32} 8 : index + %c0 = arith.constant {ssbuffer.block_id = 31 : i32} 0 : index + %c64 = arith.constant {ssbuffer.block_id = 31 : i32} 64 : index + %cst_2 = arith.constant {ssbuffer.block_id = 31 : i32} 0.000000e+00 : bf16 + %c512 = arith.constant {ssbuffer.block_id = 31 : i32} 512 : index + %c1024 = arith.constant {ssbuffer.block_id = 31 : i32} 1024 : index + %c128 = arith.constant {ssbuffer.block_id = 31 : i32} 128 : index + scope.scope : () -> () { + %0 = tensor.empty() {ssbuffer.block_id = 31 : i32} : tensor<64x64xf32> + %1 = linalg.fill {ssbuffer.block_id = 31 : i32} ins(%cst_1 : f32) outs(%0 : tensor<64x64xf32>) -> tensor<64x64xf32> + %2 = tensor.empty() {ssbuffer.block_id = 31 : i32} : tensor<64xf32> + %3 = linalg.fill {ssbuffer.block_id = 31 : i32} ins(%cst_1 : f32) outs(%2 : tensor<64xf32>) -> tensor<64xf32> + %4 = arith.divsi %arg18, %c8_i32 {ssbuffer.block_id = 31 : i32} : i32 + %5 = arith.remsi %arg18, %c8_i32 {ssbuffer.block_id = 31 : i32} : i32 + %6 = arith.muli %4, %arg13 {ssbuffer.block_id = 31 : i32} : i32 + %7 = arith.muli %6, %c8_i32 {ssbuffer.block_id = 31 : i32} : i32 + %8 = arith.addi %7, %5 {ssbuffer.block_id = 31 : i32} : i32 + %9 = arith.muli %arg17, %c64_i32 {ssbuffer.block_id = 31 : i32} : i32 + %10 = arith.maxsi %9, %c0_i32 {ssbuffer.block_id = 31 : i32} : i32 + %11 = arith.index_cast %10 {ssbuffer.block_id = 31 : i32} : i32 to index + %12 = arith.muli %11, %c8 {ssbuffer.block_id = 31 : i32} : index + %13 = arith.index_cast %arg13 {ssbuffer.block_id = 31 : i32} : i32 to index + %14 = arith.divsi %12, %c8 {ssbuffer.block_id = 31 : i32} : index + %15 = arith.subi %13, %14 {ssbuffer.block_id = 31 : i32} : index + %16 = arith.maxsi %15, %c0 {ssbuffer.block_id = 31 : i32} : index + %17 = arith.minsi %16, %c64 {ssbuffer.block_id = 31 : i32} : index + %18 = arith.subi %c0_i32, %9 {ssbuffer.block_id = 31 : i32} : i32 + %19 = arith.maxsi %18, %c0_i32 {ssbuffer.block_id = 31 : i32} : i32 + %20 = arith.index_cast %19 {ssbuffer.block_id = 31 : i32} : i32 to index + %21 = arith.minsi %20, %17 {ssbuffer.block_id = 31 : i32} : index + %22 = arith.subi %17, %21 {ssbuffer.block_id = 31 : i32} : index + %23 = arith.cmpi slt, %22, %c64 {ssbuffer.block_id = 31 : i32} : index + %24 = arith.muli %8, %c128_i32 {ssbuffer.block_id = 31 : i32} : i32 + %25 = arith.index_cast %24 {ssbuffer.block_id = 31 : i32} : i32 to index + %alloc = memref.alloc() {ssbuffer.block_id = 32 : i32} : memref<64xbf16> + %alloc_3 = memref.alloc() {ssbuffer.block_id = 32 : i32} : memref<64xbf16> + scf.if %23 { + linalg.fill {ssbuffer.block_id = 32 : i32} ins(%cst_2 : bf16) outs(%alloc : memref<64xbf16>) + linalg.fill {ssbuffer.block_id = 32 : i32} ins(%cst_2 : bf16) outs(%alloc_3 : memref<64xbf16>) + } + %26 = arith.index_cast %8 {ssbuffer.block_id = 32 : i32} : i32 to index + %27 = arith.addi %12, %26 {ssbuffer.block_id = 32 : i32} : index + %reinterpret_cast = memref.reinterpret_cast %arg4 to offset: [%27], sizes: [64], strides: [8] {ssbuffer.block_id = 32 : i32} : memref to memref<64xbf16, strided<[8], offset: ?>> + %subview = memref.subview %reinterpret_cast[0] [%22] [1] {ssbuffer.block_id = 32 : i32} : memref<64xbf16, strided<[8], offset: ?>> to memref> + %subview_4 = memref.subview %alloc[%21] [%22] [1] {ssbuffer.block_id = 32 : i32} : memref<64xbf16> to memref> + memref.copy %subview, %subview_4 {ssbuffer.block_id = 32 : i32} : memref> to memref> + %28 = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 32 : i32} : memref<64xbf16> to tensor<64xbf16> + %29 = tensor.empty() {ssbuffer.block_id = 32 : i32} : tensor<64x64xbf16> + %reinterpret_cast_5 = memref.reinterpret_cast %arg5 to offset: [%27], sizes: [64], strides: [8] {ssbuffer.block_id = 32 : i32} : memref to memref<64xbf16, strided<[8], offset: ?>> + %subview_6 = memref.subview %reinterpret_cast_5[0] [%22] [1] {ssbuffer.block_id = 32 : i32} : memref<64xbf16, strided<[8], offset: ?>> to memref> + %subview_7 = memref.subview %alloc_3[%21] [%22] [1] {ssbuffer.block_id = 32 : i32} : memref<64xbf16> to memref> + memref.copy %subview_6, %subview_7 {ssbuffer.block_id = 32 : i32} : memref> to memref> + %30 = bufferization.to_tensor %alloc_3 restrict writable {ssbuffer.block_id = 32 : i32} : memref<64xbf16> to tensor<64xbf16> + %31 = math.exp %30 {ssbuffer.block_id = 32 : i32} : tensor<64xbf16> + %32 = arith.mulf %28, %31 {ssbuffer.block_id = 32 : i32} : tensor<64xbf16> + %expanded = tensor.expand_shape %32 [[0, 1]] output_shape [64, 1] {ssbuffer.block_id = 32 : i32} : tensor<64xbf16> into tensor<64x1xbf16> + %broadcasted = linalg.broadcast ins(%32 : tensor<64xbf16>) outs(%29 : tensor<64x64xbf16>) dimensions = [1] {ssbuffer.block_id = 32 : i32} + %33 = arith.extf %expanded {ssbuffer.block_id = 32 : i32} : tensor<64x1xbf16> to tensor<64x1xf32> + %collapsed = tensor.collapse_shape %33 [[0, 1]] {ssbuffer.block_id = 32 : i32} : tensor<64x1xf32> into tensor<64xf32> + %broadcasted_8 = linalg.broadcast ins(%collapsed : tensor<64xf32>) outs(%0 : tensor<64x64xf32>) dimensions = [1] {ssbuffer.block_id = 32 : i32} + %expanded_9 = tensor.expand_shape %31 [[0, 1]] output_shape [64, 1] {ssbuffer.block_id = 32 : i32} : tensor<64xbf16> into tensor<64x1xbf16> + %34 = arith.extf %expanded_9 {ssbuffer.block_id = 32 : i32} : tensor<64x1xbf16> to tensor<64x1xf32> + %collapsed_10 = tensor.collapse_shape %34 [[0, 1]] {ssbuffer.block_id = 32 : i32} : tensor<64x1xf32> into tensor<64xf32> + %broadcasted_11 = linalg.broadcast ins(%collapsed_10 : tensor<64xf32>) outs(%0 : tensor<64x64xf32>) dimensions = [1] {ssbuffer.block_id = 32 : i32} + %expanded_12 = tensor.expand_shape %28 [[0, 1]] output_shape [64, 1] {ssbuffer.block_id = 32 : i32} : tensor<64xbf16> into tensor<64x1xbf16> + %broadcasted_13 = linalg.broadcast ins(%28 : tensor<64xbf16>) outs(%29 : tensor<64x64xbf16>) dimensions = [1] {ssbuffer.block_id = 32 : i32} + %35 = arith.extf %expanded_12 {ssbuffer.block_id = 32 : i32} : tensor<64x1xbf16> to tensor<64x1xf32> + %collapsed_14 = tensor.collapse_shape %35 [[0, 1]] {ssbuffer.block_id = 32 : i32} : tensor<64x1xf32> into tensor<64xf32> + %broadcasted_15 = linalg.broadcast ins(%collapsed_14 : tensor<64xf32>) outs(%0 : tensor<64x64xf32>) dimensions = [1] {ssbuffer.block_id = 32 : i32} + %alloc_16 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_16 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + %alloc_17 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 10 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_17 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<10>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 10 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 10 : i32}[, , ] flag = 11 + %alloc_18 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 15 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_18 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<15>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 15 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 15 : i32}[, , ] flag = -1 + %alloc_19 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 16 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_19 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<16>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 16 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 16 : i32}[, , ] flag = -1 + %alloc_20 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 17 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_20 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<17>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 17 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 17 : i32}[, , ] flag = -1 + %36:3 = scf.for %arg20 = %c0_i32 to %c2_i32 step %c1_i32 iter_args(%arg21 = %1, %arg22 = %3, %arg23 = %3) -> (tensor<64x64xf32>, tensor<64xf32>, tensor<64xf32>) : i32 { + %72 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 21 : i32} : i32 + %73 = arith.maxsi %72, %c0_i32 {ssbuffer.block_id = 21 : i32} : i32 + %74 = arith.index_cast %73 {ssbuffer.block_id = 21 : i32} : i32 to index + %75 = arith.muli %11, %c1024 {ssbuffer.block_id = 21 : i32} : index + %76 = arith.addi %75, %25 {ssbuffer.block_id = 21 : i32} : index + %77 = arith.addi %76, %74 {ssbuffer.block_id = 21 : i32} : index + %78 = arith.subi %77, %25 {ssbuffer.block_id = 21 : i32} : index + %79 = arith.divsi %78, %c1024 {ssbuffer.block_id = 21 : i32} : index + %80 = arith.subi %13, %79 {ssbuffer.block_id = 21 : i32} : index + %81 = arith.maxsi %80, %c0 {ssbuffer.block_id = 21 : i32} : index + %82 = arith.minsi %81, %c64 {ssbuffer.block_id = 21 : i32} : index + %83 = arith.remsi %78, %c1024 {ssbuffer.block_id = 21 : i32} : index + %84 = arith.subi %c128, %83 {ssbuffer.block_id = 21 : i32} : index + %85 = arith.maxsi %84, %c0 {ssbuffer.block_id = 21 : i32} : index + %86 = arith.minsi %85, %c64 {ssbuffer.block_id = 21 : i32} : index + %87 = arith.minsi %20, %82 {ssbuffer.block_id = 21 : i32} : index + %88 = arith.subi %82, %87 {ssbuffer.block_id = 21 : i32} : index + %89 = arith.subi %c0_i32, %72 {ssbuffer.block_id = 21 : i32} : i32 + %90 = arith.maxsi %89, %c0_i32 {ssbuffer.block_id = 21 : i32} : i32 + %91 = arith.index_cast %90 {ssbuffer.block_id = 21 : i32} : i32 to index + %92 = arith.minsi %91, %86 {ssbuffer.block_id = 21 : i32} : index + %93 = arith.subi %86, %92 {ssbuffer.block_id = 21 : i32} : index + %94 = arith.cmpi slt, %88, %c64 {ssbuffer.block_id = 21 : i32} : index + %95 = arith.cmpi slt, %93, %c64 {ssbuffer.block_id = 21 : i32} : index + %96 = arith.ori %94, %95 {ssbuffer.block_id = 21 : i32} : i1 + %reinterpret_cast_56 = memref.reinterpret_cast %arg9 to offset: [%77], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 21 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %subview_57 = memref.subview %reinterpret_cast_56[0, 0] [%88, %93] [1, 1] {ssbuffer.block_id = 21 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %alloc_58 = memref.alloc() {ssbuffer.block_id = 22 : i32} : memref<64x64xbf16> + %reinterpret_cast_59 = memref.reinterpret_cast %arg2 to offset: [%77], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 22 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %subview_60 = memref.subview %reinterpret_cast_59[0, 0] [%88, %93] [1, 1] {ssbuffer.block_id = 22 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %subview_61 = memref.subview %alloc_58[%87, %92] [%88, %93] [1, 1] {ssbuffer.block_id = 22 : i32} : memref<64x64xbf16> to memref> + scf.if %96 { + linalg.fill {ssbuffer.block_id = 22 : i32} ins(%cst_2 : bf16) outs(%alloc_58 : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 22 : i32} + memref.copy %subview_60, %subview_61 {ssbuffer.block_id = 22 : i32} : memref> to memref> + %97 = bufferization.to_tensor %alloc_58 restrict writable {ssbuffer.block_id = 22 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %98 = arith.mulf %97, %broadcasted {ssbuffer.block_id = 22 : i32} : tensor<64x64xbf16> + %reshape_62 = tensor.reshape %98(%cst_0) {ssbuffer.block_id = 22 : i32} : (tensor<64x64xbf16>, tensor<3xi64>) -> tensor<64x4x16xbf16> + %99 = tensor.empty() {ssbuffer.block_id = 22 : i32} : tensor<4x64x16xbf16> + %transposed_63 = linalg.transpose ins(%reshape_62 : tensor<64x4x16xbf16>) outs(%99 : tensor<4x64x16xbf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 22 : i32} + %reshape_64 = tensor.reshape %transposed_63(%cst) {ssbuffer.block_id = 22 : i32} : (tensor<4x64x16xbf16>, tensor<4xi64>) -> tensor<4x4x16x16xbf16> + hivm.hir.sync_block_wait {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.copy ins(%reshape_64 : tensor<4x4x16x16xbf16>) outs(%alloc_16 : memref<4x4x16x16xbf16, #hivm.address_space>) {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 1 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 17 : i32}[, , ] flag = -1 + %memspacecast_65 = memref.memory_space_cast %alloc_20 {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 17 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %100 = bufferization.to_tensor %memspacecast_65 restrict writable {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 17 : i32} : memref<64x64xf32> to tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 16 : i32}[, , ] flag = -1 + %memspacecast_66 = memref.memory_space_cast %alloc_19 {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 16 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %101 = bufferization.to_tensor %memspacecast_66 restrict writable {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 16 : i32} : memref<64x64xf32> to tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 15 : i32}[, , ] flag = -1 + %memspacecast_67 = memref.memory_space_cast %alloc_18 {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 15 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %102 = bufferization.to_tensor %memspacecast_67 restrict writable {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 15 : i32} : memref<64x64xf32> to tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 10 : i32}[, , ] flag = 11 + %memspacecast_68 = memref.memory_space_cast %alloc_17 {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 10 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %103 = bufferization.to_tensor %memspacecast_68 restrict writable {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 10 : i32} : memref<64x64xf32> to tensor<64x64xf32> + %104 = arith.addf %103, %arg21 {ssbuffer.block_id = 23 : i32} : tensor<64x64xf32> + %105 = arith.mulf %100, %broadcasted_8 {ssbuffer.block_id = 23 : i32} : tensor<64x64xf32> + %106 = arith.extf %97 {ssbuffer.block_id = 23 : i32} : tensor<64x64xbf16> to tensor<64x64xf32> + %107 = arith.mulf %101, %106 {ssbuffer.block_id = 23 : i32} : tensor<64x64xf32> + %108 = arith.mulf %107, %broadcasted_11 {ssbuffer.block_id = 23 : i32} : tensor<64x64xf32> + %reduced_69 = linalg.reduce ins(%108 : tensor<64x64xf32>) outs(%3 : tensor<64xf32>) dimensions = [1] {ssbuffer.block_id = 23 : i32} + (%in: f32, %init: f32) { + %114 = arith.addf %in, %init : f32 + linalg.yield %114 : f32 + } + %109 = arith.addf %arg22, %reduced_69 {ssbuffer.block_id = 23 : i32} : tensor<64xf32> + %110 = arith.extf %98 {ssbuffer.block_id = 23 : i32} : tensor<64x64xbf16> to tensor<64x64xf32> + %111 = arith.mulf %102, %110 {ssbuffer.block_id = 23 : i32} : tensor<64x64xf32> + %reduced_70 = linalg.reduce ins(%111 : tensor<64x64xf32>) outs(%3 : tensor<64xf32>) dimensions = [1] {ssbuffer.block_id = 23 : i32} + (%in: f32, %init: f32) { + %114 = arith.addf %in, %init : f32 + linalg.yield %114 : f32 + } + %112 = arith.addf %arg23, %reduced_70 {ssbuffer.block_id = 23 : i32} : tensor<64xf32> + %113 = arith.truncf %105 {ssbuffer.block_id = 23 : i32} : tensor<64x64xf32> to tensor<64x64xbf16> + %extracted_slice_71 = tensor.extract_slice %113[%87, %92] [%88, %93] [1, 1] {ssbuffer.block_id = 23 : i32} : tensor<64x64xbf16> to tensor + bufferization.materialize_in_destination %extracted_slice_71 in writable %subview_57 {ssbuffer.block_id = 23 : i32} : (tensor, memref>) -> () + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 10 : i32}[, , ] flag = 11 + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 15 : i32}[, , ] flag = -1 + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 16 : i32}[, , ] flag = -1 + hivm.hir.sync_block_set {ssbuffer.block_id = 23 : i32, ssbuffer.transfer_id = 17 : i32}[, , ] flag = -1 + scf.yield %104, %109, %112 : tensor<64x64xf32>, tensor<64xf32>, tensor<64xf32> + } {ssbuffer.block_id = 37 : i32, ssbuffer.main_loop = 0 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %alloc_21 = memref.alloc() {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 3 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_21 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 3 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + %alloc_22 = memref.alloc() {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_22 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<7>, ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 8 + %alloc_23 = memref.alloc() {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 8 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_23 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<8>, ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 8 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 8 : i32}[, , ] flag = 9 + %alloc_24 = memref.alloc() {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 13 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_24 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<13>, ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 13 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 13 : i32}[, , ] flag = 14 + %37:2 = scf.for %arg20 = %c0_i32 to %c2_i32 step %c1_i32 iter_args(%arg21 = %36#0, %arg22 = %36#1) -> (tensor<64x64xf32>, tensor<64xf32>) : i32 { + %72 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 25 : i32} : i32 + %73 = arith.maxsi %72, %c0_i32 {ssbuffer.block_id = 25 : i32} : i32 + %74 = arith.index_cast %73 {ssbuffer.block_id = 25 : i32} : i32 to index + %75 = arith.muli %11, %c1024 {ssbuffer.block_id = 25 : i32} : index + %76 = arith.addi %75, %25 {ssbuffer.block_id = 25 : i32} : index + %77 = arith.addi %76, %74 {ssbuffer.block_id = 25 : i32} : index + %78 = arith.subi %77, %25 {ssbuffer.block_id = 25 : i32} : index + %79 = arith.divsi %78, %c1024 {ssbuffer.block_id = 25 : i32} : index + %80 = arith.subi %13, %79 {ssbuffer.block_id = 25 : i32} : index + %81 = arith.maxsi %80, %c0 {ssbuffer.block_id = 25 : i32} : index + %82 = arith.minsi %81, %c64 {ssbuffer.block_id = 25 : i32} : index + %83 = arith.remsi %78, %c1024 {ssbuffer.block_id = 25 : i32} : index + %84 = arith.subi %c128, %83 {ssbuffer.block_id = 25 : i32} : index + %85 = arith.maxsi %84, %c0 {ssbuffer.block_id = 25 : i32} : index + %86 = arith.minsi %85, %c64 {ssbuffer.block_id = 25 : i32} : index + %87 = arith.minsi %20, %82 {ssbuffer.block_id = 25 : i32} : index + %88 = arith.subi %82, %87 {ssbuffer.block_id = 25 : i32} : index + %89 = arith.subi %c0_i32, %72 {ssbuffer.block_id = 25 : i32} : i32 + %90 = arith.maxsi %89, %c0_i32 {ssbuffer.block_id = 25 : i32} : i32 + %91 = arith.index_cast %90 {ssbuffer.block_id = 25 : i32} : i32 to index + %92 = arith.minsi %91, %86 {ssbuffer.block_id = 25 : i32} : index + %93 = arith.subi %86, %92 {ssbuffer.block_id = 25 : i32} : index + %94 = arith.cmpi slt, %88, %c64 {ssbuffer.block_id = 25 : i32} : index + %95 = arith.cmpi slt, %93, %c64 {ssbuffer.block_id = 25 : i32} : index + %96 = arith.ori %94, %95 {ssbuffer.block_id = 25 : i32} : i1 + %reinterpret_cast_56 = memref.reinterpret_cast %arg10 to offset: [%77], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 25 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %subview_57 = memref.subview %reinterpret_cast_56[0, 0] [%88, %93] [1, 1] {ssbuffer.block_id = 25 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %alloc_58 = memref.alloc() {ssbuffer.block_id = 26 : i32} : memref<64x64xbf16> + %reinterpret_cast_59 = memref.reinterpret_cast %arg3 to offset: [%77], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 26 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %subview_60 = memref.subview %reinterpret_cast_59[0, 0] [%88, %93] [1, 1] {ssbuffer.block_id = 26 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %subview_61 = memref.subview %alloc_58[%87, %92] [%88, %93] [1, 1] {ssbuffer.block_id = 26 : i32} : memref<64x64xbf16> to memref> + scf.if %96 { + linalg.fill {ssbuffer.block_id = 26 : i32} ins(%cst_2 : bf16) outs(%alloc_58 : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 26 : i32} + memref.copy %subview_60, %subview_61 {ssbuffer.block_id = 26 : i32} : memref> to memref> + %97 = bufferization.to_tensor %alloc_58 restrict writable {ssbuffer.block_id = 26 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %98 = arith.mulf %97, %broadcasted_13 {ssbuffer.block_id = 26 : i32} : tensor<64x64xbf16> + %reshape_62 = tensor.reshape %98(%cst_0) {ssbuffer.block_id = 26 : i32} : (tensor<64x64xbf16>, tensor<3xi64>) -> tensor<64x4x16xbf16> + %99 = tensor.empty() {ssbuffer.block_id = 26 : i32} : tensor<4x64x16xbf16> + %transposed_63 = linalg.transpose ins(%reshape_62 : tensor<64x4x16xbf16>) outs(%99 : tensor<4x64x16xbf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 26 : i32} + %reshape_64 = tensor.reshape %transposed_63(%cst) {ssbuffer.block_id = 26 : i32} : (tensor<4x64x16xbf16>, tensor<4xi64>) -> tensor<4x4x16x16xbf16> + hivm.hir.sync_block_wait {ssbuffer.block_id = 26 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + hivm.hir.copy ins(%reshape_64 : tensor<4x4x16x16xbf16>) outs(%alloc_21 : memref<4x4x16x16xbf16, #hivm.address_space>) {ssbuffer.block_id = 26 : i32, ssbuffer.transfer_id = 3 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 26 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + hivm.hir.sync_block_wait {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 13 : i32}[, , ] flag = 14 + %memspacecast_65 = memref.memory_space_cast %alloc_24 {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 13 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %100 = bufferization.to_tensor %memspacecast_65 restrict writable {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 13 : i32} : memref<64x64xf32> to tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 8 : i32}[, , ] flag = 9 + %memspacecast_66 = memref.memory_space_cast %alloc_23 {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 8 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %101 = bufferization.to_tensor %memspacecast_66 restrict writable {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 8 : i32} : memref<64x64xf32> to tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 8 + %memspacecast_67 = memref.memory_space_cast %alloc_22 {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %102 = bufferization.to_tensor %memspacecast_67 restrict writable {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x64xf32> to tensor<64x64xf32> + %103 = arith.addf %100, %arg21 {ssbuffer.block_id = 27 : i32} : tensor<64x64xf32> + %104 = arith.mulf %101, %broadcasted_15 {ssbuffer.block_id = 27 : i32} : tensor<64x64xf32> + %105 = arith.extf %97 {ssbuffer.block_id = 27 : i32} : tensor<64x64xbf16> to tensor<64x64xf32> + %106 = arith.mulf %102, %105 {ssbuffer.block_id = 27 : i32} : tensor<64x64xf32> + %reduced_68 = linalg.reduce ins(%106 : tensor<64x64xf32>) outs(%3 : tensor<64xf32>) dimensions = [1] {ssbuffer.block_id = 27 : i32} + (%in: f32, %init: f32) { + %109 = arith.addf %in, %init : f32 + linalg.yield %109 : f32 + } + %107 = arith.addf %arg22, %reduced_68 {ssbuffer.block_id = 27 : i32} : tensor<64xf32> + %108 = arith.truncf %104 {ssbuffer.block_id = 27 : i32} : tensor<64x64xf32> to tensor<64x64xbf16> + %extracted_slice_69 = tensor.extract_slice %108[%87, %92] [%88, %93] [1, 1] {ssbuffer.block_id = 27 : i32} : tensor<64x64xbf16> to tensor + bufferization.materialize_in_destination %extracted_slice_69 in writable %subview_57 {ssbuffer.block_id = 27 : i32} : (tensor, memref>) -> () + hivm.hir.sync_block_set {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 8 + hivm.hir.sync_block_set {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 8 : i32}[, , ] flag = 9 + hivm.hir.sync_block_set {ssbuffer.block_id = 27 : i32, ssbuffer.transfer_id = 13 : i32}[, , ] flag = 14 + scf.yield %103, %107 : tensor<64x64xf32>, tensor<64xf32> + } {ssbuffer.block_id = 38 : i32, ssbuffer.main_loop = 1 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %38 = tensor.empty() {ssbuffer.block_id = 33 : i32} : tensor<64xi32> + %39 = linalg.generic {indexing_maps = [#map], iterator_types = ["parallel"]} outs(%38 : tensor<64xi32>) attrs = {ssbuffer.block_id = 33 : i32, tt.from_make_range, tt.make_range_offset = 0 : index, tt.make_range_size = 64 : index} { + ^bb0(%out: i32): + %72 = linalg.index 0 : index + %73 = arith.index_cast %72 : index to i32 + linalg.yield %73 : i32 + } -> tensor<64xi32> + %40 = linalg.fill {ssbuffer.block_id = 33 : i32} ins(%9 : i32) outs(%38 : tensor<64xi32>) -> tensor<64xi32> + %41 = arith.addi %40, %39 {ssbuffer.block_id = 33 : i32} : tensor<64xi32> + %42 = linalg.fill {ssbuffer.block_id = 33 : i32} ins(%arg13 : i32) outs(%38 : tensor<64xi32>) -> tensor<64xi32> + %43 = arith.cmpi slt, %41, %42 {ssbuffer.block_id = 33 : i32} : tensor<64xi32> + %44 = tensor.empty() {ssbuffer.block_id = 33 : i32} : tensor<64x64xi32> + %broadcasted_25 = linalg.broadcast ins(%41 : tensor<64xi32>) outs(%44 : tensor<64x64xi32>) dimensions = [1] {ssbuffer.block_id = 33 : i32} + %broadcasted_26 = linalg.broadcast ins(%41 : tensor<64xi32>) outs(%44 : tensor<64x64xi32>) dimensions = [0] {ssbuffer.block_id = 33 : i32} + %45 = arith.cmpi sgt, %broadcasted_25, %broadcasted_26 {ssbuffer.block_id = 33 : i32} : tensor<64x64xi32> + %46 = tensor.empty() {ssbuffer.block_id = 33 : i32} : tensor<64x64xi1> + %broadcasted_27 = linalg.broadcast ins(%43 : tensor<64xi1>) outs(%46 : tensor<64x64xi1>) dimensions = [1] {ssbuffer.block_id = 33 : i32} + %broadcasted_28 = linalg.broadcast ins(%43 : tensor<64xi1>) outs(%46 : tensor<64x64xi1>) dimensions = [0] {ssbuffer.block_id = 33 : i32} + %47 = arith.andi %broadcasted_27, %broadcasted_28 {ssbuffer.block_id = 33 : i32} : tensor<64x64xi1> + %48 = arith.andi %45, %47 {ssbuffer.block_id = 33 : i32} : tensor<64x64xi1> + %49 = arith.select %48, %37#0, %1 {ssbuffer.block_id = 33 : i32} : tensor<64x64xi1>, tensor<64x64xf32> + %50 = arith.truncf %49 {ssbuffer.block_id = 33 : i32} : tensor<64x64xf32> to tensor<64x64xbf16> + %reshape = tensor.reshape %50(%cst_0) {ssbuffer.block_id = 33 : i32} : (tensor<64x64xbf16>, tensor<3xi64>) -> tensor<64x4x16xbf16> + %51 = tensor.empty() {ssbuffer.block_id = 33 : i32} : tensor<4x64x16xbf16> + %transposed = linalg.transpose ins(%reshape : tensor<64x4x16xbf16>) outs(%51 : tensor<4x64x16xbf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 33 : i32} + %reshape_29 = tensor.reshape %transposed(%cst) {ssbuffer.block_id = 33 : i32} : (tensor<4x64x16xbf16>, tensor<4xi64>) -> tensor<4x4x16x16xbf16> + %alloc_30 = memref.alloc() {ssbuffer.block_id = 33 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_30 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 33 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + hivm.hir.copy ins(%reshape_29 : tensor<4x4x16x16xbf16>) outs(%alloc_30 : memref<4x4x16x16xbf16, #hivm.address_space>) {ssbuffer.block_id = 33 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 33 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 9 : i32}[, , ] flag = 10 + %alloc_31 = memref.alloc() {ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 9 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_31 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<9>, ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 9 : i32} : memref<64x64xf32, #hivm.address_space> + %memspacecast = memref.memory_space_cast %alloc_31 {ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 9 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %52 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 9 : i32} : memref<64x64xf32> to tensor<64x64xf32> + %53 = arith.truncf %52 {ssbuffer.block_id = 34 : i32} : tensor<64x64xf32> to tensor<64x64xbf16> + %reshape_32 = tensor.reshape %53(%cst_0) {ssbuffer.block_id = 34 : i32} : (tensor<64x64xbf16>, tensor<3xi64>) -> tensor<64x4x16xbf16> + %54 = tensor.empty() {ssbuffer.block_id = 34 : i32} : tensor<4x64x16xbf16> + %transposed_33 = linalg.transpose ins(%reshape_32 : tensor<64x4x16xbf16>) outs(%54 : tensor<4x64x16xbf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 34 : i32} + %reshape_34 = tensor.reshape %transposed_33(%cst) {ssbuffer.block_id = 34 : i32} : (tensor<4x64x16xbf16>, tensor<4xi64>) -> tensor<4x4x16x16xbf16> + %alloc_35 = memref.alloc() {ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 4 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_35 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 4 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + hivm.hir.copy ins(%reshape_34 : tensor<4x4x16x16xbf16>) outs(%alloc_35 : memref<4x4x16x16xbf16, #hivm.address_space>) {ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 4 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 34 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 5 + hivm.hir.sync_block_wait {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 14 : i32}[, , ] flag = -1 + %alloc_36 = memref.alloc() {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 14 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_36 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<14>, ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 14 : i32} : memref<64x64xf32, #hivm.address_space> + %memspacecast_37 = memref.memory_space_cast %alloc_36 {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 14 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %55 = bufferization.to_tensor %memspacecast_37 restrict writable {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 14 : i32} : memref<64x64xf32> to tensor<64x64xf32> + %broadcasted_38 = linalg.broadcast ins(%30 : tensor<64xbf16>) outs(%29 : tensor<64x64xbf16>) dimensions = [1] {ssbuffer.block_id = 35 : i32} + %broadcasted_39 = linalg.broadcast ins(%30 : tensor<64xbf16>) outs(%29 : tensor<64x64xbf16>) dimensions = [0] {ssbuffer.block_id = 35 : i32} + %56 = arith.subf %broadcasted_38, %broadcasted_39 {ssbuffer.block_id = 35 : i32} : tensor<64x64xbf16> + %57 = arith.extf %56 {ssbuffer.block_id = 35 : i32} : tensor<64x64xbf16> to tensor<64x64xf32> + %58 = math.exp %57 {ssbuffer.block_id = 35 : i32} : tensor<64x64xf32> + %59 = arith.mulf %55, %58 {ssbuffer.block_id = 35 : i32} : tensor<64x64xf32> + %60 = arith.subf %1, %59 {ssbuffer.block_id = 35 : i32} : tensor<64x64xf32> + %61 = arith.select %48, %60, %1 {ssbuffer.block_id = 35 : i32} : tensor<64x64xi1>, tensor<64x64xf32> + %62 = arith.truncf %61 {ssbuffer.block_id = 35 : i32} : tensor<64x64xf32> to tensor<64x64xbf16> + %reshape_40 = tensor.reshape %62(%cst_0) {ssbuffer.block_id = 35 : i32} : (tensor<64x64xbf16>, tensor<3xi64>) -> tensor<64x4x16xbf16> + %63 = tensor.empty() {ssbuffer.block_id = 35 : i32} : tensor<4x64x16xbf16> + %transposed_41 = linalg.transpose ins(%reshape_40 : tensor<64x4x16xbf16>) outs(%63 : tensor<4x64x16xbf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 35 : i32} + %reshape_42 = tensor.reshape %transposed_41(%cst) {ssbuffer.block_id = 35 : i32} : (tensor<4x64x16xbf16>, tensor<4xi64>) -> tensor<4x4x16x16xbf16> + %alloc_43 = memref.alloc() {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 2 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_43 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 2 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + hivm.hir.copy ins(%reshape_42 : tensor<4x4x16x16xbf16>) outs(%alloc_43 : memref<4x4x16x16xbf16, #hivm.address_space>) {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 2 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %alloc_44 = memref.alloc() {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 6 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_44 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<6>, ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 6 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + hivm.hir.copy ins(%reshape_42 : tensor<4x4x16x16xbf16>) outs(%alloc_44 : memref<4x4x16x16xbf16, #hivm.address_space>) {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 6 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 35 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 7 + %alloc_45 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 5 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_45 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<5>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 5 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + %alloc_46 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 11 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_46 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<11>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 11 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 11 : i32}[, , ] flag = 12 + %alloc_47 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 12 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_47 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<12>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 12 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 12 : i32}[, , ] flag = 13 + %alloc_48 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 18 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_48 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<18>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 18 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 18 : i32}[, , ] flag = -1 + %alloc_49 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 19 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_49 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<19>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 19 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 19 : i32}[, , ] flag = -1 + %64:2 = scf.for %arg20 = %c0_i32 to %c2_i32 step %c1_i32 iter_args(%arg21 = %1, %arg22 = %37#1) -> (tensor<64x64xf32>, tensor<64xf32>) : i32 { + %72 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 29 : i32} : i32 + %73 = arith.maxsi %72, %c0_i32 {ssbuffer.block_id = 29 : i32} : i32 + %74 = arith.index_cast %73 {ssbuffer.block_id = 29 : i32} : i32 to index + %75 = arith.muli %11, %c1024 {ssbuffer.block_id = 29 : i32} : index + %76 = arith.addi %75, %25 {ssbuffer.block_id = 29 : i32} : index + %77 = arith.addi %76, %74 {ssbuffer.block_id = 29 : i32} : index + %reinterpret_cast_56 = memref.reinterpret_cast %arg2 to offset: [%77], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 29 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %alloc_57 = memref.alloc() {ssbuffer.block_id = 29 : i32} : memref<64x64xbf16> + %78 = arith.subi %77, %25 {ssbuffer.block_id = 29 : i32} : index + %79 = arith.divsi %78, %c1024 {ssbuffer.block_id = 29 : i32} : index + %80 = arith.subi %13, %79 {ssbuffer.block_id = 29 : i32} : index + %81 = arith.maxsi %80, %c0 {ssbuffer.block_id = 29 : i32} : index + %82 = arith.minsi %81, %c64 {ssbuffer.block_id = 29 : i32} : index + %83 = arith.remsi %78, %c1024 {ssbuffer.block_id = 29 : i32} : index + %84 = arith.subi %c128, %83 {ssbuffer.block_id = 29 : i32} : index + %85 = arith.maxsi %84, %c0 {ssbuffer.block_id = 29 : i32} : index + %86 = arith.minsi %85, %c64 {ssbuffer.block_id = 29 : i32} : index + %87 = arith.minsi %20, %82 {ssbuffer.block_id = 29 : i32} : index + %88 = arith.subi %82, %87 {ssbuffer.block_id = 29 : i32} : index + %89 = arith.subi %c0_i32, %72 {ssbuffer.block_id = 29 : i32} : i32 + %90 = arith.maxsi %89, %c0_i32 {ssbuffer.block_id = 29 : i32} : i32 + %91 = arith.index_cast %90 {ssbuffer.block_id = 29 : i32} : i32 to index + %92 = arith.minsi %91, %86 {ssbuffer.block_id = 29 : i32} : index + %93 = arith.subi %86, %92 {ssbuffer.block_id = 29 : i32} : index + %94 = arith.cmpi slt, %88, %c64 {ssbuffer.block_id = 29 : i32} : index + %95 = arith.cmpi slt, %93, %c64 {ssbuffer.block_id = 29 : i32} : index + %96 = arith.ori %94, %95 {ssbuffer.block_id = 29 : i32} : i1 + %subview_58 = memref.subview %reinterpret_cast_56[0, 0] [%88, %93] [1, 1] {ssbuffer.block_id = 29 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %subview_59 = memref.subview %alloc_57[%87, %92] [%88, %93] [1, 1] {ssbuffer.block_id = 29 : i32} : memref<64x64xbf16> to memref> + memref.copy %subview_58, %subview_59 {ssbuffer.block_id = 29 : i32} : memref> to memref> + %97 = bufferization.to_tensor %alloc_57 restrict writable {ssbuffer.block_id = 29 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %98 = arith.mulf %97, %broadcasted_13 {ssbuffer.block_id = 29 : i32} : tensor<64x64xbf16> + %reshape_60 = tensor.reshape %98(%cst_0) {ssbuffer.block_id = 29 : i32} : (tensor<64x64xbf16>, tensor<3xi64>) -> tensor<64x4x16xbf16> + %99 = tensor.empty() {ssbuffer.block_id = 29 : i32} : tensor<4x64x16xbf16> + %transposed_61 = linalg.transpose ins(%reshape_60 : tensor<64x4x16xbf16>) outs(%99 : tensor<4x64x16xbf16>) permutation = [1, 0, 2] {ssbuffer.block_id = 29 : i32} + %reshape_62 = tensor.reshape %transposed_61(%cst) {ssbuffer.block_id = 29 : i32} : (tensor<4x64x16xbf16>, tensor<4xi64>) -> tensor<4x4x16x16xbf16> + hivm.hir.sync_block_wait {ssbuffer.block_id = 29 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + hivm.hir.copy ins(%reshape_62 : tensor<4x4x16x16xbf16>) outs(%alloc_45 : memref<4x4x16x16xbf16, #hivm.address_space>) {ssbuffer.block_id = 29 : i32, ssbuffer.transfer_id = 5 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 29 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + hivm.hir.sync_block_wait {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 19 : i32}[, , ] flag = -1 + %memspacecast_63 = memref.memory_space_cast %alloc_49 {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 19 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %100 = bufferization.to_tensor %memspacecast_63 restrict writable {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 19 : i32} : memref<64x64xf32> to tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 18 : i32}[, , ] flag = -1 + %memspacecast_64 = memref.memory_space_cast %alloc_48 {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 18 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %101 = bufferization.to_tensor %memspacecast_64 restrict writable {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 18 : i32} : memref<64x64xf32> to tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 12 : i32}[, , ] flag = 13 + %memspacecast_65 = memref.memory_space_cast %alloc_47 {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 12 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %102 = bufferization.to_tensor %memspacecast_65 restrict writable {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 12 : i32} : memref<64x64xf32> to tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 11 : i32}[, , ] flag = 12 + %memspacecast_66 = memref.memory_space_cast %alloc_46 {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 11 : i32} : memref<64x64xf32, #hivm.address_space> to memref<64x64xf32> + %103 = bufferization.to_tensor %memspacecast_66 restrict writable {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 11 : i32} : memref<64x64xf32> to tensor<64x64xf32> + %alloc_67 = memref.alloc() {ssbuffer.block_id = 30 : i32} : memref<64x64xbf16> + %reinterpret_cast_68 = memref.reinterpret_cast %arg9 to offset: [%77], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 30 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %subview_69 = memref.subview %reinterpret_cast_68[0, 0] [%88, %93] [1, 1] {ssbuffer.block_id = 30 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %subview_70 = memref.subview %alloc_67[%87, %92] [%88, %93] [1, 1] {ssbuffer.block_id = 30 : i32} : memref<64x64xbf16> to memref> + scf.if %96 { + linalg.fill {ssbuffer.block_id = 30 : i32} ins(%cst_2 : bf16) outs(%alloc_67 : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 30 : i32} + %104 = arith.addf %101, %arg21 {ssbuffer.block_id = 30 : i32} : tensor<64x64xf32> + %105 = arith.extf %97 {ssbuffer.block_id = 30 : i32} : tensor<64x64xbf16> to tensor<64x64xf32> + %106 = arith.mulf %102, %105 {ssbuffer.block_id = 30 : i32} : tensor<64x64xf32> + %reduced_71 = linalg.reduce ins(%106 : tensor<64x64xf32>) outs(%3 : tensor<64xf32>) dimensions = [1] {ssbuffer.block_id = 30 : i32} + (%in: f32, %init: f32) { + %114 = arith.addf %in, %init : f32 + linalg.yield %114 : f32 + } + %107 = arith.addf %arg22, %reduced_71 {ssbuffer.block_id = 30 : i32} : tensor<64xf32> + %108 = arith.mulf %103, %broadcasted_15 {ssbuffer.block_id = 30 : i32} : tensor<64x64xf32> + %transposed_72 = linalg.transpose ins(%100 : tensor<64x64xf32>) outs(%0 : tensor<64x64xf32>) permutation = [1, 0] {ssbuffer.block_id = 30 : i32} + %109 = arith.addf %108, %transposed_72 {ssbuffer.block_id = 30 : i32} : tensor<64x64xf32> + memref.copy %subview_69, %subview_70 {ssbuffer.block_id = 30 : i32} : memref> to memref> + %110 = bufferization.to_tensor %alloc_67 restrict writable {ssbuffer.block_id = 30 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %111 = arith.extf %110 {ssbuffer.block_id = 30 : i32} : tensor<64x64xbf16> to tensor<64x64xf32> + %112 = arith.addf %109, %111 {ssbuffer.block_id = 30 : i32} : tensor<64x64xf32> + %113 = arith.truncf %112 {ssbuffer.block_id = 30 : i32} : tensor<64x64xf32> to tensor<64x64xbf16> + %extracted_slice_73 = tensor.extract_slice %113[%87, %92] [%88, %93] [1, 1] {ssbuffer.block_id = 30 : i32} : tensor<64x64xbf16> to tensor + bufferization.materialize_in_destination %extracted_slice_73 in writable %subview_69 {ssbuffer.block_id = 30 : i32} : (tensor, memref>) -> () + hivm.hir.sync_block_set {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 11 : i32}[, , ] flag = 12 + hivm.hir.sync_block_set {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 12 : i32}[, , ] flag = 13 + hivm.hir.sync_block_set {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 18 : i32}[, , ] flag = -1 + hivm.hir.sync_block_set {ssbuffer.block_id = 30 : i32, ssbuffer.transfer_id = 19 : i32}[, , ] flag = -1 + scf.yield %104, %107 : tensor<64x64xf32>, tensor<64xf32> + } {ssbuffer.block_id = 39 : i32, ssbuffer.main_loop = 2 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + %reinterpret_cast_50 = memref.reinterpret_cast %arg11 to offset: [%27], sizes: [64], strides: [8] {ssbuffer.block_id = 36 : i32} : memref to memref<64xbf16, strided<[8], offset: ?>> + gpu.barrier {ssbuffer.block_id = 36 : i32} + %65 = arith.truncf %64#1 {ssbuffer.block_id = 36 : i32} : tensor<64xf32> to tensor<64xbf16> + %extracted_slice = tensor.extract_slice %65[%21] [%22] [1] {ssbuffer.block_id = 36 : i32} : tensor<64xbf16> to tensor + %subview_51 = memref.subview %reinterpret_cast_50[0] [%22] [1] {ssbuffer.block_id = 36 : i32} : memref<64xbf16, strided<[8], offset: ?>> to memref> + bufferization.materialize_in_destination %extracted_slice in writable %subview_51 {ssbuffer.block_id = 36 : i32} : (tensor, memref>) -> () + %66 = arith.mulf %64#0, %broadcasted_15 {ssbuffer.block_id = 36 : i32} : tensor<64x64xf32> + %67 = arith.extf %62 {ssbuffer.block_id = 36 : i32} : tensor<64x64xbf16> to tensor<64x64xf32> + %68 = arith.mulf %67, %66 {ssbuffer.block_id = 36 : i32} : tensor<64x64xf32> + %reinterpret_cast_52 = memref.reinterpret_cast %arg12 to offset: [%27], sizes: [64], strides: [8] {ssbuffer.block_id = 36 : i32} : memref to memref<64xbf16, strided<[8], offset: ?>> + %reduced = linalg.reduce ins(%68 : tensor<64x64xf32>) outs(%3 : tensor<64xf32>) dimensions = [1] {ssbuffer.block_id = 36 : i32} + (%in: f32, %init: f32) { + %72 = arith.addf %in, %init : f32 + linalg.yield %72 : f32 + } + %reduced_53 = linalg.reduce ins(%68 : tensor<64x64xf32>) outs(%3 : tensor<64xf32>) dimensions = [0] {ssbuffer.block_id = 36 : i32} + (%in: f32, %init: f32) { + %72 = arith.addf %in, %init : f32 + linalg.yield %72 : f32 + } + %69 = arith.subf %reduced, %reduced_53 {ssbuffer.block_id = 36 : i32} : tensor<64xf32> + %70 = arith.addf %36#2, %69 {ssbuffer.block_id = 36 : i32} : tensor<64xf32> + %71 = arith.truncf %70 {ssbuffer.block_id = 36 : i32} : tensor<64xf32> to tensor<64xbf16> + %extracted_slice_54 = tensor.extract_slice %71[%21] [%22] [1] {ssbuffer.block_id = 36 : i32} : tensor<64xbf16> to tensor + %subview_55 = memref.subview %reinterpret_cast_52[0] [%22] [1] {ssbuffer.block_id = 36 : i32} : memref<64xbf16, strided<[8], offset: ?>> to memref> + bufferization.materialize_in_destination %extracted_slice_54 in writable %subview_55 {ssbuffer.block_id = 36 : i32} : (tensor, memref>) -> () + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + scope.scope : () -> () { + %0 = arith.divsi %arg18, %c8_i32 {ssbuffer.block_id = 16 : i32} : i32 + %1 = arith.remsi %arg18, %c8_i32 {ssbuffer.block_id = 16 : i32} : i32 + %2 = arith.muli %0, %arg13 {ssbuffer.block_id = 16 : i32} : i32 + %3 = arith.muli %2, %c8_i32 {ssbuffer.block_id = 16 : i32} : i32 + %4 = arith.addi %3, %1 {ssbuffer.block_id = 16 : i32} : i32 + %5 = arith.muli %arg17, %c64_i32 {ssbuffer.block_id = 16 : i32} : i32 + %6 = arith.maxsi %5, %c0_i32 {ssbuffer.block_id = 16 : i32} : i32 + %7 = arith.index_cast %6 {ssbuffer.block_id = 16 : i32} : i32 to index + %8 = arith.index_cast %arg13 {ssbuffer.block_id = 16 : i32} : i32 to index + %9 = arith.muli %4, %c64_i32 {ssbuffer.block_id = 16 : i32} : i32 + %10 = arith.index_cast %9 {ssbuffer.block_id = 16 : i32} : i32 to index + %11 = arith.subi %c0_i32, %5 {ssbuffer.block_id = 16 : i32} : i32 + %12 = arith.maxsi %11, %c0_i32 {ssbuffer.block_id = 16 : i32} : i32 + %13 = arith.index_cast %12 {ssbuffer.block_id = 16 : i32} : i32 to index + %14 = arith.muli %7, %c512 {ssbuffer.block_id = 16 : i32} : index + %15 = arith.addi %14, %10 {ssbuffer.block_id = 16 : i32} : index + %16 = arith.divsi %14, %c512 {ssbuffer.block_id = 16 : i32} : index + %17 = arith.subi %8, %16 {ssbuffer.block_id = 16 : i32} : index + %18 = arith.maxsi %17, %c0 {ssbuffer.block_id = 16 : i32} : index + %19 = arith.minsi %18, %c64 {ssbuffer.block_id = 16 : i32} : index + %20 = arith.remsi %14, %c512 {ssbuffer.block_id = 16 : i32} : index + %21 = arith.subi %c64, %20 {ssbuffer.block_id = 16 : i32} : index + %22 = arith.maxsi %21, %c0 {ssbuffer.block_id = 16 : i32} : index + %23 = arith.minsi %22, %c64 {ssbuffer.block_id = 16 : i32} : index + %24 = arith.minsi %13, %19 {ssbuffer.block_id = 16 : i32} : index + %25 = arith.subi %19, %24 {ssbuffer.block_id = 16 : i32} : index + %26 = arith.minsi %23, %c0 {ssbuffer.block_id = 16 : i32} : index + %27 = arith.subi %23, %26 {ssbuffer.block_id = 16 : i32} : index + %28 = tensor.empty() {ssbuffer.block_id = 16 : i32} : tensor<64x64xbf16> + %29 = arith.muli %4, %c128_i32 {ssbuffer.block_id = 16 : i32} : i32 + %30 = arith.index_cast %29 {ssbuffer.block_id = 16 : i32} : i32 to index + %31 = tensor.empty() {ssbuffer.block_id = 31 : i32} : tensor<64x64xf32> + %32 = linalg.fill {ssbuffer.block_id = 31 : i32} ins(%cst_1 : f32) outs(%31 : tensor<64x64xf32>) -> tensor<64x64xf32> + %33 = arith.divsi %arg18, %c8_i32 {ssbuffer.block_id = 31 : i32} : i32 + %34 = arith.remsi %arg18, %c8_i32 {ssbuffer.block_id = 31 : i32} : i32 + %35 = arith.muli %33, %arg13 {ssbuffer.block_id = 31 : i32} : i32 + %36 = arith.muli %35, %c8_i32 {ssbuffer.block_id = 31 : i32} : i32 + %37 = arith.addi %36, %34 {ssbuffer.block_id = 31 : i32} : i32 + %38 = arith.muli %arg17, %c64_i32 {ssbuffer.block_id = 31 : i32} : i32 + %39 = arith.maxsi %38, %c0_i32 {ssbuffer.block_id = 31 : i32} : i32 + %40 = arith.index_cast %39 {ssbuffer.block_id = 31 : i32} : i32 to index + %41 = arith.index_cast %arg13 {ssbuffer.block_id = 31 : i32} : i32 to index + %42 = arith.subi %c0_i32, %38 {ssbuffer.block_id = 31 : i32} : i32 + %43 = arith.maxsi %42, %c0_i32 {ssbuffer.block_id = 31 : i32} : i32 + %44 = arith.index_cast %43 {ssbuffer.block_id = 31 : i32} : i32 to index + %45 = arith.muli %40, %c512 {ssbuffer.block_id = 31 : i32} : index + %46 = arith.divsi %45, %c512 {ssbuffer.block_id = 31 : i32} : index + %47 = arith.subi %41, %46 {ssbuffer.block_id = 31 : i32} : index + %48 = arith.maxsi %47, %c0 {ssbuffer.block_id = 31 : i32} : index + %49 = arith.minsi %48, %c64 {ssbuffer.block_id = 31 : i32} : index + %50 = arith.remsi %45, %c512 {ssbuffer.block_id = 31 : i32} : index + %51 = arith.subi %c64, %50 {ssbuffer.block_id = 31 : i32} : index + %52 = arith.maxsi %51, %c0 {ssbuffer.block_id = 31 : i32} : index + %53 = arith.minsi %52, %c64 {ssbuffer.block_id = 31 : i32} : index + %54 = arith.minsi %44, %49 {ssbuffer.block_id = 31 : i32} : index + %55 = arith.subi %49, %54 {ssbuffer.block_id = 31 : i32} : index + %56 = arith.minsi %53, %c0 {ssbuffer.block_id = 31 : i32} : index + %57 = arith.subi %53, %56 {ssbuffer.block_id = 31 : i32} : index + %58 = arith.cmpi slt, %55, %c64 {ssbuffer.block_id = 31 : i32} : index + %59 = arith.cmpi slt, %57, %c64 {ssbuffer.block_id = 31 : i32} : index + %60 = arith.ori %58, %59 {ssbuffer.block_id = 31 : i32} : i1 + %61 = arith.muli %37, %c128_i32 {ssbuffer.block_id = 31 : i32} : i32 + %62 = arith.index_cast %61 {ssbuffer.block_id = 31 : i32} : i32 to index + %reinterpret_cast = memref.reinterpret_cast %arg6 to offset: [%15], sizes: [64, 64], strides: [512, 1] {ssbuffer.block_id = 17 : i32} : memref to memref<64x64xbf16, strided<[512, 1], offset: ?>> + %alloc = memref.alloc() {ssbuffer.block_id = 17 : i32} : memref<64x64xbf16> + %subview = memref.subview %reinterpret_cast[0, 0] [%25, %27] [1, 1] {ssbuffer.block_id = 17 : i32} : memref<64x64xbf16, strided<[512, 1], offset: ?>> to memref> + %subview_3 = memref.subview %alloc[%24, %26] [%25, %27] [1, 1] {ssbuffer.block_id = 17 : i32} : memref<64x64xbf16> to memref> + scf.if %60 { + linalg.fill {ssbuffer.block_id = 17 : i32} ins(%cst_2 : bf16) outs(%alloc : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 17 : i32} + memref.copy %subview, %subview_3 {ssbuffer.block_id = 17 : i32} : memref> to memref> + %63 = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 17 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %transposed = linalg.transpose ins(%63 : tensor<64x64xbf16>) outs(%28 : tensor<64x64xbf16>) permutation = [1, 0] {ssbuffer.block_id = 17 : i32} + %alloc_4 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 1 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %alloc_5 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 10 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<10>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 10 : i32} : memref<64x64xf32, #hivm.address_space> + %alloc_6 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 15 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<15>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 15 : i32} : memref<64x64xf32, #hivm.address_space> + %alloc_7 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 16 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_7 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<16>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 16 : i32} : memref<64x64xf32, #hivm.address_space> + %alloc_8 = memref.alloc() {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 17 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_8 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<17>, ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 17 : i32} : memref<64x64xf32, #hivm.address_space> + scf.for %arg20 = %c0_i32 to %c2_i32 step %c1_i32 : i32 { + %74 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 21 : i32} : i32 + %75 = arith.maxsi %74, %c0_i32 {ssbuffer.block_id = 21 : i32} : i32 + %76 = arith.index_cast %75 {ssbuffer.block_id = 21 : i32} : i32 to index + %77 = arith.muli %40, %c1024 {ssbuffer.block_id = 21 : i32} : index + %78 = arith.addi %77, %62 {ssbuffer.block_id = 21 : i32} : index + %79 = arith.addi %78, %76 {ssbuffer.block_id = 21 : i32} : index + %80 = arith.subi %79, %62 {ssbuffer.block_id = 21 : i32} : index + %81 = arith.divsi %80, %c1024 {ssbuffer.block_id = 21 : i32} : index + %82 = arith.subi %41, %81 {ssbuffer.block_id = 21 : i32} : index + %83 = arith.maxsi %82, %c0 {ssbuffer.block_id = 21 : i32} : index + %84 = arith.minsi %83, %c64 {ssbuffer.block_id = 21 : i32} : index + %85 = arith.remsi %80, %c1024 {ssbuffer.block_id = 21 : i32} : index + %86 = arith.subi %c128, %85 {ssbuffer.block_id = 21 : i32} : index + %87 = arith.maxsi %86, %c0 {ssbuffer.block_id = 21 : i32} : index + %88 = arith.minsi %87, %c64 {ssbuffer.block_id = 21 : i32} : index + %89 = arith.minsi %44, %84 {ssbuffer.block_id = 21 : i32} : index + %90 = arith.subi %84, %89 {ssbuffer.block_id = 21 : i32} : index + %91 = arith.subi %c0_i32, %74 {ssbuffer.block_id = 21 : i32} : i32 + %92 = arith.maxsi %91, %c0_i32 {ssbuffer.block_id = 21 : i32} : i32 + %93 = arith.index_cast %92 {ssbuffer.block_id = 21 : i32} : i32 to index + %94 = arith.minsi %93, %88 {ssbuffer.block_id = 21 : i32} : index + %95 = arith.subi %88, %94 {ssbuffer.block_id = 21 : i32} : index + %96 = arith.cmpi slt, %90, %c64 {ssbuffer.block_id = 21 : i32} : index + %97 = arith.cmpi slt, %95, %c64 {ssbuffer.block_id = 21 : i32} : index + %98 = arith.ori %96, %97 {ssbuffer.block_id = 21 : i32} : i1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %99 = hivm.hir.convert_layout %alloc_4 output_shape [64, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 1 : i32} : (memref<4x4x16x16xbf16, #hivm.address_space>) -> memref<64x64xbf16, #hivm.address_space> + %memspacecast_27 = memref.memory_space_cast %99 {ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 1 : i32} : memref<64x64xbf16, #hivm.address_space> to memref<64x64xbf16> + %100 = bufferization.to_tensor %memspacecast_27 restrict writable {ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 1 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %alloc_28 = memref.alloc() {ssbuffer.block_id = 2 : i32} : memref<64x64xbf16> + scf.if %98 { + linalg.fill {ssbuffer.block_id = 2 : i32} ins(%cst_2 : bf16) outs(%alloc_28 : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 2 : i32} + %101 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 2 : i32} : i32 + %102 = arith.maxsi %101, %c0_i32 {ssbuffer.block_id = 2 : i32} : i32 + %103 = arith.index_cast %102 {ssbuffer.block_id = 2 : i32} : i32 to index + %104 = arith.muli %7, %c1024 {ssbuffer.block_id = 2 : i32} : index + %105 = arith.addi %104, %30 {ssbuffer.block_id = 2 : i32} : index + %106 = arith.addi %105, %103 {ssbuffer.block_id = 2 : i32} : index + %reinterpret_cast_29 = memref.reinterpret_cast %arg7 to offset: [%106], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 2 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %107 = arith.subi %106, %30 {ssbuffer.block_id = 2 : i32} : index + %108 = arith.divsi %107, %c1024 {ssbuffer.block_id = 2 : i32} : index + %109 = arith.subi %8, %108 {ssbuffer.block_id = 2 : i32} : index + %110 = arith.maxsi %109, %c0 {ssbuffer.block_id = 2 : i32} : index + %111 = arith.minsi %110, %c64 {ssbuffer.block_id = 2 : i32} : index + %112 = arith.remsi %107, %c1024 {ssbuffer.block_id = 2 : i32} : index + %113 = arith.subi %c128, %112 {ssbuffer.block_id = 2 : i32} : index + %114 = arith.maxsi %113, %c0 {ssbuffer.block_id = 2 : i32} : index + %115 = arith.minsi %114, %c64 {ssbuffer.block_id = 2 : i32} : index + %116 = arith.minsi %13, %111 {ssbuffer.block_id = 2 : i32} : index + %117 = arith.subi %111, %116 {ssbuffer.block_id = 2 : i32} : index + %118 = arith.subi %c0_i32, %101 {ssbuffer.block_id = 2 : i32} : i32 + %119 = arith.maxsi %118, %c0_i32 {ssbuffer.block_id = 2 : i32} : i32 + %120 = arith.index_cast %119 {ssbuffer.block_id = 2 : i32} : i32 to index + %121 = arith.minsi %120, %115 {ssbuffer.block_id = 2 : i32} : index + %122 = arith.subi %115, %121 {ssbuffer.block_id = 2 : i32} : index + %subview_30 = memref.subview %reinterpret_cast_29[0, 0] [%117, %122] [1, 1] {ssbuffer.block_id = 2 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %subview_31 = memref.subview %alloc_28[%116, %121] [%117, %122] [1, 1] {ssbuffer.block_id = 2 : i32} : memref<64x64xbf16> to memref> + memref.copy %subview_30, %subview_31 {ssbuffer.block_id = 2 : i32} : memref> to memref> + %123 = bufferization.to_tensor %alloc_28 restrict writable {ssbuffer.block_id = 2 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %transposed_32 = linalg.transpose ins(%100 : tensor<64x64xbf16>) outs(%28 : tensor<64x64xbf16>) permutation = [1, 0] {ssbuffer.block_id = 2 : i32} + %124 = tensor.empty() {ssbuffer.block_id = 2 : i32} : tensor<64x64xf32> + %125 = linalg.fill {ssbuffer.block_id = 2 : i32} ins(%cst_1 : f32) outs(%124 : tensor<64x64xf32>) -> tensor<64x64xf32> + %126 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 2 : i32} ins(%123, %transposed_32 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%125 : tensor<64x64xf32>) -> tensor<64x64xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 10 : i32}[, , ] flag = 11 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 10 : i32} ins(%126 : tensor<64x64xf32>) outs(%alloc_5 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 2 : i32, ssbuffer.transfer_id = 10 : i32}[, , ] flag = 11 + %127 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 3 : i32} ins(%transposed, %123 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%32 : tensor<64x64xf32>) -> tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 15 : i32}[, , ] flag = -1 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 15 : i32} ins(%127 : tensor<64x64xf32>) outs(%alloc_6 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 15 : i32}[, , ] flag = -1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 16 : i32}[, , ] flag = -1 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 16 : i32} ins(%127 : tensor<64x64xf32>) outs(%alloc_7 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 16 : i32}[, , ] flag = -1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 17 : i32}[, , ] flag = -1 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 17 : i32} ins(%127 : tensor<64x64xf32>) outs(%alloc_8 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 3 : i32, ssbuffer.transfer_id = 17 : i32}[, , ] flag = -1 + } {ssbuffer.block_id = 37 : i32, ssbuffer.main_loop = 0 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 17 : i32}[, , ] flag = -1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 16 : i32}[, , ] flag = -1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 15 : i32}[, , ] flag = -1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 37 : i32, ssbuffer.transfer_id = 10 : i32}[, , ] flag = 11 + %alloc_9 = memref.alloc() {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 3 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_9 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 3 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %alloc_10 = memref.alloc() {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_10 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<7>, ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x64xf32, #hivm.address_space> + %alloc_11 = memref.alloc() {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 8 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_11 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<8>, ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 8 : i32} : memref<64x64xf32, #hivm.address_space> + %alloc_12 = memref.alloc() {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 13 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_12 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<13>, ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 13 : i32} : memref<64x64xf32, #hivm.address_space> + scf.for %arg20 = %c0_i32 to %c2_i32 step %c1_i32 : i32 { + %74 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 25 : i32} : i32 + %75 = arith.maxsi %74, %c0_i32 {ssbuffer.block_id = 25 : i32} : i32 + %76 = arith.index_cast %75 {ssbuffer.block_id = 25 : i32} : i32 to index + %77 = arith.muli %40, %c1024 {ssbuffer.block_id = 25 : i32} : index + %78 = arith.addi %77, %62 {ssbuffer.block_id = 25 : i32} : index + %79 = arith.addi %78, %76 {ssbuffer.block_id = 25 : i32} : index + %80 = arith.subi %79, %62 {ssbuffer.block_id = 25 : i32} : index + %81 = arith.divsi %80, %c1024 {ssbuffer.block_id = 25 : i32} : index + %82 = arith.subi %41, %81 {ssbuffer.block_id = 25 : i32} : index + %83 = arith.maxsi %82, %c0 {ssbuffer.block_id = 25 : i32} : index + %84 = arith.minsi %83, %c64 {ssbuffer.block_id = 25 : i32} : index + %85 = arith.remsi %80, %c1024 {ssbuffer.block_id = 25 : i32} : index + %86 = arith.subi %c128, %85 {ssbuffer.block_id = 25 : i32} : index + %87 = arith.maxsi %86, %c0 {ssbuffer.block_id = 25 : i32} : index + %88 = arith.minsi %87, %c64 {ssbuffer.block_id = 25 : i32} : index + %89 = arith.minsi %44, %84 {ssbuffer.block_id = 25 : i32} : index + %90 = arith.subi %84, %89 {ssbuffer.block_id = 25 : i32} : index + %91 = arith.subi %c0_i32, %74 {ssbuffer.block_id = 25 : i32} : i32 + %92 = arith.maxsi %91, %c0_i32 {ssbuffer.block_id = 25 : i32} : i32 + %93 = arith.index_cast %92 {ssbuffer.block_id = 25 : i32} : i32 to index + %94 = arith.minsi %93, %88 {ssbuffer.block_id = 25 : i32} : index + %95 = arith.subi %88, %94 {ssbuffer.block_id = 25 : i32} : index + %96 = arith.cmpi slt, %90, %c64 {ssbuffer.block_id = 25 : i32} : index + %97 = arith.cmpi slt, %95, %c64 {ssbuffer.block_id = 25 : i32} : index + %98 = arith.ori %96, %97 {ssbuffer.block_id = 25 : i32} : i1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %99 = hivm.hir.convert_layout %alloc_9 output_shape [64, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32} : (memref<4x4x16x16xbf16, #hivm.address_space>) -> memref<64x64xbf16, #hivm.address_space> + %memspacecast_27 = memref.memory_space_cast %99 {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32} : memref<64x64xbf16, #hivm.address_space> to memref<64x64xbf16> + %100 = bufferization.to_tensor %memspacecast_27 restrict writable {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %alloc_28 = memref.alloc() {ssbuffer.block_id = 6 : i32} : memref<64x64xbf16> + scf.if %98 { + linalg.fill {ssbuffer.block_id = 6 : i32} ins(%cst_2 : bf16) outs(%alloc_28 : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 6 : i32} + %101 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 6 : i32} : i32 + %102 = arith.maxsi %101, %c0_i32 {ssbuffer.block_id = 6 : i32} : i32 + %103 = arith.index_cast %102 {ssbuffer.block_id = 6 : i32} : i32 to index + %104 = arith.muli %7, %c1024 {ssbuffer.block_id = 6 : i32} : index + %105 = arith.addi %104, %30 {ssbuffer.block_id = 6 : i32} : index + %106 = arith.addi %105, %103 {ssbuffer.block_id = 6 : i32} : index + %reinterpret_cast_29 = memref.reinterpret_cast %arg8 to offset: [%106], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 6 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %107 = arith.subi %106, %30 {ssbuffer.block_id = 6 : i32} : index + %108 = arith.divsi %107, %c1024 {ssbuffer.block_id = 6 : i32} : index + %109 = arith.subi %8, %108 {ssbuffer.block_id = 6 : i32} : index + %110 = arith.maxsi %109, %c0 {ssbuffer.block_id = 6 : i32} : index + %111 = arith.minsi %110, %c64 {ssbuffer.block_id = 6 : i32} : index + %112 = arith.remsi %107, %c1024 {ssbuffer.block_id = 6 : i32} : index + %113 = arith.subi %c128, %112 {ssbuffer.block_id = 6 : i32} : index + %114 = arith.maxsi %113, %c0 {ssbuffer.block_id = 6 : i32} : index + %115 = arith.minsi %114, %c64 {ssbuffer.block_id = 6 : i32} : index + %116 = arith.minsi %13, %111 {ssbuffer.block_id = 6 : i32} : index + %117 = arith.subi %111, %116 {ssbuffer.block_id = 6 : i32} : index + %118 = arith.subi %c0_i32, %101 {ssbuffer.block_id = 6 : i32} : i32 + %119 = arith.maxsi %118, %c0_i32 {ssbuffer.block_id = 6 : i32} : i32 + %120 = arith.index_cast %119 {ssbuffer.block_id = 6 : i32} : i32 to index + %121 = arith.minsi %120, %115 {ssbuffer.block_id = 6 : i32} : index + %122 = arith.subi %115, %121 {ssbuffer.block_id = 6 : i32} : index + %subview_30 = memref.subview %reinterpret_cast_29[0, 0] [%117, %122] [1, 1] {ssbuffer.block_id = 6 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %subview_31 = memref.subview %alloc_28[%116, %121] [%117, %122] [1, 1] {ssbuffer.block_id = 6 : i32} : memref<64x64xbf16> to memref> + memref.copy %subview_30, %subview_31 {ssbuffer.block_id = 6 : i32} : memref> to memref> + %123 = bufferization.to_tensor %alloc_28 restrict writable {ssbuffer.block_id = 6 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %transposed_32 = linalg.transpose ins(%100 : tensor<64x64xbf16>) outs(%28 : tensor<64x64xbf16>) permutation = [1, 0] {ssbuffer.block_id = 6 : i32} + %124 = tensor.empty() {ssbuffer.block_id = 6 : i32} : tensor<64x64xf32> + %125 = linalg.fill {ssbuffer.block_id = 6 : i32} ins(%cst_1 : f32) outs(%124 : tensor<64x64xf32>) -> tensor<64x64xf32> + %126 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 6 : i32} ins(%123, %transposed_32 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%125 : tensor<64x64xf32>) -> tensor<64x64xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + hivm.hir.sync_block_wait {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 13 : i32}[, , ] flag = 14 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 13 : i32} ins(%126 : tensor<64x64xf32>) outs(%alloc_12 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 6 : i32, ssbuffer.transfer_id = 13 : i32}[, , ] flag = 14 + %127 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 7 : i32} ins(%transposed, %123 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%32 : tensor<64x64xf32>) -> tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 8 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 7 : i32} ins(%127 : tensor<64x64xf32>) outs(%alloc_10 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 8 + hivm.hir.sync_block_wait {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 8 : i32}[, , ] flag = 9 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 8 : i32} ins(%127 : tensor<64x64xf32>) outs(%alloc_11 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 7 : i32, ssbuffer.transfer_id = 8 : i32}[, , ] flag = 9 + } {ssbuffer.block_id = 38 : i32, ssbuffer.main_loop = 1 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 13 : i32}[, , ] flag = 14 + hivm.hir.sync_block_wait {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 8 : i32}[, , ] flag = 9 + hivm.hir.sync_block_wait {ssbuffer.block_id = 38 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 8 + hivm.hir.sync_block_wait {ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + %alloc_13 = memref.alloc() {ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_13 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + %64 = hivm.hir.convert_layout %alloc_13 output_shape [64, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 0 : i32} : (memref<4x4x16x16xbf16, #hivm.address_space>) -> memref<64x64xbf16, #hivm.address_space> + %memspacecast = memref.memory_space_cast %64 {ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 0 : i32} : memref<64x64xbf16, #hivm.address_space> to memref<64x64xbf16> + %65 = bufferization.to_tensor %memspacecast restrict writable {ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 0 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %66 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 14 : i32} ins(%65, %transposed : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%32 : tensor<64x64xf32>) -> tensor<64x64xf32> + %alloc_14 = memref.alloc() {ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 9 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_14 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<9>, ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 9 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 9 : i32} ins(%66 : tensor<64x64xf32>) outs(%alloc_14 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 14 : i32, ssbuffer.transfer_id = 9 : i32}[, , ] flag = 10 + hivm.hir.sync_block_wait {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 5 + %alloc_15 = memref.alloc() {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 4 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_15 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 4 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + %67 = hivm.hir.convert_layout %alloc_15 output_shape [64, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 4 : i32} : (memref<4x4x16x16xbf16, #hivm.address_space>) -> memref<64x64xbf16, #hivm.address_space> + %memspacecast_16 = memref.memory_space_cast %67 {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 4 : i32} : memref<64x64xbf16, #hivm.address_space> to memref<64x64xbf16> + %68 = bufferization.to_tensor %memspacecast_16 restrict writable {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 4 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %69 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 15 : i32} ins(%transposed, %68 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%32 : tensor<64x64xf32>) -> tensor<64x64xf32> + %alloc_17 = memref.alloc() {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 14 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_17 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<14>, ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 14 : i32} : memref<64x64xf32, #hivm.address_space> + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 14 : i32} ins(%69 : tensor<64x64xf32>) outs(%alloc_17 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 15 : i32, ssbuffer.transfer_id = 14 : i32}[, , ] flag = -1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 7 + %alloc_18 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 6 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_18 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<6>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 6 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + %70 = hivm.hir.convert_layout %alloc_18 output_shape [64, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 6 : i32} : (memref<4x4x16x16xbf16, #hivm.address_space>) -> memref<64x64xbf16, #hivm.address_space> + %memspacecast_19 = memref.memory_space_cast %70 {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x64xbf16, #hivm.address_space> to memref<64x64xbf16> + %71 = bufferization.to_tensor %memspacecast_19 restrict writable {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + hivm.hir.sync_block_wait {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %alloc_20 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 2 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_20 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 2 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + %72 = hivm.hir.convert_layout %alloc_20 output_shape [64, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 2 : i32} : (memref<4x4x16x16xbf16, #hivm.address_space>) -> memref<64x64xbf16, #hivm.address_space> + %memspacecast_21 = memref.memory_space_cast %72 {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 2 : i32} : memref<64x64xbf16, #hivm.address_space> to memref<64x64xbf16> + %73 = bufferization.to_tensor %memspacecast_21 restrict writable {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 2 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %alloc_22 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 5 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_22 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<5>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 5 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + %alloc_23 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 11 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_23 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<11>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 11 : i32} : memref<64x64xf32, #hivm.address_space> + %alloc_24 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 12 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_24 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<12>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 12 : i32} : memref<64x64xf32, #hivm.address_space> + %alloc_25 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 18 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_25 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<18>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 18 : i32} : memref<64x64xf32, #hivm.address_space> + %alloc_26 = memref.alloc() {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 19 : i32} : memref<64x64xf32, #hivm.address_space> + annotation.mark %alloc_26 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<19>, ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 19 : i32} : memref<64x64xf32, #hivm.address_space> + scf.for %arg20 = %c0_i32 to %c2_i32 step %c1_i32 : i32 { + %74 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 29 : i32} : i32 + %75 = arith.maxsi %74, %c0_i32 {ssbuffer.block_id = 29 : i32} : i32 + %76 = arith.index_cast %75 {ssbuffer.block_id = 29 : i32} : i32 to index + %77 = arith.muli %40, %c1024 {ssbuffer.block_id = 29 : i32} : index + %78 = arith.addi %77, %62 {ssbuffer.block_id = 29 : i32} : index + %79 = arith.addi %78, %76 {ssbuffer.block_id = 29 : i32} : index + %80 = arith.subi %79, %62 {ssbuffer.block_id = 29 : i32} : index + %81 = arith.divsi %80, %c1024 {ssbuffer.block_id = 29 : i32} : index + %82 = arith.subi %41, %81 {ssbuffer.block_id = 29 : i32} : index + %83 = arith.maxsi %82, %c0 {ssbuffer.block_id = 29 : i32} : index + %84 = arith.minsi %83, %c64 {ssbuffer.block_id = 29 : i32} : index + %85 = arith.remsi %80, %c1024 {ssbuffer.block_id = 29 : i32} : index + %86 = arith.subi %c128, %85 {ssbuffer.block_id = 29 : i32} : index + %87 = arith.maxsi %86, %c0 {ssbuffer.block_id = 29 : i32} : index + %88 = arith.minsi %87, %c64 {ssbuffer.block_id = 29 : i32} : index + %89 = arith.minsi %44, %84 {ssbuffer.block_id = 29 : i32} : index + %90 = arith.subi %84, %89 {ssbuffer.block_id = 29 : i32} : index + %91 = arith.subi %c0_i32, %74 {ssbuffer.block_id = 29 : i32} : i32 + %92 = arith.maxsi %91, %c0_i32 {ssbuffer.block_id = 29 : i32} : i32 + %93 = arith.index_cast %92 {ssbuffer.block_id = 29 : i32} : i32 to index + %94 = arith.minsi %93, %88 {ssbuffer.block_id = 29 : i32} : index + %95 = arith.subi %88, %94 {ssbuffer.block_id = 29 : i32} : index + %96 = arith.cmpi slt, %90, %c64 {ssbuffer.block_id = 29 : i32} : index + %97 = arith.cmpi slt, %95, %c64 {ssbuffer.block_id = 29 : i32} : index + %98 = arith.ori %96, %97 {ssbuffer.block_id = 29 : i32} : i1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + %99 = hivm.hir.convert_layout %alloc_22 output_shape [64, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout, ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 5 : i32} : (memref<4x4x16x16xbf16, #hivm.address_space>) -> memref<64x64xbf16, #hivm.address_space> + %memspacecast_27 = memref.memory_space_cast %99 {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 5 : i32} : memref<64x64xbf16, #hivm.address_space> to memref<64x64xbf16> + %100 = bufferization.to_tensor %memspacecast_27 restrict writable {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 5 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %transposed_28 = linalg.transpose ins(%100 : tensor<64x64xbf16>) outs(%28 : tensor<64x64xbf16>) permutation = [1, 0] {ssbuffer.block_id = 12 : i32} + %101 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 12 : i32} ins(%transposed_28, %71 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%32 : tensor<64x64xf32>) -> tensor<64x64xf32> + hivm.hir.sync_block_set {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + hivm.hir.sync_block_wait {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 19 : i32}[, , ] flag = -1 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 19 : i32} ins(%101 : tensor<64x64xf32>) outs(%alloc_26 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 12 : i32, ssbuffer.transfer_id = 19 : i32}[, , ] flag = -1 + %alloc_29 = memref.alloc() {ssbuffer.block_id = 10 : i32} : memref<64x64xbf16> + scf.if %98 { + linalg.fill {ssbuffer.block_id = 10 : i32} ins(%cst_2 : bf16) outs(%alloc_29 : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 10 : i32} + %102 = arith.muli %arg20, %c64_i32 {ssbuffer.block_id = 10 : i32} : i32 + %103 = arith.maxsi %102, %c0_i32 {ssbuffer.block_id = 10 : i32} : i32 + %104 = arith.index_cast %103 {ssbuffer.block_id = 10 : i32} : i32 to index + %105 = arith.muli %7, %c1024 {ssbuffer.block_id = 10 : i32} : index + %106 = arith.addi %105, %30 {ssbuffer.block_id = 10 : i32} : index + %107 = arith.addi %106, %104 {ssbuffer.block_id = 10 : i32} : index + %reinterpret_cast_30 = memref.reinterpret_cast %arg2 to offset: [%107], sizes: [64, 64], strides: [1024, 1] {ssbuffer.block_id = 10 : i32} : memref to memref<64x64xbf16, strided<[1024, 1], offset: ?>> + %108 = arith.subi %107, %30 {ssbuffer.block_id = 10 : i32} : index + %109 = arith.divsi %108, %c1024 {ssbuffer.block_id = 10 : i32} : index + %110 = arith.subi %8, %109 {ssbuffer.block_id = 10 : i32} : index + %111 = arith.maxsi %110, %c0 {ssbuffer.block_id = 10 : i32} : index + %112 = arith.minsi %111, %c64 {ssbuffer.block_id = 10 : i32} : index + %113 = arith.remsi %108, %c1024 {ssbuffer.block_id = 10 : i32} : index + %114 = arith.subi %c128, %113 {ssbuffer.block_id = 10 : i32} : index + %115 = arith.maxsi %114, %c0 {ssbuffer.block_id = 10 : i32} : index + %116 = arith.minsi %115, %c64 {ssbuffer.block_id = 10 : i32} : index + %117 = arith.minsi %13, %112 {ssbuffer.block_id = 10 : i32} : index + %118 = arith.subi %112, %117 {ssbuffer.block_id = 10 : i32} : index + %119 = arith.subi %c0_i32, %102 {ssbuffer.block_id = 10 : i32} : i32 + %120 = arith.maxsi %119, %c0_i32 {ssbuffer.block_id = 10 : i32} : i32 + %121 = arith.index_cast %120 {ssbuffer.block_id = 10 : i32} : i32 to index + %122 = arith.minsi %121, %116 {ssbuffer.block_id = 10 : i32} : index + %123 = arith.subi %116, %122 {ssbuffer.block_id = 10 : i32} : index + %subview_31 = memref.subview %reinterpret_cast_30[0, 0] [%118, %123] [1, 1] {ssbuffer.block_id = 10 : i32} : memref<64x64xbf16, strided<[1024, 1], offset: ?>> to memref> + %subview_32 = memref.subview %alloc_29[%117, %122] [%118, %123] [1, 1] {ssbuffer.block_id = 10 : i32} : memref<64x64xbf16> to memref> + memref.copy %subview_31, %subview_32 {ssbuffer.block_id = 10 : i32} : memref> to memref> + %124 = bufferization.to_tensor %alloc_29 restrict writable {ssbuffer.block_id = 10 : i32} : memref<64x64xbf16> to tensor<64x64xbf16> + %transposed_33 = linalg.transpose ins(%124 : tensor<64x64xbf16>) outs(%28 : tensor<64x64xbf16>) permutation = [1, 0] {ssbuffer.block_id = 10 : i32} + %125 = tensor.empty() {ssbuffer.block_id = 10 : i32} : tensor<64x64xf32> + %126 = linalg.fill {ssbuffer.block_id = 10 : i32} ins(%cst_1 : f32) outs(%125 : tensor<64x64xf32>) -> tensor<64x64xf32> + %127 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 10 : i32} ins(%124, %transposed_33 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%126 : tensor<64x64xf32>) -> tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 18 : i32}[, , ] flag = -1 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 18 : i32} ins(%127 : tensor<64x64xf32>) outs(%alloc_25 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 10 : i32, ssbuffer.transfer_id = 18 : i32}[, , ] flag = -1 + %128 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 11 : i32} ins(%73, %124 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%32 : tensor<64x64xf32>) -> tensor<64x64xf32> + hivm.hir.sync_block_wait {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 11 : i32}[, , ] flag = 12 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 11 : i32} ins(%128 : tensor<64x64xf32>) outs(%alloc_23 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 11 : i32}[, , ] flag = 12 + hivm.hir.sync_block_wait {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 12 : i32}[, , ] flag = 13 + hivm.hir.fixpipe {dma_mode = #hivm.dma_mode, ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 12 : i32} ins(%128 : tensor<64x64xf32>) outs(%alloc_24 : memref<64x64xf32, #hivm.address_space>) + hivm.hir.sync_block_set {ssbuffer.block_id = 11 : i32, ssbuffer.transfer_id = 12 : i32}[, , ] flag = 13 + } {ssbuffer.block_id = 39 : i32, ssbuffer.main_loop = 2 : i32} + hivm.hir.sync_block_wait {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 19 : i32}[, , ] flag = -1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 18 : i32}[, , ] flag = -1 + hivm.hir.sync_block_wait {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 12 : i32}[, , ] flag = 13 + hivm.hir.sync_block_wait {ssbuffer.block_id = 39 : i32, ssbuffer.transfer_id = 11 : i32}[, , ] flag = 12 + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-function-name-not-intercept.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-function-name-not-intercept.mlir new file mode 100644 index 0000000000..a66da0f9cc --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-function-name-not-intercept.mlir @@ -0,0 +1,9 @@ +// RUN: triton-opt --analyze-name %s | FileCheck %s + +module { + // CHECK-LABEL: func.func @normal_kernel( + func.func @normal_kernel() { + // CHECK: return + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-main-loop-only-copy-or-fixpipe.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-main-loop-only-copy-or-fixpipe.mlir new file mode 100644 index 0000000000..d659add6eb --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-main-loop-only-copy-or-fixpipe.mlir @@ -0,0 +1,42 @@ +// RUN: triton-opt --analyze-scope --verify-diagnostics + +// Unit test for AnalyzeScopePass: when every main_loop id lacks either +// hivm.hir.copy or hivm.hir.fixpipe, the new isMainLoopOnlyCopyOrFixpipe +// check must trigger setFallbackAttr so the dynamic CV pipeline falls +// back to the original workflow. +// +// In this test the VECTOR main_loop (id 0) has only hivm.hir.copy with +// transfer_id (to satisfy checkVecScopeMainLoop) and no fixpipe ops; the +// CUBE main_loop (id 0) has only sync_block ops and no copy or fixpipe. +// For id 0: countCopy > 0 and countFixpipe == 0, so the rule +// (countCopy == 0 || countFixpipe == 0) is satisfied for the only id and +// isMainLoopOnlyCopyOrFixpipe returns true -- fallback is set. + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_main_loop_only_copy(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: memref {tt.divisibility = 16 : i32}, %arg9: memref {tt.divisibility = 16 : i32}, %arg10: i32, %arg11: i32 {tt.divisibility = 16 : i32}, %arg12: i32 {tt.divisibility = 16 : i32}, %arg13: i32, %arg14: i32, %arg15: i32, %arg16: i32, %arg17: i32, %arg18: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant {ssbuffer.block_id = 14 : i32} 0 : i32 + %c1_i32 = arith.constant {Undefined, ssbuffer.block_id = 14 : i32} 1 : i32 + %c32_i32 = arith.constant {ssbuffer.block_id = 14 : i32} 32 : i32 + %cst = arith.constant {ssbuffer.block_id = 14 : i32} 0.000000e+00 : bf16 + %reshape = tensor.empty() {ssbuffer.block_id = 14 : i32} : tensor<4x4x16x16xbf16> + scope.scope : () -> () { + %alloc = memref.alloc() {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 0 : i32} : memref<4x4x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + scf.for %arg19 = %c0_i32 to %c32_i32 step %c1_i32 : i32 { + hivm.hir.sync_block_wait {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + hivm.hir.copy ins(%reshape : tensor<4x4x16x16xbf16>) outs(%alloc : memref<4x4x16x16xbf16, #hivm.address_space>) {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 0 : i32} + hivm.hir.sync_block_set {ssbuffer.block_id = 22 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + } {ssbuffer.block_id = 22 : i32, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + scope.scope : () -> () { + scf.for %arg19 = %c0_i32 to %c32_i32 step %c1_i32 : i32 { + hivm.hir.sync_block_set {ssbuffer.block_id = 22 : i32}[, , ] flag = 2 + hivm.hir.sync_block_wait {ssbuffer.block_id = 22 : i32}[, , ] flag = 2 + } {ssbuffer.block_id = 22 : i32, ssbuffer.main_loop = 0 : i32} + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-update-before-use.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-update-before-use.mlir new file mode 100644 index 0000000000..b0a05598e2 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-update-before-use.mlir @@ -0,0 +1,85 @@ +// RUN: triton-opt --split-input-file --analyze-args %s | FileCheck %s + +// Buffer count is set to 2/1 on every module so the existing +// `checkTensorArgsInMainLoop` gate (intra=3 && inter=2) is disabled; these +// tests exercise only the new update-before-use check. + +// Test 1 (positive, scf.for): tensor iter_arg %arg34 is used twice. +// The defining op %82 of yield's i=0 operand is the iter_arg's update; it +// is defined BEFORE %83 (update-before-use) and at a different block_id +// (10 vs 8). This must trigger ERRCODE_IGNORED. +// CHECK: triton_ascend.dynamic_cv_pipeline.rc = 2 +// CHECK: @update_before_use_for +module attributes {ssbuffer.inter_core_buf_count = 1 : i32, ssbuffer.intra_buf_count = 2 : i32} { + func.func @update_before_use_for() -> () { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c10 = arith.constant 10 : index + %init = tensor.empty() : tensor<64x64xf32> + %result = scf.for %iv = %c0 to %c10 step %c1 iter_args(%arg34 = %init) -> (tensor<64x64xf32>) { + // First use of %arg34 at block_id=10 (before update). + %81 = arith.mulf %arg34, %init {ssbuffer.block_id = 10 : i32} : tensor<64x64xf32> + // Update-defining op at block_id=10. + %82 = arith.addf %81, %init {ssbuffer.block_id = 10 : i32} : tensor<64x64xf32> + // Late use of %arg34 AFTER update at block_id=8 — different block_id. + %83 = arith.mulf %arg34, %init {ssbuffer.block_id = 8 : i32} : tensor<64x64xf32> + scf.yield %82 : tensor<64x64xf32> + } {ssbuffer.main_loop = 0 : i32} + return + } +} + +// ----- + +// Test 2 (positive, scf.while): same update-before-use pattern as Test 1 +// but on scf.while's do-region. ERRCODE_IGNORED must fire. +// CHECK: triton_ascend.dynamic_cv_pipeline.rc = 2 +// CHECK: @update_before_use_while +module attributes {ssbuffer.inter_core_buf_count = 1 : i32, ssbuffer.intra_buf_count = 2 : i32} { + func.func @update_before_use_while() -> () { + %c0_i64 = arith.constant 0 : i64 + %c1_i64 = arith.constant 1 : i64 + %init = tensor.empty() : tensor<64xf32> + %result:2 = scf.while (%arg2 = %c0_i64, %arg3 = %init) : (i64, tensor<64xf32>) -> (i64, tensor<64xf32>) { + %cmp = arith.cmpi slt, %arg2, %c1_i64 : i64 + scf.condition(%cmp) %arg2, %arg3 : i64, tensor<64xf32> + } do { + ^bb0(%arg2: i64, %arg3: tensor<64xf32>): + // First use of %arg3 at block_id=10 (before update). + %mul = arith.mulf %arg3, %arg3 {ssbuffer.block_id = 10 : i32} : tensor<64xf32> + // Update-defining op at block_id=10. + %update = arith.addf %mul, %init {ssbuffer.block_id = 10 : i32} : tensor<64xf32> + // Late use of %arg3 AFTER update at block_id=8 — different block_id. + %late = arith.mulf %arg3, %init {ssbuffer.block_id = 8 : i32} : tensor<64xf32> + %next = arith.addi %arg2, %c1_i64 : i64 + scf.yield %next, %update : i64, tensor<64xf32> + } attributes {ssbuffer.main_loop = 0 : i32} + return + } +} + +// ----- + +// Test 3 (negative): tensor iter_arg with code-order update-before-use +// where the late use shares the same block_id as the update. The block_id +// mismatch is what triggers the fallback; with same block_id, no fallback +// must be set on this module. +// CHECK-NOT: triton_ascend.dynamic_cv_pipeline.rc +// CHECK: @no_update_before_use_same_block +// CHECK: return +module attributes {ssbuffer.inter_core_buf_count = 1 : i32, ssbuffer.intra_buf_count = 2 : i32} { + func.func @no_update_before_use_same_block() -> () { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c10 = arith.constant 10 : index + %init = tensor.empty() : tensor<64x64xf32> + %result = scf.for %iv = %c0 to %c10 step %c1 iter_args(%arg34 = %init) -> (tensor<64x64xf32>) { + %81 = arith.mulf %arg34, %init {ssbuffer.block_id = 10 : i32} : tensor<64x64xf32> + %82 = arith.addf %81, %init {ssbuffer.block_id = 10 : i32} : tensor<64x64xf32> + // Late use AFTER update with SAME block_id — must not trigger. + %83 = arith.mulf %arg34, %init {ssbuffer.block_id = 10 : i32} : tensor<64x64xf32> + scf.yield %82 : tensor<64x64xf32> + } {ssbuffer.main_loop = 0 : i32} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-not-main-loop.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-not-main-loop.mlir new file mode 100644 index 0000000000..c813569322 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-not-main-loop.mlir @@ -0,0 +1,35 @@ +// RUN: triton-opt --analyze-while-condition-args %s | FileCheck %s + +// Same tensor-driven condition update as check-while-cond-tensor-update.mlir, +// but the scf.while is not tagged as the main loop. Only the main loop drives +// the cube/vector split, so this one is none of the pass' business and the +// fallback attr must NOT be set. +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @while_cond_tensor_update_not_main_loop(%arg0: memref, %arg1: i64) { + %c0_i64 = arith.constant 0 : i64 + %cst = arith.constant 0.000000e+00 : f32 + %0 = tensor.empty() : tensor<64xf32> + %1 = linalg.fill ins(%cst : f32) outs(%0 : tensor<64xf32>) -> tensor<64xf32> + %2:2 = scf.while (%arg2 = %c0_i64, %arg3 = %c0_i64) : (i64, i64) -> (i64, i64) { + %3 = arith.cmpi slt, %arg3, %arg1 : i64 + scf.condition(%3) %arg2, %arg3 : i64, i64 + } do { + ^bb0(%arg2: i64, %arg3: i64): + %3 = tensor.empty() : tensor + %4 = linalg.fill ins(%cst : f32) outs(%3 : tensor) -> tensor + %reduced = linalg.reduce ins(%1 : tensor<64xf32>) outs(%4 : tensor) dimensions = [0] + (%in: f32, %init: f32) { + %7 = arith.addf %in, %init : f32 + linalg.yield %7 : f32 + } + %5 = tensor.extract %reduced[] : tensor + %6 = arith.fptosi %5 : f32 to i64 + %7 = arith.addi %arg3, %6 : i64 + scf.yield %arg2, %7 : i64, i64 + } + return + } +} + +// CHECK-NOT: triton_ascend.dynamic_cv_pipeline.rc +// CHECK: return diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-outside-tensor-update.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-outside-tensor-update.mlir new file mode 100644 index 0000000000..2d5a04a362 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-outside-tensor-update.mlir @@ -0,0 +1,35 @@ +// RUN: triton-opt --analyze-while-condition-args %s | FileCheck %s + +// The tensor reduction feeding the loop-carried arg %arg3 is computed outside +// the scf.while; the do-region only extracts a scalar out of it. The non-scalar +// computation is not part of the scope that has to be cloned, so the fallback +// attr must NOT be set. +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @while_cond_updated_by_outside_tensor(%arg0: memref, %arg1: i64) { + %c0_i64 = arith.constant 0 : i64 + %cst = arith.constant 0.000000e+00 : f32 + %0 = tensor.empty() : tensor<64xf32> + %1 = linalg.fill ins(%cst : f32) outs(%0 : tensor<64xf32>) -> tensor<64xf32> + %2 = tensor.empty() : tensor + %3 = linalg.fill ins(%cst : f32) outs(%2 : tensor) -> tensor + %reduced = linalg.reduce ins(%1 : tensor<64xf32>) outs(%3 : tensor) dimensions = [0] + (%in: f32, %init: f32) { + %4 = arith.addf %in, %init : f32 + linalg.yield %4 : f32 + } + %5:2 = scf.while (%arg2 = %c0_i64, %arg3 = %c0_i64) : (i64, i64) -> (i64, i64) { + %6 = arith.cmpi slt, %arg3, %arg1 : i64 + scf.condition(%6) %arg2, %arg3 : i64, i64 + } do { + ^bb0(%arg2: i64, %arg3: i64): + %6 = tensor.extract %reduced[] : tensor + %7 = arith.fptosi %6 : f32 to i64 + %8 = arith.addi %arg3, %7 : i64 + scf.yield %arg2, %8 : i64, i64 + } attributes {ssbuffer.main_loop = 1 : i64} + return + } +} + +// CHECK-NOT: triton_ascend.dynamic_cv_pipeline.rc +// CHECK: return diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-scalar-update.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-scalar-update.mlir new file mode 100644 index 0000000000..a1fd2d0555 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-scalar-update.mlir @@ -0,0 +1,29 @@ +// RUN: triton-opt --analyze-while-condition-args %s | FileCheck %s + +// The loop-carried args feeding the scf.condition are updated purely with +// scalars (memref.load of a scalar + arith). This is the supported pattern, +// so the fallback attr must NOT be set. +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @while_cond_updated_by_scalar_load(%arg0: memref, %arg1: i64) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c0_i64 = arith.constant 0 : i64 + %c1_i64 = arith.constant 1 : i64 + %0:2 = scf.while (%arg2 = %c0_i64, %arg3 = %c0_i64) : (i64, i64) -> (i64, i64) { + %1 = arith.addi %arg2, %arg3 : i64 + %2 = arith.cmpi sge, %arg1, %1 : i64 + scf.condition(%2) %arg2, %arg3 : i64, i64 + } do { + ^bb0(%arg2: i64, %arg3: i64): + %1 = arith.addi %arg2, %c1_i64 : i64 + %2 = arith.index_cast %1 : i64 to index + %reinterpret_cast = memref.reinterpret_cast %arg0 to offset: [%2], sizes: [1], strides: [1] : memref to memref<1xi64, strided<[1], offset: ?>> + %3 = memref.load %reinterpret_cast[%c0] : memref<1xi64, strided<[1], offset: ?>> + scf.yield %1, %3 : i64, i64 + } attributes {ssbuffer.main_loop = 1 : i64} + return + } +} + +// CHECK-NOT: triton_ascend.dynamic_cv_pipeline.rc +// CHECK: return diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-tensor-update.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-tensor-update.mlir new file mode 100644 index 0000000000..ba574f3bcd --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/check-while-cond-tensor-update.mlir @@ -0,0 +1,33 @@ +// RUN: triton-opt --analyze-while-condition-args %s | FileCheck %s + +// The loop-carried arg %arg3 feeds the scf.condition and is updated in the +// do-region by a scalar produced from a tensor reduction (tl.sum style). +// The dynamic CV pipeline cannot handle this, so the fallback attr must be set. +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @while_cond_updated_by_tensor_sum(%arg0: memref, %arg1: i64) { + %c0_i64 = arith.constant 0 : i64 + %cst = arith.constant 0.000000e+00 : f32 + %0 = tensor.empty() : tensor<64xf32> + %1 = linalg.fill ins(%cst : f32) outs(%0 : tensor<64xf32>) -> tensor<64xf32> + %2:2 = scf.while (%arg2 = %c0_i64, %arg3 = %c0_i64) : (i64, i64) -> (i64, i64) { + %3 = arith.cmpi slt, %arg3, %arg1 : i64 + scf.condition(%3) %arg2, %arg3 : i64, i64 + } do { + ^bb0(%arg2: i64, %arg3: i64): + %3 = tensor.empty() : tensor + %4 = linalg.fill ins(%cst : f32) outs(%3 : tensor) -> tensor + %reduced = linalg.reduce ins(%1 : tensor<64xf32>) outs(%4 : tensor) dimensions = [0] + (%in: f32, %init: f32) { + %7 = arith.addf %in, %init : f32 + linalg.yield %7 : f32 + } + %5 = tensor.extract %reduced[] : tensor + %6 = arith.fptosi %5 : f32 to i64 + %7 = arith.addi %arg3, %6 : i64 + scf.yield %arg2, %7 : i64, i64 + } attributes {ssbuffer.main_loop = 1 : i64} + return + } +} + +// CHECK: triton_ascend.dynamic_cv_pipeline.rc diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/test-single-scope.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/test-single-scope.mlir new file mode 100644 index 0000000000..0cf0ea76c9 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/AnalyzeDataFlow/test-single-scope.mlir @@ -0,0 +1,73 @@ +// RUN: triton-opt --analyze-data-flow --verify-diagnostics %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { // expected-error + func.func @chunk_gated_delta_rule_fwd_kernel_h_blockdim64(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg8: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c16384_i32 = arith.constant {ssbuffer.block_id = 16 : i32} 16384 : i32 + %cst = arith.constant {ssbuffer.block_id = 12 : i32} dense<[2, 4, 16, 16]> : tensor<4xi64> + %cst_0 = arith.constant {ssbuffer.block_id = 12 : i32} dense<[64, 2, 16]> : tensor<3xi64> + %cst_1 = arith.constant {ssbuffer.block_id = 15 : i32} 0.000000e+00 : f32 + %c0_i32 = arith.constant {ssbuffer.block_id = 15 : i32} 0 : i32 + %c128_i64 = arith.constant {ssbuffer.block_id = 15 : i32} 128 : i64 + %c1_i32 = arith.constant {ssbuffer.block_id = 15 : i32} 1 : i32 + %c64_i32 = arith.constant {MixUse, ssbuffer.block_id = 15 : i32} 64 : i32 + %c32_i32 = arith.constant {ssbuffer.block_id = 15 : i32} 32 : i32 + %c64 = arith.constant {DataUse, ssbuffer.block_id = 15 : i32} 64 : index + %c0 = arith.constant {DataUse, ssbuffer.block_id = 15 : i32} 0 : index + %c63_i32 = arith.constant {ssbuffer.block_id = 15 : i32} 63 : i32 + %c16384_i64 = arith.constant {ssbuffer.block_id = 15 : i32} 16384 : i64 + %c524288_i64 = arith.constant {ssbuffer.block_id = 15 : i32} 524288 : i64 + %c128 = arith.constant {ssbuffer.block_id = 15 : i32} 128 : index + %c32 = arith.constant {ssbuffer.block_id = 15 : i32} 32 : index + %c4096 = arith.constant {ssbuffer.block_id = 15 : i32} 4096 : index + %cst_2 = arith.constant {ssbuffer.block_id = 15 : i32} 0.000000e+00 : bf16 + %c8192 = arith.constant {ssbuffer.block_id = 15 : i32} 8192 : index + scope.scope : () -> () { + %0 = arith.divsi %arg14, %c32_i32 {ssbuffer.block_id = 9 : i32} : i32 + %1 = arith.remsi %arg14, %c32_i32 {ssbuffer.block_id = 9 : i32} : i32 + %2 = arith.muli %0, %arg9 {ssbuffer.block_id = 9 : i32} : i32 + %3 = arith.muli %2, %c32_i32 {ssbuffer.block_id = 9 : i32} : i32 + %4 = arith.addi %3, %1 {ssbuffer.block_id = 9 : i32} : i32 + %5 = arith.extsi %4 {ssbuffer.block_id = 9 : i32} : i32 to i64 + %6 = arith.muli %5, %c128_i64 {ssbuffer.block_id = 9 : i32} : i64 + %7 = arith.index_cast %6 {ssbuffer.block_id = 9 : i32} : i64 to index + %8 = tensor.empty() {ssbuffer.block_id = 15 : i32} : tensor<64x32xf32> + %9 = linalg.fill {ssbuffer.block_id = 15 : i32} ins(%cst_1 : f32) outs(%8 : tensor<64x32xf32>) -> tensor<64x32xf32> + %10 = arith.divsi %arg14, %c32_i32 {ssbuffer.block_id = 15 : i32} : i32 + %11 = arith.remsi %arg14, %c32_i32 {ssbuffer.block_id = 15 : i32} : i32 + %12 = arith.muli %10, %arg9 {ssbuffer.block_id = 15 : i32} : i32 + %13 = arith.addi %arg9, %c63_i32 {ssbuffer.block_id = 15 : i32} : i32 + %14 = arith.divsi %13, %c64_i32 {ssbuffer.block_id = 15 : i32} : i32 + %15 = arith.muli %12, %c32_i32 {ssbuffer.block_id = 15 : i32} : i32 + %16 = arith.addi %15, %11 {ssbuffer.block_id = 15 : i32} : i32 + %17 = arith.extsi %16 {ssbuffer.block_id = 15 : i32} : i32 to i64 + %18 = arith.muli %17, %c128_i64 {ssbuffer.block_id = 15 : i32} : i64 + %19 = arith.index_cast %18 {ssbuffer.block_id = 15 : i32} : i64 to index + %alloc = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 0 : i32} : memref<2x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<0>, ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 0 : i32} : memref<2x4x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 0 : i32}[, , ] flag = 1 + %alloc_3 = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 1 : i32} : memref<2x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_3 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<1>, ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 1 : i32} : memref<2x4x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 1 : i32}[, , ] flag = 2 + %alloc_4 = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 2 : i32} : memref<2x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_4 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<2>, ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 2 : i32} : memref<2x4x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 2 : i32}[, , ] flag = 3 + %alloc_5 = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 3 : i32} : memref<2x4x16x16xbf16, #hivm.address_space> + annotation.mark %alloc_5 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<3>, ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 3 : i32} : memref<2x4x16x16xbf16, #hivm.address_space> + hivm.hir.sync_block_set {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 3 : i32}[, , ] flag = 4 + %alloc_6 = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 4 : i32} : memref<64x32xf32, #hivm.address_space> + annotation.mark %alloc_6 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<4>, ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 4 : i32} : memref<64x32xf32, #hivm.address_space> + %alloc_7 = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 5 : i32} : memref<64x32xf32, #hivm.address_space> + annotation.mark %alloc_7 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<5>, ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 5 : i32} : memref<64x32xf32, #hivm.address_space> + %alloc_8 = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x32xf32, #hivm.address_space> + annotation.mark %alloc_8 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<6>, ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 6 : i32} : memref<64x32xf32, #hivm.address_space> + %alloc_9 = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x32xf32, #hivm.address_space> + annotation.mark %alloc_9 {effects = ["write", "read"], hivm.tightly_coupled_buffer = #hivm.tightly_coupled_buffer<7>, ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 7 : i32} : memref<64x32xf32, #hivm.address_space> + hivm.hir.sync_block_wait {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 7 : i32}[, , ] flag = 8 + hivm.hir.sync_block_wait {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 6 : i32}[, , ] flag = 7 + hivm.hir.sync_block_wait {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 5 : i32}[, , ] flag = 6 + hivm.hir.sync_block_wait {ssbuffer.block_id = 17 : i32, ssbuffer.transfer_id = 4 : i32}[, , ] flag = 5 + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/else_placeholder_fill.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/else_placeholder_fill.mlir new file mode 100644 index 0000000000..49e6b79af3 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/else_placeholder_fill.mlir @@ -0,0 +1,78 @@ +// RUN: triton-opt --split-input-file --split-if-by-block-id %s | FileCheck %s + +// VCV then-side split: VECTOR placeholder (block_id=94) in else block +// produces linalg.fill + arith.constant 0; CUBE placeholder (block_id=95) +// reuses the matmul source tensor without fill. +// CHECK-LABEL: func.func @vector_placeholder_has_fill +// VECTOR placeholder has fill in else block. +// CHECK: tensor.empty() {ssbuffer.block_id = 94 : i32} +// CHECK: arith.constant 0.000000e+00 : f32 +// CHECK: linalg.fill {ssbuffer.block_id = 94 : i32} +// CUBE placeholder reuses matmul source, no fill. +// CHECK-NOT: linalg.fill {ssbuffer.block_id = 95 : i32} +func.func @vector_placeholder_has_fill(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m = linalg.matmul ins(%v1, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v2 = arith.mulf %m, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} + +// ----- + +// CVC else-side split: VECTOR placeholder (block_id=96) in then block +// also produces linalg.fill. +// CHECK-LABEL: func.func @vector_placeholder_has_fill_else +// CHECK: tensor.empty() {ssbuffer.block_id = 96 : i32} +// CHECK: arith.constant 0.000000e+00 : f32 +// CHECK: linalg.fill {ssbuffer.block_id = 96 : i32} +func.func @vector_placeholder_has_fill_else(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: f32, %d: f32, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : f32 + scf.if %cond { + %x = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : f32 + } else { + %m1 = linalg.matmul ins(%a, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v1 = arith.addf %m1, %m1 {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m2 = linalg.matmul ins(%a, %v1 : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 97 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + } + } + return +} + +// ----- + +// Matmul bias comes from tensor.empty + linalg.fill chain (outside the if). +// The CUBE placeholder traces through createMatmulPlaceHolderValue and +// produces only tensor::EmptyOp without linalg.fill. +// CHECK-LABEL: func.func @matmul_placeholder_no_fill +// VECTOR placeholder: fill + constant present. +// CHECK: tensor.empty() {ssbuffer.block_id = 94 : i32} +// CHECK: linalg.fill {ssbuffer.block_id = 94 : i32} +// CUBE placeholder: only tensor.empty, no linalg.fill. +// CHECK: tensor.empty() {ssbuffer.block_id = 95 : i32} +// CHECK-NOT: linalg.fill {ssbuffer.block_id = 95 : i32} +func.func @matmul_placeholder_no_fill(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %cst = arith.constant 0.000000e+00 : f32 + %init = tensor.empty() : tensor<2x2xf32> + %zero = linalg.fill ins(%cst : f32) outs(%init : tensor<2x2xf32>) -> tensor<2x2xf32> + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m = linalg.matmul ins(%v1, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%zero : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v2 = arith.mulf %m, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/positive_split.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/positive_split.mlir new file mode 100644 index 0000000000..1cac05e4ff --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/positive_split.mlir @@ -0,0 +1,139 @@ +// RUN: triton-opt --split-input-file --split-if-by-block-id %s | FileCheck %s + +// VCV: then side has VECTOR -> CUBE -> VECTOR with cross-core data flow +// (%m consumes %v1, %v2 consumes %m), should split. +// CHECK-LABEL: func.func @split_then_vcv +// CHECK: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 94 +// CHECK: scf.if +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 95 +// CHECK: scf.if +// CHECK: arith.mulf {{.*}}ssbuffer.block_id = 96 +func.func @split_then_vcv(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m = linalg.matmul ins(%v1, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v2 = arith.mulf %m, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} + +// ----- + +// CVC: else side has CUBE -> VECTOR -> CUBE with cross-core data flow +// (%v1 consumes %m1, %m2 consumes %v1), should split. Split order follows +// the SSA data chain 95 -> 96 -> 97. +// CHECK-LABEL: func.func @split_else_cvc +// CHECK: scf.if +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 95 +// CHECK: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 96 +// CHECK: scf.if +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 97 +func.func @split_else_cvc(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: f32, %d: f32, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : f32 + scf.if %cond { + %x = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : f32 + } else { + %m1 = linalg.matmul ins(%a, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v1 = arith.addf %m1, %m1 {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m2 = linalg.matmul ins(%a, %v1 : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 97 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + } + } + return +} + +// ----- + +// ForOp with only CUBE ops inside: groupIsCube classifies it as CUBE via +// getCoreTypeOfSimpleOpOrCf body walk. VCV pattern should split. +// CHECK-LABEL: func.func @split_then_vcv_for_cube_only +// CHECK: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 94 +// CHECK: scf.if +// CHECK: scf.for +// CHECK: linalg.matmul {{.*}}CUBE +// CHECK: scf.if +// CHECK: arith.mulf {{.*}}ssbuffer.block_id = 96 +func.func @split_then_vcv_for_cube_only(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %for_res = scf.for %i = %c0 to %c1 step %c1 iter_args(%arg = %v1) -> (tensor<2x2xf32>) { + %m = linalg.matmul ins(%arg, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + scf.yield %m : tensor<2x2xf32> + } {ssbuffer.block_id = 95 : i32} + %v2 = arith.mulf %for_res, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} + +// ----- + +// yield-bearing if with VECTOR+CUBE+VECTOR, after split scf.yield must NOT +// carry ssbuffer.block_id, and placeholder tensor.empty must carry it. +// CHECK-LABEL: func.func @no_block_id_on_scf_yield +// CHECK: tensor.empty() {ssbuffer.block_id = 94 : i32} +// CHECK: scf.yield +// CHECK-NOT: scf.yield {{.*}}ssbuffer.block_id +func.func @no_block_id_on_scf_yield(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) -> tensor<2x2xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %cst = arith.constant 0.000000e+00 : f32 + %init = tensor.empty() : tensor<2x2xf32> + %zero = linalg.fill ins(%cst : f32) outs(%init : tensor<2x2xf32>) -> tensor<2x2xf32> + %res = scf.for %iv = %c0 to %c1 step %c1 iter_args(%arg = %zero) -> (tensor<2x2xf32>) { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + %r = scf.if %cond -> (tensor<2x2xf32>) { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m = linalg.matmul ins(%v1, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v2 = arith.mulf %m, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + scf.yield %v2 : tensor<2x2xf32> + } else { + scf.yield %arg : tensor<2x2xf32> + } + scf.yield %r : tensor<2x2xf32> + } + return %res : tensor<2x2xf32> +} + +// ----- + +// CCVC: then side has CUBE -> CUBE -> VECTOR -> CUBE. +// Consecutive same-core-type groups are NOT merged; every block_id gets +// its own scf.if. +// CHECK-LABEL: func.func @split_then_ccvc +// CHECK: scf.if +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 95 +// CHECK: scf.if +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 96 +// CHECK: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 97 +// CHECK: scf.if +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 98 +func.func @split_then_ccvc(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %m1 = linalg.matmul ins(%c, %d : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %m2 = linalg.matmul ins(%m1, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v = arith.addf %m1, %m2 {ssbuffer.block_id = 97 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m3 = linalg.matmul ins(%v, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 98 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + } + } + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/skip_negative.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/skip_negative.mlir new file mode 100644 index 0000000000..af19a1a866 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/skip_negative.mlir @@ -0,0 +1,146 @@ +// RUN: triton-opt --split-input-file --split-if-by-block-id %s | FileCheck %s + +// VCV ordering WITHOUT cross-core data consumption: %m consumes no VECTOR +// value and no VECTOR op consumes %m. Should NOT split even though the +// group order is VECTOR -> CUBE -> VECTOR. +// CHECK-LABEL: func.func @skip_vcv_no_data_flow +// CHECK-COUNT-1: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 94 +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 95 +// CHECK: arith.mulf {{.*}}ssbuffer.block_id = 96 +func.func @skip_vcv_no_data_flow(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: f32, %d: f32, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : f32 + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %m = linalg.matmul ins(%a, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v2 = arith.mulf %c, %d {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : f32 + } + } + return +} + +// ----- + +// Non-CVC/VCV: else side has only 2 groups (CUBE+VECTOR), not alternating 3 groups, should NOT split. +// CHECK-LABEL: func.func @skip_two_groups +// CHECK-COUNT-1: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 94 +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 95 +func.func @skip_two_groups(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: f32, %d: f32, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : f32 + scf.if %cond { + %x = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : f32 + } else { + %y = linalg.matmul ins(%a, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + } + } + return +} + +// ----- + +// Non-CVC/VCV: else side has 2 pure VECTOR groups (no matmul), then side has +// 1 group. shouldSplitThen/shouldSplitElse are both false while elseGroups +// has >= 2 entries; without the needsSplit() guard this would read +// ya.groupOutputs out of bounds in materializeCandidate. +// CHECK-LABEL: func.func @skip_else_pure_vector +// CHECK-COUNT-1: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 94 +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 95 +// CHECK: arith.mulf {{.*}}ssbuffer.block_id = 96 +func.func @skip_else_pure_vector(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: f32, %d: f32, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : f32 + scf.if %cond { + %x = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : f32 + } else { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %v2 = arith.mulf %c, %d {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : f32 + } + } + return +} + +// ----- + +// ForOp with both CUBE and VECTOR ops inside: getCoreTypeOfSimpleOpOrCf +// returns CUBE_AND_VECTOR, which groupIsCube treats as unclassified. +// With no CUBE group, should NOT split. +// CHECK-LABEL: func.func @skip_vcv_for_mixed +// CHECK-COUNT-1: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 94 +// CHECK: scf.for +// CHECK: linalg.matmul {{.*}}CUBE +// CHECK: arith.addf {{.*}}VECTOR +// CHECK: arith.mulf {{.*}}ssbuffer.block_id = 96 +func.func @skip_vcv_for_mixed(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %for_res = scf.for %i = %c0 to %c1 step %c1 iter_args(%arg = %v1) -> (tensor<2x2xf32>) { + %m = linalg.matmul ins(%arg, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %inner = arith.addf %m, %m {ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + scf.yield %inner : tensor<2x2xf32> + } {ssbuffer.block_id = 95 : i32} + %v2 = arith.mulf %for_res, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} + +// ----- + +// Group with CUBE_AND_VECTOR op: not CUBE_ONLY, not VECTOR_ONLY, +// groupIsCube treats it as unclassified. With no CUBE group, should NOT split. +// CHECK-LABEL: func.func @skip_cube_and_vector_group +// CHECK-COUNT-1: scf.if +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 94 +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 95 +// CHECK: arith.mulf {{.*}}ssbuffer.block_id = 96 +func.func @skip_cube_and_vector_group(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %v2 = arith.addf %v1, %c {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE_AND_VECTOR"} : tensor<2x2xf32> + %v3 = arith.mulf %v2, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} + +// ----- + +// CCV: then side has CUBE -> CUBE -> VECTOR, no trailing CUBE group. +// Data flow is only CUBE->VECTOR (no VECTOR->CUBE), so should NOT split. +// CHECK-LABEL: func.func @skip_ccv_no_trailing_cube +// CHECK-COUNT-1: scf.if +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 95 +// CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 96 +// CHECK: arith.addf {{.*}}ssbuffer.block_id = 97 +func.func @skip_ccv_no_trailing_cube(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %m1 = linalg.matmul ins(%a, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %m2 = linalg.matmul ins(%m1, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v = arith.addf %m1, %m2 {ssbuffer.block_id = 97 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/splitted_if_tag.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/splitted_if_tag.mlir new file mode 100644 index 0000000000..fe0cba50ff --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/SplitIfByBlockId/splitted_if_tag.mlir @@ -0,0 +1,84 @@ +// RUN: triton-opt --split-input-file --split-if-by-block-id %s | FileCheck %s + +// Verify that kSplittedIf ("ssbuffer.splitted_if") is a unique int tag +// per source if-op: all split if-ops from the same source share the same +// tag, preserved after rearrangeIfOp for downstream passes. + +// VCV: then side has VECTOR -> CUBE -> VECTOR with cross-core data flow. +// All 3 split if-ops from this source share tag 0. +// CHECK-LABEL: func.func @has_splitted_if_tag +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK-NOT: ssbuffer.splitted_if = 0 : i32 +func.func @has_splitted_if_tag(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m = linalg.matmul ins(%v1, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v2 = arith.mulf %m, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} + +// ----- + +// CVC: else side has CUBE -> VECTOR -> CUBE. Single source if in this +// test case, counter resets each run, so tag is 0. +// CHECK-LABEL: func.func @has_splitted_if_tag_else +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK-NOT: ssbuffer.splitted_if = 0 : i32 +func.func @has_splitted_if_tag_else(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: f32, %d: f32, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : f32 + scf.if %cond { + %x = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : f32 + } else { + %m1 = linalg.matmul ins(%a, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v1 = arith.addf %m1, %m1 {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m2 = linalg.matmul ins(%a, %v1 : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 97 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + } + } + return +} + +// ----- + +// Two different source ifs in the same function: first gets tag 0, +// second gets tag 1. Each is VCV then-side (3 split ifs). +// CHECK-LABEL: func.func @two_ifs_different_tags +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 0 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 1 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 1 : i32} +// CHECK: } {{.*}}ssbuffer.splitted_if = 1 : i32} +// CHECK-NOT: ssbuffer.splitted_if +func.func @two_ifs_different_tags(%a: tensor<2x2xf32>, %b: tensor<2x2xf32>, %c: tensor<2x2xf32>, %d: tensor<2x2xf32>, %cond: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %iv = %c0 to %c1 step %c1 { + %cube = arith.addf %c, %d {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<2x2xf32> + // First if: VCV then-side -> tag 0 + scf.if %cond { + %v1 = arith.addf %c, %d {ssbuffer.block_id = 94 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m1 = linalg.matmul ins(%v1, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 95 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v2 = arith.mulf %m1, %b {ssbuffer.block_id = 96 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + // Second if: VCV then-side -> tag 1 + scf.if %cond { + %v3 = arith.subf %c, %d {ssbuffer.block_id = 97 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + %m2 = linalg.matmul ins(%v3, %b : tensor<2x2xf32>, tensor<2x2xf32>) outs(%a : tensor<2x2xf32>) {ssbuffer.block_id = 98 : i32, ssbuffer.core_type = "CUBE"} -> tensor<2x2xf32> + %v4 = arith.mulf %m2, %b {ssbuffer.block_id = 99 : i32, ssbuffer.core_type = "VECTOR"} : tensor<2x2xf32> + } + } + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/exp_subf_pattern.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/exp_subf_pattern.mlir new file mode 100644 index 0000000000..bb44c88e8b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/exp_subf_pattern.mlir @@ -0,0 +1,105 @@ +// RUN: triton-opt --exp-subf-pattern %s | FileCheck %s + +module { + // ============================================ + // Test 1: Simple pattern - subf -> exp + // ============================================ + // CHECK-LABEL: func @test_simple_pattern + func.func @test_simple_pattern(%arg0: f32, %arg1: f32) -> f32 { + %cst = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} 1.000000e+00 : f32 + %sub = arith.subf %arg0, %arg1 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f32 + // CHECK: math.exp %{{.*}} {ssbuffer.block_id = 1 : i32 + %exp = math.exp %sub {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %mulf = arith.mulf %exp, %cst {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %addf = arith.addf %exp, %cst {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return %addf : f32 + } + + // ============================================ + // Test 2: Extended pattern - extf(f16->f32) -> subf -> exp + // ============================================ + // CHECK-LABEL: func @test_extended_pattern + func.func @test_extended_pattern(%arg0: f16, %arg1: f16) -> f32 { + // CHECK: arith.extf %{{.*}} {ssbuffer.block_id = 1 : i32 + %extf1 = arith.extf %arg0 {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : f16 to f32 + // CHECK: arith.extf %{{.*}} {ssbuffer.block_id = 1 : i32 + %extf2 = arith.extf %arg1 {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} : f16 to f32 + %sub = arith.subf %extf1, %extf2 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f32 + // CHECK: math.exp %{{.*}} {ssbuffer.block_id = 1 : i32 + %exp = math.exp %sub {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %mulf = arith.mulf %exp, %exp {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return %mulf : f32 + } + + // ============================================ + // Test 3: Extended pattern with matching blockId + // ============================================ + // CHECK-LABEL: func @test_extended_pattern_matching_blockid + func.func @test_extended_pattern_matching_blockid(%arg0: f16, %arg1: f16) -> f32 { + // CHECK: arith.extf %{{.*}} {ssbuffer.block_id = 1 : i32 + %extf1 = arith.extf %arg0 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f16 to f32 + // CHECK: arith.extf %{{.*}} {ssbuffer.block_id = 1 : i32 + %extf2 = arith.extf %arg1 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f16 to f32 + %sub = arith.subf %extf1, %extf2 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f32 + // CHECK: math.exp %{{.*}} {ssbuffer.block_id = 1 : i32 + %exp = math.exp %sub {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %mulf = arith.mulf %exp, %exp {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return %mulf : f32 + } + + // ============================================ + // Test 4: No match - subf has multiple users + // ============================================ + // CHECK-LABEL: func @test_no_match_subf_multiple_users + func.func @test_no_match_subf_multiple_users(%arg0: f32, %arg1: f32) -> f32 { + %sub = arith.subf %arg0, %arg1 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f32 + // CHECK: math.exp %{{.*}} {ssbuffer.block_id = 2 : i32 + %exp = math.exp %sub {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %addf = arith.addf %sub, %arg0 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return %addf : f32 + } + + // ============================================ + // Test 5: No match extended - extf has multiple users + // ============================================ + // CHECK-LABEL: func @test_no_match_extf_multiple_users + func.func @test_no_match_extf_multiple_users(%arg0: f16, %arg1: f16) -> f32 { + // CHECK: arith.extf %{{.*}} {ssbuffer.block_id = 5 : i32 + %extf1 = arith.extf %arg0 {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : f16 to f32 + %extf2 = arith.extf %arg1 {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} : f16 to f32 + %sub = arith.subf %extf1, %extf2 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f32 + // CHECK: math.exp %{{.*}} {ssbuffer.block_id = 1 : i32 + %exp = math.exp %sub {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %addf = arith.addf %extf1, %extf2 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %mulf = arith.mulf %exp, %addf {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return %mulf : f32 + } + + // ============================================ + // Test 6: No match extended - wrong type conversion + // ============================================ + // CHECK-LABEL: func @test_no_match_wrong_type_conversion + func.func @test_no_match_wrong_type_conversion(%arg0: bf16, %arg1: bf16) -> f32 { + // CHECK: arith.extf %{{.*}} {ssbuffer.block_id = 5 : i32 + %extf1 = arith.extf %arg0 {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : bf16 to f32 + // CHECK: arith.extf %{{.*}} {ssbuffer.block_id = 6 : i32 + %extf2 = arith.extf %arg1 {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} : bf16 to f32 + %sub = arith.subf %extf1, %extf2 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f32 + // CHECK: math.exp %{{.*}} {ssbuffer.block_id = 1 : i32 + %exp = math.exp %sub {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return %exp : f32 + } + + // ============================================ + // Test 7: Mixed pattern - one operand with extf, one without + // ============================================ + // CHECK-LABEL: func @test_mixed_pattern + func.func @test_mixed_pattern(%arg0: f16, %arg1: f32) -> f32 { + // CHECK: arith.extf %{{.*}} {ssbuffer.block_id = 5 : i32 + %extf1 = arith.extf %arg0 {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : f16 to f32 + %sub = arith.subf %extf1, %arg1 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : f32 + // CHECK: math.exp %{{.*}} {ssbuffer.block_id = 1 : i32 + %exp = math.exp %sub {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return %exp : f32 + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_compute_block.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_compute_block.mlir new file mode 100644 index 0000000000..49c10b4acd --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_compute_block.mlir @@ -0,0 +1,140 @@ +// RUN: triton-opt --allow-unregistered-dialect --merge-compute-block %s | FileCheck %s + +// Test merge-compute-block pass: merges adjacent VECTOR blocks between CUBE blocks, +// cloning CUBE ops that downstream blocks depend on. +// +// Pattern: C1(6,CUBE) → V1(32,VECTOR) → C2(8,CUBE) → V2(33,VECTOR) → C3(10,CUBE) → C4(12,CUBE) +// After pass: +// - V2(33) merged into V1(32): all block_id 33 → 32 +// - C3(10) gets cloned alloc/copy/to_tensor chain from C2(8) + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, ssbuffer.intra_buf_count = 3 : i32, ssbuffer.inter_core_buf_count = 2 : i32} { + func.func @_swa_bwd_dkdv_kernel( + %arg0: memref {tt.tensor_kind = 0 : i32}, + %arg1: memref {tt.tensor_kind = 0 : i32}, + %arg2: i32 {tt.divisibility = 16 : i32} + ) attributes {global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst_bf16 = arith.constant 0.000000e+00 : bf16 + %cst_f32 = arith.constant 0.000000e+00 : f32 + %cst_scale = arith.constant 0.0883883461 : f32 + %c0 = arith.constant 0 : index + %c64 = arith.constant 64 : index + %c64_i32 = arith.constant 64 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0_i32 = arith.constant 0 : i32 + + %empty_2d_bf16 = tensor.empty() : tensor<64x64xbf16> + + // VECTOR block constants + %cst_v = arith.constant {ssbuffer.block_id = 49 : i32, ssbuffer.core_type = "VECTOR"} 0.000000e+00 : f32 + %empty_v = tensor.empty() {ssbuffer.block_id = 49 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + %scale = linalg.fill {ssbuffer.block_id = 49 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_scale : f32) outs(%empty_v : tensor<64x64xf32>) -> tensor<64x64xf32> + + // CUBE block constants + %cst_bf16_cube = arith.constant {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : bf16 + %cst_f32_cube = arith.constant {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f32 + %empty_cube_f32 = tensor.empty() {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "CUBE"} : tensor<64x64xf32> + %cube_init = linalg.fill {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "CUBE"} ins(%cst_f32_cube : f32) outs(%empty_cube_f32 : tensor<64x64xf32>) -> tensor<64x64xf32> + + scf.for %iv = %c0_i32 to %c64_i32 step %c1_i32 : i32 { + // Shared index computations used by C1 and C4 + %iv_idx = arith.index_cast %iv {ssbuffer.block_id = 26 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %offset64 = arith.muli %iv_idx, %c64 {ssbuffer.block_id = 26 : i32, ssbuffer.core_type = "CUBE"} : index + %stride_idx = arith.index_cast %arg2 {ssbuffer.block_id = 26 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %cond_c2 = arith.cmpi slt, %iv_idx, %c64 {ssbuffer.block_id = 26 : i32, ssbuffer.core_type = "CUBE"} : index + + // ========================================== + // C1: Block 6 (CUBE) — produces %tensor_c1 used by V1(32) and C4(12) + // ========================================== + // CHECK: memref.alloc() {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} + %alloc_c1 = memref.alloc() {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16> + %cond_c1 = arith.cmpi slt, %iv_idx, %c64 {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : index + scf.if %cond_c1 { + linalg.fill {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} ins(%cst_bf16 : bf16) outs(%alloc_c1 : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 6 : i32} + %reint_c1 = memref.reinterpret_cast %arg0 to offset: [%offset64], sizes: [64, 64], strides: [%stride_idx, 1] {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<64x64xbf16, strided<[?, 1], offset: ?>> + %subv_c1_src = memref.subview %reint_c1[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16, strided<[?, 1], offset: ?>> to memref<64x64xbf16, strided<[?, 1], offset: ?>> + %subv_c1_dst = memref.subview %alloc_c1[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16> to memref<64x64xbf16, strided<[64, 1]>> + memref.copy %subv_c1_src, %subv_c1_dst {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16, strided<[?, 1], offset: ?>> to memref<64x64xbf16, strided<[64, 1]>> + %tensor_c1 = bufferization.to_tensor %alloc_c1 restrict writable {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16> to tensor<64x64xbf16> + %matmul_c1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%tensor_c1, %tensor_c1 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%cube_init : tensor<64x64xf32>) -> tensor<64x64xf32> + + // ========================================== + // V1: Block 32 (VECTOR) — CUBE pred=6, CUBE succ=8, edge 32→33 via %v1_exp + // ========================================== + // CHECK: arith.mulf {{%.*}}, {{%.*}} {ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.subBlock = 32 : i32} + %v1_mul = arith.mulf %matmul_c1, %scale {ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + // CHECK: math.exp {{%.*}} {ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.subBlock = 32 : i32} + %v1_exp = math.exp %v1_mul {ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + // CHECK: arith.truncf {{%.*}} {ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.subBlock = 32 : i32} + %v1_trunc = arith.truncf %v1_exp {ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xbf16> + + // ========================================== + // C2: Block 8 (CUBE) — has copy chain with dedicated arith ops that C3(10) depends on + // ========================================== + // CHECK: memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} + %alloc_c2 = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16> + scf.if %cond_c2 { + linalg.fill {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} ins(%cst_bf16 : bf16) outs(%alloc_c2 : memref<64x64xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + // Dedicated arith ops in C2 for the reinterpret_cast offset computation + %c2_stride = arith.index_cast %arg2 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %c2_mul = arith.muli %iv_idx, %c2_stride {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : index + %c2_offset = arith.addi %offset64, %c2_mul {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : index + %reint_c2 = memref.reinterpret_cast %arg1 to offset: [%c2_offset], sizes: [64, 64], strides: [%c2_stride, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<64x64xbf16, strided<[?, 1], offset: ?>> + %subv_c2_src = memref.subview %reint_c2[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16, strided<[?, 1], offset: ?>> to memref<64x64xbf16, strided<[?, 1], offset: ?>> + %subv_c2_dst = memref.subview %alloc_c2[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16> to memref<64x64xbf16, strided<[64, 1]>> + // CHECK: memref.copy {{%.*}}, {{%.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} + memref.copy %subv_c2_src, %subv_c2_dst {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16, strided<[?, 1], offset: ?>> to memref<64x64xbf16, strided<[64, 1]>> + // CHECK: bufferization.to_tensor {{%.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} + %tensor_c2 = bufferization.to_tensor %alloc_c2 restrict writable {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xbf16> to tensor<64x64xbf16> + %transposed_v1 = linalg.transpose ins(%v1_trunc : tensor<64x64xbf16>) outs(%empty_2d_bf16 : tensor<64x64xbf16>) permutation = [1, 0] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} + %matmul_c2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%transposed_v1, %tensor_c2 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%cube_init : tensor<64x64xf32>) -> tensor<64x64xf32> + + // ========================================== + // C3: Block 10 (CUBE) — uses %tensor_c2 from C2, triggers clone from C2 + // After pass: cloned alloc/index_cast/muli/addi/reinterpret_cast/subview×2/copy/to_tensor from C2 + // ========================================== + // CHECK: memref.alloc() {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: scf.if {{%.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: } {hivm.unlikely_condition, ssbuffer.block_id = 10 : i32} + // CHECK: arith.index_cast {{%.*}} {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: arith.muli {{%.*}}, {{%.*}} {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: arith.addi {{%.*}}, {{%.*}} {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: memref.reinterpret_cast {{%.*}} to offset: [{{%.*}}], sizes: [64, 64], strides: [{{%.*}}, 1] {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: memref.subview {{%.*}}[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: memref.subview {{%.*}}[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: memref.copy {{%.*}}, {{%.*}} {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: bufferization.to_tensor {{%.*}} {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE" + %matmul_c3 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%tensor_c2, %tensor_c2 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%cube_init : tensor<64x64xf32>) -> tensor<64x64xf32> + + // ========================================== + // V2: Block 33 (VECTOR) — uses %v1_exp (edge 32→33), %matmul_c3 (edge 10→33) + // After pass: merged into V1(32), all block_id 33 → 32 + // ========================================== + // CHECK: arith.subf {{%.*}}, {{%.*}} {{{.*}}ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.subBlock = 33 : i32} + %v2_sub = arith.subf %matmul_c3, %v1_exp {ssbuffer.block_id = 33 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + // CHECK: arith.mulf {{%.*}}, {{%.*}} {{{.*}}ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.subBlock = 33 : i32} + %v2_mul = arith.mulf %v1_exp, %v2_sub {ssbuffer.block_id = 33 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + // CHECK: arith.mulf {{%.*}}, {{%.*}} {{{.*}}ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.subBlock = 33 : i32} + %v2_mul2 = arith.mulf %v2_mul, %scale {ssbuffer.block_id = 33 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + // CHECK: arith.truncf {{%.*}} {{{.*}}ssbuffer.block_id = 32 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.subBlock = 33 : i32} + %v2_trunc = arith.truncf %v2_mul2 {ssbuffer.block_id = 33 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xbf16> + + // ========================================== + // C4: Block 12 (CUBE) — uses %v2_trunc (edge 33→12) and %tensor_c1 (edge 6→12) + // ========================================== + %transposed_v2 = linalg.transpose ins(%v2_trunc : tensor<64x64xbf16>) outs(%empty_2d_bf16 : tensor<64x64xbf16>) permutation = [1, 0] {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "CUBE"} + // CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "CUBE" + %matmul_c4 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%transposed_v2, %tensor_c1 : tensor<64x64xbf16>, tensor<64x64xbf16>) outs(%cube_init : tensor<64x64xf32>) -> tensor<64x64xf32> + + // Store output to prevent DCE + %reint_out = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [64, 64], strides: [%stride_idx, 1] {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<64x64xbf16, strided<[?, 1], offset: ?>> + %trunc_c4 = arith.truncf %matmul_c4 {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "CUBE"} : tensor<64x64xf32> to tensor<64x64xbf16> + bufferization.materialize_in_destination %trunc_c4 in writable %reint_out {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "CUBE"} : (tensor<64x64xbf16>, memref<64x64xbf16, strided<[?, 1], offset: ?>>) -> () + } + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_cube_block.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_cube_block.mlir new file mode 100644 index 0000000000..c5a70a507a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_cube_block.mlir @@ -0,0 +1,245 @@ +// RUN: triton-opt --merge-cube-block %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // ============================================ + // Test Case 1: @test_merge_cube_blocks_with_vector + // ============================================ + // Scenario: Two cube blocks with same vector predecessors and successors + // - Vector block 1 (block_id=1) produces vec_tensor1 + // - Cube block 1 (block_id=2) uses vec_tensor1, produces cube_tensor1 + // - Cube block 2 (block_id=3) uses vec_tensor1, produces cube_tensor2 + // - Vector block 2 (block_id=4) uses cube_tensor1 and cube_tensor2 + // Expected: The two cube blocks should be merged (block_id=3 -> block_id=2) + // Wrapped in two-layer for loop (only innermost should be processed) + // ============================================ + // CHECK-LABEL: func.func @test_merge_cube_blocks_with_vector + func.func @test_merge_cube_blocks_with_vector(%arg0: memref, %arg1: memref, %arg2: memref) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c128 = arith.constant 128 : index + %cst = arith.constant 0.000000e+00 : bf16 + %cst_f32 = arith.constant 0.000000e+00 : f32 + + scf.for %i = %c0 to %c128 step %c1 { + scf.for %j = %c0 to %c128 step %c1 { + %vec_alloc1 = memref.alloc() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xbf16> + %vec_cond1 = arith.constant 1 : i1 + scf.if %vec_cond1 { + linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : bf16) outs(%vec_alloc1 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 1 : i32} + %vec_tensor1 = bufferization.to_tensor %vec_alloc1 restrict writable {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xbf16> to tensor<128x128xbf16> + + // CHECK: %{{.*}} = memref.alloc() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> + %cube_alloc1 = memref.alloc() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> + %cube_cond1 = arith.constant 1 : i1 + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + scf.if %cube_cond1 { + linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : bf16) outs(%cube_alloc1 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 2 : i32} + %cube_tensor1 = bufferization.to_tensor %cube_alloc1 restrict writable {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> to tensor<128x128xbf16> + %cube_out1 = tensor.empty() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + // CHECK: linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + %cube_matmul1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%vec_tensor1, %cube_tensor1 : tensor<128x128xbf16>, tensor<128x128xbf16>) outs(%cube_out1 : tensor<128x128xf32>) -> tensor<128x128xf32> + + // CHECK: %{{.*}} = memref.alloc() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> + %cube_alloc2 = memref.alloc() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> + %cube_cond2 = arith.constant 1 : i1 + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + scf.if %cube_cond2 { + linalg.fill {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : bf16) outs(%cube_alloc2 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 3 : i32} + %cube_tensor2 = bufferization.to_tensor %cube_alloc2 restrict writable {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> to tensor<128x128xbf16> + %cube_out2 = tensor.empty() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + // CHECK: linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + %cube_matmul2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} ins(%vec_tensor1, %cube_tensor2 : tensor<128x128xbf16>, tensor<128x128xbf16>) outs(%cube_out2 : tensor<128x128xf32>) -> tensor<128x128xf32> + + %vec_alloc2 = memref.alloc() {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xf32> + %vec_cond2 = arith.constant 1 : i1 + scf.if %vec_cond2 { + linalg.fill {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_f32 : f32) outs(%vec_alloc2 : memref<128x128xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 4 : i32} + // Uses both cube_matmul1 and cube_matmul2 + %vec_add = arith.addf %cube_matmul1, %cube_matmul2 {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + %vec_tensor2 = bufferization.to_tensor %vec_alloc2 restrict writable {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xf32> to tensor<128x128xf32> + + scf.yield + } + } + + return + } + + // ============================================ + // Test Case 2: @test_no_merge_with_extra_vector_dependency + // ============================================ + // Scenario: Based on scenario 1, add an extra vector node pointing to one cube block + // - Vector block 5 (block_id=5) produces vec_tensor1 + // - Vector block 9 (block_id=9) produces vec_tensor3 + // - Cube block 6 (block_id=6) uses vec_tensor1, produces cube_matmul1 + // - Cube block 7 (block_id=7) uses vec_tensor3, produces cube_matmul2 + // - Vector block 8 (block_id=8) uses cube_matmul1 and cube_matmul2 + // Expected: The two cube blocks should NOT be merged (different source nodes) + // Wrapped in two-layer for loop + // ============================================ + // CHECK-LABEL: func.func @test_no_merge_with_extra_vector_dependency + func.func @test_no_merge_with_extra_vector_dependency(%arg0: memref, %arg1: memref, %arg2: memref) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c128 = arith.constant 128 : index + %cst = arith.constant 0.000000e+00 : bf16 + %cst_f32 = arith.constant 0.000000e+00 : f32 + + // Outer for loop + scf.for %i = %c0 to %c128 step %c1 { + // Inner for loop (innermost, should be processed) + scf.for %j = %c0 to %c128 step %c1 { + // Vector block 1 (predecessor for cube block 1) + %vec_alloc1 = memref.alloc() {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xbf16> + %vec_cond1 = arith.constant 1 : i1 + scf.if %vec_cond1 { + linalg.fill {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : bf16) outs(%vec_alloc1 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 5 : i32} + %vec_tensor1 = bufferization.to_tensor %vec_alloc1 restrict writable {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xbf16> to tensor<128x128xbf16> + + // Cube block 1 (block_id=2) - uses vec_tensor1 + // CHECK: memref.alloc() {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} + %cube_alloc1 = memref.alloc() {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> + %cube_cond1 = arith.constant 1 : i1 + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} + scf.if %cube_cond1 { + linalg.fill {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : bf16) outs(%cube_alloc1 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 6 : i32} + %cube_tensor1 = bufferization.to_tensor %cube_alloc1 restrict writable {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> to tensor<128x128xbf16> + %cube_out1 = tensor.empty() {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + // CHECK: linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} + %cube_matmul1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} ins(%vec_tensor1, %cube_tensor1 : tensor<128x128xbf16>, tensor<128x128xbf16>) outs(%cube_out1 : tensor<128x128xf32>) -> tensor<128x128xf32> + + // Extra vector block (block_id=5) - only used by cube block 2 + %vec_alloc3 = memref.alloc() {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xbf16> + %vec_cond3 = arith.constant 1 : i1 + scf.if %vec_cond3 { + linalg.fill {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : bf16) outs(%vec_alloc3 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 9 : i32} + %vec_tensor3 = bufferization.to_tensor %vec_alloc3 restrict writable {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xbf16> to tensor<128x128xbf16> + + // Cube block 2 (block_id=3) - should NOT be merged (different source nodes) + // Uses vec_tensor3 + // CHECK: memref.alloc() {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} + %cube_alloc2 = memref.alloc() {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> + %cube_cond2 = arith.constant 1 : i1 + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} + scf.if %cube_cond2 { + linalg.fill {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : bf16) outs(%cube_alloc2 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 7 : i32} + %cube_tensor2 = bufferization.to_tensor %cube_alloc2 restrict writable {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> to tensor<128x128xbf16> + %cube_out2 = tensor.empty() {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + // CHECK: linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} + // Uses vec_tensor3 as one of the inputs + %cube_matmul2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} ins(%vec_tensor3, %cube_tensor2 : tensor<128x128xbf16>, tensor<128x128xbf16>) outs(%cube_out2 : tensor<128x128xf32>) -> tensor<128x128xf32> + + // Vector block 2 (successor for both cube blocks) + // Uses both cube_matmul1 and cube_matmul2 + %vec_alloc2 = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xf32> + %vec_cond2 = arith.constant 1 : i1 + scf.if %vec_cond2 { + linalg.fill {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_f32 : f32) outs(%vec_alloc2 : memref<128x128xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + %vec_add = arith.addf %cube_matmul1, %cube_matmul2 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + %vec_tensor2 = bufferization.to_tensor %vec_alloc2 restrict writable {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xf32> to tensor<128x128xf32> + + scf.yield + } + } + + return + } + + // ============================================ + // Test Case 3: @test_no_merge_single_layer_for + // ============================================ + // Scenario: Same as scenario 1, but only one layer of for loop + // - Vector block 1 (block_id=1) produces vec_tensor1 + // - Cube block 1 (block_id=2) uses vec_tensor1, produces cube_matmul1 + // - Cube block 2 (block_id=3) uses vec_tensor1, produces cube_matmul2 + // - Vector block 2 (block_id=4) uses cube_matmul1 and cube_matmul2 + // Expected: The two cube blocks should NOT be merged (not innermost loop) + // Only one layer of for loop + // ============================================ + // CHECK-LABEL: func.func @test_no_merge_single_layer_for + func.func @test_no_merge_single_layer_for(%arg0: memref, %arg1: memref, %arg2: memref) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c128 = arith.constant 128 : index + %cst = arith.constant 0.000000e+00 : bf16 + %cst_f32 = arith.constant 0.000000e+00 : f32 + + // Single layer for loop (not innermost, should NOT be processed) + scf.for %i = %c0 to %c128 step %c1 { + // Vector block 1 (predecessor for both cube blocks) + %vec_alloc1 = memref.alloc() {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xbf16> + %vec_cond1 = arith.constant 1 : i1 + scf.if %vec_cond1 { + linalg.fill {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : bf16) outs(%vec_alloc1 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 15 : i32} + %vec_tensor1 = bufferization.to_tensor %vec_alloc1 restrict writable {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xbf16> to tensor<128x128xbf16> + + // Cube block 1 (block_id=2) - should remain unchanged + // CHECK: memref.alloc() {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} + %cube_alloc1 = memref.alloc() {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> + %cube_cond1 = arith.constant 1 : i1 + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} + scf.if %cube_cond1 { + linalg.fill {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : bf16) outs(%cube_alloc1 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 16 : i32} + %cube_tensor1 = bufferization.to_tensor %cube_alloc1 restrict writable {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> to tensor<128x128xbf16> + %cube_out1 = tensor.empty() {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + // CHECK: linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} + %cube_matmul1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} ins(%vec_tensor1, %cube_tensor1 : tensor<128x128xbf16>, tensor<128x128xbf16>) outs(%cube_out1 : tensor<128x128xf32>) -> tensor<128x128xf32> + + // Cube block 2 (block_id=3) - should remain unchanged + // CHECK: memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "CUBE"} + %cube_alloc2 = memref.alloc() {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> + %cube_cond2 = arith.constant 1 : i1 + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "CUBE"} + scf.if %cube_cond2 { + linalg.fill {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : bf16) outs(%cube_alloc2 : memref<128x128xbf16>) + } {hivm.unlikely_condition, ssbuffer.block_id = 17 : i32} + %cube_tensor2 = bufferization.to_tensor %cube_alloc2 restrict writable {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xbf16> to tensor<128x128xbf16> + %cube_out2 = tensor.empty() {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + // CHECK: linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "CUBE"} + %cube_matmul2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "CUBE"} ins(%vec_tensor1, %cube_tensor2 : tensor<128x128xbf16>, tensor<128x128xbf16>) outs(%cube_out2 : tensor<128x128xf32>) -> tensor<128x128xf32> + + // Vector block 2 (successor for both cube blocks) + // Uses both cube_matmul1 and cube_matmul2 + %vec_alloc2 = memref.alloc() {ssbuffer.block_id = 18 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xf32> + %vec_cond2 = arith.constant 1 : i1 + scf.if %vec_cond2 { + linalg.fill {ssbuffer.block_id = 18 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_f32 : f32) outs(%vec_alloc2 : memref<128x128xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 18 : i32} + %vec_add = arith.addf %cube_matmul1, %cube_matmul2 {ssbuffer.block_id = 18 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + %vec_tensor2 = bufferization.to_tensor %vec_alloc2 restrict writable {ssbuffer.block_id = 18 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xf32> to tensor<128x128xf32> + + + %cube_alloc3 = memref.alloc() {ssbuffer.block_id = 19 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xf32> + %cube_cond3 = arith.constant 1 : i1 + scf.if %cube_cond3 { + linalg.fill {ssbuffer.block_id = 19 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : bf16) outs(%cube_alloc3 : memref<128x128xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 19 : i32} + %cube_tensor3 = bufferization.to_tensor %cube_alloc3 restrict writable {ssbuffer.block_id = 19 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xf32> to tensor<128x128xf32> + %cube_out3 = tensor.empty() {ssbuffer.block_id = 19 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + %cube_matmul3 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 19 : i32, ssbuffer.core_type = "CUBE"} ins(%vec_tensor2, %cube_tensor3 : tensor<128x128xf32>, tensor<128x128xf32>) outs(%cube_out3 : tensor<128x128xf32>) -> tensor<128x128xf32> + + + %vec_add3 = arith.addf %cube_matmul2, %cube_matmul3 {ssbuffer.block_id = 20 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + scf.yield + } + + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_input_init_shared_cube_block.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_input_init_shared_cube_block.mlir new file mode 100644 index 0000000000..3426e5fcaa --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_input_init_shared_cube_block.mlir @@ -0,0 +1,22 @@ +// RUN: triton-opt --merge-input-init-shared-cube-block %s | FileCheck %s + +module { + // ============================================ + // Test Case: @test_merge_shared_input_init + // ============================================ + // Scenario: matmulA -> matmulB(input, init) with no extra dependency path. + // Merging matmulB's block into matmulA's block does not create a cycle, so the + // two matmuls end up in the same block. + // ============================================ + // CHECK-LABEL: func.func @test_merge_shared_input_init + func.func @test_merge_shared_input_init(%a: tensor<128x128xf32>, %b: tensor<128x128xf32>) -> tensor<128x128xf32> { + %init_a = tensor.empty() : tensor<128x128xf32> + // CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "CUBE" + %mat0 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "CUBE"} ins(%a, %b : tensor<128x128xf32>, tensor<128x128xf32>) outs(%init_a : tensor<128x128xf32>) -> tensor<128x128xf32> + + // CHECK: linalg.matmul {{.*}}ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "CUBE" + // CHECK-NOT: ssbuffer.add_from_matmul + %mat1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "CUBE"} ins(%mat0, %b : tensor<128x128xf32>, tensor<128x128xf32>) outs(%mat0 : tensor<128x128xf32>) -> tensor<128x128xf32> + return %mat1 : tensor<128x128xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis.mlir new file mode 100644 index 0000000000..4516273050 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis.mlir @@ -0,0 +1,54 @@ +// RUN: triton-opt --merge-same-source-axis %s | FileCheck %s + +// Source %src is in block_id 28. Two consumers in block_id 29 do different +// "axis" processing of %src (here: mulf by different scalars) and converge +// at %sub = arith.subf %b1, %b2 (also block_id 29). All three should be +// moved to block_id 28. +// +// %src also has unrelated consumers in block 30 and block 31 that must NOT +// be touched. +module { + // CHECK-LABEL: func.func @same_source_diff_axis + func.func @same_source_diff_axis(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + // Source in block 28. + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Convergent chain in block 29 — all three should move to block 28. + // CHECK: arith.mulf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 28 + %b1 = arith.mulf %src, %arg0 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.mulf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 28 + %b2 = arith.mulf %src, %arg1 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.subf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 28 + %sub = arith.subf %b1, %b2 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Unrelated single consumer in block 30 — must stay. + // CHECK: arith.addf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 30 + %u1 = arith.addf %src, %arg0 {ssbuffer.block_id = 30 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Unrelated single consumer in block 31 — must stay. + // CHECK: arith.mulf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 31 + %u2 = arith.mulf %src, %arg1 {ssbuffer.block_id = 31 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return + } +} + +// CHECK-LABEL: func.func @same_source_diff_axis_cross_block +// Consumers u1 (block 29) and u2 (block 30) live in DIFFERENT blocks, but +// converge at %sub (block 30). All three should still move to source's +// block_id (50). +func.func @same_source_diff_axis_cross_block(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + // Source in block 50. + // CHECK: arith.addf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 50 + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 50 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // u1 in block 29, u2 in block 30 — different blocks, but the chain still + // converges at %sub. + // CHECK: arith.mulf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 50 + %u1 = arith.mulf %src, %arg0 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.mulf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 50 + %u2 = arith.mulf %src, %arg1 {ssbuffer.block_id = 30 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.subf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 50 + %sub = arith.subf %u1, %u2 {ssbuffer.block_id = 30 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_cross_block_dep.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_cross_block_dep.mlir new file mode 100644 index 0000000000..2d34357cb6 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_cross_block_dep.mlir @@ -0,0 +1,77 @@ +// RUN: triton-opt --merge-same-source-axis %s | FileCheck %s + +// Guards: if the convergence op has an upstream operand whose defOp lives +// in a block that this merge will NOT cover (neither source's block, nor +// any block of an op in chainOps), the merge must be skipped. Otherwise +// moving K to srcBlockId would leave a cross-block dependency between the +// moved K and the unaligned defOp. + +// CHECK-LABEL: func.func @convergence_with_external_dep +func.func @convergence_with_external_dep(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + // External tensor at block 50 — will NOT be moved by this merge. + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 50 + %ext = arith.addf %arg0, %arg1 {ssbuffer.block_id = 50 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Source at block 28. + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 28 + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Two consumers at block 29 — convergent chain. + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 29 + %a = arith.mulf %src, %arg0 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 29 + %b = arith.addf %src, %arg1 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Convergence uses both consumers AND %ext. The guard must skip the + // merge — moving %k to block 28 while %ext stays at block 50 would + // create a cross-block dependency that ReorderOpsByBlockId cannot + // safely resolve. + // CHECK: arith.subf {{.*}}ssbuffer.block_id = 29 + %k = arith.subf %a, %ext {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return +} + +// Even when the upstream defOp IS already in srcBlockId (28), the merge +// is still rejected. reason: willCreateCycle's DFS treats `%ext(28) → +// %k(29→28)` as an internal cycle within the target block, so the merge +// is skipped. This is the correct conservative behavior — making the +// chain self-referential at the target block (an upstream op at the +// target block feeding a chain op that just arrived at the target block) +// is exactly what the cycle detector is meant to prevent. +func.func @external_dep_in_src_block_ok(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + // Upstream tensor already at srcBlockId. + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 28 + %ext = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 28 + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Chain stays at block 29 — willCreateCycle rejects the merge. + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 29 + %a = arith.mulf %src, %arg0 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 29 + %b = arith.addf %src, %arg1 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.subf {{.*}}ssbuffer.block_id = 29 + %k = arith.subf %a, %ext {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return +} + +// Upstream defOp that is itself part of chainOps is fine too: it will be +// moved along with K, so no cross-block dep survives the merge. +func.func @upstream_defop_already_in_chain(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 28 + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // %a and %b both consume %src. Their convergent chain ends at %k, + // but %b is also an operand of %k — i.e. %b's defOp is in chainOps. + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 28 + %a = arith.mulf %src, %arg0 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 28 + %b = arith.mulf %src, %arg1 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.subf {{.*}}ssbuffer.block_id = 28 + %k = arith.subf %a, %b {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_downstream.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_downstream.mlir new file mode 100644 index 0000000000..bf2f84ab94 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_downstream.mlir @@ -0,0 +1,48 @@ +// RUN: triton-opt --merge-same-source-axis %s | FileCheck %s + +// Guards: convergence op's same-block VECTOR_ONLY direct downstream must +// be pulled along with the merge chain, otherwise the moved convergence +// op would leave its tail in the old block and create a cross-block +// dependency inconsistency. + +// CHECK-LABEL: func.func @downstream_follows_convergence +func.func @downstream_follows_convergence(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + // Source at block 28. + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 28 + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Two consumers in block 29 doing different "axis" processing. + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 28 + %a = arith.mulf %src, %arg1 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 28 + %b = arith.addf %src, %arg1 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Convergence op at block 29 — moves to 28. + // CHECK: arith.subf {{.*}}ssbuffer.block_id = 28 + %k = arith.subf %a, %b {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Direct downstream VECTOR_ONLY user at the same block_id (29) as %k. + // It must follow %k to block 28 — otherwise %k ends up in 28 while + // %down stays in 29, splitting the chain. + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 28 + %down = arith.mulf %k, %arg0 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return +} + +// Downstream that is at a DIFFERENT block_id from the convergence op is +// not touched — its placement is independent and may have been decided by +// other passes. +func.func @downstream_at_different_block_left_alone(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + %a = arith.mulf %src, %arg1 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + %b = arith.addf %src, %arg1 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.subf {{.*}}ssbuffer.block_id = 28 + %k = arith.subf %a, %b {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Downstream at a different block (50) — must stay put. + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 50 + %down = arith.mulf %k, %arg0 {ssbuffer.block_id = 50 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_no_trivial_convergence.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_no_trivial_convergence.mlir new file mode 100644 index 0000000000..556ac9c6ec --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_no_trivial_convergence.mlir @@ -0,0 +1,48 @@ +// RUN: triton-opt --merge-same-source-axis %s | FileCheck %s + +// Guards: 1-step "false convergence" must be rejected. +// +// Two VECTOR_ONLY consumers of a tensor source are in a direct +// producer-consumer relationship (%A -> %B). Without the trivial- +// convergence guard, the BFS would step once from %A and encounter %B, +// declare %B the convergence op, and reblock the chain — even though +// there is no real two-branch merge. +// +// With the guard, hitting another starting consumer is treated as a +// non-event and the BFS keeps exploring. No downstream op is reached by +// two independent paths, so findNearestConvergence returns false. + +// CHECK-LABEL: func.func @no_false_convergence_via_producer_consumer +func.func @no_false_convergence_via_producer_consumer(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + // Tensor source at block 28 (will be tried as a merge root). + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // %a is a direct consumer of %src (block 27). It has only one + // downstream user %b, which is also a consumer of %src (block 27). + // The BFS would naively see %a -> %b as a "convergence at %b" but %b + // is itself a starting consumer — must be rejected. + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 27 + %a = arith.addf %src, %arg1 {ssbuffer.block_id = 27 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 27 + %b = arith.mulf %a, %arg1 {ssbuffer.block_id = 27 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return +} + +// Same pattern but the "trivial convergence" candidate is a deeper +// consumer that the BFS would also reach in one step. Still rejected. +func.func @no_false_convergence_via_chained_consumer(%arg0: tensor<8xf32>, %arg1: tensor<8xf32>) { + %src = arith.addf %arg0, %arg1 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 30 + %x = arith.addf %src, %arg1 {ssbuffer.block_id = 30 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 30 + %y = arith.mulf %x, %arg1 {ssbuffer.block_id = 30 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // %z is downstream of both %x and %y paths only via %y; not a real + // two-branch merge. BFS from %x reaches %y in one step, but %y is a + // starting consumer — must be rejected. + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 31 + %z = arith.addf %y, %arg0 {ssbuffer.block_id = 31 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_skip_constant.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_skip_constant.mlir new file mode 100644 index 0000000000..fe50a33e78 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_same_source_axis_skip_constant.mlir @@ -0,0 +1,48 @@ +// RUN: triton-opt --merge-same-source-axis %s | FileCheck %s + +// Guards: arith.constant must NOT act as a merge source. +// +// %c0_i32 (block 31) is consumed by %79 and %80 (both block 27). %80 is +// also a direct user of %79. Without the constant-source guard the BFS +// would trigger a trivial 1-step convergence at %80 and reblock both ops +// to block 31 — which is exactly the leak pattern that motivated this +// guard. With the guard, the constant is skipped and nothing moves. + +// CHECK-LABEL: func.func @constant_source_skipped +func.func @constant_source_skipped(%arg0: i32, %arg1: i32) { + // CHECK: arith.constant {{.*}}ssbuffer.block_id = 31 + %c0_i32 = arith.constant {MixUse, ssbuffer.block_id = 31 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + // CHECK: arith.constant {{.*}}ssbuffer.block_id = 31 + %c1_i32 = arith.constant {MixUse, ssbuffer.block_id = 31 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + + // These two must stay at block 27 — constant is not a real axis source. + // CHECK: arith.subi {{.*}}ssbuffer.block_id = 27 + %79 = arith.subi %c0_i32, %arg0 {ssbuffer.block_id = 27 : i32, ssbuffer.core_type = "VECTOR"} : i32 + // CHECK: arith.maxsi {{.*}}ssbuffer.block_id = 27 + %80 = arith.maxsi %79, %c0_i32 {ssbuffer.block_id = 27 : i32, ssbuffer.core_type = "VECTOR"} : i32 + // CHECK: arith.index_cast {{.*}}ssbuffer.block_id = 27 + %81 = arith.index_cast %80 {ssbuffer.block_id = 27 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + + return +} + +// Constants used purely as scalar operands to a tensor-typed merge chain +// must also not perturb the chain. Here %src is a real tensor source at +// block 28; the chain below is a legitimate same-source-different-axes +// pattern and must still merge. +func.func @constant_consumer_does_not_break_legit_chain(%arg0: tensor<8xf32>, %arg1: f32) { + %c0_i32 = arith.constant {ssbuffer.block_id = 31 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + + // Source %src at block 28 — tensor so the pass treats it as a candidate. + %src = arith.addf %arg0, %arg0 {ssbuffer.block_id = 28 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + // Two consumers with a shared constant operand — they must still merge. + // CHECK: arith.mulf {{.*}}ssbuffer.block_id = 28 + %a = arith.mulf %src, %arg0 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.addf {{.*}}ssbuffer.block_id = 28 + %b = arith.addf %src, %arg0 {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + // CHECK: arith.subf {{.*}}ssbuffer.block_id = 28 + %k = arith.subf %a, %b {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} : tensor<8xf32> + + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_vector_if_block.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_vector_if_block.mlir new file mode 100644 index 0000000000..55ef441e51 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_vector_if_block.mlir @@ -0,0 +1,38 @@ +// RUN: triton-opt --merge-vector-if-block %s | FileCheck %s + +// A pure-Vector scf.if whose only data source (condition + values read inside) +// is block_id 10. The if (block_id 16) and its inner op (block_id 8) must be +// merged into the upstream block_id 10, and the first downstream consumer block +// (block_id 11) must be merged into 10 as well, while the second downstream +// block (block_id 12) is left untouched. +module { + // CHECK-LABEL: func.func @merge_pure_vector_if + func.func @merge_pure_vector_if(%arg0: f32, %arg1: f32) { + %one = arith.constant {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} 1.000000e+00 : f32 + %eps = arith.constant {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} 9.99999997E-7 : f32 + + // upstream data source: block_id 10 + %ext = arith.addf %arg0, %arg1 {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %cond = arith.cmpf ogt, %ext, %eps {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : f32 + + // pure-Vector if: outer block_id 16, inner block_id 8 + // CHECK: scf.if + // CHECK: arith.divf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 10 + // CHECK: } {ssbuffer.block_id = 10 + %r = scf.if %cond -> (f32) { + %d = arith.divf %one, %ext {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : f32 + scf.yield {ssbuffer.core_type = "VECTOR"} %d : f32 + } else { + scf.yield {ssbuffer.core_type = "VECTOR"} %one : f32 + } {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "VECTOR"} + + // first downstream consumer: block_id 11 -> merged into 10 + // CHECK: arith.mulf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 10 + %u1 = arith.mulf %r, %ext {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "VECTOR"} : f32 + + // second downstream consumer: block_id 12 -> unchanged + // CHECK: arith.addf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 12 + %u2 = arith.addf %r, %arg0 {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_vector_if_block_cycle.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_vector_if_block_cycle.mlir new file mode 100644 index 0000000000..b55f9aef69 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/merge_vector_if_block_cycle.mlir @@ -0,0 +1,48 @@ +// RUN: triton-opt --merge-vector-if-block %s | FileCheck %s + +// Cycle-detection guard for the shared willCreateCycle helper used by +// merge-vector-if-block. +// +// Merging the pure-Vector scf.if together with its downstream block_id 11 would +// create a block-level cycle: block_id 13 reads the upstream (block_id 10) and +// also feeds block_id 11, so unifying the if + block_id 11 into block_id 10 +// closes the loop 10 -> 13 -> 11(=10). The pass must therefore SKIP the +// downstream merge and fall back to folding the if into the upstream block_id 10 +// only, leaving block_id 13 and block_id 11 untouched. +// +// NOTE: block_id is a module-global namespace in ComputeBlockIdManager, so this +// case lives in its own file to avoid cross-function block_id interference. +module { + // CHECK-LABEL: func.func @merge_if_downstream_cycle + func.func @merge_if_downstream_cycle(%arg0: f32, %arg1: f32) { + %eps = arith.constant {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} 9.99999997E-7 : f32 + + // upstream data source: block_id 10 + %ext = arith.addf %arg0, %arg1 {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %cond = arith.cmpf ogt, %ext, %eps {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : f32 + + // block_id 13 consumes upstream (%ext) and feeds the downstream block_id 11. + // It must stay at block_id 13. + // CHECK: %[[T1:.*]] = arith.mulf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 13 + %t1 = arith.mulf %ext, %arg0 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : f32 + // CHECK: %[[T2:.*]] = arith.addf %[[T1]], %{{.*}} {{{.*}}ssbuffer.block_id = 13 + %t2 = arith.addf %t1, %arg1 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : f32 + + // pure-Vector if (outer block_id 16, inner block_id 8); its only data source + // is block_id 10, so the fallback folds it (and its inner op) into block_id 10. + // CHECK: scf.if + // CHECK: arith.divf %{{.*}}, %{{.*}} {{{.*}}ssbuffer.block_id = 10 + // CHECK: } {ssbuffer.block_id = 10 + %r = scf.if %cond -> (f32) { + %d = arith.divf %ext, %arg0 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : f32 + scf.yield {ssbuffer.core_type = "VECTOR"} %d : f32 + } else { + scf.yield {ssbuffer.core_type = "VECTOR"} %ext : f32 + } {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "VECTOR"} + + // downstream block_id 11 must stay untouched (merging it would create a cycle). + // CHECK: arith.mulf %{{.*}}, %[[T2]] {{{.*}}ssbuffer.block_id = 11 + %u1 = arith.mulf %r, %t2 {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "VECTOR"} : f32 + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/relocate_memref_decl.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/relocate_memref_decl.mlir new file mode 100644 index 0000000000..e26ee9d67a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/relocate_memref_decl.mlir @@ -0,0 +1,52 @@ +// RUN: triton-opt --relocate-memref-decl %s | FileCheck %s + +// ============================================================================ +// RelocateMemrefDeclPass: scan every op for memref-typed operands; when the +// operand's producer crosses a sync op relative to the consumer, sink the +// producer (and its forward/backward closure) into the consumer's block and +// re-partition it to the consumer's block_id group. +// +// Covered scenarios: +// 1. @relocate_across_sync - a memref.alloc declared before a gpu.barrier +// whose only consumer (memref.load) is after +// the barrier: the alloc is moved to just +// before the load and re-partitioned to the +// consumer's block (1 -> 2). +// 2. @same_side_no_relocation - producer and consumer both on the same side +// of the barrier (no sync in between): nothing +// is moved, block ids unchanged. +// ============================================================================ + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // Producer (block 1) before the barrier, consumer (block 2) after it. The + // alloc must be pulled across the barrier to just before the load and given + // the consumer's block id. + // CHECK-LABEL: func.func @relocate_across_sync + func.func @relocate_across_sync() { + %c0 = arith.constant 0 : index + // CHECK: arith.constant 0 : index + // CHECK-NOT: memref.alloc + %alloc = memref.alloc() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : memref<32x32xf32> + // CHECK: gpu.barrier {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.external_sync = 1 : i32} + gpu.barrier {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.external_sync = 1 : i32} + // CHECK: memref.alloc() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : memref<32x32xf32> + // CHECK: memref.load %{{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : memref<32x32xf32> + %ld = memref.load %alloc[%c0, %c0] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : memref<32x32xf32> + return + } + + // Producer and consumer share the same side of the barrier, so there is no + // sync between them; the pass must leave everything untouched. + // CHECK-LABEL: func.func @same_side_no_relocation + func.func @same_side_no_relocation() { + %c0 = arith.constant 0 : index + // CHECK: arith.constant 0 : index + // CHECK: memref.alloc() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : memref<16x16xf32> + %alloc = memref.alloc() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : memref<16x16xf32> + // CHECK: memref.load %{{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : memref<16x16xf32> + %ld = memref.load %alloc[%c0, %c0] {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : memref<16x16xf32> + // CHECK: gpu.barrier {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.external_sync = 1 : i32} + gpu.barrier {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.external_sync = 1 : i32} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/ub_usage_opt.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/ub_usage_opt.mlir new file mode 100644 index 0000000000..8dcc7a0d48 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/ub_usage_opt.mlir @@ -0,0 +1,95 @@ +// RUN: triton-opt --ub-usage-opt %s | FileCheck %s + +// Test UB usage optimization: dividing of V1 and V2 should change to one small ub dependency. +// math.exp<64x64> -> || ->linalg.fill<64> -> linalg.reduce<64> -> arith.subf<64> -> xxx +// math.exp<64x64> -> linalg.fill<64> -> linalg.reduce<64> -> arith.subf<64> -> || -> xxx + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // CHECK-LABEL: func.func @_attn_fwd + func.func @_attn_fwd(%arg7: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %out_offset_i64: i64) attributes {global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} 0 : i32 + %c64 = arith.constant {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} 64 : index + %cst = arith.constant {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} 1.000000e+00 : f32 + %cst_f16 = arith.constant {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} 1.000000e+00 : f16 + %c0_i32_4 = arith.constant {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %0 = tensor.empty() {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + %1 = linalg.fill {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f32) outs(%0 : tensor<64x64xf32>) -> tensor<64x64xf32> + %f16_tensor_empty = tensor.empty() {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> + %f16_tensor = linalg.fill {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_f16 : f16) outs(%f16_tensor_empty : tensor<64x64xf16>) -> tensor<64x64xf16> + %4 = tensor.empty() {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %5 = linalg.fill {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f32) outs(%4 : tensor<64xf32>) -> tensor<64xf32> + + %40:3 = scf.for %arg17 = %c0_i32_4 to %c0_i32_4 step %c0_i32_4 iter_args(%arg18 = %5, %arg19 = %1, %arg20 = %5) -> (tensor<64xf32>, tensor<64x64xf32>, tensor<64xf32>) : i32 { + %69 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%f16_tensor, %f16_tensor : tensor<64x64xf16>, tensor<64x64xf16>) outs(%1 : tensor<64x64xf32>) -> tensor<64x64xf32> + + %71 = arith.mulf %69, %1 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + %reduced = linalg.reduce ins(%71 : tensor<64x64xf32>) outs(%5 : tensor<64xf32>) dimensions = [1] {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} + (%in: f32, %init: f32) { + %93 = arith.maximumf %in, %init : f32 + linalg.yield %93 : f32 + } + %72 = arith.maximumf %arg20, %reduced {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %broadcasted_18 = linalg.broadcast ins(%72 : tensor<64xf32>) outs(%0 : tensor<64x64xf32>) dimensions = [1] {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} + %73 = arith.subf %71, %broadcasted_18 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + %74 = math.exp %73 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + %75 = arith.truncf %74 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %83 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%75, %f16_tensor : tensor<64x64xf16>, tensor<64x64xf16>) outs(%1 : tensor<64x64xf32>) -> tensor<64x64xf32> + + // CHECK: %{{.*}} = linalg.fill {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} ins(%{{.*}} : f32) outs(%{{.*}} : tensor<64xf32>) -> tensor<64xf32> + %84 = linalg.fill {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f32) outs(%4 : tensor<64xf32>) -> tensor<64xf32> + // CHECK: %{{.*}} = linalg.reduce ins(%{{.*}} : tensor<64x64xf32>) outs(%{{.*}} : tensor<64xf32>) dimensions = [1] {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} + %reduced_22 = linalg.reduce ins(%74 : tensor<64x64xf32>) outs(%84 : tensor<64xf32>) dimensions = [1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} + (%in: f32, %init: f32) { + %93 = arith.addf %in, %init : f32 + linalg.yield %93 : f32 + } + // CHECK: %{{.*}} = arith.subf %{{.*}}, %{{.*}} {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %85 = arith.subf %arg20, %72 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %86 = math.exp %85 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %87 = arith.mulf %arg18, %86 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %88 = arith.addf %87, %reduced_22 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %broadcasted_23 = linalg.broadcast ins(%86 : tensor<64xf32>) outs(%0 : tensor<64x64xf32>) dimensions = [1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} + %89 = arith.mulf %arg19, %broadcasted_23 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + %90 = arith.addf %83, %89 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + + scf.yield {ssbuffer.core_type = "VECTOR, VECTOR, VECTOR"} %88, %90, %72 : tensor<64xf32>, tensor<64x64xf32>, tensor<64xf32> + } {ssbuffer.core_type = "VECTOR, VECTOR, VECTOR, VECTOR, VECTOR", tt.divisibility_arg1 = dense<64> : tensor<1xi32>} + + %out_offset = arith.index_cast %out_offset_i64 {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : i64 to index + %reinterpret_cast_13 = memref.reinterpret_cast %arg7 to offset: [%out_offset], sizes: [64, 64], strides: [64, 1] {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<64x64xf16, strided<[64, 1], offset: ?>> + %broadcasted = linalg.broadcast ins(%40#0 : tensor<64xf32>) outs(%0 : tensor<64x64xf32>) dimensions = [1] {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} + %55 = arith.divf %40#1, %broadcasted {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + %60 = arith.truncf %55 {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + bufferization.materialize_in_destination %60 in writable %reinterpret_cast_13 {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + + return + } + + // Test 2: Index type dependency - block ID should NOT be changed + // CHECK-LABEL: func.func @test_index_dependency + func.func @test_index_dependency(%arg0: memref {tt.divisibility = 16 : i32}, %offset: i64) { + %cst = arith.constant {ssbuffer.block_id = 20 : i32, ssbuffer.core_type = "VECTOR"} 1.000000e+00 : f32 + %c0 = arith.constant {ssbuffer.block_id = 20 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + + %empty_1d = tensor.empty() {ssbuffer.block_id = 20 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %init_1d = linalg.fill {ssbuffer.block_id = 20 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f32) outs(%empty_1d : tensor<64xf32>) -> tensor<64xf32> + + %result = scf.for %iv = %c0 to %c0 step %c0 iter_args(%arg1 = %init_1d) -> (tensor<64xf32>) : i32 { + %c1 = arith.constant {ssbuffer.block_id = 21 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %c2 = arith.constant {ssbuffer.block_id = 21 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %idx = arith.index_cast %c1 {ssbuffer.block_id = 21 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + + // CHECK: arith.addi {{.*}} {ssbuffer.block_id = 22 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %idx2 = arith.addi %c1, %c2 {ssbuffer.block_id = 22 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %t1 = arith.mulf %arg1, %arg1 {ssbuffer.block_id = 24 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + + scf.yield %t1 : tensor<64xf32> + } {ssbuffer.core_type = "VECTOR, VECTOR"} + + %out_offset = arith.index_cast %offset {ssbuffer.block_id = 25 : i32, ssbuffer.core_type = "VECTOR"} : i64 to index + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%out_offset], sizes: [64], strides: [1] {ssbuffer.block_id = 25 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<64xf32, strided<[1], offset: ?>> + bufferization.materialize_in_destination %result in writable %reinterpret {ssbuffer.block_id = 25 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64xf32>, memref<64xf32, strided<[1], offset: ?>>) -> () + + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/unify_alloc_block.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/unify_alloc_block.mlir new file mode 100644 index 0000000000..edb6517f3a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/unify_alloc_block.mlir @@ -0,0 +1,310 @@ +// RUN: triton-opt --unify-alloc-block %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // ============================================ + // Test Case: @_sgemm_lora_a_kernel + // ============================================ + // This test verifies the core functionality of unify-alloc-block pass: + // - When memref.alloc, linalg.fill (inside scf.if), and memref.copy exist + // with different block_ids, the pass should unify them to the same block_id + // - Target block_id is determined by the memref.copy's block_id + // - Only coreOps (alloc, fill, scf.if, directUsers) are unified; + // condition dependencies are NOT updated + // - Two scenarios are tested: + // 1. VECTOR core: alloc(block_id=7) + fill(block_id=6) + copy(block_id=8) + // → coreOps unified to block_id=8, conditionOps unchanged + // 2. CUBE core: alloc(block_id=3) + fill(block_id=1) + copy(block_id=2) + // → coreOps unified to block_id=2, conditionOps unchanged + // ============================================ + func.func @_sgemm_lora_a_kernel(%arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}, %arg6: i32 {tt.divisibility = 16 : i32}, %arg7: i32) { + %cst = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 0.000000e+00 : f16 + %c256 = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 256 : index + %c16 = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 16 : index + %c1 = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 1 : index + %c0 = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c1_i32 = arith.constant {Undefined, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %c0_i32 = arith.constant {Undefined, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %c16_i32 = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 16 : i32 + %c256_i32 = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 256 : i32 + %14 = arith.minsi %arg5, %arg7 {MixUse, ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %25 = arith.index_cast %arg5 {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + %28 = arith.index_cast %arg6 {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + %41 = arith.index_cast %arg5 {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %24 = arith.divsi %arg6, %c256_i32 {Undefined, ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %cst_0 = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 0.000000e+00 : f32 + %0 = tensor.empty() {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<16x16xf32> + %1 = linalg.fill {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_0 : f32) outs(%0 : tensor<16x16xf32>) -> tensor<16x16xf32> + %44:3 = scf.for %arg22 = %c0_i32 to %24 step %c1_i32 iter_args(%arg23 = %1, %arg24 = %25, %arg25 = %c0) -> (tensor<16x16xf32>, index, index) : i32 { + %64 = arith.muli %arg22, %c256_i32 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %65 = arith.subi %arg6, %64 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %66 = arith.index_cast %65 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + %70 = arith.cmpi slt, %66, %c256 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + // CHECK: %alloc = memref.alloc() {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<256x16xf16> + %alloc = memref.alloc() {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : memref<256x16xf16> + %71 = arith.addi %28, %c16 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + %72 = arith.index_cast %14 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + %76 = arith.minsi %71, %c16 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + %77 = arith.cmpi slt, %76, %c16 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + %78 = arith.ori %70, %77 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : i1 + // CHECK: %{{.*}} = arith.addi %c0, %arg{{.*}} {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + %79 = arith.addi %c0, %arg25 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + // CHECK: %{{.*}} = arith.addi %arg{{.*}}, %{{.*}} {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + %80 = arith.addi %arg25, %41 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + %reinterpret_cast_16 = memref.reinterpret_cast %arg3 to offset: [%80], sizes: [256, 16], strides: [%c1, %41] {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<256x16xf16, strided<[?, ?], offset: ?>> + %subview_17 = memref.subview %reinterpret_cast_16[0, 0] [%66, %76] [1, 1] {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : memref<256x16xf16, strided<[?, ?], offset: ?>> to memref> + %subview_18 = memref.subview %alloc[0, 0] [%66, %76] [1, 1] {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : memref<256x16xf16> to memref> + // CHECK: %{{.*}} = arith.addi %arg{{.*}}, %c256 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + %81 = arith.addi %arg25, %c256 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : index + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f16) outs(%{{.*}} : memref<256x16xf16>) + // CHECK: } {hivm.unlikely_condition, ssbuffer.block_id = 8 : i32} + scf.if %78 { + linalg.fill {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f16) outs(%alloc : memref<256x16xf16>) + } {hivm.unlikely_condition} + memref.copy %subview_17, %subview_18 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref> to memref> + annotation.mark %alloc {MayImplicitTransposeWithLastAxis, ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<256x16xf16> + %82 = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<256x16xf16> to tensor<256x16xf16> + // CHECK: %{{.*}} = arith.muli %arg{{.*}}, %c256_i32 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i32 + %83 = arith.muli %arg22, %c256_i32 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i32 + // CHECK: %{{.*}} = arith.subi %arg{{.*}}, %{{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i32 + %84 = arith.subi %arg6, %83 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i32 + // CHECK: %{{.*}} = memref.alloc() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<16x256xf16> + %alloc_19 = memref.alloc() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<16x256xf16> + // CHECK: %{{.*}} = arith.addi %{{.*}}, %c16 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %85 = arith.addi %41, %c16 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.index_cast %{{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %86 = arith.index_cast %arg5 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + // CHECK: %{{.*}} = arith.maxsi %{{.*}}, %{{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %87 = arith.maxsi %41, %86 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.minsi %{{.*}}, %{{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %88 = arith.minsi %85, %87 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.subi %{{.*}}, %{{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %89 = arith.subi %88, %41 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.index_cast %{{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %90 = arith.index_cast %84 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + // CHECK: %{{.*}} = arith.maxsi %{{.*}}, %c0 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %91 = arith.maxsi %90, %c0 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.minsi %{{.*}}, %c256 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %92 = arith.minsi %91, %c256 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.minsi %{{.*}}, %c16 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %93 = arith.minsi %89, %c16 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.minsi %{{.*}}, %c256 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %94 = arith.minsi %92, %c256 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.cmpi slt, %{{.*}}, %c16 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %95 = arith.cmpi slt, %93, %c16 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.cmpi slt, %{{.*}}, %c256 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %96 = arith.cmpi slt, %94, %c256 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + // CHECK: %{{.*}} = arith.ori %{{.*}}, %{{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i1 + %97 = arith.ori %95, %96 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : i1 + + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%{{.*}} : memref<16x256xf16>) + // CHECK: } {hivm.unlikely_condition, ssbuffer.block_id = 2 : i32} + scf.if %97 { + linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%alloc_19 : memref<16x256xf16>) + } {hivm.unlikely_condition} + %98 = arith.addi %41, %arg24 {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : index + %reinterpret_cast_20 = memref.reinterpret_cast %arg2 to offset: [%98], sizes: [16, 256], strides: [%41, %c1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<16x256xf16, strided<[?, ?], offset: ?>> + %subview_21 = memref.subview %reinterpret_cast_20[0, 0] [%93, %94] [1, 1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<16x256xf16, strided<[?, ?], offset: ?>> to memref> + %subview_22 = memref.subview %alloc_19[0, 0] [%93, %94] [1, 1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<16x256xf16> to memref> + memref.copy %subview_21, %subview_22 {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref> to memref> + %99 = bufferization.to_tensor %alloc_19 restrict writable {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<16x256xf16> to tensor<16x256xf16> + %100 = tensor.empty() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : tensor<16x16xf32> + %cst_23 = arith.constant {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f32 + %101 = linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%cst_23 : f32) outs(%100 : tensor<16x16xf32>) -> tensor<16x16xf32> + %102 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%99, %82 : tensor<16x256xf16>, tensor<256x16xf16>) outs(%101 : tensor<16x16xf32>) -> tensor<16x16xf32> + annotation.mark %82 {MayImplicitTransposeWithLastAxis, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "VECTOR"} : tensor<256x16xf16> + scf.yield {ssbuffer.core_type = "VECTOR, CUBE, VECTOR"} %102, %arg24, %81 : tensor<16x16xf32>, index, index + } {DataUse, ssbuffer.core_type = "VECTOR, CUBE, VECTOR"} + return + } + + //===--------------------------------------------------------------------===// + // No scf.if wrapper (fill outside scf.if) - SHOULD skip + // Condition: linalg.fill directly uses alloc, not inside scf.if + // Expected: No change + //===--------------------------------------------------------------------===// + // CHECK-LABEL: func.func @test_no_scfif_should_skip + func.func @test_no_scfif_should_skip(%arg0: memref) { + %c0_i64 = arith.constant 0 : index + %v = arith.constant {ssbuffer.block_id = 29 : i32, ssbuffer.core_type = "VECTOR"} 0.000000e+00 : f16 + + // alloc with block_id=11 should remain unchanged (no scf.if wrap) + // CHECK: memref.alloc() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xf16> + %alloc = memref.alloc() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xf16> + + // fill without scf.if wrapper should remain unchanged + // CHECK: linalg.fill {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} ins(%{{.*}} : f16) outs(%{{.*}} : memref<64x64xf16>) + linalg.fill {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} ins(%v : f16) outs(%alloc : memref<64x64xf16>) + + %k_sub_124 = arith.constant {ssbuffer.block_id = 21 : i32, ssbuffer.core_type = "VECTOR"} 64 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + %reinterpret_cast = memref.reinterpret_cast %arg0 to offset: [%c0_i64], sizes: [64, 64], strides: [%c64, %c1] {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<64x64xf16, strided<[?, ?], offset: ?>> + %subview = memref.subview %reinterpret_cast[0, 0] [%k_sub_124, 64] [1, 1] {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xf16, strided<[?, ?], offset: ?>> to memref> + + // Add memref.copy with block_id=15 - pass should not unify because no scf.if + // CHECK: memref.copy{{.*}}{ssbuffer.block_id = 15 : i32 + %dst_alloc = memref.alloc() {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xf16> + %dst_subview = memref.subview %dst_alloc[0, 0] [%k_sub_124, 64] [1, 1] {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} : memref<64x64xf16> to memref> + memref.copy %subview, %dst_subview {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} : memref> to memref> + + return + } + + //===----------------------------------------------------------------------===// + // Test Case: @test_iter_arg_dependency + //===----------------------------------------------------------------------===// + // Pattern based on: awq_gemm_kernel + // Key scenario: scf.for iter_args with scf.if condition dependency + //===----------------------------------------------------------------------===// + // CHECK-LABEL: func.func @test_iter_arg_dependency + func.func @test_iter_arg_dependency(%arg0: memref) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c32 = arith.constant 32 : index + %c32_idx = arith.constant 32 : index + %cst = arith.constant 0.000000e+00 : f16 + + scf.for %arg17 = %c0 to %c32 step %c1 iter_args(%arg19 = %c0) -> (index) { + // CHECK: arith.constant {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} 128 : index + %c128 = arith.constant {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} 128 : index + // CHECK: arith.cmpi {{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %cond = arith.cmpi slt, %arg19, %c32 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + + // CHECK: memref.alloc() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> + %alloc = memref.alloc() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> + // CHECK: arith.addi {{.*}} {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %140 = arith.addi %arg19, %c128 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + + // CHECK: scf.if %{{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + scf.if %cond { + linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%alloc : memref<32x32xf16>) + } {hivm.unlikely_condition} + + %141 = arith.addi %arg19, %c128 {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : index + %reinterpret_cast = memref.reinterpret_cast %arg0 to offset: [%141], sizes: [32, 32], strides: [%c32_idx, %c1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<32x32xf16, strided<[?, ?], offset: ?>> + %subview_src = memref.subview %reinterpret_cast[0, 0] [%c32, %c32] [1, 1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16, strided<[?, ?], offset: ?>> to memref> + %subview_dst = memref.subview %alloc[0, 0] [%c32, %c32] [1, 1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> to memref> + + // CHECK: memref.copy{{.*}}{ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + memref.copy %subview_src, %subview_dst {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref> to memref> + + scf.yield %140 : index + } + return + } + + //===----------------------------------------------------------------------===// + // Test Case: @test_copy_source_subview_traceback + //===----------------------------------------------------------------------===// + // Pattern: trace back memref.copy's source subview to memref.reinterpret_cast + // Key scenario: The copy's source operand (%subview_src) is a subview of + // %reinterpret_cast, which has a DIFFERENT block_id (5) from the copy (2). + // The pass should trace back from the copy's source subview through the + // view-like chain to the reinterpret_cast, and unify them to the copy's + // block_id (2). + // Before: + // %reinterpret_cast {block_id=5} // source base + // %subview_src = subview %reinterpret_cast {block_id=5} // source subview + // %subview_dst = subview %alloc {block_id=5} // dst subview + // memref.copy %subview_src, %subview_dst {block_id=2} + // After (source view chain unified to block_id=2): + // %reinterpret_cast {block_id=2} + // %subview_src {block_id=2} + // %subview_dst {block_id=2} + // memref.copy {block_id=2} + // (alloc, fill, scf.if, condition deps also unified to block_id=2) + //===----------------------------------------------------------------------===// + // CHECK-LABEL: func.func @test_copy_source_subview_traceback + func.func @test_copy_source_subview_traceback(%arg0: memref) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c32 = arith.constant 32 : index + %cst = arith.constant 0.000000e+00 : f16 + + scf.for %arg17 = %c0 to %c32 step %c1 iter_args(%arg19 = %c0) -> (index) { + %c128 = arith.constant {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} 128 : index + %cond = arith.cmpi slt, %arg19, %c32 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + %alloc = memref.alloc() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> + %140 = arith.addi %arg19, %c128 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : index + scf.if %cond { + linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%alloc : memref<32x32xf16>) + } {hivm.unlikely_condition} + + // Source view chain: reinterpret_cast and subview_src have block_id=5, + // should be unified to block_id=2 by tracing copy's source subview. + // CHECK: %{{.*}} = memref.reinterpret_cast {{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + %reinterpret_cast = memref.reinterpret_cast %arg0 to offset: [%140], sizes: [32, 32], strides: [%c32, %c1] {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<32x32xf16, strided<[?, ?], offset: ?>> + // CHECK: %{{.*}} = memref.subview {{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + %subview_src = memref.subview %reinterpret_cast[0, 0] [%c32, %c32] [1, 1] {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16, strided<[?, ?], offset: ?>> to memref> + // CHECK: %{{.*}} = memref.subview {{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + %subview_dst = memref.subview %alloc[0, 0] [%c32, %c32] [1, 1] {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> to memref> + + // CHECK: memref.copy{{.*}}{ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + memref.copy %subview_src, %subview_dst {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref> to memref> + + scf.yield %140 : index + } + return + } + + //===----------------------------------------------------------------------===// + // Test Case: @test_block_boundary_source_chain + //===----------------------------------------------------------------------===// + // Verify that when the source view chain of a memref.copy spans across MLIR + // Block boundaries, only ops in the same Block as the copyOp are collected + // and unified. Ops in different Blocks are skipped. + // + // Scenario: + // - arg_subview/reinterpret_cast from external arg, defined in parent block + // - outer memref.copy (block_id=15) in the same parent block + // - inner memref.copy (block_id=2) inside scf.for, also using arg_subview + // Expected: + // - arg_subview/reinterpret_cast unified to block_id=15 (same block as + // outer copy) + // - inner copy does NOT affect arg_subview (different block -> skipped) + //===----------------------------------------------------------------------===// + // CHECK-LABEL: func.func @test_block_boundary_source_chain + func.func @test_block_boundary_source_chain(%arg0: memref) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c32 = arith.constant 32 : index + %cst = arith.constant 0.000000e+00 : f16 + + %reinterpret_cast = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [32, 32], strides: [%c32, %c1] {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<32x32xf16, strided<[?, ?], offset: ?>> + // CHECK: %{{.*}} = memref.subview {{.*}} {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} + %arg_subview = memref.subview %reinterpret_cast[0, 0] [%c32, %c32] [1, 1] {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16, strided<[?, ?], offset: ?>> to memref> + + %outer_cond = arith.constant 1 : i1 + // CHECK: %{{.*}} = memref.alloc() {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> + %alloc = memref.alloc() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> + // CHECK: scf.if {{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} + scf.if %outer_cond { + linalg.fill {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%alloc : memref<32x32xf16>) + } {hivm.unlikely_condition} + // CHECK: %{{.*}} = memref.subview {{.*}} {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} + %dst_subview = memref.subview %alloc[0, 0] [%c32, %c32] [1, 1] {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> to memref> + // CHECK: memref.copy{{.*}}{ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} + memref.copy %arg_subview, %dst_subview {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} : memref> to memref> + + scf.for %iv = %c0 to %c1 step %c1 { + %inner_cond = arith.constant 1 : i1 + // CHECK: %{{.*}} = memref.alloc() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> + %inner_alloc = memref.alloc() {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> + // CHECK: scf.if {{.*}} { + // CHECK: linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + scf.if %inner_cond { + linalg.fill {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%inner_alloc : memref<32x32xf16>) + } {hivm.unlikely_condition} + // CHECK: %{{.*}} = memref.subview {{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + %inner_dst = memref.subview %inner_alloc[0, 0] [%c32, %c32] [1, 1] {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> to memref> + // CHECK: memref.copy{{.*}}{ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + memref.copy %arg_subview, %inner_dst {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref> to memref> + scf.yield + } + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/unify_store_block.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/unify_store_block.mlir new file mode 100644 index 0000000000..0c43dc3d71 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ComputeBlockOpt/unify_store_block.mlir @@ -0,0 +1,288 @@ +// RUN: triton-opt --unify-store-block %s | FileCheck %s + +module { + // ============================================ + // Test 1: VECTOR producer hit - basic unification + // + // Pattern (from real kernel IR): + // arith.truncf(VECTOR, block_id=7) + // -> tensor.extract_slice(block_id=8) + // -> bufferization.materialize_in_destination(block_id=8) + // dest chain: memref.reinterpret_cast(block_id=8) -> + // memref.subview(block_id=8) + // + // producer = arith.truncf (VECTOR_ONLY, block_id=7). + // The store chain (extract_slice, reinterpret_cast, subview, materialize) + // and the scalar dep (%c0 used by reinterpret_cast) are all at block_id=8. + // After pass: all unified to producer's block_id=7. + // ============================================ + // CHECK-LABEL: func @test_vector_producer_hit + func.func @test_vector_producer_hit(%arg0: memref {tt.divisibility = 16 : i32}, %in: tensor<64x64xf32>) { + // CHECK: arith.constant {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c0 = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + // CHECK: arith.truncf %{{.*}} {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %trunc = arith.truncf %in {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + // CHECK: tensor.extract_slice %{{.*}} {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + %extract = tensor.extract_slice %trunc[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + // CHECK: memref.reinterpret_cast %{{.*}} {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [128, 64], strides: [64, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + // CHECK: memref.subview %{{.*}} {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %subview = memref.subview %reinterpret[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + // CHECK: bufferization.materialize_in_destination %{{.*}} {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + bufferization.materialize_in_destination %extract in writable %subview {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + return + } + + // ============================================ + // Test 2: CUBE producer skip - no unification + // + // Pattern: + // linalg.matmul(CUBE, block_id=1) -> tensor.extract_slice(block_id=2) + // -> bufferization.materialize_in_destination(block_id=2) + // (no truncf; producer traced directly to matmul) + // + // producer = linalg.matmul (CUBE_ONLY) -> pass skips this store. + // All block_ids remain unchanged. + // ============================================ + // CHECK-LABEL: func @test_cube_producer_skip + func.func @test_cube_producer_skip(%arg0: memref {tt.divisibility = 16 : i32}, %A: tensor<64x64xf16>, %B: tensor<64x64xf16>, %init: tensor<64x64xf16>) { + // CHECK: arith.constant {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c0 = arith.constant {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + // CHECK: linalg.matmul {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} + %matmul = linalg.matmul {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%A, %B : tensor<64x64xf16>, tensor<64x64xf16>) outs(%init : tensor<64x64xf16>) -> tensor<64x64xf16> + // CHECK: tensor.extract_slice %{{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + %extract = tensor.extract_slice %matmul[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + // CHECK: memref.reinterpret_cast %{{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [128, 64], strides: [64, 1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + // CHECK: memref.subview %{{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %subview = memref.subview %reinterpret[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + // CHECK: bufferization.materialize_in_destination %{{.*}} {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + bufferization.materialize_in_destination %extract in writable %subview {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + return + } + + // ============================================ + // Test 4: hivm.hir.store use case + // + // Same shape as Test 1 but using hivm.hir.store instead of + // bufferization.materialize_in_destination. Store source operand is the + // tensor %extract (getSrc()), dest is the memref %subview (getDst()). + // + // producer = arith.truncf (VECTOR_ONLY, block_id=57). + // store + extract_slice + subview chain unify to 57. + // ============================================ + // CHECK-LABEL: func @test_hivm_store + func.func @test_hivm_store(%arg0: memref {tt.divisibility = 16 : i32}, %in: tensor<64x64xf32>) { + // %c0 feeds reinterpret_cast's offset (scalar dep), unified to 57. + // CHECK: arith.constant {ssbuffer.block_id = 57 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c0 = arith.constant {ssbuffer.block_id = 58 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + // CHECK: arith.truncf %{{.*}} {ssbuffer.block_id = 57 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %trunc = arith.truncf %in {ssbuffer.block_id = 57 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + // CHECK: tensor.extract_slice %{{.*}} {ssbuffer.block_id = 57 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + %extract = tensor.extract_slice %trunc[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 58 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + // CHECK: memref.reinterpret_cast %{{.*}} {ssbuffer.block_id = 57 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [128, 64], strides: [64, 1] {ssbuffer.block_id = 58 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + // CHECK: memref.subview %{{.*}} {ssbuffer.block_id = 57 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %subview = memref.subview %reinterpret[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 58 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + // CHECK: hivm.hir.store ins({{.*}} : tensor<64x64xf16>) outs({{.*}} : memref<64x64xf16, strided<[64, 1], offset: ?>>) {ssbuffer.block_id = 57 : i32, ssbuffer.core_type = "VECTOR"} + hivm.hir.store ins(%extract : tensor<64x64xf16>) outs(%subview : memref<64x64xf16, strided<[64, 1], offset: ?>>) {ssbuffer.block_id = 58 : i32, ssbuffer.core_type = "VECTOR"} + return + } + + // ============================================ + // Test 5: Multiple vector ops in chain - producer is the first vector + // encountered when tracing back from store (closest to store), not the + // upstream one. + // + // Data chain: + // %v1 = truncf (id=67) -> %v2 = addf (id=68) -> extract_slice (id=69) -> store (id=69) + // + // traceProducerOp pierces extract_slice (view), hits %v2 (VECTOR_ONLY) and + // returns immediately. %v1 is NOT reached, so: + // producer = %v2 (id=68), stays 68 + // %v1 stays 67 (not producer, not collected) + // store + extract_slice + dest view chain + %c0 unify to 68 + // ============================================ + // CHECK-LABEL: func @test_multiple_vector_producer_first + func.func @test_multiple_vector_producer_first(%arg0: memref {tt.divisibility = 16 : i32}, %in: tensor<64x64xf32>) { + // %c0 feeds reinterpret_cast's offset (scalar dep), unified to 68. + // CHECK: arith.constant {ssbuffer.block_id = 68 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c0 = arith.constant {ssbuffer.block_id = 69 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + // upstream vector op, NOT the producer; stays 67. + // CHECK: arith.truncf %{{.*}} {ssbuffer.block_id = 67 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %v1 = arith.truncf %in {ssbuffer.block_id = 67 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + // downstream vector op (closest to store), IS the producer; stays 68. + // CHECK: arith.addf %{{.*}}, %{{.*}} {ssbuffer.block_id = 68 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> + %v2 = arith.addf %v1, %v1 {ssbuffer.block_id = 68 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> + // CHECK: tensor.extract_slice %{{.*}} {ssbuffer.block_id = 68 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + %extract = tensor.extract_slice %v2[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 69 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + // CHECK: memref.reinterpret_cast %{{.*}} {ssbuffer.block_id = 68 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [128, 64], strides: [64, 1] {ssbuffer.block_id = 69 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + // CHECK: memref.subview %{{.*}} {ssbuffer.block_id = 68 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %subview = memref.subview %reinterpret[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 69 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + // CHECK: bufferization.materialize_in_destination %{{.*}} {ssbuffer.block_id = 68 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + bufferization.materialize_in_destination %extract in writable %subview {ssbuffer.block_id = 69 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + return + } + + // ============================================ + // Test 6: clone chained scalar ops (incl. constant) shared between store + // pattern and external block. + // ============================================ + // CHECK-LABEL: func @test_clone_scalar_cross_block + func.func @test_clone_scalar_cross_block(%arg0: memref {tt.divisibility = 16 : i32}, %in: tensor<64x64xf32>, %idx: index) { + %step = arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 64 : index + %c64 = arith.constant {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} 32 : index + // clone of %step (inserted before original): keeps block 12, feeds clones of %sub/%add. + // CHECK: arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 64 : index + // original %step: stays block 12, feeds originals of %sub/%add. + // CHECK: arith.constant {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} 64 : index + // %c64 (external constant): stays block 13, feeds %ext. + // CHECK: arith.constant {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} 32 : index + // producer stays block 12. + // CHECK: arith.truncf %{{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %trunc = arith.truncf %in {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + // CHECK: tensor.extract_slice %{{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + %extract = tensor.extract_slice %trunc[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + // clone of %sub: keeps block 13 (inserted before original, feeds clone of %add). + // CHECK: arith.subi %{{.*}}, %{{.*}} {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + // original %sub: unified to producer's block 12 (feeds original %add). + // CHECK: arith.subi %{{.*}}, %{{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : index + %sub = arith.subi %idx, %step {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + // clone of %add: keeps block 13 (inserted before original, feeds external %ext). + // CHECK: arith.addi %{{.*}}, %{{.*}} {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + // original %add: unified to producer's block 12 (feeds dest chain). + // CHECK: arith.addi %{{.*}}, %{{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : index + %add = arith.addi %sub, %step {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + // dest chain uses original %add, unified to block 12. + // CHECK: memref.reinterpret_cast %{{.*}} to offset: [%{{.*}}] {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%add], sizes: [128, 64], strides: [64, 1] {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + // CHECK: memref.subview %{{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %subview = memref.subview %reinterpret[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + // CHECK: bufferization.materialize_in_destination %{{.*}} {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + bufferization.materialize_in_destination %extract in writable %subview {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + // external user stays block 13, now uses the clone of %add. + // CHECK: arith.muli %{{.*}}, %{{.*}} {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %ext = arith.muli %add, %c64 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + return + } + + // ============================================ + // Test 7: producer and store already share the same block_id, but viewOps + // in between have a different block_id. The pass should still collect and + // unify those viewOps to the producer's block_id. + // + // Pattern: + // arith.truncf(VECTOR, block_id=5) <- producer + // -> tensor.extract_slice(block_id=6) <- viewOp to unify + // -> bufferization.materialize_in_destination(block_id=5) <- store + // dest chain: memref.reinterpret_cast(block_id=6) <- viewOp to unify + // -> memref.subview(block_id=6) <- viewOp to unify + // + // All viewOps (extract_slice, reinterpret_cast, subview) at block_id=6 + // should be unified to block_id=5. + // ============================================ + // CHECK-LABEL: func @test_producer_store_same_block + func.func @test_producer_store_same_block(%arg0: memref {tt.divisibility = 16 : i32}, %in: tensor<64x64xf32>) { + // %c0: scalar dep at block 6, unified to 5. + // CHECK: arith.constant {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c0 = arith.constant {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + // producer stays at block 5. + // CHECK: arith.truncf %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %trunc = arith.truncf %in {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + // extract_slice at block 6, unified to 5. + // CHECK: tensor.extract_slice %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + %extract = tensor.extract_slice %trunc[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + // reinterpret_cast at block 6, unified to 5. + // CHECK: memref.reinterpret_cast %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [128, 64], strides: [64, 1] {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + // subview at block 6, unified to 5. + // CHECK: memref.subview %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %subview = memref.subview %reinterpret[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + // store stays at block 5. + // CHECK: bufferization.materialize_in_destination %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + bufferization.materialize_in_destination %extract in writable %subview {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + return + } + + // ============================================ + // Test 8: Producer outside scf.for, store inside → NO unification + // + // producer = arith.truncf (VECTOR_ONLY, block_id=5) lives in the function + // body, while the store and its data/dest chain are inside scf.for + // (block_id=8). traceProducerOp still finds the producer because + // %extract_slice directly references %trunc from the outer scope, but + // matchStorePattern rejects the match because producer->getBlock() != + // storeOp->getBlock() (different IR blocks). + // + // All block_ids remain unchanged. + // ============================================ + // CHECK-LABEL: func @test_producer_outside_for + func.func @test_producer_outside_for(%arg0: memref {tt.divisibility = 16 : i32}, %in: tensor<64x64xf32>) { + // %c0 feeds reinterpret_cast's offset (scalar dep), stays at block 8. + // CHECK: arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c0 = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + // producer outside scf.for, stays at block 5. + // CHECK: arith.truncf %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %trunc = arith.truncf %in {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %lb = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %ub = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 64 : index + %step = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 1 : index + // store and its chain inside scf.for; all stay at block 8. + scf.for %i = %lb to %ub step %step { + // CHECK: tensor.extract_slice %{{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + %extract = tensor.extract_slice %trunc[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + // CHECK: memref.reinterpret_cast %{{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [128, 64], strides: [64, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + // CHECK: memref.subview %{{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %subview = memref.subview %reinterpret[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + // CHECK: bufferization.materialize_in_destination %{{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + bufferization.materialize_in_destination %extract in writable %subview {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + } + return + } + + // ============================================ + // Test 9: Dest view ops outside scf.for, store and producer inside + // → unification happens + // + // producer = arith.truncf (VECTOR_ONLY, block_id=5) inside scf.for. + // store = bufferization.materialize_in_destination (block_id=8) inside for. + // data view = tensor.extract_slice (block_id=8) inside for. + // dest views = memref.reinterpret_cast, memref.subview (block_id=8) outside + // for. + // + // producer and store are in the same IR block (inside scf.for) → pattern + // matched. All matched ops unified to producer's block_id=5. Scalar + // deps outside the for loop (%c0, for-loop bounds) stay at their original + // block_ids. + // ============================================ + // CHECK-LABEL: func @test_dest_view_outside_for + func.func @test_dest_view_outside_for(%arg0: memref {tt.divisibility = 16 : i32}, %in: tensor<64x64xf32>) { + // %c0 feeds reinterpret_cast, stays at block 8 (outside for, not collected + // by scalar deps). + // CHECK: arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c0 = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + // dest view ops outside scf.for, unified to producer's block 5. + // CHECK: memref.reinterpret_cast %{{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [128, 64], strides: [64, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf16, strided<[64, 1], offset: ?>> + // CHECK: memref.subview %{{.*}} {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %subview = memref.subview %reinterpret[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf16, strided<[64, 1], offset: ?>> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %lb = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %ub = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 64 : index + %step = arith.constant {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} 1 : index + // producer and store inside scf.for. + scf.for %i = %lb to %ub step %step { + // producer stays at block 5. + // CHECK: arith.truncf %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + %trunc = arith.truncf %in {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> to tensor<64x64xf16> + // data view unified to block 5. + // CHECK: tensor.extract_slice %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + %extract = tensor.extract_slice %trunc[0, 0] [64, 64] [1, 1] {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64x64xf16> to tensor<64x64xf16> + // store unified to block 5. + // CHECK: bufferization.materialize_in_destination %{{.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + bufferization.materialize_in_destination %extract in writable %subview {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64x64xf16>, memref<64x64xf16, strided<[64, 1], offset: ?>>) -> () + } + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/implicit_transpose_test.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/implicit_transpose_test.mlir new file mode 100644 index 0000000000..475a0ba678 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/implicit_transpose_test.mlir @@ -0,0 +1,75 @@ +// RUN: triton-opt --plan-compute-block %s | FileCheck %s + +module { + // Test Case 1 & 2: Implicit Transpose with unlikely condition fill + // Verifies that: + // 1. %to_tensor, %alloc, and %fill are colored as VECTOR instead of CUBE. + // 2. The fill inside the unlikely scf.if is colored as VECTOR. + // 3. The annotation.mark has the same block_id as its defining op (to_tensor). + func.func @test_implicit_transpose_vector_coloring(%arg0: memref, %arg1: memref, %cond: i1) -> tensor<64x64xf32> { + // CHECK-LABEL: func.func @test_implicit_transpose_vector_coloring( + + // CHECK: %[[ALLOC:.+]] = memref.alloc() {ssbuffer.block_id = [[B0:[0-9]+]] : i32, ssbuffer.core_type = "VECTOR"} + %alloc = memref.alloc() : memref<64x64xf32> + %cst = arith.constant 0.000000e+00 : f32 + + // CHECK: scf.if + // CHECK: linalg.fill {ssbuffer.block_id = {{[0-9]+}} : i32, ssbuffer.core_type = "VECTOR"} + scf.if %cond { + linalg.fill ins(%cst : f32) outs(%alloc : memref<64x64xf32>) + } {hivm.unlikely_condition} + + // CHECK: %[[TENSOR:.+]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.block_id = [[B1:[0-9]+]] : i32, ssbuffer.core_type = "VECTOR"} + %to_tensor = bufferization.to_tensor %alloc restrict writable : memref<64x64xf32> to tensor<64x64xf32> + // CHECK: annotation.mark %[[TENSOR]] {MayImplicitTransposeWithLastAxis, ssbuffer.block_id = [[B1]] : i32, ssbuffer.core_type = "VECTOR"} + annotation.mark %to_tensor {MayImplicitTransposeWithLastAxis} : tensor<64x64xf32> + + %empty = tensor.empty() : tensor<64x64xf32> + %fill = linalg.fill ins(%cst : f32) outs(%empty : tensor<64x64xf32>) -> tensor<64x64xf32> + %other = tensor.empty() : tensor<64x64xf32> + + %matmul = linalg.matmul ins(%to_tensor, %other : tensor<64x64xf32>, tensor<64x64xf32>) outs(%fill : tensor<64x64xf32>) -> tensor<64x64xf32> + return %matmul : tensor<64x64xf32> + } + + // Test Case 3: Normal ToTensor without Implicit Transpose + // Verifies that a standard matmul-feeding to_tensor without the annotation is still colored as CUBE. + func.func @test_normal_to_tensor_cube_coloring(%arg0: memref) -> tensor<64x64xf32> { + // CHECK-LABEL: func.func @test_normal_to_tensor_cube_coloring( + + // CHECK: %[[ALLOC_CUBE:.+]] = memref.alloc() {ssbuffer.block_id = [[B2:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + %alloc = memref.alloc() : memref<64x64xf32> + + // CHECK: %[[TENSOR_CUBE:.+]] = bufferization.to_tensor %[[ALLOC_CUBE]] restrict writable {ssbuffer.block_id = [[B3:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + %to_tensor = bufferization.to_tensor %alloc restrict writable : memref<64x64xf32> to tensor<64x64xf32> + %cst = arith.constant 0.000000e+00 : f32 + %empty = tensor.empty() : tensor<64x64xf32> + %fill = linalg.fill ins(%cst : f32) outs(%empty : tensor<64x64xf32>) -> tensor<64x64xf32> + %other = tensor.empty() : tensor<64x64xf32> + + %matmul = linalg.matmul ins(%to_tensor, %other : tensor<64x64xf32>, tensor<64x64xf32>) outs(%fill : tensor<64x64xf32>) -> tensor<64x64xf32> + return %matmul : tensor<64x64xf32> + } + + // Test Case 4: Block ID alignment check for annotation.mark + // Verifies that annotation.mark always follows its defining operation (%to_tensor) + // in terms of block assignment, preventing mismatched block IDs. + func.func @test_mark_block_id_alignment(%arg0: memref, %cond: i1) -> tensor<64x64xf32> { + // CHECK-LABEL: func.func @test_mark_block_id_alignment( + + %alloc = memref.alloc() : memref<64x64xf32> + + // CHECK: %[[TENSOR_VAL:.+]] = bufferization.to_tensor %{{.+}} restrict writable {ssbuffer.block_id = [[B4:[0-9]+]] : i32, ssbuffer.core_type = "VECTOR"} + %to_tensor = bufferization.to_tensor %alloc restrict writable : memref<64x64xf32> to tensor<64x64xf32> + // CHECK: annotation.mark %[[TENSOR_VAL]] {MayImplicitTransposeWithLastAxis, ssbuffer.block_id = [[B4]] : i32, ssbuffer.core_type = "VECTOR"} + annotation.mark %to_tensor {MayImplicitTransposeWithLastAxis} : tensor<64x64xf32> + + %cst = arith.constant 0.000000e+00 : f32 + %empty = tensor.empty() : tensor<64x64xf32> + %fill = linalg.fill ins(%cst : f32) outs(%empty : tensor<64x64xf32>) -> tensor<64x64xf32> + %other = tensor.empty() : tensor<64x64xf32> + + %matmul = linalg.matmul ins(%to_tensor, %other : tensor<64x64xf32>, tensor<64x64xf32>) outs(%fill : tensor<64x64xf32>) -> tensor<64x64xf32> + return %matmul : tensor<64x64xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_annotation_mark_memory_effects.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_annotation_mark_memory_effects.mlir new file mode 100644 index 0000000000..1cb15da5d1 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_annotation_mark_memory_effects.mlir @@ -0,0 +1,32 @@ +// RUN: triton-opt --debug-only=memory-effects-tracker --plan-compute-block %s 2>&1 | FileCheck %s + +module { + // CHECK-LABEL: func.func @annotation_mark_memref_is_not_memory_barrier + // CHECK: %{{.*}} = memref.load + // CHECK: %[[ALLOC:[A-Za-z0-9_]+]] = memref.alloc() + // CHECK: Analyzing op annotation.mark %[[ALLOC]] {MayImplicitTransposeWithLastAxis} + // CHECK-NEXT: [memory-effects-tracker] Defs: + // CHECK-NEXT: [memory-effects-tracker] Preds: + // CHECK-NEXT: [memory-effects-tracker] linalg.fill {{.*}} outs(%[[ALLOC]] + // CHECK-NEXT: [memory-effects-tracker] Analyzing op %{{.*}} = bufferization.to_tensor %[[ALLOC]] + // CHECK-NEXT: [memory-effects-tracker] Defs: + // CHECK-NEXT: [memory-effects-tracker] linalg.fill {{.*}} outs(%[[ALLOC]] + // CHECK-NEXT: [memory-effects-tracker] Preds: + // CHECK-NEXT: [memory-effects-tracker] annotation.mark %[[ALLOC]] {MayImplicitTransposeWithLastAxis} + func.func @annotation_mark_memref_is_not_memory_barrier( + %arg0: memref<4xf32>, + %rhs: tensor<4x4xf32>) -> tensor<4x4xf32> { + %c0 = arith.constant 0 : index + %zero = arith.constant 0.0 : f32 + %unused = memref.load %arg0[%c0] : memref<4xf32> + %alloc = memref.alloc() : memref<4x4xf32> + linalg.fill ins(%zero : f32) outs(%alloc : memref<4x4xf32>) + annotation.mark %alloc {MayImplicitTransposeWithLastAxis} : memref<4x4xf32> + %lhs = bufferization.to_tensor %alloc restrict writable : memref<4x4xf32> to tensor<4x4xf32> + %out = tensor.empty() : tensor<4x4xf32> + %init = linalg.fill ins(%zero : f32) outs(%out : tensor<4x4xf32>) -> tensor<4x4xf32> + %mm = linalg.matmul ins(%lhs, %rhs : tensor<4x4xf32>, tensor<4x4xf32>) + outs(%init : tensor<4x4xf32>) -> tensor<4x4xf32> + return %mm : tensor<4x4xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_dynamic_matmul_prelegalize_bug.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_dynamic_matmul_prelegalize_bug.mlir new file mode 100644 index 0000000000..ac59b16dd5 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_dynamic_matmul_prelegalize_bug.mlir @@ -0,0 +1,20 @@ +// RUN: triton-opt --plan-compute-block %s | FileCheck %s + +// This documents a current PlanComputeBlock failure: +// preLegalizeMatmul builds tensor.dim from the original matmul result before +// that result dominates the new tensor.dim users. + +module { + // CHECK-LABEL: func.func @dynamic_matmul_prelegalize_uses_result_before_def( + // CHECK: linalg.matmul {{.*}}ssbuffer.core_type = "CUBE" + // CHECK: return + func.func @dynamic_matmul_prelegalize_uses_result_before_def( + %a: tensor, + %b: tensor, + %out: tensor) -> tensor { + %mm = linalg.matmul + ins(%a, %b : tensor, tensor) + outs(%out : tensor) -> tensor + return %mm : tensor + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_match_empty_pattern.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_match_empty_pattern.mlir new file mode 100644 index 0000000000..fa6d1017a6 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_match_empty_pattern.mlir @@ -0,0 +1,20 @@ +// RUN: triton-opt --op-classifier %s | FileCheck %s + +// Test tensor.empty pattern matching: empty outside loop, used as iter_arg, matmul uses iter_arg as output + +module { + // CHECK-LABEL: func.func @test_empty_loop_iter_arg + // CHECK: tensor.empty() {ssbuffer.core_type = "CUBE"} + // CHECK: scf.for + // CHECK: linalg.matmul {ssbuffer.core_type = "CUBE"} + func.func @test_empty_loop_iter_arg(%a: tensor<64x64xf16>, %b: tensor<64x64xf16>, %n: index) -> tensor<64x64xf32> { + %empty = tensor.empty() : tensor<64x64xf32> + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %result = scf.for %i = %c0 to %n step %c1 iter_args(%acc = %empty) -> (tensor<64x64xf32>) { + %mm = linalg.matmul ins(%a, %b : tensor<64x64xf16>, tensor<64x64xf16>) outs(%acc : tensor<64x64xf32>) -> tensor<64x64xf32> + scf.yield %mm : tensor<64x64xf32> + } + return %result : tensor<64x64xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_merge_cube_for.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_merge_cube_for.mlir new file mode 100644 index 0000000000..2d07f1b11b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_merge_cube_for.mlir @@ -0,0 +1,178 @@ +// RUN: triton-opt --split-input-file --op-classifier %s | FileCheck %s + +// Test Case 1: Store to global memory (GM) via bufferization.materialize_in_destination. +// Verifies that: +// 1. MemoryEffects::Write targeting a function BlockArgument (GM) disqualifies the +// scf.for loop from being treated as a Cube loader loop. +// 2. All operations inside the loop body are colored as VECTOR. +// 3. None of the operations inside the loop are marked as CUBE. +func.func @test_materialize_to_gm_disqualifies_cube_loader( + %arg0: memref, + %arg1: memref, + %start: i32, + %end: i32, + %step: i32, + %mat_a: tensor<128x128xf16>, + %mat_b: tensor<128x128xf16>, + %mat_out: tensor<128x128xf32> +) -> tensor<128x128xf32> { + // CHECK-LABEL: func.func @test_materialize_to_gm_disqualifies_cube_loader( + + // CHECK: scf.for + // CHECK-NOT: {ssbuffer.core_type = "CUBE"} + // CHECK: %[[ALLOC:.+]] = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<1x512xf16> + // CHECK: memref.copy {{.*}}, %[[ALLOC]] {ssbuffer.core_type = "VECTOR"} + // CHECK: %[[TENSOR:.+]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.core_type = "VECTOR"} + // CHECK: bufferization.materialize_in_destination %[[TENSOR]] in writable {{.*}} {ssbuffer.core_type = "VECTOR"} + // CHECK: } + scf.for %iv = %start to %end step %step : i32 { + %idx = arith.index_cast %iv : i32 to index + %reinterpret_in = memref.reinterpret_cast %arg0 to offset: [%idx], sizes: [1, 512], strides: [512, 1] : memref to memref<1x512xf16, strided<[512, 1], offset: ?>> + %alloc = memref.alloc() : memref<1x512xf16> + memref.copy %reinterpret_in, %alloc : memref<1x512xf16, strided<[512, 1], offset: ?>> to memref<1x512xf16> + %tensor = bufferization.to_tensor %alloc restrict writable : memref<1x512xf16> to tensor<1x512xf16> + + // Write to function argument %arg1 (GM) via reinterpret_cast + %reinterpret_out = memref.reinterpret_cast %arg1 to offset: [%idx], sizes: [1, 512], strides: [512, 1] : memref to memref<1x512xf16, strided<[512, 1], offset: ?>> + bufferization.materialize_in_destination %tensor in writable %reinterpret_out : (tensor<1x512xf16>, memref<1x512xf16, strided<[512, 1], offset: ?>>) -> () + } + + // Downstream CUBE operation ensures Cube pipeline is active + // CHECK: linalg.matmul {ssbuffer.core_type = "CUBE"} + %res = linalg.matmul ins(%mat_a, %mat_b : tensor<128x128xf16>, tensor<128x128xf16>) outs(%mat_out : tensor<128x128xf32>) -> tensor<128x128xf32> + return %res : tensor<128x128xf32> +} + +// ----- + +// Test Case 2: Store to global memory (GM) via direct memref.copy. +// Verifies that direct memref.copy whose destination aliases a function BlockArgument +// also disqualifies the loader loop, ensuring all loop operations remain VECTOR. +func.func @test_copy_to_gm_disqualifies_cube_loader( + %arg0: memref, + %arg1: memref, + %start: i32, + %end: i32, + %step: i32, + %mat_a: tensor<64x64xf32>, + %mat_b: tensor<64x64xf32>, + %mat_out: tensor<64x64xf32> +) -> tensor<64x64xf32> { + // CHECK-LABEL: func.func @test_copy_to_gm_disqualifies_cube_loader( + + // CHECK: scf.for + // CHECK-NOT: {ssbuffer.core_type = "CUBE"} + // CHECK: %[[ALLOC:.+]] = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64xf32> + // CHECK: memref.copy {{.*}}, %[[ALLOC]] {ssbuffer.core_type = "VECTOR"} + // CHECK: memref.copy %[[ALLOC]], {{.*}} {ssbuffer.core_type = "VECTOR"} + // CHECK: } + scf.for %iv = %start to %end step %step : i32 { + %idx = arith.index_cast %iv : i32 to index + %alloc = memref.alloc() : memref<64xf32> + %reinterpret_in = memref.reinterpret_cast %arg0 to offset: [%idx], sizes: [64], strides: [1] : memref to memref<64xf32, strided<[1], offset: ?>> + memref.copy %reinterpret_in, %alloc : memref<64xf32, strided<[1], offset: ?>> to memref<64xf32> + + // Direct write-back to GM argument %arg1 + %reinterpret_out = memref.reinterpret_cast %arg1 to offset: [%idx], sizes: [64], strides: [1] : memref to memref<64xf32, strided<[1], offset: ?>> + memref.copy %alloc, %reinterpret_out : memref<64xf32> to memref<64xf32, strided<[1], offset: ?>> + } + + // CHECK: linalg.matmul {ssbuffer.core_type = "CUBE"} + %res = linalg.matmul ins(%mat_a, %mat_b : tensor<64x64xf32>, tensor<64x64xf32>) outs(%mat_out : tensor<64x64xf32>) -> tensor<64x64xf32> + return %res : tensor<64x64xf32> +} + +// ----- + +// Test Case 3: Legitimate Cube loader loop without GM stores. +// Verifies that a true Cube loader loop (loading GM data into a local memref.alloc buffer +// whose results are consumed only by linalg.matmul) is NOT disqualified and is correctly +// colored as CUBE. +func.func @test_legitimate_cube_loader_coloring( + %arg0: memref, + %start: i32, + %end: i32, + %step: i32, + %mat_b: tensor<128x128xf16>, + %mat_out: tensor<128x128xf32> +) -> tensor<128x128xf32> { + // CHECK-LABEL: func.func @test_legitimate_cube_loader_coloring( + // CHECK: %[[INIT:.+]] = tensor.empty() {{.*}} : tensor<128x128xf16> + // CHECK: %[[LOADED:.+]] = scf.for %{{.*}} = %{{.*}} to %{{.*}} step %{{.*}} iter_args(%{{.*}} = %[[INIT]]) -> (tensor<128x128xf16>) + // CHECK: arith.index_cast {{.*}} {ssbuffer.core_type = "CUBE"} : i32 to index + // CHECK: memref.reinterpret_cast {{.*}} {ssbuffer.core_type = "CUBE"} + // CHECK: %[[ALLOC:.+]] = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x128xf16> + // CHECK: memref.copy {{.*}}, %[[ALLOC]] {ssbuffer.core_type = "CUBE"} + // CHECK: %[[TENSOR:.+]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.core_type = "CUBE"} + // CHECK: scf.yield {ssbuffer.core_type = "CUBE"} %[[TENSOR]] + // CHECK: } {ssbuffer.core_type = "CUBE"} + // CHECK: linalg.matmul {ssbuffer.core_type = "CUBE"} ins(%[[LOADED]], + %init = tensor.empty() : tensor<128x128xf16> + %loaded = scf.for %iv = %start to %end step %step iter_args(%iter = %init) -> (tensor<128x128xf16>) : i32 { + %idx = arith.index_cast %iv : i32 to index + %reinterpret = memref.reinterpret_cast %arg0 to offset: [%idx], sizes: [128, 128], strides: [128, 1] : memref to memref<128x128xf16, strided<[128, 1], offset: ?>> + %alloc = memref.alloc() : memref<128x128xf16> + memref.copy %reinterpret, %alloc : memref<128x128xf16, strided<[128, 1], offset: ?>> to memref<128x128xf16> + %tensor = bufferization.to_tensor %alloc restrict writable : memref<128x128xf16> to tensor<128x128xf16> + scf.yield %tensor : tensor<128x128xf16> + } + + %res = linalg.matmul ins(%loaded, %mat_b : tensor<128x128xf16>, tensor<128x128xf16>) outs(%mat_out : tensor<128x128xf32>) -> tensor<128x128xf32> + return %res : tensor<128x128xf32> +} + +// ----- + +// Test Case 4: Multi-level alias chain resolution (subview over reinterpret_cast of GM argument). +// Reproduces the exact faulty scenario from sparse_flash_attention_grad_kernel: +// 1. Loop contains conditional fill under scf.if. +// 2. Writes targeting a subview of reinterpret_cast pointing to %arg_gm_out. +// 3. Verifies that getViewSource resolves the full alias chain to the func BlockArgument. +// 4. Verifies all ops remain VECTOR and no faulty CUBE coloring occurs. +func.func @test_nested_view_gm_store_vector_coloring( + %arg_in: memref, + %arg_gm_out: memref, + %start: i32, + %end: i32, + %step: i32, + %cond: i1, + %cst: f16, + %mat_a: tensor<128x128xf16>, + %mat_b: tensor<128x128xf16>, + %mat_out: tensor<128x128xf32> +) -> tensor<128x128xf32> { + // CHECK-LABEL: func.func @test_nested_view_gm_store_vector_coloring( + + // CHECK: scf.for + // CHECK-NOT: {ssbuffer.core_type = "CUBE"} + // CHECK: %[[ALLOC:.+]] = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<1x512xf16> + // CHECK: scf.if + // CHECK: linalg.fill {ssbuffer.core_type = "VECTOR"} + // CHECK: } + // CHECK: memref.copy {{.*}}, %[[ALLOC]] {ssbuffer.core_type = "VECTOR"} + // CHECK: %[[TENSOR:.+]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.core_type = "VECTOR"} + // CHECK: %[[SUBVIEW:.+]] = memref.subview {{.*}} {ssbuffer.core_type = "VECTOR"} + // CHECK: bufferization.materialize_in_destination %[[TENSOR]] in writable %[[SUBVIEW]] {ssbuffer.core_type = "VECTOR"} + // CHECK: } + scf.for %iv = %start to %end step %step : i32 { + %idx = arith.index_cast %iv : i32 to index + %reinterpret_in = memref.reinterpret_cast %arg_in to offset: [%idx], sizes: [1, 512], strides: [512, 1] : memref to memref<1x512xf16, strided<[512, 1], offset: ?>> + %alloc = memref.alloc() : memref<1x512xf16> + + scf.if %cond { + linalg.fill ins(%cst : f16) outs(%alloc : memref<1x512xf16>) + } + + memref.copy %reinterpret_in, %alloc : memref<1x512xf16, strided<[512, 1], offset: ?>> to memref<1x512xf16> + %tensor = bufferization.to_tensor %alloc restrict writable : memref<1x512xf16> to tensor<1x512xf16> + + // Multi-level view chain: subview -> reinterpret_cast -> %arg_gm_out + %reinterpret_out = memref.reinterpret_cast %arg_gm_out to offset: [%idx], sizes: [1, 512], strides: [512, 1] : memref to memref<1x512xf16, strided<[512, 1], offset: ?>> + %subview_out = memref.subview %reinterpret_out[0, 0] [1, 512] [1, 1] : memref<1x512xf16, strided<[512, 1], offset: ?>> to memref<1x512xf16, strided<[512, 1], offset: ?>> + bufferization.materialize_in_destination %tensor in writable %subview_out : (tensor<1x512xf16>, memref<1x512xf16, strided<[512, 1], offset: ?>>) -> () + } + + // CHECK: linalg.matmul {ssbuffer.core_type = "CUBE"} + %res = linalg.matmul ins(%mat_a, %mat_b : tensor<128x128xf16>, tensor<128x128xf16>) outs(%mat_out : tensor<128x128xf32>) -> tensor<128x128xf32> + return %res : tensor<128x128xf32> +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_compute_block_regional.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_compute_block_regional.mlir new file mode 100644 index 0000000000..3d148434f8 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_compute_block_regional.mlir @@ -0,0 +1,83 @@ +// RUN: triton-opt --plan-compute-block %s | FileCheck %s + +module { + // ============================================================================ + // 1. extracted_load_store_stays_vector + // ============================================================================ + // + // CHECK-LABEL: func.func @extracted_load_store_stays_vector( + // CHECK: %[[ALLOC:[A-Za-z0-9_]+]] = memref.alloc() {ssbuffer.block_id = [[B_ID:[0-9]+]] : i32, ssbuffer.core_type = "VECTOR"} + // CHECK: scf.for + // CHECK: %[[SUBVIEW:[A-Za-z0-9_]+]] = memref.subview %[[ALLOC]] + // CHECK: memref.copy %{{.*}}, %[[SUBVIEW]] {ssbuffer.block_id = [[B_ID]] : i32, ssbuffer.core_type = "VECTOR"} + // CHECK: %[[LHS:[A-Za-z0-9_]+]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.block_id = [[B_ID]] : i32, ssbuffer.core_type = "VECTOR"} + // CHECK: linalg.matmul {ssbuffer.block_id = {{[0-9]+}} : i32, ssbuffer.core_type = "CUBE"} + func.func @extracted_load_store_stays_vector( + %arg0: memref, + %rhs: tensor<64x64xf16>) -> tensor<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c32 = arith.constant 32 : index + %alloc = memref.alloc() : memref<32x64xf16> + + scf.for %arg1 = %c0 to %c32 step %c1 { + %subview = memref.subview %alloc[%arg1, 0] [1, 64] [1, 1] : memref<32x64xf16> to memref<1x64xf16, strided<[64, 1], offset: ?>> + %reinterpret_cast = memref.reinterpret_cast %arg0 to offset: [0], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf16, strided<[64, 1], offset: ?>> + memref.copy %reinterpret_cast, %subview : memref<1x64xf16, strided<[64, 1], offset: ?>> to memref<1x64xf16, strided<[64, 1], offset: ?>> + } {ExtractedLoadOrStore} + + %lhs = bufferization.to_tensor %alloc restrict writable : memref<32x64xf16> to tensor<32x64xf16> + %out = tensor.empty() : tensor<32x64xf32> + %cst_f32 = arith.constant 0.0 : f32 + %init = linalg.fill ins(%cst_f32 : f32) outs(%out : tensor<32x64xf32>) -> tensor<32x64xf32> + %mm = linalg.matmul ins(%lhs, %rhs : tensor<32x64xf16>, tensor<64x64xf16>) outs(%init : tensor<32x64xf32>) -> tensor<32x64xf32> + return %mm : tensor<32x64xf32> + } + + // ============================================================================ + // 2. cube_control_flow_inheritance + // ============================================================================ + // + // CHECK-LABEL: func.func @cube_control_flow_inheritance( + // CHECK: %[[ALLOC:[A-Za-z0-9_]+]] = memref.alloc() {ssbuffer.block_id = [[B_ID:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK: scf.if + // CHECK: linalg.fill {ssbuffer.block_id = [[B_ID]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK: %[[LHS:[A-Za-z0-9_]+]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.block_id = [[B_ID]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK: linalg.matmul {ssbuffer.block_id = [[B_ID]] : i32, ssbuffer.core_type = "CUBE"} + func.func @cube_control_flow_inheritance( + %cond: i1, + %arg0: tensor<64x64xf16>, + %arg1: tensor<64x64xf32>) -> tensor<64x64xf32> { + %alloc = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x64xf16> + %cst = arith.constant 0.0 : f16 + + scf.if %cond { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%alloc : memref<64x64xf16>) + } + + %lhs = bufferization.to_tensor %alloc restrict writable {ssbuffer.core_type = "CUBE"} : memref<64x64xf16> to tensor<64x64xf16> + %mm = linalg.matmul {ssbuffer.core_type = "CUBE"} ins(%lhs, %arg0 : tensor<64x64xf16>, tensor<64x64xf16>) outs(%arg1 : tensor<64x64xf32>) -> tensor<64x64xf32> + return %mm : tensor<64x64xf32> + } + + // ============================================================================ + // 3. vector_control_flow_inheritance + // ============================================================================ + // + // CHECK-LABEL: func.func @vector_control_flow_inheritance( + // CHECK: %[[ALLOC:[A-Za-z0-9_]+]] = memref.alloc() {ssbuffer.block_id = [[B_ID:[0-9]+]] : i32, ssbuffer.core_type = "VECTOR"} + // CHECK: scf.if + // CHECK: linalg.fill {ssbuffer.block_id = [[B_ID]] : i32, ssbuffer.core_type = "VECTOR"} + // CHECK: memref.copy %[[ALLOC]], %{{.*}} {ssbuffer.block_id = [[B_ID]] : i32, ssbuffer.core_type = "VECTOR"} + func.func @vector_control_flow_inheritance(%cond: i1, %arg0: memref<64x64xf32>) { + %cst = arith.constant 0.0 : f32 + %alloc = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x64xf32> + + scf.if %cond { + linalg.fill {ssbuffer.core_type = "VECTOR"} ins(%cst : f32) outs(%alloc : memref<64x64xf32>) + } + + memref.copy %alloc, %arg0 {ssbuffer.core_type = "VECTOR"} : memref<64x64xf32> to memref<64x64xf32> + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_compute_block_subview_memory_deps.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_compute_block_subview_memory_deps.mlir new file mode 100644 index 0000000000..6371a3905b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_compute_block_subview_memory_deps.mlir @@ -0,0 +1,56 @@ +// RUN: triton-opt --plan-compute-block %s | FileCheck %s + +module { + // CHECK-LABEL: func.func @copy_to_subview_feeds_matmul( + // CHECK: memref.copy {{.*}}ssbuffer.core_type = "CUBE" + // CHECK: bufferization.to_tensor {{.*}}ssbuffer.core_type = "CUBE" + // CHECK: linalg.matmul {{.*}}ssbuffer.core_type = "CUBE" + func.func @copy_to_subview_feeds_matmul(%src: memref<64x64xf16>, %rhs: tensor<64x64xf16>) -> tensor<64x64xf32> { + %c0 = arith.constant 0 : index + %c64 = arith.constant 64 : index + %alloc = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x64xf16> + %src_view = memref.subview %src[%c0, %c0] [%c64, %c64] [1, 1] + : memref<64x64xf16> to memref> + %dst_view = memref.subview %alloc[%c0, %c0] [%c64, %c64] [1, 1] + : memref<64x64xf16> to memref> + memref.copy %src_view, %dst_view {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + %lhs = bufferization.to_tensor %alloc restrict writable {ssbuffer.core_type = "VECTOR"} + : memref<64x64xf16> to tensor<64x64xf16> + %out = tensor.empty() {ssbuffer.core_type = "VECTOR"} : tensor<64x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs : tensor<64x64xf16>, tensor<64x64xf16>) + outs(%out : tensor<64x64xf32>) -> tensor<64x64xf32> + return %mm : tensor<64x64xf32> + } + + // CHECK-LABEL: func.func @top_level_arg_subviews_do_not_alias( + // CHECK: [[B_VIEW:%[A-Za-z0-9_]+]] = memref.subview %arg1 + // CHECK-NEXT: linalg.fill {ssbuffer.block_id = [[VECTOR:[0-9]+]] : i32, ssbuffer.core_type = "VECTOR"} ins({{.*}} : f16) outs([[B_VIEW]] + // CHECK: [[A_VIEW:%[A-Za-z0-9_]+]] = memref.subview %arg0 + // CHECK-NEXT: [[A_TENSOR:%[0-9]+]] = bufferization.to_tensor [[A_VIEW]] restrict writable {ssbuffer.block_id = [[CUBE:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK: linalg.matmul {{.*}}ssbuffer.block_id = [[CUBE]] : i32, ssbuffer.core_type = "CUBE" + func.func @top_level_arg_subviews_do_not_alias( + %lhs_gm: memref<64x64xf16>, + %rhs_gm: memref<64x64xf16>, + %rhs: tensor<64x64xf16>) -> tensor<64x64xf32> { + %c0 = arith.constant 0 : index + %zero_f16 = arith.constant 0.0 : f16 + %zero_f32 = arith.constant 0.0 : f32 + + %rhs_view = memref.subview %rhs_gm[%c0, %c0] [64, 64] [1, 1] + : memref<64x64xf16> to memref<64x64xf16, strided<[64, 1], offset: ?>> + linalg.fill ins(%zero_f16 : f16) outs(%rhs_view : memref<64x64xf16, strided<[64, 1], offset: ?>>) + + %lhs_view = memref.subview %lhs_gm[%c0, %c0] [64, 64] [1, 1] + : memref<64x64xf16> to memref<64x64xf16, strided<[64, 1], offset: ?>> + %lhs = bufferization.to_tensor %lhs_view restrict writable + : memref<64x64xf16, strided<[64, 1], offset: ?>> to tensor<64x64xf16> + %out = tensor.empty() : tensor<64x64xf32> + %init = linalg.fill ins(%zero_f32 : f32) outs(%out : tensor<64x64xf32>) -> tensor<64x64xf32> + %mm = linalg.matmul {input_precision = "ieee"} + ins(%lhs, %rhs : tensor<64x64xf16>, tensor<64x64xf16>) + outs(%init : tensor<64x64xf32>) -> tensor<64x64xf32> + return %mm : tensor<64x64xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_cube_block_markop.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_cube_block_markop.mlir new file mode 100644 index 0000000000..6bd096ce88 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_cube_block_markop.mlir @@ -0,0 +1,131 @@ +// RUN: triton-opt --plan-cube-block %s | FileCheck %s + +module { + // ============================================================================ + // 1. fuse_mark_with_alloc + // ============================================================================ + // CHECK-LABEL: func.func @fuse_mark_with_alloc + // CHECK: %[[ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = [[ID_ALLOC:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: annotation.mark %[[ALLOC]] {ssbuffer.block_id = [[ID_ALLOC]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: %[[T0:.*]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.block_id = [[ID_MAT:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: %[[MM:.*]] = linalg.matmul {ssbuffer.block_id = [[ID_MAT]] : i32, ssbuffer.core_type = "CUBE"} + func.func @fuse_mark_with_alloc(%arg0: tensor<4x4xf16>, %arg1: tensor<4x4xf32>) -> tensor<4x4xf32> { + %alloc = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<4x4xf16> + annotation.mark %alloc {ssbuffer.core_type = "CUBE"} : memref<4x4xf16> + %0 = bufferization.to_tensor %alloc restrict writable {ssbuffer.core_type = "CUBE"} : memref<4x4xf16> to tensor<4x4xf16> + %1 = linalg.matmul {ssbuffer.core_type = "CUBE"} ins(%0, %arg0 : tensor<4x4xf16>, tensor<4x4xf16>) outs(%arg1 : tensor<4x4xf32>) -> tensor<4x4xf32> + return %1 : tensor<4x4xf32> + } + + // ----- + + // ============================================================================ + // 2. fuse_mark_with_empty + // ============================================================================ + // CHECK-LABEL: func.func @fuse_mark_with_empty + // CHECK: %[[EMPTY:.*]] = tensor.empty() {ssbuffer.block_id = [[ID:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: annotation.mark %[[EMPTY]] {ssbuffer.block_id = [[ID]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: %[[MM:.*]] = linalg.matmul {ssbuffer.block_id = [[ID]] : i32, ssbuffer.core_type = "CUBE"} + func.func @fuse_mark_with_empty(%arg0: tensor<4x4xf16>, %arg1: tensor<4x4xf32>) -> tensor<4x4xf32> { + %0 = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<4x4xf16> + annotation.mark %0 {ssbuffer.core_type = "CUBE"} : tensor<4x4xf16> + %1 = linalg.matmul {ssbuffer.core_type = "CUBE"} ins(%0, %arg0 : tensor<4x4xf16>, tensor<4x4xf16>) outs(%arg1 : tensor<4x4xf32>) -> tensor<4x4xf32> + return %1 : tensor<4x4xf32> + } + + // ----- + + // ============================================================================ + // 3. mark_on_block_arg + // ============================================================================ + // CHECK-LABEL: func.func @mark_on_block_arg + // CHECK: annotation.mark %arg0 {ssbuffer.block_id = [[ID_MARK:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: %[[MM:.*]] = linalg.matmul {ssbuffer.block_id = [[ID_MAT:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + func.func @mark_on_block_arg(%arg0: tensor<4x4xf16>, %arg1: tensor<4x4xf32>) -> tensor<4x4xf32> { + annotation.mark %arg0 {ssbuffer.core_type = "CUBE"} : tensor<4x4xf16> + %0 = linalg.matmul {ssbuffer.core_type = "CUBE"} ins(%arg0, %arg0 : tensor<4x4xf16>, tensor<4x4xf16>) outs(%arg1 : tensor<4x4xf32>) -> tensor<4x4xf32> + return %0 : tensor<4x4xf32> + } + + // ----- + + // ============================================================================ + // 4. multiple_marks_chain + // ============================================================================ + // CHECK-LABEL: func.func @multiple_marks_chain + // CHECK: %[[ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = [[ID_ALLOC:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: annotation.mark %[[ALLOC]] {ssbuffer.block_id = [[ID_ALLOC]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: annotation.mark %[[ALLOC]] {ssbuffer.block_id = [[ID_ALLOC]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: %[[T0:.*]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.block_id = [[ID_MAT:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK-NEXT: %[[MM:.*]] = linalg.matmul {ssbuffer.block_id = [[ID_MAT]] : i32, ssbuffer.core_type = "CUBE"} + func.func @multiple_marks_chain(%arg0: tensor<4x4xf16>, %arg1: tensor<4x4xf32>) -> tensor<4x4xf32> { + %alloc = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<4x4xf16> + annotation.mark %alloc {ssbuffer.core_type = "CUBE"} : memref<4x4xf16> + annotation.mark %alloc {ssbuffer.core_type = "CUBE"} : memref<4x4xf16> + %0 = bufferization.to_tensor %alloc restrict writable {ssbuffer.core_type = "CUBE"} : memref<4x4xf16> to tensor<4x4xf16> + %1 = linalg.matmul {ssbuffer.core_type = "CUBE"} ins(%0, %arg0 : tensor<4x4xf16>, tensor<4x4xf16>) outs(%arg1 : tensor<4x4xf32>) -> tensor<4x4xf32> + return %1 : tensor<4x4xf32> + } + + // ----- + + // ============================================================================ + // 5. non_cube_mark_skipped + // ============================================================================ + // CHECK-LABEL: func.func @non_cube_mark_skipped + // CHECK: %[[ALLOC:.*]] = memref.alloc() {ssbuffer.core_type = "VECTOR"} + // CHECK-NEXT: annotation.mark %[[ALLOC]] {ssbuffer.core_type = "VECTOR"} : memref<4x4xf16> + // CHECK-NOT: ssbuffer.block_id + // CHECK: %[[T0:.*]] = bufferization.to_tensor %[[ALLOC]] restrict writable {ssbuffer.core_type = "VECTOR"} + // CHECK-NEXT: %[[MM:.*]] = linalg.matmul {ssbuffer.block_id = [[ID_MAT:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + func.func @non_cube_mark_skipped(%arg0: tensor<4x4xf16>, %arg1: tensor<4x4xf32>) -> tensor<4x4xf32> { + %alloc = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<4x4xf16> + annotation.mark %alloc {ssbuffer.core_type = "VECTOR"} : memref<4x4xf16> + %0 = bufferization.to_tensor %alloc restrict writable {ssbuffer.core_type = "VECTOR"} : memref<4x4xf16> to tensor<4x4xf16> + %1 = linalg.matmul {ssbuffer.core_type = "CUBE"} ins(%0, %arg0 : tensor<4x4xf16>, tensor<4x4xf16>) outs(%arg1 : tensor<4x4xf32>) -> tensor<4x4xf32> + return %1 : tensor<4x4xf32> + } + + // ----- + + // ============================================================================ + // 6. if_fill_split_cycle_prevention + // ============================================================================ + // This test case demonstrates the prevention of a dependency cycle that arises + // from the combination of annotation.mark (which has memory side-effects) and + // scf.if containing linalg.fill (which splits the block IDs). + // + // Dependency Chain: + // %alloc (Block ID: X) -> fill -> memref.copy (Block ID: Y) -> annotation.mark (originally Y) + // + // If fuseMarkOpToDef blindly fuses annotation.mark with its defining op (%alloc), + // annotation.mark would be forced into Block X. + // This creates a cyclic dependency: Block X -> Block Y -> Block X. + // The cycle detector must identify this and prevent the fusion, assigning + // annotation.mark to Block Y instead. + // + // Note: Although these blocks are partitioned separately here to maintain safety, + // a subsequent compute block optimization pass should ideally merge the blocks + // split by the if-fill control flow back together. + // + // CHECK-LABEL: func.func @if_fill_split_cycle_prevention + // CHECK: %[[ALLOC:.*]] = memref.alloc() {ssbuffer.block_id = [[ID_ALLOC:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK: scf.if + // CHECK: linalg.fill {ssbuffer.block_id = [[ID_FILL:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK: %[[SUBVIEW_SRC:.*]] = memref.subview + // CHECK: %[[SUBVIEW_DST:.*]] = memref.subview + // CHECK: memref.copy %[[SUBVIEW_SRC]], %[[SUBVIEW_DST]] {ssbuffer.block_id = [[ID_COPY:[0-9]+]] : i32, ssbuffer.core_type = "CUBE"} + // CHECK: annotation.mark %[[ALLOC]] {MayImplicitTransposeWithLastAxis, ssbuffer.block_id = [[ID_COPY]] : i32, ssbuffer.core_type = "CUBE"} + func.func @if_fill_split_cycle_prevention(%arg0: memref<32x16xf32>, %arg1: i1) { + %cst = arith.constant 0.0 : f32 + %alloc = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<32x16xf32> + scf.if %arg1 { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%alloc : memref<32x16xf32>) + } + %subview_src = memref.subview %arg0[0, 0] [32, 16] [1, 1] {ssbuffer.core_type = "CUBE"} : memref<32x16xf32> to memref<32x16xf32, strided<[16, 1]>> + %subview_dst = memref.subview %alloc[0, 0] [32, 16] [1, 1] {ssbuffer.core_type = "CUBE"} : memref<32x16xf32> to memref<32x16xf32, strided<[16, 1]>> + memref.copy %subview_src, %subview_dst {ssbuffer.core_type = "CUBE"} : memref<32x16xf32, strided<[16, 1]>> to memref<32x16xf32, strided<[16, 1]>> + annotation.mark %alloc {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<32x16xf32> + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_cube_block_subview_memory_deps_hang.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_cube_block_subview_memory_deps_hang.mlir new file mode 100644 index 0000000000..d2f1baef84 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_plan_cube_block_subview_memory_deps_hang.mlir @@ -0,0 +1,862 @@ +// RUN: triton-opt --plan-cube-block %s -o %t +// RUN: FileCheck %s < %t + +// Regression for the PlanCubeBlock non-progress hang exposed after +// MemoryEffectsTracker started tracking subview memory dependencies. +// +// The important shape mirrors the original failure: +// 1. VECTOR subview/copy ops touch sibling VECTOR allocs. +// 2. CUBE allocs are filled under non-cube scf.if ops. +// 3. CUBE to_tensor/annotation/transpose/matmul consume those CUBE allocs. +// +// This used to recurse through a ready but unassigned CUBE op during bypass, +// so Phase 2 later scheduled the same op and released its outgoing edges a +// second time. That drove some indegrees negative and eventually made the +// scheduler stop making progress. + +module { + // CHECK-LABEL: func.func @subview_memory_deps_hang_0 + // CHECK: annotation.mark %{{.*}} {MayImplicitTransposeWithLastAxis, ssbuffer.block_id + // CHECK: linalg.matmul + // CHECK-SAME: ssbuffer.block_id + func.func @subview_memory_deps_hang_0( + %lhs_gm: memref, + %rhs_gm: memref, + %acc_init: tensor<128x64xf32>, + %m_limit: index, + %n_limit: index, + %k_limit_i32: i32, + %lhs_base: index, + %rhs_base: index, + %lhs_stride_m: index, + %lhs_stride_k: index, + %rhs_stride_n: index, + %rhs_stride_k: index) -> tensor<128x64xf32> { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c64 = arith.constant 64 : index + %c64_i64 = arith.constant 64 : i64 + %c128 = arith.constant 128 : index + %c256 = arith.constant 256 : index + %c256_i32 = arith.constant 256 : i32 + %c256_i64 = arith.constant 256 : i64 + %zero_bf16 = arith.constant 0.000000e+00 : bf16 + %zero_f32 = arith.constant 0.000000e+00 : f32 + + %trip = arith.addi %k_limit_i32, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %ub = arith.divsi %trip, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %rhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %lhs_step = arith.muli %lhs_step_i, %lhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + %rhs_step = arith.muli %rhs_step_i, %rhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + + %result:3 = scf.for %iv = %c0_i32 to %ub step %c1_i32 + iter_args(%acc = %acc_init, %lhs_off = %lhs_base, %rhs_off = %rhs_base) + -> (tensor<128x64xf32>, index, index) : i32 { + %rhs_cast = memref.reinterpret_cast %rhs_gm to offset: [%rhs_off], sizes: [64, 256], strides: [%rhs_stride_n, %rhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<64x256xbf16, strided<[?, ?], offset: ?>> + %lhs_cast = memref.reinterpret_cast %lhs_gm to offset: [%lhs_off], sizes: [128, 256], strides: [%lhs_stride_m, %lhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<128x256xbf16, strided<[?, ?], offset: ?>> + + %kk = arith.muli %iv, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %remaining_i32 = arith.subi %k_limit_i32, %kk {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> + %lhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %remaining = arith.index_cast %remaining_i32 {ssbuffer.core_type = "VECTOR"} : i32 to index + %nonneg = arith.maxsi %remaining, %c0 {ssbuffer.core_type = "VECTOR"} : index + %k_size0 = arith.minsi %nonneg, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_size = arith.minsi %m_limit, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_size = arith.minsi %k_size0, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_pad = arith.cmpi slt, %m_size, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_pad0 = arith.cmpi slt, %k_size, %c256 {ssbuffer.core_type = "VECTOR"} : index + %lhs_pad = arith.ori %m_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %lhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%lhs_cube : memref<128x256xbf16>) + } {hivm.unlikely_condition} + %lhs_src = memref.subview %lhs_cast[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16, strided<[?, ?], offset: ?>> to memref> + %lhs_dst = memref.subview %lhs_vec[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> to memref> + memref.copy %lhs_src, %lhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %lhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %lhs = bufferization.to_tensor %lhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<128x256xbf16> to tensor<128x256xbf16> + annotation.mark %lhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<128x256xbf16> + + %rhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> + %rhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %n_end = arith.addi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_max = arith.maxsi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_min = arith.minsi %n_end, %n_max {ssbuffer.core_type = "VECTOR"} : index + %n_diff = arith.subi %n_min, %n_limit {ssbuffer.core_type = "VECTOR"} : index + %n_size = arith.minsi %n_diff, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_pad = arith.cmpi slt, %n_size, %c64 {ssbuffer.core_type = "VECTOR"} : index + %rhs_pad = arith.ori %n_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %rhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%rhs_cube : memref<64x256xbf16>) + } {hivm.unlikely_condition} + %rhs_src = memref.subview %rhs_cast[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16, strided<[?, ?], offset: ?>> to memref> + %rhs_dst = memref.subview %rhs_vec[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> to memref> + memref.copy %rhs_src, %rhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %rhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %rhs = bufferization.to_tensor %rhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<64x256xbf16> to tensor<64x256xbf16> + annotation.mark %rhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<64x256xbf16> + + %rhs_t_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<256x64xbf16> + %rhs_t = linalg.transpose ins(%rhs : tensor<64x256xbf16>) outs(%rhs_t_empty : tensor<256x64xbf16>) + permutation = [1, 0] {ssbuffer.core_type = "CUBE"} + %out_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %out_init = linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_f32 : f32) outs(%out_empty : tensor<128x64xf32>) -> tensor<128x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs_t : tensor<128x256xbf16>, tensor<256x64xbf16>) + outs(%out_init : tensor<128x64xf32>) -> tensor<128x64xf32> + %next = arith.addf %mm, %acc {ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %next_lhs = arith.addi %lhs_off, %lhs_step {ssbuffer.core_type = "VECTOR"} : index + %next_rhs = arith.addi %rhs_off, %rhs_step {ssbuffer.core_type = "VECTOR"} : index + scf.yield %next, %next_lhs, %next_rhs : tensor<128x64xf32>, index, index + } + + return %result#0 : tensor<128x64xf32> + } + func.func @subview_memory_deps_hang_1( + %lhs_gm: memref, + %rhs_gm: memref, + %acc_init: tensor<128x64xf32>, + %m_limit: index, + %n_limit: index, + %k_limit_i32: i32, + %lhs_base: index, + %rhs_base: index, + %lhs_stride_m: index, + %lhs_stride_k: index, + %rhs_stride_n: index, + %rhs_stride_k: index) -> tensor<128x64xf32> { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c64 = arith.constant 64 : index + %c64_i64 = arith.constant 64 : i64 + %c128 = arith.constant 128 : index + %c256 = arith.constant 256 : index + %c256_i32 = arith.constant 256 : i32 + %c256_i64 = arith.constant 256 : i64 + %zero_bf16 = arith.constant 0.000000e+00 : bf16 + %zero_f32 = arith.constant 0.000000e+00 : f32 + + %trip = arith.addi %k_limit_i32, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %ub = arith.divsi %trip, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %rhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %lhs_step = arith.muli %lhs_step_i, %lhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + %rhs_step = arith.muli %rhs_step_i, %rhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + + %result:3 = scf.for %iv = %c0_i32 to %ub step %c1_i32 + iter_args(%acc = %acc_init, %lhs_off = %lhs_base, %rhs_off = %rhs_base) + -> (tensor<128x64xf32>, index, index) : i32 { + %rhs_cast = memref.reinterpret_cast %rhs_gm to offset: [%rhs_off], sizes: [64, 256], strides: [%rhs_stride_n, %rhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<64x256xbf16, strided<[?, ?], offset: ?>> + %lhs_cast = memref.reinterpret_cast %lhs_gm to offset: [%lhs_off], sizes: [128, 256], strides: [%lhs_stride_m, %lhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<128x256xbf16, strided<[?, ?], offset: ?>> + + %kk = arith.muli %iv, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %remaining_i32 = arith.subi %k_limit_i32, %kk {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> + %lhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %remaining = arith.index_cast %remaining_i32 {ssbuffer.core_type = "VECTOR"} : i32 to index + %nonneg = arith.maxsi %remaining, %c0 {ssbuffer.core_type = "VECTOR"} : index + %k_size0 = arith.minsi %nonneg, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_size = arith.minsi %m_limit, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_size = arith.minsi %k_size0, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_pad = arith.cmpi slt, %m_size, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_pad0 = arith.cmpi slt, %k_size, %c256 {ssbuffer.core_type = "VECTOR"} : index + %lhs_pad = arith.ori %m_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %lhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%lhs_cube : memref<128x256xbf16>) + } {hivm.unlikely_condition} + %lhs_src = memref.subview %lhs_cast[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16, strided<[?, ?], offset: ?>> to memref> + %lhs_dst = memref.subview %lhs_vec[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> to memref> + memref.copy %lhs_src, %lhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %lhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %lhs = bufferization.to_tensor %lhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<128x256xbf16> to tensor<128x256xbf16> + annotation.mark %lhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<128x256xbf16> + + %rhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> + %rhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %n_end = arith.addi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_max = arith.maxsi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_min = arith.minsi %n_end, %n_max {ssbuffer.core_type = "VECTOR"} : index + %n_diff = arith.subi %n_min, %n_limit {ssbuffer.core_type = "VECTOR"} : index + %n_size = arith.minsi %n_diff, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_pad = arith.cmpi slt, %n_size, %c64 {ssbuffer.core_type = "VECTOR"} : index + %rhs_pad = arith.ori %n_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %rhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%rhs_cube : memref<64x256xbf16>) + } {hivm.unlikely_condition} + %rhs_src = memref.subview %rhs_cast[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16, strided<[?, ?], offset: ?>> to memref> + %rhs_dst = memref.subview %rhs_vec[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> to memref> + memref.copy %rhs_src, %rhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %rhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %rhs = bufferization.to_tensor %rhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<64x256xbf16> to tensor<64x256xbf16> + annotation.mark %rhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<64x256xbf16> + + %rhs_t_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<256x64xbf16> + %rhs_t = linalg.transpose ins(%rhs : tensor<64x256xbf16>) outs(%rhs_t_empty : tensor<256x64xbf16>) + permutation = [1, 0] {ssbuffer.core_type = "CUBE"} + %out_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %out_init = linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_f32 : f32) outs(%out_empty : tensor<128x64xf32>) -> tensor<128x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs_t : tensor<128x256xbf16>, tensor<256x64xbf16>) + outs(%out_init : tensor<128x64xf32>) -> tensor<128x64xf32> + %next = arith.addf %mm, %acc {ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %next_lhs = arith.addi %lhs_off, %lhs_step {ssbuffer.core_type = "VECTOR"} : index + %next_rhs = arith.addi %rhs_off, %rhs_step {ssbuffer.core_type = "VECTOR"} : index + scf.yield %next, %next_lhs, %next_rhs : tensor<128x64xf32>, index, index + } + + return %result#0 : tensor<128x64xf32> + } + func.func @subview_memory_deps_hang_2( + %lhs_gm: memref, + %rhs_gm: memref, + %acc_init: tensor<128x64xf32>, + %m_limit: index, + %n_limit: index, + %k_limit_i32: i32, + %lhs_base: index, + %rhs_base: index, + %lhs_stride_m: index, + %lhs_stride_k: index, + %rhs_stride_n: index, + %rhs_stride_k: index) -> tensor<128x64xf32> { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c64 = arith.constant 64 : index + %c64_i64 = arith.constant 64 : i64 + %c128 = arith.constant 128 : index + %c256 = arith.constant 256 : index + %c256_i32 = arith.constant 256 : i32 + %c256_i64 = arith.constant 256 : i64 + %zero_bf16 = arith.constant 0.000000e+00 : bf16 + %zero_f32 = arith.constant 0.000000e+00 : f32 + + %trip = arith.addi %k_limit_i32, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %ub = arith.divsi %trip, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %rhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %lhs_step = arith.muli %lhs_step_i, %lhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + %rhs_step = arith.muli %rhs_step_i, %rhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + + %result:3 = scf.for %iv = %c0_i32 to %ub step %c1_i32 + iter_args(%acc = %acc_init, %lhs_off = %lhs_base, %rhs_off = %rhs_base) + -> (tensor<128x64xf32>, index, index) : i32 { + %rhs_cast = memref.reinterpret_cast %rhs_gm to offset: [%rhs_off], sizes: [64, 256], strides: [%rhs_stride_n, %rhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<64x256xbf16, strided<[?, ?], offset: ?>> + %lhs_cast = memref.reinterpret_cast %lhs_gm to offset: [%lhs_off], sizes: [128, 256], strides: [%lhs_stride_m, %lhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<128x256xbf16, strided<[?, ?], offset: ?>> + + %kk = arith.muli %iv, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %remaining_i32 = arith.subi %k_limit_i32, %kk {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> + %lhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %remaining = arith.index_cast %remaining_i32 {ssbuffer.core_type = "VECTOR"} : i32 to index + %nonneg = arith.maxsi %remaining, %c0 {ssbuffer.core_type = "VECTOR"} : index + %k_size0 = arith.minsi %nonneg, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_size = arith.minsi %m_limit, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_size = arith.minsi %k_size0, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_pad = arith.cmpi slt, %m_size, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_pad0 = arith.cmpi slt, %k_size, %c256 {ssbuffer.core_type = "VECTOR"} : index + %lhs_pad = arith.ori %m_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %lhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%lhs_cube : memref<128x256xbf16>) + } {hivm.unlikely_condition} + %lhs_src = memref.subview %lhs_cast[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16, strided<[?, ?], offset: ?>> to memref> + %lhs_dst = memref.subview %lhs_vec[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> to memref> + memref.copy %lhs_src, %lhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %lhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %lhs = bufferization.to_tensor %lhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<128x256xbf16> to tensor<128x256xbf16> + annotation.mark %lhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<128x256xbf16> + + %rhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> + %rhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %n_end = arith.addi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_max = arith.maxsi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_min = arith.minsi %n_end, %n_max {ssbuffer.core_type = "VECTOR"} : index + %n_diff = arith.subi %n_min, %n_limit {ssbuffer.core_type = "VECTOR"} : index + %n_size = arith.minsi %n_diff, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_pad = arith.cmpi slt, %n_size, %c64 {ssbuffer.core_type = "VECTOR"} : index + %rhs_pad = arith.ori %n_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %rhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%rhs_cube : memref<64x256xbf16>) + } {hivm.unlikely_condition} + %rhs_src = memref.subview %rhs_cast[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16, strided<[?, ?], offset: ?>> to memref> + %rhs_dst = memref.subview %rhs_vec[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> to memref> + memref.copy %rhs_src, %rhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %rhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %rhs = bufferization.to_tensor %rhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<64x256xbf16> to tensor<64x256xbf16> + annotation.mark %rhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<64x256xbf16> + + %rhs_t_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<256x64xbf16> + %rhs_t = linalg.transpose ins(%rhs : tensor<64x256xbf16>) outs(%rhs_t_empty : tensor<256x64xbf16>) + permutation = [1, 0] {ssbuffer.core_type = "CUBE"} + %out_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %out_init = linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_f32 : f32) outs(%out_empty : tensor<128x64xf32>) -> tensor<128x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs_t : tensor<128x256xbf16>, tensor<256x64xbf16>) + outs(%out_init : tensor<128x64xf32>) -> tensor<128x64xf32> + %next = arith.addf %mm, %acc {ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %next_lhs = arith.addi %lhs_off, %lhs_step {ssbuffer.core_type = "VECTOR"} : index + %next_rhs = arith.addi %rhs_off, %rhs_step {ssbuffer.core_type = "VECTOR"} : index + scf.yield %next, %next_lhs, %next_rhs : tensor<128x64xf32>, index, index + } + + return %result#0 : tensor<128x64xf32> + } + func.func @subview_memory_deps_hang_3( + %lhs_gm: memref, + %rhs_gm: memref, + %acc_init: tensor<128x64xf32>, + %m_limit: index, + %n_limit: index, + %k_limit_i32: i32, + %lhs_base: index, + %rhs_base: index, + %lhs_stride_m: index, + %lhs_stride_k: index, + %rhs_stride_n: index, + %rhs_stride_k: index) -> tensor<128x64xf32> { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c64 = arith.constant 64 : index + %c64_i64 = arith.constant 64 : i64 + %c128 = arith.constant 128 : index + %c256 = arith.constant 256 : index + %c256_i32 = arith.constant 256 : i32 + %c256_i64 = arith.constant 256 : i64 + %zero_bf16 = arith.constant 0.000000e+00 : bf16 + %zero_f32 = arith.constant 0.000000e+00 : f32 + + %trip = arith.addi %k_limit_i32, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %ub = arith.divsi %trip, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %rhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %lhs_step = arith.muli %lhs_step_i, %lhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + %rhs_step = arith.muli %rhs_step_i, %rhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + + %result:3 = scf.for %iv = %c0_i32 to %ub step %c1_i32 + iter_args(%acc = %acc_init, %lhs_off = %lhs_base, %rhs_off = %rhs_base) + -> (tensor<128x64xf32>, index, index) : i32 { + %rhs_cast = memref.reinterpret_cast %rhs_gm to offset: [%rhs_off], sizes: [64, 256], strides: [%rhs_stride_n, %rhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<64x256xbf16, strided<[?, ?], offset: ?>> + %lhs_cast = memref.reinterpret_cast %lhs_gm to offset: [%lhs_off], sizes: [128, 256], strides: [%lhs_stride_m, %lhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<128x256xbf16, strided<[?, ?], offset: ?>> + + %kk = arith.muli %iv, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %remaining_i32 = arith.subi %k_limit_i32, %kk {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> + %lhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %remaining = arith.index_cast %remaining_i32 {ssbuffer.core_type = "VECTOR"} : i32 to index + %nonneg = arith.maxsi %remaining, %c0 {ssbuffer.core_type = "VECTOR"} : index + %k_size0 = arith.minsi %nonneg, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_size = arith.minsi %m_limit, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_size = arith.minsi %k_size0, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_pad = arith.cmpi slt, %m_size, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_pad0 = arith.cmpi slt, %k_size, %c256 {ssbuffer.core_type = "VECTOR"} : index + %lhs_pad = arith.ori %m_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %lhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%lhs_cube : memref<128x256xbf16>) + } {hivm.unlikely_condition} + %lhs_src = memref.subview %lhs_cast[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16, strided<[?, ?], offset: ?>> to memref> + %lhs_dst = memref.subview %lhs_vec[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> to memref> + memref.copy %lhs_src, %lhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %lhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %lhs = bufferization.to_tensor %lhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<128x256xbf16> to tensor<128x256xbf16> + annotation.mark %lhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<128x256xbf16> + + %rhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> + %rhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %n_end = arith.addi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_max = arith.maxsi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_min = arith.minsi %n_end, %n_max {ssbuffer.core_type = "VECTOR"} : index + %n_diff = arith.subi %n_min, %n_limit {ssbuffer.core_type = "VECTOR"} : index + %n_size = arith.minsi %n_diff, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_pad = arith.cmpi slt, %n_size, %c64 {ssbuffer.core_type = "VECTOR"} : index + %rhs_pad = arith.ori %n_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %rhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%rhs_cube : memref<64x256xbf16>) + } {hivm.unlikely_condition} + %rhs_src = memref.subview %rhs_cast[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16, strided<[?, ?], offset: ?>> to memref> + %rhs_dst = memref.subview %rhs_vec[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> to memref> + memref.copy %rhs_src, %rhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %rhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %rhs = bufferization.to_tensor %rhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<64x256xbf16> to tensor<64x256xbf16> + annotation.mark %rhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<64x256xbf16> + + %rhs_t_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<256x64xbf16> + %rhs_t = linalg.transpose ins(%rhs : tensor<64x256xbf16>) outs(%rhs_t_empty : tensor<256x64xbf16>) + permutation = [1, 0] {ssbuffer.core_type = "CUBE"} + %out_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %out_init = linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_f32 : f32) outs(%out_empty : tensor<128x64xf32>) -> tensor<128x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs_t : tensor<128x256xbf16>, tensor<256x64xbf16>) + outs(%out_init : tensor<128x64xf32>) -> tensor<128x64xf32> + %next = arith.addf %mm, %acc {ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %next_lhs = arith.addi %lhs_off, %lhs_step {ssbuffer.core_type = "VECTOR"} : index + %next_rhs = arith.addi %rhs_off, %rhs_step {ssbuffer.core_type = "VECTOR"} : index + scf.yield %next, %next_lhs, %next_rhs : tensor<128x64xf32>, index, index + } + + return %result#0 : tensor<128x64xf32> + } + func.func @subview_memory_deps_hang_4( + %lhs_gm: memref, + %rhs_gm: memref, + %acc_init: tensor<128x64xf32>, + %m_limit: index, + %n_limit: index, + %k_limit_i32: i32, + %lhs_base: index, + %rhs_base: index, + %lhs_stride_m: index, + %lhs_stride_k: index, + %rhs_stride_n: index, + %rhs_stride_k: index) -> tensor<128x64xf32> { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c64 = arith.constant 64 : index + %c64_i64 = arith.constant 64 : i64 + %c128 = arith.constant 128 : index + %c256 = arith.constant 256 : index + %c256_i32 = arith.constant 256 : i32 + %c256_i64 = arith.constant 256 : i64 + %zero_bf16 = arith.constant 0.000000e+00 : bf16 + %zero_f32 = arith.constant 0.000000e+00 : f32 + + %trip = arith.addi %k_limit_i32, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %ub = arith.divsi %trip, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %rhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %lhs_step = arith.muli %lhs_step_i, %lhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + %rhs_step = arith.muli %rhs_step_i, %rhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + + %result:3 = scf.for %iv = %c0_i32 to %ub step %c1_i32 + iter_args(%acc = %acc_init, %lhs_off = %lhs_base, %rhs_off = %rhs_base) + -> (tensor<128x64xf32>, index, index) : i32 { + %rhs_cast = memref.reinterpret_cast %rhs_gm to offset: [%rhs_off], sizes: [64, 256], strides: [%rhs_stride_n, %rhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<64x256xbf16, strided<[?, ?], offset: ?>> + %lhs_cast = memref.reinterpret_cast %lhs_gm to offset: [%lhs_off], sizes: [128, 256], strides: [%lhs_stride_m, %lhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<128x256xbf16, strided<[?, ?], offset: ?>> + + %kk = arith.muli %iv, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %remaining_i32 = arith.subi %k_limit_i32, %kk {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> + %lhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %remaining = arith.index_cast %remaining_i32 {ssbuffer.core_type = "VECTOR"} : i32 to index + %nonneg = arith.maxsi %remaining, %c0 {ssbuffer.core_type = "VECTOR"} : index + %k_size0 = arith.minsi %nonneg, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_size = arith.minsi %m_limit, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_size = arith.minsi %k_size0, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_pad = arith.cmpi slt, %m_size, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_pad0 = arith.cmpi slt, %k_size, %c256 {ssbuffer.core_type = "VECTOR"} : index + %lhs_pad = arith.ori %m_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %lhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%lhs_cube : memref<128x256xbf16>) + } {hivm.unlikely_condition} + %lhs_src = memref.subview %lhs_cast[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16, strided<[?, ?], offset: ?>> to memref> + %lhs_dst = memref.subview %lhs_vec[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> to memref> + memref.copy %lhs_src, %lhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %lhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %lhs = bufferization.to_tensor %lhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<128x256xbf16> to tensor<128x256xbf16> + annotation.mark %lhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<128x256xbf16> + + %rhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> + %rhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %n_end = arith.addi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_max = arith.maxsi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_min = arith.minsi %n_end, %n_max {ssbuffer.core_type = "VECTOR"} : index + %n_diff = arith.subi %n_min, %n_limit {ssbuffer.core_type = "VECTOR"} : index + %n_size = arith.minsi %n_diff, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_pad = arith.cmpi slt, %n_size, %c64 {ssbuffer.core_type = "VECTOR"} : index + %rhs_pad = arith.ori %n_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %rhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%rhs_cube : memref<64x256xbf16>) + } {hivm.unlikely_condition} + %rhs_src = memref.subview %rhs_cast[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16, strided<[?, ?], offset: ?>> to memref> + %rhs_dst = memref.subview %rhs_vec[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> to memref> + memref.copy %rhs_src, %rhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %rhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %rhs = bufferization.to_tensor %rhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<64x256xbf16> to tensor<64x256xbf16> + annotation.mark %rhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<64x256xbf16> + + %rhs_t_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<256x64xbf16> + %rhs_t = linalg.transpose ins(%rhs : tensor<64x256xbf16>) outs(%rhs_t_empty : tensor<256x64xbf16>) + permutation = [1, 0] {ssbuffer.core_type = "CUBE"} + %out_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %out_init = linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_f32 : f32) outs(%out_empty : tensor<128x64xf32>) -> tensor<128x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs_t : tensor<128x256xbf16>, tensor<256x64xbf16>) + outs(%out_init : tensor<128x64xf32>) -> tensor<128x64xf32> + %next = arith.addf %mm, %acc {ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %next_lhs = arith.addi %lhs_off, %lhs_step {ssbuffer.core_type = "VECTOR"} : index + %next_rhs = arith.addi %rhs_off, %rhs_step {ssbuffer.core_type = "VECTOR"} : index + scf.yield %next, %next_lhs, %next_rhs : tensor<128x64xf32>, index, index + } + + return %result#0 : tensor<128x64xf32> + } + func.func @subview_memory_deps_hang_5( + %lhs_gm: memref, + %rhs_gm: memref, + %acc_init: tensor<128x64xf32>, + %m_limit: index, + %n_limit: index, + %k_limit_i32: i32, + %lhs_base: index, + %rhs_base: index, + %lhs_stride_m: index, + %lhs_stride_k: index, + %rhs_stride_n: index, + %rhs_stride_k: index) -> tensor<128x64xf32> { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c64 = arith.constant 64 : index + %c64_i64 = arith.constant 64 : i64 + %c128 = arith.constant 128 : index + %c256 = arith.constant 256 : index + %c256_i32 = arith.constant 256 : i32 + %c256_i64 = arith.constant 256 : i64 + %zero_bf16 = arith.constant 0.000000e+00 : bf16 + %zero_f32 = arith.constant 0.000000e+00 : f32 + + %trip = arith.addi %k_limit_i32, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %ub = arith.divsi %trip, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %rhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %lhs_step = arith.muli %lhs_step_i, %lhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + %rhs_step = arith.muli %rhs_step_i, %rhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + + %result:3 = scf.for %iv = %c0_i32 to %ub step %c1_i32 + iter_args(%acc = %acc_init, %lhs_off = %lhs_base, %rhs_off = %rhs_base) + -> (tensor<128x64xf32>, index, index) : i32 { + %rhs_cast = memref.reinterpret_cast %rhs_gm to offset: [%rhs_off], sizes: [64, 256], strides: [%rhs_stride_n, %rhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<64x256xbf16, strided<[?, ?], offset: ?>> + %lhs_cast = memref.reinterpret_cast %lhs_gm to offset: [%lhs_off], sizes: [128, 256], strides: [%lhs_stride_m, %lhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<128x256xbf16, strided<[?, ?], offset: ?>> + + %kk = arith.muli %iv, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %remaining_i32 = arith.subi %k_limit_i32, %kk {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> + %lhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %remaining = arith.index_cast %remaining_i32 {ssbuffer.core_type = "VECTOR"} : i32 to index + %nonneg = arith.maxsi %remaining, %c0 {ssbuffer.core_type = "VECTOR"} : index + %k_size0 = arith.minsi %nonneg, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_size = arith.minsi %m_limit, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_size = arith.minsi %k_size0, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_pad = arith.cmpi slt, %m_size, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_pad0 = arith.cmpi slt, %k_size, %c256 {ssbuffer.core_type = "VECTOR"} : index + %lhs_pad = arith.ori %m_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %lhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%lhs_cube : memref<128x256xbf16>) + } {hivm.unlikely_condition} + %lhs_src = memref.subview %lhs_cast[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16, strided<[?, ?], offset: ?>> to memref> + %lhs_dst = memref.subview %lhs_vec[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> to memref> + memref.copy %lhs_src, %lhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %lhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %lhs = bufferization.to_tensor %lhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<128x256xbf16> to tensor<128x256xbf16> + annotation.mark %lhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<128x256xbf16> + + %rhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> + %rhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %n_end = arith.addi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_max = arith.maxsi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_min = arith.minsi %n_end, %n_max {ssbuffer.core_type = "VECTOR"} : index + %n_diff = arith.subi %n_min, %n_limit {ssbuffer.core_type = "VECTOR"} : index + %n_size = arith.minsi %n_diff, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_pad = arith.cmpi slt, %n_size, %c64 {ssbuffer.core_type = "VECTOR"} : index + %rhs_pad = arith.ori %n_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %rhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%rhs_cube : memref<64x256xbf16>) + } {hivm.unlikely_condition} + %rhs_src = memref.subview %rhs_cast[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16, strided<[?, ?], offset: ?>> to memref> + %rhs_dst = memref.subview %rhs_vec[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> to memref> + memref.copy %rhs_src, %rhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %rhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %rhs = bufferization.to_tensor %rhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<64x256xbf16> to tensor<64x256xbf16> + annotation.mark %rhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<64x256xbf16> + + %rhs_t_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<256x64xbf16> + %rhs_t = linalg.transpose ins(%rhs : tensor<64x256xbf16>) outs(%rhs_t_empty : tensor<256x64xbf16>) + permutation = [1, 0] {ssbuffer.core_type = "CUBE"} + %out_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %out_init = linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_f32 : f32) outs(%out_empty : tensor<128x64xf32>) -> tensor<128x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs_t : tensor<128x256xbf16>, tensor<256x64xbf16>) + outs(%out_init : tensor<128x64xf32>) -> tensor<128x64xf32> + %next = arith.addf %mm, %acc {ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %next_lhs = arith.addi %lhs_off, %lhs_step {ssbuffer.core_type = "VECTOR"} : index + %next_rhs = arith.addi %rhs_off, %rhs_step {ssbuffer.core_type = "VECTOR"} : index + scf.yield %next, %next_lhs, %next_rhs : tensor<128x64xf32>, index, index + } + + return %result#0 : tensor<128x64xf32> + } + func.func @subview_memory_deps_hang_6( + %lhs_gm: memref, + %rhs_gm: memref, + %acc_init: tensor<128x64xf32>, + %m_limit: index, + %n_limit: index, + %k_limit_i32: i32, + %lhs_base: index, + %rhs_base: index, + %lhs_stride_m: index, + %lhs_stride_k: index, + %rhs_stride_n: index, + %rhs_stride_k: index) -> tensor<128x64xf32> { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c64 = arith.constant 64 : index + %c64_i64 = arith.constant 64 : i64 + %c128 = arith.constant 128 : index + %c256 = arith.constant 256 : index + %c256_i32 = arith.constant 256 : i32 + %c256_i64 = arith.constant 256 : i64 + %zero_bf16 = arith.constant 0.000000e+00 : bf16 + %zero_f32 = arith.constant 0.000000e+00 : f32 + + %trip = arith.addi %k_limit_i32, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %ub = arith.divsi %trip, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %rhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %lhs_step = arith.muli %lhs_step_i, %lhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + %rhs_step = arith.muli %rhs_step_i, %rhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + + %result:3 = scf.for %iv = %c0_i32 to %ub step %c1_i32 + iter_args(%acc = %acc_init, %lhs_off = %lhs_base, %rhs_off = %rhs_base) + -> (tensor<128x64xf32>, index, index) : i32 { + %rhs_cast = memref.reinterpret_cast %rhs_gm to offset: [%rhs_off], sizes: [64, 256], strides: [%rhs_stride_n, %rhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<64x256xbf16, strided<[?, ?], offset: ?>> + %lhs_cast = memref.reinterpret_cast %lhs_gm to offset: [%lhs_off], sizes: [128, 256], strides: [%lhs_stride_m, %lhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<128x256xbf16, strided<[?, ?], offset: ?>> + + %kk = arith.muli %iv, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %remaining_i32 = arith.subi %k_limit_i32, %kk {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> + %lhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %remaining = arith.index_cast %remaining_i32 {ssbuffer.core_type = "VECTOR"} : i32 to index + %nonneg = arith.maxsi %remaining, %c0 {ssbuffer.core_type = "VECTOR"} : index + %k_size0 = arith.minsi %nonneg, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_size = arith.minsi %m_limit, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_size = arith.minsi %k_size0, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_pad = arith.cmpi slt, %m_size, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_pad0 = arith.cmpi slt, %k_size, %c256 {ssbuffer.core_type = "VECTOR"} : index + %lhs_pad = arith.ori %m_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %lhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%lhs_cube : memref<128x256xbf16>) + } {hivm.unlikely_condition} + %lhs_src = memref.subview %lhs_cast[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16, strided<[?, ?], offset: ?>> to memref> + %lhs_dst = memref.subview %lhs_vec[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> to memref> + memref.copy %lhs_src, %lhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %lhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %lhs = bufferization.to_tensor %lhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<128x256xbf16> to tensor<128x256xbf16> + annotation.mark %lhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<128x256xbf16> + + %rhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> + %rhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %n_end = arith.addi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_max = arith.maxsi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_min = arith.minsi %n_end, %n_max {ssbuffer.core_type = "VECTOR"} : index + %n_diff = arith.subi %n_min, %n_limit {ssbuffer.core_type = "VECTOR"} : index + %n_size = arith.minsi %n_diff, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_pad = arith.cmpi slt, %n_size, %c64 {ssbuffer.core_type = "VECTOR"} : index + %rhs_pad = arith.ori %n_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %rhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%rhs_cube : memref<64x256xbf16>) + } {hivm.unlikely_condition} + %rhs_src = memref.subview %rhs_cast[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16, strided<[?, ?], offset: ?>> to memref> + %rhs_dst = memref.subview %rhs_vec[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> to memref> + memref.copy %rhs_src, %rhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %rhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %rhs = bufferization.to_tensor %rhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<64x256xbf16> to tensor<64x256xbf16> + annotation.mark %rhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<64x256xbf16> + + %rhs_t_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<256x64xbf16> + %rhs_t = linalg.transpose ins(%rhs : tensor<64x256xbf16>) outs(%rhs_t_empty : tensor<256x64xbf16>) + permutation = [1, 0] {ssbuffer.core_type = "CUBE"} + %out_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %out_init = linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_f32 : f32) outs(%out_empty : tensor<128x64xf32>) -> tensor<128x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs_t : tensor<128x256xbf16>, tensor<256x64xbf16>) + outs(%out_init : tensor<128x64xf32>) -> tensor<128x64xf32> + %next = arith.addf %mm, %acc {ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %next_lhs = arith.addi %lhs_off, %lhs_step {ssbuffer.core_type = "VECTOR"} : index + %next_rhs = arith.addi %rhs_off, %rhs_step {ssbuffer.core_type = "VECTOR"} : index + scf.yield %next, %next_lhs, %next_rhs : tensor<128x64xf32>, index, index + } + + return %result#0 : tensor<128x64xf32> + } + func.func @subview_memory_deps_hang_7( + %lhs_gm: memref, + %rhs_gm: memref, + %acc_init: tensor<128x64xf32>, + %m_limit: index, + %n_limit: index, + %k_limit_i32: i32, + %lhs_base: index, + %rhs_base: index, + %lhs_stride_m: index, + %lhs_stride_k: index, + %rhs_stride_n: index, + %rhs_stride_k: index) -> tensor<128x64xf32> { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c64 = arith.constant 64 : index + %c64_i64 = arith.constant 64 : i64 + %c128 = arith.constant 128 : index + %c256 = arith.constant 256 : index + %c256_i32 = arith.constant 256 : i32 + %c256_i64 = arith.constant 256 : i64 + %zero_bf16 = arith.constant 0.000000e+00 : bf16 + %zero_f32 = arith.constant 0.000000e+00 : f32 + + %trip = arith.addi %k_limit_i32, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %ub = arith.divsi %trip, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %rhs_step_i = arith.index_cast %c256_i64 {ssbuffer.core_type = "VECTOR"} : i64 to index + %lhs_step = arith.muli %lhs_step_i, %lhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + %rhs_step = arith.muli %rhs_step_i, %rhs_stride_k {ssbuffer.core_type = "VECTOR"} : index + + %result:3 = scf.for %iv = %c0_i32 to %ub step %c1_i32 + iter_args(%acc = %acc_init, %lhs_off = %lhs_base, %rhs_off = %rhs_base) + -> (tensor<128x64xf32>, index, index) : i32 { + %rhs_cast = memref.reinterpret_cast %rhs_gm to offset: [%rhs_off], sizes: [64, 256], strides: [%rhs_stride_n, %rhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<64x256xbf16, strided<[?, ?], offset: ?>> + %lhs_cast = memref.reinterpret_cast %lhs_gm to offset: [%lhs_off], sizes: [128, 256], strides: [%lhs_stride_m, %lhs_stride_k] + {ssbuffer.core_type = "VECTOR"} : memref to memref<128x256xbf16, strided<[?, ?], offset: ?>> + + %kk = arith.muli %iv, %c256_i32 {ssbuffer.core_type = "VECTOR"} : i32 + %remaining_i32 = arith.subi %k_limit_i32, %kk {ssbuffer.core_type = "VECTOR"} : i32 + %lhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> + %lhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %remaining = arith.index_cast %remaining_i32 {ssbuffer.core_type = "VECTOR"} : i32 to index + %nonneg = arith.maxsi %remaining, %c0 {ssbuffer.core_type = "VECTOR"} : index + %k_size0 = arith.minsi %nonneg, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_size = arith.minsi %m_limit, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_size = arith.minsi %k_size0, %c256 {ssbuffer.core_type = "VECTOR"} : index + %m_pad = arith.cmpi slt, %m_size, %c128 {ssbuffer.core_type = "VECTOR"} : index + %k_pad0 = arith.cmpi slt, %k_size, %c256 {ssbuffer.core_type = "VECTOR"} : index + %lhs_pad = arith.ori %m_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %lhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%lhs_cube : memref<128x256xbf16>) + } {hivm.unlikely_condition} + %lhs_src = memref.subview %lhs_cast[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16, strided<[?, ?], offset: ?>> to memref> + %lhs_dst = memref.subview %lhs_vec[0, 0] [%m_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<128x256xbf16> to memref> + memref.copy %lhs_src, %lhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %lhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<128x256xbf16> + %lhs = bufferization.to_tensor %lhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<128x256xbf16> to tensor<128x256xbf16> + annotation.mark %lhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<128x256xbf16> + + %rhs_vec = memref.alloc() {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> + %rhs_cube = memref.alloc() {ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %n_end = arith.addi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_max = arith.maxsi %n_limit, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_min = arith.minsi %n_end, %n_max {ssbuffer.core_type = "VECTOR"} : index + %n_diff = arith.subi %n_min, %n_limit {ssbuffer.core_type = "VECTOR"} : index + %n_size = arith.minsi %n_diff, %c64 {ssbuffer.core_type = "VECTOR"} : index + %n_pad = arith.cmpi slt, %n_size, %c64 {ssbuffer.core_type = "VECTOR"} : index + %rhs_pad = arith.ori %n_pad, %k_pad0 {ssbuffer.core_type = "VECTOR"} : i1 + scf.if %rhs_pad { + linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_bf16 : bf16) outs(%rhs_cube : memref<64x256xbf16>) + } {hivm.unlikely_condition} + %rhs_src = memref.subview %rhs_cast[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16, strided<[?, ?], offset: ?>> to memref> + %rhs_dst = memref.subview %rhs_vec[0, 0] [%n_size, %k_size] [1, 1] + {ssbuffer.core_type = "VECTOR"} : memref<64x256xbf16> to memref> + memref.copy %rhs_src, %rhs_dst {ssbuffer.core_type = "VECTOR"} + : memref> to memref> + annotation.mark %rhs_cube {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : memref<64x256xbf16> + %rhs = bufferization.to_tensor %rhs_cube restrict writable {ssbuffer.core_type = "CUBE"} + : memref<64x256xbf16> to tensor<64x256xbf16> + annotation.mark %rhs {MayImplicitTransposeWithLastAxis, ssbuffer.core_type = "CUBE"} : tensor<64x256xbf16> + + %rhs_t_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<256x64xbf16> + %rhs_t = linalg.transpose ins(%rhs : tensor<64x256xbf16>) outs(%rhs_t_empty : tensor<256x64xbf16>) + permutation = [1, 0] {ssbuffer.core_type = "CUBE"} + %out_empty = tensor.empty() {ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %out_init = linalg.fill {ssbuffer.core_type = "CUBE"} ins(%zero_f32 : f32) outs(%out_empty : tensor<128x64xf32>) -> tensor<128x64xf32> + %mm = linalg.matmul {input_precision = "ieee", ssbuffer.core_type = "CUBE"} + ins(%lhs, %rhs_t : tensor<128x256xbf16>, tensor<256x64xbf16>) + outs(%out_init : tensor<128x64xf32>) -> tensor<128x64xf32> + %next = arith.addf %mm, %acc {ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %next_lhs = arith.addi %lhs_off, %lhs_step {ssbuffer.core_type = "VECTOR"} : index + %next_rhs = arith.addi %rhs_off, %rhs_step {ssbuffer.core_type = "VECTOR"} : index + scf.yield %next, %next_lhs, %next_rhs : tensor<128x64xf32>, index, index + } + + return %result#0 : tensor<128x64xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_reorder_ops_by_block_id.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_reorder_ops_by_block_id.mlir new file mode 100644 index 0000000000..2c8961f71e --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_reorder_ops_by_block_id.mlir @@ -0,0 +1,102 @@ +// RUN: triton-opt --reorder-ops-by-block-id %s | FileCheck %s + +// Tests for orderInOneCBlock in ReorderOpsByBlockIdPass. +// +// The pass reorders operations within a compute block using a Kahn's +// topological sort with a tie-breaker that sinks store-like ops +// (materialize_in_destination / hivm.hir.store) to the end of VECTOR_ONLY +// blocks, while respecting SSA and memory dependencies. +// CUBE blocks (any block containing a non-VECTOR_ONLY op) preserve the +// original program order. + +module { + // ============================================================================ + // 1. cube_block_store_not_last + // + // A CUBE block where bufferization.materialize_in_destination is NOT the + // last op. In CUBE blocks, orderInOneCBlock preserves the original program + // order (store-like ops are NOT sunk). + // + // Program order: materialize, matmul + // Expected: materialize, matmul (unchanged) + // ============================================================================ + // CHECK-LABEL: func.func @cube_block_store_not_last( + // CHECK: bufferization.materialize_in_destination {{.*}} ssbuffer.core_type = "CUBE" + // CHECK: linalg.matmul {{.*}} ssbuffer.core_type = "CUBE" + func.func @cube_block_store_not_last( + %arg0: memref<64x64xf16>, + %in1: tensor<64x64xf16>, + %A: tensor<64x64xf16>, + %B: tensor<64x64xf16>, + %init: tensor<64x64xf16>) { + bufferization.materialize_in_destination %in1 in writable %arg0 {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : (tensor<64x64xf16>, memref<64x64xf16>) -> () + %mm = linalg.matmul {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + ins(%A, %B : tensor<64x64xf16>, tensor<64x64xf16>) + outs(%init : tensor<64x64xf16>) -> tensor<64x64xf16> + return + } + + // ============================================================================ + // 2. vector_materialize_in_middle + // + // A VECTOR_ONLY block where bufferization.materialize_in_destination sits + // between two independent VECTOR compute ops. After reorder, the store + // sinks past the independent compute op to the end. + // + // Program order: addf, materialize(addf), mulf + // Expected: addf, mulf, materialize + // ============================================================================ + // CHECK-LABEL: func.func @vector_materialize_in_middle( + // CHECK: arith.addf {{.*}} ssbuffer.core_type = "VECTOR" + // CHECK: arith.mulf {{.*}} ssbuffer.core_type = "VECTOR" + // CHECK: bufferization.materialize_in_destination {{.*}} ssbuffer.core_type = "VECTOR" + func.func @vector_materialize_in_middle( + %arg0: memref<64xf32>, + %in1: tensor<64xf32>, + %in2: tensor<64xf32>) -> (tensor<64xf32>, tensor<64xf32>) { + %a = arith.addf %in1, %in1 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + bufferization.materialize_in_destination %a in writable %arg0 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64xf32>, memref<64xf32>) -> () + %b = arith.mulf %in2, %in2 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + return %a, %b : tensor<64xf32>, tensor<64xf32> + } + + // ============================================================================ + // 3. vector_store_inside_scf_if_sinks + // + // bufferization.materialize_in_destination is wrapped inside an scf.if. + // The scf.if carries ssbuffer.block_id and ssbuffer.core_type so it is in + // the same VECTOR_ONLY compute block. The scf.if returns a value used by + // another VECTOR compute op. Since the scf.if contains a store-like op, + // isStoreLikeWithRegion(scf.if) == true, so the scf.if itself is treated + // as store-like and sinks past independent VECTOR ops, while respecting + // the SSA dependency from the consuming compute op. + // + // Program order: scf.if(+store), independent_mulf, use_addf(if_result) + // Expected: independent_mulf, scf.if, use_addf + // (scf.if sinks past mulf but not past use_addf which + // depends on it) + // ============================================================================ + // CHECK-LABEL: func.func @vector_store_inside_scf_if_sinks( + // CHECK: arith.mulf {{.*}} ssbuffer.core_type = "VECTOR" + // CHECK: scf.if + // CHECK: } {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} + // CHECK: arith.addf {{.*}} ssbuffer.core_type = "VECTOR" + func.func @vector_store_inside_scf_if_sinks( + %cond: i1, + %arg0: memref<64xf32>, + %in: tensor<64xf32>, + %in2: tensor<64xf32>) -> (tensor<64xf32>, tensor<64xf32>) { + %result = "scf.if"(%cond) ({ + %val = arith.addf %in, %in {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + bufferization.materialize_in_destination %val in writable %arg0 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64xf32>, memref<64xf32>) -> () + "scf.yield"(%val) : (tensor<64xf32>) -> () + }, { + %val2 = arith.mulf %in, %in {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + bufferization.materialize_in_destination %val2 in writable %arg0 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : (tensor<64xf32>, memref<64xf32>) -> () + "scf.yield"(%val2) : (tensor<64xf32>) -> () + }) {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : (i1) -> tensor<64xf32> + %independent = arith.mulf %in2, %in2 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %use = arith.addf %result, %result {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + return %independent, %use : tensor<64xf32>, tensor<64xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_triton_indirect_load_call_memory_effects.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_triton_indirect_load_call_memory_effects.mlir new file mode 100644 index 0000000000..1c0de09bab --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PlanComputeBlock/test_triton_indirect_load_call_memory_effects.mlir @@ -0,0 +1,33 @@ +// RUN: triton-opt --debug-only=memory-effects-tracker --plan-compute-block %s 2>&1 | FileCheck %s + +module { + func.func private @triton_indirect_load_0(%arg0: memref<4x4xf32>) -> tensor<4x4xf32> + + // CHECK-LABEL: func.func @triton_indirect_load_call_is_not_memory_barrier + // CHECK: %[[ALLOC:[A-Za-z0-9_]+]] = memref.alloc() + // CHECK: linalg.fill {{.*}} outs(%[[ALLOC]] + // CHECK: Analyzing op %{{.*}} = func.call @triton_indirect_load_0(%[[ALLOC]]) + // CHECK-NEXT: [memory-effects-tracker] Defs: + // CHECK-NEXT: [memory-effects-tracker] Preds: + // CHECK-NEXT: [memory-effects-tracker] Analyzing op %{{.*}} = bufferization.to_tensor %[[ALLOC]] + // CHECK-NEXT: [memory-effects-tracker] Defs: + // CHECK-NEXT: [memory-effects-tracker] linalg.fill {{.*}} outs(%[[ALLOC]] + // CHECK-NEXT: [memory-effects-tracker] Preds: + // CHECK-NEXT: [memory-effects-tracker] linalg.fill {{.*}} outs(%[[ALLOC]] + func.func @triton_indirect_load_call_is_not_memory_barrier( + %arg0: memref<4xf32>, + %rhs: tensor<4x4xf32>) -> tensor<4x4xf32> { + %c0 = arith.constant 0 : index + %zero = arith.constant 0.0 : f32 + %unused = memref.load %arg0[%c0] : memref<4xf32> + %alloc = memref.alloc() : memref<4x4xf32> + linalg.fill ins(%zero : f32) outs(%alloc : memref<4x4xf32>) + %ignored = func.call @triton_indirect_load_0(%alloc) : (memref<4x4xf32>) -> tensor<4x4xf32> + %lhs = bufferization.to_tensor %alloc restrict writable : memref<4x4xf32> to tensor<4x4xf32> + %out = tensor.empty() : tensor<4x4xf32> + %init = linalg.fill ins(%zero : f32) outs(%out : tensor<4x4xf32>) -> tensor<4x4xf32> + %mm = linalg.matmul ins(%lhs, %rhs : tensor<4x4xf32>, tensor<4x4xf32>) + outs(%init : tensor<4x4xf32>) -> tensor<4x4xf32> + return %mm : tensor<4x4xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PreCheckAvailable/test_have_scope.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PreCheckAvailable/test_have_scope.mlir new file mode 100644 index 0000000000..68d88d9465 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PreCheckAvailable/test_have_scope.mlir @@ -0,0 +1,19 @@ +// RUN: triton-opt --pre-check-blacklist --debug %s 2>&1 | FileCheck %s + +// Test Case: Module with scope.scope operation (blacklist op) +// Should be detected by PreCheckBlacklistPass + +//CHECK: SSBUFFER will be skipped because scope.scope operation was found +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_scope_blacklist() { + %c0 = arith.constant 0 : i32 + + scope.scope : () -> () { + %cst = arith.constant 0.0 : f32 + %empty = tensor.empty() : tensor<32x32xf32> + %filled = linalg.fill ins(%cst : f32) outs(%empty : tensor<32x32xf32>) -> tensor<32x32xf32> + } + + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PreCheckAvailable/test_no_matmul.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PreCheckAvailable/test_no_matmul.mlir new file mode 100644 index 0000000000..5bdbbf0ed4 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/PreCheckAvailable/test_no_matmul.mlir @@ -0,0 +1,24 @@ +// RUN: triton-opt --pre-check-matmul --debug %s 2>&1 | FileCheck %s + +// Test Case: Module without linalg.matmul operation +// PreCheckMatmul should fail because no matmul is found + +// CHECK: SSBUFFER will be skipped because no linalg.matmul operation was found +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_no_matmul() { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c32 = arith.constant 32 : index + %cst_f32 = arith.constant 0.0 : f32 + + %empty = tensor.empty() : tensor<32x32xf32> + %filled = linalg.fill ins(%cst_f32 : f32) outs(%empty : tensor<32x32xf32>) -> tensor<32x32xf32> + + %cst_1 = arith.constant 1.0 : f32 + + scf.for %arg0 = %c0 to %c32 step %c1 { + } + + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/test_mark_gm_load_hint.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/test_mark_gm_load_hint.mlir new file mode 100644 index 0000000000..886ea1d4d5 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/test_mark_gm_load_hint.mlir @@ -0,0 +1,28 @@ +// RUN: triton-opt --mark-gm-load %s | FileCheck %s + +// ============================================================================ +// Test 1 (hint force-on depth=2): annotation.mark on the tensor produced by +// bufferization.to_tensor after memref.copy, matching the real pre-bufferization +// pipeline order. +// ============================================================================ + +// CHECK-LABEL: func.func @hint_force_on +func.func @hint_force_on(%arg0: memref) { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %c128_i32 = arith.constant 128 : i32 + %c1_i32 = arith.constant 1 : i32 + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c128_i32 step %c1_i32 : i32 { + %reinterpret_cast = memref.reinterpret_cast %arg0 to offset: [%c0], sizes: [128], strides: [1] : memref to memref<128xf16, strided<[1], offset: ?>> + %alloc = memref.alloc() : memref<128xf16> + // CHECK: memref.alloc() : memref<128xf16> + // CHECK-NEXT: annotation.mark %{{.*}} {gm_load_hint, hivm.multi_buffer = 2 : i32} : memref<128xf16> + memref.copy %reinterpret_cast, %alloc : memref<128xf16, strided<[1], offset: ?>> to memref<128xf16> + %tensor = bufferization.to_tensor %alloc restrict writable : memref<128xf16> to tensor<128xf16> + annotation.mark %tensor {gm_load = 2 : i32} : tensor<128xf16> + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/ub_overflow_check.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/ub_overflow_check.mlir new file mode 100644 index 0000000000..a918f54254 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/ub_overflow_check.mlir @@ -0,0 +1,217 @@ +// RUN: triton-opt -split-input-file --ub-overflow-check %s | FileCheck %s + +// ============================================================================ +// Test 1: Safe — UB estimate under threshold, all marks preserved. +// Two 128x128xf32 annot buffers (alignedSize=262144 bits each). +// Wave UB = 2 x (262144 x 2) = 1048576 bits < 2031616 => SAFE. +// ============================================================================ + +func.func @safe_no_pruning() { + %c0_i32 = arith.constant 0 : i32 + %c8_i32 = arith.constant 8 : i32 + %c1_i32 = arith.constant 1 : i32 + + // CHECK-LABEL: func.func @safe_no_pruning + + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c8_i32 step %c1_i32 : i32 { + // CHECK: memref.alloc() : memref<128x128xf32> + // CHECK-NEXT: annotation.mark %{{.*}} {hivm.multi_buffer = 2 : i32} : memref<128x128xf32> + %a1 = memref.alloc() : memref<128x128xf32> + annotation.mark %a1 {hivm.multi_buffer = 2 : i32} : memref<128x128xf32> + // CHECK: memref.alloc() : memref<128x128xf32> + // CHECK-NEXT: annotation.mark %{{.*}} {hivm.multi_buffer = 2 : i32} : memref<128x128xf32> + %a2 = memref.alloc() : memref<128x128xf32> + annotation.mark %a2 {hivm.multi_buffer = 2 : i32} : memref<128x128xf32> + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Test 2: Overflow — largest multi_buffer mark pruned. +// One 256x256xf32 (alignedSize=1048576, expanded=2097152) +// Two 64x64xf32 (alignedSize=65536 each, expanded=131072 each) +// Wave UB = 2097152 + 2x131072 = 2359296 > 2031616 => OVERFLOW. +// Prune largest (256x256): annot→131072x2=262144, unannot→1048576. +// Wave UB = 1310720 < 2031616 => SAFE. +// ============================================================================ + +func.func @overflow_prune_largest() { + %c0_i32 = arith.constant 0 : i32 + %c8_i32 = arith.constant 8 : i32 + %c1_i32 = arith.constant 1 : i32 + + // CHECK-LABEL: func.func @overflow_prune_largest + + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c8_i32 step %c1_i32 : i32 { + // 256x256 mark pruned: markOp still exists but multi_buffer attr removed. + // CHECK: memref.alloc() : memref<256x256xf32> + // CHECK-NOT: multi_buffer + // CHECK: annotation.mark + %huge = memref.alloc() : memref<256x256xf32> + annotation.mark %huge {hivm.multi_buffer = 2 : i32} : memref<256x256xf32> + + // Small allocs retain marks. + // CHECK: memref.alloc() : memref<64x64xf32> + // CHECK-NEXT: annotation.mark %{{.*}} {hivm.multi_buffer = 2 : i32} : memref<64x64xf32> + %small1 = memref.alloc() : memref<64x64xf32> + annotation.mark %small1 {hivm.multi_buffer = 2 : i32} : memref<64x64xf32> + + // CHECK: memref.alloc() : memref<64x64xf32> + // CHECK-NEXT: annotation.mark %{{.*}} {hivm.multi_buffer = 2 : i32} : memref<64x64xf32> + %small2 = memref.alloc() : memref<64x64xf32> + annotation.mark %small2 {hivm.multi_buffer = 2 : i32} : memref<64x64xf32> + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Test 3: CUBE scope skipped (only VECTOR scopes are checked). +// ============================================================================ + +func.func @skip_cube_scope() { + %c0_i32 = arith.constant 0 : i32 + %c8_i32 = arith.constant 8 : i32 + %c1_i32 = arith.constant 1 : i32 + + // CHECK-LABEL: func.func @skip_cube_scope + // CHECK: annotation.mark %{{.*}} {hivm.multi_buffer = 2 : i32} : memref<256x256xf32> + + scope.scope : () -> () { + scf.for %i = %c0_i32 to %c8_i32 step %c1_i32 : i32 { + %a1 = memref.alloc() : memref<256x256xf32> + annotation.mark %a1 {hivm.multi_buffer = 2 : i32} : memref<256x256xf32> + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Test 4: Hint-protected mark survives pruning; gm_load_hint cleaned up. +// A (hint, gm_load_hint + multi_buffer=2): 256x192xf32 (alignedSize=786432) +// B (auto, multi_buffer=2): 256x192xf32 (alignedSize=786432) +// Wave UB = 2 x 1572864 = 3145728 > 2031616 => OVERFLOW. +// A is hint-protected, cannot prune. Prune B. +// Wave UB = 1572864 < 2031616 => SAFE. +// ============================================================================ + +func.func @hint_protected_kept() { + %c0_i32 = arith.constant 0 : i32 + %c8_i32 = arith.constant 8 : i32 + %c1_i32 = arith.constant 1 : i32 + + // CHECK-LABEL: func.func @hint_protected_kept + // CHECK-NOT: gm_load_hint + + scope.scope : () -> () { + // Hint-protected (A): mark kept, gm_load_hint removed. + scf.for %i = %c0_i32 to %c8_i32 step %c1_i32 : i32 { + // CHECK: memref.alloc() : memref<256x192xf32> + // CHECK-NEXT: annotation.mark %{{.*}} {hivm.multi_buffer = 2 : i32} : memref<256x192xf32> + %a = memref.alloc() : memref<256x192xf32> + annotation.mark %a {gm_load_hint, hivm.multi_buffer = 2 : i32} : memref<256x192xf32> + } + // Auto (B): pruned. + scf.for %j = %c0_i32 to %c8_i32 step %c1_i32 : i32 { + // CHECK: memref.alloc() : memref<256x192xf32> + // CHECK-NEXT: annotation.mark %{{.*}} : memref<256x192xf32> + %b = memref.alloc() : memref<256x192xf32> + annotation.mark %b {hivm.multi_buffer = 2 : i32} : memref<256x192xf32> + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + + +// ----- + +// ============================================================================ +// Test 5: Hint protection survives multiple pruning rounds. +// Hint: 256x144xf32, alignedSize=589824, expanded=1179648. +// Auto: two 256x96xf32, alignedSize=393216, expanded=786432 each. +// Initial=2752512; after one prune=2359296; after two=1966080. +// ============================================================================ + +func.func @multi_round_hint_protection() { + // CHECK-LABEL: func.func @multi_round_hint_protection + // CHECK-NOT: gm_load_hint + + scope.scope : () -> () { + // CHECK: memref.alloc() : memref<256x144xf32> + // CHECK-NEXT: annotation.mark %{{.*}} {hivm.multi_buffer = 2 : i32} : memref<256x144xf32> + %hint = memref.alloc() : memref<256x144xf32> + annotation.mark %hint {gm_load_hint, hivm.multi_buffer = 2 : i32} : memref<256x144xf32> + + // CHECK: memref.alloc() : memref<256x96xf32> + // CHECK-NEXT: annotation.mark %{{.*}} : memref<256x96xf32> + %auto1 = memref.alloc() : memref<256x96xf32> + annotation.mark %auto1 {hivm.multi_buffer = 2 : i32} : memref<256x96xf32> + + // CHECK: memref.alloc() : memref<256x96xf32> + // CHECK-NEXT: annotation.mark %{{.*}} : memref<256x96xf32> + %auto2 = memref.alloc() : memref<256x96xf32> + annotation.mark %auto2 {hivm.multi_buffer = 2 : i32} : memref<256x96xf32> + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Test 6: A blind tensor.empty contributes to UB and triggers pruning. +// Alloc expanded=1572864; tensor=524288; total=2097152 > 2031616. +// ============================================================================ + +func.func @blind_tensor_triggers_pruning() { + // CHECK-LABEL: func.func @blind_tensor_triggers_pruning + + scope.scope : () -> () { + // CHECK: memref.alloc() : memref<256x192xf32> + // CHECK-NEXT: annotation.mark %{{.*}} : memref<256x192xf32> + %alloc = memref.alloc() : memref<256x192xf32> + annotation.mark %alloc {hivm.multi_buffer = 2 : i32} : memref<256x192xf32> + + // CHECK: tensor.empty() : tensor<256x128xf32> + %empty = tensor.empty() : tensor<256x128xf32> + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Test 7: A same-shape tensor.empty is still counted independently. +// Alloc expanded=1572864; tensor=786432; total=2359296 > 2031616. +// Removing the mark leaves 1572864 bits, which is safe. +// ============================================================================ + +func.func @same_shape_tensor_counted() { + // CHECK-LABEL: func.func @same_shape_tensor_counted + + scope.scope : () -> () { + // CHECK: memref.alloc() : memref<256x192xf32> + // CHECK-NEXT: annotation.mark %{{.*}} : memref<256x192xf32> + %alloc = memref.alloc() : memref<256x192xf32> + annotation.mark %alloc {hivm.multi_buffer = 2 : i32} : memref<256x192xf32> + + // CHECK: tensor.empty() : tensor<256x192xf32> + %empty = tensor.empty() : tensor<256x192xf32> + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/ub_overflow_sdpa.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/ub_overflow_sdpa.mlir new file mode 100644 index 0000000000..48ee935ae1 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SeparateMemoryFromCompute/ub_overflow_sdpa.mlir @@ -0,0 +1,60 @@ +// RUN: triton-opt -split-input-file --ub-overflow-check %s | FileCheck %s +// RUN: triton-opt -split-input-file --ub-overflow-check --debug-only=UBOverflow %s -o /dev/null 2>&1 | FileCheck %s --check-prefix=DEBUG + +// The allocs below match the SDPA dump. With the mask double-buffered, +// alloc-only UB is 1318912 bits (161 KiB), so the mark is safe. + +// CHECK-LABEL: func.func @alloc_only_keeps_mark +func.func @alloc_only_keeps_mark() { + scope.scope : () -> () { + %cbuf = memref.alloc() : memref<16x8x16x16xf16, #hivm.address_space> + %a0 = memref.alloc() : memref<128x256xf32, #hivm.address_space> + %a1 = memref.alloc() : memref<128x128xf32, #hivm.address_space> + %a2 = memref.alloc() : memref<128xf32, #hivm.address_space> + %a3 = memref.alloc() : memref<128xf32, #hivm.address_space> + %a4 = memref.alloc() : memref<128xf32, #hivm.address_space> + %a5 = memref.alloc() : memref<128xf32, #hivm.address_space> + // CHECK: %[[SAFE_MASK:.*]] = memref.alloc() : memref<128x256xi8> + // CHECK-NEXT: annotation.mark %[[SAFE_MASK]] {hivm.multi_buffer = 2 : i32} : memref<128x256xi8> + %mask = memref.alloc() : memref<128x256xi8> + annotation.mark %mask {hivm.multi_buffer = 2 : i32} : memref<128x256xi8> + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// DEBUG: initial UB = 1318912 bits, max = 2031616 bits +// DEBUG: safe, no pruning needed + +// ----- + +// The five tensor.empty ops add 1181696 bits (144.25 KiB). +// Total UB becomes 2500608 bits (305.25 KiB), so the mark must be pruned. + +// CHECK-LABEL: func.func @tensor_sizes_prune_mark +func.func @tensor_sizes_prune_mark() { + scope.scope : () -> () { + %cbuf = memref.alloc() : memref<16x8x16x16xf16, #hivm.address_space> + %a0 = memref.alloc() : memref<128x256xf32, #hivm.address_space> + %a1 = memref.alloc() : memref<128x128xf32, #hivm.address_space> + %a2 = memref.alloc() : memref<128xf32, #hivm.address_space> + %a3 = memref.alloc() : memref<128xf32, #hivm.address_space> + %a4 = memref.alloc() : memref<128xf32, #hivm.address_space> + %a5 = memref.alloc() : memref<128xf32, #hivm.address_space> + // CHECK: %[[PRUNED_MASK:.*]] = memref.alloc() : memref<128x256xi8> + // CHECK-NEXT: annotation.mark %[[PRUNED_MASK]] : memref<128x256xi8> + %mask = memref.alloc() : memref<128x256xi8> + annotation.mark %mask {hivm.multi_buffer = 2 : i32} : memref<128x256xi8> + + %t0 = tensor.empty() : tensor<128x128xf32> + %t1 = tensor.empty() : tensor<128xf32> + %t2 = tensor.empty() : tensor<128x256xf32> + %t3 = tensor.empty() : tensor<128x256xi8> + %t4 = tensor.empty() : tensor<16x128x16xf16> + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// DEBUG: initial UB = 2500608 bits, max = 2031616 bits +// DEBUG: after deleting mark #1 (size 131072 bits): UB = 2369536 bits diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/flag_reuse_cross_direction.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/flag_reuse_cross_direction.mlir new file mode 100644 index 0000000000..a7b7d7487c --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/flag_reuse_cross_direction.mlir @@ -0,0 +1,63 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s --implicit-check-not="flag = -1" --implicit-check-not="flag = 15" --implicit-check-not=", ] flag = 2" --implicit-check-not=", ] flag = 1" + +// Regression for the cross-direction flag-id reuse bug. A Vector->Cube copy +// (set[VECTOR, MTE3, MTE1]) used to be merged onto the same flag as an adjacent +// Cube->Vector fixpipe wait (wait[VECTOR, FIX, V]), e.g. in the wild: +// +// hivm.hir.sync_block_set [, , ] flag = 1 +// hivm.hir.sync_block_wait[, , ] flag = 1 <- WRONG +// +// The two directions run concurrently on the two cores, so a shared counting +// flag lets the wait steal the set's signal. They must never share a flag id. +// Here a long Vector->Cube->Vector matmul chain needs more than MAX_FLAG_ID +// flags, forcing reuse; the result must partition strictly by direction. + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @flag_reuse_cross_direction() { + %cst = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} 0.000000e+00 : f32 + %e0 = tensor.empty() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + %v0 = linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f32) outs(%e0 : tensor<32x32xf32>) -> tensor<32x32xf32> + + // round 1: V->C (matmul consumes %v0), then C->V (exp consumes %m1) + %ef1 = tensor.empty() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %m1 = linalg.matmul {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%v0, %v0 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%ef1 : tensor<32x32xf32>) -> tensor<32x32xf32> + %v1 = math.exp %m1 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + %ef2 = tensor.empty() {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %m2 = linalg.matmul {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} ins(%v1, %v1 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%ef2 : tensor<32x32xf32>) -> tensor<32x32xf32> + %v2 = math.exp %m2 {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + %ef3 = tensor.empty() {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %m3 = linalg.matmul {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} ins(%v2, %v2 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%ef3 : tensor<32x32xf32>) -> tensor<32x32xf32> + %v3 = math.exp %m3 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + %ef4 = tensor.empty() {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %m4 = linalg.matmul {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} ins(%v3, %v3 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%ef4 : tensor<32x32xf32>) -> tensor<32x32xf32> + %v4 = math.exp %m4 {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + %ef5 = tensor.empty() {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %m5 = linalg.matmul {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "CUBE"} ins(%v4, %v4 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%ef5 : tensor<32x32xf32>) -> tensor<32x32xf32> + %v5 = math.exp %m5 {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + %ef6 = tensor.empty() {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %m6 = linalg.matmul {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "CUBE"} ins(%v5, %v5 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%ef6 : tensor<32x32xf32>) -> tensor<32x32xf32> + %v6 = math.exp %m6 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + %ef7 = tensor.empty() {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %m7 = linalg.matmul {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "CUBE"} ins(%v6, %v6 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%ef7 : tensor<32x32xf32>) -> tensor<32x32xf32> + %v7 = math.exp %m7 {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + %ef8 = tensor.empty() {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %m8 = linalg.matmul {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "CUBE"} ins(%v7, %v7 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%ef8 : tensor<32x32xf32>) -> tensor<32x32xf32> + %v8 = math.exp %m8 {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + return + } +} + +// CHECK-LABEL: func.func @flag_reuse_cross_direction +// Vector->Cube (MTE3/MTE1) copies all reuse one flag; Cube->Vector (FIX/V) +// fixpipes all reuse another; the two never collide. +// CHECK: , ] flag = 2 +// CHECK: , ] flag = 1 +// CHECK: return diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/flag_reuse_over_limit.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/flag_reuse_over_limit.mlir new file mode 100644 index 0000000000..172b0578d5 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/flag_reuse_over_limit.mlir @@ -0,0 +1,48 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s --implicit-check-not="flag = -1" --implicit-check-not="flag = 15" --implicit-check-not=", ] flag = 2" --implicit-check-not=", ] flag = 1" + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @flag_reuse_over_limit() { + %cst = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f32 + %alloc = memref.alloc() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf32> + %ta = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf32> to tensor<32x32xf32> + %empty = tensor.empty() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %fill = linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%empty : tensor<32x32xf32>) -> tensor<32x32xf32> + %mat = linalg.matmul {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%ta, %ta : tensor<32x32xf32>, tensor<32x32xf32>) outs(%fill : tensor<32x32xf32>) -> tensor<32x32xf32> + %v2 = math.exp %mat {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + %empty_3 = tensor.empty() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %c3 = linalg.transpose ins(%v2 : tensor<32x32xf32>) outs(%empty_3 : tensor<32x32xf32>) permutation = [1, 0] {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} + %v4 = math.exp %c3 {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + %empty_5 = tensor.empty() {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %c5 = linalg.transpose ins(%v4 : tensor<32x32xf32>) outs(%empty_5 : tensor<32x32xf32>) permutation = [1, 0] {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} + %v6 = math.exp %c5 {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + %empty_7 = tensor.empty() {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %c7 = linalg.transpose ins(%v6 : tensor<32x32xf32>) outs(%empty_7 : tensor<32x32xf32>) permutation = [1, 0] {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} + %v8 = math.exp %c7 {ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + %empty_9 = tensor.empty() {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %c9 = linalg.transpose ins(%v8 : tensor<32x32xf32>) outs(%empty_9 : tensor<32x32xf32>) permutation = [1, 0] {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} + %v10 = math.exp %c9 {ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + %empty_11 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %c11 = linalg.transpose ins(%v10 : tensor<32x32xf32>) outs(%empty_11 : tensor<32x32xf32>) permutation = [1, 0] {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} + %v12 = math.exp %c11 {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + %empty_13 = tensor.empty() {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %c13 = linalg.transpose ins(%v12 : tensor<32x32xf32>) outs(%empty_13 : tensor<32x32xf32>) permutation = [1, 0] {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "CUBE"} + %v14 = math.exp %c13 {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + %empty_15 = tensor.empty() {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %c15 = linalg.transpose ins(%v14 : tensor<32x32xf32>) outs(%empty_15 : tensor<32x32xf32>) permutation = [1, 0] {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "CUBE"} + %v16 = math.exp %c15 {ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + return + } +} + +// This is an alternating Cube->Vector / Vector->Cube chain that needs more than +// MAX_FLAG_ID flags before reuse. Opposite-direction transfers run concurrently +// on the two cores (a shared counting-flag would let one core's wait steal the +// other's set), so they must never share a flag id. Same-direction transfers, +// serialized by their pipe's FIFO, all collapse onto one flag. The result is a +// strict partition by direction: every Cube->Vector (FIX/V) sync uses one flag +// and every Vector->Cube (MTE3/MTE1) sync uses a different one (enforced by the +// implicit-check-not directives in the RUN line). +// CHECK-LABEL: func.func @flag_reuse_over_limit +// CHECK: , ] flag = 1 +// CHECK: , ] flag = 2 +// CHECK: return diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_bidirectional_transfer.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_bidirectional_transfer.mlir new file mode 100644 index 0000000000..ec538fba28 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_bidirectional_transfer.mlir @@ -0,0 +1,44 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_bidirectional_transfer(%arg0: memref<256x256xf16>) { + %c0_i32 = arith.constant {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} 0 : index + %c1_i32 = arith.constant {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} 1 : index + %c4_i32 = arith.constant {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} 4 : index + scf.for %iv = %c0_i32 to %c4_i32 step %c1_i32 { + %empty = tensor.empty() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : tensor<256x256xf32> + %alloc = memref.alloc() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<256x256xf16> + %t0 = bufferization.to_tensor %alloc {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<256x256xf16> to tensor<256x256xf16> + %cst = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} 0.0 : f16 + %init = linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%empty : tensor<256x256xf32>) -> tensor<256x256xf32> + %mat1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%t0, %t0 : tensor<256x256xf16>, tensor<256x256xf16>) outs(%init : tensor<256x256xf32>) -> tensor<256x256xf32> + + %exp = math.exp %mat1 {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : tensor<256x256xf32> + %exp1 = math.exp %exp {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : tensor<256x256xf32> + + %alloc2 = memref.alloc() {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<256x256xf32> + %t3 = bufferization.to_tensor %alloc2 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : memref<256x256xf32> to tensor<256x256xf32> + %mat2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} ins(%t3, %exp1 : tensor<256x256xf32>, tensor<256x256xf32>) outs(%empty : tensor<256x256xf32>) -> tensor<256x256xf32> + } + return + } +} + +// CHECK-LABEL: func.func @test_bidirectional_transfer +// CHECK: hivm.hir.sync_block_wait {{.*}}[, , ] flag = [[FLAG_1:[0-9]+]] +// CHECK: hivm.hir.fixpipe +// CHECK: hivm.hir.sync_block_set {{.*}}[, , ] flag = [[FLAG_1]] +// CHECK: hivm.hir.sync_block_wait {{.*}}[, , ] flag = [[FLAG_1]] +// CHECK: memref.memory_space_cast +// CHECK: bufferization.to_tensor + +// CHECK: hivm.hir.sync_block_wait {{.*}}[, , ] flag = [[FLAG_2:[0-9]+]] +// CHECK: hivm.hir.copy +// CHECK: hivm.hir.sync_block_set {{.*}}[, , ] flag = [[FLAG_2]] +// CHECK: hivm.hir.sync_block_set {{.*}}[, , ] flag = [[FLAG_1:[0-9]+]] + +// CHECK: hivm.hir.sync_block_wait {{.*}}[, , ] flag = [[FLAG_2]] +// CHECK: hivm.hir.convert_layout +// CHECK: memref.memory_space_cast +// CHECK: bufferization.to_tensor +// CHECK: hivm.hir.sync_block_set {{.*}}[, , ] flag = [[FLAG_2]] diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_cube_to_vector_direct_store.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_cube_to_vector_direct_store.mlir new file mode 100644 index 0000000000..10ccd98a05 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_cube_to_vector_direct_store.mlir @@ -0,0 +1,198 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync %s | FileCheck %s + +// Step 2 of the cube-to-vector-direct-store pipeline: after the VECTOR pseudo-op +// (`arith.addf` carrying `ssbuffer.add_from_matmul`) is folded away in a +// preceding pass, the for-loop now yields a VECTOR `bufferization.to_tensor` +// directly (its source memref traces back to a `hivm.tightly_coupled_buffer` +// `memref.alloc`). The yielded value flows out of the loop, through one or +// more `tensor.extract_slice`s, to a post-region store-like op. This pass +// inserts the CUBE.PIPE_FIX -> VECTOR.PIPE_MTE3 set/wait pair that guards +// the post-region store. +// CHECK-LABEL: func.func @pre_process_bwd_kernel_merged +// CHECK-NOT: ssbuffer.add_from_matmul +// CHECK: scf.for +// CHECK: scf.yield +// CHECK: hivm.hir.sync_block_set {{.*}}[, , ] +// CHECK: tensor.extract_slice +// CHECK: hivm.hir.sync_block_wait {{.*}}[, , ] +// CHECK: bufferization.materialize_in_destination +// CHECK: return +// [ReorderOpsByBlockIdPass] Output mlir: +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @pre_process_bwd_kernel_merged(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: memref {tt.divisibility = 16 : i32}, %arg6: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg7: memref {tt.divisibility = 16 : i32}, %arg8: f32, %arg9: i32, %arg10: i32, %arg11: i32, %arg12: i32, %arg13: i32, %arg14: i32, %arg15: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c256 = arith.constant {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} 256 : index + %c128 = arith.constant {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} 128 : index + %c64 = arith.constant {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} 64 : index + %c0 = arith.constant {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %c32768_i32 = arith.constant {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} 32768 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %c64_i32 = arith.constant {MixUse, ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} 64 : i32 + %c128_i32 = arith.constant {MixUse, ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} 128 : i32 + %0 = arith.muli %arg13, %c64_i32 {Undefined, ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %1 = arith.muli %arg14, %c32768_i32 {ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.cmpi sge, %0, %c128_i32 {Undefined, ssbuffer.block_id = 14 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %c63_i32 = arith.constant {MixUse, ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "VECTOR"} 63 : i32 + %c1_i32 = arith.constant {MixUse, ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %3 = arith.addi %arg9, %c63_i32 {MixUse, ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %4 = arith.divsi %3, %c64_i32 {MixUse, ssbuffer.block_id = 16 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %c1024 = arith.constant {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 1024 : index + %c128_0 = arith.constant {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 128 : index + %c64_1 = arith.constant {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 64 : index + %c0_2 = arith.constant {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 0 : index + %c512 = arith.constant {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 512 : index + %c63_i32_3 = arith.constant {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 63 : i32 + %c1_i32_4 = arith.constant {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 1 : i32 + %cst = arith.constant {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f32 + %c0_i32_5 = arith.constant {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 0 : i32 + %c64_i32_6 = arith.constant {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 64 : i32 + %c2_i32 = arith.constant {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 2 : i32 + %c128_i64 = arith.constant {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} 128 : i64 + %5 = tensor.empty() {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %6 = tensor.empty() {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + %7 = linalg.fill {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%6 : tensor<128x128xf32>) -> tensor<128x128xf32> + %8 = arith.addi %arg9, %c63_i32_3 {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i32 + %9 = arith.divsi %8, %c64_i32_6 {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i32 + %10 = arith.divsi %arg14, %c2_i32 {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i32 + %11 = arith.extsi %10 {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i32 to i64 + %12 = arith.muli %11, %c128_i64 {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i64 + %13 = arith.index_cast %12 {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i64 to index + %14 = arith.extsi %arg14 {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i32 to i64 + %15 = arith.muli %14, %c128_i64 {MixUse, ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i64 + %16 = arith.index_cast %15 {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "CUBE"} : i64 to index + %cst_7 = arith.constant {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} 0.000000e+00 : f32 + %cst_8 = arith.constant {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} 1.000000e+00 : f32 + %c2_i32_9 = arith.constant {MixUse, ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} 2 : i32 + %17 = tensor.empty() {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + %18 = linalg.fill {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_8 : f32) outs(%17 : tensor<128x64xf32>) -> tensor<128x64xf32> + %19 = linalg.fill {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_7 : f32) outs(%17 : tensor<128x64xf32>) -> tensor<128x64xf32> + %20 = tensor.empty() {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + %21 = linalg.fill {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_8 : f32) outs(%20 : tensor<128x128xf32>) -> tensor<128x128xf32> + %22 = linalg.fill {ssbuffer.block_id = 15 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst_7 : f32) outs(%20 : tensor<128x128xf32>) -> tensor<128x128xf32> + scf.if %2 { + %23 = arith.subi %arg13, %c2_i32_9 {MixUse, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %24 = tensor.empty() {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128xi32> + %25 = linalg.generic {indexing_maps = [affine_map<(d0) -> (d0)>], iterator_types = ["parallel"]} outs(%24 : tensor<128xi32>) attrs = {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR", tt.from_make_range, tt.make_range_offset = 0 : index, tt.make_range_size = 128 : index} { + ^bb0(%out: i32): + %59 = linalg.index 0 : index + %60 = arith.index_cast %59 : index to i32 + linalg.yield %60 : i32 + } -> tensor<128xi32> + %26 = tensor.empty() {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xi32> + %27 = linalg.generic {indexing_maps = [affine_map<(d0) -> (d0)>], iterator_types = ["parallel"]} outs(%26 : tensor<64xi32>) attrs = {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR", tt.from_make_range, tt.make_range_offset = 0 : index, tt.make_range_size = 64 : index} { + ^bb0(%out: i32): + %59 = linalg.index 0 : index + %60 = arith.index_cast %59 : index to i32 + linalg.yield %60 : i32 + } -> tensor<64xi32> + %28 = arith.muli %23, %c64_i32 {MixUse, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %29 = linalg.fill {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} ins(%28 : i32) outs(%26 : tensor<64xi32>) -> tensor<64xi32> + %30 = arith.addi %27, %29 {DataUse, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xi32> + %31 = tensor.empty() {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x64xi32> + %broadcasted = linalg.broadcast ins(%25 : tensor<128xi32>) outs(%31 : tensor<128x64xi32>) dimensions = [1] {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} + %broadcasted_10 = linalg.broadcast ins(%30 : tensor<64xi32>) outs(%31 : tensor<128x64xi32>) dimensions = [0] {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} + %32 = arith.cmpi eq, %broadcasted, %broadcasted_10 {DataUse, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x64xi32> + %33 = arith.select %32, %18, %19 {DataUse, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x64xi1>, tensor<128x64xf32> + %34 = tensor.empty() {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xi32> + %broadcasted_11 = linalg.broadcast ins(%25 : tensor<128xi32>) outs(%34 : tensor<128x128xi32>) dimensions = [1] {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} + %broadcasted_12 = linalg.broadcast ins(%25 : tensor<128xi32>) outs(%34 : tensor<128x128xi32>) dimensions = [0] {ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} + %35 = arith.cmpi eq, %broadcasted_11, %broadcasted_12 {DataUse, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xi32> + %36 = arith.select %35, %21, %22 {DataUse, ssbuffer.block_id = 12 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xi1>, tensor<128x128xf32> + %37 = arith.subi %9, %c1_i32_4 {MixUse, ssbuffer.block_id = 8 : i32, ssbuffer.core_type = "CUBE"} : i32 + %38 = scf.for %arg16 = %c0_i32_5 to %9 step %c1_i32_4 iter_args(%arg17 = %33) -> (tensor<128x64xf32>) : i32 { + %59 = arith.subi %37, %arg16 {MixUse, ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i32 + %60 = arith.muli %59, %c64_i32_6 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i32 + %61 = arith.maxsi %60, %c0_i32_5 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i32 + %62 = arith.index_cast %61 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %63 = arith.muli %62, %c512 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %64 = arith.index_cast %arg9 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %65 = arith.divsi %63, %c512 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %66 = arith.subi %64, %65 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %67 = arith.maxsi %66, %c0_2 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %68 = arith.minsi %67, %c64_1 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %69 = arith.remsi %63, %c512 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %70 = arith.subi %c128_0, %69 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %71 = arith.maxsi %70, %c0_2 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %72 = arith.minsi %71, %c128_0 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %73 = arith.subi %c0_i32_5, %60 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i32 + %74 = arith.maxsi %73, %c0_i32_5 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i32 + %75 = arith.index_cast %74 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i32 to index + %76 = arith.minsi %75, %68 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %77 = arith.subi %68, %76 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %78 = arith.minsi %72, %c0_2 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %79 = arith.subi %72, %78 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %80 = arith.cmpi slt, %77, %c64_1 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %81 = arith.cmpi slt, %79, %c128_0 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %82 = arith.ori %80, %81 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i1 + %83 = arith.muli %62, %c1024 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %84 = arith.divsi %83, %c1024 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %85 = arith.subi %64, %84 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %86 = arith.maxsi %85, %c0_2 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %87 = arith.minsi %86, %c64_1 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %88 = arith.remsi %83, %c1024 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %89 = arith.subi %c128_0, %88 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %90 = arith.maxsi %89, %c0_2 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %91 = arith.minsi %90, %c128_0 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %92 = arith.minsi %75, %87 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %93 = arith.subi %87, %92 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %94 = arith.minsi %91, %c0_2 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %95 = arith.subi %91, %94 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %96 = arith.cmpi slt, %93, %c64_1 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %97 = arith.cmpi slt, %95, %c128_0 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : index + %98 = arith.ori %96, %97 {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "CUBE"} : i1 + %alloc = memref.alloc() {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref<64x128xf32> + %alloc_13 = memref.alloc() {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref<64x128xf32> + scf.if %98 { + linalg.fill {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%alloc_13 : memref<64x128xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 4 : i32} + scf.if %82 { + linalg.fill {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%alloc : memref<64x128xf32>) + } {hivm.unlikely_condition, ssbuffer.block_id = 4 : i32} + %99 = arith.addi %63, %13 {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : index + %reinterpret_cast_14 = memref.reinterpret_cast %arg3 to offset: [%99], sizes: [64, 128], strides: [512, 1] {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<64x128xf32, strided<[512, 1], offset: ?>> + %subview_15 = memref.subview %reinterpret_cast_14[0, 0] [%77, %79] [1, 1] {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref<64x128xf32, strided<[512, 1], offset: ?>> to memref> + %subview_16 = memref.subview %alloc[%76, %78] [%77, %79] [1, 1] {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref<64x128xf32> to memref> + memref.copy %subview_15, %subview_16 {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref> to memref> + %100 = bufferization.to_tensor %alloc restrict writable {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref<64x128xf32> to tensor<64x128xf32> + %101 = arith.addi %83, %16 {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : index + %reinterpret_cast_17 = memref.reinterpret_cast %arg4 to offset: [%101], sizes: [64, 128], strides: [1024, 1] {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref to memref<64x128xf32, strided<[1024, 1], offset: ?>> + %subview_18 = memref.subview %reinterpret_cast_17[0, 0] [%93, %95] [1, 1] {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref<64x128xf32, strided<[1024, 1], offset: ?>> to memref> + %subview_19 = memref.subview %alloc_13[%92, %94] [%93, %95] [1, 1] {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref<64x128xf32> to memref> + memref.copy %subview_18, %subview_19 {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref> to memref> + %102 = bufferization.to_tensor %alloc_13 restrict writable {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} : memref<64x128xf32> to tensor<64x128xf32> + %transposed = linalg.transpose ins(%102 : tensor<64x128xf32>) outs(%5 : tensor<128x64xf32>) permutation = [1, 0] {ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} + %103 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%transposed, %100 : tensor<128x64xf32>, tensor<64x128xf32>) outs(%7 : tensor<128x128xf32>) -> tensor<128x128xf32> + %104 = arith.subf %36, %103 {DataUse, ssbuffer.block_id = 10 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + %105 = tensor.empty() {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x64xf32> + %106 = linalg.fill {ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%105 : tensor<128x64xf32>) -> tensor<128x64xf32> + %107 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 6 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%104, %arg17 : tensor<128x128xf32>, tensor<128x64xf32>) outs(%106 : tensor<128x64xf32>) -> tensor<128x64xf32> + %108 = arith.addf %107, %19 {ssbuffer.add_from_matmul, ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> + scf.yield {ssbuffer.core_type = "VECTOR"} %108 : tensor<128x64xf32> + } {DataUse, ssbuffer.core_type = "VECTOR"} + %39 = arith.index_cast %1 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + %40 = arith.addi %39, %c128 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %41 = arith.maxsi %28, %c0_i32 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %42 = arith.index_cast %41 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + %43 = arith.addi %40, %42 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %reinterpret_cast = memref.reinterpret_cast %arg6 to offset: [%43], sizes: [128, 64], strides: [256, 1] {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : memref to memref<128x64xf32, strided<[256, 1], offset: ?>> + %44 = arith.divsi %42, %c256 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %45 = arith.subi %c128, %44 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %46 = arith.maxsi %45, %c0 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %47 = arith.minsi %46, %c128 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %48 = arith.remsi %42, %c256 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %49 = arith.subi %c128, %48 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %50 = arith.maxsi %49, %c0 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %51 = arith.minsi %50, %c64 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %52 = arith.minsi %47, %c0 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %53 = arith.subi %47, %52 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %54 = arith.subi %c0_i32, %28 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %55 = arith.maxsi %54, %c0_i32 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : i32 + %56 = arith.index_cast %55 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : i32 to index + %57 = arith.minsi %56, %51 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %58 = arith.subi %51, %57 {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : index + %extracted_slice = tensor.extract_slice %38[%52, %57] [%53, %58] [1, 1] {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x64xf32> to tensor + %subview = memref.subview %reinterpret_cast[0, 0] [%53, %58] [1, 1] {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x64xf32, strided<[256, 1], offset: ?>> to memref> + bufferization.materialize_in_destination %extracted_slice in writable %subview {ssbuffer.block_id = 13 : i32, ssbuffer.core_type = "VECTOR"} : (tensor, memref>) -> () + } {Undefined} + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_empty_fill_skip.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_empty_fill_skip.mlir new file mode 100644 index 0000000000..c0073fc629 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_empty_fill_skip.mlir @@ -0,0 +1,15 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +// CHECK-LABEL: func.func @test_empty_fill_skip +// CHECK-NOT: hivm.hir.copy +// CHECK-NOT: hivm.hir.fixpipe +// CHECK: return +module { + func.func @test_empty_fill_skip(%arg0: memref<128x128xf16>, %a: tensor<128x128xf16>, %b: tensor<128x128xf16>) { + %cst = arith.constant {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} 0.0 : f16 + %empty = tensor.empty() {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + %fill = linalg.fill {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f16) outs(%empty : tensor<128x128xf32>) -> tensor<128x128xf32> + %mm = linalg.matmul {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%a, %b : tensor<128x128xf16>, tensor<128x128xf16>) outs(%fill : tensor<128x128xf32>) -> tensor<128x128xf32> + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_empty_func.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_empty_func.mlir new file mode 100644 index 0000000000..935ec3bfd4 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_empty_func.mlir @@ -0,0 +1,9 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +// CHECK-LABEL: func.func @test_empty_func +// CHECK-NEXT: return +module { + func.func @test_empty_func() { + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_fill_init.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_fill_init.mlir new file mode 100644 index 0000000000..5da57028a9 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_fill_init.mlir @@ -0,0 +1,43 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_fill_init(%arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}){ + %c1_i32 = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %c128_i32 = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} 128 : i32 + %c0_i32 = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %cst_0 = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} 0.000000e+00 : f32 + + %0 = tensor.empty() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %1 = linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%cst_0 : f32) outs(%0 : tensor<32x32xf32>) -> tensor<32x32xf32> + %91 = scf.for %arg20 = %c0_i32 to %c128_i32 step %c1_i32 iter_args(%arg21 = %1) -> (tensor<32x32xf32>) : i32 { + + %60 = math.exp %arg21 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "VECTOR"} : tensor<32x32xf32> + + %182 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 4 : i32, ssbuffer.core_type = "CUBE"} ins(%0, %0 : tensor<32x32xf32>, tensor<32x32xf32>) outs(%1 : tensor<32x32xf32>) -> tensor<32x32xf32> + + scf.yield {ssbuffer.core_type = "CUBE"} %182 : tensor<32x32xf32> + } {ssbuffer.core_type = "CUBE"} + return +}} + + +// CHECK-LABEL: func.func @test_fill_init +// CHECK: %[[FILL_1:[a-z0-9_]+]] = linalg.fill + +// CHECK: scf.for {{.*}} iter_args(%[[ARG_2:[a-z0-9_]+]] = %[[FILL_1]]) + +// CHECK: math.exp %[[ARG_2:[a-z0-9_]+]] +// CHECK: %[[MATMUL_4:[a-z0-9_]+]] = linalg.matmul + +// CHECK: hivm.hir.sync_block_wait +// CHECK: hivm.hir.fixpipe {{.*}} ins(%[[MATMUL_4]] : tensor<32x32xf32>) +// CHECK: hivm.hir.sync_block_set + +// CHECK: hivm.hir.sync_block_wait +// CHECK: memref.memory_space_cast +// CHECK: %[[TENSOR_5:[a-z0-9_]+]] = bufferization.to_tensor +// CHECK: arith.constant +// CHECK: hivm.hir.sync_block_set + +// CHECK: scf.yield {ssbuffer.core_type = "VECTOR"} %[[TENSOR_5]] : tensor<32x32xf32> +// CHECK: } {{{.*}}ssbuffer.core_type = "VECTOR"{{.*}}} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_fixpipe_nz2nz.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_fixpipe_nz2nz.mlir new file mode 100644 index 0000000000..c6223ca77a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_fixpipe_nz2nz.mlir @@ -0,0 +1,179 @@ +// RUN: triton-opt --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop --split-input-file %s | FileCheck %s + +// Test fixpipe nz2nz f32: channel_split = true +// Producer matmul at block_id=3 -> Consumer matmul at block_id=5 + +// CHECK-LABEL: func.func @test_c2c_fixpipe_nz2nz_f32 + +// CHECK: memref.alloc() {{.*}} : memref<32x32xf32, #hivm.address_space> + +// CHECK: hivm.hir.fixpipe {channel_split = true, ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE", ssbuffer.intraDeps = [0 : i32, 1 : i32]} ins({{%.*}} : tensor<32x32xf32>) outs({{%.*}} : memref<32x32xf32, #hivm.address_space>) + +// CHECK: memref.memory_space_cast {{%.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.intraDeps = [0 : i32, 0 : i32]} : memref<32x32xf32, #hivm.address_space> to memref<32x32xf32> +// CHECK: bufferization.to_tensor {{%.*}} restrict writable {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf32> to tensor<32x32xf32> + +// CHECK-NOT: ssbuffer.main_loop +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_c2c_fixpipe_nz2nz_f32(%arg0: memref, %arg1: memref, %a: tensor<32x1xf32>, %b: tensor<1x32xf32>, %c: tensor<32x1xf32>, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %c1_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %cst = arith.constant {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f32 + %init32x32 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %fill32x32 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%init32x32 : tensor<32x32xf32>) -> tensor<32x32xf32> + %init32x1 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x1xf32> + %fill32x1 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%init32x1 : tensor<32x1xf32>) -> tensor<32x1xf32> + scf.for %arg6 = %c0_i32 to %arg5 step %c1_i32 : i32 { + %mm1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%a, %b : tensor<32x1xf32>, tensor<1x32xf32>) outs(%fill32x32 : tensor<32x32xf32>) -> tensor<32x32xf32> + %mm2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%mm1, %c : tensor<32x32xf32>, tensor<32x1xf32>) outs(%fill32x1 : tensor<32x1xf32>) -> tensor<32x1xf32> + } + return + } +} + +// ----- + +// Test fixpipe nz2nz f32->f16 via trunc: pre_quant = F322F16 +// Producer matmul (f32) -> trunc f32->f16 -> Consumer matmul (f16) +// The trunc is folded into the fixpipe as pre_quant. + +// CHECK-LABEL: func.func @test_c2c_fixpipe_nz2nz_trunc_f32_f16 + +// CHECK: memref.alloc() {{.*}} : memref<32x32xf16, #hivm.address_space> + +// CHECK: hivm.hir.fixpipe {pre_quant = #hivm.fixpipe_pre_quant_mode, ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE", ssbuffer.intraDeps = [0 : i32, 1 : i32]} ins({{%.*}} : tensor<32x32xf32>) outs({{%.*}} : memref<32x32xf16, #hivm.address_space>) + +// CHECK: memref.memory_space_cast {{%.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.intraDeps = [0 : i32, 0 : i32]} : memref<32x32xf16, #hivm.address_space> to memref<32x32xf16> +// CHECK: bufferization.to_tensor {{%.*}} restrict writable {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> to tensor<32x32xf16> + +// CHECK-NOT: ssbuffer.main_loop +// CHECK-NOT: arith.truncf +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_c2c_fixpipe_nz2nz_trunc_f32_f16(%arg0: memref, %arg1: memref, %a: tensor<32x1xf32>, %b: tensor<1x32xf32>, %c: tensor<32x1xf16>, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %c1_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %cst_f32 = arith.constant {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f32 + %cst_f16 = arith.constant {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f16 + %init32x32 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %fill32x32 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} ins(%cst_f32 : f32) outs(%init32x32 : tensor<32x32xf32>) -> tensor<32x32xf32> + %init32x1 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x1xf16> + %fill32x1 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} ins(%cst_f16 : f16) outs(%init32x1 : tensor<32x1xf16>) -> tensor<32x1xf16> + scf.for %arg6 = %c0_i32 to %arg5 step %c1_i32 : i32 { + %mm1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%a, %b : tensor<32x1xf32>, tensor<1x32xf32>) outs(%fill32x32 : tensor<32x32xf32>) -> tensor<32x32xf32> + %trunc = arith.truncf %mm1 {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> to tensor<32x32xf16> + %mm2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%trunc, %c : tensor<32x32xf16>, tensor<32x1xf16>) outs(%fill32x1 : tensor<32x1xf16>) -> tensor<32x1xf16> + } + return + } +} + +// ----- + +// Test fixpipe nz2nz f16: channel_split = false +// f16: numElemPerBlock = 256/16 = 16, channelSplit = (16 == 8) = false + +// CHECK-LABEL: func.func @test_c2c_fixpipe_nz2nz_f16 + +// CHECK: memref.alloc() {{.*}} : memref<32x32xf16, #hivm.address_space> + +// CHECK: hivm.hir.fixpipe {ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE", ssbuffer.intraDeps = [0 : i32, 1 : i32]} ins({{%.*}} : tensor<32x32xf16>) outs({{%.*}} : memref<32x32xf16, #hivm.address_space>) +// CHECK-NOT: channel_split + +// CHECK: memref.memory_space_cast {{%.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.intraDeps = [0 : i32, 0 : i32]} : memref<32x32xf16, #hivm.address_space> to memref<32x32xf16> +// CHECK: bufferization.to_tensor {{%.*}} restrict writable {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf16> to tensor<32x32xf16> + +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_c2c_fixpipe_nz2nz_f16(%arg0: memref, %arg1: memref, %a: tensor<32x1xf16>, %b: tensor<1x32xf16>, %c: tensor<32x1xf16>, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %c1_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %cst = arith.constant {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f16 + %init32x32 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf16> + %fill32x32 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%init32x32 : tensor<32x32xf16>) -> tensor<32x32xf16> + %init32x1 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x1xf16> + %fill32x1 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%init32x1 : tensor<32x1xf16>) -> tensor<32x1xf16> + scf.for %arg6 = %c0_i32 to %arg5 step %c1_i32 : i32 { + %mm1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%a, %b : tensor<32x1xf16>, tensor<1x32xf16>) outs(%fill32x32 : tensor<32x32xf16>) -> tensor<32x32xf16> + %mm2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%mm1, %c : tensor<32x32xf16>, tensor<32x1xf16>) outs(%fill32x1 : tensor<32x1xf16>) -> tensor<32x1xf16> + } + return + } +} + +// ----- + +// Test fixpipe nz2nz f32: matmul result used as both input and init of another matmul +// Producer matmul at block_id=3 -> Consumer matmul at block_id=5 +// The bufferization.to_tensor result should only be used for the input, +// while the init should still directly use the producer matmul result. + +// CHECK-LABEL: func.func @test_c2c_fixpipe_nz2nz_input_and_init + +// CHECK: memref.alloc() {{.*}} : memref<32x32xf32, #hivm.address_space> + +// CHECK: hivm.hir.fixpipe {channel_split = true, ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE", ssbuffer.intraDeps = [0 : i32, 1 : i32]} ins([[MM1:%.*]] : tensor<32x32xf32>) outs({{%.*}} : memref<32x32xf32, #hivm.address_space>) + +// CHECK: memref.memory_space_cast {{%.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.intraDeps = [0 : i32, 0 : i32]} : memref<32x32xf32, #hivm.address_space> to memref<32x32xf32> +// CHECK: [[TO_TENSOR:%.*]] = bufferization.to_tensor {{%.*}} restrict writable {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf32> to tensor<32x32xf32> + +// Verify: the consumer matmul uses [[TO_TENSOR]] as input and [[MM1]] as init +// CHECK: linalg.matmul {{.*}} ins([[TO_TENSOR]], {{%.*}} : tensor<32x32xf32>, tensor<32x32xf32>) outs([[MM1]] : tensor<32x32xf32>) + +// CHECK-NOT: ssbuffer.main_loop +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_c2c_fixpipe_nz2nz_input_and_init(%arg0: memref, %arg1: memref, %a: tensor<32x1xf32>, %b: tensor<1x32xf32>, %c: tensor<32x32xf32>, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %c1_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %cst = arith.constant {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f32 + %init32x32 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %fill32x32 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%init32x32 : tensor<32x32xf32>) -> tensor<32x32xf32> + scf.for %arg6 = %c0_i32 to %arg5 step %c1_i32 : i32 { + %mm1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 3 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%a, %b : tensor<32x1xf32>, tensor<1x32xf32>) outs(%fill32x32 : tensor<32x32xf32>) -> tensor<32x32xf32> + %mm2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%mm1, %c : tensor<32x32xf32>, tensor<32x32xf32>) outs(%mm1 : tensor<32x32xf32>) -> tensor<32x32xf32> + } + return + } +} + +// ----- + +// Test fixpipe nz2nz f32 intra-block (IntraC2C / L0C-to-L1): matmul result +// used as both input and init of another matmul in the SAME block. + +// CHECK-LABEL: func.func @test_intra_c2c_fixpipe_nz2nz_input_and_init + +// CHECK: scf.for +// CHECK-NOT: {{^ *}$}} +// CHECK: memref.alloc() {{.*}} : memref<32x32xf32, #hivm.address_space> + +// CHECK: hivm.hir.fixpipe {channel_split = true, ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} ins([[MM1:%.*]] : tensor<32x32xf32>) outs({{%.*}} : memref<32x32xf32, #hivm.address_space>) + +// CHECK: memref.memory_space_cast {{%.*}} {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf32, #hivm.address_space> to memref<32x32xf32> +// CHECK: [[TO_TENSOR:%.*]] = bufferization.to_tensor {{%.*}} restrict writable {ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE"} : memref<32x32xf32> to tensor<32x32xf32> + +// Verify: the consumer matmul uses [[TO_TENSOR]] as input and [[MM1]] as init +// CHECK: linalg.matmul {{.*}} ins([[TO_TENSOR]], {{%.*}} : tensor<32x32xf32>, tensor<32x32xf32>) outs([[MM1]] : tensor<32x32xf32>) + +// CHECK-NOT: ssbuffer.main_loop +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_intra_c2c_fixpipe_nz2nz_input_and_init(%arg0: memref, %arg1: memref, %a: tensor<32x1xf32>, %b: tensor<1x32xf32>, %c: tensor<32x32xf32>, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %c0_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 0 : i32 + %c1_i32 = arith.constant {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR"} 1 : i32 + %cst = arith.constant {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} 0.000000e+00 : f32 + %init32x32 = tensor.empty() {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} : tensor<32x32xf32> + %fill32x32 = linalg.fill {ssbuffer.block_id = 11 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f32) outs(%init32x32 : tensor<32x32xf32>) -> tensor<32x32xf32> + scf.for %arg6 = %c0_i32 to %arg5 step %c1_i32 : i32 { + %mm1 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%a, %b : tensor<32x1xf32>, tensor<1x32xf32>) outs(%fill32x32 : tensor<32x32xf32>) -> tensor<32x32xf32> + %mm2 = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 5 : i32, ssbuffer.core_type = "CUBE", ssbuffer.loop_carried_l0c} ins(%mm1, %c : tensor<32x32xf32>, tensor<32x32xf32>) outs(%mm1 : tensor<32x32xf32>) -> tensor<32x32xf32> + } + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_func_arg_input.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_func_arg_input.mlir new file mode 100644 index 0000000000..d3afffa21b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_func_arg_input.mlir @@ -0,0 +1,15 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +// CHECK-LABEL: func.func @test_func_arg_input +// CHECK-NOT: hivm.hir.copy +// CHECK-NOT: hivm.hir.fixpipe +// CHECK: return +module { + func.func @test_func_arg_input(%arg0: memref<128x128xf16>, %a: tensor<128x128xf16>, %b: tensor<128x128xf16>, %acc: tensor<128x128xf32>) { + %cst = arith.constant {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} 0.0 : f16 + %empty = tensor.empty() {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + %fill = linalg.fill {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%empty : tensor<128x128xf32>) -> tensor<128x128xf32> + %mm = linalg.matmul {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} ins(%a, %b : tensor<128x128xf16>, tensor<128x128xf16>) outs(%fill : tensor<128x128xf32>) -> tensor<128x128xf32> + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_inline_transpose.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_inline_transpose.mlir new file mode 100644 index 0000000000..ac7e423993 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_inline_transpose.mlir @@ -0,0 +1,26 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @test_inline_transpose(%arg0: memref<256x256xf16>) { + %alloc = memref.alloc() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<128x256xf16> + %t0 = bufferization.to_tensor %alloc {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<128x256xf16> to tensor<128x256xf16> + %alloc_1 = memref.alloc() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<256x256xf16> + %t1 = bufferization.to_tensor %alloc_1 {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<256x256xf16> to tensor<256x256xf16> + %cst = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} 0.0 : f16 + %empty = tensor.empty() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x256xf32> + %init = linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%empty : tensor<128x256xf32>) -> tensor<128x256xf32> + %mat = linalg.matmul {input_precision = "ieee", ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%t0, %t1 : tensor<128x256xf16>, tensor<256x256xf16>) outs(%init : tensor<128x256xf32>) -> tensor<128x256xf32> + %empty1 = tensor.empty() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : tensor<256x128xf32> + %transposed = linalg.transpose ins(%mat : tensor<128x256xf32>) outs(%empty1 : tensor<256x128xf32>) permutation = [1, 0] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} + %exp = math.exp %transposed {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "VECTOR"} : tensor<256x128xf32> + return + } +} + +// CHECK-LABEL: func.func @test_inline_transpose +// CHECK: %[[MATMUL_3:[a-z0-9_]+]] = linalg.matmul +// CHECK: hivm.hir.fixpipe {dma_mode = #hivm.dma_mode{{.*}}} ins(%[[MATMUL_3]] : tensor<128x256xf32>) +// CHECK: %[[TENSOR_4:[a-z0-9_]+]] = bufferization.to_tensor +// CHECK: linalg.transpose ins(%[[MATMUL_3]] +// CHECK: math.exp %[[TENSOR_4]] +// CHECK: return diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_non_tensor_type.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_non_tensor_type.mlir new file mode 100644 index 0000000000..a3ea9bc320 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_non_tensor_type.mlir @@ -0,0 +1,16 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +// CHECK-LABEL: func.func @test_non_tensor_type +// CHECK-NOT: hivm.hir.copy +// CHECK-NOT: hivm.hir.fixpipe +// CHECK: return +module { + func.func @test_non_tensor_type(%a: f32, %b: f32) { + %add = arith.addf %a, %b {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %mul = arith.mulf %add, %b {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : f32 + %empty = tensor.empty() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf32> + %fill = linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%mul : f32) outs(%empty : tensor<128x128xf32>) -> tensor<128x128xf32> + %mm = linalg.matmul {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%fill, %fill : tensor<128x128xf32>, tensor<128x128xf32>) outs(%fill : tensor<128x128xf32>) -> tensor<128x128xf32> + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_pure_cube.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_pure_cube.mlir new file mode 100644 index 0000000000..d5d7e39c0a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_pure_cube.mlir @@ -0,0 +1,20 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +// CHECK-LABEL: func.func @test_pure_cube +// CHECK-NOT: hivm.hir.copy +// CHECK-NOT: hivm.hir.fixpipe +// CHECK: return +module { + func.func @test_pure_cube(%arg0: memref<128x128xf16>) { + %cst = arith.constant {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} 0.0 : f16 + + %alloc = memref.alloc() {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xf16> + %t0 = bufferization.to_tensor %alloc {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : memref<128x128xf16> to tensor<128x128xf16> + %empty = tensor.empty() {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} : tensor<128x128xf16> + %fill = linalg.fill {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} ins(%cst : f16) outs(%empty : tensor<128x128xf16>) -> tensor<128x128xf16> + + %mm = linalg.matmul {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "CUBE"} ins(%t0, %fill : tensor<128x128xf16>, tensor<128x128xf16>) outs(%empty : tensor<128x128xf16>) -> tensor<128x128xf16> + + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_pure_vector.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_pure_vector.mlir new file mode 100644 index 0000000000..15c56000f0 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_pure_vector.mlir @@ -0,0 +1,20 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +// CHECK-LABEL: func.func @test_pure_vector +// CHECK-NOT: hivm.hir.copy +// CHECK-NOT: hivm.hir.fixpipe +// CHECK: return +module { + func.func @test_pure_vector(%arg0: memref<128x128xf32>) { + %cst = arith.constant {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} 1.0 : f32 + + %alloc = memref.alloc() {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xf32> + %t0 = bufferization.to_tensor %alloc {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : memref<128x128xf32> to tensor<128x128xf32> + %fill = linalg.fill {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f32) outs(%t0 : tensor<128x128xf32>) -> tensor<128x128xf32> + + %add = arith.addf %fill, %fill {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + %mul = arith.mulf %add, %fill {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : tensor<128x128xf32> + + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_refine_args_block_id.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_refine_args_block_id.mlir new file mode 100644 index 0000000000..4aceec9fc8 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_refine_args_block_id.mlir @@ -0,0 +1,56 @@ +// RUN: triton-opt --refine-args-block-id %s | FileCheck %s + +// Test for RefineArgsBlockId pass +// This pass moves iter_arg update operations to the block that first uses the iter_arg +// +// In the unfixed version (input): +// - block_id = 8 uses iter_arg %arg5 in arith.addi +// - block_id = 33 has the update op for iter_arg (index_cast + addi) +// - scf.yield yields the result from block_id = 33 +// +// After the pass (expected): +// - The update ops should be moved to block_id = 8 +// - block_id = 33 should no longer exist in the for loop body + +module { + func.func @test_refine_args_block_id() { + %c0_i32 = arith.constant 0 : i32 + %c32_i32 = arith.constant 32 : i32 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + + // %ext_value is defined outside the for loop, used inside + %ext_value = arith.constant 100 : i32 + + %alloc_0 = memref.alloc() {ssbuffer.block_id = 1 : i32} : memref<32x32xf32> + %alloc_1 = memref.alloc() {ssbuffer.block_id = 2 : i32} : memref<32x32xf32> + %tensor_0 = bufferization.to_tensor %alloc_0 restrict writable {ssbuffer.block_id = 3 : i32} : memref<32x32xf32> to tensor<32x32xf32> + %tensor_1 = bufferization.to_tensor %alloc_1 restrict writable {ssbuffer.block_id = 4 : i32} : memref<32x32xf32> to tensor<32x32xf32> + + // iter_args: %arg2=tensor, %arg3=tensor, %arg4=index, %arg5=index + %0:4 = scf.for %arg0 = %c0_i32 to %c32_i32 step %c32_i32 iter_args(%arg2 = %tensor_0, %arg3 = %tensor_1, %arg4 = %c0, %arg5 = %c0) -> (tensor<32x32xf32>, tensor<32x32xf32>, index, index) : i32 { + // block_id = 6: some computation + %1 = arith.index_cast %arg0 {ssbuffer.block_id = 6 : i32} : i32 to index + %2 = arith.addi %arg4, %1 {ssbuffer.block_id = 6 : i32} : index + + // block_id = 8: first user of %arg5 (iter_arg index 3) + %alloc_8 = memref.alloc() {ssbuffer.block_id = 8 : i32} : memref<32x32xf32> + %3 = arith.addi %arg5, %c1 {ssbuffer.block_id = 8 : i32} : index + + // block_id = 33: update op for iter_arg %arg5 (to be moved) + %4 = arith.index_cast %ext_value {ssbuffer.block_id = 33 : i32} : i32 to index + %5 = arith.addi %arg5, %4 {ssbuffer.block_id = 33 : i32} : index + + scf.yield %tensor_0, %tensor_1, %2, %5 : tensor<32x32xf32>, tensor<32x32xf32>, index, index + } {DataUse, ssbuffer.main_loop = 1 : i32} + + return + } +} + +// CHECK-LABEL: func.func @test_refine_args_block_id +// After the pass: +// CHECK: arith.index_cast {{.*}} {ssbuffer.block_id = 8 : i32} +// CHECK: arith.addi {{.*}} {ssbuffer.block_id = 8 : i32} +// block_id = 33 should be gone from the for loop body +// CHECK-NOT: ssbuffer.block_id = 33 diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_scalar_v2c_external_input.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_scalar_v2c_external_input.mlir new file mode 100644 index 0000000000..a8c1f7c8d8 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_scalar_v2c_external_input.mlir @@ -0,0 +1,48 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync %s | FileCheck %s + +// Scalar V->C dependency via external-input. +// +// A VECTOR block produces a 1D tensor (math.floor is a VECTOR-only op), then +// `tensor.extract`s an i32 scalar from it (memref-era scalar channel only +// carries i32 through the memref SSBuffer slot). A CUBE block +// consumes that scalar (arith.addi). analyzeExternalInputs sees the scalar as +// a CUBE-block external input defined by a VECTOR op and records a V->C +// dependency. +// +// inter-core-transfer-and-sync then routes the scalar through the SSBuffer +// scalar channel: +// VECTOR side: hivm.hir.pointer_cast + memref.store %extracted +// + sync_block_set[, , ] +// CUBE side: sync_block_wait[, , ] + memref.load +// + annotation.mark {memref_ext.volatile} +// The scalar PIPE_S sync stays isolated from the tensor flag space. +// CHECK-LABEL: func.func @scalar_v2c +// CHECK: %{{.*}} = tensor.extract +// CHECK: hivm.hir.pointer_cast({{.*}}) +// CHECK: memref.store %{{.*}}, %{{.*}}[] {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.crossCoreDeps = [0 : i32, 1 : i32], ssbuffer.transfer_id = 0 : i32} : memref> +// CHECK: hivm.hir.sync_block_set {{.*}}[, , ] flag = 1 +// CHECK: hivm.hir.sync_block_wait {{.*}}[, , ] flag = 1 +// CHECK: memref.load %{{.*}}[] {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE", ssbuffer.crossCoreDeps = [0 : i32, 0 : i32], ssbuffer.transfer_id = 0 : i32} : memref> +// CHECK: annotation.mark {{.*}} {memref_ext.volatile, {{.*}}} : i32 +// The CUBE op must consume the loaded scalar, not the raw extract. +// CHECK: arith.addi +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + func.func @scalar_v2c(%arg0: memref {tt.tensor_kind = 0 : i32}) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "mix", parallel_mode = "simd"} { + %cst = arith.constant {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} 1.000000e+00 : f32 + %c0 = arith.constant {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} 0 : index + %t = tensor.empty() {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %filled = linalg.fill {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : f32) outs(%t : tensor<64xf32>) -> tensor<64xf32> + %floor = math.floor %filled {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> + %cast = arith.fptosi %floor {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xf32> to tensor<64xi32> + %scalar = tensor.extract %cast[%c0] {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} : tensor<64xi32> + %cst_c = arith.constant {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} 2 : i32 + %used = arith.addi %scalar, %cst_c {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : i32 + %dst = memref.reinterpret_cast %arg0 to offset: [0], sizes: [1], strides: [1] : memref to memref<1xi32, strided<[1]>> + %subview = memref.subview %dst[0] [1] [1] {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : memref<1xi32, strided<[1]>> to memref<1xi32, strided<[1]>> + %tt = tensor.empty() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : tensor<1xi32> + %filled_c = linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} ins(%used : i32) outs(%tt : tensor<1xi32>) -> tensor<1xi32> + %extracted_slice = tensor.extract_slice %filled_c[0] [1] [1] {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : tensor<1xi32> to tensor<1xi32> + bufferization.materialize_in_destination %extracted_slice in writable %subview {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "CUBE"} : (tensor<1xi32>, memref<1xi32, strided<[1]>>) -> () + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_skip_cases.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_skip_cases.mlir new file mode 100644 index 0000000000..ff7caedfe9 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_skip_cases.mlir @@ -0,0 +1,140 @@ +// RUN: triton-opt -split-input-file --analyze-cube-control-flow-input-chain -verify-diagnostics -debug %s 2>&1 | FileCheck %s + +// ============================================================================ +// Scenario 1: Fallback case for linalg.reduce (Vector Only Op) +// linalg.reduce is unconditionally considered vector-only and triggers fallback. +// ============================================================================ + +func.func @test_scf_if_with_linalg_reduce(%input : tensor<16x16xf32>, %init : tensor<16xf32>, %c0 : index, %zero : f32) { + scope.scope : () -> () { + %reduced = linalg.reduce ins(%input : tensor<16x16xf32>) outs(%init : tensor<16xf32>) dimensions = [0] + (%in: f32, %out: f32) { + %sum = arith.addf %in, %out : f32 + linalg.yield %sum : f32 + } + %val = tensor.extract %reduced[%c0] : tensor<16xf32> + %cond = arith.cmpf ogt, %val, %zero : f32 + + // CHECK: [analyze-cube-control-flow-input-chain] Fallback reason: incompatible upstream op for control flow: {{.*}} = linalg.reduce + scf.if %cond { + // some body + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Scenario 2: Fallback case for arith.select with RankedTensorType result +// arith.select returning a RankedTensorType is considered vector-only. +// ============================================================================ + +func.func @test_scf_for_with_tensor_select(%cond : i1, %t1 : tensor<16xindex>, %t2 : tensor<16xindex>, %c0 : index, %step : index) { + scope.scope : () -> () { + %select_tensor = arith.select %cond, %t1, %t2 : i1, tensor<16xindex> + %ub = tensor.extract %select_tensor[%c0] : tensor<16xindex> + %lb = arith.constant 0 : index + + // CHECK: [analyze-cube-control-flow-input-chain] Fallback reason: incompatible upstream op for control flow: {{.*}} = arith.select + scf.for %i = %lb to %ub step %step { + // some body + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Scenario 3: Fallback case for math.floor with RankedTensorType result +// math.floor returning a RankedTensorType is considered vector-only. +// ============================================================================ + +func.func @test_scf_while_with_tensor_floor(%input : tensor<16xf32>, %c0 : index) { + scope.scope : () -> () { + %floor_tensor = math.floor %input : tensor<16xf32> + %init_val = tensor.extract %floor_tensor[%c0] : tensor<16xf32> + + // CHECK: [analyze-cube-control-flow-input-chain] Fallback reason: incompatible upstream op for control flow: {{.*}} = math.floor + %result = scf.while (%arg = %init_val) : (f32) -> f32 { + %cond = arith.constant true + scf.condition(%cond) %arg : f32 + } do { + ^bb0(%arg2: f32): + scf.yield %arg2 : f32 + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Scenario 4: Successful case with scalar select (No Fallback) +// arith.select returning a scalar is compatible and should NOT trigger fallback. +// ============================================================================ + +// CHECK-LABEL: func.func @test_scf_if_with_scalar_select +func.func @test_scf_if_with_scalar_select(%cond : i1, %val_a : f32, %val_b : f32) { + scope.scope : () -> () { + // This select returns a scalar (f32), which is NOT a RankedTensorType. + %selected = arith.select %cond, %val_a, %val_b : f32 + %zero = arith.constant 0.0 : f32 + %if_cond = arith.cmpf ogt, %selected, %zero : f32 + + // CHECK-NOT: Fallback reason: + scf.if %if_cond { + // some body + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Scenario 5: Successful case with scalar floor (No Fallback) +// math.floor returning a scalar is compatible and should NOT trigger fallback. +// ============================================================================ + +// CHECK-LABEL: func.func @test_scf_if_with_scalar_floor +func.func @test_scf_if_with_scalar_floor(%val : f32) { + scope.scope : () -> () { + // This math.floor returns a scalar (f32), which is NOT a RankedTensorType. + %floored = math.floor %val : f32 + %zero = arith.constant 0.0 : f32 + %if_cond = arith.cmpf ogt, %floored, %zero : f32 + + // CHECK-NOT: Fallback reason: + scf.if %if_cond { + // some body + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} + +// ----- + +// ============================================================================ +// Scenario 6: Normal control flow without any vector-only ops (No Fallback) +// ============================================================================ + +// CHECK-LABEL: func.func @test_scf_if_fully_compatible +func.func @test_scf_if_fully_compatible(%val_a : f32, %val_b : f32) { + scope.scope : () -> () { + %cond = arith.cmpf ogt, %val_a, %val_b : f32 + + // CHECK-NOT: Fallback reason: + scf.if %cond { + // some body + } + scope.return + } {hivm.tcore_type = #hivm.tcore_type} + return +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_unaligned_int8_transpose.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_unaligned_int8_transpose.mlir new file mode 100644 index 0000000000..a951d57eba --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/SplitDataflow/test_unaligned_int8_transpose.mlir @@ -0,0 +1,35 @@ +// RUN: triton-opt --add-block-id-for-control-ops --data-dependency-analysis --inter-core-transfer-and-sync --mark-main-loop %s | FileCheck %s + +module { + func.func @test_unaligned_int8_transpose() { + %cst = arith.constant {ssbuffer.block_id = 0 : i32, ssbuffer.core_type = "VECTOR"} 1 : i8 + %t0 = tensor.empty() {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<3x111xi8> + %fill = linalg.fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} ins(%cst : i8) outs(%t0 : tensor<3x111xi8>) -> tensor<3x111xi8> + %exp = arith.addi %fill, %fill {ssbuffer.block_id = 1 : i32, ssbuffer.core_type = "VECTOR"} : tensor<3x111xi8> + + %alloc = memref.alloc() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<3x104xi8> + %t1 = bufferization.to_tensor %alloc {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : memref<3x104xi8> to tensor<3x104xi8> + %empty = tensor.empty() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : tensor<111x104xi32> + %cst_cube = arith.constant {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} 0 : i32 + %init = linalg.fill {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%cst_cube : i32) outs(%empty : tensor<111x104xi32>) -> tensor<111x104xi32> + %empty2 = tensor.empty() {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} : tensor<111x3xi8> + %trans = linalg.transpose ins(%exp : tensor<3x111xi8>) outs(%empty2 : tensor<111x3xi8>) permutation = [1, 0] {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} + %mat = linalg.matmul {ssbuffer.block_id = 2 : i32, ssbuffer.core_type = "CUBE"} ins(%trans, %t1 : tensor<111x3xi8>, tensor<3x104xi8>) outs(%init : tensor<111x104xi32>) -> tensor<111x104xi32> + return + } +} + +// CHECK-LABEL: func.func @test_unaligned_int8_transpose + +// CHECK: %[[ADD_2:[a-z0-9_]+]] = arith.addi +// CHECK: tensor.insert_slice %[[ADD_2]] into {{.*}}[0, 0] [3, 111] [1, 1] {{.*}} : tensor<3x111xi8> into tensor<32x128xi8> +// CHECK: tensor.reshape +// CHECK: linalg.transpose +// CHECK: %[[RESHAPE_1:[a-z0-9_]+]] = tensor.reshape +// CHECK: hivm.hir.copy ins(%[[RESHAPE_1]] : tensor<4x1x32x32xi8>) outs({{.*}} : memref<4x1x32x32xi8, #hivm.address_space>) + +// CHECK: hivm.hir.convert_layout {{.*}} output_shape [3, 111] {{.*}} : (memref<4x1x32x32xi8, #hivm.address_space>) -> memref<3x111xi8, #hivm.address_space> +// CHECK: memref.memory_space_cast +// CHECK: %[[TENSOR_11:[a-z0-9_]+]] = bufferization.to_tensor +// CHECK: %[[TRANSPOSED_4:[a-z0-9_]+]] = linalg.transpose ins(%[[TENSOR_11]] : tensor<3x111xi8>) +// CHECK: linalg.matmul {{.*}} ins(%[[TRANSPOSED_4]], {{.*}} : tensor<111x3xi8>, tensor<3x104xi8>) outs({{.*}} : tensor<111x104xi32>) -> tensor<111x104xi32> diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/StandardizeOp/split_matmul_aliased_input.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/StandardizeOp/split_matmul_aliased_input.mlir new file mode 100644 index 0000000000..6babe3343e --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/StandardizeOp/split_matmul_aliased_input.mlir @@ -0,0 +1,19 @@ +// RUN: triton-opt --ssbuf-standardize-op-pattern-match %s | FileCheck %s + +// The RHS and accumulator alias. Splitting must replace only the accumulator. +module { + // CHECK-LABEL: func.func @split_matmul_with_aliased_rhs_and_accumulator( + // CHECK-SAME: %[[A:[^:]+]]: tensor<32x32xf32>, %[[B:[^:]+]]: tensor<32x32xf32> + // CHECK: %[[ZERO:.*]] = linalg.fill + // CHECK: %[[MATMUL:.*]] = linalg.matmul {{.*}}ins(%[[A]], %[[B]] : tensor<32x32xf32>, tensor<32x32xf32>) outs(%[[ZERO]] : tensor<32x32xf32>) + // CHECK: %[[RESULT:.*]] = arith.addf %[[MATMUL]], %[[B]] {{.*}}ssbuffer.add_from_matmul + // CHECK: return %[[RESULT]] : tensor<32x32xf32> + func.func @split_matmul_with_aliased_rhs_and_accumulator( + %a: tensor<32x32xf32>, %b: tensor<32x32xf32>) + -> tensor<32x32xf32> { + %matmul = linalg.matmul {input_precision = "ieee"} + ins(%a, %b : tensor<32x32xf32>, tensor<32x32xf32>) + outs(%b : tensor<32x32xf32>) -> tensor<32x32xf32> + return %matmul : tensor<32x32xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/preserve_control_attrs_canonicalize_test.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/preserve_control_attrs_canonicalize_test.mlir new file mode 100644 index 0000000000..eee11778e8 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/preserve_control_attrs_canonicalize_test.mlir @@ -0,0 +1,159 @@ +// RUN: triton-opt %s --preserve-control-attrs-canonicalize --mlir-print-op-generic | FileCheck %s + +// CHECK-LABEL: "func.func"() <{{.*}}sym_name = "for_attr_survives_unused_iter_arg_canonicalize" +// CHECK: "scf.for" +// CHECK: }) {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.main_loop = 1 : i32 + +module { + func.func @for_attr_survives_unused_iter_arg_canonicalize(%arg0: i32) -> i32 { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %0:2 = scf.for %iv = %c0 to %c4 step %c1 iter_args(%acc = %arg0, %dead = %c0_i32) -> (i32, i32) { + %1 = arith.addi %acc, %c1_i32 : i32 + scf.yield %1, %dead : i32, i32 + } {ssbuffer.block_id = 7 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.main_loop = 1 : i32} + return %0#0 : i32 + } +} + +// ----- + +// CHECK-LABEL: "func.func"() <{{.*}}sym_name = "while_attr_survives_unused_result_canonicalize" +// CHECK: "scf.while" +// CHECK: }) {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.main_loop = 1 : i32} : (i32, i32) -> (i32, i32) + +module { + func.func @while_attr_survives_unused_result_canonicalize(%arg0: i32, %limit: i32) -> i32 { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %0:2 = scf.while (%acc = %arg0, %dead = %c0_i32) : (i32, i32) -> (i32, i32) { + %1 = arith.cmpi slt, %dead, %limit : i32 + scf.condition(%1) %acc, %dead : i32, i32 + } do { + ^bb0(%acc_iter: i32, %dead_iter: i32): + %1 = arith.addi %acc_iter, %c1_i32 : i32 + %2 = arith.addi %dead_iter, %c1_i32 : i32 + scf.yield %1, %2 : i32, i32 + } attributes {ssbuffer.block_id = 9 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.main_loop = 1 : i32} + return %0#0 : i32 + } +} + +// ----- + +// CHECK-LABEL: "func.func"() <{{.*}}sym_name = "if_attr_survives_unused_result_canonicalize" +// CHECK-DAG: "arith.select" +// CHECK-DAG: "scf.if" +// CHECK: }) {hivm.unlikely_condition, ssbuffer.block_id = 11 : i32} : (i1) -> () + +module { + func.func @if_attr_survives_unused_result_canonicalize(%cond: i1, %arg0: i32, %buf: memref<1xi32>) -> i32 { + %c0 = arith.constant 0 : index + %c0_i32 = arith.constant 0 : i32 + %0:2 = "scf.if"(%cond) ({ + memref.store %arg0, %buf[%c0] : memref<1xi32> + "scf.yield"(%arg0, %c0_i32) : (i32, i32) -> () + }, { + memref.store %c0_i32, %buf[%c0] : memref<1xi32> + "scf.yield"(%c0_i32, %arg0) : (i32, i32) -> () + }) {hivm.unlikely_condition, ssbuffer.block_id = 11 : i32} : (i1) -> (i32, i32) + return %0#0 : i32 + } +} + +// ----- + +// CHECK-LABEL: "func.func"() <{{.*}}sym_name = "merged_zero_result_if_preserves_attrs" +// CHECK: "scf.if" +// CHECK: "memref.store" +// CHECK: "memref.store" +// CHECK: }) {hivm.unlikely_condition, ssbuffer.block_id = 7 : i32} : (i1) -> () +// CHECK-NOT: "scf.if" +// CHECK: "func.return" + +module { + func.func @merged_zero_result_if_preserves_attrs(%cond: i1, %lhs: memref<1xi32>, %rhs: memref<1xi32>) { + %c0 = arith.constant 0 : index + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + scf.if %cond { + memref.store %c1_i32, %lhs[%c0] : memref<1xi32> + } {hivm.unlikely_condition, ssbuffer.block_id = 7 : i32} + scf.if %cond { + memref.store %c2_i32, %rhs[%c0] : memref<1xi32> + } {hivm.unlikely_condition, ssbuffer.block_id = 7 : i32} + return + } +} + + +// ----- + +// CHECK-LABEL: "func.func"() <{{.*}}sym_name = "nested_for_replacement_keeps_outer_main_loop" +// CHECK: %{{.*}} = "scf.for"( +// CHECK: }) {ssbuffer.block_id = 16 : i32} : (index, index, index, i32) -> i32 +// CHECK-NOT: ssbuffer.main_loop +// CHECK-NOT: ssbuffer.core_type +// CHECK-NEXT: "scf.yield" +// CHECK-NEXT: }) {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.main_loop = 0 : i32} : (index, index, index, i32) -> i32 + +module { + func.func @nested_for_replacement_keeps_outer_main_loop(%seed: i32) -> i32 { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0 = arith.constant 0 : index + %c2 = arith.constant 2 : index + %c1 = arith.constant 1 : index + %0:2 = scf.for %iv = %c0 to %c2 step %c1 iter_args(%acc = %seed, %dead = %c0_i32) -> (i32, i32) { + %1 = scf.for %j = %c0 to %c2 step %c1 iter_args(%sum = %acc) -> (i32) { + %2 = arith.addi %sum, %c1_i32 : i32 + scf.yield %2 : i32 + } {ssbuffer.block_id = 16 : i32} + scf.yield %1, %dead : i32, i32 + } {ssbuffer.block_id = 17 : i32, ssbuffer.core_type = "VECTOR", ssbuffer.main_loop = 0 : i32} + return %0#0 : i32 + } +} + +// ----- + + +// CHECK-LABEL: "func.func"() <{{.*}}sym_name = "nested_multi_for_main_loop_not_inner" +// CHECK: %{{.*}}:2 = "scf.for"( +// CHECK: }) {ssbuffer.block_id = 16 : i32} : (index, index, index, i32) -> i32 +// CHECK-NOT: ssbuffer.main_loop +// CHECK-NOT: ssbuffer.pipe_stage +// CHECK-NEXT: %{{.*}} = "scf.for"( +// CHECK-NEXT: ^bb0 +// CHECK: }) {ssbuffer.pipe_stage = 3 : i32} : (index, index, index, i32) -> i32 +// CHECK-NOT: ssbuffer.main_loop +// CHECK-NOT: ssbuffer.block_id +// CHECK-NEXT: "scf.yield" +// CHECK-NEXT: }) {ssbuffer.main_loop = 0 : i32} : (index, index, index, i32, i32) -> (i32, i32) +// CHECK-NOT: ssbuffer.block_id +// CHECK-NOT: ssbuffer.pipe_stage + +module { + func.func @nested_multi_for_main_loop_not_inner(%seed: i32) -> (i32, i32) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0 = arith.constant 0 : index + %c2 = arith.constant 2 : index + %c1 = arith.constant 1 : index + %0:2 = scf.for %iv = %c0 to %c2 step %c1 iter_args(%acc = %seed, %dead = %c0_i32) -> (i32, i32) { + %1 = scf.for %j = %c0 to %c2 step %c1 iter_args(%sum = %acc) -> (i32) { + %2 = arith.addi %sum, %c1_i32 : i32 + scf.yield %2 : i32 + } {ssbuffer.block_id = 16 : i32} + %3 = scf.for %k = %c0 to %c2 step %c1 iter_args(%sum2 = %acc) -> (i32) { + %4 = arith.addi %sum2, %c1_i32 : i32 + scf.yield %4 : i32 + } {ssbuffer.pipe_stage = 3 : i32} + scf.yield %1, %3 : i32, i32 + } {ssbuffer.main_loop = 0 : i32} + return %0#0, %0#1 : i32, i32 + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/separate_cv_scope_canonicalize_ut.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/separate_cv_scope_canonicalize_ut.mlir new file mode 100644 index 0000000000..e0191e1cff --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/separate_cv_scope_canonicalize_ut.mlir @@ -0,0 +1,912 @@ +// RUN: triton-opt %s --separate-cv-scope --preserve-control-attrs-canonicalize | FileCheck %s + +//===----------------------------------------------------------------------===// +// SeparateCVScope + canonicalize focused tests +// +// These cases keep values alive by storing into sink memrefs passed as function +// arguments, instead of local alloc/store chains that canonicalize can delete as +// dead. Control-flow tests use dynamic conditions or dynamic loop bounds, and +// the owning-side results are consumed after the control-flow op. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @basic_scope_split_and_cleanup( +// CHECK: scope.scope : () -> () { +// CHECK: arith.addi +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: arith.muli +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @basic_scope_split_and_cleanup(%vec_in: i32, %cube_in: i32, %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c2v = arith.constant {ssbuffer.core_type = "VECTOR"} 2 : i32 + %c3v = arith.constant {ssbuffer.core_type = "VECTOR"} 3 : i32 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i32 + %0 = arith.addi %vec_in, %c2v {ssbuffer.core_type = "VECTOR"} : i32 + %1 = arith.addi %0, %c3v {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.muli %cube_in, %c2c {ssbuffer.core_type = "CUBE"} : i32 + memref.store %1, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @container_if_without_core_type( +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @container_if_without_core_type(%cond: i1, %vec_in: i32, %cube_in: i32, %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c2v = arith.constant {ssbuffer.core_type = "VECTOR"} 2 : i32 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i32 + scf.if %cond { + %0 = arith.addi %vec_in, %c2v {ssbuffer.core_type = "VECTOR"} : i32 + %1 = arith.muli %cube_in, %c2c {ssbuffer.core_type = "CUBE"} : i32 + memref.store %0, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %1, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + } else { + %0 = arith.subi %vec_in, %c2v {ssbuffer.core_type = "VECTOR"} : i32 + %1 = arith.addi %cube_in, %c2c {ssbuffer.core_type = "CUBE"} : i32 + memref.store %0, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %1, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + } + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @mixed_if_yield_neutralize_scalar( +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: scf.yield +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: scf.yield +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @mixed_if_yield_neutralize_scalar(%cond: i1, %vec_in: i32, %cube_in: i32, %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i32 + %0:2 = scf.if %cond -> (i32, i32) { + %1 = arith.addi %vec_in, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.addi %cube_in, %c2c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %1, %2 : i32, i32 + } else { + %1 = arith.subi %vec_in, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.subi %cube_in, %c2c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %1, %2 : i32, i32 + } {ssbuffer.core_type = "VECTOR, CUBE"} + %1 = arith.addi %0#0, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.addi %0#1, %c2c {ssbuffer.core_type = "CUBE"} : i32 + memref.store %1, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @mixed_for_iter_args_index_neutralize( +// CHECK: scope.scope : () -> () { +// CHECK: scf.for +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.for +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @mixed_for_iter_args_index_neutralize(%lb: i32, %ub: i32, %vec_init: i32, %cube_init: index, %outv: memref<1xi32>, %outc: memref<1xindex>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %stepv = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %stepc = arith.constant {ssbuffer.core_type = "CUBE"} 2 : index + %0:2 = scf.for %i = %lb to %ub step %stepv iter_args(%a = %vec_init, %b = %cube_init) -> (i32, index) : i32 { + %1 = arith.addi %a, %stepv {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.addi %b, %stepc {ssbuffer.core_type = "CUBE"} : index + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %1, %2 : i32, index + } {ssbuffer.core_type = "VECTOR, CUBE"} + %1 = arith.addi %0#0, %stepv {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.addi %0#1, %stepc {ssbuffer.core_type = "CUBE"} : index + memref.store %1, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xindex> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @for_forwarding_use_can_be_ignored( +// CHECK: scope.scope : () -> () { +// CHECK: scf.for +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.for +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @for_forwarding_use_can_be_ignored(%lb: i32, %ub: i32, %vec_init: i32, %cube_init: i32, %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %c1c = arith.constant {ssbuffer.core_type = "CUBE"} 1 : i32 + %0:2 = scf.for %i = %lb to %ub step %c1v iter_args(%a = %vec_init, %b = %cube_init) -> (i32, i32) : i32 { + %1 = arith.addi %a, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.addi %b, %c1c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %1, %2 : i32, i32 + } {ssbuffer.core_type = "VECTOR, CUBE"} + %1:2 = scf.for %j = %lb to %ub step %c1v iter_args(%x = %0#0, %y = %0#1) -> (i32, i32) : i32 { + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %x, %y : i32, i32 + } {ssbuffer.core_type = "VECTOR, CUBE"} + %2 = arith.addi %1#0, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %3 = arith.addi %1#1, %c1c {ssbuffer.core_type = "CUBE"} : i32 + memref.store %2, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %3, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @for_cross_slot_yield_use_does_not_preserve_source( +// CHECK: scope.scope : () -> () { +// CHECK: scf.for +// CHECK: arith.addf +// CHECK: arith.addf +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.for +// CHECK-NOT: arith.addf +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @for_cross_slot_yield_use_does_not_preserve_source(%lb: i32, %ub: i32, %vec_src: f32, %vec_dst: f32, %cube_init: i32, %outv: memref<1xf32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %vf1 = arith.constant {ssbuffer.core_type = "VECTOR"} 1.000000e+00 : f32 + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %c1c = arith.constant {ssbuffer.core_type = "CUBE"} 1 : i32 + %0:3 = scf.for %i = %lb to %ub step %c1v iter_args(%a = %vec_src, %b = %vec_dst, %c = %cube_init) -> (f32, f32, i32) : i32 { + %1 = arith.addf %a, %vf1 {ssbuffer.core_type = "VECTOR"} : f32 + %2 = arith.addf %1, %b {ssbuffer.core_type = "VECTOR"} : f32 + %3 = arith.addi %c, %c1c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, VECTOR, CUBE"} %1, %2, %3 : f32, f32, i32 + } {ssbuffer.core_type = "VECTOR, VECTOR, CUBE"} + %1 = arith.addf %0#1, %vf1 {ssbuffer.core_type = "VECTOR"} : f32 + %2 = arith.addi %0#2, %c1c {ssbuffer.core_type = "CUBE"} : i32 + memref.store %1, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xf32> + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @while_dependency_preserved_for_cube( +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: scf.while +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @while_dependency_preserved_for_cube(%cond: i1, %vec_init: i64, %cube_init: i64, %limit: i64, %outv: memref<1xi64>, %outc: memref<1xi64>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i64 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i64 + %0 = scf.if %cond -> (i64) { + %1 = arith.addi %vec_init, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR"} %1 : i64 + } else { + %1 = arith.subi %limit, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR"} %1 : i64 + } {ssbuffer.core_type = "VECTOR"} + %1:3 = scf.while (%a = %vec_init, %b = %cube_init, %c = %0) : (i64, i64, i64) -> (i64, i64, i64) { + %2 = arith.addi %a, %c {ssbuffer.core_type = "VECTOR"} : i64 + %3 = arith.cmpi slt, %2, %limit {ssbuffer.core_type = "VECTOR"} : i64 + scf.condition(%3) %2, %b, %c : i64, i64, i64 + } do { + ^bb0(%a_iter: i64, %b_iter: i64, %c_iter: i64): + %2 = arith.addi %b_iter, %c2c {ssbuffer.core_type = "CUBE"} : i64 + scf.yield {ssbuffer.core_type = "CUBE, CUBE, VECTOR"} %a_iter, %2, %c_iter : i64, i64, i64 + } attributes {ssbuffer.core_type = "CUBE, CUBE, VECTOR"} + %2 = arith.addi %1#1, %c2c {ssbuffer.core_type = "CUBE"} : i64 + %3 = arith.addi %1#2, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + memref.store %3, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi64> + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi64> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @while_inner_if_terminator_use_blocks_dead_shell_erase( +// CHECK: scope.scope : () -> () { +// CHECK: scf.while +// CHECK: scf.if +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.while +// CHECK: scf.if +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @while_inner_if_terminator_use_blocks_dead_shell_erase(%if_cond: i1, %vec_init: i32, %cube_init: i32, %limit: i32, %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i32 + %0:2 = scf.while (%v = %vec_init, %c = %cube_init) : (i32, i32) -> (i32, i32) { + %keep = arith.cmpi slt, %v, %limit {ssbuffer.core_type = "VECTOR"} : i32 + scf.condition(%keep) %v, %c : i32, i32 + } do { + ^bb0(%v_iter: i32, %c_iter: i32): + %next_v = scf.if %if_cond -> (i32) { + %inc_v = arith.addi %v_iter, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR"} %inc_v : i32 + } else { + scf.yield {ssbuffer.core_type = "VECTOR"} %v_iter : i32 + } {ssbuffer.core_type = "VECTOR"} + %next_c = arith.addi %c_iter, %c2c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %next_v, %next_c : i32, i32 + } attributes {ssbuffer.core_type = "VECTOR, CUBE"} + %1 = arith.addi %0#0, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.addi %0#1, %c2c {ssbuffer.core_type = "CUBE"} : i32 + memref.store %1, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @while_forwarding_use_can_be_ignored( +// CHECK: scope.scope : () -> () { +// CHECK: scf.while +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.while +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @while_forwarding_use_can_be_ignored(%vec_init: i64, %cube_init: i64, %limit: i64, %outv: memref<1xi64>, %outc: memref<1xi64>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i64 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i64 + %0:2 = scf.while (%v = %vec_init, %c = %cube_init) : (i64, i64) -> (i64, i64) { + %keep = arith.cmpi slt, %v, %limit {ssbuffer.core_type = "VECTOR"} : i64 + scf.condition(%keep) %v, %c : i64, i64 + } do { + ^bb0(%v_iter: i64, %c_iter: i64): + %nv = arith.addi %v_iter, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + %nc = arith.addi %c_iter, %c2c {ssbuffer.core_type = "CUBE"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %nv, %nc : i64, i64 + } attributes {ssbuffer.core_type = "VECTOR, CUBE"} + %1:2 = scf.while (%x = %0#0, %y = %0#1) : (i64, i64) -> (i64, i64) { + %keep = arith.cmpi slt, %x, %limit {ssbuffer.core_type = "VECTOR"} : i64 + scf.condition(%keep) %x, %y : i64, i64 + } do { + ^bb0(%x_iter: i64, %y_iter: i64): + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %x_iter, %y_iter : i64, i64 + } attributes {ssbuffer.core_type = "VECTOR, CUBE"} + %2 = arith.addi %1#0, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + %3 = arith.addi %1#1, %c2c {ssbuffer.core_type = "CUBE"} : i64 + memref.store %2, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi64> + memref.store %3, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi64> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @for_same_slot_loop_carry_preserved( +// CHECK: scope.scope : () -> () { +// CHECK: scf.for +// CHECK: scf.if +// CHECK: scf.while +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: %[[FOR:.*]]:2 = scf.for +// CHECK: %[[IF:.*]]:2 = scf.if +// CHECK: %[[WHILE:.*]]:2 = scf.while +// CHECK: arith.extsi +// CHECK: scf.yield %[[WHILE]]#0, %[[WHILE]]#1 : i32, i64 +// CHECK: scf.yield %[[IF]]#0, %[[IF]]#1 : i32, i64 +// CHECK: memref.store %[[FOR]]#1 +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @for_same_slot_loop_carry_preserved(%lb: i32, %ub: i32, %cond: i1, %vec_init: i32, %cube_init: i64, %outv: memref<1xi32>, %outc: memref<1xi64>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %c2v = arith.constant {ssbuffer.core_type = "VECTOR"} 2 : i32 + %c1c = arith.constant {ssbuffer.core_type = "CUBE"} 1 : i64 + %0:2 = scf.for %i = %lb to %ub step %c1v iter_args(%state = %vec_init, %cube = %cube_init) -> (i32, i64) : i32 { + %1:2 = scf.if %cond -> (i32, i64) { + %2 = arith.addi %state, %c2v {ssbuffer.core_type = "VECTOR"} : i32 + %3:2 = scf.while (%iter = %state, %acc = %cube) : (i32, i64) -> (i32, i64) { + %4 = arith.cmpi slt, %iter, %2 {ssbuffer.core_type = "VECTOR"} : i32 + scf.condition(%4) %iter, %acc : i32, i64 + } do { + ^bb0(%iter_arg: i32, %acc_arg: i64): + %4 = arith.addi %iter_arg, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %5 = arith.extsi %iter_arg {ssbuffer.core_type = "CUBE"} : i32 to i64 + %6 = arith.addi %acc_arg, %5 {ssbuffer.core_type = "CUBE"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %4, %6 : i32, i64 + } attributes {ssbuffer.core_type = "VECTOR, CUBE"} + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %3#0, %3#1 : i32, i64 + } else { + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %state, %cube : i32, i64 + } {ssbuffer.core_type = "VECTOR, CUBE"} + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %1#0, %1#1 : i32, i64 + } {ssbuffer.core_type = "VECTOR, CUBE"} + memref.store %0#0, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %0#1, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi64> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @memref_slot_neutralize_uses_alloc( +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: memref.load +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @memref_slot_neutralize_uses_alloc(%cond: i1, %src: memref<4xi32>, %vec_seed: i32, %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %cube_idx = arith.constant {ssbuffer.core_type = "CUBE"} 2 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %0:2 = scf.if %cond -> (i32, memref<4xi32>) { + %1 = arith.addi %vec_seed, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %1, %src : i32, memref<4xi32> + } else { + %1 = arith.subi %vec_seed, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %1, %src : i32, memref<4xi32> + } {ssbuffer.core_type = "VECTOR, CUBE"} + %1 = arith.addi %0#0, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %2 = memref.load %0#1[%cube_idx] {ssbuffer.core_type = "CUBE"} : memref<4xi32> + memref.store %1, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @static_tensor_slot_neutralize_uses_dense_zero( +// CHECK: scope.scope : () -> () { +// CHECK: arith.addf +// CHECK: bufferization.materialize_in_destination +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @static_tensor_slot_neutralize_uses_dense_zero(%cond: i1, %src: tensor<4xf32>, %cube_seed: i32, %outv: memref<4xf32>, %outc: memref<1xi32>) { + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i32 + %0:2 = scf.if %cond -> (tensor<4xf32>, i32) { + %1 = arith.addi %cube_seed, %c2c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %src, %1 : tensor<4xf32>, i32 + } else { + %1 = arith.subi %cube_seed, %c1v {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %src, %1 : tensor<4xf32>, i32 + } {ssbuffer.core_type = "VECTOR, CUBE"} + %1 = arith.addf %0#0, %src {ssbuffer.core_type = "VECTOR"} : tensor<4xf32> + %2 = arith.addi %0#1, %c2c {ssbuffer.core_type = "CUBE"} : i32 + bufferization.materialize_in_destination %1 in writable %outv {ssbuffer.core_type = "VECTOR"} : (tensor<4xf32>, memref<4xf32>) -> () + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @cross_scope_live_user_keeps_producer( +// CHECK: scope.scope : () -> () { +// CHECK: arith.muli +// CHECK: arith.addi +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: arith.muli +// CHECK-NOT: arith.addi +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @cross_scope_live_user_keeps_producer(%cube_seed: i32, %vec_seed: i32, %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i32 + %0 = arith.muli %cube_seed, %c2c {ssbuffer.core_type = "CUBE"} : i32 + %1 = arith.addi %0, %vec_seed {ssbuffer.core_type = "VECTOR"} : i32 + memref.store %1, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %0, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @short_core_type_list_falls_back_to_first( +// CHECK: scope.scope : () -> () { +// CHECK-NOT: scf.if +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: scf.yield +// CHECK: memref.store +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @short_core_type_list_falls_back_to_first(%cond: i1, %vec_seed: i32, %cube_seed0: i32, %cube_seed1: i32, %outv: memref<1xi32>, %outc0: memref<1xi32>, %outc1: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc0 = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %idxc1 = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i32 + %0:2 = scf.if %cond -> (i32, i32) { + %1 = arith.addi %cube_seed0, %c2c {ssbuffer.core_type = "CUBE"} : i32 + %2 = arith.subi %cube_seed1, %c2c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "CUBE"} %1, %2 : i32, i32 + } else { + %1 = arith.subi %cube_seed0, %c2c {ssbuffer.core_type = "CUBE"} : i32 + %2 = arith.addi %cube_seed1, %c2c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "CUBE"} %1, %2 : i32, i32 + } {ssbuffer.core_type = "CUBE"} + %1 = arith.addi %vec_seed, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + %2 = arith.addi %0#0, %c2c {ssbuffer.core_type = "CUBE"} : i32 + %3 = arith.addi %0#1, %c2c {ssbuffer.core_type = "CUBE"} : i32 + memref.store %1, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + memref.store %2, %outc0[%idxc0] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + memref.store %3, %outc1[%idxc1] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @vector_if_bound_with_cube_loop_side_effect( +// A VECTOR-only scf.if computes a loop upper bound. The for loop itself is +// VECTOR-typed (all results are VECTOR), but its body contains a CUBE +// side-effect (memref.store). Without the fix, the CUBE scope neutralizes +// the scf.if yields to 0 because findScopeRelevantUser only follows the for's +// SSA results (all VECTOR, no CUBE users), collapsing the bound to 0 and +// causing canonicalize to erase the CUBE store entirely. +// VECTOR scope: scf.if folds to arith.select (no side effects in branches), +// for loop accumulates with VECTOR result, memref.store to outv +// CHECK: scope.scope : () -> () { +// CHECK-NEXT: %[[UB_V:.*]] = arith.select +// CHECK-NEXT: %[[RES_V:.*]] = scf.for {{.*}} to %[[UB_V]] +// CHECK-NEXT: %{{.*}} = arith.addi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } +// CHECK-NEXT: memref.store %[[RES_V]] +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CUBE scope: scf.if also folds to arith.select (preserved, NOT neutralized +// to 0); for loop runs the correct number of times with CUBE memref.store +// CHECK-NEXT: scope.scope : () -> () { +// CHECK-NEXT: %[[UB_C:.*]] = arith.select +// CHECK-NEXT: scf.for {{.*}} to %[[UB_C]] +// CHECK-NEXT: memref.store +// CHECK-NEXT: } +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @vector_if_bound_with_cube_loop_side_effect( + %cond: i1, %ub_a: i32, %ub_b: i32, %vinit: i32, %cube_val: i32, + %outv: memref<1xi32>, %outc: memref<1xi32>) { + %c0 = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : i32 + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %ub = scf.if %cond -> (i32) { + scf.yield {ssbuffer.core_type = "VECTOR"} %ub_a : i32 + } else { + scf.yield {ssbuffer.core_type = "VECTOR"} %ub_b : i32 + } {ssbuffer.core_type = "VECTOR"} + %res = scf.for %i = %c0 to %ub step %c1v + iter_args(%acc = %vinit) -> (i32) : i32 { + %nv = arith.addi %acc, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + memref.store %cube_val, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + scf.yield {ssbuffer.core_type = "VECTOR"} %nv : i32 + } {ssbuffer.core_type = "VECTOR"} + memref.store %res, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @vector_if_condition_gates_cube_if_branch( +// A VECTOR-only inner scf.if produces an i1 used as the condition of an outer +// resultful scf.if. The inner if's only SSA result is the i1 (VECTOR), so it +// has no CUBE user; without the gating fix the CUBE scope neutralizes the inner +// yields, the condition folds to a constant, and canonicalize drops the gated +// CUBE memref.store. The fix keeps the inner if live in the CUBE scope because +// the outer if body still holds CUBE content. +// VECTOR scope: the gated branch holds no VECTOR side effect, so canonicalize +// drops the whole if and only the trailing VECTOR store remains. +// CHECK: scope.scope : () -> () { +// CHECK-NEXT: memref.store +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CUBE scope: the inner if folds to arith.select (no side effects in its +// branches) and feeds the condition of the preserved outer scf.if guarding +// the CUBE store; the condition is NOT neutralized to a constant. +// CHECK-NEXT: scope.scope : () -> () { +// CHECK-NEXT: %[[CONDC:.*]] = arith.select +// CHECK-NEXT: scf.if %[[CONDC]] { +// CHECK-NEXT: memref.store +// CHECK-NEXT: } +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @vector_if_condition_gates_cube_if_branch( + %cond: i1, %va: i1, %vb: i1, %cube_val: i32, + %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %vseed = arith.constant {ssbuffer.core_type = "VECTOR"} 7 : i32 + %gate = scf.if %cond -> (i1) { + scf.yield {ssbuffer.core_type = "VECTOR"} %va : i1 + } else { + scf.yield {ssbuffer.core_type = "VECTOR"} %vb : i1 + } {ssbuffer.core_type = "VECTOR"} + %r = scf.if %gate -> (i32) { + memref.store %cube_val, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + scf.yield {ssbuffer.core_type = "VECTOR"} %vseed : i32 + } else { + scf.yield {ssbuffer.core_type = "VECTOR"} %vseed : i32 + } {ssbuffer.core_type = "VECTOR"} + memref.store %r, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + func.return + } +} + +// ----- + +// CHECK-LABEL: func.func @vector_if_condition_gates_cube_while_body( +// A VECTOR-only inner scf.if produces an i1 used as the scf.while condition. +// The while results are all VECTOR (no CUBE user of the gate), but the while +// body holds a CUBE memref.store. Without the gating fix the CUBE scope +// neutralizes the inner if (condition -> false), the while never runs, and +// canonicalize erases the CUBE store. The fix keeps the inner if live because +// the while body still holds CUBE content. +// CHECK: scope.scope : () -> () { +// CHECK-NEXT: scf.while +// CHECK-NEXT: arith.cmpi +// CHECK-NEXT: arith.select +// CHECK-NEXT: scf.condition +// CHECK-NEXT: } do { +// CHECK-NEXT: ^bb0 +// CHECK-NEXT: arith.addi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } +// CHECK-NEXT: memref.store +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CUBE scope: the inner if folds to arith.select feeding scf.condition; the +// while loop and its CUBE store body are preserved (condition not constant). +// CHECK-NEXT: scope.scope : () -> () { +// CHECK-NEXT: scf.while +// CHECK-NEXT: arith.cmpi +// CHECK-NEXT: %[[SELC:.*]] = arith.select +// CHECK-NEXT: scf.condition(%[[SELC]]) +// CHECK-NEXT: } do { +// CHECK-NEXT: ^bb0 +// CHECK-NEXT: memref.store +// CHECK-NEXT: arith.addi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @vector_if_condition_gates_cube_while_body( + %va: i1, %vb: i1, %vec_init: i32, %cube_val: i32, + %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %limit = arith.constant {ssbuffer.core_type = "VECTOR"} 4 : i32 + %0 = scf.while (%v = %vec_init) : (i32) -> (i32) { + %lt = arith.cmpi slt, %v, %limit {ssbuffer.core_type = "VECTOR"} : i32 + %gate = scf.if %lt -> (i1) { + scf.yield {ssbuffer.core_type = "VECTOR"} %va : i1 + } else { + scf.yield {ssbuffer.core_type = "VECTOR"} %vb : i1 + } {ssbuffer.core_type = "VECTOR"} + scf.condition(%gate) %v : i32 + } do { + ^bb0(%v_iter: i32): + memref.store %cube_val, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + %nv = arith.addi %v_iter, %c1v {ssbuffer.core_type = "VECTOR"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR"} %nv : i32 + } attributes {ssbuffer.core_type = "VECTOR"} + memref.store %0, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + func.return + } +} + +// ----- + +// Regression: isProducedByForeignScope gate must only apply to loop ops +// (scf.for/scf.while), never to scf.if. The isLoopOp guard prevents the +// gate from firing on scf.if. A VECTOR-only scf.if feeding a mixed +// CUBE/VECTOR scf.while must stay live in the CUBE scope. +// CHECK-LABEL: func.func @if_into_while_preserved_in_cube( +// VECTOR scope: if preserved with real branch ops, then mixed while + store. +// CHECK: scope.scope : () -> () { +// CHECK-NEXT: %[[IF:.*]] = scf.if +// CHECK-NEXT: arith.addi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } else { +// CHECK-NEXT: arith.subi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } +// CHECK-NEXT: %[[WHILE_SMALL:.*]] = scf.while +// CHECK-NEXT: arith.addi +// CHECK-NEXT: arith.cmpi +// CHECK-NEXT: scf.condition +// CHECK-NEXT: } do { +// CHECK-NEXT: ^bb0({{.*}}): +// CHECK-NEXT: arith.addi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } +// CHECK-NEXT: memref.store +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CUBE scope: if preserved, mixed while preserved with CUBE arith.addi in body. +// CHECK-NEXT: scope.scope : () -> () { +// CHECK-NEXT: %[[IFC:.*]] = scf.if +// CHECK-NEXT: arith.addi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } else { +// CHECK-NEXT: arith.subi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } +// CHECK-NEXT: %[[WHILEC:.*]]:{{[0-9]+}} = scf.while +// CHECK-NEXT: arith.addi +// CHECK-NEXT: arith.cmpi +// CHECK-NEXT: scf.condition +// CHECK-NEXT: } do { +// CHECK-NEXT: ^bb0({{.*}}): +// CHECK-NEXT: arith.addi +// CHECK-NEXT: arith.addi +// CHECK-NEXT: scf.yield +// CHECK-NEXT: } +// CHECK-NEXT: arith.addi +// CHECK-NEXT: memref.store +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @if_into_while_preserved_in_cube(%cond: i1, %vinit: i64, %cinit: i64, %limit: i64, %outv: memref<1xi64>, %outc: memref<1xi64>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i64 + %c2c = arith.constant {ssbuffer.core_type = "CUBE"} 2 : i64 + %c1c = arith.constant {ssbuffer.core_type = "CUBE"} 1 : i64 + %0 = scf.if %cond -> (i64) { + %1 = arith.addi %vinit, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR"} %1 : i64 + } else { + %1 = arith.subi %limit, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR"} %1 : i64 + } {ssbuffer.core_type = "VECTOR"} + %1:2 = scf.while (%a = %0, %b = %cinit) : (i64, i64) -> (i64, i64) { + %2 = arith.addi %a, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + %3 = arith.cmpi slt, %2, %limit {ssbuffer.core_type = "VECTOR"} : i64 + scf.condition(%3) %2, %b : i64, i64 + } do { + ^bb0(%a_iter: i64, %b_iter: i64): + %2 = arith.addi %a_iter, %c1v {ssbuffer.core_type = "VECTOR"} : i64 + %3 = arith.addi %b_iter, %c2c {ssbuffer.core_type = "CUBE"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR, CUBE"} %2, %3 : i64, i64 + } attributes {ssbuffer.core_type = "VECTOR, CUBE"} + %2 = arith.addi %1#1, %c1c {ssbuffer.core_type = "CUBE"} : i64 + memref.store %1#0, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi64> + memref.store %2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi64> + func.return + } +} + +// ----- + +// Regression: in a 2-layer nested scf.for where the inner iter_arg flows +// directly into a CUBE accumulator (arith.mulf), needsLoopCarryPreserve on the +// inner for cannot find VECTOR users because same-slot yield blocks SSA +// propagation. The producer-scope gate (guarded by isLoopOp on scf.for) detects +// the foreign CUBE producer and skips findLiveUser so the slot is neutralized +// in VECTOR scope, erasing the CUBE op from the VECTOR clone while keeping it +// in the CUBE clone. An independent CUBE consumer of the outer loop result +// ensures the CUBE scope is not optimized away. +// CHECK-LABEL: func.func @nested_for_cube_acc_not_trapped_in_vector( +// VECTOR scope: outer+inner for survive, arith.addf{VECTOR} present +// but arith.mulf{CUBE} absent, scalar CUBE consumer absent. +// CHECK: scope.scope : () -> () { +// CHECK-NEXT: [[OUTER:%.+]]:2 = scf.for +// CHECK: [[INNER:%.+]]:2 = scf.for +// CHECK: arith.addf +// CHECK-NOT: arith.mulf +// CHECK: scf.yield +// CHECK: } +// CHECK: scf.yield +// CHECK: arith.addf +// CHECK: tensor.extract +// CHECK: memref.store +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CUBE scope: outer+inner for survive, arith.mulf{CUBE} present, and +// the independent CUBE arith.addf consumer is present. +// CHECK-NEXT: scope.scope : () -> () { +// CHECK-NEXT: [[OUTERC:%.+]]:2 = scf.for +// CHECK: [[INNERC:%.+]]:2 = scf.for +// CHECK: arith.mulf +// CHECK: scf.yield +// CHECK: } +// CHECK: scf.yield +// CHECK: arith.addf +// CHECK: tensor.extract +// CHECK: memref.store +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @nested_for_cube_acc_not_trapped_in_vector( + %lb: index, %ub: index, %step: index, + %acc_init: tensor<4xf32>, %vec_init: tensor<4xf32>, + %ck: tensor<4xf32>, %vk: tensor<4xf32>, %cc_init: tensor<4xf32>, + %outv: memref<1xf32>, %outc: memref<1xf32>) { + %idx = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %1:2 = scf.for %a = %lb to %ub step %step + iter_args(%oa = %acc_init, %ov = %vec_init) -> (tensor<4xf32>, tensor<4xf32>) : index { + %2:2 = scf.for %b = %lb to %ub step %step + iter_args(%ia = %oa, %iv = %ov) -> (tensor<4xf32>, tensor<4xf32>) : index { + %3 = arith.mulf %ia, %ck {ssbuffer.core_type = "CUBE"} : tensor<4xf32> + %4 = arith.addf %iv, %vk {ssbuffer.core_type = "VECTOR"} : tensor<4xf32> + scf.yield {ssbuffer.core_type = "CUBE, VECTOR"} %3, %4 : tensor<4xf32>, tensor<4xf32> + } {ssbuffer.core_type = "CUBE, VECTOR"} + scf.yield {ssbuffer.core_type = "CUBE, VECTOR"} %2#0, %2#1 : tensor<4xf32>, tensor<4xf32> + } {ssbuffer.core_type = "CUBE, VECTOR"} + %sv = arith.addf %1#0, %vk {ssbuffer.core_type = "VECTOR"} : tensor<4xf32> + %ev = tensor.extract %sv[%idx] {ssbuffer.core_type = "VECTOR"} : tensor<4xf32> + memref.store %ev, %outv[%idx] {ssbuffer.core_type = "VECTOR"} : memref<1xf32> + %sc = arith.addf %1#0, %cc_init {ssbuffer.core_type = "CUBE"} : tensor<4xf32> + %ec = tensor.extract %sc[%idxc] {ssbuffer.core_type = "CUBE"} : tensor<4xf32> + memref.store %ec, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xf32> + func.return + } +} + +// ----- + +// A VECTOR for can initialize a VECTOR while whose scalar results are consumed +// directly by CUBE operations. The CUBE clone must retain both complete state +// transitions; keeping either loop shell with neutral yields changes the live +// while results. +// CHECK-LABEL: func.func @vector_while_results_drive_cube( +// CHECK: scope.scope : () -> () { +// CHECK: %[[INIT:.*]]:2 = scf.for +// CHECK: arith.index_cast +// CHECK-NEXT: arith.addi +// CHECK-NEXT: arith.addi +// CHECK-NEXT: scf.yield +// CHECK: %[[WHILE:.*]]:2 = scf.while +// CHECK: arith.cmpi +// CHECK-NEXT: scf.condition +// CHECK: } do { +// CHECK: arith.addi +// CHECK-NEXT: arith.addi +// CHECK-NEXT: scf.yield +// CHECK: arith.muli %[[WHILE]]#0, %[[WHILE]]#1 +// CHECK-NEXT: memref.store +// CHECK-NEXT: scope.return +// CHECK-NEXT: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @vector_while_results_drive_cube( + %index_init: i64, %offset_init: i64, %limit: i64, + %out: memref<1xi64>) { + %idx = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c0 = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %c2 = arith.constant {ssbuffer.core_type = "VECTOR"} 2 : index + %c1 = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : index + %step = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i64 + %0:2 = scf.for %i = %c0 to %c2 step %c1 + iter_args(%index = %index_init, %offset = %offset_init) + -> (i64, i64) { + %i64 = arith.index_cast %i {ssbuffer.core_type = "VECTOR"} + : index to i64 + %next_index = arith.addi %index, %i64 + {ssbuffer.core_type = "VECTOR"} : i64 + %next_offset = arith.addi %offset, %next_index + {ssbuffer.core_type = "VECTOR"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR, VECTOR"} + %next_index, %next_offset : i64, i64 + } {ssbuffer.core_type = "VECTOR, VECTOR"} + %1:2 = scf.while (%index = %0#0, %offset = %0#1) + : (i64, i64) -> (i64, i64) { + %continue = arith.cmpi slt, %index, %limit + {ssbuffer.core_type = "VECTOR"} : i64 + scf.condition(%continue) %index, %offset : i64, i64 + } do { + ^bb0(%index: i64, %offset: i64): + %next_index = arith.addi %index, %step + {ssbuffer.core_type = "VECTOR"} : i64 + %next_offset = arith.addi %offset, %next_index + {ssbuffer.core_type = "VECTOR"} : i64 + scf.yield {ssbuffer.core_type = "VECTOR, VECTOR"} + %next_index, %next_offset : i64, i64 + } attributes {ssbuffer.core_type = "VECTOR, VECTOR"} + %product = arith.muli %1#0, %1#1 {ssbuffer.core_type = "CUBE"} : i64 + memref.store %product, %out[%idx] {ssbuffer.core_type = "CUBE"} + : memref<1xi64> + func.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/separate_cv_scope_ut.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/separate_cv_scope_ut.mlir new file mode 100644 index 0000000000..4305a2ec2e --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/separate_cv_scope_ut.mlir @@ -0,0 +1,100 @@ +// RUN: triton-opt %s --separate-cv-scope | FileCheck %s + +// Regression for the structural-user erase crash in SeparateCVScope: +// a VECTOR-only scf.if feeds the iter_args of a mixed VECTOR/CUBE scf.for. +// In the CUBE clone the scf.if shell becomes semantically dead before the loop +// is fully rewritten, so the pass must not erase the shell while the raw SSA +// use from the downstream scf.for still exists. + +// CHECK-LABEL: func.func @if_results_feed_followup_loop_structural_user_repro( +// CHECK: scope.scope : () -> () { +// CHECK: scf.if +// CHECK: scf.for +// CHECK: tensor.extract +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +// CHECK: scope.scope : () -> () { +// CHECK: scf.for +// CHECK: memref.store +// CHECK: scope.return +// CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} +module { + func.func @if_results_feed_followup_loop_structural_user_repro( + %lb: i32, + %ub: i32, + %cond: i1, + %vec0: tensor<4xf32>, + %vec1: tensor<4xf32>, + %cube_init: i32, + %outv: memref<1xf32>, + %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c1v = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %c1c = arith.constant {ssbuffer.core_type = "CUBE"} 1 : i32 + + %0:2 = scf.if %cond -> (tensor<4xf32>, tensor<4xf32>) { + %1 = arith.addf %vec0, %vec1 {ssbuffer.core_type = "VECTOR"} : tensor<4xf32> + %2 = arith.mulf %1, %vec1 {ssbuffer.core_type = "VECTOR"} : tensor<4xf32> + scf.yield {ssbuffer.core_type = "VECTOR, VECTOR"} %1, %2 : tensor<4xf32>, tensor<4xf32> + } else { + scf.yield {ssbuffer.core_type = "VECTOR, VECTOR"} %vec0, %vec1 : tensor<4xf32>, tensor<4xf32> + } {ssbuffer.core_type = "VECTOR, VECTOR"} + + %1:3 = scf.for %i = %lb to %ub step %c1v iter_args(%lhs = %0#0, %rhs = %0#1, %cube = %cube_init) -> (tensor<4xf32>, tensor<4xf32>, i32) : i32 { + %2 = arith.addi %cube, %c1c {ssbuffer.core_type = "CUBE"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR, VECTOR, CUBE"} %lhs, %rhs, %2 : tensor<4xf32>, tensor<4xf32>, i32 + } {ssbuffer.core_type = "VECTOR, VECTOR, CUBE"} + + %2 = tensor.extract %1#0[%idxv] {ssbuffer.core_type = "VECTOR"} : tensor<4xf32> + memref.store %2, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xf32> + memref.store %1#2, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + func.return + } + + // A while induction variable can belong to VECTOR while still controlling + // the trip count of CUBE work. Both separated scopes must preserve the + // predicate-carried value and its update. + + // CHECK-LABEL: func.func @while_predicate_controls_mixed_scope( + // CHECK-SAME: %[[UB:.*]]: i32 + // CHECK: scope.scope : () -> () { + // CHECK: scf.while (%[[V_IV:.*]] = %{{.*}}) : (i32) -> i32 { + // CHECK: %[[V_COND:.*]] = arith.cmpi slt, %[[V_IV]], %[[UB]] + // CHECK: scf.condition(%[[V_COND]]) %[[V_IV]] : i32 + // CHECK: ^bb0(%[[V_BODY_IV:.*]]: i32): + // CHECK: %[[V_NEXT:.*]] = arith.addi %[[V_BODY_IV]], %{{.*}} + // CHECK: scf.yield %[[V_NEXT]] : i32 + // CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + // CHECK: scope.scope : () -> () { + // CHECK: scf.while (%[[C_IV:.*]] = %{{.*}}) : (i32) -> i32 { + // CHECK: %[[C_COND:.*]] = arith.cmpi slt, %[[C_IV]], %[[UB]] + // CHECK: scf.condition(%[[C_COND]]) %[[C_IV]] : i32 + // CHECK: ^bb0(%[[C_BODY_IV:.*]]: i32): + // CHECK: memref.store + // CHECK: %[[C_NEXT:.*]] = arith.addi %[[C_BODY_IV]], %{{.*}} + // CHECK: scf.yield %[[C_NEXT]] : i32 + // CHECK: } {hivm.matmul_limited_in_cube, hivm.tcore_type = #hivm.tcore_type} + func.func @while_predicate_controls_mixed_scope( + %ub: i32, %outv: memref<1xi32>, %outc: memref<1xi32>) { + %idxv = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : index + %idxc = arith.constant {ssbuffer.core_type = "CUBE"} 0 : index + %c0 = arith.constant {ssbuffer.core_type = "VECTOR"} 0 : i32 + %c1 = arith.constant {ssbuffer.core_type = "VECTOR"} 1 : i32 + %cube_value = arith.constant {ssbuffer.core_type = "CUBE"} 7 : i32 + + %result = scf.while (%iv = %c0) : (i32) -> i32 { + %cond = arith.cmpi slt, %iv, %ub {ssbuffer.core_type = "VECTOR"} : i32 + scf.condition(%cond) {ssbuffer.core_type = "VECTOR"} %iv : i32 + } do { + ^bb0(%iv: i32): + memref.store %cube_value, %outc[%idxc] {ssbuffer.core_type = "CUBE"} : memref<1xi32> + %next = arith.addi %iv, %c1 {ssbuffer.core_type = "VECTOR"} : i32 + scf.yield {ssbuffer.core_type = "VECTOR"} %next : i32 + } attributes {ssbuffer.core_type = "VECTOR"} + + memref.store %result, %outv[%idxv] {ssbuffer.core_type = "VECTOR"} : memref<1xi32> + func.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/test-remove-attrs.mlir b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/test-remove-attrs.mlir new file mode 100644 index 0000000000..d457212e72 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/test-remove-attrs.mlir @@ -0,0 +1,62 @@ +// RUN: triton-opt --remove-ssbuf-attr %s | FileCheck %s + +module { + // CHECK-LABEL: func.func @test_remove_core_type_and_block_id + func.func @test_remove_core_type_and_block_id(%arg0: memref<1024x1024xf32>) { + // CHECK: memref.alloc() : memref<1024x1024xf32> + // CHECK-NOT: ssbuffer.core_type + // CHECK-NOT: ssbuffer.block_id + %memref = memref.alloc() {ssbuffer.core_type = "CUBE", ssbuffer.block_id = 1 : i32} : memref<1024x1024xf32> + + // CHECK: bufferization.to_tensor %{{.*}} : memref<1024x1024xf32> to tensor<1024x1024xf32> + // CHECK-NOT: ssbuffer.core_type + %tensor = bufferization.to_tensor %memref {ssbuffer.core_type = "CUBE"} : memref<1024x1024xf32> to tensor<1024x1024xf32> + return + } + + // CHECK-LABEL: func.func @test_remove_transfer_and_order_attrs + func.func @test_remove_transfer_and_order_attrs(%arg0: tensor<128x128xf32>) { + // CHECK: linalg.matmul + // CHECK-NOT: ssbuffer.transfer_id + // CHECK-NOT: ssbuffer.cube_first + // CHECK-NOT: ssbuffer.vector_first + %init = tensor.empty() : tensor<128x128xf32> + %result = linalg.matmul { + ssbuffer.transfer_id = 42 : i32, + ssbuffer.cube_first, + ssbuffer.vector_first + } ins(%arg0, %arg0 : tensor<128x128xf32>, tensor<128x128xf32>) + outs(%init : tensor<128x128xf32>) -> tensor<128x128xf32> + + return + } + + // CHECK-LABEL: func.func @test_preserve_other_attributes + func.func @test_preserve_other_attributes(%arg0: tensor<64x64xf32>) { + // CHECK: linalg.transpose + // CHECK-SAME: permutation = [1, 0] + // CHECK-NOT: ssbuffer.core_type + %out = tensor.empty() : tensor<64x64xf32> + %transposed = linalg.transpose + ins(%arg0 : tensor<64x64xf32>) + outs(%out : tensor<64x64xf32>) + permutation = [1, 0] + {ssbuffer.core_type = "VECTOR"} + + return + } + + // CHECK-LABEL: func.func @test_nested_ops + func.func @test_nested_ops(%arg0: i1, %arg1: tensor<128xf32>) { + // CHECK: scf.if + %res = scf.if %arg0 -> (tensor<128xf32>) { + // CHECK: arith.addf + // CHECK-NOT: ssbuffer.core_type + %add = arith.addf %arg1, %arg1 {ssbuffer.core_type = "VECTOR"} : tensor<128xf32> + scf.yield %add : tensor<128xf32> + } else { + scf.yield %arg1 : tensor<128xf32> + } + return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/compile_ttadapter.py b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/compile_ttadapter.py new file mode 100644 index 0000000000..c3c7c0f49f --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/compile_ttadapter.py @@ -0,0 +1,684 @@ +"""Compile a Triton JIT kernel to Ascend ttadapter MLIR without launching it. + +Example: + python3 third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/compile_ttadapter.py \ + third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf01_mlir_no_loop_independent_cv.py:acf01_tc01_no_loop_cv \ + --signature '{"a_ptr":"*fp16","b_ptr":"*fp16","c_ptr":"*fp16","out_ptr":"*fp16","M":"i32","N":"i32","K":"i32","stride_am":"i32","stride_ak":"i32","stride_bk":"i32","stride_c":"i32","stride_out":"i32"}' \ + --constants '{"BLOCK_SIZE_N":64,"BLOCK_SIZE_K":32}' \ + --output /tmp/acf01.ttadapter.mlir + +From this directory, check the Mojo kernels and print a summary table: + python3.11 compile_ttadapter.py --batch dynamic_cv_cases_github_master.txt \ + --root /path/to/mojo_opset + +For a different Mojo checkout, pass ``--root /path/to/mojo_opset``. This +overrides the batch file's ``[defaults].root`` without editing the case list. +Pass ``--arch Ascend910_9589`` to compile without querying an NPU target. +The sibling ``dynamic_cv_cases_github_master.txt`` supports both the GitHub +master layout and pinned commit 73f472fa6ed9fefc5fea2d5963dc1595fae905e6. +``--validate-sources --cube-cores 40`` checks its test-derived specializations +without loading PyTorch, Triton, or an NPU driver; it does not produce MLIR. + +Batch cases with ``test`` use a source reader for the supported pytest/kernel +pair. The test function is parsed, never called. Cases without ``test`` can +still provide signature, constants and options explicitly. A batch file may +also define [sources] and [tests] alias tables to keep each case short. Use +``cases = [{kernel = "alias:name", test = "test_alias", ...}, ...]`` before +the tables for one case per line, or the equivalent ``[[cases]]`` form. +""" + +import argparse +import dataclasses +import importlib +import importlib.util +import inspect +import io +import json +import re +import sys +import tomllib +import traceback +import types +from contextlib import contextmanager, redirect_stderr, redirect_stdout +from pathlib import Path + + +def _split_npu_options(option_cls, compile_options): + """Split options into constructor args and post-init fields.""" + signature = inspect.signature(option_cls) + constructor_names = { + name + for name, parameter in signature.parameters.items() + if name != "self" and parameter.kind in { + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + } + } + field_names = {field.name for field in dataclasses.fields(option_cls)} + constructor_options = {} + post_init_options = {} + ignored_options = {} + for name, value in compile_options.items(): + if name in constructor_names: + constructor_options[name] = value + elif name in field_names: + post_init_options[name] = value + else: + ignored_options[name] = value + return constructor_options, post_init_options, ignored_options + + +def _resolve_triton_constant_values(constants): + resolved = dict(constants or {}) + dtype_constants = { + "tl.float32": "float32", + "tl.float16": "float16", + "tl.bfloat16": "bfloat16", + "tl.int32": "int32", + "tl.int64": "int64", + } + needed = {value for value in resolved.values() if isinstance(value, str) and value in dtype_constants} + if needed: + import triton.language as tl + for name, value in list(resolved.items()): + if isinstance(value, str) and value in dtype_constants: + resolved[name] = getattr(tl, dtype_constants[value]) + return resolved + + +def compile_ttadapter(kernel, signature, constants=None, *, arch="Ascend910_9589", enable_dynamic_cv_pipeline=True, + output_path=None, return_metadata=False, **compile_options): + """Return the ttadapter MLIR for a ``@triton.jit`` kernel. + + ``signature`` maps runtime argument names to Triton types (for example, + ``{"x_ptr": "*fp16", "n": "i32"}``). ``constants`` maps constexpr argument + names to their compile-time values. No tensors, NPU allocation, or kernel + launch are needed. If provided, ``output_path`` receives the same MLIR text. + Additional ``compile_options`` are forwarded to ``NPUOptions`` so they can + match the original launch. Set ``return_metadata=True`` to also receive the + compiler metadata, including whether DynamicCVPipeline fell back. + Compilation errors propagate to the caller. + """ + from triton.compiler.compiler import ASTSource + from triton.compiler.code_generator import ast_to_ttir + from triton._C.libtriton import ir + from triton._C.libtriton.ascend import ir as ascend_ir + from triton.backends.ascend.compiler import NPUOptions, make_ttir, min_dot_size, ttir_to_linalg + + constructor_options, post_init_options, ignored_options = _split_npu_options(NPUOptions, compile_options) + options = NPUOptions(arch=arch, enable_dynamic_cv_pipeline=enable_dynamic_cv_pipeline, **constructor_options) + for name, value in post_init_options.items(): + try: + setattr(options, name, value) + except dataclasses.FrozenInstanceError: + object.__setattr__(options, name, value) + if ignored_options: + setattr(options, "_ignored_compile_options", ignored_options) + source = ASTSource(kernel, signature, _resolve_triton_constant_values(constants)) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + + ttir = ast_to_ttir(kernel, source, context, options, {"min_dot_size": min_dot_size(None)}, {}) + metadata = dict(options.__dict__) + ttir = make_ttir(ttir, metadata, options) + mlir = ttir_to_linalg(ttir, metadata, options, named_ops=True) + + if output_path is not None: + path = Path(output_path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(mlir, encoding="utf-8") + return (mlir, metadata) if return_metadata else mlir + + +def dynamic_cv_status(mlir, metadata, requested): + """Summarize whether DynamicCVPipeline produced scope ops without fallback.""" + scope_count = len(re.findall(r"(?m)^\s*(?:%[^\n=]+\s*=\s*)?scope\.scope\b", mlir)) + supported_arch = metadata.get("compile_on_910_95") is True + no_fallback = metadata.get("enable_dynamic_cv_pipeline") is True + return { + "scope_count": scope_count, + "cube_scope_count": len(re.findall(r"#hivm\.tcore_type", mlir)), + "vector_scope_count": len(re.findall(r"#hivm\.tcore_type", mlir)), + "pass_applied": bool(requested and supported_arch and no_fallback and scope_count), + "fell_back": bool(requested and supported_arch and not no_fallback), + } + + +@contextmanager +def _compile_only_npu_driver(): + """Expose A5 target metadata while Mojo kernel modules are imported.""" + import triton + + driver_config = triton.runtime.driver + previous_driver = getattr(driver_config, "_active", None) + target = types.SimpleNamespace(backend="npu", arch="Ascend950PR_9579", warp_size=0) + properties = {"num_aicore": 40, "num_vectorcore": 80} + compile_only_driver = types.SimpleNamespace( + get_current_target=lambda: target, + utils=types.SimpleNamespace(get_device_properties=lambda *_args, **_kwargs: properties), + ) + driver_config.set_active(compile_only_driver) + try: + yield + finally: + driver_config._active = previous_driver + + +def _load_mojo_module(path, package_parts, package_root): + """Load one Mojo source file without running package ``__init__`` files. + + Mojo's top-level package detects an attached accelerator and selects a + backend while it is imported. Compile-only CI deliberately has no NPU, so + importing the package normally selects ``meta_device`` before the A5 kernel + module can be reached. Namespace packages preserve relative and absolute + imports used by the kernel source without triggering that runtime probe. + """ + package_path = package_root + parent_name = None + for index, part in enumerate(package_parts): + package_path /= part + package_name = ".".join(package_parts[:index + 1]) + package = sys.modules.get(package_name) + if package is None: + package = types.ModuleType(package_name) + package.__file__ = str(package_path / "__init__.py") + package.__package__ = package_name + package.__path__ = [str(package_path)] + package.__spec__ = importlib.util.spec_from_loader(package_name, loader=None, is_package=True) + sys.modules[package_name] = package + if parent_name is not None: + setattr(sys.modules[parent_name], part, package) + parent_name = package_name + + module_name = ".".join([*package_parts, path.stem]) + module = sys.modules.get(module_name) + if module is not None: + return module + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + raise ImportError(f"cannot import {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + if parent_name is not None: + setattr(sys.modules[parent_name], path.stem, module) + try: + with _compile_only_npu_driver(): + spec.loader.exec_module(module) + except Exception: + sys.modules.pop(module_name, None) + if parent_name is not None: + parent = sys.modules.get(parent_name) + if parent is not None and getattr(parent, path.stem, None) is module: + delattr(parent, path.stem) + raise + return module + + +def _load_kernel(reference): + module_name, separator, attribute = reference.partition(":") + if not separator or not module_name or not attribute: + raise ValueError("kernel must be MODULE:NAME or FILE.py:NAME") + + if module_name.endswith(".py"): + path = Path(module_name).resolve() + package_parts = [] + parent = path.parent + while (parent / "__init__.py").is_file(): + package_parts.insert(0, parent.name) + parent = parent.parent + if package_parts: + if package_parts[0] == "mojo_opset": + module = _load_mojo_module(path, package_parts, parent) + else: + # Import under the real package name so relative imports in + # ordinary package modules continue to work. + if str(parent) not in sys.path: + sys.path.insert(0, str(parent)) + module = importlib.import_module(".".join([*package_parts, path.stem])) + else: + spec = importlib.util.spec_from_file_location(path.stem, path) + if spec is None or spec.loader is None: + raise ImportError(f"cannot import {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + else: + module = importlib.import_module(module_name) + kernel = getattr(module, attribute) + return _jit_kernel_object(kernel) + + +def _json_object(value): + try: + result = json.loads(value) + except json.JSONDecodeError as error: + raise argparse.ArgumentTypeError(str(error)) from error + if not isinstance(result, dict): + raise argparse.ArgumentTypeError("expected a JSON object") + return result + + +def _alias_value(table, value, table_name): + """Resolve a TOML alias table entry while keeping normal paths valid.""" + if value is None: + return None + if not isinstance(value, str): + raise ValueError(f"{table_name} reference must be a string") + if not isinstance(table, dict): + raise ValueError(f"[{table_name}] must be a TOML table") + resolved = table.get(value, value) + if not isinstance(resolved, str): + raise ValueError(f"[{table_name}].{value} must be a string") + return resolved + + +def _resolve_kernel_reference(reference, sources): + """Resolve either FILE.py:NAME or SOURCE_ALIAS:NAME.""" + module_name, separator, kernel_name = reference.partition(":") + if not separator or not module_name or not kernel_name: + raise ValueError("kernel must be MODULE:NAME, FILE.py:NAME, or SOURCE_ALIAS:NAME") + module_name = _alias_value(sources, module_name, "sources") + return f"{module_name}:{kernel_name}", module_name, kernel_name + + +def _candidate_roots(root): + """Accept either the checkout root, its parent, or the inner package root.""" + roots = [Path(root)] + if root.name == "mojo_opset": + roots.append(root.parent) + package_root = root / "mojo_opset" + if package_root.is_dir(): + roots.append(package_root) + deduped = [] + seen = set() + for item in roots: + normalized = item.resolve() if item.exists() else item + if normalized in seen: + continue + seen.add(normalized) + deduped.append(item) + return deduped + + +def _resolve_existing_file(root, requested, alternatives=()): + """Resolve a source or test file across supported Mojo checkout layouts.""" + requested = Path(requested).expanduser() + if requested.is_absolute(): + candidates = [requested] + else: + requested_paths = [requested, *(Path(item) for item in alternatives)] + candidates = [] + for candidate_root in _candidate_roots(root): + for item in requested_paths: + candidates.append(candidate_root / item) + if item.parts and item.parts[0] == "mojo_opset": + candidates.append(candidate_root / Path(*item.parts[1:])) + deduped = [] + seen = set() + for candidate in candidates: + normalized = candidate.resolve() if candidate.exists() else candidate + if normalized in seen: + continue + seen.add(normalized) + deduped.append(candidate) + if candidate.is_file(): + return candidate + tried = "\n ".join(str(path) for path in deduped) + raise FileNotFoundError(f"{root} does not contain {requested}; tried:\n {tried}") + + +def _source_alternatives(module_path): + text = Path(module_path).as_posix() + pairs = ( + ("mojo_opset/kernels/npu_a5_triton/", "mojo_opset/backends/ttx/kernels/npu/a5/"), + ("mojo_opset/backends/ttx/kernels/npu/a5/", "mojo_opset/kernels/npu_a5_triton/"), + ) + for prefix, replacement in pairs: + if text.startswith(prefix): + return [replacement + text[len(prefix):]] + return [] + + +def _test_alternatives(test_file, kernel_name, module_path): + """Return equivalent pytest files in the old and GitHub master layouts.""" + text = Path(test_file).as_posix() + source = Path(module_path).as_posix() + if text == "tests/functions/test_flex_attention.py": + return ["mojo_opset/tests/accuracy/functions/test_flex_attention.py"] + if text == "tests/functions/test_paged_attention.py": + return ["mojo_opset/tests/accuracy/operators/test_attention.py"] + if text == "tests/functions/test_sdpa.py": + return ["mojo_opset/tests/accuracy/operators/test_attention.py"] + if text == "tests/functions/test_attention.py": + if source.endswith("/swa.py") and kernel_name == "_sdpa_infer_kernel": + return [ + "mojo_opset/tests/accuracy/operators/test_attention.py", + "mojo_opset/tests/accuracy/functions/test_attention.py" + ] + return [ + "mojo_opset/tests/accuracy/functions/test_attention.py", + "mojo_opset/tests/accuracy/operators/test_attention.py" + ] + if text == "mojo_opset/tests/accuracy/functions/test_flex_attention.py": + return ["tests/functions/test_flex_attention.py"] + if text == "mojo_opset/tests/accuracy/functions/test_attention.py": + return ["tests/functions/test_attention.py"] + if text == "mojo_opset/tests/accuracy/operators/test_attention.py": + if kernel_name in { + "paged_decode_kernel", "paged_decode_fd_kernel", "paged_prefill_kernel", + "paged_prefill_page_aggregation_kernel", "_swa_paged_decode_kernel", "_swa_paged_prefill_kernel", + "_swa_paged_prefill_aggregation_kernel" + }: + return ["tests/functions/test_paged_attention.py"] + if source.endswith("/sdpa.py") or kernel_name in { + "kernel_sdpa_fwd", "kernel_sdpa_bwd_qkv", "kernel_sdpa_bwd_q" + }: + return ["tests/functions/test_sdpa.py", "tests/functions/test_attention.py"] + return ["tests/functions/test_attention.py"] + return [] + + +def _normalize_mojo_test_case(kernel_path, kernel_name, test_name, config_id): + """Normalize logical cases when a batch file is used with the other layout.""" + old_layout = kernel_path.parent.name == "a5" and kernel_path.parent.parent.name == "npu" + master_layout = kernel_path.parent.name == "npu_a5_triton" and kernel_path.parent.parent.name == "kernels" + if old_layout: + old_tests = { + "test_business": "test_flex_attention", + "test_swa": "test_swa_function", + } + if test_name == "test_paged": + if kernel_name in {"paged_decode_kernel", "paged_decode_fd_kernel"}: + test_name = "test_paged_decode_gqa" + elif kernel_name in {"paged_prefill_kernel", "paged_prefill_page_aggregation_kernel"}: + test_name = "test_paged_prefill_gqa" + elif kernel_name == "_swa_paged_decode_kernel": + test_name = "test_paged_decode_swa" + elif kernel_name in {"_swa_paged_prefill_kernel", "_swa_paged_prefill_aggregation_kernel"}: + test_name = "test_paged_prefill_swa" + else: + test_name = old_tests.get(test_name, test_name) + if kernel_path.name == "swa.py" and kernel_name == "_sdpa_infer_kernel": + config_id = {"bf16": "M_BF16"}.get(config_id, config_id) + elif kernel_path.name == "swa.py" and kernel_name in { + "_swa_fwd_kernel", "_swa_bwd_dkdv_kernel", "_swa_bwd_dq_kernel" + }: + config_id = {"1024": "M_BF16_1024"}.get(config_id, config_id) + elif master_layout: + master_tests = { + "test_flex_attention": "test_business", + "test_swa_function": "test_swa", + "test_paged_decode_gqa": "test_paged", + "test_paged_prefill_gqa": "test_paged", + "test_paged_decode_swa": "test_paged", + "test_paged_prefill_swa": "test_paged", + } + test_name = master_tests.get(test_name, test_name) + if kernel_path.name == "swa.py" and kernel_name == "_sdpa_infer_kernel": + config_id = {"M_BF16": "bf16"}.get(config_id, config_id) + elif kernel_path.name == "swa.py" and kernel_name in { + "_swa_fwd_kernel", "_swa_bwd_dkdv_kernel", "_swa_bwd_dq_kernel" + }: + config_id = {"M_BF16_1024": "1024"}.get(config_id, config_id) + return test_name, config_id + + +def _mojo_checkout_root(root, path): + """Return the outer Mojo checkout root for helper code that reads package files.""" + path = Path(path).resolve() + parts = path.parts + for index, part in enumerate(parts): + if part != "mojo_opset" or index + 1 >= len(parts): + continue + if parts[index + 1] == "mojo_opset": + return Path(*parts[:index + 1]) if index else Path("/") + if parts[index + 1] in {"backends", "core", "tests", "kernels"}: + return Path(*parts[:index]) if index else Path("/") + return Path(root) + + +def _jit_kernel_object(kernel): + """Return the inner Triton JIT object from optional autotune wrappers.""" + seen = set() + current = kernel + while True: + if hasattr(current, "arg_names") and hasattr(current, "cache_key") and hasattr(current, "__name__"): + return current + if id(current) in seen or not hasattr(current, "fn"): + return kernel + seen.add(id(current)) + current = current.fn + + +def _run_batch(config_path, output_dir=None, verbose=False, root_override=None, arch_override=None, + validate_sources=False, cube_cores=None): + """Compile TOML cases independently and print only their Dynamic CV status.""" + config_path = config_path.resolve() + config = tomllib.loads(config_path.read_text(encoding="utf-8")) + if output_dir is None: + output_dir = config_path.parent / "ttadapter_outputs" + else: + output_dir = output_dir.expanduser() + if not output_dir.is_absolute(): + output_dir = (Path.cwd() / output_dir).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + for stale_path in output_dir.glob("*.ttadapter.mlir"): + stale_path.unlink() + for stale_path in (output_dir / "summary.tsv", output_dir / "diagnostics.log", output_dir / "diagnostics.tsv"): + if stale_path.exists(): + stale_path.unlink() + + defaults = config.get("defaults", {}) + cases = config.get("cases") + if not isinstance(defaults, dict) or not isinstance(cases, list) or not cases: + raise ValueError("batch file needs a nonempty cases array and an optional [defaults] table") + + root = Path(root_override if root_override is not None else defaults.get("root", config_path.parent)).expanduser() + if not root.is_absolute(): + root = (config_path.parent / root).resolve() + if not root.is_dir(): + raise FileNotFoundError(f"batch source root does not exist: {root}") + default_options = defaults.get("options", {}) + if not isinstance(default_options, dict): + raise ValueError("[defaults.options] must be a TOML table") + if cube_cores is not None and cube_cores <= 0: + raise ValueError("--cube-cores must be positive") + sources = config.get("sources", {}) + tests = config.get("tests", {}) + if not isinstance(sources, dict) or not all( + isinstance(key, str) and isinstance(value, str) for key, value in sources.items()): + raise ValueError("[sources] must map aliases to source paths") + if not isinstance(tests, dict) or not all( + isinstance(key, str) and isinstance(value, str) for key, value in tests.items()): + raise ValueError("[tests] must map aliases to test references") + + rows = [] + diagnostics = [] + diagnostic_rows = [] + for index, case in enumerate(cases, 1): + name = case.get("name") if isinstance(case, dict) else None + name = name or f"case_{index}" + diagnostic = io.StringIO() + try: + if not isinstance(case, dict): + raise ValueError("case must be a TOML table") + reference = case.get("kernel", defaults.get("kernel")) + if not isinstance(reference, str): + raise ValueError("kernel must be MODULE:NAME, FILE.py:NAME, or SOURCE_ALIAS:NAME") + reference, module_path, kernel_name = _resolve_kernel_reference(reference, sources) + if module_path.endswith(".py") and not Path(module_path).is_absolute(): + reference = f"{root / module_path}:{kernel_name}" + + if "name" not in case: + name = kernel_name + + source_options = {} + test_reference = case.get("test", defaults.get("test")) + if test_reference is not None: + from ttadapter_test_source import infer_mojo_case + + test_reference = _alias_value(tests, test_reference, "tests") + test_file, separator, test_name = test_reference.partition("::") + if not separator or not test_file.endswith(".py") or not module_path.endswith(".py"): + raise ValueError("test must be FILE.py::TEST_NAME and kernel must be FILE.py:NAME") + test_path = _resolve_existing_file(root, test_file, + _test_alternatives(test_file, kernel_name, module_path)) + kernel_path = _resolve_existing_file(root, module_path, _source_alternatives(module_path)) + reference = f"{kernel_path}:{kernel_name}" + source_root = _mojo_checkout_root(root, kernel_path) + test_name, config_id = _normalize_mojo_test_case(kernel_path, kernel_name, test_name, + case.get("config", defaults.get("config"))) + with redirect_stdout(diagnostic), redirect_stderr(diagnostic): + signature, constants, source_options = infer_mojo_case(source_root, test_path, test_name, + kernel_path, kernel_name, config_id, + case.get("layout", + defaults.get("layout")), cube_cores) + else: + signature = case["signature"] + constants = case.get("constants", {}) + case_options = case.get("options", {}) + if not all(isinstance(item, dict) for item in (signature, constants, case_options)): + raise ValueError("signature, constants and options must be TOML tables") + if validate_sources: + rows.append((str(name), "True")) + continue + options = {**default_options, **source_options, **case_options} + requested = options.pop("enable_dynamic_cv_pipeline", True) + arch = arch_override or case.get("arch", defaults.get("arch", "Ascend910_9589")) + safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(name)) + output_path = output_dir / f"{index:03d}_{safe_name}.ttadapter.mlir" + + with redirect_stdout(diagnostic), redirect_stderr(diagnostic): + if arch == "auto": + import triton + arch = triton.runtime.driver.active.get_current_target().arch + kernel = _load_kernel(reference) + mlir, metadata = compile_ttadapter(kernel, signature, constants, arch=arch, + enable_dynamic_cv_pipeline=requested, output_path=output_path, + return_metadata=True, **options) + status = dynamic_cv_status(mlir, metadata, requested) + applied = status["pass_applied"] + rows.append((str(name), str(applied))) + if not applied: + reason = "compiler fell back" if status["fell_back"] else "no verified Dynamic CV scopes" + diagnostics.append((name, reason, diagnostic.getvalue())) + diagnostic_rows.append((str(name), str(applied), reason)) + except Exception as error: + rows.append((str(name), "ERROR")) + reason = f"{type(error).__name__}: {error}" + details = diagnostic.getvalue() + if details and not details.endswith("\n"): + details += "\n" + details += traceback.format_exc() + diagnostics.append((name, reason, details)) + diagnostic_rows.append((str(name), "ERROR", reason)) + + name_width = max(len("Operator"), *(len(name) for name, _ in rows)) + status_header = "Source extraction" if validate_sources else "DynamicCVPipeline applied" + print(f"{'Operator':<{name_width}} | {status_header}") + print(f"{'-' * name_width}-+-{'-' * len(status_header)}") + for name, status in rows: + print(f"{name:<{name_width}} | {status}") + directory_label = "validation output dir" if validate_sources else "ttadapter output dir" + print(f"\n{directory_label}: {output_dir.resolve()}") + + summary_path = output_dir / "summary.tsv" + summary_path.write_text(f"Operator\t{status_header}\n" + "".join(f"{name}\t{status}\n" for name, status in rows), + encoding="utf-8") + + diagnostics_path = output_dir / "diagnostics.log" + diagnostics_tsv_path = output_dir / "diagnostics.tsv" + if diagnostics: + diagnostics_tsv_path.write_text( + f"Operator\t{status_header}\treason\n" + + "".join(f"{name}\t{status}\t{reason}\n" for name, status, reason in diagnostic_rows), encoding="utf-8") + diagnostics_text = [] + for name, reason, diagnostic in diagnostics: + diagnostics_text.append(f"{name}: {reason}\n") + if diagnostic: + diagnostics_text.append(diagnostic) + if not diagnostic.endswith("\n"): + diagnostics_text.append("\n") + diagnostics_text.append("\n") + diagnostics_path.write_text("".join(diagnostics_text), encoding="utf-8") + print(f"diagnostics: {diagnostics_path.resolve()}") + print(f"diagnostics tsv: {diagnostics_tsv_path.resolve()}") + + if verbose: + for name, reason, diagnostic in diagnostics: + print(f"\n{name}: {reason}", file=sys.stderr) + if diagnostic: + print(diagnostic, file=sys.stderr, end="") + return 0 if all(status == "True" for _, status in rows) else 1 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("kernel", nargs="?", help="Python module or .py file and kernel name, separated by ':'") + parser.add_argument("--batch", type=Path, help="TOML text file containing cases = [...] or [[cases]]") + parser.add_argument("--output-dir", type=Path, help="save batch ttadapter files in this directory") + parser.add_argument("--root", type=Path, help="override [defaults].root for batch source and test paths") + parser.add_argument("--validate-sources", action="store_true", + help="check source-derived batch cases without importing or compiling kernels") + parser.add_argument("--cube-cores", type=int, + help="cube core count for source validation without an NPU (paged decode FD)") + parser.add_argument("--verbose", action="store_true", help="print batch compilation errors") + parser.add_argument("--case", type=Path, help="JSON file with signature, constants and compile options") + parser.add_argument("--signature", type=_json_object, + help='runtime argument types as JSON, e.g. \'{"x_ptr":"*fp16"}\'') + parser.add_argument("--constants", type=_json_object, help="constexpr values as JSON") + parser.add_argument("--arch", help="Ascend target architecture, or 'auto' to query the NPU") + parser.add_argument("--options", type=_json_object, + help="other NPU compile options as JSON, e.g. '{\"num_warps\":16}'") + parser.add_argument("--output", type=Path, help="path for the .ttadapter.mlir file") + parser.add_argument("--require-scope", action="store_true", + help="exit with status 1 if the generated MLIR has no scope.scope op") + parser.add_argument("--require-dynamic-cv", action="store_true", + help="exit with status 1 unless DynamicCVPipeline produced scope ops without fallback") + args = parser.parse_args() + + if args.batch is not None: + if (args.kernel or args.case or args.signature is not None or args.constants is not None + or args.options is not None or args.output or args.require_scope or args.require_dynamic_cv): + parser.error("--batch reads kernels and their compile parameters from the text file") + return _run_batch(args.batch, args.output_dir, args.verbose, args.root, args.arch, args.validate_sources, + args.cube_cores) + if args.kernel is None: + parser.error("provide a kernel or --batch FILE.txt") + if (args.output_dir is not None or args.root is not None or args.validate_sources or args.cube_cores is not None): + parser.error("--output-dir, --root, --validate-sources and --cube-cores require --batch") + + case = _json_object(args.case.read_text(encoding="utf-8")) if args.case else {} + signature = args.signature if args.signature is not None else case.get("signature") + if not isinstance(signature, dict): + parser.error("provide --signature or a --case file containing 'signature'") + constants = args.constants if args.constants is not None else case.get("constants", {}) + options = dict(case.get("options", {})) + options.update(args.options or {}) + arch = args.arch or case.get("arch") or options.pop("arch", None) or "Ascend910_9589" + if arch == "auto": + import triton + arch = triton.runtime.driver.active.get_current_target().arch + options["arch"] = arch + options.setdefault("enable_dynamic_cv_pipeline", True) + + kernel = _load_kernel(args.kernel) + output = args.output or Path(f"{kernel.__name__}.ttadapter.mlir") + mlir, metadata = compile_ttadapter(kernel, signature, constants, output_path=output, return_metadata=True, + **options) + status = dynamic_cv_status(mlir, metadata, options["enable_dynamic_cv_pipeline"]) + print(f"ttadapter: {output.resolve()}") + print(f"scope.scope ops: {status['scope_count']}") + print(f"CUBE / VECTOR tcore_type markers: {status['cube_scope_count']} / {status['vector_scope_count']}") + print(f"DynamicCVPipeline applied: {status['pass_applied']}") + if status["fell_back"]: + print("DynamicCVPipeline fell back to standard compilation") + if args.require_scope and not status["scope_count"]: + return 1 + if args.require_dynamic_cv and not status["pass_applied"]: + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/dynamic_cv_cases_github_master.txt b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/dynamic_cv_cases_github_master.txt new file mode 100644 index 0000000000..33fb5f8a72 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/dynamic_cv_cases_github_master.txt @@ -0,0 +1,40 @@ +# One test-derived specialization per GitHub master npu_a5_triton kernel. +# Also supports the old layout at commit 73f472fa6ed9fefc5fea2d5963dc1595fae905e6. +# CI overrides this server path with its fresh GitHub checkout. +cases = [ + { kernel = "flex:flex_attention_kernel", test = "flex", config = "sparse_2000_22000" }, + { kernel = "flex:flex_attention_backward_dq_kernel", test = "flex", config = "sparse_2000_22000" }, + { kernel = "paged:paged_decode_kernel", test = "paged", config = "M_BF16" }, + { kernel = "paged:paged_decode_fd_kernel", test = "paged", config = "M_BF16_LONG" }, + { kernel = "paged:paged_prefill_kernel", test = "paged", config = "M_BF16_WITH_CACHE" }, + { kernel = "paged:paged_prefill_page_aggregation_kernel", test = "paged", config = "M_BF16" }, + { kernel = "swa:_sdpa_infer_kernel", name = "swa._sdpa_infer_kernel", test = "swa_infer", config = "bf16" }, + { kernel = "swa:_swa_paged_prefill_aggregation_kernel", test = "paged", config = "M_BF16" }, + { kernel = "swa:_swa_paged_prefill_kernel", test = "paged", config = "M_BF16_PADDIM" }, + { kernel = "swa:_swa_paged_decode_kernel", test = "paged", config = "M_BF16" }, + { kernel = "swa:_swa_fwd_kernel", test = "swa_training", config = "1024" }, + { kernel = "swa:_swa_bwd_dkdv_kernel", test = "swa_training", config = "1024" }, + { kernel = "swa:_swa_bwd_dq_kernel", test = "swa_training", config = "1024" }, + { kernel = "sdpa:_sdpa_infer_kernel", name = "sdpa._sdpa_infer_kernel", test = "sdpa" }, + { kernel = "sdpa:kernel_sdpa_fwd", test = "sdpa" }, + { kernel = "sdpa:kernel_sdpa_bwd_qkv", test = "sdpa" }, + { kernel = "sdpa:kernel_sdpa_bwd_q", test = "sdpa" }, +] + +[defaults] +root = "/data/autotest/optest_fast_pipeline/20260920234608/triton_ascend/test/mojo_operator_cases/mojo_opset" +arch = "auto" +layout = "ABAB" + +[sources] +flex = "mojo_opset/kernels/npu_a5_triton/flex_attention.py" +paged = "mojo_opset/kernels/npu_a5_triton/flash_attention.py" +swa = "mojo_opset/kernels/npu_a5_triton/swa.py" +sdpa = "mojo_opset/kernels/npu_a5_triton/sdpa.py" + +[tests] +flex = "tests/functions/test_flex_attention.py::test_business" +paged = "tests/functions/test_paged_attention.py::test_paged" +swa_infer = "tests/functions/test_attention.py::test_swa_infer" +swa_training = "tests/functions/test_attention.py::test_swa" +sdpa = "tests/functions/test_sdpa.py::test_sdpa" diff --git a/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/ttadapter_test_source.py b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/ttadapter_test_source.py new file mode 100644 index 0000000000..611f35bb63 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/DynamicCVPipeline/ttadapter/ttadapter_test_source.py @@ -0,0 +1,699 @@ +"""Read supported Mojo A5 pytest specializations without running tests. + +Shared helpers handle AST reading, argument types and specialization validation. +The four family readers retain their wrapper-specific shape and mask rules; +this is not a general Python interpreter or a reader for arbitrary kernels. +""" + +import ast +import math +import operator +from pathlib import Path + +_BINARY_OPS = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.FloorDiv: operator.floordiv, +} + + +def _static_value(node, values=None): + """Evaluate only simple expressions used by the SWA test and launch code.""" + values = values or {} + if isinstance(node, ast.Constant): + return node.value + if isinstance(node, ast.Name) and node.id in values: + return values[node.id] + if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name): + key = f"{node.value.id}.{node.attr}" + if key in values: + return values[key] + if node.value.id == "torch" and node.attr in {"bfloat16", "float16", "float32"}: + return key + if isinstance(node, (ast.List, ast.Tuple)): + return [_static_value(item, values) for item in node.elts] + if isinstance(node, ast.BinOp) and type(node.op) in _BINARY_OPS: + return _BINARY_OPS[type(node.op)](_static_value(node.left, values), _static_value(node.right, values)) + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): + return -_static_value(node.operand, values) + if isinstance(node, ast.Compare) and len(node.ops) == len(node.comparators) == 1: + if isinstance(node.ops[0], ast.Eq): + return _static_value(node.left, values) == _static_value(node.comparators[0], values) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): + functions = {"max": max, "min": min} + if node.func.id in functions and not node.keywords: + return functions[node.func.id](*[_static_value(arg, values) for arg in node.args]) + raise ValueError(f"cannot statically read expression: {ast.unparse(node)}") + + +def _parse(path): + return ast.parse(Path(path).read_text(encoding="utf-8"), filename=str(path)) + + +def _definition(tree, name, kind): + for node in tree.body: + if isinstance(node, kind) and node.name == name: + return node + raise ValueError(f"{name} not found in source") + + +def _assignment(tree, name): + for node in tree.body: + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == name for target in node.targets): + return node.value + raise ValueError(f"{name} not found in source") + + +def _call(function, name): + for node in ast.walk(function): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == name: + return node + raise ValueError(f"call to {name} not found in source") + + +def _launch(function, kernel_name): + for node in ast.walk(function): + if (isinstance(node, ast.Call) and isinstance(node.func, ast.Subscript) + and isinstance(node.func.value, ast.Name) and node.func.value.id == kernel_name): + return node + raise ValueError(f"launch of {kernel_name} not found in source") + + +def _gqa_interleave(root, layout): + operator_tree = _parse(root / "mojo_opset/core/operators/attention.py") + mojo_swa = _definition(operator_tree, "MojoSWA", ast.ClassDef) + constructor = next(node for node in mojo_swa.body if isinstance(node, ast.FunctionDef) and node.name == "__init__") + for node in ast.walk(constructor): + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Attribute) and target.attr == "gqa_interleave" for target in node.targets): + return _static_value(node.value, {"gqa_layout": layout}) + raise ValueError("MojoSWA.gqa_interleave assignment not found") + + +def _block_sizes(wrapper, dtype): + for node in wrapper.body: + if isinstance(node, ast.If): + try: + selected = node.body if _static_value(node.test, {"q.dtype": dtype}) else node.orelse + except ValueError: + continue + sizes = {} + for statement in selected: + if isinstance(statement, ast.Assign) and len(statement.targets) == 1: + target = statement.targets[0] + if isinstance(target, ast.Name) and target.id in {"BLOCK_M", "BLOCK_N"}: + sizes[target.id] = _static_value(statement.value) + if set(sizes) == {"BLOCK_M", "BLOCK_N"}: + return sizes["BLOCK_M"], sizes["BLOCK_N"] + raise ValueError("BLOCK_M/BLOCK_N selection not found in SWA wrapper") + + +def _block_d(wrapper, head_dim): + for node in wrapper.body: + if isinstance(node, ast.Assign) and len(node.targets) == 1: + target = node.targets[0] + if isinstance(target, ast.Name) and target.id == "BLOCK_D": + return _static_value(node.value, {"head_dim": head_dim}) + raise ValueError("BLOCK_D assignment not found in SWA wrapper") + + +def _mask_shape(kernel_tree, wrapper, global_window, local_window, block_values=None): + mask_call = _call(wrapper, "get_mask_causal_with_window") + if len(mask_call.args) < 2: + raise ValueError("SWA mask block dimensions not found") + block_m, block_n = (_static_value(node, block_values) for node in mask_call.args[:2]) + aux_size = _static_value(_assignment(kernel_tree, "AUX_MASK_SIZE")) + try: + mask_function = _definition(kernel_tree, "get_mask_causal_with_window", ast.FunctionDef) + if not any( + isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == "M_boundary" + for target in node.targets) + for node in mask_function.body): + raise ValueError("mask calculation is delegated") + except ValueError: + mask_function = _definition(kernel_tree, "_get_mask_causal_with_window_cached", ast.FunctionDef) + values = { + "BLOCK_M": block_m, "BLOCK_N": block_n, "local_window_size": local_window, "global_window_size": global_window, + "AUX_MASK_SIZE": aux_size + } + for node in mask_function.body: + if isinstance(node, ast.Assign) and len(node.targets) == 1: + target = node.targets[0] + if isinstance(target, ast.Name) and target.id in {"M", "N", "M_boundary", "N_boundary"}: + values[target.id] = _static_value(node.value, values) + if "M_boundary" not in values or "N_boundary" not in values: + raise ValueError("SWA mask shape calculation not found") + return values["M_boundary"], values["N_boundary"] + + +def _test_row(test_tree, assignment_name, config_id): + rows = _static_value(_assignment(test_tree, assignment_name)) + matching = [row for row in rows if row[-1] == config_id] + if len(matching) != 1: + raise ValueError(f"expected one {assignment_name} row with id {config_id!r}") + return matching[0] + + +def _master_test_row(test_tree, assignment_name, config_id): + """Read a named pytest.param from the refactored master test lists.""" + value = _assignment(test_tree, assignment_name) + if isinstance(value, ast.ListComp): + value = value.generators[0].iter + if not isinstance(value, (ast.List, ast.Tuple)): + raise ValueError(f"unsupported {assignment_name} parametrization") + for entry in value.elts: + if isinstance(entry, ast.Call) and isinstance(entry.func, ast.Attribute) and entry.func.attr == "param": + name = next((_static_value(kw.value) for kw in entry.keywords if kw.arg == "id"), None) + if name == config_id: + return [_static_value(arg) for arg in entry.args] + [config_id] + elif isinstance(entry, ast.Tuple): + row = _static_value(entry) + if row[-1] == config_id: + return row + raise ValueError(f"{assignment_name} row {config_id!r} not found") + + +def _master_test_window(test_tree, test_name, layout): + if test_name == "test_swa_infer": + rows = _static_value(_assignment(test_tree, "SWA_INFER_WINDOWS")) + return next((row[1], row[2]) for row in rows if row[0] == layout) + if test_name == "test_swa": + function = _definition(test_tree, test_name, ast.FunctionDef) + for decorator in function.decorator_list: + if isinstance(decorator, ast.Call) and len(decorator.args) >= 2: + try: + if _static_value(decorator.args[0]) == "interleave,global_window,local_window": + rows = _static_value(decorator.args[1]) + return next((row[1], row[2]) for row in rows if row[0] == (layout == "ABAB")) + except ValueError: + continue + if test_name == "test_paged": + cases = _assignment(test_tree, "PAGED_CASES") + for node in ast.walk(cases): + if not (isinstance(node, ast.ListComp) and isinstance(node.elt, ast.Tuple) and len(node.elt.elts) >= 6 + and isinstance(node.elt.elts[5], ast.Name) and node.elt.elts[5].id == "window"): + continue + for generator in node.generators: + if isinstance(generator.target, ast.Tuple): + names = [item.id for item in generator.target.elts if isinstance(item, ast.Name)] + if names == ["layout", "window"]: + rows = _static_value(generator.iter) + return (_static_value(node.elt.elts[4]), next(row[1] for row in rows if row[0] == layout)) + raise ValueError(f"master {test_name} window for {layout!r} not found") + + +def _test_window(test_tree, test_name, layout): + function = _definition(test_tree, test_name, ast.FunctionDef) + for decorator in function.decorator_list: + if not (isinstance(decorator, ast.Call) and len(decorator.args) >= 2 + and isinstance(decorator.args[0], ast.Constant)): + continue + names = decorator.args[0].value + if names not in {"gqa_layout, global_window, local_window", "gqa_interleave, global_window, local_window"}: + continue + rows = _static_value(decorator.args[1]) + selector = layout if names.startswith("gqa_layout") else layout == "ABAB" + matching = [row for row in rows if row[0] == selector] + if len(matching) != 1: + raise ValueError(f"expected one window row for layout {layout!r}") + return matching[0][1], matching[0][2] + raise ValueError("SWA window parametrization not found") + + +_ARG_TYPES = { + "swa": { + "*i32": + "cu_q_lens_ptr cu_total_seq_lens_ptr kv_lens_ptr kvlens_ptr seqlens_ptr block_table_ptr block_tables_ptr", + "*fp32": "o_f32_ptr lse_ptr delta_ptr", + "*i1": "causal_mask_ptr", + "fp32": "scale softmax_scale", + }, + "paged": { + "*i32": + "task_b_ptr task_q_block_ptr task_q_head_ptr core_task_offsets_ptr cu_q_lens_ptr seqlens_kv_ptr block_tables_ptr seqlens_ptr", + "*fp32": "acc_ws_ptr lse_ws_ptr", + "*i1": "aux_mask_ptr", + "fp32": "softmax_scale", + }, + "sdpa": {"*fp32": "d lse M dk dv", "*i1": "mask", "fp32": "scale"}, + "flex": { + "*bf16": "Q K V OUT DO DQ", + "*fp32": "LSE DELTA", + "*i1": "DENSE_MASK PARTIAL_MASK_PACKED", + "fp32": "SM_SCALE", + "i32": "NUM_TASKS NUM_Q_BLOCKS Q_HEAD Q_LEN KV_LEN GQA_SHARED_HEADS", + }, +} + + +def _runtime_type(name, family, dtype): + for arg_type, names in _ARG_TYPES[family].items(): + if name in names.split(): + return arg_type + if family == "flex": + if name.startswith("stride_"): + return "i32" + if name.isupper(): + return "*i32" + raise ValueError(f"cannot infer FlexAttention argument type: {name}") + if family == "sdpa" or name.endswith("_ptr"): + return {"torch.bfloat16": "*bf16", "torch.float16": "*fp16", "torch.float32": "*fp32"}[dtype] + return "i32" + + +def _specialization(kernel, wrapper, constants, dtype, family, *, values=None, options=None): + """Build and validate the common (signature, constants, options) result.""" + if dtype not in {"torch.bfloat16", "torch.float16", "torch.float32"}: + raise ValueError(f"unsupported {family} test dtype: {dtype}") + args = kernel.args.args + constexpr = {arg.arg for arg in args if arg.annotation and ast.unparse(arg.annotation) == "tl.constexpr"} + if family == "sdpa" or (family == "flex" and "USE_Q_TASK_LIST" not in constexpr): + constants = {name: value for name, value in constants.items() if name in constexpr} + # SDPA leaves optional constexpr arguments at their declared defaults. + if family == "sdpa": + optional = {"LOW_TYPE", "HIGH_TYPE"} + if kernel.args.defaults: + optional |= {arg.arg for arg in args[-len(kernel.args.defaults):]} + required = constexpr - optional + constant_names = set(constants) + if not required <= constant_names or not constant_names <= constexpr: + raise ValueError(f"{kernel.name} constexpr parameters changed: " + f"missing={required - constant_names}, extra={constant_names - constexpr}") + else: + required = constexpr + if required != set(constants): + raise ValueError(f"{kernel.name} constexpr parameters changed: {required ^ set(constants)}") + signature = {arg.arg: _runtime_type(arg.arg, family, dtype) for arg in args if arg.arg not in constexpr} + aliases = {"intra_cache_num": "buf_slot_num_of_veccore", "inter_cache_num": "buf_slot_num_of_crosscore"} + arg_names = {arg.arg for arg in args} + launch_options = { + aliases.get(kw.arg, kw.arg): _static_value(kw.value, values) + for kw in _launch(wrapper, kernel.name).keywords + if kw.arg is not None and kw.arg not in arg_names + } + return signature, constants, {**launch_options, **(options or {})} + + +def _first_autotune_config(kernel): + for decorator in kernel.decorator_list: + if not isinstance(decorator, ast.Call): + continue + for keyword in decorator.keywords: + if keyword.arg != "configs": + continue + comprehension = keyword.value + if isinstance(comprehension, ast.List): + comprehension = comprehension.elts[0] if comprehension.elts else None + if isinstance(comprehension, ast.Call): + if comprehension.args and isinstance(comprehension.args[0], ast.Dict): + return { + _static_value(key): _static_value(value) + for key, value in zip(comprehension.args[0].keys, comprehension.args[0].values) + } + if not isinstance(comprehension, ast.ListComp): + continue + names = {} + for generator in comprehension.generators: + source = generator.iter + if isinstance(source, ast.IfExp): + # The selected source file is npu/a5, so is_910() is false. + source = source.body + candidates = _static_value(source) + names[generator.target.id] = candidates[0] + config_call = comprehension.elt + if not (isinstance(config_call, ast.Call) and config_call.args + and isinstance(config_call.args[0], ast.Dict)): + continue + values = { + _static_value(key): _static_value(value, names) + for key, value in zip(config_call.args[0].keys, config_call.args[0].values) + } + return values + return {} + + +def infer_mojo_swa_family(root, test_path, test_name, kernel_path, kernel_name, config_id, layout): + """Read one specialization for the seven Ascend 950 SWA JIT kernels.""" + master = Path(kernel_path).parent.name == "npu_a5_triton" + families = { + "_sdpa_infer_kernel": ("test_swa_infer", "SWA_INFER_CASES" if master else "test_configs_swa_infer", "infer"), + "_swa_paged_prefill_kernel": + ("test_paged" if master else "test_paged_prefill_swa", "test_configs_swa_prefill", "prefill"), + "_swa_paged_prefill_aggregation_kernel": + ("test_paged" if master else "test_paged_prefill_swa", "test_configs_swa_prefill", "prefill"), + "_swa_paged_decode_kernel": + ("test_paged" if master else "test_paged_decode_swa", "test_configs_swa_decode", "decode"), + "_swa_fwd_kernel": + ("test_swa" if master else "test_swa_function", "SWA_CASES" if master else "test_configs_swa", "training"), + "_swa_bwd_dkdv_kernel": ("test_swa" if master else "test_swa_function", + "SWA_CASES" if master else "test_configs_swa", "training"), + "_swa_bwd_dq_kernel": ("test_swa" if master else "test_swa_function", + "SWA_CASES" if master else "test_configs_swa", "training"), + } + if kernel_name not in families: + raise ValueError(f"no SWA source reader for {kernel_name}") + expected_test, config_assignment, family = families[kernel_name] + if test_name != expected_test: + raise ValueError(f"{kernel_name} requires {expected_test}") + root = Path(root) + test_tree = _parse(test_path) + kernel_tree = _parse(kernel_path) + row = (_master_test_row if master else _test_row)(test_tree, config_assignment, config_id) + global_window, local_window = (_master_test_window if master else _test_window)(test_tree, test_name, layout) + if family == "infer": + batch, q_heads, kv_heads, head_dim, _, _, dtype, _ = row + page_size = None + elif family == "prefill": + batch, q_heads, kv_heads, head_dim, _, _, page_size, dtype, _ = row + elif family == "decode": + batch, _, q_heads, kv_heads, head_dim, _, page_size, dtype, _ = row + else: + batch, q_heads, kv_heads, head_dim, _, _, dtype, _ = row + page_size = None + if batch <= 0 or layout not in {"ABAB", "AABB"}: + raise ValueError("unsupported SWA test batch or layout") + + if family == "training": + interleave = layout == "ABAB" + test_function = _definition(test_tree, test_name, ast.FunctionDef) + if master: + options_call = _call(test_function, "dict") + is_causal = next(_static_value(kw.value) for kw in options_call.keywords if kw.arg == "is_causal") + output_f32 = next( + _static_value(kw.value) for node in ast.walk(test_function) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "swa" + for kw in node.keywords if kw.arg == "output_f32") + else: + test_call = _call(test_function, "swa_func") + is_causal = _static_value(test_call.args[5]) + output_f32 = _static_value(test_call.args[10]) + if not isinstance(is_causal, bool) or not isinstance(output_f32, bool): + raise ValueError("SWA training test flags could not be read") + else: + if master: + interleave = layout == "ABAB" + is_causal = True + else: + interleave = _gqa_interleave(root, layout) + test_function = _definition(test_tree, test_name, ast.FunctionDef) + constructor = _call(test_function, + {"infer": "MojoSWA", "prefill": "MojoPagedPrefillSWA", "decode": + "MojoPagedDecodeSWA"}[family]) + is_causal = next((_static_value(k.value) for k in constructor.keywords if k.arg == "is_causal"), None) + if not isinstance(is_causal, bool): + raise ValueError("SWA test is_causal flag could not be read") + output_f32 = False + + wrapper_name = { + "infer": "swa_infer_impl", "prefill": "swa_paged_prefill_impl", "decode": "swa_paged_decode_impl", "training": + "swa_fwd_impl" + }[family] + if family == "training" and kernel_name != "_swa_fwd_kernel": + wrapper_name = ({"_swa_bwd_dkdv_kernel": "swa_dkdv", "_swa_bwd_dq_kernel": "swa_dq"}[kernel_name] + if master else "swa_bwd_impl") + wrapper = _definition(kernel_tree, wrapper_name, ast.FunctionDef) + kernel = _definition(kernel_tree, kernel_name, ast.FunctionDef) + + if family == "infer": + block_m, block_n = _block_sizes(wrapper, dtype) + block_d = _block_d(wrapper, head_dim) + elif family == "prefill": + block_m = 128 if dtype != "torch.float32" else 64 + block_n = min(block_m, 1 << (page_size - 1).bit_length()) + block_d = _block_d(wrapper, head_dim) + elif family == "decode": + block_m = None + block_n = min(128, 1 << (page_size - 1).bit_length()) + block_d = 1 << (head_dim - 1).bit_length() + else: + autotune = _first_autotune_config(kernel) + block_m = autotune["BLOCK_M"] + block_n = autotune["BLOCK_N"] + block_d = _block_d(wrapper, head_dim) + + constants = {} + if family != "decode": + mask_m, mask_n = _mask_shape(kernel_tree, wrapper, global_window, local_window, + {"BLOCK_M": block_m, "BLOCK_N": block_n}) + constants.update(causal_mask_m_size=mask_m, causal_mask_n_size=mask_n, IS_CAUSAL=is_causal) + if family in {"infer", "prefill", "training"}: + constants.update(GLOBAL_WINDOW=global_window, LOCAL_WINDOW=local_window, NUM_Q_HEADS=q_heads, + NUM_KV_HEADS=kv_heads, GQA_INTERLEAVE=interleave, HEAD_DIM=head_dim, BLOCK_M=block_m, + BLOCK_N=block_n, BLOCK_D=block_d) + else: + constants.update(GLOBAL_WINDOW=global_window, LOCAL_WINDOW=local_window, NUM_Q_HEADS=q_heads, + NUM_KV_HEADS=kv_heads, GQA_INTERLEAVE=interleave, HEAD_DIM=head_dim, PAGE_SIZE=page_size, + BLOCK_SIZE_D=block_d, BLOCK_SIZE_N=block_n) + if family == "prefill": + constants["PAGE_SIZE"] = page_size + if kernel_name == "_swa_paged_prefill_aggregation_kernel": + if not (page_size < 128 and 128 % page_size == 0): + raise ValueError("selected test case does not launch page aggregation") + constants["PAGE_AGGREGATION_NUM"] = 128 // page_size + elif page_size < 128 and 128 % page_size == 0: + raise ValueError("selected test case launches page aggregation instead") + if family == "training": + if kernel_name == "_swa_fwd_kernel": + constants["OUTPUT_F32"] = output_f32 + if block_m is None or block_n is None: + raise ValueError("SWA autotune block sizes unavailable") + + options = {"multibuffer": autotune["multibuffer"]} if family == "training" else {} + return _specialization(kernel, wrapper, constants, dtype, "swa", values={"unit_flag": True}, options=options) + + +def infer_mojo_paged_attention(root, test_path, test_name, kernel_path, kernel_name, config_id, layout, + cube_cores=None): + """Read one Ascend 950 paged attention specialization from source.""" + master = Path(kernel_path).parent.name == "npu_a5_triton" + prefill_names = {"paged_prefill_kernel", "paged_prefill_page_aggregation_kernel"} + decode_names = {"paged_decode_kernel", "paged_decode_fd_kernel"} + if kernel_name in prefill_names: + expected_test = "test_paged_prefill_gqa" + row_name = "test_configs_prefill" + elif kernel_name in decode_names: + expected_test = "test_paged_decode_gqa" + row_name = "test_configs_decode" + else: + raise ValueError(f"no paged attention source reader for {kernel_name}") + if test_name != ("test_paged" if master else expected_test) or layout not in {"ABAB", "AABB"}: + raise ValueError(f"{kernel_name} requires {expected_test} and an ABAB/AABB layout") + + test_tree = _parse(test_path) + test_function = _definition(test_tree, test_name, ast.FunctionDef) + expected_call = ("paged_case" + if master else "MojoPagedPrefillGQA" if kernel_name in prefill_names else "MojoPagedDecodeGQA") + if not any( + isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == expected_call + for node in ast.walk(test_function)): + raise ValueError("test no longer calls the expected paged attention operator") + row = _test_row(test_tree, row_name, config_id) + if kernel_name in prefill_names: + batch, q_heads, kv_heads, head_dim, _, _, page_size, dtype, _ = row + else: + batch, q_heads, kv_heads, head_dim, max_kv_len, page_size, dtype, _ = row + if dtype not in {"torch.bfloat16", "torch.float16", "torch.float32"}: + raise ValueError(f"unsupported paged attention dtype: {dtype}") + kernel_tree = _parse(kernel_path) + kernel = _definition(kernel_tree, kernel_name, ast.FunctionDef) + wrapper_name = "paged_attention_prefill_impl" if kernel_name in prefill_names else "paged_attention_decode_impl" + wrapper = _definition(kernel_tree, wrapper_name, ast.FunctionDef) + + block_n = min(128, 1 << (page_size - 1).bit_length()) + constants = { + "NUM_Q_HEADS": q_heads, "NUM_KV_HEADS": kv_heads, "GQA_INTERLEAVE": layout == "ABAB", "HEAD_DIM": head_dim, + "PAGE_SIZE": page_size + } + if kernel_name in prefill_names: + if kernel_name == "paged_prefill_page_aggregation_kernel": + if not (page_size < 128 and 128 % page_size == 0): + raise ValueError("selected test case does not launch page aggregation") + constants["PAGE_AGGREGATION_NUM"] = 128 // page_size + elif page_size < 128 and 128 % page_size == 0: + raise ValueError("selected test case launches page aggregation instead") + for node in ast.walk(wrapper): + if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "ones" + and node.args): + constants["AUX_MASK_SIZE"] = _static_value(node.args[0]) + break + constants.update(BLOCK_SIZE_M=128, BLOCK_SIZE_N=block_n, BLOCK_SIZE_D=head_dim) + else: + constants.update(BLOCK_SIZE_D=1 << (head_dim - 1).bit_length(), BLOCK_SIZE_N=block_n) + if kernel_name == "paged_decode_fd_kernel": + # The test draws sequence lengths at random. Use its declared + # maximum as a deterministic representative specialization. + if cube_cores is None: + import triton + cube_cores = triton.runtime.driver.active.utils.get_device_properties("npu")["num_aicore"] + loop_times = batch * kv_heads + constants["KV_SPLIT_PARTS"] = max(1, min(cube_cores // loop_times, max_kv_len // 256)) + return _specialization(kernel, wrapper, constants, dtype, "paged") + + +def infer_mojo_sdpa(root, test_path, test_name, kernel_path, kernel_name): + """Use one SDPA accuracy test shape for the four Ascend 950 SDPA kernels.""" + master = Path(kernel_path).parent.name == "npu_a5_triton" + supported = {"_sdpa_infer_kernel", "kernel_sdpa_fwd", "kernel_sdpa_bwd_qkv", "kernel_sdpa_bwd_q"} + if kernel_name not in supported or test_name != "test_sdpa": + raise ValueError("SDPA source reader requires test_sdpa and an Ascend 950 SDPA kernel") + test_tree = _parse(test_path) + test_function = _definition(test_tree, test_name, ast.FunctionDef) + row = _static_value(_assignment(test_tree, "SDPA_CASES"))[0] if master else None + if not master: + for decorator in test_function.decorator_list: + if (isinstance(decorator, ast.Call) and len(decorator.args) >= 2 + and isinstance(decorator.args[0], ast.Constant) + and decorator.args[0].value == "bsz, q_head_num, kv_head_num, head_dim, seq_length, block_size"): + rows = _static_value(decorator.args[1]) + if len(rows) != 1: + raise ValueError("test_sdpa must have exactly one source-readable test shape") + row = rows[0] + break + if row is None: + raise ValueError("test_sdpa shape parametrization not found") + batch, q_heads, kv_heads, head_dim, test_seq_len, _ = row + generator = _definition(test_tree, "make_sdpa_case" if master else "generate_diffusion_attn_test_data", + ast.FunctionDef) + for node in ast.walk(generator): + if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "randn" + and len(node.args) >= 4): + sequence = test_seq_len if master else _static_value(node.args[2], {"seq_length": test_seq_len}) + dtype = next((_static_value(k.value) for k in node.keywords if k.arg == "dtype"), None) + if master and dtype is None: + dtype = "torch.bfloat16" if any( + isinstance(k.value, ast.Attribute) and k.value.attr == "bfloat16" + for k in node.keywords + if k.arg == "dtype") else None + break + else: + raise ValueError("test_sdpa tensor creation not found") + if dtype != "torch.bfloat16": + raise ValueError(f"unsupported test_sdpa dtype: {dtype}") + scale = 1.0 / math.sqrt(head_dim) + + kernel_tree = _parse(kernel_path) + kernel = _definition(kernel_tree, kernel_name, ast.FunctionDef) + wrapper_name = { + "_sdpa_infer_kernel": "sdpa_infer_impl", "kernel_sdpa_fwd": "sdpa_fwd_impl", "kernel_sdpa_bwd_qkv": + "sdpa_bwd_impl", "kernel_sdpa_bwd_q": "sdpa_bwd_impl" + }[kernel_name] + wrapper = _definition(kernel_tree, wrapper_name, ast.FunctionDef) + tune = _first_autotune_config(kernel) + q_strides = (q_heads * sequence * head_dim, sequence * head_dim, head_dim, 1) + kv_strides = (kv_heads * sequence * head_dim, sequence * head_dim, head_dim, 1) + d_strides = (q_heads * sequence, sequence, 1) + + constants = {} + if kernel_name == "_sdpa_infer_kernel": + prefixes = {"q": q_strides, "k": kv_strides, "v": kv_strides, "o": q_strides} + suffixes = { + "q": ("z", "h", "m", "k"), "k": ("z", "h", "n", "k"), "v": ("z", "h", "n", "k"), "o": ("z", "h", "m", "n") + } + for prefix, strides in prefixes.items(): + constants.update({f"stride_{prefix}{axis}": value for axis, value in zip(suffixes[prefix], strides)}) + constants.update(BSZ=batch, Q_HEAD_NUM=q_heads, KV_HEAD_NUM=kv_heads, SEQ=sequence, HEAD_DIM=head_dim, + HAS_MASK=True) + else: + constants.update(scale=scale, num_group=kv_heads, B=batch, N=q_heads, S=sequence, H=head_dim, + LOW_TYPE="tl.bfloat16", HIGH_TYPE="tl.float32") + for prefix, strides in (("Q", q_strides), ("K", kv_strides), ("V", kv_strides)): + constants.update({f"STRIDE_{prefix}_{axis}": value for axis, value in zip(("B", "N", "S", "H"), strides)}) + constants.update({f"STRIDE_D_{axis}": value for axis, value in zip(("B", "N", "S"), d_strides)}) + constants.update({name: value for name, value in tune.items() if name.startswith("BLOCK_")}) + + options = {"multibuffer": tune["multibuffer"]} if "multibuffer" in tune else {} + return _specialization(kernel, wrapper, constants, dtype, "sdpa", options=options) + + +def infer_mojo_flex(root, test_path, test_name, kernel_path, kernel_name, config_id): + """Read one FlexAttention test shape and a source-backed JIT specialization.""" + master = Path(kernel_path).parent.name == "npu_a5_triton" + if test_name != ("test_business" if master else "test_flex_attention") or kernel_name not in { + "flex_attention_kernel", "flex_attention_backward_dq_kernel" + }: + raise ValueError("FlexAttention reader requires test_flex_attention and a supported JIT kernel") + test_tree = _parse(test_path) + function = _definition(test_tree, test_name, ast.FunctionDef) + row = None + nodes = (ast.walk(_assignment(test_tree, "BUSINESS_FLEX_CASES")) if master else + (node for decorator in function.decorator_list for node in ast.walk(decorator))) + for node in nodes: + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "param"): + continue + case_id = next((_static_value(k.value) for k in node.keywords if k.arg == "id"), None) + if case_id == config_id: + if row is not None: + raise ValueError(f"duplicate FlexAttention test id {config_id!r}") + row = node + if row is None: + raise ValueError(f"FlexAttention test id {config_id!r} not found") + _, q_heads, kv_heads, head_dim = [_static_value(node) for node in row.args[:4]] + dtype = _static_value(row.args[8]) + if dtype != "torch.bfloat16": + raise ValueError(f"unsupported FlexAttention dtype: {dtype}") + + kernel_tree = _parse(kernel_path) + kernel = _definition(kernel_tree, kernel_name, ast.FunctionDef) + wrapper_name = ( + ("_flex_attention_forward_leaf" if kernel_name == "flex_attention_kernel" else "_flex_attention_dq_leaf") + if master else + ("flex_attention_fwd_impl" if kernel_name == "flex_attention_kernel" else "flex_attention_bwd_impl")) + wrapper = _definition(kernel_tree, wrapper_name, ast.FunctionDef) + tile = _static_value(_assignment(kernel_tree, "TILE_BLOCK_SIZE")) + if master: + mask_call = next(node for node in ast.walk(_definition(test_tree, "_business_inputs", ast.FunctionDef)) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + and node.func.id == "create_flex_block_mask") + sparse_q = sparse_kv = next(_static_value(kw.value) for kw in mask_call.keywords if kw.arg == "BLOCK_SIZE") + else: + sparse_q = _static_value(_assignment(test_tree, "Q_BLOCK_SIZE")) + sparse_kv = _static_value(_assignment(test_tree, "KV_BLOCK_SIZE")) + if sparse_q % tile or sparse_kv % tile: + raise ValueError("FlexAttention test block sizes do not match kernel tile") + + # The test builds a packed BlockMask, while the Q-task list is calculated + # from mask contents at runtime. Choose the direct scheduling variant. + constants = { + "QK_HEAD_DIM": head_dim, "V_HEAD_DIM": head_dim, "BLOCK_M": tile, "BLOCK_N": tile, "SPARSE_Q_BLOCK_SIZE": + sparse_q, "SPARSE_KV_BLOCK_SIZE": sparse_kv, "HAS_FULL_BLOCKS": True, "USE_PACKED_PARTIAL_MASK": True, + "USE_Q_TASK_LIST": False + } + if kernel_name == "flex_attention_backward_dq_kernel": + constants["SM_SCALE"] = 1.0 / math.sqrt(head_dim) + constants["NUM_KV_SUB_BLOCKS"] = sparse_kv // tile + constants["GQA_SHARED_HEADS"] = q_heads // kv_heads + + return _specialization(kernel, wrapper, constants, dtype, "flex") + + +def infer_mojo_case(root, test_path, test_name, kernel_path, kernel_name, config_id=None, layout=None, cube_cores=None): + """Dispatch an A5 kernel to its source reader, with no test execution.""" + kernel_path = Path(kernel_path) + old_layout = kernel_path.parent.name == "a5" and kernel_path.parent.parent.name == "npu" + master_layout = kernel_path.parent.name == "npu_a5_triton" and kernel_path.parent.parent.name == "kernels" + if not (old_layout or master_layout): + raise ValueError(f"source reader supports only A5 Triton kernels: {kernel_path}") + readers = { + "swa.py": + lambda: infer_mojo_swa_family(root, test_path, test_name, kernel_path, kernel_name, config_id, layout), + "flash_attention.py": + lambda: infer_mojo_paged_attention(root, test_path, test_name, kernel_path, kernel_name, config_id, layout, + cube_cores), + "sdpa.py": + lambda: infer_mojo_sdpa(root, test_path, test_name, kernel_path, kernel_name), + "flex_attention.py": + lambda: infer_mojo_flex(root, test_path, test_name, kernel_path, kernel_name, config_id), + } + if kernel_path.name not in readers: + raise ValueError(f"no A5 source reader for {kernel_path.name}") + return readers[kernel_path.name]() + + +def infer_mojo_swa_infer(root, test_path, test_name, kernel_path, kernel_name, config_id, layout): + """Compatibility entry point for the original SWA inference reader.""" + if test_name != "test_swa_infer" or kernel_name != "_sdpa_infer_kernel": + raise ValueError("source extraction currently supports test_swa_infer -> _sdpa_infer_kernel") + return infer_mojo_swa_family(root, test_path, test_name, kernel_path, kernel_name, config_id, layout) diff --git a/third_party/ascend/unittest/Conversion/General/TritonAscendAllPass/conv1d.mlir b/third_party/ascend/unittest/Conversion/General/TritonAscendAllPass/conv1d.mlir new file mode 100644 index 0000000000..ffe0134e14 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonAscendAllPass/conv1d.mlir @@ -0,0 +1,160 @@ +// RUN: triton-opt --triton-to-structured '--discrete-mask-access-conversion=compile-on-910-95=False compile-mode=simd' '--triton-to-unstructure=compile-on-910-95=False compile-mode=simd' --triton-to-hivm --triton-to-hfusion --triton-to-llvm --bubble-up-operation --triton-to-structured '--triton-to-linalg=compile-mode=simd' --split-input-file %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @triton_conv1d_2d_kernel(%input_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %weight_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %bias_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %output_ptr: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %output_offsets = arith.constant dense<126> : tensor<16x1xi32> + %weight_offsets = arith.constant dense<3> : tensor<1x16x1xi32> + %weight_offsets_0 = arith.constant dense<48> : tensor<16x1x1xi32> + %input_offsets = arith.constant dense<128> : tensor<16x1xi32> + %c_in_offsets = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %c_in_offsets_1 = tt.expand_dims %c_in_offsets {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %l_in_offsets = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %l_in_offsets_2 = tt.expand_dims %l_in_offsets {axis = 0 : i32} : tensor<128xi32> -> tensor<1x128xi32> + %input_offsets_3 = arith.muli %c_in_offsets_1, %input_offsets : tensor<16x1xi32> + %input_offsets_4 = tt.broadcast %input_offsets_3 : tensor<16x1xi32> -> tensor<16x128xi32> + %input_offsets_5 = tt.broadcast %l_in_offsets_2 : tensor<1x128xi32> -> tensor<16x128xi32> + %input_offsets_6 = arith.addi %input_offsets_4, %input_offsets_5 : tensor<16x128xi32> + %input_tensor = tt.splat %input_ptr : !tt.ptr -> tensor<16x128x!tt.ptr> + %input_tensor_7 = tt.addptr %input_tensor, %input_offsets_6 : tensor<16x128x!tt.ptr>, tensor<16x128xi32> + %input_tensor_8 = tt.load %input_tensor_7 : tensor<16x128x!tt.ptr> + %c_out_offsets = tt.expand_dims %c_in_offsets_1 {axis = 2 : i32} : tensor<16x1xi32> -> tensor<16x1x1xi32> + %c_in_group_offsets = tt.expand_dims %c_in_offsets {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %c_in_group_offsets_9 = tt.expand_dims %c_in_group_offsets {axis = 2 : i32} : tensor<1x16xi32> -> tensor<1x16x1xi32> + %k_offsets = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %k_offsets_10 = tt.expand_dims %k_offsets {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %k_offsets_11 = tt.expand_dims %k_offsets_10 {axis = 1 : i32} : tensor<1x3xi32> -> tensor<1x1x3xi32> + %weight_offsets_12 = arith.muli %c_out_offsets, %weight_offsets_0 : tensor<16x1x1xi32> + %weight_offsets_13 = arith.muli %c_in_group_offsets_9, %weight_offsets : tensor<1x16x1xi32> + %weight_offsets_14 = tt.broadcast %weight_offsets_12 : tensor<16x1x1xi32> -> tensor<16x16x1xi32> + %weight_offsets_15 = tt.broadcast %weight_offsets_13 : tensor<1x16x1xi32> -> tensor<16x16x1xi32> + %weight_offsets_16 = arith.addi %weight_offsets_14, %weight_offsets_15 : tensor<16x16x1xi32> + %weight_offsets_17 = tt.broadcast %weight_offsets_16 : tensor<16x16x1xi32> -> tensor<16x16x3xi32> + %weight_offsets_18 = tt.broadcast %k_offsets_11 : tensor<1x1x3xi32> -> tensor<16x16x3xi32> + %weight_offsets_19 = arith.addi %weight_offsets_17, %weight_offsets_18 : tensor<16x16x3xi32> + %weight_tensor = tt.splat %weight_ptr : !tt.ptr -> tensor<16x16x3x!tt.ptr> + %weight_tensor_20 = tt.addptr %weight_tensor, %weight_offsets_19 : tensor<16x16x3x!tt.ptr>, tensor<16x16x3xi32> + %weight_tensor_21 = tt.load %weight_tensor_20 : tensor<16x16x3x!tt.ptr> + %bias_tensor = tt.splat %bias_ptr : !tt.ptr -> tensor<16x!tt.ptr> + %bias_tensor_22 = tt.addptr %bias_tensor, %c_in_offsets : tensor<16x!tt.ptr>, tensor<16xi32> + %bias_tensor_23 = tt.load %bias_tensor_22 : tensor<16x!tt.ptr> + %output = ascend.conv1d(%input_tensor_8, %weight_tensor_21, %bias_tensor_23) {dilation = 1 : i64, groups = 1 : i64, padding_size = 0 : i64, stride = 1 : i64} : (tensor<16x128xf16>, tensor<16x16x3xf16>, tensor<16xf16>) -> tensor<16x126xf16> + %l_out_off = tt.make_range {end = 126 : i32, start = 0 : i32} : tensor<126xi32> + %l_out_off_24 = tt.expand_dims %l_out_off {axis = 0 : i32} : tensor<126xi32> -> tensor<1x126xi32> + %output_offsets_25 = arith.muli %c_in_offsets_1, %output_offsets : tensor<16x1xi32> + %output_offsets_26 = tt.broadcast %output_offsets_25 : tensor<16x1xi32> -> tensor<16x126xi32> + %output_offsets_27 = tt.broadcast %l_out_off_24 : tensor<1x126xi32> -> tensor<16x126xi32> + %output_offsets_28 = arith.addi %output_offsets_26, %output_offsets_27 : tensor<16x126xi32> + %0 = tt.splat %output_ptr : !tt.ptr -> tensor<16x126x!tt.ptr> + %1 = tt.addptr %0, %output_offsets_28 : tensor<16x126x!tt.ptr>, tensor<16x126xi32> + tt.store %1, %output : tensor<16x126x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @triton_conv1d_2d_kernel( +// CHECK: %[[VAL_0:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [16, 128], strides: [128, 1] +// CHECK: %[[VAL_1:.*]] = memref.alloc() : memref<16x128xf16> +// CHECK: memref.copy %[[VAL_0]], %[[VAL_1]] +// CHECK: %[[VAL_2:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [16, 16, 3], strides: [48, 3, 1] +// CHECK: %[[VAL_3:.*]] = memref.alloc() : memref<16x16x3xf16> +// CHECK: memref.copy %[[VAL_2]], %[[VAL_3]] +// CHECK: %[[VAL_4:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [16], strides: [1] +// CHECK: %[[VAL_5:.*]] = memref.alloc() : memref<16xf16> +// CHECK: memref.copy %[[VAL_4]], %[[VAL_5]] +// CHECK: %[[VAL_6:.*]] = hfusion.conv1d +// CHECK-SAME: {dilation = 1 : i32, groups = 1 : i32, padding = 0 : i32, stride = 1 : i32} +// CHECK: %[[VAL_7:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [16, 126], strides: [126, 1] +// CHECK: bufferization.materialize_in_destination %[[VAL_6]] in writable %[[VAL_7]] + + +// ----- + + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @triton_conv1d_3d_kernel(%input_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %weight_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %bias_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %output_ptr: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %output_offsets = arith.constant dense<128> : tensor<1x30x1xi32> + %output_offsets_0 = arith.constant dense<3840> : tensor<2x1x1xi32> + %weight_offsets = arith.constant dense<3> : tensor<1x32x1xi32> + %weight_offsets_1 = arith.constant dense<96> : tensor<30x1x1xi32> + %input_offsets = arith.constant dense<128> : tensor<1x32x1xi32> + %input_offsets_2 = arith.constant dense<4096> : tensor<2x1x1xi32> + %n_offsets = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %n_offsets_3 = tt.expand_dims %n_offsets {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %n_offsets_4 = tt.expand_dims %n_offsets_3 {axis = 2 : i32} : tensor<2x1xi32> -> tensor<2x1x1xi32> + %c_in_offsets = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %c_in_offsets_5 = tt.expand_dims %c_in_offsets {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %c_in_offsets_6 = tt.expand_dims %c_in_offsets_5 {axis = 2 : i32} : tensor<1x32xi32> -> tensor<1x32x1xi32> + %l_in_offsets = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %l_in_offsets_7 = tt.expand_dims %l_in_offsets {axis = 0 : i32} : tensor<128xi32> -> tensor<1x128xi32> + %l_in_offsets_8 = tt.expand_dims %l_in_offsets_7 {axis = 1 : i32} : tensor<1x128xi32> -> tensor<1x1x128xi32> + %input_offsets_9 = arith.muli %n_offsets_4, %input_offsets_2 : tensor<2x1x1xi32> + %input_offsets_10 = arith.muli %c_in_offsets_6, %input_offsets : tensor<1x32x1xi32> + %input_offsets_11 = tt.broadcast %input_offsets_9 : tensor<2x1x1xi32> -> tensor<2x32x1xi32> + %input_offsets_12 = tt.broadcast %input_offsets_10 : tensor<1x32x1xi32> -> tensor<2x32x1xi32> + %input_offsets_13 = arith.addi %input_offsets_11, %input_offsets_12 : tensor<2x32x1xi32> + %input_offsets_14 = tt.broadcast %input_offsets_13 : tensor<2x32x1xi32> -> tensor<2x32x128xi32> + %input_offsets_15 = tt.broadcast %l_in_offsets_8 : tensor<1x1x128xi32> -> tensor<2x32x128xi32> + %input_offsets_16 = arith.addi %input_offsets_14, %input_offsets_15 : tensor<2x32x128xi32> + %input_tensor = tt.splat %input_ptr : !tt.ptr -> tensor<2x32x128x!tt.ptr> + %input_tensor_17 = tt.addptr %input_tensor, %input_offsets_16 : tensor<2x32x128x!tt.ptr>, tensor<2x32x128xi32> + %input_tensor_18 = tt.load %input_tensor_17 : tensor<2x32x128x!tt.ptr> + %c_out_offsets = tt.make_range {end = 30 : i32, start = 0 : i32} : tensor<30xi32> + %c_out_offsets_19 = tt.expand_dims %c_out_offsets {axis = 1 : i32} : tensor<30xi32> -> tensor<30x1xi32> + %c_out_offsets_20 = tt.expand_dims %c_out_offsets_19 {axis = 2 : i32} : tensor<30x1xi32> -> tensor<30x1x1xi32> + %k_offsets = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %k_offsets_21 = tt.expand_dims %k_offsets {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %k_offsets_22 = tt.expand_dims %k_offsets_21 {axis = 1 : i32} : tensor<1x3xi32> -> tensor<1x1x3xi32> + %weight_offsets_23 = arith.muli %c_out_offsets_20, %weight_offsets_1 : tensor<30x1x1xi32> + %weight_offsets_24 = arith.muli %c_in_offsets_6, %weight_offsets : tensor<1x32x1xi32> + %weight_offsets_25 = tt.broadcast %weight_offsets_23 : tensor<30x1x1xi32> -> tensor<30x32x1xi32> + %weight_offsets_26 = tt.broadcast %weight_offsets_24 : tensor<1x32x1xi32> -> tensor<30x32x1xi32> + %weight_offsets_27 = arith.addi %weight_offsets_25, %weight_offsets_26 : tensor<30x32x1xi32> + %weight_offsets_28 = tt.broadcast %weight_offsets_27 : tensor<30x32x1xi32> -> tensor<30x32x3xi32> + %weight_offsets_29 = tt.broadcast %k_offsets_22 : tensor<1x1x3xi32> -> tensor<30x32x3xi32> + %weight_offsets_30 = arith.addi %weight_offsets_28, %weight_offsets_29 : tensor<30x32x3xi32> + %weight_tensor = tt.splat %weight_ptr : !tt.ptr -> tensor<30x32x3x!tt.ptr> + %weight_tensor_31 = tt.addptr %weight_tensor, %weight_offsets_30 : tensor<30x32x3x!tt.ptr>, tensor<30x32x3xi32> + %weight_tensor_32 = tt.load %weight_tensor_31 : tensor<30x32x3x!tt.ptr> + %bias_tensor = tt.splat %bias_ptr : !tt.ptr -> tensor<30x!tt.ptr> + %bias_tensor_33 = tt.addptr %bias_tensor, %c_out_offsets : tensor<30x!tt.ptr>, tensor<30xi32> + %bias_tensor_34 = tt.load %bias_tensor_33 : tensor<30x!tt.ptr> + %output = ascend.conv1d(%input_tensor_18, %weight_tensor_32, %bias_tensor_34) {dilation = 1 : i64, groups = 1 : i64, padding_size = 1 : i64, stride = 1 : i64} : (tensor<2x32x128xf16>, tensor<30x32x3xf16>, tensor<30xf16>) -> tensor<2x30x128xf16> + %c_out_offsets_35 = tt.expand_dims %c_out_offsets {axis = 0 : i32} : tensor<30xi32> -> tensor<1x30xi32> + %c_out_offsets_36 = tt.expand_dims %c_out_offsets_35 {axis = 2 : i32} : tensor<1x30xi32> -> tensor<1x30x1xi32> + %output_offsets_37 = arith.muli %n_offsets_4, %output_offsets_0 : tensor<2x1x1xi32> + %output_offsets_38 = arith.muli %c_out_offsets_36, %output_offsets : tensor<1x30x1xi32> + %output_offsets_39 = tt.broadcast %output_offsets_37 : tensor<2x1x1xi32> -> tensor<2x30x1xi32> + %output_offsets_40 = tt.broadcast %output_offsets_38 : tensor<1x30x1xi32> -> tensor<2x30x1xi32> + %output_offsets_41 = arith.addi %output_offsets_39, %output_offsets_40 : tensor<2x30x1xi32> + %output_offsets_42 = tt.broadcast %output_offsets_41 : tensor<2x30x1xi32> -> tensor<2x30x128xi32> + %output_offsets_43 = tt.broadcast %l_in_offsets_8 : tensor<1x1x128xi32> -> tensor<2x30x128xi32> + %output_offsets_44 = arith.addi %output_offsets_42, %output_offsets_43 : tensor<2x30x128xi32> + %0 = tt.splat %output_ptr : !tt.ptr -> tensor<2x30x128x!tt.ptr> + %1 = tt.addptr %0, %output_offsets_44 : tensor<2x30x128x!tt.ptr>, tensor<2x30x128xi32> + tt.store %1, %output : tensor<2x30x128x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @triton_conv1d_3d_kernel( +// CHECK: %[[VAL_0:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [2, 32, 128], strides: [4096, 128, 1] +// CHECK: %[[VAL_1:.*]] = memref.alloc() : memref<2x32x128xf16> +// CHECK: memref.copy %[[VAL_0]], %[[VAL_1]] +// CHECK: %[[VAL_2:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [30, 32, 3], strides: [96, 3, 1] +// CHECK: %[[VAL_3:.*]] = memref.alloc() : memref<30x32x3xf16> +// CHECK: memref.copy %[[VAL_2]], %[[VAL_3]] +// CHECK: %[[VAL_4:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [30], strides: [1] +// CHECK: %[[VAL_5:.*]] = memref.alloc() : memref<30xf16> +// CHECK: memref.copy %[[VAL_4]], %[[VAL_5]] +// CHECK: %[[VAL_6:.*]] = hfusion.conv1d +// CHECK-SAME: {dilation = 1 : i32, groups = 1 : i32, padding = 1 : i32, stride = 1 : i32} +// CHECK: %[[VAL_7:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [2, 30, 128], strides: [3840, 128, 1] +// CHECK: bufferization.materialize_in_destination %[[VAL_6]] in writable %[[VAL_7]] diff --git a/third_party/ascend/unittest/Conversion/General/TritonAscendAllPass/conv2d.mlir b/third_party/ascend/unittest/Conversion/General/TritonAscendAllPass/conv2d.mlir new file mode 100644 index 0000000000..2cf3f1c9f7 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonAscendAllPass/conv2d.mlir @@ -0,0 +1,102 @@ +// RUN: triton-opt --triton-to-structured '--discrete-mask-access-conversion=compile-on-910-95=False compile-mode=simd' '--triton-to-unstructure=compile-on-910-95=False compile-mode=simd' --triton-to-hivm --triton-to-hfusion --triton-to-llvm --bubble-up-operation --triton-to-structured '--triton-to-linalg=compile-mode=simd' --split-input-file %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @triton_conv2d_3d_kernel(%input_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %weight_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %output_ptr: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c_in_offsets = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %c_in_offsets_0 = tt.expand_dims %c_in_offsets {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %c_in_offsets_1 = tt.expand_dims %c_in_offsets_0 {axis = 2 : i32} : tensor<8x1xi32> -> tensor<8x1x1xi32> + %h_offsets = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %h_offsets_2 = tt.expand_dims %h_offsets {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %h_offsets_3 = tt.expand_dims %h_offsets_2 {axis = 2 : i32} : tensor<1x16xi32> -> tensor<1x16x1xi32> + %w_offsets = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %w_offsets_4 = tt.expand_dims %w_offsets {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %w_offsets_5 = tt.expand_dims %w_offsets_4 {axis = 1 : i32} : tensor<1x16xi32> -> tensor<1x1x16xi32> + %input_offs = arith.constant dense<256> : tensor<8x1x1xi32> + %input_offs_6 = arith.constant dense<16> : tensor<1x16x1xi32> + %input_offs_h = arith.muli %c_in_offsets_1, %input_offs : tensor<8x1x1xi32> + %input_offs_hw = arith.muli %h_offsets_3, %input_offs_6 : tensor<1x16x1xi32> + %input_offs_t0 = tt.broadcast %input_offs_h : tensor<8x1x1xi32> -> tensor<8x16x1xi32> + %input_offs_t1 = tt.broadcast %input_offs_hw : tensor<1x16x1xi32> -> tensor<8x16x1xi32> + %input_offs_t2 = arith.addi %input_offs_t0, %input_offs_t1 : tensor<8x16x1xi32> + %input_offs_t3 = tt.broadcast %input_offs_t2 : tensor<8x16x1xi32> -> tensor<8x16x16xi32> + %input_offs_t4 = tt.broadcast %w_offsets_5 : tensor<1x1x16xi32> -> tensor<8x16x16xi32> + %input_offs_t5 = arith.addi %input_offs_t3, %input_offs_t4 : tensor<8x16x16xi32> + %input_tensor = tt.splat %input_ptr : !tt.ptr -> tensor<8x16x16x!tt.ptr> + %input_tensor_7 = tt.addptr %input_tensor, %input_offs_t5 : tensor<8x16x16x!tt.ptr>, tensor<8x16x16xi32> + %input_tile = tt.load %input_tensor_7 : tensor<8x16x16x!tt.ptr> + %co_offs = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %co_offs_0 = tt.expand_dims %co_offs {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %co_offs_1 = tt.expand_dims %co_offs_0 {axis = 2 : i32} : tensor<4x1xi32> -> tensor<4x1x1xi32> + %co_offs_2 = tt.expand_dims %co_offs_1 {axis = 3 : i32} : tensor<4x1x1xi32> -> tensor<4x1x1x1xi32> + %ci_offs = tt.expand_dims %c_in_offsets {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %ci_offs_0 = tt.expand_dims %ci_offs {axis = 2 : i32} : tensor<1x8xi32> -> tensor<1x8x1xi32> + %ci_offs_1 = tt.expand_dims %ci_offs_0 {axis = 3 : i32} : tensor<1x8x1xi32> -> tensor<1x8x1x1xi32> + %kh_offs = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %kh_offs_0 = tt.expand_dims %kh_offs {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %kh_offs_1 = tt.expand_dims %kh_offs_0 {axis = 1 : i32} : tensor<1x3xi32> -> tensor<1x1x3xi32> + %kh_offs_2 = tt.expand_dims %kh_offs_1 {axis = 3 : i32} : tensor<1x1x3xi32> -> tensor<1x1x3x1xi32> + %kw_offs = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %kw_offs_0 = tt.expand_dims %kw_offs {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %kw_offs_1 = tt.expand_dims %kw_offs_0 {axis = 1 : i32} : tensor<1x3xi32> -> tensor<1x1x3xi32> + %kw_offs_2 = tt.expand_dims %kw_offs_1 {axis = 2 : i32} : tensor<1x1x3xi32> -> tensor<1x1x1x3xi32> + %weight_offs_c = arith.constant dense<72> : tensor<4x1x1x1xi32> + %weight_offs_ci = arith.constant dense<9> : tensor<1x8x1x1xi32> + %weight_offs_kh = arith.constant dense<3> : tensor<1x1x3x1xi32> + %weight_t0 = arith.muli %co_offs_2, %weight_offs_c : tensor<4x1x1x1xi32> + %weight_t1 = arith.muli %ci_offs_1, %weight_offs_ci : tensor<1x8x1x1xi32> + %weight_t2 = arith.muli %kh_offs_2, %weight_offs_kh : tensor<1x1x3x1xi32> + %weight_t3 = tt.broadcast %weight_t0 : tensor<4x1x1x1xi32> -> tensor<4x8x1x1xi32> + %weight_t4 = tt.broadcast %weight_t1 : tensor<1x8x1x1xi32> -> tensor<4x8x1x1xi32> + %weight_t5 = arith.addi %weight_t3, %weight_t4 : tensor<4x8x1x1xi32> + %weight_t6 = tt.broadcast %weight_t5 : tensor<4x8x1x1xi32> -> tensor<4x8x3x1xi32> + %weight_t7 = tt.broadcast %weight_t2 : tensor<1x1x3x1xi32> -> tensor<4x8x3x1xi32> + %weight_t8 = arith.addi %weight_t6, %weight_t7 : tensor<4x8x3x1xi32> + %weight_t9 = tt.broadcast %weight_t8 : tensor<4x8x3x1xi32> -> tensor<4x8x3x3xi32> + %weight_t10 = tt.broadcast %kw_offs_2 : tensor<1x1x1x3xi32> -> tensor<4x8x3x3xi32> + %weight_offs = arith.addi %weight_t9, %weight_t10 : tensor<4x8x3x3xi32> + %weight_tensor = tt.splat %weight_ptr : !tt.ptr -> tensor<4x8x3x3x!tt.ptr> + %weight_tensor_ = tt.addptr %weight_tensor, %weight_offs : tensor<4x8x3x3x!tt.ptr>, tensor<4x8x3x3xi32> + %weight_tile = tt.load %weight_tensor_ : tensor<4x8x3x3x!tt.ptr> + %output = ascend.conv2d(%input_tile, %weight_tile) {dilation = array, groups = 1 : i64, padding = array, stride = array} : (tensor<8x16x16xf16>, tensor<4x8x3x3xf16>) -> tensor<4x14x14xf16> + %lo_h = tt.make_range {end = 14 : i32, start = 0 : i32} : tensor<14xi32> + %lo_h_0 = tt.expand_dims %lo_h {axis = 0 : i32} : tensor<14xi32> -> tensor<1x14xi32> + %lo_h_1 = tt.expand_dims %lo_h_0 {axis = 2 : i32} : tensor<1x14xi32> -> tensor<1x14x1xi32> + %lo_w = tt.make_range {end = 14 : i32, start = 0 : i32} : tensor<14xi32> + %lo_w_0 = tt.expand_dims %lo_w {axis = 0 : i32} : tensor<14xi32> -> tensor<1x14xi32> + %lo_w_1 = tt.expand_dims %lo_w_0 {axis = 1 : i32} : tensor<1x14xi32> -> tensor<1x1x14xi32> + %out_offs_c = arith.constant dense<196> : tensor<4x1x1xi32> + %out_offs_h = arith.constant dense<14> : tensor<1x14x1xi32> + %co_offs_3 = tt.expand_dims %co_offs {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %co_offs_4 = tt.expand_dims %co_offs_3 {axis = 2 : i32} : tensor<4x1xi32> -> tensor<4x1x1xi32> + %out_t0 = arith.muli %co_offs_4, %out_offs_c : tensor<4x1x1xi32> + %out_t1 = arith.muli %lo_h_1, %out_offs_h : tensor<1x14x1xi32> + %out_t2 = tt.broadcast %out_t0 : tensor<4x1x1xi32> -> tensor<4x14x1xi32> + %out_t3 = tt.broadcast %out_t1 : tensor<1x14x1xi32> -> tensor<4x14x1xi32> + %out_t4 = arith.addi %out_t2, %out_t3 : tensor<4x14x1xi32> + %out_t5 = tt.broadcast %out_t4 : tensor<4x14x1xi32> -> tensor<4x14x14xi32> + %out_t6 = tt.broadcast %lo_w_1 : tensor<1x1x14xi32> -> tensor<4x14x14xi32> + %out_offs = arith.addi %out_t5, %out_t6 : tensor<4x14x14xi32> + %0 = tt.splat %output_ptr : !tt.ptr -> tensor<4x14x14x!tt.ptr> + %1 = tt.addptr %0, %out_offs : tensor<4x14x14x!tt.ptr>, tensor<4x14x14xi32> + tt.store %1, %output : tensor<4x14x14x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @triton_conv2d_3d_kernel( +// CHECK: %[[VAL_0:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [8, 16, 16], strides: [256, 16, 1] +// CHECK: %[[VAL_1:.*]] = memref.alloc() : memref<8x16x16xf16> +// CHECK: memref.copy %[[VAL_0]], %[[VAL_1]] +// CHECK: %[[VAL_2:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [4, 8, 3, 3], strides: [72, 9, 3, 1] +// CHECK: %[[VAL_3:.*]] = memref.alloc() : memref<4x8x3x3xf16> +// CHECK: memref.copy %[[VAL_2]], %[[VAL_3]] +// CHECK: hfusion.conv2d +// CHECK-SAME: dilation = array +// CHECK-SAME: groups = 1 : i32 +// CHECK-SAME: padding = array +// CHECK-SAME: stride = array +// CHECK: %[[VAL_6:.*]] = memref.reinterpret_cast +// CHECK-SAME: to offset: [0], sizes: [4, 14, 14], strides: [196, 14, 1] +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[VAL_6]] diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/block_ptr_addptr_base_invalid.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/block_ptr_addptr_base_invalid.mlir new file mode 100644 index 0000000000..b66c97fc27 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/block_ptr_addptr_base_invalid.mlir @@ -0,0 +1,33 @@ +// RUN: triton-opt --triton-control-flow-opt %s | FileCheck %s + +module { + tt.func public @direct_addptr_base(%base: !tt.ptr) { + %c0_i32 = arith.constant 0 : i32 + %c3_i32 = arith.constant 3 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %shifted = tt.addptr %base, %c3_i32 : !tt.ptr, i32 + %ptr = tt.make_tensor_ptr %shifted, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + tt.return + } + + tt.func public @selected_addptr_base(%base0: !tt.ptr, %base1: !tt.ptr, %cond: i1) { + %c0_i32 = arith.constant 0 : i32 + %c7_i32 = arith.constant 7 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %shifted = tt.addptr %base0, %c7_i32 : !tt.ptr, i32 + %selected = arith.select %cond, %shifted, %base1 : !tt.ptr + %ptr = tt.make_tensor_ptr %selected, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + tt.return + } +} + +// CHECK-LABEL: tt.func public @direct_addptr_base +// CHECK: %[[SHIFTED:.*]] = tt.addptr +// CHECK: tt.make_tensor_ptr %[[SHIFTED]], + +// CHECK-LABEL: tt.func public @selected_addptr_base +// CHECK: %[[SHIFTED:.*]] = tt.addptr +// CHECK: %[[SELECTED:.*]] = arith.select %{{.*}}, %[[SHIFTED]], %{{.*}} : !tt.ptr +// CHECK: tt.make_tensor_ptr %[[SELECTED]], diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/block_ptr_different_base.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/block_ptr_different_base.mlir new file mode 100644 index 0000000000..fe18f81732 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/block_ptr_different_base.mlir @@ -0,0 +1,35 @@ +// RUN: triton-opt --triton-control-flow-opt %s | FileCheck %s + +module { + tt.func public @if_block_ptr_different_base(%base0: !tt.ptr, %base1: !tt.ptr, %cond: i1) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %ptr0 = tt.make_tensor_ptr %base0, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %ptr1 = tt.make_tensor_ptr %base1, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %selected = scf.if %cond -> (!tt.ptr>) { + %then_ptr = tt.advance %ptr0, [%c1_i32] : !tt.ptr> + scf.yield %then_ptr : !tt.ptr> + } else { + %else_ptr = tt.advance %ptr1, [%c2_i32] : !tt.ptr> + scf.yield %else_ptr : !tt.ptr> + } + tt.return %selected : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @if_block_ptr_different_base( +// CHECK-SAME: %[[BASE0:[^ ,]+]]: !tt.ptr, %[[BASE1:[^ ,]+]]: !tt.ptr +// CHECK: %[[BASE0_ADDR:.*]] = tt.ptr_to_int %[[BASE0]] +// CHECK: %[[BASE1_ADDR:.*]] = tt.ptr_to_int %[[BASE1]] +// CHECK: %[[SELECTED:[^ :]+]]:2 = scf.if %{{[^ ]+}} -> (i64, i32) { +// CHECK: scf.yield %[[BASE0_ADDR]], %{{[^ ,]+}} : i64, i32 +// CHECK: } else { +// CHECK: scf.yield %[[BASE1_ADDR]], %{{[^ ,]+}} : i64, i32 +// CHECK: } +// CHECK: %[[SELECTED_BASE:.*]] = tt.int_to_ptr %[[SELECTED]]#0 +// CHECK: %[[REBUILT:.*]] = tt.make_tensor_ptr %[[SELECTED_BASE]], +// CHECK-SAME: [%{{.*}}], [%{{.*}}], [%[[SELECTED]]#1] +// CHECK: tt.return %[[REBUILT]] : !tt.ptr> diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_terminal_return.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_terminal_return.mlir new file mode 100644 index 0000000000..0d65f681ec --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_terminal_return.mlir @@ -0,0 +1,150 @@ +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s | FileCheck %s --implicit-check-not="cf.cond_br" --implicit-check-not="cf.br" + +module { + tt.func public @terminal_return_void(%cond: i1) { + cf.cond_br %cond, ^bb1, ^bb2 + ^bb1: + tt.return + ^bb2: + cf.br ^bb3 + ^bb3: + tt.return + } +} + +// CHECK-LABEL: tt.func public @terminal_return_void +// CHECK: scf.if %{{.*}} { +// CHECK: } else { +// CHECK: } +// CHECK: tt.return + +// ----- + +module { + tt.func public @terminal_return_value(%cond: i1, %lhs: i32, %rhs: i32) -> i32 { + cf.cond_br %cond, ^bb1, ^bb2 + ^bb1: + %then = arith.addi %lhs, %rhs : i32 + tt.return %then : i32 + ^bb2: + %else = arith.subi %lhs, %rhs : i32 + tt.return %else : i32 + } +} + +// CHECK-LABEL: tt.func public @terminal_return_value +// CHECK: %[[IF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.subi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: tt.return %[[IF]] : i32 + +// ----- + +module { + tt.func public @terminal_return_nested(%outer: i1, %inner: i1, %x: i32, %y: i32, %z: i32) -> i32 { + cf.cond_br %outer, ^bb1, ^bb2 + ^bb1: + %then = arith.addi %x, %y : i32 + tt.return %then : i32 + ^bb2: + cf.cond_br %inner, ^bb3, ^bb4 + ^bb3: + %inner_then = arith.addi %x, %z : i32 + tt.return %inner_then : i32 + ^bb4: + %inner_else = arith.subi %x, %z : i32 + tt.return %inner_else : i32 + } +} + +// CHECK-LABEL: tt.func public @terminal_return_nested +// CHECK: %[[OUTER:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: %[[INNER:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.subi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: scf.yield %[[INNER]] : i32 +// CHECK: } +// CHECK: tt.return %[[OUTER]] : i32 + +// ----- + +module { + tt.func private @entry_return_with_unreachable_block() -> tensor<32xf32> { + %cst = arith.constant dense<0.000000e+00> : tensor<32xf32> + tt.return %cst : tensor<32xf32> + ^bb1: + %poison = ub.poison : tensor<32xf32> + tt.return %poison : tensor<32xf32> + } +} + +// CHECK-LABEL: tt.func private @entry_return_with_unreachable_block +// CHECK: tt.return +// CHECK-NOT: ^bb1 + +// ----- + +module { + tt.func public @terminal_return_branch_args(%cond: i1, %lhs: i32, %rhs: i32, %bias: i32) -> i32 { + cf.cond_br %cond, ^bb1(%lhs : i32), ^bb2(%rhs : i32) + ^bb1(%v1: i32): + %then = arith.addi %v1, %bias : i32 + tt.return %then : i32 + ^bb2(%v2: i32): + %else = arith.subi %v2, %bias : i32 + tt.return %else : i32 + } +} + +// CHECK-LABEL: tt.func public @terminal_return_branch_args +// CHECK: %[[IF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.subi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: tt.return %[[IF]] : i32 + +// ----- + +module { + tt.func public @terminal_return_nested_with_join(%outer: i1, %inner: i1, %x: i32, %y: i32, %z: i32) -> i32 { + cf.cond_br %outer, ^bb1(%x : i32), ^bb_join(%y : i32) + ^bb1(%v: i32): + cf.cond_br %inner, ^bb_ret, ^bb_join(%v : i32) + ^bb_ret: + %early = arith.addi %x, %z : i32 + tt.return %early : i32 + ^bb_join(%j: i32): + %cont = arith.subi %j, %z : i32 + tt.return %cont : i32 + } +} + +// CHECK-LABEL: tt.func public @terminal_return_nested_with_join +// CHECK: %[[OUTER:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: %[[INNER:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.subi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: scf.yield %[[INNER]] : i32 +// CHECK: } else { +// CHECK: arith.subi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: tt.return %[[OUTER]] : i32 diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_to_scf_empty_result_if.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_to_scf_empty_result_if.mlir new file mode 100644 index 0000000000..59c2c0782f --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_to_scf_empty_result_if.mlir @@ -0,0 +1,20 @@ +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s | FileCheck %s --implicit-check-not="cf.cond_br" --implicit-check-not="cf.br" + +module { + tt.func public @cf_to_scf_empty_result_if(%cond: i1, %lhs: i32, %rhs: i32) -> i32 { + %c0_i32 = arith.constant 0 : i32 + cf.cond_br %cond, ^bb_return, ^bb_compute + ^bb_return: + tt.return %c0_i32 : i32 + ^bb_compute: + %unused = arith.addi %lhs, %rhs : i32 + cf.br ^bb_return + } +} + +// CHECK-LABEL: tt.func public @cf_to_scf_empty_result_if +// CHECK: scf.if %{{.*}} { +// CHECK: } else { +// CHECK: arith.addi +// CHECK: } +// CHECK: tt.return diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_to_scf_result_types.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_to_scf_result_types.mlir new file mode 100644 index 0000000000..b0a97cea73 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/cf_to_scf_result_types.mlir @@ -0,0 +1,25 @@ +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s | FileCheck %s --implicit-check-not="cf.cond_br" --implicit-check-not="cf.br" + +module { + tt.func public @cf_to_scf_result_types(%cond: i1, %lhs: i32, %rhs: i32) -> i32 { + cf.cond_br %cond, ^bb1, ^bb2 + ^bb1: + %then = arith.addi %lhs, %rhs : i32 + cf.br ^bb3(%then : i32) + ^bb2: + %else = arith.subi %lhs, %rhs : i32 + cf.br ^bb3(%else : i32) + ^bb3(%result: i32): + tt.return %result : i32 + } +} + +// CHECK-LABEL: tt.func public @cf_to_scf_result_types +// CHECK: %[[IF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.subi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: tt.return %[[IF]] : i32 diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/d007_per_axis_structured_opaque_e2e.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/d007_per_axis_structured_opaque_e2e.mlir new file mode 100644 index 0000000000..2b6dd3ab00 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/d007_per_axis_structured_opaque_e2e.mlir @@ -0,0 +1,159 @@ +// RUN: triton-opt --triton-control-flow-opt %s -verify-each | FileCheck %s --check-prefix=CFO +// RUN: triton-opt --triton-control-flow-opt --triton-to-unstructure --triton-to-linalg %s -verify-each | FileCheck %s --check-prefix=E2E --implicit-check-not='!tt.ptr' --implicit-check-not=unrealized_conversion_cast --implicit-check-not=PointerDescriptorBoundary --implicit-check-not=PointerDescriptorRebuild --implicit-check-not=PointerDescriptorOffsetForm --implicit-check-not=PointerDescriptorStructuredAxes + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + // A rank-1 affine range plus a uniform scalar offset remains one + // Structured axis and keeps the strided_1d handoff. + tt.func public @d007_rank1_structured( + %base: !tt.ptr, %output: !tt.ptr, %condition: i1) { + %c0 = arith.constant 0 : i32 + %c2 = arith.constant 2 : i32 + %c5 = arith.constant 5 : i32 + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %stride = tt.splat %c2 : i32 -> tensor<4xi32> + %scaled = arith.muli %range, %stride : tensor<4xi32> + %uniform = tt.splat %c5 : i32 -> tensor<4xi32> + %offset = arith.addi %scaled, %uniform : tensor<4xi32> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %selected = scf.if %condition -> (tensor<4x!tt.ptr>) { + %ptr = tt.addptr %base_tensor, %offset : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %ptr : tensor<4x!tt.ptr> + } else { + %uniform_alt = tt.splat %c0 : i32 -> tensor<4xi32> + %offset_alt = arith.addi %scaled, %uniform_alt : tensor<4xi32> + %ptr = tt.addptr %base_tensor, %offset_alt : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %ptr : tensor<4x!tt.ptr> + } + %loaded = tt.load %selected : tensor<4x!tt.ptr> + %output_tensor = tt.splat %output : !tt.ptr -> tensor<4x!tt.ptr> + %output_ptr = tt.addptr %output_tensor, %range : tensor<4x!tt.ptr>, tensor<4xi32> + tt.store %output_ptr, %loaded : tensor<4x!tt.ptr> + tt.return + } + + // An arbitrary rank-1 offset tensor is Opaque as a whole. The scalar stride + // and uniform offset fields must be normalized to zero. + tt.func public @d007_rank1_opaque( + %base: !tt.ptr, %output: !tt.ptr, %condition: i1, + %then_offsets: tensor<4xi32>, %else_offsets: tensor<4xi32>) { + %base_tensor = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %selected = scf.if %condition -> (tensor<4x!tt.ptr>) { + %ptr = tt.addptr %base_tensor, %then_offsets : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %ptr : tensor<4x!tt.ptr> + } else { + %ptr = tt.addptr %base_tensor, %else_offsets : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %ptr : tensor<4x!tt.ptr> + } + %loaded = tt.load %selected : tensor<4x!tt.ptr> + %output_offsets = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %output_tensor = tt.splat %output : !tt.ptr -> tensor<4x!tt.ptr> + %output_ptr = tt.addptr %output_tensor, %output_offsets : tensor<4x!tt.ptr>, tensor<4xi32> + tt.store %output_ptr, %loaded : tensor<4x!tt.ptr> + tt.return + } + + // Axis 0 is a make_range contribution while axis 1 comes from an opaque + // lane-wise input. The joined descriptor must retain [Structured, Opaque]. + tt.func public @d007_rank2_mixed( + %base: !tt.ptr, %output: !tt.ptr, %condition: i1, + %then_axis1: tensor<4xi32>, %else_axis1: tensor<4xi32>) { + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis0_expanded = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis0_broadcast = tt.broadcast %axis0_expanded : tensor<2x1xi32> -> tensor<2x4xi32> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<2x4x!tt.ptr> + %selected = scf.if %condition -> (tensor<2x4x!tt.ptr>) { + %axis1_expanded = tt.expand_dims %then_axis1 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %axis1_broadcast = tt.broadcast %axis1_expanded : tensor<1x4xi32> -> tensor<2x4xi32> + %offset = arith.addi %axis0_broadcast, %axis1_broadcast : tensor<2x4xi32> + %ptr = tt.addptr %base_tensor, %offset : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + scf.yield %ptr : tensor<2x4x!tt.ptr> + } else { + %axis1_expanded = tt.expand_dims %else_axis1 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %axis1_broadcast = tt.broadcast %axis1_expanded : tensor<1x4xi32> -> tensor<2x4xi32> + %offset = arith.addi %axis0_broadcast, %axis1_broadcast : tensor<2x4xi32> + %ptr = tt.addptr %base_tensor, %offset : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + scf.yield %ptr : tensor<2x4x!tt.ptr> + } + %loaded = tt.load %selected : tensor<2x4x!tt.ptr> + %output_offsets = arith.constant dense<[[0, 1, 2, 3], [4, 5, 6, 7]]> : tensor<2x4xi32> + %output_tensor = tt.splat %output : !tt.ptr -> tensor<2x4x!tt.ptr> + %output_ptr = tt.addptr %output_tensor, %output_offsets : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + tt.store %output_ptr, %loaded : tensor<2x4x!tt.ptr> + tt.return + } + + // A uniform backedge update must remain the scalar uniform-offset field. + // The invariant opaque-axis contribution stays outside the loop instead of + // becoming a full tensor loop carrier on every iteration. + tt.func public @d007_rank2_mixed_uniform_loop( + %base: !tt.ptr, %output: !tt.ptr, + %axis1: tensor<4xi32>, %upper: index) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<32> : tensor<2x4xi32> + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis0_expanded = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis0_broadcast = tt.broadcast %axis0_expanded : tensor<2x1xi32> -> tensor<2x4xi32> + %axis1_expanded = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %axis1_broadcast = tt.broadcast %axis1_expanded : tensor<1x4xi32> -> tensor<2x4xi32> + %offset = arith.addi %axis0_broadcast, %axis1_broadcast : tensor<2x4xi32> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<2x4x!tt.ptr> + %initial = tt.addptr %base_tensor, %offset : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%pointer = %initial) -> (tensor<2x4x!tt.ptr>) { + %next = tt.addptr %pointer, %delta : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + scf.yield %next : tensor<2x4x!tt.ptr> + } + %loaded = tt.load %final : tensor<2x4x!tt.ptr> + %output_offsets = arith.constant dense<[[0, 1, 2, 3], [4, 5, 6, 7]]> : tensor<2x4xi32> + %output_tensor = tt.splat %output : !tt.ptr -> tensor<2x4x!tt.ptr> + %output_ptr = tt.addptr %output_tensor, %output_offsets : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + tt.store %output_ptr, %loaded : tensor<2x4x!tt.ptr> + tt.return + } +} + +// CFO-LABEL: tt.func public @d007_rank1_structured +// CFO: PointerDescriptorStructuredAxes = array +// CFO: PointerDescriptorOffsetForm = "strided_1d" +// CFO-LABEL: tt.func public @d007_rank1_opaque +// CFO: PointerDescriptorStructuredAxes = array +// CFO-LABEL: tt.func public @d007_rank2_mixed +// CFO: PointerDescriptorStructuredAxes = array +// CFO-LABEL: tt.func public @d007_rank2_mixed_uniform_loop +// CFO: scf.for +// CFO-SAME: i32 +// CFO-NOT: scf.yield {{.*}}tensor<2x4xi32> +// CFO: PointerDescriptorStructuredAxes = array + +// E2E-LABEL: func.func @d007_rank1_structured +// E2E-NOT: memref.load +// E2E: %[[SOURCE_VIEW:.*]] = memref.reinterpret_cast %{{.*}} to offset: [%{{.*}}], sizes: [8], strides: [1] +// E2E-SAME: to memref<8xf32, strided<[1], offset: ?>> +// E2E-NOT: memref.load +// E2E: %[[COPY_BUFFER:.*]] = memref.alloc() : memref<8xf32> +// E2E-NOT: memref.load +// E2E: memref.copy %[[SOURCE_VIEW]], %[[COPY_BUFFER]] : memref<8xf32, strided<[1], offset: ?>> to memref<8xf32> +// E2E-NOT: memref.load +// E2E: %[[SOURCE_TENSOR:.*]] = bufferization.to_tensor %[[COPY_BUFFER]] +// E2E-NOT: memref.load +// E2E: %[[SLICE:.*]] = tensor.extract_slice %[[SOURCE_TENSOR]][0] [4] [2] : tensor<8xf32> to tensor<4xf32> +// E2E-NOT: memref.load +// E2E: bufferization.materialize_in_destination %[[SLICE]] +// E2E-NOT: memref.load +// E2E: return +// E2E-NOT: memref.load +// E2E-LABEL: func.func @d007_rank1_opaque +// E2E: memref.load +// E2E: bufferization.materialize_in_destination +// E2E: return +// E2E-LABEL: func.func @d007_rank2_mixed +// E2E: memref.load +// E2E: bufferization.materialize_in_destination +// E2E: return +// E2E-LABEL: func.func @d007_rank2_mixed_uniform_loop +// E2E: scf.for +// E2E-SAME: iter_args(%{{.*}} = %{{.*}}) -> (i32) +// E2E: memref.load +// E2E: bufferization.materialize_in_destination +// E2E: return diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/nested_cf_to_scf.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/nested_cf_to_scf.mlir new file mode 100644 index 0000000000..bb3569c94b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/nested_cf_to_scf.mlir @@ -0,0 +1,40 @@ +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s | FileCheck %s --implicit-check-not="cf.cond_br" --implicit-check-not="cf.br" + +module { + tt.func public @nested_cf_to_scf(%outer: i1, %inner: i1, %x: i32, %y: i32, %z: i32) -> i32 { + cf.cond_br %outer, ^bb1, ^bb2 + ^bb1: + cf.cond_br %inner, ^bb3, ^bb4 + ^bb2: + %outer_else = arith.subi %y, %z : i32 + cf.br ^bb6(%outer_else : i32) + ^bb3: + %inner_then = arith.addi %x, %z : i32 + cf.br ^bb5(%inner_then : i32) + ^bb4: + %inner_else = arith.subi %x, %z : i32 + cf.br ^bb5(%inner_else : i32) + ^bb5(%inner_result: i32): + %then_result = arith.addi %inner_result, %y : i32 + cf.br ^bb6(%then_result : i32) + ^bb6(%result: i32): + tt.return %result : i32 + } +} + +// CHECK-LABEL: tt.func public @nested_cf_to_scf +// CHECK: %[[OUTER_IF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: %[[INNER_IF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.subi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: arith.addi %[[INNER_IF]] +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.subi +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: tt.return %[[OUTER_IF]] : i32 diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/pointer_descriptor_boundary_marker.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/pointer_descriptor_boundary_marker.mlir new file mode 100644 index 0000000000..0172eb1e19 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/pointer_descriptor_boundary_marker.mlir @@ -0,0 +1,46 @@ +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s | FileCheck %s + +module { + tt.func public @block_ptr_loop_marker(%base: !tt.ptr, %upper: index) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %initial = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %final = scf.for %iv = %c0 to %upper step %c1 iter_args(%ptr = %initial) -> (!tt.ptr>) { + %next = tt.advance %ptr, [%c1_i32] : !tt.ptr> + scf.yield %next : !tt.ptr> + } + tt.return %final : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @block_ptr_loop_marker +// CHECK: scf.for +// CHECK: PointerDescriptorBoundary + +// ----- + +module { + tt.func public @tensor_ptr_loop_marker(%base: !tt.ptr, %upper: index) -> tensor<4x!tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %zero = tt.splat %c0_i32 : i32 -> tensor<4xi32> + %delta = tt.splat %c1_i32 : i32 -> tensor<4xi32> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %initial = tt.addptr %base_tensor, %zero : tensor<4x!tt.ptr>, tensor<4xi32> + %final = scf.for %iv = %c0 to %upper step %c1 iter_args(%ptr = %initial) -> (tensor<4x!tt.ptr>) { + %next = tt.addptr %ptr, %delta : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %next : tensor<4x!tt.ptr> + } + tt.return %final : tensor<4x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @tensor_ptr_loop_marker +// CHECK: scf.for +// CHECK: PointerDescriptorBoundary diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scf_pointer_decouple.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scf_pointer_decouple.mlir new file mode 100644 index 0000000000..7cec2c6b56 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scf_pointer_decouple.mlir @@ -0,0 +1,913 @@ +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s | FileCheck %s +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s | FileCheck %s --check-prefix=NO-ADVANCE + +// NO-ADVANCE: module +// NO-ADVANCE-NOT: tt.advance + +module { + tt.func public @for_block_ptr_dynamic_step(%base: !tt.ptr, %ub: index) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %final = scf.for %iv = %c0 to %ub step %c1 iter_args(%ptr = %ptr0) -> (!tt.ptr>) { + %iv_i32 = arith.index_cast %iv : index to i32 + %next = tt.advance %ptr, [%iv_i32] : !tt.ptr> + scf.yield %next : !tt.ptr> + } + tt.return %final : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @for_block_ptr_dynamic_step +// CHECK: %[[FOR:.*]] = scf.for {{.*}} iter_args(%[[OFF:.*]] = %{{.*}}) -> (i32) { +// CHECK-NOT: arith.muli +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[OFF]]] {{.*}}order = array{{.*}} : > +// CHECK: %[[NEXT:.*]] = arith.addi %[[OFF]], %{{.*}} : i32 +// CHECK: scf.yield %[[NEXT]] : i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[FOR]]] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @for_block_ptr_invariant_delta_carried(%base: !tt.ptr, %ub: index, %stride: i32) -> tensor<32xf16> { + %c0_i32 = arith.constant 0 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %empty_i32 = tt.splat %c0_i32 : i32 -> tensor<32xi32> + %empty_f16 = arith.sitofp %empty_i32 : tensor<32xi32> to tensor<32xf16> + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %res:2 = scf.for %iv = %c0 to %ub step %c1 iter_args(%ptr = %ptr0, %acc = %empty_f16) -> (!tt.ptr>, tensor<32xf16>) { + %next = tt.advance %ptr, [%stride] : !tt.ptr> + %loaded = tt.load %next : !tt.ptr> + scf.yield %next, %loaded : !tt.ptr>, tensor<32xf16> + } + tt.return %res#1 : tensor<32xf16> + } +} + +// CHECK-LABEL: tt.func public @for_block_ptr_invariant_delta_carried( +// CHECK-SAME: %[[BASE:[^ ,]+]]: !tt.ptr +// CHECK: %[[BASE_ADDRESS:.*]] = tt.ptr_to_int %[[BASE]] +// CHECK: %[[FOR:[^:]+]]:2 = scf.for +// CHECK-SAME: {{.*}} -> (i32, tensor<32xf16>) { +// CHECK-NOT: arith.index_cast +// CHECK-NOT: arith.muli +// CHECK: %[[NEXT:.*]] = arith.addi %{{.*}}, %{{.*}} : i32 +// CHECK-NOT: tt.int_to_ptr %[[BASE_ADDRESS]] +// CHECK: %[[LOAD_PTR:.*]] = tt.make_tensor_ptr %[[BASE]], [%{{.*}}], [%{{.*}}], [%[[NEXT]]] {order = array} : > +// CHECK: %[[LOADED:.*]] = tt.load %[[LOAD_PTR]] : !tt.ptr> +// CHECK: scf.yield %[[NEXT]], %[[LOADED]] : i32, tensor<32xf16> +// CHECK: } +// CHECK: tt.return %[[FOR]]#1 : tensor<32xf16> + +// ----- + +module { + tt.func public @for_block_ptr_dynamic_bounds_and_step(%base: !tt.ptr, %lb: index, %ub: index, %step: index, %delta: i32) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %final = scf.for %iv = %lb to %ub step %step iter_args(%ptr = %ptr0) -> (!tt.ptr>) { + %next = tt.advance %ptr, [%delta] : !tt.ptr> + scf.yield %next : !tt.ptr> + } + tt.return %final : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @for_block_ptr_dynamic_bounds_and_step +// CHECK: %[[FOR:.*]] = scf.for {{.*}} iter_args(%[[OFF:.*]] = %{{.*}}) -> (i32) { +// CHECK-NOT: arith.muli +// CHECK: %[[NEXT:.*]] = arith.addi %[[OFF]], %{{.*}} : i32 +// CHECK: scf.yield %[[NEXT]] : i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[FOR]]] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @for_block_ptr_iter_arg_delta(%base: !tt.ptr, %ub: index) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %res:2 = scf.for %iv = %c0 to %ub step %c1 iter_args(%ptr = %ptr0, %delta = %c1_i32) -> (!tt.ptr>, i32) { + %next_ptr = tt.advance %ptr, [%delta] : !tt.ptr> + %next_delta = arith.addi %delta, %c1_i32 : i32 + scf.yield %next_ptr, %next_delta : !tt.ptr>, i32 + } + tt.return %res#0 : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @for_block_ptr_iter_arg_delta +// CHECK: %[[FOR:.*]]:2 = scf.for {{.*}} iter_args(%[[OFF:.*]] = %{{.*}}, %[[DELTA:.*]] = %{{.*}}) -> (i32, i32) { +// CHECK: %[[NEXT_PTR:.*]] = arith.addi %[[OFF]], %[[DELTA]] : i32 +// CHECK: %[[NEXT_DELTA:.*]] = arith.addi %[[DELTA]], %{{.*}} : i32 +// CHECK: scf.yield %[[NEXT_PTR]], %[[NEXT_DELTA]] : i32, i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[FOR]]#0] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @for_block_ptr_multidim_delta(%base: !tt.ptr) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c3_i32 = arith.constant 3 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %c32_i64 = arith.constant 32 : i64 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %ptr0 = tt.make_tensor_ptr %base, [%c16_i64, %c32_i64], [%c32_i64, %c1_i64], [%c2_i32, %c3_i32] {order = array} : !tt.ptr> + %final = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %ptr0) -> (!tt.ptr>) { + %next = tt.advance %ptr, [%c1_i32, %c2_i32] : !tt.ptr> + scf.yield %next : !tt.ptr> + } + tt.return %final : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @for_block_ptr_multidim_delta +// CHECK: %[[FOR:.*]]:2 = scf.for {{.*}} iter_args(%[[OFF0:.*]] = %{{.*}}, %[[OFF1:.*]] = %{{.*}}) +// CHECK: %[[NEXT0:.*]] = arith.addi %[[OFF0]], %{{.*}} : i32 +// CHECK: %[[NEXT1:.*]] = arith.addi %[[OFF1]], %{{.*}} : i32 +// CHECK: scf.yield %[[NEXT0]], %[[NEXT1]] : i32, i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}, %{{.*}}], [%{{.*}}, %{{.*}}], [%[[FOR]]#0, %[[FOR]]#1] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @for_block_ptr_zero_trip(%base: !tt.ptr) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %final = scf.for %iv = %c4 to %c0 step %c1 iter_args(%ptr = %ptr0) -> (!tt.ptr>) { + %next = tt.advance %ptr, [%c1_i32] : !tt.ptr> + scf.yield %next : !tt.ptr> + } + tt.return %final : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @for_block_ptr_zero_trip +// CHECK: %[[FOR:.*]] = scf.for {{.*}} iter_args(%[[OFF:.*]] = %{{.*}}) -> (i32) { +// CHECK: %[[NEXT:.*]] = arith.addi %[[OFF]], %{{.*}} : i32 +// CHECK: scf.yield %[[NEXT]] : i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[FOR]]] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @while_block_ptr_basic(%base: !tt.ptr, %n: i32) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %res:2 = scf.while (%i = %c0_i32, %ptr = %ptr0) : (i32, !tt.ptr>) -> (i32, !tt.ptr>) { + %cond = arith.cmpi slt, %i, %n : i32 + scf.condition(%cond) %i, %ptr : i32, !tt.ptr> + } do { + ^bb0(%i_a: i32, %ptr_a: !tt.ptr>): + %next_i = arith.addi %i_a, %c1_i32 : i32 + %next_ptr = tt.advance %ptr_a, [%c1_i32] : !tt.ptr> + scf.yield %next_i, %next_ptr : i32, !tt.ptr> + } + tt.return %res#1 : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @while_block_ptr_basic +// CHECK: %[[WHILE:.*]]:2 = scf.while +// CHECK-SAME: : (i32, i32) -> (i32, i32) { +// CHECK: scf.condition(%{{.*}}) %{{.*}}, %{{.*}} : i32, i32 +// CHECK: } do { +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%{{.*}}] {{.*}}order = array{{.*}} : > +// CHECK: scf.yield %{{.*}}, %{{.*}} : i32, i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[WHILE]]#1] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @if_block_ptr_same_base_offsets(%base: !tt.ptr, %cond: i1) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %selected = scf.if %cond -> (!tt.ptr>) { + %then_ptr = tt.advance %ptr0, [%c1_i32] : !tt.ptr> + scf.yield %then_ptr : !tt.ptr> + } else { + %else_ptr = tt.advance %ptr0, [%c2_i32] : !tt.ptr> + scf.yield %else_ptr : !tt.ptr> + } + tt.return %selected : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @if_block_ptr_same_base_offsets +// CHECK: %[[OFF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[OFF]]] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @if_tensor_ptr_same_base_offsets(%base: !tt.ptr, %cond: i1) -> tensor<4x!tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %off1 = tt.splat %c1_i32 : i32 -> tensor<4xi32> + %off2 = tt.splat %c2_i32 : i32 -> tensor<4xi32> + %splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %selected = scf.if %cond -> (tensor<4x!tt.ptr>) { + %then_ptr = tt.addptr %splat, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %then_ptr : tensor<4x!tt.ptr> + } else { + %else_ptr = tt.addptr %splat, %off2 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %else_ptr : tensor<4x!tt.ptr> + } + tt.return %selected : tensor<4x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @if_tensor_ptr_same_base_offsets +// CHECK: %[[OFF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: tt.splat %{{.*}} : !tt.ptr -> tensor<4x!tt.ptr> +// CHECK: PointerDescriptorOffsetForm = "strided_1d" +// CHECK-SAME: PointerDescriptorRebuild + +// ----- + +module { + tt.func public @if_tensor_ptr_same_base_chained_addptr( + %base: !tt.ptr, %output: !tt.ptr, %cond: i1) { + %then_offset = arith.constant dense<2> : tensor<16xi32> + %else_offset = arith.constant dense<5> : tensor<16xi32> + %lane = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %selected = scf.if %cond -> (tensor<16x!tt.ptr>) { + %base_tensor = tt.splat %base : !tt.ptr -> tensor<16x!tt.ptr> + %lane_ptr = tt.addptr %base_tensor, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %then_ptr = tt.addptr %lane_ptr, %then_offset : tensor<16x!tt.ptr>, tensor<16xi32> + scf.yield %then_ptr : tensor<16x!tt.ptr> + } else { + %base_tensor = tt.splat %base : !tt.ptr -> tensor<16x!tt.ptr> + %lane_ptr = tt.addptr %base_tensor, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %else_ptr = tt.addptr %lane_ptr, %else_offset : tensor<16x!tt.ptr>, tensor<16xi32> + scf.yield %else_ptr : tensor<16x!tt.ptr> + } + %output_tensor = tt.splat %output : !tt.ptr -> tensor<16x!tt.ptr> + %output_ptr = tt.addptr %output_tensor, %lane : tensor<16x!tt.ptr>, tensor<16xi32> + %loaded = tt.load %selected : tensor<16x!tt.ptr> + tt.store %output_ptr, %loaded : tensor<16x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: tt.func public @if_tensor_ptr_same_base_chained_addptr +// CHECK: %[[CHAINED_OFF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: %[[CHAINED_PTR:.*]] = tt.addptr +// CHECK-SAME: PointerDescriptorOffsetForm = "strided_1d" +// CHECK-SAME: PointerDescriptorRebuild +// CHECK: tt.load %[[CHAINED_PTR]] : tensor<16x!tt.ptr> + +// ----- + +module { + tt.func public @while_block_ptr_large_step(%base: !tt.ptr, %n: i32) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c37_i32 = arith.constant 37 : i32 + %c1_i64 = arith.constant 1 : i64 + %c128_i64 = arith.constant 128 : i64 + %ptr0 = tt.make_tensor_ptr %base, [%c128_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %res:2 = scf.while (%i = %c0_i32, %ptr = %ptr0) : (i32, !tt.ptr>) -> (i32, !tt.ptr>) { + %cond = arith.cmpi slt, %i, %n : i32 + scf.condition(%cond) %i, %ptr : i32, !tt.ptr> + } do { + ^bb0(%i_a: i32, %ptr_a: !tt.ptr>): + %next_i = arith.addi %i_a, %c1_i32 : i32 + %next_ptr = tt.advance %ptr_a, [%c37_i32] : !tt.ptr> + scf.yield %next_i, %next_ptr : i32, !tt.ptr> + } + tt.return %res#1 : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @while_block_ptr_large_step +// CHECK-DAG: %[[C37:.*]] = arith.constant 37 : i32 +// CHECK: %[[WHILE:.*]]:2 = scf.while +// CHECK-SAME: : (i32, i32) -> (i32, i32) +// CHECK: } do { +// CHECK: %[[NEXT_OFF:.*]] = arith.addi %{{.*}}, %[[C37]] : i32 +// CHECK: scf.yield %{{.*}}, %[[NEXT_OFF]] : i32, i32 + +// ----- + +module { + tt.func public @while_tensor_ptr_addptr_step(%base: !tt.ptr, %n: i32) -> tensor<4x!tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c4_i32 = arith.constant 4 : i32 + %splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %res:2 = scf.while (%i = %c0_i32, %ptr = %splat) : (i32, tensor<4x!tt.ptr>) -> (i32, tensor<4x!tt.ptr>) { + %cond = arith.cmpi slt, %i, %n : i32 + scf.condition(%cond) %i, %ptr : i32, tensor<4x!tt.ptr> + } do { + ^bb0(%i_a: i32, %ptr_a: tensor<4x!tt.ptr>): + %next_i = arith.addi %i_a, %c1_i32 : i32 + %step = tt.splat %c4_i32 : i32 -> tensor<4xi32> + %next_ptr = tt.addptr %ptr_a, %step : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %next_i, %next_ptr : i32, tensor<4x!tt.ptr> + } + tt.return %res#1 : tensor<4x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @while_tensor_ptr_addptr_step +// CHECK-DAG: %[[C4:.*]] = arith.constant 4 : i32 +// CHECK: scf.while +// CHECK-SAME: i32 +// CHECK: } do { +// CHECK: %[[STEP:.*]] = tt.splat %{{.*}} : i32 -> tensor<4xi32> +// CHECK: %[[NEXT_I:.*]] = arith.addi %{{.*}}, %{{.*}} : i32 +// CHECK: %[[NEXT_OFF:.*]] = arith.addi %{{.*}}, %[[C4]] : i32 +// CHECK: scf.yield %[[NEXT_I]], %[[NEXT_OFF]] : i32, i32 + +// ----- + +module { + tt.func public @for_tensor_ptr_preserves_wide_loop_offset(%base: !tt.ptr, %ub: index) -> tensor<4x!tt.ptr> { + %c1_i32 = arith.constant 1 : i32 + %c8_i64 = arith.constant 8 : i64 + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %wide_offset = tt.splat %c8_i64 : i64 -> tensor<4xi64> + %narrow_step = tt.splat %c1_i32 : i32 -> tensor<4xi32> + %splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %ptr0 = tt.addptr %splat, %wide_offset : tensor<4x!tt.ptr>, tensor<4xi64> + %final = scf.for %iv = %c0 to %ub step %c1 iter_args(%ptr = %ptr0) -> (tensor<4x!tt.ptr>) { + %next = tt.addptr %ptr, %narrow_step : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %next : tensor<4x!tt.ptr> + } + tt.return %final : tensor<4x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @for_tensor_ptr_preserves_wide_loop_offset +// CHECK: %[[STEP32:.*]] = arith.constant 1 : i32 +// CHECK: %[[INITIAL:.*]] = arith.constant 8 : i64 +// CHECK: %[[FOR:.*]] = scf.for +// CHECK-SAME: iter_args(%[[LOOP_OFFSET:.*]] = %[[INITIAL]]) -> (i64) { +// CHECK: %[[STEP64:.*]] = arith.extsi %[[STEP32]] : i32 to i64 +// CHECK: %[[NEXT:.*]] = arith.addi %[[LOOP_OFFSET]], %[[STEP64]] : i64 +// CHECK: scf.yield %[[NEXT]] : i64 +// CHECK: } {PointerDescriptorBoundary = array} +// CHECK: %[[TENSOR_OFFSET:.*]] = tt.splat %[[FOR]] : i64 -> tensor<4xi64> +// CHECK: tt.addptr %{{.*}}, %{{.*}} {{.*}}PointerDescriptorRebuild{{.*}} : tensor<4x!tt.ptr>, tensor<4xi64> + +// ----- + +module { + tt.func public @for_if_block_ptr_same_base_post_advance(%base: !tt.ptr, %cond: i1) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c3_i32 = arith.constant 3 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %final = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %ptr0) -> (!tt.ptr>) { + %selected = scf.if %cond -> (!tt.ptr>) { + %then_ptr = tt.advance %ptr, [%c1_i32] : !tt.ptr> + scf.yield %then_ptr : !tt.ptr> + } else { + %else_ptr = tt.advance %ptr, [%c3_i32] : !tt.ptr> + scf.yield %else_ptr : !tt.ptr> + } + %next = tt.advance %selected, [%c2_i32] : !tt.ptr> + scf.yield %next : !tt.ptr> + } + tt.return %final : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @for_if_block_ptr_same_base_post_advance +// CHECK: %[[FOR:.*]] = scf.for {{.*}} iter_args(%[[OFF:.*]] = %{{.*}}) -> (i32) { +// CHECK: %[[SELECTED:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi %[[OFF]], %{{.*}} : i32 +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.addi %[[OFF]], %{{.*}} : i32 +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: %[[NEXT:.*]] = arith.addi %[[SELECTED]], %{{.*}} : i32 +// CHECK: scf.yield %[[NEXT]] : i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[FOR]]] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @while_if_block_ptr_same_base_post_advance(%base: !tt.ptr, %n: i32) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c3_i32 = arith.constant 3 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %res:2 = scf.while (%i = %c0_i32, %ptr = %ptr0) : (i32, !tt.ptr>) -> (i32, !tt.ptr>) { + %cond = arith.cmpi slt, %i, %n : i32 + scf.condition(%cond) %i, %ptr : i32, !tt.ptr> + } do { + ^bb0(%i_a: i32, %ptr_a: !tt.ptr>): + %is_one = arith.cmpi eq, %i_a, %c1_i32 : i32 + %selected = scf.if %is_one -> (!tt.ptr>) { + %then_ptr = tt.advance %ptr_a, [%c3_i32] : !tt.ptr> + scf.yield %then_ptr : !tt.ptr> + } else { + %else_ptr = tt.advance %ptr_a, [%c1_i32] : !tt.ptr> + scf.yield %else_ptr : !tt.ptr> + } + %next_ptr = tt.advance %selected, [%c2_i32] : !tt.ptr> + %next_i = arith.addi %i_a, %c1_i32 : i32 + scf.yield %next_i, %next_ptr : i32, !tt.ptr> + } + tt.return %res#1 : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @while_if_block_ptr_same_base_post_advance +// CHECK: %[[WHILE:.*]]:2 = scf.while +// CHECK-SAME: (i32, i32) -> (i32, i32) +// CHECK: } do { +// CHECK: %[[SELECTED:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: %[[NEXT:.*]] = arith.addi %[[SELECTED]], %{{.*}} : i32 +// CHECK: scf.yield %{{.*}}, %[[NEXT]] : i32, i32 +// CHECK: } +// CHECK: tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[WHILE]]#1] {{.*}}order = array{{.*}} : > + +// ----- + +module { + tt.func public @for_if_tensor_ptr_same_base_post_addptr(%base: !tt.ptr, %cond: i1) -> tensor<4x!tt.ptr> { + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c3_i32 = arith.constant 3 : i32 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %off1 = tt.splat %c1_i32 : i32 -> tensor<4xi32> + %off2 = tt.splat %c2_i32 : i32 -> tensor<4xi32> + %off3 = tt.splat %c3_i32 : i32 -> tensor<4xi32> + %splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %final = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %splat) -> (tensor<4x!tt.ptr>) { + %selected = scf.if %cond -> (tensor<4x!tt.ptr>) { + %then_ptr = tt.addptr %ptr, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %then_ptr : tensor<4x!tt.ptr> + } else { + %else_ptr = tt.addptr %ptr, %off3 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %else_ptr : tensor<4x!tt.ptr> + } + %next = tt.addptr %selected, %off2 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %next : tensor<4x!tt.ptr> + } + tt.return %final : tensor<4x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @for_if_tensor_ptr_same_base_post_addptr +// CHECK-DAG: %[[C2:.*]] = arith.constant 2 : i32 +// CHECK-DAG: %[[POST_STEP:.*]] = tt.splat %[[C2]] : i32 -> tensor<4xi32> +// CHECK: %[[FOR:.*]] = scf.for {{.*}} iter_args(%[[OFF:.*]] = %{{.*}}) -> (i32) { +// CHECK: %[[SELECTED:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi %{{.*}}, %{{.*}} : i32 +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.addi %{{.*}}, %{{.*}} : i32 +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: %[[NEXT:.*]] = arith.addi %[[SELECTED]], %{{.*}} : i32 +// CHECK: scf.yield %[[NEXT]] : i32 +// CHECK: } +// CHECK: PointerDescriptorOffsetForm = "strided_1d" +// CHECK-SAME: PointerDescriptorRebuild + +// ----- + +module { + tt.func public @for_if_block_ptr_load_after_post_advance(%base: !tt.ptr, %cond: i1) -> tensor<32xf16> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c3_i32 = arith.constant 3 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %empty = tt.splat %c0_i32 : i32 -> tensor<32xi32> + %empty_f16 = arith.sitofp %empty : tensor<32xi32> to tensor<32xf16> + %ptr0 = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %res:2 = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %ptr0, %acc = %empty_f16) -> (!tt.ptr>, tensor<32xf16>) { + %selected = scf.if %cond -> (!tt.ptr>) { + %then_ptr = tt.advance %ptr, [%c1_i32] : !tt.ptr> + scf.yield %then_ptr : !tt.ptr> + } else { + %else_ptr = tt.advance %ptr, [%c3_i32] : !tt.ptr> + scf.yield %else_ptr : !tt.ptr> + } + %next = tt.advance %selected, [%c2_i32] : !tt.ptr> + %loaded = tt.load %next : !tt.ptr> + scf.yield %next, %loaded : !tt.ptr>, tensor<32xf16> + } + tt.return %res#1 : tensor<32xf16> + } +} + +// CHECK-LABEL: tt.func public @for_if_block_ptr_load_after_post_advance +// CHECK: %[[FOR:.*]]:2 = scf.for {{.*}} iter_args(%[[OFF:.*]] = %{{.*}}, %{{.*}} = %{{.*}}) +// CHECK: %[[SELECTED:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: arith.addi %[[OFF]], %{{.*}} : i32 +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: arith.addi %[[OFF]], %{{.*}} : i32 +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: %[[NEXT:.*]] = arith.addi %[[SELECTED]], %{{.*}} : i32 +// CHECK: %[[LOAD_PTR:.*]] = tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[NEXT]]] {order = array} : > +// CHECK: %[[LOADED:.*]] = tt.load %[[LOAD_PTR]] : !tt.ptr> +// CHECK: scf.yield %[[NEXT]], %[[LOADED]] : i32, tensor<32xf16> +// CHECK: } +// CHECK: tt.return %[[FOR]]#1 : tensor<32xf16> + +// ----- + +module { + tt.func public @for_nested_if_block_ptr_load_after_post_advance(%base: !tt.ptr, %cond0: i1, %cond1: i1) -> tensor<16xf32> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c3_i32 = arith.constant 3 : i32 + %c4_i32 = arith.constant 4 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %empty_i32 = tt.splat %c0_i32 : i32 -> tensor<16xi32> + %empty_f32 = arith.sitofp %empty_i32 : tensor<16xi32> to tensor<16xf32> + %ptr0 = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %res:2 = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %ptr0, %acc = %empty_f32) -> (!tt.ptr>, tensor<16xf32>) { + %selected = scf.if %cond0 -> (!tt.ptr>) { + %a = tt.advance %ptr, [%c1_i32] : !tt.ptr> + %inner = scf.if %cond1 -> (!tt.ptr>) { + %b = tt.advance %a, [%c2_i32] : !tt.ptr> + scf.yield %b : !tt.ptr> + } else { + %b = tt.advance %a, [%c4_i32] : !tt.ptr> + scf.yield %b : !tt.ptr> + } + scf.yield %inner : !tt.ptr> + } else { + %a = tt.advance %ptr, [%c3_i32] : !tt.ptr> + %inner = scf.if %cond1 -> (!tt.ptr>) { + %b = tt.advance %a, [%c1_i32] : !tt.ptr> + scf.yield %b : !tt.ptr> + } else { + %b = tt.advance %a, [%c2_i32] : !tt.ptr> + scf.yield %b : !tt.ptr> + } + scf.yield %inner : !tt.ptr> + } + %next = tt.advance %selected, [%c1_i32] : !tt.ptr> + %loaded = tt.load %next : !tt.ptr> + scf.yield %next, %loaded : !tt.ptr>, tensor<16xf32> + } + tt.return %res#1 : tensor<16xf32> + } +} + +// CHECK-LABEL: tt.func public @for_nested_if_block_ptr_load_after_post_advance +// CHECK: %[[FOR:.*]]:2 = scf.for {{.*}} iter_args(%[[OFF:.*]] = %{{.*}}, %{{.*}} = %{{.*}}) +// CHECK: %[[SELECTED:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: %[[THEN_INNER:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: scf.yield %[[THEN_INNER]] : i32 +// CHECK: } else { +// CHECK: %[[ELSE_INNER:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: scf.yield %[[ELSE_INNER]] : i32 +// CHECK: } +// CHECK: %[[NEXT:.*]] = arith.addi %[[SELECTED]], %{{.*}} : i32 +// CHECK: %[[LOAD_PTR:.*]] = tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[NEXT]]] {order = array} : > +// CHECK: %[[LOADED:.*]] = tt.load %[[LOAD_PTR]] : !tt.ptr> +// CHECK: scf.yield %[[NEXT]], %[[LOADED]] : i32, tensor<16xf32> +// CHECK: } +// CHECK: tt.return %[[FOR]]#1 : tensor<16xf32> + +// ----- + +module { + tt.func public @for_nested_if_tensor_ptr_load_after_post_addptr(%base: !tt.ptr, %cond0: i1, %cond1: i1) -> tensor<4xf32> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c3_i32 = arith.constant 3 : i32 + %c4_i32 = arith.constant 4 : i32 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %off1 = tt.splat %c1_i32 : i32 -> tensor<4xi32> + %off2 = tt.splat %c2_i32 : i32 -> tensor<4xi32> + %off3 = tt.splat %c3_i32 : i32 -> tensor<4xi32> + %off4 = tt.splat %c4_i32 : i32 -> tensor<4xi32> + %empty_i32 = tt.splat %c0_i32 : i32 -> tensor<4xi32> + %empty_f32 = arith.sitofp %empty_i32 : tensor<4xi32> to tensor<4xf32> + %ptr0 = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %res:2 = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %ptr0, %acc = %empty_f32) -> (tensor<4x!tt.ptr>, tensor<4xf32>) { + %selected = scf.if %cond0 -> (tensor<4x!tt.ptr>) { + %a = tt.addptr %ptr, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + %inner = scf.if %cond1 -> (tensor<4x!tt.ptr>) { + %b = tt.addptr %a, %off2 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %b : tensor<4x!tt.ptr> + } else { + %b = tt.addptr %a, %off4 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %b : tensor<4x!tt.ptr> + } + scf.yield %inner : tensor<4x!tt.ptr> + } else { + %a = tt.addptr %ptr, %off3 : tensor<4x!tt.ptr>, tensor<4xi32> + %inner = scf.if %cond1 -> (tensor<4x!tt.ptr>) { + %b = tt.addptr %a, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %b : tensor<4x!tt.ptr> + } else { + %b = tt.addptr %a, %off2 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %b : tensor<4x!tt.ptr> + } + scf.yield %inner : tensor<4x!tt.ptr> + } + %next = tt.addptr %selected, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + %loaded = tt.load %next : tensor<4x!tt.ptr> + scf.yield %next, %loaded : tensor<4x!tt.ptr>, tensor<4xf32> + } + tt.return %res#1 : tensor<4xf32> + } +} + +// CHECK-LABEL: tt.func public @for_nested_if_tensor_ptr_load_after_post_addptr +// CHECK: %[[FOR:.*]]:2 = scf.for +// CHECK-SAME: i32 +// CHECK: %[[SELECTED:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: %[[THEN_INNER:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: PointerDescriptorOffsetForm = "strided_1d" +// CHECK-SAME: PointerDescriptorRebuild +// CHECK: scf.yield %[[THEN_INNER]] : i32 +// CHECK: } else { +// CHECK: %[[ELSE_INNER:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: PointerDescriptorOffsetForm = "strided_1d" +// CHECK-SAME: PointerDescriptorRebuild +// CHECK: scf.yield %[[ELSE_INNER]] : i32 +// CHECK: } +// CHECK: arith.addi %{{.*}}, %{{.*}} : i32 +// CHECK: tt.addptr %{{.*}}, %{{.*}} : tensor<4x!tt.ptr>, tensor<4xi32> +// CHECK: %[[LOADED:.*]] = tt.load %{{.*}} : tensor<4x!tt.ptr> +// CHECK: scf.yield %{{.*}}, %[[LOADED]] : i32, tensor<4xf32> +// CHECK: } +// CHECK: tt.return %[[FOR]]#1 : tensor<4xf32> + +// ----- + +module { + tt.func public @if_block_ptr_same_base_dynamic_shape(%base: !tt.ptr, %cond: i1, %shape0: i64, %shape1: i64) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i64 = arith.constant 1 : i64 + %selected = scf.if %cond -> (!tt.ptr>) { + %then_ptr = tt.make_tensor_ptr %base, [%shape0], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + scf.yield %then_ptr : !tt.ptr> + } else { + %else_ptr = tt.make_tensor_ptr %base, [%shape1], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + scf.yield %else_ptr : !tt.ptr> + } + tt.return %selected : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @if_block_ptr_same_base_dynamic_shape( +// CHECK-SAME: %[[BASE:[^ ,]+]]: !tt.ptr +// CHECK: %[[BASE_ADDRESS:.*]] = tt.ptr_to_int %[[BASE]] +// CHECK: %[[SHAPE:.*]] = scf.if %{{.*}} -> (i64) { +// CHECK: scf.yield %{{.*}} : i64 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i64 +// CHECK: } +// CHECK-NOT: tt.int_to_ptr %[[BASE_ADDRESS]] +// CHECK: tt.make_tensor_ptr %[[BASE]], [%[[SHAPE]]], [%{{.*}}], [%{{.*}}] {PointerDescriptorRebuild, order = array} : > + +// ----- + +module { + tt.func public @if_same_nested_result_fully_decoupled(%base: !tt.ptr, %cond0: i1, %cond1: i1) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %ptr = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %inner = scf.if %cond0 -> (!tt.ptr>) { + %then_ptr = tt.advance %ptr, [%c1_i32] : !tt.ptr> + scf.yield %then_ptr : !tt.ptr> + } else { + %else_ptr = tt.advance %ptr, [%c2_i32] : !tt.ptr> + scf.yield %else_ptr : !tt.ptr> + } + %outer = scf.if %cond1 -> (!tt.ptr>) { + scf.yield %inner : !tt.ptr> + } else { + scf.yield %inner : !tt.ptr> + } + tt.return %outer : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @if_same_nested_result_fully_decoupled +// CHECK: %[[INNER_OFF:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: scf.if %{{.*}} { +// CHECK-NEXT: } else { +// CHECK-NEXT: } +// CHECK: %[[OUTER_PTR:.*]] = tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[INNER_OFF]]] {PointerDescriptorRebuild, order = array} +// CHECK: tt.return %[[OUTER_PTR]] : !tt.ptr> + +// ----- + +module { + tt.func public @if_mixed_block_and_tensor_ptr(%block_base: !tt.ptr, %tensor_base: !tt.ptr, %cond: i1) -> (!tt.ptr>, tensor<4x!tt.ptr>) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %tensor_off1 = tt.splat %c1_i32 : i32 -> tensor<4xi32> + %tensor_off2 = tt.splat %c2_i32 : i32 -> tensor<4xi32> + %block_ptr = tt.make_tensor_ptr %block_base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %tensor_ptr = tt.splat %tensor_base : !tt.ptr -> tensor<4x!tt.ptr> + %result:2 = scf.if %cond -> (!tt.ptr>, tensor<4x!tt.ptr>) { + %then_block = tt.advance %block_ptr, [%c1_i32] : !tt.ptr> + %then_tensor = tt.addptr %tensor_ptr, %tensor_off1 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %then_block, %then_tensor : !tt.ptr>, tensor<4x!tt.ptr> + } else { + %else_block = tt.advance %block_ptr, [%c2_i32] : !tt.ptr> + %else_tensor = tt.addptr %tensor_ptr, %tensor_off2 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %else_block, %else_tensor : !tt.ptr>, tensor<4x!tt.ptr> + } + tt.return %result#0, %result#1 : !tt.ptr>, tensor<4x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @if_mixed_block_and_tensor_ptr +// CHECK: %[[RESULT:.*]]:2 = scf.if %{{.*}} -> (i32, i32) { +// CHECK: scf.yield %{{.*}}, %{{.*}} : i32, i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}}, %{{.*}} : i32, i32 +// CHECK: } +// CHECK-DAG: %[[BLOCK:.*]] = tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[RESULT]]#0] {PointerDescriptorRebuild, order = array} : > +// CHECK-DAG: %[[TENSOR:.*]] = tt.addptr %{{.*}}, %{{.*}} {{.*}}PointerDescriptorOffsetForm = "strided_1d"{{.*}}PointerDescriptorRebuild{{.*}} : tensor<4x!tt.ptr>, tensor<4xi32> +// CHECK: tt.return %[[BLOCK]], %[[TENSOR]] : !tt.ptr>, tensor<4x!tt.ptr> + +// ----- + +module { + tt.func public @if_without_else_rewrites_nested_block_ptr(%base: !tt.ptr, %cond: i1) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %ptr0 = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + scf.if %cond { + %final = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %ptr0) -> (!tt.ptr>) { + %next = tt.advance %ptr, [%c1_i32] : !tt.ptr> + scf.yield %next : !tt.ptr> + } + %loaded = tt.load %final : !tt.ptr> + scf.yield + } + tt.return + } +} + +// CHECK-LABEL: tt.func public @if_without_else_rewrites_nested_block_ptr +// CHECK: scf.if %{{.*}} { +// CHECK: %[[FOR:.*]] = scf.for +// CHECK-SAME: -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: %[[PTR:.*]] = tt.make_tensor_ptr %{{.*}}, [%{{.*}}], [%{{.*}}], [%[[FOR]]] {{.*}}order = array{{.*}} : > +// CHECK: tt.load %[[PTR]] : !tt.ptr> +// CHECK: } + +// ----- + +module { + tt.func public @sibling_if_result_initializes_for_tensor_ptr(%base: !tt.ptr, %cond: i1) -> tensor<4x!tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %off1 = tt.splat %c1_i32 : i32 -> tensor<4xi32> + %off2 = tt.splat %c2_i32 : i32 -> tensor<4xi32> + %ptr0 = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %selected = scf.if %cond -> (tensor<4x!tt.ptr>) { + %then_ptr = tt.addptr %ptr0, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %then_ptr : tensor<4x!tt.ptr> + } else { + %else_ptr = tt.addptr %ptr0, %off2 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %else_ptr : tensor<4x!tt.ptr> + } + %final = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %selected) -> (tensor<4x!tt.ptr>) { + %next = tt.addptr %ptr, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %next : tensor<4x!tt.ptr> + } + tt.return %final : tensor<4x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @sibling_if_result_initializes_for_tensor_ptr +// CHECK: %[[IF_OFFSETS:.*]] = scf.if %{{.*}} -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } else { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: PointerDescriptorOffsetForm = "strided_1d" +// CHECK-SAME: PointerDescriptorRebuild +// CHECK: %[[FOR_OFFSETS:.*]] = scf.for +// CHECK-SAME: -> (i32) { +// CHECK: scf.yield %{{.*}} : i32 +// CHECK: } +// CHECK: %[[FINAL:.*]] = tt.addptr %{{.*}}, %{{.*}} {{.*}}PointerDescriptorOffsetForm = "strided_1d"{{.*}}PointerDescriptorRebuild{{.*}} : tensor<4x!tt.ptr>, tensor<4xi32> +// CHECK: tt.return %[[FINAL]] : tensor<4x!tt.ptr> diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scf_scalar_pointer_boundary.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scf_scalar_pointer_boundary.mlir new file mode 100644 index 0000000000..915aef70b2 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scf_scalar_pointer_boundary.mlir @@ -0,0 +1,21 @@ +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s | FileCheck %s + +module { + tt.func public @scalar_pointer_scf_iter_arg_not_decoupled(%base: !tt.ptr) -> !tt.ptr { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %result = scf.for %iv = %c0 to %c4 step %c1 iter_args(%ptr = %base) -> (!tt.ptr) { + %next = tt.addptr %ptr, %c1_i32 : !tt.ptr, i32 + scf.yield %next : !tt.ptr + } + tt.return %result : !tt.ptr + } +} + +// CHECK-LABEL: tt.func public @scalar_pointer_scf_iter_arg_not_decoupled +// CHECK: scf.for {{.*}} iter_args(%{{.*}} = %{{.*}}) -> (!tt.ptr) +// CHECK: tt.addptr +// CHECK: tt.return %{{.*}} : !tt.ptr diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_decouple.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_decouple.mlir new file mode 100644 index 0000000000..ae6712bcfd --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_decouple.mlir @@ -0,0 +1,208 @@ +// RUN: triton-opt --triton-control-flow-opt --split-input-file %s -verify-each | FileCheck %s --check-prefix=CFO + +module { + tt.func public @scope_block_ptr_result(%base: !tt.ptr, %delta: i32) -> !tt.ptr> { + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %c0_i32 = arith.constant 0 : i32 + %result = scope.scope : () -> (!tt.ptr>) { + %pointer = tt.make_tensor_ptr %base, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %next = tt.advance %pointer, [%delta] : !tt.ptr> + scope.return %next : !tt.ptr> + } {hivm.disable_auto_sync = true, hivm.tcore_type = #hivm.tcore_type, noinline, scope_test_attr = "kept"} + tt.return %result : !tt.ptr> + } +} + +// CFO-LABEL: tt.func public @scope_block_ptr_result +// CFO: %[[SCOPE:.*]]:4 = scope.scope : () -> (i64, i64, i64, i32) { +// CFO: scope.return %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}} : i64, i64, i64, i32 +// CFO: } {hivm.disable_auto_sync = true, hivm.tcore_type = #hivm.tcore_type, noinline, scope_test_attr = "kept"} +// CFO: tt.make_tensor_ptr +// CFO-SAME: PointerDescriptorRebuild +// CFO-NOT: PointerDescriptorBoundary + +// ----- + +module { + tt.func public @scope_block_ptr_mixed_results(%base: !tt.ptr) -> !tt.ptr> { + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %c0_i32 = arith.constant 0 : i32 + %results:3 = scope.scope : () -> (i32, !tt.ptr>, tensor<4xi32>) { + %ordinary = arith.constant 7 : i32 + %tensor = arith.constant dense<9> : tensor<4xi32> + %pointer = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + scope.return %ordinary, %pointer, %tensor : i32, !tt.ptr>, tensor<4xi32> + } + tt.return %results#1 : !tt.ptr> + } +} + +// CFO-LABEL: tt.func public @scope_block_ptr_mixed_results +// CFO: %[[SCOPE:.*]]:6 = scope.scope : () -> (i32, i64, i64, i64, i32, tensor<4xi32>) { +// CFO: scope.return %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}} : i32, i64, i64, i64, i32, tensor<4xi32> +// CFO: } +// CFO: %[[POINTER:.*]] = tt.make_tensor_ptr +// CFO-SAME: PointerDescriptorRebuild +// CFO: tt.return %[[POINTER]] : !tt.ptr> + +// ----- + +module { + tt.func public @scope_tensor_ptr_scalar_base(%base: !tt.ptr) -> tensor<4x!tt.ptr> { + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %result = scope.scope : () -> (tensor<4x!tt.ptr>) { + %pointer = tt.addptr %base_tensor, %range : tensor<4x!tt.ptr>, tensor<4xi32> + scope.return %pointer : tensor<4x!tt.ptr> + } + tt.return %result : tensor<4x!tt.ptr> + } +} + +// CFO-LABEL: tt.func public @scope_tensor_ptr_scalar_base +// CFO: %[[SCOPE:.*]]:4 = scope.scope +// CFO-SAME: -> (i64, i32, i32, tensor<4xi32>) +// CFO: scope.return %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}} : i64, i32, i32, tensor<4xi32> +// CFO: } +// CFO: %[[POINTER:.*]] = tt.addptr +// CFO-SAME: PointerDescriptorOffsetForm = "strided_1d" +// CFO-SAME: PointerDescriptorRebuild +// CFO-SAME: PointerDescriptorStructuredAxes = array +// CFO: tt.return %[[POINTER]] : tensor<4x!tt.ptr> + +// ----- + +module { + tt.func public @scope_tensor_ptr_external_opaque_base(%lhs_base: !tt.ptr, %rhs_base: !tt.ptr, %cond: i1) -> tensor<4x!tt.ptr> { + %lhs = tt.splat %lhs_base : !tt.ptr -> tensor<4x!tt.ptr> + %rhs = tt.splat %rhs_base : !tt.ptr -> tensor<4x!tt.ptr> + %condition = tt.splat %cond : i1 -> tensor<4xi1> + %opaque = arith.select %condition, %lhs, %rhs : tensor<4xi1>, tensor<4x!tt.ptr> + %delta = arith.constant dense<3> : tensor<4xi32> + %result = scope.scope : () -> (tensor<4x!tt.ptr>) { + %pointer = tt.addptr %opaque, %delta : tensor<4x!tt.ptr>, tensor<4xi32> + scope.return %pointer : tensor<4x!tt.ptr> + } + tt.return %result : tensor<4x!tt.ptr> + } +} + +// CFO-LABEL: tt.func public @scope_tensor_ptr_external_opaque_base +// CFO: %[[OPAQUE:.*]] = arith.select +// CFO: %[[SCOPE:.*]]:3 = scope.scope +// CFO-SAME: -> (i32, i32, tensor<4xi32>) +// CFO: scope.return +// CFO: } +// CFO: %[[REBUILT:.*]] = tt.addptr %[[OPAQUE]], +// CFO-SAME: PointerDescriptorRebuild +// CFO-NOT: PointerDescriptorOffsetForm +// CFO: tt.return %[[REBUILT]] : tensor<4x!tt.ptr> + +// ----- + +module { + tt.func public @scope_contains_pointer_for(%base: !tt.ptr) -> !tt.ptr> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c2 = arith.constant 2 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %result = scope.scope : () -> (!tt.ptr>) { + %initial = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %final = scf.for %iv = %c0 to %c2 step %c1 iter_args(%pointer = %initial) -> (!tt.ptr>) { + %next = tt.advance %pointer, [%c1_i32] : !tt.ptr> + scf.yield %next : !tt.ptr> + } + scope.return %final : !tt.ptr> + } {scope_test_attr = "outer"} + tt.return %result : !tt.ptr> + } +} + +// CFO-LABEL: tt.func public @scope_contains_pointer_for +// CFO: scope.scope +// CFO: scf.for +// CFO-SAME: -> (i32) +// CFO: } {PointerDescriptorBoundary = array} +// CFO: } {scope_test_attr = "outer"} + +// ----- + +module { + tt.func public @nested_scope_pointer_result(%base: !tt.ptr) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %outer = scope.scope : () -> (!tt.ptr>) { + %inner = scope.scope : () -> (!tt.ptr>) { + %pointer = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + scope.return %pointer : !tt.ptr> + } {scope_test_attr = "inner"} + %next = tt.advance %inner, [%c1_i32] : !tt.ptr> + scope.return %next : !tt.ptr> + } {scope_test_attr = "outer"} + tt.return %outer : !tt.ptr> + } +} + +// CFO-LABEL: tt.func public @nested_scope_pointer_result +// CFO: scope.scope : () -> (i64, i64, i64, i32) { +// CFO: scope.scope : () -> (i64, i64, i64, i32) { +// CFO: } {scope_test_attr = "inner"} +// CFO: } {scope_test_attr = "outer"} + +// ----- + +module { + tt.func public @resultless_scope_contains_pointer_if(%base: !tt.ptr, %cond: i1) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + scope.scope : () -> () { + %initial = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %selected = scf.if %cond -> (!tt.ptr>) { + %then = tt.advance %initial, [%c1_i32] : !tt.ptr> + scf.yield %then : !tt.ptr> + } else { + %else = tt.advance %initial, [%c2_i32] : !tt.ptr> + scf.yield %else : !tt.ptr> + } + %loaded = tt.load %selected : !tt.ptr> + scope.return + } {scope_test_attr = "resultless"} + tt.return + } +} + +// CFO-LABEL: tt.func public @resultless_scope_contains_pointer_if +// CFO: scope.scope : () -> () { +// CFO: scf.if +// CFO-SAME: -> (i32) +// CFO: tt.load +// CFO: } {scope_test_attr = "resultless"} + +// ----- + +module { + tt.func public @scope_attributes_preserved(%base: !tt.ptr) -> !tt.ptr> { + %c0_i32 = arith.constant 0 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %result = scope.scope : () -> (!tt.ptr>) { + %pointer = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + scope.return %pointer : !tt.ptr> + } {hivm.disable_auto_sync = true, hivm.tcore_type = #hivm.tcore_type, noinline, scope_test_attr = "all-preserved"} + tt.return %result : !tt.ptr> + } +} + +// CFO-LABEL: tt.func public @scope_attributes_preserved +// CFO: scope.scope : () -> (i64, i64, i64, i32) { +// CFO: } {hivm.disable_auto_sync = true, hivm.tcore_type = #hivm.tcore_type, noinline, scope_test_attr = "all-preserved"} diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_downstream.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_downstream.mlir new file mode 100644 index 0000000000..c36a5a0f3b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_downstream.mlir @@ -0,0 +1,51 @@ +// RUN: triton-opt --triton-control-flow-opt --triton-to-unstructure --triton-to-linalg --split-input-file %s -verify-each | FileCheck %s --check-prefix=E2E --implicit-check-not='!tt.ptr' --implicit-check-not=unrealized_conversion_cast --implicit-check-not=PointerDescriptorBoundary --implicit-check-not=PointerDescriptorRebuild --implicit-check-not=PointerDescriptorOffsetForm --implicit-check-not=PointerDescriptorStructuredAxes + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @scope_block_ptr_load_store(%base: !tt.ptr, %output: !tt.ptr) { + %c0_i32 = arith.constant 0 : i32 + %c3_i32 = arith.constant 3 : i32 + %c1_i64 = arith.constant 1 : i64 + %c16_i64 = arith.constant 16 : i64 + %pointer = scope.scope : () -> (!tt.ptr>) { + %initial = tt.make_tensor_ptr %base, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %next = tt.advance %initial, [%c3_i32] : !tt.ptr> + scope.return %next : !tt.ptr> + } {hivm.tcore_type = #hivm.tcore_type} + %value = tt.load %pointer : !tt.ptr> + %output_pointer = tt.make_tensor_ptr %output, [%c16_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + tt.store %output_pointer, %value : !tt.ptr> + tt.return + } +} + +// E2E-LABEL: func.func @scope_block_ptr_load_store +// E2E: scope.scope +// E2E: memref.copy +// E2E: bufferization.materialize_in_destination %{{.*}} in writable %{{.*}} +// E2E: return + +// ----- + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @scope_tensor_ptr_masked_load_store(%base: !tt.ptr, %output: !tt.ptr) { + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %pointer = scope.scope : () -> (tensor<4x!tt.ptr>) { + %next = tt.addptr %base_tensor, %range : tensor<4x!tt.ptr>, tensor<4xi32> + scope.return %next : tensor<4x!tt.ptr> + } {hivm.tcore_type = #hivm.tcore_type} + %mask = arith.constant dense : tensor<4xi1> + %other = arith.constant dense<0.000000e+00> : tensor<4xf32> + %value = tt.load %pointer, %mask, %other : tensor<4x!tt.ptr> + %output_tensor = tt.splat %output : !tt.ptr -> tensor<4x!tt.ptr> + %output_pointer = tt.addptr %output_tensor, %range : tensor<4x!tt.ptr>, tensor<4xi32> + tt.store %output_pointer, %value, %mask : tensor<4x!tt.ptr> + tt.return + } +} + +// E2E-LABEL: func.func @scope_tensor_ptr_masked_load_store +// E2E: scope.scope +// E2E: memref.copy +// E2E: bufferization.materialize_in_destination %{{.*}} in writable %{{.*}} +// E2E: return diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_unsupported.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_unsupported.mlir new file mode 100644 index 0000000000..e88c8e4219 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/scope_pointer_unsupported.mlir @@ -0,0 +1,20 @@ +// RUN: triton-opt --triton-control-flow-opt %s -verify-each | FileCheck %s --check-prefix=FALLBACK + +module { + tt.func public @scope_internal_opaque_tensor_pointer(%lhs_base: !tt.ptr, %rhs_base: !tt.ptr, %cond: i1) -> tensor<4x!tt.ptr> { + %lhs = tt.splat %lhs_base : !tt.ptr -> tensor<4x!tt.ptr> + %rhs = tt.splat %rhs_base : !tt.ptr -> tensor<4x!tt.ptr> + %condition = tt.splat %cond : i1 -> tensor<4xi1> + %result = scope.scope : () -> (tensor<4x!tt.ptr>) { + %opaque = arith.select %condition, %lhs, %rhs : tensor<4xi1>, tensor<4x!tt.ptr> + scope.return %opaque : tensor<4x!tt.ptr> + } + tt.return %result : tensor<4x!tt.ptr> + } +} + +// FALLBACK-LABEL: tt.func public @scope_internal_opaque_tensor_pointer +// FALLBACK: scope.scope +// FALLBACK-SAME: tensor<4x!tt.ptr> +// FALLBACK: arith.select +// FALLBACK: tt.return diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_mixed_broadcast.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_mixed_broadcast.mlir new file mode 100644 index 0000000000..9883941288 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_mixed_broadcast.mlir @@ -0,0 +1,97 @@ +// RUN: triton-opt --triton-control-flow-opt %s -verify-each | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + // The original left-associated expression broadcasts pointers after adding + // opaque row offsets. Preserve the structured column axis and i64 residual. + // Only the uniform offset changes on the loop backedge. + tt.func public @mixed_pointer_broadcast_loop( + %base: !tt.ptr, %rows: tensor<2xi64>, %upper: index) + -> tensor<2x32x!tt.ptr> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<32> : tensor<2x32xi32> + %row_offsets = tt.expand_dims %rows {axis = 1 : i32} : tensor<2xi64> -> tensor<2x1xi64> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<2x1x!tt.ptr> + %row_ptrs = tt.addptr %base_tensor, %row_offsets : tensor<2x1x!tt.ptr>, tensor<2x1xi64> + %broadcast = tt.broadcast %row_ptrs : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> + %range = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %columns = tt.expand_dims %range {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %column_offsets = tt.broadcast %columns : tensor<1x32xi32> -> tensor<2x32xi32> + %initial = tt.addptr %broadcast, %column_offsets : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%pointer = %initial) -> (tensor<2x32x!tt.ptr>) { + %next = tt.addptr %pointer, %delta : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + scf.yield %next : tensor<2x32x!tt.ptr> + } + tt.return %final : tensor<2x32x!tt.ptr> + } + + // A unit column dimension can have a nonzero symbolic stride. Broadcasting + // it repeats the same pointer: reset that stride, but keep the opaque rows. + // Both branches must transfer their own broadcast residual through the if. + tt.func public @mixed_pointer_broadcast_if( + %base: !tt.ptr, %then_rows: tensor<2xi64>, + %else_rows: tensor<2xi64>, %condition: i1) + -> tensor<2x32x!tt.ptr> { + %single = tt.make_range {end = 4 : i32, start = 3 : i32} : tensor<1xi32> + %single_column = tt.expand_dims %single {axis = 0 : i32} : tensor<1xi32> -> tensor<1x1xi32> + %column_offsets = tt.broadcast %single_column : tensor<1x1xi32> -> tensor<2x1xi32> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<2x1x!tt.ptr> + %result = scf.if %condition -> (tensor<2x32x!tt.ptr>) { + %rows = tt.expand_dims %then_rows {axis = 1 : i32} : tensor<2xi64> -> tensor<2x1xi64> + %row_ptrs = tt.addptr %base_tensor, %rows : tensor<2x1x!tt.ptr>, tensor<2x1xi64> + %ptrs = tt.addptr %row_ptrs, %column_offsets : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %broadcast = tt.broadcast %ptrs : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> + scf.yield %broadcast : tensor<2x32x!tt.ptr> + } else { + %rows = tt.expand_dims %else_rows {axis = 1 : i32} : tensor<2xi64> -> tensor<2x1xi64> + %row_ptrs = tt.addptr %base_tensor, %rows : tensor<2x1x!tt.ptr>, tensor<2x1xi64> + %ptrs = tt.addptr %row_ptrs, %column_offsets : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %broadcast = tt.broadcast %ptrs : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> + scf.yield %broadcast : tensor<2x32x!tt.ptr> + } + tt.return %result : tensor<2x32x!tt.ptr> + } + + // Unknown tensor bases do not acquire a scalar-base descriptor or a claimed + // structured axis merely because they are broadcast. + tt.func public @unknown_pointer_broadcast( + %source: tensor<2x1x!tt.ptr>, %upper: index) + -> tensor<2x32x!tt.ptr> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<32> : tensor<2x32xi32> + %initial = tt.broadcast %source : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%pointer = %initial) -> (tensor<2x32x!tt.ptr>) { + %next = tt.addptr %pointer, %delta : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + scf.yield %next : tensor<2x32x!tt.ptr> + } + tt.return %final : tensor<2x32x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @mixed_pointer_broadcast_loop +// CHECK: tt.broadcast {{.*}} : tensor<2x1xi64> -> tensor<2x32xi64> +// CHECK-NOT: arith.subi +// CHECK: scf.for {{.*}} iter_args(%[[OFFSET:.*]] = %{{.*}}) -> (i64) +// CHECK: %[[NEXT:.*]] = arith.addi %[[OFFSET]], %{{.*}} : i64 +// CHECK: scf.yield %[[NEXT]] : i64 +// CHECK: PointerDescriptorStructuredAxes = array +// CHECK: tt.return + +// CHECK-LABEL: tt.func public @mixed_pointer_broadcast_if +// CHECK: %[[SELECTED:.*]] = scf.if {{.*}} -> (tensor<2x32xi64>) +// CHECK: tt.broadcast {{.*}} : tensor<2x1xi64> -> tensor<2x32xi64> +// CHECK: scf.yield {{.*}} : tensor<2x32xi64> +// CHECK: } else { +// CHECK: tt.broadcast {{.*}} : tensor<2x1xi64> -> tensor<2x32xi64> +// CHECK: scf.yield {{.*}} : tensor<2x32xi64> +// CHECK: tt.addptr %{{.*}}, %[[SELECTED]] {PointerDescriptorRebuild, PointerDescriptorStructuredAxes = array} +// CHECK: tt.return + +// CHECK-LABEL: tt.func public @unknown_pointer_broadcast +// CHECK: tt.broadcast {{.*}} : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> +// CHECK: scf.for {{.*}} -> (tensor<2x32xi32>) +// CHECK: PointerDescriptorStructuredAxes = array +// CHECK: tt.return diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_mixed_residual.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_mixed_residual.mlir new file mode 100644 index 0000000000..2dc5e9f2e2 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_mixed_residual.mlir @@ -0,0 +1,133 @@ +// RUN: triton-opt --triton-control-flow-opt %s -verify-each | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + // A nonzero range start and an explicit extension may update the scalar + // fields without computing (rows + columns) - columns. The loop still + // transfers only its i64 uniform offset. + tt.func public @mixed_extended_range( + %base: !tt.ptr, %rows: tensor<2xi64>, %upper: index) + -> tensor<2x32x!tt.ptr> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<32> : tensor<2x32xi32> + %row = tt.expand_dims %rows {axis = 1 : i32} : tensor<2xi64> -> tensor<2x1xi64> + %splat = tt.splat %base : !tt.ptr -> tensor<2x1x!tt.ptr> + %row_ptrs = tt.addptr %splat, %row : tensor<2x1x!tt.ptr>, tensor<2x1xi64> + %broadcast = tt.broadcast %row_ptrs : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> + %range = tt.make_range {end = 39 : i32, start = 7 : i32} : tensor<32xi32> + %expanded = tt.expand_dims %range {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %extended = arith.extsi %expanded : tensor<1x32xi32> to tensor<1x32xi64> + %columns = tt.broadcast %extended : tensor<1x32xi64> -> tensor<2x32xi64> + %initial = tt.addptr %broadcast, %columns : tensor<2x32x!tt.ptr>, tensor<2x32xi64> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%pointer = %initial) -> (tensor<2x32x!tt.ptr>) { + %next = tt.addptr %pointer, %delta : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + scf.yield %next : tensor<2x32x!tt.ptr> + } + tt.return %final : tensor<2x32x!tt.ptr> + } + + // Do not distribute sign-extension across i32 arithmetic: shift=INT_MAX + // wraps some column lanes before tt.addptr sign-extends them to i64. + tt.func public @mixed_narrow_add( + %base: !tt.ptr, %rows: tensor<2xi64>, %shift: i32, %upper: index) + -> tensor<2x32x!tt.ptr> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<32> : tensor<2x32xi32> + %row = tt.expand_dims %rows {axis = 1 : i32} : tensor<2xi64> -> tensor<2x1xi64> + %splat = tt.splat %base : !tt.ptr -> tensor<2x1x!tt.ptr> + %row_ptrs = tt.addptr %splat, %row : tensor<2x1x!tt.ptr>, tensor<2x1xi64> + %broadcast = tt.broadcast %row_ptrs : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> + %range = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %expanded = tt.expand_dims %range {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %columns = tt.broadcast %expanded : tensor<1x32xi32> -> tensor<2x32xi32> + %shifts = tt.splat %shift : i32 -> tensor<2x32xi32> + %offsets = arith.addi %columns, %shifts : tensor<2x32xi32> + %result = tt.addptr %broadcast, %offsets : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%pointer = %result) -> (tensor<2x32x!tt.ptr>) { + %next = tt.addptr %pointer, %delta : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + scf.yield %next : tensor<2x32x!tt.ptr> + } + tt.return %final : tensor<2x32x!tt.ptr> + } + + // The existing descriptor itself can be narrow. Its complete i32 offset + // must be reconstructed before widening, even if the new range is safe. + tt.func public @mixed_narrow_descriptor( + %base: !tt.ptr, %rows: tensor<2xi32>, %upper: index) + -> tensor<2x32x!tt.ptr> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<32> : tensor<2x32xi32> + %row = tt.expand_dims %rows {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %row_offsets = tt.broadcast %row : tensor<2x1xi32> -> tensor<2x32xi32> + %range = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %expanded = tt.expand_dims %range {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %columns = tt.broadcast %expanded : tensor<1x32xi32> -> tensor<2x32xi32> + %offsets = arith.addi %row_offsets, %columns : tensor<2x32xi32> + %splat = tt.splat %base : !tt.ptr -> tensor<2x32x!tt.ptr> + %initial = tt.addptr %splat, %offsets : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + %wide_columns = arith.extsi %columns : tensor<2x32xi32> to tensor<2x32xi64> + %result = tt.addptr %initial, %wide_columns : tensor<2x32x!tt.ptr>, tensor<2x32xi64> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%pointer = %result) -> (tensor<2x32x!tt.ptr>) { + %next = tt.addptr %pointer, %delta : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + scf.yield %next : tensor<2x32x!tt.ptr> + } + tt.return %final : tensor<2x32x!tt.ptr> + } + + // A range on an opaque axis belongs in the residual, even though the range + // itself has no opaque contribution and does not change the axis kinds. + tt.func public @mixed_opaque_axis_update( + %base: !tt.ptr, %rows: tensor<2xi64>, %upper: index) + -> tensor<2x32x!tt.ptr> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<32> : tensor<2x32xi32> + %row = tt.expand_dims %rows {axis = 1 : i32} : tensor<2xi64> -> tensor<2x1xi64> + %splat = tt.splat %base : !tt.ptr -> tensor<2x1x!tt.ptr> + %row_ptrs = tt.addptr %splat, %row : tensor<2x1x!tt.ptr>, tensor<2x1xi64> + %broadcast = tt.broadcast %row_ptrs : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> + %range = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %expanded = tt.expand_dims %range {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %row_delta = tt.broadcast %expanded : tensor<2x1xi32> -> tensor<2x32xi32> + %result = tt.addptr %broadcast, %row_delta : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%pointer = %result) -> (tensor<2x32x!tt.ptr>) { + %next = tt.addptr %pointer, %delta : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + scf.yield %next : tensor<2x32x!tt.ptr> + } + tt.return %final : tensor<2x32x!tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @mixed_extended_range +// CHECK: %[[ROWS:.*]] = tt.broadcast {{.*}} : tensor<2x1xi64> -> tensor<2x32xi64> +// CHECK-NOT: arith.subi +// CHECK: scf.for {{.*}} iter_args(%[[OFFSET:.*]] = %{{.*}}) -> (i64) +// CHECK: %[[NEXT:.*]] = arith.addi %[[OFFSET]], %{{.*}} : i64 +// CHECK: scf.yield %[[NEXT]] : i64 +// CHECK: arith.addi %{{.*}}, %[[ROWS]] : tensor<2x32xi64> +// CHECK: tt.return + +// CHECK-LABEL: tt.func public @mixed_narrow_add +// CHECK: %[[NARROW:.*]] = arith.addi {{.*}} : tensor<2x32xi32> +// CHECK: arith.extsi %[[NARROW]] : tensor<2x32xi32> to tensor<2x32xi64> +// CHECK: arith.subi {{.*}} : tensor<2x32xi64> +// CHECK: tt.return + +// CHECK-LABEL: tt.func public @mixed_narrow_descriptor +// CHECK: arith.addi {{.*}} : tensor<2x32xi32> +// CHECK: arith.extsi {{.*}} : tensor<2x32xi32> to tensor<2x32xi64> +// CHECK: arith.subi {{.*}} : tensor<2x32xi64> +// CHECK: tt.return + +// CHECK-LABEL: tt.func public @mixed_opaque_axis_update +// CHECK: %[[ROWS:.*]] = tt.broadcast {{.*}} : tensor<2x1xi64> -> tensor<2x32xi64> +// CHECK: %[[DELTA:.*]] = arith.extsi {{.*}} : tensor<2x32xi32> to tensor<2x32xi64> +// CHECK: arith.addi %[[ROWS]], %[[DELTA]] : tensor<2x32xi64> +// CHECK: PointerDescriptorStructuredAxes = array +// CHECK: tt.return diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_rankn_extsi.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_rankn_extsi.mlir new file mode 100644 index 0000000000..d55ce76385 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_pointer_rankn_extsi.mlir @@ -0,0 +1,33 @@ +// RUN: triton-opt --triton-control-flow-opt %s -verify-each | FileCheck %s --check-prefix=CFO + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @tensor_pointer_rankn_extsi( + %base: !tt.ptr, %upper: index) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<1> : tensor<2x4xi32> + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %expanded = tt.expand_dims %range {axis = 0 : i32} + : tensor<4xi32> -> tensor<1x4xi32> + %extended = arith.extsi %expanded + : tensor<1x4xi32> to tensor<1x4xi64> + %offset = tt.broadcast %extended + : tensor<1x4xi64> -> tensor<2x4xi64> + %base_tensor = tt.splat %base : !tt.ptr + -> tensor<2x4x!tt.ptr> + %initial = tt.addptr %base_tensor, %offset + : tensor<2x4x!tt.ptr>, tensor<2x4xi64> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%ptr = %initial) -> (tensor<2x4x!tt.ptr>) { + %next = tt.addptr %ptr, %delta + : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + scf.yield %next : tensor<2x4x!tt.ptr> + } + %value = tt.load %final : tensor<2x4x!tt.ptr> + tt.return + } +} + +// CFO-LABEL: tt.func public @tensor_pointer_rankn_extsi +// CFO: scf.for {{.*}}iter_args({{.*}}) -> (i64) +// CFO: PointerDescriptorStructuredAxes = array diff --git a/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_ptr_different_base_invalid.mlir b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_ptr_different_base_invalid.mlir new file mode 100644 index 0000000000..6348111db2 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonControlFlowOpt/tensor_ptr_different_base_invalid.mlir @@ -0,0 +1,63 @@ +// RUN: triton-opt --triton-control-flow-opt %s -verify-each | FileCheck %s --check-prefix=CFO +// RUN: triton-opt --triton-control-flow-opt --triton-to-unstructure %s -verify-each | FileCheck %s --check-prefix=T2U --implicit-check-not='tensor.extract {{.*}} : tensor<4x!tt.ptr>' +// RUN: triton-opt --triton-control-flow-opt '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' %s -verify-each | FileCheck %s --check-prefix=T2U --implicit-check-not='tensor.extract {{.*}} : tensor<4x!tt.ptr>' +// RUN: triton-opt --triton-control-flow-opt --triton-to-unstructure --triton-to-linalg %s -verify-each | FileCheck %s --check-prefix=LINALG --implicit-check-not='!tt.ptr' --implicit-check-not=unrealized_conversion_cast --implicit-check-not=PointerDescriptorBoundary --implicit-check-not=PointerDescriptorRebuild --implicit-check-not=PointerDescriptorOffsetForm + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @if_tensor_ptr_different_base( + %base0: !tt.ptr, %base1: !tt.ptr, + %output: !tt.ptr, %cond: i1) { + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %off1 = tt.splat %c1_i32 : i32 -> tensor<4xi32> + %off2 = tt.splat %c2_i32 : i32 -> tensor<4xi32> + %selected = scf.if %cond -> (tensor<4x!tt.ptr>) { + %splat0 = tt.splat %base0 : !tt.ptr -> tensor<4x!tt.ptr> + %then_ptr = tt.addptr %splat0, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %then_ptr : tensor<4x!tt.ptr> + } else { + %splat1 = tt.splat %base1 : !tt.ptr -> tensor<4x!tt.ptr> + %else_ptr = tt.addptr %splat1, %off2 : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %else_ptr : tensor<4x!tt.ptr> + } + // Ownership must survive an ordinary addptr after the descriptor rebuild. + %post_addptr = tt.addptr %selected, %off1 : tensor<4x!tt.ptr>, tensor<4xi32> + %value = tt.load %post_addptr : tensor<4x!tt.ptr> + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %output_splat = tt.splat %output : !tt.ptr -> tensor<4x!tt.ptr> + %output_ptr = tt.addptr %output_splat, %range : tensor<4x!tt.ptr>, tensor<4xi32> + tt.store %output_ptr, %value : tensor<4x!tt.ptr> + tt.return + } +} + +// CFO-LABEL: tt.func public @if_tensor_ptr_different_base +// CFO-DAG: %[[BASE0:.*]] = tt.ptr_to_int %{{.*}} : !tt.ptr -> i64 +// CFO-DAG: %[[BASE1:.*]] = tt.ptr_to_int %{{.*}} : !tt.ptr -> i64 +// CFO: %[[SELECTED:.*]]:2 = scf.if %{{.*}} -> (i64, i32) { +// CFO: scf.yield %[[BASE0]], %{{.*}} : i64, i32 +// CFO: } else { +// CFO: scf.yield %[[BASE1]], %{{.*}} : i64, i32 +// CFO: } +// CFO: PointerDescriptorOffsetForm = "strided_1d" +// CFO: PointerDescriptorRebuild +// CFO: tt.addptr +// CFO: tt.load +// CFO: tt.store + +// T2U-LABEL: tt.func public @if_tensor_ptr_different_base +// T2U-DAG: %[[BASE0:.*]] = tt.ptr_to_int %{{.*}} : !tt.ptr -> i64 +// T2U-DAG: %[[BASE1:.*]] = tt.ptr_to_int %{{.*}} : !tt.ptr -> i64 +// T2U: %[[SELECTED_BASE:.*]] = arith.select %{{.*}}, %[[BASE0]], %[[BASE1]] : i64 +// T2U: %[[SELECTED_OFFSET:.*]] = arith.select %{{.*}}, %{{.*}}, %{{.*}} : i32 +// T2U: %[[BASE_PTR:.*]] = tt.int_to_ptr %[[SELECTED_BASE]] : i64 -> !tt.ptr +// T2U: tt.addptr +// T2U: tt.load + +// LINALG-LABEL: func.func @if_tensor_ptr_different_base +// LINALG: arith.select +// LINALG: hivm.hir.pointer_cast +// LINALG: memref.reinterpret_cast +// LINALG: memref.copy +// LINALG: bufferization.to_tensor +// LINALG: return diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/convert_modulo_to_mask.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/convert_modulo_to_mask.mlir new file mode 100644 index 0000000000..bf5df8178d --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/convert_modulo_to_mask.mlir @@ -0,0 +1,948 @@ +// RUN: triton-opt %s --graph-optimize='rule-mask=256' --split-input-file \ +// RUN: | FileCheck %s +// RUN: triton-opt %s --graph-optimize='rule-mask=0' --split-input-file \ +// RUN: | FileCheck %s --check-prefix=DISABLED + +// ConvertModuloToMask drops a modulo that only wraps tile addresses, so the +// addresses stay linear, and gives the loads a boundary mask so the lanes that +// used to wrap read in-bounds zeros instead. rule-mask=256 isolates the rule so +// nothing below can be attributed to another graph rule. +// +// The rewrite no longer consults tt.divisibility: the boundary mask is injected +// unconditionally, which is what keeps the transfer inside the tensor even when +// nothing can be proven about the bound. + +// ----- +// The load has no mask of its own, so it gets the boundary mask plus a zero +// fill. This is the fused_moe weight-tile shape. +// CHECK-LABEL: tt.func public @modulo_remsi_unmasked_load +// CHECK-NOT: arith.remsi +// CHECK: %[[BOUND:.*]] = arith.cmpi slt +// CHECK: %[[EXPAND:.*]] = tt.expand_dims %[[BOUND]] {axis = 0 : i32} +// CHECK: %[[MASK:.*]] = tt.broadcast %[[EXPAND]] +// CHECK: %[[FILL:.*]] = arith.constant dense<0.000000e+00> : tensor<32x16xf16> +// CHECK: tt.load %{{.*}}, %[[MASK]], %[[FILL]] : tensor<32x16x!tt.ptr> +// CHECK-NOT: arith.remsi +// DISABLED-LABEL: tt.func public @modulo_remsi_unmasked_load +// DISABLED: arith.remsi +tt.func public @modulo_remsi_unmasked_load(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// A load that already carries a mask keeps it: the boundary mask is combined +// with arith.andi and the existing fill is left untouched. +// CHECK-LABEL: tt.func public @modulo_remsi_masked_load +// CHECK-NOT: arith.remsi +// CHECK: %[[BOUND:.*]] = arith.cmpi slt +// CHECK: %[[EXPAND:.*]] = tt.expand_dims %[[BOUND]] {axis = 0 : i32} +// CHECK: %[[MASK:.*]] = tt.broadcast %[[EXPAND]] +// CHECK: %[[BOTH:.*]] = arith.andi %{{.*}}, %[[MASK]] +// CHECK: tt.load %{{.*}}, %[[BOTH]], %{{.*}} : tensor<32x16x!tt.ptr> +// CHECK-NOT: arith.remsi +// DISABLED-LABEL: tt.func public @modulo_remsi_masked_load +// DISABLED: arith.remsi +// DISABLED-NOT: arith.andi +tt.func public @modulo_remsi_masked_load(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32, %k: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + + // The row axis has its own boundary mask, unrelated to the modulo. + %k_splat = tt.splat %k : i32 -> tensor<32x1xi32> + %row_guard = arith.cmpi slt, %row_expd, %k_splat : tensor<32x1xi32> + %row_guard_bc = tt.broadcast %row_guard : tensor<32x1xi1> -> tensor<32x16xi1> + %fill = arith.constant dense<1.000000e+00> : tensor<32x16xf16> + %val = tt.load %ptrs, %row_guard_bc, %fill : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Canonicalization expands a modulo into divsi/muli/subi. All three ops go +// away together, since the identity x == (x / d) * d + x % d makes the subi +// exactly the modulo. +// CHECK-LABEL: tt.func public @modulo_expanded_into_divsi_muli_subi +// CHECK-NOT: arith.divsi +// CHECK-NOT: arith.remsi +// CHECK: %[[BOUND:.*]] = arith.cmpi slt +// CHECK: tt.load %{{.*}}, %{{.*}}, %{{.*}} : tensor<32x16x!tt.ptr> +// CHECK-NOT: arith.divsi +// DISABLED-LABEL: tt.func public @modulo_expanded_into_divsi_muli_subi +// DISABLED: arith.divsi +tt.func public @modulo_expanded_into_divsi_muli_subi(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %quot = arith.divsi %offs, %n_splat : tensor<16xi32> + %prod = arith.muli %quot, %n_splat : tensor<16xi32> + %rem = arith.subi %offs, %prod : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// The store-mask guard is often written on the already expanded offset, so the +// comparison is found through tt.expand_dims as well. +// CHECK-LABEL: tt.func public @modulo_guard_through_expand_dims +// CHECK-NOT: arith.remsi +// CHECK: tt.load %{{.*}}, %{{.*}}, %{{.*}} : tensor<32x16x!tt.ptr> +tt.func public @modulo_guard_through_expand_dims(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %n_splat_2d = tt.splat %n : i32 -> tensor<1x16xi32> + %guard = arith.cmpi slt, %col_expd, %n_splat_2d : tensor<1x16xi32> + %guard_bc = tt.broadcast %guard : tensor<1x16xi1> -> tensor<32x16xi1> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// The wrapped index can address rows instead of columns. The mask is expanded +// on the same axis, so it varies along the same dimension of the load. +// CHECK-LABEL: tt.func public @modulo_on_row_axis +// CHECK-NOT: arith.remsi +// CHECK: %[[BOUND:.*]] = arith.cmpi slt +// CHECK: %[[EXPAND:.*]] = tt.expand_dims %[[BOUND]] {axis = 1 : i32} +// CHECK: %[[MASK:.*]] = tt.broadcast %[[EXPAND]] +// CHECK: tt.load %{{.*}}, %[[MASK]], %{{.*}} : tensor<16x32x!tt.ptr> +tt.func public @modulo_on_row_axis(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %m: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %m_splat = tt.splat %m : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %m_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_stride = arith.constant dense<32> : tensor<16x1xi32> + %rem_scaled = arith.muli %rem_expd, %row_stride : tensor<16x1xi32> + %rem_bc = tt.broadcast %rem_scaled : tensor<16x1xi32> -> tensor<16x32xi32> + %col = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %col_expd = tt.expand_dims %col {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x32xi32> -> tensor<16x32xi32> + %addr = arith.addi %rem_bc, %col_bc : tensor<16x32xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<16x32x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<16x32x!tt.ptr>, tensor<16x32xi32> + %val = tt.load %ptrs : tensor<16x32x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %m_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 1 : i32} : tensor<16xi1> -> tensor<16x1xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<16x1xi1> -> tensor<16x32xi1> + %out_row = tt.expand_dims %offs {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %out_scaled = arith.muli %out_row, %row_stride : tensor<16x1xi32> + %out_bc = tt.broadcast %out_scaled : tensor<16x1xi32> -> tensor<16x32xi32> + %out_addr = arith.addi %out_bc, %col_bc : tensor<16x32xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<16x32x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<16x32x!tt.ptr>, tensor<16x32xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<16x32x!tt.ptr> + tt.return +} + +// ----- +// The address is normally carried around the reduction loop, so the walk has to +// follow scf.for iter_args to reach the load it feeds. +// CHECK-LABEL: tt.func public @modulo_load_inside_loop +// CHECK-NOT: arith.remsi +// CHECK: %[[BOUND:.*]] = arith.cmpi slt +// CHECK: scf.for +// CHECK: %[[EXPAND:.*]] = tt.expand_dims %[[BOUND]] {axis = 0 : i32} +// CHECK: %[[MASK:.*]] = tt.broadcast %[[EXPAND]] +// CHECK: tt.load %{{.*}}, %[[MASK]], %{{.*}} : tensor<32x16x!tt.ptr> +tt.func public @modulo_load_inside_loop(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c4 = arith.constant 4 : i32 + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %step = arith.constant dense<512> : tensor<32x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<32x16xf16> + + %loop:2 = scf.for %iv = %c0 to %c4 step %c1 iter_args(%acc = %zeros, %cur = %ptrs) -> (tensor<32x16xf16>, tensor<32x16x!tt.ptr>) : i32 { + %tile = tt.load %cur : tensor<32x16x!tt.ptr> + %next_acc = arith.addf %acc, %tile : tensor<32x16xf16> + %next_ptrs = tt.addptr %cur, %step : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + scf.yield %next_acc, %next_ptrs : tensor<32x16xf16>, tensor<32x16x!tt.ptr> + } + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %loop#0, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: a pointer carried by an outer loop and then by an inner loop is +// deliberately not linearized. The direct-memory lowerer must preserve the +// inner result as the outer loop state before this case can become eligible. +// CHECK-LABEL: tt.func public @modulo_load_inside_nested_loop +// CHECK: arith.remsi +tt.func public @modulo_load_inside_nested_loop(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c4 = arith.constant 4 : i32 + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %step = arith.constant dense<512> : tensor<32x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<32x16xf16> + + %outer:2 = scf.for %outer_iv = %c0 to %c4 step %c1 iter_args(%outer_acc = %zeros, %outer_ptrs = %ptrs) -> (tensor<32x16xf16>, tensor<32x16x!tt.ptr>) : i32 { + %inner:2 = scf.for %inner_iv = %c0 to %c4 step %c1 iter_args(%inner_acc = %outer_acc, %inner_ptrs = %outer_ptrs) -> (tensor<32x16xf16>, tensor<32x16x!tt.ptr>) : i32 { + %tile = tt.load %inner_ptrs : tensor<32x16x!tt.ptr> + %next_acc = arith.addf %inner_acc, %tile : tensor<32x16xf16> + %next_ptrs = tt.addptr %inner_ptrs, %step : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + scf.yield %next_acc, %next_ptrs : tensor<32x16xf16>, tensor<32x16x!tt.ptr> + } + scf.yield %inner#0, %inner#1 : tensor<32x16xf16>, tensor<32x16x!tt.ptr> + } + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %outer#0, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: a compile-time constant divisor belongs to TritonToStructured, +// whose visitOperandRem keeps the wrap and re-expresses it as a strided access. +// That is exactly equivalent, so it is always preferable to discarding the wrap. +// CHECK-LABEL: tt.func public @modulo_skip_constant_divisor +// CHECK: arith.remsi +tt.func public @modulo_skip_constant_divisor(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}) { + %c16 = arith.constant 16 : i32 + %c1024 = arith.constant 1024 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %bound = tt.splat %c1024 : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %bound : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %bound : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: nothing proves that the kernel already discards the wrapped lanes, +// so the wrap has to stay. +// CHECK-LABEL: tt.func public @modulo_skip_without_store_mask_guard +// CHECK: arith.remsi +tt.func public @modulo_skip_without_store_mask_guard(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the guard compares against a different runtime scalar than the +// divisor, so it says nothing about the wrapped lanes. +// CHECK-LABEL: tt.func public @modulo_skip_mismatched_bound +// CHECK: arith.remsi +tt.func public @modulo_skip_mismatched_bound(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32, %other: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %other_splat = tt.splat %other : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %other_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the comparison exists but only selects data, so it does not prove +// that the wrapped lanes stay out of memory. +// CHECK-LABEL: tt.func public @modulo_skip_guard_not_on_a_store +// CHECK: arith.remsi +tt.func public @modulo_skip_guard_not_on_a_store(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + // The comparison only picks between two tiles; it never reaches a store mask. + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %zeros = arith.constant dense<0.000000e+00> : tensor<32x16xf16> + %picked = arith.select %guard_bc, %val, %zeros : tensor<32x16xi1>, tensor<32x16xf16> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %picked : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the wrapped index also addresses the store. Dropping the wrap +// would move where results are written, not just which data is read. +// CHECK-LABEL: tt.func public @modulo_skip_result_addresses_store +// CHECK: arith.remsi +tt.func public @modulo_skip_result_addresses_store(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the wrapped index is also the data a load falls back to, so the +// widened index would become observable in the loaded values. +// CHECK-LABEL: tt.func public @modulo_skip_result_used_as_load_fill +// CHECK: arith.remsi +tt.func public @modulo_skip_result_used_as_load_fill(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + // The wrapped index is the fill value here, not only an address. + %val = tt.load %ptrs, %guard_bc, %rem_bc : tensor<32x16x!tt.ptr> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the wrapped index leaves the address computation, so the widened +// value would be observable. +// CHECK-LABEL: tt.func public @modulo_skip_result_leaves_address_chain +// CHECK: arith.remsi +tt.func public @modulo_skip_result_leaves_address_chain(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + // The index becomes data here. + %as_float = arith.sitofp %rem_bc : tensor<32x16xi32> to tensor<32x16xf32> + %scaled = arith.mulf %val, %as_float : tensor<32x16xf32> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %scaled, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the index is expanded on two different axes, so it addresses two +// different dimensions and no single mask orientation would be correct. +// CHECK-LABEL: tt.func public @modulo_skip_conflicting_expand_axes +// CHECK: arith.remsi +tt.func public @modulo_skip_conflicting_expand_axes(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %col_index = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_index : tensor<1x16xi32> -> tensor<16x16xi32> + %row_index = tt.expand_dims %rem {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_stride = arith.constant dense<16> : tensor<16x1xi32> + %row_scaled = arith.muli %row_index, %row_stride : tensor<16x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<16x1xi32> -> tensor<16x16xi32> + %addr = arith.addi %row_bc, %col_bc : tensor<16x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<16x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %val = tt.load %ptrs : tensor<16x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<16x16xi1> + %out_col = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %out_bc = tt.broadcast %out_col : tensor<1x16xi32> -> tensor<16x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<16x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_bc : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<16x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: a transpose moves the index to another dimension, so the axis its +// expand_dims recorded would no longer describe where it varies, and a mask +// built on that axis would zero the wrong lanes. +// CHECK-LABEL: tt.func public @modulo_skip_transposed_address +// CHECK: arith.remsi +tt.func public @modulo_skip_transposed_address(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<16x1xi32> -> tensor<16x8xi32> + %transposed = tt.trans %rem_bc {order = array} : tensor<16x8xi32> -> tensor<8x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<8x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %transposed : tensor<8x16x!tt.ptr>, tensor<8x16xi32> + %val = tt.load %ptrs : tensor<8x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<8x16xi1> + %out_col = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %out_bc = tt.broadcast %out_col : tensor<1x16xi32> -> tensor<8x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<8x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_bc : tensor<8x16x!tt.ptr>, tensor<8x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<8x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: a rank-one load has no second dimension for the boundary mask to +// leave alone, and it is already a contiguous transfer. +// CHECK-LABEL: tt.func public @modulo_skip_rank_one_load +// CHECK: arith.remsi +tt.func public @modulo_skip_rank_one_load(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %rem : tensor<16x!tt.ptr>, tensor<16xi32> + %val = tt.load %ptrs : tensor<16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %offs : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %out_ptrs, %val, %guard : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the wrapped value is not a tile offset at all, so there is no +// linear access to recover. +// CHECK-LABEL: tt.func public @modulo_skip_without_make_range +// CHECK: arith.remsi +tt.func public @modulo_skip_without_make_range(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %offs = tt.splat %blk : i32 -> tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %rem = arith.remsi %offs, %n_splat : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %row_bc : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the divisor varies per lane, so one boundary comparison cannot +// describe the wrap. +// CHECK-LABEL: tt.func public @modulo_skip_non_splat_divisor +// CHECK: arith.remsi +tt.func public @modulo_skip_non_splat_divisor(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %divisors = arith.addi %n_splat, %range : tensor<16xi32> + %rem = arith.remsi %offs, %divisors : tensor<16xi32> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %row = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %row_expd = tt.expand_dims %row {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %row_stride = arith.constant dense<16> : tensor<32x1xi32> + %row_scaled = arith.muli %row_expd, %row_stride : tensor<32x1xi32> + %row_bc = tt.broadcast %row_scaled : tensor<32x1xi32> -> tensor<32x16xi32> + %addr = arith.addi %row_bc, %rem_bc : tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %out_addr = arith.addi %row_bc, %col_bc : tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %out_addr : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: a two-dimensional wrap is not a tile offset in the form this rule +// reasons about. +// CHECK-LABEL: tt.func public @modulo_skip_two_dimensional_wrap +// CHECK: arith.remsi +tt.func public @modulo_skip_two_dimensional_wrap(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %offs_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %offs_bc = tt.broadcast %offs_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %n_splat_2d = tt.splat %n : i32 -> tensor<32x16xi32> + %rem = arith.remsi %offs_bc, %n_splat_2d : tensor<32x16xi32> + + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %rem : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %offs_bc : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: the wrap is on 64-bit offsets, which is not the tile-offset form +// this rule reasons about. +// CHECK-LABEL: tt.func public @modulo_skip_wide_offsets +// CHECK: arith.remsi +tt.func public @modulo_skip_wide_offsets(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %offs_wide = arith.extsi %offs : tensor<16xi32> to tensor<16xi64> + %n_wide = arith.extsi %n : i32 to i64 + %n_splat_wide = tt.splat %n_wide : i64 -> tensor<16xi64> + %rem = arith.remsi %offs_wide, %n_splat_wide : tensor<16xi64> + + %rem_expd = tt.expand_dims %rem {axis = 0 : i32} : tensor<16xi64> -> tensor<1x16xi64> + %rem_bc = tt.broadcast %rem_expd : tensor<1x16xi64> -> tensor<32x16xi64> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %rem_bc : tensor<32x16x!tt.ptr>, tensor<32x16xi64> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %col_bc : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} + +// ----- +// Negative: a subtraction that is not the divsi/muli/subi expansion of a wrap +// must not be mistaken for one. +// CHECK-LABEL: tt.func public @modulo_skip_plain_subtraction +// CHECK: arith.subi +tt.func public @modulo_skip_plain_subtraction(%base: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %c16 = arith.constant 16 : i32 + %pid = tt.get_program_id x : i32 + %blk = arith.muli %pid, %c16 : i32 + %blk_splat = tt.splat %blk : i32 -> tensor<16xi32> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %offs = arith.addi %blk_splat, %range : tensor<16xi32> + %n_splat = tt.splat %n : i32 -> tensor<16xi32> + // A quotient of a different value makes this subtraction unrelated to a wrap. + %quot = arith.divsi %range, %n_splat : tensor<16xi32> + %prod = arith.muli %quot, %n_splat : tensor<16xi32> + %shifted = arith.subi %offs, %prod : tensor<16xi32> + + %expd = tt.expand_dims %shifted {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %bc = tt.broadcast %expd : tensor<1x16xi32> -> tensor<32x16xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<32x16x!tt.ptr> + %ptrs = tt.addptr %base_splat, %bc : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %val = tt.load %ptrs : tensor<32x16x!tt.ptr> + + %guard = arith.cmpi slt, %offs, %n_splat : tensor<16xi32> + %guard_expd = tt.expand_dims %guard {axis = 0 : i32} : tensor<16xi1> -> tensor<1x16xi1> + %guard_bc = tt.broadcast %guard_expd : tensor<1x16xi1> -> tensor<32x16xi1> + %col_expd = tt.expand_dims %offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<32x16xi32> + %dst_splat = tt.splat %dst : !tt.ptr -> tensor<32x16x!tt.ptr> + %out_ptrs = tt.addptr %dst_splat, %col_bc : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + tt.store %out_ptrs, %val, %guard_bc : tensor<32x16x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/diagonal_mask_removal.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/diagonal_mask_removal.mlir new file mode 100644 index 0000000000..39bab15809 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/diagonal_mask_removal.mlir @@ -0,0 +1,885 @@ +// RUN: triton-opt %s --graph-optimize='rule-mask=128' --split-input-file \ +// RUN: | FileCheck %s +// RUN: triton-opt %s --graph-optimize='rule-mask=0' --split-input-file \ +// RUN: | FileCheck %s --check-prefix=DISABLED + +// DiagonalMaskRemoval collapses a diagonal-mask shift of a scan result into a +// single subtraction. rule-mask=128 isolates the rule so nothing below can be +// attributed to another graph rule. + +// ----- +// Forward cumulative sum selected by the sub-diagonal (row == col + 1) is a +// left shift, which the forward identity scan[i] - x[i] == scan[i - 1] covers. +// CHECK-LABEL: tt.func public @diagonal_forward_sub_diagonal +// CHECK-NOT: tt.reduce +// CHECK: arith.subf +// CHECK-NOT: tt.reduce +tt.func public @diagonal_forward_sub_diagonal(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// Reverse scan selected by the super-diagonal (row == col - 1) is a right +// shift, covered by scan[i] - x[i] == scan[i + 1]. +// CHECK-LABEL: tt.func public @diagonal_reverse_super_diagonal +// CHECK-NOT: tt.reduce +// CHECK: arith.subf +tt.func public @diagonal_reverse_super_diagonal(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = true}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.subi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// Integer accumulation has an exact inverse, so an addi scan is rewritten to +// arith.subi. +// CHECK-LABEL: tt.func public @diagonal_integer_cumsum +// CHECK-NOT: tt.reduce +// CHECK: arith.subi +tt.func public @diagonal_integer_cumsum(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: i32, %b: i32): + %s = arith.addi %a, %b : i32 + tt.scan.return %s : i32 + }) : (tensor<16xi32>) -> tensor<16xi32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %bcast = tt.broadcast %expd : tensor<1x16xi32> -> tensor<16x16xi32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0> : tensor<16x16xi32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xi32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: i32, %b: i32): + %s = arith.addi %a, %b : i32 + tt.reduce.return %s : i32 + }) : (tensor<16x16xi32>) -> tensor<16xi32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// The diagonal offset may sit on the 1-D range, before expand_dims, and it may +// be carried by a tt.splat instead of a dense constant. +// CHECK-LABEL: tt.func public @diagonal_offset_before_expand_dims +// CHECK-NOT: tt.reduce +// CHECK: arith.subf +tt.func public @diagonal_offset_before_expand_dims(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %c1 = arith.constant 1 : i32 + %one_splat = tt.splat %c1 : i32 -> tensor<16xi32> + %col_shifted = arith.addi %col_range, %one_splat : tensor<16xi32> + %col_expd = tt.expand_dims %col_shifted {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// `row - 1 == col` denotes the same sub-diagonal as `row == col + 1`, so the +// offset is accepted on the kept side too. +// CHECK-LABEL: tt.func public @diagonal_offset_on_kept_side +// CHECK-NOT: tt.reduce +// CHECK: arith.subf +tt.func public @diagonal_offset_on_kept_side(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_one = arith.constant dense<1> : tensor<16x1xi32> + %row_shifted = arith.subi %row_expd, %row_one : tensor<16x1xi32> + %row_bc = tt.broadcast %row_shifted : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_bc = tt.broadcast %col_expd : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// A non-zero make_range start shifts the diagonal exactly like an explicit +// offset, so start 2 against offset 3 is still a unit sub-diagonal. +// CHECK-LABEL: tt.func public @diagonal_non_zero_range_start +// CHECK-NOT: tt.reduce +// CHECK: arith.subf +tt.func public @diagonal_non_zero_range_start(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 18 : i32, start = 2 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %three = arith.constant dense<3> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %three : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// The identity holds per batch row, so a leading batch dimension is supported +// as long as the replicated dimension stays adjacent to the scan axis. +// CHECK-LABEL: tt.func public @diagonal_batched_scan +// CHECK-NOT: tt.reduce +// CHECK: arith.subf +tt.func public @diagonal_batched_scan(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<4x16x!tt.ptr> + %batch = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %batch_expd = tt.expand_dims %batch {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %batch_bc = tt.broadcast %batch_expd : tensor<4x1xi32> -> tensor<4x16xi32> + %stride = arith.constant dense<16> : tensor<4x16xi32> + %batch_off = arith.muli %batch_bc, %stride : tensor<4x16xi32> + %lane = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %lane_expd = tt.expand_dims %lane {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %lane_bc = tt.broadcast %lane_expd : tensor<1x16xi32> -> tensor<4x16xi32> + %offset = arith.addi %batch_off, %lane_bc : tensor<4x16xi32> + %src_ptr = tt.addptr %src_base, %offset : tensor<4x16x!tt.ptr>, tensor<4x16xi32> + %x = tt.load %src_ptr : tensor<4x16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 1 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<4x16xf32>) -> tensor<4x16xf32> + + %expd = tt.expand_dims %scan {axis = 1 : i32} : tensor<4x16xf32> -> tensor<4x1x16xf32> + %bcast = tt.broadcast %expd : tensor<4x1x16xf32> -> tensor<4x16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_lane = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_batch = tt.expand_dims %row_lane {axis = 0 : i32} : tensor<16x1xi32> -> tensor<1x16x1xi32> + %row_bc = tt.broadcast %row_batch : tensor<1x16x1xi32> -> tensor<4x16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_lane = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_batch = tt.expand_dims %col_lane {axis = 0 : i32} : tensor<1x16xi32> -> tensor<1x1x16xi32> + %one = arith.constant dense<1> : tensor<1x1x16xi32> + %col_shifted = arith.addi %col_batch, %one : tensor<1x1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x1x16xi32> -> tensor<4x16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<4x16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<4x16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<4x16x16xi1>, tensor<4x16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 2 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<4x16x16xf32>) -> tensor<4x16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<4x16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<4x16x!tt.ptr>, tensor<4x16xi32> + tt.store %dst_ptr, %red : tensor<4x16x!tt.ptr> + tt.return +} + +// ----- +// A shift of two is a genuine data movement with no O(N) identity, so the +// quadratic form must survive. +// CHECK-LABEL: tt.func public @diagonal_skip_shift_of_two +// CHECK: tt.reduce +tt.func public @diagonal_skip_shift_of_two(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %two = arith.constant dense<2> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %two : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// A non-zero range start that pushes the effective offset to two must not be +// mistaken for a unit shift. The historical matcher ignored the start and +// would have rewritten this incorrectly. +// CHECK-LABEL: tt.func public @diagonal_skip_range_start_breaks_unit_shift +// CHECK: tt.reduce +tt.func public @diagonal_skip_range_start_breaks_unit_shift(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 17 : i32, start = 1 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// A sub-diagonal left shift needs the forward identity; a reverse scan does +// not satisfy it. +// CHECK-LABEL: tt.func public @diagonal_skip_direction_mismatch +// CHECK: tt.reduce +tt.func public @diagonal_skip_direction_mismatch(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = true}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// The unselected lanes must contribute the add identity; a non-zero fill makes +// the boundary element observable. +// CHECK-LABEL: tt.func public @diagonal_skip_non_zero_fill +// CHECK: tt.reduce +tt.func public @diagonal_skip_non_zero_fill(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %fill = arith.constant dense<1.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %fill : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// A maximum reduce extracts the same element but leaves a different boundary +// value, so it is not covered by the subtraction. +// CHECK-LABEL: tt.func public @diagonal_skip_max_reduce +// CHECK: tt.reduce +tt.func public @diagonal_skip_max_reduce(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.maximumf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// Reducing the replicated dimension instead of the scan dimension does not +// select a shifted element at all. +// CHECK-LABEL: tt.func public @diagonal_skip_wrong_reduce_axis +// CHECK: tt.reduce +tt.func public @diagonal_skip_wrong_reduce_axis(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 0 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// Here every type still lines up, but the replicated dimension is inserted in +// front of the batch dimension, so the reduced result is the transpose of the +// scan result and the subtraction would silently change the values. +// CHECK-LABEL: tt.func public @diagonal_skip_replicated_dim_not_adjacent +// CHECK: tt.reduce +tt.func public @diagonal_skip_replicated_dim_not_adjacent(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x16x!tt.ptr> + %row = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_e = tt.expand_dims %row {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_b = tt.broadcast %row_e : tensor<16x1xi32> -> tensor<16x16xi32> + %stride = arith.constant dense<16> : tensor<16x16xi32> + %row_off = arith.muli %row_b, %stride : tensor<16x16xi32> + %col = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_e = tt.expand_dims %col {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %col_b = tt.broadcast %col_e : tensor<1x16xi32> -> tensor<16x16xi32> + %offset = arith.addi %row_off, %col_b : tensor<16x16xi32> + %src_ptr = tt.addptr %src_base, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %x = tt.load %src_ptr : tensor<16x16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 1 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16x16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16x16xf32> -> tensor<1x16x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16x16xf32> -> tensor<16x16x16xf32> + + %mrow_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %mrow_lane = tt.expand_dims %mrow_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %mrow_batch = tt.expand_dims %mrow_lane {axis = 2 : i32} : tensor<16x1xi32> -> tensor<16x1x1xi32> + %mrow_bc = tt.broadcast %mrow_batch : tensor<16x1x1xi32> -> tensor<16x16x16xi32> + + %mcol_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %mcol_lane = tt.expand_dims %mcol_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %mcol_batch = tt.expand_dims %mcol_lane {axis = 0 : i32} : tensor<1x16xi32> -> tensor<1x1x16xi32> + %one = arith.constant dense<1> : tensor<1x1x16xi32> + %mcol_shifted = arith.addi %mcol_batch, %one : tensor<1x1x16xi32> + %mcol_bc = tt.broadcast %mcol_shifted : tensor<1x1x16xi32> -> tensor<16x16x16xi32> + + %cmp = arith.cmpi eq, %mrow_bc, %mcol_bc : tensor<16x16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16x16xi1>, tensor<16x16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 2 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16x16xf32>) -> tensor<16x16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %dst_ptr, %red : tensor<16x16x!tt.ptr> + tt.return +} + +// ----- +// The reduced result must have exactly the scan result's type; a rectangular +// broadcast cannot be replaced by an elementwise subtraction. +// CHECK-LABEL: tt.func public @diagonal_skip_non_square +// CHECK: tt.reduce +tt.func public @diagonal_skip_non_square(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<8x16xf32> + + %row_range = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<8x1xi32> -> tensor<8x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<8x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<8x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<8x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<8x16xi1>, tensor<8x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<8x16xf32>) -> tensor<8xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<8x!tt.ptr> + %dst_range = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %dst_ptr = tt.addptr %dst_base, %dst_range : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_ptr, %red : tensor<8x!tt.ptr> + tt.return +} + +// ----- +// An inequality predicate selects a triangle, not a single diagonal. +// CHECK-LABEL: tt.func public @diagonal_skip_slt_predicate +// CHECK: tt.reduce +tt.func public @diagonal_skip_slt_predicate(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi slt, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// A multiplicative scan has no subtraction inverse. +// CHECK-LABEL: tt.func public @diagonal_skip_mul_scan +// CHECK: tt.reduce +tt.func public @diagonal_skip_mul_scan(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.mulf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} + +// ----- +// A zero rule mask must leave the pattern alone. Only the DISABLED run +// inspects this section. +// DISABLED-LABEL: tt.func public @diagonal_disabled_by_rule_mask +// DISABLED: tt.reduce +// DISABLED-NOT: arith.subf +tt.func public @diagonal_disabled_by_rule_mask(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %src_base = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %src_ptr = tt.addptr %src_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + %x = tt.load %src_ptr : tensor<16x!tt.ptr> + + %scan = "tt.scan"(%x) <{axis = 0 : i32, reverse = false}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.scan.return %s : f32 + }) : (tensor<16xf32>) -> tensor<16xf32> + + %expd = tt.expand_dims %scan {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %bcast = tt.broadcast %expd : tensor<1x16xf32> -> tensor<16x16xf32> + + %row_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %row_expd = tt.expand_dims %row_range {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %row_bc = tt.broadcast %row_expd : tensor<16x1xi32> -> tensor<16x16xi32> + + %col_range = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %col_expd = tt.expand_dims %col_range {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %one = arith.constant dense<1> : tensor<1x16xi32> + %col_shifted = arith.addi %col_expd, %one : tensor<1x16xi32> + %col_bc = tt.broadcast %col_shifted : tensor<1x16xi32> -> tensor<16x16xi32> + + %cmp = arith.cmpi eq, %row_bc, %col_bc : tensor<16x16xi32> + %zeros = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %sel = arith.select %cmp, %bcast, %zeros : tensor<16x16xi1>, tensor<16x16xf32> + + %red = "tt.reduce"(%sel) <{axis = 1 : i32}> ({ + ^bb0(%a: f32, %b: f32): + %s = arith.addf %a, %b : f32 + tt.reduce.return %s : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + + %dst_base = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptr = tt.addptr %dst_base, %range : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %dst_ptr, %red : tensor<16x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-driver.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-driver.mlir new file mode 100644 index 0000000000..511d4ad00d --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-driver.mlir @@ -0,0 +1,18 @@ +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=0 max-rewrites-per-function=1' -o - | FileCheck %s +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize -graph-optimize -o - | FileCheck %s + +// CHECK-LABEL: tt.func @graph_optimize_first() +tt.func @graph_optimize_first() { + // CHECK-NEXT: tt.return + tt.return + // CHECK-NEXT: } +} + +// ----- + +// CHECK-LABEL: tt.func @graph_optimize_second( +tt.func @graph_optimize_second(%value: i32) -> i32 { + // CHECK-NEXT: tt.return %{{.*}} : i32 + tt.return %value : i32 + // CHECK-NEXT: } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-load-store-lowering.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-load-store-lowering.mlir new file mode 100644 index 0000000000..bce6549ed7 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-load-store-lowering.mlir @@ -0,0 +1,54 @@ +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -o - | FileCheck %s --check-prefix=GRAPH +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -triton-to-linalg -o /dev/null +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -triton-to-linalg -o - | FileCheck %s --check-prefix=LOWER + +// Keep a lowering regression on the unified, type-changing layout rewrite. +// The original access is tensor<2x3>; the graph pass must emit the equivalent +// tensor<3x2> access before the normal Triton-to-Linalg lowering runs. +// GRAPH-LABEL: tt.func @lower_unified_layout( +// GRAPH: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<3x1xi1> +// GRAPH: tt.load {{.*}} : tensor<3x2x!tt.ptr> +// GRAPH: math.tan {{.*}} : tensor<3x2xf32> +// GRAPH: tt.store {{.*}} : tensor<3x2x!tt.ptr> +tt.func @lower_unified_layout(%source: !tt.ptr, %destination: !tt.ptr) { + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis0_expand = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis1_expand = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %two = arith.constant 2 : i32 + %axis1_scale = tt.splat %two : i32 -> tensor<1x3xi32> + %axis1_scaled = arith.muli %axis1_expand, %axis1_scale : tensor<1x3xi32> + %axis1_scaled_i64 = arith.extsi %axis1_scaled : tensor<1x3xi32> to tensor<1x3xi64> + %axis1_max = arith.constant dense<2147483647> : tensor<1x3xi64> + %axis1_min = arith.constant dense<-2147483648> : tensor<1x3xi64> + %axis1_le = arith.cmpi sle, %axis1_scaled_i64, %axis1_max : tensor<1x3xi64> + %axis1_ge = arith.cmpi sge, %axis1_scaled_i64, %axis1_min : tensor<1x3xi64> + %axis1_ok = arith.andi %axis1_le, %axis1_ge : tensor<1x3xi1> + tt.assert %axis1_ok, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : tensor<1x3xi1> + %axis0_full = tt.broadcast %axis0_expand : tensor<2x1xi32> -> tensor<2x3xi32> + %axis1_full = tt.broadcast %axis1_scaled : tensor<1x3xi32> -> tensor<2x3xi32> + %offset = arith.addi %axis0_full, %axis1_full : tensor<2x3xi32> + %source_base = tt.splat %source : !tt.ptr -> tensor<2x3x!tt.ptr> + %destination_base = tt.splat %destination : !tt.ptr -> tensor<2x3x!tt.ptr> + %source_ptr = tt.addptr %source_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %destination_ptr = tt.addptr %destination_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %loaded = tt.load %source_ptr : tensor<2x3x!tt.ptr> + %result = math.tan %loaded : tensor<2x3xf32> + tt.store %destination_ptr, %result : tensor<2x3x!tt.ptr> + tt.return +} + +// The matching message alone must not make a user-authored device_assert +// disappear during lowering. Only the private frontend marker identifies an +// automatic overflow assertion. +// LOWER: func.func private @{{.*}}(i1) attributes {msg = "int32 overflow detected for operation mul"} +// LOWER-LABEL: func.func @lower_user_same_message_assert( +// LOWER: call @{{.*}}({{.*}}) : (i1) -> () +// LOWER-NOT: call @ +// LOWER: return +tt.func @lower_user_same_message_assert() { + %true = arith.constant true + tt.assert %true, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : i1 + tt.assert %true, "int32 overflow detected for operation mul" : i1 + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-load-store.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-load-store.mlir new file mode 100644 index 0000000000..1be82bf111 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-load-store.mlir @@ -0,0 +1,480 @@ +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -o - | FileCheck %s +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -canonicalize -cse -o - | FileCheck %s --check-prefix=POST +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -o - | FileCheck %s --check-prefix=CLEAN +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -canonicalize -cse -o - | FileCheck %s --check-prefix=POST-CLEAN +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -canonicalize -cse -o - | FileCheck %s --check-prefix=POST-EXPAND +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -canonicalize -cse -o - | FileCheck %s --check-prefix=POST-SPLAT +// RUN: triton-opt %s --verify-each '-graph-optimize=rule-mask=1' -canonicalize -cse -o - | FileCheck %s --check-prefix=POST-MULI + +// The rule owns one closed component: two masked loads, two stores, a binary +// fanout, dense-splat `other`/accumulator values, an ordered tt.assert, four +// loop-carried pointers, and two accumulators. N is a runtime physical stride +// rather than a dynamic tensor dimension. The active mask is intentionally +// retained so N=1 remains a legal runtime case. +// CHECK-LABEL: tt.func @masked_dynamic_stride_loop( +// CHECK: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<3x1xi1> +// CHECK: tt.assert {{.*}}, "int32 overflow detected for operation add"{{.*}} : tensor<3x2xi1> +// CHECK: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<3x1xi1> +// CHECK: tt.assert {{.*}}, "int32 overflow detected for operation add"{{.*}} : tensor<3x2xi1> +// CHECK-NOT: tensor<1x3xi64> +// CHECK-NOT: tensor<2x3xi64> +// CHECK: scf.for {{.*}} -> (tensor<3x2x!tt.ptr>, tensor<3x2x!tt.ptr>, tensor<3x2x!tt.ptr>, tensor<3x2x!tt.ptr>, tensor<3x2xf32>, tensor<3x2xf32>) +// CHECK: tt.load {{.*}}, {{.*}}, {{.*}} : tensor<3x2x!tt.ptr> +// CHECK: tt.load {{.*}}, {{.*}}, {{.*}} : tensor<3x2x!tt.ptr> +// CHECK: arith.addf {{.*}} : tensor<3x2xf32> +// CHECK: arith.mulf {{.*}} : tensor<3x2xf32> +// CHECK: tt.assert {{.*}}, "preserve loop assertion" : i1 +// CHECK: tt.store {{.*}}, {{.*}}, {{.*}} : tensor<3x2x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}}, {{.*}} : tensor<3x2x!tt.ptr> +// CHECK: "tt.reduce"({{.*}}) <{axis = 0 : i32}> +// CHECK: (tensor<3x2xf32>) -> tensor<2xf32> +// POST-LABEL: tt.func @masked_dynamic_stride_loop( +// POST: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<3x1xi1> +// POST: tt.assert {{.*}}, "int32 overflow detected for operation add"{{.*}} : tensor<3x2xi1> +// POST: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<3x1xi1> +// POST: tt.assert {{.*}}, "int32 overflow detected for operation add"{{.*}} : tensor<3x2xi1> +// POST-NOT: tensor<1x3xi64> +// POST-NOT: tensor<2x3xi64> +// POST: tt.load {{.*}} : tensor<3x2x!tt.ptr> + +// Keep the old i64 *and* i1 layout shapes forbidden over the whole function, +// rather than only in the interval after the checked assertions. This makes +// a residual guard producer before the first assert observable to FileCheck. +// CLEAN-LABEL: tt.func @masked_dynamic_stride_loop( +// CLEAN-NOT: tensor<1x3xi64> +// CLEAN-NOT: tensor<2x3xi64> +// CLEAN-NOT: tensor<1x3xi1> +// CLEAN-NOT: tensor<2x3xi1> +// CLEAN-LABEL: tt.func @rank3_static_permutation_loop( +// POST-CLEAN-LABEL: tt.func @masked_dynamic_stride_loop( +// POST-CLEAN-NOT: tensor<1x3xi64> +// POST-CLEAN-NOT: tensor<2x3xi64> +// POST-CLEAN-NOT: tensor<1x3xi1> +// POST-CLEAN-NOT: tensor<2x3xi1> +// POST-CLEAN-LABEL: tt.func @rank3_static_permutation_loop( + +// CSE/canonicalize is optional after graph-optimize, but it must not leave a +// second layout DAG in this minimal closed component. Count each operation +// class in a dedicated FileCheck run because the three classes interleave. +// POST-EXPAND-LABEL: tt.func @masked_dynamic_stride_loop( +// POST-EXPAND-COUNT-2: tt.expand_dims +// POST-EXPAND-NOT: tt.expand_dims +// POST-EXPAND-LABEL: tt.func @rank3_static_permutation_loop( +// POST-SPLAT-LABEL: tt.func @masked_dynamic_stride_loop( +// POST-SPLAT-COUNT-9: tt.splat +// POST-SPLAT-NOT: tt.splat +// POST-SPLAT-LABEL: tt.func @rank3_static_permutation_loop( +// POST-MULI-LABEL: tt.func @masked_dynamic_stride_loop( +// POST-MULI-COUNT-2: arith.muli +// POST-MULI-NOT: arith.muli +// POST-MULI-LABEL: tt.func @rank3_static_permutation_loop( +tt.func @masked_dynamic_stride_loop(%src0: !tt.ptr, %src1: !tt.ptr, %dst0: !tt.ptr, %dst1: !tt.ptr, %out: !tt.ptr, %n: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %row = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %column = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %row_expand = tt.expand_dims %row {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %column_expand = tt.expand_dims %column {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %n_row = tt.splat %n : i32 -> tensor<1x3xi32> + %row_i64 = arith.extsi %row_expand : tensor<1x3xi32> to tensor<1x3xi64> + %n_i64 = arith.extsi %n : i32 to i64 + %n_i64_row = tt.splat %n_i64 : i64 -> tensor<1x3xi64> + %row_product_i64 = arith.muli %row_i64, %n_i64_row : tensor<1x3xi64> + %row_i64_max = arith.constant dense<2147483647> : tensor<1x3xi64> + %row_i64_min = arith.constant dense<-2147483648> : tensor<1x3xi64> + %row_product_le = arith.cmpi sle, %row_product_i64, %row_i64_max : tensor<1x3xi64> + %row_product_ge = arith.cmpi sge, %row_product_i64, %row_i64_min : tensor<1x3xi64> + %row_product_ok = arith.andi %row_product_le, %row_product_ge : tensor<1x3xi1> + tt.assert %row_product_ok, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : tensor<1x3xi1> + %row_scaled = arith.muli %row_expand, %n_row : tensor<1x3xi32> + %row_full = tt.broadcast %row_scaled : tensor<1x3xi32> -> tensor<2x3xi32> + %column_full = tt.broadcast %column_expand : tensor<2x1xi32> -> tensor<2x3xi32> + %row_full_i64 = arith.extsi %row_full : tensor<2x3xi32> to tensor<2x3xi64> + %column_full_i64 = arith.extsi %column_full : tensor<2x3xi32> to tensor<2x3xi64> + %offset_i64 = arith.addi %row_full_i64, %column_full_i64 : tensor<2x3xi64> + %offset_i64_max = arith.constant dense<2147483647> : tensor<2x3xi64> + %offset_i64_min = arith.constant dense<-2147483648> : tensor<2x3xi64> + %offset_le = arith.cmpi sle, %offset_i64, %offset_i64_max : tensor<2x3xi64> + %offset_ge = arith.cmpi sge, %offset_i64, %offset_i64_min : tensor<2x3xi64> + %offset_ok = arith.andi %offset_le, %offset_ge : tensor<2x3xi1> + tt.assert %offset_ok, "int32 overflow detected for operation add" {tt.auto_overflow_assert} : tensor<2x3xi1> + %offset = arith.addi %column_full, %row_full : tensor<2x3xi32> + %src0_base = tt.splat %src0 : !tt.ptr -> tensor<2x3x!tt.ptr> + %src1_base = tt.splat %src1 : !tt.ptr -> tensor<2x3x!tt.ptr> + %dst0_base = tt.splat %dst0 : !tt.ptr -> tensor<2x3x!tt.ptr> + %dst1_base = tt.splat %dst1 : !tt.ptr -> tensor<2x3x!tt.ptr> + %src0_ptr = tt.addptr %src0_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %src1_ptr = tt.addptr %src1_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %dst0_ptr = tt.addptr %dst0_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %dst1_ptr = tt.addptr %dst1_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + tt.assert %row_product_ok, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : tensor<1x3xi1> + tt.assert %offset_ok, "int32 overflow detected for operation add" {tt.auto_overflow_assert} : tensor<2x3xi1> + %n_column = tt.splat %n : i32 -> tensor<2x1xi32> + %column_mask = arith.cmpi slt, %column_expand, %n_column : tensor<2x1xi32> + %mask = tt.broadcast %column_mask : tensor<2x1xi1> -> tensor<2x3xi1> + %zero = arith.constant dense<0.000000e+00> : tensor<2x3xf32> + // A shared producer is a legal fork DAG, not a recursive cycle. The + // external-value preflight must admit it and LayoutValueCloner must reuse + // one natural-layout increment producer for both operands. + %delta_base = tt.splat %n : i32 -> tensor<2x3xi32> + %delta = arith.andi %delta_base, %delta_base : tensor<2x3xi32> + %true = arith.constant true + %loop:6 = scf.for %iv = %c0 to %c1 step %c1 iter_args(%p0 = %src0_ptr, %p1 = %src1_ptr, %p2 = %dst0_ptr, %p3 = %dst1_ptr, %acc0 = %zero, %acc1 = %zero) -> (tensor<2x3x!tt.ptr>, tensor<2x3x!tt.ptr>, tensor<2x3x!tt.ptr>, tensor<2x3x!tt.ptr>, tensor<2x3xf32>, tensor<2x3xf32>) : i32 { + %a = tt.load %p0, %mask, %zero : tensor<2x3x!tt.ptr> + %b = tt.load %p1, %mask, %zero : tensor<2x3x!tt.ptr> + %sum = arith.addf %a, %b : tensor<2x3xf32> + %product = arith.mulf %a, %b : tensor<2x3xf32> + tt.assert %true, "preserve loop assertion" : i1 + %next_acc0 = arith.addf %acc0, %sum : tensor<2x3xf32> + %next_acc1 = arith.addf %acc1, %product : tensor<2x3xf32> + tt.store %p2, %sum, %mask : tensor<2x3x!tt.ptr> + tt.store %p3, %product, %mask : tensor<2x3x!tt.ptr> + %next_p0 = tt.addptr %p0, %delta : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %next_p1 = tt.addptr %p1, %delta : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %next_p2 = tt.addptr %p2, %delta : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %next_p3 = tt.addptr %p3, %delta : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + scf.yield %next_p0, %next_p1, %next_p2, %next_p3, %next_acc0, %next_acc1 : tensor<2x3x!tt.ptr>, tensor<2x3x!tt.ptr>, tensor<2x3x!tt.ptr>, tensor<2x3x!tt.ptr>, tensor<2x3xf32>, tensor<2x3xf32> + } + %reduced = "tt.reduce"(%loop#4) <{axis = 1 : i32}> ({ + ^bb0(%lhs: f32, %rhs: f32): + %combined = arith.addf %lhs, %rhs : f32 + tt.reduce.return %combined : f32 + }) : (tensor<2x3xf32>) -> tensor<2xf32> + %out_base = tt.splat %out : !tt.ptr -> tensor<2x!tt.ptr> + %out_ptr = tt.addptr %out_base, %column : tensor<2x!tt.ptr>, tensor<2xi32> + %out_n = tt.splat %n : i32 -> tensor<2xi32> + %out_mask = arith.cmpi slt, %column, %out_n : tensor<2xi32> + tt.store %out_ptr, %reduced, %out_mask : tensor<2x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @rank3_static_permutation_loop( +// CHECK: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<3x1x1xi1> +// CHECK: scf.for {{.*}} -> (tensor<3x4x2x!tt.ptr>, tensor<3x4x2x!tt.ptr>) +// CHECK: tt.load {{.*}} : tensor<3x4x2x!tt.ptr> +// CHECK: tt.trans {{.*}} {order = array} : tensor<3x4x2xf32> -> tensor<3x2x4xf32> +// CHECK: tt.trans {{.*}} {order = array} : tensor<3x2x4xf32> -> tensor<3x4x2xf32> +// CHECK: tt.store {{.*}} : tensor<3x4x2x!tt.ptr> +tt.func @rank3_static_permutation_loop(%src: !tt.ptr, %dst: !tt.ptr) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis2 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %axis0_e1 = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis0_e2 = tt.expand_dims %axis0_e1 {axis = 2 : i32} : tensor<2x1xi32> -> tensor<2x1x1xi32> + %axis1_e0 = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %axis1_e2 = tt.expand_dims %axis1_e0 {axis = 2 : i32} : tensor<1x3xi32> -> tensor<1x3x1xi32> + %axis2_e0 = tt.expand_dims %axis2 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %axis2_e1 = tt.expand_dims %axis2_e0 {axis = 0 : i32} : tensor<1x4xi32> -> tensor<1x1x4xi32> + %one = arith.constant 1 : i32 + %two = arith.constant 2 : i32 + %eight = arith.constant 8 : i32 + %s0 = tt.splat %one : i32 -> tensor<2x1x1xi32> + %s1 = tt.splat %eight : i32 -> tensor<1x3x1xi32> + %s2 = tt.splat %two : i32 -> tensor<1x1x4xi32> + %t0 = arith.muli %axis0_e2, %s0 : tensor<2x1x1xi32> + %axis1_i64 = arith.extsi %axis1_e2 : tensor<1x3x1xi32> to tensor<1x3x1xi64> + %eight_i64 = arith.extsi %eight : i32 to i64 + %s1_i64 = tt.splat %eight_i64 : i64 -> tensor<1x3x1xi64> + %t1_i64 = arith.muli %axis1_i64, %s1_i64 : tensor<1x3x1xi64> + %t1_i64_max = arith.constant dense<2147483647> : tensor<1x3x1xi64> + %t1_i64_min = arith.constant dense<-2147483648> : tensor<1x3x1xi64> + %t1_le = arith.cmpi sle, %t1_i64, %t1_i64_max : tensor<1x3x1xi64> + %t1_ge = arith.cmpi sge, %t1_i64, %t1_i64_min : tensor<1x3x1xi64> + %t1_ok = arith.andi %t1_le, %t1_ge : tensor<1x3x1xi1> + tt.assert %t1_ok, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : tensor<1x3x1xi1> + %t1 = arith.muli %axis1_e2, %s1 : tensor<1x3x1xi32> + %t2 = arith.muli %axis2_e1, %s2 : tensor<1x1x4xi32> + %b0 = tt.broadcast %t0 : tensor<2x1x1xi32> -> tensor<2x3x4xi32> + %b1 = tt.broadcast %t1 : tensor<1x3x1xi32> -> tensor<2x3x4xi32> + %b2 = tt.broadcast %t2 : tensor<1x1x4xi32> -> tensor<2x3x4xi32> + %offset01 = arith.addi %b0, %b1 : tensor<2x3x4xi32> + %offset = arith.addi %offset01, %b2 : tensor<2x3x4xi32> + %src_base = tt.splat %src : !tt.ptr -> tensor<2x3x4x!tt.ptr> + %dst_base = tt.splat %dst : !tt.ptr -> tensor<2x3x4x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offset : tensor<2x3x4x!tt.ptr>, tensor<2x3x4xi32> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<2x3x4x!tt.ptr>, tensor<2x3x4xi32> + %zero = arith.constant dense<0> : tensor<2x3x4xi32> + %loop:2 = scf.for %iv = %c0 to %c1 step %c1 iter_args(%p0 = %src_ptr, %p1 = %dst_ptr) -> (tensor<2x3x4x!tt.ptr>, tensor<2x3x4x!tt.ptr>) : i32 { + %value = tt.load %p0 : tensor<2x3x4x!tt.ptr> + %transposed = tt.trans %value {order = array} : tensor<2x3x4xf32> -> tensor<4x3x2xf32> + %restored = tt.trans %transposed {order = array} : tensor<4x3x2xf32> -> tensor<2x3x4xf32> + %result = arith.negf %restored : tensor<2x3x4xf32> + tt.store %p1, %result : tensor<2x3x4x!tt.ptr> + %next_p0 = tt.addptr %p0, %zero : tensor<2x3x4x!tt.ptr>, tensor<2x3x4xi32> + %next_p1 = tt.addptr %p1, %zero : tensor<2x3x4x!tt.ptr>, tensor<2x3x4xi32> + scf.yield %next_p0, %next_p1 : tensor<2x3x4x!tt.ptr>, tensor<2x3x4x!tt.ptr> + } + tt.return +} + +// A rank-3 dynamic physical layout uses the canonical mixed-radix strides +// 3*N, N, 1. Only the innermost lane is runtime-bounded by N, but that is +// enough to prove the two adjacent capacity relations 3*N >= 3*N and +// N >= 1*N. The rule must therefore handle an N-dimensional symbolic proof, +// not a rank-2 special case. +// CHECK-LABEL: tt.func @rank3_dynamic_stride_block( +// CHECK: tt.load {{.*}} : tensor<2x3x4x!tt.ptr> +// CHECK: tt.store {{.*}} : tensor<2x3x4x!tt.ptr> +tt.func @rank3_dynamic_stride_block(%src: !tt.ptr, %dst: !tt.ptr, %n: i32) { + %axis0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis2 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis0_e1 = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %axis0_e2 = tt.expand_dims %axis0_e1 {axis = 2 : i32} : tensor<4x1xi32> -> tensor<4x1x1xi32> + %axis1_e0 = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %axis1_e2 = tt.expand_dims %axis1_e0 {axis = 2 : i32} : tensor<1x3xi32> -> tensor<1x3x1xi32> + %axis2_e0 = tt.expand_dims %axis2 {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> + %axis2_e1 = tt.expand_dims %axis2_e0 {axis = 1 : i32} : tensor<1x2xi32> -> tensor<1x1x2xi32> + %n_axis1 = tt.splat %n : i32 -> tensor<1x3x1xi32> + %axis1_scaled = arith.muli %axis1_e2, %n_axis1 : tensor<1x3x1xi32> + %three = arith.constant 3 : i32 + %three_n = arith.muli %three, %n : i32 + %three_n_axis2 = tt.splat %three_n : i32 -> tensor<1x1x2xi32> + %axis2_scaled = arith.muli %axis2_e1, %three_n_axis2 : tensor<1x1x2xi32> + %axis0_full = tt.broadcast %axis0_e2 : tensor<4x1x1xi32> -> tensor<4x3x2xi32> + %axis1_full = tt.broadcast %axis1_scaled : tensor<1x3x1xi32> -> tensor<4x3x2xi32> + %axis2_full = tt.broadcast %axis2_scaled : tensor<1x1x2xi32> -> tensor<4x3x2xi32> + %offset01 = arith.addi %axis0_full, %axis1_full : tensor<4x3x2xi32> + %offset = arith.addi %offset01, %axis2_full : tensor<4x3x2xi32> + %src_base = tt.splat %src : !tt.ptr -> tensor<4x3x2x!tt.ptr> + %dst_base = tt.splat %dst : !tt.ptr -> tensor<4x3x2x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offset : tensor<4x3x2x!tt.ptr>, tensor<4x3x2xi32> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<4x3x2x!tt.ptr>, tensor<4x3x2xi32> + %n_axis0 = tt.splat %n : i32 -> tensor<4x1x1xi32> + %axis0_mask = arith.cmpi slt, %axis0_e2, %n_axis0 : tensor<4x1x1xi32> + %mask = tt.broadcast %axis0_mask : tensor<4x1x1xi1> -> tensor<4x3x2xi1> + %value = tt.load %src_ptr, %mask : tensor<4x3x2x!tt.ptr> + %result = arith.negf %value : tensor<4x3x2xf32> + tt.store %dst_ptr, %result, %mask : tensor<4x3x2x!tt.ptr> + tt.return +} + +// The same closed component may live directly in a function block. Its +// pointer/mask producer DAG is cloned locally, while the anchor remains one +// exact Block and does not require a synthetic scf.for port. +// CHECK-LABEL: tt.func @plain_block_component( +// CHECK: tt.load {{.*}} : tensor<3x2x!tt.ptr> +// CHECK: tt.store {{.*}} : tensor<3x2x!tt.ptr> +tt.func @plain_block_component(%src: !tt.ptr, %dst: !tt.ptr) { + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis0_expand = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis1_expand = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %two = arith.constant 2 : i32 + %axis1_scale = tt.splat %two : i32 -> tensor<1x3xi32> + %axis1_scaled = arith.muli %axis1_expand, %axis1_scale : tensor<1x3xi32> + %axis0_full = tt.broadcast %axis0_expand : tensor<2x1xi32> -> tensor<2x3xi32> + %axis1_full = tt.broadcast %axis1_scaled : tensor<1x3xi32> -> tensor<2x3xi32> + %offset = arith.addi %axis0_full, %axis1_full : tensor<2x3xi32> + %src_base = tt.splat %src : !tt.ptr -> tensor<2x3x!tt.ptr> + %dst_base = tt.splat %dst : !tt.ptr -> tensor<2x3x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %value = tt.load %src_ptr : tensor<2x3x!tt.ptr> + %result = arith.negf %value : tensor<2x3xf32> + tt.store %dst_ptr, %result : tensor<2x3x!tt.ptr> + tt.return +} + +// A branch-local closure must also be eligible. No result crosses the if +// port; when a layout value would reach scf.yield, the rule rejects rather +// than changing one branch's result type. +// CHECK-LABEL: tt.func @if_local_block_component( +// CHECK: scf.if {{.*}} { +// CHECK: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<3x1xi1> +// CHECK: tt.load {{.*}} : tensor<3x2x!tt.ptr> +// CHECK: tt.store {{.*}} : tensor<3x2x!tt.ptr> +tt.func @if_local_block_component(%src: !tt.ptr, %dst: !tt.ptr, %condition: i1) { + scf.if %condition { + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis0_expand = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis1_expand = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %two = arith.constant 2 : i32 + %axis1_scale = tt.splat %two : i32 -> tensor<1x3xi32> + %axis1_scaled = arith.muli %axis1_expand, %axis1_scale : tensor<1x3xi32> + %axis1_scaled_i64 = arith.extsi %axis1_scaled : tensor<1x3xi32> to tensor<1x3xi64> + %axis1_max = arith.constant dense<2147483647> : tensor<1x3xi64> + %axis1_min = arith.constant dense<-2147483648> : tensor<1x3xi64> + %axis1_le = arith.cmpi sle, %axis1_scaled_i64, %axis1_max : tensor<1x3xi64> + %axis1_ge = arith.cmpi sge, %axis1_scaled_i64, %axis1_min : tensor<1x3xi64> + %axis1_ok = arith.andi %axis1_le, %axis1_ge : tensor<1x3xi1> + tt.assert %axis1_ok, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : tensor<1x3xi1> + %axis0_full = tt.broadcast %axis0_expand : tensor<2x1xi32> -> tensor<2x3xi32> + %axis1_full = tt.broadcast %axis1_scaled : tensor<1x3xi32> -> tensor<2x3xi32> + %offset = arith.addi %axis0_full, %axis1_full : tensor<2x3xi32> + %src_base = tt.splat %src : !tt.ptr -> tensor<2x3x!tt.ptr> + %dst_base = tt.splat %dst : !tt.ptr -> tensor<2x3x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %value = tt.load %src_ptr : tensor<2x3x!tt.ptr> + %result = arith.negf %value : tensor<2x3xf32> + tt.store %dst_ptr, %result : tensor<2x3x!tt.ptr> + } else { + } + tt.return +} + +// The generic Block path also applies inside a while body when the entire +// pointer/guard/load/store closure is materialized in that one body Block. +// The scalar induction variable crosses the Region port, but no rank-2 layout +// value does. +// CHECK-LABEL: tt.func @while_local_block_component( +// CHECK: scf.while +// CHECK: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<3x1xi1> +// CHECK: tt.load {{.*}} : tensor<3x2x!tt.ptr> +// CHECK: tt.store {{.*}} : tensor<3x2x!tt.ptr> +tt.func @while_local_block_component(%src: !tt.ptr, %dst: !tt.ptr) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %loop:1 = scf.while (%iter = %c0) : (i32) -> (i32) { + %condition = arith.cmpi slt, %iter, %c1 : i32 + scf.condition(%condition) %iter : i32 + } do { + ^bb0(%iter: i32): + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis0_expand = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis1_expand = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %two = arith.constant 2 : i32 + %axis1_scale = tt.splat %two : i32 -> tensor<1x3xi32> + %axis1_scaled = arith.muli %axis1_expand, %axis1_scale : tensor<1x3xi32> + %axis1_scaled_i64 = arith.extsi %axis1_scaled : tensor<1x3xi32> to tensor<1x3xi64> + %axis1_max = arith.constant dense<2147483647> : tensor<1x3xi64> + %axis1_min = arith.constant dense<-2147483648> : tensor<1x3xi64> + %axis1_le = arith.cmpi sle, %axis1_scaled_i64, %axis1_max : tensor<1x3xi64> + %axis1_ge = arith.cmpi sge, %axis1_scaled_i64, %axis1_min : tensor<1x3xi64> + %axis1_ok = arith.andi %axis1_le, %axis1_ge : tensor<1x3xi1> + tt.assert %axis1_ok, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : tensor<1x3xi1> + %axis0_full = tt.broadcast %axis0_expand : tensor<2x1xi32> -> tensor<2x3xi32> + %axis1_full = tt.broadcast %axis1_scaled : tensor<1x3xi32> -> tensor<2x3xi32> + %offset = arith.addi %axis0_full, %axis1_full : tensor<2x3xi32> + %src_base = tt.splat %src : !tt.ptr -> tensor<2x3x!tt.ptr> + %dst_base = tt.splat %dst : !tt.ptr -> tensor<2x3x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %value = tt.load %src_ptr : tensor<2x3x!tt.ptr> + %result = arith.negf %value : tensor<2x3xf32> + tt.store %dst_ptr, %result : tensor<2x3x!tt.ptr> + %next = arith.addi %iter, %c1 : i32 + scf.yield %next : i32 + } + tt.return +} + +// The same Block rule must fail closed when its transformed value would leave +// through an if result. This is intentionally not a cross-branch rewrite. +// CHECK-LABEL: tt.func @if_port_escape_is_rejected( +// CHECK-NOT: tensor<3x2x!tt.ptr> +// CHECK: tt.load {{.*}} : tensor<2x3x!tt.ptr> +// CHECK: tt.store {{.*}} : tensor<2x3x!tt.ptr> +// CHECK: tt.return +tt.func @if_port_escape_is_rejected(%src: !tt.ptr, %dst: !tt.ptr, %condition: i1) { + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis0_expand = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis1_expand = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %two = arith.constant 2 : i32 + %axis1_scale = tt.splat %two : i32 -> tensor<1x3xi32> + %axis1_scaled = arith.muli %axis1_expand, %axis1_scale : tensor<1x3xi32> + %axis0_full = tt.broadcast %axis0_expand : tensor<2x1xi32> -> tensor<2x3xi32> + %axis1_full = tt.broadcast %axis1_scaled : tensor<1x3xi32> -> tensor<2x3xi32> + %offset = arith.addi %axis0_full, %axis1_full : tensor<2x3xi32> + %src_base = tt.splat %src : !tt.ptr -> tensor<2x3x!tt.ptr> + %dst_base = tt.splat %dst : !tt.ptr -> tensor<2x3x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %if_result = scf.if %condition -> (tensor<2x3xf32>) { + %value = tt.load %src_ptr : tensor<2x3x!tt.ptr> + %result = arith.negf %value : tensor<2x3xf32> + tt.store %dst_ptr, %result : tensor<2x3x!tt.ptr> + scf.yield %result : tensor<2x3xf32> + } else { + %zero = arith.constant dense<0.000000e+00> : tensor<2x3xf32> + scf.yield %zero : tensor<2x3xf32> + } + tt.store %dst_ptr, %if_result : tensor<2x3x!tt.ptr> + tt.return +} + +// A user-authored assertion is not an overflow port, even if it deliberately +// uses the same message. The frontend-only marker distinguishes it from the +// preceding automatic assertion. Because it observes a value in the old +// layout closure, the entire candidate must fail closed instead of silently +// transposing that user assertion. +// CHECK-LABEL: tt.func @user_assert_guard_escape_is_rejected( +// CHECK-COUNT-2: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<1x3xi1> +// CHECK-NOT: tensor<3x2x!tt.ptr> +// CHECK: tt.load {{.*}} : tensor<2x3x!tt.ptr> +// CHECK: tt.store {{.*}} : tensor<2x3x!tt.ptr> +tt.func @user_assert_guard_escape_is_rejected(%src: !tt.ptr, %dst: !tt.ptr) { + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis0_expand = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis1_expand = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %two = arith.constant 2 : i32 + %axis1_scale = tt.splat %two : i32 -> tensor<1x3xi32> + %axis1_scaled = arith.muli %axis1_expand, %axis1_scale : tensor<1x3xi32> + %axis1_scaled_i64 = arith.extsi %axis1_scaled : tensor<1x3xi32> to tensor<1x3xi64> + %axis1_max = arith.constant dense<2147483647> : tensor<1x3xi64> + %axis1_min = arith.constant dense<-2147483648> : tensor<1x3xi64> + %axis1_le = arith.cmpi sle, %axis1_scaled_i64, %axis1_max : tensor<1x3xi64> + %axis1_ge = arith.cmpi sge, %axis1_scaled_i64, %axis1_min : tensor<1x3xi64> + %axis1_ok = arith.andi %axis1_le, %axis1_ge : tensor<1x3xi1> + tt.assert %axis1_ok, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : tensor<1x3xi1> + tt.assert %axis1_ok, "int32 overflow detected for operation mul" : tensor<1x3xi1> + %axis0_full = tt.broadcast %axis0_expand : tensor<2x1xi32> -> tensor<2x3xi32> + %axis1_full = tt.broadcast %axis1_scaled : tensor<1x3xi32> -> tensor<2x3xi32> + %offset = arith.addi %axis0_full, %axis1_full : tensor<2x3xi32> + %src_base = tt.splat %src : !tt.ptr -> tensor<2x3x!tt.ptr> + %dst_base = tt.splat %dst : !tt.ptr -> tensor<2x3x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %value = tt.load %src_ptr : tensor<2x3x!tt.ptr> + %result = arith.negf %value : tensor<2x3xf32> + tt.store %dst_ptr, %result : tensor<2x3x!tt.ptr> + tt.return +} + +// The automatic guard itself is eligible, but this pointer/guard closure +// originates outside the branch that owns the load/store endpoint. Crossing +// a Region boundary would require a new port, so the block candidate is +// intentionally rejected. +// CHECK-LABEL: tt.func @cross_region_guard_is_rejected( +// CHECK: tt.assert {{.*}}, "int32 overflow detected for operation mul"{{.*}} : tensor<1x3xi1> +// CHECK: scf.if {{.*}} { +// CHECK-NOT: tensor<3x2x!tt.ptr> +// CHECK: tt.load {{.*}} : tensor<2x3x!tt.ptr> +// CHECK: tt.store {{.*}} : tensor<2x3x!tt.ptr> +tt.func @cross_region_guard_is_rejected(%src: !tt.ptr, %dst: !tt.ptr, %condition: i1) { + %axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %axis0_expand = tt.expand_dims %axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %axis1_expand = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %two = arith.constant 2 : i32 + %axis1_scale = tt.splat %two : i32 -> tensor<1x3xi32> + %axis1_scaled = arith.muli %axis1_expand, %axis1_scale : tensor<1x3xi32> + %axis1_scaled_i64 = arith.extsi %axis1_scaled : tensor<1x3xi32> to tensor<1x3xi64> + %axis1_max = arith.constant dense<2147483647> : tensor<1x3xi64> + %axis1_min = arith.constant dense<-2147483648> : tensor<1x3xi64> + %axis1_le = arith.cmpi sle, %axis1_scaled_i64, %axis1_max : tensor<1x3xi64> + %axis1_ge = arith.cmpi sge, %axis1_scaled_i64, %axis1_min : tensor<1x3xi64> + %axis1_ok = arith.andi %axis1_le, %axis1_ge : tensor<1x3xi1> + tt.assert %axis1_ok, "int32 overflow detected for operation mul" {tt.auto_overflow_assert} : tensor<1x3xi1> + %axis0_full = tt.broadcast %axis0_expand : tensor<2x1xi32> -> tensor<2x3xi32> + %axis1_full = tt.broadcast %axis1_scaled : tensor<1x3xi32> -> tensor<2x3xi32> + %offset = arith.addi %axis0_full, %axis1_full : tensor<2x3xi32> + %src_base = tt.splat %src : !tt.ptr -> tensor<2x3x!tt.ptr> + %dst_base = tt.splat %dst : !tt.ptr -> tensor<2x3x!tt.ptr> + %src_ptr = tt.addptr %src_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + %dst_ptr = tt.addptr %dst_base, %offset : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + scf.if %condition { + %value = tt.load %src_ptr : tensor<2x3x!tt.ptr> + %result = arith.negf %value : tensor<2x3xf32> + tt.store %dst_ptr, %result : tensor<2x3x!tt.ptr> + } else { + } + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-store-coalescing.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-store-coalescing.mlir new file mode 100644 index 0000000000..17a3d74d44 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-store-coalescing.mlir @@ -0,0 +1,680 @@ +// RUN: triton-opt %s --verify-each -graph-optimize='rule-mask=4 ub-capacity-bytes=64' -o - | FileCheck %s --check-prefix=CHECK +// RUN: triton-opt %s --verify-each -graph-optimize='rule-mask=4 ub-capacity-bytes=0' -o - | FileCheck %s --check-prefix=CAP0 +// RUN: triton-opt %s --verify-each -graph-optimize='rule-mask=4 ub-capacity-bytes=256' -o - | FileCheck %s --check-prefix=DYNAMIC +// RUN: triton-opt %s --verify-each -graph-optimize='rule-mask=4 ub-capacity-bytes=64 compile-mode=simt_only' -o - | FileCheck %s --check-prefix=PURE-SIMT + +// Address order, rather than program order, determines the packed value +// layout. The high interval is deliberately stored first. The replacement +// must still be anchored at the program-order last store (the low interval), +// after both source values dominate that anchor. +// CHECK-LABEL: tt.func @pack_reverse_program_order( +// CHECK-NOT: tt.store +// CHECK: %[[EMPTY:.*]] = tensor.empty() : tensor<8xi32> +// CHECK-NEXT: %[[LOW:.*]] = tensor.insert_slice %{{.*}} into %[[EMPTY]][0] [4] [1] : tensor<4xi32> into tensor<8xi32> +// CHECK-NEXT: %[[PACKED:.*]] = tensor.insert_slice %{{.*}} into %[[LOW]][4] [4] [1] : tensor<4xi32> into tensor<8xi32> +// CHECK-NEXT: %[[RANGE:.*]] = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> +// CHECK-NEXT: %[[SPLAT:.*]] = tt.splat %{{.*}} : !tt.ptr -> tensor<8x!tt.ptr> +// CHECK-NEXT: %[[ADDRESS:.*]] = tt.addptr %[[SPLAT]], %[[RANGE]] : tensor<8x!tt.ptr>, tensor<8xi32> +// CHECK-NEXT: tt.store %[[ADDRESS]], %[[PACKED]] : tensor<8x!tt.ptr> +// CHECK-NOT: tt.store +// CHECK-NOT: tensor.insert_slice +tt.func @pack_reverse_program_order(%base: !tt.ptr) { + %low_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %high_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %low_addresses = tt.addptr %base_splat, %low_range : tensor<4x!tt.ptr>, tensor<4xi32> + %high_addresses = tt.addptr %base_splat, %high_range : tensor<4x!tt.ptr>, tensor<4xi32> + %low_value = arith.constant dense<1> : tensor<4xi32> + %high_value = arith.constant dense<2> : tensor<4xi32> + tt.store %high_addresses, %high_value : tensor<4x!tt.ptr> + tt.store %low_addresses, %low_value : tensor<4x!tt.ptr> + tt.return +} + +// PURE-SIMT-LABEL: tt.func @pack_reverse_program_order( +// PURE-SIMT-NOT: tensor.empty +// PURE-SIMT: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// PURE-SIMT: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// PURE-SIMT-NOT: tensor.empty +// PURE-SIMT-LABEL: tt.func @pack_rank2_row_major_nonzero_origin( + +// N-D values may be flattened only when their logical row-major order is +// exactly address order. This fixture also uses a nonzero row origin for the +// second tile and stores that high tile first in program order. +// CHECK-LABEL: tt.func @pack_rank2_row_major_nonzero_origin( +// CHECK: %[[LOW_VALUE:.*]] = arith.constant dense<1> : tensor<2x3xi32> +// CHECK-NEXT: %[[HIGH_VALUE:.*]] = arith.constant dense<2> : tensor<2x3xi32> +// CHECK-NOT: tt.store +// CHECK: %[[EMPTY2D:.*]] = tensor.empty() : tensor<12xi32> +// CHECK-NEXT: %[[LOW_FLAT:.*]] = tt.reshape %[[LOW_VALUE]] : tensor<2x3xi32> -> tensor<6xi32> +// CHECK-NEXT: %[[LOW_PACKED:.*]] = tensor.insert_slice %[[LOW_FLAT]] into %[[EMPTY2D]][0] [6] [1] : tensor<6xi32> into tensor<12xi32> +// CHECK-NEXT: %[[HIGH_FLAT:.*]] = tt.reshape %[[HIGH_VALUE]] : tensor<2x3xi32> -> tensor<6xi32> +// CHECK-NEXT: %[[PACKED2D:.*]] = tensor.insert_slice %[[HIGH_FLAT]] into %[[LOW_PACKED]][6] [6] [1] : tensor<6xi32> into tensor<12xi32> +// CHECK-NEXT: %[[RANGE2D:.*]] = tt.make_range {end = 12 : i32, start = 0 : i32} : tensor<12xi32> +// CHECK-NEXT: %[[SPLAT2D:.*]] = tt.splat {{%.*}} : !tt.ptr -> tensor<12x!tt.ptr> +// CHECK-NEXT: %[[ADDRESSES2D:.*]] = tt.addptr %[[SPLAT2D]], %[[RANGE2D]] : tensor<12x!tt.ptr>, tensor<12xi32> +// CHECK-NEXT: tt.store %[[ADDRESSES2D]], %[[PACKED2D]] : tensor<12x!tt.ptr> +tt.func @pack_rank2_row_major_nonzero_origin(%base: !tt.ptr) { + %low_row = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %low_column = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %high_row = tt.make_range {end = 4 : i32, start = 2 : i32} : tensor<2xi32> + %high_column = tt.make_range {end = 3 : i32, start = 0 : i32} : tensor<3xi32> + %three = arith.constant 3 : i32 + %one = arith.constant 1 : i32 + + %low_row_expanded = tt.expand_dims %low_row {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %low_column_expanded = tt.expand_dims %low_column {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %low_row_stride = tt.splat %three : i32 -> tensor<2x1xi32> + %low_column_stride = tt.splat %one : i32 -> tensor<1x3xi32> + %low_row_term = arith.muli %low_row_expanded, %low_row_stride : tensor<2x1xi32> + %low_column_term = arith.muli %low_column_expanded, %low_column_stride : tensor<1x3xi32> + %low_row_broadcast = tt.broadcast %low_row_term : tensor<2x1xi32> -> tensor<2x3xi32> + %low_column_broadcast = tt.broadcast %low_column_term : tensor<1x3xi32> -> tensor<2x3xi32> + %low_offsets = arith.addi %low_row_broadcast, %low_column_broadcast : tensor<2x3xi32> + %low_base = tt.splat %base : !tt.ptr -> tensor<2x3x!tt.ptr> + %low_addresses = tt.addptr %low_base, %low_offsets : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + + %high_row_expanded = tt.expand_dims %high_row {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %high_column_expanded = tt.expand_dims %high_column {axis = 0 : i32} : tensor<3xi32> -> tensor<1x3xi32> + %high_row_stride = tt.splat %three : i32 -> tensor<2x1xi32> + %high_column_stride = tt.splat %one : i32 -> tensor<1x3xi32> + %high_row_term = arith.muli %high_row_expanded, %high_row_stride : tensor<2x1xi32> + %high_column_term = arith.muli %high_column_expanded, %high_column_stride : tensor<1x3xi32> + %high_row_broadcast = tt.broadcast %high_row_term : tensor<2x1xi32> -> tensor<2x3xi32> + %high_column_broadcast = tt.broadcast %high_column_term : tensor<1x3xi32> -> tensor<2x3xi32> + %high_offsets = arith.addi %high_row_broadcast, %high_column_broadcast : tensor<2x3xi32> + %high_base = tt.splat %base : !tt.ptr -> tensor<2x3x!tt.ptr> + %high_addresses = tt.addptr %high_base, %high_offsets : tensor<2x3x!tt.ptr>, tensor<2x3xi32> + + %low_value = arith.constant dense<1> : tensor<2x3xi32> + %high_value = arith.constant dense<2> : tensor<2x3xi32> + tt.store %high_addresses, %high_value : tensor<2x3x!tt.ptr> + tt.store %low_addresses, %low_value : tensor<2x3x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @pack_rank3_row_major( +// CHECK-NOT: tt.store +// CHECK: tensor.empty() : tensor<16xi32> +// CHECK: tt.reshape {{%.*}} : tensor<2x2x2xi32> -> tensor<8xi32> +// CHECK: tt.reshape {{%.*}} : tensor<2x2x2xi32> -> tensor<8xi32> +// CHECK: tt.store {{%.*}}, {{%.*}} : tensor<16x!tt.ptr> +tt.func @pack_rank3_row_major(%base: !tt.ptr) { + %low_axis0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %high_axis0 = tt.make_range {end = 4 : i32, start = 2 : i32} : tensor<2xi32> + %axis1 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %axis2 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %four = arith.constant 4 : i32 + %two = arith.constant 2 : i32 + %one = arith.constant 1 : i32 + + %low_axis0_e1 = tt.expand_dims %low_axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %low_axis0_e2 = tt.expand_dims %low_axis0_e1 {axis = 2 : i32} : tensor<2x1xi32> -> tensor<2x1x1xi32> + %high_axis0_e1 = tt.expand_dims %high_axis0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %high_axis0_e2 = tt.expand_dims %high_axis0_e1 {axis = 2 : i32} : tensor<2x1xi32> -> tensor<2x1x1xi32> + %axis1_e1 = tt.expand_dims %axis1 {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> + %axis1_e2 = tt.expand_dims %axis1_e1 {axis = 2 : i32} : tensor<1x2xi32> -> tensor<1x2x1xi32> + %axis2_e1 = tt.expand_dims %axis2 {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> + %axis2_e2 = tt.expand_dims %axis2_e1 {axis = 0 : i32} : tensor<1x2xi32> -> tensor<1x1x2xi32> + %stride0 = tt.splat %four : i32 -> tensor<2x1x1xi32> + %stride1 = tt.splat %two : i32 -> tensor<1x2x1xi32> + %stride2 = tt.splat %one : i32 -> tensor<1x1x2xi32> + %low_term0 = arith.muli %low_axis0_e2, %stride0 : tensor<2x1x1xi32> + %high_term0 = arith.muli %high_axis0_e2, %stride0 : tensor<2x1x1xi32> + %term1 = arith.muli %axis1_e2, %stride1 : tensor<1x2x1xi32> + %term2 = arith.muli %axis2_e2, %stride2 : tensor<1x1x2xi32> + %low_term0_b = tt.broadcast %low_term0 : tensor<2x1x1xi32> -> tensor<2x2x2xi32> + %high_term0_b = tt.broadcast %high_term0 : tensor<2x1x1xi32> -> tensor<2x2x2xi32> + %term1_b = tt.broadcast %term1 : tensor<1x2x1xi32> -> tensor<2x2x2xi32> + %term2_b = tt.broadcast %term2 : tensor<1x1x2xi32> -> tensor<2x2x2xi32> + %low_partial = arith.addi %low_term0_b, %term1_b : tensor<2x2x2xi32> + %high_partial = arith.addi %high_term0_b, %term1_b : tensor<2x2x2xi32> + %low_offsets = arith.addi %low_partial, %term2_b : tensor<2x2x2xi32> + %high_offsets = arith.addi %high_partial, %term2_b : tensor<2x2x2xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<2x2x2x!tt.ptr> + %low_addresses = tt.addptr %base_splat, %low_offsets : tensor<2x2x2x!tt.ptr>, tensor<2x2x2xi32> + %high_addresses = tt.addptr %base_splat, %high_offsets : tensor<2x2x2x!tt.ptr>, tensor<2x2x2xi32> + %low_value = arith.constant dense<1> : tensor<2x2x2xi32> + %high_value = arith.constant dense<2> : tensor<2x2x2xi32> + tt.store %high_addresses, %high_value : tensor<2x2x2x!tt.ptr> + tt.store %low_addresses, %low_value : tensor<2x2x2x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @reject_gap( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_gap(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 9 : i32, start = 5 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// Any overlap poisons the entire bucket, rather than merely being skipped. +// CHECK-LABEL: tt.func @reject_overlap( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_overlap(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 6 : i32, start = 2 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @reject_mask( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_mask(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %mask = arith.constant dense : tensor<4xi1> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value, %mask : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// i1 has no byte-addressable element width, so capacity accounting must not +// round it up and pack the stores. +// CHECK-LABEL: tt.func @reject_i1_element( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_i1_element(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense : tensor<4xi1> + %second_value = arith.constant dense : tensor<4xi1> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// Complete store attributes must match; cache policy alone makes these two +// stores distinct buckets. +// CHECK-LABEL: tt.func @reject_different_cache( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} cacheModifier = ca : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} cacheModifier = cg : tensor<4x!tt.ptr> +tt.func @reject_different_cache(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value cacheModifier = ca : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value cacheModifier = cg : tensor<4x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @reject_different_evict( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} evictionPolicy = evict_first : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} evictionPolicy = evict_last : tensor<4x!tt.ptr> +tt.func @reject_different_evict(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value evictionPolicy = evict_first : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value evictionPolicy = evict_last : tensor<4x!tt.ptr> + tt.return +} + +// Exact SSA base identity, not an alias guess, is required for a bucket. +// CHECK-LABEL: tt.func @reject_different_base( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_different_base(%first_base: !tt.ptr, %second_base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %first_splat = tt.splat %first_base : !tt.ptr -> tensor<4x!tt.ptr> + %second_splat = tt.splat %second_base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %first_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %second_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// The middle load touches only the suffix store's interval. The first store +// is delayed across the load, but their intervals are disjoint, so packing is +// legal. This rejects an implementation that protects the whole run instead +// of only the delayed program-order prefix. +// CAP0-LABEL: tt.func @pack_suffix_load_between_stores( +// CAP0-NOT: tensor.empty +// CAP0-NOT: tensor.insert_slice +// CAP0: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CAP0: tt.load {{.*}} : tensor<4x!tt.ptr> +// CAP0: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK-LABEL: tt.func @pack_suffix_load_between_stores( +// CHECK-NOT: tt.store +// CHECK: tt.load %{{.*}} : tensor<4x!tt.ptr> +// CHECK-NOT: tt.store +// CHECK: tensor.empty() : tensor<8xi32> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<8x!tt.ptr> +// CHECK-NOT: tt.store +tt.func @pack_suffix_load_between_stores(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + %suffix_load = tt.load %second_addresses : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// Address order is low/high, while program order is high/low. The side store +// overlaps only the suffix low store and has a different cache attribute so it +// remains an intervening effect instead of joining the planned-store bucket. +// CHECK-LABEL: tt.func @pack_reverse_program_order_with_suffix_side_store( +// CHECK-NOT: tt.store +// CHECK: tt.store {{.*}}, {{.*}} cacheModifier = ca : tensor<4x!tt.ptr> +// CHECK-NOT: tt.store +// CHECK: tensor.empty() : tensor<8xi32> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<8x!tt.ptr> +// CHECK-NOT: tt.store +tt.func @pack_reverse_program_order_with_suffix_side_store(%base: !tt.ptr) { + %low_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %high_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %low_addresses = tt.addptr %base_splat, %low_range : tensor<4x!tt.ptr>, tensor<4xi32> + %high_addresses = tt.addptr %base_splat, %high_range : tensor<4x!tt.ptr>, tensor<4xi32> + %low_value = arith.constant dense<1> : tensor<4xi32> + %high_value = arith.constant dense<2> : tensor<4xi32> + %side_value = arith.constant dense<3> : tensor<4xi32> + tt.store %high_addresses, %high_value : tensor<4x!tt.ptr> + tt.store %low_addresses, %side_value cacheModifier = ca : tensor<4x!tt.ptr> + tt.store %low_addresses, %low_value : tensor<4x!tt.ptr> + tt.return +} + +// The side store overlaps the delayed prefix store, so packing must remain +// rejected even though it uses a distinct cache-attribute bucket. +// CHECK-LABEL: tt.func @reject_prefix_overlapping_side_store( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} cacheModifier = ca : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_prefix_overlapping_side_store(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + %side_value = arith.constant dense<3> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %first_addresses, %side_value cacheModifier = ca : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// Each gap gets its own delayed prefix: both loads touch only future stores, +// so the three planned stores can be packed together. +// CHECK-LABEL: tt.func @pack_three_stores_with_suffix_loads( +// CHECK-NOT: tt.store +// CHECK: tt.load %{{.*}} : tensor<4x!tt.ptr> +// CHECK-NOT: tt.store +// CHECK: tt.load %{{.*}} : tensor<4x!tt.ptr> +// CHECK-NOT: tt.store +// CHECK: tensor.empty() : tensor<12xi32> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<12x!tt.ptr> +// CHECK-NOT: tt.store +tt.func @pack_three_stores_with_suffix_loads(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %third_range = tt.make_range {end = 12 : i32, start = 8 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %third_addresses = tt.addptr %base_splat, %third_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + %third_value = arith.constant dense<3> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + %second_load = tt.load %second_addresses : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + %third_load = tt.load %third_addresses : tensor<4x!tt.ptr> + tt.store %third_addresses, %third_value : tensor<4x!tt.ptr> + tt.return +} + +// In the second gap, the load overlaps S0 rather than its immediate +// predecessor S1. The full delayed prefix must be protected, so no packing +// is legal. +// CHECK-LABEL: tt.func @reject_earlier_prefix_overlap_in_three_store_run( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.load {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_earlier_prefix_overlap_in_three_store_run(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %third_range = tt.make_range {end = 12 : i32, start = 8 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %third_addresses = tt.addptr %base_splat, %third_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + %third_value = arith.constant dense<3> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + %first_load = tt.load %first_addresses : tensor<4x!tt.ptr> + tt.store %third_addresses, %third_value : tensor<4x!tt.ptr> + tt.return +} + +// This load overlaps the delayed prefix S1, so it cannot be crossed by the +// packed store. +// CHECK-LABEL: tt.func @reject_load_between_stores( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.load {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_load_between_stores(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + %unused = tt.load %first_addresses : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// A distinct entry root does not conflict with the delayed output store under +// the StoreCoalescing ABI contract. The side store remains in program order, +// and the two %base stores pack at the final anchor. +// CHECK-LABEL: tt.func @pack_distinct_root_store_between_stores( +// CHECK-NOT: tt.store +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK-NOT: tt.store +// CHECK: tensor.empty() : tensor<8xi32> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<8x!tt.ptr> +// CHECK-NOT: tt.store +tt.func @pack_distinct_root_store_between_stores(%base: !tt.ptr, %other_base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %other_splat = tt.splat %other_base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %other_addresses = tt.addptr %other_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + %other_value = arith.constant dense<3> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %other_addresses, %other_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// Distinct roots answer only the alias question. A masked intervening load +// remains outside the direct, unpredicated access subset and must block the +// rewrite. +// CHECK-LABEL: tt.func @reject_distinct_root_masked_load_between_stores( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.load {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_distinct_root_masked_load_between_stores( + %base: !tt.ptr, %other_base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %other_splat = tt.splat %other_base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %other_addresses = tt.addptr %other_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + %mask = arith.constant dense : tensor<4xi1> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + %masked_load = tt.load %other_addresses, %mask : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// An arbitrary integer-to-pointer conversion has no tracked entry-argument +// provenance. It must remain Unknown rather than being treated as a distinct +// no-alias root, so the intervening load blocks the rewrite. +// CHECK-LABEL: tt.func @reject_unknown_root_load_between_stores( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.int_to_ptr {{.*}} : i64 -> !tt.ptr +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.load {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_unknown_root_load_between_stores(%base: !tt.ptr, + %address: i64) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %unknown_base = tt.int_to_ptr %address : i64 -> !tt.ptr + %unknown_splat = tt.splat %unknown_base : !tt.ptr -> tensor<4x!tt.ptr> + %unknown_addresses = tt.addptr %unknown_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + %unknown_load = tt.load %unknown_addresses : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @reject_barrier_between_stores( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: gpu.barrier +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_barrier_between_stores(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + gpu.barrier + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// A valid direct-body same-block value defined before the anchor naturally +// dominates it. The matcher still checks that condition defensively during +// discovery and revalidation. This nested-region fixture exercises the +// accompanying direct tt.func body/same-block gate: it must remain untouched. +// CHECK-LABEL: tt.func @reject_nested_region_stores( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_nested_region_stores(%base: !tt.ptr, %condition: i1) { + scf.if %condition { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + } + tt.return +} + +// CAP0-LABEL: tt.func @reject_capacity_zero( +// CAP0-NOT: tensor.empty +// CAP0-NOT: tensor.insert_slice +// CAP0: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CAP0: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_capacity_zero(%base: !tt.ptr) { + %first_range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %second_range = tt.make_range {end = 8 : i32, start = 4 : i32} : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_range : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_range : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// The two output stores share one dynamic origin. Both the scalar scale load +// and the dynamically indexed input load use distinct entry roots, so they +// may remain between the delayed K store and final packed store. +// CAP0-LABEL: tt.func @pack_dynamic_origin_distinct_entry_loads( +// CAP0-NOT: tensor.empty +// CAP0-NOT: tensor.insert_slice +// CAP0: tt.store {{.*}}, {{.*}} : tensor<64x!tt.ptr> +// CAP0: tt.load {{.*}} : !tt.ptr +// CAP0: tt.load {{.*}} : tensor<64x!tt.ptr> +// CAP0: tt.store {{.*}}, {{.*}} : tensor<64x!tt.ptr> +// DYNAMIC: tt.func @pack_dynamic_origin_distinct_entry_loads(%{{.*}}: !tt.ptr, %{{.*}}: !tt.ptr, %[[OUT_ARG:.*]]: !tt.ptr, %[[ORIGIN_ARG:.*]]: i32) { +// DYNAMIC-NOT: tt.store +// DYNAMIC: tt.load {{.*}} : !tt.ptr +// DYNAMIC: tt.load {{.*}} : tensor<64x!tt.ptr> +// DYNAMIC-NOT: tt.store +// DYNAMIC: %[[EMPTY:.*]] = tensor.empty() : tensor<128xbf16> +// DYNAMIC: %[[FIRST:.*]] = tensor.insert_slice %{{.*}} into %[[EMPTY]][0] [64] [1] : tensor<64xbf16> into tensor<128xbf16> +// DYNAMIC: %[[PACKED:.*]] = tensor.insert_slice %{{.*}} into %[[FIRST]][64] [64] [1] : tensor<64xbf16> into tensor<128xbf16> +// DYNAMIC: %[[RANGE:.*]] = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> +// DYNAMIC: %[[ORIGIN:.*]] = tt.splat %[[ORIGIN_ARG]] : i32 -> tensor<128xi32> +// DYNAMIC: %[[OFFSET:.*]] = arith.addi %[[ORIGIN]], %[[RANGE]] : tensor<128xi32> +// DYNAMIC: %[[OUT:.*]] = tt.splat %[[OUT_ARG]] : !tt.ptr -> tensor<128x!tt.ptr> +// DYNAMIC: %[[ADDRESSES:.*]] = tt.addptr %[[OUT]], %[[OFFSET]] : tensor<128x!tt.ptr>, tensor<128xi32> +// DYNAMIC: tt.store %[[ADDRESSES]], %[[PACKED]] : tensor<128x!tt.ptr> +// DYNAMIC-NOT: tt.store +// DYNAMIC: tt.return +tt.func @pack_dynamic_origin_distinct_entry_loads( + %input: !tt.ptr, %scale: !tt.ptr, %out: !tt.ptr, + %origin: i32) { + %c64 = arith.constant 64 : i32 + %range = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %origin_splat = tt.splat %origin : i32 -> tensor<64xi32> + %k_offsets = arith.addi %origin_splat, %range : tensor<64xi32> + %v_origin = arith.addi %origin, %c64 : i32 + %v_origin_splat = tt.splat %v_origin : i32 -> tensor<64xi32> + %v_offsets = arith.addi %v_origin_splat, %range : tensor<64xi32> + %out_splat = tt.splat %out : !tt.ptr -> tensor<64x!tt.ptr> + %k_addresses = tt.addptr %out_splat, %k_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %v_addresses = tt.addptr %out_splat, %v_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %k_value = arith.constant dense<1.000000e+00> : tensor<64xbf16> + %v_value = arith.constant dense<2.000000e+00> : tensor<64xbf16> + tt.store %k_addresses, %k_value : tensor<64x!tt.ptr> + %scale_value = tt.load %scale : !tt.ptr + %input_splat = tt.splat %input : !tt.ptr -> tensor<64x!tt.ptr> + %input_offsets = arith.addi %origin_splat, %range : tensor<64xi32> + %input_addresses = tt.addptr %input_splat, %input_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %input_value = tt.load %input_addresses : tensor<64x!tt.ptr> + tt.store %v_addresses, %v_value : tensor<64x!tt.ptr> + tt.return +} + +// Equal output bases are insufficient when their dynamic origins differ. +// CHECK-LABEL: tt.func @reject_dynamic_origin_mismatch( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_dynamic_origin_mismatch( + %base: !tt.ptr, %first_origin: i32, %second_origin: i32) { + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %first_origin_splat = tt.splat %first_origin : i32 -> tensor<4xi32> + %second_origin_splat = tt.splat %second_origin : i32 -> tensor<4xi32> + %first_offsets = arith.addi %first_origin_splat, %range : tensor<4xi32> + %second_offsets = arith.addi %second_origin_splat, %range : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_offsets : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_offsets : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} + +// Same-root effects still require interval proof. This middle load overlaps +// the delayed first dynamic-origin store, so it must block packing. +// CHECK-LABEL: tt.func @reject_dynamic_origin_same_root_load( +// CHECK-NOT: tensor.empty +// CHECK-NOT: tensor.insert_slice +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.load {{.*}} : tensor<4x!tt.ptr> +// CHECK: tt.store {{.*}}, {{.*}} : tensor<4x!tt.ptr> +tt.func @reject_dynamic_origin_same_root_load(%base: !tt.ptr, %origin: i32) { + %c4 = arith.constant 4 : i32 + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %origin_splat = tt.splat %origin : i32 -> tensor<4xi32> + %first_offsets = arith.addi %origin_splat, %range : tensor<4xi32> + %second_origin = arith.addi %origin, %c4 : i32 + %second_origin_splat = tt.splat %second_origin : i32 -> tensor<4xi32> + %second_offsets = arith.addi %second_origin_splat, %range : tensor<4xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %first_addresses = tt.addptr %base_splat, %first_offsets : tensor<4x!tt.ptr>, tensor<4xi32> + %second_addresses = tt.addptr %base_splat, %second_offsets : tensor<4x!tt.ptr>, tensor<4xi32> + %first_value = arith.constant dense<1> : tensor<4xi32> + %second_value = arith.constant dense<2> : tensor<4xi32> + tt.store %first_addresses, %first_value : tensor<4x!tt.ptr> + %same_root_load = tt.load %first_addresses : tensor<4x!tt.ptr> + tt.store %second_addresses, %second_value : tensor<4x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-transpose-pointwise.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-transpose-pointwise.mlir new file mode 100644 index 0000000000..5449ff3881 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/graph-optimize-transpose-pointwise.mlir @@ -0,0 +1,258 @@ +// RUN: triton-opt --verify-each %s -graph-optimize='rule-mask=2' -o - | FileCheck %s + +// This is deliberately pre-layout TTIR. A direct tt.trans -> pointwise -> +// tt.dot chain is valid before GPU layouts are assigned; once GPU encodings +// exist, tt.dot requires DotOperandEncodingAttr while tt.trans inference only +// supports Blocked/Shared inputs. The rule keeps its encoded-input preflight +// fail-closed, but its structural rewrite is exercised here at the pipeline + // stage where the chain can exist. + +// Rule 2 asks the registered TritonGPU TensorOrMemDesc type interface to +// preflight a moved tt.trans. Keep every test function itself pre-layout +// TTIR, but instantiate the dialect once so standalone triton-opt has the +// same interface availability as the compiler pipeline. +#blocked = #ttg.blocked<{sizePerThread = [1, 1], threadsPerWarp = [32, 1], warpsPerCTA = [1, 1], order = [1, 0]}> + +module { + // CHECK-LABEL: tt.func @positive_a_truncf_bitcast( + // CHECK-SAME: %[[A_SRC:arg[0-9]+]]: tensor<8x16xf32> + // CHECK-SAME: %[[A_B:arg[0-9]+]]: tensor<8x16xi16> + // CHECK-SAME: %[[A_C:arg[0-9]+]]: tensor<16x16xi32> + // CHECK-NOT: tt.trans %[[A_SRC]] + // CHECK: %[[A_TRUNC:.*]] = arith.truncf %[[A_SRC]] : tensor<8x16xf32> to tensor<8x16xf16> + // CHECK-NEXT: %[[A_BITCAST:.*]] = arith.bitcast %[[A_TRUNC]] : tensor<8x16xf16> to tensor<8x16xi16> + // CHECK-NEXT: %[[A_TRANS:.*]] = tt.trans %[[A_BITCAST]] {order = array} : tensor<8x16xi16> -> tensor<16x8xi16> + // CHECK-NEXT: %[[A_DOT:.*]] = tt.dot %[[A_TRANS]], %[[A_B]], %[[A_C]] : tensor<16x8xi16> * tensor<8x16xi16> -> tensor<16x16xi32> + tt.func @positive_a_truncf_bitcast( + %a_src: tensor<8x16xf32>, + %b: tensor<8x16xi16>, + %c: tensor<16x16xi32>) { + %a_trans = tt.trans %a_src {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + %a_trunc = arith.truncf %a_trans : tensor<16x8xf32> to tensor<16x8xf16> + %a_bitcast = arith.bitcast %a_trunc : tensor<16x8xf16> to tensor<16x8xi16> + %dot = tt.dot %a_bitcast, %b, %c : tensor<16x8xi16> * tensor<8x16xi16> -> tensor<16x16xi32> + tt.return + } + + // CHECK-LABEL: tt.func @positive_b_extf( + // CHECK-SAME: %[[B_A:arg[0-9]+]]: tensor<16x8xf32> + // CHECK-SAME: %[[B_SRC:arg[0-9]+]]: tensor<16x8xf16> + // CHECK-SAME: %[[B_C:arg[0-9]+]]: tensor<16x16xf32> + // CHECK-NOT: tt.trans %[[B_SRC]] + // CHECK: %[[B_EXT:.*]] = arith.extf %[[B_SRC]] : tensor<16x8xf16> to tensor<16x8xf32> + // CHECK-NEXT: %[[B_TRANS:.*]] = tt.trans %[[B_EXT]] {order = array} : tensor<16x8xf32> -> tensor<8x16xf32> + // CHECK-NEXT: %[[B_DOT:.*]] = tt.dot %[[B_A]], %[[B_TRANS]], %[[B_C]] : tensor<16x8xf32> * tensor<8x16xf32> -> tensor<16x16xf32> + tt.func @positive_b_extf( + %a: tensor<16x8xf32>, + %b_src: tensor<16x8xf16>, + %c: tensor<16x16xf32>) { + %b_trans = tt.trans %b_src {order = array} : tensor<16x8xf16> -> tensor<8x16xf16> + %b_ext = arith.extf %b_trans : tensor<8x16xf16> to tensor<8x16xf32> + %dot = tt.dot %a, %b_ext, %c : tensor<16x8xf32> * tensor<8x16xf32> -> tensor<16x16xf32> + tt.return + } + + // CHECK-LABEL: tt.func @positive_a_mixed_cast_math_chain( + // CHECK-SAME: %[[M_SRC:arg[0-9]+]]: tensor<8x16xf32> + // CHECK-SAME: %[[M_B:arg[0-9]+]]: tensor<8x16xi16> + // CHECK-SAME: %[[M_C:arg[0-9]+]]: tensor<16x16xi32> + // CHECK-NOT: tt.trans %[[M_SRC]] + // CHECK: %[[M_NEG:.*]] = arith.negf %[[M_SRC]] : tensor<8x16xf32> + // CHECK-NEXT: %[[M_ABS:.*]] = math.absf %[[M_NEG]] : tensor<8x16xf32> + // CHECK-NEXT: %[[M_TRUNC:.*]] = arith.truncf %[[M_ABS]] : tensor<8x16xf32> to tensor<8x16xf16> + // CHECK-NEXT: %[[M_EXP:.*]] = math.exp %[[M_TRUNC]] : tensor<8x16xf16> + // CHECK-NEXT: %[[M_BITS:.*]] = arith.bitcast %[[M_EXP]] : tensor<8x16xf16> to tensor<8x16xi16> + // CHECK-NEXT: %[[M_TRANS:.*]] = tt.trans %[[M_BITS]] {order = array} : tensor<8x16xi16> -> tensor<16x8xi16> + // CHECK-NEXT: %{{.*}} = tt.dot %[[M_TRANS]], %[[M_B]], %[[M_C]] : tensor<16x8xi16> * tensor<8x16xi16> -> tensor<16x16xi32> + tt.func @positive_a_mixed_cast_math_chain( + %a_src: tensor<8x16xf32>, + %b: tensor<8x16xi16>, + %c: tensor<16x16xi32>) { + %trans = tt.trans %a_src {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + %neg = arith.negf %trans : tensor<16x8xf32> + %abs = math.absf %neg : tensor<16x8xf32> + %trunc = arith.truncf %abs : tensor<16x8xf32> to tensor<16x8xf16> + %exp = math.exp %trunc : tensor<16x8xf16> + %bits = arith.bitcast %exp : tensor<16x8xf16> to tensor<16x8xi16> + %dot = tt.dot %bits, %b, %c : tensor<16x8xi16> * tensor<8x16xi16> -> tensor<16x16xi32> + tt.return + } + + // CHECK-LABEL: tt.func @positive_a_long_float_unary_chain( + // CHECK-SAME: %[[L_SRC:arg[0-9]+]]: tensor<8x16xf32> + // CHECK-NOT: tt.trans %[[L_SRC]] + // CHECK: %[[L0:.*]] = arith.negf %[[L_SRC]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L1:.*]] = math.absf %[[L0]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L2:.*]] = math.ceil %[[L1]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L3:.*]] = math.floor %[[L2]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L4:.*]] = math.cos %[[L3]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L5:.*]] = math.sin %[[L4]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L6:.*]] = math.erf %[[L5]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L7:.*]] = math.exp %[[L6]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L8:.*]] = math.exp2 %[[L7]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L9:.*]] = math.log %[[L8]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L10:.*]] = math.log2 %[[L9]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L11:.*]] = math.sqrt %[[L10]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L12:.*]] = math.rsqrt %[[L11]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L13:.*]] = math.tanh %[[L12]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L14:.*]] = tt.precise_sqrt %[[L13]] : tensor<8x16xf32> + // CHECK-NEXT: %[[L_TRANS:.*]] = tt.trans %[[L14]] {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + // CHECK-NEXT: %{{.*}} = tt.dot %[[L_TRANS]], %{{.*}}, %{{.*}} : tensor<16x8xf32> * tensor<8x16xf32> -> tensor<16x16xf32> + tt.func @positive_a_long_float_unary_chain( + %a_src: tensor<8x16xf32>, + %b: tensor<8x16xf32>, + %c: tensor<16x16xf32>) { + %trans = tt.trans %a_src {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + %v0 = arith.negf %trans : tensor<16x8xf32> + %v1 = math.absf %v0 : tensor<16x8xf32> + %v2 = math.ceil %v1 : tensor<16x8xf32> + %v3 = math.floor %v2 : tensor<16x8xf32> + %v4 = math.cos %v3 : tensor<16x8xf32> + %v5 = math.sin %v4 : tensor<16x8xf32> + %v6 = math.erf %v5 : tensor<16x8xf32> + %v7 = math.exp %v6 : tensor<16x8xf32> + %v8 = math.exp2 %v7 : tensor<16x8xf32> + %v9 = math.log %v8 : tensor<16x8xf32> + %v10 = math.log2 %v9 : tensor<16x8xf32> + %v11 = math.sqrt %v10 : tensor<16x8xf32> + %v12 = math.rsqrt %v11 : tensor<16x8xf32> + %v13 = math.tanh %v12 : tensor<16x8xf32> + %v14 = tt.precise_sqrt %v13 : tensor<16x8xf32> + %dot = tt.dot %v14, %b, %c : tensor<16x8xf32> * tensor<8x16xf32> -> tensor<16x16xf32> + tt.return + } + + // CHECK-LABEL: tt.func @positive_a_absi( + // CHECK-SAME: %[[I_SRC:arg[0-9]+]]: tensor<8x16xi16> + // CHECK-NOT: tt.trans %[[I_SRC]] + // CHECK: %[[I_ABS:.*]] = math.absi %[[I_SRC]] : tensor<8x16xi16> + // CHECK-NEXT: %[[I_TRANS:.*]] = tt.trans %[[I_ABS]] {order = array} : tensor<8x16xi16> -> tensor<16x8xi16> + // CHECK-NEXT: %{{.*}} = tt.dot %[[I_TRANS]], %{{.*}}, %{{.*}} : tensor<16x8xi16> * tensor<8x16xi16> -> tensor<16x16xi32> + tt.func @positive_a_absi( + %a_src: tensor<8x16xi16>, + %b: tensor<8x16xi16>, + %c: tensor<16x16xi32>) { + %trans = tt.trans %a_src {order = array} : tensor<8x16xi16> -> tensor<16x8xi16> + %abs = math.absi %trans : tensor<16x8xi16> + %dot = tt.dot %abs, %b, %c : tensor<16x8xi16> * tensor<8x16xi16> -> tensor<16x16xi32> + tt.return + } + + // CHECK-LABEL: tt.func @reject_external_trans_user( + // CHECK: %[[EXTERNAL_TRANS:.*]] = tt.trans %{{.*}} {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + // CHECK-NEXT: %[[EXTERNAL_CAST:.*]] = arith.truncf %[[EXTERNAL_TRANS]] : tensor<16x8xf32> to tensor<16x8xf16> + // CHECK-NEXT: %{{.*}} = tt.trans %[[EXTERNAL_TRANS]] {order = array} : tensor<16x8xf32> -> tensor<8x16xf32> + // CHECK-NEXT: %[[EXTERNAL_DOT:.*]] = tt.dot %[[EXTERNAL_CAST]], %{{.*}}, %{{.*}} : tensor<16x8xf16> * tensor<8x16xf16> -> tensor<16x16xf32> + tt.func @reject_external_trans_user( + %a_src: tensor<8x16xf32>, + %b: tensor<8x16xf16>, + %c: tensor<16x16xf32>) { + %a_trans = tt.trans %a_src {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + %a_trunc = arith.truncf %a_trans : tensor<16x8xf32> to tensor<16x8xf16> + %extra = tt.trans %a_trans {order = array} : tensor<16x8xf32> -> tensor<8x16xf32> + %dot = tt.dot %a_trunc, %b, %c : tensor<16x8xf16> * tensor<8x16xf16> -> tensor<16x16xf32> + tt.return + } + + // CHECK-LABEL: tt.func @reject_addf( + // CHECK: %[[ADD_TRANS:.*]] = tt.trans %{{.*}} {order = array} : tensor<8x16xf16> -> tensor<16x8xf16> + // CHECK-NEXT: %[[ADD:.*]] = arith.addf %[[ADD_TRANS]], %{{.*}} : tensor<16x8xf16> + // CHECK-NEXT: %[[ADD_DOT:.*]] = tt.dot %[[ADD]], %{{.*}}, %{{.*}} : tensor<16x8xf16> * tensor<8x16xf16> -> tensor<16x16xf32> + tt.func @reject_addf( + %a_src: tensor<8x16xf16>, + %b: tensor<8x16xf16>, + %c: tensor<16x16xf32>) { + %zero = arith.constant dense<0.0> : tensor<16x8xf16> + %a_trans = tt.trans %a_src {order = array} : tensor<8x16xf16> -> tensor<16x8xf16> + %a_add = arith.addf %a_trans, %zero : tensor<16x8xf16> + %dot = tt.dot %a_add, %b, %c : tensor<16x8xf16> * tensor<8x16xf16> -> tensor<16x16xf32> + tt.return + } + + // CHECK-LABEL: tt.func @positive_a_generic_elementwise( + // CHECK-SAME: %[[G_SRC:arg[0-9]+]]: tensor<8x16xf32> + // CHECK-NOT: tt.trans %[[G_SRC]] + // CHECK: %[[G_ATAN:.*]] = math.atan %[[G_SRC]] : tensor<8x16xf32> + // CHECK-NEXT: %[[G_TAN:.*]] = math.tan %[[G_ATAN]] : tensor<8x16xf32> + // CHECK-NEXT: %[[G_TRANS:.*]] = tt.trans %[[G_TAN]] {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + // CHECK-NEXT: %{{.*}} = tt.dot %[[G_TRANS]], %{{.*}}, %{{.*}} : tensor<16x8xf32> * tensor<8x16xf32> -> tensor<16x16xf32> + tt.func @positive_a_generic_elementwise( + %a_src: tensor<8x16xf32>, + %b: tensor<8x16xf32>, + %c: tensor<16x16xf32>) { + %trans = tt.trans %a_src {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + %atan = math.atan %trans : tensor<16x8xf32> + %tan = math.tan %atan : tensor<16x8xf32> + %dot = tt.dot %tan, %b, %c : tensor<16x8xf32> * tensor<8x16xf32> -> tensor<16x16xf32> + tt.return + } + + // CHECK-LABEL: tt.func @reject_type_changing_unary( + // CHECK-SAME: %[[U_SRC:arg[0-9]+]]: tensor<8x16xf32> + // CHECK-NOT: arith.fptosi %[[U_SRC]] + // CHECK: %[[U_TRANS:.*]] = tt.trans %[[U_SRC]] {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + // CHECK-NEXT: %[[U_CONVERTED:.*]] = arith.fptosi %[[U_TRANS]] : tensor<16x8xf32> to tensor<16x8xi16> + // CHECK-NEXT: %{{.*}} = tt.dot %[[U_CONVERTED]], %{{.*}}, %{{.*}} : tensor<16x8xi16> * tensor<8x16xi16> -> tensor<16x16xi32> + tt.func @reject_type_changing_unary( + %a_src: tensor<8x16xf32>, + %b: tensor<8x16xi16>, + %c: tensor<16x16xi32>) { + %trans = tt.trans %a_src {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + %converted = arith.fptosi %trans : tensor<16x8xf32> to tensor<16x8xi16> + %dot = tt.dot %converted, %b, %c : tensor<16x8xi16> * tensor<8x16xi16> -> tensor<16x16xi32> + tt.return + } + + // CHECK-LABEL: tt.func @reject_unary_external_user( + // CHECK: %[[E_TRANS:.*]] = tt.trans %{{.*}} {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + // CHECK-NEXT: %[[E_EXP:.*]] = math.exp %[[E_TRANS]] : tensor<16x8xf32> + // CHECK-NEXT: %{{.*}} = math.cos %[[E_EXP]] : tensor<16x8xf32> + // CHECK-NEXT: %{{.*}} = tt.dot %[[E_EXP]], %{{.*}}, %{{.*}} : tensor<16x8xf32> * tensor<8x16xf32> -> tensor<16x16xf32> + tt.func @reject_unary_external_user( + %a_src: tensor<8x16xf32>, + %b: tensor<8x16xf32>, + %c: tensor<16x16xf32>) { + %trans = tt.trans %a_src {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + %exp = math.exp %trans : tensor<16x8xf32> + %extra = math.cos %exp : tensor<16x8xf32> + %dot = tt.dot %exp, %b, %c : tensor<16x8xf32> * tensor<8x16xf32> -> tensor<16x16xf32> + tt.return + } + + // The accumulator has the matching shape but is deliberately not an A/B + // candidate. Its original transpose/cast chain must stay untouched. + // CHECK-LABEL: tt.func @reject_dot_accumulator( + // CHECK: %[[C_TRANS:.*]] = tt.trans %{{.*}} {order = array} : tensor<16x16xf64> -> tensor<16x16xf64> + // CHECK-NEXT: %[[C_TRUNC:.*]] = arith.truncf %[[C_TRANS]] : tensor<16x16xf64> to tensor<16x16xf32> + // CHECK-NEXT: %[[C_DOT:.*]] = tt.dot %{{.*}}, %{{.*}}, %[[C_TRUNC]] : tensor<16x8xf16> * tensor<8x16xf16> -> tensor<16x16xf32> + tt.func @reject_dot_accumulator( + %a: tensor<16x8xf16>, + %b: tensor<8x16xf16>, + %c_src: tensor<16x16xf64>) { + %c_trans = tt.trans %c_src {order = array} : tensor<16x16xf64> -> tensor<16x16xf64> + %c_trunc = arith.truncf %c_trans : tensor<16x16xf64> to tensor<16x16xf32> + %dot = tt.dot %a, %b, %c_trunc : tensor<16x8xf16> * tensor<8x16xf16> -> tensor<16x16xf32> + tt.return + } + + // CHECK-LABEL: tt.func @reject_region_boundary( + // CHECK: %[[REGION_TRANS:.*]] = tt.trans %{{.*}} {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + // CHECK: scf.if %{{.*}} -> (tensor<16x16xf32>) { + // CHECK: %[[REGION_CAST:.*]] = arith.truncf %[[REGION_TRANS]] : tensor<16x8xf32> to tensor<16x8xf16> + // CHECK-NEXT: %[[REGION_DOT:.*]] = tt.dot %[[REGION_CAST]], %{{.*}}, %{{.*}} : tensor<16x8xf16> * tensor<8x16xf16> -> tensor<16x16xf32> + tt.func @reject_region_boundary( + %condition: i1, + %a_src: tensor<8x16xf32>, + %b: tensor<8x16xf16>, + %c: tensor<16x16xf32>) { + %a_trans = tt.trans %a_src {order = array} : tensor<8x16xf32> -> tensor<16x8xf32> + %result = scf.if %condition -> (tensor<16x16xf32>) { + %a_trunc = arith.truncf %a_trans : tensor<16x8xf32> to tensor<16x8xf16> + %dot = tt.dot %a_trunc, %b, %c : tensor<16x8xf16> * tensor<8x16xf16> -> tensor<16x16xf32> + scf.yield %dot : tensor<16x16xf32> + } else { + scf.yield %c : tensor<16x16xf32> + } + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/merge_concat_load_buffer.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/merge_concat_load_buffer.mlir new file mode 100644 index 0000000000..d925a9bc61 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/merge_concat_load_buffer.mlir @@ -0,0 +1,619 @@ +// RUN: triton-opt --merge-concat-load-buffer --split-input-file %s | FileCheck %s + +//===----------------------------------------------------------------------===// +// Positive: the canonical tl.cat pattern. +// +// Two masked loads write disjoint column ranges of identically shaped UB +// buffers and are concatenated by extract_slice + insert_slice. The store +// reads [0, %n) x [0, 512), which the union of the two copies covers exactly, +// so the allocs merge into one and both fills die. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @merge_and_drop_fill +// CHECK-NOT: linalg.fill +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<32x512xf32> +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK: memref.subview %[[ALLOC]][0, 0] +// CHECK: memref.copy +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK: memref.subview %[[ALLOC]][0, 256] +// CHECK: memref.copy +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK-NOT: tensor.insert_slice +// CHECK: %[[T:.*]] = bufferization.to_tensor %[[ALLOC]] restrict writable +// CHECK: tensor.extract_slice %[[T]][0, 0] [%{{.*}}, 512] [1, 1] +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK-NOT: tensor.insert_slice +func.func @merge_and_drop_fill(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + %out = tensor.extract_slice %ins[0, 0] [%n, 512] [1, 1] : tensor<32x512xf32> to tensor + return %out : tensor +} + +// ----- + +//===----------------------------------------------------------------------===// +// Positive: the shape actually emitted by triton-to-linalg for a cat kernel, +// i.e. inside scf.for with memref.reinterpret_cast sources and a +// materialize_in_destination store. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @merge_inside_scf_for +// CHECK: scf.for +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<32x512xf32> +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK: memref.subview %[[ALLOC]][0, 0] +// CHECK: memref.copy +// CHECK: memref.subview %[[ALLOC]][0, 256] +// CHECK: memref.copy +// CHECK-NOT: tensor.insert_slice +// CHECK: %[[T:.*]] = bufferization.to_tensor %[[ALLOC]] restrict writable +// CHECK: tensor.extract_slice %[[T]] +// CHECK: bufferization.materialize_in_destination +// CHECK-NOT: linalg.fill +// CHECK-NOT: tensor.insert_slice +func.func @merge_inside_scf_for(%in0: memref, %in1: memref, %out: memref, %ub: i32, %bound: i32) { + %cst = arith.constant 0.000000e+00 : f32 + %cn256 = arith.constant -256 : index + %c0 = arith.constant 0 : index + %c32 = arith.constant 32 : index + %c256 = arith.constant 256 : index + %c512 = arith.constant 512 : index + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c32_i32 = arith.constant 32 : i32 + + scf.for %i = %c0_i32 to %ub step %c1_i32 : i32 { + %row = arith.muli %i, %c32_i32 : i32 + %row_idx = arith.index_cast %row : i32 to index + %off0 = arith.muli %row_idx, %c256 : index + %rc0 = memref.reinterpret_cast %in0 to offset: [%off0], sizes: [32, 512], strides: [256, 1] : memref to memref<32x512xf32, strided<[256, 1], offset: ?>> + + %alloc0 = memref.alloc() : memref<32x512xf32> + %hi = arith.addi %row_idx, %c32 : index + %bnd = arith.index_cast %bound : i32 to index + %lo = arith.maxsi %row_idx, %bnd : index + %clamped = arith.minsi %hi, %lo : index + %len = arith.subi %clamped, %row_idx : index + %len32 = arith.minsi %len, %c32 : index + %rows = arith.maxsi %len32, %c0 : index + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %rc0[0, 0] [%rows, 256] [1, 1] : memref<32x512xf32, strided<[256, 1], offset: ?>> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%rows, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %off1 = arith.addi %off0, %cn256 : index + %rc1 = memref.reinterpret_cast %in1 to offset: [%off1], sizes: [32, 512], strides: [256, 1] : memref to memref<32x512xf32, strided<[256, 1], offset: ?>> + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %rc1[0, 256] [%rows, 256] [1, 1] : memref<32x512xf32, strided<[256, 1], offset: ?>> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%rows, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + + %off_out = arith.muli %row_idx, %c512 : index + %rc_out = memref.reinterpret_cast %out to offset: [%off_out], sizes: [32, 512], strides: [512, 1] : memref to memref<32x512xf32, strided<[512, 1], offset: ?>> + %store_src = tensor.extract_slice %ins[0, 0] [%rows, 512] [1, 1] : tensor<32x512xf32> to tensor + %store_dst = memref.subview %rc_out[0, 0] [%rows, 512] [1, 1] : memref<32x512xf32, strided<[512, 1], offset: ?>> to memref> + bufferization.materialize_in_destination %store_src in writable %store_dst : (tensor, memref>) -> () + } + return +} + +// ----- + +//===----------------------------------------------------------------------===// +// Positive: LoadConverter inserts a memref.cast between the destination +// subview and the copy. The pass must look through it. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @merge_through_memref_cast +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<32x512xf32> +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK: memref.subview %[[ALLOC]][0, 0] +// CHECK: memref.copy +// CHECK: memref.subview %[[ALLOC]][0, 256] +// CHECK: memref.copy +// CHECK: bufferization.to_tensor %[[ALLOC]] restrict writable +// CHECK-NOT: linalg.fill +// CHECK-NOT: tensor.insert_slice +func.func @merge_through_memref_cast(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %cast0 = memref.cast %sd0 : memref> to memref> + memref.copy %ss0, %cast0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %cast1 = memref.cast %sd1 : memref> to memref> + memref.copy %ss1, %cast1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + %out = tensor.extract_slice %ins[0, 0] [%n, 512] [1, 1] : tensor<32x512xf32> to tensor + return %out : tensor +} + +// ----- + +//===----------------------------------------------------------------------===// +// Positive: the padding fill is still wrapped in the scf.if that +// fillTensorWithOtherForMaskScenario emits (not yet folded by the +// canonicalizer). A dead fill must take its guarding scf.if with it. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @merge_drop_guarded_fill +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<32x512xf32> +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK-NOT: scf.if +// CHECK: memref.subview %[[ALLOC]][0, 0] +// CHECK: memref.copy +// CHECK: memref.subview %[[ALLOC]][0, 256] +// CHECK: memref.copy +// CHECK: bufferization.to_tensor %[[ALLOC]] restrict writable +// CHECK-NOT: linalg.fill +// CHECK-NOT: scf.if +// CHECK-NOT: tensor.insert_slice +func.func @merge_drop_guarded_fill(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor { + %cst = arith.constant 0.000000e+00 : f32 + %c32 = arith.constant 32 : index + %partial = arith.cmpi slt, %n, %c32 : index + + %alloc0 = memref.alloc() : memref<32x512xf32> + scf.if %partial { + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + } {hivm.unlikely_condition} + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + scf.if %partial { + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + } {hivm.unlikely_condition} + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + %out = tensor.extract_slice %ins[0, 0] [%n, 512] [1, 1] : tensor<32x512xf32> to tensor + return %out : tensor +} + +// ----- + +//===----------------------------------------------------------------------===// +// Positive: 1-D cat. The concatenated tensor is consumed whole, but the two +// static copies together cover the entire buffer, so the fills are still dead. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @merge_1d_full_cover +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<512xf32> +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK: memref.subview %[[ALLOC]][0] +// CHECK: memref.copy +// CHECK: memref.subview %[[ALLOC]][256] +// CHECK: memref.copy +// CHECK-NOT: tensor.insert_slice +// CHECK: bufferization.to_tensor %[[ALLOC]] restrict writable +// CHECK-NOT: linalg.fill +// CHECK-NOT: tensor.insert_slice +func.func @merge_1d_full_cover(%arg0: memref<512xf32>, %arg1: memref<512xf32>) -> tensor<512xf32> { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<512xf32>) + %ss0 = memref.subview %arg0[0] [256] [1] : memref<512xf32> to memref<256xf32, strided<[1]>> + %sd0 = memref.subview %alloc0[0] [256] [1] : memref<512xf32> to memref<256xf32, strided<[1]>> + memref.copy %ss0, %sd0 : memref<256xf32, strided<[1]>> to memref<256xf32, strided<[1]>> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<512xf32> to tensor<512xf32> + + %alloc1 = memref.alloc() : memref<512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<512xf32>) + %ss1 = memref.subview %arg1[256] [256] [1] : memref<512xf32> to memref<256xf32, strided<[1], offset: 256>> + %sd1 = memref.subview %alloc1[256] [256] [1] : memref<512xf32> to memref<256xf32, strided<[1], offset: 256>> + memref.copy %ss1, %sd1 : memref<256xf32, strided<[1], offset: 256>> to memref<256xf32, strided<[1], offset: 256>> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<512xf32> to tensor<512xf32> + + %ex = tensor.extract_slice %t0[0] [256] [1] : tensor<512xf32> to tensor<256xf32> + %ins = tensor.insert_slice %ex into %t1[0] [256] [1] : tensor<256xf32> into tensor<512xf32> + return %ins : tensor<512xf32> +} + +// ----- + +//===----------------------------------------------------------------------===// +// Fallback: the concatenated tensor is consumed whole while the copies only +// cover [0, %n) rows, so the padding is observable. The allocs still merge but +// exactly one fill survives, placed before both copies. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @merge_keep_single_fill +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<32x512xf32> +// CHECK: linalg.fill ins(%{{.*}} : f32) outs(%[[ALLOC]] : memref<32x512xf32>) +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK: memref.subview %[[ALLOC]][0, 0] +// CHECK: memref.copy +// CHECK: memref.subview %[[ALLOC]][0, 256] +// CHECK: memref.copy +// CHECK-NOT: tensor.insert_slice +// CHECK: bufferization.to_tensor %[[ALLOC]] restrict writable +// CHECK-NOT: memref.alloc() +// CHECK-NOT: linalg.fill +// CHECK-NOT: tensor.insert_slice +func.func @merge_keep_single_fill(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor<32x512xf32> { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + return %ins : tensor<32x512xf32> +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: both copies target the same column range, so the dest write is not +// disjoint from the insert region. Merging would clobber data. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_overlapping_writes +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: tensor.insert_slice +func.func @no_merge_overlapping_writes(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + %out = tensor.extract_slice %ins[0, 0] [%n, 512] [1, 1] : tensor<32x512xf32> to tensor + return %out : tensor +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: the extract offset differs from the insert offset, so the source +// data is shifted on its way into the dest. A plain RAUW would be wrong. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_shifted_placement +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: tensor.insert_slice +func.func @no_merge_shifted_placement(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 256] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + %out = tensor.extract_slice %ins[0, 0] [%n, 512] [1, 1] : tensor<32x512xf32> to tensor + return %out : tensor +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: the two buffers have different shapes, so they cannot share one +// allocation. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_shape_mismatch +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: memref.alloc() : memref<64x512xf32> +// CHECK: tensor.insert_slice +func.func @no_merge_shape_mismatch(%arg0: memref<32x512xf32>, %arg1: memref<64x512xf32>, %n: index) -> tensor<64x512xf32> { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<64x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<64x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<64x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<64x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<64x512xf32> to tensor<64x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<64x512xf32> + return %ins : tensor<64x512xf32> +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: the source buffer has a user the pass does not model (a dealloc), +// so its lifetime cannot be folded into the dest buffer. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_unmodeled_alloc_user +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: tensor.insert_slice +// CHECK: memref.dealloc +func.func @no_merge_unmodeled_alloc_user(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + %out = tensor.extract_slice %ins[0, 0] [%n, 512] [1, 1] : tensor<32x512xf32> to tensor + memref.dealloc %alloc0 : memref<32x512xf32> + return %out : tensor +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: the source tensor is read a second time, so the buffer it comes +// from is still live after the concat. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_source_tensor_reused +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: tensor.insert_slice +func.func @no_merge_source_tensor_reused(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> (tensor<32x512xf32>, tensor<32x512xf32>) { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + return %ins, %t0 : tensor<32x512xf32>, tensor<32x512xf32> +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: the padding is observable and the two fills use different values, +// so neither dropping nor merging the fills is legal. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_conflicting_fill_values +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: linalg.fill +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: linalg.fill +// CHECK: tensor.insert_slice +func.func @no_merge_conflicting_fill_values(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor<32x512xf32> { + %zero = arith.constant 0.000000e+00 : f32 + %one = arith.constant 1.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%zero : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%one : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + return %ins : tensor<32x512xf32> +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: a strided insert_slice does not describe a contiguous region, so +// the box arithmetic the pass relies on does not apply. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_strided_insert +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: tensor.insert_slice +func.func @no_merge_strided_insert(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor<32x512xf32> { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 2] : tensor<32x256xf32> into tensor<32x512xf32> + return %ins : tensor<32x512xf32> +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: the fill of the source buffer runs *after* the copy it would +// initialise, so the padding is what the concat observes rather than the copied +// data. The read region is fully covered, so without an ordering check the +// rewrite would wrongly delete both fills. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_late_fill +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: memref.copy +// CHECK: linalg.fill +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: tensor.insert_slice +func.func @no_merge_late_fill(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %alloc1 = memref.alloc() : memref<32x512xf32> + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + %out = tensor.extract_slice %ins[0, 0] [%n, 512] [1, 1] : tensor<32x512xf32> to tensor + return %out : tensor +} + +// ----- + +//===----------------------------------------------------------------------===// +// Negative: each fill precedes the copies into its own buffer, but the fill of +// the surviving (earlier) alloc sits after the copy into the other one. Merging +// would let that fill clobber data the other copy had already written, so the +// keep-one-fill fallback must be refused too. +//===----------------------------------------------------------------------===// + +// CHECK-LABEL: func.func @no_merge_fill_after_other_copy +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: memref.alloc() : memref<32x512xf32> +// CHECK: tensor.insert_slice +func.func @no_merge_fill_after_other_copy(%arg0: memref<32x512xf32>, %arg1: memref<32x512xf32>, %n: index) -> tensor<32x512xf32> { + %cst = arith.constant 0.000000e+00 : f32 + + %alloc0 = memref.alloc() : memref<32x512xf32> + %alloc1 = memref.alloc() : memref<32x512xf32> + + linalg.fill ins(%cst : f32) outs(%alloc1 : memref<32x512xf32>) + %ss1 = memref.subview %arg1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd1 = memref.subview %alloc1[0, 256] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss1, %sd1 : memref> to memref> + + linalg.fill ins(%cst : f32) outs(%alloc0 : memref<32x512xf32>) + %ss0 = memref.subview %arg0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + %sd0 = memref.subview %alloc0[0, 0] [%n, 256] [1, 1] : memref<32x512xf32> to memref> + memref.copy %ss0, %sd0 : memref> to memref> + + %t0 = bufferization.to_tensor %alloc0 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + %t1 = bufferization.to_tensor %alloc1 restrict writable : memref<32x512xf32> to tensor<32x512xf32> + + %ex = tensor.extract_slice %t0[0, 0] [32, 256] [1, 1] : tensor<32x512xf32> to tensor<32x256xf32> + %ins = tensor.insert_slice %ex into %t1[0, 0] [32, 256] [1, 1] : tensor<32x256xf32> into tensor<32x512xf32> + return %ins : tensor<32x512xf32> +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-basic.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-basic.mlir new file mode 100644 index 0000000000..b60d35a31f --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-basic.mlir @@ -0,0 +1,302 @@ +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=8' -canonicalize -cse -o %t +// RUN: FileCheck %s < %t +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=8 compile-mode=simt_only' -canonicalize -cse -o %t.simt +// RUN: triton-opt --split-input-file --verify-each %t -graph-optimize='rule-mask=8' -canonicalize -cse -o %t.twice +// RUN: diff %t %t.twice +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=0' -canonicalize -cse -o %t.disabled +// RUN: triton-opt --split-input-file %s -canonicalize -cse -o %t.control +// RUN: diff %t.control %t.disabled +// RUN: diff %t.control %t.simt + +// The same valid candidates must remain unchanged on non-Ascend 950 targets. +// RUN: sed 's/Ascend950PR_9579/Ascend910B2/g' %s > %t.a2.mlir +// RUN: triton-opt --split-input-file %t.a2.mlir -canonicalize -cse -o %t.a2.control +// RUN: triton-opt --split-input-file --verify-each %t.a2.mlir -graph-optimize='rule-mask=8' -canonicalize -cse -o %t.a2 +// RUN: diff %t.a2.control %t.a2 +// RUN: sed 's/Ascend950PR_9579/Ascend910_9391/g' %s > %t.a3.mlir +// RUN: triton-opt --split-input-file %t.a3.mlir -canonicalize -cse -o %t.a3.control +// RUN: triton-opt --split-input-file --verify-each %t.a3.mlir -graph-optimize='rule-mask=8' -canonicalize -cse -o %t.a3 +// RUN: diff %t.a3.control %t.a3 +// Missing target information must not enable the Ascend 950-only pattern. +// RUN: sed 's/hacc.target = #hacc.target<"Ascend950PR_9579">, //g' %s > %t.no-target.mlir +// RUN: triton-opt --split-input-file %t.no-target.mlir -canonicalize -cse -o %t.no-target.control +// RUN: triton-opt --split-input-file --verify-each %t.no-target.mlir -graph-optimize='rule-mask=8' -canonicalize -cse -o %t.no-target +// RUN: diff %t.no-target.control %t.no-target + +// Merge independent dot rows while keeping the RHS shared; preserve per-row trip counts. +// CHECK: module attributes {{.*}}hacc.coalesce_factor = 8 : i32 +// CHECK-LABEL: tt.func @one_dot_m16( +// CHECK: tt.load {{.*}} : tensor<8x16x16x!tt.ptr> +// CHECK: scf.for +// CHECK: %[[RHS:.*]] = tt.load {{.*}} : tensor<16x16x!tt.ptr> +// CHECK: tt.dot %{{.*}}, %[[RHS]], %{{.*}} : tensor<128x16xf16> * tensor<16x16xf16> -> tensor<128x16xf32> +// CHECK: arith.cmpi slt, {{.*}} : tensor<8xi32> +// CHECK: arith.select {{.*}} : tensor<8x16x16xi1>, tensor<8x16x16xf32> +// CHECK: tt.store {{.*}} : tensor<8x16x16x!tt.ptr> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @one_dot_m16(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK: module attributes {{.*}}hacc.coalesce_factor = 8 : i32 +// CHECK-LABEL: tt.func @two_accumulators( +// CHECK: tt.load {{.*}} : tensor<8x16x16x!tt.ptr> +// CHECK: scf.for +// CHECK: %[[RHS:.*]] = tt.load {{.*}} : tensor<16x16x!tt.ptr> +// CHECK: tt.dot %{{.*}}, %[[RHS]], %{{.*}} : tensor<128x16xf16> * tensor<16x16xf16> -> tensor<128x16xf32> +// CHECK: arith.cmpi slt, {{.*}} : tensor<8xi32> +// CHECK: arith.select {{.*}} : tensor<8x16x16xi1>, tensor<8x16x16xf32> +// CHECK: tt.store {{.*}} : tensor<8x16x16x!tt.ptr> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @two_accumulators(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %zero1 = arith.constant dense<1.0> : tensor<16x16xf32> + %loop:2 = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0, %acc1 = %zero1) -> (tensor<16x16xf32>, tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + %dot1_0 = tt.dot %a, %b, %acc1 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0, %dot1_0 : tensor<16x16xf32>, tensor<16x16xf32> + } + %sum1 = arith.addf %loop#0, %loop#1 : tensor<16x16xf32> + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %sum1 : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK: module attributes {{.*}}hacc.coalesce_factor = 8 : i32 +// CHECK-LABEL: tt.func @commuted_monotonic_bound( +// CHECK: tt.load {{.*}} : tensor<8x16x16x!tt.ptr> +// CHECK: scf.for +// CHECK: %[[RHS:.*]] = tt.load {{.*}} : tensor<16x16x!tt.ptr> +// CHECK: tt.dot %{{.*}}, %[[RHS]], %{{.*}} : tensor<128x16xf16> * tensor<16x16xf16> -> tensor<128x16xf32> +// CHECK: arith.cmpi slt, {{.*}} : tensor<8xi32> +// CHECK: arith.select {{.*}} : tensor<8x16x16xi1>, tensor<8x16x16xf32> +// CHECK: tt.store {{.*}} : tensor<8x16x16x!tt.ptr> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @commuted_monotonic_bound(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.addi %c2, %pid : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK: module attributes {{.*}}hacc.coalesce_factor = 4 : i32 +// CHECK-LABEL: tt.func @smaller_exact_grid_group( +// CHECK: tt.load {{.*}} : tensor<4x16x16x!tt.ptr> +// CHECK: scf.for +// CHECK: %[[RHS:.*]] = tt.load {{.*}} : tensor<16x16x!tt.ptr> +// CHECK: tt.dot %{{.*}}, %[[RHS]], %{{.*}} : tensor<64x16xf16> * tensor<16x16xf16> -> tensor<64x16xf32> +// CHECK: arith.cmpi slt, {{.*}} : tensor<4xi32> +// CHECK: arith.select {{.*}} : tensor<4x16x16xi1>, tensor<4x16x16xf32> +// CHECK: tt.store {{.*}} : tensor<4x16x16x!tt.ptr> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 12 : i32} { + tt.func @smaller_exact_grid_group(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK: module attributes {{.*}}hacc.coalesce_factor = 8 : i32 +// CHECK-LABEL: tt.func @bf16_dot( +// CHECK: tt.load {{.*}} : tensor<8x16x16x!tt.ptr> +// CHECK: scf.for +// CHECK: %[[RHS:.*]] = tt.load {{.*}} : tensor<16x16x!tt.ptr> +// CHECK: tt.dot %{{.*}}, %[[RHS]], %{{.*}} : tensor<128x16xbf16> * tensor<16x16xbf16> -> tensor<128x16xf32> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @bf16_dot(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xbf16> * tensor<16x16xbf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-fallback.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-fallback.mlir new file mode 100644 index 0000000000..ac2c107ba3 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-fallback.mlir @@ -0,0 +1,668 @@ +// RUN: triton-opt --split-input-file %s -canonicalize -cse -o %t.control +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=8' -canonicalize -cse -o %t.template +// RUN: diff %t.control %t.template +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=8 compile-mode=simt_only' -canonicalize -cse -o %t.simt +// RUN: diff %t.control %t.simt + +// Unsupported inputs must remain unchanged, including all module attributes. +// scalar_select_preflight_fallback reaches private rewrite verification: +// a row-vector condition cannot select a higher-rank row tensor directly. +// A failed optional rewrite must not make valid input fail compilation. +// odd_grid_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 13 : i32} { + tt.func @odd_grid_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// overflow_bound_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @overflow_bound_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %large = arith.constant 2147483647 : i32 + %upper = arith.addi %pid, %large : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// overlapping_store_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @overlapping_store_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %local : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// fp32_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @fp32_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf32> * tensor<16x16xf32> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// unsafe_speculation_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @unsafe_speculation_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.addi %c2, %pid : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %den = arith.subi %upper, %iv : i32 + %quotient = arith.divsi %count, %den : i32 + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// captured_combiner_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @captured_combiner_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %reduced = "tt.reduce"(%loop) <{axis = 0 : i32}> ({ + ^bb0(%reduce_a: f32, %reduce_b: f32): + %sum = arith.addf %reduce_a, %reduce_b : f32 + %captured = arith.sitofp %count : i32 to f32 + %biased = arith.addf %sum, %captured : f32 + tt.reduce.return %biased : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + %expanded = tt.expand_dims %reduced {axis = 0 : i32} : tensor<16xf32> -> tensor<1x16xf32> + %output = tt.broadcast %expanded : tensor<1x16xf32> -> tensor<16x16xf32> + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %output : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// scalar_select_preflight_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @scalar_select_preflight_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %condition = arith.cmpi slt, %pid, %count : i32 + %selected = arith.select %condition, %loop, %loop : tensor<16x16xf32> + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %selected : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// row_rhs_load_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @row_rhs_load_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// loop_carried_rhs_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @loop_carried_rhs_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = arith.truncf %acc0 : tensor<16x16xf32> to tensor<16x16xf16> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// row_dependent_load_mask_fallback +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @row_dependent_load_mask_fallback(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %active = arith.cmpi slt, %pid, %iv : i32 + %mask = tt.splat %active : i1 -> tensor<16x16xi1> + %zero = arith.constant dense<0.0> : tensor<16x16xf16> + %b = tt.load %pb, %mask, %zero : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// Reductions are unsupported, even with self-contained combiners. +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @loop_reduction(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + %reduced = "tt.reduce"(%dot0_0) <{axis = 1 : i32}> ({ + ^bb0(%reduce_a: f32, %reduce_b: f32): + %sum = arith.addf %reduce_a, %reduce_b : f32 + tt.reduce.return %sum : f32 + }) : (tensor<16x16xf32>) -> tensor<16xf32> + %expanded = tt.expand_dims %reduced {axis = 1 : i32} : tensor<16xf32> -> tensor<16x1xf32> + %output = tt.broadcast %expanded : tensor<16x1xf32> -> tensor<16x16xf32> + scf.yield %output : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} +// ----- + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @scalar_epilogue(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %flat = tt.reshape %loop : tensor<16x16xf32> -> tensor<256xf32> + %reduced = "tt.reduce"(%flat) <{axis = 0 : i32}> ({ + ^bb0(%reduce_a: f32, %reduce_b: f32): + %sum = arith.addf %reduce_a, %reduce_b : f32 + tt.reduce.return %sum : f32 + }) : (tensor<256xf32>) -> f32 + %biased = arith.addf %reduced, %reduced : f32 + %output = tt.splat %biased : f32 -> tensor<16x16xf32> + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %output : tensor<16x16x!tt.ptr> + tt.return + } +} +// ----- + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @tuple_reduction(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %pair:2 = "tt.reduce"(%loop, %loop) <{axis = 1 : i32}> ({ + ^bb0(%reduce_a: f32, %reduce_b: f32, %reduce_c: f32, %reduce_d: f32): + %sum = arith.addf %reduce_a, %reduce_c : f32 + %max = arith.maximumf %reduce_b, %reduce_d : f32 + tt.reduce.return %sum, %max : f32, f32 + }) : (tensor<16x16xf32>, tensor<16x16xf32>) -> (tensor<16xf32>, tensor<16xf32>) + %combined = arith.addf %pair#0, %pair#1 : tensor<16xf32> + %expanded = tt.expand_dims %combined {axis = 1 : i32} : tensor<16xf32> -> tensor<16x1xf32> + %output = tt.broadcast %expanded : tensor<16x1xf32> -> tensor<16x16xf32> + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %output : tensor<16x16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-operation-rewrite.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-operation-rewrite.mlir new file mode 100644 index 0000000000..6c6c3c7b52 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-operation-rewrite.mlir @@ -0,0 +1,65 @@ +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=8' -canonicalize -cse -o %t +// RUN: FileCheck %s < %t +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=8 compile-mode=simt_only' -canonicalize -cse -o %t.simt +// RUN: triton-opt --split-input-file --verify-each %t -graph-optimize='rule-mask=8' -canonicalize -cse -o %t.twice +// RUN: diff %t %t.twice +// RUN: triton-opt --split-input-file --verify-each %s -graph-optimize='rule-mask=0' -canonicalize -cse -o %t.disabled +// RUN: triton-opt --split-input-file %s -canonicalize -cse -o %t.control +// RUN: diff %t.control %t.disabled +// RUN: diff %t.control %t.simt + +// CHECK: module attributes {{.*}}hacc.coalesce_factor = 8 : i32 +// CHECK-LABEL: tt.func @reshape_transpose( +// CHECK: tt.load {{.*}} : tensor<8x16x16x!tt.ptr> +// CHECK: scf.for +// CHECK: %[[RHS:.*]] = tt.load {{.*}} : tensor<16x16x!tt.ptr> +// CHECK: tt.dot %{{.*}}, %[[RHS]], %{{.*}} : tensor<128x16xf16> * tensor<16x16xf16> -> tensor<128x16xf32> +// CHECK: tt.trans {{.*}}order = array +// CHECK: tt.store {{.*}} : tensor<8x16x16x!tt.ptr> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">, hacc.grid_num_tiles = 64 : i32} { + tt.func @reshape_transpose(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %view = tt.reshape %loop : tensor<16x16xf32> -> tensor<4x64xf32> + %transposed = tt.trans %view {order = array} : tensor<4x64xf32> -> tensor<64x4xf32> + %output = tt.reshape %transposed : tensor<64x4xf32> -> tensor<16x16xf32> + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %output : tensor<16x16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-runtime-grid.mlir b/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-runtime-grid.mlir new file mode 100644 index 0000000000..fa8eb3c386 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToGraph/row-coalescing-dot-runtime-grid.mlir @@ -0,0 +1,146 @@ +// RUN: triton-opt --split-input-file %s -graph-optimize='rule-mask=8' -canonicalize -cse --verify-each -o %t +// RUN: FileCheck %s < %t +// RUN: triton-opt --split-input-file %t -graph-optimize='rule-mask=8' -canonicalize -cse --verify-each -o %t.twice +// RUN: diff %t %t.twice +// RUN: triton-opt --split-input-file %s -graph-optimize='rule-mask=8 compile-mode=simt_only' -canonicalize -cse -o %t.simt +// RUN: triton-opt --split-input-file %s -canonicalize -cse -o %t.control +// RUN: diff %t.control %t.simt + +// No static grid hint is required. The launcher supplies the original extent +// and shrinks the x-grid; per-row memory masks protect a partial final group. +// CHECK: hacc.program_grid_transforms = {{.*}}factor = 8 : i64 +// CHECK-LABEL: tt.func @runtime_grid( +// CHECK-SAME: %[[EXTENT:arg[0-9]+]]: i32, %{{arg[0-9]+}}: i32) +// CHECK: %[[LIMIT:.*]] = tt.splat %[[EXTENT]] : i32 -> tensor<8xi32> +// CHECK: arith.cmpi slt, {{.*}}, %[[LIMIT]] : tensor<8xi32> +// CHECK: tt.load {{.*}}, %{{.*}}, %{{.*}} : tensor<8x16x16x!tt.ptr> +// CHECK: tt.dot {{.*}} : tensor<128x16xf16> * tensor<16x16xf16> -> tensor<128x16xf32> +// CHECK: tt.store {{.*}}, %{{.*}}, %{{.*}} : tensor<8x16x16x!tt.ptr> + + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func @runtime_grid(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %a = tt.load %pa : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.constant 2 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop : tensor<16x16x!tt.ptr> + tt.return + } +} + +// ----- + +// Dynamic tail rows must preserve the original memory mask and clamp the +// last row before evaluating the shared pid-dependent loop bound. +// CHECK-LABEL: tt.func @runtime_grid_masked_bounds( +// CHECK-SAME: %[[COLUMNS:arg[0-9]+]]: i32, %[[GRID:arg[0-9]+]]: i32, %{{arg[0-9]+}}: i32) +// CHECK-DAG: %[[ONE:.*]] = arith.constant 1 : i32 +// CHECK-DAG: %[[ONES:.*]] = arith.constant dense<1> : tensor<8xi32> +// CHECK: %[[END:.*]] = arith.subi %[[GRID]], %[[ONE]] : i32 +// CHECK: %[[LAST:.*]] = arith.minsi %{{.*}}, %[[END]] : i32 +// CHECK: %[[ROWS:.*]] = arith.addi {{.*}} : tensor<8xi32> +// CHECK: %[[GRID_TILE:.*]] = tt.splat %[[GRID]] : i32 -> tensor<8xi32> +// CHECK: %[[VALID:.*]] = arith.cmpi slt, %[[ROWS]], %[[GRID_TILE]] : tensor<8xi32> +// CHECK: %[[COUNT:.*]] = tt.splat %[[COLUMNS]] : i32 -> tensor<16x16xi32> +// CHECK: %[[ORIGINAL:.*]] = arith.cmpi slt, %{{.*}}, %[[COUNT]] : tensor<16x16xi32> +// CHECK: %[[VALID1:.*]] = tt.expand_dims %[[VALID]] {axis = 1 : i32} +// CHECK: %[[VALID2:.*]] = tt.expand_dims %[[VALID1]] {axis = 2 : i32} +// CHECK: %[[VALID_TILE:.*]] = tt.broadcast %[[VALID2]] +// CHECK: %[[ORIGINAL1:.*]] = tt.expand_dims %[[ORIGINAL]] {axis = 0 : i32} +// CHECK: %[[ORIGINAL_TILE:.*]] = tt.broadcast %[[ORIGINAL1]] +// CHECK: %[[MASK:.*]] = arith.andi %[[VALID_TILE]], %[[ORIGINAL_TILE]] : tensor<8x16x16xi1> +// CHECK: tt.load %{{.*}}, %[[MASK]], %{{.*}} : tensor<8x16x16x!tt.ptr> +// CHECK: %[[UPPER:.*]] = arith.addi %[[LAST]], %[[ONE]] : i32 +// CHECK: %[[ROW_UPPER:.*]] = arith.addi %[[ROWS]], %[[ONES]] : tensor<8xi32> +// CHECK: %[[RESULT:.*]] = scf.for %[[IV:.*]] = %{{.*}} to %[[UPPER]] step %[[ONE]] iter_args(%[[ACC:.*]] = %{{.*}}) +// CHECK: %[[IV_TILE:.*]] = tt.splat %[[IV]] : i32 -> tensor<8xi32> +// CHECK: %[[ACTIVE:.*]] = arith.cmpi slt, %[[IV_TILE]], %[[ROW_UPPER]] : tensor<8xi32> +// CHECK: %[[ACTIVE1:.*]] = tt.expand_dims %[[ACTIVE]] {axis = 1 : i32} +// CHECK: %[[ACTIVE2:.*]] = tt.expand_dims %[[ACTIVE1]] {axis = 2 : i32} +// CHECK: %[[ACTIVE_TILE:.*]] = tt.broadcast %[[ACTIVE2]] +// CHECK: %[[NEXT:.*]] = arith.select %[[ACTIVE_TILE]], %{{.*}}, %[[ACC]] +// CHECK: scf.yield %[[NEXT]] +// CHECK: tt.store %{{.*}}, %[[RESULT]], %[[MASK]] : tensor<8x16x16x!tt.ptr> + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func @runtime_grid_masked_bounds(%A: !tt.ptr, %B: !tt.ptr, %C: !tt.ptr, %count: i32) { + %pid = tt.get_program_id x : i32 + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %c2 = arith.constant 2 : i32 + %stride = arith.constant 256 : i32 + %base = arith.muli %pid, %stride : i32 + %r = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %c = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %r2 = tt.expand_dims %r {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %c2d = tt.expand_dims %c {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %sixteen = arith.constant dense<16> : tensor<16x1xi32> + %rs = arith.muli %r2, %sixteen : tensor<16x1xi32> + %rr = tt.broadcast %rs : tensor<16x1xi32> -> tensor<16x16xi32> + %cc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %local = arith.addi %rr, %cc : tensor<16x16xi32> + %bases = tt.splat %base : i32 -> tensor<16x16xi32> + %offset = arith.addi %bases, %local : tensor<16x16xi32> + %ap = tt.splat %A : !tt.ptr -> tensor<16x16x!tt.ptr> + %pa = tt.addptr %ap, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %count_tile = tt.splat %count : i32 -> tensor<16x16xi32> + %mask = arith.cmpi slt, %cc, %count_tile : tensor<16x16xi32> + %zero = arith.constant dense<0.0> : tensor<16x16xf16> + %a = tt.load %pa, %mask, %zero : tensor<16x16x!tt.ptr> + %br = tt.expand_dims %c {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %bs = arith.constant dense<16> : tensor<16x1xi32> + %bro = arith.muli %br, %bs : tensor<16x1xi32> + %brr = tt.broadcast %bro : tensor<16x1xi32> -> tensor<16x16xi32> + %bcc = tt.broadcast %c2d : tensor<1x16xi32> -> tensor<16x16xi32> + %boff = arith.addi %brr, %bcc : tensor<16x16xi32> + %bp = tt.splat %B : !tt.ptr -> tensor<16x16x!tt.ptr> + %pb = tt.addptr %bp, %boff : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %upper = arith.addi %pid, %c1 : i32 + %zero0 = arith.constant dense<0.0> : tensor<16x16xf32> + %loop = scf.for %iv = %c0 to %upper step %c1 iter_args(%acc0 = %zero0) -> (tensor<16x16xf32>) : i32 { + %b = tt.load %pb : tensor<16x16x!tt.ptr> + %dot0_0 = tt.dot %a, %b, %acc0 : tensor<16x16xf16> * tensor<16x16xf16> -> tensor<16x16xf32> + scf.yield %dot0_0 : tensor<16x16xf32> + } + + %cp = tt.splat %C : !tt.ptr -> tensor<16x16x!tt.ptr> + %pc = tt.addptr %cp, %offset : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %pc, %loop, %mask : tensor<16x16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToHFusion/fp_to_fp_rtz.mlir b/third_party/ascend/unittest/Conversion/General/TritonToHFusion/fp_to_fp_rtz.mlir new file mode 100644 index 0000000000..fbff6cb341 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToHFusion/fp_to_fp_rtz.mlir @@ -0,0 +1,18 @@ +// RUN: triton-opt %s -triton-to-hfusion | FileCheck %s + +// CHECK-LABEL: tt.func @test_fp32_to_fp16_rtz +tt.func @test_fp32_to_fp16_rtz(%arg0: tensor<1024xf32>) -> tensor<1024xf16> { + // CHECK: %[[EMPTY:.*]] = tensor.empty() : tensor<1024xf16> + // CHECK: %[[RESULT:.*]] = hfusion.cast {mode = #hfusion.round_mode} ins(%arg0 : tensor<1024xf32>) outs(%[[EMPTY]] : tensor<1024xf16>) -> tensor<1024xf16> + %0 = tt.fp_to_fp %arg0, rounding = rtz : tensor<1024xf32> -> tensor<1024xf16> + // CHECK: return %[[RESULT]] + tt.return %0 : tensor<1024xf16> +} + + +// CHECK-LABEL: tt.func @test_fp32_to_fp16_rtz_fail +tt.func @test_fp32_to_fp16_rtz_fail(%arg0: tensor<1024xf32>) -> tensor<1024xf16> { + %0 = tt.fp_to_fp %arg0, rounding = rtne : tensor<1024xf32> -> tensor<1024xf16> + // CHECK: %{{.*}} = tt.fp_to_fp %arg0, rounding = rtne : tensor<1024xf32> -> tensor<1024xf16> + tt.return %0 : tensor<1024xf16> +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToHFusion/histogram.mlir b/third_party/ascend/unittest/Conversion/General/TritonToHFusion/histogram.mlir new file mode 100644 index 0000000000..5d51b5ec34 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToHFusion/histogram.mlir @@ -0,0 +1,11 @@ +// RUN: triton-opt %s -triton-to-hfusion | FileCheck %s + + +// CHECK-LABEL: tt.func @test_histogram +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func @test_histogram(%arg0 : tensor<16xi32>) -> tensor<2xi32> { + // CHECK: hfusion.histogram %{{.*}}, 2 + %res = tt.histogram %arg0 : tensor<16xi32> -> tensor<2xi32> + tt.return %res : tensor<2xi32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToHFusion/histogram_expect_fail.mlir b/third_party/ascend/unittest/Conversion/General/TritonToHFusion/histogram_expect_fail.mlir new file mode 100644 index 0000000000..1ecc9ec6c6 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToHFusion/histogram_expect_fail.mlir @@ -0,0 +1,20 @@ +// RUN: ! triton-opt %s -triton-to-hfusion 2>&1 | FileCheck %s --check-prefix=CHECK-FAIL-ZERO +// Expected failure case, only for branch coverage +// CHECK-FAIL-ZERO: error: 'hfusion.histogram' op output length (0) must equal num_bins +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func @test_histogram_fail_with_zero_n(%arg0 : tensor<16xi32>) -> tensor<0xi32> { + %res = tt.histogram %arg0 : tensor<16xi32> -> tensor<0xi32> + tt.return %res : tensor<0xi32> + } +} + + +// RUN: ! triton-opt %s -triton-to-hfusion 2>&1 | FileCheck %s --check-prefix=CHECK-FAIL-DYN +// Expected failure case, only for branch coverage +// CHECK-FAIL-DYN: error: 'hfusion.histogram' op output must have static shape +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func @test_histogram_fail_with_dyn_n(%arg0 : tensor<16xi32>) -> tensor { + %res = tt.histogram %arg0 : tensor<16xi32> -> tensor + tt.return %res : tensor + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToHFusion/mod.mlir b/third_party/ascend/unittest/Conversion/General/TritonToHFusion/mod.mlir new file mode 100644 index 0000000000..d4c264913a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToHFusion/mod.mlir @@ -0,0 +1,11 @@ +// RUN: triton-opt %s -triton-to-hfusion | FileCheck %s + +// CHECK: tensor.empty() : tensor<1xf32> +// CHECK: hfusion.elemwise_binary {fun = #hfusion.binary_fn} ins(%arg0, %arg1 : tensor<1xf32>, tensor<1xf32>) outs(%0 : tensor<1xf32>) -> tensor<1xf32> + +module { + tt.func @test_mod(%arg0: tensor<1xf32>, %arg1: tensor<1xf32>) -> tensor<1xf32> { + %0 = ascend.mod %arg0, %arg1 : tensor<1xf32> tensor<1xf32> -> tensor<1xf32> + tt.return %0 : tensor<1xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToHIVM/sync_block_op_conversion.mlir b/third_party/ascend/unittest/Conversion/General/TritonToHIVM/sync_block_op_conversion.mlir new file mode 100644 index 0000000000..9f83ec41af --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToHIVM/sync_block_op_conversion.mlir @@ -0,0 +1,20 @@ +// RUN: triton-opt %s --triton-to-hivm | FileCheck %s + +// CHECK-LABEL: tt.func @triton_func +tt.func @triton_func() { + ascend.custom "sync_block_set" {str_args = ["vector", 1 : i32]} + ascend.custom "sync_block_wait" {str_args = ["vector", 1 : i32]} + ascend.custom "sync_block_set" {str_args = ["cube", 2 : i32]} + ascend.custom "sync_block_wait" {str_args = ["cube", 2 : i32]} + ascend.custom "sync_block_all" {str_args = ["all_cube", 1 : i32]} + ascend.custom "sync_block_all" {str_args = ["all_vector", 1 : i32]} + ascend.custom "sync_block_all" {str_args = ["all", 1 : i32]} + tt.return +} +// CHECK: hivm.hir.sync_block_set[, , ] flag = 1 +// CHECK: hivm.hir.sync_block_wait[, , ] flag = 1 +// CHECK: hivm.hir.sync_block_set[, , ] flag = 2 +// CHECK: hivm.hir.sync_block_wait[, , ] flag = 2 +// CHECK: hivm.hir.sync_block[, 1 : i16] tcube_pipe = cube_pipe = +// CHECK: hivm.hir.sync_block[, 1 : i16] tvector_pipe = vector_pipe = +// CHECK: hivm.hir.sync_block[, 1 : i16] tcube_pipe = cube_pipe = tvector_pipe = vector_pipe = diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLLVM/normalize_debug_line_locations.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLLVM/normalize_debug_line_locations.mlir new file mode 100644 index 0000000000..e03375f99b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLLVM/normalize_debug_line_locations.mlir @@ -0,0 +1,868 @@ +// RUN: triton-opt %s -split-input-file --normalize-debug-line-locations --allow-unregistered-dialect --mlir-print-debuginfo | FileCheck %s + +//===----------------------------------------------------------------------===// +// control_ops +// +// Verifies that control-flow operations remain user-visible debug anchors. +// cf.br, scf.if, scf.for, and func.return must be classified as +// "control" and must keep their original real source locations. +//===----------------------------------------------------------------------===// + +module { +func.func @control_ops(%cond: i1) { +cf.br ^bb1 loc("control.py":2:3) +^bb1: +scf.if %cond { +} else { +} loc("control.py":3:5) +%c0 = arith.constant 0 : index loc("control.py":4:5) +%c1 = arith.constant 1 : index loc("control.py":4:9) +scf.for %i = %c0 to %c1 step %c1 { +scf.yield loc("control.py":5:9) +} loc("control.py":5:5) +func.return loc("control.py":6:3) +} +} + +// CHECK-LABEL: func.func @control_ops +// CHECK: cf.br +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: loc(#[[CONTROL_BR_LOC:[A-Za-z0-9_]+]]) +// CHECK: scf.if +// CHECK: } {triton.debug_line.class = "control"} loc(#[[CONTROL_IF_LOC:[A-Za-z0-9_]+]]) +// CHECK: scf.for +// CHECK: } {triton.debug_line.class = "control"} loc(#[[CONTROL_FOR_LOC:[A-Za-z0-9_]+]]) +// CHECK: return +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: loc(#[[CONTROL_RETURN_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[CONTROL_BR_LOC]] = loc("control.py":2:3) +// CHECK-DAG: #[[CONTROL_IF_LOC]] = loc("control.py":3:5) +// CHECK-DAG: #[[CONTROL_FOR_LOC]] = loc("control.py":5:5) +// CHECK-DAG: #[[CONTROL_RETURN_LOC]] = loc("control.py":6:3) + +// ----- + +//===----------------------------------------------------------------------===// +// synthetic_glue +// +// Verifies the default handling of helper/glue operations that do not have a +// unique user-visible store anchor. unrealized_conversion_cast and +// memref.subview must be classified as "synthetic", preserve their original +// location in triton.debug_line.origin, and move to glue.py:0:0. The +// surrounding control operations must remain at their real source locations. +//===----------------------------------------------------------------------===// + +module { +func.func @synthetic_glue(%arg0: memref<8xf32>, %idx: index, %cond: i1) { +%cast = builtin.unrealized_conversion_cast %idx : index to i64 loc("glue.py":2:5) +%sub = memref.subview %arg0[%idx] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> loc("glue.py":3:5) +scf.if %cond { +} loc("glue.py":4:5) +func.return loc("glue.py":5:3) +} +} + +// CHECK-DAG: #[[$GLUE_CAST_ORIGIN:[A-Za-z0-9_]+]] = loc("glue.py":2:5) +// CHECK-DAG: #[[$GLUE_SUBVIEW_ORIGIN:[A-Za-z0-9_]+]] = loc("glue.py":3:5) + +// CHECK-LABEL: func.func @synthetic_glue +// CHECK: builtin.unrealized_conversion_cast +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$GLUE_CAST_ORIGIN]] +// CHECK-SAME: loc(#[[GLUE_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: memref.subview +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$GLUE_SUBVIEW_ORIGIN]] +// CHECK-SAME: : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> +// CHECK-SAME: loc(#[[GLUE_SYNTH_LOC]]) +// CHECK: scf.if +// CHECK: } {triton.debug_line.class = "control"} loc(#[[GLUE_IF_LOC:[A-Za-z0-9_]+]]) +// CHECK: return +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: loc(#[[GLUE_RETURN_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[GLUE_SYNTH_LOC]] = loc("glue.py":0:0) +// CHECK-DAG: #[[GLUE_IF_LOC]] = loc("glue.py":4:5) +// CHECK-DAG: #[[GLUE_RETURN_LOC]] = loc("glue.py":5:3) + +// ----- + +//===----------------------------------------------------------------------===// +// floating_point_conversion_classification +// +// Verifies that an arith.extf/truncf on its own source line remains semantic, +// while the same operations are synthetic when a load/store already provides +// the semantic anchor for that source line. +//===----------------------------------------------------------------------===// + +module { +func.func @floating_point_conversion_classification(%src: memref<4xf16>, %dst: memref<4xf16>, %wide: f32) { +%c0 = arith.constant 0 : index loc("fp_conversion.py":10:3) +%loaded = memref.load %src[%c0] : memref<4xf16> loc("fp_conversion.py":10:5) +%aux_ext = arith.extf %loaded : f16 to f32 loc("fp_conversion.py":10:20) +%standalone_ext = arith.extf %loaded : f16 to f32 loc("fp_conversion.py":11:5) +%standalone_trunc = arith.truncf %standalone_ext : f32 to f16 loc("fp_conversion.py":12:5) +%aux_trunc = arith.truncf %wide : f32 to f16 loc("fp_conversion.py":13:5) +memref.store %aux_trunc, %dst[%c0] : memref<4xf16> loc("fp_conversion.py":13:20) +func.return loc("fp_conversion.py":14:3) +} +} + +// CHECK-DAG: #[[$FP_AUX_EXT_ORIGIN:[A-Za-z0-9_]+]] = loc("fp_conversion.py":10:20) +// CHECK-DAG: #[[$FP_AUX_TRUNC_ORIGIN:[A-Za-z0-9_]+]] = loc("fp_conversion.py":13:5) + +// CHECK-LABEL: func.func @floating_point_conversion_classification +// CHECK: %[[FP_LOADED:[A-Za-z0-9_]+]] = memref.load +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[FP_LOAD_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[FP_AUX_EXT:[A-Za-z0-9_]+]] = arith.extf %[[FP_LOADED]] +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$FP_AUX_EXT_ORIGIN]] +// CHECK-SAME: loc(#[[FP_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[FP_STANDALONE_EXT:[A-Za-z0-9_]+]] = arith.extf %[[FP_LOADED]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[FP_STANDALONE_EXT_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[FP_STANDALONE_TRUNC:[A-Za-z0-9_]+]] = arith.truncf %[[FP_STANDALONE_EXT]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[FP_STANDALONE_TRUNC_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[FP_AUX_TRUNC:[A-Za-z0-9_]+]] = arith.truncf +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$FP_AUX_TRUNC_ORIGIN]] +// CHECK-SAME: loc(#[[FP_SYNTH_LOC]]) +// CHECK: memref.store %[[FP_AUX_TRUNC]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[FP_STORE_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[FP_SYNTH_LOC]] = loc("fp_conversion.py":0:0) +// CHECK-DAG: #[[FP_LOAD_LOC]] = loc("fp_conversion.py":10:5) +// CHECK-DAG: #[[FP_STANDALONE_EXT_LOC]] = loc("fp_conversion.py":11:5) +// CHECK-DAG: #[[FP_STANDALONE_TRUNC_LOC]] = loc("fp_conversion.py":12:5) +// CHECK-DAG: #[[FP_STORE_LOC]] = loc("fp_conversion.py":13:20) + +// ----- + +//===----------------------------------------------------------------------===// +// source_line_write_anchor +// +// Verifies the main write-anchor normalization case modeled after the loop +// reordering problem. Address/view computations are retargeted only when their +// original source line matches the user-visible store line. The ticket index is +// produced on line 23 and must therefore remain synthetic instead of being +// retargeted to the store on line 24. Same-line view and tensor.insert paths may +// still be retargeted. The test also covers NameLoc preservation for the load. +//===----------------------------------------------------------------------===// + +module { +func.func @source_line_write_anchor(%trace: memref<8xi32>, %counter: memref<1xi32>) { +%c0 = arith.constant 0 : index loc("loop_reordering.py":23:21) +%ticket = memref.load %counter[%c0] : memref<1xi32> loc("ticket"("loop_reordering.py":23:21)) +%ticket_idx = arith.index_cast %ticket : i32 to index loc("ticket"("loop_reordering.py":23:21)) +%c200_i32 = arith.constant 200 : i32 loc("loop_reordering.py":24:33) +%empty = tensor.empty() : tensor<1xi32> loc("loop_reordering.py":24:33) +%filled = linalg.fill ins(%c200_i32 : i32) outs(%empty : tensor<1xi32>) -> tensor<1xi32> loc("loop_reordering.py":24:33) +%view = memref.reinterpret_cast %trace to offset: [%ticket_idx], sizes: [1], strides: [1] : memref<8xi32> to memref<1xi32, strided<[1], offset: ?>> loc("loop_reordering.py":24:33) +bufferization.materialize_in_destination %filled in writable %view : (tensor<1xi32>, memref<1xi32, strided<[1], offset: ?>>) -> () loc("loop_reordering.py":24:33) +%c1_i32 = arith.constant 1 : i32 loc("loop_reordering.py":25:35) +%next_ticket = arith.addi %ticket, %c1_i32 : i32 loc("loop_reordering.py":25:35) +%inserted = tensor.insert %next_ticket into %empty[%c0] : tensor<1xi32> loc("loop_reordering.py":25:26) +bufferization.materialize_in_destination %inserted in writable %counter : (tensor<1xi32>, memref<1xi32>) -> () loc("loop_reordering.py":25:26) +func.return loc("loop_reordering.py":26:3) +} +} + +// CHECK-DAG: #[[$LOOP_TICKET_FILE_LOC:[A-Za-z0-9_]+]] = loc("loop_reordering.py":23:21) +// CHECK-DAG: #[[$LOOP_TICKET_LOC:[A-Za-z0-9_]+]] = loc("ticket"(#[[$LOOP_TICKET_FILE_LOC]])) +// CHECK-DAG: #[[$LOOP_WRITE_LOC:[A-Za-z0-9_]+]] = loc("loop_reordering.py":24:33) +// CHECK-DAG: #[[$LOOP_NEXT_VALUE_LOC:[A-Za-z0-9_]+]] = loc("loop_reordering.py":25:35) + +// CHECK-LABEL: func.func @source_line_write_anchor +// CHECK: %[[LOOP_TICKET:[A-Za-z0-9_]+]] = memref.load +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: : memref<1xi32> +// CHECK-SAME: loc(#[[$LOOP_TICKET_LOC]]) + +// CHECK: %[[LOOP_TICKET_IDX:[A-Za-z0-9_]+]] = arith.index_cast %[[LOOP_TICKET]] +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$LOOP_TICKET_LOC]] +// CHECK-SAME: : i32 to index +// CHECK-SAME: loc(#[[LOOP_SYNTH_LOC:[A-Za-z0-9_]+]]) + +// CHECK: %[[LOOP_C200:[A-Za-z0-9_]+]] = arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$LOOP_WRITE_LOC]] +// CHECK-SAME: 200 : i32 +// CHECK-SAME: loc(#[[LOOP_SYNTH_LOC]]) + +// CHECK: %[[LOOP_EMPTY:[A-Za-z0-9_]+]] = tensor.empty +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$LOOP_WRITE_LOC]] +// CHECK-SAME: tensor<1xi32> +// CHECK-SAME: loc(#[[LOOP_SYNTH_LOC]]) + +// CHECK: %[[LOOP_FILLED:[A-Za-z0-9_]+]] = linalg.fill +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$LOOP_WRITE_LOC]] +// CHECK-SAME: loc(#[[LOOP_SYNTH_LOC]]) + +// CHECK: %[[LOOP_VIEW:[A-Za-z0-9_]+]] = memref.reinterpret_cast +// CHECK-SAME: %[[LOOP_TICKET_IDX]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: triton.debug_line.origin = #[[$LOOP_WRITE_LOC]] +// CHECK-SAME: memref<8xi32> to memref<1xi32, strided<[1], offset: ?>> +// CHECK-SAME: loc(#[[$LOOP_WRITE_LOC]]) + +// CHECK: bufferization.materialize_in_destination %[[LOOP_FILLED]] in writable %[[LOOP_VIEW]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[$LOOP_WRITE_LOC]]) + +// CHECK: %[[LOOP_C1:[A-Za-z0-9_]+]] = arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$LOOP_NEXT_VALUE_LOC]] +// CHECK-SAME: 1 : i32 +// CHECK-SAME: loc(#[[LOOP_SYNTH_LOC]]) + +// CHECK: %[[LOOP_NEXT:[A-Za-z0-9_]+]] = arith.addi %[[LOOP_TICKET]], %[[LOOP_C1]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: triton.debug_line.origin = #[[$LOOP_NEXT_VALUE_LOC]] +// CHECK-SAME: : i32 +// CHECK-SAME: loc(#[[LOOP_NEXT_STORE_LOC:[A-Za-z0-9_]+]]) + +// CHECK: %[[LOOP_INSERTED:[A-Za-z0-9_]+]] = tensor.insert %[[LOOP_NEXT]] into %[[LOOP_EMPTY]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: tensor<1xi32> +// CHECK-SAME: loc(#[[LOOP_NEXT_STORE_LOC]]) + +// CHECK: bufferization.materialize_in_destination %[[LOOP_INSERTED]] in writable +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[LOOP_NEXT_STORE_LOC]]) + +// CHECK-DAG: #[[LOOP_SYNTH_LOC]] = loc("loop_reordering.py":0:0) +// CHECK-DAG: #[[LOOP_NEXT_STORE_LOC]] = loc("loop_reordering.py":25:26) + +// ----- + +//===----------------------------------------------------------------------===// +// relocate_unique_load_debug_nop +// +// Verifies that a debug NOP is moved immediately before the unique memref.copy +// carrying the same source line. The helper constant originally between the +// NOP and the copy remains in place and becomes synthetic. +//===----------------------------------------------------------------------===// + +module { +func.func @relocate_unique_load_debug_nop(%src: memref<4xf32>, %dst: memref<4xf32>) { +llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc("load.py":10:5) +%c0 = arith.constant 0 : i32 loc("load.py":10:10) +memref.copy %src, %dst : memref<4xf32> to memref<4xf32> loc("load.py":10:20) +func.return loc("load.py":11:3) +} +} + +// CHECK-LABEL: func.func @relocate_unique_load_debug_nop +// CHECK: arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-NEXT: llvm.inline_asm +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[LOAD_NOP_LOC:[A-Za-z0-9_]+]]) +// CHECK-NEXT: memref.copy +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[LOAD_COPY_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[LOAD_NOP_LOC]] = loc("load.py":10:5) +// CHECK-DAG: #[[LOAD_COPY_LOC]] = loc("load.py":10:20) + +// ----- + +//===----------------------------------------------------------------------===// +// keep_ambiguous_load_debug_nop +// +// Verifies that a debug NOP is not moved when multiple memref.copy anchors use +// the same source line. Relocation is skipped because the matching load anchor +// is ambiguous. +//===----------------------------------------------------------------------===// + +module { +func.func @keep_ambiguous_load_debug_nop(%src0: memref<4xf32>, %src1: memref<4xf32>, %dst0: memref<4xf32>, %dst1: memref<4xf32>) { +llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc("ambiguous_load.py":20:5) +%c0 = arith.constant 0 : i32 loc("ambiguous_load.py":20:10) +memref.copy %src0, %dst0 : memref<4xf32> to memref<4xf32> loc("ambiguous_load.py":20:20) +memref.copy %src1, %dst1 : memref<4xf32> to memref<4xf32> loc("ambiguous_load.py":20:30) +func.return loc("ambiguous_load.py":21:3) +} +} + +// CHECK-LABEL: func.func @keep_ambiguous_load_debug_nop +// CHECK: llvm.inline_asm +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[AMBIGUOUS_LOAD_NOP_LOC:[A-Za-z0-9_]+]]) +// CHECK-NEXT: arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-NEXT: memref.copy +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-NEXT: memref.copy +// CHECK-SAME: triton.debug_line.class = "semantic" + +// CHECK-DAG: #[[AMBIGUOUS_LOAD_NOP_LOC]] = loc("ambiguous_load.py":20:5) + +// ----- + +//===----------------------------------------------------------------------===// +// demote_redundant_store_debug_nop +// +// Verifies that a debug NOP becomes synthetic when the same source line already +// has a materialize_in_destination store anchor. The store remains semantic and +// the redundant NOP is moved to store.py:0:0. +//===----------------------------------------------------------------------===// + +module { +func.func @demote_redundant_store_debug_nop(%src: tensor<4xf32>, %dst: memref<4xf32>) { +llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc("store.py":30:5) +bufferization.materialize_in_destination %src in writable %dst : (tensor<4xf32>, memref<4xf32>) -> () loc("store.py":30:20) +func.return loc("store.py":31:3) +} +} + +// CHECK-DAG: #[[$STORE_NOP_ORIGIN:[A-Za-z0-9_]+]] = loc("store.py":30:5) + +// CHECK-LABEL: func.func @demote_redundant_store_debug_nop +// CHECK: llvm.inline_asm +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$STORE_NOP_ORIGIN]] +// CHECK-SAME: loc(#[[STORE_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: bufferization.materialize_in_destination +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[STORE_ANCHOR_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[STORE_SYNTH_LOC]] = loc("store.py":0:0) +// CHECK-DAG: #[[STORE_ANCHOR_LOC]] = loc("store.py":30:20) + +// ----- + +//===----------------------------------------------------------------------===// +// memref_fill_anchor +// +// Verifies that linalg.fill writing directly to a memref is treated as a real +// semantic memory write anchor. The scalar constant used as the fill value is +// synthetic and is moved to memfill.py:0:0, while the memref fill keeps its +// user-visible source location. +//===----------------------------------------------------------------------===// + +module { +func.func @memref_fill_anchor(%dst: memref<4xf32>) { +%cst = arith.constant 0.000000e+00 : f32 loc("memfill.py":3:5) +linalg.fill ins(%cst : f32) outs(%dst : memref<4xf32>) loc("memfill.py":3:7) +func.return loc("memfill.py":4:3) +} +} + +// CHECK-DAG: #[[$MEMFILL_CONST_ORIGIN:[A-Za-z0-9_]+]] = loc("memfill.py":3:5) + +// CHECK-LABEL: func.func @memref_fill_anchor +// CHECK: arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$MEMFILL_CONST_ORIGIN]] +// CHECK-SAME: loc(#[[MEMFILL_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: linalg.fill +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: outs +// CHECK-SAME: loc(#[[MEMFILL_LOC:[A-Za-z0-9_]+]]) +// CHECK: return +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: loc(#[[MEMFILL_RETURN_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[MEMFILL_SYNTH_LOC]] = loc("memfill.py":0:0) +// CHECK-DAG: #[[MEMFILL_LOC]] = loc("memfill.py":3:7) +// CHECK-DAG: #[[MEMFILL_RETURN_LOC]] = loc("memfill.py":4:3) + +// ----- + +//===----------------------------------------------------------------------===// +// future_line_constant +// +// Verifies the backward-step heuristic and helper constant handling. Constants +// and reordered arithmetic that would otherwise introduce misleading source +// locations are classified as synthetic and moved to future.py:0:0, while the +// real load and return keep their visible locations. +//===----------------------------------------------------------------------===// + +module { +func.func @future_line_constant(%arg0: memref<4xf32>) { +%c0 = arith.constant 0 : index loc("future.py":8:5) +%c1 = arith.constant 1 : index loc("future.py":24:5) +%v = memref.load %arg0[%c0] : memref<4xf32> loc("future.py":24:7) +%sum = arith.addi %c0, %c1 : index loc("future.py":13:5) +func.return loc("future.py":14:3) +} +} + +// CHECK-DAG: #[[$FUTURE_C0_ORIGIN:[A-Za-z0-9_]+]] = loc("future.py":8:5) +// CHECK-DAG: #[[$FUTURE_C1_ORIGIN:[A-Za-z0-9_]+]] = loc("future.py":24:5) +// CHECK-DAG: #[[$FUTURE_ADDI_ORIGIN:[A-Za-z0-9_]+]] = loc("future.py":13:5) + +// CHECK-LABEL: func.func @future_line_constant +// CHECK: arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$FUTURE_C0_ORIGIN]] +// CHECK-SAME: 0 : index +// CHECK-SAME: loc(#[[FUTURE_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$FUTURE_C1_ORIGIN]] +// CHECK-SAME: 1 : index +// CHECK-SAME: loc(#[[FUTURE_SYNTH_LOC]]) +// CHECK: memref.load +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[FUTURE_LOAD_LOC:[A-Za-z0-9_]+]]) +// CHECK: arith.addi +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$FUTURE_ADDI_ORIGIN]] +// CHECK-SAME: : index +// CHECK-SAME: loc(#[[FUTURE_SYNTH_LOC]]) +// CHECK: return +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: loc(#[[FUTURE_RETURN_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[FUTURE_SYNTH_LOC]] = loc("future.py":0:0) +// CHECK-DAG: #[[FUTURE_LOAD_LOC]] = loc("future.py":24:7) +// CHECK-DAG: #[[FUTURE_RETURN_LOC]] = loc("future.py":14:3) + +// ----- + +//===----------------------------------------------------------------------===// +// llvm_constant +// +// Verifies LLVM dialect helper constants. llvm.mlir.constant is classified as +// synthetic, its original location is preserved in triton.debug_line.origin, +// and the operation location is rewritten to llvm.py:0:0. llvm.return remains +// a control operation at its real source location. +//===----------------------------------------------------------------------===// + +module { +llvm.func @llvm_constant() { +%0 = llvm.mlir.constant(7 : i32) : i32 loc("llvm.py":20:5) +llvm.return loc("llvm.py":21:3) +} +} + +// CHECK-DAG: #[[$LLVM_CONST_ORIGIN:[A-Za-z0-9_]+]] = loc("llvm.py":20:5) + +// CHECK-LABEL: llvm.func @llvm_constant +// CHECK: llvm.mlir.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$LLVM_CONST_ORIGIN]] +// CHECK-SAME: loc(#[[LLVM_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: llvm.return +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: loc(#[[LLVM_RETURN_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[LLVM_SYNTH_LOC]] = loc("llvm.py":0:0) +// CHECK-DAG: #[[LLVM_RETURN_LOC]] = loc("llvm.py":21:3) + +// ----- + +//===----------------------------------------------------------------------===// +// nameloc_is_source / fused_scope_is_preserved +// +// Verifies location canonicalization rules for semantic operations. A +// non-internal NameLoc such as "trace_ptr" must be preserved as source-like +// metadata. A fused location carrying an LLVM DIScope must also be preserved +// intact, so already materialized debug-scope metadata is not stripped. Helper +// constants in both functions still become synthetic zero-line operations. +//===----------------------------------------------------------------------===// + +#di_file = #llvm.di_file<"scope.py" in "/tmp"> +#di_cu = #llvm.di_compile_unit, sourceLanguage = DW_LANG_C, file = #di_file, producer = "triton", isOptimized = true, emissionKind = LineTablesOnly> +#di_sp = #llvm.di_subprogram + +module { +func.func @nameloc_is_source(%arg0: memref<4xf32>) { +%c0 = arith.constant 0 : index loc("name.py":2:5) +%v = memref.load %arg0[%c0] : memref<4xf32> loc("trace_ptr"("name.py":7:9)) +func.return loc("name.py":8:3) +} + +func.func @fused_scope_is_preserved(%arg0: memref<4xf32>) { +%c0 = arith.constant 0 : index loc("scope.py":2:5) +%v = memref.load %arg0[%c0] : memref<4xf32> loc(fused<#di_sp>["scope.py":7:9]) +func.return loc("scope.py":8:3) +} +} + +// CHECK-DAG: #[[$NAME_CONST_ORIGIN:[A-Za-z0-9_]+]] = loc("name.py":2:5) +// CHECK-DAG: #[[$SCOPE_CONST_ORIGIN:[A-Za-z0-9_]+]] = loc("scope.py":2:5) + +// CHECK-LABEL: func.func @nameloc_is_source +// CHECK: arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$NAME_CONST_ORIGIN]] +// CHECK-SAME: loc(#[[$NAME_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: memref.load +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[$TRACE_LOC:[A-Za-z0-9_]+]]) +// CHECK: return +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: loc(#[[$NAME_RETURN_LOC:[A-Za-z0-9_]+]]) + +// CHECK-LABEL: func.func @fused_scope_is_preserved +// CHECK: arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$SCOPE_CONST_ORIGIN]] +// CHECK-SAME: loc(#[[SCOPE_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: memref.load +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[FUSED_LOC:[A-Za-z0-9_]+]]) +// CHECK: return +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: loc(#[[SCOPE_RETURN_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[$NAME_SYNTH_LOC]] = loc("name.py":0:0) +// CHECK-DAG: #[[$NAME_FILE_LOC:[A-Za-z0-9_]+]] = loc("name.py":7:9) +// CHECK-DAG: #[[$TRACE_LOC]] = loc("trace_ptr"(#[[$NAME_FILE_LOC]])) +// CHECK-DAG: #[[$NAME_RETURN_LOC]] = loc("name.py":8:3) +// CHECK-DAG: #[[SCOPE_SYNTH_LOC]] = loc("scope.py":0:0) +// CHECK-DAG: #[[SCOPE_FILE_LOC:[A-Za-z0-9_]+]] = loc("scope.py":7:9) +// CHECK-DAG: #[[FUSED_LOC]] = loc(fused<{{.*}}>[#[[SCOPE_FILE_LOC]]]) +// CHECK-DAG: #[[SCOPE_RETURN_LOC]] = loc("scope.py":8:3) + +// ----- + +//===----------------------------------------------------------------------===// +// shape_is_unchanged +// +// Verifies that the pass does not change IR semantics or structure. The test +// captures function arguments, result types, operands, produced values, and the +// returned value. Only operation locations and diagnostic debug-line +// attributes are expected to change. +//===----------------------------------------------------------------------===// + +module { +func.func @shape_is_unchanged(%arg0: memref<8xf32>, %arg1: memref<8xf32>, %idx: index) -> i32 { +%sub = memref.subview %arg0[%idx] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> loc("shape.py":2:5) +%v = memref.load %arg1[%idx] : memref<8xf32> loc("shape.py":3:5) +%c1 = arith.constant 1 : i32 loc("shape.py":4:5) +%r = arith.addi %c1, %c1 : i32 loc("shape.py":5:5) +return %r : i32 loc("shape.py":6:3) +} +} + +// CHECK-DAG: #[[$SHAPE_SUBVIEW_ORIGIN:[A-Za-z0-9_]+]] = loc("shape.py":2:5) +// CHECK-DAG: #[[$SHAPE_CONST_ORIGIN:[A-Za-z0-9_]+]] = loc("shape.py":4:5) + +// CHECK-LABEL: func.func @shape_is_unchanged( +// CHECK-SAME: %[[SHAPE_ARG0:[A-Za-z0-9_]+]]: memref<8xf32> +// CHECK-SAME: %[[SHAPE_ARG1:[A-Za-z0-9_]+]]: memref<8xf32> +// CHECK-SAME: %[[SHAPE_IDX:[A-Za-z0-9_]+]]: index +// CHECK-SAME: -> i32 +// CHECK: %[[SHAPE_SUB:[A-Za-z0-9_]+]] = memref.subview %[[SHAPE_ARG0]][%[[SHAPE_IDX]]] [4] [1] +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$SHAPE_SUBVIEW_ORIGIN]] +// CHECK-SAME: : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> +// CHECK-SAME: loc(#[[SHAPE_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[SHAPE_V:[A-Za-z0-9_]+]] = memref.load %[[SHAPE_ARG1]][%[[SHAPE_IDX]]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: : memref<8xf32> +// CHECK-SAME: loc(#[[SHAPE_LOAD_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[SHAPE_C1:[A-Za-z0-9_]+]] = arith.constant +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$SHAPE_CONST_ORIGIN]] +// CHECK-SAME: 1 : i32 +// CHECK-SAME: loc(#[[SHAPE_SYNTH_LOC]]) +// CHECK: %[[SHAPE_R:[A-Za-z0-9_]+]] = arith.addi %[[SHAPE_C1]], %[[SHAPE_C1]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: : i32 +// CHECK-SAME: loc(#[[SHAPE_ADDI_LOC:[A-Za-z0-9_]+]]) +// CHECK: return +// CHECK-SAME: triton.debug_line.class = "control" +// CHECK-SAME: %[[SHAPE_R]] : i32 +// CHECK-SAME: loc(#[[SHAPE_RETURN_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[SHAPE_SYNTH_LOC]] = loc("shape.py":0:0) +// CHECK-DAG: #[[SHAPE_LOAD_LOC]] = loc("shape.py":3:5) +// CHECK-DAG: #[[SHAPE_ADDI_LOC]] = loc("shape.py":5:5) +// CHECK-DAG: #[[SHAPE_RETURN_LOC]] = loc("shape.py":6:3) + +// ----- + +//===----------------------------------------------------------------------===// +// direct_destination_view_ambiguity +// +// Verifies deterministic behavior when one destination view has multiple +// direct materialize_in_destination users with distinct store locations. The +// view must not be retargeted based on getUsers() iteration order; it remains +// synthetic and moves to direct_ambiguity.py:0:0, while each store keeps its +// own semantic location. +//===----------------------------------------------------------------------===// + +module { +func.func @direct_destination_view_ambiguity(%src: tensor<4xf32>, %dst: memref<8xf32>, %idx: index) { +%view = memref.subview %dst[%idx] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> loc("direct_ambiguity.py":10:5) +bufferization.materialize_in_destination %src in writable %view : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("direct_ambiguity.py":10:20) +bufferization.materialize_in_destination %src in writable %view : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("direct_ambiguity.py":10:30) +func.return loc("direct_ambiguity.py":11:3) +} +} + +// CHECK-DAG: #[[$DIRECT_AMBIGUITY_ORIGIN:[A-Za-z0-9_]+]] = loc("direct_ambiguity.py":10:5) + +// CHECK-LABEL: func.func @direct_destination_view_ambiguity +// CHECK: %[[DIRECT_AMBIGUOUS_VIEW:[A-Za-z0-9_]+]] = memref.subview +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$DIRECT_AMBIGUITY_ORIGIN]] +// CHECK-SAME: loc(#[[DIRECT_AMBIGUITY_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[DIRECT_AMBIGUOUS_VIEW]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[DIRECT_STORE_0_LOC:[A-Za-z0-9_]+]]) +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[DIRECT_AMBIGUOUS_VIEW]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[DIRECT_STORE_1_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[DIRECT_AMBIGUITY_SYNTH_LOC]] = loc("direct_ambiguity.py":0:0) +// CHECK-DAG: #[[DIRECT_STORE_0_LOC]] = loc("direct_ambiguity.py":10:20) +// CHECK-DAG: #[[DIRECT_STORE_1_LOC]] = loc("direct_ambiguity.py":10:30) + +// ----- + +//===----------------------------------------------------------------------===// +// through_destination_view_ambiguity +// +// Verifies deterministic behavior when one operation feeds multiple +// destination views. The shared index_cast has two distinct store candidates +// through different subviews, so it must remain synthetic instead of choosing +// an arbitrary user. Each subview has a unique store and may be retargeted to +// its own semantic store location. +//===----------------------------------------------------------------------===// + +module { +func.func @through_destination_view_ambiguity(%src: tensor<4xf32>, %dst0: memref<8xf32>, %dst1: memref<8xf32>, %offset: i32) { +%idx = arith.index_cast %offset : i32 to index loc("through_view_ambiguity.py":20:5) +%view0 = memref.subview %dst0[%idx] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> loc("through_view_ambiguity.py":21:5) +bufferization.materialize_in_destination %src in writable %view0 : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("through_view_ambiguity.py":21:20) +%view1 = memref.subview %dst1[%idx] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> loc("through_view_ambiguity.py":22:5) +bufferization.materialize_in_destination %src in writable %view1 : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("through_view_ambiguity.py":22:20) +func.return loc("through_view_ambiguity.py":23:3) +} +} + +// CHECK-DAG: #[[$THROUGH_VIEW_AMBIGUITY_ORIGIN:[A-Za-z0-9_]+]] = loc("through_view_ambiguity.py":20:5) + +// CHECK-LABEL: func.func @through_destination_view_ambiguity +// CHECK: %[[THROUGH_VIEW_IDX:[A-Za-z0-9_]+]] = arith.index_cast +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$THROUGH_VIEW_AMBIGUITY_ORIGIN]] +// CHECK-SAME: loc(#[[THROUGH_VIEW_AMBIGUITY_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[THROUGH_VIEW_0:[A-Za-z0-9_]+]] = memref.subview {{.*}}[%[[THROUGH_VIEW_IDX]]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[THROUGH_VIEW_STORE_0_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[THROUGH_VIEW_1:[A-Za-z0-9_]+]] = memref.subview {{.*}}[%[[THROUGH_VIEW_IDX]]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[THROUGH_VIEW_STORE_1_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[THROUGH_VIEW_AMBIGUITY_SYNTH_LOC]] = loc("through_view_ambiguity.py":0:0) +// CHECK-DAG: #[[THROUGH_VIEW_STORE_0_LOC]] = loc("through_view_ambiguity.py":21:20) +// CHECK-DAG: #[[THROUGH_VIEW_STORE_1_LOC]] = loc("through_view_ambiguity.py":22:20) + +// ----- + +//===----------------------------------------------------------------------===// +// through_tensor_insert_ambiguity +// +// Verifies deterministic behavior for the tensor.insert retargeting path. The +// same arithmetic result is inserted into two tensors and reaches two distinct +// stores. Because there is no unique canonical store location, the arithmetic +// operation remains synthetic and moves to tensor_insert_ambiguity.py:0:0. +//===----------------------------------------------------------------------===// + +module { +func.func @through_tensor_insert_ambiguity(%lhs: f32, %rhs: f32, %base0: tensor<1xf32>, %base1: tensor<1xf32>, %dst0: memref<1xf32>, %dst1: memref<1xf32>, %idx: index) { +%sum = arith.addf %lhs, %rhs : f32 loc("tensor_insert_ambiguity.py":30:5) +%inserted0 = tensor.insert %sum into %base0[%idx] : tensor<1xf32> loc("tensor_insert_ambiguity.py":30:10) +bufferization.materialize_in_destination %inserted0 in writable %dst0 : (tensor<1xf32>, memref<1xf32>) -> () loc("tensor_insert_ambiguity.py":30:30) +%inserted1 = tensor.insert %sum into %base1[%idx] : tensor<1xf32> loc("tensor_insert_ambiguity.py":30:11) +bufferization.materialize_in_destination %inserted1 in writable %dst1 : (tensor<1xf32>, memref<1xf32>) -> () loc("tensor_insert_ambiguity.py":30:40) +func.return loc("tensor_insert_ambiguity.py":31:3) +} +} + +// CHECK-DAG: #[[$TENSOR_INSERT_AMBIGUITY_ORIGIN:[A-Za-z0-9_]+]] = loc("tensor_insert_ambiguity.py":30:5) + +// CHECK-LABEL: func.func @through_tensor_insert_ambiguity +// CHECK: %[[TENSOR_INSERT_SUM:[A-Za-z0-9_]+]] = arith.addf +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$TENSOR_INSERT_AMBIGUITY_ORIGIN]] +// CHECK-SAME: loc(#[[TENSOR_INSERT_AMBIGUITY_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[TENSOR_INSERTED_0:[A-Za-z0-9_]+]] = tensor.insert %[[TENSOR_INSERT_SUM]] +// CHECK: bufferization.materialize_in_destination %[[TENSOR_INSERTED_0]] +// CHECK-SAME: loc(#[[TENSOR_INSERT_STORE_0_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[TENSOR_INSERTED_1:[A-Za-z0-9_]+]] = tensor.insert %[[TENSOR_INSERT_SUM]] +// CHECK: bufferization.materialize_in_destination %[[TENSOR_INSERTED_1]] +// CHECK-SAME: loc(#[[TENSOR_INSERT_STORE_1_LOC:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[TENSOR_INSERT_AMBIGUITY_SYNTH_LOC]] = loc("tensor_insert_ambiguity.py":0:0) +// CHECK-DAG: #[[TENSOR_INSERT_STORE_0_LOC]] = loc("tensor_insert_ambiguity.py":30:30) +// CHECK-DAG: #[[TENSOR_INSERT_STORE_1_LOC]] = loc("tensor_insert_ambiguity.py":30:40) + +// ----- + +//===----------------------------------------------------------------------===// +// duplicate_canonical_store_candidates +// +// Verifies that duplicate candidates with the same canonical source location +// are treated as one unique store anchor. One store location is wrapped in an +// internal NameLoc and the other is a direct FileLineColLoc, but both +// canonicalize to duplicate_candidates.py:40:30, so the view can be safely +// retargeted there. +//===----------------------------------------------------------------------===// + +module { +func.func @duplicate_canonical_store_candidates(%src: tensor<4xf32>, %dst: memref<8xf32>, %idx: index) { +%view = memref.subview %dst[%idx] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> loc("duplicate_candidates.py":40:5) +bufferization.materialize_in_destination %src in writable %view : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("synthetic_store"("duplicate_candidates.py":40:30)) +bufferization.materialize_in_destination %src in writable %view : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("duplicate_candidates.py":40:30) +func.return loc("duplicate_candidates.py":41:3) +} +} + +// CHECK-DAG: #[[$DUPLICATE_VIEW_ORIGIN:[A-Za-z0-9_]+]] = loc("duplicate_candidates.py":40:5) + +// CHECK-LABEL: func.func @duplicate_canonical_store_candidates +// CHECK: %[[DUPLICATE_VIEW:[A-Za-z0-9_]+]] = memref.subview +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: triton.debug_line.origin = #[[$DUPLICATE_VIEW_ORIGIN]] +// CHECK-SAME: loc(#[[DUPLICATE_STORE_LOC:[A-Za-z0-9_]+]]) +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[DUPLICATE_VIEW]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[DUPLICATE_STORE_LOC]]) +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[DUPLICATE_VIEW]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[DUPLICATE_STORE_LOC]]) + +// CHECK-DAG: #[[DUPLICATE_STORE_LOC]] = loc("duplicate_candidates.py":40:30) + +// ----- + +//===----------------------------------------------------------------------===// +// direct_and_nested_destination_view_ambiguity +// +// Verifies ambiguity detection across direct and nested destination-view +// paths. The base view has several distinct possible store anchors, including +// direct stores and a store through a nested subview. The pass must not select +// one arbitrarily, so the base view remains synthetic and moves to +// cross_view_ambiguity.py:0:0. +//===----------------------------------------------------------------------===// + +module { +func.func @direct_and_nested_destination_view_ambiguity(%src: tensor<4xf32>, %dst: memref<8xf32>, %idx: index) { +%view = memref.subview %dst[%idx] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: ?>> loc("cross_view_ambiguity.py":50:5) +bufferization.materialize_in_destination %src in writable %view : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("cross_view_ambiguity.py":50:20) +bufferization.materialize_in_destination %src in writable %view : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("cross_view_ambiguity.py":50:30) +%nested = memref.subview %view[0] [4] [1] : memref<4xf32, strided<[1], offset: ?>> to memref<4xf32, strided<[1], offset: ?>> loc("cross_view_ambiguity.py":51:5) +bufferization.materialize_in_destination %src in writable %nested : (tensor<4xf32>, memref<4xf32, strided<[1], offset: ?>>) -> () loc("cross_view_ambiguity.py":51:20) +func.return loc("cross_view_ambiguity.py":52:3) +} +} + +// CHECK-DAG: #[[$CROSS_VIEW_ORIGIN:[A-Za-z0-9_]+]] = loc("cross_view_ambiguity.py":50:5) + +// CHECK-LABEL: func.func @direct_and_nested_destination_view_ambiguity +// CHECK: %[[CROSS_VIEW:[A-Za-z0-9_]+]] = memref.subview +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$CROSS_VIEW_ORIGIN]] +// CHECK-SAME: loc(#[[CROSS_VIEW_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[CROSS_VIEW]] +// CHECK-SAME: loc(#[[CROSS_VIEW_STORE_A:[A-Za-z0-9_]+]]) +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[CROSS_VIEW]] +// CHECK-SAME: loc(#[[CROSS_VIEW_STORE_B:[A-Za-z0-9_]+]]) +// CHECK: %[[CROSS_NESTED:[A-Za-z0-9_]+]] = memref.subview %[[CROSS_VIEW]] +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[CROSS_NESTED]] +// CHECK-SAME: loc(#[[CROSS_VIEW_STORE_C:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[CROSS_VIEW_SYNTH_LOC]] = loc("cross_view_ambiguity.py":0:0) +// CHECK-DAG: #[[CROSS_VIEW_STORE_A]] = loc("cross_view_ambiguity.py":50:20) +// CHECK-DAG: #[[CROSS_VIEW_STORE_B]] = loc("cross_view_ambiguity.py":50:30) +// CHECK-DAG: #[[CROSS_VIEW_STORE_C]] = loc("cross_view_ambiguity.py":51:20) + +// ----- + +//===----------------------------------------------------------------------===// +// destination_view_and_tensor_insert_ambiguity +// +// Verifies ambiguity detection across different retargeting mechanisms. The +// same index_cast feeds both a destination-view store path and a tensor.insert +// store path, and the two paths resolve to different store locations. Since the +// combined candidate set is not unique, the index_cast remains synthetic and +// moves to cross_path_ambiguity.py:0:0. +//===----------------------------------------------------------------------===// + +module { +func.func @destination_view_and_tensor_insert_ambiguity(%offset: i32, %src: tensor<1xindex>, %base: tensor<1xindex>, %dst0: memref<4xindex>, %dst1: memref<1xindex>) { +%c0 = arith.constant 0 : index loc("cross_path_ambiguity.py":60:3) +%idx = arith.index_cast %offset : i32 to index loc("cross_path_ambiguity.py":60:5) +%view = memref.subview %dst0[%idx] [1] [1] : memref<4xindex> to memref<1xindex, strided<[1], offset: ?>> loc("cross_path_ambiguity.py":61:5) +bufferization.materialize_in_destination %src in writable %view : (tensor<1xindex>, memref<1xindex, strided<[1], offset: ?>>) -> () loc("cross_path_ambiguity.py":61:20) +%inserted = tensor.insert %idx into %base[%c0] : tensor<1xindex> loc("cross_path_ambiguity.py":62:5) +bufferization.materialize_in_destination %inserted in writable %dst1 : (tensor<1xindex>, memref<1xindex>) -> () loc("cross_path_ambiguity.py":62:20) +func.return loc("cross_path_ambiguity.py":63:3) +} +} + +// CHECK-DAG: #[[$CROSS_PATH_AMBIGUITY_ORIGIN:[A-Za-z0-9_]+]] = loc("cross_path_ambiguity.py":60:5) + +// CHECK-LABEL: func.func @destination_view_and_tensor_insert_ambiguity +// CHECK: %[[CROSS_PATH_IDX:[A-Za-z0-9_]+]] = arith.index_cast +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$CROSS_PATH_AMBIGUITY_ORIGIN]] +// CHECK-SAME: loc(#[[CROSS_PATH_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[CROSS_PATH_VIEW:[A-Za-z0-9_]+]] = memref.subview {{.*}}[%[[CROSS_PATH_IDX]]] +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[CROSS_PATH_VIEW]] +// CHECK-SAME: loc(#[[CROSS_PATH_STORE_A:[A-Za-z0-9_]+]]) +// CHECK: %[[CROSS_PATH_INSERTED:[A-Za-z0-9_]+]] = tensor.insert %[[CROSS_PATH_IDX]] +// CHECK: bufferization.materialize_in_destination %[[CROSS_PATH_INSERTED]] +// CHECK-SAME: loc(#[[CROSS_PATH_STORE_B:[A-Za-z0-9_]+]]) + +// CHECK-DAG: #[[CROSS_PATH_SYNTH_LOC]] = loc("cross_path_ambiguity.py":0:0) +// CHECK-DAG: #[[CROSS_PATH_STORE_A]] = loc("cross_path_ambiguity.py":61:20) +// CHECK-DAG: #[[CROSS_PATH_STORE_B]] = loc("cross_path_ambiguity.py":62:20) + +// ----- + +//===----------------------------------------------------------------------===// +// matching_destination_view_and_tensor_insert_candidates +// +// Verifies that uniqueness alone does not allow cross-line retargeting. The +// index_cast feeds both a destination-view store path and a tensor.insert store +// path, and all candidates canonicalize to cross_path_unique.py:71:20. However, +// the index_cast originates on line 70, so it remains synthetic. The destination +// view originates on line 71 and may still be retargeted to the store anchor. +//===----------------------------------------------------------------------===// + +module { +func.func @matching_destination_view_and_tensor_insert_candidates(%offset: i32, %src: tensor<1xindex>, %base: tensor<1xindex>, %dst0: memref<4xindex>, %dst1: memref<1xindex>) { +%c0 = arith.constant 0 : index loc("cross_path_unique.py":70:3) +%idx = arith.index_cast %offset : i32 to index loc("cross_path_unique.py":70:5) +%view = memref.subview %dst0[%idx] [1] [1] : memref<4xindex> to memref<1xindex, strided<[1], offset: ?>> loc("cross_path_unique.py":71:5) +bufferization.materialize_in_destination %src in writable %view : (tensor<1xindex>, memref<1xindex, strided<[1], offset: ?>>) -> () loc("synthetic_store"("cross_path_unique.py":71:20)) +%inserted = tensor.insert %idx into %base[%c0] : tensor<1xindex> loc("cross_path_unique.py":71:6) +bufferization.materialize_in_destination %inserted in writable %dst1 : (tensor<1xindex>, memref<1xindex>) -> () loc("cross_path_unique.py":71:20) +func.return loc("cross_path_unique.py":72:3) +} +} + +// CHECK-DAG: #[[$CROSS_PATH_UNIQUE_ORIGIN:[A-Za-z0-9_]+]] = loc("cross_path_unique.py":70:5) + +// CHECK-LABEL: func.func @matching_destination_view_and_tensor_insert_candidates +// CHECK: %[[CROSS_PATH_UNIQUE_IDX:[A-Za-z0-9_]+]] = arith.index_cast +// CHECK-SAME: triton.debug_line.class = "synthetic" +// CHECK-SAME: triton.debug_line.origin = #[[$CROSS_PATH_UNIQUE_ORIGIN]] +// CHECK-SAME: loc(#[[CROSS_PATH_UNIQUE_SYNTH_LOC:[A-Za-z0-9_]+]]) +// CHECK: %[[CROSS_PATH_UNIQUE_VIEW:[A-Za-z0-9_]+]] = memref.subview {{.*}}[%[[CROSS_PATH_UNIQUE_IDX]]] +// CHECK-SAME: triton.debug_line.class = "semantic" +// CHECK-SAME: loc(#[[CROSS_PATH_UNIQUE_STORE:[A-Za-z0-9_]+]]) +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[CROSS_PATH_UNIQUE_VIEW]] +// CHECK-SAME: loc(#[[CROSS_PATH_UNIQUE_STORE]]) +// CHECK: %[[CROSS_PATH_UNIQUE_INSERTED:[A-Za-z0-9_]+]] = tensor.insert %[[CROSS_PATH_UNIQUE_IDX]] +// CHECK: bufferization.materialize_in_destination %[[CROSS_PATH_UNIQUE_INSERTED]] +// CHECK-SAME: loc(#[[CROSS_PATH_UNIQUE_STORE]]) + +// CHECK-DAG: #[[CROSS_PATH_UNIQUE_SYNTH_LOC]] = loc("cross_path_unique.py":0:0) +// CHECK-DAG: #[[CROSS_PATH_UNIQUE_STORE]] = loc("cross_path_unique.py":71:20) diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/atomic_rmw.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/atomic_rmw.mlir new file mode 100644 index 0000000000..f3f36e5dcc --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/atomic_rmw.mlir @@ -0,0 +1,109 @@ +// RUN: triton-opt --triton-to-linalg="named-ops=True" --split-input-file %s | FileCheck %s +// CHECK-LABEL: func.func @matmul_atomic_add +// CHECK-NOT: GenericAtomicRMW +// CHECK: tensor.extract_slice +// CHECK: hivm.hir.store ins(%{{.*}} : tensor) outs(%{{.*}} : memref) atomic = + + tt.func public @matmul_atomic_add(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: i32, %arg4: i32, %arg5: i32, %arg6: i32, %arg7: i32, %arg8: i32, %arg9: i32, %arg10: i32, %arg11: i32) attributes {noinline = false} { + %0 = tt.get_program_id x : i32 + %1 = tt.get_program_id y : i32 + %2 = tt.get_program_id z : i32 + %c16_i32 = arith.constant 16 : i32 + %c16_i32_0 = arith.constant 16 : i32 + %3 = arith.muli %0, %c16_i32_0 : i32 + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.splat %3 : i32 -> tensor<16xi32> + %6 = arith.addi %5, %4 : tensor<16xi32> + %c16_i32_1 = arith.constant 16 : i32 + %c16_i32_2 = arith.constant 16 : i32 + %7 = arith.muli %1, %c16_i32_2 : i32 + %8 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %9 = tt.splat %7 : i32 -> tensor<16xi32> + %10 = arith.addi %9, %8 : tensor<16xi32> + %11 = tt.expand_dims %6 {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %12 = tt.splat %arg10 : i32 -> tensor<16x1xi32> + %13 = arith.muli %11, %12 : tensor<16x1xi32> + %14 = tt.splat %arg2 : !tt.ptr -> tensor<16x1x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<16x1x!tt.ptr>, tensor<16x1xi32> + %16 = tt.expand_dims %10 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %17 = tt.splat %arg11 : i32 -> tensor<1x16xi32> + %18 = arith.muli %16, %17 : tensor<1x16xi32> + %19 = tt.broadcast %15 : tensor<16x1x!tt.ptr> -> tensor<16x16x!tt.ptr> + %20 = tt.broadcast %18 : tensor<1x16xi32> -> tensor<16x16xi32> + %21 = tt.addptr %19, %20 : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %22 = tt.expand_dims %6 {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %23 = tt.splat %arg3 : i32 -> tensor<16x1xi32> + %24 = arith.cmpi slt, %22, %23 : tensor<16x1xi32> + %25 = tt.expand_dims %10 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %26 = tt.splat %arg4 : i32 -> tensor<1x16xi32> + %27 = arith.cmpi slt, %25, %26 : tensor<1x16xi32> + %28 = tt.broadcast %24 : tensor<16x1xi1> -> tensor<16x16xi1> + %29 = tt.broadcast %27 : tensor<1x16xi1> -> tensor<16x16xi1> + %30 = arith.andi %28, %29 : tensor<16x16xi1> + %c16_i32_3 = arith.constant 16 : i32 + %c16_i32_4 = arith.constant 16 : i32 + %31 = arith.muli %2, %c16_i32_4 : i32 + %c32_i32 = arith.constant 32 : i32 + %32 = arith.bitcast %31 : i32 to i32 + %33 = arith.bitcast %arg5 : i32 to i32 + %34 = arith.bitcast %c32_i32 : i32 to i32 + %35 = ub.poison : i32 + scf.for %arg12 = %32 to %33 step %34 : i32 { + %36 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %37 = tt.splat %arg12 : i32 -> tensor<16xi32> + %38 = arith.addi %37, %36 : tensor<16xi32> + %39 = tt.expand_dims %6 {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %40 = tt.splat %arg6 : i32 -> tensor<16x1xi32> + %41 = arith.muli %39, %40 : tensor<16x1xi32> + %42 = tt.splat %arg0 : !tt.ptr -> tensor<16x1x!tt.ptr> + %43 = tt.addptr %42, %41 : tensor<16x1x!tt.ptr>, tensor<16x1xi32> + %44 = tt.expand_dims %38 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %45 = tt.splat %arg7 : i32 -> tensor<1x16xi32> + %46 = arith.muli %44, %45 : tensor<1x16xi32> + %47 = tt.broadcast %43 : tensor<16x1x!tt.ptr> -> tensor<16x16x!tt.ptr> + %48 = tt.broadcast %46 : tensor<1x16xi32> -> tensor<16x16xi32> + %49 = tt.addptr %47, %48 : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %50 = tt.expand_dims %38 {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %51 = tt.splat %arg8 : i32 -> tensor<16x1xi32> + %52 = arith.muli %50, %51 : tensor<16x1xi32> + %53 = tt.splat %arg1 : !tt.ptr -> tensor<16x1x!tt.ptr> + %54 = tt.addptr %53, %52 : tensor<16x1x!tt.ptr>, tensor<16x1xi32> + %55 = tt.expand_dims %10 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %56 = tt.splat %arg9 : i32 -> tensor<1x16xi32> + %57 = arith.muli %55, %56 : tensor<1x16xi32> + %58 = tt.broadcast %54 : tensor<16x1x!tt.ptr> -> tensor<16x16x!tt.ptr> + %59 = tt.broadcast %57 : tensor<1x16xi32> -> tensor<16x16xi32> + %60 = tt.addptr %58, %59 : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + %61 = tt.expand_dims %6 {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %62 = tt.splat %arg3 : i32 -> tensor<16x1xi32> + %63 = arith.cmpi slt, %61, %62 : tensor<16x1xi32> + %64 = tt.expand_dims %38 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %65 = tt.splat %arg5 : i32 -> tensor<1x16xi32> + %66 = arith.cmpi slt, %64, %65 : tensor<1x16xi32> + %67 = tt.broadcast %63 : tensor<16x1xi1> -> tensor<16x16xi1> + %68 = tt.broadcast %66 : tensor<1x16xi1> -> tensor<16x16xi1> + %69 = arith.andi %67, %68 : tensor<16x16xi1> + %70 = tt.expand_dims %38 {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %71 = tt.splat %arg5 : i32 -> tensor<16x1xi32> + %72 = arith.cmpi slt, %70, %71 : tensor<16x1xi32> + %73 = tt.expand_dims %10 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %74 = tt.splat %arg4 : i32 -> tensor<1x16xi32> + %75 = arith.cmpi slt, %73, %74 : tensor<1x16xi32> + %76 = tt.broadcast %72 : tensor<16x1xi1> -> tensor<16x16xi1> + %77 = tt.broadcast %75 : tensor<1x16xi1> -> tensor<16x16xi1> + %78 = arith.andi %76, %77 : tensor<16x16xi1> + %c0_i32 = arith.constant 0 : i32 + %cst = arith.constant dense<0> : tensor<16x16xi32> + %79 = arith.sitofp %cst : tensor<16x16xi32> to tensor<16x16xf32> + %80 = tt.load %49, %69, %79 : tensor<16x16x!tt.ptr> + %c0_i32_5 = arith.constant 0 : i32 + %cst_6 = arith.constant dense<0> : tensor<16x16xi32> + %81 = arith.sitofp %cst_6 : tensor<16x16xi32> to tensor<16x16xf32> + %82 = tt.load %60, %78, %81 : tensor<16x16x!tt.ptr> + %cst_7 = arith.constant 0.000000e+00 : f32 + %cst_8 = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %83 = tt.dot %80, %82, %cst_8 : tensor<16x16xf32> * tensor<16x16xf32> -> tensor<16x16xf32> + %84 = tt.atomic_rmw fadd, acq_rel, gpu, %21, %83, %30 : (tensor<16x16x!tt.ptr>, tensor<16x16xf32>, tensor<16x16xi1>) -> tensor<16x16xf32> + } + tt.return + } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/atomic_rmw_block.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/atomic_rmw_block.mlir new file mode 100644 index 0000000000..9eaa677edb --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/atomic_rmw_block.mlir @@ -0,0 +1,54 @@ +// RUN: triton-opt -allow-unregistered-dialect --triton-to-linalg="named-ops=True" --split-input-file %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @moe_align_block_size_stage4(%arg0: !tt.ptr {tt.divisibility = 16 : i32} , %arg1: !tt.ptr {tt.divisibility = 16 : i32} , %arg2: !tt.ptr {tt.divisibility = 16 : i32} , %arg3: !tt.ptr {tt.divisibility = 16 : i32} , %arg4: !tt.ptr {tt.divisibility = 16 : i32} , %arg5: i32) attributes {noinline = false} { + %cst = arith.constant dense<1> : tensor<1xi32> + %cst_0 = arith.constant dense<0> : tensor<1xi32> + %c250_i32 = arith.constant 250 : i32 + %c16_i32 = arith.constant 16 : i32 + %c1_i32 = arith.constant 1 : i32 + %0 = tt.get_program_id x : i32 + %1 = tt.addptr %arg4, %0 : !tt.ptr, i32 + %2 = tt.load %1 : !tt.ptr + %3 = tt.addptr %1, %c1_i32 : !tt.ptr, i32 + %4 = tt.load %3 : !tt.ptr + scf.for %arg6 = %2 to %4 step %c16_i32 : i32 { + %22 = arith.divsi %arg6, %c16_i32 : i32 + %23 = tt.addptr %arg2, %22 : !tt.ptr, i32 + tt.store %23, %0 : !tt.ptr + } + %5 = arith.muli %0, %c250_i32 : i32 + %6 = tt.splat %0 : i32 -> tensor<1xi32> + %7 = arith.cmpi slt, %0, %arg5 : i32 + %8 = tt.splat %7 : i1 -> tensor<1xi1> + %9 = tt.addptr %arg0, %0 : !tt.ptr, i32 + %10 = tt.splat %9 : !tt.ptr -> tensor<1x!tt.ptr> + %11 = tt.load %10, %8, %cst_0 : tensor<1x!tt.ptr> + %12 = tt.addptr %arg3, %5 : !tt.ptr, i32 + %13 = tt.splat %12 : !tt.ptr -> tensor<1x!tt.ptr> + %14 = tt.addptr %13, %11 : tensor<1x!tt.ptr>, tensor<1xi32> + %15 = tt.atomic_rmw add, acq_rel, gpu, %14, %cst, %8 : (tensor<1x!tt.ptr>, tensor<1xi32>, tensor<1xi1>) -> tensor<1xi32> + %16 = tt.splat %arg4 : !tt.ptr -> tensor<1x!tt.ptr> + %17 = tt.addptr %16, %11 : tensor<1x!tt.ptr>, tensor<1xi32> + %18 = tt.load %17, %8, %cst_0 : tensor<1x!tt.ptr> + %19 = arith.addi %15, %18 : tensor<1xi32> + %20 = tt.splat %arg1 : !tt.ptr -> tensor<1x!tt.ptr> + %21 = tt.addptr %20, %19 : tensor<1x!tt.ptr>, tensor<1xi32> + tt.store %21, %6, %8 : tensor<1x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @moe_align_block_size_stage4 + +// CHECK: %[[MASK:.*]] = arith.cmpi slt +// CHECK: %[[MASK_SIZE:.*]] = arith.index_castui %[[MASK]] : i1 to index +// CHECK: %[[ATOMIC_PTR:.*]] = memref.reinterpret_cast %{{.*}} to offset: [%{{.*}}], sizes: [1], strides: [1] : memref to memref<1xi32, strided<[1], offset: ?>> +// CHECK: %[[ATOMIC_VIEW:.*]] = memref.subview %[[ATOMIC_PTR]][0] [%[[MASK_SIZE]]] [1] : memref<1xi32, strided<[1], offset: ?>> to memref> +// CHECK: %[[ATOMIC_VALUE:.*]] = tensor.extract_slice %{{.*}}[0] [%[[MASK_SIZE]]] [1] : tensor<1xi32> to tensor +// CHECK: %[[RESULT_VIEW:.*]] = memref.subview %{{.*}}[0] [%[[MASK_SIZE]]] [1] : memref<1xi32> to memref> +// CHECK: %[[LOCK:.*]] = hivm.hir.create_sync_block_lock : memref<1xi64> +// CHECK: hivm.hir.sync_block_lock lock_var(%[[LOCK]] : memref<1xi64>) +// CHECK: memref.copy %[[ATOMIC_VIEW]], %[[RESULT_VIEW]] : +// CHECK: hivm.hir.store ins(%[[ATOMIC_VALUE]] : tensor) outs(%[[ATOMIC_VIEW]] : memref>) {already_sync} atomic = +// CHECK: hivm.hir.sync_block_unlock lock_var(%[[LOCK]] : memref<1xi64>) diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/clamp_fp8.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/clamp_fp8.mlir new file mode 100644 index 0000000000..b258c9bbd3 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/clamp_fp8.mlir @@ -0,0 +1,84 @@ +// RUN: triton-opt '--discrete-mask-access-conversion=compile-on-910-95=True compile-mode=simd' --triton-to-annotation '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd' --triton-to-hivm --triton-to-hfusion --triton-to-llvm --bubble-up-operation '--triton-to-linalg=global-kernel=false named-ops=True enable-nd2nz-on-vector=False compile-on-910-95=True compile-mode=simd' --split-input-file %s | FileCheck %s + +module { + tt.func public @tt_clamp_3d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<4> : tensor<1x29x1xi32> + %cst_0 = arith.constant dense<4> : tensor<2x1x1xi32> + %cst_1 = arith.constant dense<29> : tensor<2x1x1xi32> + %c4_i32 = arith.constant 4 : i32 + %c29_i32 = arith.constant 29 : i32 + %c2_i32 = arith.constant 2 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c2_i32 : i32 + %2 = tt.get_program_id y : i32 + %3 = arith.muli %2, %c29_i32 : i32 + %4 = tt.get_program_id z : i32 + %5 = arith.muli %4, %c4_i32 : i32 + %6 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %7 = tt.splat %1 : i32 -> tensor<2xi32> + %8 = arith.addi %6, %7 : tensor<2xi32> + %9 = tt.make_range {end = 29 : i32, start = 0 : i32} : tensor<29xi32> + %10 = tt.splat %3 : i32 -> tensor<29xi32> + %11 = arith.addi %9, %10 : tensor<29xi32> + %12 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %13 = tt.splat %5 : i32 -> tensor<4xi32> + %14 = arith.addi %12, %13 : tensor<4xi32> + %15 = tt.expand_dims %8 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %16 = tt.expand_dims %15 {axis = 2 : i32} : tensor<2x1xi32> -> tensor<2x1x1xi32> + %17 = arith.muli %16, %cst_1 : tensor<2x1x1xi32> + %18 = arith.muli %17, %cst_0 : tensor<2x1x1xi32> + %19 = tt.expand_dims %11 {axis = 0 : i32} : tensor<29xi32> -> tensor<1x29xi32> + %20 = tt.expand_dims %19 {axis = 2 : i32} : tensor<1x29xi32> -> tensor<1x29x1xi32> + %21 = arith.muli %20, %cst : tensor<1x29x1xi32> + %22 = tt.broadcast %18 : tensor<2x1x1xi32> -> tensor<2x29x1xi32> + %23 = tt.broadcast %21 : tensor<1x29x1xi32> -> tensor<2x29x1xi32> + %24 = arith.addi %22, %23 : tensor<2x29x1xi32> + %25 = tt.expand_dims %14 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %26 = tt.expand_dims %25 {axis = 1 : i32} : tensor<1x4xi32> -> tensor<1x1x4xi32> + %27 = tt.broadcast %24 : tensor<2x29x1xi32> -> tensor<2x29x4xi32> + %28 = tt.broadcast %26 : tensor<1x1x4xi32> -> tensor<2x29x4xi32> + %29 = arith.addi %27, %28 : tensor<2x29x4xi32> + %30 = tt.splat %arg0 : !tt.ptr -> tensor<2x29x4x!tt.ptr> + %31 = tt.addptr %30, %29 : tensor<2x29x4x!tt.ptr>, tensor<2x29x4xi32> + %32 = tt.load %31 : tensor<2x29x4x!tt.ptr> + %33 = tt.splat %arg2 : !tt.ptr -> tensor<2x29x4x!tt.ptr> + %34 = tt.addptr %33, %29 : tensor<2x29x4x!tt.ptr>, tensor<2x29x4xi32> + %35 = tt.load %34 : tensor<2x29x4x!tt.ptr> + %36 = tt.splat %arg3 : !tt.ptr -> tensor<2x29x4x!tt.ptr> + %37 = tt.addptr %36, %29 : tensor<2x29x4x!tt.ptr>, tensor<2x29x4xi32> + %38 = tt.load %37 : tensor<2x29x4x!tt.ptr> + %39 = tt.fp_to_fp %32 : tensor<2x29x4xf8E4M3FN> -> tensor<2x29x4xf32> + %40 = tt.clampf %39, %35, %38, propagateNan = none : tensor<2x29x4xf32> + %41 = tt.splat %arg1 : !tt.ptr -> tensor<2x29x4x!tt.ptr> + %42 = tt.addptr %41, %29 : tensor<2x29x4x!tt.ptr>, tensor<2x29x4xi32> + %43 = tt.fp_to_fp %40, rounding = rtne : tensor<2x29x4xf32> -> tensor<2x29x4xf8E4M3FN> + tt.store %42, %43 : tensor<2x29x4x!tt.ptr> + tt.return + } +} + +// CHECK: %[[CAST_F8:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg2 to offset: [%9], sizes: [2, 29, 4], strides: [116, 4, 1] : memref to memref<2x29x4xf8E4M3FN, strided<[116, 4, 1], offset: ?>> +// CHECK: %[[ALLOC_F8:[A-Za-z0-9_]+]] = memref.alloc() : memref<2x29x4xf8E4M3FN> +// CHECK: memref.copy %[[CAST_F8]], %[[ALLOC_F8]] : memref<2x29x4xf8E4M3FN, strided<[116, 4, 1], offset: ?>> to memref<2x29x4xf8E4M3FN> +// CHECK: %[[TENSOR_F8:[A-Za-z0-9_]+]] = bufferization.to_tensor %[[ALLOC_F8]] restrict writable : memref<2x29x4xf8E4M3FN> + +// CHECK: %[[CAST_F32_MIN:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg4 to offset: [%9], sizes: [2, 29, 4], strides: [116, 4, 1] : memref to memref<2x29x4xf32, strided<[116, 4, 1], offset: ?>> +// CHECK: %[[ALLOC_F32_MIN:[A-Za-z0-9_]+]] = memref.alloc() : memref<2x29x4xf32> +// CHECK: memref.copy %[[CAST_F32_MIN]], %[[ALLOC_F32_MIN]] : memref<2x29x4xf32, strided<[116, 4, 1], offset: ?>> to memref<2x29x4xf32> +// CHECK: %[[TENSOR_F32_MIN:[A-Za-z0-9_]+]] = bufferization.to_tensor %[[ALLOC_F32_MIN]] restrict writable : memref<2x29x4xf32> + +// CHECK: %[[CAST_F32_MAX:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg5 to offset: [%9], sizes: [2, 29, 4], strides: [116, 4, 1] : memref to memref<2x29x4xf32, strided<[116, 4, 1], offset: ?>> +// CHECK: %[[ALLOC_F32_MAX:[A-Za-z0-9_]+]] = memref.alloc() : memref<2x29x4xf32> +// CHECK: memref.copy %[[CAST_F32_MAX]], %[[ALLOC_F32_MAX]] : memref<2x29x4xf32, strided<[116, 4, 1], offset: ?>> to memref<2x29x4xf32> +// CHECK: %[[TENSOR_F32_MAX:[A-Za-z0-9_]+]] = bufferization.to_tensor %[[ALLOC_F32_MAX]] restrict writable : memref<2x29x4xf32> + +// CHECK: %[[F8_TO_F32:[A-Za-z0-9_]+]] = arith.extf %[[TENSOR_F8]] {round_mode = #hfusion.round_mode} : tensor<2x29x4xf8E4M3FN> to tensor<2x29x4xf32> +// CHECK: %[[CLAMP_MAX:[A-Za-z0-9_]+]] = arith.minnumf %[[F8_TO_F32]], %[[TENSOR_F32_MAX]] : tensor<2x29x4xf32> +// CHECK: %[[CLAMP_RESULT:[A-Za-z0-9_]+]] = arith.maxnumf %[[TENSOR_F32_MIN]], %[[CLAMP_MAX]] : tensor<2x29x4xf32> + +// CHECK: %[[CAST_OUTPUT:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg3 to offset: [%9], sizes: [2, 29, 4], strides: [116, 4, 1] : memref to memref<2x29x4xf8E4M3FN, strided<[116, 4, 1], offset: ?>> +// CHECK: %[[F32_TO_F8:[A-Za-z0-9_]+]] = arith.truncf %[[CLAMP_RESULT]] {round_mode = #hfusion.round_mode} : tensor<2x29x4xf32> to tensor<2x29x4xf8E4M3FN> +// CHECK: bufferization.materialize_in_destination %[[F32_TO_F8]] in writable %[[CAST_OUTPUT]] : (tensor<2x29x4xf8E4M3FN>, memref<2x29x4xf8E4M3FN, strided<[116, 4, 1], offset: ?>>) -> () + + +// ----- diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_macro_op.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_macro_op.mlir new file mode 100644 index 0000000000..de181f49b0 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_macro_op.mlir @@ -0,0 +1,83 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s + +// Test hivm.hir.custom_macro survives triton-to-linalg with sync attrs preserved. +// CHECK-LABEL: func.func @test_custom_macro_op +// CHECK: hivm.hir.custom_macro +// CHECK: hivm.pipe_in = #hivm.pipe +// CHECK: hivm.pipe_out = #hivm.pipe +// CHECK: hivm.tcore_type = #hivm.tcore_type +// CHECK: hivm.vf_mode = #hivm.vf_mode +// CHECK: sync_event_slots = [ + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_custom_macro_op(%src: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<8xf32> + %empty = tensor.empty() : tensor<8xf32> + %c8_i32 = arith.constant 8 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %base = arith.muli %pid, %c8_i32 : i32 + %base_splat = tt.splat %base : i32 -> tensor<8xi32> + %offs_full = arith.addi %offs, %base_splat : tensor<8xi32> + %src_ptrs = tt.splat %src : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr = tt.addptr %src_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + %custom_result = hivm.hir.custom_macro + {hivm.tcore_type = #hivm.tcore_type, + hivm.vf_mode = #hivm.vf_mode, + hivm.pipe_in = #hivm.pipe, + hivm.pipe_out = #hivm.pipe, + symbol = "test_macro_sync_impl", + sync_event_slots = [ + #hivm.sync_event_slot<#hivm.pipe, #hivm.pipe, wait, > + ]} + "test_macro_sync" + ins(%src_addptr, %cst : tensor<8x!tt.ptr>, tensor<8xf32>) + outs(%empty : tensor<8xf32>) -> tensor<8xf32> + %dst_ptrs = tt.splat %src : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %custom_result : tensor<8x!tt.ptr> + tt.return + } +} + +// ----- + +// Test hivm.hir.custom_macro with distributed tensor output. +// CHECK-LABEL: func.func @test_custom_macro_op_ptr_output +// CHECK: hivm.hir.custom_macro +// CHECK: hivm.is_distributed +// CHECK: hivm.pipe_in = #hivm.pipe +// CHECK: hivm.pipe_out = #hivm.pipe +// CHECK: hivm.tcore_type = #hivm.tcore_type +// CHECK: hivm.vf_mode = #hivm.vf_mode +// CHECK: memref + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_custom_macro_op_ptr_output(%src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %empty = tensor.empty() : tensor<8x!tt.ptr> + %c8_i32 = arith.constant 8 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %base = arith.muli %pid, %c8_i32 : i32 + %base_splat = tt.splat %base : i32 -> tensor<8xi32> + %offs_full = arith.addi %offs, %base_splat : tensor<8xi32> + %src_ptrs = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr = tt.addptr %src_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + %custom_result = hivm.hir.custom_macro + {hivm.is_distributed, + hivm.tcore_type = #hivm.tcore_type, + hivm.vf_mode = #hivm.vf_mode, + hivm.pipe_in = #hivm.pipe, + hivm.pipe_out = #hivm.pipe, + symbol = "test_macro_ptr_op_impl", + SrcPtrIndex = array} + "test_macro_ptr_op" + ins(%src_addptr : tensor<8x!tt.ptr>) + outs(%empty : tensor<8x!tt.ptr>) -> tensor<8x!tt.ptr> + %data = tt.load %custom_result : tensor<8x!tt.ptr> + %dst_ptrs = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %data : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_op_pointer_splat.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_op_pointer_splat.mlir new file mode 100644 index 0000000000..49e891d58a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_op_pointer_splat.mlir @@ -0,0 +1,127 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s +// RUN: triton-opt --canonicalize --triton-reorder-broadcast --triton-to-linalg --split-input-file %s | FileCheck %s + +// Start at the TD -> HIVM boundary: no distributed dialect is needed in TA. +// A custom result used only by a load must not make its pointer input a +// MetaUse clone. The pointer and token must both survive custom conversion. +// CHECK-LABEL: func.func @consume_splat_zero( +// CHECK-SAME: %[[SRC:arg[0-9]+]]: memref{{[^%]*}}%[[DST:arg[0-9]+]]: memref{{[^%]*}}%[[TOKEN:arg[0-9]+]]: i32 +// CHECK: %[[INPUT:.*]] = memref.reinterpret_cast %[[SRC]] to offset: [0], sizes: [1], strides: [1] +// CHECK: %[[CONSUMED:.*]] = hivm.hir.custom +// CHECK-SAME: "dist.aclshmem_consume_token_int64_ptr_1d" ins(%[[INPUT]], %[[TOKEN]] : memref<1xi64, strided<[1]>>, i32) -> memref<1xi64> +// CHECK: %[[READ:.*]] = memref.reinterpret_cast %[[CONSUMED]] to offset: [0], sizes: [1], strides: [1] +// CHECK: memref.copy %[[READ]], +// CHECK: return +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @consume_splat_zero(%src: !tt.ptr, %dst: !tt.ptr, %token: i32) { + %zero = arith.constant 0 : i32 + %base = tt.addptr %src, %zero : !tt.ptr, i32 + %ptrs = tt.splat %base : !tt.ptr -> tensor<1x!tt.ptr> + %empty = tensor.empty() : tensor<1x!tt.ptr> + %consumed = hivm.hir.custom + {hivm.is_distributed, SrcPtrIndex = array, no_side_effect, + hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, + symbol = "aclshmem_consume_token_int64_ptr_1d"} + "dist.aclshmem_consume_token_int64_ptr_1d" + ins(%ptrs, %token : tensor<1x!tt.ptr>, i32) + outs(%empty : tensor<1x!tt.ptr>) -> tensor<1x!tt.ptr> + %value = tt.load %consumed : tensor<1x!tt.ptr> + %out = tt.splat %dst : !tt.ptr -> tensor<1x!tt.ptr> + tt.store %out, %value : tensor<1x!tt.ptr> + tt.return + } +} + +// ----- + +// ReorderBroadcast may turn addptr(splat(base), splat(offset)) into +// splat(addptr(base, offset)). Preserve the nonzero input address and token. +// CHECK-LABEL: func.func @consume_splat_offset( +// CHECK-SAME: %[[SRC:arg[0-9]+]]: memref{{[^%]*}}%[[DST:arg[0-9]+]]: memref{{[^%]*}}%[[TOKEN:arg[0-9]+]]: i32 +// CHECK: %[[INPUT:.*]] = memref.reinterpret_cast %[[SRC]] to offset: [7], sizes: [1], strides: [1] +// CHECK: %[[CONSUMED:.*]] = hivm.hir.custom +// CHECK-SAME: "dist.aclshmem_consume_token_int64_ptr_1d" ins(%[[INPUT]], %[[TOKEN]] : memref<1xi64, strided<[1], offset: 7>>, i32) -> memref<1xi64> +// CHECK: %[[READ:.*]] = memref.reinterpret_cast %[[CONSUMED]] to offset: [7], sizes: [1], strides: [1] +// CHECK: memref.copy %[[READ]], +// CHECK: return +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @consume_splat_offset(%src: !tt.ptr, %dst: !tt.ptr, %token: i32) { + %offsets = arith.constant dense<7> : tensor<1xi32> + %base = tt.splat %src : !tt.ptr -> tensor<1x!tt.ptr> + %ptrs = tt.addptr %base, %offsets : tensor<1x!tt.ptr>, tensor<1xi32> + %empty = tensor.empty() : tensor<1x!tt.ptr> + %consumed = hivm.hir.custom + {hivm.is_distributed, SrcPtrIndex = array, no_side_effect, + hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, + symbol = "aclshmem_consume_token_int64_ptr_1d"} + "dist.aclshmem_consume_token_int64_ptr_1d" + ins(%ptrs, %token : tensor<1x!tt.ptr>, i32) + outs(%empty : tensor<1x!tt.ptr>) -> tensor<1x!tt.ptr> + %value = tt.load %consumed : tensor<1x!tt.ptr> + %out = tt.splat %dst : !tt.ptr -> tensor<1x!tt.ptr> + tt.store %out, %value : tensor<1x!tt.ptr> + tt.return + } +} + +// ----- + +// The same pointer has a metadata user (load) and a custom consumer. Only the +// ordinary load may use the metadata clone; the custom needs a live memref. +// CHECK-LABEL: func.func @consume_shared_splat( +// CHECK-SAME: %[[SRC:arg[0-9]+]]: memref{{[^%]*}}%[[DST:arg[0-9]+]]: memref{{[^%]*}}%[[TOKEN:arg[0-9]+]]: i32 +// CHECK: %[[INPUT:.*]] = memref.reinterpret_cast %[[SRC]] to offset: [0], sizes: [1], strides: [1] +// CHECK: %[[CONSUMED:.*]] = hivm.hir.custom +// CHECK-SAME: "dist.aclshmem_consume_token_int64_ptr_1d" ins(%[[INPUT]], %[[TOKEN]] : memref<1xi64, strided<[1]>>, i32) -> memref<1xi64> +// CHECK: %[[READ:.*]] = memref.reinterpret_cast %[[CONSUMED]] to offset: [0], sizes: [1], strides: [1] +// CHECK: memref.copy %[[READ]], +// CHECK: arith.addi +// CHECK: return +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @consume_shared_splat(%src: !tt.ptr, %dst: !tt.ptr, %token: i32) { + %ptrs = tt.splat %src : !tt.ptr -> tensor<1x!tt.ptr> + %before = tt.load %ptrs : tensor<1x!tt.ptr> + %empty = tensor.empty() : tensor<1x!tt.ptr> + %consumed = hivm.hir.custom + {hivm.is_distributed, SrcPtrIndex = array, no_side_effect, + hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, + symbol = "aclshmem_consume_token_int64_ptr_1d"} + "dist.aclshmem_consume_token_int64_ptr_1d" + ins(%ptrs, %token : tensor<1x!tt.ptr>, i32) + outs(%empty : tensor<1x!tt.ptr>) -> tensor<1x!tt.ptr> + %after = tt.load %consumed : tensor<1x!tt.ptr> + %sum = arith.addi %before, %after : tensor<1xi64> + %out = tt.splat %dst : !tt.ptr -> tensor<1x!tt.ptr> + tt.store %out, %sum : tensor<1x!tt.ptr> + tt.return + } +} + +// ----- + +// CustomMacroOp follows the same operand-use contract as CustomOp. +// CHECK-LABEL: func.func @consume_macro_splat( +// CHECK-SAME: %[[SRC:arg[0-9]+]]: memref{{[^%]*}}%[[DST:arg[0-9]+]]: memref{{[^%]*}}%[[TOKEN:arg[0-9]+]]: i32 +// CHECK: %[[INPUT:.*]] = memref.reinterpret_cast %[[SRC]] to offset: [0], sizes: [1], strides: [1] +// CHECK: %[[CONSUMED:.*]] = hivm.hir.custom_macro +// CHECK-SAME: "consume_macro" ins(%[[INPUT]], %[[TOKEN]] : memref<1xi64, strided<[1]>>, i32) -> memref<1xi64> +// CHECK: %[[READ:.*]] = memref.reinterpret_cast %[[CONSUMED]] to offset: [0], sizes: [1], strides: [1] +// CHECK: memref.copy %[[READ]], +// CHECK: return +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @consume_macro_splat(%src: !tt.ptr, %dst: !tt.ptr, %token: i32) { + %ptrs = tt.splat %src : !tt.ptr -> tensor<1x!tt.ptr> + %empty = tensor.empty() : tensor<1x!tt.ptr> + %consumed = hivm.hir.custom_macro + {hivm.is_distributed, SrcPtrIndex = array, + hivm.pipe_in = #hivm.pipe, hivm.pipe_out = #hivm.pipe, + hivm.tcore_type = #hivm.tcore_type, symbol = "consume_macro_impl"} + "consume_macro" + ins(%ptrs, %token : tensor<1x!tt.ptr>, i32) + outs(%empty : tensor<1x!tt.ptr>) -> tensor<1x!tt.ptr> + %value = tt.load %consumed : tensor<1x!tt.ptr> + %out = tt.splat %dst : !tt.ptr -> tensor<1x!tt.ptr> + tt.store %out, %value : tensor<1x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_op_ptr.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_op_ptr.mlir new file mode 100644 index 0000000000..f0709459b0 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/custom_op_ptr.mlir @@ -0,0 +1,84 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s + +// Test hivm.hir.custom with distributed type producing tensor results +// The custom op result type is converted to memref, and load uses the remapped result +// CHECK-LABEL: func.func @test_custom_op_ptr_output +// CHECK: hivm.hir.custom +// CHECK-SAME: hivm.is_distributed +// CHECK: memref + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_custom_op_ptr_output(%src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<8xf32> + %empty = tensor.empty() : tensor<8x!tt.ptr> + %c8_i32 = arith.constant 8 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %base = arith.muli %pid, %c8_i32 : i32 + %base_splat = tt.splat %base : i32 -> tensor<8xi32> + %offs_full = arith.addi %offs, %base_splat : tensor<8xi32> + + // Create input tensor + %src_ptrs = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr = tt.addptr %src_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + + // hivm.hir.custom with distributed type producing tensor output + // The SrcPtrIndex maps result to input operand 0 for structured analysis + %custom_result = hivm.hir.custom + {hivm.is_distributed, hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, symbol = "test_ptr_op_impl", SrcPtrIndex = array} + "test_ptr_op" + ins(%src_addptr : tensor<8x!tt.ptr>) + outs(%empty : tensor<8x!tt.ptr>) -> tensor<8x!tt.ptr> + + // Load from custom result pointer - uses remapped value after conversion + %data = tt.load %custom_result : tensor<8x!tt.ptr> + + // Store result + %dst_ptrs = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %data : tensor<8x!tt.ptr> + tt.return + } +} + +// ----- + +// Test hivm.hir.custom with distributed type producing tt.ptr results +// The custom op result type is converted to memref, and load uses the remapped result +// CHECK-LABEL: func.func @test_custom_op_ptr_output +// CHECK: hivm.hir.custom +// CHECK-SAME: hivm.is_distributed +// CHECK: memref + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_custom_op_ptr_output(%src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<8xf32> + %c8_i32 = arith.constant 8 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %base = arith.muli %pid, %c8_i32 : i32 + %base_splat = tt.splat %base : i32 -> tensor<8xi32> + %offs_full = arith.addi %offs, %base_splat : tensor<8xi32> + + // Create input tensor + %src_ptrs = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr = tt.addptr %src_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + + // hivm.hir.custom with distributed type producing tensor output + // The SrcPtrIndex maps result to input operand 0 for structured analysis + %custom_result = hivm.hir.custom + {hivm.is_distributed, hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, symbol = "test_ptr_op_impl"} + "test_ptr_op" + ins(%src_ptr : !tt.ptr) -> !tt.ptr + %custom_splat = tt.splat %custom_result : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr1 = tt.addptr %custom_splat, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + // Load from custom result pointer - uses remapped value after conversion + %data = tt.load %src_addptr1 : tensor<8x!tt.ptr> + + // Store result + %dst_ptrs = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %data : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/debug_nop.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/debug_nop.mlir new file mode 100644 index 0000000000..4330fd9e4c --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/debug_nop.mlir @@ -0,0 +1,55 @@ +// Test: insertDebugNop emits llvm.inline_asm "nop" ops only when +// TRITON_DEBUG=1 and LLVM_EXTRACT_DI_LOCAL_VARIABLES=1 is set, with the original source location preserved +// on each NOP. Without TRITON_DEBUG and LLVM_EXTRACT_DI_LOCAL_VARIABLES, no inline asm is emitted. +// +// RUN: triton-opt --triton-to-linalg --mlir-print-debuginfo --split-input-file %s | FileCheck %s --check-prefix=NODEBUG +// RUN: env TRITON_DEBUG=1 LLVM_EXTRACT_DI_LOCAL_VARIABLES=1 triton-opt --triton-to-linalg --mlir-print-debuginfo --split-input-file %s | FileCheck %s --check-prefix=DEBUG + +#loc = loc("test.py":6:0) +#loc1 = loc("test.py":11:24) +#loc2 = loc("test.py":12:32) +#loc3 = loc("test.py":16:28) +#loc4 = loc("test.py":18:34) +#loc_pid = loc("pid"(#loc1)) +#loc_numprogs = loc("num_progs"(#loc2)) +#loc_offsets = loc("offsets"(#loc3)) +#loc_full = loc("scalar_one"(#loc4)) + +tt.func public @test_kernel( + %ptr: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32 {tt.divisibility = 16 : i32}, + %arg2: i32, %arg3: i32, %arg4: i32, + %arg5: i32, %arg6: i32, %arg7: i32 +) attributes {noinline = false} { + %pid = tt.get_program_id x : i32 loc(#loc_pid) + %np = tt.get_num_programs x : i32 loc(#loc_numprogs) + %c128 = arith.constant 128 : i32 + %block_start = arith.muli %pid, %c128 : i32 + %offs = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %splat_bs = tt.splat %block_start : i32 -> tensor<128xi32> + %abs_offs = arith.addi %splat_bs, %offs : tensor<128xi32> loc(#loc_offsets) + %splat_ptr = tt.splat %ptr : !tt.ptr -> tensor<128x!tt.ptr> + %addptr = tt.addptr %splat_ptr, %abs_offs : tensor<128x!tt.ptr>, tensor<128xi32> loc(#loc_offsets) + // tl.full((128,), 1.0) lowers to: scalar constant + tt.splat to tensor<128xf32>. + // Without TRITON_DEBUG and LLVM_EXTRACT_DI_LOCAL_VARIABLES, SplatOp::fold collapses this into a dense<1.0> constant + // and the loc on the splat is lost. With TRITON_DEBUG=1 and LLVM_EXTRACT_DI_LOCAL_VARIABLES=1, the fold is suppressed + // and SplatConverter emits a NOP carrying loc("scalar_one"). + %cst_one = arith.constant 1.000000e+00 : f32 + %full = tt.splat %cst_one : f32 -> tensor<128xf32> loc(#loc_full) + %sum = arith.addi %pid, %np : i32 + %splat_sum = tt.splat %sum : i32 -> tensor<128xi32> + %sum_f32 = arith.sitofp %splat_sum : tensor<128xi32> to tensor<128xf32> + %final = arith.addf %sum_f32, %full : tensor<128xf32> + tt.store %addptr, %final : tensor<128x!tt.ptr> + tt.return +} loc(#loc) + +// DEBUG-LABEL: func.func @test_kernel +// DEBUG-COUNT-5: llvm.inline_asm has_side_effects asm_dialect = att "nop" +// DEBUG-DAG: loc("pid" +// DEBUG-DAG: loc("num_progs" +// DEBUG-DAG: loc("offsets" +// DEBUG-DAG: loc("scalar_one" + +// NODEBUG-LABEL: func.func @test_kernel +// NODEBUG-NOT: llvm.inline_asm diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dedup_nops.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dedup_nops.mlir new file mode 100644 index 0000000000..4179438720 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dedup_nops.mlir @@ -0,0 +1,37 @@ +// RUN: env LLVM_EXTRACT_DI_LOCAL_VARIABLES=1 triton-opt %s \ +// RUN: --deduplicate-debug-nops | FileCheck %s --check-prefix=DEDUP +// +// RUN: env LLVM_EXTRACT_DI_LOCAL_VARIABLES=0 triton-opt %s \ +// RUN: --deduplicate-debug-nops | FileCheck %s --check-prefix=NODEDUP + +// Four NOPs at line 40 (different columns). Three at line 42. +// Expected after dedup: 1 NOP at line 40, 1 NOP at line 42. + +module { + func.func @kernel() { + llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc(#loc_40_a) + llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc(#loc_40_b) + llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc(#loc_40_c) + llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc(#loc_40_d) + llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc(#loc_42_a) + llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc(#loc_42_b) + llvm.inline_asm has_side_effects asm_dialect = att "nop", "" : () -> () loc(#loc_42_c) + return + } loc(#loc_fn) +} + +#loc_fn = loc("test.py":1:0) +#loc_40_a = loc("test.py":40:10) +#loc_40_b = loc("test.py":40:20) +#loc_40_c = loc("test.py":40:30) +#loc_40_d = loc("test.py":40:40) +#loc_42_a = loc("test.py":42:10) +#loc_42_b = loc("test.py":42:20) +#loc_42_c = loc("test.py":42:30) + +// DEDUP: func @kernel +// DEDUP-COUNT-2: llvm.inline_asm{{.*}}"nop" +// DEDUP-NOT: llvm.inline_asm{{.*}}"nop" + +// NODEDUP: func @kernel +// NODEDUP-COUNT-7: llvm.inline_asm{{.*}}"nop" diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dotScaled_fp4.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dotScaled_fp4.mlir new file mode 100644 index 0000000000..a7852f24da --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dotScaled_fp4.mlir @@ -0,0 +1,60 @@ +// RUN: triton-opt --pass-pipeline="builtin.module(triton-to-unstructure{compile-on-910-95=true compile-mode=simd},triton-to-linalg{compile-on-910-95=true enable-nd2nz-on-vector=false global-kernel=false named-ops=true compile-mode=simd})" --split-input-file %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @_dot_scaled_test_fp4(%a_ptr: !tt.ptr, %a_scale_ptr: !tt.ptr, %b_ptr: !tt.ptr, %b_scale_ptr: !tt.ptr, %c_ptr: !tt.ptr) attributes {noinline = false} { + %cst = arith.constant dense<16> : tensor<16x1xi32> + %acc = arith.constant dense<0.000000e+00> : tensor<16x16xf32> + %b = arith.constant dense<16> : tensor<32x1xi32> + %cst_0 = arith.constant dense<2> : tensor<16x1xi32> + %a = arith.constant dense<32> : tensor<16x1xi32> + %m_offs = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %k_packed_offs = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %scale_offs = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %a_1 = tt.expand_dims %m_offs {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %a_2 = arith.muli %a_1, %a : tensor<16x1xi32> + %a_3 = tt.splat %a_ptr : !tt.ptr -> tensor<16x1x!tt.ptr> + %a_4 = tt.addptr %a_3, %a_2 : tensor<16x1x!tt.ptr>, tensor<16x1xi32> + %a_5 = tt.expand_dims %k_packed_offs {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %a_6 = tt.broadcast %a_4 : tensor<16x1x!tt.ptr> -> tensor<16x32x!tt.ptr> + %a_7 = tt.broadcast %a_5 : tensor<1x32xi32> -> tensor<16x32xi32> + %a_8 = tt.addptr %a_6, %a_7 : tensor<16x32x!tt.ptr>, tensor<16x32xi32> + %a_9 = tt.load %a_8 : tensor<16x32x!tt.ptr> + %a_scale = arith.muli %a_1, %cst_0 : tensor<16x1xi32> + %a_scale_10 = tt.splat %a_scale_ptr : !tt.ptr -> tensor<16x1x!tt.ptr> + %a_scale_11 = tt.addptr %a_scale_10, %a_scale : tensor<16x1x!tt.ptr>, tensor<16x1xi32> + %a_scale_12 = tt.expand_dims %scale_offs {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> + %a_scale_13 = tt.broadcast %a_scale_11 : tensor<16x1x!tt.ptr> -> tensor<16x2x!tt.ptr> + %a_scale_14 = tt.broadcast %a_scale_12 : tensor<1x2xi32> -> tensor<16x2xi32> + %a_scale_15 = tt.addptr %a_scale_13, %a_scale_14 : tensor<16x2x!tt.ptr>, tensor<16x2xi32> + %a_scale_16 = tt.load %a_scale_15 : tensor<16x2x!tt.ptr> + %b_17 = tt.expand_dims %k_packed_offs {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %b_18 = arith.muli %b_17, %b : tensor<32x1xi32> + %b_19 = tt.splat %b_ptr : !tt.ptr -> tensor<32x1x!tt.ptr> + %b_20 = tt.addptr %b_19, %b_18 : tensor<32x1x!tt.ptr>, tensor<32x1xi32> + %b_21 = tt.expand_dims %m_offs {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %b_22 = tt.broadcast %b_20 : tensor<32x1x!tt.ptr> -> tensor<32x16x!tt.ptr> + %b_23 = tt.broadcast %b_21 : tensor<1x16xi32> -> tensor<32x16xi32> + %b_24 = tt.addptr %b_22, %b_23 : tensor<32x16x!tt.ptr>, tensor<32x16xi32> + %b_25 = tt.load %b_24 : tensor<32x16x!tt.ptr> + %b_scale = tt.splat %b_scale_ptr : !tt.ptr -> tensor<16x1x!tt.ptr> + %b_scale_26 = tt.addptr %b_scale, %a_scale : tensor<16x1x!tt.ptr>, tensor<16x1xi32> + %b_scale_27 = tt.broadcast %b_scale_26 : tensor<16x1x!tt.ptr> -> tensor<16x2x!tt.ptr> + %b_scale_28 = tt.addptr %b_scale_27, %a_scale_14 : tensor<16x2x!tt.ptr>, tensor<16x2xi32> + %b_scale_29 = tt.load %b_scale_28 : tensor<16x2x!tt.ptr> + %acc_30 = tt.dot_scaled %a_9 scale %a_scale_16, %b_25 scale %b_scale_29, %acc lhs = e2m1 rhs = e2m1 {fastMath = false} : tensor<16x32xi8>, tensor<16x2xi8> * tensor<32x16xi8>, tensor<16x2xi8> -> tensor<16x16xf32> + %0 = arith.muli %a_1, %cst : tensor<16x1xi32> + %1 = tt.splat %c_ptr : !tt.ptr -> tensor<16x1x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<16x1x!tt.ptr>, tensor<16x1xi32> + %3 = tt.broadcast %2 : tensor<16x1x!tt.ptr> -> tensor<16x16x!tt.ptr> + %4 = tt.broadcast %b_21 : tensor<1x16xi32> -> tensor<16x16xi32> + %5 = tt.addptr %3, %4 : tensor<16x16x!tt.ptr>, tensor<16x16xi32> + tt.store %5, %acc_30 : tensor<16x16x!tt.ptr> + tt.return + } +} + + +// CHECK-LABEL: func.func @_dot_scaled_test_fp4 +// CHECK: hfusion.matmul_mx +// CHECK: outs([[OUT:.*]] : tensor<16x16xf32>) -> tensor<16x16xf32> +// CHECK: bufferization.materialize_in_destination diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dotScaled_fp8.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dotScaled_fp8.mlir new file mode 100644 index 0000000000..70d5914a8e --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dotScaled_fp8.mlir @@ -0,0 +1,62 @@ +// RUN: triton-opt --pass-pipeline="builtin.module(triton-to-unstructure{compile-on-910-95=true compile-mode=simd},triton-to-linalg{compile-on-910-95=true enable-nd2nz-on-vector=false global-kernel=false named-ops=true compile-mode=simd})" --split-input-file %s | FileCheck %s + +module { + tt.func public @dot_scale_fp8_kernel(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: i32 {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: !tt.ptr {tt.divisibility = 16 : i32}, %arg4: i32 {tt.divisibility = 16 : i32}, %arg5: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<32> : tensor<32x1xi32> + %cst_0 = arith.constant dense<0.000000e+00> : tensor<32x32xf32> + %cst_1 = arith.constant dense<4> : tensor<32x1xi32> + %0 = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %2 = tt.splat %arg0 : !tt.ptr -> tensor<32x1x!tt.ptr> + %3 = tt.addptr %2, %1 : tensor<32x1x!tt.ptr>, tensor<32x1xi32> + %4 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<128xi32> -> tensor<1x128xi32> + %6 = tt.splat %arg1 : i32 -> tensor<1x128xi32> + %7 = arith.muli %5, %6 : tensor<1x128xi32> + %8 = tt.broadcast %3 : tensor<32x1x!tt.ptr> -> tensor<32x128x!tt.ptr> + %9 = tt.broadcast %7 : tensor<1x128xi32> -> tensor<32x128xi32> + %10 = tt.addptr %8, %9 : tensor<32x128x!tt.ptr>, tensor<32x128xi32> + %11 = tt.expand_dims %4 {axis = 1 : i32} : tensor<128xi32> -> tensor<128x1xi32> + %12 = tt.splat %arg3 : !tt.ptr -> tensor<128x1x!tt.ptr> + %13 = tt.addptr %12, %11 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %14 = tt.expand_dims %0 {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %15 = tt.splat %arg4 : i32 -> tensor<1x32xi32> + %16 = arith.muli %14, %15 : tensor<1x32xi32> + %17 = tt.broadcast %13 : tensor<128x1x!tt.ptr> -> tensor<128x32x!tt.ptr> + %18 = tt.broadcast %16 : tensor<1x32xi32> -> tensor<128x32xi32> + %19 = tt.addptr %17, %18 : tensor<128x32x!tt.ptr>, tensor<128x32xi32> + %20 = arith.muli %1, %cst_1 : tensor<32x1xi32> + %21 = tt.splat %arg2 : !tt.ptr -> tensor<32x1x!tt.ptr> + %22 = tt.addptr %21, %20 : tensor<32x1x!tt.ptr>, tensor<32x1xi32> + %23 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %24 = tt.expand_dims %23 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %25 = tt.broadcast %22 : tensor<32x1x!tt.ptr> -> tensor<32x4x!tt.ptr> + %26 = tt.broadcast %24 : tensor<1x4xi32> -> tensor<32x4xi32> + %27 = tt.addptr %25, %26 : tensor<32x4x!tt.ptr>, tensor<32x4xi32> + %28 = tt.load %10 : tensor<32x128x!tt.ptr> + %29 = tt.load %19 : tensor<128x32x!tt.ptr> + %30 = tt.load %27 : tensor<32x4x!tt.ptr> + %31 = tt.bitcast %28 : tensor<32x128xi8> -> tensor<32x128xf8E4M3FN> + %32 = tt.bitcast %29 : tensor<128x32xi8> -> tensor<128x32xf8E4M3FN> + %33 = tt.dot_scaled %31 scale %30, %32, %cst_0 lhs = e4m3 rhs = e4m3 {fastMath = false} : tensor<32x128xf8E4M3FN>, tensor<32x4xi8> * tensor<128x32xf8E4M3FN> -> tensor<32x32xf32> + %34 = arith.muli %1, %cst : tensor<32x1xi32> + %35 = tt.splat %arg5 : !tt.ptr -> tensor<32x1x!tt.ptr> + %36 = tt.addptr %35, %34 : tensor<32x1x!tt.ptr>, tensor<32x1xi32> + %37 = tt.broadcast %36 : tensor<32x1x!tt.ptr> -> tensor<32x32x!tt.ptr> + %38 = tt.broadcast %14 : tensor<1x32xi32> -> tensor<32x32xi32> + %39 = tt.addptr %37, %38 : tensor<32x32x!tt.ptr>, tensor<32x32xi32> + %40 = arith.truncf %33 : tensor<32x32xf32> to tensor<32x32xbf16> + tt.store %39, %40 : tensor<32x32x!tt.ptr> + tt.return + } +} + + +// CHECK-LABEL: func.func @dot_scale_fp8_kernel +// CHECK: hfusion.matmul_mx +// CHECK-DAG: tensor<32x128xf8E4M3FN> +// CHECK-DAG: tensor<128x32xf8E4M3FN> +// CHECK-DAG: tensor<32x4xi8> +// CHECK: outs([[OUT:.*]] : tensor<32x32xf32>) -> tensor<32x32xf32> +// CHECK: arith.truncf {{.*}} : tensor<32x32xf32> to tensor<32x32xbf16> +// CHECK: bufferization.materialize_in_destination diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dot_fractal.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dot_fractal.mlir new file mode 100644 index 0000000000..9b76f595af --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dot_fractal.mlir @@ -0,0 +1,398 @@ +// RUN: triton-opt --triton-to-linalg="named-ops=True" --split-input-file %s | FileCheck %s + +module { + tt.func public @case1_a_fractal(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) attributes {noinline = false} { + %cst = arith.constant dense<80> : tensor<160x1xi32> + %cst_0 = arith.constant dense<80> : tensor<320x1xi32> + %cst_1 = arith.constant dense<16> : tensor<1x1x16x1xi32> + %cst_2 = arith.constant dense<256> : tensor<1x10x1x1xi32> + %cst_3 = arith.constant dense<2560> : tensor<20x1x1x1xi32> + %0 = tt.make_range {end = 20 : i32, start = 0 : i32} : tensor<20xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<20xi32> -> tensor<20x1xi32> + %2 = tt.expand_dims %1 {axis = 2 : i32} : tensor<20x1xi32> -> tensor<20x1x1xi32> + %3 = tt.expand_dims %2 {axis = 3 : i32} : tensor<20x1x1xi32> -> tensor<20x1x1x1xi32> + %4 = tt.make_range {end = 10 : i32, start = 0 : i32} : tensor<10xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<10xi32> -> tensor<1x10xi32> + %6 = tt.expand_dims %5 {axis = 2 : i32} : tensor<1x10xi32> -> tensor<1x10x1xi32> + %7 = tt.expand_dims %6 {axis = 3 : i32} : tensor<1x10x1xi32> -> tensor<1x10x1x1xi32> + %8 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %9 = tt.expand_dims %8 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %10 = tt.expand_dims %9 {axis = 1 : i32} : tensor<1x16xi32> -> tensor<1x1x16xi32> + %11 = tt.expand_dims %10 {axis = 3 : i32} : tensor<1x1x16xi32> -> tensor<1x1x16x1xi32> + %12 = tt.expand_dims %10 {axis = 2 : i32} : tensor<1x1x16xi32> -> tensor<1x1x1x16xi32> + %13 = arith.muli %3, %cst_3 : tensor<20x1x1x1xi32> + %14 = arith.muli %7, %cst_2 : tensor<1x10x1x1xi32> + %15 = tt.broadcast %13 : tensor<20x1x1x1xi32> -> tensor<20x10x1x1xi32> + %16 = tt.broadcast %14 : tensor<1x10x1x1xi32> -> tensor<20x10x1x1xi32> + %17 = arith.addi %15, %16 : tensor<20x10x1x1xi32> + %18 = arith.muli %11, %cst_1 : tensor<1x1x16x1xi32> + %19 = tt.broadcast %17 : tensor<20x10x1x1xi32> -> tensor<20x10x16x1xi32> + %20 = tt.broadcast %18 : tensor<1x1x16x1xi32> -> tensor<20x10x16x1xi32> + %21 = arith.addi %19, %20 : tensor<20x10x16x1xi32> + %22 = tt.broadcast %21 : tensor<20x10x16x1xi32> -> tensor<20x10x16x16xi32> + %23 = tt.broadcast %12 : tensor<1x1x1x16xi32> -> tensor<20x10x16x16xi32> + %24 = arith.addi %22, %23 : tensor<20x10x16x16xi32> + %25 = tt.splat %arg0 : !tt.ptr -> tensor<20x10x16x16x!tt.ptr> + %26 = tt.addptr %25, %24 : tensor<20x10x16x16x!tt.ptr>, tensor<20x10x16x16xi32> + %27 = tt.load %26 : tensor<20x10x16x16x!tt.ptr> + %28 = tt.make_range {end = 320 : i32, start = 0 : i32} : tensor<320xi32> + %29 = tt.expand_dims %28 {axis = 1 : i32} : tensor<320xi32> -> tensor<320x1xi32> + %30 = arith.muli %29, %cst_0 : tensor<320x1xi32> + %31 = tt.make_range {end = 80 : i32, start = 0 : i32} : tensor<80xi32> + %32 = tt.expand_dims %31 {axis = 0 : i32} : tensor<80xi32> -> tensor<1x80xi32> + %33 = tt.broadcast %30 : tensor<320x1xi32> -> tensor<320x80xi32> + %34 = tt.broadcast %32 : tensor<1x80xi32> -> tensor<320x80xi32> + %35 = arith.addi %33, %34 : tensor<320x80xi32> + %36 = tt.splat %arg1 : !tt.ptr -> tensor<320x80x!tt.ptr> + %37 = tt.addptr %36, %35 : tensor<320x80x!tt.ptr>, tensor<320x80xi32> + %38 = tt.load %37 : tensor<320x80x!tt.ptr> + %39 = ascend.dot %27, %38 {fractal_a = true} : tensor<20x10x16x16xf16>, tensor<320x80xf16> -> tensor<160x80xf32> + %40 = tt.make_range {end = 160 : i32, start = 0 : i32} : tensor<160xi32> + %41 = tt.expand_dims %40 {axis = 1 : i32} : tensor<160xi32> -> tensor<160x1xi32> + %42 = arith.muli %41, %cst : tensor<160x1xi32> + %43 = tt.broadcast %42 : tensor<160x1xi32> -> tensor<160x80xi32> + %44 = tt.broadcast %32 : tensor<1x80xi32> -> tensor<160x80xi32> + %45 = arith.addi %43, %44 : tensor<160x80xi32> + %46 = tt.splat %arg2 : !tt.ptr -> tensor<160x80x!tt.ptr> + %47 = tt.addptr %46, %45 : tensor<160x80x!tt.ptr>, tensor<160x80xi32> + %48 = arith.truncf %39 : tensor<160x80xf32> to tensor<160x80xf16> + tt.store %47, %48 : tensor<160x80x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @case1_a_fractal +// CHECK-SAME: mix_mode = "mix" +// CHECK: hivm.hir.convert_layout %{{.*}} output_shape [160, 320] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<20x10x16x16xf16>) -> tensor<160x320xf16> +// CHECK: %[[ACC:.*]] = tensor.empty() : tensor<160x80xf32> +// CHECK: linalg.matmul {input_precision = "ieee"} ins(%{{.*}}, %{{.*}} : tensor<160x320xf16>, tensor<320x80xf16>) outs(%[[ACC]] : tensor<160x80xf32>) -> tensor<160x80xf32> +// ND result: no ND->Fractal convert afterwards. +// CHECK-NOT: hivm.hir.convert_layout + +// ----- + +module { + tt.func public @s_C1_pure_cube(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) attributes {noinline = false} { + %cst = arith.constant dense<2560> : tensor<5x1x1x1xi32> + %cst_0 = arith.constant dense<256> : tensor<1x20x1x1xi32> + %cst_1 = arith.constant dense<5120> : tensor<5x1x1x1xi32> + %cst_2 = arith.constant dense<16> : tensor<1x1x16x1xi32> + %cst_3 = arith.constant dense<256> : tensor<1x10x1x1xi32> + %cst_4 = arith.constant dense<2560> : tensor<20x1x1x1xi32> + %0 = tt.make_range {end = 20 : i32, start = 0 : i32} : tensor<20xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<20xi32> -> tensor<20x1xi32> + %2 = tt.expand_dims %1 {axis = 2 : i32} : tensor<20x1xi32> -> tensor<20x1x1xi32> + %3 = tt.expand_dims %2 {axis = 3 : i32} : tensor<20x1x1xi32> -> tensor<20x1x1x1xi32> + %4 = arith.muli %3, %cst_4 : tensor<20x1x1x1xi32> + %5 = tt.make_range {end = 10 : i32, start = 0 : i32} : tensor<10xi32> + %6 = tt.expand_dims %5 {axis = 0 : i32} : tensor<10xi32> -> tensor<1x10xi32> + %7 = tt.expand_dims %6 {axis = 2 : i32} : tensor<1x10xi32> -> tensor<1x10x1xi32> + %8 = tt.expand_dims %7 {axis = 3 : i32} : tensor<1x10x1xi32> -> tensor<1x10x1x1xi32> + %9 = arith.muli %8, %cst_3 : tensor<1x10x1x1xi32> + %10 = tt.broadcast %4 : tensor<20x1x1x1xi32> -> tensor<20x10x1x1xi32> + %11 = tt.broadcast %9 : tensor<1x10x1x1xi32> -> tensor<20x10x1x1xi32> + %12 = arith.addi %10, %11 : tensor<20x10x1x1xi32> + %13 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %14 = tt.expand_dims %13 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %15 = tt.expand_dims %14 {axis = 1 : i32} : tensor<1x16xi32> -> tensor<1x1x16xi32> + %16 = tt.expand_dims %15 {axis = 3 : i32} : tensor<1x1x16xi32> -> tensor<1x1x16x1xi32> + %17 = arith.muli %16, %cst_2 : tensor<1x1x16x1xi32> + %18 = tt.broadcast %12 : tensor<20x10x1x1xi32> -> tensor<20x10x16x1xi32> + %19 = tt.broadcast %17 : tensor<1x1x16x1xi32> -> tensor<20x10x16x1xi32> + %20 = arith.addi %18, %19 : tensor<20x10x16x1xi32> + %21 = tt.expand_dims %15 {axis = 2 : i32} : tensor<1x1x16xi32> -> tensor<1x1x1x16xi32> + %22 = tt.broadcast %20 : tensor<20x10x16x1xi32> -> tensor<20x10x16x16xi32> + %23 = tt.broadcast %21 : tensor<1x1x1x16xi32> -> tensor<20x10x16x16xi32> + %24 = arith.addi %22, %23 : tensor<20x10x16x16xi32> + %25 = tt.splat %arg0 : !tt.ptr -> tensor<20x10x16x16x!tt.ptr> + %26 = tt.addptr %25, %24 : tensor<20x10x16x16x!tt.ptr>, tensor<20x10x16x16xi32> + %27 = tt.load %26 : tensor<20x10x16x16x!tt.ptr> + %28 = tt.make_range {end = 5 : i32, start = 0 : i32} : tensor<5xi32> + %29 = tt.expand_dims %28 {axis = 1 : i32} : tensor<5xi32> -> tensor<5x1xi32> + %30 = tt.expand_dims %29 {axis = 2 : i32} : tensor<5x1xi32> -> tensor<5x1x1xi32> + %31 = tt.expand_dims %30 {axis = 3 : i32} : tensor<5x1x1xi32> -> tensor<5x1x1x1xi32> + %32 = arith.muli %31, %cst_1 : tensor<5x1x1x1xi32> + %33 = tt.expand_dims %0 {axis = 0 : i32} : tensor<20xi32> -> tensor<1x20xi32> + %34 = tt.expand_dims %33 {axis = 2 : i32} : tensor<1x20xi32> -> tensor<1x20x1xi32> + %35 = tt.expand_dims %34 {axis = 3 : i32} : tensor<1x20x1xi32> -> tensor<1x20x1x1xi32> + %36 = arith.muli %35, %cst_0 : tensor<1x20x1x1xi32> + %37 = tt.broadcast %32 : tensor<5x1x1x1xi32> -> tensor<5x20x1x1xi32> + %38 = tt.broadcast %36 : tensor<1x20x1x1xi32> -> tensor<5x20x1x1xi32> + %39 = arith.addi %37, %38 : tensor<5x20x1x1xi32> + %40 = tt.broadcast %39 : tensor<5x20x1x1xi32> -> tensor<5x20x16x1xi32> + %41 = tt.broadcast %17 : tensor<1x1x16x1xi32> -> tensor<5x20x16x1xi32> + %42 = arith.addi %40, %41 : tensor<5x20x16x1xi32> + %43 = tt.broadcast %42 : tensor<5x20x16x1xi32> -> tensor<5x20x16x16xi32> + %44 = tt.broadcast %21 : tensor<1x1x1x16xi32> -> tensor<5x20x16x16xi32> + %45 = arith.addi %43, %44 : tensor<5x20x16x16xi32> + %46 = tt.splat %arg1 : !tt.ptr -> tensor<5x20x16x16x!tt.ptr> + %47 = tt.addptr %46, %45 : tensor<5x20x16x16x!tt.ptr>, tensor<5x20x16x16xi32> + %48 = tt.load %47 : tensor<5x20x16x16x!tt.ptr> + %49 = ascend.dot %27, %48 {fractal_a = true, fractal_b = true, fractal_c = true} : tensor<20x10x16x16xf16>, tensor<5x20x16x16xf16> -> tensor<5x10x16x16xf32> + %50 = arith.muli %31, %cst : tensor<5x1x1x1xi32> + %51 = tt.broadcast %50 : tensor<5x1x1x1xi32> -> tensor<5x10x1x1xi32> + %52 = tt.broadcast %9 : tensor<1x10x1x1xi32> -> tensor<5x10x1x1xi32> + %53 = arith.addi %51, %52 : tensor<5x10x1x1xi32> + %54 = tt.broadcast %53 : tensor<5x10x1x1xi32> -> tensor<5x10x16x1xi32> + %55 = tt.broadcast %17 : tensor<1x1x16x1xi32> -> tensor<5x10x16x1xi32> + %56 = arith.addi %54, %55 : tensor<5x10x16x1xi32> + %57 = tt.broadcast %56 : tensor<5x10x16x1xi32> -> tensor<5x10x16x16xi32> + %58 = tt.broadcast %21 : tensor<1x1x1x16xi32> -> tensor<5x10x16x16xi32> + %59 = arith.addi %57, %58 : tensor<5x10x16x16xi32> + %60 = tt.splat %arg2 : !tt.ptr -> tensor<5x10x16x16x!tt.ptr> + %61 = tt.addptr %60, %59 : tensor<5x10x16x16x!tt.ptr>, tensor<5x10x16x16xi32> + %62 = arith.truncf %49 : tensor<5x10x16x16xf32> to tensor<5x10x16x16xf16> + tt.store %61, %62 : tensor<5x10x16x16x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @s_C1_pure_cube +// CHECK-SAME: mix_mode = "mix" +// CHECK: hivm.hir.convert_layout %{{.*}} output_shape [160, 320] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<20x10x16x16xf16>) -> tensor<160x320xf16> +// CHECK: hivm.hir.convert_layout %{{.*}} output_shape [320, 80] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<5x20x16x16xf16>) -> tensor<320x80xf16> +// CHECK: tensor.empty() : tensor<160x80xf32> +// CHECK: %[[MM:.*]] = linalg.matmul {input_precision = "ieee"} ins(%{{.*}}, %{{.*}} : tensor<160x320xf16>, tensor<320x80xf16>) outs(%{{.*}} : tensor<160x80xf32>) -> tensor<160x80xf32> +// fractal_c: ND [M,N] -> [N/16, M/16, 16, 16], carrying the f32 accumulator dtype. +// CHECK: hivm.hir.convert_layout %[[MM]] output_shape [5, 10, 16, 16] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<160x80xf32>) -> tensor<5x10x16x16xf32> + +// ----- + +module { + tt.func public @s_C_bothND_fracC(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) attributes {noinline = false} { + %cst = arith.constant dense<16> : tensor<1x1x16x1xi32> + %cst_0 = arith.constant dense<256> : tensor<1x10x1x1xi32> + %cst_1 = arith.constant dense<2560> : tensor<5x1x1x1xi32> + %cst_2 = arith.constant dense<80> : tensor<320x1xi32> + %cst_3 = arith.constant dense<320> : tensor<160x1xi32> + %0 = tt.make_range {end = 160 : i32, start = 0 : i32} : tensor<160xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<160xi32> -> tensor<160x1xi32> + %2 = arith.muli %1, %cst_3 : tensor<160x1xi32> + %3 = tt.make_range {end = 320 : i32, start = 0 : i32} : tensor<320xi32> + %4 = tt.expand_dims %3 {axis = 0 : i32} : tensor<320xi32> -> tensor<1x320xi32> + %5 = tt.broadcast %2 : tensor<160x1xi32> -> tensor<160x320xi32> + %6 = tt.broadcast %4 : tensor<1x320xi32> -> tensor<160x320xi32> + %7 = arith.addi %5, %6 : tensor<160x320xi32> + %8 = tt.splat %arg0 : !tt.ptr -> tensor<160x320x!tt.ptr> + %9 = tt.addptr %8, %7 : tensor<160x320x!tt.ptr>, tensor<160x320xi32> + %10 = tt.load %9 : tensor<160x320x!tt.ptr> + %11 = tt.expand_dims %3 {axis = 1 : i32} : tensor<320xi32> -> tensor<320x1xi32> + %12 = arith.muli %11, %cst_2 : tensor<320x1xi32> + %13 = tt.make_range {end = 80 : i32, start = 0 : i32} : tensor<80xi32> + %14 = tt.expand_dims %13 {axis = 0 : i32} : tensor<80xi32> -> tensor<1x80xi32> + %15 = tt.broadcast %12 : tensor<320x1xi32> -> tensor<320x80xi32> + %16 = tt.broadcast %14 : tensor<1x80xi32> -> tensor<320x80xi32> + %17 = arith.addi %15, %16 : tensor<320x80xi32> + %18 = tt.splat %arg1 : !tt.ptr -> tensor<320x80x!tt.ptr> + %19 = tt.addptr %18, %17 : tensor<320x80x!tt.ptr>, tensor<320x80xi32> + %20 = tt.load %19 : tensor<320x80x!tt.ptr> + %21 = ascend.dot %10, %20 {fractal_c = true} : tensor<160x320xf16>, tensor<320x80xf16> -> tensor<5x10x16x16xf32> + %22 = tt.make_range {end = 5 : i32, start = 0 : i32} : tensor<5xi32> + %23 = tt.expand_dims %22 {axis = 1 : i32} : tensor<5xi32> -> tensor<5x1xi32> + %24 = tt.expand_dims %23 {axis = 2 : i32} : tensor<5x1xi32> -> tensor<5x1x1xi32> + %25 = tt.expand_dims %24 {axis = 3 : i32} : tensor<5x1x1xi32> -> tensor<5x1x1x1xi32> + %26 = arith.muli %25, %cst_1 : tensor<5x1x1x1xi32> + %27 = tt.make_range {end = 10 : i32, start = 0 : i32} : tensor<10xi32> + %28 = tt.expand_dims %27 {axis = 0 : i32} : tensor<10xi32> -> tensor<1x10xi32> + %29 = tt.expand_dims %28 {axis = 2 : i32} : tensor<1x10xi32> -> tensor<1x10x1xi32> + %30 = tt.expand_dims %29 {axis = 3 : i32} : tensor<1x10x1xi32> -> tensor<1x10x1x1xi32> + %31 = arith.muli %30, %cst_0 : tensor<1x10x1x1xi32> + %32 = tt.broadcast %26 : tensor<5x1x1x1xi32> -> tensor<5x10x1x1xi32> + %33 = tt.broadcast %31 : tensor<1x10x1x1xi32> -> tensor<5x10x1x1xi32> + %34 = arith.addi %32, %33 : tensor<5x10x1x1xi32> + %35 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %36 = tt.expand_dims %35 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %37 = tt.expand_dims %36 {axis = 1 : i32} : tensor<1x16xi32> -> tensor<1x1x16xi32> + %38 = tt.expand_dims %37 {axis = 3 : i32} : tensor<1x1x16xi32> -> tensor<1x1x16x1xi32> + %39 = arith.muli %38, %cst : tensor<1x1x16x1xi32> + %40 = tt.broadcast %34 : tensor<5x10x1x1xi32> -> tensor<5x10x16x1xi32> + %41 = tt.broadcast %39 : tensor<1x1x16x1xi32> -> tensor<5x10x16x1xi32> + %42 = arith.addi %40, %41 : tensor<5x10x16x1xi32> + %43 = tt.expand_dims %37 {axis = 2 : i32} : tensor<1x1x16xi32> -> tensor<1x1x1x16xi32> + %44 = tt.broadcast %42 : tensor<5x10x16x1xi32> -> tensor<5x10x16x16xi32> + %45 = tt.broadcast %43 : tensor<1x1x1x16xi32> -> tensor<5x10x16x16xi32> + %46 = arith.addi %44, %45 : tensor<5x10x16x16xi32> + %47 = tt.splat %arg2 : !tt.ptr -> tensor<5x10x16x16x!tt.ptr> + %48 = tt.addptr %47, %46 : tensor<5x10x16x16x!tt.ptr>, tensor<5x10x16x16xi32> + %49 = arith.truncf %21 : tensor<5x10x16x16xf32> to tensor<5x10x16x16xf16> + tt.store %48, %49 : tensor<5x10x16x16x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @s_C_bothND_fracC +// CHECK-SAME: mix_mode = "mix" +// ND operands: no Fractal->ND convert is emitted before the matmul. +// CHECK-NOT: hivm.hir.convert_layout +// CHECK: tensor.empty() : tensor<160x80xf32> +// CHECK: %[[MM:.*]] = linalg.matmul {input_precision = "ieee"} ins(%{{.*}}, %{{.*}} : tensor<160x320xf16>, tensor<320x80xf16>) outs(%{{.*}} : tensor<160x80xf32>) -> tensor<160x80xf32> +// CHECK: hivm.hir.convert_layout %[[MM]] output_shape [5, 10, 16, 16] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<160x80xf32>) -> tensor<5x10x16x16xf32> + +// ----- + +module { + tt.func public @s_mix_f32(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) attributes {noinline = false} { + %cst = arith.constant dense<80> : tensor<160x1xi32> + %cst_0 = arith.constant dense<80> : tensor<320x1xi32> + %cst_1 = arith.constant dense<8> : tensor<1x1x16x1xi32> + %cst_2 = arith.constant dense<128> : tensor<1x10x1x1xi32> + %cst_3 = arith.constant dense<1280> : tensor<40x1x1x1xi32> + %0 = tt.make_range {end = 40 : i32, start = 0 : i32} : tensor<40xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<40xi32> -> tensor<40x1xi32> + %2 = tt.expand_dims %1 {axis = 2 : i32} : tensor<40x1xi32> -> tensor<40x1x1xi32> + %3 = tt.expand_dims %2 {axis = 3 : i32} : tensor<40x1x1xi32> -> tensor<40x1x1x1xi32> + %4 = arith.muli %3, %cst_3 : tensor<40x1x1x1xi32> + %5 = tt.make_range {end = 10 : i32, start = 0 : i32} : tensor<10xi32> + %6 = tt.expand_dims %5 {axis = 0 : i32} : tensor<10xi32> -> tensor<1x10xi32> + %7 = tt.expand_dims %6 {axis = 2 : i32} : tensor<1x10xi32> -> tensor<1x10x1xi32> + %8 = tt.expand_dims %7 {axis = 3 : i32} : tensor<1x10x1xi32> -> tensor<1x10x1x1xi32> + %9 = arith.muli %8, %cst_2 : tensor<1x10x1x1xi32> + %10 = tt.broadcast %4 : tensor<40x1x1x1xi32> -> tensor<40x10x1x1xi32> + %11 = tt.broadcast %9 : tensor<1x10x1x1xi32> -> tensor<40x10x1x1xi32> + %12 = arith.addi %10, %11 : tensor<40x10x1x1xi32> + %13 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %14 = tt.expand_dims %13 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %15 = tt.expand_dims %14 {axis = 1 : i32} : tensor<1x16xi32> -> tensor<1x1x16xi32> + %16 = tt.expand_dims %15 {axis = 3 : i32} : tensor<1x1x16xi32> -> tensor<1x1x16x1xi32> + %17 = arith.muli %16, %cst_1 : tensor<1x1x16x1xi32> + %18 = tt.broadcast %12 : tensor<40x10x1x1xi32> -> tensor<40x10x16x1xi32> + %19 = tt.broadcast %17 : tensor<1x1x16x1xi32> -> tensor<40x10x16x1xi32> + %20 = arith.addi %18, %19 : tensor<40x10x16x1xi32> + %21 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %22 = tt.expand_dims %21 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %23 = tt.expand_dims %22 {axis = 1 : i32} : tensor<1x8xi32> -> tensor<1x1x8xi32> + %24 = tt.expand_dims %23 {axis = 2 : i32} : tensor<1x1x8xi32> -> tensor<1x1x1x8xi32> + %25 = tt.broadcast %20 : tensor<40x10x16x1xi32> -> tensor<40x10x16x8xi32> + %26 = tt.broadcast %24 : tensor<1x1x1x8xi32> -> tensor<40x10x16x8xi32> + %27 = arith.addi %25, %26 : tensor<40x10x16x8xi32> + %28 = tt.splat %arg0 : !tt.ptr -> tensor<40x10x16x8x!tt.ptr> + %29 = tt.addptr %28, %27 : tensor<40x10x16x8x!tt.ptr>, tensor<40x10x16x8xi32> + %30 = tt.load %29 : tensor<40x10x16x8x!tt.ptr> + %31 = tt.make_range {end = 320 : i32, start = 0 : i32} : tensor<320xi32> + %32 = tt.expand_dims %31 {axis = 1 : i32} : tensor<320xi32> -> tensor<320x1xi32> + %33 = arith.muli %32, %cst_0 : tensor<320x1xi32> + %34 = tt.make_range {end = 80 : i32, start = 0 : i32} : tensor<80xi32> + %35 = tt.expand_dims %34 {axis = 0 : i32} : tensor<80xi32> -> tensor<1x80xi32> + %36 = tt.broadcast %33 : tensor<320x1xi32> -> tensor<320x80xi32> + %37 = tt.broadcast %35 : tensor<1x80xi32> -> tensor<320x80xi32> + %38 = arith.addi %36, %37 : tensor<320x80xi32> + %39 = tt.splat %arg1 : !tt.ptr -> tensor<320x80x!tt.ptr> + %40 = tt.addptr %39, %38 : tensor<320x80x!tt.ptr>, tensor<320x80xi32> + %41 = tt.load %40 : tensor<320x80x!tt.ptr> + %42 = ascend.dot %30, %41 {fractal_a = true} : tensor<40x10x16x8xf32>, tensor<320x80xf32> -> tensor<160x80xf32> + %43 = tt.make_range {end = 160 : i32, start = 0 : i32} : tensor<160xi32> + %44 = tt.expand_dims %43 {axis = 1 : i32} : tensor<160xi32> -> tensor<160x1xi32> + %45 = arith.muli %44, %cst : tensor<160x1xi32> + %46 = tt.broadcast %45 : tensor<160x1xi32> -> tensor<160x80xi32> + %47 = tt.broadcast %35 : tensor<1x80xi32> -> tensor<160x80xi32> + %48 = arith.addi %46, %47 : tensor<160x80xi32> + %49 = tt.splat %arg2 : !tt.ptr -> tensor<160x80x!tt.ptr> + %50 = tt.addptr %49, %48 : tensor<160x80x!tt.ptr>, tensor<160x80xi32> + tt.store %50, %42 : tensor<160x80x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @s_mix_f32 +// CHECK-SAME: mix_mode = "mix" +// CHECK: hivm.hir.convert_layout %{{.*}} output_shape [160, 320] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<40x10x16x8xf32>) -> tensor<160x320xf32> +// CHECK: tensor.empty() : tensor<160x80xf32> +// CHECK: linalg.matmul {input_precision = "ieee"} ins(%{{.*}}, %{{.*}} : tensor<160x320xf32>, tensor<320x80xf32>) outs(%{{.*}} : tensor<160x80xf32>) -> tensor<160x80xf32> + +// ----- + +module { + tt.func public @s_C_int8(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) attributes {noinline = false} { + %cst = arith.constant dense<16> : tensor<1x1x16x1xi32> + %cst_0 = arith.constant dense<256> : tensor<1x10x1x1xi32> + %cst_1 = arith.constant dense<2560> : tensor<4x1x1x1xi32> + %cst_2 = arith.constant dense<32> : tensor<1x1x32x1xi32> + %cst_3 = arith.constant dense<1024> : tensor<1x10x1x1xi32> + %cst_4 = arith.constant dense<10240> : tensor<2x1x1x1xi32> + %cst_5 = arith.constant dense<32> : tensor<1x1x16x1xi32> + %cst_6 = arith.constant dense<512> : tensor<1x10x1x1xi32> + %cst_7 = arith.constant dense<5120> : tensor<10x1x1x1xi32> + %0 = tt.make_range {end = 10 : i32, start = 0 : i32} : tensor<10xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<10xi32> -> tensor<10x1xi32> + %2 = tt.expand_dims %1 {axis = 2 : i32} : tensor<10x1xi32> -> tensor<10x1x1xi32> + %3 = tt.expand_dims %2 {axis = 3 : i32} : tensor<10x1x1xi32> -> tensor<10x1x1x1xi32> + %4 = arith.muli %3, %cst_7 : tensor<10x1x1x1xi32> + %5 = tt.expand_dims %0 {axis = 0 : i32} : tensor<10xi32> -> tensor<1x10xi32> + %6 = tt.expand_dims %5 {axis = 2 : i32} : tensor<1x10xi32> -> tensor<1x10x1xi32> + %7 = tt.expand_dims %6 {axis = 3 : i32} : tensor<1x10x1xi32> -> tensor<1x10x1x1xi32> + %8 = arith.muli %7, %cst_6 : tensor<1x10x1x1xi32> + %9 = tt.broadcast %4 : tensor<10x1x1x1xi32> -> tensor<10x10x1x1xi32> + %10 = tt.broadcast %8 : tensor<1x10x1x1xi32> -> tensor<10x10x1x1xi32> + %11 = arith.addi %9, %10 : tensor<10x10x1x1xi32> + %12 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %13 = tt.expand_dims %12 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %14 = tt.expand_dims %13 {axis = 1 : i32} : tensor<1x16xi32> -> tensor<1x1x16xi32> + %15 = tt.expand_dims %14 {axis = 3 : i32} : tensor<1x1x16xi32> -> tensor<1x1x16x1xi32> + %16 = arith.muli %15, %cst_5 : tensor<1x1x16x1xi32> + %17 = tt.broadcast %11 : tensor<10x10x1x1xi32> -> tensor<10x10x16x1xi32> + %18 = tt.broadcast %16 : tensor<1x1x16x1xi32> -> tensor<10x10x16x1xi32> + %19 = arith.addi %17, %18 : tensor<10x10x16x1xi32> + %20 = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %21 = tt.expand_dims %20 {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %22 = tt.expand_dims %21 {axis = 1 : i32} : tensor<1x32xi32> -> tensor<1x1x32xi32> + %23 = tt.expand_dims %22 {axis = 2 : i32} : tensor<1x1x32xi32> -> tensor<1x1x1x32xi32> + %24 = tt.broadcast %19 : tensor<10x10x16x1xi32> -> tensor<10x10x16x32xi32> + %25 = tt.broadcast %23 : tensor<1x1x1x32xi32> -> tensor<10x10x16x32xi32> + %26 = arith.addi %24, %25 : tensor<10x10x16x32xi32> + %27 = tt.splat %arg0 : !tt.ptr -> tensor<10x10x16x32x!tt.ptr> + %28 = tt.addptr %27, %26 : tensor<10x10x16x32x!tt.ptr>, tensor<10x10x16x32xi32> + %29 = tt.load %28 : tensor<10x10x16x32x!tt.ptr> + %30 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %31 = tt.expand_dims %30 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %32 = tt.expand_dims %31 {axis = 2 : i32} : tensor<2x1xi32> -> tensor<2x1x1xi32> + %33 = tt.expand_dims %32 {axis = 3 : i32} : tensor<2x1x1xi32> -> tensor<2x1x1x1xi32> + %34 = arith.muli %33, %cst_4 : tensor<2x1x1x1xi32> + %35 = arith.muli %7, %cst_3 : tensor<1x10x1x1xi32> + %36 = tt.broadcast %34 : tensor<2x1x1x1xi32> -> tensor<2x10x1x1xi32> + %37 = tt.broadcast %35 : tensor<1x10x1x1xi32> -> tensor<2x10x1x1xi32> + %38 = arith.addi %36, %37 : tensor<2x10x1x1xi32> + %39 = tt.expand_dims %22 {axis = 3 : i32} : tensor<1x1x32xi32> -> tensor<1x1x32x1xi32> + %40 = arith.muli %39, %cst_2 : tensor<1x1x32x1xi32> + %41 = tt.broadcast %38 : tensor<2x10x1x1xi32> -> tensor<2x10x32x1xi32> + %42 = tt.broadcast %40 : tensor<1x1x32x1xi32> -> tensor<2x10x32x1xi32> + %43 = arith.addi %41, %42 : tensor<2x10x32x1xi32> + %44 = tt.broadcast %43 : tensor<2x10x32x1xi32> -> tensor<2x10x32x32xi32> + %45 = tt.broadcast %23 : tensor<1x1x1x32xi32> -> tensor<2x10x32x32xi32> + %46 = arith.addi %44, %45 : tensor<2x10x32x32xi32> + %47 = tt.splat %arg1 : !tt.ptr -> tensor<2x10x32x32x!tt.ptr> + %48 = tt.addptr %47, %46 : tensor<2x10x32x32x!tt.ptr>, tensor<2x10x32x32xi32> + %49 = tt.load %48 : tensor<2x10x32x32x!tt.ptr> + %50 = ascend.dot %29, %49 {fractal_a = true, fractal_b = true, fractal_c = true} : tensor<10x10x16x32xi8>, tensor<2x10x32x32xi8> -> tensor<4x10x16x16xi32> + %51 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %52 = tt.expand_dims %51 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %53 = tt.expand_dims %52 {axis = 2 : i32} : tensor<4x1xi32> -> tensor<4x1x1xi32> + %54 = tt.expand_dims %53 {axis = 3 : i32} : tensor<4x1x1xi32> -> tensor<4x1x1x1xi32> + %55 = arith.muli %54, %cst_1 : tensor<4x1x1x1xi32> + %56 = arith.muli %7, %cst_0 : tensor<1x10x1x1xi32> + %57 = tt.broadcast %55 : tensor<4x1x1x1xi32> -> tensor<4x10x1x1xi32> + %58 = tt.broadcast %56 : tensor<1x10x1x1xi32> -> tensor<4x10x1x1xi32> + %59 = arith.addi %57, %58 : tensor<4x10x1x1xi32> + %60 = arith.muli %15, %cst : tensor<1x1x16x1xi32> + %61 = tt.broadcast %59 : tensor<4x10x1x1xi32> -> tensor<4x10x16x1xi32> + %62 = tt.broadcast %60 : tensor<1x1x16x1xi32> -> tensor<4x10x16x1xi32> + %63 = arith.addi %61, %62 : tensor<4x10x16x1xi32> + %64 = tt.expand_dims %14 {axis = 2 : i32} : tensor<1x1x16xi32> -> tensor<1x1x1x16xi32> + %65 = tt.broadcast %63 : tensor<4x10x16x1xi32> -> tensor<4x10x16x16xi32> + %66 = tt.broadcast %64 : tensor<1x1x1x16xi32> -> tensor<4x10x16x16xi32> + %67 = arith.addi %65, %66 : tensor<4x10x16x16xi32> + %68 = tt.splat %arg2 : !tt.ptr -> tensor<4x10x16x16x!tt.ptr> + %69 = tt.addptr %68, %67 : tensor<4x10x16x16x!tt.ptr>, tensor<4x10x16x16xi32> + tt.store %69, %50 : tensor<4x10x16x16x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @s_C_int8 +// CHECK-SAME: mix_mode = "mix" +// CHECK: hivm.hir.convert_layout %{{.*}} output_shape [160, 320] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<10x10x16x32xi8>) -> tensor<160x320xi8> +// CHECK: hivm.hir.convert_layout %{{.*}} output_shape [320, 64] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<2x10x32x32xi8>) -> tensor<320x64xi8> +// i32 accumulator for an int8 input. +// CHECK: tensor.empty() : tensor<160x64xi32> +// CHECK: %[[MM:.*]] = linalg.matmul {input_precision = "ieee"} ins(%{{.*}}, %{{.*}} : tensor<160x320xi8>, tensor<320x64xi8>) outs(%{{.*}} : tensor<160x64xi32>) -> tensor<160x64xi32> +// fractal_c block stays [16, 16] even though the int8 input block is [16, 32]. +// CHECK: hivm.hir.convert_layout %[[MM]] output_shape [4, 10, 16, 16] {dstLayout = #hivm.data_layout, srcLayout = #hivm.data_layout} : (tensor<160x64xi32>) -> tensor<4x10x16x16xi32> diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dot_int4.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dot_int4.mlir new file mode 100644 index 0000000000..c536393550 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/dot_int4.mlir @@ -0,0 +1,80 @@ +// RUN: triton-opt --pass-pipeline="builtin.module(auto-blockify{auto-blockify-size=1},triton-to-structured{enable-mask-fallback-conversion=false optimize-dynamic-offset=false},discrete-mask-access-conversion{compile-on-910-95=false compile-mode=simd_simt_template},triton-to-annotation,triton-to-unstructure{compile-on-910-95=false compile-mode=simd_simt_template},triton-to-hivm,triton-to-hfusion,triton-to-llvm,bubble-up-operation,triton-to-structured{enable-mask-fallback-conversion=false optimize-dynamic-offset=false},triton-to-linalg{compile-on-910-95=false enable-nd2nz-on-vector=false enable-select-analysis=true global-kernel=false named-ops=true compile-mode=simd_simt_template})" --split-input-file %s | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @matmul_kernel(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: i32, %arg4: i32, %arg5: i32, %arg6: i32, %arg7: i32, %arg8: i32) attributes {noinline = false} { + %cst = arith.constant dense<0> : tensor<242x128xi8> + %cst_0 = arith.constant dense<0> : tensor<128x242xi8> + %cst_1 = arith.constant dense<0> : tensor<128x128xi32> + %c128_i32 = arith.constant 128 : i32 + %0 = tt.get_program_id x : i32 + %1 = tt.get_program_id y : i32 + %2 = arith.muli %0, %c128_i32 : i32 + %3 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %4 = tt.splat %2 : i32 -> tensor<128xi32> + %5 = arith.addi %4, %3 : tensor<128xi32> + %6 = arith.muli %1, %c128_i32 : i32 + %7 = tt.splat %6 : i32 -> tensor<128xi32> + %8 = arith.addi %7, %3 : tensor<128xi32> + %9 = tt.make_range {end = 242 : i32, start = 0 : i32} : tensor<242xi32> + %10 = tt.expand_dims %5 {axis = 1 : i32} : tensor<128xi32> -> tensor<128x1xi32> + %11 = tt.splat %arg6 : i32 -> tensor<128x1xi32> + %12 = arith.muli %10, %11 : tensor<128x1xi32> + %13 = tt.splat %arg0 : !tt.ptr -> tensor<128x1x!tt.ptr> + %14 = tt.addptr %13, %12 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %15 = tt.expand_dims %9 {axis = 0 : i32} : tensor<242xi32> -> tensor<1x242xi32> + %16 = tt.broadcast %14 : tensor<128x1x!tt.ptr> -> tensor<128x242x!tt.ptr> + %17 = tt.broadcast %15 : tensor<1x242xi32> -> tensor<128x242xi32> + %18 = tt.addptr %16, %17 : tensor<128x242x!tt.ptr>, tensor<128x242xi32> + %19 = tt.expand_dims %9 {axis = 1 : i32} : tensor<242xi32> -> tensor<242x1xi32> + %20 = tt.splat %arg7 : i32 -> tensor<242x1xi32> + %21 = arith.muli %19, %20 : tensor<242x1xi32> + %22 = tt.splat %arg1 : !tt.ptr -> tensor<242x1x!tt.ptr> + %23 = tt.addptr %22, %21 : tensor<242x1x!tt.ptr>, tensor<242x1xi32> + %24 = tt.expand_dims %8 {axis = 0 : i32} : tensor<128xi32> -> tensor<1x128xi32> + %25 = tt.broadcast %23 : tensor<242x1x!tt.ptr> -> tensor<242x128x!tt.ptr> + %26 = tt.broadcast %24 : tensor<1x128xi32> -> tensor<242x128xi32> + %27 = tt.addptr %25, %26 : tensor<242x128x!tt.ptr>, tensor<242x128xi32> + %28 = tt.splat %arg3 : i32 -> tensor<128x1xi32> + %29 = arith.cmpi slt, %10, %28 : tensor<128x1xi32> + %30 = tt.splat %arg5 : i32 -> tensor<1x242xi32> + %31 = arith.cmpi slt, %15, %30 : tensor<1x242xi32> + %32 = tt.broadcast %29 : tensor<128x1xi1> -> tensor<128x242xi1> + %33 = tt.broadcast %31 : tensor<1x242xi1> -> tensor<128x242xi1> + %34 = arith.andi %32, %33 : tensor<128x242xi1> + %35 = tt.load %18, %34, %cst_0 : tensor<128x242x!tt.ptr> + %36 = tt.splat %arg5 : i32 -> tensor<242x1xi32> + %37 = arith.cmpi slt, %19, %36 : tensor<242x1xi32> + %38 = tt.splat %arg4 : i32 -> tensor<1x128xi32> + %39 = arith.cmpi slt, %24, %38 : tensor<1x128xi32> + %40 = tt.broadcast %37 : tensor<242x1xi1> -> tensor<242x128xi1> + %41 = tt.broadcast %39 : tensor<1x128xi1> -> tensor<242x128xi1> + %42 = arith.andi %40, %41 : tensor<242x128xi1> + %43 = tt.load %27, %42, %cst : tensor<242x128x!tt.ptr> + %44 = tt.dot %35, %43, %cst_1 : tensor<128x242xi8> * tensor<242x128xi8> -> tensor<128x128xi32> + annotation.mark %44 {enable_i4} : tensor<128x128xi32> + tt.print " acc: " {hex = false, isSigned = array} : %44 : tensor<128x128xi32> + %45 = tt.splat %arg8 : i32 -> tensor<128x1xi32> + %46 = arith.muli %10, %45 : tensor<128x1xi32> + %47 = tt.splat %arg2 : !tt.ptr -> tensor<128x1x!tt.ptr> + %48 = tt.addptr %47, %46 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %49 = tt.broadcast %48 : tensor<128x1x!tt.ptr> -> tensor<128x128x!tt.ptr> + %50 = tt.broadcast %24 : tensor<1x128xi32> -> tensor<128x128xi32> + %51 = tt.addptr %49, %50 : tensor<128x128x!tt.ptr>, tensor<128x128xi32> + %52 = tt.broadcast %29 : tensor<128x1xi1> -> tensor<128x128xi1> + %53 = tt.broadcast %39 : tensor<1x128xi1> -> tensor<128x128xi1> + %54 = arith.andi %52, %53 : tensor<128x128xi1> + tt.store %51, %44, %54 : tensor<128x128x!tt.ptr> + tt.return + } +} + + +// CHECK-LABEL: func.func @matmul_kernel +// CHECK: linalg.fill +// CHECK: memref.alloc +// CHECK: memref.copy +// CHECK: linalg.matmul +// CHECK: annotation.mark %{{[0-9]+}} {enable_i4} : tensor<128x128xi32> +// CHECK: call @triton_print{{[_0-9]*}} +// CHECK: bufferization.materialize_in_destination +// CHECK: return diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/fold_one_hot_gather_after_max_with_index.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/fold_one_hot_gather_after_max_with_index.mlir new file mode 100644 index 0000000000..88c3b0669c --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/fold_one_hot_gather_after_max_with_index.mlir @@ -0,0 +1,36 @@ +// RUN: triton-opt --triton-to-linalg="named-ops=True" --split-input-file %s | FileCheck %s + +module { + func.func @fold_one_hot_gather_after_max_with_index(%logits: tensor<2x4xf32>, %indices: tensor<2x4xi32>, %mask: tensor<2x4xi1>) -> tensor<2x1xf32> { + %cst = arith.constant dense<0.000000e+00> : tensor<2x4xf32> + %masked = arith.select %mask, %logits, %cst : tensor<2x4xi1>, tensor<2x4xf32> + %max:2 = "tt.reduce"(%masked, %indices) <{axis = 1 : i32}> ({ + ^bb0(%in: f32, %in_index: i32, %init: f32, %init_index: i32): + %is_greater = arith.cmpf ogt, %in, %init : f32 + %new_value = arith.select %is_greater, %in, %init : f32 + %new_index = arith.select %is_greater, %in_index, %init_index : i32 + tt.reduce.return %new_value, %new_index : f32, i32 + }) : (tensor<2x4xf32>, tensor<2x4xi32>) -> (tensor<2xf32>, tensor<2xi32>) + %expanded_index = tt.expand_dims %max#1 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %broadcasted_index = tt.broadcast %expanded_index : tensor<2x1xi32> -> tensor<2x4xi32> + %one_hot = arith.cmpi eq, %broadcasted_index, %indices : tensor<2x4xi32> + %one_hot_f32 = arith.uitofp %one_hot : tensor<2x4xi1> to tensor<2x4xf32> + %selected = arith.mulf %logits, %one_hot_f32 : tensor<2x4xf32> + %sum = "tt.reduce"(%selected) <{axis = 1 : i32}> ({ + ^bb0(%in: f32, %init: f32): + %add = arith.addf %in, %init : f32 + tt.reduce.return %add : f32 + }) : (tensor<2x4xf32>) -> tensor<2xf32> + %result = tensor.expand_shape %sum [[0, 1]] output_shape [2, 1] : tensor<2xf32> into tensor<2x1xf32> + return %result : tensor<2x1xf32> + } +} + +// CHECK-LABEL: func.func @fold_one_hot_gather_after_max_with_index +// CHECK: %[[MAX:.*]]:2 = linalg.reduce +// CHECK-SAME: reduce_mode = "max_with_index" +// CHECK-NOT: arith.uitofp +// CHECK-NOT: arith.mulf +// CHECK-NOT: arith.cmpi eq +// CHECK-NOT: linalg.reduce ins(%{{.*}} : tensor<2x4xf32>) outs(%{{.*}} : tensor<2xf32>) dimensions = [1] +// CHECK: tensor.expand_shape %[[MAX]]#0 {{\[\[}}0, 1{{\]\]}} output_shape [2, 1] diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/fp_to_fp_scalar_rtne.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/fp_to_fp_scalar_rtne.mlir new file mode 100644 index 0000000000..fae3e530d9 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/fp_to_fp_scalar_rtne.mlir @@ -0,0 +1,84 @@ +// RUN: triton-opt --triton-to-hfusion --triton-to-linalg %s | FileCheck %s + +// CHECK-LABEL: func.func @scalar_fp32_to_fp8_rtne +// CHECK-NOT: tt.fp_to_fp +// CHECK: %[[CAST:.*]] = arith.truncf %{{.*}} {round_mode = #hfusion.round_mode} : f32 to f8E4M3FN +// CHECK-NOT: tt.fp_to_fp +// CHECK: return %[[CAST]] : f8E4M3FN +// CHECK-LABEL: func.func @scalar_fp8_to_fp32_rtne +// CHECK-NOT: tt.fp_to_fp +// CHECK: %[[FP8_TO_F32:.*]] = arith.extf %{{.*}} {round_mode = #hfusion.round_mode} : f8E4M3FN to f32 +// CHECK-NOT: tt.fp_to_fp +// CHECK: return %[[FP8_TO_F32]] : f32 +// CHECK-LABEL: func.func @scalar_f8e4m3fn_to_f8e5m2_rtne +// CHECK-NOT: tt.fp_to_fp +// CHECK: %[[E4_TO_F32:.*]] = arith.extf %{{.*}} {round_mode = #hfusion.round_mode} : f8E4M3FN to f32 +// CHECK-NOT: tt.fp_to_fp +// CHECK: %[[F32_TO_E5:.*]] = arith.truncf %[[E4_TO_F32]] {round_mode = #hfusion.round_mode} : f32 to f8E5M2 +// CHECK-NOT: tt.fp_to_fp +// CHECK: return %[[F32_TO_E5]] : f8E5M2 +// CHECK-LABEL: func.func @scalar_f8e5m2_to_f8e4m3fn_default +// CHECK-NOT: tt.fp_to_fp +// CHECK: %[[E5_TO_F32:.*]] = arith.extf %{{.*}} {round_mode = #hfusion.round_mode} : f8E5M2 to f32 +// CHECK-NOT: tt.fp_to_fp +// CHECK: %[[F32_TO_E4:.*]] = arith.truncf %[[E5_TO_F32]] {round_mode = #hfusion.round_mode} : f32 to f8E4M3FN +// CHECK-NOT: tt.fp_to_fp +// CHECK: return %[[F32_TO_E4]] : f8E4M3FN +// CHECK-LABEL: func.func @scalar_fp16_to_bf16_rtne +// CHECK-NOT: tt.fp_to_fp +// CHECK: %[[F16_TO_F32:.*]] = arith.extf %{{.*}} {round_mode = #hfusion.round_mode} : f16 to f32 +// CHECK-NOT: tt.fp_to_fp +// CHECK: %[[F32_TO_BF16:.*]] = arith.truncf %[[F16_TO_F32]] {round_mode = #hfusion.round_mode} : f32 to bf16 +// CHECK-NOT: tt.fp_to_fp +// CHECK: return %[[F32_TO_BF16]] : bf16 +// CHECK-LABEL: func.func @tensor_f8e4m3fn_to_f8e5m2_rtne +// CHECK-NOT: tt.fp_to_fp +// CHECK: arith.extf %{{.*}} {round_mode = #hfusion.round_mode} : tensor<8xf8E4M3FN> to tensor<8xf32> +// CHECK-NOT: tt.fp_to_fp +// CHECK: arith.truncf %{{.*}} {round_mode = #hfusion.round_mode} : tensor<8xf32> to tensor<8xf8E5M2> +// CHECK-NOT: tt.fp_to_fp +// CHECK: return +// CHECK-LABEL: func.func @scalar_f8e4m3fn_identity +// CHECK-NOT: tt.fp_to_fp +// CHECK: return %{{.*}} : f8E4M3FN +module { + tt.func @scalar_fp32_to_fp8_rtne(%arg0: f32) -> f8E4M3FN { + %0 = tt.fp_to_fp %arg0, rounding = rtne : f32 -> f8E4M3FN + tt.return %0 : f8E4M3FN + } + + tt.func @scalar_fp8_to_fp32_rtne(%arg0: f8E4M3FN) -> f32 { + %0 = tt.fp_to_fp %arg0, rounding = rtne : f8E4M3FN -> f32 + tt.return %0 : f32 + } + + tt.func @scalar_f8e4m3fn_to_f8e5m2_rtne(%arg0: f8E4M3FN) -> f8E5M2 { + %0 = tt.fp_to_fp %arg0, rounding = rtne : f8E4M3FN -> f8E5M2 + tt.return %0 : f8E5M2 + } + + tt.func @scalar_f8e5m2_to_f8e4m3fn_default(%arg0: f8E5M2) -> f8E4M3FN { + %0 = tt.fp_to_fp %arg0 : f8E5M2 -> f8E4M3FN + tt.return %0 : f8E4M3FN + } + + tt.func @scalar_fp16_to_bf16_rtne(%arg0: f16) -> bf16 { + %0 = tt.fp_to_fp %arg0, rounding = rtne : f16 -> bf16 + tt.return %0 : bf16 + } + + tt.func @tensor_f8e4m3fn_to_f8e5m2_rtne(%arg0: f8E4M3FN, %arg1: !tt.ptr) { + %input = tt.splat %arg0 : f8E4M3FN -> tensor<8xf8E4M3FN> + %0 = tt.fp_to_fp %input, rounding = rtne : tensor<8xf8E4M3FN> -> tensor<8xf8E5M2> + %offsets = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %output_ptrs = tt.splat %arg1 : !tt.ptr -> tensor<8x!tt.ptr> + %output_addrs = tt.addptr %output_ptrs, %offsets : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %output_addrs, %0 : tensor<8x!tt.ptr> + tt.return + } + + tt.func @scalar_f8e4m3fn_identity(%arg0: f8E4M3FN) -> f8E4M3FN { + %0 = tt.fp_to_fp %arg0 : f8E4M3FN -> f8E4M3FN + tt.return %0 : f8E4M3FN + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/full_row_copy.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/full_row_copy.mlir new file mode 100644 index 0000000000..24154be3f2 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/full_row_copy.mlir @@ -0,0 +1,288 @@ +// RUN: triton-opt "--triton-to-linalg=global-kernel=false" --split-input-file %s | FileCheck %s + +// Full rows recover a static copy; the tail keeps the exact original extent. +// The condition and allocation must stay outside both row loops. + +// CHECK: module attributes {{.*}}tt.full_row_copy +// CHECK-LABEL: func.func @full_row_copy +// CHECK: %[[BUFFER:.*]] = memref.alloc() : memref<32x64xf32> +// CHECK: %[[FULL:.*]] = arith.cmpi eq, +// CHECK: scf.if %[[FULL]] { +// CHECK-NEXT: scf.for +// CHECK-NOT: memref.alloc +// CHECK-NOT: scf.if +// CHECK: memref.copy {{.*}} : memref<1x64xf32, {{.*}}> to memref<1x64xf32, {{.*}}> +// CHECK: } else { +// CHECK-NEXT: scf.for +// CHECK-NOT: memref.alloc +// CHECK-NOT: scf.if +// CHECK: memref.copy {{.*}} : memref<1x?xf32, {{.*}}> to memref<1x?xf32, {{.*}}> +// CHECK: return %[[BUFFER]] + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @full_row_copy(%input: memref, %rows: index, %cols: index) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + %buffer = memref.alloc() : memref<32x64xf32> + scf.for %i = %c0 to %rows step %c1 { + %base = arith.muli %i, %c64 : index + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + + %read = memref.subview %src[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + %write = memref.subview %row[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + + + } {ExtractedLoadOrStore, hivm.parallel_loop} + return %buffer : memref<32x64xf32> + } +} + +// ----- + +// The source row may already have been hoisted. Cloning must reuse that value. +// CHECK: module attributes {{.*}}tt.full_row_copy +// CHECK-LABEL: func.func @invariant_source +// CHECK: %[[SOURCE:.*]] = memref.reinterpret_cast +// CHECK: scf.if +// CHECK-NEXT: scf.for +// CHECK: memref.copy %[[SOURCE]], {{.*}} : memref<1x64xf32, {{.*}}> to memref<1x64xf32, {{.*}}> +// CHECK: } else { +// CHECK: memref.copy {{.*}} : memref<1x?xf32, {{.*}}> to memref<1x?xf32, {{.*}}> +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @invariant_source(%input: memref, %rows: index, %cols: index, %base: index) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %buffer = memref.alloc() : memref<32x64xf32> + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> + scf.for %i = %c0 to %rows step %c1 { + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + %read = memref.subview %src[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + %write = memref.subview %row[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + } {ExtractedLoadOrStore, hivm.parallel_loop} + return %buffer : memref<32x64xf32> + } +} + +// ----- + +// CHECK: module attributes +// CHECK-NOT: tt.full_row_copy +// CHECK-LABEL: func.func @row_dependent_extent +// CHECK-NOT: scf.if +// CHECK: scf.for +// CHECK-NOT: scf.if +// CHECK: memref.copy +// CHECK-NOT: scf.if +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @row_dependent_extent(%input: memref, %rows: index, %cols: index) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + %buffer = memref.alloc() : memref<32x64xf32> + scf.for %i = %c0 to %rows step %c1 { + %base = arith.muli %i, %c64 : index + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + %local = arith.minsi %i, %cols : index + %read = memref.subview %src[0, 0] [1, %local] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + %write = memref.subview %row[0, 0] [1, %local] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + + + } {ExtractedLoadOrStore, hivm.parallel_loop} + return %buffer : memref<32x64xf32> + } +} + +// ----- + +// CHECK-LABEL: func.func @nonzero_column_offset +// CHECK-NOT: scf.if +// CHECK: scf.for +// CHECK-NOT: scf.if +// CHECK: memref.copy +// CHECK-NOT: scf.if +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @nonzero_column_offset(%input: memref, %rows: index, %cols: index) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + %buffer = memref.alloc() : memref<32x64xf32> + scf.for %i = %c0 to %rows step %c1 { + %base = arith.muli %i, %c64 : index + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + + %read = memref.subview %src[0, 1] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + %write = memref.subview %row[0, 1] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + + + } {ExtractedLoadOrStore, hivm.parallel_loop} + return %buffer : memref<32x64xf32> + } +} + +// ----- + +// CHECK-LABEL: func.func @strided_columns +// CHECK-NOT: scf.if +// CHECK: scf.for +// CHECK-NOT: scf.if +// CHECK: memref.copy +// CHECK-NOT: scf.if +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @strided_columns(%input: memref, %rows: index, %cols: index) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + %buffer = memref.alloc() : memref<32x64xf32> + scf.for %i = %c0 to %rows step %c1 { + %base = arith.muli %i, %c64 : index + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [128, 2] : memref to memref<1x64xf32, strided<[128, 2], offset: ?>> + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + + %read = memref.subview %src[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[128, 2], offset: ?>> to memref<1x?xf32, strided<[128, 2], offset: ?>> + %write = memref.subview %row[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[128, 2], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + + + } {ExtractedLoadOrStore, hivm.parallel_loop} + return %buffer : memref<32x64xf32> + } +} + +// ----- + +// CHECK-LABEL: func.func @unmarked_loop +// CHECK-NOT: scf.if +// CHECK: scf.for +// CHECK-NOT: scf.if +// CHECK: memref.copy +// CHECK-NOT: scf.if +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @unmarked_loop(%input: memref, %rows: index, %cols: index) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + %buffer = memref.alloc() : memref<32x64xf32> + scf.for %i = %c0 to %rows step %c1 { + %base = arith.muli %i, %c64 : index + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + + %read = memref.subview %src[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + %write = memref.subview %row[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + + + } + return %buffer : memref<32x64xf32> + } +} + +// ----- + +// CHECK-LABEL: func.func @additional_read +// CHECK-NOT: scf.if +// CHECK: scf.for +// CHECK-NOT: scf.if +// CHECK: memref.copy +// CHECK-NOT: scf.if +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @additional_read(%input: memref, %rows: index, %cols: index) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + %buffer = memref.alloc() : memref<32x64xf32> + scf.for %i = %c0 to %rows step %c1 { + %base = arith.muli %i, %c64 : index + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + + %read = memref.subview %src[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + %write = memref.subview %row[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + + %extra = memref.load %input[%i] : memref + memref.store %extra, %buffer[%i, %c0] : memref<32x64xf32> + } {ExtractedLoadOrStore, hivm.parallel_loop} + return %buffer : memref<32x64xf32> + } +} + +// ----- + +// CHECK-LABEL: func.func @multiple_copies +// CHECK-NOT: scf.if +// CHECK: scf.for +// CHECK-NOT: scf.if +// CHECK: memref.copy +// CHECK-NOT: scf.if +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @multiple_copies(%input: memref, %rows: index, %cols: index) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + %buffer = memref.alloc() : memref<32x64xf32> + scf.for %i = %c0 to %rows step %c1 { + %base = arith.muli %i, %c64 : index + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + + %read = memref.subview %src[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + %write = memref.subview %row[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + + } {ExtractedLoadOrStore, hivm.parallel_loop} + return %buffer : memref<32x64xf32> + } +} + +// ----- + +// CHECK-LABEL: func.func @external_destination +// CHECK-NOT: scf.if +// CHECK: scf.for +// CHECK-NOT: scf.if +// CHECK: memref.copy +// CHECK-NOT: scf.if +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + func.func @external_destination(%input: memref, %rows: index, %cols: index, %buffer: memref<32x64xf32>) -> memref<32x64xf32> { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c64 = arith.constant 64 : index + + scf.for %i = %c0 to %rows step %c1 { + %base = arith.muli %i, %c64 : index + %src = memref.reinterpret_cast %input to offset: [%base], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> + %row = memref.subview %buffer[%i, 0] [1, 64] [1, 1] : memref<32x64xf32> to memref<1x64xf32, strided<[64, 1], offset: ?>> + + %read = memref.subview %src[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + %write = memref.subview %row[0, 0] [1, %cols] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + memref.copy %read, %write : memref<1x?xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> + + + } {ExtractedLoadOrStore, hivm.parallel_loop} + return %buffer : memref<32x64xf32> + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/histogram.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/histogram.mlir new file mode 100644 index 0000000000..a81077e62c --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/histogram.mlir @@ -0,0 +1,43 @@ +// RUN: triton-opt "--triton-to-linalg=compile-on-910-95=true" --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @test_histogram_i32 +// CHECK: %[[INPUT:.*]] = bufferization.to_tensor {{.*}} : memref<1024xi32> to tensor<1024xi32> +// CHECK: %[[INIT:.*]] = linalg.fill ins(%{{.*}} : i32) outs(%{{.*}} : tensor<256xi32>) -> tensor<256xi32> +// CHECK: %[[HIST:.*]] = hivm.hir.custom {gm_addr_args_indices = array, hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "__builtin_histogram"} "__builtin_histogram" ins(%[[INPUT]], %{{.*}} : tensor<1024xi32>, i64) outs(%[[INIT]] : tensor<256xi32>) -> tensor<256xi32> +// CHECK: bufferization.materialize_in_destination %[[HIST]] in writable {{.*}} : (tensor<256xi32>, memref<256xi32, strided<[1]>>) -> () +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @test_histogram_i32(%arg0: !tt.ptr, %arg1: !tt.ptr) attributes {noinline = false} { + %in_offsets = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %out_offsets = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %in_ptrs = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %in_addrs = tt.addptr %in_ptrs, %in_offsets : tensor<1024x!tt.ptr>, tensor<1024xi32> + %input = tt.load %in_addrs : tensor<1024x!tt.ptr> + %hist = tt.histogram %input : tensor<1024xi32> -> tensor<256xi32> + %out_ptrs = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %out_addrs = tt.addptr %out_ptrs, %out_offsets : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %out_addrs, %hist : tensor<256x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @test_histogram_i16 +// CHECK: %[[INPUT:.*]] = bufferization.to_tensor {{.*}} : memref<16xi16> to tensor<16xi16> +// CHECK: %[[INIT:.*]] = linalg.fill ins(%{{.*}} : i16) outs(%{{.*}} : tensor<8xi16>) -> tensor<8xi16> +// CHECK: %[[HIST:.*]] = hivm.hir.custom {gm_addr_args_indices = array, hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "__builtin_histogram"} "__builtin_histogram" ins(%[[INPUT]], %{{.*}} : tensor<16xi16>, i64) outs(%[[INIT]] : tensor<8xi16>) -> tensor<8xi16> +// CHECK: bufferization.materialize_in_destination %[[HIST]] in writable {{.*}} : (tensor<8xi16>, memref<8xi16, strided<[1]>>) -> () +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @test_histogram_i16(%arg0: !tt.ptr, %arg1: !tt.ptr) attributes {noinline = false} { + %in_offsets = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %out_offsets = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %in_ptrs = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %in_addrs = tt.addptr %in_ptrs, %in_offsets : tensor<16x!tt.ptr>, tensor<16xi32> + %input = tt.load %in_addrs : tensor<16x!tt.ptr> + %hist = tt.histogram %input : tensor<16xi16> -> tensor<8xi16> + %out_ptrs = tt.splat %arg1 : !tt.ptr -> tensor<8x!tt.ptr> + %out_addrs = tt.addptr %out_ptrs, %out_offsets : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %out_addrs, %hist : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/hoist_broadcast.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/hoist_broadcast.mlir new file mode 100644 index 0000000000..efb3abf186 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/hoist_broadcast.mlir @@ -0,0 +1,76 @@ +// RUN: triton-opt --triton-to-linalg="named-ops=True" --split-input-file %s | FileCheck %s + +module { +tt.func public @copy_all_layer_kv_cache2(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c0_i32 = arith.constant 0 : i32 + %cst = arith.constant dense<0> : tensor<1x128xi8> + %c1_i64 = arith.constant 1 : i64 + %c0_i64 = arith.constant 0 : i64 + %c127_i64 = arith.constant 127 : i64 + %true = arith.constant true + %c128_i64 = arith.constant 128 : i64 + + %0 = tt.get_program_id x : i32 + %1 = tt.addptr %arg1, %0 : !tt.ptr, i32 + %2 = tt.load %1 : !tt.ptr + %3 = tt.addptr %arg0, %0 : !tt.ptr, i32 + %4 = tt.load %3 : !tt.ptr + %5 = tt.int_to_ptr %4 : i64 -> !tt.ptr + + %6 = tt.addptr %arg2, %c0_i32 : !tt.ptr, i32 + %7 = tt.splat %6 : !tt.ptr -> tensor<1x!tt.ptr> + %8 = tt.load %7 : tensor<1x!tt.ptr> + + %9 = tt.addptr %arg3, %c0_i32 : !tt.ptr, i32 + %10 = tt.splat %9 : !tt.ptr -> tensor<1x!tt.ptr> + %11 = tt.load %10 : tensor<1x!tt.ptr> + + tt.assert %true, "int32 overflow detected for operation sub" : i1 + + %12 = arith.addi %2, %c127_i64 : i64 + %13 = arith.divsi %12, %c128_i64 : i64 + + %14 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %15 = arith.extsi %14 : tensor<128xi32> to tensor<128xi64> + %16 = tt.splat %2 : i64 -> tensor<128xi64> + + %17 = tt.expand_dims %11 {axis = 1 : i32} : tensor<1xi32> -> tensor<1x1xi32> + %18 = arith.extsi %17 : tensor<1x1xi32> to tensor<1x1xi64> + %19 = tt.splat %2 : i64 -> tensor<1x1xi64> + %20 = arith.muli %18, %19 : tensor<1x1xi64> + %21 = tt.broadcast %20 : tensor<1x1xi64> -> tensor<1x128xi64> + %22 = tt.splat %5 : !tt.ptr -> tensor<1x128x!tt.ptr> + + %23 = tt.expand_dims %8 {axis = 1 : i32} : tensor<1xi32> -> tensor<1x1xi32> + %24 = arith.extsi %23 : tensor<1x1xi32> to tensor<1x1xi64> + %25 = arith.muli %24, %19 : tensor<1x1xi64> + %26 = tt.broadcast %25 : tensor<1x1xi64> -> tensor<1x128xi64> + + scf.for %arg4 = %c0_i64 to %13 step %c1_i64 : i64 { + %27 = arith.muli %arg4, %c128_i64 : i64 + %28 = tt.splat %27 : i64 -> tensor<128xi64> + %29 = arith.addi %15, %28 : tensor<128xi64> + %30 = arith.cmpi slt, %29, %16 : tensor<128xi64> + %31 = tt.expand_dims %30 {axis = 0 : i32} : tensor<128xi1> -> tensor<1x128xi1> + %32 = tt.expand_dims %29 {axis = 0 : i32} : tensor<128xi64> -> tensor<1x128xi64> + %33 = arith.addi %21, %32 : tensor<1x128xi64> + %34 = tt.addptr %22, %33 : tensor<1x128x!tt.ptr>, tensor<1x128xi64> + %35 = tt.load %34, %31, %cst : tensor<1x128x!tt.ptr> + %36 = arith.addi %26, %32 : tensor<1x128xi64> + %37 = tt.addptr %22, %36 : tensor<1x128x!tt.ptr>, tensor<1x128xi64> + tt.store %37, %35, %31 : tensor<1x128x!tt.ptr> + } + + tt.return +} +} + +// CHECK-LABEL: func.func @copy_all_layer_kv_cache2 +// CHECK: memref.reinterpret_cast +// CHECK: memref.load +// CHECK: memref.alloc +// CHECK: memref.copy +// CHECK: scf.for +// CHECK: linalg.fill +// CHECK: memref.subview +// CHECK: bufferization.materialize_in_destination diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/hoist_broadcast2.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/hoist_broadcast2.mlir new file mode 100644 index 0000000000..be3654d657 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/hoist_broadcast2.mlir @@ -0,0 +1,145 @@ +// RUN: triton-opt --pass-pipeline="builtin.module(auto-blockify{auto-blockify-size=1},triton-to-structured{enable-mask-fallback-conversion=false optimize-dynamic-offset=false},discrete-mask-access-conversion{compile-on-910-95=true compile-mode=simd},triton-to-annotation,triton-to-unstructure{compile-on-910-95=true force-scalarize-mode=false compile-mode=simd},triton-to-hivm,triton-to-hfusion,triton-to-llvm,bubble-up-operation{enable-aggressive-mode=true},triton-to-structured{enable-mask-fallback-conversion=false optimize-dynamic-offset=false},triton-to-linalg{compile-on-910-95=true enable-nd2nz-on-vector=false enable-select-analysis=true global-kernel=false named-ops=true compile-mode=simd})" --split-input-file %s | FileCheck %s + +module { + tt.func public @_attn_bwd(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %arg2: !tt.ptr {tt.divisibility = 16 : i32}, + %arg3: !tt.ptr {tt.divisibility = 16 : i32}, + %arg4: !tt.ptr {tt.divisibility = 16 : i32}, + %arg5: !tt.ptr {tt.divisibility = 16 : i32}, + %arg6: !tt.ptr {tt.divisibility = 16 : i32}, + %arg7: !tt.ptr {tt.divisibility = 16 : i32}, + %arg8: !tt.ptr {tt.divisibility = 16 : i32}, + %arg9: f32) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<128x128xf32> + %cst_0 = arith.constant dense<0.000000e+00> : tensor<128x64xf32> + %c1_i32 = arith.constant 1 : i32 + %c0_i32 = arith.constant 0 : i32 + %c28_i32 = arith.constant 28 : i32 + %c8192_i32 = arith.constant 8192 : i32 + %cst_1 = arith.constant dense : tensor<128x64xi1> + %cst_2 = arith.constant dense<64> : tensor<128x1xi32> + %c128_i32 = arith.constant 128 : i32 + %c524288_i32 = arith.constant 524288 : i32 + %c65536_i32 = arith.constant 65536 : i32 + %c1024_i32 = arith.constant 1024 : i32 + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %1 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %2 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %3 = tt.expand_dims %1 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %4 = tt.broadcast %3 : tensor<1x64xi32> -> tensor<128x64xi32> + %5 = tt.splat %arg9 : f32 -> tensor<128x128xf32> + scf.for %arg10 = %0 to %c8192_i32 step %c28_i32 : i32 { + %6 = arith.divsi %arg10, %c8_i32 : i32 + %7 = arith.muli %6, %c8_i32 : i32 + %8 = arith.subi %arg10, %7 : i32 + %9 = arith.muli %6, %c1024_i32 : i32 + %10 = arith.extsi %9 : i32 to i64 + %11 = arith.remsi %6, %c8_i32 : i32 + %12 = arith.muli %11, %c65536_i32 : i32 + %13 = arith.divsi %6, %c8_i32 : i32 + %14 = arith.muli %13, %c524288_i32 : i32 + %15 = arith.addi %12, %14 : i32 + %16 = arith.extsi %15 : i32 to i64 + %17 = tt.addptr %arg0, %16 : !tt.ptr, i64 + %18 = tt.addptr %arg1, %16 : !tt.ptr, i64 + %19 = tt.addptr %arg2, %16 : !tt.ptr, i64 + %20 = tt.addptr %arg3, %16 : !tt.ptr, i64 + %21 = tt.addptr %arg4, %16 : !tt.ptr, i64 + %22 = tt.addptr %arg5, %16 : !tt.ptr, i64 + %23 = tt.addptr %arg6, %16 : !tt.ptr, i64 + %24 = tt.addptr %arg7, %10 : !tt.ptr, i64 + %25 = tt.addptr %arg8, %10 : !tt.ptr, i64 + %26 = arith.muli %8, %c128_i32 : i32 + %27 = tt.splat %26 : i32 -> tensor<128xi32> + %28 = arith.addi %27, %2 : tensor<128xi32> + %29 = tt.expand_dims %28 {axis = 1 : i32} : tensor<128xi32> -> tensor<128x1xi32> + %30 = arith.muli %29, %cst_2 : tensor<128x1xi32> + %31 = tt.splat %18 : !tt.ptr -> tensor<128x1x!tt.ptr> + %32 = tt.addptr %31, %30 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %33 = tt.broadcast %32 : tensor<128x1x!tt.ptr> -> tensor<128x64x!tt.ptr> + %34 = tt.addptr %33, %4 : tensor<128x64x!tt.ptr>, tensor<128x64xi32> + %35 = tt.load %34 : tensor<128x64x!tt.ptr> + %36 = tt.splat %19 : !tt.ptr -> tensor<128x1x!tt.ptr> + %37 = tt.addptr %36, %30 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %38 = tt.broadcast %37 : tensor<128x1x!tt.ptr> -> tensor<128x64x!tt.ptr> + %39 = tt.addptr %38, %4 : tensor<128x64x!tt.ptr>, tensor<128x64xi32> + %40 = tt.load %39 : tensor<128x64x!tt.ptr> + %41 = tt.splat %17 : !tt.ptr -> tensor<128x1x!tt.ptr> + %42 = tt.splat %20 : !tt.ptr -> tensor<128x1x!tt.ptr> + %43 = tt.splat %24 : !tt.ptr -> tensor<128x!tt.ptr> + %44 = tt.splat %25 : !tt.ptr -> tensor<128x!tt.ptr> + %45 = tt.trans %35 {order = array} : tensor<128x64xf16> -> tensor<64x128xf16> + %46 = tt.trans %40 {order = array} : tensor<128x64xf16> -> tensor<64x128xf16> + %47 = tt.splat %21 : !tt.ptr -> tensor<128x1x!tt.ptr> + %48:2 = scf.for %arg11 = %c0_i32 to %c8_i32 step %c1_i32 iter_args(%arg12 = %cst_0, %arg13 = %cst_0) -> (tensor<128x64xf32>, tensor<128x64xf32>) : i32 { + %59 = arith.muli %arg11, %c128_i32 : i32 + %60 = tt.splat %59 : i32 -> tensor<128xi32> + %61 = arith.addi %60, %2 : tensor<128xi32> + %62 = tt.expand_dims %61 {axis = 1 : i32} : tensor<128xi32> -> tensor<128x1xi32> + %63 = arith.muli %62, %cst_2 : tensor<128x1xi32> + %64 = tt.addptr %41, %63 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %65 = tt.broadcast %64 : tensor<128x1x!tt.ptr> -> tensor<128x64x!tt.ptr> + %66 = tt.addptr %65, %4 : tensor<128x64x!tt.ptr>, tensor<128x64xi32> + %67 = tt.load %66 : tensor<128x64x!tt.ptr> + %68 = tt.addptr %42, %63 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %69 = tt.broadcast %68 : tensor<128x1x!tt.ptr> -> tensor<128x64x!tt.ptr> + %70 = tt.addptr %69, %4 : tensor<128x64x!tt.ptr>, tensor<128x64xi32> + %71 = tt.load %70 : tensor<128x64x!tt.ptr> + %72 = tt.addptr %43, %61 : tensor<128x!tt.ptr>, tensor<128xi32> + %73 = tt.load %72 : tensor<128x!tt.ptr> + %74 = tt.addptr %44, %61 : tensor<128x!tt.ptr>, tensor<128xi32> + %75 = tt.load %74 : tensor<128x!tt.ptr> + %76 = tt.dot %67, %45, %cst : tensor<128x64xf16> * tensor<64x128xf16> -> tensor<128x128xf32> + %77 = arith.mulf %76, %5 : tensor<128x128xf32> + %78 = tt.expand_dims %73 {axis = 1 : i32} : tensor<128xf32> -> tensor<128x1xf32> + %79 = tt.broadcast %78 : tensor<128x1xf32> -> tensor<128x128xf32> + %80 = arith.subf %77, %79 : tensor<128x128xf32> + %81 = math.exp %80 : tensor<128x128xf32> + %82 = arith.truncf %81 : tensor<128x128xf32> to tensor<128x128xf16> + %83 = tt.trans %82 {order = array} : tensor<128x128xf16> -> tensor<128x128xf16> + %84 = tt.dot %83, %71, %arg13 : tensor<128x128xf16> * tensor<128x64xf16> -> tensor<128x64xf32> + %85 = tt.dot %71, %46, %cst : tensor<128x64xf16> * tensor<64x128xf16> -> tensor<128x128xf32> + %86 = tt.expand_dims %75 {axis = 1 : i32} : tensor<128xf32> -> tensor<128x1xf32> + %87 = tt.broadcast %86 : tensor<128x1xf32> -> tensor<128x128xf32> + %88 = arith.subf %85, %87 : tensor<128x128xf32> + %89 = arith.extf %82 : tensor<128x128xf16> to tensor<128x128xf32> + %90 = arith.mulf %89, %88 : tensor<128x128xf32> + %91 = arith.mulf %90, %5 : tensor<128x128xf32> + %92 = arith.truncf %91 : tensor<128x128xf32> to tensor<128x128xf16> + %93 = tt.trans %92 {order = array} : tensor<128x128xf16> -> tensor<128x128xf16> + %94 = tt.dot %93, %67, %arg12 : tensor<128x128xf16> * tensor<128x64xf16> -> tensor<128x64xf32> + %95 = tt.dot %92, %35, %cst_0 : tensor<128x128xf16> * tensor<128x64xf16> -> tensor<128x64xf32> + %96 = tt.addptr %47, %63 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %97 = tt.broadcast %96 : tensor<128x1x!tt.ptr> -> tensor<128x64x!tt.ptr> + %98 = tt.addptr %97, %4 : tensor<128x64x!tt.ptr>, tensor<128x64xi32> + %99 = arith.truncf %95 : tensor<128x64xf32> to tensor<128x64xf16> + %100 = tt.atomic_rmw fadd, acq_rel, gpu, %98, %99, %cst_1 : (tensor<128x64x!tt.ptr>, tensor<128x64xf16>, tensor<128x64xi1>) -> tensor<128x64xf16> + scf.yield %94, %84 : tensor<128x64xf32>, tensor<128x64xf32> + } + %49 = tt.splat %22 : !tt.ptr -> tensor<128x1x!tt.ptr> + %50 = tt.addptr %49, %30 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %51 = tt.broadcast %50 : tensor<128x1x!tt.ptr> -> tensor<128x64x!tt.ptr> + %52 = tt.addptr %51, %4 : tensor<128x64x!tt.ptr>, tensor<128x64xi32> + %53 = arith.truncf %48#0 : tensor<128x64xf32> to tensor<128x64xf16> + tt.store %52, %53 : tensor<128x64x!tt.ptr> + %54 = tt.splat %23 : !tt.ptr -> tensor<128x1x!tt.ptr> + %55 = tt.addptr %54, %30 : tensor<128x1x!tt.ptr>, tensor<128x1xi32> + %56 = tt.broadcast %55 : tensor<128x1x!tt.ptr> -> tensor<128x64x!tt.ptr> + %57 = tt.addptr %56, %4 : tensor<128x64x!tt.ptr>, tensor<128x64xi32> + %58 = arith.truncf %48#1 : tensor<128x64xf32> to tensor<128x64xf16> + tt.store %57, %58 : tensor<128x64x!tt.ptr> + } + tt.return + } +} + +// CHECK-LABEL: func.func @_attn_bwd +// CHECK: linalg.fill +// CHECK: memref.reinterpret_cast +// CHECK: memref.alloc +// CHECK: memref.copy +// CHECK: scf.for +// CHECK: hivm.hir.store +// CHECK: bufferization.materialize_in_destination diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/index_select_simd_bool.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/index_select_simd_bool.mlir new file mode 100644 index 0000000000..7f714e3bcc --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/index_select_simd_bool.mlir @@ -0,0 +1,22 @@ +// RUN: triton-opt --triton-to-linalg="named-ops=True" --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @index_select_simd_bool +// CHECK: memref.reinterpret_cast {{.*}} : memref to memref to tensor<2x8xi8> +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + tt.func public @index_select_simd_bool(%src: !tt.ptr, %dst: !tt.ptr) attributes {noinline = false} { + %c0 = arith.constant 0 : index + %c-1 = arith.constant -1 : index + %c4 = arith.constant 4 : index + %c8 = arith.constant 8 : index + %indices = arith.constant dense<[1, 3]> : tensor<2xi32> + %result = ascend.index_select_simd %src, %indices, 0, [%c4, %c8], [%c-1, %c0], [-1, 8] {was_bool_to_int8 = true} : !tt.ptr, tensor<2xi32> -> tensor<2x8xi8> + %offsets = tt.make_range {start = 0 : i32, end = 16 : i32} : tensor<16xi32> + %dsts = tt.splat %dst : !tt.ptr -> tensor<16x!tt.ptr> + %dst_ptrs = tt.addptr %dsts, %offsets : tensor<16x!tt.ptr>, tensor<16xi32> + %flat = tt.reshape %result : tensor<2x8xi8> -> tensor<16xi8> + tt.store %dst_ptrs, %flat : tensor<16x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/isin_by_search_kernel.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/isin_by_search_kernel.mlir new file mode 100644 index 0000000000..62e6cf19cb --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/isin_by_search_kernel.mlir @@ -0,0 +1,69 @@ +// RUN: triton-opt --discrete-mask-access-conversion --triton-to-unstructure --triton-to-linalg --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @isin_by_search_kernel +// The one-element loop mask controls the load itself, including in SIMD mode. +// CHECK: %[[FOR_VAR:.*]]:4 = scf.for +// CHECK-SAME: iter_args({{.*}}, %[[MASK:.*]] = %{{.*}}) -> (tensor<1xi1>, tensor<1xi32>, tensor<1xi32>, tensor<1xi1>) : i32 +// CHECK: %[[PREDICATE:.*]] = tensor.extract %[[MASK]][{{.*}}] {{.*}} : tensor<1xi1> +// CHECK: %[[VALUE:.*]] = scf.if %[[PREDICATE]] -> (i16) { +// CHECK-NEXT: %[[LOAD:.*]] = memref.load +// CHECK-NEXT: scf.yield %[[LOAD]] : i16 +// CHECK-NEXT: } else { +// CHECK-NEXT: scf.yield %{{.*}} : i16 +module { + tt.func public @isin_by_search_kernel( + %arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %arg2: !tt.ptr {tt.divisibility = 16 : i32}, + %arg3: i32 {tt.divisibility = 16 : i32} + ) attributes {noinline = false} { + %c1_i64 = arith.constant 1 : i64 + %cst = arith.constant dense<0> : tensor<1xi32> + %cst_0 = arith.constant dense<2> : tensor<1xi32> + %cst_1 = arith.constant dense<1> : tensor<1xi32> + %cst_2 = arith.constant dense<0> : tensor<1xi64> + %c0_i32 = arith.constant 0 : i32 + %c20_i32 = arith.constant 20 : i32 + %c1_i32 = arith.constant 1 : i32 + %cst_3 = arith.constant dense<0> : tensor<1xi16> + %cst_4 = arith.constant dense : tensor<1xi1> + %0 = tt.get_program_id x : i32 + %1 = arith.extsi %0 : i32 to i64 + %2 = arith.cmpi slt, %1, %c1_i64 : i64 + %3 = tt.splat %2 : i1 -> tensor<1xi1> + %4 = tt.addptr %arg0, %1 : !tt.ptr, i64 + %5 = tt.splat %4 : !tt.ptr -> tensor<1x!tt.ptr> + %6 = tt.load %5, %3, %cst_2 : tensor<1x!tt.ptr> + %7 = tt.splat %arg3 : i32 -> tensor<1xi32> + %8 = arith.cmpi sgt, %arg3, %c0_i32 : i32 + %9 = tt.splat %8 : i1 -> tensor<1xi1> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<1x!tt.ptr> + %11:4 = scf.for %arg4 = %c0_i32 to %c20_i32 step %c1_i32 iter_args(%arg5 = %cst_4, %arg6 = %cst, %arg7 = %7, %arg8 = %9) -> (tensor<1xi1>, tensor<1xi32>, tensor<1xi32>, tensor<1xi1>) : i32 { + %16 = arith.subi %arg7, %arg6 : tensor<1xi32> + %17 = arith.divsi %16, %cst_0 : tensor<1xi32> + %18 = arith.addi %arg6, %17 : tensor<1xi32> + %19 = arith.select %arg8, %18, %cst : tensor<1xi1>, tensor<1xi32> + %20 = tt.addptr %10, %19 : tensor<1x!tt.ptr>, tensor<1xi32> + %21 = tt.load %20, %arg8, %cst_3 : tensor<1x!tt.ptr> + %22 = arith.extsi %21 : tensor<1xi16> to tensor<1xi64> + %23 = arith.cmpi eq, %22, %6 : tensor<1xi64> + %24 = arith.ori %cst_4, %23 : tensor<1xi1> + %25 = arith.select %arg8, %24, %arg5 : tensor<1xi1>, tensor<1xi1> + %26 = arith.cmpi slt, %22, %6 : tensor<1xi64> + %27 = arith.andi %arg8, %26 : tensor<1xi1> + %28 = arith.addi %19, %cst_1 : tensor<1xi32> + %29 = arith.select %27, %28, %arg6 : tensor<1xi1>, tensor<1xi32> + %30 = arith.cmpi sgt, %22, %6 : tensor<1xi64> + %31 = arith.andi %arg8, %30 : tensor<1xi1> + %32 = arith.select %31, %19, %arg7 : tensor<1xi1>, tensor<1xi32> + %33 = arith.cmpi slt, %29, %32 : tensor<1xi32> + scf.yield %25, %29, %32, %33 : tensor<1xi1>, tensor<1xi32>, tensor<1xi32>, tensor<1xi1> + } + %12 = tt.addptr %arg2, %1 : !tt.ptr, i64 + %13 = tt.bitcast %12 : !tt.ptr -> !tt.ptr + %14 = tt.splat %13 : !tt.ptr -> tensor<1x!tt.ptr> + %15 = arith.extui %11#0 : tensor<1xi1> to tensor<1xi8> + tt.store %14, %15, %3 : tensor<1x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/mask_nop.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/mask_nop.mlir new file mode 100644 index 0000000000..869b29a93c --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/mask_nop.mlir @@ -0,0 +1,32 @@ +// RUN: triton-opt --triton-to-linalg --mlir-print-debuginfo --split-input-file %s \ +// RUN: | FileCheck %s --check-prefix=NODEBUG +// RUN: env LLVM_EXTRACT_DI_LOCAL_VARIABLES=1 triton-opt --triton-to-linalg \ +// RUN: --mlir-print-debuginfo --split-input-file %s | FileCheck %s --check-prefix=DEBUG + +#loc = loc("mask.py":1:0) +#loc_off = loc("mask.py":10:11) +#loc_mcmp = loc("mask.py":11:16) +#loc_msk = loc("offs_lt_n"(#loc_mcmp)) + +tt.func public @mask_kernel(%x: !tt.ptr {tt.divisibility = 16 : i32}, + %out: !tt.ptr {tt.divisibility = 16 : i32}, + %n: i32) { + %offs = tt.make_range {start = 0 : i32, end = 128 : i32} : tensor<128xi32> loc(#loc_off) + %ns = tt.splat %n : i32 -> tensor<128xi32> + %mask = arith.cmpi slt, %offs, %ns : tensor<128xi32> loc(#loc_msk) + %xptr = tt.splat %x : !tt.ptr -> tensor<128x!tt.ptr> + %optr = tt.splat %out : !tt.ptr -> tensor<128x!tt.ptr> + %xp = tt.addptr %xptr, %offs : tensor<128x!tt.ptr>, tensor<128xi32> + %op = tt.addptr %optr, %offs : tensor<128x!tt.ptr>, tensor<128xi32> + %v = tt.load %xp, %mask : tensor<128x!tt.ptr> + tt.store %op, %v, %mask : tensor<128x!tt.ptr> + tt.return +} loc(#loc) + +// DEBUG-LABEL: func.func @mask_kernel +// DEBUG: llvm.inline_asm has_side_effects asm_dialect = att "nop" +// DEBUG-DAG: loc("mask.py":11 +// The mask line (11) must appear as an anchored nop, not only as the cmpi. + +// NODEBUG-LABEL: func.func @mask_kernel +// NODEBUG-NOT: llvm.inline_asm diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/multi_input_reduce.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/multi_input_reduce.mlir new file mode 100644 index 0000000000..ebc7c90ea4 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/multi_input_reduce.mlir @@ -0,0 +1,32 @@ +// RUN: triton-opt --triton-to-linalg %s | FileCheck %s + +// CHECK-LABEL: func.func @three_input_reduce +// CHECK-NOT: linalg.reduce +// CHECK: scf.for +// CHECK: scf.yield +// CHECK-NOT: linalg.reduce +// CHECK: return + +module attributes {hacc.target = #hacc.target<"Ascend910_9589">} { + tt.func public @three_input_reduce(%a_ptr: !tt.ptr, %b_ptr: !tt.ptr, %c_ptr: !tt.ptr, %out_ptr: !tt.ptr) attributes {noinline = false} { + %offsets = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %a_ptrs = tt.splat %a_ptr : !tt.ptr -> tensor<4x!tt.ptr> + %b_ptrs = tt.splat %b_ptr : !tt.ptr -> tensor<4x!tt.ptr> + %c_ptrs = tt.splat %c_ptr : !tt.ptr -> tensor<4x!tt.ptr> + %a_addrs = tt.addptr %a_ptrs, %offsets : tensor<4x!tt.ptr>, tensor<4xi32> + %b_addrs = tt.addptr %b_ptrs, %offsets : tensor<4x!tt.ptr>, tensor<4xi32> + %c_addrs = tt.addptr %c_ptrs, %offsets : tensor<4x!tt.ptr>, tensor<4xi32> + %a = tt.load %a_addrs : tensor<4x!tt.ptr> + %b = tt.load %b_addrs : tensor<4x!tt.ptr> + %c = tt.load %c_addrs : tensor<4x!tt.ptr> + %0:3 = "tt.reduce"(%a, %b, %c) <{axis = 0 : i32}> ({ + ^bb0(%a0: f32, %b0: f32, %c0: f32, %a1: f32, %b1: f32, %c1: f32): + %a_sum = arith.addf %a0, %a1 : f32 + %b_sum = arith.addf %b0, %b1 : f32 + %c_sum = arith.addf %c0, %c1 : f32 + tt.reduce.return %a_sum, %b_sum, %c_sum : f32, f32, f32 + }) : (tensor<4xf32>, tensor<4xf32>, tensor<4xf32>) -> (f32, f32, f32) + tt.store %out_ptr, %0#2 : !tt.ptr + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/parse_custom.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/parse_custom.mlir new file mode 100644 index 0000000000..a9ef1c7cdb --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/parse_custom.mlir @@ -0,0 +1,70 @@ +// RUN: triton-opt --triton-to-unstructure --triton-to-linalg="named-ops=True" --split-input-file %s | FileCheck %s +// CHECK-LABEL: func.func @parse_custom +// CHECK: %4 = hivm.hir.custom +// CHECK: %reinterpret_cast = memref.reinterpret_cast %4 to offset: [0], sizes: [32], strides: [1] : memref to memref<32xf16, strided<[1]>> +// CHECK: %alloc = memref.alloc() : memref<32xf16> +// CHECK: memref.copy %reinterpret_cast, %alloc : memref<32xf16, strided<[1]>> to memref<32xf16> + +module { + tt.func public @parse_custom(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: i32, %arg3: i32) { + %c0_i64 = arith.constant 0 : i64 + %c1_i32 = arith.constant 1 : i32 + %0 = hivm.hir.get_sub_block_idx -> i64 + %1 = arith.cmpi eq, %0, %c0_i64 : i64 + scf.if %1 { + %2 = arith.addi %arg2, %c1_i32 : i32 + %3 = arith.remsi %2, %arg3 : i32 + %4 = hivm.hir.custom {hivm.is_distributed, hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "foo1"} "foo1" ins(%arg0, %3 : !tt.ptr, i32) -> !tt.ptr + %5 = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %6 = tt.splat %4 : !tt.ptr -> tensor<32x!tt.ptr> + %7 = tt.addptr %6, %5 : tensor<32x!tt.ptr>, tensor<32xi32> + %8 = tt.load %7 : tensor<32x!tt.ptr> + %9 = tt.splat %arg1 : !tt.ptr -> tensor<32x!tt.ptr> + %10 = tt.addptr %9, %5 : tensor<32x!tt.ptr>, tensor<32xi32> + tt.store %10, %8 : tensor<32x!tt.ptr> + } + hivm.hir.custom {hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "foo2"} "foo2" + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @parse_custom( +// CHECK-SAME: %[[BASE_I64:arg[0-9]+]]: memref, %[[NUM_PROGRAMS_X:arg[0-9]+]]: i32 +// CHECK: %[[FOO1:.*]] = hivm.hir.custom {{.*}} "foo1" ins(%[[BASE_I64]], %[[NUM_PROGRAMS_X]], %c0_i64 : memref, i32, i64) -> i32 +// CHECK: %[[FOO2:.*]] = hivm.hir.custom {{.*}} "foo2" ins(%arg2, %3 : memref, i32) -> memref +// CHECK: %[[FOO2_VIEW:.*]] = memref.reinterpret_cast %[[FOO2]] to offset: [0], sizes: [32], strides: [1] : memref to memref<32xf16, strided<[1]>> +// CHECK: %[[FOO3:.*]] = hivm.hir.custom {{.*}} "foo3" ins(%[[FOO2_VIEW]], %[[FOO1]] : memref<32xf16, strided<[1]>>, i32) -> memref<32xf16> +// CHECK: %[[FOO3_VIEW:.*]] = memref.reinterpret_cast %[[FOO3]] to offset: [0], sizes: [32], strides: [1] : memref<32xf16> to memref<32xf16, strided<[1]>> +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<32xf16> +// CHECK: memref.copy %[[FOO3_VIEW]], %[[ALLOC]] : memref<32xf16, strided<[1]>> to memref<32xf16> +module { + tt.func public @parse_custom(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: i32, %arg3: i32, %arg4: !tt.ptr) { + %c0_i64 = arith.constant 0 : i64 + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %0 = hivm.hir.get_sub_block_idx -> i64 + %1 = arith.cmpi eq, %0, %c0_i64 : i64 + scf.if %1 { + %2 = arith.addi %arg2, %c1_i32 : i32 + %3 = arith.remsi %2, %arg3 : i32 + %4 = tt.get_num_programs x : i32 + %5 = tt.addptr %arg4, %c0_i32 : !tt.ptr, i32 + %6 = hivm.hir.custom {hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "foo1"} "foo1" ins(%5, %4, %c0_i64 : !tt.ptr, i32, i64) -> i32 + %7 = hivm.hir.custom {hivm.is_distributed, hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "foo2"} "foo2" ins(%arg0, %3 : !tt.ptr, i32) -> !tt.ptr + %8 = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %9 = tt.splat %7 : !tt.ptr -> tensor<32x!tt.ptr> + %10 = tt.addptr %9, %8 : tensor<32x!tt.ptr>, tensor<32xi32> + %11 = tensor.empty() : tensor<32x!tt.ptr> + %12 = hivm.hir.custom {SrcPtrIndex = array, hivm.is_distributed, hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "foo3"} "foo3" ins(%10, %6 : tensor<32x!tt.ptr>, i32) outs(%11 : tensor<32x!tt.ptr>) -> tensor<32x!tt.ptr> + annotation.mark %12 {ContinuousMemAccess} : tensor<32x!tt.ptr> + %13 = tt.load %12 : tensor<32x!tt.ptr> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<32x!tt.ptr> + %15 = tt.addptr %14, %8 : tensor<32x!tt.ptr>, tensor<32xi32> + tt.store %15, %13 : tensor<32x!tt.ptr> + } + hivm.hir.custom {hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "foo4"} "foo4" + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/parse_select.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/parse_select.mlir new file mode 100644 index 0000000000..f844ca90d5 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/parse_select.mlir @@ -0,0 +1,37 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s -verify-each 2>&1 | FileCheck %s --check-prefix=NOERR +// NOERR-NOT: parseSelect currently supports all-ones shape unless cond=i1 with dense constants +// CHECK-LABEL: func.func public @triton_for_if_load + +module { + tt.func public @triton_for_if_load(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0> : tensor<16xi32> + %c0_i32 = arith.constant 0 : i32 + %c16_i32 = arith.constant 16 : i32 + %cst_0 = arith.constant dense<1> : tensor<16xi32> + %cst_1 = arith.constant dense<32> : tensor<16xi32> + %cst_2 = arith.constant dense<0.000000e+00> : tensor<16xf32> + %c2_i32 = arith.constant 2 : i32 + %c1_i32 = arith.constant 1 : i32 + %0 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %1 = tt.get_program_id x : i32 + %2 = arith.cmpi ne, %1, %c0_i32 : i32 + %3 = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %4 = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %5:2 = scf.for %arg2 = %c0_i32 to %c2_i32 step %c1_i32 iter_args(%arg3 = %0, %arg4 = %0) -> (tensor<16xi32>, tensor<16xi32>) : i32 { + %6 = arith.muli %arg2, %c16_i32 : i32 + %7 = tt.splat %6 : i32 -> tensor<16xi32> + %8 = arith.addi %arg3, %7 : tensor<16xi32> + %9 = arith.addi %arg4, %7 : tensor<16xi32> + %10 = arith.select %2, %cst_0, %cst : tensor<16xi32> + %11 = arith.addi %8, %10 : tensor<16xi32> + %12 = tt.addptr %3, %11 : tensor<16x!tt.ptr>, tensor<16xi32> + %13 = arith.cmpi slt, %11, %cst_1 : tensor<16xi32> + %14 = tt.load %12 : tensor<16x!tt.ptr> + %15 = arith.select %13, %14, %cst_2 : tensor<16xi1>, tensor<16xf32> + %16 = tt.addptr %4, %9 : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %16, %15 : tensor<16x!tt.ptr> + scf.yield %11, %9 : tensor<16xi32>, tensor<16xi32> + } + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/pointer_bitcast_byte_buffer.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/pointer_bitcast_byte_buffer.mlir new file mode 100644 index 0000000000..e20053d2af --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/pointer_bitcast_byte_buffer.mlir @@ -0,0 +1,116 @@ +// RUN: triton-opt --triton-to-unstructure --triton-to-linalg="named-ops=True" --split-input-file %s | FileCheck %s +// RUN: triton-opt --triton-to-unstructure --split-input-file %s | FileCheck %s --check-prefix=UNSTRUCTURE +// RUN: triton-opt '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' --split-input-file %s | FileCheck %s --check-prefix=SIMT +// RUN: triton-opt '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' '--triton-to-linalg=compile-on-910-95=True compile-mode=simd_simt_template' --split-input-file %s | FileCheck %s --check-prefix=SIMT-LINALG + +// CHECK-LABEL: func.func @widen_tensor_offset +// CHECK: scf.for +// CHECK: hivm.hir.pointer_cast{{.*}} : memref +// CHECK: return +// UNSTRUCTURE-LABEL: tt.func public @widen_tensor_offset +// UNSTRUCTURE: arith.divsi +// UNSTRUCTURE: tt.ptr_to_int +// UNSTRUCTURE: tt.int_to_ptr +// UNSTRUCTURE-NOT: tt.bitcast +// SIMT-LABEL: tt.func public @widen_tensor_offset +// SIMT: ascend.indirect_load +// SIMT-NOT: tt.bitcast +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @widen_tensor_offset( + %src: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %range = tt.make_range {start = 0 : i32, end = 8 : i32} : tensor<8xi32> + %four = arith.constant dense<4> : tensor<8xi32> + %offsets = arith.muli %range, %four : tensor<8xi32> + %srcs = tt.splat %src : !tt.ptr -> tensor<8x!tt.ptr> + %byte_ptrs = tt.addptr %srcs, %offsets : tensor<8x!tt.ptr>, tensor<8xi32> + %i32_ptrs = tt.bitcast %byte_ptrs : tensor<8x!tt.ptr> -> tensor<8x!tt.ptr> + %values = tt.load %i32_ptrs : tensor<8x!tt.ptr> + %dsts = tt.splat %dst : !tt.ptr -> tensor<8x!tt.ptr> + %dst_ptrs = tt.addptr %dsts, %range : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_ptrs, %values : tensor<8x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @widen_scalar_multi_addptr +// CHECK: arith.addi +// CHECK: hivm.hir.pointer_cast{{.*}} : memref +// CHECK: return +// UNSTRUCTURE-LABEL: tt.func public @widen_scalar_multi_addptr +// UNSTRUCTURE: tt.ptr_to_int +// UNSTRUCTURE: arith.addi +// UNSTRUCTURE: tt.int_to_ptr +// UNSTRUCTURE-NOT: tt.bitcast +// SIMT-LINALG-LABEL: func.func @widen_scalar_multi_addptr +// SIMT-LINALG-NOT: tt.int_to_ptr +// SIMT-LINALG: memref.extract_aligned_pointer_as_index +// SIMT-LINALG: memref.reinterpret_cast +// SIMT-LINALG: bufferization.materialize_in_destination +// SIMT-LINALG-NOT: tt.int_to_ptr +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @widen_scalar_multi_addptr( + %values: !tt.ptr {tt.divisibility = 16 : i32}, + %cache: !tt.ptr {tt.divisibility = 16 : i32}, + %block: i64, + %position: i64, + %block_stride: i64 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %range = tt.make_range {start = 0 : i32, end = 8 : i32} : tensor<8xi32> + %c576_i64 = arith.constant 576 : i64 + %c448_i64 = arith.constant 448 : i64 + %block_offset = arith.muli %block, %block_stride : i64 + %block_base = tt.addptr %cache, %block_offset : !tt.ptr, i64 + %token_offset = arith.muli %position, %c576_i64 : i64 + %token_base = tt.addptr %block_base, %token_offset : !tt.ptr, i64 + %rope_bytes = tt.addptr %token_base, %c448_i64 : !tt.ptr, i64 + %rope_bf16 = tt.bitcast %rope_bytes : !tt.ptr -> !tt.ptr + %rope_bases = tt.splat %rope_bf16 : !tt.ptr -> tensor<8x!tt.ptr> + %rope_ptrs = tt.addptr %rope_bases, %range : tensor<8x!tt.ptr>, tensor<8xi32> + %value_bases = tt.splat %values : !tt.ptr -> tensor<8x!tt.ptr> + %value_ptrs = tt.addptr %value_bases, %range : tensor<8x!tt.ptr>, tensor<8xi32> + %loaded = tt.load %value_ptrs : tensor<8x!tt.ptr> + tt.store %rope_ptrs, %loaded : tensor<8x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @widen_pointer_inside_loop +// CHECK: scf.for +// CHECK: hivm.hir.pointer_cast{{.*}} : memref +// CHECK: return +// UNSTRUCTURE-LABEL: tt.func public @widen_pointer_inside_loop +// UNSTRUCTURE: scf.for +// UNSTRUCTURE: tt.ptr_to_int +// UNSTRUCTURE: tt.int_to_ptr +// UNSTRUCTURE: tt.splat +// UNSTRUCTURE: tt.addptr +// UNSTRUCTURE-NOT: tt.bitcast +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @widen_pointer_inside_loop( + %src: !tt.ptr {tt.divisibility = 16 : i32}, + %dst: !tt.ptr {tt.divisibility = 16 : i32}, + %count: i64) attributes {noinline = false} { + %range = tt.make_range {start = 0 : i32, end = 8 : i32} : tensor<8xi32> + %c0_i64 = arith.constant 0 : i64 + %c1_i64 = arith.constant 1 : i64 + %c576_i64 = arith.constant 576 : i64 + %c448_i64 = arith.constant 448 : i64 + scf.for %position = %c0_i64 to %count step %c1_i64 : i64 { + %token_offset = arith.muli %position, %c576_i64 : i64 + %token_base = tt.addptr %src, %token_offset : !tt.ptr, i64 + %rope_bytes = tt.addptr %token_base, %c448_i64 : !tt.ptr, i64 + %rope_bf16 = tt.bitcast %rope_bytes : !tt.ptr -> !tt.ptr + %rope_bases = tt.splat %rope_bf16 : !tt.ptr -> tensor<8x!tt.ptr> + %rope_ptrs = tt.addptr %rope_bases, %range : tensor<8x!tt.ptr>, tensor<8xi32> + %values = tt.load %rope_ptrs : tensor<8x!tt.ptr> + %dsts = tt.splat %dst : !tt.ptr -> tensor<8x!tt.ptr> + %dst_ptrs = tt.addptr %dsts, %range : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_ptrs, %values : tensor<8x!tt.ptr> + } + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/scalar_masked_load.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/scalar_masked_load.mlir new file mode 100644 index 0000000000..bfbbaa796e --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/scalar_masked_load.mlir @@ -0,0 +1,102 @@ +// RUN: triton-opt %s --triton-to-linalg -verify-each | FileCheck %s --implicit-check-not='tensor<' --implicit-check-not='arith.select' +// RUN: triton-opt %s --triton-to-linalg='named-ops=true' -verify-each | FileCheck %s --implicit-check-not='tensor<' --implicit-check-not='arith.select' +// RUN: sed 's/Ascend910B2/Ascend950DT_9582/' %s | triton-opt --triton-to-linalg='compile-on-910-95=true' -verify-each | FileCheck %s --implicit-check-not='tensor<' --implicit-check-not='arith.select' + +// Scalar masked loads must keep their result scalar and only read memory in +// the active branch. In particular, selecting the result of an unconditional +// load does not protect an invalid masked-off address. +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + // CHECK-LABEL: func.func @masked_i32( + // CHECK-SAME: %[[MASK:[^:]+]]: i1, %[[OTHER:[^:]+]]: i32 + // CHECK-NOT: memref.load + // CHECK: %[[RESULT:.*]] = scf.if %[[MASK]] -> (i32) { + // CHECK-NEXT: %[[VALUE:.*]] = memref.load + // CHECK-NEXT: scf.yield %[[VALUE]] : i32 + // CHECK-NEXT: } else { + // CHECK-NEXT: scf.yield %[[OTHER]] : i32 + // CHECK-NEXT: } + // CHECK-NOT: memref.load + // CHECK: return %[[RESULT]] : i32 + tt.func public @masked_i32(%ptr: !tt.ptr, %mask: i1, %other: i32, %offset: i32) -> i32 { + %address = tt.addptr %ptr, %offset : !tt.ptr, i32 + %value = tt.load %address, %mask, %other : !tt.ptr + tt.return %value : i32 + } + + // CHECK-LABEL: func.func @masked_f32( + // CHECK-SAME: %[[MASK:[^:]+]]: i1, %[[OTHER:[^:]+]]: f32 + // CHECK-NOT: memref.load + // CHECK: %[[RESULT:.*]] = scf.if %[[MASK]] -> (f32) { + // CHECK-NEXT: %[[VALUE:.*]] = memref.load + // CHECK-NEXT: scf.yield %[[VALUE]] : f32 + // CHECK-NEXT: } else { + // CHECK-NEXT: scf.yield %[[OTHER]] : f32 + // CHECK-NEXT: } + // CHECK-NOT: memref.load + // CHECK: return %[[RESULT]] : f32 + tt.func public @masked_f32(%ptr: !tt.ptr, %mask: i1, %other: f32) -> f32 { + %value = tt.load %ptr, %mask, %other : !tt.ptr + tt.return %value : f32 + } + + // No `other` leaves the inactive result unspecified, but the load must + // still be conditional. Do not require a particular inactive result value. + // CHECK-LABEL: func.func @masked_without_other( + // CHECK-SAME: %[[MASK:[^:]+]]: i1 + // CHECK-NOT: memref.load + // CHECK: %[[RESULT:.*]] = scf.if %[[MASK]] -> (i32) { + // CHECK-NEXT: %[[VALUE:.*]] = memref.load + // CHECK-NEXT: scf.yield %[[VALUE]] : i32 + // CHECK-NEXT: } else { + // CHECK-NOT: memref.load + // CHECK: scf.yield %{{.*}} : i32 + // CHECK-NEXT: } + // CHECK-NOT: memref.load + // CHECK: return %[[RESULT]] : i32 + tt.func public @masked_without_other(%ptr: !tt.ptr, %mask: i1) -> i32 { + %value = tt.load %ptr, %mask : !tt.ptr + tt.return %value : i32 + } + + // CHECK-LABEL: func.func @unmasked( + // CHECK-NOT: scf.if + // CHECK: %[[VALUE:.*]] = memref.load + // CHECK-NOT: scf.if + // CHECK: return %[[VALUE]] : i32 + tt.func public @unmasked(%ptr: !tt.ptr) -> i32 { + %value = tt.load %ptr : !tt.ptr + tt.return %value : i32 + } + + // CHECK-LABEL: func.func @mask_true( + // CHECK-NOT: scf.if + // CHECK: %[[VALUE:.*]] = memref.load + // CHECK-NOT: scf.if + // CHECK: return %[[VALUE]] : i32 + tt.func public @mask_true(%ptr: !tt.ptr, %other: i32) -> i32 { + %true = arith.constant true + %value = tt.load %ptr, %true, %other : !tt.ptr + tt.return %value : i32 + } + + // CHECK-LABEL: func.func @mask_false( + // CHECK-SAME: %[[OTHER:[^:]+]]: i32 + // CHECK-NOT: memref.load + // CHECK-NOT: scf.if + // CHECK: return %[[OTHER]] : i32 + tt.func public @mask_false(%ptr: !tt.ptr, %other: i32) -> i32 { + %false = arith.constant false + %value = tt.load %ptr, %false, %other : !tt.ptr + tt.return %value : i32 + } + + // CHECK-LABEL: func.func @mask_false_without_other( + // CHECK-NOT: memref.load + // CHECK-NOT: scf.if + // CHECK: return %{{.*}} : f32 + tt.func public @mask_false_without_other(%ptr: !tt.ptr) -> f32 { + %false = arith.constant false + %value = tt.load %ptr, %false : !tt.ptr + tt.return %value : f32 + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/scalar_pointer_select.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/scalar_pointer_select.mlir new file mode 100644 index 0000000000..6d8ac31dfd --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/scalar_pointer_select.mlir @@ -0,0 +1,83 @@ +// RUN: triton-opt --triton-to-linalg %s -verify-each | FileCheck %s + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @scalar_pointer_select(%lhs: !tt.ptr, %rhs: !tt.ptr, %condition: i1) -> i64 { + %selected = arith.select %condition, %lhs, %rhs : !tt.ptr + %address = tt.ptr_to_int %selected : !tt.ptr -> i64 + tt.return %address : i64 + } + + tt.func public @scalar_pointer_roundtrip(%address: i64) -> i64 { + %pointer = tt.int_to_ptr %address : i64 -> !tt.ptr + %roundtrip = tt.ptr_to_int %pointer : !tt.ptr -> i64 + tt.return %roundtrip : i64 + } + + func.func @pointer_cast_static_offset(%address: i64) -> f32 { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %pointer = hivm.hir.pointer_cast(%address) [%c1] : memref + %view = memref.reinterpret_cast %pointer to offset: [1], sizes: [1], strides: [1] + : memref to memref<1xf32, strided<[1], offset: 1>> + %value = memref.load %view[%c0] : memref<1xf32, strided<[1], offset: 1>> + return %value : f32 + } + + func.func @pointer_cast_dynamic_offset_subviews(%address: i64, %offset: index, %size: index) -> f32 { + %c0 = arith.constant 0 : index + %c8 = arith.constant 8 : index + %pointer = hivm.hir.pointer_cast(%address) [%c8] : memref + %view = memref.reinterpret_cast %pointer to offset: [%offset], sizes: [1, 8], strides: [8, 1] + : memref to memref<1x8xf32, strided<[8, 1], offset: ?>> + %rank_reduced = memref.subview %view[0, 0] [1, %size] [1, 1] + : memref<1x8xf32, strided<[8, 1], offset: ?>> to memref> + %subview = memref.subview %rank_reduced[0] [%size] [1] + : memref> to memref> + %value = memref.load %subview[%c0] : memref> + return %value : f32 + } +} + +// CHECK-LABEL: func.func @scalar_pointer_select( +// CHECK-SAME: %[[LHS:[^ ,]+]]: memref, %[[RHS:[^ ,]+]]: memref +// CHECK: %[[LHS_INDEX:.*]] = memref.extract_aligned_pointer_as_index %[[LHS]] +// CHECK: %[[LHS_ADDRESS:.*]] = arith.index_cast %[[LHS_INDEX]] : index to i64 +// CHECK: %[[RHS_INDEX:.*]] = memref.extract_aligned_pointer_as_index %[[RHS]] +// CHECK: %[[RHS_ADDRESS:.*]] = arith.index_cast %[[RHS_INDEX]] : index to i64 +// CHECK: %[[SELECTED:.*]] = arith.select %{{.*}}, %[[LHS_ADDRESS]], %[[RHS_ADDRESS]] : i64 +// CHECK-NOT: arith.select {{.*}} : memref +// CHECK-NOT: hivm.hir.pointer_cast +// CHECK-NOT: memref.extract_aligned_pointer_as_index +// CHECK: return %[[SELECTED]] : i64 + +// CHECK-LABEL: func.func @scalar_pointer_roundtrip( +// CHECK-SAME: %[[ADDRESS:[^ ,]+]]: i64 +// CHECK-NOT: hivm.hir.pointer_cast +// CHECK-NOT: memref.extract_aligned_pointer_as_index +// CHECK: return %[[ADDRESS]] : i64 + +// CHECK-LABEL: func.func @pointer_cast_static_offset( +// CHECK-SAME: %[[ADDRESS:[^ ,]+]]: i64 +// CHECK: %[[OFFSET:.*]] = arith.constant 1 : i64 +// CHECK: %[[BYTE_WIDTH:.*]] = arith.constant 4 : i64 +// CHECK: %[[BYTE_OFFSET:.*]] = arith.muli %[[OFFSET]], %[[BYTE_WIDTH]] : i64 +// CHECK: %[[REAL_ADDRESS:.*]] = arith.addi %[[ADDRESS]], %[[BYTE_OFFSET]] : i64 +// CHECK: %[[REBASED_POINTER:.*]] = hivm.hir.pointer_cast(%[[REAL_ADDRESS]]) [%[[STATIC_CAPACITY:[A-Za-z0-9_]+]]] : memref +// CHECK: %[[VIEW:.*]] = memref.reinterpret_cast %[[REBASED_POINTER]] to offset: [0], sizes: [1], strides: [1] +// CHECK-SAME: to memref<1xf32, strided<[1]>> +// CHECK: memref.load %[[VIEW]][%{{.*}}] : memref<1xf32, strided<[1]>> + +// CHECK-LABEL: func.func @pointer_cast_dynamic_offset_subviews( +// CHECK-SAME: %[[ADDRESS:[^ ,]+]]: i64, %[[OFFSET:[^ ,]+]]: index, %[[DYNAMIC_SIZE:[^ ,]+]]: index +// CHECK: %[[OFFSET_I64:.*]] = arith.index_cast %[[OFFSET]] : index to i64 +// CHECK: %[[BYTE_WIDTH:.*]] = arith.constant 4 : i64 +// CHECK: %[[BYTE_OFFSET:.*]] = arith.muli %[[OFFSET_I64]], %[[BYTE_WIDTH]] : i64 +// CHECK: %[[REAL_ADDRESS:.*]] = arith.addi %[[ADDRESS]], %[[BYTE_OFFSET]] : i64 +// CHECK: %[[REBASED_POINTER:.*]] = hivm.hir.pointer_cast(%[[REAL_ADDRESS]]) [%[[DYNAMIC_CAPACITY:[A-Za-z0-9_]+]]] : memref +// CHECK: %[[VIEW:.*]] = memref.reinterpret_cast %[[REBASED_POINTER]] to offset: [0], sizes: [1, 8], strides: [8, 1] +// CHECK-SAME: to memref<1x8xf32, strided<[8, 1], offset: ?>> +// CHECK: %[[RANK_REDUCED:.*]] = memref.subview %[[VIEW]][0, 0] [1, %[[DYNAMIC_SIZE]]] [1, 1] +// CHECK-SAME: to memref> +// CHECK: %[[SUBVIEW:.*]] = memref.subview %[[RANK_REDUCED]][0] [%[[DYNAMIC_SIZE]]] [1] +// CHECK-SAME: to memref> +// CHECK: memref.load %[[SUBVIEW]][%{{.*}}] : memref> diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/tensor_pointer_descriptor_loop.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/tensor_pointer_descriptor_loop.mlir new file mode 100644 index 0000000000..46f4c7d81e --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/tensor_pointer_descriptor_loop.mlir @@ -0,0 +1,144 @@ +// RUN: triton-opt --triton-control-flow-opt %s -verify-each | FileCheck %s --check-prefix=CFO +// RUN: triton-opt --triton-control-flow-opt --triton-to-unstructure %s -verify-each | FileCheck %s --check-prefix=T2U +// RUN: triton-opt --triton-control-flow-opt --triton-to-unstructure --bubble-up-operation --triton-to-linalg %s -verify-each | FileCheck %s --check-prefix=LINALG + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @tensor_pointer_descriptor_loop( + %base: !tt.ptr, %output: !tt.ptr, %upper: index) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<1> : tensor<4xi32> + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %initial = tt.addptr %base_tensor, %range : tensor<4x!tt.ptr>, tensor<4xi32> + %final = scf.for %iv = %c0 to %upper step %c1 iter_args(%ptr = %initial) -> (tensor<4x!tt.ptr>) { + %next = tt.addptr %ptr, %delta : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %next : tensor<4x!tt.ptr> + } + %value = tt.load %final : tensor<4x!tt.ptr> + %output_tensor = tt.splat %output : !tt.ptr -> tensor<4x!tt.ptr> + %output_ptr = tt.addptr %output_tensor, %range : tensor<4x!tt.ptr>, tensor<4xi32> + tt.store %output_ptr, %value : tensor<4x!tt.ptr> + tt.return + } + + // A CFO descriptor loop may also carry an ordinary integer offset tensor. + // Recover its affine row/column layout from the carrier provenance instead + // of lowering the corresponding load as an indirect gather. + tt.func public @marked_affine_index_carrier( + %input: !tt.ptr, %output: !tt.ptr, %row_stride: i32, + %upper: index) { + %c2 = arith.constant 2 : index + %c3 = arith.constant 3 : index + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %rows = tt.expand_dims %range {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %stride = tt.splat %row_stride : i32 -> tensor<4x1xi32> + %scaled_rows = arith.muli %rows, %stride : tensor<4x1xi32> + %row_offsets = tt.broadcast %scaled_rows : tensor<4x1xi32> -> tensor<4x4xi32> + %columns = tt.expand_dims %range {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %column_offsets = tt.broadcast %columns : tensor<1x4xi32> -> tensor<4x4xi32> + %initial_offsets = arith.addi %row_offsets, %column_offsets : tensor<4x4xi32> + %input_base = tt.splat %input : !tt.ptr -> tensor<4x4x!tt.ptr> + %output_base = tt.splat %output : !tt.ptr -> tensor<4x4x!tt.ptr> + %initial_output = tt.addptr %output_base, %initial_offsets : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %advance = arith.constant dense<16> : tensor<4x4xi32> + %result:2 = scf.for %iv = %c2 to %upper step %c3 + iter_args(%offsets = %initial_offsets, %output_ptrs = %initial_output) + -> (tensor<4x4xi32>, tensor<4x4x!tt.ptr>) { + %input_ptrs = tt.addptr %input_base, %offsets : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %value = tt.load %input_ptrs : tensor<4x4x!tt.ptr> + tt.store %output_ptrs, %value : tensor<4x4x!tt.ptr> + %next_offsets = arith.addi %offsets, %advance : tensor<4x4xi32> + %next_output = tt.addptr %output_ptrs, %advance : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + scf.yield %next_offsets, %next_output : tensor<4x4xi32>, tensor<4x4x!tt.ptr> + } + tt.return + } + // A pointer broadcast after opaque i64 row indexing must retain contiguous + // i32 columns. Keep the invariant rows outside the loop and load each row + // with a contiguous copy after advancing only a scalar uniform offset. + tt.func public @mixed_pointer_broadcast_load( + %base: !tt.ptr, %output: !tt.ptr, + %row_source: !tt.ptr, %upper: index) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %delta = arith.constant dense<32> : tensor<2x32xi32> + %row_lanes = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %row_base = tt.splat %row_source : !tt.ptr -> tensor<2x!tt.ptr> + %row_ptr = tt.addptr %row_base, %row_lanes : tensor<2x!tt.ptr>, tensor<2xi32> + %rows = tt.load %row_ptr : tensor<2x!tt.ptr> + %row_offsets = tt.expand_dims %rows {axis = 1 : i32} : tensor<2xi64> -> tensor<2x1xi64> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<2x1x!tt.ptr> + %row_ptrs = tt.addptr %base_tensor, %row_offsets : tensor<2x1x!tt.ptr>, tensor<2x1xi64> + %broadcast = tt.broadcast %row_ptrs : tensor<2x1x!tt.ptr> -> tensor<2x32x!tt.ptr> + %range = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %columns = tt.expand_dims %range {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %column_offsets = tt.broadcast %columns : tensor<1x32xi32> -> tensor<2x32xi32> + %initial = tt.addptr %broadcast, %column_offsets : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + %final = scf.for %iv = %c0 to %upper step %c1 + iter_args(%pointer = %initial) -> (tensor<2x32x!tt.ptr>) { + %next = tt.addptr %pointer, %delta : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + scf.yield %next : tensor<2x32x!tt.ptr> + } + %loaded = tt.load %final : tensor<2x32x!tt.ptr> + %out_row_range = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %out_rows = tt.expand_dims %out_row_range {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %out_stride = arith.constant dense<32> : tensor<2x1xi32> + %out_scaled_rows = arith.muli %out_rows, %out_stride : tensor<2x1xi32> + %out_row_offsets = tt.broadcast %out_scaled_rows : tensor<2x1xi32> -> tensor<2x32xi32> + %out_offsets = arith.addi %out_row_offsets, %column_offsets : tensor<2x32xi32> + %out_base = tt.splat %output : !tt.ptr -> tensor<2x32x!tt.ptr> + %out_ptrs = tt.addptr %out_base, %out_offsets : tensor<2x32x!tt.ptr>, tensor<2x32xi32> + tt.store %out_ptrs, %loaded : tensor<2x32x!tt.ptr> + tt.return + } +} + +// CFO-LABEL: tt.func public @tensor_pointer_descriptor_loop +// CFO: scf.for +// CFO-SAME: i32 +// CFO: tt.addptr {{.*}}PointerDescriptorOffsetForm = "strided_1d" +// CFO-SAME: PointerDescriptorStructuredAxes = array + +// LINALG-LABEL: func.func @tensor_pointer_descriptor_loop +// LINALG: scf.for +// LINALG-SAME: i32 +// LINALG-NOT: tensor<4x!tt.ptr +// LINALG-NOT: unrealized_conversion_cast + +// CFO-LABEL: tt.func public @marked_affine_index_carrier +// CFO: scf.for +// CFO: PointerDescriptorBoundary + +// T2U-LABEL: tt.func public @marked_affine_index_carrier +// T2U: scf.for %[[IV:[a-zA-Z0-9_]+]] = +// T2U: arith.subi %[[IV]], +// T2U: arith.divui +// T2U: tt.addptr {{.*}}PointerDescriptorRebuild +// T2U-SAME: PointerDescriptorStructuredAxes = array +// T2U: tt.load + +// LINALG-LABEL: func.func @marked_affine_index_carrier +// LINALG: scf.for +// LINALG-NOT: triton_indirect_load +// LINALG: return + +// CFO-LABEL: tt.func public @mixed_pointer_broadcast_load +// CFO: tt.broadcast {{.*}} : tensor<2x1xi64> -> tensor<2x32xi64> +// CFO-NOT: arith.subi +// CFO: scf.for {{.*}} iter_args(%{{.*}} = %{{.*}}) -> (i64) +// CFO: scf.yield {{.*}} : i64 +// CFO: PointerDescriptorStructuredAxes = array + +// T2U-LABEL: tt.func public @mixed_pointer_broadcast_load +// T2U: scf.for {{.*}} iter_args(%{{.*}} = %{{.*}}) -> (i64) +// T2U: tt.load {{.*}} : tensor<1x32x!tt.ptr> + +// LINALG-LABEL: func.func @mixed_pointer_broadcast_load +// LINALG: scf.for {{.*}} iter_args(%{{.*}} = %{{.*}}) -> (i64) +// LINALG-NOT: triton_indirect_load +// LINALG-NOT: memref.load +// LINALG: memref.copy +// LINALG-NOT: triton_indirect_load +// LINALG-NOT: memref.load +// LINALG: return diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_device_print_offset_rewrite.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_device_print_offset_rewrite.mlir new file mode 100644 index 0000000000..6c7ec7efa1 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_device_print_offset_rewrite.mlir @@ -0,0 +1,459 @@ +// RUN: triton-opt --pass-pipeline="builtin.module(auto-blockify{auto-blockify-size=1},triton-to-structured{enable-mask-fallback-conversion=false optimize-dynamic-offset=false},discrete-mask-access-conversion{compile-on-910-95=false compile-mode=simd_simt_template},triton-to-annotation,triton-to-unstructure{compile-on-910-95=false compile-mode=simd_simt_template},triton-to-hivm,triton-to-hfusion,triton-to-llvm,bubble-up-operation,triton-to-structured{enable-mask-fallback-conversion=false optimize-dynamic-offset=false},triton-to-linalg{compile-on-910-95=false enable-nd2nz-on-vector=false enable-select-analysis=true global-kernel=false named-ops=true compile-mode=simd_simt_template})" --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @kernel_B8_dynamic_offset +// CHECK: %[[PIDOFF:.*]] = arith.muli +// CHECK: scf.for %[[IV:.*]] = {{.*}} to {{.*}} step {{.*}} { +// CHECK: %[[IV32:.*]] = arith.index_cast %[[IV]] : index to i32 +// CHECK: arith.addi %[[PIDOFF]], %[[IV32]] : i32 +// CHECK: func.call @triton_print{{[_0-9]*}}({{.*}}) : (i32) -> () +// CHECK: } + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @kernel_B8_dynamic_offset(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c8_i32 : i32 + %2 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %3 = tt.splat %1 : i32 -> tensor<8xi32> + %4 = arith.addi %3, %2 : tensor<8xi32> + %5 = tt.splat %arg0 : !tt.ptr -> tensor<8x!tt.ptr> + %6 = tt.addptr %5, %4 : tensor<8x!tt.ptr>, tensor<8xi32> + %7 = tt.load %6 : tensor<8x!tt.ptr> + tt.print " idx_B8: " {hex = false, isSigned = array} : %4 : tensor<8xi32> + %8 = tt.splat %arg1 : !tt.ptr -> tensor<8x!tt.ptr> + %9 = tt.addptr %8, %4 : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %9, %7 : tensor<8x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func private @triton_print{{[_0-9]*}}(i32) +// CHECK-LABEL: func.func @triton_max_6d_dimm1 +// CHECK-DAG: %[[C31744:.*]] = arith.constant 31744 : index +// CHECK-DAG: %[[C1024:.*]] = arith.constant 1024 : index +// CHECK-DAG: %[[C256:.*]] = arith.constant 256 : index +// CHECK: scf.for %[[IV0:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: scf.for %[[IV1:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: scf.for %[[IV2:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: scf.for %[[IV3:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: %{{.*}} = arith.muli %[[IV0]], %[[C31744]] : index +// CHECK: %{{.*}} = arith.muli %[[IV1]], %[[C1024]] : index +// CHECK: %{{.*}} = arith.addi +// CHECK: %{{.*}} = arith.muli %[[IV2]], %[[C256]] : index +// CHECK: %{{.*}} = arith.addi +// CHECK: %{{.*}} = arith.addi %{{.*}}, %[[IV3]] : index +// CHECK: %{{.*}} = arith.index_cast %{{.*}} : index to i32 +// CHECK: func.call @triton_print{{[_0-9]*}}(%{{.*}}) : (i32) -> () +// CHECK: } +// CHECK: } +// CHECK: } +// CHECK: } + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @triton_max_6d_dimm1(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %true = arith.constant true + %cst = arith.constant dense<256> : tensor<1x1x4x1x1xi32> + %cst_0 = arith.constant dense<256> : tensor<1x31x1x1x1xi32> + %cst_1 = arith.constant dense<4> : tensor<1x31x1x1x1xi32> + %cst_2 = arith.constant dense<256> : tensor<4x1x1x1x1xi32> + %cst_3 = arith.constant dense<4> : tensor<4x1x1x1x1xi32> + %cst_4 = arith.constant dense<31> : tensor<4x1x1x1x1xi32> + %cst_5 = arith.constant dense<256> : tensor<1x1x4x1x1x1xi32> + %cst_6 = arith.constant dense<256> : tensor<1x31x1x1x1x1xi32> + %cst_7 = arith.constant dense<4> : tensor<1x31x1x1x1x1xi32> + %cst_8 = arith.constant dense<256> : tensor<4x1x1x1x1x1xi32> + %cst_9 = arith.constant dense<4> : tensor<4x1x1x1x1x1xi32> + %cst_10 = arith.constant dense<31> : tensor<4x1x1x1x1x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.make_range {end = 31 : i32, start = 0 : i32} : tensor<31xi32> + %2 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %3 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %4 = tt.expand_dims %3 {axis = 2 : i32} : tensor<4x1xi32> -> tensor<4x1x1xi32> + %5 = tt.expand_dims %4 {axis = 3 : i32} : tensor<4x1x1xi32> -> tensor<4x1x1x1xi32> + %6 = tt.expand_dims %5 {axis = 4 : i32} : tensor<4x1x1x1xi32> -> tensor<4x1x1x1x1xi32> + %7 = tt.expand_dims %6 {axis = 5 : i32} : tensor<4x1x1x1x1xi32> -> tensor<4x1x1x1x1x1xi32> + %8 = arith.muli %7, %cst_10 : tensor<4x1x1x1x1x1xi32> + %9 = arith.muli %8, %cst_9 : tensor<4x1x1x1x1x1xi32> + %10 = arith.muli %9, %cst_8 : tensor<4x1x1x1x1x1xi32> + %11 = tt.expand_dims %1 {axis = 0 : i32} : tensor<31xi32> -> tensor<1x31xi32> + %12 = tt.expand_dims %11 {axis = 2 : i32} : tensor<1x31xi32> -> tensor<1x31x1xi32> + %13 = tt.expand_dims %12 {axis = 3 : i32} : tensor<1x31x1xi32> -> tensor<1x31x1x1xi32> + %14 = tt.expand_dims %13 {axis = 4 : i32} : tensor<1x31x1x1xi32> -> tensor<1x31x1x1x1xi32> + %15 = tt.expand_dims %14 {axis = 5 : i32} : tensor<1x31x1x1x1xi32> -> tensor<1x31x1x1x1x1xi32> + %16 = arith.muli %15, %cst_7 : tensor<1x31x1x1x1x1xi32> + %17 = arith.muli %16, %cst_6 : tensor<1x31x1x1x1x1xi32> + %18 = tt.broadcast %10 : tensor<4x1x1x1x1x1xi32> -> tensor<4x31x1x1x1x1xi32> + %19 = tt.broadcast %17 : tensor<1x31x1x1x1x1xi32> -> tensor<4x31x1x1x1x1xi32> + %20 = arith.addi %18, %19 : tensor<4x31x1x1x1x1xi32> + %21 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %22 = tt.expand_dims %21 {axis = 1 : i32} : tensor<1x4xi32> -> tensor<1x1x4xi32> + %23 = tt.expand_dims %22 {axis = 3 : i32} : tensor<1x1x4xi32> -> tensor<1x1x4x1xi32> + %24 = tt.expand_dims %23 {axis = 4 : i32} : tensor<1x1x4x1xi32> -> tensor<1x1x4x1x1xi32> + %25 = tt.expand_dims %24 {axis = 5 : i32} : tensor<1x1x4x1x1xi32> -> tensor<1x1x4x1x1x1xi32> + %26 = arith.muli %25, %cst_5 : tensor<1x1x4x1x1x1xi32> + %27 = tt.broadcast %20 : tensor<4x31x1x1x1x1xi32> -> tensor<4x31x4x1x1x1xi32> + %28 = tt.broadcast %26 : tensor<1x1x4x1x1x1xi32> -> tensor<4x31x4x1x1x1xi32> + %29 = arith.addi %27, %28 : tensor<4x31x4x1x1x1xi32> + %30 = tt.expand_dims %2 {axis = 0 : i32} : tensor<256xi32> -> tensor<1x256xi32> + %31 = tt.expand_dims %30 {axis = 1 : i32} : tensor<1x256xi32> -> tensor<1x1x256xi32> + %32 = tt.expand_dims %31 {axis = 2 : i32} : tensor<1x1x256xi32> -> tensor<1x1x1x256xi32> + %33 = tt.expand_dims %32 {axis = 4 : i32} : tensor<1x1x1x256xi32> -> tensor<1x1x1x256x1xi32> + %34 = tt.expand_dims %33 {axis = 5 : i32} : tensor<1x1x1x256x1xi32> -> tensor<1x1x1x256x1x1xi32> + %35 = tt.broadcast %29 : tensor<4x31x4x1x1x1xi32> -> tensor<4x31x4x256x1x1xi32> + %36 = tt.broadcast %34 : tensor<1x1x1x256x1x1xi32> -> tensor<4x31x4x256x1x1xi32> + %37 = arith.addi %35, %36 : tensor<4x31x4x256x1x1xi32> + tt.print ": " {hex = false, isSigned = array} : %37 : tensor<4x31x4x256x1x1xi32> + %38 = tt.splat %arg0 : !tt.ptr -> tensor<4x31x4x256x1x1x!tt.ptr> + %39 = tt.addptr %38, %37 : tensor<4x31x4x256x1x1x!tt.ptr>, tensor<4x31x4x256x1x1xi32> + %40 = tt.load %39 : tensor<4x31x4x256x1x1x!tt.ptr> + tt.assert %true, "Expecting input to be integer type" : i1 + %41 = "tt.reduce"(%40) <{axis = 5 : i32}> ({ + ^bb0(%arg2: i8, %arg3: i8): + %59 = arith.maxsi %arg2, %arg3 : i8 + tt.reduce.return %59 : i8 + }) : (tensor<4x31x4x256x1x1xi8>) -> tensor<4x31x4x256x1xi8> + %42 = arith.muli %6, %cst_4 : tensor<4x1x1x1x1xi32> + %43 = arith.muli %42, %cst_3 : tensor<4x1x1x1x1xi32> + %44 = arith.muli %43, %cst_2 : tensor<4x1x1x1x1xi32> + %45 = arith.muli %14, %cst_1 : tensor<1x31x1x1x1xi32> + %46 = arith.muli %45, %cst_0 : tensor<1x31x1x1x1xi32> + %47 = tt.broadcast %44 : tensor<4x1x1x1x1xi32> -> tensor<4x31x1x1x1xi32> + %48 = tt.broadcast %46 : tensor<1x31x1x1x1xi32> -> tensor<4x31x1x1x1xi32> + %49 = arith.addi %47, %48 : tensor<4x31x1x1x1xi32> + %50 = arith.muli %24, %cst : tensor<1x1x4x1x1xi32> + %51 = tt.broadcast %49 : tensor<4x31x1x1x1xi32> -> tensor<4x31x4x1x1xi32> + %52 = tt.broadcast %50 : tensor<1x1x4x1x1xi32> -> tensor<4x31x4x1x1xi32> + %53 = arith.addi %51, %52 : tensor<4x31x4x1x1xi32> + %54 = tt.broadcast %53 : tensor<4x31x4x1x1xi32> -> tensor<4x31x4x256x1xi32> + %55 = tt.broadcast %33 : tensor<1x1x1x256x1xi32> -> tensor<4x31x4x256x1xi32> + %56 = arith.addi %54, %55 : tensor<4x31x4x256x1xi32> + %57 = tt.splat %arg1 : !tt.ptr -> tensor<4x31x4x256x1x!tt.ptr> + %58 = tt.addptr %57, %56 : tensor<4x31x4x256x1x!tt.ptr>, tensor<4x31x4x256x1xi32> + tt.store %58, %41 : tensor<4x31x4x256x1x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @triton_max_6d_dimm1 +// CHECK-DAG: %[[C95232:.*]] = arith.constant 95232 : index +// CHECK-DAG: %[[C3072:.*]] = arith.constant 3072 : index +// CHECK-DAG: %[[C768:.*]] = arith.constant 768 : index +// CHECK-DAG: %[[C3:.*]] = arith.constant 3 : index +// CHECK: scf.for %[[IV0:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: scf.for %[[IV1:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: scf.for %[[IV2:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: scf.for %[[IV3:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: %{{.*}} = arith.muli %[[IV0]], %[[C95232]] : index +// CHECK: %{{.*}} = arith.muli %[[IV1]], %[[C3072]] : index +// CHECK: %{{.*}} = arith.addi +// CHECK: %{{.*}} = arith.muli %[[IV2]], %[[C768]] : index +// CHECK: %{{.*}} = arith.addi +// CHECK: %{{.*}} = arith.muli %[[IV3]], %[[C3]] : index +// CHECK: %{{.*}} = arith.addi +// CHECK: %{{.*}} = arith.index_cast {{.*}} : index to i32 +// CHECK: func.call @triton_print{{[_0-9]*}}({{.*}}) : (i32) -> () +// CHECK: } +// CHECK: } +// CHECK: } +// CHECK: } + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @triton_max_6d_dimm1(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %true = arith.constant true + %cst = arith.constant dense<256> : tensor<1x1x4x1x1xi32> + %cst_0 = arith.constant dense<256> : tensor<1x31x1x1x1xi32> + %cst_1 = arith.constant dense<4> : tensor<1x31x1x1x1xi32> + %cst_2 = arith.constant dense<256> : tensor<4x1x1x1x1xi32> + %cst_3 = arith.constant dense<4> : tensor<4x1x1x1x1xi32> + %cst_4 = arith.constant dense<31> : tensor<4x1x1x1x1xi32> + %cst_5 = arith.constant dense<3> : tensor<4x31x4x256x1x1xi32> + %cst_6 = arith.constant dense<256> : tensor<1x1x4x1x1x1xi32> + %cst_7 = arith.constant dense<256> : tensor<1x31x1x1x1x1xi32> + %cst_8 = arith.constant dense<4> : tensor<1x31x1x1x1x1xi32> + %cst_9 = arith.constant dense<256> : tensor<4x1x1x1x1x1xi32> + %cst_10 = arith.constant dense<4> : tensor<4x1x1x1x1x1xi32> + %cst_11 = arith.constant dense<31> : tensor<4x1x1x1x1x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.make_range {end = 31 : i32, start = 0 : i32} : tensor<31xi32> + %2 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %3 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %4 = tt.expand_dims %3 {axis = 2 : i32} : tensor<4x1xi32> -> tensor<4x1x1xi32> + %5 = tt.expand_dims %4 {axis = 3 : i32} : tensor<4x1x1xi32> -> tensor<4x1x1x1xi32> + %6 = tt.expand_dims %5 {axis = 4 : i32} : tensor<4x1x1x1xi32> -> tensor<4x1x1x1x1xi32> + %7 = tt.expand_dims %6 {axis = 5 : i32} : tensor<4x1x1x1x1xi32> -> tensor<4x1x1x1x1x1xi32> + %8 = arith.muli %7, %cst_11 : tensor<4x1x1x1x1x1xi32> + %9 = arith.muli %8, %cst_10 : tensor<4x1x1x1x1x1xi32> + %10 = arith.muli %9, %cst_9 : tensor<4x1x1x1x1x1xi32> + %11 = tt.expand_dims %1 {axis = 0 : i32} : tensor<31xi32> -> tensor<1x31xi32> + %12 = tt.expand_dims %11 {axis = 2 : i32} : tensor<1x31xi32> -> tensor<1x31x1xi32> + %13 = tt.expand_dims %12 {axis = 3 : i32} : tensor<1x31x1xi32> -> tensor<1x31x1x1xi32> + %14 = tt.expand_dims %13 {axis = 4 : i32} : tensor<1x31x1x1xi32> -> tensor<1x31x1x1x1xi32> + %15 = tt.expand_dims %14 {axis = 5 : i32} : tensor<1x31x1x1x1xi32> -> tensor<1x31x1x1x1x1xi32> + %16 = arith.muli %15, %cst_8 : tensor<1x31x1x1x1x1xi32> + %17 = arith.muli %16, %cst_7 : tensor<1x31x1x1x1x1xi32> + %18 = tt.broadcast %10 : tensor<4x1x1x1x1x1xi32> -> tensor<4x31x1x1x1x1xi32> + %19 = tt.broadcast %17 : tensor<1x31x1x1x1x1xi32> -> tensor<4x31x1x1x1x1xi32> + %20 = arith.addi %18, %19 : tensor<4x31x1x1x1x1xi32> + %21 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %22 = tt.expand_dims %21 {axis = 1 : i32} : tensor<1x4xi32> -> tensor<1x1x4xi32> + %23 = tt.expand_dims %22 {axis = 3 : i32} : tensor<1x1x4xi32> -> tensor<1x1x4x1xi32> + %24 = tt.expand_dims %23 {axis = 4 : i32} : tensor<1x1x4x1xi32> -> tensor<1x1x4x1x1xi32> + %25 = tt.expand_dims %24 {axis = 5 : i32} : tensor<1x1x4x1x1xi32> -> tensor<1x1x4x1x1x1xi32> + %26 = arith.muli %25, %cst_6 : tensor<1x1x4x1x1x1xi32> + %27 = tt.broadcast %20 : tensor<4x31x1x1x1x1xi32> -> tensor<4x31x4x1x1x1xi32> + %28 = tt.broadcast %26 : tensor<1x1x4x1x1x1xi32> -> tensor<4x31x4x1x1x1xi32> + %29 = arith.addi %27, %28 : tensor<4x31x4x1x1x1xi32> + %30 = tt.expand_dims %2 {axis = 0 : i32} : tensor<256xi32> -> tensor<1x256xi32> + %31 = tt.expand_dims %30 {axis = 1 : i32} : tensor<1x256xi32> -> tensor<1x1x256xi32> + %32 = tt.expand_dims %31 {axis = 2 : i32} : tensor<1x1x256xi32> -> tensor<1x1x1x256xi32> + %33 = tt.expand_dims %32 {axis = 4 : i32} : tensor<1x1x1x256xi32> -> tensor<1x1x1x256x1xi32> + %34 = tt.expand_dims %33 {axis = 5 : i32} : tensor<1x1x1x256x1xi32> -> tensor<1x1x1x256x1x1xi32> + %35 = tt.broadcast %29 : tensor<4x31x4x1x1x1xi32> -> tensor<4x31x4x256x1x1xi32> + %36 = tt.broadcast %34 : tensor<1x1x1x256x1x1xi32> -> tensor<4x31x4x256x1x1xi32> + %37 = arith.addi %35, %36 : tensor<4x31x4x256x1x1xi32> + %38 = arith.muli %37, %cst_5 : tensor<4x31x4x256x1x1xi32> + tt.print ": " {hex = false, isSigned = array} : %38 : tensor<4x31x4x256x1x1xi32> + %39 = tt.splat %arg0 : !tt.ptr -> tensor<4x31x4x256x1x1x!tt.ptr> + %40 = tt.addptr %39, %38 : tensor<4x31x4x256x1x1x!tt.ptr>, tensor<4x31x4x256x1x1xi32> + %41 = tt.load %40 : tensor<4x31x4x256x1x1x!tt.ptr> + tt.assert %true, "Expecting input to be integer type" : i1 + %42 = "tt.reduce"(%41) <{axis = 5 : i32}> ({ + ^bb0(%arg2: i8, %arg3: i8): + %60 = arith.maxsi %arg2, %arg3 : i8 + tt.reduce.return %60 : i8 + }) : (tensor<4x31x4x256x1x1xi8>) -> tensor<4x31x4x256x1xi8> + %43 = arith.muli %6, %cst_4 : tensor<4x1x1x1x1xi32> + %44 = arith.muli %43, %cst_3 : tensor<4x1x1x1x1xi32> + %45 = arith.muli %44, %cst_2 : tensor<4x1x1x1x1xi32> + %46 = arith.muli %14, %cst_1 : tensor<1x31x1x1x1xi32> + %47 = arith.muli %46, %cst_0 : tensor<1x31x1x1x1xi32> + %48 = tt.broadcast %45 : tensor<4x1x1x1x1xi32> -> tensor<4x31x1x1x1xi32> + %49 = tt.broadcast %47 : tensor<1x31x1x1x1xi32> -> tensor<4x31x1x1x1xi32> + %50 = arith.addi %48, %49 : tensor<4x31x1x1x1xi32> + %51 = arith.muli %24, %cst : tensor<1x1x4x1x1xi32> + %52 = tt.broadcast %50 : tensor<4x31x1x1x1xi32> -> tensor<4x31x4x1x1xi32> + %53 = tt.broadcast %51 : tensor<1x1x4x1x1xi32> -> tensor<4x31x4x1x1xi32> + %54 = arith.addi %52, %53 : tensor<4x31x4x1x1xi32> + %55 = tt.broadcast %54 : tensor<4x31x4x1x1xi32> -> tensor<4x31x4x256x1xi32> + %56 = tt.broadcast %33 : tensor<1x1x1x256x1xi32> -> tensor<4x31x4x256x1xi32> + %57 = arith.addi %55, %56 : tensor<4x31x4x256x1xi32> + %58 = tt.splat %arg1 : !tt.ptr -> tensor<4x31x4x256x1x!tt.ptr> + %59 = tt.addptr %58, %57 : tensor<4x31x4x256x1x!tt.ptr>, tensor<4x31x4x256x1xi32> + tt.store %59, %42 : tensor<4x31x4x256x1x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @kernel_B1_modulo_offset +// CHECK: %[[C2:.*]] = arith.constant 2 : i32 +// CHECK: %[[C4:.*]] = arith.constant 4 : i32 +// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: %[[IV32:.*]] = arith.index_cast %[[IV]] : index to i32 +// CHECK: %[[DIV:.*]] = arith.divsi %[[IV32]], %[[C4]] : i32 +// CHECK: %[[MUL:.*]] = arith.muli %[[DIV]], %[[C2]] : i32 +// CHECK: %[[REM:.*]] = arith.remsi %[[IV32]], %{{.*}} : i32 +// CHECK: %{{.*}} = arith.addi %[[MUL]], %[[REM]] : i32 +// CHECK: func.call @triton_print{{[_0-9]*}}({{.*}}) : (i32) -> () +// CHECK: } +// CHECK-NOT: arith.remsi {{.*}} : tensor<16xi32> +// CHECK-NOT: arith.divsi {{.*}} : tensor<16xi32> + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @kernel_B1_modulo_offset(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<2> : tensor<16xi32> + %cst_0 = arith.constant dense<4> : tensor<16xi32> + %0 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %1 = arith.remsi %0, %cst_0 : tensor<16xi32> + %2 = arith.divsi %0, %cst_0 : tensor<16xi32> + %3 = arith.muli %2, %cst : tensor<16xi32> + %4 = arith.addi %3, %1 : tensor<16xi32> + tt.print " idx_B1: " {hex = false, isSigned = array} : %4 : tensor<16xi32> + %5 = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %6 = tt.addptr %5, %4 : tensor<16x!tt.ptr>, tensor<16xi32> + %7 = tt.load %6 : tensor<16x!tt.ptr> + %8 = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %9 = tt.addptr %8, %0 : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %9, %7 : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @kernel_B9_neg_stride +// CHECK: %[[C15:.*]] = arith.constant 15 : i32 +// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: %[[IV32:.*]] = arith.index_cast %[[IV]] : index to i32 +// CHECK: %{{.*}} = arith.subi %[[C15]], %[[IV32]] : i32 +// CHECK: func.call @triton_print{{[_0-9]*}}({{.*}}) : (i32) -> () +// CHECK: } +// CHECK-NOT: arith.subi {{.*}} : tensor<16xi32> + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @kernel_B9_neg_stride(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<15> : tensor<16xi32> + %0 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %1 = arith.subi %cst, %0 : tensor<16xi32> + %2 = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %3 = tt.addptr %2, %1 : tensor<16x!tt.ptr>, tensor<16xi32> + %4 = tt.load %3 : tensor<16x!tt.ptr> + tt.print " idx_B9: " {hex = false, isSigned = array} : %1 : tensor<16xi32> + %5 = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %6 = tt.addptr %5, %0 : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %6, %4 : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @kernel_B1_modulo_offset +// CHECK: %[[C4:.*]] = arith.constant 4 : i32 +// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: %[[IV32:.*]] = arith.index_cast %[[IV]] : index to i32 +// CHECK: %{{.*}} = arith.remsi %[[IV32]], %[[C4]] : i32 +// CHECK: func.call @triton_print{{[_0-9]*}}({{.*}}) : (i32) -> () +// CHECK: } +// CHECK-NOT: arith.remsi {{.*}} : tensor<16xi32> + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @kernel_B1_modulo_offset(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<4> : tensor<16xi32> + %0 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %1 = arith.remsi %0, %cst : tensor<16xi32> + tt.print " idx_B1: " {hex = false, isSigned = array} : %1 : tensor<16xi32> + %2 = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %3 = tt.addptr %2, %1 : tensor<16x!tt.ptr>, tensor<16xi32> + %4 = tt.load %3 : tensor<16x!tt.ptr> + %5 = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %6 = tt.addptr %5, %0 : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %6, %4 : tensor<16x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK-LABEL: func.func @triton_max_1d_dimm1 +// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: %[[IV32:.*]] = arith.index_cast %[[IV]] : index to i32 +// CHECK: func.call @triton_print{{[_0-9]*}}(%[[IV32]]) : (i32) -> () +// CHECK: } +// CHECK-NOT: linalg.generic {{.*}} attrs {{.*}} tt.from_make_range {{.*}} tensor<2xi32> + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @triton_max_1d_dimm1(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %true = arith.constant true + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + tt.print " idx: " {hex = false, isSigned = array} : %0 : tensor<2xi32> + %1 = tt.splat %arg0 : !tt.ptr -> tensor<2x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<2x!tt.ptr>, tensor<2xi32> + %3 = tt.load %2 : tensor<2x!tt.ptr> + tt.print " x: " {hex = false, isSigned = array} : %3 : tensor<2xi8> + tt.assert %true, "Expecting input to be integer type" : i1 + %4 = "tt.reduce"(%3) <{axis = 0 : i32}> ({ + ^bb0(%arg2: i8, %arg3: i8): + %8 = arith.maxui %arg2, %arg3 : i8 + tt.reduce.return %8 : i8 + }) : (tensor<2xi8>) -> i8 + tt.print " ret: " {hex = false, isSigned = array} : %4 : i8 + // Wrap scalar store in tensor form so PtrAnalysis can lower it. + %5 = tt.splat %arg1 : !tt.ptr -> tensor<1x!tt.ptr> + %6 = tt.splat %4 : i8 -> tensor<1xi8> + %7 = tt.make_range {end = 1 : i32, start = 0 : i32} : tensor<1xi32> + %8 = tt.addptr %5, %7 : tensor<1x!tt.ptr>, tensor<1xi32> + tt.store %8, %6 : tensor<1x!tt.ptr> + tt.return + } +} + +// ----- + +// CHECK: func.func private @triton_print{{[_0-9]*}}(i32){{.*}}prefix = " idx_B10: " +// CHECK-LABEL: func.func @kernel_B10_2d_dynamic_offset +// CHECK: scf.for %{{.*}} = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: scf.for %{{.*}} = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: func.call @triton_print{{[_0-9]*}}(%{{.*}}) : (i32) -> () +// CHECK: } +// CHECK: } + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @kernel_B10_2d_dynamic_offset(%arg0: !tt.ptr {tt.divisibility = 16 : i32},%arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<16> : tensor<4x1xi32> + %c8_i32 = arith.constant 8 : i32 + %c4_i32 = arith.constant 4 : i32 + %0 = tt.get_program_id x : i32 + %1 = tt.get_program_id y : i32 + %2 = arith.muli %0, %c4_i32 : i32 + %3 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %4 = tt.splat %2 : i32 -> tensor<4xi32> + %5 = arith.addi %4, %3 : tensor<4xi32> + %6 = arith.muli %1, %c8_i32 : i32 + %7 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %8 = tt.splat %6 : i32 -> tensor<8xi32> + %9 = arith.addi %8, %7 : tensor<8xi32> + %10 = tt.expand_dims %5 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %11 = arith.muli %10, %cst : tensor<4x1xi32> + %12 = tt.expand_dims %9 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %13 = tt.broadcast %11 : tensor<4x1xi32> -> tensor<4x8xi32> + %14 = tt.broadcast %12 : tensor<1x8xi32> -> tensor<4x8xi32> + %15 = arith.addi %13, %14 : tensor<4x8xi32> + %16 = tt.splat %arg0 : !tt.ptr -> tensor<4x8x!tt.ptr> + %17 = tt.addptr %16, %15 : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + %18 = tt.load %17 : tensor<4x8x!tt.ptr> + tt.print " idx_B10: " {hex = false, isSigned = array} : %15 : tensor<4x8xi32> + %19 = tt.splat %arg1 : !tt.ptr -> tensor<4x8x!tt.ptr> + %20 = tt.addptr %19, %15 : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + tt.store %20, %18 : tensor<4x8x!tt.ptr> + tt.return + } +} + +// ----- + + +// CHECK-LABEL: func.func @kernel_B11_2d_divmod_linearization +// CHECK-NOT: func.call @triton_print{{[_0-9]*}}({{.*}}) : (tensor<{{.*}}xi32>) +// CHECK: scf.for %{{.*}} = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK: scf.for %{{.*}} = %{{.*}} to %{{.*}} step %{{.*}} { +// CHECK-DAG: arith.divsi +// CHECK-DAG: arith.remsi +// CHECK: func.call @triton_print{{[_0-9]*}}(%{{.*}}) : (i32) -> () +// CHECK: } +// CHECK: } + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @kernel_B11_2d_divmod_linearization( + %arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<4> : tensor<4x8xi32> + %cst_0 = arith.constant dense<8> : tensor<4x8xi32> + %cst_1 = arith.constant dense<8> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %3 = tt.expand_dims %2 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %4 = arith.muli %1, %cst_1 : tensor<4x1xi32> + %5 = tt.broadcast %4 : tensor<4x1xi32> -> tensor<4x8xi32> + %6 = tt.broadcast %3 : tensor<1x8xi32> -> tensor<4x8xi32> + %7 = arith.addi %5, %6 : tensor<4x8xi32> + %8 = arith.divsi %7, %cst_0 : tensor<4x8xi32> + %9 = arith.remsi %7, %cst_0 : tensor<4x8xi32> + %10 = arith.muli %8, %cst : tensor<4x8xi32> + %11 = arith.addi %10, %9 : tensor<4x8xi32> + %12 = tt.splat %arg0 : !tt.ptr -> tensor<4x8x!tt.ptr> + %13 = tt.addptr %12, %11 : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + %14 = tt.load %13 : tensor<4x8x!tt.ptr> + tt.print " addr_B11: " {hex = false, isSigned = array} : %11 : tensor<4x8xi32> + %15 = tt.splat %arg1 : !tt.ptr -> tensor<4x8x!tt.ptr> + %16 = tt.addptr %15, %11 : tensor<4x8x!tt.ptr>, tensor<4x8xi32> + tt.store %16, %14 : tensor<4x8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_implicit_permute.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_implicit_permute.mlir new file mode 100644 index 0000000000..e7593291d6 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_implicit_permute.mlir @@ -0,0 +1,224 @@ +// RUN: triton-opt '--triton-to-linalg=global-kernel=False named-ops=True enable-nd2nz-on-vector=False enable-select-analysis=True compile-on-910-95=True' --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @addptr_implicit_perm_load_2d( +// CHECK: %[[IN_STRIDE0:[A-Za-z0-9_]+]] = arith.index_cast %arg6 : i32 to index +// CHECK: %[[IN_CAST0:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg2 to offset: [%[[IN_OFF0:[A-Za-z0-9_]+]]], sizes: [4, 64], strides: [1, %[[IN_STRIDE0]]] : memref to memref<4x64xf32, strided<[1, ?], offset: ?>> +// CHECK: memref.copy %{{.*}}, %{{.*}} : memref> to memref> +// CHECK: %[[TENSOR0:[A-Za-z0-9_]+]] = bufferization.to_tensor %{{.*}} restrict writable : memref<4x64xf32> +// CHECK: %[[OUT_STRIDE0:[A-Za-z0-9_]+]] = arith.index_cast %arg7 : i32 to index +// CHECK: %[[OUT_CAST0:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg3 to offset: [%[[OUT_OFF0:[A-Za-z0-9_]+]]], sizes: [4, 64], strides: [%[[OUT_STRIDE0]], 1] : memref to memref<4x64xf32, strided<[?, 1], offset: ?>> +// CHECK: %[[SLICE0:[A-Za-z0-9_]+]] = tensor.extract_slice %[[TENSOR0]][0, 0] [%[[ROWS0:[A-Za-z0-9_]+]], %[[COLS0:[A-Za-z0-9_]+]]] [1, 1] : tensor<4x64xf32> to tensor +// CHECK-NOT: linalg.transpose + +// CHECK-LABEL: func.func @addptr_implicit_perm_store_2d( +// CHECK: %[[IN_STRIDE1:[A-Za-z0-9_]+]] = arith.index_cast %arg6 : i32 to index +// CHECK: %[[IN_CAST1:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg2 to offset: [%[[IN_OFF1:[A-Za-z0-9_]+]]], sizes: [4, 64], strides: [%[[IN_STRIDE1]], 1] : memref to memref<4x64xf32, strided<[?, 1], offset: ?>> +// CHECK: memref.copy %{{.*}}, %{{.*}} : memref> to memref> +// CHECK: %[[TENSOR1:[A-Za-z0-9_]+]] = bufferization.to_tensor %{{.*}} restrict writable : memref<4x64xf32> +// CHECK: %[[OUT_STRIDE1:[A-Za-z0-9_]+]] = arith.index_cast %arg7 : i32 to index +// CHECK: %[[OUT_CAST1:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg3 to offset: [%[[OUT_OFF1:[A-Za-z0-9_]+]]], sizes: [64, 4], strides: [%[[OUT_STRIDE1]], 1] : memref to memref<64x4xf32, strided<[?, 1], offset: ?>> +// CHECK: %[[EMPTY1:[A-Za-z0-9_]+]] = tensor.empty() : tensor<64x4xf32> +// CHECK-NEXT: %[[TRANS1:[A-Za-z0-9_]+]] = linalg.transpose ins(%[[TENSOR1]] : tensor<4x64xf32>) outs(%[[EMPTY1]] : tensor<64x4xf32>) permutation = [1, 0] +// CHECK: %[[SLICE1:[A-Za-z0-9_]+]] = tensor.extract_slice %[[TRANS1]][0, 0] [%[[COLS1:[A-Za-z0-9_]+]], %[[ROWS1:[A-Za-z0-9_]+]]] [1, 1] : tensor<64x4xf32> to tensor + +// CHECK-LABEL: func.func @make_tensor_ptr_implicit_perm_load_2d( +// CHECK: %[[IN_STRIDE2:[A-Za-z0-9_]+]] = arith.index_cast %arg6 : i32 to index +// CHECK: %[[IN_CAST2:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg2 to offset: [%[[IN_OFF2:[A-Za-z0-9_]+]]], sizes: [4, 64], strides: [1, %[[IN_STRIDE2]]] : memref to memref<4x64xf32, strided<[1, ?], offset: ?>> +// CHECK: memref.copy %[[IN_CAST2]], %{{.*}} : memref<4x64xf32, strided<[1, ?], offset: ?>> to memref<4x64xf32> +// CHECK: %[[TENSOR2:[A-Za-z0-9_]+]] = bufferization.to_tensor %{{.*}} restrict writable : memref<4x64xf32> +// CHECK: %[[OUT_STRIDE2:[A-Za-z0-9_]+]] = arith.index_cast %arg7 : i32 to index +// CHECK: %[[OUT_CAST2:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg3 to offset: [%[[OUT_OFF2:[A-Za-z0-9_]+]]], sizes: [4, 64], strides: [%[[OUT_STRIDE2]], 1] : memref to memref<4x64xf32, strided<[?, 1], offset: ?>> +// CHECK: %[[SLICE2:[A-Za-z0-9_]+]] = tensor.extract_slice %[[TENSOR2]][0, 0] [%[[ROWS2:[A-Za-z0-9_]+]], %[[COLS2:[A-Za-z0-9_]+]]] [1, 1] : tensor<4x64xf32> to tensor +// CHECK-NOT: linalg.transpose + +// CHECK-LABEL: func.func @make_tensor_ptr_implicit_perm_store_2d( +// CHECK: %[[IN_STRIDE3:[A-Za-z0-9_]+]] = arith.index_cast %arg6 : i32 to index +// CHECK: %[[IN_CAST3:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg2 to offset: [%[[IN_OFF3:[A-Za-z0-9_]+]]], sizes: [4, 64], strides: [%[[IN_STRIDE3]], 1] : memref to memref<4x64xf32, strided<[?, 1], offset: ?>> +// CHECK: memref.copy %{{.*}}, %{{.*}} : memref> to memref> +// CHECK: %[[TENSOR3:[A-Za-z0-9_]+]] = bufferization.to_tensor %{{.*}} restrict writable : memref<4x64xf32> +// CHECK: %[[OUT_STRIDE3:[A-Za-z0-9_]+]] = arith.index_cast %arg7 : i32 to index +// CHECK: %[[OUT_CAST3:[A-Za-z0-9_]+]] = memref.reinterpret_cast %arg3 to offset: [%[[OUT_OFF3:[A-Za-z0-9_]+]]], sizes: [64, 4], strides: [%[[OUT_STRIDE3]], 1] : memref to memref<64x4xf32, strided<[?, 1], offset: ?>> +// CHECK: %[[EMPTY3:[A-Za-z0-9_]+]] = tensor.empty() : tensor<64x4xf32> +// CHECK-NEXT: %[[TRANS3:[A-Za-z0-9_]+]] = linalg.transpose ins(%[[TENSOR3]] : tensor<4x64xf32>) outs(%[[EMPTY3]] : tensor<64x4xf32>) permutation = [1, 0] +// CHECK: %[[SLICE3:[A-Za-z0-9_]+]] = tensor.extract_slice %[[TRANS3]]{{.*}} : tensor<64x4xf32> to tensor + +// addptr load implicit permutation +module { + tt.func public @addptr_implicit_perm_load_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: i32 {tt.divisibility = 16 : i32}, %arg3: i32 {tt.divisibility = 16 : i32}, %arg4: i32 {tt.divisibility = 16 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<4x64xf32> + %c64_i32 = arith.constant 64 : i32 + %c4_i32 = arith.constant 4 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c4_i32 : i32 + %2 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %3 = tt.expand_dims %2 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %4 = tt.splat %1 : i32 -> tensor<4x1xi32> + %5 = arith.addi %4, %3 : tensor<4x1xi32> + %6 = tt.get_program_id y : i32 + %7 = arith.muli %6, %c64_i32 : i32 + %8 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %9 = tt.expand_dims %8 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %10 = tt.splat %7 : i32 -> tensor<1x64xi32> + %11 = arith.addi %10, %9 : tensor<1x64xi32> + %12 = tt.splat %arg3 : i32 -> tensor<4x1xi32> + %13 = arith.cmpi slt, %5, %12 : tensor<4x1xi32> + %14 = tt.splat %arg2 : i32 -> tensor<1x64xi32> + %15 = arith.cmpi slt, %11, %14 : tensor<1x64xi32> + %16 = tt.broadcast %13 : tensor<4x1xi1> -> tensor<4x64xi1> + %17 = tt.broadcast %15 : tensor<1x64xi1> -> tensor<4x64xi1> + %18 = arith.andi %16, %17 : tensor<4x64xi1> + %19 = tt.splat %arg4 : i32 -> tensor<1x64xi32> + %20 = arith.muli %11, %19 : tensor<1x64xi32> + %21 = tt.broadcast %5 : tensor<4x1xi32> -> tensor<4x64xi32> + %22 = tt.broadcast %20 : tensor<1x64xi32> -> tensor<4x64xi32> + %23 = arith.addi %21, %22 : tensor<4x64xi32> + %24 = tt.splat %arg0 : !tt.ptr -> tensor<4x64x!tt.ptr> + %25 = tt.addptr %24, %23 : tensor<4x64x!tt.ptr>, tensor<4x64xi32> + %26 = tt.load %25, %18, %cst : tensor<4x64x!tt.ptr> + %27 = tt.splat %arg5 : i32 -> tensor<4x1xi32> + %28 = arith.muli %5, %27 : tensor<4x1xi32> + %29 = tt.broadcast %28 : tensor<4x1xi32> -> tensor<4x64xi32> + %30 = tt.broadcast %11 : tensor<1x64xi32> -> tensor<4x64xi32> + %31 = arith.addi %29, %30 : tensor<4x64xi32> + %32 = tt.splat %arg1 : !tt.ptr -> tensor<4x64x!tt.ptr> + %33 = tt.addptr %32, %31 : tensor<4x64x!tt.ptr>, tensor<4x64xi32> + tt.store %33, %26, %18 : tensor<4x64x!tt.ptr> + tt.return + } +} + +// ----- + +// addptr store implicit permutation +module { + tt.func public @addptr_implicit_perm_store_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: i32 {tt.divisibility = 16 : i32}, %arg3: i32 {tt.divisibility = 16 : i32}, %arg4: i32 {tt.divisibility = 16 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<4x64xf32> + %c64_i32 = arith.constant 64 : i32 + %c4_i32 = arith.constant 4 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c4_i32 : i32 + %2 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %3 = tt.expand_dims %2 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %4 = tt.splat %1 : i32 -> tensor<4x1xi32> + %5 = arith.addi %4, %3 : tensor<4x1xi32> + %6 = tt.get_program_id y : i32 + %7 = arith.muli %6, %c64_i32 : i32 + %8 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %9 = tt.expand_dims %8 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %10 = tt.splat %7 : i32 -> tensor<1x64xi32> + %11 = arith.addi %10, %9 : tensor<1x64xi32> + %12 = tt.splat %arg3 : i32 -> tensor<4x1xi32> + %13 = arith.cmpi slt, %5, %12 : tensor<4x1xi32> + %14 = tt.splat %arg2 : i32 -> tensor<1x64xi32> + %15 = arith.cmpi slt, %11, %14 : tensor<1x64xi32> + %16 = tt.broadcast %13 : tensor<4x1xi1> -> tensor<4x64xi1> + %17 = tt.broadcast %15 : tensor<1x64xi1> -> tensor<4x64xi1> + %18 = arith.andi %16, %17 : tensor<4x64xi1> + %19 = tt.splat %arg4 : i32 -> tensor<4x1xi32> + %20 = arith.muli %5, %19 : tensor<4x1xi32> + %21 = tt.broadcast %20 : tensor<4x1xi32> -> tensor<4x64xi32> + %22 = tt.broadcast %11 : tensor<1x64xi32> -> tensor<4x64xi32> + %23 = arith.addi %21, %22 : tensor<4x64xi32> + %24 = tt.splat %arg0 : !tt.ptr -> tensor<4x64x!tt.ptr> + %25 = tt.addptr %24, %23 : tensor<4x64x!tt.ptr>, tensor<4x64xi32> + %26 = tt.load %25, %18, %cst : tensor<4x64x!tt.ptr> + %27 = tt.splat %arg5 : i32 -> tensor<1x64xi32> + %28 = arith.muli %11, %27 : tensor<1x64xi32> + %29 = tt.broadcast %5 : tensor<4x1xi32> -> tensor<4x64xi32> + %30 = tt.broadcast %28 : tensor<1x64xi32> -> tensor<4x64xi32> + %31 = arith.addi %29, %30 : tensor<4x64xi32> + %32 = tt.splat %arg1 : !tt.ptr -> tensor<4x64x!tt.ptr> + %33 = tt.addptr %32, %31 : tensor<4x64x!tt.ptr>, tensor<4x64xi32> + tt.store %33, %26, %18 : tensor<4x64x!tt.ptr> + tt.return + } +} + +// ----- + +// make_tensor_ptr load implicit permutation +module { + tt.func public @make_tensor_ptr_implicit_perm_load_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: i32 {tt.divisibility = 16 : i32}, %arg3: i32 {tt.divisibility = 16 : i32}, %arg4: i32 {tt.divisibility = 16 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c1_i64 = arith.constant 1 : i64 + %c4_i32 = arith.constant 4 : i32 + %c64_i32 = arith.constant 64 : i32 + %0 = tt.get_program_id y : i32 + %1 = arith.muli %0, %c64_i32 : i32 + %2 = tt.get_program_id x : i32 + %3 = arith.muli %2, %c4_i32 : i32 + %4 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %6 = tt.splat %1 : i32 -> tensor<1x64xi32> + %7 = arith.addi %6, %5 : tensor<1x64xi32> + %8 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %9 = tt.expand_dims %8 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %10 = tt.splat %3 : i32 -> tensor<4x1xi32> + %11 = arith.addi %10, %9 : tensor<4x1xi32> + %12 = tt.splat %arg3 : i32 -> tensor<4x1xi32> + %13 = arith.cmpi slt, %11, %12 : tensor<4x1xi32> + %14 = tt.splat %arg2 : i32 -> tensor<1x64xi32> + %15 = arith.cmpi slt, %7, %14 : tensor<1x64xi32> + %16 = tt.broadcast %13 : tensor<4x1xi1> -> tensor<4x64xi1> + %17 = tt.broadcast %15 : tensor<1x64xi1> -> tensor<4x64xi1> + %18 = arith.andi %16, %17 : tensor<4x64xi1> + %19 = arith.extsi %arg3 : i32 to i64 + %20 = arith.extsi %arg2 : i32 to i64 + %21 = arith.extsi %arg4 : i32 to i64 + %22 = tt.make_tensor_ptr %arg0, [%19, %20], [%c1_i64, %21], [%3, %1] {order = array} : > + %23 = tt.load %22 : !tt.ptr> + %24 = tt.splat %arg5 : i32 -> tensor<4x1xi32> + %25 = arith.muli %11, %24 : tensor<4x1xi32> + %26 = tt.broadcast %25 : tensor<4x1xi32> -> tensor<4x64xi32> + %27 = tt.broadcast %7 : tensor<1x64xi32> -> tensor<4x64xi32> + %28 = arith.addi %26, %27 : tensor<4x64xi32> + %29 = tt.splat %arg1 : !tt.ptr -> tensor<4x64x!tt.ptr> + %30 = tt.addptr %29, %28 : tensor<4x64x!tt.ptr>, tensor<4x64xi32> + tt.store %30, %23, %18 : tensor<4x64x!tt.ptr> + tt.return + } +} + +// ----- + +// make_tensor_ptr store implicit permutation +module { + tt.func public @make_tensor_ptr_implicit_perm_store_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: i32 {tt.divisibility = 16 : i32}, %arg3: i32 {tt.divisibility = 16 : i32}, %arg4: i32 {tt.divisibility = 16 : i32}, %arg5: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c1_i64 = arith.constant 1 : i64 + %cst = arith.constant dense<0.000000e+00> : tensor<4x64xf32> + %c4_i32 = arith.constant 4 : i32 + %c64_i32 = arith.constant 64 : i32 + %0 = tt.get_program_id y : i32 + %1 = arith.muli %0, %c64_i32 : i32 + %2 = tt.get_program_id x : i32 + %3 = arith.muli %2, %c4_i32 : i32 + %4 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %6 = tt.splat %1 : i32 -> tensor<1x64xi32> + %7 = arith.addi %6, %5 : tensor<1x64xi32> + %8 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %9 = tt.expand_dims %8 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %10 = tt.splat %3 : i32 -> tensor<4x1xi32> + %11 = arith.addi %10, %9 : tensor<4x1xi32> + %12 = tt.splat %arg3 : i32 -> tensor<4x1xi32> + %13 = arith.cmpi slt, %11, %12 : tensor<4x1xi32> + %14 = tt.splat %arg2 : i32 -> tensor<1x64xi32> + %15 = arith.cmpi slt, %7, %14 : tensor<1x64xi32> + %16 = tt.broadcast %13 : tensor<4x1xi1> -> tensor<4x64xi1> + %17 = tt.broadcast %15 : tensor<1x64xi1> -> tensor<4x64xi1> + %18 = arith.andi %16, %17 : tensor<4x64xi1> + %19 = tt.splat %arg4 : i32 -> tensor<4x1xi32> + %20 = arith.muli %11, %19 : tensor<4x1xi32> + %21 = tt.broadcast %20 : tensor<4x1xi32> -> tensor<4x64xi32> + %22 = tt.broadcast %7 : tensor<1x64xi32> -> tensor<4x64xi32> + %23 = arith.addi %21, %22 : tensor<4x64xi32> + %24 = tt.splat %arg0 : !tt.ptr -> tensor<4x64x!tt.ptr> + %25 = tt.addptr %24, %23 : tensor<4x64x!tt.ptr>, tensor<4x64xi32> + %26 = tt.load %25, %18, %cst : tensor<4x64x!tt.ptr> + %27 = arith.extsi %arg3 : i32 to i64 + %28 = arith.extsi %arg2 : i32 to i64 + %29 = arith.extsi %arg5 : i32 to i64 + %30 = tt.make_tensor_ptr %arg1, [%27, %28], [%c1_i64, %29], [%3, %1] {order = array} : > + tt.store %30, %26 {boundaryCheck = array} : !tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_inttoptr_offset_reset.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_inttoptr_offset_reset.mlir new file mode 100644 index 0000000000..e1a752fe90 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_inttoptr_offset_reset.mlir @@ -0,0 +1,70 @@ +// RUN: triton-opt "--triton-to-linalg=global-kernel=false named-ops=True" --split-input-file %s | FileCheck %s + +// Test the positive static offset representation selected when a scalar +// pointer reaches the layout-sensitive memref.copy boundary. +// +// When a scalar tt.addptr adds a constant offset to a tt.int_to_ptr result, +// the conversion retains that offset in the memref view and expands the raw +// pointer capacity to cover both the leading offset and the eight-element +// extent. The copy must consume that exact offset-bearing view. + +// CHECK-LABEL: func.func @test_inttoptr_offset_reset +// CHECK: %[[EXTENT:.*]] = arith.constant 8 : index +// CHECK: %[[LEADING_OFFSET:.*]] = arith.constant 4 : index +// CHECK: %[[CAPACITY:.*]] = arith.addi %[[EXTENT]], %[[LEADING_OFFSET]] : index +// CHECK: %[[POINTER:.*]] = hivm.hir.pointer_cast(%{{.*}}) [%[[CAPACITY]]] : memref +// CHECK: %[[OFFSET_VIEW:.*]] = memref.reinterpret_cast %[[POINTER]] to offset: [4], sizes: [8], strides: [1] +// CHECK-SAME: to memref<8xi32, strided<[1], offset: 4>> +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<8xi32> +// CHECK: memref.copy %[[OFFSET_VIEW]], %[[ALLOC]] : memref<8xi32, strided<[1], offset: 4>> to memref<8xi32> + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @test_inttoptr_offset_reset(%addr_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %out_ptr: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c4_i32 = arith.constant 4 : i32 + // Load a raw i64 address from memory, then convert to pointer. + // This creates a hivm.pointer_cast during conversion. + %raw_addr = tt.load %addr_ptr : !tt.ptr + %ptr = tt.int_to_ptr %raw_addr : i64 -> !tt.ptr + // Scalar addptr with a constant offset — the offset ends up in the + // ReinterpretCastOp result type's strided layout. + %offset_ptr = tt.addptr %ptr, %c4_i32 : !tt.ptr, i32 + // Splat to tensor and load via indirect addressing. + %ptrs = tt.splat %offset_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %offsets = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %load_ptrs = tt.addptr %ptrs, %offsets : tensor<8x!tt.ptr>, tensor<8xi32> + %data = tt.load %load_ptrs : tensor<8x!tt.ptr> + // Store result. + %out_ptrs = tt.splat %out_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %out_addptr = tt.addptr %out_ptrs, %offsets : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %out_addptr, %data : tensor<8x!tt.ptr> + tt.return + } +} + +// ----- + +// Dynamic offset variant: the scalar tt.addptr uses a runtime value as +// offset, so the original ReinterpretCastOp has a dynamic offset (?) +// in its result type. After the optimization bakes the offset into the +// base address, the new ReinterpretCastOp still have a dynamic offset +// in the result type. + +// CHECK-LABEL: func.func @test_inttoptr_dyn_offset_reset +// CHECK: memref.reinterpret_cast {{.*}} strided<[1], offset: ?> + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @test_inttoptr_dyn_offset_reset(%addr_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %out_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dyn_offset: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %raw_addr = tt.load %addr_ptr : !tt.ptr + %ptr = tt.int_to_ptr %raw_addr : i64 -> !tt.ptr + // Scalar addptr with a dynamic (runtime) offset. + %offset_ptr = tt.addptr %ptr, %dyn_offset : !tt.ptr, i32 + %ptrs = tt.splat %offset_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %offsets = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %load_ptrs = tt.addptr %ptrs, %offsets : tensor<8x!tt.ptr>, tensor<8xi32> + %data = tt.load %load_ptrs : tensor<8x!tt.ptr> + %out_ptrs = tt.splat %out_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %out_addptr = tt.addptr %out_ptrs, %offsets : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %out_addptr, %data : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_load_out_of_bound.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_load_out_of_bound.mlir new file mode 100644 index 0000000000..05b526ed98 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_load_out_of_bound.mlir @@ -0,0 +1,107 @@ +// RUN: triton-opt "--triton-to-linalg=global-kernel=false named-ops=True" --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @_triton_mrope_forward +// CHECK: %[[CST:.*]] = arith.constant 0.000000e+00 : f32 +// CHECK: %[[ALLOC_5:.*]] = memref.alloc() : memref<64xf32> +// CHECK: linalg.fill ins(%[[CST]] : f32) outs(%[[ALLOC_5]] : memref<64xf32>) +// CHECK: bufferization.to_tensor %[[ALLOC_5]] restrict writable : memref<64xf32> to tensor<64xf32> + +module attributes {hacc.target = #hacc.target<"Ascend910_9382">} { + tt.func public @_triton_mrope_forward(%q_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %k_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %cos: !tt.ptr {tt.divisibility = 16 : i32}, %sin: !tt.ptr {tt.divisibility = 16 : i32}, %num_tokens: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<16x64xf32> + %cst_0 = arith.constant dense<0.000000e+00> : tensor<64xf32> + %cst_1 = arith.constant dense<64> : tensor<16x64xi32> + %cst_2 = arith.constant dense<64> : tensor<1x64xi32> + %cst_3 = arith.constant dense<16> : tensor<16x1xi32> + %cst_4 = arith.constant dense<128> : tensor<16x1xi32> + %cst_5 = arith.constant dense<64> : tensor<64xi32> + %cst_6 = arith.constant dense<32> : tensor<64xi32> + %c64_i32 = arith.constant 64 : i32 + %c2048_i32 = arith.constant 2048 : i32 + %pid = tt.get_program_id x : i32 + %q_ptr_7 = arith.muli %pid, %c2048_i32 : i32 + %q_ptr_8 = tt.addptr %q_ptr, %q_ptr_7 : !tt.ptr, i32 + %k_ptr_9 = tt.addptr %k_ptr, %q_ptr_7 : !tt.ptr, i32 + %t_cos = arith.muli %pid, %c64_i32 : i32 + %t_cos_10 = tt.addptr %cos, %t_cos : !tt.ptr, i32 + %h_cos = arith.muli %num_tokens, %c64_i32 : i32 + %h_cos_11 = tt.addptr %t_cos_10, %h_cos : !tt.ptr, i32 + %w_cos = tt.addptr %h_cos_11, %h_cos : !tt.ptr, i32 + %t_sin = tt.addptr %sin, %t_cos : !tt.ptr, i32 + %h_sin = tt.addptr %t_sin, %h_cos : !tt.ptr, i32 + %w_sin = tt.addptr %h_sin, %h_cos : !tt.ptr, i32 + %cos_offsets = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %t_mask = arith.cmpi slt, %cos_offsets, %cst_6 : tensor<64xi32> + %h_mask = arith.cmpi sge, %cos_offsets, %cst_6 : tensor<64xi32> + %h_mask_12 = arith.cmpi slt, %cos_offsets, %cst_5 : tensor<64xi32> + %h_mask_13 = arith.andi %h_mask, %h_mask_12 : tensor<64xi1> + %w_mask = arith.cmpi sge, %cos_offsets, %cst_5 : tensor<64xi32> + %w_mask_14 = arith.andi %w_mask, %h_mask_12 : tensor<64xi1> + %t_cos_row = tt.splat %t_cos_10 : !tt.ptr -> tensor<64x!tt.ptr> + %t_cos_row_15 = tt.addptr %t_cos_row, %cos_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %t_cos_row_16 = tt.load %t_cos_row_15, %t_mask, %cst_0 : tensor<64x!tt.ptr> + %h_cos_row = tt.splat %h_cos_11 : !tt.ptr -> tensor<64x!tt.ptr> + %h_cos_row_17 = tt.addptr %h_cos_row, %cos_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %h_cos_row_18 = tt.load %h_cos_row_17, %h_mask_13, %cst_0 : tensor<64x!tt.ptr> + %w_cos_row = tt.splat %w_cos : !tt.ptr -> tensor<64x!tt.ptr> + %w_cos_row_19 = tt.addptr %w_cos_row, %cos_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %w_cos_row_20 = tt.load %w_cos_row_19, %w_mask_14, %cst_0 : tensor<64x!tt.ptr> + %t_sin_row = tt.splat %t_sin : !tt.ptr -> tensor<64x!tt.ptr> + %t_sin_row_21 = tt.addptr %t_sin_row, %cos_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %t_sin_row_22 = tt.load %t_sin_row_21, %t_mask, %cst_0 : tensor<64x!tt.ptr> + %h_sin_row = tt.splat %h_sin : !tt.ptr -> tensor<64x!tt.ptr> + %h_sin_row_23 = tt.addptr %h_sin_row, %cos_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %h_sin_row_24 = tt.load %h_sin_row_23, %h_mask_13, %cst_0 : tensor<64x!tt.ptr> + %w_sin_row = tt.splat %w_sin : !tt.ptr -> tensor<64x!tt.ptr> + %w_sin_row_25 = tt.addptr %w_sin_row, %cos_offsets : tensor<64x!tt.ptr>, tensor<64xi32> + %w_sin_row_26 = tt.load %w_sin_row_25, %w_mask_14, %cst_0 : tensor<64x!tt.ptr> + %cos_row = arith.addf %t_cos_row_16, %h_cos_row_18 : tensor<64xf32> + %cos_row_27 = arith.addf %cos_row, %w_cos_row_20 : tensor<64xf32> + %sin_row = arith.addf %t_sin_row_22, %h_sin_row_24 : tensor<64xf32> + %sin_row_28 = arith.addf %sin_row, %w_sin_row_26 : tensor<64xf32> + %first_half_q_offsets = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %first_half_q_offsets_29 = tt.expand_dims %first_half_q_offsets {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %first_half_q_offsets_30 = arith.muli %first_half_q_offsets_29, %cst_4 : tensor<16x1xi32> + %first_half_q_offsets_31 = tt.expand_dims %cos_offsets {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %first_half_q_offsets_32 = tt.broadcast %first_half_q_offsets_30 : tensor<16x1xi32> -> tensor<16x64xi32> + %first_half_q_offsets_33 = tt.broadcast %first_half_q_offsets_31 : tensor<1x64xi32> -> tensor<16x64xi32> + %first_half_q_offsets_34 = arith.addi %first_half_q_offsets_32, %first_half_q_offsets_33 : tensor<16x64xi32> + %first_q_mask = arith.cmpi slt, %first_half_q_offsets_29, %cst_3 : tensor<16x1xi32> + %first_q_mask_35 = arith.cmpi slt, %first_half_q_offsets_31, %cst_2 : tensor<1x64xi32> + %first_q_mask_36 = tt.broadcast %first_q_mask : tensor<16x1xi1> -> tensor<16x64xi1> + %first_q_mask_37 = tt.broadcast %first_q_mask_35 : tensor<1x64xi1> -> tensor<16x64xi1> + %first_q_mask_38 = arith.andi %first_q_mask_36, %first_q_mask_37 : tensor<16x64xi1> + %q_tile_1 = tt.splat %q_ptr_8 : !tt.ptr -> tensor<16x64x!tt.ptr> + %q_tile_1_39 = tt.addptr %q_tile_1, %first_half_q_offsets_34 : tensor<16x64x!tt.ptr>, tensor<16x64xi32> + %q_tile_1_40 = tt.load %q_tile_1_39, %first_q_mask_38, %cst : tensor<16x64x!tt.ptr> + %k_tile_1 = tt.splat %k_ptr_9 : !tt.ptr -> tensor<16x64x!tt.ptr> + %k_tile_1_41 = tt.addptr %k_tile_1, %first_half_q_offsets_34 : tensor<16x64x!tt.ptr>, tensor<16x64xi32> + %k_tile_1_42 = tt.load %k_tile_1_41, %first_q_mask_38, %cst : tensor<16x64x!tt.ptr> + %second_half_q_offsets = arith.addi %first_half_q_offsets_34, %cst_1 : tensor<16x64xi32> + %q_tile_2 = tt.addptr %q_tile_1, %second_half_q_offsets : tensor<16x64x!tt.ptr>, tensor<16x64xi32> + %q_tile_2_43 = tt.load %q_tile_2, %first_q_mask_38, %cst : tensor<16x64x!tt.ptr> + %k_tile_2 = tt.addptr %k_tile_1, %second_half_q_offsets : tensor<16x64x!tt.ptr>, tensor<16x64xi32> + %k_tile_2_44 = tt.load %k_tile_2, %first_q_mask_38, %cst : tensor<16x64x!tt.ptr> + %new_q_tile_1 = tt.expand_dims %cos_row_27 {axis = 0 : i32} : tensor<64xf32> -> tensor<1x64xf32> + %new_q_tile_1_45 = tt.broadcast %new_q_tile_1 : tensor<1x64xf32> -> tensor<16x64xf32> + %new_q_tile_1_46 = arith.mulf %q_tile_1_40, %new_q_tile_1_45 : tensor<16x64xf32> + %new_q_tile_1_47 = tt.expand_dims %sin_row_28 {axis = 0 : i32} : tensor<64xf32> -> tensor<1x64xf32> + %new_q_tile_1_48 = tt.broadcast %new_q_tile_1_47 : tensor<1x64xf32> -> tensor<16x64xf32> + %new_q_tile_1_49 = arith.mulf %q_tile_2_43, %new_q_tile_1_48 : tensor<16x64xf32> + %new_q_tile_1_50 = arith.subf %new_q_tile_1_46, %new_q_tile_1_49 : tensor<16x64xf32> + tt.store %q_tile_1_39, %new_q_tile_1_50, %first_q_mask_38 : tensor<16x64x!tt.ptr> + %new_q_tile_2 = arith.mulf %q_tile_2_43, %new_q_tile_1_45 : tensor<16x64xf32> + %new_q_tile_2_51 = arith.mulf %q_tile_1_40, %new_q_tile_1_48 : tensor<16x64xf32> + %new_q_tile_2_52 = arith.addf %new_q_tile_2, %new_q_tile_2_51 : tensor<16x64xf32> + tt.store %q_tile_2, %new_q_tile_2_52, %first_q_mask_38 : tensor<16x64x!tt.ptr> + %new_k_tile_1 = arith.mulf %k_tile_1_42, %new_q_tile_1_45 : tensor<16x64xf32> + %new_k_tile_1_53 = arith.mulf %k_tile_2_44, %new_q_tile_1_48 : tensor<16x64xf32> + %new_k_tile_1_54 = arith.subf %new_k_tile_1, %new_k_tile_1_53 : tensor<16x64xf32> + tt.store %k_tile_1_41, %new_k_tile_1_54, %first_q_mask_38 : tensor<16x64x!tt.ptr> + %new_k_tile_2 = arith.mulf %k_tile_2_44, %new_q_tile_1_45 : tensor<16x64xf32> + %new_k_tile_2_55 = arith.mulf %k_tile_1_42, %new_q_tile_1_48 : tensor<16x64xf32> + %new_k_tile_2_56 = arith.addf %new_k_tile_2, %new_k_tile_2_55 : tensor<16x64xf32> + tt.store %k_tile_2, %new_k_tile_2_56, %first_q_mask_38 : tensor<16x64x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_load_with_other.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_load_with_other.mlir new file mode 100644 index 0000000000..ca52e22123 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_load_with_other.mlir @@ -0,0 +1,173 @@ +// RUN: triton-opt --discrete-mask-access-conversion "--triton-to-linalg=global-kernel=false named-ops=True" --split-input-file %s | FileCheck %s + +// Case 1: other = uitofp(mask) -> select(mask, loaded, zeros) +// CHECK-LABEL: func.func @load_other_mask +// CHECK: %[[CST:.*]] = arith.constant 0.000000e+00 : f32 +// CHECK: %[[MASK:.*]] = arith.cmpi slt +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<128xf32> +// CHECK: memref.copy +// CHECK: %[[LOADED:.*]] = bufferization.to_tensor %[[ALLOC]] +// CHECK: linalg.fill ins(%[[CST]] : f32) +// CHECK: arith.select %[[MASK]], %[[LOADED]], {{.*}} +// CHECK-NOT: tensor.insert_slice + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @load_other_mask(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: i32) attributes {noinline = false} { + %c128_i32 = arith.constant 128 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c128_i32 : i32 + %2 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %3 = tt.splat %1 : i32 -> tensor<128xi32> + %4 = arith.addi %3, %2 : tensor<128xi32> + %5 = tt.splat %arg2 : i32 -> tensor<128xi32> + %6 = arith.cmpi slt, %4, %5 : tensor<128xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<128x!tt.ptr> + %8 = tt.addptr %7, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %9 = arith.uitofp %6 : tensor<128xi1> to tensor<128xf32> + %10 = tt.load %8, %6, %9 : tensor<128x!tt.ptr> + %11 = tt.splat %arg1 : !tt.ptr -> tensor<128x!tt.ptr> + %12 = tt.addptr %11, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + tt.store %12, %10, %6 : tensor<128x!tt.ptr> + tt.return + } +} + +// ----- + +// Case 1b: other = extui(mask) for int8 — same select with zeros. +// CHECK-LABEL: func.func @load_other_mask_i8 +// CHECK: %[[CST:.*]] = arith.constant 0 : i8 +// CHECK: %[[MASK:.*]] = arith.cmpi slt +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<128xi8> +// CHECK: memref.copy +// CHECK: %[[LOADED:.*]] = bufferization.to_tensor %[[ALLOC]] +// CHECK: linalg.fill ins(%[[CST]] : i8) +// CHECK: arith.select %[[MASK]], %[[LOADED]], {{.*}} +// CHECK-NOT: tensor.insert_slice + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @load_other_mask_i8(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: i32) attributes {noinline = false} { + %c128_i32 = arith.constant 128 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c128_i32 : i32 + %2 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %3 = tt.splat %1 : i32 -> tensor<128xi32> + %4 = arith.addi %3, %2 : tensor<128xi32> + %5 = tt.splat %arg2 : i32 -> tensor<128xi32> + %6 = arith.cmpi slt, %4, %5 : tensor<128xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<128x!tt.ptr> + %8 = tt.addptr %7, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %9 = arith.extui %6 : tensor<128xi1> to tensor<128xi8> + %10 = tt.load %8, %6, %9 : tensor<128x!tt.ptr> + %11 = tt.splat %arg1 : !tt.ptr -> tensor<128x!tt.ptr> + %12 = tt.addptr %11, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + tt.store %12, %10, %6 : tensor<128x!tt.ptr> + tt.return + } +} + +// ----- + +// Case 2: other = prior full load tensor -> select(mask, loaded, other) +// CHECK-LABEL: func.func @load_other_tensor +// CHECK: %[[MASK:.*]] = arith.cmpi slt +// CHECK: memref.copy +// CHECK: %[[OTHER:.*]] = bufferization.to_tensor +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<128xf32> +// CHECK: memref.copy +// CHECK: %[[LOADED:.*]] = bufferization.to_tensor %[[ALLOC]] +// CHECK: arith.select %[[MASK]], %[[LOADED]], %[[OTHER]] +// CHECK-NOT: tensor.insert_slice + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @load_other_tensor(%in_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %out_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %fill_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %N: i32) attributes {noinline = false} { + %c128_i32 = arith.constant 128 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c128_i32 : i32 + %2 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %3 = tt.splat %1 : i32 -> tensor<128xi32> + %4 = arith.addi %3, %2 : tensor<128xi32> + %5 = tt.splat %N : i32 -> tensor<128xi32> + %6 = arith.cmpi slt, %4, %5 : tensor<128xi32> + %7 = tt.splat %fill_ptr : !tt.ptr -> tensor<128x!tt.ptr> + %8 = tt.addptr %7, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %9 = tt.load %8 : tensor<128x!tt.ptr> + %10 = tt.splat %in_ptr : !tt.ptr -> tensor<128x!tt.ptr> + %11 = tt.addptr %10, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %12 = tt.load %11, %6, %9 : tensor<128x!tt.ptr> + %13 = tt.splat %out_ptr : !tt.ptr -> tensor<128x!tt.ptr> + %14 = tt.addptr %13, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + tt.store %14, %12 : tensor<128x!tt.ptr> + tt.return + } +} + +// ----- + +// Case 3: Scalar other=0.0 (unchanged fill path) +// CHECK-LABEL: func.func @load_other_scalar_zero +// CHECK: %[[CST:.*]] = arith.constant 0.000000e+00 : f32 +// CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<128xf32> +// CHECK: linalg.fill ins(%[[CST]] : f32) outs(%[[ALLOC]] : memref<128xf32>) +// CHECK: memref.copy +// CHECK: bufferization.to_tensor %[[ALLOC]] + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @load_other_scalar_zero(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: i32) attributes {noinline = false} { + %c128_i32 = arith.constant 128 : i32 + %cst = arith.constant dense<0.000000e+00> : tensor<128xf32> + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c128_i32 : i32 + %2 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %3 = tt.splat %1 : i32 -> tensor<128xi32> + %4 = arith.addi %3, %2 : tensor<128xi32> + %5 = tt.splat %arg2 : i32 -> tensor<128xi32> + %6 = arith.cmpi slt, %4, %5 : tensor<128xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<128x!tt.ptr> + %8 = tt.addptr %7, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %9 = tt.load %8, %6, %cst : tensor<128x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<128x!tt.ptr> + %11 = tt.addptr %10, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + tt.store %11, %9, %6 : tensor<128x!tt.ptr> + tt.return + } +} + +// ----- + +// Case 4: discrete mask (offs % 2 == 0) + other = prior tensor. +// discrete-mask-access-conversion already rewrites to full load + arith.select. +// CHECK-LABEL: func.func @load_other_tensor_discrete_mask +// CHECK: memref.copy +// CHECK: %[[OTHER:.*]] = bufferization.to_tensor +// CHECK: memref.copy +// CHECK: %[[LOADED:.*]] = bufferization.to_tensor +// CHECK: arith.select {{.*}}, %[[LOADED]], %[[OTHER]] +// CHECK-NOT: tensor.insert_slice + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @load_other_tensor_discrete_mask(%in_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %out_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %other_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %N: i32) attributes {noinline = false} { + %c128_i32 = arith.constant 128 : i32 + %c2_i32 = arith.constant 2 : i32 + %c0_i32 = arith.constant 0 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c128_i32 : i32 + %2 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %3 = tt.splat %1 : i32 -> tensor<128xi32> + %4 = arith.addi %3, %2 : tensor<128xi32> + %5 = tt.splat %c2_i32 : i32 -> tensor<128xi32> + %6 = arith.remsi %4, %5 : tensor<128xi32> + %7 = tt.splat %c0_i32 : i32 -> tensor<128xi32> + %8 = arith.cmpi eq, %6, %7 : tensor<128xi32> + %9 = tt.splat %other_ptr : !tt.ptr -> tensor<128x!tt.ptr> + %10 = tt.addptr %9, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %11 = tt.load %10 : tensor<128x!tt.ptr> + %12 = tt.splat %in_ptr : !tt.ptr -> tensor<128x!tt.ptr> + %13 = tt.addptr %12, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %14 = tt.load %13, %8, %11 : tensor<128x!tt.ptr> + %15 = tt.splat %out_ptr : !tt.ptr -> tensor<128x!tt.ptr> + %16 = tt.addptr %15, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + tt.store %16, %14 : tensor<128x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_make_block_ptr_boundary_size.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_make_block_ptr_boundary_size.mlir new file mode 100644 index 0000000000..81de97711f --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_make_block_ptr_boundary_size.mlir @@ -0,0 +1,120 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s +// RUN: triton-opt --triton-to-linalg --split-input-file %s 2>&1 | FileCheck %s --check-prefix=WARN + +// Unit tests for ConverterUtils::getBoundarySizes() (PR #1838, +// "[TritonToLinalg](fix) boundary size Error for make_block_ptr"). +// +// `tl.make_block_ptr` is lowered to `tt.make_tensor_ptr` before +// TritonToLinalg runs. getBoundarySizes() reconstructs the per-axis +// in-bounds size by decomposing the flat block offset with the full-shape +// strides; three defects in that decomposition are covered here: +// +// 1. (primary) An axis that is *not* boundary-checked never reduced the +// flat offset (offset % stride), so a checked trailing axis saw the +// offset contributed by the leading axis, shrinking its boundary to 0 +// and silently dropping the loaded/stored data. The fix strips every +// non-zero-stride axis, checked or not. +// +// 2. (incidental) A zero-stride (broadcast) axis listed in boundary_check +// reached divOpFoldResult(offsetShift, 0), which emits "cannot div 0!" +// and returns an empty OpFoldResult, crashing the compiler. The fix +// skips such axes (emitting a warning) and keeps the current block size. +// +// 3. Logical axis order need not be physical major-to-minor order. For a +// permuted layout with strides [1, 800, 20], decomposing axes as [0, 1, 2] +// treats the entire linear offset as axis 0. The fix visits axes in +// descending stride order [1, 2, 0]. + +// The skipped zero-stride axis of the second case is reported through a +// warning instead of the previous "cannot div 0!" error (the diagnostic +// precedes the module on stderr). +// WARN-NOT: error: +// WARN: warning: getBoundarySizes() cannot reconstruct boundary on checked zero-stride axis 0 +// WARN-LABEL: func.func @boundary_size_zero_stride_axis + +// Primary case: axis 0 (stride 64) is *not* checked, axis 1 is. The block +// starts at (16, 36) of a (64, 60) tensor: the flat offset 16*64 + 36 = 1060 +// must be reduced by axis 0 first, leaving 36 for axis 1 -> boundary +// 60 - 36 = 24. Before the fix the un-reduced offset 1060 was used, +// clamping the axis-1 boundary to 0 ([16, 0]) and dropping the whole load. + +// CHECK-LABEL: func.func @boundary_size_unchecked_axis +// CHECK: memref.subview {{.*}}[0, 0] [16, 24] [1, 1] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @boundary_size_unchecked_axis( + %base_ptr: !tt.ptr {tt.divisibility = 16 : i32}, + %out_ptr: !tt.ptr {tt.divisibility = 16 : i32} + ) attributes {noinline = false} { + %c16_i32 = arith.constant 16 : i32 + %c36_i32 = arith.constant 36 : i32 + %c1_i64 = arith.constant 1 : i64 + %c60_i64 = arith.constant 60 : i64 + %c64_i64 = arith.constant 64 : i64 + %load_ptr = tt.make_tensor_ptr %base_ptr, [%c64_i64, %c60_i64], [%c64_i64, %c1_i64], [%c16_i32, %c36_i32] {order = array} : > + %data = tt.load %load_ptr {boundaryCheck = array} : !tt.ptr> + %store_ptr = tt.make_tensor_ptr %out_ptr, [%c64_i64, %c60_i64], [%c64_i64, %c1_i64], [%c16_i32, %c36_i32] {order = array} : > + tt.store %store_ptr, %data {boundaryCheck = array} : !tt.ptr> + tt.return + } +} + +// ----- +// Incidental case: axis 0 is a broadcast axis (stride 0) and is +// boundary-checked. The zero-stride axis keeps its full block size (16) +// while the axis-1 boundary is reconstructed as 60 - 36 = 24. + +// CHECK-LABEL: func.func @boundary_size_zero_stride_axis +// CHECK: memref.subview {{.*}}[0, 0] [16, 24] [1, 1] +// CHECK: memref.copy +// CHECK: memref.subview {{.*}}[0, 0] [16, 24] [1, 1] +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @boundary_size_zero_stride_axis( + %base_ptr: !tt.ptr {tt.divisibility = 16 : i32}, + %out_ptr: !tt.ptr {tt.divisibility = 16 : i32} + ) attributes {noinline = false} { + %c0_i32 = arith.constant 0 : i32 + %c36_i32 = arith.constant 36 : i32 + %c0_i64 = arith.constant 0 : i64 + %c1_i64 = arith.constant 1 : i64 + %c60_i64 = arith.constant 60 : i64 + %c64_i64 = arith.constant 64 : i64 + %load_ptr = tt.make_tensor_ptr %base_ptr, [%c64_i64, %c60_i64], [%c0_i64, %c1_i64], [%c0_i32, %c36_i32] {order = array} : > + %data = tt.load %load_ptr {boundaryCheck = array} : !tt.ptr> + %store_ptr = tt.make_tensor_ptr %out_ptr, [%c64_i64, %c60_i64], [%c64_i64, %c1_i64], [%c0_i32, %c36_i32] {order = array} : > + tt.store %store_ptr, %data {boundaryCheck = array} : !tt.ptr> + tt.return + } +} + +// ----- +// Permuted layout: logical axes [0, 1, 2] have physical strides [1, 800, 20]. +// The block starts at logical offset [7, 18, 32] in shape [20, 30, 40]. Its +// flat offset is 7 + 18*800 + 32*20 = 15047. Decomposing in physical order +// [1, 2, 0] recovers [18, 32, 7], which maps back to logical boundary sizes +// [1, min(16, 30-18), min(64, 40-32)] = [1, 12, 8]. + +// CHECK-LABEL: func.func @boundary_size_permuted_strides +// CHECK: memref.subview {{.*}}[0, 0, 0] [1, 12, 8] [1, 1, 1] +// CHECK: memref.copy +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @boundary_size_permuted_strides( + %base_ptr: !tt.ptr {tt.divisibility = 16 : i32}, + %out_ptr: !tt.ptr {tt.divisibility = 16 : i32} + ) attributes {noinline = false} { + %c1_i64 = arith.constant 1 : i64 + %c7_i32 = arith.constant 7 : i32 + %c18_i32 = arith.constant 18 : i32 + %c32_i32 = arith.constant 32 : i32 + %c20_i64 = arith.constant 20 : i64 + %c30_i64 = arith.constant 30 : i64 + %c40_i64 = arith.constant 40 : i64 + %c800_i64 = arith.constant 800 : i64 + %c1200_i64 = arith.constant 1200 : i64 + %load_ptr = tt.make_tensor_ptr %base_ptr, [%c20_i64, %c30_i64, %c40_i64], [%c1_i64, %c800_i64, %c20_i64], [%c7_i32, %c18_i32, %c32_i32] {order = array} : > + %data = tt.load %load_ptr {boundaryCheck = array} : !tt.ptr> + %store_ptr = tt.make_tensor_ptr %out_ptr, [%c20_i64, %c30_i64, %c40_i64], [%c1200_i64, %c40_i64, %c1_i64], [%c7_i32, %c18_i32, %c32_i32] {order = array} : > + tt.store %store_ptr, %data {boundaryCheck = array} : !tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_nd2nz_block_ptr.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_nd2nz_block_ptr.mlir new file mode 100644 index 0000000000..8edd3bf152 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_nd2nz_block_ptr.mlir @@ -0,0 +1,20 @@ +// RUN: triton-opt --triton-to-linalg=enable-nd2nz-on-vector=true %s | FileCheck %s + +// CHECK-LABEL: func.func @test_nd2nz_block_ptr + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_nd2nz_block_ptr( + %base_ptr: !tt.ptr {tt.divisibility = 16 : i32}, + %out_ptr: !tt.ptr {tt.divisibility = 16 : i32} + ) attributes {noinline = false} { + %c0_i32 = arith.constant 0 : i32 + %c1_i64 = arith.constant 1 : i64 + %c128_i64 = arith.constant 128 : i64 + %c32_i64 = arith.constant 32 : i64 + %load_ptr = tt.make_tensor_ptr %base_ptr, [%c128_i64, %c32_i64], [%c32_i64, %c1_i64], [%c0_i32, %c0_i32] {order = array} : > + %data = tt.load %load_ptr {boundaryCheck = array} : !tt.ptr> + %store_ptr = tt.make_tensor_ptr %out_ptr, [%c128_i64, %c32_i64], [%c32_i64, %c1_i64], [%c0_i32, %c0_i32] {order = array} : > + tt.store %store_ptr, %data {boundaryCheck = array} : !tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_atomic_rmw.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_atomic_rmw.mlir new file mode 100644 index 0000000000..dbe1f2636a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_atomic_rmw.mlir @@ -0,0 +1,41 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @test_parse_atomic_rmw +// CHECK: %[[C8_I32:.*]] = arith.constant 8 : i32 +// CHECK: %[[FILL:.*]] = linalg.fill ins(%[[C8_I32]] : i32) +// CHECK: hivm.hir.store ins(%[[FILL]] : tensor<1xi32>) outs(%{{.*}} : memref<1xi32, strided<[1]>>) atomic = +// CHECK: %[[EXTRACTED:.*]] = tensor.extract %{{.*}}[%{{.*}}] : tensor<1xi32> +// CHECK: %[[IDX_CAST:.*]] = arith.index_cast %[[EXTRACTED]] : i32 to index +// CHECK: memref.reinterpret_cast %{{.*}} to offset: [%[[IDX_CAST]]], sizes: [8], strides: [1] + + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_parse_atomic_rmw(%counter_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %numel: i32) attributes {noinline = false} { + %zero_i32 = arith.constant dense<0> : tensor<8xi32> + %c8_i32 = arith.constant 8 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %base = arith.muli %pid, %c8_i32 : i32 + %base_splat = tt.splat %base : i32 -> tensor<8xi32> + %offs_full = arith.addi %offs, %base_splat : tensor<8xi32> + %numel_splat = tt.splat %numel : i32 -> tensor<8xi32> + %mask = arith.cmpi slt, %offs_full, %numel_splat : tensor<8xi32> + %src_ptrs = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr = tt.addptr %src_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + %data = tt.load %src_addptr, %mask, %zero_i32 : tensor<8x!tt.ptr> + + // Tensor-level atomic: tensor<1> + %ctr_range = tt.make_range {start = 0 : i32, end = 1 : i32} : tensor<1xi32> + %ctr_ptrs = tt.splat %counter_ptr : !tt.ptr -> tensor<1x!tt.ptr> + %ctr_addptr = tt.addptr %ctr_ptrs, %ctr_range : tensor<1x!tt.ptr>, tensor<1xi32> + %incr = arith.constant dense<8> : tensor<1xi32> + %old = tt.atomic_rmw add, acq_rel, gpu, %ctr_addptr, %incr : (tensor<1x!tt.ptr>, tensor<1xi32>) -> tensor<1xi32> + %old_broadcast = tt.broadcast %old : tensor<1xi32> -> tensor<8xi32> + %write_offs = arith.addi %old_broadcast, %offs : tensor<8xi32> + + %dst_ptrs = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %write_offs : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %data, %mask : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_gather.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_gather.mlir new file mode 100644 index 0000000000..6dbacfe07a --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_gather.mlir @@ -0,0 +1,41 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @tensor_descriptor_gather_rows +// CHECK-NOT: tt.descriptor_gather +// CHECK: %[[IDX_VIEW:.*]] = memref.reinterpret_cast %{{.*}} to offset: [0], sizes: [32], strides: [1] +// CHECK: %[[IDX_ALLOC:.*]] = memref.alloc() : memref<32xi32> +// CHECK: memref.copy %[[IDX_VIEW]], %[[IDX_ALLOC]] +// CHECK: %[[XOFFSETS:.*]] = bufferization.to_tensor %[[IDX_ALLOC]] restrict writable : memref<32xi32> to tensor<32xi32> +// CHECK: %[[OUT_ALLOC:.*]] = memref.alloc() : memref<32x32xf32> +// CHECK: scf.for %{{.*}} = %{{.*}} to %{{.*}} step %{{.*}} +// CHECK: %[[XOFFSET:.*]] = tensor.extract %[[XOFFSETS]][%{{.*}}] : tensor<32xi32> +// CHECK: %[[DESC_VIEW:.*]] = memref.reinterpret_cast %{{.*}} to offset: [%{{.*}}], sizes: [1, 32], strides: [128, 1] +// CHECK: %[[OUT_SUBVIEW:.*]] = memref.subview %[[OUT_ALLOC]][%{{.*}}, 0] [1, 32] [1, 1] +// CHECK: memref.copy %{{.*}}, %{{.*}} +// CHECK: %[[OUT_TENSOR:.*]] = bufferization.to_tensor %[[OUT_ALLOC]] restrict writable : memref<32x32xf32> to tensor<32x32xf32> +// CHECK: bufferization.materialize_in_destination %[[OUT_TENSOR]] in writable %{{.*}} : (tensor<32x32xf32>, memref<32x32xf32, strided<[32, 1]>>) -> () + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @tensor_descriptor_gather_rows(%out_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %in_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %idx_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %y: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<32> : tensor<32x1xi32> + %desc = arith.constant 1 : i64 + %desc_0 = arith.constant 128 : i64 + %c128_i32 = arith.constant 128 : i32 + %idx = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %idx_1 = tt.splat %idx_ptr : !tt.ptr -> tensor<32x!tt.ptr> + %idx_2 = tt.addptr %idx_1, %idx : tensor<32x!tt.ptr>, tensor<32xi32> + %idx_3 = tt.load %idx_2 : tensor<32x!tt.ptr> + %desc_4 = tt.make_tensor_descriptor %in_ptr, [%c128_i32, %c128_i32], [%desc_0, %desc] : , > + %out = tt.descriptor_gather %desc_4[%idx_3, %y] : (!tt.tensordesc>, tensor<32xi32>, i32) -> tensor<32x32xf32> + %0 = tt.expand_dims %idx {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %1 = arith.muli %0, %cst : tensor<32x1xi32> + %2 = tt.splat %out_ptr : !tt.ptr -> tensor<32x1x!tt.ptr> + %3 = tt.addptr %2, %1 : tensor<32x1x!tt.ptr>, tensor<32x1xi32> + %4 = tt.expand_dims %idx {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %5 = tt.broadcast %3 : tensor<32x1x!tt.ptr> -> tensor<32x32x!tt.ptr> + %6 = tt.broadcast %4 : tensor<1x32xi32> -> tensor<32x32xi32> + %7 = tt.addptr %5, %6 : tensor<32x32x!tt.ptr>, tensor<32x32xi32> + tt.store %7, %out : tensor<32x32x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_reduce.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_reduce.mlir new file mode 100644 index 0000000000..e0b3aa62ba --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_reduce.mlir @@ -0,0 +1,38 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @kernel +// CHECK: %[[SUBVIEW:.*]] = memref.subview %[[RC:.*]][%[[VAL_36:.*]], %[[VAL_39:.*]]] [%[[VAL_38:.*]], %[[VAL_41:.*]]] [1, 1] : memref<2x16xi32, strided<[?, 1], offset: ?>> to memref> +// CHECK: %[[EXTRACT:.*]] = tensor.extract_slice %[[VAL_7:.*]][%[[VAL_36]], %[[VAL_39]]] [%[[VAL_38]], %[[VAL_41]]] [1, 1] : tensor<2x16xi32> to tensor +// CHECK: hivm.hir.store ins(%[[EXTRACT]] : tensor) outs(%[[SUBVIEW]] : memref>) atomic = + +module { + tt.func public @kernel(%a_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %out_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %M: i32 , %N: i32 {tt.divisibility = 16 : i32} ) attributes {noinline = false} { + %desc = arith.constant 1 : i64 + %c16_i32 = arith.constant 16 : i32 + %c2_i32 = arith.constant 2 : i32 + %moffset = tt.get_program_id x : i32 + %moffset_0 = arith.muli %moffset, %c2_i32 : i32 + %noffset = tt.get_program_id y : i32 + %noffset_1 = arith.muli %noffset, %c16_i32 : i32 + %midx = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %midx_2 = tt.expand_dims %midx {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %midx_3 = tt.splat %moffset_0 : i32 -> tensor<2x1xi32> + %midx_4 = arith.addi %midx_3, %midx_2 : tensor<2x1xi32> + %nidx = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %nidx_5 = tt.expand_dims %nidx {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %nidx_6 = tt.splat %noffset_1 : i32 -> tensor<1x16xi32> + %nidx_7 = arith.addi %nidx_6, %nidx_5 : tensor<1x16xi32> + %idx = tt.splat %N : i32 -> tensor<2x1xi32> + %idx_8 = arith.muli %midx_4, %idx : tensor<2x1xi32> + %idx_9 = tt.broadcast %idx_8 : tensor<2x1xi32> -> tensor<2x16xi32> + %idx_10 = tt.broadcast %nidx_7 : tensor<1x16xi32> -> tensor<2x16xi32> + %idx_11 = arith.addi %idx_9, %idx_10 : tensor<2x16xi32> + %val = tt.splat %a_ptr : !tt.ptr -> tensor<2x16x!tt.ptr> + %val_12 = tt.addptr %val, %idx_11 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %val_13 = tt.load %val_12 : tensor<2x16x!tt.ptr> + %desc_14 = arith.extsi %N : i32 to i64 + %desc_15 = tt.make_tensor_descriptor %out_ptr, [%M, %N], [%desc_14, %desc] : , > + tt.descriptor_reduce add, %desc_15[%moffset_0, %noffset_1], %val_13 : !tt.tensordesc>, tensor<2x16xi32> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_scatter.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_scatter.mlir new file mode 100644 index 0000000000..725604afd4 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_descriptor_scatter.mlir @@ -0,0 +1,37 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @tensor_descriptor_scatter_rows_kernel +// CHECK-NOT: tt.descriptor_scatter +// CHECK: %[[C128:.*]] = arith.constant 128 : index +// CHECK: scf.for %{{.*}} = %{{.*}} to %{{.*}} step %{{.*}} +// CHECK: %[[XOFFSET:.*]] = tensor.extract %{{.*}}[%{{.*}}] : tensor<32xi32> +// CHECK: %[[BASE:.*]] = arith.muli %{{.*}}, %[[C128]] : index +// CHECK: %[[VIEW:.*]] = memref.reinterpret_cast %{{.*}} to offset: [%{{.*}}], sizes: [1, 32], strides: [128, 1] +// CHECK: %[[ROW:.*]] = tensor.extract_slice %{{.*}}[%{{.*}}, 0] [1, 32] [1, 1] : tensor<32x32xf32> to tensor<1x32xf32> +// CHECK: %[[SUBVIEW:.*]] = memref.subview %[[VIEW]] +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[SUBVIEW]] + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @tensor_descriptor_scatter_rows_kernel(%out_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %in_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %idx_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %y: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %desc = arith.constant 1 : i64 + %desc_0 = arith.constant 128 : i64 + %c128_i32 = arith.constant 128 : i32 + %data = arith.constant dense<32> : tensor<32x1xi32> + %idx = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %idx_1 = tt.splat %idx_ptr : !tt.ptr -> tensor<32x!tt.ptr> + %idx_2 = tt.addptr %idx_1, %idx : tensor<32x!tt.ptr>, tensor<32xi32> + %idx_3 = tt.load %idx_2 : tensor<32x!tt.ptr> + %data_4 = tt.expand_dims %idx {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32> + %data_5 = arith.muli %data_4, %data : tensor<32x1xi32> + %data_6 = tt.splat %in_ptr : !tt.ptr -> tensor<32x1x!tt.ptr> + %data_7 = tt.addptr %data_6, %data_5 : tensor<32x1x!tt.ptr>, tensor<32x1xi32> + %data_8 = tt.expand_dims %idx {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %data_9 = tt.broadcast %data_7 : tensor<32x1x!tt.ptr> -> tensor<32x32x!tt.ptr> + %data_10 = tt.broadcast %data_8 : tensor<1x32xi32> -> tensor<32x32xi32> + %data_11 = tt.addptr %data_9, %data_10 : tensor<32x32x!tt.ptr>, tensor<32x32xi32> + %data_12 = tt.load %data_11 : tensor<32x32x!tt.ptr> + %desc_13 = tt.make_tensor_descriptor %out_ptr, [%c128_i32, %c128_i32], [%desc_0, %desc] : , > + tt.descriptor_scatter %desc_13[%idx_3, %y], %data_12 : !tt.tensordesc>, tensor<32xi32>, i32, tensor<32x32xf32> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_div.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_div.mlir new file mode 100644 index 0000000000..adb5243fff --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_div.mlir @@ -0,0 +1,45 @@ +// RUN: triton-opt --triton-to-linalg=named-ops=false --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @test_parse_div +// CHECK: %[[SUB:.*]] = linalg.generic +// CHECK-SAME: iterator_types = ["parallel"] +// CHECK: ^bb0(%[[IN:.*]]: i32, %[[IN_5:.*]]: i32, %[[OUT:.*]]: i32): +// CHECK: %[[SUBI:.*]] = arith.subi %[[IN]], %[[IN_5]] : i32 +// CHECK: linalg.yield %[[SUBI]] : i32 + +// CHECK: %[[DIV:.*]] = linalg.generic +// CHECK-SAME: iterator_types = ["parallel"] +// CHECK: ^bb0(%[[IN:.*]]: i32, %[[IN_5:.*]]: i32, %[[OUT:.*]]: i32): +// CHECK: %[[DIVI:.*]] = arith.divsi %[[IN]], %[[IN_5]] : i32 +// CHECK: linalg.yield %[[DIVI]] : i32 + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_parse_div(%idx_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %numel: i32, %bias: i32, %stride: i32) attributes {noinline = false} { + %zero_f32 = arith.constant dense<0.000000e+00> : tensor<8xf32> + %zero_i32 = arith.constant dense<0> : tensor<8xi32> + %c8_i32 = arith.constant 8 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %base = arith.muli %pid, %c8_i32 : i32 + %base_splat = tt.splat %base : i32 -> tensor<8xi32> + %offs_full = arith.addi %offs, %base_splat : tensor<8xi32> + %numel_splat = tt.splat %numel : i32 -> tensor<8xi32> + %mask = arith.cmpi slt, %offs_full, %numel_splat : tensor<8xi32> + %idx_ptrs = tt.splat %idx_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %idx_addptr = tt.addptr %idx_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + %indices = tt.load %idx_addptr, %mask, %zero_i32 : tensor<8x!tt.ptr> + %bias_splat = tt.splat %bias : i32 -> tensor<8xi32> + %subbed = arith.subi %indices, %bias_splat : tensor<8xi32> + %bias_splat2 = tt.splat %bias : i32 -> tensor<8xi32> + %stride_splat = tt.splat %stride : i32 -> tensor<8xi32> + %scalar_div = arith.divsi %bias_splat2, %stride_splat : tensor<8xi32> + %combined = arith.addi %subbed, %scalar_div : tensor<8xi32> + %src_ptrs = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr = tt.addptr %src_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + %data = tt.load %src_addptr, %mask, %zero_f32 : tensor<8x!tt.ptr> + %dst_ptrs = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %combined : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %data, %mask : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_fptosi.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_fptosi.mlir new file mode 100644 index 0000000000..448cd52ee2 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_fptosi.mlir @@ -0,0 +1,34 @@ +// RUN: triton-opt --triton-to-linalg=named-ops=false --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @test_parse_fptosi +// CHECK: %[[GENERIC:.*]] = linalg.generic +// CHECK-SAME: iterator_types = ["parallel"] +// CHECK: ^bb0(%[[IN:.*]]: f32, %[[OUT:.*]]: i32): +// CHECK: %[[FPTOSI:.*]] = arith.fptosi %[[IN]] : f32 to i32 +// CHECK: linalg.yield %[[FPTOSI]] : i32 + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_parse_fptosi(%fidx_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %numel: i32) attributes {noinline = false} { + %zero_f32 = arith.constant dense<0.000000e+00> : tensor<8xf32> + %c8_i32 = arith.constant 8 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %base = arith.muli %pid, %c8_i32 : i32 + %base_splat = tt.splat %base : i32 -> tensor<8xi32> + %offs_full = arith.addi %offs, %base_splat : tensor<8xi32> + %numel_splat = tt.splat %numel : i32 -> tensor<8xi32> + %mask = arith.cmpi slt, %offs_full, %numel_splat : tensor<8xi32> + %offs_i64 = arith.extsi %offs_full : tensor<8xi32> to tensor<8xi64> + %fidx_ptrs = tt.splat %fidx_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %fidx_addptr = tt.addptr %fidx_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + %float_indices = tt.load %fidx_addptr, %mask, %zero_f32 : tensor<8x!tt.ptr> + %int_indices = arith.fptosi %float_indices : tensor<8xf32> to tensor<8xi32> + %src_ptrs = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr = tt.addptr %src_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + %data = tt.load %src_addptr, %mask, %zero_f32 : tensor<8x!tt.ptr> + %dst_ptrs = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %int_indices : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %data, %mask : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_select_dense_const.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_select_dense_const.mlir new file mode 100644 index 0000000000..ecb5ba4efb --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_select_dense_const.mlir @@ -0,0 +1,36 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @test_parse_select_dense_const +// CHECK: %[[C32_I32:.*]] = arith.constant 32 : i32 +// CHECK: %[[C0_I32:.*]] = arith.constant 0 : i32 +// CHECK: %[[CMP:.*]] = arith.cmpi sgt, %{{.*}}, %[[C0_I32]] : i32 +// CHECK: %[[SELECT:.*]] = arith.select %[[CMP]], %[[C0_I32]], %[[C32_I32]] : i32 +// CHECK: %[[IDX_CAST:.*]] = arith.index_cast %[[SELECT]] : i32 to index +// CHECK: %[[ADD:.*]] = arith.addi %[[IDX_CAST]], %{{.*}} : index + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_parse_select_dense_const(%src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %numel: i32, %flag: i32) attributes {noinline = false} { + %zero_f32 = arith.constant dense<0.000000e+00> : tensor<8xf32> + %c8_i32 = arith.constant 8 : i32 + %c0_i32 = arith.constant 0 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %base = arith.muli %pid, %c8_i32 : i32 + %base_splat = tt.splat %base : i32 -> tensor<8xi32> + %offs_full = arith.addi %offs, %base_splat : tensor<8xi32> + %numel_splat = tt.splat %numel : i32 -> tensor<8xi32> + %mask = arith.cmpi slt, %offs_full, %numel_splat : tensor<8xi32> + %cond = arith.cmpi sgt, %flag, %c0_i32 : i32 + %const_a = arith.constant dense<0> : tensor<8xi32> + %const_b = arith.constant dense<32> : tensor<8xi32> + %chosen = arith.select %cond, %const_a, %const_b : tensor<8xi32> + %write_offs = arith.addi %chosen, %offs_full : tensor<8xi32> + %src_ptrs = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %src_addptr = tt.addptr %src_ptrs, %offs_full : tensor<8x!tt.ptr>, tensor<8xi32> + %data = tt.load %src_addptr, %mask, %zero_f32 : tensor<8x!tt.ptr> + %dst_ptrs = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %write_offs : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %data, %mask : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_sub.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_sub.mlir new file mode 100644 index 0000000000..b9d1086358 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_parse_sub.mlir @@ -0,0 +1,34 @@ +// RUN: triton-opt --triton-to-linalg --split-input-file %s | FileCheck %s + +// CHECK-LABEL: func.func @sub_offset_kernel + +// CHECK: memref.subview {{.*}} : memref<8xi32, strided<[1], offset: ?>> to memref> +// CHECK: memref.subview {{.*}} : memref<8xi32> to memref> +// CHECK: memref.copy + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @sub_offset_kernel(%idx_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}, %numel: i32, %correction: i32) attributes {noinline = false} { + %val = arith.constant dense<0.000000e+00> : tensor<8xf32> + %base_idx = arith.constant dense<0> : tensor<8xi32> + %c8_i32 = arith.constant 8 : i32 + %pid = tt.get_program_id x : i32 + %offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %offs_0 = arith.muli %pid, %c8_i32 : i32 + %offs_1 = tt.splat %offs_0 : i32 -> tensor<8xi32> + %offs_2 = arith.addi %offs, %offs_1 : tensor<8xi32> + %mask = tt.splat %numel : i32 -> tensor<8xi32> + %mask_3 = arith.cmpi slt, %offs_2, %mask : tensor<8xi32> + %base_idx_4 = tt.splat %idx_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %base_idx_5 = tt.addptr %base_idx_4, %offs_2 : tensor<8x!tt.ptr>, tensor<8xi32> + %base_idx_6 = tt.load %base_idx_5, %mask_3, %base_idx : tensor<8x!tt.ptr> + %adjusted = tt.splat %correction : i32 -> tensor<8xi32> + %adjusted_7 = arith.subi %base_idx_6, %adjusted : tensor<8xi32> + %val_8 = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %val_9 = tt.addptr %val_8, %offs_2 : tensor<8x!tt.ptr>, tensor<8xi32> + %val_10 = tt.load %val_9, %mask_3, %val : tensor<8x!tt.ptr> + %0 = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %1 = tt.addptr %0, %adjusted_7 : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %1, %val_10, %mask_3 : tensor<8x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_tensor_descriptor.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_tensor_descriptor.mlir new file mode 100644 index 0000000000..70cd13a374 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_tensor_descriptor.mlir @@ -0,0 +1,114 @@ +// RUN: triton-opt --triton-rewrite-tensor-descriptor-to-pointer --split-input-file %s | FileCheck %s --check-prefix=CHECK +// RUN: triton-opt --triton-rewrite-tensor-descriptor-to-pointer --canonicalize "--triton-to-linalg=global-kernel=false named-ops=True" --split-input-file %s | FileCheck %s --check-prefix=CHECK-LINALG + +// ----- + +// Case 1: tt.descriptor_load/store -> tt.load/store (rewrite_tensor_descriptor_to_pointer) +// CHECK-LABEL: tt.func public @desc_copy_device +// CHECK-NOT: tt.make_tensor_descriptor +// CHECK-NOT: tt.descriptor_load +// CHECK-NOT: tt.descriptor_store +// CHECK-NOT: !tt.tensordesc +// CHECK: tt.load {{.*}}, {{.*}}, {{.*}} : tensor<16x32x!tt.ptr> +// CHECK: tt.store +// +// CHECK-LINALG-LABEL: func.func @desc_copy_device +// CHECK-LINALG: %[[ZERO:.*]] = arith.constant 0.000000e+00 : f32 +// CHECK-LINALG: %[[ALLOC:.*]] = memref.alloc() : memref<16x32xf32> +// CHECK-LINALG: scf.if +// CHECK-LINALG: linalg.fill ins(%[[ZERO]] : f32) outs(%[[ALLOC]] : memref<16x32xf32>) +// CHECK-LINALG: memref.copy +// CHECK-LINALG: bufferization.to_tensor %[[ALLOC]] + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @desc_copy_device(%in_ptr: !tt.ptr, %out_ptr: !tt.ptr, %M: i32, %N: i32) { + %c1 = arith.constant 1 : i64 + %c0 = arith.constant 0 : i32 + %Ns = arith.extsi %N : i32 to i64 + %in_desc = tt.make_tensor_descriptor %in_ptr, [%M, %N], [%Ns, %c1] : , > + %out_desc = tt.make_tensor_descriptor %out_ptr, [%M, %N], [%Ns, %c1] : , > + %tile = tt.descriptor_load %in_desc[%c0, %c0] : !tt.tensordesc> -> tensor<16x32xf32> + tt.descriptor_store %out_desc[%c0, %c0], %tile : !tt.tensordesc>, tensor<16x32xf32> + tt.return + } +} + +// ----- + +// Case 2: host TensorDescriptor ABI (padding as i1) -> linalg, getScalarValue peels +// other = select(padding, nan, zero) into a scalar fill. +// CHECK-LABEL: tt.func public @host_desc_load +// CHECK: arith.select %{{.*}}, %{{.*}}, %{{.*}} : tensor<16x32xf32> +// CHECK: tt.load {{.*}}, {{.*}}, {{.*}} : tensor<16x32x!tt.ptr> +// CHECK: tt.store +// +// CHECK-LINALG-LABEL: func.func @host_desc_load +// CHECK-LINALG-DAG: %[[ZERO:.*]] = arith.constant 0.000000e+00 : f32 +// CHECK-LINALG-DAG: %[[NAN:.*]] = arith.constant 0x7FC00000 : f32 +// CHECK-LINALG-DAG: %[[ALLOC:.*]] = memref.alloc() : memref<16x32xf32> +// CHECK-LINALG: %[[PAD:.*]] = arith.select %{{.*}}, %[[NAN]], %[[ZERO]] : f32 +// CHECK-LINALG: scf.if +// CHECK-LINALG: linalg.fill ins(%[[PAD]] : f32) outs(%[[ALLOC]] : memref<16x32xf32>) +// CHECK-LINALG: memref.copy +// CHECK-LINALG: bufferization.to_tensor %[[ALLOC]] + +module attributes {hacc.target = #hacc.target<"Ascend910B4">} { + tt.func public @host_desc_load( + %ptr: !tt.ptr, + %shape0: i64, %shape1: i64, + %stride0: i64, %stride1: i64, + %padding: i1, + %shape0_i32: i32, %shape1_i32: i32, + %stride0_dup: i64, %stride1_dup: i64, + %out: !tt.ptr, + %om: i32, %on: i32) { + %cst_nan = arith.constant dense<0x7FC00000> : tensor<16x32xf32> + %cst_zero = arith.constant dense<0.000000e+00> : tensor<16x32xf32> + %other = arith.select %padding, %cst_nan, %cst_zero : tensor<16x32xf32> + + %om64 = arith.extsi %om : i32 to i64 + %on64 = arith.extsi %on : i32 to i64 + %offs_m = tt.splat %om64 : i64 -> tensor<16xi64> + %range_m = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %range_m64 = arith.extsi %range_m : tensor<16xi32> to tensor<16xi64> + %idx_m = arith.addi %offs_m, %range_m64 : tensor<16xi64> + %idx_m2 = tt.expand_dims %idx_m {axis = 1 : i32} : tensor<16xi64> -> tensor<16x1xi64> + + %offs_n = tt.splat %on64 : i64 -> tensor<32xi64> + %range_n = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %range_n64 = arith.extsi %range_n : tensor<32xi32> to tensor<32xi64> + %idx_n = arith.addi %offs_n, %range_n64 : tensor<32xi64> + %idx_n2 = tt.expand_dims %idx_n {axis = 0 : i32} : tensor<32xi64> -> tensor<1x32xi64> + + %base = tt.splat %ptr : !tt.ptr -> tensor<16x32x!tt.ptr> + %s0 = tt.splat %stride0 : i64 -> tensor<16x1xi64> + %m0 = arith.muli %idx_m2, %s0 : tensor<16x1xi64> + %b0 = tt.broadcast %m0 : tensor<16x1xi64> -> tensor<16x32xi64> + %s1 = tt.splat %stride1 : i64 -> tensor<1x32xi64> + %m1 = arith.muli %idx_n2, %s1 : tensor<1x32xi64> + %b1 = tt.broadcast %m1 : tensor<1x32xi64> -> tensor<16x32xi64> + %off = arith.addi %b0, %b1 : tensor<16x32xi64> + %ptrs = tt.addptr %base, %off : tensor<16x32x!tt.ptr>, tensor<16x32xi64> + + %c0 = arith.constant dense<0> : tensor<16x1xi64> + %ge0 = arith.cmpi sge, %idx_m2, %c0 : tensor<16x1xi64> + %sh0 = tt.splat %shape0 : i64 -> tensor<16x1xi64> + %lt0 = arith.cmpi slt, %idx_m2, %sh0 : tensor<16x1xi64> + %mmsk = arith.andi %ge0, %lt0 : tensor<16x1xi1> + %mmsk2 = tt.broadcast %mmsk : tensor<16x1xi1> -> tensor<16x32xi1> + %c0n = arith.constant dense<0> : tensor<1x32xi64> + %ge1 = arith.cmpi sge, %idx_n2, %c0n : tensor<1x32xi64> + %sh1 = tt.splat %shape1 : i64 -> tensor<1x32xi64> + %lt1 = arith.cmpi slt, %idx_n2, %sh1 : tensor<1x32xi64> + %nmsk = arith.andi %ge1, %lt1 : tensor<1x32xi1> + %nmsk2 = tt.broadcast %nmsk : tensor<1x32xi1> -> tensor<16x32xi1> + %mask = arith.andi %mmsk2, %nmsk2 : tensor<16x32xi1> + + %tile = tt.load %ptrs, %mask, %other : tensor<16x32x!tt.ptr> + + %obase = tt.splat %out : !tt.ptr -> tensor<16x32x!tt.ptr> + %optrs = tt.addptr %obase, %off : tensor<16x32x!tt.ptr>, tensor<16x32xi64> + tt.store %optrs, %tile, %mask : tensor<16x32x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_while_loop_index_tensor.mlir b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_while_loop_index_tensor.mlir new file mode 100644 index 0000000000..097c0cf466 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToLinalg/test_while_loop_index_tensor.mlir @@ -0,0 +1,48 @@ +// RUN: triton-opt --triton-to-linalg %s | FileCheck %s + +// CHECK-LABEL: func.func @test_while_loop_index_tensor +// CHECK: %[[C8:.*]] = arith.constant 8 : index +// CHECK: %[[C0_I32:.*]] = arith.constant 0 : i32 +// CHECK: %[[C1_I32:.*]] = arith.constant 1 : i32 +// CHECK: %[[C4_I32:.*]] = arith.constant 4 : i32 +// CHECK: %[[C0:.*]] = arith.constant 0 : index +// CHECK: scf.while (%[[ARG9:.*]] = %[[C0_I32]], %[[ARG10:.*]] = %[[C0]]) +// CHECK: arith.cmpi slt, %[[ARG9]], %[[C4_I32]] +// CHECK: scf.condition +// CHECK: ^bb0(%[[ARG9:.*]]: i32, %[[ARG10:.*]]: index) +// CHECK: arith.addi %[[ARG9]], %[[C1_I32]] +// CHECK: arith.addi %[[ARG10]], %[[C8]] + +module attributes {hacc.target = #hacc.target<"Ascend910B2">} { + tt.func public @test_while_loop_index_tensor( + %src_ptr: !tt.ptr {tt.divisibility = 16 : i32}, + %dst_ptr: !tt.ptr {tt.divisibility = 16 : i32}, + %numel: i32 + ) attributes {noinline = false} { + %zero_f32 = arith.constant dense<0.000000e+00> : tensor<8xf32> + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %c8_i32 = arith.constant 8 : i32 + %c4_i32 = arith.constant 4 : i32 + %init_offs = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %step = arith.constant dense<8> : tensor<8xi32> + %result:2 = scf.while (%offs = %init_offs, %iter = %c0_i32) : (tensor<8xi32>, i32) -> (tensor<8xi32>, i32) { + %cond = arith.cmpi slt, %iter, %c4_i32 : i32 + scf.condition(%cond) %offs, %iter : tensor<8xi32>, i32 + } do { + ^bb0(%offs_arg: tensor<8xi32>, %iter_arg: i32): + %ptrs = tt.splat %src_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %addptr = tt.addptr %ptrs, %offs_arg : tensor<8x!tt.ptr>, tensor<8xi32> + %numel_splat = tt.splat %numel : i32 -> tensor<8xi32> + %mask = arith.cmpi slt, %offs_arg, %numel_splat : tensor<8xi32> + %data = tt.load %addptr, %mask, %zero_f32 : tensor<8x!tt.ptr> + %dst_ptrs = tt.splat %dst_ptr : !tt.ptr -> tensor<8x!tt.ptr> + %dst_addptr = tt.addptr %dst_ptrs, %offs_arg : tensor<8x!tt.ptr>, tensor<8xi32> + tt.store %dst_addptr, %data, %mask : tensor<8x!tt.ptr> + %new_offs = arith.addi %offs_arg, %step : tensor<8xi32> + %new_iter = arith.addi %iter_arg, %c1_i32 : i32 + scf.yield %new_offs, %new_iter : tensor<8xi32>, i32 + } + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/CmpConverter.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/CmpConverter.mlir new file mode 100644 index 0000000000..909de6174d --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/CmpConverter.mlir @@ -0,0 +1,24 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --split-input-file %s | FileCheck %s + +module { + tt.func public @test_cmp(%arg0: tensor<128xi32>) -> tensor<128xi1> { + %cst_12 = arith.constant dense<0> : tensor<128xi32> + %cst_13 = arith.constant dense<1> : tensor<128xi32> + %cst_14 = arith.constant dense<100> : tensor<128xi32> + %39 = arith.cmpi slt, %arg0, %cst_14 : tensor<128xi32> + %40 = arith.select %39, %cst_13, %cst_12 : tensor<128xi1>, tensor<128xi32> + %41 = arith.cmpi ne, %40, %cst_12 : tensor<128xi32> + tt.return %41 : tensor<128xi1> + } +} + +// CHECK-LABEL: tt.func public @test_cmp( +// CHECK-SAME: %[[VAL_0:.*]]: tensor<128xi32>) -> tensor<128xi1> { +// CHECK: %[[VAL_1:.*]] = arith.constant dense<0> : tensor<128xi32> +// CHECK: %[[VAL_2:.*]] = arith.constant dense<1> : tensor<128xi32> +// CHECK: %[[VAL_3:.*]] = arith.constant dense<100> : tensor<128xi32> +// CHECK: %[[VAL_4:.*]] = arith.cmpi slt, %[[VAL_0]], %[[VAL_3]] : tensor<128xi32> +// CHECK: %[[VAL_5:.*]] = arith.select %[[VAL_4]], %[[VAL_2]], %[[VAL_1]] : tensor<128xi1>, tensor<128xi32> +// CHECK: %[[VAL_6:.*]] = arith.cmpi ne, %[[VAL_5]], %[[VAL_1]] : tensor<128xi32> +// CHECK: tt.return %[[VAL_6]] : tensor<128xi1> +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/LoadBroadcastConverter.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/LoadBroadcastConverter.mlir new file mode 100644 index 0000000000..bdfb85ff16 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/LoadBroadcastConverter.mlir @@ -0,0 +1,144 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --triton-to-linalg --split-input-file %s | FileCheck %s + +// Tests for LoadBroadcastConverter: +// Case 1: ptr broadcast src shape matches mask broadcast src shape → +// rewrite to small load (with mask/other) + broadcast of result. +// Case 2: ptr broadcast src shape ≠ mask broadcast src shape (e.g. different +// broadcast axes) → rewrite to small load (no mask/other) + broadcast + +// arith.select with the full mask and other. + +// ----- + +// Case 2: ptr broadcast [1x64] -> [256x64] (axis 0), mask broadcast [256x1] -> +// [256x64] (axis 1). Different broadcast axes → small load [1x64] without +// mask/other, broadcast to full shape, then arith.select with full mask + other. +module { + tt.func public @kernel_load_diff_axis_broadcast(%X_ptr: !tt.ptr, %Out_ptr: !tt.ptr, %M: i32) attributes {noinline = false} { + %offsets = arith.constant dense<64> : tensor<256x1xi32> + %val = arith.constant dense<0.000000e+00> : tensor<256x64xf32> + %c64_i32 = arith.constant 64 : i32 + %c256_i32 = arith.constant 256 : i32 + %offsetx = tt.get_program_id x : i32 + %offsetx_0 = arith.muli %offsetx, %c256_i32 : i32 + %offsetx_1 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %offsetx_2 = tt.splat %offsetx_0 : i32 -> tensor<256xi32> + %offsetx_3 = arith.addi %offsetx_2, %offsetx_1 : tensor<256xi32> + %offsety = tt.get_program_id y : i32 + %offsety_4 = arith.muli %offsety, %c64_i32 : i32 + %offsety_5 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %offsety_6 = tt.splat %offsety_4 : i32 -> tensor<64xi32> + %offsety_7 = arith.addi %offsety_6, %offsety_5 : tensor<64xi32> + %ptr = tt.expand_dims %offsety_7 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %ptr_8 = tt.splat %X_ptr : !tt.ptr -> tensor<1x64x!tt.ptr> + %ptr_9 = tt.addptr %ptr_8, %ptr : tensor<1x64x!tt.ptr>, tensor<1x64xi32> + %mask = tt.splat %M : i32 -> tensor<256xi32> + %mask_10 = arith.cmpi slt, %offsetx_3, %mask : tensor<256xi32> + %mask_11 = tt.expand_dims %mask_10 {axis = 1 : i32} : tensor<256xi1> -> tensor<256x1xi1> + %val_12 = tt.broadcast %ptr_9 : tensor<1x64x!tt.ptr> -> tensor<256x64x!tt.ptr> + %val_13 = tt.broadcast %mask_11 : tensor<256x1xi1> -> tensor<256x64xi1> + %val_14 = tt.load %val_12, %val_13, %val : tensor<256x64x!tt.ptr> + %offsets_15 = tt.expand_dims %offsetx_3 {axis = 1 : i32} : tensor<256xi32> -> tensor<256x1xi32> + %offsets_16 = arith.muli %offsets_15, %offsets : tensor<256x1xi32> + %offsets_17 = tt.broadcast %offsets_16 : tensor<256x1xi32> -> tensor<256x64xi32> + %offsets_18 = tt.broadcast %ptr : tensor<1x64xi32> -> tensor<256x64xi32> + %offsets_19 = arith.addi %offsets_17, %offsets_18 : tensor<256x64xi32> + %0 = tt.splat %Out_ptr : !tt.ptr -> tensor<256x64x!tt.ptr> + %1 = tt.addptr %0, %offsets_19 : tensor<256x64x!tt.ptr>, tensor<256x64xi32> + tt.store %1, %val_14 : tensor<256x64x!tt.ptr> + tt.return + } +} +// CHECK-LABEL: func.func @kernel_load_diff_axis_broadcast +// CHECK: tensor.extract_slice %{{.*}}[0, 0] [%{{.*}}, 64] [1, 1] : tensor<256x64xf32> to tensor +// CHECK: tensor.insert_slice %{{.*}} into %{{.*}}[0, 0] [%{{.*}}, 64] [1, 1] : tensor into tensor<256x64xf32> + +// ----- + +// Case 2 variant: same different-broadcast-axes pattern but the load has no +// 'other' operand. The pass creates a default zero constant and inserts an +// arith.select with it. +module { + tt.func public @kernel_load_diff_axis_broadcast_no_other(%X_ptr: !tt.ptr, %Out_ptr: !tt.ptr, %M: i32) attributes {noinline = false} { + %offsets = arith.constant dense<64> : tensor<256x1xi32> + %val = arith.constant dense<0.000000e+00> : tensor<256x64xf32> + %c64_i32 = arith.constant 64 : i32 + %c256_i32 = arith.constant 256 : i32 + %offsetx = tt.get_program_id x : i32 + %offsetx_0 = arith.muli %offsetx, %c256_i32 : i32 + %offsetx_1 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %offsetx_2 = tt.splat %offsetx_0 : i32 -> tensor<256xi32> + %offsetx_3 = arith.addi %offsetx_2, %offsetx_1 : tensor<256xi32> + %offsety = tt.get_program_id y : i32 + %offsety_4 = arith.muli %offsety, %c64_i32 : i32 + %offsety_5 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %offsety_6 = tt.splat %offsety_4 : i32 -> tensor<64xi32> + %offsety_7 = arith.addi %offsety_6, %offsety_5 : tensor<64xi32> + %ptr = tt.expand_dims %offsety_7 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %ptr_8 = tt.splat %X_ptr : !tt.ptr -> tensor<1x64x!tt.ptr> + %ptr_9 = tt.addptr %ptr_8, %ptr : tensor<1x64x!tt.ptr>, tensor<1x64xi32> + %mask = tt.splat %M : i32 -> tensor<256xi32> + %mask_10 = arith.cmpi slt, %offsetx_3, %mask : tensor<256xi32> + %mask_11 = tt.expand_dims %mask_10 {axis = 1 : i32} : tensor<256xi1> -> tensor<256x1xi1> + %val_12 = tt.broadcast %ptr_9 : tensor<1x64x!tt.ptr> -> tensor<256x64x!tt.ptr> + %val_13 = tt.broadcast %mask_11 : tensor<256x1xi1> -> tensor<256x64xi1> + %val_14 = tt.load %val_12, %val_13, %val : tensor<256x64x!tt.ptr> + %offsets_15 = tt.expand_dims %offsetx_3 {axis = 1 : i32} : tensor<256xi32> -> tensor<256x1xi32> + %offsets_16 = arith.muli %offsets_15, %offsets : tensor<256x1xi32> + %offsets_17 = tt.broadcast %offsets_16 : tensor<256x1xi32> -> tensor<256x64xi32> + %offsets_18 = tt.broadcast %ptr : tensor<1x64xi32> -> tensor<256x64xi32> + %offsets_19 = arith.addi %offsets_17, %offsets_18 : tensor<256x64xi32> + %0 = tt.splat %Out_ptr : !tt.ptr -> tensor<256x64x!tt.ptr> + %1 = tt.addptr %0, %offsets_19 : tensor<256x64x!tt.ptr>, tensor<256x64xi32> + tt.store %1, %val_14 : tensor<256x64x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @kernel_load_diff_axis_broadcast_no_other +// CHECK: tensor.extract_slice %{{.*}}[0, 0] [%{{.*}}, 64] [1, 1] : tensor<256x64xf32> to tensor +// CHECK: tensor.insert_slice %{{.*}} into %{{.*}}[0, 0] [%{{.*}}, 64] [1, 1] : tensor into tensor<256x64xf32> + +// ----- + +// Negative: the load ptr is not produced by a tt.broadcast (src shape is +// already [1x64] and the Triton frontend did not insert a broadcast before +// the load). LoadBroadcastConverter does not match; load stays unchanged. +module { + tt.func public @kernel_load_same_axis_broadcast(%X_ptr: !tt.ptr, %Out_ptr: !tt.ptr, %YM: i32) attributes {noinline = false} { + %offsets = arith.constant dense<64> : tensor<256x1xi32> + %val = arith.constant dense<0.000000e+00> : tensor<1x64xf32> + %c64_i32 = arith.constant 64 : i32 + %c256_i32 = arith.constant 256 : i32 + %offsetx = tt.get_program_id x : i32 + %offsetx_0 = arith.muli %offsetx, %c256_i32 : i32 + %offsetx_1 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %offsetx_2 = tt.splat %offsetx_0 : i32 -> tensor<256xi32> + %offsetx_3 = arith.addi %offsetx_2, %offsetx_1 : tensor<256xi32> + %offsety = tt.get_program_id y : i32 + %offsety_4 = arith.muli %offsety, %c64_i32 : i32 + %offsety_5 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %offsety_6 = tt.splat %offsety_4 : i32 -> tensor<64xi32> + %offsety_7 = arith.addi %offsety_6, %offsety_5 : tensor<64xi32> + %ptr = tt.expand_dims %offsety_7 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %ptr_8 = tt.splat %X_ptr : !tt.ptr -> tensor<1x64x!tt.ptr> + %ptr_9 = tt.addptr %ptr_8, %ptr : tensor<1x64x!tt.ptr>, tensor<1x64xi32> + %mask = tt.splat %YM : i32 -> tensor<64xi32> + %mask_10 = arith.cmpi slt, %offsety_7, %mask : tensor<64xi32> + %mask_11 = tt.expand_dims %mask_10 {axis = 0 : i32} : tensor<64xi1> -> tensor<1x64xi1> + %val_12 = tt.load %ptr_9, %mask_11, %val : tensor<1x64x!tt.ptr> + %offsets_13 = tt.expand_dims %offsetx_3 {axis = 1 : i32} : tensor<256xi32> -> tensor<256x1xi32> + %offsets_14 = arith.muli %offsets_13, %offsets : tensor<256x1xi32> + %offsets_15 = tt.broadcast %offsets_14 : tensor<256x1xi32> -> tensor<256x64xi32> + %offsets_16 = tt.broadcast %ptr : tensor<1x64xi32> -> tensor<256x64xi32> + %offsets_17 = arith.addi %offsets_15, %offsets_16 : tensor<256x64xi32> + %0 = tt.splat %Out_ptr : !tt.ptr -> tensor<256x64x!tt.ptr> + %1 = tt.addptr %0, %offsets_17 : tensor<256x64x!tt.ptr>, tensor<256x64xi32> + %2 = tt.broadcast %val_12 : tensor<1x64xf32> -> tensor<256x64xf32> + tt.store %1, %2 : tensor<256x64x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @kernel_load_same_axis_broadcast +// CHECK: bufferization.to_tensor %{{.*}} restrict writable : memref<1x64xf32> to tensor<1x64xf32> +// CHECK: linalg.broadcast ins(%{{.*}} : tensor<64xf32>) outs(%{{.*}} : tensor<256x64xf32>) dimensions = [0] diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/PromotePointerIterArgsPattern.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/PromotePointerIterArgsPattern.mlir new file mode 100644 index 0000000000..7163b89f5f --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/PromotePointerIterArgsPattern.mlir @@ -0,0 +1,73 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --split-input-file %s | FileCheck %s + +module { + tt.func public @test_promote_pointer_iter(%base_ptr: !tt.ptr {tt.divisibility = 16 : i32}) -> !tt.ptr { + %c1_i32 = arith.constant 1 : i32 + %c0_f32 = arith.constant 0.000000e+00 : f32 + %true_mask = arith.constant 1 : i1 + %c0_index = arith.constant 0 : index + %c10_index = arith.constant 10 : index + %c1_index = arith.constant 1 : index + %final_ptr = scf.for %iv = %c0_index to %c10_index step %c1_index iter_args(%ptr = %base_ptr) -> (!tt.ptr) { + %data = tt.load %ptr, %true_mask, %c0_f32 : !tt.ptr + %new_ptr = tt.addptr %ptr, %c1_i32 : !tt.ptr, i32 + scf.yield %new_ptr : !tt.ptr + } + tt.return %final_ptr : !tt.ptr + } +} + +// CHECK-LABEL: tt.func public @test_promote_pointer_iter( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) -> !tt.ptr { +// CHECK: %[[VAL_1:.*]] = arith.constant 1 : i32 +// CHECK: %[[VAL_2:.*]] = arith.constant 0 : index +// CHECK: %[[VAL_3:.*]] = arith.constant 10 : index +// CHECK: %[[VAL_4:.*]] = arith.constant 1 : index +// CHECK: %[[VAL_5:.*]] = scf.for %[[VAL_6:.*]] = %[[VAL_2]] to %[[VAL_3]] step %[[VAL_4]] iter_args(%[[VAL_7:.*]] = %[[VAL_0]]) -> (!tt.ptr) { +// CHECK: %[[VAL_8:.*]] = tt.addptr %[[VAL_7]], %[[VAL_1]] : !tt.ptr, i32 +// CHECK: scf.yield %[[VAL_8]] : !tt.ptr +// CHECK: } +// CHECK: tt.return %[[VAL_5]] : !tt.ptr +// CHECK: } + + +// ----- + + +module { + tt.func public @test_promote_pointer_iter_advance(%base_ptr: !tt.ptr) -> !tt.ptr>{ + %c0_i32 = arith.constant 0 : i32 + %c1_i64 = arith.constant 1 : i64 + %c32_i64 = arith.constant 32 : i64 + %c1_i32 = arith.constant 1 : i32 // nonZeroConstant 需要 1 + %c0_i32_2 = arith.constant 0 : i32 + %c0_index = arith.constant 0 : index + %c10_index = arith.constant 10 : index + %c1_index = arith.constant 1 : index + %cst = arith.constant dense<0.000000e+00> : tensor<32xf16> + %ptr0 = tt.make_tensor_ptr %base_ptr, [%c32_i64], [%c1_i64], [%c0_i32] {order = array} : !tt.ptr> + %final_ptr = scf.for %iv = %c0_index to %c10_index step %c1_index iter_args(%ptr = %ptr0) -> !tt.ptr> { + %data = tt.load %ptr : !tt.ptr> + %new_ptr = tt.advance %ptr, [%c1_i32, %c0_i32_2] : !tt.ptr> + scf.yield %new_ptr : !tt.ptr> + } + tt.return %final_ptr : !tt.ptr> + } +} + +// CHECK-LABEL: tt.func public @test_promote_pointer_iter_advance( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr) -> !tt.ptr> { +// CHECK: %[[VAL_1:.*]] = arith.constant 0 : i32 +// CHECK: %[[VAL_2:.*]] = arith.constant 1 : i64 +// CHECK: %[[VAL_3:.*]] = arith.constant 32 : i64 +// CHECK: %[[VAL_4:.*]] = arith.constant 1 : i32 +// CHECK: %[[VAL_5:.*]] = arith.constant 0 : index +// CHECK: %[[VAL_6:.*]] = arith.constant 10 : index +// CHECK: %[[VAL_7:.*]] = arith.constant 1 : index +// CHECK: %[[VAL_8:.*]] = tt.make_tensor_ptr %[[VAL_0]], [%[[VAL_3]]], [%[[VAL_2]]], [%[[VAL_1]]] {order = array} : > +// CHECK: %[[VAL_9:.*]] = scf.for %[[VAL_10:.*]] = %[[VAL_5]] to %[[VAL_6]] step %[[VAL_7]] iter_args(%[[VAL_11:.*]] = %[[VAL_8]]) -> (!tt.ptr>) { +// CHECK: %[[VAL_12:.*]] = tt.advance %[[VAL_11]], [%[[VAL_4]], %[[VAL_1]]] : > +// CHECK: scf.yield %[[VAL_12]] : !tt.ptr> +// CHECK: } +// CHECK: tt.return %[[VAL_9]] : !tt.ptr> +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/SplatCmpConverter.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/SplatCmpConverter.mlir new file mode 100644 index 0000000000..f046891ec1 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/SplatCmpConverter.mlir @@ -0,0 +1,16 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --split-input-file %s | FileCheck %s +module { + tt.func public @test_splat_cmp(%arg0: i32, %arg1: i32) -> tensor<128xi1> { + %0 = tt.splat %arg0 : i32 -> tensor<128xi32> + %1 = tt.splat %arg1 : i32 -> tensor<128xi32> + %2 = arith.cmpi slt, %0, %1 : tensor<128xi32> + tt.return %2 : tensor<128xi1> + } +} + +// CHECK-LABEL: tt.func public @test_splat_cmp( +// CHECK-SAME: %[[VAL_0:.*]]: i32, %[[VAL_1:.*]]: i32) -> tensor<128xi1> { +// CHECK: %[[VAL_2:.*]] = arith.cmpi slt, %[[VAL_0]], %[[VAL_1]] : i32 +// CHECK: %[[VAL_3:.*]] = tt.splat %[[VAL_2]] : i1 -> tensor<128xi1> +// CHECK: tt.return %[[VAL_3]] : tensor<128xi1> +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/StoreBroadcastConverter.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/StoreBroadcastConverter.mlir new file mode 100644 index 0000000000..a8e03dcf0d --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/StoreBroadcastConverter.mlir @@ -0,0 +1,98 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --triton-to-linalg --split-input-file %s | FileCheck %s + +// Tests for StoreBroadcastConverter: when store.ptr is defined by a +// tt.broadcast, every pointer along the broadcast axis points to the same +// memory location. The converter rewrites to store only the first index +// along each broadcast axis via tensor.extract_slice, using the broadcast +// source pointer. + +// ----- + +// Case 2: ptr broadcast [1x64] -> [256x64] (axis 0), mask broadcast +// [1x64] -> [256x64] (axis 0). Same broadcast axis → mask's broadcast +// source shape [1x64] matches ptr's broadcast source shape [1x64], so +// the mask src is reused directly (no extract_slice for mask). +module { + tt.func public @kernel_store_same_axis_broadcast(%X_ptr: !tt.ptr, %Out_ptr: !tt.ptr, %YM: i32) attributes {noinline = false} { + %c64_i32 = arith.constant 64 : i32 + %offsety = tt.get_program_id y : i32 + %offsety_0 = arith.muli %offsety, %c64_i32 : i32 + %offsety_1 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %offsety_2 = tt.splat %offsety_0 : i32 -> tensor<64xi32> + %offsety_3 = arith.addi %offsety_2, %offsety_1 : tensor<64xi32> + %val = tt.expand_dims %offsety_3 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %val_4 = tt.splat %X_ptr : !tt.ptr -> tensor<1x64x!tt.ptr> + %val_5 = tt.addptr %val_4, %val : tensor<1x64x!tt.ptr>, tensor<1x64xi32> + %val_6 = tt.load %val_5 : tensor<1x64x!tt.ptr> + %val_7 = tt.broadcast %val_6 : tensor<1x64xf32> -> tensor<256x64xf32> + %ptr = tt.splat %Out_ptr : !tt.ptr -> tensor<1x64x!tt.ptr> + %ptr_8 = tt.addptr %ptr, %val : tensor<1x64x!tt.ptr>, tensor<1x64xi32> + %ptr_9 = tt.broadcast %ptr_8 : tensor<1x64x!tt.ptr> -> tensor<256x64x!tt.ptr> + %mask = tt.splat %YM : i32 -> tensor<64xi32> + %mask_10 = arith.cmpi slt, %offsety_3, %mask : tensor<64xi32> + %mask_11 = tt.expand_dims %mask_10 {axis = 0 : i32} : tensor<64xi1> -> tensor<1x64xi1> + %0 = tt.broadcast %mask_11 : tensor<1x64xi1> -> tensor<256x64xi1> + tt.store %ptr_9, %val_7, %0 : tensor<256x64x!tt.ptr> + tt.return + } +} +// CHECK-LABEL: func.func @kernel_store_same_axis_broadcast +// CHECK: %[[VAL1:.*]] = tensor.extract_slice %{{.*}}[0, 0] [1, %{{.*}}] [1, 1] : tensor<1x64xf32> to tensor<1x?xf32> +// CHECK: %[[VAL2:.*]]= memref.subview %{{.*}}[0, 0] [1, %{{.*}}] [1, 1] : memref<1x64xf32, strided<[64, 1], offset: ?>> to memref<1x?xf32, strided<[64, 1], offset: ?>> +// CHECK: bufferization.materialize_in_destination %[[VAL1]] in writable %[[VAL2]]: (tensor<1x?xf32>, memref<1x?xf32, strided<[64, 1], offset: ?>>) -> () + +// ----- + +// Case 3: ptr broadcast [1x64] -> [256x64] (axis 0), no mask. +// StoreBroadcastConverter still fires: value gets extract_slice to [1x64], +// and the store uses the broadcast source pointer without mask. +module { + tt.func public @kernel_store_no_mask_broadcast(%X_ptr: !tt.ptr, %Out_ptr: !tt.ptr) attributes {noinline = false} { + %c64_i32 = arith.constant 64 : i32 + %offsety = tt.get_program_id y : i32 + %offsety_0 = arith.muli %offsety, %c64_i32 : i32 + %offsety_1 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %offsety_2 = tt.splat %offsety_0 : i32 -> tensor<64xi32> + %offsety_3 = arith.addi %offsety_2, %offsety_1 : tensor<64xi32> + %val = tt.expand_dims %offsety_3 {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %val_4 = tt.splat %X_ptr : !tt.ptr -> tensor<1x64x!tt.ptr> + %val_5 = tt.addptr %val_4, %val : tensor<1x64x!tt.ptr>, tensor<1x64xi32> + %val_6 = tt.load %val_5 : tensor<1x64x!tt.ptr> + %val_7 = tt.broadcast %val_6 : tensor<1x64xf32> -> tensor<256x64xf32> + %ptr = tt.splat %Out_ptr : !tt.ptr -> tensor<1x64x!tt.ptr> + %ptr_8 = tt.addptr %ptr, %val : tensor<1x64x!tt.ptr>, tensor<1x64xi32> + %ptr_9 = tt.broadcast %ptr_8 : tensor<1x64x!tt.ptr> -> tensor<256x64x!tt.ptr> + tt.store %ptr_9, %val_7 : tensor<256x64x!tt.ptr> + tt.return + } +} +// CHECK-LABEL: func.func @kernel_store_no_mask_broadcast +// CHECK: %[[VAL1:.*]]= memref.reinterpret_cast %{{.*}}to offset: [%{{.*}}], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> +// CHECK: %[[VAL2:.*]]= memref.reinterpret_cast %{{.*}}to offset: [%{{.*}}], sizes: [1, 64], strides: [64, 1] : memref to memref<1x64xf32, strided<[64, 1], offset: ?>> +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %[[VAL2]]: (tensor<1x64xf32>, memref<1x64xf32, strided<[64, 1], offset: ?>>) + + +// ----- + +// The pointer is the canonical form of a non-singleton zero-stride dimension: +// an addptr over the non-zero-stride axis, followed by a one-dimensional +// pointer broadcast. StoreConverter must defer this legal form to the later +// pointer-broadcast lowering instead of rebuilding a lower-rank store. +module { + tt.func public @zero_stride_store_broadcast( + %arg0: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %offsets = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32> + %offsets_2d = tt.expand_dims %offsets {axis = 0 : i32} : tensor<64xi32> -> tensor<1x64xi32> + %ptr_splat = tt.splat %arg0 : !tt.ptr -> tensor<1x64x!tt.ptr> + %ptr_small = tt.addptr %ptr_splat, %offsets_2d : tensor<1x64x!tt.ptr>, tensor<1x64xi32> + %ptr = tt.broadcast %ptr_small : tensor<1x64x!tt.ptr> -> tensor<16x64x!tt.ptr> + %zero = arith.constant 0.000000e+00 : f32 + %value = tt.splat %zero : f32 -> tensor<16x64xf32> + %mask = arith.constant dense : tensor<16x64xi1> + tt.store %ptr, %value, %mask : tensor<16x64x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: func.func @zero_stride_store_broadcast +// CHECK: bufferization.materialize_in_destination %{{.*}} in writable %{{.*}} : (tensor<1x64xf32>, memref<1x64xf32, strided<[64, 1]>>) -> () diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseCmp.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseCmp.mlir new file mode 100644 index 0000000000..abdae3fa16 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseCmp.mlir @@ -0,0 +1,117 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --split-input-file %s | FileCheck %s + +tt.func public @test_cmp_ult(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<1024xf32> + %cst_0 = arith.constant dense<512> : tensor<1024xi32> + %c1024_i32 = arith.constant 1024 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c1024_i32 {tt.divisibility = dense<512> : tensor<1xi32>} : i32 + %2 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %3 = tt.splat %1 : i32 -> tensor<1024xi32> + %4 = arith.addi %3, %2 : tensor<1024xi32> + %5 = arith.divsi %4, %cst_0 : tensor<1024xi32> + %6 = arith.cmpi ult, %5, %cst_0 : tensor<1024xi32> + %7 = arith.muli %5, %cst_0 : tensor<1024xi32> + %8 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %9 = tt.addptr %8, %7 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %10 = tt.load %9, %6, %cst : tensor<1024x!tt.ptr> + %11 = arith.muli %0, %c1024_i32 : i32 + %12 = tt.splat %11 : i32 -> tensor<1024xi32> + %13 = arith.addi %12, %2 : tensor<1024xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %15, %10 : tensor<1024x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @test_cmp_ult( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_1:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { +// CHECK: %[[VAL_2:.*]] = arith.constant dense<1024> : tensor<1xi64> +// CHECK: %[[VAL_3:.*]] = arith.constant dense<0.000000e+00> : tensor<2xf32> +// CHECK: %[[VAL_4:.*]] = arith.constant dense<512> : tensor<2xi32> +// CHECK: %[[VAL_5:.*]] = arith.constant 512 : index +// CHECK: %[[VAL_6:.*]] = arith.constant 1024 : i32 +// CHECK: %[[VAL_7:.*]] = tt.get_program_id x : i32 +// CHECK: %[[VAL_8:.*]] = arith.muli %[[VAL_7]], %[[VAL_6]] {tt.divisibility = dense<512> : tensor<1xi32>} : i32 +// CHECK: %[[VAL_9:.*]] = arith.index_cast %[[VAL_8]] : i32 to index +// CHECK: %[[VAL_10:.*]] = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> +// CHECK: %[[VAL_11:.*]] = arith.divsi %[[VAL_9]], %[[VAL_5]] : index +// CHECK: %[[VAL_12:.*]] = arith.muli %[[VAL_11]], %[[VAL_5]] : index +// CHECK: %[[VAL_13:.*]] = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> +// CHECK: %[[VAL_14:.*]] = arith.muli %[[VAL_13]], %[[VAL_4]] : tensor<2xi32> +// CHECK: %[[VAL_15:.*]] = arith.index_cast %[[VAL_12]] : index to i32 +// CHECK: %[[VAL_16:.*]] = tt.splat %[[VAL_15]] : i32 -> tensor<2xi32> +// CHECK: %[[VAL_17:.*]] = arith.addi %[[VAL_14]], %[[VAL_16]] : tensor<2xi32> +// CHECK: %[[VAL_18:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<2x!tt.ptr> +// CHECK: %[[VAL_19:.*]] = tt.addptr %[[VAL_18]], %[[VAL_17]] : tensor<2x!tt.ptr>, tensor<2xi32> +// CHECK: %[[VAL_20:.*]] = arith.index_cast %[[VAL_11]] : index to i32 +// CHECK: %[[VAL_21:.*]] = tt.splat %[[VAL_20]] : i32 -> tensor<2xi32> +// CHECK: %[[VAL_22:.*]] = arith.addi %[[VAL_13]], %[[VAL_21]] : tensor<2xi32> +// CHECK: %[[VAL_23:.*]] = arith.cmpi slt, %[[VAL_22]], %[[VAL_4]] : tensor<2xi32> +// CHECK: %[[VAL_24:.*]] = tt.load %[[VAL_19]], %[[VAL_23]], %[[VAL_3]] : tensor<2x!tt.ptr> +// CHECK: %[[VAL_25:.*]] = tensor.empty() : tensor<2x512xf32> +// CHECK: %[[VAL_26:.*]] = linalg.broadcast ins(%[[VAL_24]] : tensor<2xf32>) outs(%[[VAL_25]] : tensor<2x512xf32>) dimensions = [1] +// CHECK: %[[VAL_27:.*]] = tensor.reshape %[[VAL_26]](%[[VAL_2]]) : (tensor<2x512xf32>, tensor<1xi64>) -> tensor<1024xf32> +// CHECK: %[[VAL_28:.*]] = arith.muli %[[VAL_7]], %[[VAL_6]] : i32 +// CHECK: %[[VAL_29:.*]] = tt.splat %[[VAL_28]] : i32 -> tensor<1024xi32> +// CHECK: %[[VAL_30:.*]] = arith.addi %[[VAL_29]], %[[VAL_10]] : tensor<1024xi32> +// CHECK: %[[VAL_31:.*]] = tt.splat %[[VAL_1]] : !tt.ptr -> tensor<1024x!tt.ptr> +// CHECK: %[[VAL_32:.*]] = tt.addptr %[[VAL_31]], %[[VAL_30]] : tensor<1024x!tt.ptr>, tensor<1024xi32> +// CHECK: tt.store %[[VAL_32]], %[[VAL_27]] : tensor<1024x!tt.ptr> +// CHECK: tt.return +// CHECK: } + + +// ----- + + +tt.func public @test_cmp_uge(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<1024xf32> + %cst_0 = arith.constant dense<511> : tensor<1024xi32> + %cst_1 = arith.constant dense<512> : tensor<1024xi32> + %c1024_i32 = arith.constant 1024 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c1024_i32 {tt.divisibility = dense<512> : tensor<1xi32>} : i32 + %2 = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> + %3 = tt.splat %1 : i32 -> tensor<1024xi32> + %4 = arith.addi %3, %2 : tensor<1024xi32> + %5 = arith.divsi %4, %cst_1 : tensor<1024xi32> + %6 = arith.cmpi uge, %cst_0, %5 : tensor<1024xi32> + %7 = arith.muli %5, %cst_1 : tensor<1024xi32> + %8 = tt.splat %arg0 : !tt.ptr -> tensor<1024x!tt.ptr> + %9 = tt.addptr %8, %7 : tensor<1024x!tt.ptr>, tensor<1024xi32> + %10 = tt.load %9, %6, %cst : tensor<1024x!tt.ptr> + %11 = arith.muli %0, %c1024_i32 : i32 + %12 = tt.splat %11 : i32 -> tensor<1024xi32> + %13 = arith.addi %12, %2 : tensor<1024xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<1024x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<1024x!tt.ptr>, tensor<1024xi32> + tt.store %15, %10 : tensor<1024x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @test_cmp_uge( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_1:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { +// CHECK: %[[VAL_2:.*]] = arith.constant dense<0.000000e+00> : tensor<1024xf32> +// CHECK: %[[VAL_3:.*]] = arith.constant dense<511> : tensor<1024xi32> +// CHECK: %[[VAL_4:.*]] = arith.constant dense<512> : tensor<1024xi32> +// CHECK: %[[VAL_5:.*]] = arith.constant 1024 : i32 +// CHECK: %[[VAL_6:.*]] = tt.get_program_id x : i32 +// CHECK: %[[VAL_7:.*]] = arith.muli %[[VAL_6]], %[[VAL_5]] {tt.divisibility = dense<512> : tensor<1xi32>} : i32 +// CHECK: %[[VAL_8:.*]] = tt.make_range {end = 1024 : i32, start = 0 : i32} : tensor<1024xi32> +// CHECK: %[[VAL_9:.*]] = tt.splat %[[VAL_7]] : i32 -> tensor<1024xi32> +// CHECK: %[[VAL_10:.*]] = arith.addi %[[VAL_9]], %[[VAL_8]] : tensor<1024xi32> +// CHECK: %[[VAL_11:.*]] = arith.divsi %[[VAL_10]], %[[VAL_4]] : tensor<1024xi32> +// CHECK: %[[VAL_12:.*]] = arith.cmpi ule, %[[VAL_11]], %[[VAL_3]] : tensor<1024xi32> +// CHECK: %[[VAL_13:.*]] = arith.muli %[[VAL_11]], %[[VAL_4]] : tensor<1024xi32> +// CHECK: %[[VAL_14:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<1024x!tt.ptr> +// CHECK: %[[VAL_15:.*]] = tt.addptr %[[VAL_14]], %[[VAL_13]] : tensor<1024x!tt.ptr>, tensor<1024xi32> +// CHECK: %[[VAL_16:.*]] = tt.load %[[VAL_15]], %[[VAL_12]], %[[VAL_2]] : tensor<1024x!tt.ptr> +// CHECK: %[[VAL_17:.*]] = arith.muli %[[VAL_6]], %[[VAL_5]] : i32 +// CHECK: %[[VAL_18:.*]] = tt.splat %[[VAL_17]] : i32 -> tensor<1024xi32> +// CHECK: %[[VAL_19:.*]] = arith.addi %[[VAL_18]], %[[VAL_8]] : tensor<1024xi32> +// CHECK: %[[VAL_20:.*]] = tt.splat %[[VAL_1]] : !tt.ptr -> tensor<1024x!tt.ptr> +// CHECK: %[[VAL_21:.*]] = tt.addptr %[[VAL_20]], %[[VAL_19]] : tensor<1024x!tt.ptr>, tensor<1024xi32> +// CHECK: tt.store %[[VAL_21]], %[[VAL_16]] : tensor<1024x!tt.ptr> +// CHECK: tt.return +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseConstant.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseConstant.mlir new file mode 100644 index 0000000000..4e1053793d --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseConstant.mlir @@ -0,0 +1,28 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --split-input-file %s | FileCheck %s + +tt.func public @test_non_splat_mask(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) { + %non_splat_mask = arith.constant dense<[false, true]> : tensor<2xi1> + %c0_f32 = arith.constant dense<0.000000e+00> : tensor<2xf32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.splat %arg0 : !tt.ptr -> tensor<2x!tt.ptr> + %ptr_load = tt.addptr %1, %0 : tensor<2x!tt.ptr>, tensor<2xi32> + %2 = tt.load %ptr_load, %non_splat_mask, %c0_f32 : tensor<2x!tt.ptr> + %3 = tt.splat %arg1 : !tt.ptr -> tensor<2x!tt.ptr> + %ptr_store = tt.addptr %3, %0 : tensor<2x!tt.ptr>, tensor<2xi32> + tt.store %ptr_store, %2 : tensor<2x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @test_non_splat_mask( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_1:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) { +// CHECK: %[[VAL_2:.*]] = arith.constant dense<[false, true]> : tensor<2xi1> +// CHECK: %[[VAL_3:.*]] = arith.constant dense<0.000000e+00> : tensor<2xf32> +// CHECK: %[[VAL_4:.*]] = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> +// CHECK: %[[VAL_5:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<2x!tt.ptr> +// CHECK: %[[VAL_6:.*]] = tt.addptr %[[VAL_5]], %[[VAL_4]] : tensor<2x!tt.ptr>, tensor<2xi32> +// CHECK: %[[VAL_7:.*]] = tt.load %[[VAL_6]], %[[VAL_2]], %[[VAL_3]] : tensor<2x!tt.ptr> +// CHECK: %[[VAL_8:.*]] = tt.splat %[[VAL_1]] : !tt.ptr -> tensor<2x!tt.ptr> +// CHECK: %[[VAL_9:.*]] = tt.addptr %[[VAL_8]], %[[VAL_4]] : tensor<2x!tt.ptr>, tensor<2xi32> +// CHECK: tt.store %[[VAL_9]], %[[VAL_7]] : tensor<2x!tt.ptr> +// CHECK: tt.return +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseMakeRange.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseMakeRange.mlir new file mode 100644 index 0000000000..efdaad3204 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseMakeRange.mlir @@ -0,0 +1,24 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --split-input-file %s | FileCheck %s + +module { + tt.func public @test_stride_not_one(%arg0: !tt.ptr {tt.divisibility = 16 : i32}) { + %c0_f32 = arith.constant dense<0.000000e+00> : tensor<4xf32> + %fake_range_mask = arith.constant dense<[false, false, false, false]> : tensor<4xi1> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.splat %arg0 : !tt.ptr -> tensor<4x!tt.ptr> + %ptr = tt.addptr %1, %0 : tensor<4x!tt.ptr>, tensor<4xi32> + %2 = tt.load %ptr, %fake_range_mask, %c0_f32 : tensor<4x!tt.ptr> + tt.store %ptr, %2 : tensor<4x!tt.ptr> + tt.return + } +} + +// CHECK-LABEL: tt.func public @test_stride_not_one( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) { +// CHECK: %[[VAL_1:.*]] = arith.constant dense<0.000000e+00> : tensor<4xf32> +// CHECK: %[[VAL_2:.*]] = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> +// CHECK: %[[VAL_3:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<4x!tt.ptr> +// CHECK: %[[VAL_4:.*]] = tt.addptr %[[VAL_3]], %[[VAL_2]] : tensor<4x!tt.ptr>, tensor<4xi32> +// CHECK: tt.store %[[VAL_4]], %[[VAL_1]] : tensor<4x!tt.ptr> +// CHECK: tt.return +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseRem.mlir b/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseRem.mlir new file mode 100644 index 0000000000..b48d7fc288 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToStructured/parseRem.mlir @@ -0,0 +1,81 @@ +// RUN: triton-opt '--triton-to-structured=enable-mask-fallback-conversion=false optimize-dynamic-offset=true' --split-input-file %s | FileCheck %s + +tt.func public @kernel_with_rem_safe(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<0.000000e+00> : tensor<256xf32> + %cst_0 = arith.constant dense<1024> : tensor<256xi32> + %c256_i32 = arith.constant 256 : i32 + %cst_1 = arith.constant dense<64> : tensor<256xi32> + %cst_2 = arith.constant dense<128> : tensor<256xi32> + %0 = tt.get_program_id x : i32 + %1 = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> + %2 = arith.remsi %1, %cst_2 : tensor<256xi32> + %3 = arith.cmpi slt, %2, %cst_1 : tensor<256xi32> + %4 = arith.muli %0, %c256_i32 : i32 + %5 = tt.splat %4 : i32 -> tensor<256xi32> + %6 = arith.addi %5, %1 : tensor<256xi32> + %7 = arith.cmpi slt, %6, %cst_0 : tensor<256xi32> + %8 = arith.andi %3, %7 : tensor<256xi1> + %9 = tt.splat %arg0 : !tt.ptr -> tensor<256x!tt.ptr> + %10 = tt.addptr %9, %2 : tensor<256x!tt.ptr>, tensor<256xi32> + %11 = tt.load %10, %8, %cst : tensor<256x!tt.ptr> + %12 = tt.splat %arg1 : !tt.ptr -> tensor<256x!tt.ptr> + %13 = tt.addptr %12, %1 : tensor<256x!tt.ptr>, tensor<256xi32> + tt.store %13, %11, %8 : tensor<256x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @kernel_with_rem_safe( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_1:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { +// CHECK: %[[VAL_2:.*]] = arith.constant dense<0.000000e+00> : tensor<256xf32> +// CHECK: %[[VAL_3:.*]] = arith.constant dense<1024> : tensor<256xi32> +// CHECK: %[[VAL_4:.*]] = arith.constant 256 : i32 +// CHECK: %[[VAL_5:.*]] = arith.constant dense<64> : tensor<256xi32> +// CHECK: %[[VAL_6:.*]] = arith.constant dense<128> : tensor<256xi32> +// CHECK: %[[VAL_7:.*]] = tt.get_program_id x : i32 +// CHECK: %[[VAL_8:.*]] = tt.make_range {end = 256 : i32, start = 0 : i32} : tensor<256xi32> +// CHECK: %[[VAL_9:.*]] = arith.remsi %[[VAL_8]], %[[VAL_6]] : tensor<256xi32> +// CHECK: %[[VAL_10:.*]] = arith.cmpi slt, %[[VAL_9]], %[[VAL_5]] : tensor<256xi32> +// CHECK: %[[VAL_11:.*]] = arith.muli %[[VAL_7]], %[[VAL_4]] : i32 +// CHECK: %[[VAL_12:.*]] = tt.splat %[[VAL_11]] : i32 -> tensor<256xi32> +// CHECK: %[[VAL_13:.*]] = arith.addi %[[VAL_12]], %[[VAL_8]] : tensor<256xi32> +// CHECK: %[[VAL_14:.*]] = arith.cmpi slt, %[[VAL_13]], %[[VAL_3]] : tensor<256xi32> +// CHECK: %[[VAL_15:.*]] = arith.andi %[[VAL_10]], %[[VAL_14]] : tensor<256xi1> +// CHECK: %[[VAL_16:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<256x!tt.ptr> +// CHECK: %[[VAL_17:.*]] = tt.addptr %[[VAL_16]], %[[VAL_9]] : tensor<256x!tt.ptr>, tensor<256xi32> +// CHECK: %[[VAL_18:.*]] = tt.load %[[VAL_17]], %[[VAL_15]], %[[VAL_2]] : tensor<256x!tt.ptr> +// CHECK: %[[VAL_19:.*]] = tt.splat %[[VAL_1]] : !tt.ptr -> tensor<256x!tt.ptr> +// CHECK: %[[VAL_20:.*]] = tt.addptr %[[VAL_19]], %[[VAL_8]] : tensor<256x!tt.ptr>, tensor<256xi32> +// CHECK: tt.store %[[VAL_20]], %[[VAL_18]], %[[VAL_15]] : tensor<256x!tt.ptr> +// CHECK: tt.return +// CHECK: } + + +// ----- + + +tt.func public @test_remsi_with_broadcast(%arg0: !tt.ptr {tt.divisibility = 16 : i32}) -> tensor<2x2xf32> { + %c0_f32 = arith.constant dense<0.000000e+00> : tensor<2x2xf32> + %c4 = arith.constant dense<4> : tensor<2x2xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> + %2 = tt.broadcast %1 : tensor<1x2xi32> -> tensor<2x2xi32> + %3 = arith.remsi %2, %c4 : tensor<2x2xi32> + %4 = arith.trunci %3 : tensor<2x2xi32> to tensor<2x2xi1> + %ptrs = tt.splat %arg0 : !tt.ptr -> tensor<2x2x!tt.ptr> + %vals = tt.load %ptrs, %4, %c0_f32 : tensor<2x2x!tt.ptr> + tt.return %vals : tensor<2x2xf32> +} + +// CHECK-LABEL: tt.func public @test_remsi_with_broadcast( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) -> tensor<2x2xf32> { +// CHECK: %[[VAL_1:.*]] = arith.constant dense<0.000000e+00> : tensor<2x2xf32> +// CHECK: %[[VAL_2:.*]] = arith.constant dense<4> : tensor<2x2xi32> +// CHECK: %[[VAL_3:.*]] = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> +// CHECK: %[[VAL_4:.*]] = tt.expand_dims %[[VAL_3]] {axis = 0 : i32} : tensor<2xi32> -> tensor<1x2xi32> +// CHECK: %[[VAL_5:.*]] = tt.broadcast %[[VAL_4]] : tensor<1x2xi32> -> tensor<2x2xi32> +// CHECK: %[[VAL_6:.*]] = arith.remsi %[[VAL_5]], %[[VAL_2]] : tensor<2x2xi32> +// CHECK: %[[VAL_7:.*]] = arith.trunci %[[VAL_6]] : tensor<2x2xi32> to tensor<2x2xi1> +// CHECK: %[[VAL_8:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<2x2x!tt.ptr> +// CHECK: %[[VAL_9:.*]] = tt.load %[[VAL_8]], %[[VAL_7]], %[[VAL_1]] : tensor<2x2x!tt.ptr> +// CHECK: tt.return %[[VAL_9]] : tensor<2x2xf32> +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/bubbleupoperation.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/bubbleupoperation.mlir new file mode 100644 index 0000000000..6567367982 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/bubbleupoperation.mlir @@ -0,0 +1,172 @@ +// RUN: triton-opt %s --bubble-up-operation | FileCheck %s + +// CHECK-LABEL: tt.func @test_subi_extract_bubbleup +tt.func @test_subi_extract_bubbleup(%a: tensor<128xi32>, %b: tensor<128xi32>, %i: index, %c: i32) -> i32 { + %0 = arith.subi %a, %b : tensor<128xi32> + %1 = tensor.extract %0[%i] : tensor<128xi32> + %2 = arith.muli %1, %c : i32 + tt.return %2 : i32 +} + + +// CHECK-LABEL: tt.func @test_maxsi_extract_bubbleup +tt.func @test_maxsi_extract_bubbleup(%a: tensor<128xi32>, %b: tensor<128xi32>, %i: index, %c: i32) -> i32 { + %0 = arith.maxsi %a, %b : tensor<128xi32> + %1 = tensor.extract %0[%i] : tensor<128xi32> + %2 = arith.muli %1, %c : i32 + tt.return %2 : i32 +} + + +// CHECK-LABEL: tt.func @test_minsi_extract_bubbleup +tt.func @test_minsi_extract_bubbleup(%a: tensor<128xi32>, %b: tensor<128xi32>, %i: index, %c: i32) -> i32 { + %0 = arith.minsi %a, %b : tensor<128xi32> + %1 = tensor.extract %0[%i] : tensor<128xi32> + %2 = arith.muli %1, %c : i32 + tt.return %2 : i32 +} + + +// CHECK-LABEL: tt.func @test_extf_extract_bubbleup +tt.func @test_extf_extract_bubbleup(%a: tensor<128xf16>, %i: index, %c: f32) -> f32 { + %0 = arith.extf %a : tensor<128xf16> to tensor<128xf32> + %1 = tensor.extract %0[%i] : tensor<128xf32> + %2 = arith.mulf %1, %c : f32 + tt.return %2 : f32 +} + + +// CHECK-LABEL: tt.func @test_minnumf_extract_bubbleup +tt.func @test_minnumf_extract_bubbleup(%a: tensor<128xf32>, %b: tensor<128xf32>, %i: index, %c: f32) -> f32 { + %0 = arith.minnumf %a, %b : tensor<128xf32> + %1 = tensor.extract %0[%i] : tensor<128xf32> + %2 = arith.mulf %1, %c : f32 + tt.return %2 : f32 +} + + +// CHECK-LABEL: tt.func @test_maxnumf_extract_bubbleup +tt.func @test_maxnumf_extract_bubbleup(%a: tensor<128xf32>, %b: tensor<128xf32>, %i: index, %c: f32) -> f32 { + %0 = arith.maxnumf %a, %b : tensor<128xf32> + %1 = tensor.extract %0[%i] : tensor<128xf32> + %2 = arith.mulf %1, %c : f32 + tt.return %2 : f32 +} + + +// CHECK-LABEL: tt.func @test_cmpf_extract_bubbleup +tt.func @test_cmpf_extract_bubbleup(%a: tensor<128xf32>, %b: tensor<128xf32>, %i: index) -> i1 { + %0 = arith.cmpf olt, %a, %b : tensor<128xf32> + %1 = tensor.extract %0[%i] : tensor<128xi1> + tt.return %1 : i1 +} + + +// CHECK-LABEL: tt.func @test_addptr_extract_bubbleup +tt.func @test_addptr_extract_bubbleup(%a: tensor<128x!tt.ptr>, %b: tensor<128xi32>, %i: index) -> !tt.ptr { + %0 = tt.addptr %a, %b : tensor<128x!tt.ptr>, tensor<128xi32> + %1 = tensor.extract %0[%i] : tensor<128x!tt.ptr> + tt.return %1 : !tt.ptr +} + +// CHECK-LABEL: tt.func @test_select_pointer_extract_bubbleup +// CHECK: %[[CONDITION:.*]] = tensor.extract %{{.*}}[%{{.*}}] {DiscreteMemAccess} : tensor<4xi1> +// CHECK: %[[TRUE_VALUE:.*]] = tensor.extract %{{.*}}[%{{.*}}] {DiscreteMemAccess} : tensor<4x!tt.ptr> +// CHECK: %[[FALSE_VALUE:.*]] = tensor.extract %{{.*}}[%{{.*}}] {DiscreteMemAccess} : tensor<4x!tt.ptr> +// CHECK: arith.select %[[CONDITION]], %[[TRUE_VALUE]], %[[FALSE_VALUE]] : !tt.ptr +tt.func @test_select_pointer_extract_bubbleup( + %condition: tensor<4xi1>, %true_value: tensor<4x!tt.ptr>, + %false_value: tensor<4x!tt.ptr>, %i: index) -> !tt.ptr { + %selected = arith.select %condition, %true_value, %false_value : tensor<4xi1>, tensor<4x!tt.ptr> + %pointer = tensor.extract %selected[%i] : tensor<4x!tt.ptr> + tt.return %pointer : !tt.ptr +} + +// CHECK-LABEL: tt.func @test_discrete_masked_select_extract_not_bubbled +// CHECK: %[[SELECTED:.*]] = arith.select %{{.*}}, %{{.*}}, %{{.*}} {DiscreteMemAccess} : tensor<4xi1>, tensor<4xf32> +// CHECK: %[[VALUE:.*]] = tensor.extract %[[SELECTED]][%{{.*}}] : tensor<4xf32> +// CHECK: tt.return %[[VALUE]] : f32 +tt.func @test_discrete_masked_select_extract_not_bubbled( + %condition: tensor<4xi1>, %true_value: tensor<4xf32>, + %false_value: tensor<4xf32>, %i: index) -> f32 { + %selected = arith.select %condition, %true_value, %false_value {DiscreteMemAccess} : tensor<4xi1>, tensor<4xf32> + %value = tensor.extract %selected[%i] : tensor<4xf32> + tt.return %value : f32 +} + +// CHECK-LABEL: tt.func @test_discrete_load_loop_select_extract_not_bubbled +// CHECK: %[[LOADED:.*]] = scf.for +// CHECK: } {ExtractedLoadOrStore} +// CHECK: %[[SELECTED:.*]] = arith.select %{{.*}}, %[[LOADED]], %{{.*}} : tensor<4xi1>, tensor<4xf32> +// CHECK: %[[VALUE:.*]] = tensor.extract %[[SELECTED]][%{{.*}}] : tensor<4xf32> +// CHECK: tt.return %[[VALUE]] : f32 +tt.func @test_discrete_load_loop_select_extract_not_bubbled( + %condition: tensor<4xi1>, %initial: tensor<4xf32>, + %other: tensor<4xf32>, %loaded_element: f32, %i: index) -> f32 { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c4 = arith.constant 4 : index + %loaded = scf.for %iv = %c0 to %c4 step %c1 iter_args(%result = %initial) -> tensor<4xf32> { + %inserted = tensor.insert %loaded_element into %result[%iv] : tensor<4xf32> + scf.yield %inserted : tensor<4xf32> + } {ExtractedLoadOrStore} + %selected = arith.select %condition, %loaded, %other : tensor<4xi1>, tensor<4xf32> + %value = tensor.extract %selected[%i] : tensor<4xf32> + tt.return %value : f32 +} + +// CHECK-LABEL: tt.func @test_ceil_extract_bubbleup +tt.func @test_ceil_extract_bubbleup(%a: tensor<128xf32>, %i: index, %c: f32) -> f32 { + %0 = math.ceil %a : tensor<128xf32> + %1 = tensor.extract %0[%i] : tensor<128xf32> + %2 = arith.mulf %1, %c : f32 + tt.return %2 : f32 +} + + +// CHECK-LABEL: tt.func @test_slice_extract_dropdim_bubbleup +tt.func @test_slice_extract_dropdim_bubbleup(%a: tensor<128x128x128xf32>, %i: index, %j: index) -> f32 { + %0 = tensor.extract_slice %a[0, %i, 0][1, 1, 128][1, 1, 1] : tensor<128x128x128xf32> to tensor<128xf32> + %1 = tensor.extract %0[%j] : tensor<128xf32> + tt.return %1 : f32 +} + + +// CHECK-LABEL: tt.func @test_expand_slice_bubbleup +tt.func @test_expand_slice_bubbleup(%a: tensor<128xf32>, %i: index, %c: f32) -> tensor<1x1xf32> { + %0 = tt.expand_dims %a {axis = 0 : i32} : tensor<128xf32> -> tensor<1x128xf32> + %1 = tensor.extract_slice %0[0, %i][1, 1][1, 1] : tensor<1x128xf32> to tensor<1x1xf32> + tt.return %1 : tensor<1x1xf32> +} + + +// CHECK-LABEL: tt.func @test_expand_slice_dropdim_bubbleup +tt.func @test_expand_slice_dropdim_bubbleup(%a: tensor<128x128xf32>, %i: index, %c: f32) -> tensor<128x1xf32> { + %0 = tt.expand_dims %a {axis = 2 : i32} : tensor<128x128xf32> -> tensor<128x128x1xf32> + %1 = tensor.extract_slice %0[%i, 0, 0][1, 128, 1][1, 1, 1] : tensor<128x128x1xf32> to tensor<128x1xf32> + tt.return %1 : tensor<128x1xf32> +} + + +// CHECK-LABEL: tt.func @test_splat_slice_bubbleup +tt.func @test_splat_slice_bubbleup(%a: f32, %i: index, %c: f32) -> tensor<1xf32> { + %0 = tt.splat %a : f32 -> tensor<128xf32> + %1 = tensor.extract_slice %0[%i][1][1] : tensor<128xf32> to tensor<1xf32> + tt.return %1 : tensor<1xf32> +} + + +// CHECK-LABEL: tt.func @test_makerange_slice_bubbleup +tt.func @test_makerange_slice_bubbleup(%i: index, %c: f32) -> tensor<1xi32> { + %0 = tt.make_range {start = 0 : i32, end = 128 : i32} : tensor<128xi32> + %1 = tensor.extract_slice %0[%i][1][1] : tensor<128xi32> to tensor<1xi32> + tt.return %1 : tensor<1xi32> +} + + +// CHECK-LABEL: tt.func @test_slice_all_bubbleup +tt.func @test_slice_all_bubbleup(%i: index, %c: f32) -> tensor<128xi32> { + %0 = tt.make_range {start = 0 : i32, end = 128 : i32} : tensor<128xi32> + %1 = tensor.extract_slice %0[0][128][1] : tensor<128xi32> to tensor<128xi32> + tt.return %1 : tensor<128xi32> +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/custom_op_offset.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/custom_op_offset.mlir new file mode 100644 index 0000000000..e7d7a947e4 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/custom_op_offset.mlir @@ -0,0 +1,28 @@ +// RUN: triton-opt %s --triton-to-unstructure | FileCheck %s + +// Test hivm.hir.custom with distributed type and ContinuousMemAccess annotation +// The annotation is processed by checkStructureAnnotated and erased during analysis +// CHECK-LABEL: tt.func @test_custom_op_offset +// CHECK: hivm.hir.custom +// CHECK-SAME: hivm.is_distributed +// CHECK-NOT: annotation.mark + +tt.func @test_custom_op_offset(%arg0: !tt.ptr, %arg1: !tt.ptr) -> tensor<8xf32> { + %0 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %1 = tt.splat %arg0 : !tt.ptr -> tensor<8x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<8x!tt.ptr>, tensor<8xi32> + %empty = tensor.empty() : tensor<8x!tt.ptr> + + // hivm.hir.custom with distributed type producing tensor output + %3 = hivm.hir.custom + {hivm.is_distributed, hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, symbol = "test_offset_op_impl", SrcPtrIndex = array} + "test_offset_op" + ins(%2 : tensor<8x!tt.ptr>) + outs(%empty : tensor<8x!tt.ptr>) -> tensor<8x!tt.ptr> + + // Mark as continuous mem access - this annotation is processed and erased during analysis + annotation.mark %3 {ContinuousMemAccess} : tensor<8x!tt.ptr> + + %4 = tt.load %3 : tensor<8x!tt.ptr> + tt.return %4 : tensor<8xf32> +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/if_simplifier.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/if_simplifier.mlir new file mode 100644 index 0000000000..7f71ec94a9 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/if_simplifier.mlir @@ -0,0 +1,45 @@ +// RUN: triton-opt --triton-to-unstructure --split-input-file %s | FileCheck %s --implicit-check-not="DiscreteMemAccess" +// CHECK-LABEL: tt.func public @triton_for_if_load +// CHECK: %[[CST:.*]] = arith.constant dense<0> : tensor<16xi32> +// CHECK: %[[CST0:.*]] = arith.constant dense<1> : tensor<16xi32> +// CHECK: %[[SEL:.*]] = arith.select %{{.*}}, %[[CST0]], %[[CST]] : tensor<16xi32> +// CHECK: %[[ADD:.*]] = arith.addi %{{.*}}, %[[SEL]] : tensor<16xi32> +// CHECK: %[[ADDPTR:.*]] = tt.addptr %{{.*}}, %[[ADD]] : tensor<16x!tt.ptr>, tensor<16xi32> + + +module { + tt.func public @triton_for_if_load(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c1_i32 = arith.constant 1 : i32 + %c2_i32 = arith.constant 2 : i32 + %cst = arith.constant dense<0.000000e+00> : tensor<16xf32> + %cst_0 = arith.constant dense<32> : tensor<16xi32> + %cst_1 = arith.constant dense<1> : tensor<16xi32> + %c16_i32 = arith.constant 16 : i32 + %c0_i32 = arith.constant 0 : i32 + %0 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %1 = tt.get_program_id x : i32 + %2 = arith.cmpi ne, %1, %c0_i32 : i32 + %3 = tt.splat %arg0 : !tt.ptr -> tensor<16x!tt.ptr> + %4 = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %5:2 = scf.for %arg2 = %c0_i32 to %c2_i32 step %c1_i32 iter_args(%arg3 = %0, %arg4 = %0) -> (tensor<16xi32>, tensor<16xi32>) : i32 { + %6 = arith.muli %arg2, %c16_i32 : i32 + %7 = tt.splat %6 : i32 -> tensor<16xi32> + %8 = arith.addi %arg3, %7 : tensor<16xi32> + %9 = arith.addi %arg4, %7 : tensor<16xi32> + %10 = scf.if %2 -> (tensor<16xi32>) { + %16 = arith.addi %8, %cst_1 : tensor<16xi32> + scf.yield %16 : tensor<16xi32> + } else { + scf.yield %8 : tensor<16xi32> + } + %11 = tt.addptr %3, %10 : tensor<16x!tt.ptr>, tensor<16xi32> + %12 = arith.cmpi slt, %10, %cst_0 : tensor<16xi32> + %13 = tt.load %11 : tensor<16x!tt.ptr> + %14 = arith.select %12, %13, %cst : tensor<16xi1>, tensor<16xf32> + %15 = tt.addptr %4, %9 : tensor<16x!tt.ptr>, tensor<16xi32> + tt.store %15, %14 : tensor<16x!tt.ptr> + scf.yield %10, %9 : tensor<16xi32>, tensor<16xi32> + } + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_atomic.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_atomic.mlir new file mode 100644 index 0000000000..9bfb371f84 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_atomic.mlir @@ -0,0 +1,574 @@ +// RUN: triton-opt '--discrete-mask-access-conversion=compile-on-910-95=True compile-mode=simd_simt_template' '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' --split-input-file %s | FileCheck %s + +// CHECK-LABEL: tt.func public @structured_discrete_mask_atomic_add_1d +// CHECK: arith.select {{.*}} : tensor<8xi1>, tensor<8xi32> +// CHECK: tt.atomic_rmw add, acq_rel, gpu, {{.*}} : (tensor<8x!tt.ptr>, tensor<8xi32>) -> tensor<8xi32> +tt.func public @structured_discrete_mask_atomic_add_1d(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<2> : tensor<8xi32> + %cst_0 = arith.constant dense<8> : tensor<8xi32> + %0 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %1 = arith.muli %0, %cst : tensor<8xi32> + %2 = arith.cmpi slt, %1, %cst_0 : tensor<8xi32> + %3 = tt.splat %arg0 : !tt.ptr -> tensor<8x!tt.ptr> + %4 = tt.addptr %3, %0 : tensor<8x!tt.ptr>, tensor<8xi32> + %5 = tt.load %4 : tensor<8x!tt.ptr> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<8x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<8x!tt.ptr>, tensor<8xi32> + %8 = tt.atomic_rmw add, acq_rel, gpu, %7, %5, %2 : (tensor<8x!tt.ptr>, tensor<8xi32>, tensor<8xi1>) -> tensor<8xi32> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @structured_discrete_mask_atomic_add_i16_fallback +// CHECK: arith.select {{.*}} : tensor<8xi1>, tensor<8xi16> +// CHECK-NOT: scf.for +// CHECK-NOT: scf.if +// CHECK: tt.atomic_rmw add, acq_rel, gpu, {{.*}} : (tensor<8x!tt.ptr>, tensor<8xi16>) -> tensor<8xi16> +tt.func public @structured_discrete_mask_atomic_add_i16_fallback(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %cst = arith.constant dense<2> : tensor<8xi32> + %cst_0 = arith.constant dense<8> : tensor<8xi32> + %0 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %1 = arith.muli %0, %cst : tensor<8xi32> + %2 = arith.cmpi slt, %1, %cst_0 : tensor<8xi32> + %3 = tt.splat %arg0 : !tt.ptr -> tensor<8x!tt.ptr> + %4 = tt.addptr %3, %0 : tensor<8x!tt.ptr>, tensor<8xi32> + %5 = tt.load %4 : tensor<8x!tt.ptr> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<8x!tt.ptr> + %7 = tt.addptr %6, %0 : tensor<8x!tt.ptr>, tensor<8xi32> + %8 = tt.atomic_rmw add, acq_rel, gpu, %7, %5, %2 : (tensor<8x!tt.ptr>, tensor<8xi16>, tensor<8xi1>) -> tensor<8xi16> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @fully_unstructured_atomic_add_2d +// CHECK: %[[MASK_TRUE:.*]] = arith.constant dense<1> : tensor<16xi8> +// CHECK: %[[OFFSET_FLAT:.*]] = tt.reshape {{.*}} : tensor<4x4xi64> -> tensor<16xi64> +// CHECK: %[[VALUE_FLAT:.*]] = tt.reshape {{.*}} : tensor<4x4xi32> -> tensor<16xi32> +// CHECK: hivm.hir.custom {extra_attr = "operate=add"{{.*}}} "__builtin_indirect_atomic" ins(%arg2, %[[OFFSET_FLAT]], %[[VALUE_FLAT]], %[[MASK_TRUE]] +tt.func public @fully_unstructured_atomic_add_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense : tensor<4x4xi1> + %cst_0 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_0 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %8 = tt.addptr %7, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %9 = tt.load %8 : tensor<4x4x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %11 = tt.addptr %10, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %12 = tt.load %11 : tensor<4x4x!tt.ptr> + %13 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %14 = tt.addptr %13, %9 : tensor<4x4x!tt.ptr>, tensor<4x4xi64> + %15 = tt.atomic_rmw add, acq_rel, gpu, %14, %12, %cst : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %16 = tt.splat %arg3 : !tt.ptr -> tensor<4x4x!tt.ptr> + %17 = tt.addptr %16, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %17, %15 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// sizeInByte >= 64, falls back to scf.for loop unrolling + DiscreteMemAccess +// CHECK-LABEL: tt.func public @partial_structured_atomic_add_2d +// CHECK: scf.for +// CHECK: tt.atomic_rmw add, acq_rel, gpu, {{.*}} {DiscreteMemAccess} +// CHECK: {ExtractedLoadOrStore} +tt.func public @partial_structured_atomic_add_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense : tensor<2x16xi1> + %cst_0 = arith.constant dense<16> : tensor<2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %1, %cst_0 : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = arith.muli %3, %cst_0 : tensor<2x1xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<2x1x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %16 = tt.broadcast %15 : tensor<2x1x!tt.ptr> -> tensor<2x16x!tt.ptr> + %17 = tt.addptr %16, %8 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %18 = tt.atomic_rmw add, acq_rel, gpu, %17, %12, %cst : (tensor<2x16x!tt.ptr>, tensor<2x16xi32>, tensor<2x16xi1>) -> tensor<2x16xi32> + %19 = tt.splat %arg2 : !tt.ptr -> tensor<2x16x!tt.ptr> + %20 = tt.addptr %19, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + tt.store %20, %18 : tensor<2x16x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @fully_unstructured_atomic_and_2d +// CHECK: hivm.hir.custom {extra_attr = "operate=and" +tt.func public @fully_unstructured_atomic_and_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: !tt.ptr) { + %cst = arith.constant dense : tensor<4x4xi1> + %cst_0 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_0 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %8 = tt.addptr %7, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %9 = tt.load %8 : tensor<4x4x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %11 = tt.addptr %10, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %12 = tt.load %11 : tensor<4x4x!tt.ptr> + %13 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %14 = tt.addptr %13, %9 : tensor<4x4x!tt.ptr>, tensor<4x4xi64> + %15 = tt.atomic_rmw and, acq_rel, gpu, %14, %12, %cst : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %16 = tt.splat %arg3 : !tt.ptr -> tensor<4x4x!tt.ptr> + %17 = tt.addptr %16, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %17, %15 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// sizeInByte >= 64, falls back to scf.for loop unrolling + DiscreteMemAccess +// CHECK-LABEL: tt.func public @partial_structured_atomic_and_2d +// CHECK: scf.for +// CHECK: tt.atomic_rmw and, acq_rel, gpu, {{.*}} {DiscreteMemAccess} +// CHECK: {ExtractedLoadOrStore} +tt.func public @partial_structured_atomic_and_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense : tensor<2x16xi1> + %cst_0 = arith.constant dense<16> : tensor<2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %1, %cst_0 : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = arith.muli %3, %cst_0 : tensor<2x1xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<2x1x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %16 = tt.broadcast %15 : tensor<2x1x!tt.ptr> -> tensor<2x16x!tt.ptr> + %17 = tt.addptr %16, %8 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %18 = tt.atomic_rmw and, acq_rel, gpu, %17, %12, %cst : (tensor<2x16x!tt.ptr>, tensor<2x16xi32>, tensor<2x16xi1>) -> tensor<2x16xi32> + %19 = tt.splat %arg2 : !tt.ptr -> tensor<2x16x!tt.ptr> + %20 = tt.addptr %19, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + tt.store %20, %18 : tensor<2x16x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @fully_unstructured_atomic_or_2d +// CHECK: hivm.hir.custom {extra_attr = "operate=or" +tt.func public @fully_unstructured_atomic_or_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: !tt.ptr) { + %cst = arith.constant dense : tensor<4x4xi1> + %cst_0 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_0 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %8 = tt.addptr %7, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %9 = tt.load %8 : tensor<4x4x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %11 = tt.addptr %10, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %12 = tt.load %11 : tensor<4x4x!tt.ptr> + %13 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %14 = tt.addptr %13, %9 : tensor<4x4x!tt.ptr>, tensor<4x4xi64> + %15 = tt.atomic_rmw or, acq_rel, gpu, %14, %12, %cst : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %16 = tt.splat %arg3 : !tt.ptr -> tensor<4x4x!tt.ptr> + %17 = tt.addptr %16, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %17, %15 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// sizeInByte >= 64, falls back to scf.for loop unrolling + DiscreteMemAccess +// CHECK-LABEL: tt.func public @partial_structured_atomic_or_2d +// CHECK: scf.for +// CHECK: tt.atomic_rmw or, acq_rel, gpu, {{.*}} {DiscreteMemAccess} +tt.func public @partial_structured_atomic_or_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense : tensor<2x16xi1> + %cst_0 = arith.constant dense<16> : tensor<2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %1, %cst_0 : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = arith.muli %3, %cst_0 : tensor<2x1xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<2x1x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %16 = tt.broadcast %15 : tensor<2x1x!tt.ptr> -> tensor<2x16x!tt.ptr> + %17 = tt.addptr %16, %8 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %18 = tt.atomic_rmw or, acq_rel, gpu, %17, %12, %cst : (tensor<2x16x!tt.ptr>, tensor<2x16xi32>, tensor<2x16xi1>) -> tensor<2x16xi32> + %19 = tt.splat %arg2 : !tt.ptr -> tensor<2x16x!tt.ptr> + %20 = tt.addptr %19, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + tt.store %20, %18 : tensor<2x16x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @fully_unstructured_atomic_xor_2d +// CHECK: hivm.hir.custom {extra_attr = "operate=xor" +tt.func public @fully_unstructured_atomic_xor_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: !tt.ptr) { + %cst = arith.constant dense : tensor<4x4xi1> + %cst_0 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_0 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %8 = tt.addptr %7, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %9 = tt.load %8 : tensor<4x4x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %11 = tt.addptr %10, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %12 = tt.load %11 : tensor<4x4x!tt.ptr> + %13 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %14 = tt.addptr %13, %9 : tensor<4x4x!tt.ptr>, tensor<4x4xi64> + %15 = tt.atomic_rmw xor, acq_rel, gpu, %14, %12, %cst : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %16 = tt.splat %arg3 : !tt.ptr -> tensor<4x4x!tt.ptr> + %17 = tt.addptr %16, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %17, %15 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// sizeInByte >= 64, falls back to scf.for loop unrolling + DiscreteMemAccess +// CHECK-LABEL: tt.func public @partial_structured_atomic_xor_2d +// CHECK: scf.for +// CHECK: tt.atomic_rmw xor, acq_rel, gpu, {{.*}} {DiscreteMemAccess} +tt.func public @partial_structured_atomic_xor_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense : tensor<2x16xi1> + %cst_0 = arith.constant dense<16> : tensor<2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %1, %cst_0 : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = arith.muli %3, %cst_0 : tensor<2x1xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<2x1x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %16 = tt.broadcast %15 : tensor<2x1x!tt.ptr> -> tensor<2x16x!tt.ptr> + %17 = tt.addptr %16, %8 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %18 = tt.atomic_rmw xor, acq_rel, gpu, %17, %12, %cst : (tensor<2x16x!tt.ptr>, tensor<2x16xi32>, tensor<2x16xi1>) -> tensor<2x16xi32> + %19 = tt.splat %arg2 : !tt.ptr -> tensor<2x16x!tt.ptr> + %20 = tt.addptr %19, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + tt.store %20, %18 : tensor<2x16x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @fully_unstructured_atomic_xchg_2d +// CHECK: hivm.hir.custom {extra_attr = "operate=xchg" +tt.func public @fully_unstructured_atomic_xchg_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: !tt.ptr) { + %cst = arith.constant dense : tensor<4x4xi1> + %cst_0 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_0 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %8 = tt.addptr %7, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %9 = tt.load %8 : tensor<4x4x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %11 = tt.addptr %10, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %12 = tt.load %11 : tensor<4x4x!tt.ptr> + %13 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %14 = tt.addptr %13, %9 : tensor<4x4x!tt.ptr>, tensor<4x4xi64> + %15 = tt.atomic_rmw exch, acq_rel, gpu, %14, %12, %cst : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %16 = tt.splat %arg3 : !tt.ptr -> tensor<4x4x!tt.ptr> + %17 = tt.addptr %16, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %17, %15 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// sizeInByte >= 64, falls back to scf.for loop unrolling + DiscreteMemAccess +// CHECK-LABEL: tt.func public @partial_structured_atomic_xchg_2d +// CHECK: scf.for +// CHECK: tt.atomic_rmw exch, acq_rel, gpu, {{.*}} {DiscreteMemAccess} +tt.func public @partial_structured_atomic_xchg_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense : tensor<2x16xi1> + %cst_0 = arith.constant dense<16> : tensor<2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %1, %cst_0 : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = arith.muli %3, %cst_0 : tensor<2x1xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<2x1x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %16 = tt.broadcast %15 : tensor<2x1x!tt.ptr> -> tensor<2x16x!tt.ptr> + %17 = tt.addptr %16, %8 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %18 = tt.atomic_rmw exch, acq_rel, gpu, %17, %12, %cst : (tensor<2x16x!tt.ptr>, tensor<2x16xi32>, tensor<2x16xi1>) -> tensor<2x16xi32> + %19 = tt.splat %arg2 : !tt.ptr -> tensor<2x16x!tt.ptr> + %20 = tt.addptr %19, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + tt.store %20, %18 : tensor<2x16x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @fully_unstructured_atomic_max_2d +// CHECK: hivm.hir.custom {extra_attr = "operate=max" +tt.func public @fully_unstructured_atomic_max_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: !tt.ptr) { + %cst = arith.constant dense : tensor<4x4xi1> + %cst_0 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_0 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %8 = tt.addptr %7, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %9 = tt.load %8 : tensor<4x4x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %11 = tt.addptr %10, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %12 = tt.load %11 : tensor<4x4x!tt.ptr> + %13 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %14 = tt.addptr %13, %9 : tensor<4x4x!tt.ptr>, tensor<4x4xi64> + %15 = tt.atomic_rmw max, acq_rel, gpu, %14, %12, %cst : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %16 = tt.splat %arg3 : !tt.ptr -> tensor<4x4x!tt.ptr> + %17 = tt.addptr %16, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %17, %15 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// sizeInByte >= 64, falls back to scf.for loop unrolling + DiscreteMemAccess +// CHECK-LABEL: tt.func public @partial_structured_atomic_max_2d +// CHECK: scf.for +// CHECK: tt.atomic_rmw max, acq_rel, gpu, {{.*}} {DiscreteMemAccess} +tt.func public @partial_structured_atomic_max_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense : tensor<2x16xi1> + %cst_0 = arith.constant dense<16> : tensor<2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %1, %cst_0 : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = arith.muli %3, %cst_0 : tensor<2x1xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<2x1x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %16 = tt.broadcast %15 : tensor<2x1x!tt.ptr> -> tensor<2x16x!tt.ptr> + %17 = tt.addptr %16, %8 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %18 = tt.atomic_rmw max, acq_rel, gpu, %17, %12, %cst : (tensor<2x16x!tt.ptr>, tensor<2x16xi32>, tensor<2x16xi1>) -> tensor<2x16xi32> + %19 = tt.splat %arg2 : !tt.ptr -> tensor<2x16x!tt.ptr> + %20 = tt.addptr %19, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + tt.store %20, %18 : tensor<2x16x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @fully_unstructured_atomic_min_2d +// CHECK: hivm.hir.custom {extra_attr = "operate=min" +tt.func public @fully_unstructured_atomic_min_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: !tt.ptr) { + %cst = arith.constant dense : tensor<4x4xi1> + %cst_0 = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst_0 : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %8 = tt.addptr %7, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %9 = tt.load %8 : tensor<4x4x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %11 = tt.addptr %10, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %12 = tt.load %11 : tensor<4x4x!tt.ptr> + %13 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %14 = tt.addptr %13, %9 : tensor<4x4x!tt.ptr>, tensor<4x4xi64> + %15 = tt.atomic_rmw min, acq_rel, gpu, %14, %12, %cst : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi1>) -> tensor<4x4xi32> + %16 = tt.splat %arg3 : !tt.ptr -> tensor<4x4x!tt.ptr> + %17 = tt.addptr %16, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %17, %15 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// sizeInByte >= 64, falls back to scf.for loop unrolling + DiscreteMemAccess +// CHECK-LABEL: tt.func public @partial_structured_atomic_min_2d +// CHECK: scf.for +// CHECK: tt.atomic_rmw min, acq_rel, gpu, {{.*}} {DiscreteMemAccess} +tt.func public @partial_structured_atomic_min_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %cst = arith.constant dense : tensor<2x16xi1> + %cst_0 = arith.constant dense<16> : tensor<2x1xi32> + %cst_1 = arith.constant dense<2> : tensor<2x1xi32> + %cst_2 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_2 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_1 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %1, %cst_0 : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = arith.muli %3, %cst_0 : tensor<2x1xi32> + %14 = tt.splat %arg1 : !tt.ptr -> tensor<2x1x!tt.ptr> + %15 = tt.addptr %14, %13 : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %16 = tt.broadcast %15 : tensor<2x1x!tt.ptr> -> tensor<2x16x!tt.ptr> + %17 = tt.addptr %16, %8 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %18 = tt.atomic_rmw min, acq_rel, gpu, %17, %12, %cst : (tensor<2x16x!tt.ptr>, tensor<2x16xi32>, tensor<2x16xi1>) -> tensor<2x16xi32> + %19 = tt.splat %arg2 : !tt.ptr -> tensor<2x16x!tt.ptr> + %20 = tt.addptr %19, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + tt.store %20, %18 : tensor<2x16x!tt.ptr> + tt.return +} + +// ----- + +// CHECK-LABEL: tt.func public @fully_unstructured_atomic_cas_2d +// CHECK: %[[OFFSET_FLAT:.*]] = tt.reshape {{.*}} : tensor<4x4xi64> -> tensor<16xi64> +// CHECK: %[[CMP_FLAT:.*]] = tt.reshape {{.*}} : tensor<4x4xi32> -> tensor<16xi32> +// CHECK: %[[VAL_FLAT:.*]] = tt.reshape {{.*}} : tensor<4x4xi32> -> tensor<16xi32> +// CHECK: hivm.hir.custom {extra_attr = "operate=cas" +tt.func public @fully_unstructured_atomic_cas_2d(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: !tt.ptr, %arg4: !tt.ptr) attributes {noinline = false} { + %cst = arith.constant dense<4> : tensor<4x1xi32> + %0 = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<4xi32> -> tensor<4x1xi32> + %2 = tt.expand_dims %0 {axis = 0 : i32} : tensor<4xi32> -> tensor<1x4xi32> + %3 = arith.muli %1, %cst : tensor<4x1xi32> + %4 = tt.broadcast %3 : tensor<4x1xi32> -> tensor<4x4xi32> + %5 = tt.broadcast %2 : tensor<1x4xi32> -> tensor<4x4xi32> + %6 = arith.addi %4, %5 : tensor<4x4xi32> + %7 = tt.splat %arg0 : !tt.ptr -> tensor<4x4x!tt.ptr> + %8 = tt.addptr %7, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %9 = tt.load %8 : tensor<4x4x!tt.ptr> + %10 = tt.splat %arg1 : !tt.ptr -> tensor<4x4x!tt.ptr> + %11 = tt.addptr %10, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %12 = tt.load %11 : tensor<4x4x!tt.ptr> + %13 = tt.splat %arg2 : !tt.ptr -> tensor<4x4x!tt.ptr> + %14 = tt.addptr %13, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + %15 = tt.load %14 : tensor<4x4x!tt.ptr> + %16 = tt.splat %arg3 : !tt.ptr -> tensor<4x4x!tt.ptr> + %17 = tt.addptr %16, %9 : tensor<4x4x!tt.ptr>, tensor<4x4xi64> + %18 = tt.atomic_cas acq_rel, gpu, %17, %12, %15 : (tensor<4x4x!tt.ptr>, tensor<4x4xi32>, tensor<4x4xi32>) -> tensor<4x4xi32> + %19 = tt.splat %arg4 : !tt.ptr -> tensor<4x4x!tt.ptr> + %20 = tt.addptr %19, %6 : tensor<4x4x!tt.ptr>, tensor<4x4xi32> + tt.store %20, %18 : tensor<4x4x!tt.ptr> + tt.return +} + +// ----- + +// sizeInByte >= 64, falls back to scf.for loop unrolling + DiscreteMemAccess +// CHECK-LABEL: tt.func public @partial_structured_atomic_cas_2d +// CHECK: scf.for +// CHECK: tt.atomic_cas acq_rel, gpu, {{.*}} {DiscreteMemAccess} +// CHECK: {ExtractedLoadOrStore} +tt.func public @partial_structured_atomic_cas_2d(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<16> : tensor<2x1xi32> + %cst_0 = arith.constant dense<2> : tensor<2x1xi32> + %cst_1 = arith.constant dense<1> : tensor<2x1xi32> + %0 = tt.make_range {end = 2 : i32, start = 0 : i32} : tensor<2xi32> + %1 = tt.expand_dims %0 {axis = 1 : i32} : tensor<2xi32> -> tensor<2x1xi32> + %2 = arith.addi %1, %cst_1 : tensor<2x1xi32> + %3 = arith.remsi %2, %cst_0 : tensor<2x1xi32> + %4 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %5 = tt.expand_dims %4 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %6 = arith.muli %1, %cst : tensor<2x1xi32> + %7 = tt.broadcast %6 : tensor<2x1xi32> -> tensor<2x16xi32> + %8 = tt.broadcast %5 : tensor<1x16xi32> -> tensor<2x16xi32> + %9 = arith.addi %7, %8 : tensor<2x16xi32> + %10 = tt.splat %arg0 : !tt.ptr -> tensor<2x16x!tt.ptr> + %11 = tt.addptr %10, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %12 = tt.load %11 : tensor<2x16x!tt.ptr> + %13 = tt.splat %arg1 : !tt.ptr -> tensor<2x16x!tt.ptr> + %14 = tt.addptr %13, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %15 = tt.load %14 : tensor<2x16x!tt.ptr> + %16 = arith.muli %3, %cst : tensor<2x1xi32> + %17 = tt.splat %arg2 : !tt.ptr -> tensor<2x1x!tt.ptr> + %18 = tt.addptr %17, %16 : tensor<2x1x!tt.ptr>, tensor<2x1xi32> + %19 = tt.broadcast %18 : tensor<2x1x!tt.ptr> -> tensor<2x16x!tt.ptr> + %20 = tt.addptr %19, %8 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + %21 = tt.atomic_cas acq_rel, gpu, %20, %12, %15 : (tensor<2x16x!tt.ptr>, tensor<2x16xi32>, tensor<2x16xi32>) -> tensor<2x16xi32> + %22 = tt.splat %arg3 : !tt.ptr -> tensor<2x16x!tt.ptr> + %23 = tt.addptr %22, %9 : tensor<2x16x!tt.ptr>, tensor<2x16xi32> + tt.store %23, %21 : tensor<2x16x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_func.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_func.mlir new file mode 100644 index 0000000000..3224b59fd7 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_func.mlir @@ -0,0 +1,141 @@ +// RUN: triton-opt --triton-to-structured '--discrete-mask-access-conversion=compile-on-910-95=True compile-mode=simd_simt_template' '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' %s --split-input-file | FileCheck %s + +// tt.load -> tt.indirect_load +tt.func public @triton_ldst_indirect_05_kernel(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: i32) attributes {noinline = false} { + %cst = arith.constant dense<16> : tensor<8x1xi32> + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c8_i32 : i32 + %2 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %3 = tt.splat %1 : i32 -> tensor<8xi32> + %4 = arith.addi %3, %2 : tensor<8xi32> + %5 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %7 = tt.addptr %6, %5 : tensor<16x!tt.ptr>, tensor<16xi32> + %8 = tt.load %7 : tensor<16x!tt.ptr> + %9 = tt.expand_dims %2 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %10 = tt.splat %arg3 : i32 -> tensor<8x1xi32> + %11 = arith.muli %9, %10 : tensor<8x1xi32> + %12 = tt.expand_dims %8 {axis = 0 : i32} : tensor<16xi64> -> tensor<1x16xi64> + %13 = arith.extsi %11 : tensor<8x1xi32> to tensor<8x1xi64> + %14 = tt.broadcast %13 : tensor<8x1xi64> -> tensor<8x16xi64> + %15 = tt.broadcast %12 : tensor<1x16xi64> -> tensor<8x16xi64> + %16 = arith.addi %14, %15 : tensor<8x16xi64> + %17 = tt.splat %arg2 : !tt.ptr -> tensor<8x16x!tt.ptr> + %18 = tt.addptr %17, %16 : tensor<8x16x!tt.ptr>, tensor<8x16xi64> + %19 = tt.load %18 : tensor<8x16x!tt.ptr> + %20 = math.exp %19 : tensor<8x16xf32> + %21 = tt.expand_dims %4 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %22 = arith.muli %21, %cst : tensor<8x1xi32> + %23 = tt.expand_dims %5 {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> + %24 = tt.broadcast %22 : tensor<8x1xi32> -> tensor<8x16xi32> + %25 = tt.broadcast %23 : tensor<1x16xi32> -> tensor<8x16xi32> + %26 = arith.addi %24, %25 : tensor<8x16xi32> + %27 = tt.splat %arg0 : !tt.ptr -> tensor<8x16x!tt.ptr> + %28 = tt.addptr %27, %26 : tensor<8x16x!tt.ptr>, tensor<8x16xi32> + tt.store %28, %20 : tensor<8x16x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @triton_ldst_indirect_05_kernel( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr, %[[VAL_1:.*]]: !tt.ptr, %[[VAL_2:.*]]: !tt.ptr, %[[VAL_3:.*]]: i32) attributes {noinline = false} { +// CHECK: %[[VAL_4:.*]] = arith.constant dense<16> : tensor<8x1xi32> +// CHECK: %[[VAL_6:.*]] = tt.get_program_id x : i32 +// CHECK: %[[VAL_7:.*]] = arith.muli %[[VAL_6]], %{{.*}} : i32 +// CHECK: %[[VAL_8:.*]] = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> +// CHECK: %[[VAL_9:.*]] = tt.splat %[[VAL_7]] : i32 -> tensor<8xi32> +// CHECK: %[[VAL_10:.*]] = arith.addi %[[VAL_9]], %[[VAL_8]] : tensor<8xi32> +// CHECK: %[[VAL_11:.*]] = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> +// CHECK: %[[VAL_12:.*]] = tt.splat %[[VAL_1:.*]] : !tt.ptr -> tensor<16x!tt.ptr> +// CHECK: %[[VAL_13:.*]] = tt.addptr %[[VAL_12]], %[[VAL_11]] : tensor<16x!tt.ptr>, tensor<16xi32> +// CHECK: %[[VAL_14:.*]] = tt.load %[[VAL_13]] : tensor<16x!tt.ptr> +// CHECK: %[[VAL_15:.*]] = tt.expand_dims %[[VAL_8]] {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> +// CHECK: %[[VAL_16:.*]] = tt.splat %[[VAL_3:.*]] : i32 -> tensor<8x1xi32> +// CHECK: %[[VAL_17:.*]] = arith.muli %[[VAL_15]], %[[VAL_16]] : tensor<8x1xi32> +// CHECK: %[[VAL_18:.*]] = tt.expand_dims %[[VAL_14]] {axis = 0 : i32} : tensor<16xi64> -> tensor<1x16xi64> +// CHECK: %[[VAL_19:.*]] = arith.extsi %[[VAL_17]] : tensor<8x1xi32> to tensor<8x1xi64> +// CHECK: %[[VAL_20:.*]] = tt.broadcast %[[VAL_19]] : tensor<8x1xi64> -> tensor<8x16xi64> +// CHECK: %[[VAL_21:.*]] = tt.broadcast %[[VAL_18]] : tensor<1x16xi64> -> tensor<8x16xi64> +// CHECK: %[[VAL_22:.*]] = arith.addi %[[VAL_20]], %[[VAL_21]] : tensor<8x16xi64> +// CHECK: %[[VAL_23:.*]] = ascend.indirect_load %[[VAL_2:.*]] : , %[[VAL_22]] : tensor<8x16xi64> {{.*}}-> tensor<8x16xf32> +// CHECK: %[[VAL_24:.*]] = math.exp %[[VAL_23]] : tensor<8x16xf32> +// CHECK: %[[VAL_25:.*]] = tt.expand_dims %[[VAL_10]] {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> +// CHECK: %[[VAL_26:.*]] = arith.muli %[[VAL_25]], %[[VAL_4]] : tensor<8x1xi32> +// CHECK: %[[VAL_27:.*]] = tt.expand_dims %[[VAL_11]] {axis = 0 : i32} : tensor<16xi32> -> tensor<1x16xi32> +// CHECK: %[[VAL_28:.*]] = tt.broadcast %[[VAL_26]] : tensor<8x1xi32> -> tensor<8x16xi32> +// CHECK: %[[VAL_29:.*]] = tt.broadcast %[[VAL_27]] : tensor<1x16xi32> -> tensor<8x16xi32> +// CHECK: %[[VAL_30:.*]] = arith.addi %[[VAL_28]], %[[VAL_29]] : tensor<8x16xi32> +// CHECK: %[[VAL_31:.*]] = tt.splat %[[VAL_0:.*]] : !tt.ptr -> tensor<8x16x!tt.ptr> +// CHECK: %[[VAL_32:.*]] = tt.addptr %[[VAL_31]], %[[VAL_30]] : tensor<8x16x!tt.ptr>, tensor<8x16xi32> +// CHECK: tt.store %[[VAL_32]], %[[VAL_24]] : tensor<8x16x!tt.ptr> +// CHECK: tt.return +// CHECK: } + +// ----- + +// tt.store -> tt.indirect_store +tt.func public @triton_ldst_indirect_08_kernel(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: i32) attributes {noinline = false} { + %cst = arith.constant dense<32> : tensor<8x1xi32> + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c8_i32 : i32 + %2 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %3 = tt.splat %1 : i32 -> tensor<8xi32> + %4 = arith.addi %3, %2 : tensor<8xi32> + %5 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %7 = tt.addptr %6, %5 : tensor<16x!tt.ptr>, tensor<16xi32> + %8 = tt.load %7 : tensor<16x!tt.ptr> + %9 = tt.expand_dims %2 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %10 = tt.splat %arg3 : i32 -> tensor<8x1xi32> + %11 = arith.muli %9, %10 : tensor<8x1xi32> + %12 = tt.expand_dims %8 {axis = 0 : i32} : tensor<16xi64> -> tensor<1x16xi64> + %13 = arith.extsi %11 : tensor<8x1xi32> to tensor<8x1xi64> + %14 = tt.broadcast %13 : tensor<8x1xi64> -> tensor<8x16xi64> + %15 = tt.broadcast %12 : tensor<1x16xi64> -> tensor<8x16xi64> + %16 = arith.addi %14, %15 : tensor<8x16xi64> + %17 = tt.splat %arg2 : !tt.ptr -> tensor<8x16x!tt.ptr> + %18 = tt.addptr %17, %16 : tensor<8x16x!tt.ptr>, tensor<8x16xi64> + %19 = tt.load %18 : tensor<8x16x!tt.ptr> + %20 = math.exp %19 : tensor<8x16xf32> + %21 = tt.expand_dims %4 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %22 = arith.muli %21, %cst : tensor<8x1xi32> + %23 = arith.extsi %22 : tensor<8x1xi32> to tensor<8x1xi64> + %24 = tt.broadcast %23 : tensor<8x1xi64> -> tensor<8x16xi64> + %25 = arith.addi %24, %15 : tensor<8x16xi64> + %26 = tt.splat %arg0 : !tt.ptr -> tensor<8x16x!tt.ptr> + %27 = tt.addptr %26, %25 : tensor<8x16x!tt.ptr>, tensor<8x16xi64> + tt.store %27, %20 : tensor<8x16x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @triton_ldst_indirect_08_kernel( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr, %[[VAL_1:.*]]: !tt.ptr, %[[VAL_2:.*]]: !tt.ptr, %[[VAL_3:.*]]: i32) attributes {noinline = false} { +// CHECK: %[[VAL_4:.*]] = arith.constant dense<32> : tensor<8x1xi32> +// CHECK: %[[VAL_6:.*]] = tt.get_program_id x : i32 +// CHECK: %[[VAL_7:.*]] = arith.muli %[[VAL_6:.*]], %{{.*}} : i32 +// CHECK: %[[VAL_8:.*]] = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> +// CHECK: %[[VAL_9:.*]] = tt.splat %[[VAL_7:.*]] : i32 -> tensor<8xi32> +// CHECK: %[[VAL_10:.*]] = arith.addi %[[VAL_9:.*]], %[[VAL_8:.*]] : tensor<8xi32> +// CHECK: %[[VAL_11:.*]] = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> +// CHECK: %[[VAL_12:.*]] = tt.splat %[[VAL_1:.*]] : !tt.ptr -> tensor<16x!tt.ptr> +// CHECK: %[[VAL_13:.*]] = tt.addptr %[[VAL_12:.*]], %[[VAL_11:.*]] : tensor<16x!tt.ptr>, tensor<16xi32> +// CHECK: %[[VAL_14:.*]] = tt.load %[[VAL_13:.*]] : tensor<16x!tt.ptr> +// CHECK: %[[VAL_15:.*]] = tt.expand_dims %[[VAL_8:.*]] {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> +// CHECK: %[[VAL_16:.*]] = tt.splat %[[VAL_3:.*]] : i32 -> tensor<8x1xi32> +// CHECK: %[[VAL_17:.*]] = arith.muli %[[VAL_15:.*]], %[[VAL_16:.*]] : tensor<8x1xi32> +// CHECK: %[[VAL_18:.*]] = tt.expand_dims %[[VAL_14:.*]] {axis = 0 : i32} : tensor<16xi64> -> tensor<1x16xi64> +// CHECK: %[[VAL_19:.*]] = arith.extsi %[[VAL_17:.*]] : tensor<8x1xi32> to tensor<8x1xi64> +// CHECK: %[[VAL_20:.*]] = tt.broadcast %[[VAL_19:.*]] : tensor<8x1xi64> -> tensor<8x16xi64> +// CHECK: %[[VAL_21:.*]] = tt.broadcast %[[VAL_18:.*]] : tensor<1x16xi64> -> tensor<8x16xi64> +// CHECK: %[[VAL_22:.*]] = arith.addi %[[VAL_20:.*]], %[[VAL_21:.*]] : tensor<8x16xi64> +// CHECK: %[[VAL_23:.*]] = ascend.indirect_load %[[VAL_2:.*]] : , %[[VAL_22:.*]] : tensor<8x16xi64> {{.*}}-> tensor<8x16xf32> +// CHECK: %[[VAL_24:.*]] = math.exp %[[VAL_23:.*]] : tensor<8x16xf32> +// CHECK: %[[VAL_25:.*]] = tt.expand_dims %[[VAL_10:.*]] {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> +// CHECK: %[[VAL_26:.*]] = arith.muli %[[VAL_25:.*]], %[[VAL_4:.*]] : tensor<8x1xi32> +// CHECK: %[[VAL_27:.*]] = arith.extsi %[[VAL_26:.*]] : tensor<8x1xi32> to tensor<8x1xi64> +// CHECK: %[[VAL_28:.*]] = tt.broadcast %[[VAL_27:.*]] : tensor<8x1xi64> -> tensor<8x16xi64> +// CHECK: %[[VAL_29:.*]] = arith.addi %[[VAL_28:.*]], %[[VAL_21:.*]] : tensor<8x16xi64> +// CHECK: ascend.indirect_store %[[VAL_0:.*]] : , %[[VAL_29:.*]] : tensor<8x16xi64>, %[[VAL_24:.*]] : tensor<8x16xf32> +// CHECK: tt.return +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_load.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_load.mlir new file mode 100644 index 0000000000..2dc0c76ffc --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_load.mlir @@ -0,0 +1,58 @@ +// RUN: triton-opt --triton-to-structured '--discrete-mask-access-conversion=compile-on-910-95=True compile-mode=simd_simt_template' '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' %s --split-input-file | FileCheck %s + +// tt.load -> ascend.indirect_load +tt.func public @triton_indirect_load_kernel(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: i32) attributes {noinline = false} { + %cst = arith.constant dense<32> : tensor<8x1xi32> + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c8_i32 : i32 + %2 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %3 = tt.splat %1 : i32 -> tensor<8xi32> + %4 = arith.addi %3, %2 : tensor<8xi32> + %5 = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<32x!tt.ptr> + %7 = tt.addptr %6, %5 : tensor<32x!tt.ptr>, tensor<32xi32> + %8 = tt.load %7 : tensor<32x!tt.ptr> + %9 = tt.expand_dims %2 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %10 = tt.splat %arg3 : i32 -> tensor<8x1xi32> + %11 = arith.muli %9, %10 : tensor<8x1xi32> + %12 = tt.expand_dims %8 {axis = 0 : i32} : tensor<32xi64> -> tensor<1x32xi64> + %13 = arith.extsi %11 : tensor<8x1xi32> to tensor<8x1xi64> + %14 = tt.broadcast %13 : tensor<8x1xi64> -> tensor<8x32xi64> + %15 = tt.broadcast %12 : tensor<1x32xi64> -> tensor<8x32xi64> + %16 = arith.addi %14, %15 : tensor<8x32xi64> + %17 = tt.splat %arg2 : !tt.ptr -> tensor<8x32x!tt.ptr> + %18 = tt.addptr %17, %16 : tensor<8x32x!tt.ptr>, tensor<8x32xi64> + %19 = tt.load %18 : tensor<8x32x!tt.ptr> + %20 = math.exp %19 : tensor<8x32xf32> + %21 = tt.expand_dims %4 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %22 = arith.muli %21, %cst : tensor<8x1xi32> + %23 = tt.expand_dims %5 {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %24 = tt.broadcast %22 : tensor<8x1xi32> -> tensor<8x32xi32> + %25 = tt.broadcast %23 : tensor<1x32xi32> -> tensor<8x32xi32> + %26 = arith.addi %24, %25 : tensor<8x32xi32> + %27 = tt.splat %arg0 : !tt.ptr -> tensor<8x32x!tt.ptr> + %28 = tt.addptr %27, %26 : tensor<8x32x!tt.ptr>, tensor<8x32xi32> + tt.store %28, %20 : tensor<8x32x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @triton_indirect_load_kernel +// CHECK: ascend.indirect_load{{.*}} : , {{.*}} : tensor<8x32xi64> {{.*}}-> tensor<8x32xf32> +// CHECK: tt.store + +// ----- + +// Block-pointer load with mayDiscretememaccess must not use SIMT indirect_load. +tt.func public @block_ptr_load_skip_indirect(%arg0: !tt.ptr) -> tensor<32x32xf32> attributes {noinline = false} { + %c32_i64 = arith.constant 32 : i64 + %c1_i64 = arith.constant 1 : i64 + %c0_i32 = arith.constant 0 : i32 + %0 = tt.make_tensor_ptr %arg0, [%c32_i64, %c32_i64], [%c32_i64, %c1_i64], [%c0_i32, %c0_i32] {order = array} : > + %1 = tt.load %0 : !tt.ptr> + annotation.mark %1 {mayDiscretememaccess} : tensor<32x32xf32> + tt.return %1 : tensor<32x32xf32> +} + +// CHECK-LABEL: tt.func public @block_ptr_load_skip_indirect +// CHECK-NOT: ascend.indirect_load diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_mem_access.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_mem_access.mlir new file mode 100644 index 0000000000..20f055a905 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_mem_access.mlir @@ -0,0 +1,129 @@ +// RUN: triton-opt %s --triton-to-unstructure | FileCheck %s +// RUN: triton-opt %s '--triton-to-unstructure=force-scalarize-mode=True' | FileCheck %s +// RUN: triton-opt %s '--triton-to-unstructure=compile-on-910-95=True' | FileCheck %s +// RUN: triton-opt %s '--triton-to-unstructure=compile-mode=simd_simt_template' | FileCheck %s + + +// CHECK-LABEL: tt.func @triton_indirect_load +tt.func @triton_indirect_load(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) -> tensor<4096xf32> { + %0 = tt.make_range {end = 4096 : i32, start = 0 : i32} : tensor<4096xi32> + %1 = tt.splat %arg2 : !tt.ptr -> tensor<4096x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %3 = tt.load %2 : tensor<4096x!tt.ptr> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<4096x!tt.ptr> + %5 = tt.addptr %4, %3 : tensor<4096x!tt.ptr>, tensor<4096xi64> + %6 = tt.load %5 : tensor<4096x!tt.ptr> + annotation.mark %6 {mayDiscretememaccess} : tensor<4096xf32> + tt.return %6 : tensor<4096xf32> +} + + +// CHECK-LABEL: tt.func @triton_indirect_load_error_mark +tt.func @triton_indirect_load_error_mark(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) -> tensor<4096xf32> { + %0 = tt.make_range {end = 4096 : i32, start = 0 : i32} : tensor<4096xi32> + %1 = tt.splat %arg2 : !tt.ptr -> tensor<4096x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %3 = tt.load %2 : tensor<4096x!tt.ptr> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<4096x!tt.ptr> + %5 = tt.addptr %4, %3 : tensor<4096x!tt.ptr>, tensor<4096xi64> + %6 = tt.load %5 : tensor<4096x!tt.ptr> + annotation.mark %6 {error_mark} : tensor<4096xf32> + tt.return %6 : tensor<4096xf32> +} + + +// CHECK-LABEL: tt.func @triton_indirect_store +tt.func @triton_indirect_store(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %0 = tt.make_range {end = 4096 : i32, start = 0 : i32} : tensor<4096xi32> + %1 = tt.splat %arg2 : !tt.ptr -> tensor<4096x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %3 = tt.load %2 : tensor<4096x!tt.ptr> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<4096x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %6 = tt.load %5 : tensor<4096x!tt.ptr> + annotation.mark %6 {mayDiscretememaccess} : tensor<4096xf32> + %7 = tt.splat %arg1 : !tt.ptr -> tensor<4096x!tt.ptr> + %8 = tt.addptr %7, %3 : tensor<4096x!tt.ptr>, tensor<4096xi64> + tt.store %8, %6 : tensor<4096x!tt.ptr> + tt.return +} + + +// CHECK-LABEL: tt.func @triton_indirect_store_error_mark +tt.func @triton_indirect_store_error_mark(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr) { + %0 = tt.make_range {end = 4096 : i32, start = 0 : i32} : tensor<4096xi32> + %1 = tt.splat %arg2 : !tt.ptr -> tensor<4096x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %3 = tt.load %2 : tensor<4096x!tt.ptr> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<4096x!tt.ptr> + %5 = tt.addptr %4, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %6 = tt.load %5 : tensor<4096x!tt.ptr> + annotation.mark %6 {error_mark} : tensor<4096xf32> + %7 = tt.splat %arg1 : !tt.ptr -> tensor<4096x!tt.ptr> + %8 = tt.addptr %7, %3 : tensor<4096x!tt.ptr>, tensor<4096xi64> + tt.store %8, %6 : tensor<4096x!tt.ptr> + tt.return +} + + +// CHECK-LABEL: tt.func @triton_indirect_atomic_add +tt.func @triton_indirect_atomic_add( + %arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %arg2: !tt.ptr {tt.divisibility = 16 : i32} +) attributes {noinline = false} { + %cst = arith.constant dense<1.000000e+00> : tensor<4096xf32> + %cst_0 = arith.constant dense : tensor<4096xi1> + %0 = tt.make_range {end = 4096 : i32, start = 0 : i32} : tensor<4096xi32> + %1 = tt.splat %arg2 : !tt.ptr -> tensor<4096x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %3 = tt.load %2 : tensor<4096x!tt.ptr> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<4096x!tt.ptr> + %5 = tt.addptr %4, %3 : tensor<4096x!tt.ptr>, tensor<4096xi64> + %6 = tt.atomic_rmw fadd, acq_rel, gpu, %5, %cst, %cst_0 : (tensor<4096x!tt.ptr>, tensor<4096xf32>, tensor<4096xi1>) -> tensor<4096xf32> + tt.return +} + + +// CHECK-LABEL: tt.func @triton_indirect_atomic_add_with_discret_mask +tt.func @triton_indirect_atomic_add_with_discret_mask( + %arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %arg2: !tt.ptr {tt.divisibility = 16 : i32} +) attributes {noinline = false} { + %cst = arith.constant dense<1.000000e+00> : tensor<4096xf32> + %cst_0 = arith.constant dense<4096> : tensor<4096xi64> + %c1 = arith.constant 1.0 : f32 + %c0 = arith.constant 0.0 : f32 + %cst_true = tt.splat %c1 : f32 -> tensor<4096xf32> + %cst_false = tt.splat %c0 : f32 -> tensor<4096xf32> + %0 = tt.make_range {end = 4096 : i32, start = 0 : i32} : tensor<4096xi32> + %1 = tt.splat %arg2 : !tt.ptr -> tensor<4096x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %3 = tt.load %2 : tensor<4096x!tt.ptr> + %mask = arith.cmpi slt, %3, %cst_0 : tensor<4096xi64> + %selected_val = arith.select %mask, %cst_true, %cst_false : tensor<4096xi1>, tensor<4096xf32> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<4096x!tt.ptr> + %5 = tt.addptr %4, %3 : tensor<4096x!tt.ptr>, tensor<4096xi64> + %6 = tt.atomic_rmw fadd, acq_rel, gpu, %5, %selected_val, %mask {DiscreteMask} : (tensor<4096x!tt.ptr>, tensor<4096xf32>, tensor<4096xi1>) -> tensor<4096xf32> + tt.return +} + + +// CHECK-LABEL: tt.func @triton_indirect_atomic_cas +tt.func @triton_indirect_atomic_cas( + %arg0: !tt.ptr {tt.divisibility = 16 : i32}, + %arg1: !tt.ptr {tt.divisibility = 16 : i32}, + %arg2: !tt.ptr {tt.divisibility = 16 : i32} +) attributes {noinline = false} { + %cst = arith.constant dense<2.0> : tensor<4096xf32> + %cst_0 = arith.constant dense<1.0> : tensor<4096xf32> + %0 = tt.make_range {end = 4096 : i32, start = 0 : i32} : tensor<4096xi32> + %1 = tt.splat %arg2 : !tt.ptr -> tensor<4096x!tt.ptr> + %2 = tt.addptr %1, %0 : tensor<4096x!tt.ptr>, tensor<4096xi32> + %3 = tt.load %2 : tensor<4096x!tt.ptr> + %4 = tt.splat %arg0 : !tt.ptr -> tensor<4096x!tt.ptr> + %5 = tt.addptr %4, %3 : tensor<4096x!tt.ptr>, tensor<4096xi64> + %6 = tt.atomic_cas acq_rel, gpu, %5, %cst_0, %cst : (tensor<4096x!tt.ptr>, tensor<4096xf32>, tensor<4096xf32>) -> tensor<4096xf32> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_store.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_store.mlir new file mode 100644 index 0000000000..ef1d679898 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/indirect_store.mlir @@ -0,0 +1,72 @@ +// RUN: triton-opt --triton-to-structured '--discrete-mask-access-conversion=compile-on-910-95=True compile-mode=simd_simt_template' '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' %s --split-input-file | FileCheck %s +// RUN: triton-opt '--triton-to-unstructure=compile-on-910-95=True compile-mode=simd_simt_template' '--triton-to-linalg=compile-on-910-95=True compile-mode=simd_simt_template' --split-input-file %s | FileCheck %s --check-prefix=LINALG + +// tt.store -> ascend.indirect_store +tt.func public @triton_indirect_store_kernel(%arg0: !tt.ptr, %arg1: !tt.ptr, %arg2: !tt.ptr, %arg3: i32) attributes {noinline = false} { + %cst = arith.constant dense<32> : tensor<8x1xi32> + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c8_i32 : i32 + %2 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %3 = tt.splat %1 : i32 -> tensor<8xi32> + %4 = arith.addi %3, %2 : tensor<8xi32> + %5 = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<32x!tt.ptr> + %7 = tt.addptr %6, %5 : tensor<32x!tt.ptr>, tensor<32xi32> + %8 = tt.load %7 : tensor<32x!tt.ptr> + %9 = tt.expand_dims %2 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %10 = tt.splat %arg3 : i32 -> tensor<8x1xi32> + %11 = arith.muli %9, %10 : tensor<8x1xi32> + %12 = tt.expand_dims %8 {axis = 0 : i32} : tensor<32xi64> -> tensor<1x32xi64> + %13 = arith.extsi %11 : tensor<8x1xi32> to tensor<8x1xi64> + %14 = tt.broadcast %13 : tensor<8x1xi64> -> tensor<8x32xi64> + %15 = tt.broadcast %12 : tensor<1x32xi64> -> tensor<8x32xi64> + %16 = arith.addi %14, %15 : tensor<8x32xi64> + %17 = tt.splat %arg2 : !tt.ptr -> tensor<8x32x!tt.ptr> + %18 = tt.addptr %17, %16 : tensor<8x32x!tt.ptr>, tensor<8x32xi64> + %19 = tt.load %18 : tensor<8x32x!tt.ptr> + %20 = math.exp %19 : tensor<8x32xf32> + %21 = tt.expand_dims %4 {axis = 1 : i32} : tensor<8xi32> -> tensor<8x1xi32> + %22 = arith.muli %21, %cst : tensor<8x1xi32> + %23 = arith.extsi %22 : tensor<8x1xi32> to tensor<8x1xi64> + %24 = tt.broadcast %23 : tensor<8x1xi64> -> tensor<8x32xi64> + %25 = arith.addi %24, %15 : tensor<8x32xi64> + %26 = tt.splat %arg0 : !tt.ptr -> tensor<8x32x!tt.ptr> + %27 = tt.addptr %26, %25 : tensor<8x32x!tt.ptr>, tensor<8x32xi64> + tt.store %27, %20 : tensor<8x32x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @triton_indirect_store_kernel( +// CHECK: ascend.indirect_store {{.*}} : , {{.*}} : tensor<8x32xi64>, {{.*}} : tensor<8x32xf32> +// CHECK: tt.return +// CHECK: } + +// tt.int_to_ptr + tt.store -> ascend.indirect_store with the original pointer. +// TritonToLinalg materializes the pointer at the indirect-store consumer. +// CHECK-LABEL: tt.func public @triton_indirect_store_int_to_ptr_kernel( +// CHECK: %[[BASE:.*]] = tt.int_to_ptr {{.*}} : i64 -> !tt.ptr +// CHECK-NOT: tt.addptr %[[BASE]] +// CHECK: ascend.indirect_store %[[BASE]] : , {{.*}} : tensor<8xi64>, {{.*}} : tensor<8xf32> +// CHECK: tt.return +// CHECK: } + +// The same kernel lowered through TritonToLinalg must produce a +// hivm::PointerCastOp (memref) directly from int_to_ptr and finally a func.call +// @triton_indirect_store. +// LINALG-LABEL: func.func @triton_indirect_store_int_to_ptr_kernel +// LINALG: hivm.hir.pointer_cast(%{{.*}}) [%{{.*}}] : memref +// LINALG: call @triton_indirect_store{{.*}}(%{{.*}}, %{{.*}}, %{{.*}}) : (memref<1xf32, strided<[1]>>, tensor<8xi64>, tensor<8xf32>) -> () +tt.func public @triton_indirect_store_int_to_ptr_kernel(%arg0: !tt.ptr, %arg1: !tt.ptr) { + %base_i64 = arith.constant 1024 : i64 + %base = tt.int_to_ptr %base_i64 : i64 -> !tt.ptr + %cst = arith.constant dense<32> : tensor<8xi32> + %range = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %idx = arith.addi %range, %cst : tensor<8xi32> + %base_splat = tt.splat %base : !tt.ptr -> tensor<8x!tt.ptr> + %ptr = tt.addptr %base_splat, %idx : tensor<8x!tt.ptr>, tensor<8xi32> + %val = tt.load %arg1 : !tt.ptr + %val_splat = tt.splat %val : f32 -> tensor<8xf32> + tt.store %ptr, %val_splat {MixCompileDiscreteMask} : tensor<8x!tt.ptr> + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/mixed_axis_load_mask.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/mixed_axis_load_mask.mlir new file mode 100644 index 0000000000..a35eda5c64 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/mixed_axis_load_mask.mlir @@ -0,0 +1,70 @@ +// RUN: triton-opt %s --triton-to-unstructure --verify-each | FileCheck %s --check-prefix=T2U +// RUN: triton-opt %s --triton-to-unstructure --bubble-up-operation --triton-to-linalg --verify-each | FileCheck %s --check-prefix=LOWER +// RUN: triton-opt %s --triton-to-unstructure='compile-on-910-95=true compile-mode=simd_simt_template' --bubble-up-operation --triton-to-linalg='compile-on-910-95=true compile-mode=simd_simt_template' --verify-each | FileCheck %s --check-prefix=LOWER +// RUN: sed 's/%mask, %other/%mask/' %s | triton-opt --triton-to-unstructure --bubble-up-operation --triton-to-linalg --verify-each | FileCheck %s --check-prefix=LOWER +// RUN: sed 's/cmpi slt, %x/cmpi eq, %x/' %s | triton-opt --triton-to-unstructure --verify-each | FileCheck %s --check-prefix=KEEP +// RUN: sed 's/cmpi slt, %x/cmpi sge, %x/' %s | triton-opt --triton-to-unstructure --verify-each | FileCheck %s --check-prefix=KEEP + +// The row addresses are nonstructured, but columns have a fixed stride. Both +// mask bounds must guard memory access, even when the result is selected later. +// Runtime lower/upper bounds also cover nonzero starts and empty intersections. +module attributes {hacc.target = #hacc.target<"Ascend950PR_9579">} { + // T2U-LABEL: tt.func public @mixed_axis_load_mask( + // T2U: %[[COLMASK:.*]] = tt.broadcast %{{.*}} : tensor<1x32xi1> -> tensor<3x32xi1> + // T2U: %[[FULLMASK:.*]] = arith.andi + // T2U: scf.for + // T2U: %[[MASK:.*]] = tensor.extract_slice %[[COLMASK]][%{{.*}}, 0] [1, 32] [1, 1] {DiscreteMemAccess} : tensor<3x32xi1> to tensor<1x32xi1> + // T2U: tt.load %{{[^,]+}}, %[[MASK]] {DiscreteMemAccess} : tensor<1x32x!tt.ptr> + // T2U: arith.select %[[FULLMASK]], + // KEEP-LABEL: tt.func public @mixed_axis_load_mask( + // KEEP: %[[FULLMASK:.*]] = arith.andi %{{.*}}, %{{.*}} : tensor<3x32xi1> + // KEEP: scf.for + // KEEP: %[[MASK:.*]] = tensor.extract_slice %[[FULLMASK]][%{{.*}}, 0] [1, 32] [1, 1] {DiscreteMemAccess} : tensor<3x32xi1> to tensor<1x32xi1> + // KEEP: tt.load %{{[^,]+}}, %[[MASK]] {DiscreteMemAccess} : tensor<1x32x!tt.ptr> + // LOWER-LABEL: func.func @mixed_axis_load_mask( + // LOWER: scf.for + // LOWER: %[[BASE:.*]] = memref.reinterpret_cast + // LOWER: %[[SOURCE:.*]] = memref.subview %[[BASE]] + // LOWER: memref.copy %[[SOURCE]], %{{.*}} : memref<1x?xf32, + // LOWER: return + tt.func public @mixed_axis_load_mask(%input: !tt.ptr, %output: !tt.ptr, %rows: i32, %lower: i32, %upper: i32) { + %x = tt.make_range {start = 0 : i32, end = 3 : i32} : tensor<3xi32> + %r = tt.make_range {start = 0 : i32, end = 32 : i32} : tensor<32xi32> + %two = arith.constant dense<2> : tensor<3xi32> + %stride = arith.constant dense<16> : tensor<32xi32> + %row = arith.remsi %x, %two : tensor<3xi32> + %col = arith.muli %r, %stride : tensor<32xi32> + %row2d = tt.expand_dims %row {axis = 1 : i32} : tensor<3xi32> -> tensor<3x1xi32> + %col2d = tt.expand_dims %col {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %rowb = tt.broadcast %row2d : tensor<3x1xi32> -> tensor<3x32xi32> + %colb = tt.broadcast %col2d : tensor<1x32xi32> -> tensor<3x32xi32> + %offset = arith.addi %rowb, %colb : tensor<3x32xi32> + %base = tt.splat %input : !tt.ptr -> tensor<3x32x!tt.ptr> + %ptr = tt.addptr %base, %offset : tensor<3x32x!tt.ptr>, tensor<3x32xi32> + %rowsb = tt.splat %rows : i32 -> tensor<3xi32> + %xm = arith.cmpi slt, %x, %rowsb : tensor<3xi32> + %lo = tt.splat %lower : i32 -> tensor<32xi32> + %hi = tt.splat %upper : i32 -> tensor<32xi32> + %rmlo = arith.cmpi sge, %r, %lo : tensor<32xi32> + %rmhi = arith.cmpi slt, %r, %hi : tensor<32xi32> + %rm = arith.andi %rmlo, %rmhi : tensor<32xi1> + %xm2d = tt.expand_dims %xm {axis = 1 : i32} : tensor<3xi1> -> tensor<3x1xi1> + %rm2d = tt.expand_dims %rm {axis = 0 : i32} : tensor<32xi1> -> tensor<1x32xi1> + %xmb = tt.broadcast %xm2d : tensor<3x1xi1> -> tensor<3x32xi1> + %rmb = tt.broadcast %rm2d : tensor<1x32xi1> -> tensor<3x32xi1> + %mask = arith.andi %xmb, %rmb : tensor<3x32xi1> + %other = arith.constant dense<-7.0> : tensor<3x32xf32> + %value = tt.load %ptr, %mask, %other : tensor<3x32x!tt.ptr> + %outstride = arith.constant dense<32> : tensor<3xi32> + %outrow = arith.muli %x, %outstride : tensor<3xi32> + %outrow2d = tt.expand_dims %outrow {axis = 1 : i32} : tensor<3xi32> -> tensor<3x1xi32> + %r2d = tt.expand_dims %r {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32> + %outrowb = tt.broadcast %outrow2d : tensor<3x1xi32> -> tensor<3x32xi32> + %rb = tt.broadcast %r2d : tensor<1x32xi32> -> tensor<3x32xi32> + %outoffset = arith.addi %outrowb, %rb : tensor<3x32xi32> + %outbase = tt.splat %output : !tt.ptr -> tensor<3x32x!tt.ptr> + %outptr = tt.addptr %outbase, %outoffset : tensor<3x32x!tt.ptr>, tensor<3x32xi32> + tt.store %outptr, %value : tensor<3x32x!tt.ptr> + tt.return + } +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/nested_loop.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/nested_loop.mlir new file mode 100644 index 0000000000..e19caadd25 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/nested_loop.mlir @@ -0,0 +1,204 @@ +// RUN: triton-opt --triton-to-unstructure --split-input-file %s | FileCheck %s + +tt.func public @test_kernel(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c3_i32 = arith.constant 3 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0_i32 = arith.constant 0 : i32 + %cst = arith.constant dense<128> : tensor<128xi32> + %cst_0 = arith.constant dense<0> : tensor<128xi32> + %cst_1 = arith.constant dense<300> : tensor<128xi32> + %c128_i32 = arith.constant 128 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c128_i32 : i32 + %2 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %3 = tt.splat %1 : i32 -> tensor<128xi32> + %4 = arith.addi %3, %2 : tensor<128xi32> + %5 = tt.splat %arg0 : !tt.ptr -> tensor<128x!tt.ptr> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<128x!tt.ptr> + %7 = tt.addptr %6, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %8 = tt.splat %arg2 : !tt.ptr -> tensor<128x!tt.ptr> + %9 = tt.addptr %8, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %10 = tt.load %9 : tensor<128x!tt.ptr> + %11 = tt.splat %arg3 : !tt.ptr -> tensor<128x!tt.ptr> + %12 = tt.addptr %11, %10 : tensor<128x!tt.ptr>, tensor<128xi32> + %13:3 = scf.for %arg4 = %c0_i32 to %c3_i32 step %c1_i32 iter_args(%arg5 = %4, %arg6 = %7, %arg7 = %12) -> (tensor<128xi32>, tensor<128x!tt.ptr>, tensor<128x!tt.ptr>) : i32 { + %14:3 = scf.for %arg8 = %c0_i32 to %c3_i32 step %c1_i32 iter_args(%arg9 = %arg5, %arg10 = %arg6, %arg11 = %arg7) -> (tensor<128xi32>, tensor<128x!tt.ptr>, tensor<128x!tt.ptr>) : i32 { + %18 = arith.cmpi slt, %arg9, %cst_1 : tensor<128xi32> + %19 = tt.addptr %5, %arg9 : tensor<128x!tt.ptr>, tensor<128xi32> + %20 = tt.load %19, %18, %cst_0 : tensor<128x!tt.ptr> + %21 = tt.load %arg11 : tensor<128x!tt.ptr> + %22 = arith.addi %20, %21 : tensor<128xi32> + tt.store %arg10, %22, %18 : tensor<128x!tt.ptr> + %23 = arith.addi %arg9, %cst : tensor<128xi32> + %24 = tt.addptr %arg10, %cst : tensor<128x!tt.ptr>, tensor<128xi32> + %25 = tt.addptr %arg11, %cst : tensor<128x!tt.ptr>, tensor<128xi32> + scf.yield %23, %24, %25 : tensor<128xi32>, tensor<128x!tt.ptr>, tensor<128x!tt.ptr> + } + %15 = arith.addi %14#0, %cst : tensor<128xi32> + %16 = tt.addptr %14#1, %cst : tensor<128x!tt.ptr>, tensor<128xi32> + %17 = tt.addptr %14#2, %cst : tensor<128x!tt.ptr>, tensor<128xi32> + scf.yield %15, %16, %17 : tensor<128xi32>, tensor<128x!tt.ptr>, tensor<128x!tt.ptr> + } + tt.return +} + +// CHECK-LABEL: tt.func public @test_kernel( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_1:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_2:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_3:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { +// CHECK: %[[VAL_4:.*]] = arith.constant dense<128> : tensor<128xi64> +// CHECK: %[[VAL_8:.*]] = arith.constant 3 : i32 +// CHECK: %[[VAL_15:.*]] = tt.get_program_id x : i32 +// CHECK: %[[VAL_16:.*]] = arith.muli %[[VAL_15]], %{{.*}} : i32 +// CHECK: %[[VAL_17:.*]] = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> +// CHECK: %[[VAL_18:.*]] = tt.splat %[[VAL_16]] : i32 -> tensor<128xi32> +// CHECK: %[[VAL_19:.*]] = arith.addi %[[VAL_18]], %[[VAL_17]] : tensor<128xi32> +// CHECK: %[[VAL_20:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<128x!tt.ptr> +// CHECK: %[[VAL_21:.*]] = arith.extsi %[[VAL_19]] : tensor<128xi32> to tensor<128xi64> +// CHECK: %[[VAL_22:.*]] = tt.splat %[[VAL_2]] : !tt.ptr -> tensor<128x!tt.ptr> +// CHECK: %[[VAL_23:.*]] = tt.addptr %[[VAL_22]], %[[VAL_19]] : tensor<128x!tt.ptr>, tensor<128xi32> +// CHECK: %[[VAL_24:.*]] = tt.load %[[VAL_23]] : tensor<128x!tt.ptr> +// CHECK: %[[VAL_25:.*]] = arith.extsi %[[VAL_24]] : tensor<128xi32> to tensor<128xi64> +// CHECK: %[[VAL_26:.*]]:3 = scf.for %[[VAL_27:.*]] = %{{.*}} to %[[VAL_8]] step %{{.*}} iter_args(%[[VAL_28:.*]] = %[[VAL_19]], %[[VAL_29:.*]] = %[[VAL_21]], %[[VAL_30:.*]] = %[[VAL_25]]) -> (tensor<128xi32>, tensor<128xi64>, tensor<128xi64>) : i32 { +// CHECK: %[[VAL_31:.*]]:3 = scf.for %[[VAL_32:.*]] = %{{.*}} to %[[VAL_8]] step %{{.*}} iter_args(%[[VAL_33:.*]] = %[[VAL_28]], %[[VAL_34:.*]] = %[[VAL_29]], %[[VAL_35:.*]] = %[[VAL_30]]) -> (tensor<128xi32>, tensor<128xi64>, tensor<128xi64>) : i32 { +// CHECK: %[[VAL_36:.*]] = tt.splat %[[VAL_1]] : !tt.ptr -> tensor<128x!tt.ptr> +// CHECK: %[[VAL_37:.*]] = tt.addptr %[[VAL_36]], %[[VAL_34]] : tensor<128x!tt.ptr>, tensor<128xi64> +// CHECK: %[[VAL_38:.*]] = arith.cmpi slt, %[[VAL_33]], %{{.*}} : tensor<128xi32> +// CHECK: %[[VAL_39:.*]] = tt.addptr %[[VAL_20]], %[[VAL_33]] : tensor<128x!tt.ptr>, tensor<128xi32> +// CHECK: %[[VAL_40:.*]] = tt.load %[[VAL_39]], %[[VAL_38]], %{{.*}} : tensor<128x!tt.ptr> +// CHECK: %[[VAL_41:.*]] = tensor.empty() : tensor<128xi32> +// CHECK: %[[VAL_42:.*]] = scf.for %[[VAL_43:.*]] = %{{.*}} to %{{.*}} step %{{.*}} iter_args(%[[VAL_44:.*]] = %[[VAL_41]]) -> (tensor<128xi32>) { +// CHECK: %[[VAL_45:.*]] = tensor.extract %[[VAL_35]]{{\[}}%[[VAL_43]]] {DiscreteMemAccess} : tensor<128xi64> +// CHECK: %[[VAL_46:.*]] = tt.addptr %[[VAL_3]], %[[VAL_45]] : !tt.ptr, i64 +// CHECK: %[[VAL_47:.*]] = tt.load %[[VAL_46]] {DiscreteMemAccess} : !tt.ptr +// CHECK: scf.yield {DiscreteMemAccess} %{{.*}} : tensor<128xi32> +// CHECK: } {ExtractedLoadOrStore} +// CHECK: %[[VAL_50:.*]] = arith.addi %[[VAL_40]], %[[VAL_42]] : tensor<128xi32> +// CHECK: tt.store %[[VAL_37]], %[[VAL_50]], %[[VAL_38]] : tensor<128x!tt.ptr> +// CHECK: %[[VAL_51:.*]] = arith.addi %[[VAL_33]], %{{.*}} : tensor<128xi32> +// CHECK: %[[VAL_52:.*]] = arith.addi %[[VAL_34]], %[[VAL_4]] : tensor<128xi64> +// CHECK: %[[VAL_53:.*]] = arith.addi %[[VAL_35]], %[[VAL_4]] : tensor<128xi64> +// CHECK: scf.yield %[[VAL_51]], %[[VAL_52]], %[[VAL_53]] : tensor<128xi32>, tensor<128xi64>, tensor<128xi64> +// CHECK: } +// CHECK: %[[VAL_54:.*]] = arith.addi %[[VAL_55:.*]]#0, %{{.*}} : tensor<128xi32> +// CHECK: %[[VAL_56:.*]] = arith.addi %[[VAL_55]]#1, %[[VAL_4]] : tensor<128xi64> +// CHECK: %[[VAL_57:.*]] = arith.addi %[[VAL_55]]#2, %[[VAL_4]] : tensor<128xi64> +// CHECK: scf.yield %[[VAL_54]], %[[VAL_56]], %[[VAL_57]] : tensor<128xi32>, tensor<128xi64>, tensor<128xi64> +// CHECK: } +// CHECK: tt.return +// CHECK: } + +// ----- + +tt.func public @test_kernel2(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %c300_i32 = arith.constant 300 : i32 + %c3_i32 = arith.constant 3 : i32 + %c1_i32 = arith.constant 1 : i32 + %c0_i32 = arith.constant 0 : i32 + %cst = arith.constant dense<128> : tensor<128xi32> + %cst_0 = arith.constant dense<0> : tensor<128xi32> + %cst_1 = arith.constant dense<300> : tensor<128xi32> + %c128_i32 = arith.constant 128 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c128_i32 : i32 + %2 = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> + %3 = tt.splat %1 : i32 -> tensor<128xi32> + %4 = arith.addi %3, %2 : tensor<128xi32> + %5 = tt.splat %arg0 : !tt.ptr -> tensor<128x!tt.ptr> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<128x!tt.ptr> + %7 = tt.addptr %6, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %8 = tt.splat %arg2 : !tt.ptr -> tensor<128x!tt.ptr> + %9 = tt.addptr %8, %4 : tensor<128x!tt.ptr>, tensor<128xi32> + %10 = tt.load %9 : tensor<128x!tt.ptr> + %11 = tt.splat %arg3 : !tt.ptr -> tensor<128x!tt.ptr> + %12 = tt.addptr %11, %10 : tensor<128x!tt.ptr>, tensor<128xi32> + %13:3 = scf.while (%arg4 = %cst, %arg5 = %4, %arg6 = %7, %arg7 = %12) : (tensor<128xi32>, tensor<128xi32>, tensor<128x!tt.ptr>, tensor<128x!tt.ptr>) -> (tensor<128x!tt.ptr>, tensor<128xi32>, tensor<128x!tt.ptr>) { + %14 = "tt.reduce"(%arg4) <{axis = 0 : i32}> ({ + ^bb0(%arg8: i32, %arg9: i32): + %16 = arith.addi %arg8, %arg9 : i32 + tt.reduce.return %16 : i32 + }) : (tensor<128xi32>) -> i32 + %15 = arith.cmpi slt, %14, %c300_i32 : i32 + scf.condition(%15) %arg7, %arg5, %arg6 : tensor<128x!tt.ptr>, tensor<128xi32>, tensor<128x!tt.ptr> + } do { + ^bb0(%arg4: tensor<128x!tt.ptr>, %arg5: tensor<128xi32>, %arg6: tensor<128x!tt.ptr>): + %14:4 = scf.while (%arg7 = %c0_i32, %arg8 = %arg6, %arg9 = %arg4, %arg10 = %arg5) : (i32, tensor<128x!tt.ptr>, tensor<128x!tt.ptr>, tensor<128xi32>) -> (tensor<128xi32>, i32, tensor<128x!tt.ptr>, tensor<128x!tt.ptr>) { + %18 = arith.cmpi slt, %arg7, %c3_i32 : i32 + scf.condition(%18) %arg10, %arg7, %arg8, %arg9 : tensor<128xi32>, i32, tensor<128x!tt.ptr>, tensor<128x!tt.ptr> + } do { + ^bb0(%arg7: tensor<128xi32>, %arg8: i32, %arg9: tensor<128x!tt.ptr>, %arg10: tensor<128x!tt.ptr>): + %18 = arith.cmpi slt, %arg7, %cst_1 : tensor<128xi32> + %19 = tt.addptr %5, %arg7 : tensor<128x!tt.ptr>, tensor<128xi32> + %20 = tt.load %19, %18, %cst_0 : tensor<128x!tt.ptr> + %21 = tt.load %arg10 : tensor<128x!tt.ptr> + %22 = arith.addi %20, %21 : tensor<128xi32> + tt.store %arg9, %22, %18 : tensor<128x!tt.ptr> + %23 = arith.addi %arg7, %cst : tensor<128xi32> + %24 = arith.addi %arg8, %c1_i32 : i32 + %25 = tt.addptr %arg9, %cst : tensor<128x!tt.ptr>, tensor<128xi32> + %26 = tt.addptr %arg10, %cst : tensor<128x!tt.ptr>, tensor<128xi32> + scf.yield %24, %25, %26, %23 : i32, tensor<128x!tt.ptr>, tensor<128x!tt.ptr>, tensor<128xi32> + } + %15 = arith.addi %14#0, %cst : tensor<128xi32> + %16 = tt.addptr %14#2, %cst : tensor<128x!tt.ptr>, tensor<128xi32> + %17 = tt.addptr %14#3, %cst : tensor<128x!tt.ptr>, tensor<128xi32> + scf.yield %14#0, %15, %16, %17 : tensor<128xi32>, tensor<128xi32>, tensor<128x!tt.ptr>, tensor<128x!tt.ptr> + } + tt.return +} + +// CHECK-LABEL: tt.func public @test_kernel2( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_1:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_2:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_3:.*]]: !tt.ptr {tt.divisibility = 16 : i32}) attributes {noinline = false} { +// CHECK: %[[VAL_4:.*]] = arith.constant dense<128> : tensor<128xi64> +// CHECK: %[[VAL_8:.*]] = arith.constant 300 : i32 +// CHECK: %[[VAL_16:.*]] = tt.get_program_id x : i32 +// CHECK: %[[VAL_17:.*]] = arith.muli %[[VAL_16]], %{{.*}} : i32 +// CHECK: %[[VAL_18:.*]] = tt.make_range {end = 128 : i32, start = 0 : i32} : tensor<128xi32> +// CHECK: %[[VAL_19:.*]] = tt.splat %[[VAL_17]] : i32 -> tensor<128xi32> +// CHECK: %[[VAL_20:.*]] = arith.addi %[[VAL_19]], %[[VAL_18]] : tensor<128xi32> +// CHECK: %[[VAL_21:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<128x!tt.ptr> +// CHECK: %[[VAL_22:.*]] = arith.extsi %[[VAL_20]] : tensor<128xi32> to tensor<128xi64> +// CHECK: %[[VAL_23:.*]] = tt.splat %[[VAL_2]] : !tt.ptr -> tensor<128x!tt.ptr> +// CHECK: %[[VAL_24:.*]] = tt.addptr %[[VAL_23]], %[[VAL_20]] : tensor<128x!tt.ptr>, tensor<128xi32> +// CHECK: %[[VAL_25:.*]] = tt.load %[[VAL_24]] : tensor<128x!tt.ptr> +// CHECK: %[[VAL_26:.*]] = arith.extsi %[[VAL_25]] : tensor<128xi32> to tensor<128xi64> +// CHECK: %[[VAL_27:.*]]:3 = scf.while (%[[VAL_28:.*]] = %{{.*}}, %[[VAL_29:.*]] = %[[VAL_20]], %[[VAL_30:.*]] = %[[VAL_22]], %[[VAL_31:.*]] = %[[VAL_26]]) : (tensor<128xi32>, tensor<128xi32>, tensor<128xi64>, tensor<128xi64>) -> (tensor<128xi64>, tensor<128xi32>, tensor<128xi64>) { +// CHECK: %[[VAL_32:.*]] = "tt.reduce"(%[[VAL_28]]) <{axis = 0 : i32}> ({ +// CHECK: ^bb0(%[[VAL_33:.*]]: i32, %[[VAL_34:.*]]: i32): +// CHECK: %[[VAL_35:.*]] = arith.addi %[[VAL_33]], %[[VAL_34]] : i32 +// CHECK: tt.reduce.return %[[VAL_35]] : i32 +// CHECK: }) : (tensor<128xi32>) -> i32 +// CHECK: %[[VAL_36:.*]] = arith.cmpi slt, %[[VAL_32]], %[[VAL_8]] : i32 +// CHECK: scf.condition(%[[VAL_36]]) %[[VAL_31]], %[[VAL_29]], %[[VAL_30]] : tensor<128xi64>, tensor<128xi32>, tensor<128xi64> +// CHECK: } do { +// CHECK: ^bb0(%[[VAL_37:.*]]: tensor<128xi64>, %[[VAL_38:.*]]: tensor<128xi32>, %[[VAL_39:.*]]: tensor<128xi64>): +// CHECK: %[[VAL_40:.*]]:4 = scf.while (%[[VAL_41:.*]] = %{{.*}}, %[[VAL_42:.*]] = %[[VAL_39]], %[[VAL_43:.*]] = %[[VAL_37]], %[[VAL_44:.*]] = %[[VAL_38]]) : (i32, tensor<128xi64>, tensor<128xi64>, tensor<128xi32>) -> (i32, tensor<128xi64>, tensor<128xi64>, tensor<128xi32>) { +// CHECK: %[[VAL_45:.*]] = arith.cmpi slt, %[[VAL_41]], %{{.*}} : i32 +// CHECK: scf.condition(%[[VAL_45]]) %[[VAL_41]], %[[VAL_42]], %[[VAL_43]], %[[VAL_44]] : i32, tensor<128xi64>, tensor<128xi64>, tensor<128xi32> +// CHECK: } do { +// CHECK: ^bb0(%[[VAL_46:.*]]: i32, %[[VAL_47:.*]]: tensor<128xi64>, %[[VAL_48:.*]]: tensor<128xi64>, %[[VAL_49:.*]]: tensor<128xi32>): +// CHECK: %[[VAL_50:.*]] = tt.splat %[[VAL_1]] : !tt.ptr -> tensor<128x!tt.ptr> +// CHECK: %[[VAL_51:.*]] = tt.addptr %[[VAL_50]], %[[VAL_47]] : tensor<128x!tt.ptr>, tensor<128xi64> +// CHECK: %[[VAL_52:.*]] = arith.cmpi slt, %[[VAL_49]], %{{.*}} : tensor<128xi32> +// CHECK: %[[VAL_53:.*]] = tt.addptr %[[VAL_21]], %[[VAL_49]] : tensor<128x!tt.ptr>, tensor<128xi32> +// CHECK: %[[VAL_54:.*]] = tt.load %[[VAL_53]], %[[VAL_52]], %{{.*}} : tensor<128x!tt.ptr> +// CHECK: %[[VAL_55:.*]] = tensor.empty() : tensor<128xi32> +// CHECK: %[[VAL_56:.*]] = scf.for %[[VAL_57:.*]] = %{{.*}} to %{{.*}} step %{{.*}} iter_args(%[[VAL_58:.*]] = %[[VAL_55]]) -> (tensor<128xi32>) { +// CHECK: %[[VAL_59:.*]] = tensor.extract %[[VAL_48]]{{\[}}%[[VAL_57]]] {DiscreteMemAccess} : tensor<128xi64> +// CHECK: %[[VAL_60:.*]] = tt.addptr %[[VAL_3]], %[[VAL_59]] : !tt.ptr, i64 +// CHECK: %[[VAL_61:.*]] = tt.load %[[VAL_60]] {DiscreteMemAccess} : !tt.ptr +// CHECK: scf.yield {DiscreteMemAccess} %{{.*}} : tensor<128xi32> +// CHECK: } {ExtractedLoadOrStore} +// CHECK: %[[VAL_64:.*]] = arith.addi %[[VAL_54]], %[[VAL_56]] : tensor<128xi32> +// CHECK: tt.store %[[VAL_51]], %[[VAL_64]], %[[VAL_52]] : tensor<128x!tt.ptr> +// CHECK: %[[VAL_65:.*]] = arith.addi %[[VAL_49]], %{{.*}} : tensor<128xi32> +// CHECK: %[[VAL_66:.*]] = arith.addi %[[VAL_46]], %{{.*}} : i32 +// CHECK: %[[VAL_67:.*]] = arith.addi %[[VAL_47]], %[[VAL_4]] : tensor<128xi64> +// CHECK: %[[VAL_68:.*]] = arith.addi %[[VAL_48]], %[[VAL_4]] : tensor<128xi64> +// CHECK: scf.yield %[[VAL_66]], %[[VAL_67]], %[[VAL_68]], %[[VAL_65]] : i32, tensor<128xi64>, tensor<128xi64>, tensor<128xi32> +// CHECK: } +// CHECK: %[[VAL_69:.*]] = arith.addi %[[VAL_70:.*]]#3, %{{.*}} : tensor<128xi32> +// CHECK: %[[VAL_71:.*]] = arith.addi %[[VAL_70]]#1, %[[VAL_4]] : tensor<128xi64> +// CHECK: %[[VAL_72:.*]] = arith.addi %[[VAL_70]]#2, %[[VAL_4]] : tensor<128xi64> +// CHECK: scf.yield %[[VAL_70]]#3, %[[VAL_69]], %[[VAL_71]], %[[VAL_72]] : tensor<128xi32>, tensor<128xi32>, tensor<128xi64>, tensor<128xi64> +// CHECK: } +// CHECK: tt.return +// CHECK: } diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/nested_pointer_retirement.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/nested_pointer_retirement.mlir new file mode 100644 index 0000000000..5305c4604b --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/nested_pointer_retirement.mlir @@ -0,0 +1,49 @@ +// RUN: triton-opt %s --triton-to-unstructure | FileCheck %s +// RUN: triton-opt %s --triton-to-unstructure | FileCheck %s --check-prefix=COUNT +// +// Nested scalar and tensor pointer carriers must retain their live offsets. +// Retired copies must not remain in the function and duplicate its stores. +// +// CHECK-LABEL: tt.func public @nested_pointer_retirement( +// CHECK-DAG: %[[ONE:.*]] = arith.constant 1 : i64 +// CHECK-DAG: %[[STEP:.*]] = arith.constant dense<1> : tensor<4xi64> +// CHECK: scf.for {{.*}} iter_args(%[[OUTER_SCALAR:.*]] = {{.*}}, %[[OUTER_VECTOR:.*]] = {{.*}}) -> (i64, tensor<4xi64>) +// CHECK: %[[INNER:.*]]:2 = scf.for {{.*}} iter_args(%[[SCALAR:.*]] = %[[OUTER_SCALAR]], %[[VECTOR:.*]] = %[[OUTER_VECTOR]]) -> (i64, tensor<4xi64>) +// CHECK: tt.addptr {{.*}}, %[[VECTOR]] : tensor<4x!tt.ptr>, tensor<4xi64> +// CHECK: %[[POINTER:.*]] = tt.addptr {{.*}}, %[[SCALAR]] : !tt.ptr, i64 +// CHECK: tt.load %[[POINTER]] : !tt.ptr +// CHECK: tt.store +// CHECK-DAG: %[[NEXT_SCALAR:.*]] = arith.addi %[[SCALAR]], %[[ONE]] : i64 +// CHECK-DAG: %[[NEXT_VECTOR:.*]] = arith.addi %[[VECTOR]], %[[STEP]] : tensor<4xi64> +// CHECK: scf.yield %[[NEXT_SCALAR]], %[[NEXT_VECTOR]] : i64, tensor<4xi64> +// CHECK: scf.yield %[[INNER]]#0, %[[INNER]]#1 : i64, tensor<4xi64> +// CHECK: tt.return +// +// COUNT-LABEL: tt.func public @nested_pointer_retirement( +// COUNT-COUNT-2: scf.for +// COUNT-NOT: scf.for +// COUNT: tt.store +// COUNT-NOT: scf.for +// COUNT-NOT: tt.store +// COUNT: tt.return + +tt.func public @nested_pointer_retirement(%base: !tt.ptr, %n: i32) { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %step = arith.constant dense<1> : tensor<4xi32> + %splat = tt.splat %base : !tt.ptr -> tensor<4x!tt.ptr> + %initial = tt.addptr %splat, %range : tensor<4x!tt.ptr>, tensor<4xi32> + %outer:2 = scf.for %i = %c0 to %n step %c1 iter_args(%p = %base, %v = %initial) -> (!tt.ptr, tensor<4x!tt.ptr>) : i32 { + %inner:2 = scf.for %j = %c0 to %n step %c1 iter_args(%q = %p, %w = %v) -> (!tt.ptr, tensor<4x!tt.ptr>) : i32 { + %y = tt.load %q : !tt.ptr + %ys = tt.splat %y : i32 -> tensor<4xi32> + tt.store %w, %ys : tensor<4x!tt.ptr> + %nextq = tt.addptr %q, %c1 : !tt.ptr, i32 + %nextw = tt.addptr %w, %step : tensor<4x!tt.ptr>, tensor<4xi32> + scf.yield %nextq, %nextw : !tt.ptr, tensor<4x!tt.ptr> + } + scf.yield %inner#0, %inner#1 : !tt.ptr, tensor<4x!tt.ptr> + } + tt.return +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/pointer_reshape.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/pointer_reshape.mlir new file mode 100644 index 0000000000..082fda1f9c --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/pointer_reshape.mlir @@ -0,0 +1,137 @@ +// RUN: triton-opt %s --triton-to-unstructure | FileCheck %s --check-prefixes=CHECK,SCALAR +// RUN: triton-opt %s --triton-to-unstructure=compile-on-910-95=true | FileCheck %s --check-prefixes=CHECK,INDIRECT + +// Rank reduction must retain the common base, the i64 offsets, the mask, and +// the nonzero "other" value. It must not leave a pointer reshape for T2L. +tt.func @masked_pointer_reshape(%base: !tt.ptr, %offsets: tensor<2x2x4xi64>, %mask: tensor<4x4xi1>) -> tensor<4x4xf32> { + %other = arith.constant dense<-7.0> : tensor<4x4xf32> + %bases = tt.splat %base : !tt.ptr -> tensor<2x2x4x!tt.ptr> + %ptrs = tt.addptr %bases, %offsets : tensor<2x2x4x!tt.ptr>, tensor<2x2x4xi64> + %flat = tt.reshape %ptrs : tensor<2x2x4x!tt.ptr> -> tensor<4x4x!tt.ptr> + %loaded = tt.load %flat, %mask, %other : tensor<4x4x!tt.ptr> + tt.return %loaded : tensor<4x4xf32> +} + +// CHECK-LABEL: tt.func @masked_pointer_reshape +// CHECK-SAME: %[[BASE:.*]]: !tt.ptr, %[[OFFSETS:.*]]: tensor<2x2x4xi64>, %[[MASK:.*]]: tensor<4x4xi1> +// CHECK-DAG: %[[OTHER:.*]] = arith.constant dense<-7.000000e+00> : tensor<4x4xf32> +// CHECK-DAG: %[[FLAT:.*]] = tt.reshape %[[OFFSETS]] : tensor<2x2x4xi64> -> tensor<4x4xi64> +// INDIRECT: ascend.indirect_load %[[BASE]] : , %[[FLAT]] : tensor<4x4xi64>, %[[MASK]] : tensor<4x4xi1>, %[[OTHER]] : tensor<4x4xf32> +// SCALAR: %[[LOADED:.*]] = scf.for +// SCALAR: tensor.extract %[[FLAT]] +// SCALAR: tt.load +// SCALAR: arith.select %[[MASK]], %[[LOADED]], %[[OTHER]] +// CHECK: tt.return + +// Reshape the complete displacement after addptr analysis has performed the +// signed extension. The original narrow integer computation remains intact. +tt.func @store_pointer_reshape(%base: !tt.ptr, %offsets: tensor<2x4xi32>, %values: tensor<8xi32>, %mask: tensor<8xi1>) { + %bases = tt.splat %base : !tt.ptr -> tensor<2x4x!tt.ptr> + %ptrs = tt.addptr %bases, %offsets : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + %flat = tt.reshape %ptrs : tensor<2x4x!tt.ptr> -> tensor<8x!tt.ptr> + tt.store %flat, %values, %mask : tensor<8x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func @store_pointer_reshape +// CHECK-SAME: %[[BASE:.*]]: !tt.ptr, %[[OFFSETS:.*]]: tensor<2x4xi32>, %[[VALUES:.*]]: tensor<8xi32>, %[[MASK:.*]]: tensor<8xi1> +// CHECK: %[[WIDE:.*]] = arith.extsi %[[OFFSETS]] : tensor<2x4xi32> to tensor<2x4xi64> +// CHECK: %[[FLAT:.*]] = tt.reshape %[[WIDE]] : tensor<2x4xi64> -> tensor<8xi64> +// INDIRECT: ascend.indirect_store %[[BASE]] : , %[[FLAT]] : tensor<8xi64>, %[[VALUES]] : tensor<8xi32>, %[[MASK]] : tensor<8xi1> +// SCALAR: scf.for +// SCALAR: tensor.extract %[[FLAT]] +// SCALAR: tt.store +// CHECK: tt.return + +// Fix one legal lane order for both analyzed memory uses and remaining pointer +// users. The layout hint and the unreshaped pointer's other uses survive. +tt.func @reshape_attributes_and_other_use(%base: !tt.ptr, %offsets: tensor<8xi32>) -> (tensor<2x4xf32>, tensor<2x4x!tt.ptr>, tensor<8x!tt.ptr>) { + %bases = tt.splat %base : !tt.ptr -> tensor<8x!tt.ptr> + %ptrs = tt.addptr %bases, %offsets : tensor<8x!tt.ptr>, tensor<8xi32> + %reshaped = tt.reshape %ptrs allow_reorder efficient_layout : tensor<8x!tt.ptr> -> tensor<2x4x!tt.ptr> + %loaded = tt.load %reshaped : tensor<2x4x!tt.ptr> + tt.return %loaded, %reshaped, %ptrs : tensor<2x4xf32>, tensor<2x4x!tt.ptr>, tensor<8x!tt.ptr> +} + +// CHECK-LABEL: tt.func @reshape_attributes_and_other_use +// CHECK-SAME: %[[BASE:.*]]: !tt.ptr, %[[OFFSETS:.*]]: tensor<8xi32> +// CHECK: %[[WIDE:.*]] = arith.extsi %[[OFFSETS]] : tensor<8xi32> to tensor<8xi64> +// CHECK: %[[ORIGINAL:.*]] = tt.addptr {{.*}} : tensor<8x!tt.ptr>, tensor<8xi32> +// CHECK: %[[OFFSET:.*]] = tt.reshape %[[WIDE]] efficient_layout : tensor<8xi64> -> tensor<2x4xi64> +// CHECK: %[[PTRS:.*]] = tt.reshape %[[ORIGINAL]] efficient_layout : tensor<8x!tt.ptr> -> tensor<2x4x!tt.ptr> +// INDIRECT: ascend.indirect_load %[[BASE]] : , %[[OFFSET]] : tensor<2x4xi64> +// SCALAR: scf.for +// CHECK: tt.return {{.*}}, %[[PTRS]], %[[ORIGINAL]] + +// A common base can be behind multiple addptr and reshape operations. Widen +// each addptr offset before accumulating it, then reshape the complete sum. +tt.func @nested_addptr_and_reshape(%base: !tt.ptr, %first: tensor<2x4xi32>, %second: tensor<2x4xi64>) -> tensor<8xf32> { + %bases = tt.splat %base : !tt.ptr -> tensor<2x4x!tt.ptr> + %p0 = tt.addptr %bases, %first : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + %p1 = tt.addptr %p0, %second : tensor<2x4x!tt.ptr>, tensor<2x4xi64> + %p2 = tt.reshape %p1 : tensor<2x4x!tt.ptr> -> tensor<4x2x!tt.ptr> + %p3 = tt.reshape %p2 : tensor<4x2x!tt.ptr> -> tensor<8x!tt.ptr> + %loaded = tt.load %p3 : tensor<8x!tt.ptr> + tt.return %loaded : tensor<8xf32> +} + +// CHECK-LABEL: tt.func @nested_addptr_and_reshape +// CHECK-SAME: %[[BASE:.*]]: !tt.ptr, %[[FIRST:.*]]: tensor<2x4xi32>, %[[SECOND:.*]]: tensor<2x4xi64> +// CHECK: %[[WIDE:.*]] = arith.extsi %[[FIRST]] : tensor<2x4xi32> to tensor<2x4xi64> +// CHECK: %[[SUM:.*]] = arith.addi %[[WIDE]], %[[SECOND]] : tensor<2x4xi64> +// CHECK: %[[FLAT:.*]] = tt.reshape %[[SUM]] : tensor<2x4xi64> -> tensor<8xi64> +// INDIRECT: ascend.indirect_load %[[BASE]] : , %[[FLAT]] : tensor<8xi64> +// SCALAR: scf.for +// SCALAR: tensor.extract %[[FLAT]] +// CHECK: tt.return + +// A broadcast changes which lanes share offsets. Keep the complete broadcast +// displacement, without copying the source's axis classification through reshape. +tt.func @broadcast_pointer_reshape(%base: !tt.ptr, %offsets: tensor<2x1xi64>) -> tensor<8xf32> { + %bases = tt.splat %base : !tt.ptr -> tensor<2x1x!tt.ptr> + %ptrs = tt.addptr %bases, %offsets : tensor<2x1x!tt.ptr>, tensor<2x1xi64> + %broadcast = tt.broadcast %ptrs : tensor<2x1x!tt.ptr> -> tensor<2x4x!tt.ptr> + %flat = tt.reshape %broadcast : tensor<2x4x!tt.ptr> -> tensor<8x!tt.ptr> + %loaded = tt.load %flat : tensor<8x!tt.ptr> + tt.return %loaded : tensor<8xf32> +} + +// CHECK-LABEL: tt.func @broadcast_pointer_reshape +// CHECK-SAME: %[[BASE:.*]]: !tt.ptr, %[[OFFSETS:.*]]: tensor<2x1xi64> +// CHECK: %[[BROADCAST:.*]] = tt.broadcast %[[OFFSETS]] : tensor<2x1xi64> -> tensor<2x4xi64> +// CHECK: %[[FLAT:.*]] = tt.reshape %[[BROADCAST]] : tensor<2x4xi64> -> tensor<8xi64> +// INDIRECT: ascend.indirect_load %[[BASE]] : , %[[FLAT]] : tensor<8xi64> +// SCALAR: scf.for +// CHECK: tt.return + +// setUnstructured must not leave scalarLike=true, which would bypass the +// conservative access path before conversion examines any individual axis. +tt.func @uniform_pointer_reshape(%base: !tt.ptr) -> tensor<8xf32> { + %bases = tt.splat %base : !tt.ptr -> tensor<2x4x!tt.ptr> + %flat = tt.reshape %bases : tensor<2x4x!tt.ptr> -> tensor<8x!tt.ptr> + %loaded = tt.load %flat : tensor<8x!tt.ptr> + tt.return %loaded : tensor<8xf32> +} + +// CHECK-LABEL: tt.func @uniform_pointer_reshape +// INDIRECT: ascend.indirect_load +// SCALAR: scf.for +// CHECK: tt.return + +// A lane-wise base is not a common scalar base. Extract actual reshaped +// pointers lane by lane, rather than choosing one allocation for indirect_load. +tt.func @keep_lane_wise_base(%bases: tensor<2x4x!tt.ptr>, %offsets: tensor<2x4xi32>) -> tensor<8xf32> { + %ptrs = tt.addptr %bases, %offsets : tensor<2x4x!tt.ptr>, tensor<2x4xi32> + %flat = tt.reshape %ptrs : tensor<2x4x!tt.ptr> -> tensor<8x!tt.ptr> + %loaded = tt.load %flat : tensor<8x!tt.ptr> + tt.return %loaded : tensor<8xf32> +} + +// CHECK-LABEL: tt.func @keep_lane_wise_base +// CHECK: %[[PTRS:.*]] = tt.addptr +// CHECK: %[[FLAT:.*]] = tt.reshape %[[PTRS]] : tensor<2x4x!tt.ptr> -> tensor<8x!tt.ptr> +// CHECK: scf.for +// CHECK: tensor.extract %[[FLAT]] +// CHECK: tt.load +// CHECK-NOT: ascend.indirect_load +// CHECK: tt.return diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/scalar_pointer_while_if_backedge.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/scalar_pointer_while_if_backedge.mlir new file mode 100644 index 0000000000..abeda72049 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/scalar_pointer_while_if_backedge.mlir @@ -0,0 +1,38 @@ +// RUN: triton-opt %s --triton-to-unstructure -verify-each | FileCheck %s + +// A pointer selected by an scf.if is an opaque complete address. When that +// value is yielded to an enclosing scf.while, every edge of the while boundary +// must use the complete-address representation instead of mixing i64 region +// arguments with a pointer backedge. +tt.func public @scalar_pointer_while_if_backedge( + %initial: !tt.ptr, %alternate: !tt.ptr) -> i32 { + %c0 = arith.constant 0 : i32 + %c1 = arith.constant 1 : i32 + %result:2 = scf.while (%i = %c0, %ptr = %initial) + : (i32, !tt.ptr) -> (i32, !tt.ptr) { + %continue = arith.cmpi slt, %i, %c1 : i32 + scf.condition(%continue) %i, %ptr : i32, !tt.ptr + } do { + ^bb0(%i: i32, %ptr: !tt.ptr): + %next_i = arith.addi %i, %c1 : i32 + %take_alternate = arith.cmpi eq, %i, %c0 : i32 + %next:2 = scf.if %take_alternate -> (!tt.ptr, i32) { + scf.yield %alternate, %next_i : !tt.ptr, i32 + } else { + scf.yield %ptr, %next_i : !tt.ptr, i32 + } + scf.yield %next#1, %next#0 : i32, !tt.ptr + } + %loaded = tt.load %result#1 : !tt.ptr + tt.return %loaded : i32 +} + +// CHECK-LABEL: tt.func public @scalar_pointer_while_if_backedge +// CHECK: scf.while +// CHECK-SAME: (i32, i64) -> (i32, i64) +// CHECK: scf.condition +// CHECK-SAME: i32, i64 +// CHECK: arith.select {{.*}} : !tt.ptr +// CHECK: tt.ptr_to_int +// CHECK: scf.yield +// CHECK-SAME: i32, i64 diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/splat.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/splat.mlir new file mode 100644 index 0000000000..5ff35c244d --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/splat.mlir @@ -0,0 +1,14 @@ +// RUN: triton-opt %s --triton-to-unstructure | FileCheck %s + +// CHECK-LABEL: tt.func @test_unstructure_splatandloadscenario +// CHECK: %[[EXT:.*]] = tensor.extract %{{.*}}[%{{.*}}] {DiscreteMemAccess} : tensor<128x!tt.ptr> +// CHECK: %[[VAL1:.*]] = tt.load %[[EXT]] : !tt.ptr +// CHECK: %[[VAL2:.*]] = tt.splat %[[VAL1]] : f32 -> tensor<128xf32> +tt.func @test_unstructure_splatandloadscenario(%base: !tt.ptr) -> tensor<128xf32> { + %offset = arith.constant 10 : i64 + %offset_tensor = tt.splat %offset : i64 -> tensor<128xi64> + %base_tensor = tt.splat %base : !tt.ptr -> tensor<128x!tt.ptr> + %ptr = tt.addptr %base_tensor, %offset_tensor : tensor<128x!tt.ptr>, tensor<128xi64> + %val = tt.load %ptr : tensor<128x!tt.ptr> + tt.return %val : tensor<128xf32> +} diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/tensor_pointer_select.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/tensor_pointer_select.mlir new file mode 100644 index 0000000000..6ae53301db --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/tensor_pointer_select.mlir @@ -0,0 +1,27 @@ +// RUN: triton-opt %s --triton-to-unstructure | FileCheck %s + +tt.func public @opaque_tensor_pointer_select( + %lhs: !tt.ptr, %rhs: !tt.ptr) -> tensor<4xf32> { + %zero = arith.constant dense<0> : tensor<4xi32> + %one = arith.constant dense<1> : tensor<4xi32> + %range = tt.make_range {end = 4 : i32, start = 0 : i32} : tensor<4xi32> + %lhs_tensor = tt.splat %lhs : !tt.ptr -> tensor<4x!tt.ptr> + %rhs_tensor = tt.splat %rhs : !tt.ptr -> tensor<4x!tt.ptr> + %lhs_ptrs = tt.addptr %lhs_tensor, %range : tensor<4x!tt.ptr>, tensor<4xi32> + %rhs_ptrs = tt.addptr %rhs_tensor, %range : tensor<4x!tt.ptr>, tensor<4xi32> + %bits = arith.andi %range, %one : tensor<4xi32> + %condition = arith.cmpi eq, %bits, %zero : tensor<4xi32> + %selected = arith.select %condition, %lhs_ptrs, %rhs_ptrs : tensor<4xi1>, tensor<4x!tt.ptr> + %advanced = tt.addptr %selected, %one : tensor<4x!tt.ptr>, tensor<4xi32> + %loaded = tt.load %advanced : tensor<4x!tt.ptr> + tt.return %loaded : tensor<4xf32> +} + +// CHECK-LABEL: tt.func public @opaque_tensor_pointer_select +// CHECK: %[[SELECTED:.*]] = arith.select {{.*}} : tensor<4xi1>, tensor<4x!tt.ptr> +// CHECK: scf.for +// CHECK: %[[LANE_PTR:.*]] = tensor.extract %[[SELECTED]]{{\[}}%{{.*}}] {DiscreteMemAccess} : tensor<4x!tt.ptr> +// CHECK: %[[ACCESS_PTR:.*]] = tt.addptr %[[LANE_PTR]], +// CHECK-SAME: : !tt.ptr, i64 +// CHECK: tt.load %[[ACCESS_PTR]] {DiscreteMemAccess} : !tt.ptr +// CHECK: tt.return diff --git a/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/unstructure_mix.mlir b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/unstructure_mix.mlir new file mode 100644 index 0000000000..4516711f08 --- /dev/null +++ b/third_party/ascend/unittest/Conversion/General/TritonToUnstructure/unstructure_mix.mlir @@ -0,0 +1,82 @@ +// RUN: triton-opt --triton-to-unstructure %s | FileCheck %s + +tt.func public @indirect_mix_kernel(%arg0: !tt.ptr {tt.divisibility = 16 : i32}, %arg1: !tt.ptr {tt.divisibility = 16 : i32}, %arg2: !tt.ptr {tt.divisibility = 16 : i32}, %arg3: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { + %cst = arith.constant dense<16> : tensor<1x8xi32> + %c8_i32 = arith.constant 8 : i32 + %0 = tt.get_program_id x : i32 + %1 = arith.muli %0, %c8_i32 : i32 + %2 = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> + %3 = tt.splat %1 : i32 -> tensor<8xi32> + %4 = arith.addi %3, %2 : tensor<8xi32> + %5 = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> + %6 = tt.splat %arg1 : !tt.ptr -> tensor<16x!tt.ptr> + %7 = tt.addptr %6, %5 : tensor<16x!tt.ptr>, tensor<16xi32> + %8 = tt.load %7 : tensor<16x!tt.ptr> + %9 = tt.expand_dims %2 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %10 = tt.splat %arg3 : i32 -> tensor<1x8xi32> + %11 = arith.muli %9, %10 : tensor<1x8xi32> + %12 = tt.expand_dims %8 {axis = 1 : i32} : tensor<16xi64> -> tensor<16x1xi64> + %13 = arith.extsi %11 : tensor<1x8xi32> to tensor<1x8xi64> + %14 = tt.broadcast %13 : tensor<1x8xi64> -> tensor<16x8xi64> + %15 = tt.broadcast %12 : tensor<16x1xi64> -> tensor<16x8xi64> + %16 = arith.addi %14, %15 : tensor<16x8xi64> + %17 = tt.splat %arg2 : !tt.ptr -> tensor<16x8x!tt.ptr> + %18 = tt.addptr %17, %16 : tensor<16x8x!tt.ptr>, tensor<16x8xi64> + %19 = tt.load %18 : tensor<16x8x!tt.ptr> + %20 = math.exp %19 : tensor<16x8xf32> + %21 = tt.expand_dims %4 {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> + %22 = arith.muli %21, %cst : tensor<1x8xi32> + %23 = tt.expand_dims %5 {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> + %24 = tt.broadcast %22 : tensor<1x8xi32> -> tensor<16x8xi32> + %25 = tt.broadcast %23 : tensor<16x1xi32> -> tensor<16x8xi32> + %26 = arith.addi %24, %25 : tensor<16x8xi32> + %27 = tt.splat %arg0 : !tt.ptr -> tensor<16x8x!tt.ptr> + %28 = tt.addptr %27, %26 : tensor<16x8x!tt.ptr>, tensor<16x8xi32> + tt.store %28, %20 : tensor<16x8x!tt.ptr> + tt.return +} + +// CHECK-LABEL: tt.func public @indirect_mix_kernel( +// CHECK-SAME: %[[VAL_0:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_1:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_2:.*]]: !tt.ptr {tt.divisibility = 16 : i32}, %[[VAL_3:.*]]: i32 {tt.divisibility = 16 : i32}) attributes {noinline = false} { +// CHECK: %[[VAL_4:.*]] = arith.constant 16 : index +// CHECK: %[[VAL_5:.*]] = arith.constant 1 : index +// CHECK: %[[VAL_6:.*]] = arith.constant 0 : index +// CHECK: %[[VAL_7:.*]] = arith.constant dense<16> : tensor<1x8xi32> +// CHECK: %[[VAL_9:.*]] = tt.get_program_id x : i32 +// CHECK: %[[VAL_10:.*]] = arith.muli %[[VAL_9]], %{{.*}} : i32 +// CHECK: %[[VAL_11:.*]] = tt.make_range {end = 8 : i32, start = 0 : i32} : tensor<8xi32> +// CHECK: %[[VAL_12:.*]] = tt.splat %[[VAL_10]] : i32 -> tensor<8xi32> +// CHECK: %[[VAL_13:.*]] = arith.addi %[[VAL_12]], %[[VAL_11]] : tensor<8xi32> +// CHECK: %[[VAL_14:.*]] = tt.make_range {end = 16 : i32, start = 0 : i32} : tensor<16xi32> +// CHECK: %[[VAL_15:.*]] = tt.splat %[[VAL_1]] : !tt.ptr -> tensor<16x!tt.ptr> +// CHECK: %[[VAL_16:.*]] = tt.addptr %[[VAL_15]], %[[VAL_14]] : tensor<16x!tt.ptr>, tensor<16xi32> +// CHECK: %[[VAL_17:.*]] = tt.load %[[VAL_16]] : tensor<16x!tt.ptr> +// CHECK: %[[VAL_18:.*]] = tt.expand_dims %[[VAL_11]] {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> +// CHECK: %[[VAL_19:.*]] = tt.splat %[[VAL_3]] : i32 -> tensor<1x8xi32> +// CHECK: %[[VAL_20:.*]] = arith.muli %[[VAL_18]], %[[VAL_19]] : tensor<1x8xi32> +// CHECK: %[[VAL_21:.*]] = tt.expand_dims %[[VAL_17]] {axis = 1 : i32} : tensor<16xi64> -> tensor<16x1xi64> +// CHECK: %[[VAL_22:.*]] = arith.extsi %[[VAL_20]] : tensor<1x8xi32> to tensor<1x8xi64> +// CHECK: %[[VAL_23:.*]] = tt.broadcast %[[VAL_22]] : tensor<1x8xi64> -> tensor<16x8xi64> +// CHECK: %[[VAL_24:.*]] = tt.broadcast %[[VAL_21]] : tensor<16x1xi64> -> tensor<16x8xi64> +// CHECK: %[[VAL_25:.*]] = arith.addi %[[VAL_23]], %[[VAL_24]] : tensor<16x8xi64> +// CHECK: %[[VAL_26:.*]] = tensor.empty() : tensor<16x8xf32> +// CHECK: %[[VAL_27:.*]] = scf.for %[[VAL_28:.*]] = %[[VAL_6]] to %[[VAL_4]] step %[[VAL_5]] iter_args(%[[VAL_29:.*]] = %[[VAL_26]]) -> (tensor<16x8xf32>) { +// CHECK: %[[VAL_30:.*]] = tensor.extract_slice %[[VAL_25]]{{\[}}%[[VAL_28]], 0] [1, 8] [1, 1] {DiscreteMemAccess} : tensor<16x8xi64> to tensor<1x8xi64> +// CHECK: %[[VAL_31:.*]] = tt.splat %[[VAL_2]] : !tt.ptr -> tensor<1x8x!tt.ptr> +// CHECK: %[[VAL_32:.*]] = tt.addptr %[[VAL_31]], %[[VAL_30]] : tensor<1x8x!tt.ptr>, tensor<1x8xi64> +// CHECK: %[[VAL_33:.*]] = tt.load %[[VAL_32]] {DiscreteMemAccess} : tensor<1x8x!tt.ptr> +// CHECK: %[[VAL_34:.*]] = tensor.insert_slice %[[VAL_33]] into %[[VAL_29]]{{\[}}%[[VAL_28]], 0] [1, 8] [1, 1] : tensor<1x8xf32> into tensor<16x8xf32> +// CHECK: scf.yield {DiscreteMemAccess} %[[VAL_34]] : tensor<16x8xf32> +// CHECK: } {ExtractedLoadOrStore} +// CHECK: %[[VAL_35:.*]] = math.exp %[[VAL_27]] : tensor<16x8xf32> +// CHECK: %[[VAL_36:.*]] = tt.expand_dims %[[VAL_13]] {axis = 0 : i32} : tensor<8xi32> -> tensor<1x8xi32> +// CHECK: %[[VAL_37:.*]] = arith.muli %[[VAL_36]], %[[VAL_7]] : tensor<1x8xi32> +// CHECK: %[[VAL_38:.*]] = tt.expand_dims %[[VAL_14]] {axis = 1 : i32} : tensor<16xi32> -> tensor<16x1xi32> +// CHECK: %[[VAL_39:.*]] = tt.broadcast %[[VAL_37]] : tensor<1x8xi32> -> tensor<16x8xi32> +// CHECK: %[[VAL_40:.*]] = tt.broadcast %[[VAL_38]] : tensor<16x1xi32> -> tensor<16x8xi32> +// CHECK: %[[VAL_41:.*]] = arith.addi %[[VAL_39]], %[[VAL_40]] : tensor<16x8xi32> +// CHECK: %[[VAL_42:.*]] = tt.splat %[[VAL_0]] : !tt.ptr -> tensor<16x8x!tt.ptr> +// CHECK: %[[VAL_43:.*]] = tt.addptr %[[VAL_42]], %[[VAL_41]] : tensor<16x8x!tt.ptr>, tensor<16x8xi32> +// CHECK: tt.store %[[VAL_43]], %[[VAL_35]] : tensor<16x8x!tt.ptr> +// CHECK: tt.return +// CHECK: } diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/dynamic-cv-pipeline-ci.sh b/third_party/ascend/unittest/DynamicCVPipeline_ut/dynamic-cv-pipeline-ci.sh new file mode 100755 index 0000000000..4a96e10287 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/dynamic-cv-pipeline-ci.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +set -e + +# define color +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT_DIR="${GITHUB_WORKSPACE}/result" +TEST_FILE_PATH="${SCRIPT_DIR}" + +# test files are named with pattern 'test_*.py' +TEST_FILES=$(find "${TEST_FILE_PATH}" -maxdepth 1 -type f -name "test_*.py") + +function install_triton_ascend_build_output() { + WHL_DIR=$1 + readarray -d '' WHL_FILES < <(find "$WHL_DIR" -maxdepth 1 -type f -name "*.whl" -print0) + + WHL_COUNT=${#WHL_FILES[@]} + + # ensure only one .whl file built + if [ "$WHL_COUNT" -eq 0 ]; then + echo -e "${RED}ERROR: no .whl file found.${NC}" + return 1 + + elif [ "$WHL_COUNT" -gt 1 ]; then + echo -e "${RED}ERROR: expected 1 .whl file, find $WHL_COUNT files.${NC}" + for file in "${WHL_FILES[@]}"; do + echo -e "${RED} - $(basename "$file")${NC}" + done + return 1 + + else + TARGET_WHL="${WHL_FILES[0]}" + pip install --force-reinstall --no-deps "$TARGET_WHL" + + if [ $? -eq 0 ]; then + echo -e "${GREEN}install $TARGET_WHL success!${NC}" + return 0 + else + echo -e "${RED}install $TARGET_WHL failed!${NC}" + return 1 + fi + fi +} + +function run_test_cases() { + echo "Start running dynamic-cv-pipeline test cases..." + echo "-----------------------------------------------" + + SUCCESS_COUNT=0 + FAILURE_COUNT=0 + + for file in $TEST_FILES; do + filename=$(basename "$file") + pytest -sv --confcutdir="${TEST_FILE_PATH}" "$file" + + if [ $? -eq 0 ]; then + echo -e "${GREEN}test case $filename success!${NC}" + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + else + echo -e "${RED}test case $filename failed!${NC}" + FAILURE_COUNT=$((FAILURE_COUNT + 1)) + fi + echo "-----------------------------------------------" + done + + echo -e "${RED}Reports:${NC}" + echo -e "${RED} Success cases count: $SUCCESS_COUNT${NC}" + echo -e "${RED} Failed cases count: $FAILURE_COUNT${NC}" + + if [ $FAILURE_COUNT -gt 0 ]; then + return 1 + fi +} + +function compare_mlir() { + arg_num="$#" + if [ $arg_num -lt 2 ]; then + echo -e "${RED}ERROR: invalid argument number: $arg_num < 2${NC}" + return 1 + fi + + BASE_DIR="$1" + PR_DIR="$2" + DIFF_OUTPUT_DIR=${3:-"${OUTPUT_DIR}/diff"} + + if ! [ -d "$BASE_DIR" ]; then + echo -e "${RED}ERROR: base directory does not exist -> $BASE_DIR${NC}" + return 1 + fi + + if ! [ -d "$PR_DIR" ]; then + echo -e "${RED}ERROR: pr directory does not exist -> $PR_DIR${NC}" + return 1 + fi + + BASE_DIR="${BASE_DIR%/}" + PR_DIR="${PR_DIR%/}" + + python3 "$SCRIPT_DIR/mlir_diff.py" "$BASE_DIR" "$PR_DIR" "$DIFF_OUTPUT_DIR" + + echo -e "\n${GREEN}==================================================${NC}" + echo -e "${GREEN} Compare and diff finished${NC}" + echo -e "${GREEN}==================================================${NC}" +} + +function main() { + rm -rf "${OUTPUT_DIR}" + + echo " Creating output dir ${OUTPUT_DIR}" + mkdir -p "${OUTPUT_DIR}" + + # End if there are no test cases + if [ -z "$TEST_FILES" ]; then + echo -e "${YELLOW}No test cases found in $TEST_FILE_PATH${NC}" + return 0 + fi + + # mlir output dir created by test cases + TESTCASE_MLIR_OUTPUT=${TEST_FILE_PATH}/mlir_output + + PR_MLIR_DIR=${OUTPUT_DIR}/mlir/pr + BASE_MLIR_DIR=${OUTPUT_DIR}/mlir/base + rm -rf "${PR_MLIR_DIR}" + rm -rf "${BASE_MLIR_DIR}" + + PR_PKG=${GITHUB_WORKSPACE}/pr/wheelhouse + install_triton_ascend_build_output "$PR_PKG" + + if ! run_test_cases; then + echo -e "${RED} Run test cases on pr package failed!${NC}" + return 1 + fi + + mkdir -p "${PR_MLIR_DIR}" + cp -r "${TESTCASE_MLIR_OUTPUT}/." "${PR_MLIR_DIR}/" + + BASE_PKG=${GITHUB_WORKSPACE}/base/wheelhouse + install_triton_ascend_build_output "$BASE_PKG" + + # re-run test cases after installing base package + # Will not return on failure + if ! run_test_cases; then + echo -e "${YELLOW} Run test cases on base package failed!${NC}" + fi + + mkdir -p "${BASE_MLIR_DIR}" + cp -r "${TESTCASE_MLIR_OUTPUT}/." "${BASE_MLIR_DIR}/" + + cd "${OUTPUT_DIR}" + compare_mlir "${BASE_MLIR_DIR}" "${PR_MLIR_DIR}" +} + +main "$@" diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/mlir_diff.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/mlir_diff.py new file mode 100755 index 0000000000..6b40e359ce --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/mlir_diff.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Compare two folders and output differences to a file.""" + +import argparse +import difflib +import os +import sys + + +def get_relative_paths(folder): + result = set() + for root, _, files in os.walk(folder): + for f in files: + full = os.path.join(root, f) + rel = os.path.relpath(full, folder) + result.add(rel) + return result + + +def is_binary(filepath): + try: + with open(filepath, 'rb') as f: + chunk = f.read(8192) + return b'\x00' in chunk + except IOError: + return True + + +def read_lines(filepath): + try: + with open(filepath, 'r', encoding='utf-8', errors='replace') as f: + return f.readlines() + except IOError: + return [] + + +def compare_folders(folder_base, folder_pr, output_dir): + base_paths = get_relative_paths(folder_base) + pr_paths = get_relative_paths(folder_pr) + + only_in_base = sorted(base_paths - pr_paths) + only_in_pr = sorted(pr_paths - base_paths) + common = sorted(base_paths & pr_paths) + + diff_files = [] + skipped_files = [] + unchanged_mlir_files = [] + same_count = 0 + + # Ensure output directory exists + if not os.path.exists(output_dir): + os.makedirs(output_dir, exist_ok=True) + + for rel in common: + # compare .mlir files only + if not rel.endswith('.mlir'): + print(f" Not a mlir file: {rel}, skipped") + continue + + full_name_base = os.path.join(folder_base, rel) + full_name_pr = os.path.join(folder_pr, rel) + if is_binary(full_name_base) or is_binary(full_name_pr): + # skip binary file + print(f" Encountered binary file, skipped: {rel}") + skipped_files.append(rel) + continue + else: + try: + # Text files can be read into lines for difflib + lines_base = read_lines(full_name_base) + lines_pr = read_lines(full_name_pr) + + if lines_base != lines_pr: + diff_files.append(rel) + diff = difflib.unified_diff(lines_base, lines_pr, fromfile=f'base/{rel}', tofile=f'pr/{rel}', + lineterm='') + diff_text = ''.join(diff) + if diff_text: + _write_diff_file(rel, diff_text + "\n", output_dir) + else: + same_count += 1 + unchanged_mlir_files.append(rel) + except IOError: + diff_files.append(rel) + _write_diff_file(rel, f" Cannot read: {rel}\n", output_dir) + + # names of unchanged .mlir files will be written into unchanged_cases.txt + if unchanged_mlir_files: + changed_txt_path = os.path.join(output_dir, "unchanged_cases.txt") + print(f"\nUnchanged mlir files written to : '{changed_txt_path}/'") + with open(changed_txt_path, 'w', encoding='utf-8') as f: + f.write('\n'.join(unchanged_mlir_files) + '\n') + + # Print summary to console + print(f"\n{'='*40}") + print(f"Comparison Summary") + print(f"{'='*40}") + print(f" Only in base count: {len(only_in_base)}") + print(f" Only in pr count: {len(only_in_pr)}") + print(f" Identical count: {same_count}") + print(f" Different count: {len(diff_files)}") + print(f" Skipped (binary) count: {len(skipped_files)}") + + if diff_files: + print(f"\nDifferences written to directory: '{output_dir}/'") + + +def _write_diff_file(rel_path, content, output_dir): + # Helper to write diff content to a specific file based on its base name. + base_name = os.path.basename(rel_path) + diff_filename = os.path.splitext(base_name)[0] + '.diff' + diff_path = os.path.join(output_dir, diff_filename) + + try: + with open(diff_path, 'w', encoding='utf-8') as f: + f.write(content) + except Exception as e: + print(f"Writing diff file {diff_filename} failed, exception is : {e}") + + +def main(): + parser = argparse.ArgumentParser( + description='Compare base and base+pr mlir folders and output differences to a file.') + parser.add_argument('folder_base', help='Path to base folder') + parser.add_argument('folder_pr', help='Path to pr folder') + parser.add_argument('output', + help='Path to output directory where diff files and unchanged_cases.txt will be stored') + args = parser.parse_args() + + if not os.path.isdir(args.folder_base): + print(f"Error: '{args.folder_base}' is not a directory", file=sys.stderr) + sys.exit(1) + if not os.path.isdir(args.folder_pr): + print(f"Error: '{args.folder_pr}' is not a directory", file=sys.stderr) + sys.exit(1) + + compare_folders(args.folder_base, args.folder_pr, args.output) + + +if __name__ == '__main__': + main() diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf01_mlir_no_loop_independent_cv.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf01_mlir_no_loop_independent_cv.py new file mode 100644 index 0000000000..3d2559bf06 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf01_mlir_no_loop_independent_cv.py @@ -0,0 +1,260 @@ +""" +Test Case: ACF01 - No for-loop, independent Cube + Vector in main body, no data dependency + +[MLIR Validation] Refactored version + +Description: No for-loop, independent Cube (outer product) + Vector (cube_result + c). + No data dependency. Not supporting SSBuf optimization. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - ACF01-TC01: float16, M=128, N=64, K=32 + - ACF01-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# ACF01-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of independent CV operations without for-loop under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def acf01_tc01_no_loop_cv( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + a = tl.load(a_ptr + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + + # Build (K, N) result directly via element-wise broadcasting: + # b has only K elements, so for column j >= K, the outer product is 0 + # Load b with N-sized index, mask to K so b[j]=0 for j>=K + b_n = tl.load(b_ptr + offs_n * stride_bk, mask=offs_n < K, other=0.0) # (N,) padded with 0 + # outer product: a[:, None] * b_n[None, :] -> (K, N) + cube_result = a[:, None] * b_n[None, :] # (K, N) + vec_result = cube_result + c[None, :] # (K, N) + (1, N) -> (K, N) + tl.store(out_ptr + offs_k[:, None] * stride_out + offs_n[None, :], vec_result, + mask=(offs_k[:, None] < K) & (offs_n[None, :] < N)) + + +# ---------------------------------------------------------------------------- +# ACF01-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of independent CV operations without for-loop under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf01_tc02_no_loop_cv( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + a = tl.load(a_ptr + offs_k * stride_ak, mask=offs_k < K, other=0.0) + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + + b_n = tl.load(b_ptr + offs_n * stride_bk, mask=offs_n < K, other=0.0) + cube_result = a[:, None] * b_n[None, :] # (K, N) + vec_result = cube_result + c[None, :] # (K, N) + (1, N) -> (K, N) + tl.store(out_ptr + offs_k[:, None] * stride_out + offs_n[None, :], vec_result, + mask=(offs_k[:, None] < K) & (offs_n[None, :] < N)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def test_acf01_tc01(): + """ACF01-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile acf01_tc01_no_loop_cv kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = { + "a_ptr": "*fp16", + "b_ptr": "*fp16", + "c_ptr": "*fp16", + "out_ptr": "*fp16", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_c": "i32", + "stride_out": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf01_tc01_no_loop_cv, signature, constants) + _write_mlir_to_file(mlir, "acf01_tc01_no_loop_cv.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf01_tc01_no_loop_cv(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_acf01_tc02(): + """ACF01-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile acf01_tc02_no_loop_cv kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = { + "a_ptr": "*fp32", + "b_ptr": "*fp32", + "c_ptr": "*fp32", + "out_ptr": "*fp32", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_c": "i32", + "stride_out": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf01_tc02_no_loop_cv, signature, constants) + _write_mlir_to_file(mlir, "acf01_tc02_no_loop_cv.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf01_tc02_no_loop_cv(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ + +if __name__ == "__main__": + test_acf01_tc01() + test_acf01_tc02() + print("All ACF01 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf02_mlir_loop_pure_cube_cv_outside.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf02_mlir_loop_pure_cube_cv_outside.py new file mode 100644 index 0000000000..f1f973fa4b --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf02_mlir_loop_pure_cube_cv_outside.py @@ -0,0 +1,283 @@ +""" +Test Case: ACF02 - For loop with pure Cube inside, CV outside the loop (Vector + independent Cube) + +[MLIR Validation] Refactored version + +Description: Outer vector operation (c * d), then for loop with pure Cube operation (inner product). + Not supporting SSBuf optimization (for loop pure C, no internal V). + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - ACF02-TC01: float16, M=128, N=64, K=32 + - ACF02-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# ACF02-TC01: float16, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf02_tc01_outer_cv_pure_cube( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + out2_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out, + stride_out2_0, + stride_out2_1, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + outer_vec = c * d # (N,) + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, outer_vec, mask=offs_n < N) + + # Hoist loop-invariant Vector and memory operations out of the for loop + a_ptrs_base = a_ptr + offs_k * stride_ak + b_ptrs_base = b_ptr + offs_k * stride_bn + out2_ptrs = out2_ptr + offs_k[:, None] * stride_out2_0 + offs_k[None, :] * stride_out2_1 + mask_a = offs_k < K + mask_b = offs_k < K + mask_out2 = (offs_k[:, None] < K) & (offs_k[None, :] < K) + + # Loads outside the loop + a = tl.load(a_ptrs_base, mask=mask_a, other=0.0) # (K,) + b = tl.load(b_ptrs_base, mask=mask_b, other=0.0) # (K,) + + # Initialize cube_result before the loop to avoid "not defined" error. + # Use fp32 since tl.dot accumulates in fp32 (even for fp16 inputs). + cube_result = tl.zeros((BLOCK_SIZE_K, BLOCK_SIZE_K), dtype=tl.float32) # (K,K) + + # Pure Cube inside the loop + for k in range(K): + cube_result = tl.dot(a[:, None], b[None, :]) # (K,K) + + # Store outside the loop + tl.store(out2_ptrs, cube_result, mask=mask_out2) + + +# ---------------------------------------------------------------------------- +# ACF02-TC02: float32, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf02_tc02_outer_cv_pure_cube( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + out2_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out, + stride_out2_0, + stride_out2_1, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + outer_vec = c * d + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, outer_vec, mask=offs_n < N) + + # Hoist loop-invariant Vector and memory operations out of the for loop + a_ptrs_base = a_ptr + offs_k * stride_ak + b_ptrs_base = b_ptr + offs_k * stride_bn + out2_ptrs = out2_ptr + offs_k[:, None] * stride_out2_0 + offs_k[None, :] * stride_out2_1 + mask_a = offs_k < K + mask_b = offs_k < K + mask_out2 = (offs_k[:, None] < K) & (offs_k[None, :] < K) + + # Loads outside the loop + a = tl.load(a_ptrs_base, mask=mask_a, other=0.0) + b = tl.load(b_ptrs_base, mask=mask_b, other=0.0) + + # Initialize cube_result before the loop to avoid "not defined" error. + # Use fp32 since tl.dot accumulates in fp32 (even for fp16 inputs). + cube_result = tl.zeros((BLOCK_SIZE_K, BLOCK_SIZE_K), dtype=tl.float32) + + # Pure Cube inside the loop + for k in range(K): + cube_result = tl.dot(a[:, None], b[None, :]) + + # Store outside the loop + tl.store(out2_ptrs, cube_result, mask=mask_out2) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def test_acf02_tc01(): + """ACF02-TC01: Verify float16 kernel compilation generates correct MLIR code.""" + signature = { + "a_ptr": "*fp16", + "b_ptr": "*fp16", + "c_ptr": "*fp16", + "d_ptr": "*fp16", + "out_ptr": "*fp16", + "out2_ptr": "*fp16", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out": "i32", + "stride_out2_0": "i32", + "stride_out2_1": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf02_tc01_outer_cv_pure_cube, signature, constants) + _write_mlir_to_file(mlir, "acf02_tc01_outer_cv_pure_cube.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf02_tc01_outer_cv_pure_cube(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_acf02_tc02(): + """ACF02-TC02: Verify float32 kernel compilation generates correct MLIR code.""" + signature = { + "a_ptr": "*fp32", + "b_ptr": "*fp32", + "c_ptr": "*fp32", + "d_ptr": "*fp32", + "out_ptr": "*fp32", + "out2_ptr": "*fp32", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out": "i32", + "stride_out2_0": "i32", + "stride_out2_1": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf02_tc02_outer_cv_pure_cube, signature, constants) + _write_mlir_to_file(mlir, "acf02_tc02_outer_cv_pure_cube.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf02_tc02_outer_cv_pure_cube(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +if __name__ == "__main__": + test_acf02_tc01() + test_acf02_tc02() + print("All ACF02 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf03_mlir_loop_pure_vector_cv_outside.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf03_mlir_loop_pure_vector_cv_outside.py new file mode 100644 index 0000000000..1d82aa99f0 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf03_mlir_loop_pure_vector_cv_outside.py @@ -0,0 +1,253 @@ +""" +Test Case: ACF03 - For loop with pure Vector inside, CV outside the loop (Cube + independent Vector) + +[MLIR Validation] Refactored version + +Description: Outer Cube operation (outer product), then for loop with pure Vector operation (c+d+e). + Not supporting SSBuf optimization (for loop pure V, no internal C). + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - ACF03-TC01: float16, M=128, N=64, K=32 + - ACF03-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# ACF03-TC01: float16, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf03_tc01_outer_cube_pure_vec( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + out_ptr, + out2_ptr, + M, + N, + K, + stride_ak, + stride_bk, + stride_c, + stride_d, + stride_e, + stride_out0, + stride_out1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + a = tl.load(a_ptr + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + offs_k * stride_bk, mask=offs_k < K, other=0.0) # (K,) + outer_cube = tl.dot(a[:, None], b[None, :]) # (K,K) + out_ptrs = out_ptr + offs_k[:, None] * stride_out0 + offs_k[None, :] * stride_out1 + tl.store(out_ptrs, outer_cube, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + vec_result = c + d + e # (N,) + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, vec_result, mask=offs_n < N) + + +# ---------------------------------------------------------------------------- +# ACF03-TC02: float32, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf03_tc02_outer_cube_pure_vec( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + out_ptr, + out2_ptr, + M, + N, + K, + stride_ak, + stride_bk, + stride_c, + stride_d, + stride_e, + stride_out0, + stride_out1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + a = tl.load(a_ptr + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + offs_k * stride_bk, mask=offs_k < K, other=0.0) + outer_cube = tl.dot(a[:, None], b[None, :]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out0 + offs_k[None, :] * stride_out1 + tl.store(out_ptrs, outer_cube, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) + vec_result = c + d + e + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, vec_result, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def test_acf03_tc01(): + """ACF03-TC01: Verify float16 kernel compilation generates correct MLIR code.""" + signature = { + "a_ptr": "*fp16", + "b_ptr": "*fp16", + "c_ptr": "*fp16", + "d_ptr": "*fp16", + "e_ptr": "*fp16", + "out_ptr": "*fp16", + "out2_ptr": "*fp16", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_e": "i32", + "stride_out0": "i32", + "stride_out1": "i32", + "stride_out2": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf03_tc01_outer_cube_pure_vec, signature, constants) + _write_mlir_to_file(mlir, "acf03_tc01_outer_cube_pure_vec.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf03_tc01_outer_cube_pure_vec(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_acf03_tc02(): + """ACF03-TC02: Verify float32 kernel compilation generates correct MLIR code.""" + signature = { + "a_ptr": "*fp32", + "b_ptr": "*fp32", + "c_ptr": "*fp32", + "d_ptr": "*fp32", + "e_ptr": "*fp32", + "out_ptr": "*fp32", + "out2_ptr": "*fp32", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_e": "i32", + "stride_out0": "i32", + "stride_out1": "i32", + "stride_out2": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf03_tc02_outer_cube_pure_vec, signature, constants) + _write_mlir_to_file(mlir, "acf03_tc02_outer_cube_pure_vec.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf03_tc02_outer_cube_pure_vec(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +if __name__ == "__main__": + test_acf03_tc01() + test_acf03_tc02() + print("All ACF03 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf04_mlir_multi_cube_single_vector.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf04_mlir_multi_cube_single_vector.py new file mode 100644 index 0000000000..e1d6076a21 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf04_mlir_multi_cube_single_vector.py @@ -0,0 +1,377 @@ +""" +Test Case: ACF04 - >14 independent Cube + single independent Vector, no data dependency + +[MLIR Validation] Refactored version + +Description: >14 independent Cube (15 independent outer products) + single independent Vector (c+1.0), + no data dependency. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - ACF04-TC01: float16, M=128, N=64, K=32 + - ACF04-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# ACF04-TC01: float16, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf04_tc01_many_cubes( + a1_ptr, + a2_ptr, + a3_ptr, + a4_ptr, + a5_ptr, + a6_ptr, + a7_ptr, + a8_ptr, + a9_ptr, + a10_ptr, + a11_ptr, + a12_ptr, + a13_ptr, + a14_ptr, + a15_ptr, + b1_ptr, + b2_ptr, + b3_ptr, + b4_ptr, + b5_ptr, + b6_ptr, + b7_ptr, + b8_ptr, + b9_ptr, + b10_ptr, + b11_ptr, + b12_ptr, + b13_ptr, + b14_ptr, + b15_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bn, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a1 = tl.load(a1_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b1 = tl.load(b1_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) # (K,) + cube1 = tl.dot(a1[:, None], b1[None, :]) # (K,K) + + a2 = tl.load(a2_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b2 = tl.load(b2_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube2 = tl.dot(a2[:, None], b2[None, :]) + + a3 = tl.load(a3_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b3 = tl.load(b3_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube3 = tl.dot(a3[:, None], b3[None, :]) + + a4 = tl.load(a4_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b4 = tl.load(b4_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube4 = tl.dot(a4[:, None], b4[None, :]) + + a5 = tl.load(a5_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b5 = tl.load(b5_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube5 = tl.dot(a5[:, None], b5[None, :]) + + a6 = tl.load(a6_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b6 = tl.load(b6_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube6 = tl.dot(a6[:, None], b6[None, :]) + + a7 = tl.load(a7_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b7 = tl.load(b7_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube7 = tl.dot(a7[:, None], b7[None, :]) + + a8 = tl.load(a8_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b8 = tl.load(b8_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube8 = tl.dot(a8[:, None], b8[None, :]) + + a9 = tl.load(a9_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b9 = tl.load(b9_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube9 = tl.dot(a9[:, None], b9[None, :]) + + a10 = tl.load(a10_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b10 = tl.load(b10_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube10 = tl.dot(a10[:, None], b10[None, :]) + + a11 = tl.load(a11_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b11 = tl.load(b11_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube11 = tl.dot(a11[:, None], b11[None, :]) + + a12 = tl.load(a12_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b12 = tl.load(b12_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube12 = tl.dot(a12[:, None], b12[None, :]) + + a13 = tl.load(a13_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b13 = tl.load(b13_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube13 = tl.dot(a13[:, None], b13[None, :]) + + a14 = tl.load(a14_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b14 = tl.load(b14_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube14 = tl.dot(a14[:, None], b14[None, :]) + + a15 = tl.load(a15_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b15 = tl.load(b15_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube15 = tl.dot(a15[:, None], b15[None, :]) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + vec_result = c + 1.0 # (N,) + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, vec_result, mask=offs_n < N) + + +# ---------------------------------------------------------------------------- +# ACF04-TC02: float32, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf04_tc02_many_cubes( + a1_ptr, + a2_ptr, + a3_ptr, + a4_ptr, + a5_ptr, + a6_ptr, + a7_ptr, + a8_ptr, + a9_ptr, + a10_ptr, + a11_ptr, + a12_ptr, + a13_ptr, + a14_ptr, + a15_ptr, + b1_ptr, + b2_ptr, + b3_ptr, + b4_ptr, + b5_ptr, + b6_ptr, + b7_ptr, + b8_ptr, + b9_ptr, + b10_ptr, + b11_ptr, + b12_ptr, + b13_ptr, + b14_ptr, + b15_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bn, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for k in range(K): + a1 = tl.load(a1_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b1 = tl.load(b1_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube1 = tl.dot(a1[:, None], b1[None, :]) + + a2 = tl.load(a2_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b2 = tl.load(b2_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube2 = tl.dot(a2[:, None], b2[None, :]) + + a3 = tl.load(a3_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b3 = tl.load(b3_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube3 = tl.dot(a3[:, None], b3[None, :]) + + a4 = tl.load(a4_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b4 = tl.load(b4_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube4 = tl.dot(a4[:, None], b4[None, :]) + + a5 = tl.load(a5_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b5 = tl.load(b5_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube5 = tl.dot(a5[:, None], b5[None, :]) + + a6 = tl.load(a6_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b6 = tl.load(b6_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube6 = tl.dot(a6[:, None], b6[None, :]) + + a7 = tl.load(a7_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b7 = tl.load(b7_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube7 = tl.dot(a7[:, None], b7[None, :]) + + a8 = tl.load(a8_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b8 = tl.load(b8_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube8 = tl.dot(a8[:, None], b8[None, :]) + + a9 = tl.load(a9_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b9 = tl.load(b9_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube9 = tl.dot(a9[:, None], b9[None, :]) + + a10 = tl.load(a10_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b10 = tl.load(b10_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube10 = tl.dot(a10[:, None], b10[None, :]) + + a11 = tl.load(a11_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b11 = tl.load(b11_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube11 = tl.dot(a11[:, None], b11[None, :]) + + a12 = tl.load(a12_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b12 = tl.load(b12_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube12 = tl.dot(a12[:, None], b12[None, :]) + + a13 = tl.load(a13_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b13 = tl.load(b13_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube13 = tl.dot(a13[:, None], b13[None, :]) + + a14 = tl.load(a14_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b14 = tl.load(b14_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube14 = tl.dot(a14[:, None], b14[None, :]) + + a15 = tl.load(a15_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b15 = tl.load(b15_ptr + k * stride_bn + offs_k, mask=offs_k < K, other=0.0) + cube15 = tl.dot(a15[:, None], b15[None, :]) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + vec_result = c + 1.0 + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, vec_result, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_acf04_signature(dtype): + """Build the argument signature for the ACF04 kernel.""" + ptr_type = f"*{dtype}" + sig = {} + for i in range(1, 16): + sig[f"a{i}_ptr"] = ptr_type + sig[f"b{i}_ptr"] = ptr_type + sig["c_ptr"] = ptr_type + sig["out_ptr"] = ptr_type + for s in ["M", "N", "K", "stride_am", "stride_ak", "stride_bn", "stride_c", "stride_out"]: + sig[s] = "i32" + return sig + + +def test_acf04_tc01(): + """ACF04-TC01: Verify float16 kernel compilation generates correct MLIR code.""" + signature = _build_acf04_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf04_tc01_many_cubes, signature, constants) + _write_mlir_to_file(mlir, "acf04_tc01_many_cubes.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf04_tc01_many_cubes(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_acf04_tc02(): + """ACF04-TC02: Verify float32 kernel compilation generates correct MLIR code.""" + signature = _build_acf04_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf04_tc02_many_cubes, signature, constants) + _write_mlir_to_file(mlir, "acf04_tc02_many_cubes.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf04_tc02_many_cubes(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +if __name__ == "__main__": + test_acf04_tc01() + test_acf04_tc02() + print("All ACF04 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf05_mlir_if_cond_cube_to_vector.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf05_mlir_if_cond_cube_to_vector.py new file mode 100644 index 0000000000..44c846e34f --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf05_mlir_if_cond_cube_to_vector.py @@ -0,0 +1,231 @@ +""" +Test Case: ACF05 - loop with if control flow, condition from Cube to execute Vector + +[MLIR Validation] Refactored version + +Description: For loop with if control flow: condition based on cube_result to decide + whether to execute Vector operation. No data dependency. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - ACF05-TC01: float16, M=128, N=64, K=32 + - ACF05-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# ACF05-TC01: float16, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf05_tc01_cube_cond_vec( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube_result = tl.dot(a[:, None], b[None, :]) # (K, K) + + # Ascend backend does not support tensor[constexpr] indexing (e.g. cube_result[0]). + # Use tl.sum to reduce the (K,K) matrix to a scalar for the if-condition. + cond = tl.sum(cube_result) > 0.0 + if cond: + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + vec_result = c + 1.0 # (N,) + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, vec_result, mask=offs_n < N) + + +# ---------------------------------------------------------------------------- +# ACF05-TC02: float32, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf05_tc02_cube_cond_vec( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + cube_result = tl.dot(a[:, None], b[None, :]) + + cond = tl.sum(cube_result) > 0.0 + if cond: + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + vec_result = c + 1.0 + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, vec_result, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def test_acf05_tc01(): + """ACF05-TC01: Verify float16 kernel compilation generates correct MLIR code.""" + signature = { + "a_ptr": "*fp16", + "b_ptr": "*fp16", + "c_ptr": "*fp16", + "out_ptr": "*fp16", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_out": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf05_tc01_cube_cond_vec, signature, constants) + _write_mlir_to_file(mlir, "acf05_tc01_cube_cond_vec.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf05_tc01_cube_cond_vec(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_acf05_tc02(): + """ACF05-TC02: Verify float32 kernel compilation generates correct MLIR code.""" + signature = { + "a_ptr": "*fp32", + "b_ptr": "*fp32", + "c_ptr": "*fp32", + "out_ptr": "*fp32", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_out": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf05_tc02_cube_cond_vec, signature, constants) + _write_mlir_to_file(mlir, "acf05_tc02_cube_cond_vec.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf05_tc02_cube_cond_vec(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +if __name__ == "__main__": + test_acf05_tc01() + test_acf05_tc02() + print("All ACF05 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf06_mlir_if_else_cond_vector_to_cv.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf06_mlir_if_else_cond_vector_to_cv.py new file mode 100644 index 0000000000..ec44366cbf --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf06_mlir_if_else_cond_vector_to_cv.py @@ -0,0 +1,257 @@ +""" +Test Case: ACF06 - loop with if/else control flow, condition from Vector to decide Cube or Vector + +[MLIR Validation] Refactored version + +Description: For loop with if/else control flow: tl.sum(c) as condition to decide + whether to execute Cube (if >0) or Vector (else). No data dependency. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - ACF06-TC01: float16, M=128, N=64, K=32 + - ACF06-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# ACF06-TC01: float16, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf06_tc01_vec_cond_cube( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out1_0, + stride_out1_1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + vec_cond = tl.sum(c) # scalar + + if vec_cond > 0.0: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube_result = tl.dot(a[:, None], b[None, :]) # (K, K) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1_0 + offs_k[None, :] * stride_out1_1 + tl.store(out1_ptrs, cube_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + else: + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + vec_result = d * 2.0 # (N,) + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, vec_result, mask=offs_n < N) + + +# ---------------------------------------------------------------------------- +# ACF06-TC02: float32, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf06_tc02_vec_cond_cube( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out1_0, + stride_out1_1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + vec_cond = tl.sum(c) + + if vec_cond > 0.0: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + cube_result = tl.dot(a[:, None], b[None, :]) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1_0 + offs_k[None, :] * stride_out1_1 + tl.store(out1_ptrs, cube_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + else: + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + vec_result = d * 2.0 + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, vec_result, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def test_acf06_tc01(): + """ACF06-TC01: Verify float16 kernel compilation generates correct MLIR code.""" + signature = { + "a_ptr": "*fp16", + "b_ptr": "*fp16", + "c_ptr": "*fp16", + "d_ptr": "*fp16", + "out1_ptr": "*fp16", + "out2_ptr": "*fp16", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out1_0": "i32", + "stride_out1_1": "i32", + "stride_out2": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf06_tc01_vec_cond_cube, signature, constants) + _write_mlir_to_file(mlir, "acf06_tc01_vec_cond_cube.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf06_tc01_vec_cond_cube(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_acf06_tc02(): + """ACF06-TC02: Verify float32 kernel compilation generates correct MLIR code.""" + signature = { + "a_ptr": "*fp32", + "b_ptr": "*fp32", + "c_ptr": "*fp32", + "d_ptr": "*fp32", + "out1_ptr": "*fp32", + "out2_ptr": "*fp32", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out1_0": "i32", + "stride_out1_1": "i32", + "stride_out2": "i32", + } + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf06_tc02_vec_cond_cube, signature, constants) + _write_mlir_to_file(mlir, "acf06_tc02_vec_cond_cube.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf06_tc02_vec_cond_cube(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +if __name__ == "__main__": + test_acf06_tc01() + test_acf06_tc02() + print("All ACF06 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf07_mlir_if_else_v2c_dependency.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf07_mlir_if_else_v2c_dependency.py new file mode 100644 index 0000000000..c22d6a76af --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_acf07_mlir_if_else_v2c_dependency.py @@ -0,0 +1,252 @@ +""" +Test Case: ACF07 - loop with if/else control flow, V2C dependency (condition from Vector, Cube depends on Vector) + +[MLIR Validation] Refactored version + +Description: For loop with if/else: tl.max(c) as condition. If >0, Cube depends on Vector result (V2C), + else execute Vector (d / 2.0). + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - ACF07-TC01: float16, M=128, N=64, K=32 + - ACF07-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# ACF07-TC01: float16, M=128, N=64, K=32 +# Note: K is tl.constexpr, not in signature, put in constants +# ---------------------------------------------------------------------------- +@triton.jit +def acf07_tc01_v2c_control_flow( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + M, + N, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out_0, + stride_out_1, + K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + out_ptrs = out_ptr + offs_k[:, None] * stride_out_0 + offs_k[None, :] * stride_out_1 + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + vec_cond = tl.max(c) # scalar + + if vec_cond > 0.0: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube_result = tl.dot(a[:, None], b[None, :]) + vec_cond # (K,K) + scalar + tl.store(out_ptrs, cube_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + else: + loaded_d = tl.load(d_ptr + offs_k * stride_d, mask=offs_k < K, other=0.0) # (K,) + vec_result = loaded_d / 2.0 # (K,) + tl.store(out_ptrs, vec_result[None, :], mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ---------------------------------------------------------------------------- +# ACF07-TC02: float32, M=128, N=64, K=32 +# ---------------------------------------------------------------------------- +@triton.jit +def acf07_tc02_v2c_control_flow( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + M, + N, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out_0, + stride_out_1, + K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + out_ptrs = out_ptr + offs_k[:, None] * stride_out_0 + offs_k[None, :] * stride_out_1 + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + vec_cond = tl.max(c) + + if vec_cond > 0.0: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + cube_result = tl.dot(a[:, None], b[None, :]) + vec_cond + tl.store(out_ptrs, cube_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + else: + loaded_d = tl.load(d_ptr + offs_k * stride_d, mask=offs_k < K, other=0.0) + vec_result = loaded_d / 2.0 + tl.store(out_ptrs, vec_result[None, :], mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def test_acf07_tc01(): + """ACF07-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Note: K is a constexpr parameter, put in constants rather than signature. + """ + signature = { + "a_ptr": "*fp16", + "b_ptr": "*fp16", + "c_ptr": "*fp16", + "d_ptr": "*fp16", + "out_ptr": "*fp16", + "M": "i32", + "N": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out_0": "i32", + "stride_out_1": "i32", + } + constants = {"K": 32, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf07_tc01_v2c_control_flow, signature, constants) + _write_mlir_to_file(mlir, "acf07_tc01_v2c_control_flow.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf07_tc01_v2c_control_flow(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_acf07_tc02(): + """ACF07-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Note: K is a constexpr parameter, put in constants rather than signature. + """ + signature = { + "a_ptr": "*fp32", + "b_ptr": "*fp32", + "c_ptr": "*fp32", + "d_ptr": "*fp32", + "out_ptr": "*fp32", + "M": "i32", + "N": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out_0": "i32", + "stride_out_1": "i32", + } + constants = {"K": 32, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(acf07_tc02_v2c_control_flow, signature, constants) + _write_mlir_to_file(mlir, "acf07_tc02_v2c_control_flow.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @acf07_tc02_v2c_control_flow(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +if __name__ == "__main__": + test_acf07_tc01() + test_acf07_tc02() + print("All ACF07 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb01_mlir_single_cube_single_vector.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb01_mlir_single_cube_single_vector.py new file mode 100644 index 0000000000..a3c0bb2a80 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb01_mlir_single_cube_single_vector.py @@ -0,0 +1,329 @@ +""" +Test Case: PCB01 - Single independent Cube + single independent Vector, no data dependency + + +[MLIR Validation] Refactored version + + +Description: Single independent Cube (outer product) + single independent Vector (element-wise add), + no data dependency between Cube and Vector operations. + + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + + +Test Cases: + - PCB01-TC01: float16, M=128, N=64, K=32 + - PCB01-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + """Write MLIR code to a file at the specified path. + + Args: + mlir: MLIR code string + filename: output filename (without path), e.g. "pcb01_tc01.mlir" + """ + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + # The normal compilation path obtains this via backend.get_codegen_implementation(options); + # here we import min_dot_size directly from the Ascend backend and construct it. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB01-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of independent CUBE + VECTOR operations under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb01_tc01_single_cube_vector( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out1m, + stride_out1n, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + a_mat = a[:, None] # (K, 1) + b_mat = b[None, :] # (1, K) + cube_result = tl.dot(a_mat, b_mat) # (K, K) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1m + offs_k[None, :] * stride_out1n + out1_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out1_ptrs, cube_result, mask=out1_mask) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + vec_result = c + d # (N,) + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, vec_result, mask=offs_n < N) + + +# ---------------------------------------------------------------------------- +# PCB01-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of independent CUBE + VECTOR operations under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb01_tc02_single_cube_vector( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out1m, + stride_out1n, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + a_mat = a[:, None] # (K, 1) + b_mat = b[None, :] # (1, K) + cube_result = tl.dot(a_mat, b_mat) # (K, K) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1m + offs_k[None, :] * stride_out1n + out1_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out1_ptrs, cube_result, mask=out1_mask) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + vec_result = c + d # (N,) + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, vec_result, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def test_pcb01_tc01(): + """PCB01-TC01: Verify float16 kernel compilation generates correct MLIR code. + + + Test steps: + 1. Compile pcb01_tc01_single_cube_vector kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + # Define argument type signature: float16 pointers + int32 scalar arguments + signature = { + "a_ptr": "*fp16", + "b_ptr": "*fp16", + "c_ptr": "*fp16", + "d_ptr": "*fp16", + "out1_ptr": "*fp16", + "out2_ptr": "*fp16", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out1m": "i32", + "stride_out1n": "i32", + "stride_out2": "i32", + } + # constexpr parameter + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + # Compile Compile kernel to MLIR + mlir = compile_kernel(pcb01_tc01_single_cube_vector, signature, constants) + + # Output MLIR code to the specified path + _write_mlir_to_file(mlir, "pcb01_tc01_single_cube_vector.mlir") + + # Verify MLIR generated successfully + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + + # Verify MLIR contains function definition + assert "func.func @pcb01_tc01_single_cube_vector(" in mlir, \ + "Kernel function definition not found in MLIR code" + + # Verify MLIR code contains the "scope" keyword + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + +def test_pcb01_tc02(): + """PCB01-TC02: Verify float32 kernel compilation generates correct MLIR code. + + + Test steps: + 1. Compile pcb01_tc02_single_cube_vector kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + # Define argument type signature: float32 pointers + int32 scalar arguments + signature = { + "a_ptr": "*fp32", + "b_ptr": "*fp32", + "c_ptr": "*fp32", + "d_ptr": "*fp32", + "out1_ptr": "*fp32", + "out2_ptr": "*fp32", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out1m": "i32", + "stride_out1n": "i32", + "stride_out2": "i32", + } + # constexpr parameter + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + # Compile Compile kernel to MLIR + mlir = compile_kernel(pcb01_tc02_single_cube_vector, signature, constants) + + # Output MLIR code to the specified path + _write_mlir_to_file(mlir, "pcb01_tc02_single_cube_vector.mlir") + + # Verify MLIR generated successfully + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + + # Verify MLIR contains function definition + assert "func.func @pcb01_tc02_single_cube_vector(" in mlir, \ + "Kernel function definition not found in MLIR code" + + # Verify MLIR code contains the "scope" keyword + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + +# ============================================================================ +# Main for manual testing +# ============================================================================ + +if __name__ == "__main__": + test_pcb01_tc01() + test_pcb01_tc02() + print("All PCB01 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb02_mlir_multi_cube_multi_vector.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb02_mlir_multi_cube_multi_vector.py new file mode 100644 index 0000000000..7fa9d3a32e --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb02_mlir_multi_cube_multi_vector.py @@ -0,0 +1,347 @@ +""" +Test Case: PCB02 - Multiple independent Cube + multiple independent Vector, no data dependency + +[MLIR Validation] Refactored version + +Description: 2 independent Cube (outer product) + 2 independent Vector (element-wise sub), + no data dependency between Cube and Vector operations. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB02-TC01: float16, M=128, N=64, K=32 + - PCB02-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB02-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of multiple independent CUBE + VECTOR operations under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb02_tc01_multi_cube_vector( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + out1_ptr, + out2_ptr, + out3_ptr, + out4_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_ck, + stride_dk, + stride_dn, + stride_e, + stride_f, + stride_g, + stride_h, + stride_out1m, + stride_out1n, + stride_out2m, + stride_out2n, + stride_out3, + stride_out4, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a1 = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b1 = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube_result1 = tl.dot(a1[:, None], b1[None, :]) # (K, K) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1m + offs_k[None, :] * stride_out1n + out1_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out1_ptrs, cube_result1, mask=out1_mask) + + a2 = tl.load(c_ptr + k * stride_cm + offs_k * stride_ck, mask=offs_k < K, other=0.0) # (K,) + b2 = tl.load(d_ptr + k * stride_dk + offs_k * stride_dn, mask=offs_k < K, other=0.0) # (K,) + cube_result2 = tl.dot(a2[:, None], b2[None, :]) # (K, K) + out2_ptrs = out2_ptr + offs_k[:, None] * stride_out2m + offs_k[None, :] * stride_out2n + out2_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out2_ptrs, cube_result2, mask=out2_mask) + + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) # (N,) + vec_result1 = e - f # (N,) + out3_ptrs = out3_ptr + offs_n * stride_out3 + tl.store(out3_ptrs, vec_result1, mask=offs_n < N) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) # (N,) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) # (N,) + vec_result2 = g - h # (N,) + out4_ptrs = out4_ptr + offs_n * stride_out4 + tl.store(out4_ptrs, vec_result2, mask=offs_n < N) + + +# ---------------------------------------------------------------------------- +# PCB02-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of multiple independent CUBE + VECTOR operations under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb02_tc02_multi_cube_vector( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + out1_ptr, + out2_ptr, + out3_ptr, + out4_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_ck, + stride_dk, + stride_dn, + stride_e, + stride_f, + stride_g, + stride_h, + stride_out1m, + stride_out1n, + stride_out2m, + stride_out2n, + stride_out3, + stride_out4, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a1 = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b1 = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube_result1 = tl.dot(a1[:, None], b1[None, :]) # (K, K) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1m + offs_k[None, :] * stride_out1n + out1_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out1_ptrs, cube_result1, mask=out1_mask) + + a2 = tl.load(c_ptr + k * stride_cm + offs_k * stride_ck, mask=offs_k < K, other=0.0) # (K,) + b2 = tl.load(d_ptr + k * stride_dk + offs_k * stride_dn, mask=offs_k < K, other=0.0) # (K,) + cube_result2 = tl.dot(a2[:, None], b2[None, :]) # (K, K) + out2_ptrs = out2_ptr + offs_k[:, None] * stride_out2m + offs_k[None, :] * stride_out2n + out2_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out2_ptrs, cube_result2, mask=out2_mask) + + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) # (N,) + vec_result1 = e - f # (N,) + out3_ptrs = out3_ptr + offs_n * stride_out3 + tl.store(out3_ptrs, vec_result1, mask=offs_n < N) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) # (N,) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) # (N,) + vec_result2 = g - h # (N,) + out4_ptrs = out4_ptr + offs_n * stride_out4 + tl.store(out4_ptrs, vec_result2, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb02_signature(dtype_str): + """Build the argument type signature for the PCB02 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "out1_ptr": f"*{dtype_str}", + "out2_ptr": f"*{dtype_str}", + "out3_ptr": f"*{dtype_str}", + "out4_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_cm": "i32", + "stride_ck": "i32", + "stride_dk": "i32", + "stride_dn": "i32", + "stride_e": "i32", + "stride_f": "i32", + "stride_g": "i32", + "stride_h": "i32", + "stride_out1m": "i32", + "stride_out1n": "i32", + "stride_out2m": "i32", + "stride_out2n": "i32", + "stride_out3": "i32", + "stride_out4": "i32", + } + + +def test_pcb02_tc01(): + """PCB02-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb02_tc01_multi_cube_vector kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb02_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb02_tc01_multi_cube_vector, signature, constants) + _write_mlir_to_file(mlir, "pcb02_tc01_multi_cube_vector.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb02_tc01_multi_cube_vector(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb02_tc02(): + """PCB02-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb02_tc02_multi_cube_vector kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb02_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb02_tc02_multi_cube_vector, signature, constants) + _write_mlir_to_file(mlir, "pcb02_tc02_multi_cube_vector.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb02_tc02_multi_cube_vector(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb02_tc01() + test_pcb02_tc02() + print("All PCB02 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb03_mlir_cube_chain_c2c_independent_vector.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb03_mlir_cube_chain_c2c_independent_vector.py new file mode 100644 index 0000000000..4d96b2b212 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb03_mlir_cube_chain_c2c_independent_vector.py @@ -0,0 +1,308 @@ +""" +Test Case: PCB03 - Cube chain dependency C2C (Cube1->Cube2->Cube3) + independent Vector + +[MLIR Validation] Refactored version + +Description: Cube1 (outer product) -> Cube2 (dot with C) -> Cube3 (dot with D) chain dependency, + independent Vector (element-wise multiply E * F). + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions; this is a fallback scenario, + so the MLIR code must NOT contain the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB03-TC01: float16, M=128, N=64, K=32 + - PCB03-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB03-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of Cube chain dependency (C2C) + independent VECTOR operations under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb03_tc01_cube_chain( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_cube_ptr, + out_vec_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_ck, + stride_dm, + stride_dk, + stride_e, + stride_f, + stride_out_cube, + stride_out_vec, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube1_result = tl.dot(a[:, None], b[None, :]).to(tl.float16) # (K, K) + + c = tl.load(c_ptr + k * stride_cm + offs_k * stride_ck, mask=offs_k < K, other=0.0) # (K,) + cube2_result = tl.dot(cube1_result, c[:, None]).to(tl.float16) # (K, 1) + + d = tl.load(d_ptr + k * stride_dm + offs_k * stride_dk, mask=offs_k < K, other=0.0) # (K,) + cube3_result = tl.dot(cube2_result, d[None, :]) # (K, K) + out_cube_ptrs = out_cube_ptr + offs_k[:, None] * stride_out_cube + offs_k[None, :] + tl.store(out_cube_ptrs, cube3_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) # (N,) + vec_result = e * f # (N,) + out_vec_ptrs = out_vec_ptr + offs_n * stride_out_vec + tl.store(out_vec_ptrs, vec_result, mask=offs_n < N) + + +# ---------------------------------------------------------------------------- +# PCB03-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of Cube chain dependency (C2C) + independent VECTOR operations under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb03_tc02_cube_chain( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_cube_ptr, + out_vec_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_ck, + stride_dm, + stride_dk, + stride_e, + stride_f, + stride_out_cube, + stride_out_vec, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube1_result = tl.dot(a[:, None], b[None, :]) # (K, K) + + c = tl.load(c_ptr + k * stride_cm + offs_k * stride_ck, mask=offs_k < K, other=0.0) # (K,) + cube2_result = tl.dot(cube1_result, c[:, None]) # (K, 1) + + d = tl.load(d_ptr + k * stride_dm + offs_k * stride_dk, mask=offs_k < K, other=0.0) # (K,) + cube3_result = tl.dot(cube2_result, d[None, :]) # (K, K) + out_cube_ptrs = out_cube_ptr + offs_k[:, None] * stride_out_cube + offs_k[None, :] + tl.store(out_cube_ptrs, cube3_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) # (N,) + vec_result = e * f # (N,) + out_vec_ptrs = out_vec_ptr + offs_n * stride_out_vec + tl.store(out_vec_ptrs, vec_result, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb03_signature(dtype_str): + """Build the argument type signature for the PCB03 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_cube_ptr": f"*{dtype_str}", + "out_vec_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_cm": "i32", + "stride_ck": "i32", + "stride_dm": "i32", + "stride_dk": "i32", + "stride_e": "i32", + "stride_f": "i32", + "stride_out_cube": "i32", + "stride_out_vec": "i32", + } + + +def test_pcb03_tc01(): + """PCB03-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb03_tc01_cube_chain kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb03_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb03_tc01_cube_chain, signature, constants) + _write_mlir_to_file(mlir, "pcb03_tc01_cube_chain.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb03_tc01_cube_chain(" in mlir, \ + "Kernel function definition not found in MLIR code" + # The cube chain stages through L1 (L0C -> cbuf), which stays on CUBE, so the + # only cross-core traffic here is the V->C copies: a one-way interaction, and + # the pipeline falls back on those, as in sdf10 and the other one-way cases. + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb03_tc02(): + """PCB03-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb03_tc02_cube_chain kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb03_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb03_tc02_cube_chain, signature, constants) + _write_mlir_to_file(mlir, "pcb03_tc02_cube_chain.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb03_tc02_cube_chain(" in mlir, \ + "Kernel function definition not found in MLIR code" + # The cube chain stages through L1 (L0C -> cbuf), which stays on CUBE, so the + # only cross-core traffic here is the V->C copies: a one-way interaction, and + # the pipeline falls back on those, as in sdf10 and the other one-way cases. + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb03_tc01() + test_pcb03_tc02() + print("All PCB03 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb04_mlir_cube_chain_c2c_vector_chain_v2v.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb04_mlir_cube_chain_c2c_vector_chain_v2v.py new file mode 100644 index 0000000000..adde220d41 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb04_mlir_cube_chain_c2c_vector_chain_v2v.py @@ -0,0 +1,304 @@ +""" +Test Case: PCB04 - Cube chain C2C (Cube1->Cube2) + Vector chain V2V (Vec1->Vec2) + +[MLIR Validation] Refactored version + +Description: Cube chain C2C (Cube1->Cube2) + Vector chain V2V (Vec1->Vec2) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions; this is a fallback scenario, + so the MLIR code must NOT contain the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB04-TC01: float16, M=128, N=64, K=32 + - PCB04-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB04-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of Cube chain dependency (C2C) + VECTOR chain dependency (V2V) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb04_tc01_cube_vec_chain( + a_ptr, + b_ptr, + c_ptr, + e_ptr, + f_ptr, + g_ptr, + out_cube_ptr, + out_vec_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_ck, + stride_e, + stride_f, + stride_g, + stride_out_cube, + stride_out_vec, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube1_result = tl.dot(a[:, None], b[None, :]).to(tl.float16) # (K, K) + + c = tl.load(c_ptr + k * stride_cm + offs_k * stride_ck, mask=offs_k < K, other=0.0) # (K,) + cube2_result = tl.dot(cube1_result, c[:, None]).to(tl.float16) # (K, 1) + out_cube_ptrs = out_cube_ptr + offs_k[:, None] * stride_out_cube + tl.store(out_cube_ptrs, cube2_result, mask=offs_k[:, None] < K) + + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) # (N,) + vec1_result = e / (f + 1e-6) # (N,) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) # (N,) + vec2_result = vec1_result + g # (N,) + out_vec_ptrs = out_vec_ptr + offs_n * stride_out_vec + tl.store(out_vec_ptrs, vec2_result, mask=offs_n < N) + + +# ---------------------------------------------------------------------------- +# PCB04-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of Cube chain dependency (C2C) + VECTOR chain dependency (V2V) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb04_tc02_cube_vec_chain( + a_ptr, + b_ptr, + c_ptr, + e_ptr, + f_ptr, + g_ptr, + out_cube_ptr, + out_vec_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_ck, + stride_e, + stride_f, + stride_g, + stride_out_cube, + stride_out_vec, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube1_result = tl.dot(a[:, None], b[None, :]) # (K, K) + + c = tl.load(c_ptr + k * stride_cm + offs_k * stride_ck, mask=offs_k < K, other=0.0) # (K,) + cube2_result = tl.dot(cube1_result, c[:, None]) # (K, 1) + out_cube_ptrs = out_cube_ptr + offs_k[:, None] * stride_out_cube + tl.store(out_cube_ptrs, cube2_result, mask=offs_k[:, None] < K) + + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) # (N,) + vec1_result = e / (f + 1e-6) # (N,) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) # (N,) + vec2_result = vec1_result + g # (N,) + out_vec_ptrs = out_vec_ptr + offs_n * stride_out_vec + tl.store(out_vec_ptrs, vec2_result, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb04_signature(dtype_str): + """Build the argument type signature for the PCB04 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "out_cube_ptr": f"*{dtype_str}", + "out_vec_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_cm": "i32", + "stride_ck": "i32", + "stride_e": "i32", + "stride_f": "i32", + "stride_g": "i32", + "stride_out_cube": "i32", + "stride_out_vec": "i32", + } + + +def test_pcb04_tc01(): + """PCB04-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb04_tc01_cube_vec_chain kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb04_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb04_tc01_cube_vec_chain, signature, constants) + _write_mlir_to_file(mlir, "pcb04_tc01_cube_vec_chain.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb04_tc01_cube_vec_chain(" in mlir, \ + "Kernel function definition not found in MLIR code" + # The cube chain stages through L1 (L0C -> cbuf), which stays on CUBE, so the + # only cross-core traffic here is the V->C copies: a one-way interaction, and + # the pipeline falls back on those, as in sdf10 and the other one-way cases. + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb04_tc02(): + """PCB04-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb04_tc02_cube_vec_chain kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb04_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb04_tc02_cube_vec_chain, signature, constants) + _write_mlir_to_file(mlir, "pcb04_tc02_cube_vec_chain.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb04_tc02_cube_vec_chain(" in mlir, \ + "Kernel function definition not found in MLIR code" + # The cube chain stages through L1 (L0C -> cbuf), which stays on CUBE, so the + # only cross-core traffic here is the V->C copies: a one-way interaction, and + # the pipeline falls back on those, as in sdf10 and the other one-way cases. + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb04_tc01() + test_pcb04_tc02() + print("All PCB04 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb08_mlir_loop_transpose_c2v_argmax.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb08_mlir_loop_transpose_c2v_argmax.py new file mode 100644 index 0000000000..8e2e5527da --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb08_mlir_loop_transpose_c2v_argmax.py @@ -0,0 +1,257 @@ +""" +Test Case: PCB08 - for loop, Cube with transpose upstream + C2V dependency (tl.argmax) + +[MLIR Validation] Refactored version + +Description: for loop, Cube with transpose upstream + C2V dependency (tl.argmax) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB08-TC01: float16, M=128, N=64, K=32 + - PCB08-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB08-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of Cube with transpose upstream + C2V dependency (tl.argmax) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb08_tc01_kernel( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + + cube_result = tl.dot(a[:, None], b[None, :]) # (K,N) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + c = c.to(cube_result.dtype) + vec_result = tl.argmax(tl.ravel(cube_result + c), axis=0) # (K,N)+(N,) + tl.store(out_ptr + pid * stride_out, vec_result) + + +# ---------------------------------------------------------------------------- +# PCB08-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of Cube with transpose upstream + C2V dependency (tl.argmax) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb08_tc02_kernel( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + + cube_result = tl.dot(a[:, None], b[None, :]) # (K,N) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + c = c.to(cube_result.dtype) + vec_result = tl.argmax(tl.ravel(cube_result + c), axis=0) # (K,N)+(N,) + tl.store(out_ptr + pid * stride_out, vec_result) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb08_signature(dtype_str): + """Build the argument type signature for the PCB08 kernel. + + Note: out_ptr stores the argmax result with type i64. + """ + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "out_ptr": "*i64", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_out": "i32", + } + + +def test_pcb08_tc01(): + """PCB08-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb08_tc01_kernel kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb08_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb08_tc01_kernel, signature, constants) + _write_mlir_to_file(mlir, "pcb08_tc01_kernel.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb08_tc01_kernel(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb08_tc02(): + """PCB08-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb08_tc02_kernel kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb08_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb08_tc02_kernel, signature, constants) + _write_mlir_to_file(mlir, "pcb08_tc02_kernel.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb08_tc02_kernel(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb08_tc01() + test_pcb08_tc02() + print("All PCB08 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb09_mlir_loop_fill_c2v_argmin.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb09_mlir_loop_fill_c2v_argmin.py new file mode 100644 index 0000000000..393a00a07a --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb09_mlir_loop_fill_c2v_argmin.py @@ -0,0 +1,255 @@ +""" +Test Case: PCB09 - for loop, Cube with fill upstream + C2V dependency (tl.argmin) + +[MLIR Validation] Refactored version + +Description: for loop, Cube with fill upstream + C2V dependency (tl.argmin) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB09-TC01: float16, M=128, N=64, K=32 + - PCB09-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB09-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of Cube with fill upstream + C2V dependency (tl.argmin) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb09_tc01_kernel( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + + cube_result = tl.dot(a[:, None], b[None, :]) # (K,N) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + vec_result = tl.argmin(tl.ravel(cube_result + c), axis=0) # (K,N)+(N,) + tl.store(out_ptr + pid * stride_out, vec_result) + + +# ---------------------------------------------------------------------------- +# PCB09-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of Cube with fill upstream + C2V dependency (tl.argmin) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb09_tc02_kernel( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + + cube_result = tl.dot(a[:, None], b[None, :]) # (K,N) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + vec_result = tl.argmin(tl.ravel(cube_result + c), axis=0) # (K,N)+(N,) + tl.store(out_ptr + pid * stride_out, vec_result) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb09_signature(dtype_str): + """Build the argument type signature for the PCB09 kernel. + + Note: out_ptr stores the argmin result with type i64. + """ + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "out_ptr": "*i64", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_out": "i32", + } + + +def test_pcb09_tc01(): + """PCB09-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb09_tc01_kernel kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb09_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb09_tc01_kernel, signature, constants) + _write_mlir_to_file(mlir, "pcb09_tc01_kernel.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb09_tc01_kernel(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb09_tc02(): + """PCB09-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb09_tc02_kernel kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb09_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb09_tc02_kernel, signature, constants) + _write_mlir_to_file(mlir, "pcb09_tc02_kernel.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb09_tc02_kernel(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb09_tc01() + test_pcb09_tc02() + print("All PCB09 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb10_mlir_loop_multi_v2c_xor_sum.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb10_mlir_loop_multi_v2c_xor_sum.py new file mode 100644 index 0000000000..7757de7564 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb10_mlir_loop_multi_v2c_xor_sum.py @@ -0,0 +1,274 @@ +""" +Test Case: PCB10 - for loop, multiple V2C dependency (tl.xor_sum + add -> Cube) + +[MLIR Validation] Refactored version + +Description: for loop, multiple V2C dependency (tl.xor_sum + add -> Cube) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB10-TC01: float16, M=128, N=64, K=32 + - PCB10-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB10-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of multiple V2C dependency (tl.xor_sum + add -> Cube) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb10_tc01_kernel( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_e, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + c_int = c.to(tl.int32) # xor_sum requires integer type + vec1_result = tl.xor_sum(c_int) # scalar + + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + vec2_result = d + e # (N,) + + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + cube_result = tl.dot(a[:, None], b[None, :]) + vec1_result + vec2_result # (K,N) + scalar + (N,) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + offs_n[None, :] + tl.store(out_ptrs, cube_result, mask=(offs_k[:, None] < K) & (offs_n[None, :] < N)) + + +# ---------------------------------------------------------------------------- +# PCB10-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of multiple V2C dependency (tl.xor_sum + add -> Cube) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb10_tc02_kernel( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_e, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + c_int = c.to(tl.int32) # xor_sum requires integer type + vec1_result = tl.xor_sum(c_int) # scalar + + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + e = tl.load(e_ptr + offs_n * stride_e, mask=offs_n < N, other=0.0) # (N,) + vec2_result = d + e # (N,) + + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + cube_result = tl.dot(a[:, None], b[None, :]) + vec1_result + vec2_result # (K,N) + scalar + (N,) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + offs_n[None, :] + tl.store(out_ptrs, cube_result, mask=(offs_k[:, None] < K) & (offs_n[None, :] < N)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb10_signature(dtype_str): + """Build the argument type signature for the PCB10 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_e": "i32", + "stride_out": "i32", + } + + +def test_pcb10_tc01(): + """PCB10-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb10_tc01_kernel kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb10_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb10_tc01_kernel, signature, constants) + _write_mlir_to_file(mlir, "pcb10_tc01_kernel.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb10_tc01_kernel(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb10_tc02(): + """PCB10-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb10_tc02_kernel kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb10_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb10_tc02_kernel, signature, constants) + _write_mlir_to_file(mlir, "pcb10_tc02_kernel.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb10_tc02_kernel(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb10_tc01() + test_pcb10_tc02() + print("All PCB10 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb11_mlir_loop_v2c_shared_vector.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb11_mlir_loop_v2c_shared_vector.py new file mode 100644 index 0000000000..572e49922f --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb11_mlir_loop_v2c_shared_vector.py @@ -0,0 +1,288 @@ +""" +Test Case: PCB11 - for loop, V2C dependency (reduce -> Cube1, Cube2 share same Vector) + +[MLIR Validation] Refactored version + +Description: for loop, V2C dependency (reduce -> Cube1, Cube2 share same Vector) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB11-TC01: float16, M=128, N=64, K=32 + - PCB11-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB11-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of V2C dependency (reduce -> Cube1, Cube2 share same Vector) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb11_tc01_kernel( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_dm, + stride_dk, + stride_em, + stride_ek, + stride_out1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + vec_result = tl.sum(c) # scalar + + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube1_result = tl.dot(a[:, None], b[None, :]) + vec_result # (K,K) + scalar = (K,K) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1 + offs_k[None, :] + tl.store(out1_ptrs, cube1_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + d = tl.load(d_ptr + k * stride_dm + offs_k * stride_dk, mask=offs_k < K, other=0.0) # (K,) + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + cube2_result = tl.dot(d[:, None], e[None, :]) + vec_result # (K,K) + scalar = (K,K) + out2_ptrs = out2_ptr + offs_k[:, None] * stride_out2 + offs_k[None, :] + tl.store(out2_ptrs, cube2_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ---------------------------------------------------------------------------- +# PCB11-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of V2C dependency (reduce -> Cube1, Cube2 share same Vector) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb11_tc02_kernel( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_dm, + stride_dk, + stride_em, + stride_ek, + stride_out1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + vec_result = tl.sum(c) # scalar + + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + cube1_result = tl.dot(a[:, None], b[None, :]) + vec_result # (K,K) + scalar = (K,K) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1 + offs_k[None, :] + tl.store(out1_ptrs, cube1_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + d = tl.load(d_ptr + k * stride_dm + offs_k * stride_dk, mask=offs_k < K, other=0.0) # (K,) + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + cube2_result = tl.dot(d[:, None], e[None, :]) + vec_result # (K,K) + scalar = (K,K) + out2_ptrs = out2_ptr + offs_k[:, None] * stride_out2 + offs_k[None, :] + tl.store(out2_ptrs, cube2_result, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb11_signature(dtype_str): + """Build the argument type signature for the PCB11 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "out1_ptr": f"*{dtype_str}", + "out2_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_dm": "i32", + "stride_dk": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_out1": "i32", + "stride_out2": "i32", + } + + +def test_pcb11_tc01(): + """PCB11-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb11_tc01_kernel kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb11_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb11_tc01_kernel, signature, constants) + _write_mlir_to_file(mlir, "pcb11_tc01_kernel.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb11_tc01_kernel(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb11_tc02(): + """PCB11-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb11_tc02_kernel kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb11_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb11_tc02_kernel, signature, constants) + _write_mlir_to_file(mlir, "pcb11_tc02_kernel.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb11_tc02_kernel(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb11_tc01() + test_pcb11_tc02() + print("All PCB11 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb12_mlir_while_loop_c_zero_fill.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb12_mlir_while_loop_c_zero_fill.py new file mode 100644 index 0000000000..c7a650e7ed --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb12_mlir_while_loop_c_zero_fill.py @@ -0,0 +1,253 @@ +""" +Test Case: PCB12 - while loop, A*B+C where C=0 (fill) + +[MLIR Validation] Refactored version + +Description: while loop, A*B+C where C=0 (fill) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB12-TC01: float16, M=128, N=64, K=32 + - PCB12-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB12-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of while-loop A*B+C (C=0 fill) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb12_tc01_while_matmul_fill( + a_ptr, + b_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + k = 0 + while k < K: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + c = tl.full([BLOCK_SIZE_K, BLOCK_SIZE_N], 0.0, tl.float32) # (K,N) + dot_result = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,N) = (K,N) + result = dot_result + c # (K,N) + (K,N) = (K,N) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, result, mask=out_mask) + k += 1 + + +# ---------------------------------------------------------------------------- +# PCB12-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of while-loop A*B+C (C=0 fill) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb12_tc02_while_matmul_fill( + a_ptr, + b_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + k = 0 + while k < K: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + c = tl.full([BLOCK_SIZE_K, BLOCK_SIZE_N], 0.0, tl.float32) # (K,N) + dot_result = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,N) = (K,N) + result = dot_result + c # (K,N) + (K,N) = (K,N) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, result, mask=out_mask) + k += 1 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb12_signature(dtype_str): + """Build the argument type signature for the PCB12 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_pcb12_tc01(): + """PCB12-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb12_tc01_while_matmul_fill kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb12_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb12_tc01_while_matmul_fill, signature, constants) + _write_mlir_to_file(mlir, "pcb12_tc01_while_matmul_fill.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb12_tc01_while_matmul_fill(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb12_tc02(): + """PCB12-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb12_tc02_while_matmul_fill kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb12_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb12_tc02_while_matmul_fill, signature, constants) + _write_mlir_to_file(mlir, "pcb12_tc02_while_matmul_fill.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb12_tc02_while_matmul_fill(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb12_tc01() + test_pcb12_tc02() + print("All PCB12 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb13_mlir_while_loop_c_nonzero.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb13_mlir_while_loop_c_nonzero.py new file mode 100644 index 0000000000..70b6edd36d --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb13_mlir_while_loop_c_nonzero.py @@ -0,0 +1,261 @@ +""" +Test Case: PCB13 - while loop, A*B+C where C!=0 + +[MLIR Validation] Refactored version + +Description: while loop, A*B+C where C!=0 + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB13-TC01: float16, M=128, N=64, K=32 + - PCB13-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB13-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of while-loop A*B+C (C!=0) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb13_tc01_while_matmul_add( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cn, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + c = tl.load(c_ptr + offs_n * stride_cn, mask=offs_n < N, other=0.0) # (N,) + + k = 0 + while k < K: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + dot_result = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,N) -> (K, N) + result = dot_result + c[None, :] # (K,N) + (1,N) -> (K,N) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, result, mask=out_mask) + k += 1 + + +# ---------------------------------------------------------------------------- +# PCB13-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of while-loop A*B+C (C!=0) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb13_tc02_while_matmul_add( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cn, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + c = tl.load(c_ptr + offs_n * stride_cn, mask=offs_n < N, other=0.0) # (N,) + + k = 0 + while k < K: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + dot_result = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,N) -> (K, N) + result = dot_result + c[None, :] # (K,N) + (1,N) -> (K,N) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, result, mask=out_mask) + k += 1 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb13_signature(dtype_str): + """Build the argument type signature for the PCB13 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_cn": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_pcb13_tc01(): + """PCB13-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb13_tc01_while_matmul_add kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb13_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb13_tc01_while_matmul_add, signature, constants) + _write_mlir_to_file(mlir, "pcb13_tc01_while_matmul_add.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb13_tc01_while_matmul_add(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb13_tc02(): + """PCB13-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb13_tc02_while_matmul_add kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb13_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb13_tc02_while_matmul_add, signature, constants) + _write_mlir_to_file(mlir, "pcb13_tc02_while_matmul_add.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb13_tc02_while_matmul_add(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb13_tc01() + test_pcb13_tc02() + print("All PCB13 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb14_mlir_while_loop_c_broadcast_scalar.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb14_mlir_while_loop_c_broadcast_scalar.py new file mode 100644 index 0000000000..8e9ccb0427 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_pcb14_mlir_while_loop_c_broadcast_scalar.py @@ -0,0 +1,260 @@ +""" +Test Case: PCB14 - while loop, A*B+C where C from broadcast scalar + +[MLIR Validation] Refactored version + +Description: while loop, A*B+C where C from broadcast scalar + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - PCB14-TC01: float16, M=128, N=64, K=32 + - PCB14-TC02: float32, M=128, N=64, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# PCB14-TC01: float16, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of while-loop A*B+C (C=broadcast scalar) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb14_tc01_while_matmul_scalar( + a_ptr, + b_ptr, + c_scalar_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + c = tl.load(c_scalar_ptr) # scalar + c_broadcast = tl.full([BLOCK_SIZE_K, BLOCK_SIZE_N], c, tl.float32) # (K, N) + + k = 0 + while k < K: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + dot_result = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,N) -> (K, N) + result = dot_result + c_broadcast # (K,N) + (K,N) -> (K,N) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, result, mask=out_mask) + k += 1 + + +# ---------------------------------------------------------------------------- +# PCB14-TC02: float32, M=128, N=64, K=32 +# Test purpose: Verify MLIR generation of while-loop A*B+C (C=broadcast scalar) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def pcb14_tc02_while_matmul_scalar( + a_ptr, + b_ptr, + c_scalar_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + c = tl.load(c_scalar_ptr) # scalar + c_broadcast = tl.full([BLOCK_SIZE_K, BLOCK_SIZE_N], c, tl.float32) # (K, N) + + k = 0 + while k < K: + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + dot_result = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,N) -> (K, N) + result = dot_result + c_broadcast # (K,N) + (K,N) -> (K,N) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, result, mask=out_mask) + k += 1 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_pcb14_signature(dtype_str): + """Build the argument type signature for the PCB14 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_scalar_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_pcb14_tc01(): + """PCB14-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb14_tc01_while_matmul_scalar kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb14_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb14_tc01_while_matmul_scalar, signature, constants) + _write_mlir_to_file(mlir, "pcb14_tc01_while_matmul_scalar.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb14_tc01_while_matmul_scalar(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_pcb14_tc02(): + """PCB14-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile pcb14_tc02_while_matmul_scalar kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_pcb14_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(pcb14_tc02_while_matmul_scalar, signature, constants) + _write_mlir_to_file(mlir, "pcb14_tc02_while_matmul_scalar.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @pcb14_tc02_while_matmul_scalar(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_pcb14_tc01() + test_pcb14_tc02() + print("All PCB14 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf01_mlir_v2c_unaligned.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf01_mlir_v2c_unaligned.py new file mode 100644 index 0000000000..71c65b009f --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf01_mlir_v2c_unaligned.py @@ -0,0 +1,257 @@ +""" +Test Case: SDF01 - V2C dependency, not satisfying 32B alignment (N=3) + +[MLIR Validation] Refactored version + +Description: V2C dependency, not satisfying 32B alignment (N=3) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF01-TC01: float16, M=128, N=3, K=32 + - SDF01-TC02: float32, M=128, N=3, K=32 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF01-TC01: float16, M=128, N=3, K=32 +# Test purpose: Verify MLIR generation of V2C dependency, non-32B aligned (N=3) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf01_tc01_v2c_unaligned( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) = (32,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) = (3,) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) = (3,) + vec_result = c * 2.0 # (N,) = (3,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) = (32,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) = (3,) + cube_result = tl.dot(a[:, None], b[None, :]) + vec_result # (K,1)@(1,N)+(N,) = (K,N)+(1,N) = (K,N) = (32,3) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn # (K,N) + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) # (K,N) + tl.store(out_ptrs, cube_result, mask=out_mask) + + +# ---------------------------------------------------------------------------- +# SDF01-TC02: float32, M=128, N=3, K=32 +# Test purpose: Verify MLIR generation of V2C dependency, non-32B aligned (N=3) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf01_tc02_v2c_unaligned( + a_ptr, + b_ptr, + c_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) = (32,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) = (3,) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) = (3,) + vec_result = c * 2.0 # (N,) = (3,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) = (32,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) = (3,) + cube_result = tl.dot(a[:, None], b[None, :]) + vec_result # (K,N) = (32,3) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, cube_result, mask=out_mask) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf01_signature(dtype_str): + """Build the argument type signature for the SDF01 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_sdf01_tc01(): + """SDF01-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf01_tc01_v2c_unaligned kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf01_signature("fp16") + constants = {"BLOCK_SIZE_N": 3, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(sdf01_tc01_v2c_unaligned, signature, constants) + _write_mlir_to_file(mlir, "sdf01_tc01_v2c_unaligned.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf01_tc01_v2c_unaligned(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf01_tc02(): + """SDF01-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf01_tc02_v2c_unaligned kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf01_signature("fp32") + constants = {"BLOCK_SIZE_N": 3, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(sdf01_tc02_v2c_unaligned, signature, constants) + _write_mlir_to_file(mlir, "sdf01_tc02_v2c_unaligned.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf01_tc02_v2c_unaligned(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf01_tc01() + test_sdf01_tc02() + print("All SDF01 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf02_mlir_c2v_war_memory.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf02_mlir_c2v_war_memory.py new file mode 100644 index 0000000000..aa634a0001 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf02_mlir_c2v_war_memory.py @@ -0,0 +1,278 @@ +""" +Test Case: SDF02 - C2V dependency, WAR memory dependency + +[MLIR Validation] Refactored version + +Description: C2V dependency, WAR memory dependency + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF02-TC01: float16, M=128, N=64, K=128 + - SDF02-TC02: float32, M=128, N=64, K=128 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF02-TC01: float16, M=128, N=64, K=128 +# Test purpose: Verify MLIR generation of C2V dependency + WAR memory dependency under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf02_tc01_c2v_war( + a_ptr, + b_ptr, + c_ptr, + buf_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_buf_kk, + stride_buf_n, + stride_out_kk, + stride_out_n, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) = (128,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) = (64,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) = (128,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) = (64,) + cube_result = tl.dot(a[:, None], b[None, :]) # (K,1)@(1,N) = (K,N) = (128,64) + + old_val = tl.load(buf_ptr + offs_k[:, None] * stride_buf_kk + offs_n[None, :] * stride_buf_n, + mask=(offs_k[:, None] < K) & (offs_n[None, :] < N), other=0.0) # (K,N) = (128,64) + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) = (64,) + + vec_result = cube_result + c # (K,N) + (N,) = (K,N) = (128,64) + + store_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) # (K,N) + tl.store(buf_ptr + offs_k[:, None] * stride_buf_kk + offs_n[None, :] * stride_buf_n, vec_result, + mask=store_mask) + tl.store(out_ptr + offs_k[:, None] * stride_out_kk + offs_n[None, :] * stride_out_n, vec_result + old_val, + mask=store_mask) + + +# ---------------------------------------------------------------------------- +# SDF02-TC02: float32, M=128, N=64, K=128 +# Test purpose: Verify MLIR generation of C2V dependency + WAR memory dependency under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf02_tc02_c2v_war( + a_ptr, + b_ptr, + c_ptr, + buf_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_buf_kk, + stride_buf_n, + stride_out_kk, + stride_out_n, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) = (128,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) = (64,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) + cube_result = tl.dot(a[:, None], b[None, :]) # (K,N) = (128,64) + + old_val = tl.load(buf_ptr + offs_k[:, None] * stride_buf_kk + offs_n[None, :] * stride_buf_n, + mask=(offs_k[:, None] < K) & (offs_n[None, :] < N), other=0.0) + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + + vec_result = cube_result + c # (K,N) + (N,) = (K,N) + + store_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(buf_ptr + offs_k[:, None] * stride_buf_kk + offs_n[None, :] * stride_buf_n, vec_result, + mask=store_mask) + tl.store(out_ptr + offs_k[:, None] * stride_out_kk + offs_n[None, :] * stride_out_n, vec_result + old_val, + mask=store_mask) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf02_signature(dtype_str): + """Build the argument type signature for the SDF02 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "buf_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_buf_kk": "i32", + "stride_buf_n": "i32", + "stride_out_kk": "i32", + "stride_out_n": "i32", + } + + +def test_sdf02_tc01(): + """SDF02-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf02_tc01_c2v_war kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf02_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 128} + + mlir = compile_kernel(sdf02_tc01_c2v_war, signature, constants) + _write_mlir_to_file(mlir, "sdf02_tc01_c2v_war.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf02_tc01_c2v_war(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf02_tc02(): + """SDF02-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf02_tc02_c2v_war kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf02_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 128} + + mlir = compile_kernel(sdf02_tc02_c2v_war, signature, constants) + _write_mlir_to_file(mlir, "sdf02_tc02_c2v_war.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf02_tc02_c2v_war(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf02_tc01() + test_sdf02_tc02() + print("All SDF02 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf03_mlir_v2c_raw_memory.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf03_mlir_v2c_raw_memory.py new file mode 100644 index 0000000000..a4f2d33b55 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf03_mlir_v2c_raw_memory.py @@ -0,0 +1,269 @@ +""" +Test Case: SDF03 - V2C dependency, RAW memory dependency + +[MLIR Validation] Refactored version + +Description: V2C dependency, RAW memory dependency + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF03-TC01: float16, M=128, N=64, K=128 + - SDF03-TC02: float32, M=128, N=64, K=128 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF03-TC01: float16, M=128, N=64, K=128 +# Test purpose: Verify MLIR generation of V2C dependency + RAW memory dependency under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf03_tc01_v2c_raw( + a_ptr, + b_ptr, + c_ptr, + buf_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_buf, + stride_out_kk, + stride_out_k, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) = (128,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) = (64,) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) = (64,) + vec_result = c + 1.0 # (N,) = (64,) + tl.store(buf_ptr + offs_n * stride_buf, vec_result, mask=offs_n < N) + + buf_val = tl.load(buf_ptr + offs_n * stride_buf, mask=offs_n < N, other=0.0) # (N,) = (64,) + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) = (128,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) = (64,) + cube_result = tl.dot(a[:, None], b[None, :]) + buf_val # (K,1)@(1,N)+(N,) = (K,N) = (128,64) + + store_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) # (K,N) + tl.store(out_ptr + offs_k[:, None] * stride_out_kk + offs_n[None, :] * stride_out_k, cube_result, + mask=store_mask) + + +# ---------------------------------------------------------------------------- +# SDF03-TC02: float32, M=128, N=64, K=128 +# Test purpose: Verify MLIR generation of V2C dependency + RAW memory dependency under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf03_tc02_v2c_raw( + a_ptr, + b_ptr, + c_ptr, + buf_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_buf, + stride_out_kk, + stride_out_k, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for k in range(K): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + vec_result = c + 1.0 + tl.store(buf_ptr + offs_n * stride_buf, vec_result, mask=offs_n < N) + + buf_val = tl.load(buf_ptr + offs_n * stride_buf, mask=offs_n < N, other=0.0) + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) + cube_result = tl.dot(a[:, None], b[None, :]) + buf_val + + store_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptr + offs_k[:, None] * stride_out_kk + offs_n[None, :] * stride_out_k, cube_result, + mask=store_mask) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf03_signature(dtype_str): + """Build the argument type signature for the SDF03 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "buf_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_buf": "i32", + "stride_out_kk": "i32", + "stride_out_k": "i32", + } + + +def test_sdf03_tc01(): + """SDF03-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf03_tc01_v2c_raw kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf03_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 128} + + mlir = compile_kernel(sdf03_tc01_v2c_raw, signature, constants) + _write_mlir_to_file(mlir, "sdf03_tc01_v2c_raw.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf03_tc01_v2c_raw(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf03_tc02(): + """SDF03-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf03_tc02_v2c_raw kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf03_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 128} + + mlir = compile_kernel(sdf03_tc02_v2c_raw, signature, constants) + _write_mlir_to_file(mlir, "sdf03_tc02_v2c_raw.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf03_tc02_v2c_raw(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf03_tc01() + test_sdf03_tc02() + print("All SDF03 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf04_mlir_c2c_waw_memory.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf04_mlir_c2c_waw_memory.py new file mode 100644 index 0000000000..a35a1c924f --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf04_mlir_c2c_waw_memory.py @@ -0,0 +1,288 @@ +""" +Test Case: SDF04 - C2C dependency, WAW memory dependency + +[MLIR Validation] Refactored version + +Description: C2C dependency, WAW memory dependency + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF04-TC01: float16, M=128, N=64, K=128 + - SDF04-TC02: float32, M=128, N=64, K=128 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF04-TC01: float16, M=128, N=64, K=128 +# Test purpose: Verify MLIR generation of C2C dependency + WAW memory dependency under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf04_tc01_c2c_waw( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + buf_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_ck, + stride_dk, + stride_dn, + stride_buf_kk, + stride_buf_k, + stride_out_kk, + stride_out_k, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) = (128,) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) = (128,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) = (128,) + cube1_result = tl.dot(a[:, None], b[None, :]) # (K,K) = (128,128) + + store_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(buf_ptr + offs_k[:, None] * stride_buf_kk + offs_k[None, :] * stride_buf_k, cube1_result, + mask=store_mask) + + c = tl.load(c_ptr + k * stride_cm + offs_k * stride_ck, mask=offs_k < K, other=0.0) # (K,) = (128,) + d = tl.load(d_ptr + k * stride_dk + offs_k * stride_dn, mask=offs_k < K, other=0.0) # (K,) = (128,) + cube2_result = tl.dot(c[:, None], d[None, :]) # (K,K) = (128,128) + + tl.store(buf_ptr + offs_k[:, None] * stride_buf_kk + offs_k[None, :] * stride_buf_k, cube2_result, + mask=store_mask) + tl.store(out_ptr + offs_k[:, None] * stride_out_kk + offs_k[None, :] * stride_out_k, cube2_result, + mask=store_mask) + + +# ---------------------------------------------------------------------------- +# SDF04-TC02: float32, M=128, N=64, K=128 +# Test purpose: Verify MLIR generation of C2C dependency + WAW memory dependency under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf04_tc02_c2c_waw( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + buf_ptr, + out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_ck, + stride_dk, + stride_dn, + stride_buf_kk, + stride_buf_k, + stride_out_kk, + stride_out_k, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + cube1_result = tl.dot(a[:, None], b[None, :]) + + store_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(buf_ptr + offs_k[:, None] * stride_buf_kk + offs_k[None, :] * stride_buf_k, cube1_result, + mask=store_mask) + + c = tl.load(c_ptr + k * stride_cm + offs_k * stride_ck, mask=offs_k < K, other=0.0) + d = tl.load(d_ptr + k * stride_dk + offs_k * stride_dn, mask=offs_k < K, other=0.0) + cube2_result = tl.dot(c[:, None], d[None, :]) + + tl.store(buf_ptr + offs_k[:, None] * stride_buf_kk + offs_k[None, :] * stride_buf_k, cube2_result, + mask=store_mask) + tl.store(out_ptr + offs_k[:, None] * stride_out_kk + offs_k[None, :] * stride_out_k, cube2_result, + mask=store_mask) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf04_signature(dtype_str): + """Build the argument type signature for the SDF04 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "buf_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_cm": "i32", + "stride_ck": "i32", + "stride_dk": "i32", + "stride_dn": "i32", + "stride_buf_kk": "i32", + "stride_buf_k": "i32", + "stride_out_kk": "i32", + "stride_out_k": "i32", + } + + +def test_sdf04_tc01(): + """SDF04-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf04_tc01_c2c_waw kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf04_signature("fp16") + constants = {"BLOCK_SIZE_K": 128} + + mlir = compile_kernel(sdf04_tc01_c2c_waw, signature, constants) + _write_mlir_to_file(mlir, "sdf04_tc01_c2c_waw.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf04_tc01_c2c_waw(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf04_tc02(): + """SDF04-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf04_tc02_c2c_waw kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf04_signature("fp32") + constants = {"BLOCK_SIZE_K": 128} + + mlir = compile_kernel(sdf04_tc02_c2c_waw, signature, constants) + _write_mlir_to_file(mlir, "sdf04_tc02_c2c_waw.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf04_tc02_c2c_waw(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf04_tc01() + test_sdf04_tc02() + print("All SDF04 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf05_mlir_two_layer_independent_cv.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf05_mlir_two_layer_independent_cv.py new file mode 100644 index 0000000000..ed5555f2ce --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf05_mlir_two_layer_independent_cv.py @@ -0,0 +1,328 @@ +""" +Test Case: SDF05 - 2-layer nested, inner and outer each have independent CV, no data dependency + +[MLIR Validation] Refactored version + +Description: 2-layer nested, inner and outer each have independent CV, no data dependency + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF05-TC01: float16, M=128, N=64, K=128, L=3 + - SDF05-TC02: float32, M=128, N=64, K=128, L=3 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF05-TC01: float16, M=128, N=64, K=128, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, inner independent CV under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf05_tc01_nested( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + L, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fk, + stride_fn, + stride_g, + stride_h, + stride_out1_kk, + stride_out1_k, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) = (128,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) = (64,) + + for i in range(L): + a = tl.load(a_ptr + i * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + i * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + outer_cube = tl.dot(a[:, None], b[None, :]) # (K,K) = (128,128) + + store_mask_k = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out1_ptr + offs_k[:, None] * stride_out1_kk + offs_k[None, :] * stride_out1_k, outer_cube, + mask=store_mask_k) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + outer_vec = c - d # (N,) + tl.store(out2_ptr + offs_n * stride_out2, outer_vec, mask=offs_n < N) + + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + f = tl.load(f_ptr + k * stride_fk + offs_k * stride_fn, mask=offs_k < K, other=0.0) # (K,) + inner_cube = tl.dot(e[:, None], f[None, :]) # (K,K) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) # (N,) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) # (N,) + inner_vec = g + h # (N,) + + +# ---------------------------------------------------------------------------- +# SDF05-TC02: float32, M=128, N=64, K=128, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, inner independent CV under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf05_tc02_nested( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + L, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fk, + stride_fn, + stride_g, + stride_h, + stride_out1_kk, + stride_out1_k, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for i in range(L): + a = tl.load(a_ptr + i * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + i * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + outer_cube = tl.dot(a[:, None], b[None, :]) + + store_mask_k = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out1_ptr + offs_k[:, None] * stride_out1_kk + offs_k[None, :] * stride_out1_k, outer_cube, + mask=store_mask_k) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + outer_vec = c - d + tl.store(out2_ptr + offs_n * stride_out2, outer_vec, mask=offs_n < N) + + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + f = tl.load(f_ptr + k * stride_fk + offs_k * stride_fn, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(e[:, None], f[None, :]) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + inner_vec = g + h + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf05_signature(dtype_str): + """Build the argument type signature for the SDF05 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "out1_ptr": f"*{dtype_str}", + "out2_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fk": "i32", + "stride_fn": "i32", + "stride_g": "i32", + "stride_h": "i32", + "stride_out1_kk": "i32", + "stride_out1_k": "i32", + "stride_out2": "i32", + } + + +def test_sdf05_tc01(): + """SDF05-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf05_tc01_nested kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf05_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 128, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf05_tc01_nested, signature, constants) + _write_mlir_to_file(mlir, "sdf05_tc01_nested.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf05_tc01_nested(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf05_tc02(): + """SDF05-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf05_tc02_nested kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf05_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 128, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf05_tc02_nested, signature, constants) + _write_mlir_to_file(mlir, "sdf05_tc02_nested.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf05_tc02_nested(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf05_tc01() + test_sdf05_tc02() + print("All SDF05 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf06_mlir_two_layer_inner_cv_only.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf06_mlir_two_layer_inner_cv_only.py new file mode 100644 index 0000000000..fabd68a6d7 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf06_mlir_two_layer_inner_cv_only.py @@ -0,0 +1,274 @@ +""" +Test Case: SDF06 - 2-layer nested, only inner has CV, outer has scalar only + +[MLIR Validation] Refactored version + +Description: 2-layer nested, only inner has CV, outer has scalar only. +Outer loop over L (scalar i*2), inner loop over K with CV (tl.dot + vector multiply). + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF06-TC01: float16, M=128, N=64, K=32, L=3 + - SDF06-TC02: float32, M=128, N=64, K=32, L=3 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF06-TC01: float16, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, inner CV + outer scalar under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf06_tc01_inner_cv_only( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for i in range(L): + scalar_val = i * 2 + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + inner_cube = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,N) = (K,N) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + inner_vec = c * d # (N,) + + out_ptrs = out_ptr + offs_k[:, None] * stride_out + offs_n[None, :] + tl.store(out_ptrs, inner_cube + inner_vec[None, :], mask=(offs_k[:, None] < K) & (offs_n[None, :] < N)) + + +# ---------------------------------------------------------------------------- +# SDF06-TC02: float32, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, inner CV + outer scalar under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf06_tc02_inner_cv_only( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + for i in range(L): + scalar_val = i * 2 + + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_n * stride_bn, mask=offs_n < N, other=0.0) # (N,) + cube1_result = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,N) = (K,N) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + vec_result = c * d # (N,) + + out_ptrs = out_ptr + offs_k[:, None] * stride_out + offs_n[None, :] + tl.store(out_ptrs, cube1_result + vec_result[None, :], mask=(offs_k[:, None] < K) & (offs_n[None, :] < N)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf06_signature(dtype_str): + """Build the argument type signature for the SDF06 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out": "i32", + } + + +def test_sdf06_tc01(): + """SDF06-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf06_tc01_inner_cv_only kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf06_signature("fp16") + constants = {"BLOCK_SIZE_K": 32, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf06_tc01_inner_cv_only, signature, constants) + _write_mlir_to_file(mlir, "sdf06_tc01_inner_cv_only.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf06_tc01_inner_cv_only(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf06_tc02(): + """SDF06-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf06_tc02_inner_cv_only kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf06_signature("fp32") + constants = {"BLOCK_SIZE_K": 32, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf06_tc02_inner_cv_only, signature, constants) + _write_mlir_to_file(mlir, "sdf06_tc02_inner_cv_only.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf06_tc02_inner_cv_only(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf06_tc01() + test_sdf06_tc02() + print("All SDF06 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf07_mlir_two_layer_outer_cv_only.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf07_mlir_two_layer_outer_cv_only.py new file mode 100644 index 0000000000..9133961725 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf07_mlir_two_layer_outer_cv_only.py @@ -0,0 +1,270 @@ +""" +Test Case: SDF07 - 2-layer nested, only outer has CV, inner has scalar only + +[MLIR Validation] Refactored version + +Description: 2-layer nested, only outer has CV, inner has scalar only. +Outer loop over L with CV (tl.dot + vector divide), inner loop over K (scalar k+i). + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF07-TC01: float16, M=128, N=64, K=32, L=3 + - SDF07-TC02: float32, M=128, N=64, K=32, L=3 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF07-TC01: float16, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, outer CV + inner scalar under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf07_tc01_outer_cv_only( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_out, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) # (L,) + + for i in range(L): + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) # (L,) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bn, mask=offs_l < L, other=0.0) # (L,) + outer_cube = tl.dot(a[:, None], b[None, :]).to(tl.float16) # (L,1) @ (1,L) = (L,L) + + c = tl.load(c_ptr + offs_l * stride_c, mask=offs_l < L, other=0.0) # (L,) + d = tl.load(d_ptr + offs_l * stride_d, mask=offs_l < L, other=0.0) # (L,) + outer_vec = c / (d + 1e-6) # (L,) + + out_ptrs = out_ptr + offs_l[:, None] * stride_out + offs_l[None, :] + tl.store(out_ptrs, outer_cube + outer_vec[:, None], mask=offs_l[:, None] < L) + + for k in range(K): + scalar_val = k + i + + +# ---------------------------------------------------------------------------- +# SDF07-TC02: float32, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, outer CV + inner scalar under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf07_tc02_outer_cv_only( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_out, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) # (L,) + + for i in range(L): + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) # (L,) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bn, mask=offs_l < L, other=0.0) # (L,) + outer_cube = tl.dot(a[:, None], b[None, :]) # (L,1) @ (1,L) = (L,L) + + c = tl.load(c_ptr + offs_l * stride_c, mask=offs_l < L, other=0.0) # (L,) + d = tl.load(d_ptr + offs_l * stride_d, mask=offs_l < L, other=0.0) # (L,) + outer_vec = c / (d + 1e-6) # (L,) + + out_ptrs = out_ptr + offs_l[:, None] * stride_out + offs_l[None, :] + tl.store(out_ptrs, outer_cube + outer_vec[:, None], mask=offs_l[:, None] < L) + + for k in range(K): + scalar_val = k + i + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf07_signature(dtype_str): + """Build the argument type signature for the SDF07 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "stride_am": "i32", + "stride_al": "i32", + "stride_bl": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out": "i32", + } + + +def test_sdf07_tc01(): + """SDF07-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf07_tc01_outer_cv_only kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf07_signature("fp16") + constants = {"BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf07_tc01_outer_cv_only, signature, constants) + _write_mlir_to_file(mlir, "sdf07_tc01_outer_cv_only.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf07_tc01_outer_cv_only(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf07_tc02(): + """SDF07-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf07_tc02_outer_cv_only kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf07_signature("fp32") + constants = {"BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf07_tc02_outer_cv_only, signature, constants) + _write_mlir_to_file(mlir, "sdf07_tc02_outer_cv_only.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf07_tc02_outer_cv_only(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf07_tc01() + test_sdf07_tc02() + print("All SDF07 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf08_mlir_two_layer_c2c_outer_dep_inner.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf08_mlir_two_layer_c2c_outer_dep_inner.py new file mode 100644 index 0000000000..32bcc97b42 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf08_mlir_two_layer_c2c_outer_dep_inner.py @@ -0,0 +1,303 @@ +""" +Test Case: SDF08 - 2-layer nested, outer C depends on inner C (C2C cross-layer) + +[MLIR Validation] Refactored version + +Description: 2-layer nested, outer C depends on inner C (C2C cross-layer). +Inner loop over K accumulates inner_cube_acc, outer loop over L uses acc in tl.dot with E. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF08-TC01: float16, M=128, N=64, K=32, L=3 + - SDF08-TC02: float32, M=128, N=64, K=32, L=3 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF08-TC01: float16, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting C2C cross-layer dependency (inner CV -> outer CV) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf08_tc01_c2c_cross_layer( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_f, + stride_out, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + inner_cube_acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) # (K,K) + + for i in range(L): + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + inner_cube = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,K) = (K,K) + inner_cube_acc = inner_cube_acc + inner_cube # (K,K) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + inner_vec = tl.sum(c + d) # scalar + + e = tl.load(e_ptr + i * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + e = e.to(tl.float32) + outer_cube = tl.dot(inner_cube_acc, e[:, None]) # (K,K) @ (K,1) = (K,1) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, outer_cube, mask=offs_k[:, None] < K) + + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) + outer_vec = f + 1.0 + + +# ---------------------------------------------------------------------------- +# SDF08-TC02: float32, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting C2C cross-layer dependency (inner CV -> outer CV) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf08_tc02_c2c_cross_layer( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_f, + stride_out, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + inner_cube_acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) # (K,K) + + for i in range(L): + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + inner_cube = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,K) = (K,K) + inner_cube_acc = inner_cube_acc + inner_cube # (K,K) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + inner_vec = tl.sum(c + d) # scalar + + e = tl.load(e_ptr + i * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + e = e.to(tl.float32) + outer_cube = tl.dot(inner_cube_acc, e[:, None]) # (K,K) @ (K,1) = (K,1) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, outer_cube, mask=offs_k[:, None] < K) + + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) + outer_vec = f + 1.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf08_signature(dtype_str): + """Build the argument type signature for the SDF08 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_f": "i32", + "stride_out": "i32", + } + + +def test_sdf08_tc01(): + """SDF08-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf08_tc01_c2c_cross_layer kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf08_signature("fp16") + constants = {"BLOCK_SIZE_K": 32, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf08_tc01_c2c_cross_layer, signature, constants) + _write_mlir_to_file(mlir, "sdf08_tc01_c2c_cross_layer.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf08_tc01_c2c_cross_layer(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf08_tc02(): + """SDF08-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf08_tc02_c2c_cross_layer kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf08_signature("fp32") + constants = {"BLOCK_SIZE_K": 32, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf08_tc02_c2c_cross_layer, signature, constants) + _write_mlir_to_file(mlir, "sdf08_tc02_c2c_cross_layer.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf08_tc02_c2c_cross_layer(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf08_tc01() + test_sdf08_tc02() + print("All SDF08 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf09_mlir_two_layer_c2c_inner_dep_outer.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf09_mlir_two_layer_c2c_inner_dep_outer.py new file mode 100644 index 0000000000..c79cc3a1b5 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf09_mlir_two_layer_c2c_inner_dep_outer.py @@ -0,0 +1,310 @@ +""" +Test Case: SDF09 - 2-layer nested, inner C depends on outer C (C2C cross-layer, iter_args) + +[MLIR Validation] Refactored version + +Description: 2-layer nested, inner C depends on outer C (C2C cross-layer, iter_args). +Outer loop over L with CV (tl.dot + vector subtract), inner loop over K uses outer_cube in tl.dot with E, +accumulating outer_cube_acc. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF09-TC01: float16, M=128, N=64, K=32, L=3 + - SDF09-TC02: float32, M=128, N=64, K=32, L=3 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF09-TC01: float16, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting C2C cross-layer dependency (outer CV -> inner CV, iter_args) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf09_tc01_c2c_iter_args( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_el, + stride_f, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) # (L,) + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + outer_cube_acc = tl.zeros([BLOCK_SIZE_L, BLOCK_SIZE_L], tl.float32) # (L,L) + + for i in range(L): + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) # (L,) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bn, mask=offs_l < L, other=0.0) # (L,) + outer_cube = tl.dot(a[:, None], b[None, :]).to(tl.float16) # (L,1) @ (1,L) = (L,L) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + outer_vec = c - d # (N,) + + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_l * stride_el, mask=offs_l < L, other=0.0) # (L,) + inner_cube = tl.dot(outer_cube, e[:, None]) # (L,L) @ (L,1) = (L,1) + + inner_cube_bc = tl.broadcast_to(inner_cube, [BLOCK_SIZE_L, BLOCK_SIZE_L]) # (L,1) -> (L,L) + outer_cube_acc = outer_cube_acc + inner_cube_bc # (L,L) + + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) # (N,) + inner_vec = f * 2.0 + + out_ptrs = out_ptr + offs_l[:, None] * stride_out + offs_l[None, :] + tl.store(out_ptrs, outer_cube_acc, mask=offs_l[:, None] < L) + + +# ---------------------------------------------------------------------------- +# SDF09-TC02: float32, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting C2C cross-layer dependency (outer CV -> inner CV, iter_args) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf09_tc02_c2c_iter_args( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_el, + stride_f, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) # (L,) + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + + outer_cube_acc = tl.zeros([BLOCK_SIZE_L, BLOCK_SIZE_L], tl.float32) # (L,L) + + for i in range(L): + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) # (L,) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bn, mask=offs_l < L, other=0.0) # (L,) + outer_cube = tl.dot(a[:, None], b[None, :]) # (L,1) @ (1,L) = (L,L) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + outer_vec = c - d # (N,) + + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_l * stride_el, mask=offs_l < L, other=0.0) # (L,) + inner_cube = tl.dot(outer_cube, e[:, None]) # (L,L) @ (L,1) = (L,1) + + inner_cube_bc = tl.broadcast_to(inner_cube, [BLOCK_SIZE_L, BLOCK_SIZE_L]) # (L,1) -> (L,L) + outer_cube_acc = outer_cube_acc + inner_cube_bc # (L,L) + + f = tl.load(f_ptr + offs_n * stride_f, mask=offs_n < N, other=0.0) # (N,) + inner_vec = f * 2.0 + + out_ptrs = out_ptr + offs_l[:, None] * stride_out + offs_l[None, :] + tl.store(out_ptrs, outer_cube_acc, mask=offs_l[:, None] < L) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf09_signature(dtype_str): + """Build the argument type signature for the SDF09 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "stride_am": "i32", + "stride_al": "i32", + "stride_bl": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_el": "i32", + "stride_f": "i32", + "stride_out": "i32", + } + + +def test_sdf09_tc01(): + """SDF09-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf09_tc01_c2c_iter_args kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf09_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf09_tc01_c2c_iter_args, signature, constants) + _write_mlir_to_file(mlir, "sdf09_tc01_c2c_iter_args.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf09_tc01_c2c_iter_args(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf09_tc02(): + """SDF09-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf09_tc02_c2c_iter_args kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf09_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf09_tc02_c2c_iter_args, signature, constants) + _write_mlir_to_file(mlir, "sdf09_tc02_c2c_iter_args.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf09_tc02_c2c_iter_args(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf09_tc01() + test_sdf09_tc02() + print("All SDF09 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf10_mlir_two_layer_v2c_inner_dep_outer.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf10_mlir_two_layer_v2c_inner_dep_outer.py new file mode 100644 index 0000000000..710bf6b168 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf10_mlir_two_layer_v2c_inner_dep_outer.py @@ -0,0 +1,299 @@ +""" +Test Case: SDF10 - 2-layer nested, inner C depends on outer V (V2C cross-layer) + +[MLIR Validation] Refactored version + +Description: 2-layer nested, inner C depends on outer V (V2C cross-layer) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF10-TC01: float16, M=128, N=64, K=32, L=3 + - SDF10-TC02: float32, M=128, N=64, K=32, L=3 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF10-TC01: float16, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, inner C depends on outer V (V2C cross-layer) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf10_tc01_nested_v2c( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_al, + stride_bl, + stride_bl2, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_outm, + stride_outn, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for i in range(L): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + outer_vec = c + d + + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bl2, mask=offs_l < L, other=0.0) + + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + inner_cube = tl.dot(e[:, None], outer_vec[None, :]) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, inner_cube, mask=out_mask) + + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + inner_vec = f / 2.0 + + +# ---------------------------------------------------------------------------- +# SDF10-TC02: float32, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, inner C depends on outer V (V2C cross-layer) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf10_tc02_nested_v2c( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_al, + stride_bl, + stride_bl2, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_outm, + stride_outn, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for i in range(L): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + outer_vec = c + d + + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bl2, mask=offs_l < L, other=0.0) + + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(e[:, None], outer_vec[None, :]) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_n[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_n[None, :] < N) + tl.store(out_ptrs, inner_cube, mask=out_mask) + + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + inner_vec = f / 2.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf10_signature(dtype_str): + """Build the argument type signature for the SDF10 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "stride_am": "i32", + "stride_al": "i32", + "stride_bl": "i32", + "stride_bl2": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fn": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_sdf10_tc01(): + """SDF10-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf10_tc01_nested_v2c kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf10_signature("fp16") + constants = {"BLOCK_SIZE_L": 3, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(sdf10_tc01_nested_v2c, signature, constants) + _write_mlir_to_file(mlir, "sdf10_tc01_nested_v2c.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf10_tc01_nested_v2c(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf10_tc02(): + """SDF10-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf10_tc02_nested_v2c kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf10_signature("fp32") + constants = {"BLOCK_SIZE_L": 3, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32} + + mlir = compile_kernel(sdf10_tc02_nested_v2c, signature, constants) + _write_mlir_to_file(mlir, "sdf10_tc02_nested_v2c.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf10_tc02_nested_v2c(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf10_tc01() + test_sdf10_tc02() + print("All SDF10 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf11_mlir_two_layer_c2v_outer_dep_inner.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf11_mlir_two_layer_c2v_outer_dep_inner.py new file mode 100644 index 0000000000..3f40acb90c --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf11_mlir_two_layer_c2v_outer_dep_inner.py @@ -0,0 +1,303 @@ +""" +Test Case: SDF11 - 2-layer nested, outer V depends on inner C (C2V cross-layer) + +[MLIR Validation] Refactored version + +Description: 2-layer nested, outer V depends on inner C (C2V cross-layer) + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF11-TC01: float16, M=128, N=64, K=32, L=3 + - SDF11-TC02: float32, M=128, N=64, K=32, L=3 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF11-TC01: float16, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, outer V depends on inner C (C2V cross-layer) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf11_tc01_nested_c2v( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_ek, + stride_fm, + stride_fl, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + offs_l = tl.arange(0, BLOCK_SIZE_L) # (L,) + + for i in range(L): + inner_cube_acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) # (K,K) + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + inner_cube = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,K) = (K,K) + inner_cube_acc = inner_cube_acc + inner_cube # (K,K) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + inner_vec = tl.min(c + d) # scalar + + e = tl.load(e_ptr + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + outer_vec = inner_cube_acc + e[None, :] # (K,K) + (1,K) = (K,K) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_k[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out_ptrs, outer_vec, mask=out_mask) + + f = tl.load(f_ptr + i * stride_fm + offs_l * stride_fl, mask=offs_l < L, other=0.0) + + +# ---------------------------------------------------------------------------- +# SDF11-TC02: float32, M=128, N=64, K=32, L=3 +# Test purpose: Verify MLIR generation of 2-layer nesting, outer V depends on inner C (C2V cross-layer) under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf11_tc02_nested_c2v( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_ek, + stride_fm, + stride_fl, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) # (K,) + offs_n = tl.arange(0, BLOCK_SIZE_N) # (N,) + offs_l = tl.arange(0, BLOCK_SIZE_L) # (L,) + + for i in range(L): + inner_cube_acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) # (K,K) + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + inner_cube = tl.dot(a[:, None], b[None, :]) # (K,1) @ (1,K) = (K,K) + inner_cube_acc = inner_cube_acc + inner_cube # (K,K) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + inner_vec = tl.min(c + d) # scalar + + e = tl.load(e_ptr + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + outer_vec = inner_cube_acc + e[None, :] # (K,K) + (1,K) = (K,K) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_k[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out_ptrs, outer_vec, mask=out_mask) + + f = tl.load(f_ptr + i * stride_fm + offs_l * stride_fl, mask=offs_l < L, other=0.0) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf11_signature(dtype_str): + """Build the argument type signature for the SDF11 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_ek": "i32", + "stride_fm": "i32", + "stride_fl": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_sdf11_tc01(): + """SDF11-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf11_tc01_nested_c2v kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf11_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf11_tc01_nested_c2v, signature, constants) + _write_mlir_to_file(mlir, "sdf11_tc01_nested_c2v.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf11_tc01_nested_c2v(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf11_tc02(): + """SDF11-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf11_tc02_nested_c2v kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf11_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3} + + mlir = compile_kernel(sdf11_tc02_nested_c2v, signature, constants) + _write_mlir_to_file(mlir, "sdf11_tc02_nested_c2v.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf11_tc02_nested_c2v(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf11_tc01() + test_sdf11_tc02() + print("All SDF11 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf12_mlir_three_layer_independent_cv.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf12_mlir_three_layer_independent_cv.py new file mode 100644 index 0000000000..67edc27df3 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf12_mlir_three_layer_independent_cv.py @@ -0,0 +1,364 @@ +""" +Test Case: SDF12 - 3-layer nested loop, each layer has independent CV, no data dependency + +[MLIR Validation] Refactored version + +Description: 3-layer nested loop (L, P, K). Each layer has independent Cube and Vector. + No data dependency across layers. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF12-TC01: float16, M=128, N=64, K=32, L=3, P=2 + - SDF12-TC02: float32, M=128, N=64, K=32, L=3, P=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF12-TC01: float16, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting independent CV under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf12_tc01_three_layer_independent( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + i_ptr, + j_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_g, + stride_h, + stride_im, + stride_il, + stride_jl, + stride_jn, + stride_out1_0, + stride_out1_1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_p = tl.arange(0, BLOCK_SIZE_P) + + for i in range(L): + for j in range(P): + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) # (K,) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) # (K,) + inner_cube = tl.dot(a[:, None], b[None, :]) # (K,K) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1_0 + offs_k[None, :] * stride_out1_1 + tl.store(out1_ptrs, inner_cube, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + inner_vec = c + d # (N,) + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, inner_vec, mask=offs_n < N) + + e = tl.load(e_ptr + j * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + j * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + mid_cube = tl.dot(e[:, None], f[None, :]) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + mid_vec = g - h + + i_a = tl.load(i_ptr + i * stride_im + offs_l * stride_il, mask=offs_l < L, other=0.0) + i_b = tl.load(j_ptr + i * stride_jl + offs_l * stride_jn, mask=offs_l < L, other=0.0) + outer_cube = tl.dot(i_a[:, None], i_b[None, :]) + + +# ---------------------------------------------------------------------------- +# SDF12-TC02: float32, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting independent CV under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf12_tc02_three_layer_independent( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + i_ptr, + j_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_g, + stride_h, + stride_im, + stride_il, + stride_jl, + stride_jn, + stride_out1_0, + stride_out1_1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_p = tl.arange(0, BLOCK_SIZE_P) + + for i in range(L): + for j in range(P): + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(a[:, None], b[None, :]) + out1_ptrs = out1_ptr + offs_k[:, None] * stride_out1_0 + offs_k[None, :] * stride_out1_1 + tl.store(out1_ptrs, inner_cube, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + inner_vec = c + d + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, inner_vec, mask=offs_n < N) + + e = tl.load(e_ptr + j * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + j * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + mid_cube = tl.dot(e[:, None], f[None, :]) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + mid_vec = g - h + + i_a = tl.load(i_ptr + i * stride_im + offs_l * stride_il, mask=offs_l < L, other=0.0) + i_b = tl.load(j_ptr + i * stride_jl + offs_l * stride_jn, mask=offs_l < L, other=0.0) + outer_cube = tl.dot(i_a[:, None], i_b[None, :]) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf12_signature(dtype_str): + """Build the argument type signature for the SDF12 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "i_ptr": f"*{dtype_str}", + "j_ptr": f"*{dtype_str}", + "out1_ptr": f"*{dtype_str}", + "out2_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ep": "i32", + "stride_fp": "i32", + "stride_fn": "i32", + "stride_g": "i32", + "stride_h": "i32", + "stride_im": "i32", + "stride_il": "i32", + "stride_jl": "i32", + "stride_jn": "i32", + "stride_out1_0": "i32", + "stride_out1_1": "i32", + "stride_out2": "i32", + } + + +def test_sdf12_tc01(): + """SDF12-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf12_tc01_three_layer_independent kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf12_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3, "BLOCK_SIZE_P": 2} + + mlir = compile_kernel(sdf12_tc01_three_layer_independent, signature, constants) + _write_mlir_to_file(mlir, "sdf12_tc01_three_layer_independent.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf12_tc01_three_layer_independent(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf12_tc02(): + """SDF12-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf12_tc02_three_layer_independent kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf12_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3, "BLOCK_SIZE_P": 2} + + mlir = compile_kernel(sdf12_tc02_three_layer_independent, signature, constants) + _write_mlir_to_file(mlir, "sdf12_tc02_three_layer_independent.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf12_tc02_three_layer_independent(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf12_tc01() + test_sdf12_tc02() + print("All SDF12 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf13_mlir_three_layer_alternating_pure.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf13_mlir_three_layer_alternating_pure.py new file mode 100644 index 0000000000..b777f90add --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf13_mlir_three_layer_alternating_pure.py @@ -0,0 +1,337 @@ +""" +Test Case: SDF13 - 3-layer nested loop, alternating pure C or pure V, no data dependency + +[MLIR Validation] Refactored version + +Description: 3-layer nested loop (L, P, K). Outer layer pure C, middle layer pure V, inner layer pure C. + No data dependency across layers. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF13-TC01: float16, M=128, N=64, K=32, L=3, P=2 + - SDF13-TC02: float32, M=128, N=64, K=32, L=3, P=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF13-TC01: float16, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting alternating pure C / pure V under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf13_tc01_alternating_pure( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + out_outer_ptr, + out_mid_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fk, + stride_fn, + stride_out_0, + stride_out_1, + stride_outer_0, + stride_outer_1, + stride_mid, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_p = tl.arange(0, BLOCK_SIZE_P) + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for i in range(L): + # --- outer: pure C (cube) --- + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) # (L,) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bn, mask=offs_l < L, other=0.0) # (L,) + outer_cube = tl.dot(a[:, None], b[None, :]) # (L,L) + outer_ptrs = out_outer_ptr + offs_l[:, None] * stride_outer_0 + offs_l[None, :] * stride_outer_1 + tl.store(outer_ptrs, outer_cube, mask=(offs_l[:, None] < L) & (offs_l[None, :] < L)) + + for j in range(P): + # --- mid: pure V (vector) --- + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) # (N,) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) # (N,) + mid_vec = c + d # (N,) + mid_ptrs = out_mid_ptr + offs_n * stride_mid + tl.store(mid_ptrs, mid_vec, mask=offs_n < N) + + for k in range(K): + # --- inner: pure C (cube) --- + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) # (K,) + f = tl.load(f_ptr + k * stride_fk + offs_k * stride_fn, mask=offs_k < K, other=0.0) # (K,) + inner_cube = tl.dot(e[:, None], f[None, :]) # (K,K) + out_ptrs = out_ptr + offs_k[:, None] * stride_out_0 + offs_k[None, :] * stride_out_1 + tl.store(out_ptrs, inner_cube, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ---------------------------------------------------------------------------- +# SDF13-TC02: float32, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting alternating pure C / pure V under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf13_tc02_alternating_pure( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + out_outer_ptr, + out_mid_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fk, + stride_fn, + stride_out_0, + stride_out_1, + stride_outer_0, + stride_outer_1, + stride_mid, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_p = tl.arange(0, BLOCK_SIZE_P) + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for i in range(L): + # --- outer: pure C (cube) --- + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bn, mask=offs_l < L, other=0.0) + outer_cube = tl.dot(a[:, None], b[None, :]) + outer_ptrs = out_outer_ptr + offs_l[:, None] * stride_outer_0 + offs_l[None, :] * stride_outer_1 + tl.store(outer_ptrs, outer_cube, mask=(offs_l[:, None] < L) & (offs_l[None, :] < L)) + + for j in range(P): + # --- mid: pure V (vector) --- + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + mid_vec = c + d + mid_ptrs = out_mid_ptr + offs_n * stride_mid + tl.store(mid_ptrs, mid_vec, mask=offs_n < N) + + for k in range(K): + # --- inner: pure C (cube) --- + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + f = tl.load(f_ptr + k * stride_fk + offs_k * stride_fn, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(e[:, None], f[None, :]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out_0 + offs_k[None, :] * stride_out_1 + tl.store(out_ptrs, inner_cube, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf13_signature(dtype_str): + """Build the argument type signature for the SDF13 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "out_outer_ptr": f"*{dtype_str}", + "out_mid_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "stride_am": "i32", + "stride_al": "i32", + "stride_bl": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fk": "i32", + "stride_fn": "i32", + "stride_out_0": "i32", + "stride_out_1": "i32", + "stride_outer_0": "i32", + "stride_outer_1": "i32", + "stride_mid": "i32", + } + + +def test_sdf13_tc01(): + """SDF13-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf13_tc01_alternating_pure kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf13_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3, "BLOCK_SIZE_P": 2} + + mlir = compile_kernel(sdf13_tc01_alternating_pure, signature, constants) + _write_mlir_to_file(mlir, "sdf13_tc01_alternating_pure.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf13_tc01_alternating_pure(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf13_tc02(): + """SDF13-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf13_tc02_alternating_pure kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf13_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3, "BLOCK_SIZE_P": 2} + + mlir = compile_kernel(sdf13_tc02_alternating_pure, signature, constants) + _write_mlir_to_file(mlir, "sdf13_tc02_alternating_pure.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf13_tc02_alternating_pure(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf13_tc01() + test_sdf13_tc02() + print("All SDF13 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf14_mlir_three_layer_c2c_outer_dep_inner.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf14_mlir_three_layer_c2c_outer_dep_inner.py new file mode 100644 index 0000000000..5439df4f13 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf14_mlir_three_layer_c2c_outer_dep_inner.py @@ -0,0 +1,340 @@ +""" +Test Case: SDF14 - 3-layer nested loop, outer C depends on inner C (C2C cross-layer) + +[MLIR Validation] Refactored version + +Description: 3-layer nested (L, P, K). Inner loop accumulates inner_cube_acc (K-size). + Middle loop has independent CV. Outer C uses inner_cube_acc in tl.dot. + Outer V: h + 1.0. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF14-TC01: float16, M=128, N=64, K=32, L=3, P=2 + - SDF14-TC02: float32, M=128, N=64, K=32, L=3, P=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF14-TC01: float16, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting C2C cross-layer dependency under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf14_tc01_outer_c_dep_inner_c( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + out_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_gm, + stride_gk, + stride_h, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_p = tl.arange(0, BLOCK_SIZE_P) + + inner_cube_acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) + + for i in range(L): + for j in range(P): + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(a[:, None], b[None, :]) + inner_cube_acc = inner_cube_acc + inner_cube + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + inner_vec = c * d + + e = tl.load(e_ptr + j * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + j * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + mid_cube = tl.dot(e[:, None], f[None, :]) + + g = tl.load(g_ptr + i * stride_gm + offs_k * stride_gk, mask=offs_k < K, other=0.0) + g = g.to(tl.float32) + outer_cube = tl.dot(inner_cube_acc, g[:, None]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, outer_cube, mask=offs_k[:, None] < K) + + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + outer_vec = h + 1.0 + + +# ---------------------------------------------------------------------------- +# SDF14-TC02: float32, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting C2C cross-layer dependency under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf14_tc02_outer_c_dep_inner_c( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + out_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_gm, + stride_gk, + stride_h, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_p = tl.arange(0, BLOCK_SIZE_P) + + inner_cube_acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) + + for i in range(L): + for j in range(P): + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(a[:, None], b[None, :]) + inner_cube_acc = inner_cube_acc + inner_cube + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + inner_vec = c * d + + e = tl.load(e_ptr + j * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + j * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + mid_cube = tl.dot(e[:, None], f[None, :]) + + g = tl.load(g_ptr + i * stride_gm + offs_k * stride_gk, mask=offs_k < K, other=0.0) + outer_cube = tl.dot(inner_cube_acc, g[:, None]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, outer_cube, mask=offs_k[:, None] < K) + + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + outer_vec = h + 1.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf14_signature(dtype_str): + """Build the argument type signature for the SDF14 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ep": "i32", + "stride_fp": "i32", + "stride_fn": "i32", + "stride_gm": "i32", + "stride_gk": "i32", + "stride_h": "i32", + "stride_out": "i32", + } + + +def test_sdf14_tc01(): + """SDF14-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf14_tc01_outer_c_dep_inner_c kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf14_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3, "BLOCK_SIZE_P": 2} + + mlir = compile_kernel(sdf14_tc01_outer_c_dep_inner_c, signature, constants) + _write_mlir_to_file(mlir, "sdf14_tc01_outer_c_dep_inner_c.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf14_tc01_outer_c_dep_inner_c(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf14_tc02(): + """SDF14-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf14_tc02_outer_c_dep_inner_c kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf14_signature("fp32") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3, "BLOCK_SIZE_P": 2} + + mlir = compile_kernel(sdf14_tc02_outer_c_dep_inner_c, signature, constants) + _write_mlir_to_file(mlir, "sdf14_tc02_outer_c_dep_inner_c.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf14_tc02_outer_c_dep_inner_c(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf14_tc01() + test_sdf14_tc02() + print("All SDF14 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf15_mlir_three_layer_c2c_inner_dep_outer.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf15_mlir_three_layer_c2c_inner_dep_outer.py new file mode 100644 index 0000000000..f54a61aa71 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf15_mlir_three_layer_c2c_inner_dep_outer.py @@ -0,0 +1,318 @@ +""" +Test Case: SDF15 - 3-layer nested, inner C depends on outer C (C2C cross-layer, iter_args) + +[MLIR Validation] Refactored version + +Description: 3-layer nested (L, P, K). Outer loop has CV. Middle loop marks time. + Inner loop uses outer_cube in tl.dot with E. Accumulates in outer_cube_acc. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF15-TC01: float16, M=128, N=64, K=32, L=3, P=2 + - SDF15-TC02: float32, M=128, N=64, K=32, L=3, P=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF15-TC01: float16, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting, inner C depends on outer C under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf15_tc01_inner_c_dep_outer_c( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + L, + P, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_out, + K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for i in range(L): + a = tl.load(a_ptr + i * stride_am + offs_k * stride_al, mask=offs_k < L, other=0.0) + b = tl.load(b_ptr + i * stride_bl + offs_k * stride_bn, mask=offs_k < L, other=0.0) + outer_cube = tl.dot(a[:, None], b[None, :]) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + outer_vec = c - d + + for j in range(P): + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(outer_cube.to(tl.float16), e[:, None]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, inner_cube, mask=offs_k[:, None] < K) + + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + inner_vec = f * 2.0 + + +# ---------------------------------------------------------------------------- +# SDF15-TC02: float32, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting, inner C depends on outer C under float32 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf15_tc02_inner_c_dep_outer_c( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + L, + P, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_out, + K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for i in range(L): + a = tl.load(a_ptr + i * stride_am + offs_k * stride_al, mask=offs_k < L, other=0.0) + b = tl.load(b_ptr + i * stride_bl + offs_k * stride_bn, mask=offs_k < L, other=0.0) + outer_cube = tl.dot(a[:, None], b[None, :]) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + outer_vec = c - d + + for j in range(P): + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(outer_cube, e[:, None]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, inner_cube, mask=offs_k[:, None] < K) + + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + inner_vec = f * 2.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf15_signature(dtype_str): + """Build the argument type signature for the SDF15 kernel. + + Note: In SDF15, K is a constexpr parameter and does not appear in signature. + """ + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "L": "i32", + "P": "i32", + "stride_am": "i32", + "stride_al": "i32", + "stride_bl": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fn": "i32", + "stride_out": "i32", + } + + +def test_sdf15_tc01(): + """SDF15-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf15_tc01_inner_c_dep_outer_c kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf15_signature("fp16") + constants = { + "K": 32, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 3, + "BLOCK_SIZE_P": 2, + } + + mlir = compile_kernel(sdf15_tc01_inner_c_dep_outer_c, signature, constants) + _write_mlir_to_file(mlir, "sdf15_tc01_inner_c_dep_outer_c.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf15_tc01_inner_c_dep_outer_c(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +def test_sdf15_tc02(): + """SDF15-TC02: Verify float32 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf15_tc02_inner_c_dep_outer_c kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf15_signature("fp32") + constants = { + "K": 32, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 3, + "BLOCK_SIZE_P": 2, + } + + mlir = compile_kernel(sdf15_tc02_inner_c_dep_outer_c, signature, constants) + _write_mlir_to_file(mlir, "sdf15_tc02_inner_c_dep_outer_c.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf15_tc02_inner_c_dep_outer_c(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf15_tc01() + test_sdf15_tc02() + print("All SDF15 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf16_mlir_three_layer_v2c_inner_dep_outer.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf16_mlir_three_layer_v2c_inner_dep_outer.py new file mode 100644 index 0000000000..32c5bbde27 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf16_mlir_three_layer_v2c_inner_dep_outer.py @@ -0,0 +1,219 @@ +""" +Test Case: SDF16 - 3-layer nested, inner C depends on outer V (V2C cross-layer) + +[MLIR Validation] Refactored version + +Description: 3-layer nested (L, P, K). Outer V (tl.max) feeds into inner C via V2C. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF16-TC01: float16, M=128, N=64, K=32, L=3, P=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF16-TC01: float16, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting, inner C depends on outer V (V2C cross-layer) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf16_tc01_inner_c_dep_outer_v( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_al, + stride_bl, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_out, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_l = tl.arange(0, BLOCK_SIZE_L) + + for i in range(L): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + outer_vec = tl.max(c + d) + + a = tl.load(a_ptr + i * stride_am + offs_l * stride_al, mask=offs_l < L, other=0.0) + b = tl.load(b_ptr + i * stride_bl + offs_l * stride_bn, mask=offs_l < L, other=0.0) + outer_cube = tl.dot(a[:, None], b[None, :]) + + for j in range(P): + for k in range(K): + e = tl.load(e_ptr + k * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(e[:, None], outer_vec.to(tl.float16)[None, None]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, inner_cube, mask=offs_k[:, None] < K) + + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + inner_vec = f / 2.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf16_signature(dtype_str): + """Build the argument type signature for the SDF16 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "stride_am": "i32", + "stride_al": "i32", + "stride_bl": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fn": "i32", + "stride_out": "i32", + } + + +def test_sdf16_tc01(): + """SDF16-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf16_tc01_inner_c_dep_outer_v kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf16_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3, "BLOCK_SIZE_P": 2} + + mlir = compile_kernel(sdf16_tc01_inner_c_dep_outer_v, signature, constants) + _write_mlir_to_file(mlir, "sdf16_tc01_inner_c_dep_outer_v.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf16_tc01_inner_c_dep_outer_v(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf16_tc01() + print("All SDF16 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf17_mlir_three_layer_c2v_outer_dep_inner.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf17_mlir_three_layer_c2v_outer_dep_inner.py new file mode 100644 index 0000000000..026ca8f405 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf17_mlir_three_layer_c2v_outer_dep_inner.py @@ -0,0 +1,224 @@ +""" +Test Case: SDF17 - 3-layer nested, outer V depends on inner C (C2V cross-layer) + +[MLIR Validation] Refactored version + +Description: 3-layer nested (L, P, K). Inner loop accumulates inner_cube_acc (K-size). + Middle loop marks time. Outer V uses inner_cube_acc + E. Outer Cube: F. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF17-TC01: float16, M=128, N=64, K=32, L=3, P=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF17-TC01: float16, M=128, N=64, K=32, L=3, P=2 +# Test purpose: Verify MLIR generation of 3-layer nesting, outer V depends on inner C (C2V cross-layer) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf17_tc01_outer_v_dep_inner_c( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_e, + stride_fm, + stride_fl, + stride_outm, + stride_outn, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_l = tl.arange(0, BLOCK_SIZE_L) + + for i in range(L): + inner_cube_acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) + for j in range(P): + for k in range(K): + a = tl.load(a_ptr + k * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + k * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(a[:, None], b[None, :]) + inner_cube_acc = inner_cube_acc + inner_cube + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + inner_vec = tl.min(c + d) + + e = tl.load(e_ptr + offs_k * stride_e, mask=offs_k < K, other=0.0) + outer_vec = inner_cube_acc + e[None, :] + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_k[None, :] * stride_outn + out_mask = (offs_k[:, None] < K) & (offs_k[None, :] < K) + tl.store(out_ptrs, outer_vec, mask=out_mask) + + f = tl.load(f_ptr + i * stride_fm + offs_l * stride_fl, mask=offs_l < L, other=0.0) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf17_signature(dtype_str): + """Build the argument type signature for the SDF17 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_e": "i32", + "stride_fm": "i32", + "stride_fl": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_sdf17_tc01(): + """SDF17-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf17_tc01_outer_v_dep_inner_c kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf17_signature("fp16") + constants = {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32, "BLOCK_SIZE_L": 3, "BLOCK_SIZE_P": 2} + + mlir = compile_kernel(sdf17_tc01_outer_v_dep_inner_c, signature, constants) + _write_mlir_to_file(mlir, "sdf17_tc01_outer_v_dep_inner_c.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf17_tc01_outer_v_dep_inner_c(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf17_tc01() + print("All SDF17 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf18_mlir_four_layer_independent_cv.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf18_mlir_four_layer_independent_cv.py new file mode 100644 index 0000000000..15dc90c77c --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf18_mlir_four_layer_independent_cv.py @@ -0,0 +1,268 @@ +""" +Test Case: SDF18 - 4-layer nested loop, each layer has independent CV, no data dependency + +[MLIR Validation] Refactored version + +Description: 4-layer nested (Q, L, P, K). Each layer has independent Cube and Vector. + No data dependency across layers. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF18-TC01: float16, M=128, N=64, K=32, L=3, P=2, Q=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF18-TC01: float16, M=128, N=64, K=32, L=3, P=2, Q=2 +# Test purpose: Verify MLIR generation of 4-layer nesting independent CV operations under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf18_tc01_four_layer_independent( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + i_ptr, + j_ptr, + out1_ptr, + out2_ptr, + M, + N, + K, + L, + P, + Q, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_g, + stride_h, + stride_im, + stride_il, + stride_jl, + stride_jn, + stride_out1_0, + stride_out1_1, + stride_out2, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, +): + pid = tl.program_id(0) + + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_p = tl.arange(0, BLOCK_SIZE_P) + offs_q = tl.arange(0, BLOCK_SIZE_Q) + + for l1 in range(Q): + for l2 in range(L): + for l3 in range(P): + for l4 in range(K): + a = tl.load(a_ptr + l4 * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + l4 * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(a[:, None], b[None, :]) + + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + inner_vec = c + d + out2_ptrs = out2_ptr + offs_n * stride_out2 + tl.store(out2_ptrs, inner_vec, mask=offs_n < N) + + e = tl.load(e_ptr + l3 * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + l3 * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + l3_cube = tl.dot(e[:, None], f[None, :]) + + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + l3_vec = g - h + + i_a = tl.load(i_ptr + l2 * stride_im + offs_l * stride_il, mask=offs_l < L, other=0.0) + i_b = tl.load(j_ptr + l2 * stride_jl + offs_l * stride_jn, mask=offs_l < L, other=0.0) + l2_cube = tl.dot(i_a[:, None], i_b[None, :]) + out1_ptrs = out1_ptr + offs_l[:, None] * stride_out1_0 + offs_l[None, :] * stride_out1_1 + tl.store(out1_ptrs, l2_cube, mask=(offs_l[:, None] < L) & (offs_l[None, :] < L)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf18_signature(dtype_str): + """Build the argument type signature for the SDF18 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "i_ptr": f"*{dtype_str}", + "j_ptr": f"*{dtype_str}", + "out1_ptr": f"*{dtype_str}", + "out2_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ep": "i32", + "stride_fp": "i32", + "stride_fn": "i32", + "stride_g": "i32", + "stride_h": "i32", + "stride_im": "i32", + "stride_il": "i32", + "stride_jl": "i32", + "stride_jn": "i32", + "stride_out1_0": "i32", + "stride_out1_1": "i32", + "stride_out2": "i32", + } + + +def test_sdf18_tc01(): + """SDF18-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf18_tc01_four_layer_independent kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf18_signature("fp16") + constants = { + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 3, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + } + + mlir = compile_kernel(sdf18_tc01_four_layer_independent, signature, constants) + _write_mlir_to_file(mlir, "sdf18_tc01_four_layer_independent.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf18_tc01_four_layer_independent(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf18_tc01() + print("All SDF18 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf19_mlir_four_layer_alternating_pure.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf19_mlir_four_layer_alternating_pure.py new file mode 100644 index 0000000000..8ac26ff2ee --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf19_mlir_four_layer_alternating_pure.py @@ -0,0 +1,269 @@ +""" +Test Case: SDF19 - 4-layer nested, alternating pure C or pure V, no data dependency + +[MLIR Validation] Refactored version + +Description: Q-layer pure C, L-layer pure V, P-layer pure C, K-layer pure V. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: + - SDF19-TC01: float16, M=128, N=64, K=32, L=3, P=2, Q=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF19-TC01: float16, M=128, N=64, K=32, L=3, P=2, Q=2 +# Test purpose: Verify MLIR generation of 4-layer nesting alternating pure C / pure V under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf19_tc01_alternating_pure( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + out_ptr, + out_l1_ptr, + out_l2_ptr, + out_l3_ptr, + M, + N, + K, + L, + P, + Q, + stride_am, + stride_aq, + stride_bq, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_g, + stride_h, + stride_out, + stride_l1m, + stride_l1n, + stride_l2, + stride_l3m, + stride_l3n, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, +): + pid = tl.program_id(0) + + offs_q = tl.arange(0, BLOCK_SIZE_Q) + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_p = tl.arange(0, BLOCK_SIZE_P) + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + + for l1 in range(Q): + # --- l1: pure C (cube) --- + a = tl.load(a_ptr + l1 * stride_am + offs_q * stride_aq, mask=offs_q < Q, other=0.0) + b = tl.load(b_ptr + l1 * stride_bq + offs_q * stride_bn, mask=offs_q < Q, other=0.0) + l1_cube = tl.dot(a[:, None], b[None, :]) + l1_ptrs = out_l1_ptr + offs_q[:, None] * stride_l1m + offs_q[None, :] * stride_l1n + tl.store(l1_ptrs, l1_cube, mask=(offs_q[:, None] < Q) & (offs_q[None, :] < Q)) + + for l2 in range(L): + # --- l2: pure V (vector) --- + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + l2_vec = c + d + l2_ptrs = out_l2_ptr + offs_n * stride_l2 + tl.store(l2_ptrs, l2_vec, mask=offs_n < N) + + for l3 in range(P): + # --- l3: pure C (cube) --- + e = tl.load(e_ptr + l3 * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + l3 * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + l3_cube = tl.dot(e[:, None], f[None, :]) + l3_ptrs = out_l3_ptr + offs_p[:, None] * stride_l3m + offs_p[None, :] * stride_l3n + tl.store(l3_ptrs, l3_cube, mask=(offs_p[:, None] < P) & (offs_p[None, :] < P)) + + for l4 in range(K): + # --- l4: pure V (vector) --- + g = tl.load(g_ptr + offs_n * stride_g, mask=offs_n < N, other=0.0) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + l4_vec = g - h + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, l4_vec, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf19_signature(dtype_str): + """Build the argument type signature for the SDF19 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "out_l1_ptr": f"*{dtype_str}", + "out_l2_ptr": f"*{dtype_str}", + "out_l3_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "stride_am": "i32", + "stride_aq": "i32", + "stride_bq": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ep": "i32", + "stride_fp": "i32", + "stride_fn": "i32", + "stride_g": "i32", + "stride_h": "i32", + "stride_out": "i32", + "stride_l1m": "i32", + "stride_l1n": "i32", + "stride_l2": "i32", + "stride_l3m": "i32", + "stride_l3n": "i32", + } + + +def test_sdf19_tc01(): + """SDF19-TC01: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf19_tc01_alternating_pure kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf19_signature("fp16") + constants = { + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 3, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + } + + mlir = compile_kernel(sdf19_tc01_alternating_pure, signature, constants) + _write_mlir_to_file(mlir, "sdf19_tc01_alternating_pure.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf19_tc01_alternating_pure(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf19_tc01() + print("All SDF19 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf20_mlir_four_layer_c2c_outer_dep_inner.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf20_mlir_four_layer_c2c_outer_dep_inner.py new file mode 100644 index 0000000000..5ad8294ee1 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf20_mlir_four_layer_c2c_outer_dep_inner.py @@ -0,0 +1,202 @@ +""" +Test Case: SDF20 - 4-layer nested, outer C depends on inner C (C2C cross-layer) + +[MLIR Validation] Refactored version + +Description: 4-layer nested (Q, L, P, K). Inner loop accumulates, outer uses. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable + +Test Cases: SDF20-TC01: float16, M=128, N=64, K=32, L=3, P=2, Q=2 +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +def compile_kernel(kernel, signature, constants): + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +@triton.jit +def sdf20_tc01( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + l_ptr, + m_ptr, + g_ptr, + h_ptr, + out_ptr, + M, + N, + K, + L, + P, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_lm, + stride_ll, + stride_mm, + stride_ml, + stride_gm, + stride_gq, + stride_h, + stride_out, + Q: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, +): + pid = tl.program_id(0) + offs_k = tl.arange(0, BLOCK_SIZE_K) + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_q = tl.arange(0, BLOCK_SIZE_Q) + offs_p = tl.arange(0, BLOCK_SIZE_P) + offs_l = tl.arange(0, BLOCK_SIZE_L) + inner_cube_acc = tl.zeros([BLOCK_SIZE_Q, BLOCK_SIZE_Q], tl.float32) + for l1 in range(Q): + for l2 in range(L): + for l3 in range(P): + for l4 in range(K): + a = tl.load(a_ptr + l4 * stride_am + offs_q * stride_ak, mask=offs_q < Q, other=0.0) + b = tl.load(b_ptr + l4 * stride_bk + offs_q * stride_bn, mask=offs_q < Q, other=0.0) + inner_cube = tl.dot(a[:, None], b[None, :]) + inner_cube_acc = inner_cube_acc + inner_cube + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + inner_vec = c * d + e = tl.load(e_ptr + l3 * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + l3 * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + mid_cube_p = tl.dot(e[:, None], f[None, :]) + l_ptr_val = tl.load(l_ptr + l2 * stride_lm + offs_l * stride_ll, mask=offs_l < L, other=0.0) + m_ptr_val = tl.load(m_ptr + l2 * stride_mm + offs_l * stride_ml, mask=offs_l < L, other=0.0) + mid_cube_l = tl.dot(l_ptr_val[:, None], m_ptr_val[None, :]) + g = tl.load(g_ptr + l1 * stride_gm + offs_q * stride_gq, mask=offs_q < Q, other=0.0).to(tl.float32) + l1_cube = tl.dot(inner_cube_acc, g[:, None]) + out_ptrs = out_ptr + offs_q[:, None] * stride_out + tl.store(out_ptrs, l1_cube, mask=offs_q[:, None] < Q) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + l1_vec = h + 1.0 + + +def _build_sdf20_signature(dtype_str): + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "l_ptr": f"*{dtype_str}", + "m_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ep": "i32", + "stride_fp": "i32", + "stride_fn": "i32", + "stride_lm": "i32", + "stride_ll": "i32", + "stride_mm": "i32", + "stride_ml": "i32", + "stride_gm": "i32", + "stride_gq": "i32", + "stride_h": "i32", + "stride_out": "i32", + } + + +def test_sdf20_tc01(): + signature = _build_sdf20_signature("fp16") + constants = { + "Q": 2, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 3, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + } + + mlir = compile_kernel(sdf20_tc01, signature, constants) + _write_mlir_to_file(mlir, "sdf20_tc01.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf20_tc01(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + +if __name__ == "__main__": + test_sdf20_tc01() + print("All SDF20 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf21_mlir_four_layer_c2c_inner_dep_outer.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf21_mlir_four_layer_c2c_inner_dep_outer.py new file mode 100644 index 0000000000..f5c05d3a0f --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf21_mlir_four_layer_c2c_inner_dep_outer.py @@ -0,0 +1,222 @@ +""" +Test Case: SDF21 - 4-layer nested, inner C depends on outer C (C2C cross-layer) + +[MLIR Validation] Refactored version + +Description: 4-layer nested (Q, L, P, K). Inner loop uses l1_cube from outer in tl.dot. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF21: 4-layer nested, inner C depends on outer C +# Test purpose: Verify MLIR generation of 4-layer nesting, inner C depends on outer C under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf21( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + L, + P, + Q, + stride_am, + stride_aq, + stride_bq, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_out, + K: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, +): + pid = tl.program_id(0) + offs_q, offs_k, offs_n = tl.arange(0, BLOCK_SIZE_Q), tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N) + for l1 in range(Q): + a = tl.load(a_ptr + l1 * stride_am + offs_k * stride_aq, mask=offs_k < Q, other=0.0) + b = tl.load(b_ptr + l1 * stride_bq + offs_k * stride_bn, mask=offs_k < Q, other=0.0) + l1_cube = tl.dot(a[:, None], b[None, :]) + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + l1_vec = c - d + for l2 in range(L): + for l3 in range(P): + for l4 in range(K): + e = tl.load(e_ptr + l4 * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + inner_cube = tl.dot(l1_cube.to(tl.float16), e[:, None]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, inner_cube, mask=offs_k[:, None] < K) + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + inner_vec = f * 2.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf21_signature(dtype_str): + """Build the argument type signature for the SDF21 kernel. + + Note: In SDF21, K is a constexpr parameter and does not appear in signature. + """ + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "stride_am": "i32", + "stride_aq": "i32", + "stride_bq": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fn": "i32", + "stride_out": "i32", + } + + +def test_sdf21(): + """SDF21: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf21 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf21_signature("fp16") + constants = { + "K": 32, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 3, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_N": 64, + } + + mlir = compile_kernel(sdf21, signature, constants) + _write_mlir_to_file(mlir, "sdf21.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf21(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf21() + print("All SDF21 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf22_mlir_four_layer_v2c_inner_dep_outer.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf22_mlir_four_layer_v2c_inner_dep_outer.py new file mode 100644 index 0000000000..08c8ba0cd6 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf22_mlir_four_layer_v2c_inner_dep_outer.py @@ -0,0 +1,227 @@ +""" +Test Case: SDF22 - 4-layer nested, inner C depends on outer V (V2C cross-layer) + +[MLIR Validation] Refactored version + +Description: 4-layer nested (Q, L, P, K). Outer V (tl.max) feeds into inner C via V2C. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF22: 4-layer nested, inner C depends on outer V +# Test purpose: Verify MLIR generation of 4-layer nesting, inner C depends on outer V (V2C cross-layer) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf22( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + out_cube_ptr, + M, + N, + K, + L, + P, + Q, + stride_am, + stride_aq, + stride_bq, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_out, + stride_out_cubem, + stride_out_cuben, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + offs_k, offs_n, offs_q = tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N), tl.arange(0, BLOCK_SIZE_Q) + for l1 in range(Q): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + l1_vec = tl.max(c + d) + a = tl.load(a_ptr + l1 * stride_am + offs_q * stride_aq, mask=offs_q < Q, other=0.0) + b = tl.load(b_ptr + l1 * stride_bq + offs_q * stride_bn, mask=offs_q < Q, other=0.0) + l1_cube = tl.dot(a[:, None], b[None, :]) + cube_ptrs = out_cube_ptr + offs_q[:, None] * stride_out_cubem + offs_q[None, :] * stride_out_cuben + tl.store(cube_ptrs, l1_cube, mask=(offs_q[:, None] < Q) & (offs_q[None, :] < Q)) + for l2 in range(L): + for l3 in range(P): + for l4 in range(K): + e = tl.load(e_ptr + l4 * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + inner_cube = e[:, None] * l1_vec + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, inner_cube, mask=offs_k[:, None] < K) + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + inner_vec = f / 2.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf22_signature(dtype_str): + """Build the argument type signature for the SDF22 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "out_cube_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "stride_am": "i32", + "stride_aq": "i32", + "stride_bq": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fn": "i32", + "stride_out": "i32", + "stride_out_cubem": "i32", + "stride_out_cuben": "i32", + } + + +def test_sdf22(): + """SDF22: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf22 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf22_signature("fp16") + constants = { + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_L": 3, + "BLOCK_SIZE_P": 2, + } + + mlir = compile_kernel(sdf22, signature, constants) + _write_mlir_to_file(mlir, "sdf22.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf22(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf22() + print("All SDF22 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf23_mlir_four_layer_c2v_outer_dep_inner.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf23_mlir_four_layer_c2v_outer_dep_inner.py new file mode 100644 index 0000000000..331da62307 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf23_mlir_four_layer_c2v_outer_dep_inner.py @@ -0,0 +1,221 @@ +""" +Test Case: SDF23 - 4-layer nested, outer V depends on inner C (C2V cross-layer) + +[MLIR Validation] Refactored version + +Description: 4-layer nested (Q, L, P, K). Inner loop accumulates, outer V uses acc + e. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF23: 4-layer nested, outer V depends on inner C +# Test purpose: Verify MLIR generation of 4-layer nesting, outer V depends on inner C accumulation (C2V cross-layer) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf23( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + P, + Q, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_e, + stride_fm, + stride_fq, + stride_outm, + stride_outn, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, +): + pid = tl.program_id(0) + offs_k, offs_n, offs_q = tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N), tl.arange(0, BLOCK_SIZE_Q) + for l1 in range(Q): + acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) + for l2 in range(L): + for l3 in range(P): + for l4 in range(K): + a = tl.load(a_ptr + l4 * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + l4 * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + ic = tl.dot(a[:, None], b[None, :]) + acc = acc + ic + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + iv = tl.min(c + d) + e = tl.load(e_ptr + offs_k * stride_e, mask=offs_k < K, other=0.0) + l1_vec = acc + e[None, :] + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_k[None, :] * stride_outn + tl.store(out_ptrs, l1_vec, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf23_signature(dtype_str): + """Build the argument type signature for the SDF23 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_e": "i32", + "stride_fm": "i32", + "stride_fq": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_sdf23(): + """SDF23: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf23 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf23_signature("fp16") + constants = { + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_L": 3, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + } + + mlir = compile_kernel(sdf23, signature, constants) + _write_mlir_to_file(mlir, "sdf23.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf23(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf23() + print("All SDF23 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf24_mlir_five_layer_independent_cv.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf24_mlir_five_layer_independent_cv.py new file mode 100644 index 0000000000..8ffc895414 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf24_mlir_five_layer_independent_cv.py @@ -0,0 +1,244 @@ +""" +Test Case: SDF24 - 5-layer independent CV + +[MLIR Validation] Refactored version + +Description: 5-layer nested (R, Q, L, P, K). Each layer has independent CV operations. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF24: 5-layer independent CV +# Test purpose: Verify MLIR generation of 5-layer nesting independent CV operations under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf24( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + l5_cube_out_ptr, + l4_cube_out_ptr, + M, + N, + K, + L, + P, + Q, + R, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_out, + stride_l5_cube_row, + stride_l5_cube_col, + stride_l4_cube_row, + stride_l4_cube_col, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_R: tl.constexpr, +): + pid = tl.program_id(0) + offs_k, offs_n = tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N) + offs_p = tl.arange(0, BLOCK_SIZE_P) + for l1 in range(R): + for l2 in range(Q): + for l3 in range(L): + for l4 in range(P): + for l5 in range(K): + a = tl.load(a_ptr + l5 * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + l5 * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + l5_cube = tl.dot(a[:, None], b[None, :]) + l5_cube_ptrs = l5_cube_out_ptr + offs_k[:, None] * stride_l5_cube_row + offs_k[ + None, :] * stride_l5_cube_col + tl.store(l5_cube_ptrs, l5_cube, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + l5_vec = c + d + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, l5_vec, mask=offs_n < N) + e = tl.load(e_ptr + l4 * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + l4 * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + l4_cube = tl.dot(e[:, None], f[None, :]) + l4_cube_ptrs = l4_cube_out_ptr + offs_p[:, None] * stride_l4_cube_row + offs_p[ + None, :] * stride_l4_cube_col + tl.store(l4_cube_ptrs, l4_cube, mask=(offs_p[:, None] < P) & (offs_p[None, :] < P)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf24_signature(dtype_str): + """Build the argument type signature for the SDF24 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "l5_cube_out_ptr": f"*{dtype_str}", + "l4_cube_out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "R": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ep": "i32", + "stride_fp": "i32", + "stride_fn": "i32", + "stride_out": "i32", + "stride_l5_cube_row": "i32", + "stride_l5_cube_col": "i32", + "stride_l4_cube_row": "i32", + "stride_l4_cube_col": "i32", + } + + +def test_sdf24(): + """SDF24: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf24 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf24_signature("fp16") + constants = { + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 2, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_R": 2, + } + + mlir = compile_kernel(sdf24, signature, constants) + _write_mlir_to_file(mlir, "sdf24.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf24(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf24() + print("All SDF24 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf25_mlir_five_layer_alternating_pure.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf25_mlir_five_layer_alternating_pure.py new file mode 100644 index 0000000000..ae9cd591ff --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf25_mlir_five_layer_alternating_pure.py @@ -0,0 +1,292 @@ +""" +Test Case: SDF25 - 5-layer alternating pure C or pure V + +[MLIR Validation] Refactored version + +Description: 5-layer nested (R, Q, L, P, K). Layers alternate pure C and pure V. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF25: 5-layer alternating pure C or pure V +# Test purpose: Verify MLIR generation of 5-layer nesting alternating pure C / pure V under float16 +# l1(C): tl.dot(a,b) -> store out_l1 [R,R] +# l2(V): c + d -> store out_l2 [N] +# l3(C): tl.dot(g,h) -> store out_l3 [L,L] +# l4(V): i + j -> store out_l4 [P] +# l5(C): tl.dot(e,f) -> store out [K,K] +# ---------------------------------------------------------------------------- +@triton.jit +def sdf25( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + i_ptr, + j_ptr, + out_ptr, + out_l1_ptr, + out_l2_ptr, + out_l3_ptr, + out_l4_ptr, + M, + N, + K, + L, + P, + Q, + R, + stride_am, + stride_ar, + stride_br, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fk, + stride_fn, + stride_gm, + stride_gl, + stride_hl, + stride_hn, + stride_i, + stride_j, + stride_outm, + stride_outn, + stride_l1m, + stride_l1n, + stride_l2, + stride_l3m, + stride_l3n, + stride_l4, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_R: tl.constexpr, +): + pid = tl.program_id(0) + offs_r, offs_q, offs_l, offs_p, offs_k, offs_n = tl.arange(0, BLOCK_SIZE_R), tl.arange(0, BLOCK_SIZE_Q), tl.arange( + 0, BLOCK_SIZE_L), tl.arange(0, BLOCK_SIZE_P), tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N) + for l1 in range(R): + # --- l1: pure C (cube) --- + a = tl.load(a_ptr + l1 * stride_am + offs_r * stride_ar, mask=offs_r < R, other=0.0) + b = tl.load(b_ptr + l1 * stride_br + offs_r * stride_bn, mask=offs_r < R, other=0.0) + l1_cube = tl.dot(a[:, None], b[None, :]) + l1_ptrs = out_l1_ptr + offs_r[:, None] * stride_l1m + offs_r[None, :] * stride_l1n + tl.store(l1_ptrs, l1_cube, mask=(offs_r[:, None] < R) & (offs_r[None, :] < R)) + for l2 in range(Q): + # --- l2: pure V (vector) --- + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + l2_vec = c + d + l2_ptrs = out_l2_ptr + offs_n * stride_l2 + tl.store(l2_ptrs, l2_vec, mask=offs_n < N) + for l3 in range(L): + # --- l3: pure C (cube) --- + g = tl.load(g_ptr + l3 * stride_gm + offs_l * stride_gl, mask=offs_l < L, other=0.0) + h = tl.load(h_ptr + l3 * stride_hl + offs_l * stride_hn, mask=offs_l < L, other=0.0) + l3_cube = tl.dot(g[:, None], h[None, :]) + l3_ptrs = out_l3_ptr + offs_l[:, None] * stride_l3m + offs_l[None, :] * stride_l3n + tl.store(l3_ptrs, l3_cube, mask=(offs_l[:, None] < L) & (offs_l[None, :] < L)) + for l4 in range(P): + # --- l4: pure V (vector) --- + i = tl.load(i_ptr + offs_p * stride_i, mask=offs_p < P, other=0.0) + j = tl.load(j_ptr + offs_p * stride_j, mask=offs_p < P, other=0.0) + l4_vec = i + j + l4_ptrs = out_l4_ptr + offs_p * stride_l4 + tl.store(l4_ptrs, l4_vec, mask=offs_p < P) + for l5 in range(K): + # --- l5: pure C (cube) --- + e = tl.load(e_ptr + l5 * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + f = tl.load(f_ptr + l5 * stride_fk + offs_k * stride_fn, mask=offs_k < K, other=0.0) + l5_cube = tl.dot(e[:, None], f[None, :]) + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_k[None, :] * stride_outn + tl.store(out_ptrs, l5_cube, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf25_signature(dtype_str): + """Build the argument type signature for the SDF25 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "i_ptr": f"*{dtype_str}", + "j_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "out_l1_ptr": f"*{dtype_str}", + "out_l2_ptr": f"*{dtype_str}", + "out_l3_ptr": f"*{dtype_str}", + "out_l4_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "R": "i32", + "stride_am": "i32", + "stride_ar": "i32", + "stride_br": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fk": "i32", + "stride_fn": "i32", + "stride_gm": "i32", + "stride_gl": "i32", + "stride_hl": "i32", + "stride_hn": "i32", + "stride_i": "i32", + "stride_j": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + "stride_l1m": "i32", + "stride_l1n": "i32", + "stride_l2": "i32", + "stride_l3m": "i32", + "stride_l3n": "i32", + "stride_l4": "i32", + } + + +def test_sdf25(): + """SDF25: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf25 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf25_signature("fp16") + constants = { + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 2, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_R": 2, + } + + mlir = compile_kernel(sdf25, signature, constants) + _write_mlir_to_file(mlir, "sdf25.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf25(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf25() + print("All SDF25 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf26_mlir_five_layer_c2c_outer_dep_inner.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf26_mlir_five_layer_c2c_outer_dep_inner.py new file mode 100644 index 0000000000..05266a6351 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf26_mlir_five_layer_c2c_outer_dep_inner.py @@ -0,0 +1,195 @@ +""" +Test Case: SDF26 - 5-layer outer C depends inner C + +[MLIR Validation] Refactored version + +Description: 5-layer nested (R, Q, L, P, K). Inner loop accumulates, outer uses. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +def compile_kernel(kernel, signature, constants): + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = {**options.__dict__} + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +@triton.jit +def sdf26( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + g_ptr, + h_ptr, + out_ptr, + M, + N, + K, + L, + P, + Q, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ep, + stride_fp, + stride_fn, + stride_gm, + stride_gr, + stride_h, + stride_out, + R: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_R: tl.constexpr, +): + pid = tl.program_id(0) + offs_k, offs_n, offs_r = tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N), tl.arange(0, BLOCK_SIZE_R) + offs_p = tl.arange(0, BLOCK_SIZE_P) + offs_l = tl.arange(0, BLOCK_SIZE_L) + offs_q = tl.arange(0, BLOCK_SIZE_Q) + acc = tl.zeros([BLOCK_SIZE_R, BLOCK_SIZE_R], tl.float32) + for l1 in range(R): + for l2 in range(Q): + for l3 in range(L): + for l4 in range(P): + for l5 in range(K): + a = tl.load(a_ptr + l5 * stride_am + offs_r * stride_ak, mask=offs_r < R, other=0.0) + b = tl.load(b_ptr + l5 * stride_bk + offs_r * stride_bn, mask=offs_r < R, other=0.0) + ic = tl.dot(a[:, None], b[None, :]) + acc = acc + ic + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + iv = c * d + e = tl.load(e_ptr + l4 * stride_em + offs_p * stride_ep, mask=offs_p < P, other=0.0) + f = tl.load(f_ptr + l4 * stride_fp + offs_p * stride_fn, mask=offs_p < P, other=0.0) + mc_p = tl.dot(e[:, None], f[None, :]) + e = tl.load(e_ptr + l3 * stride_em + offs_l * stride_ep, mask=offs_l < L, other=0.0) + f = tl.load(f_ptr + l3 * stride_fp + offs_l * stride_fn, mask=offs_l < L, other=0.0) + mc_l = tl.dot(e[:, None], f[None, :]) + e = tl.load(e_ptr + l2 * stride_em + offs_q * stride_ep, mask=offs_q < Q, other=0.0) + f = tl.load(f_ptr + l2 * stride_fp + offs_q * stride_fn, mask=offs_q < Q, other=0.0) + mc_q = tl.dot(e[:, None], f[None, :]) + g = tl.load(g_ptr + l1 * stride_gm + offs_r * stride_gr, mask=offs_r < R, other=0.0).to(tl.float32) + l1c = tl.dot(acc, g[:, None]) + out_ptrs = out_ptr + offs_r[:, None] * stride_out + tl.store(out_ptrs, l1c, mask=offs_r[:, None] < R) + h = tl.load(h_ptr + offs_n * stride_h, mask=offs_n < N, other=0.0) + l1v = h + 1.0 + + +def _build_sdf26_signature(dtype_str): + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "g_ptr": f"*{dtype_str}", + "h_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ep": "i32", + "stride_fp": "i32", + "stride_fn": "i32", + "stride_gm": "i32", + "stride_gr": "i32", + "stride_h": "i32", + "stride_out": "i32", + } + + +def test_sdf26(): + signature = _build_sdf26_signature("fp16") + constants = { + "R": 2, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_L": 2, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_R": 2, + } + + mlir = compile_kernel(sdf26, signature, constants) + _write_mlir_to_file(mlir, "sdf26.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf26(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + +if __name__ == "__main__": + test_sdf26() + print("All SDF26 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf27_mlir_five_layer_c2c_inner_dep_outer.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf27_mlir_five_layer_c2c_inner_dep_outer.py new file mode 100644 index 0000000000..2a82116232 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf27_mlir_five_layer_c2c_inner_dep_outer.py @@ -0,0 +1,227 @@ +""" +Test Case: SDF27 - 5-layer inner C depends outer C + +[MLIR Validation] Refactored version + +Description: 5-layer nested (R, Q, L, P, K). Inner loop uses l1c from outer in tl.dot. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF27: 5-layer inner C depends outer C +# Test purpose: Verify MLIR generation of 5-layer nesting, inner C depends on outer C under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf27( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + L, + P, + Q, + R, + stride_am, + stride_ar, + stride_br, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_out, + K: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_R: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + offs_r, offs_k, offs_n = tl.arange(0, BLOCK_SIZE_R), tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N) + for l1 in range(R): + a = tl.load(a_ptr + l1 * stride_am + offs_k * stride_ar, mask=offs_k < R, other=0.0) + b = tl.load(b_ptr + l1 * stride_br + offs_k * stride_bn, mask=offs_k < R, other=0.0) + l1c = tl.dot(a[:, None], b[None, :]) + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + l1v = c - d + for l2 in range(Q): + for l3 in range(L): + for l4 in range(P): + for l5 in range(K): + e = tl.load(e_ptr + l5 * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + ic = tl.dot(l1c.to(tl.float16), e[:, None]) + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, ic, mask=offs_k[:, None] < K) + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + iv = f * 2.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf27_signature(dtype_str): + """Build the argument type signature for the SDF27 kernel. + + Note: In SDF27, K is a constexpr parameter and does not appear in signature. + """ + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "R": "i32", + "stride_am": "i32", + "stride_ar": "i32", + "stride_br": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fn": "i32", + "stride_out": "i32", + } + + +def test_sdf27(): + """SDF27: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf27 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf27_signature("fp16") + constants = { + "K": 32, + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_R": 2, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_L": 2, + "BLOCK_SIZE_P": 2, + } + + mlir = compile_kernel(sdf27, signature, constants) + _write_mlir_to_file(mlir, "sdf27.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf27(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" in mlir, "MLIR code does not contain the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf27() + print("All SDF27 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf28_mlir_five_layer_v2c_inner_dep_outer.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf28_mlir_five_layer_v2c_inner_dep_outer.py new file mode 100644 index 0000000000..a0e4dedd08 --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf28_mlir_five_layer_v2c_inner_dep_outer.py @@ -0,0 +1,232 @@ +""" +Test Case: SDF28 - 5-layer inner C depends outer V + +[MLIR Validation] Refactored version + +Description: 5-layer nested (R, Q, L, P, K). Outer V (tl.max) feeds into inner C via V2C. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF28: 5-layer inner C depends outer V +# Test purpose: Verify MLIR generation of 5-layer nesting, inner C depends on outer V (V2C cross-layer) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf28( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + out_cube_ptr, + M, + N, + K, + L, + P, + Q, + R, + stride_am, + stride_ar, + stride_br, + stride_bn, + stride_c, + stride_d, + stride_em, + stride_ek, + stride_fn, + stride_out, + stride_out_cubem, + stride_out_cuben, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_R: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + offs_k, offs_n, offs_r = tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N), tl.arange(0, BLOCK_SIZE_R) + for l1 in range(R): + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + l1v = tl.max(c + d) + a = tl.load(a_ptr + l1 * stride_am + offs_r * stride_ar, mask=offs_r < R, other=0.0) + b = tl.load(b_ptr + l1 * stride_br + offs_r * stride_bn, mask=offs_r < R, other=0.0) + l1c = tl.dot(a[:, None], b[None, :]) + cube_ptrs = out_cube_ptr + offs_r[:, None] * stride_out_cubem + offs_r[None, :] * stride_out_cuben + tl.store(cube_ptrs, l1c, mask=(offs_r[:, None] < R) & (offs_r[None, :] < R)) + for l2 in range(Q): + for l3 in range(L): + for l4 in range(P): + for l5 in range(K): + e = tl.load(e_ptr + l5 * stride_em + offs_k * stride_ek, mask=offs_k < K, other=0.0) + ic = e[:, None] * l1v + out_ptrs = out_ptr + offs_k[:, None] * stride_out + tl.store(out_ptrs, ic, mask=offs_k[:, None] < K) + f = tl.load(f_ptr + offs_n * stride_fn, mask=offs_n < N, other=0.0) + iv = f / 2.0 + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf28_signature(dtype_str): + """Build the argument type signature for the SDF28 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "out_cube_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "R": "i32", + "stride_am": "i32", + "stride_ar": "i32", + "stride_br": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_em": "i32", + "stride_ek": "i32", + "stride_fn": "i32", + "stride_out": "i32", + "stride_out_cubem": "i32", + "stride_out_cuben": "i32", + } + + +def test_sdf28(): + """SDF28: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf28 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf28_signature("fp16") + constants = { + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_R": 2, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_L": 2, + "BLOCK_SIZE_P": 2, + } + + mlir = compile_kernel(sdf28, signature, constants) + _write_mlir_to_file(mlir, "sdf28.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf28(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf28() + print("All SDF28 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf29_mlir_five_layer_c2v_outer_dep_inner.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf29_mlir_five_layer_c2v_outer_dep_inner.py new file mode 100644 index 0000000000..df0bcb799f --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf29_mlir_five_layer_c2v_outer_dep_inner.py @@ -0,0 +1,226 @@ +""" +Test Case: SDF29 - 5-layer outer V depends inner C + +[MLIR Validation] Refactored version + +Description: 5-layer nested (R, Q, L, P, K). Inner loop accumulates, outer V uses acc + e. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF29: 5-layer outer V depends inner C +# Test purpose: Verify MLIR generation of 5-layer nesting, outer V depends on inner C accumulation (C2V cross-layer) under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf29( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + e_ptr, + f_ptr, + out_ptr, + M, + N, + K, + L, + P, + Q, + R, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_e, + stride_fm, + stride_fr, + stride_outm, + stride_outn, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_R: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, +): + pid = tl.program_id(0) + offs_k, offs_n, offs_r = tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N), tl.arange(0, BLOCK_SIZE_R) + for l1 in range(R): + acc = tl.zeros([BLOCK_SIZE_K, BLOCK_SIZE_K], tl.float32) + for l2 in range(Q): + for l3 in range(L): + for l4 in range(P): + for l5 in range(K): + a = tl.load(a_ptr + l5 * stride_am + offs_k * stride_ak, mask=offs_k < K, other=0.0) + b = tl.load(b_ptr + l5 * stride_bk + offs_k * stride_bn, mask=offs_k < K, other=0.0) + ic = tl.dot(a[:, None], b[None, :]) + acc = acc + ic + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + iv = tl.min(c + d) + e = tl.load(e_ptr + offs_k * stride_e, mask=offs_k < K, other=0.0) + l1v = acc + e[None, :] + out_ptrs = out_ptr + offs_k[:, None] * stride_outm + offs_k[None, :] * stride_outn + tl.store(out_ptrs, l1v, mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf29_signature(dtype_str): + """Build the argument type signature for the SDF29 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "e_ptr": f"*{dtype_str}", + "f_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "R": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_e": "i32", + "stride_fm": "i32", + "stride_fr": "i32", + "stride_outm": "i32", + "stride_outn": "i32", + } + + +def test_sdf29(): + """SDF29: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf29 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf29_signature("fp16") + constants = { + "BLOCK_SIZE_K": 32, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_R": 2, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_L": 2, + "BLOCK_SIZE_P": 2, + } + + mlir = compile_kernel(sdf29, signature, constants) + _write_mlir_to_file(mlir, "sdf29.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf29(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf29() + print("All SDF29 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf30_mlir_ten_layer_independent_cv.py b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf30_mlir_ten_layer_independent_cv.py new file mode 100644 index 0000000000..8f73b4673b --- /dev/null +++ b/third_party/ascend/unittest/DynamicCVPipeline_ut/test_sdf30_mlir_ten_layer_independent_cv.py @@ -0,0 +1,242 @@ +""" +Test Case: SDF30 - 10-layer independent CV + +[MLIR Validation] Refactored version + +Description: 10-layer nested (V, U, T, S, R, Q, L, P, K). Each layer has independent CV. + +Refactoring approach (refer to test_custom.py): + 1. Remove precision comparison logic between Kernel and reference implementation + 2. Refer to compile_kernel in test_custom.py to implement MLIR code generation for each Triton Kernel + 3. Add MLIR content validation in test functions to ensure the MLIR code contains the "scope" keyword + 4. Keep the test framework complete and maintainable +""" + +import os +import subprocess +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir, ttir_to_linalg, min_dot_size +from triton.backends.ascend import _apply_ascend_patch +import pytest + +# Apply Ascend patch to inject hacc.target attribute into MLIR modules. +# Required by bishengir FixpipeOp::verify() which checks isAscend950(moduleOp) +# via the hacc.target attribute; without it, dst=UB fixpipe ops are rejected. +_apply_ascend_patch() + + +# ============================================================================ +# Compile helper: compile Triton Kernel to MLIR (linalg dialect) +# Reference: compile_kernel implementation in test_custom.py +# ============================================================================ +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect. + + Compile a Triton Kernel to MLIR in linalg dialect for subsequent content validation. + + Args: + kernel: Triton JIT-compiled kernel function + signature: argument type signature dict, e.g. {"x_ptr": "*fp32", "n": "i32"} + constants: constexpr argument dict, e.g. {"BLOCK": 256} + + Returns: + str: compiled MLIR code string; returns None on compilation failure + """ + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions(arch="Ascend910_9589", enable_dynamic_cv_pipeline=True) + # Register codegen_fns, including min_dot_size required by tl.dot. + codegen_fns = {"min_dot_size": min_dot_size(None)} + ttir = ast_to_ttir(kernel, src, context, options, codegen_fns, {}) + metadata = { + **options.__dict__, + } + # Call make_ttir for TTIR optimization (consistent with the normal compilation path), + # including key optimization passes such as inliner/canonicalizer/cse/licm/loop_unroll. + # Without this step, complex kernels (e.g. while loops) will + # raise RuntimeError: PassManager::run failed during ttir_to_linalg lowering + ttir = make_ttir(ttir, metadata, options) + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============================================================================ +# MLIR output configuration +# ============================================================================ +# MLIR output directory: mlir_output subdirectory alongside this test file +MLIR_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mlir_output") + + +def _write_mlir_to_file(mlir, filename): + os.makedirs(MLIR_OUTPUT_DIR, exist_ok=True) + output_path = os.path.join(MLIR_OUTPUT_DIR, filename) + with open(output_path, "w", encoding="utf-8") as f: + f.write(mlir) + print(f"MLIR code written to: {output_path}") + + +# ============================================================================ +# Kernel definitions +# ============================================================================ + + +# ---------------------------------------------------------------------------- +# SDF30: 10-layer independent CV +# Test purpose: Verify MLIR generation of 10-layer nesting independent CV operations under float16 +# ---------------------------------------------------------------------------- +@triton.jit +def sdf30( + a_ptr, + b_ptr, + c_ptr, + d_ptr, + out_ptr, + l9_cube_out_ptr, + M, + N, + K, + L, + P, + Q, + R, + S, + T, + U, + V, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_c, + stride_d, + stride_out, + stride_l9_cube_row, + stride_l9_cube_col, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + BLOCK_SIZE_L: tl.constexpr, + BLOCK_SIZE_P: tl.constexpr, + BLOCK_SIZE_Q: tl.constexpr, + BLOCK_SIZE_R: tl.constexpr, + BLOCK_SIZE_S: tl.constexpr, + BLOCK_SIZE_T: tl.constexpr, + BLOCK_SIZE_U: tl.constexpr, + BLOCK_SIZE_V: tl.constexpr, +): + pid = tl.program_id(0) + offs_k, offs_n = tl.arange(0, BLOCK_SIZE_K), tl.arange(0, BLOCK_SIZE_N) + for l1 in range(V): + for l2 in range(U): + for l3 in range(T): + for l4 in range(S): + for l5 in range(R): + for l6 in range(Q): + for l7 in range(L): + for l8 in range(P): + for l9 in range(K): + a = tl.load(a_ptr + l9 * stride_am + offs_k * stride_ak, mask=offs_k < K, + other=0.0) + b = tl.load(b_ptr + l9 * stride_bk + offs_k * stride_bn, mask=offs_k < K, + other=0.0) + l9_cube = tl.dot(a[:, None], b[None, :]) + l9_cube_ptrs = l9_cube_out_ptr + offs_k[:, None] * stride_l9_cube_row + offs_k[ + None, :] * stride_l9_cube_col + tl.store(l9_cube_ptrs, l9_cube, + mask=(offs_k[:, None] < K) & (offs_k[None, :] < K)) + c = tl.load(c_ptr + offs_n * stride_c, mask=offs_n < N, other=0.0) + d = tl.load(d_ptr + offs_n * stride_d, mask=offs_n < N, other=0.0) + l9_vec = c + d + out_ptrs = out_ptr + offs_n * stride_out + tl.store(out_ptrs, l9_vec, mask=offs_n < N) + + +# ============================================================================ +# Pytest test cases +# ============================================================================ + + +def _build_sdf30_signature(dtype_str): + """Build the argument type signature for the SDF30 kernel.""" + return { + "a_ptr": f"*{dtype_str}", + "b_ptr": f"*{dtype_str}", + "c_ptr": f"*{dtype_str}", + "d_ptr": f"*{dtype_str}", + "out_ptr": f"*{dtype_str}", + "l9_cube_out_ptr": f"*{dtype_str}", + "M": "i32", + "N": "i32", + "K": "i32", + "L": "i32", + "P": "i32", + "Q": "i32", + "R": "i32", + "S": "i32", + "T": "i32", + "U": "i32", + "V": "i32", + "stride_am": "i32", + "stride_ak": "i32", + "stride_bk": "i32", + "stride_bn": "i32", + "stride_c": "i32", + "stride_d": "i32", + "stride_out": "i32", + "stride_l9_cube_row": "i32", + "stride_l9_cube_col": "i32", + } + + +def test_sdf30(): + """SDF30: Verify float16 kernel compilation generates correct MLIR code. + + Test steps: + 1. Compile sdf30 kernel to MLIR + 2. Verify MLIR code is successfully generated and non-empty + 3. Verify MLIR code contains the function definition + 4. Verify MLIR code contains the "scope" keyword + """ + signature = _build_sdf30_signature("fp16") + constants = { + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 8, + "BLOCK_SIZE_L": 2, + "BLOCK_SIZE_P": 2, + "BLOCK_SIZE_Q": 2, + "BLOCK_SIZE_R": 2, + "BLOCK_SIZE_S": 2, + "BLOCK_SIZE_T": 2, + "BLOCK_SIZE_U": 2, + "BLOCK_SIZE_V": 2, + } + + mlir = compile_kernel(sdf30, signature, constants) + _write_mlir_to_file(mlir, "sdf30.mlir") + + assert mlir and len(mlir) > 0, "MLIR code generation failed or is empty" + assert "func.func @sdf30(" in mlir, \ + "Kernel function definition not found in MLIR code" + assert "scope" not in mlir, "Fallback scenario: MLIR code unexpectedly contains the 'scope' keyword" + + # Output MLIR code to the specified path + + +# ============================================================================ +# Main for manual testing +# ============================================================================ +if __name__ == "__main__": + test_sdf30() + print("All SDF30 v3 MLIR validation tests passed!") diff --git a/third_party/ascend/unittest/affine_map/affine_map.py b/third_party/ascend/unittest/affine_map/affine_map.py new file mode 100755 index 0000000000..5d8460aed6 --- /dev/null +++ b/third_party/ascend/unittest/affine_map/affine_map.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + + +def main(): + with ir.context() as ctx: + ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + + d0 = ascend_ir.affine_expr.get_dim(0) + d1 = ascend_ir.affine_expr.get_dim(1) + c2 = ascend_ir.affine_expr.get_constant(2) + + expr = (d0 + c2) * d1 + print("expr:", expr) + print("expr pure affine:", expr.is_pure_affine()) + print("expr hashable:", hash(expr)) + + m0 = ascend_ir.affine_map.get_identity(2) + m1 = ascend_ir.affine_map.get(2, 0, [d1, d0]) + m2 = ascend_ir.affine_map.get(2, 0, [d0 + d1, d1]) + m3 = ascend_ir.affine_map.get_constant(7) + minor = ascend_ir.affine_map.get_minor_identity(3, 2) + + print("m0:", m0) + print("m1:", m1) + print("m2:", m2) + print("m1 inverse:", m1.inverse_permutation()) + print("m2 submap[1]:", m2.get_sub_map([1])) + print("m2 compose m1:", m2.compose(m1)) + print("m1 as dict:", m1.to_dict()) + print("m3 constant:", m3, "value=", m3.get_constant_result()) + print("minor identity:", minor) + print("m2 results:", [str(x) for x in m2.get_results()]) + + +if __name__ == "__main__": + main() diff --git a/third_party/ascend/unittest/affine_map/affine_map_buffer_type_demo.py b/third_party/ascend/unittest/affine_map/affine_map_buffer_type_demo.py new file mode 100755 index 0000000000..a3e248a454 --- /dev/null +++ b/third_party/ascend/unittest/affine_map/affine_map_buffer_type_demo.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + + +def main(): + with ir.context() as ctx: + ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + + builder = ascend_ir.ascendnpu_ir_builder(ctx) + f32 = builder.get_float_ty() + ub_space = builder.get_target_attribute(ascend_ir.AddressSpace.UB) + + # Build a memref type using an explicit affine map layout. + transpose_map = ascend_ir.affine_map.get(2, 0, [1, 0]) + memref_ty = builder.get_buffer_ty_with_affine_map([8, 16], f32, transpose_map, ub_space) + map_attr = builder.get_affine_map_attr(transpose_map) + + print("affine map:", transpose_map) + print("affine map attr:", map_attr) + print("memref type:", memref_ty) + + +if __name__ == "__main__": + main() diff --git a/third_party/ascend/unittest/affine_map/affine_map_complex_expr_demo.py b/third_party/ascend/unittest/affine_map/affine_map_complex_expr_demo.py new file mode 100755 index 0000000000..4748e69a02 --- /dev/null +++ b/third_party/ascend/unittest/affine_map/affine_map_complex_expr_demo.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + + +def main(): + with ir.context() as ctx: + ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + + d0 = ascend_ir.affine_expr.get_dim(0) + d1 = ascend_ir.affine_expr.get_dim(1) + s0 = ascend_ir.affine_expr.get_symbol(0) + c3 = ascend_ir.affine_expr.get_constant(3) + c4 = ascend_ir.affine_expr.get_constant(4) + + # Complex expressions with symbols and integer arithmetic. + tiled_row = (d0 + s0).floordiv(c4) + tiled_col = (d1 + c3).ceildiv(c4) + inner = (d0 + d1).mod(c4) + + map_a = ascend_ir.affine_map.get(2, 1, [tiled_row, tiled_col, inner]) + map_b = ascend_ir.affine_map.get(2, 0, [d1, d0]) + map_comp = map_a.compose(map_b) + + print("map_a:", map_a) + print("map_b:", map_b) + print("map_a composed with map_b:", map_comp) + print("map_a results:", [str(r) for r in map_a.get_results()]) + print("map_a submap [0, 2]:", map_a.get_sub_map([0, 2])) + print("map_a metadata:", map_a.to_dict()) + + +if __name__ == "__main__": + main() diff --git a/third_party/ascend/unittest/affine_map/affine_map_indexing_map_demo.py b/third_party/ascend/unittest/affine_map/affine_map_indexing_map_demo.py new file mode 100755 index 0000000000..d8be55d37e --- /dev/null +++ b/third_party/ascend/unittest/affine_map/affine_map_indexing_map_demo.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + + +def main(): + with ir.context() as ctx: + ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + + builder = ascend_ir.ascendnpu_ir_builder(ctx) + + d0 = ascend_ir.affine_expr.get_dim(0) + d1 = ascend_ir.affine_expr.get_dim(1) + c8 = ascend_ir.affine_expr.get_constant(8) + + # Example indexing maps: transpose and a tiled/reduced projection. + map_in0 = ascend_ir.affine_map.get(2, 0, [d1, d0]) + map_in1 = ascend_ir.affine_map.get(2, 0, [d0, d1]) + map_out = ascend_ir.affine_map.get(2, 0, [d0.floordiv(c8), d1.mod(c8)]) + + indexing_map_attr = builder.get_affine_map_array_attr([map_in0, map_in1, map_out]) + print("indexing_map attr:", indexing_map_attr) + + ub_space = builder.get_target_attribute(ascend_ir.AddressSpace.UB) + f32 = builder.get_float_ty() + memref_ty = builder.get_buffer_ty_with_affine_map([16, 32], f32, map_in0, ub_space) + print("buffer type with map_in0:", memref_ty) + + +if __name__ == "__main__": + main() diff --git a/third_party/ascend/unittest/affine_map/affine_map_parse_demo.py b/third_party/ascend/unittest/affine_map/affine_map_parse_demo.py new file mode 100755 index 0000000000..9bb8e09667 --- /dev/null +++ b/third_party/ascend/unittest/affine_map/affine_map_parse_demo.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + + +def main(): + with ir.context() as ctx: + ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + + identity_map = ascend_ir.affine_map.get_identity(2) + transpose_map = ascend_ir.affine_map.get(2, 0, [1, 0]) + + print("identity map:", identity_map) + print(" dims:", identity_map.get_num_dims()) + print(" symbols:", identity_map.get_num_symbols()) + print(" results:", identity_map.get_num_results()) + print(" is_identity:", identity_map.is_identity()) + print(" is_permutation:", identity_map.is_permutation()) + + print("transpose map:", transpose_map) + print(" is_identity:", transpose_map.is_identity()) + print(" is_permutation:", transpose_map.is_permutation()) + print(" as python object:", transpose_map.to_dict()) + + +if __name__ == "__main__": + main() diff --git a/third_party/ascend/unittest/autotune_ut/01-vector-add.py b/third_party/ascend/unittest/autotune_ut/01-vector-add.py new file mode 100644 index 0000000000..1219c1a2e3 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/01-vector-add.py @@ -0,0 +1,86 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Vector Add +============= +""" + +import os + +import torch +import torch_npu +import triton +import triton.language as tl +import triton.backends.ascend.runtime +from triton.backends.ascend.testing import do_bench_npu + + +@triton.autotune(configs=[], key=["n_elements"]) +@triton.jit +def add_kernel(x_ptr, # *Pointer* to first input vector. + y_ptr, # *Pointer* to second input vector. + output_ptr, # *Pointer* to output vector. + n_elements, # Size of the vector. + BLOCK_SIZE: tl.constexpr, # Number of elements each program should process. + # NOTE: `constexpr` so it can be used as a shape value. + ): + # There are multiple 'programs' processing different data. We identify which program + # we are here: + pid = tl.program_id(axis=0) # We use a 1D launch grid so axis is 0. + # This program will process inputs that are offset from the initial data. + # For instance, if you had a vector of length 256 and block_size of 64, the programs + # would each access the elements [0:64, 64:128, 128:192, 192:256]. + # Note that offsets is a list of pointers: + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + # Create a mask to guard memory operations against out-of-bounds accesses. + mask = offsets < n_elements + # Load x and y from DRAM, masking out any extra elements in case the input is not a + # multiple of the block size. + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + # Write x + y back to DRAM. + tl.store(output_ptr + offsets, output, mask=mask) + + +def add_torch(x, y): + return x + y + + +def add_autotune(x, y): + output = torch.empty_like(x) + n_elements = output.numel() + add_kernel[lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]), )](x, y, output, n_elements) + return output + + +def test_add(size: int): + x = torch.rand(size, device="npu") + y = torch.rand(size, device="npu") + + output_torch = add_torch(x, y) + output_triton = add_autotune(x, y) + assert torch.allclose(output_triton, output_torch) + print(f"Vector Add {size} PASSED!") + + +if __name__ == "__main__": + test_add(98432) diff --git a/third_party/ascend/unittest/autotune_ut/02-fused-softmax.py b/third_party/ascend/unittest/autotune_ut/02-fused-softmax.py new file mode 100644 index 0000000000..66f6c7a371 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/02-fused-softmax.py @@ -0,0 +1,99 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Fused Softmax +============= +""" + +import os + +import torch +import torch_npu +import triton +import triton.language as tl +import triton.backends.ascend.runtime +from triton.backends.ascend.testing import do_bench_npu + + +@triton.autotune( + configs=[], + key=["n_rows", "n_cols"], +) +@triton.jit +def softmax_kernel( + output_ptr, + input_ptr, + input_row_stride, + output_row_stride, + n_rows, + n_cols, + BLOCK_SIZE: tl.constexpr, + XBLOCK: tl.constexpr, + XBLOCK_SUB: tl.constexpr, +): + # starting row of the program + row_start = tl.program_id(0) * XBLOCK + for row_idx in tl.range(0, XBLOCK, XBLOCK_SUB): + # The stride represents how much we need to increase the pointer to advance 1 row + row_offsets = row_start + row_idx + tl.arange(0, XBLOCK_SUB)[:, None] + col_offsets = tl.arange(0, BLOCK_SIZE)[None, :] + xmask = row_offsets < n_rows + ymask = col_offsets < n_cols + mask = xmask & ymask + input_ptrs = input_ptr + (row_offsets * input_row_stride + col_offsets) + # Load the row into SRAM, using a mask since BLOCK_SIZE may be > than n_cols + row = tl.load(input_ptrs, mask=mask, other=-float("inf")) + # Subtract maximum for numerical stability + row_minus_max = row - tl.max(row, axis=1).reshape(XBLOCK_SUB, 1).broadcast_to(XBLOCK_SUB, BLOCK_SIZE) + # Note that exponentiation in Triton is fast but approximate (i.e., think __expf in CUDA) + numerator = tl.exp(row_minus_max) + denominator = (tl.sum(numerator, axis=1).reshape(XBLOCK_SUB, 1).broadcast_to(XBLOCK_SUB, BLOCK_SIZE)) + softmax_output = numerator / denominator + # Write back output to DRAM + output_ptrs = output_ptr + (row_offsets * output_row_stride + col_offsets) + tl.store(output_ptrs, softmax_output, mask=mask) + + +def softmax_torch(x): + return torch.softmax(x, axis=-1) + + +def softmax_autotune(x): + n_rows, n_cols = x.shape + BLOCK_SIZE = n_cols + + # Allocate output + y = torch.empty_like(x) + # Create a number of persistent programs. + softmax_kernel[lambda meta: (triton.cdiv(n_rows, meta["XBLOCK"]), 1, 1)](y, x, x.stride(0), y.stride(0), n_rows, + n_cols, BLOCK_SIZE=BLOCK_SIZE) + return y + + +def test_softmax(shape, dtype): + x = torch.randn(shape, dtype=dtype, device="npu") + y_torch = softmax_torch(x) + y_triton = softmax_autotune(x) + assert torch.allclose(y_triton, y_torch) + print(f"Fused Softmax {shape} {dtype} PASSED!") + + +if __name__ == "__main__": + test_softmax((16896, 1024), torch.float32) diff --git a/third_party/ascend/unittest/autotune_ut/03-layer-norm.py b/third_party/ascend/unittest/autotune_ut/03-layer-norm.py new file mode 100644 index 0000000000..ab547cfa36 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/03-layer-norm.py @@ -0,0 +1,133 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Layer Normalization +============= +""" + +import os + +import torch +import torch_npu +import triton +import triton.language as tl +import triton.backends.ascend.runtime +from triton.backends.ascend.testing import do_bench_npu + + +@triton.autotune( + configs=[], + key=["M", "N"], +) +@triton.jit +def _layer_norm_fwd_fused( + X, # pointer to the input + Y, # pointer to the output + W, # pointer to the weights + B, # pointer to the biases + Mean, # pointer to the mean + Rstd, # pointer to the 1/std + stride, # how much to increase the pointer when moving by 1 row + N, + M, # number of columns in X + eps, # epsilon to avoid division by zero + XBLOCK_SIZE: tl.constexpr, + RBLOCK_SIZE: tl.constexpr, +): + # Map the program id to the row of X and Y it should compute. + row_begin = tl.program_id(0) * XBLOCK_SIZE + row_idx = row_begin + tl.arange(0, XBLOCK_SIZE) + row_mask = row_idx < M + row_offsets = row_idx[:, None] * stride + # Compute mean + _mean = tl.zeros((XBLOCK_SIZE, RBLOCK_SIZE), dtype=tl.float32) + for off in range(0, N, RBLOCK_SIZE): + col_idx = off + tl.arange(0, RBLOCK_SIZE) + col_mask = col_idx < N + mask = row_mask[:, None] & col_mask[None, :] + a = tl.load(X + row_offsets + col_idx[None, :], mask=mask, other=0.0).to(tl.float32) + _mean += a + mean = tl.sum(_mean, axis=1, keep_dims=True) / N + # Compute variance + _var = tl.zeros((XBLOCK_SIZE, RBLOCK_SIZE), dtype=tl.float32) + for off in range(0, N, RBLOCK_SIZE): + col_idx = off + tl.arange(0, RBLOCK_SIZE) + col_mask = col_idx < N + mask = row_mask[:, None] & col_mask[None, :] + x = tl.load(X + row_offsets + col_idx[None, :], mask=mask, other=0.0).to(tl.float32) + x = tl.where(mask, x - mean, 0.0) + _var += x * x + var = tl.sum(_var, axis=1, keep_dims=True) / N + rstd = 1 / tl.sqrt(var + eps) + # Write mean / rstd + tl.store(Mean + row_idx[:, None], mean, mask=row_mask[:, None]) + tl.store(Rstd + row_idx[:, None], rstd, mask=row_mask[:, None]) + # Normalize and apply linear transformation + for off in range(0, N, RBLOCK_SIZE): + col_idx = off + tl.arange(0, RBLOCK_SIZE) + col_mask = col_idx < N + mask = row_mask[:, None] & col_mask[None, :] + w = tl.load(W + col_idx, mask=col_mask).reshape((1, RBLOCK_SIZE)) + b = tl.load(B + col_idx, mask=col_mask).reshape((1, RBLOCK_SIZE)) + x = tl.load(X + row_offsets + col_idx[None, :], mask=mask, other=0.0).to(tl.float32) + x_hat = (x - mean) * rstd + y = x_hat * w + b + # Write output + tl.store(Y + row_offsets + col_idx[None, :], y, mask=mask) + + +def layer_norm_torch(args): + x, w_shape, weight, bias, eps, dtype = args + return torch.nn.functional.layer_norm(x, w_shape, weight, bias, eps).to(dtype) + + +def layer_norm_autotune(args): + x, weight, bias, eps = args + # allocate output + y = torch.empty_like(x) + # reshape input data into 2D tensor + x_arg = x.reshape(-1, x.shape[-1]) + M, N = x_arg.shape + mean = torch.empty((M, ), dtype=torch.float32, device=x.device) + rstd = torch.empty((M, ), dtype=torch.float32, device=x.device) + + # enqueue kernel + _layer_norm_fwd_fused[lambda meta: (triton.cdiv(M, meta["XBLOCK_SIZE"]), 1, 1)]( # + x_arg, y, weight, bias, mean, rstd, x_arg.stride(0), N, M, eps # + ) + return y + + +def test_layer_norm(shape, dtype, eps=1e-5): + M, N = shape + device = "npu" + x_shape = shape + w_shape = (x_shape[-1], ) + weight = torch.rand(w_shape, dtype=dtype, device=device) + bias = torch.rand(w_shape, dtype=dtype, device=device) + x = -2.3 + 0.5 * torch.randn(x_shape, dtype=dtype, device=device) + y_torch = layer_norm_torch((x, w_shape, weight, bias, eps, dtype)) + y_triton = layer_norm_autotune((x, weight, bias, eps)) + assert torch.allclose(y_triton, y_torch, atol=1e-2, rtol=0) + print(f"Layer Normalization {M},{N} {dtype} PASSED!") + + +if __name__ == "__main__": + test_layer_norm((128, 32), torch.float16) diff --git a/third_party/ascend/unittest/autotune_ut/04-libentry.py b/third_party/ascend/unittest/autotune_ut/04-libentry.py new file mode 100644 index 0000000000..e956ba034b --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/04-libentry.py @@ -0,0 +1,97 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Vector Add with Libentry +============= +""" + +import os + +import torch +import torch_npu +import triton +import triton.language as tl +import triton.backends.ascend.runtime +from triton.runtime.libentry import libentry +from triton.backends.ascend.testing import do_bench_npu + + +# NB: Inserting any other decorator between @triton.autotune and @triton.jit disables +# parallel compilation during autotuning. To enable parallel compilation, apply @triton.autotune +# directly around @triton.jit (i.e., nest autotune as the outermost decorator on the JIT-compiled function) +@triton.autotune( + configs=[ + triton.Config({'BLOCK_SIZE': 1 * 1024, 'multibuffer': True}), + triton.Config({'BLOCK_SIZE': 12 * 1024, 'multibuffer': True}), + triton.Config({'BLOCK_SIZE': 12 * 1024, 'multibuffer': False}), + triton.Config({'BLOCK_SIZE': 8 * 1024, 'multibuffer': True}), + ], key=["n_elements"]) +@libentry() +@triton.jit +def add_kernel(x_ptr, # *Pointer* to first input vector. + y_ptr, # *Pointer* to second input vector. + output_ptr, # *Pointer* to output vector. + n_elements, # Size of the vector. + BLOCK_SIZE: tl.constexpr, # Number of elements each program should process. + # NOTE: `constexpr` so it can be used as a shape value. + ): + # There are multiple 'programs' processing different data. We identify which program + # we are here: + pid = tl.program_id(axis=0) # We use a 1D launch grid so axis is 0. + # This program will process inputs that are offset from the initial data. + # For instance, if you had a vector of length 256 and block_size of 64, the programs + # would each access the elements [0:64, 64:128, 128:192, 192:256]. + # Note that offsets is a list of pointers: + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + # Create a mask to guard memory operations against out-of-bounds accesses. + mask = offsets < n_elements + # Load x and y from DRAM, masking out any extra elements in case the input is not a + # multiple of the block size. + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + # Write x + y back to DRAM. + tl.store(output_ptr + offsets, output, mask=mask) + + +def add_torch(x, y): + return x + y + + +def add_autotune(x, y): + output = torch.empty_like(x) + n_elements = output.numel() + add_kernel[lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]), )](x, y, output, n_elements) + return output + + +def test_add(size: int): + x = torch.rand(size, device="npu") + y = torch.rand(size, device="npu") + + output_torch = add_torch(x, y) + output_triton = add_autotune(x, y) + assert torch.allclose(output_triton, output_torch) + print(f"Vector Add {size} with libentry PASSED!") + + +if __name__ == "__main__": + test_add(98432) diff --git a/third_party/ascend/unittest/autotune_ut/test_autotune_param_valid.py b/third_party/ascend/unittest/autotune_ut/test_autotune_param_valid.py new file mode 100644 index 0000000000..fd83e0f21f --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_autotune_param_valid.py @@ -0,0 +1,1706 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import ast +import importlib.util +import os +from collections.abc import Sequence +from pathlib import Path +from types import SimpleNamespace +from typing import Any, Dict, List, Optional + +import pytest +import torch +import torch_npu +import triton +import triton.backends.ascend.runtime +import triton.backends.ascend.runtime.autotuner as ascend_autotuner +from triton.backends.ascend.runtime.tile_generator import KernelMeta +from triton.backends.ascend.runtime.utils import is_valid_axis_name +import triton.language as tl + +VALID_AXIS_NAMES = ["x", "y", "z", "w", "v", "t"] +AUTOTUNER_PATH = Path(__file__).resolve().parents[2] / "backend" / "runtime" / "autotuner.py" +VECTOR_AXES_PATH = Path(__file__).resolve().parents[2] / "backend" / "runtime" / "vector_axes.py" + + +def _load_vector_axes_module(): + spec = importlib.util.spec_from_file_location("vector_axes_test_runtime", VECTOR_AXES_PATH) + module = importlib.util.module_from_spec(spec) + import sys + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def _load_autotuner_methods(*method_names): + source = AUTOTUNER_PATH.read_text(encoding="utf-8") + module = ast.parse(source, filename=str(AUTOTUNER_PATH)) + class_node = next(node for node in module.body if isinstance(node, ast.ClassDef) and node.name == "AutoTilingTuner") + selected = [ + node for node in class_node.body if isinstance(node, ast.FunctionDef) and node.name in set(method_names) + ] + extracted_module = ast.Module(body=selected, type_ignores=[]) + ast.fix_missing_locations(extracted_module) + namespace = { + "Any": Any, + "Dict": Dict, + "List": List, + "Optional": Optional, + "Sequence": Sequence, + "valid_axis_names": VALID_AXIS_NAMES, + "VectorAxes": _load_vector_axes_module().VectorAxes, + "_InternalNPUOptionInt": ascend_autotuner._InternalNPUOptionInt, + "_DEFAULT_COMPILE_MODE": ascend_autotuner._DEFAULT_COMPILE_MODE, + "_multibuffer_mode_to_tuple": ascend_autotuner._multibuffer_mode_to_tuple, + "_normalize_config_hints": ascend_autotuner._normalize_config_hints, + } + exec(compile(extracted_module, str(AUTOTUNER_PATH), "exec"), namespace) + return namespace + + +def _normalize_loaded_method(method): + if isinstance(method, staticmethod): + return method.__func__ + return method + + +def _init_axis_state_for_test( + key, + *, + hints_axes, + split_params, + tiling_params, + low_dim_axes, + reduction_axes, + enable_vv_parser_v2=False, + vv_parser_v2_mode="off", +): + namespace = _load_autotuner_methods( + "_parse_hints_axes", + "_get_runtime_arg_names_for_hints_axes", + "_infer_hints_axes_from_key", + "_normalize_axis_name_mapping", + "_rebuild_vector_axes", + "_get_parser_axis_arg_names", + "_is_direct_runtime_length_arg_name", + "_init_axis_params", + ) + tuner = SimpleNamespace() + tuner.arg_names = ["x_ptr", "n_elements", "BLOCK_SIZE", "BLOCK_SIZE_SUB"] + tuner._get_constexpr_candidates = lambda: ["BLOCK_SIZE", "BLOCK_SIZE_SUB"] + parse_hints_axes = _normalize_loaded_method(namespace.get("_parse_hints_axes")) + if parse_hints_axes is not None: + tuner._parse_hints_axes = parse_hints_axes.__get__(tuner, SimpleNamespace) + get_runtime_arg_names_for_hints_axes = _normalize_loaded_method( + namespace.get("_get_runtime_arg_names_for_hints_axes")) + if get_runtime_arg_names_for_hints_axes is not None: + tuner._get_runtime_arg_names_for_hints_axes = get_runtime_arg_names_for_hints_axes.__get__( + tuner, + SimpleNamespace, + ) + rebuild_vector_axes = _normalize_loaded_method(namespace.get("_rebuild_vector_axes")) + if rebuild_vector_axes is not None: + tuner._rebuild_vector_axes = rebuild_vector_axes.__get__(tuner, SimpleNamespace) + normalize_axis_name_mapping = _normalize_loaded_method(namespace.get("_normalize_axis_name_mapping")) + if normalize_axis_name_mapping is not None: + tuner._normalize_axis_name_mapping = normalize_axis_name_mapping.__get__( + tuner, + SimpleNamespace, + ) + infer_hints_axes_from_key = _normalize_loaded_method(namespace.get("_infer_hints_axes_from_key")) + if infer_hints_axes_from_key is not None: + tuner._infer_hints_axes_from_key = infer_hints_axes_from_key.__get__(tuner, SimpleNamespace) + get_parser_axis_arg_names = _normalize_loaded_method(namespace.get("_get_parser_axis_arg_names")) + if get_parser_axis_arg_names is not None: + tuner._get_parser_axis_arg_names = get_parser_axis_arg_names.__get__(tuner, SimpleNamespace) + is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace.get("_is_direct_runtime_length_arg_name")) + if is_direct_runtime_length_arg_name is not None: + tuner._is_direct_runtime_length_arg_name = is_direct_runtime_length_arg_name + tuner.enable_vv_parser_v2 = enable_vv_parser_v2 + tuner.vv_parser_v2_mode = vv_parser_v2_mode + init_axis_params = _normalize_loaded_method(namespace["_init_axis_params"]) + init_signature = init_axis_params.__code__.co_varnames[:init_axis_params.__code__.co_argcount] + if "hints_axes" in init_signature: + init_axis_params( + tuner, + key, + split_params, + tiling_params, + low_dim_axes, + reduction_axes, + hints_axes, + ) + else: + init_axis_params( + tuner, + key, + split_params, + tiling_params, + low_dim_axes, + reduction_axes, + ) + return tuner + + +def test_init_axis_params_keeps_key_as_argument_name_list(): + tuner = _init_axis_state_for_test( + ["n_elements"], + hints_axes={"x": "n_elements"}, + split_params={"x": "BLOCK_SIZE"}, + tiling_params={"x": "BLOCK_SIZE_SUB"}, + low_dim_axes=["x"], + reduction_axes=[], + ) + + assert tuner.keys == ["n_elements"] + assert tuner.hints_axes == {"x": "n_elements"} + assert tuner.axis_arg_names == {"x": "n_elements"} + + +def test_init_axis_params_falls_back_to_key_order_when_hints_axes_missing(): + tuner = _init_axis_state_for_test( + ["n_elements"], + hints_axes=None, + split_params=None, + tiling_params=None, + low_dim_axes=None, + reduction_axes=None, + ) + + assert tuner.keys == ["n_elements"] + assert tuner.hints_axes == {"x": "n_elements"} + assert tuner.vector_axes.axis_length_exprs == {"x": "n_elements"} + assert tuner.axis_arg_names == {"x": "n_elements"} + + +def test_init_axis_params_defers_key_fallback_when_vv_assist_without_hints_axes(): + tuner = _init_axis_state_for_test( + ["n_elements"], + hints_axes=None, + split_params=None, + tiling_params=None, + low_dim_axes=None, + reduction_axes=None, + enable_vv_parser_v2=True, + vv_parser_v2_mode="assist", + ) + + assert tuner.keys == ["n_elements"] + assert tuner.hints_axes == {} + assert tuner.vector_axes.axis_length_exprs == {} + assert tuner.axis_arg_names == {} + + +def test_init_axis_params_normalizes_tuple_key(): + tuner = _init_axis_state_for_test( + ("n_elements", ), + hints_axes={"x": "n_elements"}, + split_params={"x": "BLOCK_SIZE"}, + tiling_params={"x": "BLOCK_SIZE_SUB"}, + low_dim_axes=["x"], + reduction_axes=[], + ) + + assert tuner.keys == ["n_elements"] + assert tuner.axis_arg_names == {"x": "n_elements"} + + +def test_init_axis_params_rejects_invalid_hints_axes_name(): + with pytest.raises(ValueError, match="All keys in 'hints.axes' must be valid axis names"): + _init_axis_state_for_test( + ["n_elements"], + hints_axes={"x0": "n_elements"}, + split_params={"x": "BLOCK_SIZE"}, + tiling_params={"x": "BLOCK_SIZE_SUB"}, + low_dim_axes=["x"], + reduction_axes=[], + ) + + +def test_init_axis_params_rejects_non_string_hints_axes_value(): + with pytest.raises(ValueError, match="All values in 'hints.axes' must be non-empty argument names"): + _init_axis_state_for_test( + ["n_elements"], + hints_axes={"x": 123}, + split_params=None, + tiling_params=None, + low_dim_axes=None, + reduction_axes=None, + ) + + +def test_init_axis_params_rejects_unknown_hints_axes_arg_name(): + with pytest.raises(ValueError, match="must reference runtime non-constexpr argument names"): + _init_axis_state_for_test( + ["n_elements"], + hints_axes={"x": "unknown_arg"}, + split_params=None, + tiling_params=None, + low_dim_axes=None, + reduction_axes=None, + ) + + +def test_init_axis_params_rejects_constexpr_hints_axes_arg_name(): + with pytest.raises(ValueError, match="must reference runtime non-constexpr argument names"): + _init_axis_state_for_test( + ["n_elements"], + hints_axes={"x": "BLOCK_SIZE"}, + split_params=None, + tiling_params=None, + low_dim_axes=None, + reduction_axes=None, + ) + + +def test_init_axis_params_accepts_literal_hints_axes_value(): + tuner = _init_axis_state_for_test( + ["n_elements"], + hints_axes={"x": "128"}, + split_params=None, + tiling_params=None, + low_dim_axes=None, + reduction_axes=None, + ) + + assert tuner.hints_axes == {"x": "128"} + assert tuner.vector_axes.axis_length_exprs == {"x": "128"} + + +def test_init_axis_params_rejects_dict_key(): + with pytest.raises(ValueError, match="key must be a list"): + _init_axis_state_for_test( + {"x": "n_elements"}, + hints_axes={"x": "n_elements"}, + split_params={"x": "BLOCK_SIZE"}, + tiling_params={"x": "BLOCK_SIZE_SUB"}, + low_dim_axes=["x"], + reduction_axes=[], + ) + + +def test_init_axis_params_requires_hints_axes_when_axis_metadata_is_present(): + with pytest.raises(ValueError, match="hints.axes must be provided when axis metadata"): + _init_axis_state_for_test( + ["n_elements"], + hints_axes=None, + split_params={"x": "BLOCK_SIZE"}, + tiling_params={"x": "BLOCK_SIZE_SUB"}, + low_dim_axes=["x"], + reduction_axes=[], + ) + + +def test_init_axis_params_rejects_axis_metadata_axes_missing_from_hints_axes(): + with pytest.raises(ValueError, match="missing from 'hints.axes'"): + _init_axis_state_for_test( + ["n_elements"], + hints_axes={"x": "n_elements"}, + split_params={"y": "BLOCK_SIZE"}, + tiling_params={"x": "BLOCK_SIZE_SUB"}, + low_dim_axes=["x"], + reduction_axes=[], + ) + + +def test_resolve_axis_length_arg_name_uses_internal_axis_map_not_self_keys(): + namespace = _load_autotuner_methods( + "_is_direct_runtime_length_arg_name", + "_get_parser_axis_arg_names", + "_resolve_axis_length_arg_name", + ) + vector_axes_module = _load_vector_axes_module() + tuner = SimpleNamespace( + vv_adapter_result_v2=None, + enable_vv_parser_v2=False, + parser_mode="vector", + vector_axes=vector_axes_module.VectorAxes.from_hints_axes({"x": "n_elements"}), + axis_arg_names={"x": "n_elements"}, + keys=["different_cache_key"], + ) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + + result = _normalize_loaded_method(namespace["_resolve_axis_length_arg_name"])(tuner, "x") + + assert result == "n_elements" + + +def test_resolve_axis_length_arg_name_does_not_fallback_to_key_order(): + namespace = _load_autotuner_methods( + "_is_direct_runtime_length_arg_name", + "_get_parser_axis_arg_names", + "_resolve_axis_length_arg_name", + ) + tuner = SimpleNamespace( + vv_adapter_result_v2=None, + enable_vv_parser_v2=False, + parser_mode="vector", + vector_axes=None, + axis_arg_names={}, + keys=["n_elements"], + ) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + + result = _normalize_loaded_method(namespace["_resolve_axis_length_arg_name"])(tuner, "x") + + assert result is None + + +def test_resolve_axis_length_arg_name_uses_hints_axes_when_vv_enabled(): + namespace = _load_autotuner_methods( + "_is_direct_runtime_length_arg_name", + "_get_parser_axis_arg_names", + "_resolve_axis_length_arg_name", + ) + vector_axes_module = _load_vector_axes_module() + tuner = SimpleNamespace( + vv_adapter_result_v2=SimpleNamespace(axis_length_exprs={}), + enable_vv_parser_v2=True, + parser_mode="vector", + vector_axes=vector_axes_module.VectorAxes.from_hints_axes({"x": "n_elements"}), + axis_arg_names={"x": "n_elements"}, + keys=["different_cache_key"], + ) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + + result = _normalize_loaded_method(namespace["_resolve_axis_length_arg_name"])(tuner, "x") + + assert result == "n_elements" + + +def test_resolve_axis_length_arg_name_uses_base_vv_axis_expr_for_reduction_axis(): + namespace = _load_autotuner_methods( + "_is_direct_runtime_length_arg_name", + "_get_parser_axis_arg_names", + "_resolve_axis_length_arg_name", + ) + tuner = SimpleNamespace( + vv_adapter_result_v2=SimpleNamespace(axis_length_exprs={"x": "n_elements"}), + enable_vv_parser_v2=True, + parser_mode="vector", + vector_axes=None, + axis_arg_names={}, + keys=[], + ) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + + result = _normalize_loaded_method(namespace["_resolve_axis_length_arg_name"])(tuner, "x") + + assert result == "n_elements" + + +def test_is_valid_axis_name_rejects_reduction_prefix(): + assert is_valid_axis_name("x") is True + assert is_valid_axis_name("rx") is False + + +def test_kernel_meta_marks_reduction_axes_from_explicit_list(): + kernel_meta = KernelMeta( + axis_sizes={"x": 128, "y": 64}, + split_params={"x": "XBLOCK"}, + fixed_split_params={}, + tiling_params={"y": "YBLOCK_SUB"}, + low_dims=["y"], + reduction_axes=["y"], + dtype=torch.float16, + persistent_reduction=True, + dual_reduction=False, + num_buffers=1, + is_simt_mode=False, + ) + + axis_by_name = {axis.name: axis for axis in kernel_meta.axis_info} + assert axis_by_name["x"].is_reduction is False + assert axis_by_name["y"].is_reduction is True + assert [axis.name for axis in kernel_meta.low_dims_axis] == ["y"] + + +def test_kernel_meta_rejects_prefixed_reduction_axes(): + with pytest.raises(ValueError, match="reduction axis"): + KernelMeta( + axis_sizes={"x": 128, "y": 64}, + split_params={"x": "XBLOCK"}, + fixed_split_params={}, + tiling_params={"y": "YBLOCK_SUB"}, + low_dims=["y"], + reduction_axes=["ry"], + dtype=torch.float16, + persistent_reduction=True, + dual_reduction=False, + num_buffers=1, + is_simt_mode=False, + ) + + +def test_apply_vv_axis_semantic_result_promotes_internal_axis_map_only(): + namespace = _load_autotuner_methods( + "_normalize_reduction_axis_name", + "_normalize_reduction_axes", + "_normalize_vv_reduction_axes", + "_get_axis_base_name", + "_get_parser_axis_arg_names", + "_is_direct_runtime_length_arg_name", + "_promote_axis_arg_name_to_reduction", + "_apply_vv_axis_semantic_result", + ) + vector_axes_module = _load_vector_axes_module() + tuner = SimpleNamespace( + vv_adapter_result_v2=SimpleNamespace( + status="ok", + reduction_axes=["x"], + low_dim_axes=[], + split_params={}, + tiling_params={}, + axis_pid_dims={}, + ), + reduction_axes=[], + low_dim_axes=[], + split_params={}, + tiling_params={}, + axis_pid_dims={}, + vector_axes=vector_axes_module.VectorAxes.from_hints_axes({"x": "n_elements"}), + axis_arg_names={"x": "n_elements"}, + keys=["n_elements"], + dual_reduction=False, + ) + tuner._normalize_reduction_axis_name = _normalize_loaded_method(namespace["_normalize_reduction_axis_name"]) + tuner._normalize_reduction_axes = _normalize_loaded_method(namespace["_normalize_reduction_axes"]).__get__( + tuner, SimpleNamespace) + tuner._normalize_vv_reduction_axes = _normalize_loaded_method(namespace["_normalize_vv_reduction_axes"]).__get__( + tuner, SimpleNamespace) + tuner._get_axis_base_name = _normalize_loaded_method(namespace["_get_axis_base_name"]) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._promote_axis_arg_name_to_reduction = _normalize_loaded_method( + namespace["_promote_axis_arg_name_to_reduction"]).__get__( + tuner, + SimpleNamespace, + ) + + applied = _normalize_loaded_method(namespace["_apply_vv_axis_semantic_result"])(tuner) + + assert applied is True + assert tuner.keys == ["n_elements"] + assert tuner.axis_arg_names == {"x": "n_elements"} + assert tuner.reduction_axes == ["x"] + + +def test_promote_reduction_axis_rejects_prefixed_axis_name(): + namespace = _load_autotuner_methods( + "_get_axis_base_name", + "_get_parser_axis_arg_names", + "_is_direct_runtime_length_arg_name", + "_promote_axis_arg_name_to_reduction", + ) + vector_axes_module = _load_vector_axes_module() + vector_axes = vector_axes_module.VectorAxes.from_hints_axes({"y": "r1_numel"}) + tuner = SimpleNamespace( + vector_axes=vector_axes, + axis_arg_names={"y": "r1_numel"}, + ) + tuner._get_axis_base_name = _normalize_loaded_method(namespace["_get_axis_base_name"]) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._promote_axis_arg_name_to_reduction = _normalize_loaded_method( + namespace["_promote_axis_arg_name_to_reduction"]).__get__(tuner, SimpleNamespace) + + with pytest.raises(ValueError, match="r-prefixed"): + tuner._promote_axis_arg_name_to_reduction("ry") + + +def test_generate_key_and_configs_uses_axis_arg_names_for_kv_dict(): + namespace = _load_autotuner_methods( + "_parse_hints_axes", + "_get_runtime_arg_names_for_hints_axes", + "_normalize_axis_name_mapping", + "_rebuild_vector_axes", + "_get_axis_base_name", + "_get_parser_axis_arg_names", + "_is_direct_runtime_length_arg_name", + "_promote_axis_arg_name_to_reduction", + "_init_axis_params", + "generate_key_and_configs", + ) + captured = {} + + class FakeArg: + dtype = "float16" + + def fake_get_byte_per_numel(dtype): + return 0 if dtype is None else 1 + + namespace["get_byte_per_numel"] = fake_get_byte_per_numel + namespace["_expand_configs_with_hints"] = lambda fn, configs, config_hints: configs + + tuner = SimpleNamespace( + arg_names=["x_ptr", "n_elements"], + fn=SimpleNamespace(), + _get_constexpr_candidates=lambda: [], + cache={}, + auto_gen_config=True, + parser_mode="vector", + config_hints={}, + _raw_config_hints={}, + gen_configs=[], + user_configs=[], + is_simt_mode=False, + user_specified_warps=None, + user_specified_multibuffer=None, + _autoparse_axis_params=lambda all_args: None, + _gen_tile_configs=lambda kv_dict, dtype, all_args: captured.update( + kv_dict=dict(kv_dict), + dtype=dtype, + all_args=dict(all_args), + ) or setattr(tuner, "gen_configs", [SimpleNamespace(kwargs={"BLOCK_SIZE": 128})]), + ) + tuner._parse_hints_axes = _normalize_loaded_method(namespace["_parse_hints_axes"]).__get__(tuner, SimpleNamespace) + tuner._get_runtime_arg_names_for_hints_axes = _normalize_loaded_method( + namespace["_get_runtime_arg_names_for_hints_axes"]).__get__(tuner, SimpleNamespace) + tuner._rebuild_vector_axes = _normalize_loaded_method(namespace["_rebuild_vector_axes"]).__get__( + tuner, SimpleNamespace) + tuner._get_axis_base_name = _normalize_loaded_method(namespace["_get_axis_base_name"]) + tuner._normalize_axis_name_mapping = _normalize_loaded_method(namespace["_normalize_axis_name_mapping"]).__get__( + tuner, SimpleNamespace) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._promote_axis_arg_name_to_reduction = _normalize_loaded_method( + namespace["_promote_axis_arg_name_to_reduction"]).__get__(tuner, SimpleNamespace) + + _normalize_loaded_method(namespace["_init_axis_params"])( + tuner, + ["n_elements"], + None, + None, + None, + None, + {"x": "n_elements"}, + ) + + key = _normalize_loaded_method(namespace["generate_key_and_configs"])( + tuner, + FakeArg(), + 17, + ) + + assert key == (17, "float16", ("compile_mode", "simd_simt_template")) + simt_key = _normalize_loaded_method(namespace["generate_key_and_configs"])( + tuner, + FakeArg(), + 17, + compile_mode="simt_only", + ) + assert simt_key == (17, "float16", ("compile_mode", "simt_only")) + assert captured["kv_dict"] == {"x": 17} + + +def test_generate_key_and_configs_preserves_promoted_reduction_axis_identity(): + namespace = _load_autotuner_methods( + "_parse_hints_axes", + "_get_runtime_arg_names_for_hints_axes", + "_normalize_axis_name_mapping", + "_rebuild_vector_axes", + "_get_axis_base_name", + "_get_parser_axis_arg_names", + "_is_direct_runtime_length_arg_name", + "_promote_axis_arg_name_to_reduction", + "_init_axis_params", + "generate_key_and_configs", + ) + captured = {} + + class FakeArg: + dtype = "float16" + + namespace["get_byte_per_numel"] = lambda dtype: 0 if dtype is None else 1 + namespace["_expand_configs_with_hints"] = lambda fn, configs, config_hints: configs + + tuner = SimpleNamespace( + arg_names=["x_ptr", "n_elements"], + fn=SimpleNamespace(), + _get_constexpr_candidates=lambda: [], + cache={}, + auto_gen_config=True, + parser_mode="vector", + config_hints={}, + _raw_config_hints={}, + gen_configs=[], + user_configs=[], + is_simt_mode=False, + user_specified_warps=None, + user_specified_multibuffer=None, + _autoparse_axis_params=lambda all_args: None, + _gen_tile_configs=lambda kv_dict, dtype, all_args: captured.update(kv_dict=dict(kv_dict)) or setattr( + tuner, "gen_configs", [SimpleNamespace(kwargs={"BLOCK_SIZE": 128})]), + ) + tuner._parse_hints_axes = _normalize_loaded_method(namespace["_parse_hints_axes"]).__get__(tuner, SimpleNamespace) + tuner._get_runtime_arg_names_for_hints_axes = _normalize_loaded_method( + namespace["_get_runtime_arg_names_for_hints_axes"]).__get__(tuner, SimpleNamespace) + tuner._rebuild_vector_axes = _normalize_loaded_method(namespace["_rebuild_vector_axes"]).__get__( + tuner, SimpleNamespace) + tuner._get_axis_base_name = _normalize_loaded_method(namespace["_get_axis_base_name"]) + tuner._normalize_axis_name_mapping = _normalize_loaded_method(namespace["_normalize_axis_name_mapping"]).__get__( + tuner, SimpleNamespace) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._promote_axis_arg_name_to_reduction = _normalize_loaded_method( + namespace["_promote_axis_arg_name_to_reduction"]).__get__(tuner, SimpleNamespace) + + _normalize_loaded_method(namespace["_init_axis_params"])( + tuner, + ["n_elements"], + None, + None, + None, + None, + {"x": "n_elements"}, + ) + tuner._promote_axis_arg_name_to_reduction("x") + + _normalize_loaded_method(namespace["generate_key_and_configs"])( + tuner, + FakeArg(), + 23, + ) + + assert tuner.keys == ["n_elements"] + assert captured["kv_dict"] == {"x": 23} + + +def test_refresh_vector_axes_keeps_base_axis_arg_names_without_reduction_aliases(): + namespace = _load_autotuner_methods( + "_parse_hints_axes", + "_get_runtime_arg_names_for_hints_axes", + "_normalize_axis_name_mapping", + "_rebuild_vector_axes", + "_get_axis_base_name", + "_get_parser_axis_arg_names", + "_is_direct_runtime_length_arg_name", + "_promote_axis_arg_name_to_reduction", + "_refresh_vector_axes", + "_init_axis_params", + "generate_key_and_configs", + ) + captured = {} + + class FakeArg: + dtype = "float16" + + namespace["get_byte_per_numel"] = lambda dtype: 0 if dtype is None else 1 + namespace["_expand_configs_with_hints"] = lambda fn, configs, config_hints: configs + + tuner = SimpleNamespace( + arg_names=["x_ptr", "n_elements"], + fn=SimpleNamespace(), + _get_constexpr_candidates=lambda: [], + cache={}, + auto_gen_config=True, + parser_mode="vector", + config_hints={}, + _raw_config_hints={}, + gen_configs=[], + user_configs=[], + is_simt_mode=False, + user_specified_warps=None, + user_specified_multibuffer=None, + _autoparse_axis_params=lambda all_args: None, + _gen_tile_configs=lambda kv_dict, dtype, all_args: captured.update(kv_dict=dict(kv_dict)) or setattr( + tuner, "gen_configs", [SimpleNamespace(kwargs={"BLOCK_SIZE": 128})]), + ) + tuner._parse_hints_axes = _normalize_loaded_method(namespace["_parse_hints_axes"]).__get__(tuner, SimpleNamespace) + tuner._get_runtime_arg_names_for_hints_axes = _normalize_loaded_method( + namespace["_get_runtime_arg_names_for_hints_axes"]).__get__(tuner, SimpleNamespace) + tuner._rebuild_vector_axes = _normalize_loaded_method(namespace["_rebuild_vector_axes"]).__get__( + tuner, SimpleNamespace) + tuner._get_axis_base_name = _normalize_loaded_method(namespace["_get_axis_base_name"]) + tuner._normalize_axis_name_mapping = _normalize_loaded_method(namespace["_normalize_axis_name_mapping"]).__get__( + tuner, SimpleNamespace) + tuner._get_parser_axis_arg_names = _normalize_loaded_method(namespace["_get_parser_axis_arg_names"]).__get__( + tuner, SimpleNamespace) + tuner._is_direct_runtime_length_arg_name = _normalize_loaded_method(namespace["_is_direct_runtime_length_arg_name"]) + tuner._promote_axis_arg_name_to_reduction = _normalize_loaded_method( + namespace["_promote_axis_arg_name_to_reduction"]).__get__(tuner, SimpleNamespace) + tuner._refresh_vector_axes = _normalize_loaded_method(namespace["_refresh_vector_axes"]).__get__( + tuner, SimpleNamespace) + + _normalize_loaded_method(namespace["_init_axis_params"])( + tuner, + ["n_elements"], + None, + None, + None, + None, + {"x": "n_elements"}, + ) + + tuner.reduction_axes = [] + tuner._refresh_vector_axes() + + _normalize_loaded_method(namespace["generate_key_and_configs"])( + tuner, + FakeArg(), + 29, + ) + + assert tuner.axis_arg_names == {"x": "n_elements"} + assert captured["kv_dict"] == {"x": 29} + + +def test_legacy_low_dim_and_tiling_parsers_consume_base_axis_names(): + namespace = _load_autotuner_methods( + "_get_axis_base_name", + "_normalize_axis_name_mapping", + "_autoparse_tiling_params", + "_autoparse_low_dim_axes", + ) + parser_inputs = {} + + class StubTilingAxesParser: + + def __init__(self, func_ast, axis_arg_names, candidates_params): + parser_inputs["tiling"] = dict(axis_arg_names) + + def parse(self): + return {"x": "X0BLOCK_SUB", "y": "R1BLOCK_SUB"} + + class StubLowDimsAxesParser: + + def __init__(self, func_ast, axis_arg_names): + parser_inputs["low_dim"] = dict(axis_arg_names) + + def parse(self): + return ["y"] + + namespace["TilingAxesParser"] = StubTilingAxesParser + namespace["LowDimsAxesParser"] = StubLowDimsAxesParser + + refresh_calls = [] + tuner = SimpleNamespace( + fn=SimpleNamespace(parse=lambda: "fake-func-ast"), + print_autotuning=False, + tiling_params={}, + low_dim_axes=[], + _get_parser_axis_arg_names=lambda: {"x": "x0_numel", "y": "r1_numel"}, + _refresh_vector_axes=lambda: refresh_calls.append(True), + ) + tuner._get_axis_base_name = _normalize_loaded_method(namespace["_get_axis_base_name"]) + tuner._normalize_axis_name_mapping = _normalize_loaded_method(namespace["_normalize_axis_name_mapping"]).__get__( + tuner, SimpleNamespace) + + tiling_params = _normalize_loaded_method(namespace["_autoparse_tiling_params"])( + tuner, + ["X0BLOCK_SUB", "R1BLOCK_SUB"], + ) + low_dim_axes = _normalize_loaded_method(namespace["_autoparse_low_dim_axes"])(tuner) + + assert parser_inputs["tiling"] == {"x": "x0_numel", "y": "r1_numel"} + assert parser_inputs["low_dim"] == {"x": "x0_numel", "y": "r1_numel"} + assert tiling_params == {"x": "X0BLOCK_SUB", "y": "R1BLOCK_SUB"} + assert tuner.tiling_params == {"x": "X0BLOCK_SUB", "y": "R1BLOCK_SUB"} + assert low_dim_axes == ["y"] + assert tuner.low_dim_axes == ["y"] + assert refresh_calls == [True, True] + + +def test_parse_vv_axis_info_v2_collects_fixed_tiling_expr_for_provided_constexpr(): + from triton.backends.ascend.runtime.dsl_analysis.vv_param_parser_v2 import ( + parse_vv_axis_info_v2, ) + + func_ast = ast.parse(""" +def silu_like_kernel( + x, + y, + stride_row, + n_rows, + n_cols, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_M: tl.constexpr, +): + pid = tl.program_id(axis=0) + grid_size = tl.num_programs(axis=0) + num_row_tasks = (n_rows + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M + for row_task_id in range(pid, num_row_tasks, grid_size): + block_start_row = row_task_id * BLOCK_SIZE_M + rows_off = block_start_row + tl.arange(0, BLOCK_SIZE_M) + rows_mask = rows_off < n_rows + for col_offset in range(0, n_cols, BLOCK_SIZE_N): + cols_off = col_offset + tl.arange(0, BLOCK_SIZE_N) + cols_mask = cols_off < n_cols + block_mask = rows_mask[:, None] & cols_mask[None, :] + x_ptrs = x + rows_off[:, None] * stride_row + cols_off[None, :] + y_ptrs = y + rows_off[:, None] * stride_row + cols_off[None, :] + x_chunk = tl.load(x_ptrs, mask=block_mask, other=0.0) + tl.store(y_ptrs, x_chunk, mask=block_mask) +""").body[0] + + result = parse_vv_axis_info_v2( + func_ast, + provided_args={ + "n_rows": 4096, + "n_cols": 8192, + "BLOCK_SIZE_N": 2048, + }, + hints={"vv_parser_v2_mode": "assist"}, + ) + + assert result.axis_length_exprs == {"x": "n_rows", "y": "n_cols"} + assert result.fixed_tiling_exprs == {"y": "BLOCK_SIZE_N"} + + +def test_parse_vv_axis_info_v2_prefers_extent_resolved_site_for_binned_copy_pattern(): + from triton.backends.ascend.runtime.dsl_analysis.vv_param_parser_v2 import ( + parse_vv_axis_info_v2, ) + + func_ast = ast.parse(""" +def binned_copy_like_kernel( + x, + grad, + wgrad, + num_experts, + expert_capacity, + indices, + bins, + NUM_COLUMNS: tl.constexpr, + TOP_K: tl.constexpr, + BLOCK_X: tl.constexpr, +): + expert_idx = tl.program_id(0) + entry_idx = tl.program_id(1) + start = 0 + if expert_idx > 0: + start = tl.load(bins + expert_idx - 1) + end = tl.load(bins + expert_idx) + num_tokens = end - start + if entry_idx >= num_tokens: + return + index_out = tl.load(indices + start + entry_idx) + wgrad += index_out + grad += tl.multiple_of((index_out // TOP_K) * NUM_COLUMNS, NUM_COLUMNS) + offsets = tl.max_contiguous(tl.arange(0, BLOCK_X), BLOCK_X) + acc = tl.zeros((BLOCK_X,), dtype=tl.float32) + iterations = tl.cdiv(NUM_COLUMNS, BLOCK_X) + for _ in range(iterations): + mask = offsets < NUM_COLUMNS + data = tl.load(x + offsets, mask=mask).to(tl.float32) + scale = tl.load(grad + offsets, mask=mask).to(tl.float32) + acc += data * scale + offsets += BLOCK_X + out = tl.sum(acc).to(wgrad.dtype.element_ty) + tl.store(wgrad, out) +""").body[0] + + result = parse_vv_axis_info_v2( + func_ast, + provided_args={"NUM_COLUMNS": 1536, "TOP_K": 4}, + hints={"vv_parser_v2_mode": "assist"}, + ) + + assert result.axis_length_exprs == {"x": "NUM_COLUMNS"} + assert result.tiling_params == {"x": "BLOCK_X"} + assert result.low_dim_axes == ["x"] + + +def test_vector_axes_materialize_axis_sizes_prefers_fixed_tiling_expr_for_non_split_axis(): + vector_axes_module = _load_vector_axes_module() + vector_axes = vector_axes_module.VectorAxes.from_hints_axes({"x": "n_rows", "y": "n_cols"}) + vector_axes.apply_semantic_fields( + split_params={"x": "BLOCK_SIZE_M"}, + low_dim_axes=["y"], + fixed_tiling_exprs={"y": "BLOCK_SIZE_N"}, + ) + + axis_sizes, diagnostics = vector_axes.materialize_axis_sizes({ + "n_rows": 4096, + "n_cols": 8192, + "BLOCK_SIZE_N": 2048, + }) + + assert axis_sizes == {"x": 4096, "y": 2048} + assert diagnostics["y"]["resolved_by"] == "fixed_tiling_expr_arg" + + +def test_parse_cv_params_preserves_block_size_n_for_inline_dot_rhs_expression(): + from triton.backends.ascend.runtime.dsl_analysis.cv_param_parser import ( + parse_cv_params, ) + + func_ast = ast.parse(""" +def matmul_kernel( + a_ptr, + b_ptr, + c_ptr, + M, + N, + K: tl.constexpr, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_cn, + phy_grids, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + GROUP_SIZE_M: tl.constexpr = 4, +): + offs_am = (0 + tl.arange(0, BLOCK_SIZE_M)) % M + offs_bn = (0 + tl.arange(0, BLOCK_SIZE_N)) % N + offs_k = tl.arange(0, BLOCK_SIZE_K) + a_ptrs = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) + b_ptrs = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.int32) + for i in range(4): + for j in range(0, tl.cdiv(K // 4, BLOCK_SIZE_K)): + k = i * tl.cdiv(K // 4, BLOCK_SIZE_K) + j + a = tl.load(a_ptrs, mask=offs_k[None, :] < K - k * BLOCK_SIZE_K, other=0) + b_uint8 = tl.load(b_ptrs, mask=offs_k[:, None] < K, other=0) + mask = 3 << (2 * i) + b = (b_uint8 & mask) >> (2 * i) + tensor_full = tl.full((1,), 1, dtype=tl.int8) + accumulator += tl.dot(a, b.to(tl.int8) - tensor_full, out_dtype=tl.int32) +""").body[0] + + parse_result = parse_cv_params( + func_ast, + parser_mode="mix", + arg_names=[ + "a_ptr", + "b_ptr", + "c_ptr", + "M", + "N", + "K", + "stride_am", + "stride_ak", + "stride_bk", + "stride_bn", + "stride_cm", + "stride_cn", + "phy_grids", + ], + provided_args={ + "M": 2, + "N": 3, + "K": 128, + "stride_am": 128, + "stride_ak": 1, + "stride_bk": 3, + "stride_bn": 1, + "stride_cm": 3, + "stride_cn": 1, + "phy_grids": 1, + }, + explicit_tunable_params=["BLOCK_SIZE_M", "BLOCK_SIZE_N", "BLOCK_SIZE_K"], + ) + + tunable_names = {item.name for item in parse_result.tunable_params} + + assert "BLOCK_SIZE_M" in tunable_names + assert "BLOCK_SIZE_N" in tunable_names + assert "BLOCK_SIZE_K" in tunable_names + assert parse_result.dot_sites + assert parse_result.dot_sites[0].n.tunable_param == "BLOCK_SIZE_N" + + +def test_parse_cv_params_resolves_multi_level_k_alias_for_dot_operands(): + from triton.backends.ascend.runtime.dsl_analysis.cv_param_parser import ( + parse_cv_params, ) + + func_ast = ast.parse(""" +def matmul_kernel( + a_ptr, + b_ptr, + c_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, +): + offs_m = tl.arange(0, BLOCK_M) + offs_n = tl.arange(0, BLOCK_N) + offs_k = tl.arange(0, BLOCK_K) + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + for k in range(0, K, BLOCK_K): + current_k = k + offs_k + a = tl.load( + a_ptr + offs_m[:, None] * stride_am + current_k[None, :] * stride_ak, + mask=current_k[None, :] < K, + other=0.0, + ) + b = tl.load( + b_ptr + current_k[:, None] * stride_bk + offs_n[None, :] * stride_bn, + mask=current_k[:, None] < K, + other=0.0, + ) + acc += tl.dot(a, b) +""").body[0] + + arg_names = [arg.arg for arg in func_ast.args.args] + parse_result = parse_cv_params( + func_ast, + parser_mode="mix", + arg_names=arg_names, + provided_args={}, + ) + + assert parse_result.dot_sites + assert parse_result.dot_sites[0].k.axis_symbol == "current_k" + assert parse_result.dot_sites[0].k.tunable_param == "BLOCK_K" + assert parse_result.dot_sites[0].k.length_expr == "K" + assert parse_result.dot_sites[0].k.state == "tunable" + + +@triton.autotune( + configs=[], key=["n_elements"], hints={ + "axes": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SIZE_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + }) +@triton.jit +def add_kernel( + x_ptr, + y_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + BLOCK_SIZE_SUB: tl.constexpr, +): + offset = tl.program_id(0) * BLOCK_SIZE + loops1 = (BLOCK_SIZE + BLOCK_SIZE_SUB - 1) // BLOCK_SIZE_SUB + for loop in range(0, loops1): + x0 = offset + loop * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE_SUB) + mask = x0 < n_elements + x = tl.load(x_ptr + x0, mask) + y = tl.load(y_ptr + x0, mask) + output = x + y + tl.store(output_ptr + x0, output) + + +def add_torch(x, y): + return x + y + + +def add_autotune(x, y): + output = torch.empty_like(x) + n_elements = output.numel() + add_kernel[lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]), )](x, y, output, n_elements) + return output + + +@pytest.mark.autotune +@pytest.mark.parametrize('size', [ + 2048, +]) +def test_add(size: int): + x = torch.rand(size, device="npu") + y = torch.rand(size, device="npu") + + output_torch = add_torch(x, y) + output_triton = add_autotune(x, y) + assert torch.allclose(output_triton, output_torch) + + +@pytest.mark.autotune +def test_add_no_reduction_axes(): + with pytest.raises(ValueError, match="reduction_axes must be a list"): + + @triton.autotune( + configs=[], key=["n_elements"], hints={ + "axes": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SIZE_SUB"}, + "low_dim_axes": ["x"], + }) + @triton.jit + def add_kernel_exception( + x_ptr, + y_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + BLOCK_SIZE_SUB: tl.constexpr, + ): + pass + + +@pytest.mark.autotune +def test_add_no_low_dim_axes(): + with pytest.raises(ValueError, match="low_dim_axes must be a list"): + + @triton.autotune( + configs=[], key=["n_elements"], hints={ + "axes": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SIZE_SUB"}, + "reduction_axes": [], + }) + @triton.jit + def add_kernel_exception( + x_ptr, + y_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + BLOCK_SIZE_SUB: tl.constexpr, + ): + pass + + +@pytest.mark.autotune +def test_add_no_tiling_params(): + with pytest.raises(ValueError, match="tiling_params must be a dict"): + + @triton.autotune( + configs=[], key=["n_elements"], hints={ + "axes": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + }) + @triton.jit + def add_kernel_exception( + x_ptr, + y_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + BLOCK_SIZE_SUB: tl.constexpr, + ): + pass + + +@pytest.mark.autotune +def test_add_no_split_params(): + with pytest.raises(ValueError, match="split_params must be a dict"): + + @triton.autotune( + configs=[], key=["n_elements"], hints={ + "axes": {"x": "n_elements"}, + "tiling_params": {"x": "BLOCK_SIZE_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + }) + @triton.jit + def add_kernel_exception( + x_ptr, + y_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + BLOCK_SIZE_SUB: tl.constexpr, + ): + pass + + +@pytest.mark.autotune +def test_add_invalid_hints_axes_name(): + with pytest.raises(ValueError, match="All keys in 'hints.axes' must be valid axis names"): + + @triton.autotune( + configs=[], key=["n_elements"], hints={ + "axes": {"x0": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SIZE_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + }) + @triton.jit + def add_kernel_exception(): + pass + + +def _materialize_expanded_configs(kernel): + x = torch.zeros(1, dtype=torch.float32) + output = torch.empty_like(x) + kernel.cache = {} + kernel.generate_key_and_configs(x, output, x.numel()) + return kernel.configs + + +def test_expand_explicit_configs_with_hints(): + base_configs = [ + triton.Config({"BLOCK_SIZE": 128}), + triton.Config({"BLOCK_SIZE": 256}), + ] + + @triton.autotune(configs=base_configs, key=["n_elements"], hints={ + "GROUP_M": [2, 4, 8], + }) + @triton.jit + def kernel_with_group_hint( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + GROUP_M: tl.constexpr, + ): + pass + + assert len(kernel_with_group_hint.configs) == 2 + expanded_configs = _materialize_expanded_configs(kernel_with_group_hint) + assert len(expanded_configs) == 12 + actual_triplets = {(cfg.kwargs["BLOCK_SIZE"], cfg.kwargs["GROUP_M"], cfg.num_stages) for cfg in expanded_configs} + expected_triplets = { + (128, 2, 1), + (128, 2, 2), + (128, 4, 1), + (128, 4, 2), + (128, 8, 1), + (128, 8, 2), + (256, 2, 1), + (256, 2, 2), + (256, 4, 1), + (256, 4, 2), + (256, 8, 1), + (256, 8, 2), + } + assert actual_triplets == expected_triplets + + +def test_expand_hints_multibuffer_maps_to_num_stages(): + + @triton.autotune(configs=[triton.Config({"BLOCK_SIZE": 128})], key=["n_elements"], hints={ + "GROUP_M": [2], + "multibuffer": [True, False], + }) + @triton.jit + def kernel_with_multibuffer_alias( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + GROUP_M: tl.constexpr, + ): + pass + + expanded_configs = _materialize_expanded_configs(kernel_with_multibuffer_alias) + assert len(expanded_configs) == 2 + assert {cfg.num_stages for cfg in expanded_configs} == {1, 2} + assert all("multibuffer" not in cfg.kwargs for cfg in expanded_configs) + assert kernel_with_multibuffer_alias.config_hints == { + "GROUP_M": [2], + "num_stages": [2, 1], + } + + +def test_expand_hints_explicit_num_stages_precedes_multibuffer(): + + @triton.autotune(configs=[triton.Config({"BLOCK_SIZE": 128})], key=["n_elements"], hints={ + "GROUP_M": [2, 4], + "num_stages": [1], + "multibuffer": [True, False], + }) + @triton.jit + def kernel_num_stages_precedence( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + GROUP_M: tl.constexpr, + ): + pass + + expanded_configs = _materialize_expanded_configs(kernel_num_stages_precedence) + assert len(expanded_configs) == 2 + assert {cfg.num_stages for cfg in expanded_configs} == {1} + assert kernel_num_stages_precedence.config_hints == { + "GROUP_M": [2, 4], + "num_stages": [1], + } + + +def test_expand_explicit_configs_with_mixed_hints(): + base_configs = [ + triton.Config({"BLOCK_SIZE": 128}), + ] + + @triton.autotune(configs=base_configs, key=["n_elements"], hints={ + "GROUP_M": [2, 4], + "num_stages": [1, 2], + "enable_ubuf_saving": [True, False], + }) + @triton.jit + def kernel_with_mixed_hints( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + GROUP_M: tl.constexpr, + ): + pass + + expanded_configs = _materialize_expanded_configs(kernel_with_mixed_hints) + assert len(expanded_configs) == 8 + assert {cfg.num_stages for cfg in expanded_configs} == {1, 2} + assert {cfg.kwargs["GROUP_M"] for cfg in expanded_configs} == {2, 4} + assert {cfg.kwargs["enable_ubuf_saving"] for cfg in expanded_configs} == {True, False} + + +def test_expand_hints_coexist_with_axis_hints(): + base_configs = [ + triton.Config({"BLOCK_SIZE": 128, "BLOCK_SIZE_SUB": 32}), + triton.Config({"BLOCK_SIZE": 256, "BLOCK_SIZE_SUB": 64}), + ] + + @triton.autotune( + configs=base_configs, key=["n_elements"], hints={ + "axes": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SIZE_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + "GROUP_M": [2, 4], + }) + @triton.jit + def kernel_with_axis_hints( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + BLOCK_SIZE_SUB: tl.constexpr, + GROUP_M: tl.constexpr, + ): + pass + + expanded_configs = _materialize_expanded_configs(kernel_with_axis_hints) + assert len(expanded_configs) == 8 + assert {cfg.num_stages for cfg in expanded_configs} == {1, 2} + assert kernel_with_axis_hints.hints == { + "axes": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SIZE_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + assert kernel_with_axis_hints.config_hints == { + "GROUP_M": [2, 4], + "num_stages": [1, 2], + } + + +def test_expand_hints_defer_explicit_configs_until_runtime(): + + @triton.autotune(configs=[triton.Config({"BLOCK_SIZE": 128})], key=["n_elements"], hints={ + "GROUP_M": [2, 4], + }) + @triton.jit + def kernel_defer_explicit_configs( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + GROUP_M: tl.constexpr, + ): + pass + + assert len(kernel_defer_explicit_configs.configs) == 1 + assert kernel_defer_explicit_configs.config_hints == { + "GROUP_M": [2, 4], + "num_stages": [1, 2], + } + + expanded_configs = _materialize_expanded_configs(kernel_defer_explicit_configs) + assert len(expanded_configs) == 4 + assert {(cfg.kwargs["GROUP_M"], cfg.num_stages) + for cfg in expanded_configs} == { + (2, 1), + (2, 2), + (4, 1), + (4, 2), + } + + +def test_expand_hints_invalid_key(): + with pytest.raises(ValueError, match="Unsupported hints keys"): + + @triton.autotune(configs=[triton.Config({"BLOCK_SIZE": 128})], key=["n_elements"], hints={ + "INVALID_KEY": [1, 2], + }) + @triton.jit + def kernel_invalid_hint_key( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + ): + pass + + +def test_expand_hints_invalid_value_container(): + + @triton.autotune(configs=[triton.Config({"BLOCK_SIZE": 128})], key=["n_elements"], hints={ + "GROUP_M": 4, + }) + @triton.jit + def kernel_invalid_hint_value( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + GROUP_M: tl.constexpr, + ): + pass + + with pytest.raises(ValueError, match="must be a non-empty list/tuple"): + _materialize_expanded_configs(kernel_invalid_hint_value) + + +def test_expand_hints_invalid_multibuffer_values(): + with pytest.raises(ValueError, match="must contain only boolean values"): + + @triton.autotune(configs=[triton.Config({"BLOCK_SIZE": 128})], key=["n_elements"], hints={ + "multibuffer": [1, 0], + }) + @triton.jit + def kernel_invalid_multibuffer_hint( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + ): + pass + + +def test_expand_hints_invalid_compile_option_value(): + + @triton.autotune(configs=[triton.Config({"BLOCK_SIZE": 128})], key=["n_elements"], hints={ + "num_stages": [3], + }) + @triton.jit + def kernel_invalid_compile_hint( + x_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + ): + pass + + with pytest.raises(ValueError, match="Invalid value for 'num_stages'"): + _materialize_expanded_configs(kernel_invalid_compile_hint) + + +def test_expand_hints_defer_when_only_auto_generated_configs_exist(): + + @triton.autotune(configs=[], key=["n_elements"], hints={ + "GROUP_M": [2, 4], + }) + @triton.jit + def kernel_require_configs( + x_ptr, + output_ptr, + n_elements, + GROUP_M: tl.constexpr, + ): + pass + + assert len(kernel_require_configs.configs) == 1 + assert all("GROUP_M" not in cfg.kwargs for cfg in kernel_require_configs.configs) + assert kernel_require_configs.config_hints == { + "GROUP_M": [2, 4], + } + + +def test_non_simt_num_stages_candidates_priority(): + tuner = object.__new__(ascend_autotuner.AutoTilingTuner) + tuner.user_specified_num_stages = None + tuner.user_specified_multibuffer = None + + assert tuner._get_non_simt_num_stages_candidates() == [1, 2] + + tuner.user_specified_multibuffer = True + assert tuner._get_non_simt_num_stages_candidates() == [2] + + tuner.user_specified_num_stages = 1 + assert tuner._get_non_simt_num_stages_candidates() == [1] + + +def test_expand_simt_num_warps_configs_default_candidates(): + tuner = object.__new__(ascend_autotuner.AutoTilingTuner) + tuner.user_specified_warps = None + tuner.print_autotuning = False + + expanded_configs = tuner._expand_simt_num_warps_configs([triton.Config({"BLOCK_SIZE": 128})]) + + assert len(expanded_configs) == 4 + assert [cfg.num_warps for cfg in expanded_configs] == [8, 16, 32, 64] + + +@pytest.mark.parametrize( + "axis", + [ + "x", + "y", + "z", + ], +) +def test_normalize_reduction_axis_name_accepts_base_axes_only(axis): + namespace = _load_autotuner_methods("_normalize_reduction_axis_name") + + result = _normalize_loaded_method(namespace["_normalize_reduction_axis_name"])(axis) + + assert result == axis + + +@pytest.mark.parametrize("axis", ["rx", "ry", "rry"]) +def test_normalize_reduction_axis_name_rejects_prefixed_axes(axis): + namespace = _load_autotuner_methods("_normalize_reduction_axis_name") + + with pytest.raises(ValueError, match="r-prefixed"): + _normalize_loaded_method(namespace["_normalize_reduction_axis_name"])(axis) + + +def test_autoparse_reduction_axes_rejects_prefixed_parser_output(): + namespace = _load_autotuner_methods( + "_normalize_reduction_axis_name", + "_normalize_reduction_axes", + "_get_axis_base_name", + "_autoparse_reduction_axes", + ) + + class StubReductionAxesParser: + + def __init__(self, func_ast, axis_arg_names): + self.func_ast = func_ast + self.axis_arg_names = axis_arg_names + + def parse(self): + return ["ry"] + + namespace["ReductionAxesParser"] = StubReductionAxesParser + + promoted_axes = [] + refresh_calls = [] + + tuner = SimpleNamespace( + fn=SimpleNamespace(parse=lambda: "fake-func-ast"), + axis_arg_names={"x": "seq_len", "y": "dim"}, + reduction_axes=[], + print_autotuning=False, + _get_parser_axis_arg_names=lambda: {"x": "seq_len", "y": "dim"}, + _promote_axis_arg_name_to_reduction=lambda axis: promoted_axes.append(axis), + _refresh_vector_axes=lambda: refresh_calls.append(True), + ) + tuner._get_axis_base_name = _normalize_loaded_method(namespace["_get_axis_base_name"]) + tuner._normalize_reduction_axis_name = _normalize_loaded_method(namespace["_normalize_reduction_axis_name"]) + tuner._normalize_reduction_axes = _normalize_loaded_method(namespace["_normalize_reduction_axes"]).__get__( + tuner, SimpleNamespace) + + with pytest.raises(ValueError, match="r-prefixed"): + _normalize_loaded_method(namespace["_autoparse_reduction_axes"])(tuner) + + assert promoted_axes == [] + assert tuner.reduction_axes == [] + assert refresh_calls == [] + + +def test_inject_grid_num_tiles_uses_only_static_grid_and_preserves_internal_value(): + namespace = _load_autotuner_methods("_inject_grid_num_tiles") + inject_grid_num_tiles = _normalize_loaded_method(namespace["_inject_grid_num_tiles"]) + + static_grid = {"grid": (2, 16)} + inject_grid_num_tiles(static_grid) + assert static_grid["grid_num_tiles"] == 16 + assert isinstance(static_grid["grid_num_tiles"], ascend_autotuner._InternalNPUOptionInt) + + # Only the first three launch dimensions are visible to the compiler; use + # the outermost one among those, matching the ChunkCoalescing contract. + four_dim_grid = {"grid": [2, 3, 32, 64]} + inject_grid_num_tiles(four_dim_grid) + assert four_dim_grid["grid_num_tiles"] == 32 + + callable_grid = {"grid": lambda _: (2, 16)} + inject_grid_num_tiles(callable_grid) + assert "grid_num_tiles" not in callable_grid + + internal_hint = { + "grid": (2, 16), + "grid_num_tiles": ascend_autotuner._InternalNPUOptionInt(99), + } + inject_grid_num_tiles(internal_hint) + assert internal_hint["grid_num_tiles"] == 99 + + +def test_make_kernel_call_extracts_name_from_jit_run(): + namespace = _load_autotuner_methods("_make_kernel_call") + _make_kernel_call = _normalize_loaded_method(namespace["_make_kernel_call"]) + + @triton.jit + def test_kernel_jit(x_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(0) + offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + tl.store(x_ptr + offsets, x, mask=mask) + + fake_self = SimpleNamespace(fn=test_kernel_jit, pre_hook=lambda full_nargs: None, + post_hook=lambda full_nargs, exception=None: None, nargs={}) + fake_config = SimpleNamespace(kwargs={"BLOCK_SIZE": 32}, all_kwargs=lambda: {"BLOCK_SIZE": 32}, pre_hook=None) + + x = torch.zeros(128, dtype=torch.float32, device="npu") + kernel_call_closure = _make_kernel_call(fake_self, x, x.numel(), config=fake_config, grid=(1, )) + kernel_call_closure(warmup=False) + + assert kernel_call_closure.target_kernel_name == "test_kernel_jit" + + +def test_make_kernel_call_extracts_name_from_libentry_tuple(): + namespace = _load_autotuner_methods("_make_kernel_call") + _make_kernel_call = _normalize_loaded_method(namespace["_make_kernel_call"]) + + from triton.runtime.libentry import libentry + + @libentry() + @triton.jit + def test_kernel_libentry(x_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(0) + offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + tl.store(x_ptr + offsets, x, mask=mask) + + fake_self = SimpleNamespace(fn=test_kernel_libentry, pre_hook=lambda full_nargs: None, + post_hook=lambda full_nargs, exception=None: None, nargs={}) + fake_config = SimpleNamespace(kwargs={"BLOCK_SIZE": 32}, all_kwargs=lambda: {"BLOCK_SIZE": 32}, pre_hook=None) + + x = torch.zeros(128, dtype=torch.float32, device="npu") + kernel_call_closure = _make_kernel_call(fake_self, x, x.numel(), config=fake_config, grid=(1, )) + kernel_call_closure(warmup=False) + + assert kernel_call_closure.target_kernel_name == "test_kernel_libentry" diff --git a/third_party/ascend/unittest/autotune_ut/test_common.py b/third_party/ascend/unittest/autotune_ut/test_common.py new file mode 100644 index 0000000000..9522369557 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_common.py @@ -0,0 +1,161 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest.mock as mock + +import pytest +import torch + + +def _serialize_vector_axes(vector_axes): + if vector_axes is None: + return { + "axis_length_exprs": {}, + "fixed_tiling_exprs": {}, + "split_params": {}, + "tiling_params": {}, + "low_dim_axes": [], + "reduction_axes": [], + } + return { + "axis_length_exprs": dict(getattr(vector_axes, "axis_length_exprs", {}) or {}), + "fixed_tiling_exprs": dict(getattr(vector_axes, "fixed_tiling_exprs", {}) or {}), + "split_params": dict(getattr(vector_axes, "split_params", {}) or {}), + "tiling_params": dict(getattr(vector_axes, "tiling_params", {}) or {}), + "low_dim_axes": list(getattr(vector_axes, "low_dim_axes", []) or []), + "reduction_axes": list(getattr(vector_axes, "reduction_axes", []) or []), + } + + +def _get_semantic_axes_state(result: dict): + vector_axes = result.get("vector_axes") or {} + if vector_axes.get("axis_length_exprs"): + return { + "keys": dict(vector_axes["axis_length_exprs"]), + "split_params": dict(vector_axes.get("split_params", {}) or {}), + "tiling_params": dict(vector_axes.get("tiling_params", {}) or {}), + "low_dim_axes": list(vector_axes.get("low_dim_axes", []) or []), + "reduction_axes": list(vector_axes.get("reduction_axes", []) or []), + } + + keys = result.get("keys") + return { + "keys": dict(keys or {}) if isinstance(keys, dict) else {}, + "split_params": dict(result.get("split_params", {}) or {}), + "tiling_params": dict(result.get("tiling_params", {}) or {}), + "low_dim_axes": list(result.get("low_dim_axes", []) or []), + "reduction_axes": list(result.get("reduction_axes", []) or []), + } + + +def MockAutoTilingTunerRun(self, *args, **kwargs): + self.nargs = dict(zip(self.arg_names, args)) + + # generate key + all_args = {**self.nargs, **kwargs} + try: + self._autoparse_axis_params(all_args) + except ValueError as e: + if "Missing required arguments" in str(e): + pass + else: + raise + vector_axes = _serialize_vector_axes(getattr(self, "vector_axes", None)) + has_semantic_axes = bool(vector_axes["axis_length_exprs"]) + semantic_state = _get_semantic_axes_state({"keys": getattr(self, "keys", None), "vector_axes": vector_axes}) + return { + "keys": semantic_state["keys"] if has_semantic_axes else self.keys, + "vector_axes": vector_axes, + "split_params": semantic_state["split_params"] if has_semantic_axes else self.split_params, + "tiling_params": semantic_state["tiling_params"] if has_semantic_axes else self.tiling_params, + "low_dim_axes": semantic_state["low_dim_axes"] if has_semantic_axes else self.low_dim_axes, + "reduction_axes": semantic_state["reduction_axes"] if has_semantic_axes else self.reduction_axes, + "persistent_reduction": self.persistent_reduction, + "cv_parse_result": getattr(self, "cv_parse_result", None), + "vv_parse_result_v2": getattr(self, "vv_parse_result_v2", None), + "vv_adapter_result_v2": getattr(self, "vv_adapter_result_v2", None), + } + + +def check_axes_parse_res(act: dict, ref: dict): + """ + Compare two axes parse results that may use different symbolic axis names, + but map to the same semantic dimensions via the semantic axis view. + """ + ref_state = _get_semantic_axes_state(ref) + act_state = _get_semantic_axes_state(act) + ref_axis_lengths = ref_state["keys"] + act_axis_lengths = act_state["keys"] + + assert set(ref_axis_lengths.values()) == set(act_axis_lengths.values()), \ + f"Semantic dimensions mismatch: ref={set(ref_axis_lengths.values())}, act={set(act_axis_lengths.values())}" + + def resolve_symbol(sym: str, sym_to_sem: dict) -> str: + if sym in sym_to_sem: + return sym + if isinstance(sym, str) and sym.startswith("r") and sym[1:] in sym_to_sem: + return sym[1:] + raise KeyError(sym) + + def normalize_param_dict(param_dict: dict, sym_to_sem: dict) -> dict: + """Convert {symbol: value} -> {semantic: value}""" + return {sym_to_sem[resolve_symbol(sym, sym_to_sem)]: value for sym, value in param_dict.items()} + + ref_split = normalize_param_dict(ref_state["split_params"], ref_axis_lengths) + act_split = normalize_param_dict(act_state["split_params"], act_axis_lengths) + + ref_tiling = normalize_param_dict(ref_state["tiling_params"], ref_axis_lengths) + act_tiling = normalize_param_dict(act_state["tiling_params"], act_axis_lengths) + + def normalize_axis_list(axis_list: list, sym_to_sem: dict) -> list: + return sorted(sym_to_sem[resolve_symbol(sym, sym_to_sem)] for sym in axis_list) + + ref_low = normalize_axis_list(ref_state["low_dim_axes"], ref_axis_lengths) + act_low = normalize_axis_list(act_state["low_dim_axes"], act_axis_lengths) + + ref_red = normalize_axis_list(ref_state["reduction_axes"], ref_axis_lengths) + act_red = normalize_axis_list(act_state["reduction_axes"], act_axis_lengths) + + # Compare normalized structures + assert ref_split == act_split, f"split_params mismatch: {ref_split} vs {act_split}" + assert ref_tiling == act_tiling, f"tiling_params mismatch: {ref_tiling} vs {act_tiling}" + assert ref_low == act_low, f"low_dim_axes mismatch: {ref_low} vs {act_low}" + assert ref_red == act_red, f"reduction_axes mismatch: {ref_red} vs {act_red}" + + +@pytest.fixture +def mock_autotuner(): + with mock.patch("triton.backends.ascend.runtime.autotuner.AutoTilingTuner.run", new=MockAutoTilingTunerRun): + yield + + +def generate_tensor(shape, dtype): + if dtype == 'float32' or dtype == 'float16' or dtype == 'bfloat16': + return torch.randn(size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16': + return torch.randint(low=0, high=2000, size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'int8': + return torch.randint(low=0, high=127, size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'bool': + return torch.randint(low=0, high=2, size=shape).bool() + elif dtype == 'uint8': + return torch.randint(low=0, high=255, size=shape, dtype=torch.uint8) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) diff --git a/third_party/ascend/unittest/autotune_ut/test_compile_failure_cache.py b/third_party/ascend/unittest/autotune_ut/test_compile_failure_cache.py new file mode 100644 index 0000000000..858ef1064c --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_compile_failure_cache.py @@ -0,0 +1,258 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import threading +from collections import defaultdict +from types import MethodType, SimpleNamespace + +import pytest +import triton +import triton.language as tl +import triton.backends.ascend.runtime.autotuner as ascend_autotuner +from triton.backends.ascend.runtime.autotuner import AutoTilingTuner +from triton.runtime.autotuner import Config +from triton.runtime.errors import OutOfResources + + +@triton.jit +def _compile_key_kernel( + x, + m, + BLOCK_SIZE: tl.constexpr, + EVEN_M: tl.constexpr, +): + pass + + +def _make_compile_key_tuner(monkeypatch, wrapped_fn): + tuner = object.__new__(AutoTilingTuner) + tuner.fn = wrapped_fn + tuner.compile_parallel = False + tuner.user_defined_pre_hook = False + + binder_calls = [] + + def binder(*args, **kwargs): + binder_calls.append(dict(kwargs)) + # Model m as a do_not_specialize runtime argument. Only constexpr + # values injected by Config/Heuristics participate in specialization. + specialization = [ + ("constexpr", kwargs["BLOCK_SIZE"]), + ("constexpr", kwargs["EVEN_M"]), + ] + options = tuple((name, repr(kwargs.get(name))) for name in ( + "num_warps", + "num_ctas", + "num_stages", + "debug", + "instrumentation_mode", + )) + return {}, specialization, options + + monkeypatch.setattr( + triton.runtime.driver, + "_active", + SimpleNamespace(get_current_device=lambda: 0), + ) + monkeypatch.setattr(ascend_autotuner, "get_cache_invalidating_env_vars", lambda: {}) + monkeypatch.delenv("TRITON_ENABLE_UBTUNER", raising=False) + monkeypatch.setattr(_compile_key_kernel, "hash", "test-jit-cache-key") + monkeypatch.setattr(_compile_key_kernel, "debug", None) + monkeypatch.setattr(_compile_key_kernel, "pre_run_hooks", []) + monkeypatch.setattr( + _compile_key_kernel, + "device_caches", + {0: ({}, {}, "test-target", None, binder)}, + ) + return tuner, binder_calls + + +def test_compile_failure_key_supports_direct_jit_function(monkeypatch): + tuner, binder_calls = _make_compile_key_tuner(monkeypatch, _compile_key_kernel) + config = Config({"BLOCK_SIZE": 128, "EVEN_M": True}) + + compile_key = tuner._get_jit_compile_cache_key( + object(), + 16, + config=config, + grid=(1, ), + ) + + assert compile_key is not None + assert len(binder_calls) == 1 + assert binder_calls[0]["BLOCK_SIZE"] == 128 + assert binder_calls[0]["EVEN_M"] is True + assert "grid" not in binder_calls[0] + assert "warmup" not in binder_calls[0] + + +def test_compile_failure_key_replays_heuristics_before_binder(monkeypatch): + heuristic_calls = [] + + def even_m(args): + heuristic_calls.append(dict(args)) + return args["m"] % 2 == 0 + + wrapped_fn = triton.runtime.Heuristics( + _compile_key_kernel, + _compile_key_kernel.arg_names, + {"EVEN_M": even_m}, + ) + tuner, binder_calls = _make_compile_key_tuner(monkeypatch, wrapped_fn) + config = Config({"BLOCK_SIZE": 128}) + + even_key = tuner._get_jit_compile_cache_key( + object(), + 16, + config=config, + grid=(1, ), + ) + odd_key = tuner._get_jit_compile_cache_key( + object(), + 17, + config=config, + grid=(1, ), + ) + second_even_key = tuner._get_jit_compile_cache_key( + object(), + 18, + config=config, + grid=(1, ), + ) + + assert even_key is not None + assert odd_key is not None + assert second_even_key is not None + assert even_key == second_even_key + assert even_key != odd_key + assert [call["EVEN_M"] for call in binder_calls] == [True, False, True] + assert heuristic_calls[0]["BLOCK_SIZE"] == 128 + assert heuristic_calls[0]["grid"] == (1, ) + assert heuristic_calls[0]["warmup"] is False + assert "grid" not in binder_calls[0] + assert "warmup" not in binder_calls[0] + + +def test_compile_failure_key_rejects_unknown_wrapper(monkeypatch): + + class UnknownWrapper: + + def __init__(self, fn): + self.fn = fn + + tuner, binder_calls = _make_compile_key_tuner( + monkeypatch, + UnknownWrapper(_compile_key_kernel), + ) + config = Config({"BLOCK_SIZE": 128, "EVEN_M": True}) + + compile_key = tuner._get_jit_compile_cache_key( + object(), + 16, + config=config, + grid=(1, ), + ) + + assert compile_key is None + assert binder_calls == [] + + +def test_compile_failure_key_is_disabled_for_jit_pre_run_hook(monkeypatch): + tuner, binder_calls = _make_compile_key_tuner(monkeypatch, _compile_key_kernel) + monkeypatch.setattr(_compile_key_kernel, "pre_run_hooks", [lambda *args, **kwargs: None]) + config = Config({"BLOCK_SIZE": 128, "EVEN_M": True}) + + compile_key = tuner._get_jit_compile_cache_key( + object(), + 16, + config=config, + grid=(1, ), + ) + + assert compile_key is None + assert binder_calls == [] + + +def _make_batch_bench_tuner(): + tuner = object.__new__(AutoTilingTuner) + tuner.compile_parallel = False + tuner.enable_ubtuner = False + tuner.print_autotuning = False + tuner.user_defined_do_bench = True + tuner._compile_failure_cache = {} + tuner._compile_failure_cache_lock = threading.Lock() + tuner._cached_compile_failed_configs = [] + + kernel_calls = defaultdict(int) + + def get_compile_key(self, *args, config, **kwargs): + return ("compile-key", config.kwargs["ID"]) + + def make_kernel_call(self, *args, config, **kwargs): + + def kernel_call(warmup): + kernel_calls[config] += 1 + if config.kwargs["ID"] == "bad": + raise OutOfResources(2, 1, "UB") + return None + + return kernel_call + + def do_bench(fn, quantiles): + fn() + return 1.0 + + tuner._get_jit_compile_cache_key = MethodType(get_compile_key, tuner) + tuner._make_kernel_call = MethodType(make_kernel_call, tuner) + tuner.do_bench = do_bench + return tuner, kernel_calls + + +def test_batch_bench_skips_config_cached_as_compile_failure(): + tuner, kernel_calls = _make_batch_bench_tuner() + bad = Config({"ID": "bad"}) + good = Config({"ID": "good"}) + + first_result = tuner._batch_bench(configs=[bad, good]) + + assert first_result == {good: 1.0} + assert kernel_calls[bad] == 1 + assert tuner._compile_failure_cache[("compile-key", "bad")] == { + "exception_type": "OutOfResources", + } + assert tuner._cached_compile_failed_configs == [] + + second_result = tuner._batch_bench(configs=[bad, good]) + + assert second_result == {good: 1.0} + assert kernel_calls[bad] == 1 + assert tuner._compile_failed_configs == [] + assert tuner._cached_compile_failed_configs == [bad] + + +def test_batch_bench_raises_when_all_configs_are_cached_failures(): + tuner, kernel_calls = _make_batch_bench_tuner() + bad = Config({"ID": "bad"}) + tuner._remember_compile_failure(("compile-key", "bad"), OutOfResources(2, 1, "UB")) + + with pytest.raises(RuntimeError, match="All triton configs are cached compile failures"): + tuner._batch_bench(configs=[bad]) + + assert kernel_calls[bad] == 0 diff --git a/third_party/ascend/unittest/autotune_ut/test_customized_config.py b/third_party/ascend/unittest/autotune_ut/test_customized_config.py new file mode 100644 index 0000000000..3431109569 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_customized_config.py @@ -0,0 +1,91 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os + +import pytest +import torch +import torch_npu +import triton +import triton.backends.ascend.runtime +import triton.language as tl + +os.environ['TRITON_PRINT_AUTOTUNING'] = '0' + + +@triton.autotune( + configs=[ + triton.Config({'XBLOCK': 128, 'XBLOCK_SUB': 32}), + triton.Config({'XBLOCK': 128, 'XBLOCK_SUB': 64}), + triton.Config({'XBLOCK': 396, 'XBLOCK_SUB': 6}), + ], key=["n_rows", "n_cols"], hints={ + "auto_gen_config": False, + }) +@triton.jit +def softmax_kernel( + output_ptr, + input_ptr, + input_row_stride, + output_row_stride, + n_rows, + n_cols, + BLOCK_SIZE: tl.constexpr, + XBLOCK: tl.constexpr, + XBLOCK_SUB: tl.constexpr, +): + row_start = tl.program_id(0) * XBLOCK + for row_idx in tl.range(0, XBLOCK, XBLOCK_SUB): + row_offsets = row_start + row_idx + tl.arange(0, XBLOCK_SUB)[:, None] + col_offsets = tl.arange(0, BLOCK_SIZE)[None, :] + xmask = row_offsets < n_rows + ymask = col_offsets < n_cols + mask = xmask & ymask + input_ptrs = input_ptr + (row_offsets * input_row_stride + col_offsets) + row = tl.load(input_ptrs, mask=mask, other=-float("inf")) + row_minus_max = row - tl.max(row, axis=1).reshape(XBLOCK_SUB, 1).broadcast_to(XBLOCK_SUB, BLOCK_SIZE) + numerator = tl.exp(row_minus_max) + denominator = (tl.sum(numerator, axis=1).reshape(XBLOCK_SUB, 1).broadcast_to(XBLOCK_SUB, BLOCK_SIZE)) + softmax_output = numerator / denominator + output_ptrs = output_ptr + (row_offsets * output_row_stride + col_offsets) + tl.store(output_ptrs, softmax_output, mask=mask) + + +def softmax_torch(x): + return torch.softmax(x, axis=-1) + + +def softmax_autotune(x): + n_rows, n_cols = x.shape + BLOCK_SIZE = n_cols + y = torch.empty_like(x) + softmax_kernel[lambda meta: (triton.cdiv(n_rows, meta["XBLOCK"]), 1, 1)](y, x, x.stride(0), y.stride(0), n_rows, + n_cols, BLOCK_SIZE=BLOCK_SIZE) + return y + + +@pytest.mark.autotune +@pytest.mark.parametrize('shape,dtype', [ + ((16896, 1024), torch.float32), +]) +def test_softmax(shape, dtype): + x = torch.randn(shape, dtype=dtype, device="npu") + y_torch = softmax_torch(x) + y_triton = softmax_autotune(x) + torch.testing.assert_close(y_torch, y_triton, rtol=1e-03, atol=1e-03, equal_nan=True) diff --git a/third_party/ascend/unittest/autotune_ut/test_do_bench_compat.py b/third_party/ascend/unittest/autotune_ut/test_do_bench_compat.py new file mode 100644 index 0000000000..0c76d72542 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_do_bench_compat.py @@ -0,0 +1,423 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from types import MethodType, SimpleNamespace + +import pytest +import triton +import triton.backends.ascend.runtime.autotuner as ascend_autotuner +from triton.runtime.autotuner import Config +from triton.backends.ascend.runtime.autotuner import AutoTilingTuner + + +def _make_tuner(do_bench): + tuner = object.__new__(AutoTilingTuner) + tuner.compile_parallel = False + tuner.do_bench = do_bench + tuner.user_defined_do_bench = True + + def _make_kernel_call(self, *args, config, **meta): + + def kernel_call(warmup): + return None + + return kernel_call + + tuner._make_kernel_call = MethodType(_make_kernel_call, tuner) + return tuner + + +def _make_run_tuner(configs): + key = ("disk-cache-key", ) + tuner = object.__new__(AutoTilingTuner) + + def generate_key_and_configs(*args, **kwargs): + tuner.nargs = {} + return key + + tuner.arg_names = [] + tuner.cache = {} + tuner.is_simt_mode = False + tuner.generate_key_and_configs = generate_key_and_configs + tuner.prune_configs = lambda kwargs: configs + tuner.enable_ubtuner = False + tuner.cache_results = True + tuner.print_autotuning = False + tuner.auto_profile_dir = None + tuner.nargs = {} + tuner.pre_hook = lambda kwargs, reset_only=False: None + tuner.run_kwargs = [] + tuner.fn = SimpleNamespace(run=lambda *args, **kwargs: tuner.run_kwargs.append(kwargs) or "kernel-result") + return tuner, key + + +def test_batch_bench_supports_do_bench_with_quantiles(): + record = {} + + def _do_bench(fn, quantiles): + record["quantiles"] = quantiles + fn() + return (1.0, 1.0, 1.0) + + tuner = _make_tuner(_do_bench) + cfg = Config({}) + + result = tuner._batch_bench(configs=[cfg]) + + assert result[cfg] == (1.0, 1.0, 1.0) + assert record["quantiles"] == (0.5, 0.2, 0.8) + + +def test_batch_bench_requires_do_bench_quantiles_parameter(): + + def _do_bench(fn): + fn() + return (2.0, 2.0, 2.0) + + tuner = _make_tuner(_do_bench) + cfg = Config({}) + + with pytest.raises(TypeError): + tuner._batch_bench(configs=[cfg]) + + +def test_batch_bench_npu_env_respects_user_do_bench(monkeypatch): + calls = {"do_bench": 0} + + def _do_bench(fn, quantiles): + calls["do_bench"] += 1 + fn() + return (3.0, 3.0, 3.0) + + def _unexpected_do_bench_npu(*args, **kwargs): + raise AssertionError("do_bench_npu should not be used when user do_bench is provided") + + tuner = _make_tuner(_do_bench) + cfg0 = Config({"ID": 0}) + cfg1 = Config({"ID": 1}) + monkeypatch.setenv("TRITON_BENCH_METHOD", "npu") + monkeypatch.setattr("triton.backends.ascend.testing.do_bench_npu", _unexpected_do_bench_npu) + + result = tuner._batch_bench(configs=[cfg0, cfg1]) + + assert calls["do_bench"] == 2 + assert result[cfg0] == (3.0, 3.0, 3.0) + assert result[cfg1] == (3.0, 3.0, 3.0) + + +def test_batch_bench_npu_env_uses_do_bench_npu_without_user_do_bench(monkeypatch): + + def _do_bench(fn, quantiles): + raise AssertionError("self.do_bench should not be used when no user do_bench is provided") + + calls = {"do_bench_npu": 0} + + def _do_bench_npu(funcs, clear_l2_cache=False, warmup=5, active=30, target_kernel_name=None, **kwargs): + calls["do_bench_npu"] += 1 + assert len(funcs) == 2 + assert clear_l2_cache is True + return [1.0, 2.0] + + tuner = _make_tuner(_do_bench) + tuner.user_defined_do_bench = False + cfg0 = Config({"ID": 0}) + cfg1 = Config({"ID": 1}) + monkeypatch.setenv("TRITON_BENCH_METHOD", "npu") + monkeypatch.setattr("triton.backends.ascend.testing.do_bench_npu", _do_bench_npu) + + result = tuner._batch_bench(configs=[cfg0, cfg1]) + + assert calls["do_bench_npu"] == 1 + assert result[cfg0] == 1.0 + assert result[cfg1] == 2.0 + + +def test_autotilingtuner_marks_user_defined_do_bench(): + marker = {"called": False} + + def _do_bench(fn, quantiles): + marker["called"] = True + return (0.0, 0.0, 0.0) + + def _dummy_kernel(): + return None + + _dummy_kernel.arg_names = [] + + tuner = AutoTilingTuner( + _dummy_kernel, + [], + [Config({})], + [], + None, + None, + do_bench=_do_bench, + ) + + assert tuner.user_defined_do_bench is True + assert marker["called"] is False + + +def test_print_benchmark_results_formats_quantiles_and_selected_config(capsys): + + def _dummy_kernel(): + return None + + selected = Config({"BLOCK_SIZE": 128}, num_warps=4) + other = Config({"BLOCK_SIZE": 256}, num_warps=8) + tuner = object.__new__(AutoTilingTuner) + tuner.print_autotuning = True + tuner.base_fn = _dummy_kernel + tuner.best_config = selected + + tuner._print_benchmark_results({ + selected: (1.23456, 1.0, 1.8), + other: 2.34567, + }) + + output = capsys.readouterr().out + assert "Triton autotuning benchmark results for function _dummy_kernel:" in output + assert "config=BLOCK_SIZE: 128, num_warps: 4" in output + assert "p50=1.2346 ms, p20=1.0000 ms, p80=1.8000 ms [selected]" in output + assert "config=BLOCK_SIZE: 256, num_warps: 8" in output + assert "mean=2.3457 ms" in output + + +def test_print_benchmark_results_is_disabled_without_debug_flag(capsys): + tuner = object.__new__(AutoTilingTuner) + tuner.print_autotuning = False + + tuner._print_benchmark_results({Config({"BLOCK_SIZE": 128}): 1.0}) + + assert capsys.readouterr().out == "" + + +def test_run_prints_benchmark_results_after_tuning(capsys): + + def _dummy_kernel(): + return None + + selected = Config({"BLOCK_SIZE": 128}, num_warps=4) + other = Config({"BLOCK_SIZE": 256}, num_warps=8) + tuner, _ = _make_run_tuner([selected, other]) + tuner.cache_results = False + tuner.print_autotuning = True + tuner.base_fn = _dummy_kernel + tuner.arg_names = ["cache_key", "input"] + tuner.keys = ["cache_key"] + tuner.nargs = {"cache_key": "disk-cache-key", "input": SimpleNamespace(dtype="torch.float32")} + tuner.user_specified_multibuffer_mode = (2, 3) + tuner.generate_key_and_configs = lambda *args, **kwargs: ("disk-cache-key", "torch.float32", + ("compile_mode", "simd"), ("multibuffer_mode", (2, 3))) + tuner._batch_bench = lambda *args, configs, **kwargs: { + selected: (1.0, 0.9, 1.1), + other: (2.0, 1.8, 2.2), + } + + assert tuner.run() == "kernel-result" + output = capsys.readouterr().out + assert "with key as " not in output + assert ("with key fields as cache_key='disk-cache-key', input.dtype='torch.float32', " + "compile_mode='simd', multibuffer_mode=(2, 3),") in output + assert "Triton autotuning benchmark results for function _dummy_kernel:" in output + assert "p50=1.0000 ms, p20=0.9000 ms, p80=1.1000 ms [selected]" in output + + +def test_ascend_autotune_decorator_forwards_do_bench(monkeypatch): + import triton.backends.ascend.runtime.autotuner as ascend_autotuner + + captured = {} + + class DummyAutoTilingTuner: + + def __init__(self, *args, **kwargs): + captured["do_bench"] = kwargs.get("do_bench") + + monkeypatch.setattr(ascend_autotuner, "AutoTilingTuner", DummyAutoTilingTuner) + + def _dummy_kernel(): + return None + + _dummy_kernel.arg_names = [] + my_do_bench = lambda kernel_call, quantiles: (0.0, 0.0, 0.0) + + ascend_autotuner.autotune(configs=[object()], key=[], do_bench=my_do_bench)(_dummy_kernel) + + assert captured["do_bench"] is my_do_bench + + +def test_run_skips_gc_on_autotune_disk_cache_hit(monkeypatch): + configs = [Config({"BLOCK_SIZE": 16}), Config({"BLOCK_SIZE": 32})] + tuner, key = _make_run_tuner(configs) + gc_calls = [] + profile_calls = [] + + def check_disk_cache(tuning_key, pruned_configs, benchmark): + assert tuning_key == key + tuner.cache[tuning_key] = pruned_configs[0] + return True + + def unexpected_batch_bench(*args, **kwargs): + raise AssertionError("benchmark must not run on a disk-cache hit") + + tuner.check_disk_cache = check_disk_cache + tuner._batch_bench = unexpected_batch_bench + tuner.auto_profile_dir = "profile-output" + tuner._profile = lambda *args, config, **kwargs: profile_calls.append(config) + monkeypatch.setattr(ascend_autotuner.gc, "collect", lambda: gc_calls.append(True)) + + assert tuner.run() == "kernel-result" + assert gc_calls == [] + assert profile_calls == [] + + +def test_run_keeps_gc_on_autotune_disk_cache_miss(monkeypatch): + configs = [Config({"BLOCK_SIZE": 16}), Config({"BLOCK_SIZE": 32})] + tuner, key = _make_run_tuner(configs) + gc_calls = [] + benchmark_calls = [] + profile_calls = [] + + def check_disk_cache(tuning_key, pruned_configs, benchmark): + assert tuning_key == key + benchmark() + return False + + def batch_bench(*args, configs, **kwargs): + benchmark_calls.append(configs) + return {configs[0]: 1.0, configs[1]: 2.0} + + tuner.check_disk_cache = check_disk_cache + tuner._batch_bench = batch_bench + tuner.auto_profile_dir = "profile-output" + tuner._profile = lambda *args, config, **kwargs: profile_calls.append(config) + monkeypatch.setattr(ascend_autotuner.gc, "collect", lambda: gc_calls.append(True)) + + assert tuner.run() == "kernel-result" + assert benchmark_calls == [configs] + assert gc_calls == [True] + assert profile_calls == [configs[0]] + + assert tuner.run() == "kernel-result" + assert benchmark_calls == [configs] + assert gc_calls == [True] + assert profile_calls == [configs[0]] + + +def test_run_caches_single_config_and_skips_gc(monkeypatch): + config = Config({"BLOCK_SIZE": 16, "compile_mode": "simt_only"}) + tuner, key = _make_run_tuner([config]) + gc_calls = [] + profile_calls = [] + prune_calls = [] + + def prune_configs(kwargs): + prune_calls.append(kwargs) + return [config] + + def unexpected_disk_cache(*args, **kwargs): + raise AssertionError("single-config path must not probe disk cache") + + def unexpected_batch_bench(*args, **kwargs): + raise AssertionError("single-config path must not benchmark") + + tuner.prune_configs = prune_configs + tuner.check_disk_cache = unexpected_disk_cache + tuner._batch_bench = unexpected_batch_bench + tuner.auto_profile_dir = "profile-output" + tuner._profile = lambda *args, config, **kwargs: profile_calls.append(config) + monkeypatch.setattr(ascend_autotuner.gc, "collect", lambda: gc_calls.append(True)) + + assert tuner.run() == "kernel-result" + assert tuner.cache[key] is config + assert tuner.run() == "kernel-result" + assert len(prune_calls) == 1 + assert len(tuner.run_kwargs) == 2 + assert gc_calls == [] + assert profile_calls == [] + + +def test_run_does_not_cache_failed_single_config(monkeypatch): + config = Config({"BLOCK_SIZE": 16}) + tuner, key = _make_run_tuner([config]) + gc_calls = [] + prune_calls = [] + run_calls = [] + + def prune_configs(kwargs): + prune_calls.append(kwargs) + return [config] + + def run(*args, **kwargs): + run_calls.append(kwargs) + if len(run_calls) == 1: + raise RuntimeError("kernel failed") + return "kernel-result" + + def unexpected_disk_cache(*args, **kwargs): + raise AssertionError("single-config path must not probe disk cache") + + def unexpected_batch_bench(*args, **kwargs): + raise AssertionError("single-config path must not benchmark") + + tuner.prune_configs = prune_configs + tuner.fn = SimpleNamespace(run=run) + tuner.check_disk_cache = unexpected_disk_cache + tuner._batch_bench = unexpected_batch_bench + monkeypatch.setattr(ascend_autotuner.gc, "collect", lambda: gc_calls.append(True)) + + with pytest.raises(RuntimeError, match="kernel failed"): + tuner.run() + assert key not in tuner.cache + + assert tuner.run() == "kernel-result" + assert tuner.cache[key] is config + assert tuner.run() == "kernel-result" + assert len(prune_calls) == 2 + assert len(run_calls) == 3 + assert gc_calls == [] + + +def test_run_caches_single_config_after_pruning(monkeypatch): + config = Config({"BLOCK_SIZE": 16}) + tuner, key = _make_run_tuner([config, Config({"BLOCK_SIZE": 32})]) + gc_calls = [] + prune_calls = [] + + def prune_configs(kwargs): + prune_calls.append(kwargs) + return [config] + + def unexpected_disk_cache(*args, **kwargs): + raise AssertionError("single-config path must not probe disk cache") + + def unexpected_batch_bench(*args, **kwargs): + raise AssertionError("single-config path must not benchmark") + + tuner.prune_configs = prune_configs + tuner.check_disk_cache = unexpected_disk_cache + tuner._batch_bench = unexpected_batch_bench + monkeypatch.setattr(ascend_autotuner.gc, "collect", lambda: gc_calls.append(True)) + + assert tuner.run() == "kernel-result" + assert tuner.cache[key] is config + assert tuner.run() == "kernel-result" + assert len(prune_calls) == 1 + assert gc_calls == [] diff --git a/third_party/ascend/unittest/autotune_ut/test_ir_override.py b/third_party/ascend/unittest/autotune_ut/test_ir_override.py new file mode 100644 index 0000000000..8a4e989d33 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_ir_override.py @@ -0,0 +1,193 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Test `ir_override` for Ascend NPU compilation stages. + +Strategy (inspired by upstream test_autotuner.py): + 1. Compile a "donor" kernel (does ``×10``) with ``TRITON_KERNEL_DUMP=1`` + to obtain dumped IR files (``.ttir`` / ``.ttadapter`` / ``.bcmlir``). + 2. Replace the donor function name with the target function name in each + dumped file and save them as override inputs. + 3. Run a "target" kernel (identity: load → store) with ``ir_override`` + pointing to the renamed IR files. + 4. Verify the target output equals ``input × 10`` — proving that the + override actually replaced the compilation stage. +""" + +import os +import pathlib + +import pytest +import torch +import torch_npu + +import triton +import triton.backends.ascend.runtime # noqa: F401 – patches triton.autotune +import triton.language as tl + +# ── helpers ──────────────────────────────────────────────────────────── + + +def _find_dumped_files(dump_root: str): + """Walk *dump_root* and return ``{ext: filepath}`` for Ascend IR stages.""" + extensions = ("ttir", "ttadapter", "bcmlir") + found = {} + for dirpath, _, filenames in os.walk(dump_root): + for fn in filenames: + for ext in extensions: + if fn.endswith(f".{ext}"): + found[ext] = os.path.join(dirpath, fn) + return found + + +# ── test class ───────────────────────────────────────────────────────── + + +class TestIrOverride: + """ + End-to-end tests for ``ir_override`` at three Ascend compilation stages: + ``.ttir``, ``.ttadapter``, and ``.bcmlir``. + """ + + N = 1024 + DONOR_NAME = "_donor_mul10" + TARGET_NAME = "_target_identity" + + @pytest.fixture(scope="class") + def donor_ir(self, tmp_path_factory): + """Compile donor kernel once, dump its IR (shared across all tests).""" + tmp_path = tmp_path_factory.mktemp("ir_override") + dump_dir = tmp_path / "dump" + dump_dir.mkdir() + + # Route dumped IR into a known, isolated directory. + # TRITON_ALWAYS_COMPILE forces recompilation so the stage loop + # runs and dumps every IR stage even when a cached compiled + # kernel already exists. + _old_dump = os.environ.get("TRITON_KERNEL_DUMP") + _old_dump_dir = os.environ.get("TRITON_DUMP_DIR") + _old_always = os.environ.get("TRITON_ALWAYS_COMPILE") + os.environ["TRITON_KERNEL_DUMP"] = "1" + os.environ["TRITON_DUMP_DIR"] = str(dump_dir) + os.environ["TRITON_ALWAYS_COMPILE"] = "1" + try: + # ── donor kernel: ×10 ── + @triton.autotune( + configs=[triton.Config({"BLOCK_SIZE": 32}, num_warps=4, num_stages=2, num_ctas=1)], + key=["N"], + do_bench=False, + hints={"auto_gen_config": False}, + ) + @triton.jit + def _donor_mul10(x_ptr, output_ptr, N, BLOCK_SIZE: tl.constexpr): + offsets = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < N + x = tl.load(x_ptr + offsets, mask=mask) + output = x * 10.0 + tl.store(output_ptr + offsets, output, mask=mask) + + x = torch.randn(self.N, device="npu") + out = torch.empty_like(x) + grid = lambda meta: (triton.cdiv(self.N, meta["BLOCK_SIZE"]), ) + _donor_mul10[grid](x, out, self.N) + torch.testing.assert_close(out, x * 10.0) + finally: + if _old_dump is not None: + os.environ["TRITON_KERNEL_DUMP"] = _old_dump + else: + os.environ.pop("TRITON_KERNEL_DUMP", None) + if _old_dump_dir is not None: + os.environ["TRITON_DUMP_DIR"] = _old_dump_dir + else: + os.environ.pop("TRITON_DUMP_DIR", None) + if _old_always is not None: + os.environ["TRITON_ALWAYS_COMPILE"] = _old_always + else: + os.environ.pop("TRITON_ALWAYS_COMPILE", None) + + # Return (dumped_files, tmp_path) for test methods. + return { + "dumped": _find_dumped_files(str(dump_dir)), + "tmp_path": tmp_path, + } + + # ── per-stage test runner ────────────────────────────────────────── + + def _run_override_test(self, donor_ir, ext: str): + """Run target kernel (identity) with ``ir_override`` from donor IR.""" + dumped = donor_ir["dumped"] + tmp_path = donor_ir["tmp_path"] + + src_path = dumped.get(ext) + if src_path is None: + pytest.skip(f"No dumped .{ext} file found " + f"(bytecode mode may be disabled or stage not reached)") + + # Copy donor IR and rename the function symbol inside. + content = pathlib.Path(src_path).read_text() + content = content.replace(self.DONOR_NAME, self.TARGET_NAME) + override_path = tmp_path / f"override.{ext}" + override_path.write_text(content) + + # ── target kernel: identity ── + @triton.autotune( + configs=[ + triton.Config( + {"BLOCK_SIZE": 32, "ir_override": str(override_path)}, + num_warps=4, + num_stages=2, + num_ctas=1, + ) + ], + key=["N"], + do_bench=False, + hints={"auto_gen_config": False}, + ) + @triton.jit + def _target_identity(x_ptr, output_ptr, N, BLOCK_SIZE: tl.constexpr): + offsets = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < N + x = tl.load(x_ptr + offsets, mask=mask) + tl.store(output_ptr + offsets, x, mask=mask) + + x = torch.randn(self.N, device="npu") + out = torch.empty_like(x) + grid = lambda meta: (triton.cdiv(self.N, meta["BLOCK_SIZE"]), ) + _target_identity[grid](x, out, self.N) + + # If override works → ×10. If silently ignored → identity. + torch.testing.assert_close(out, x * 10.0, msg=f"ir_override with .{ext} did not take effect") + + # ── test cases ───────────────────────────────────────────────────── + + @pytest.mark.autotune + def test_override_ttir(self, donor_ir): + """Override the ``.ttir`` compilation stage.""" + self._run_override_test(donor_ir, "ttir") + + @pytest.mark.autotune + def test_override_ttadapter(self, donor_ir): + """Override the ``.ttadapter`` compilation stage.""" + self._run_override_test(donor_ir, "ttadapter") + + @pytest.mark.autotune + def test_override_bcmlir(self, donor_ir): + """Override the ``.bcmlir`` compilation stage (bytecode → MLIR text).""" + self._run_override_test(donor_ir, "bcmlir") diff --git a/third_party/ascend/unittest/autotune_ut/test_low_dim_axes_parse.py b/third_party/ascend/unittest/autotune_ut/test_low_dim_axes_parse.py new file mode 100644 index 0000000000..e876dd5f5f --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_low_dim_axes_parse.py @@ -0,0 +1,223 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest.mock as mock + +from test_common import check_axes_parse_res, mock_autotuner + +import triton +import triton.language as tl + + +def _mock_single_reduction_axis(self): + self.vector_axes.ensure_axis("x").length_expr = "n_elements" + self.vector_axes.apply_semantic_fields(reduction_axes=["x"]) + return ["x"] + + +def _mock_apply_vv_with_low_dim_axes(orig_apply, low_dim_axes): + + def _mock(self): + applied = orig_apply(self) + self.low_dim_axes = list(low_dim_axes) + self._refresh_vector_axes() + return applied + + return _mock + + +def test_low_dim_axis_parse_base_case1(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_low_dim_axis_parse_base_case1(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE # <- Separate assignment + + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + + tl.store(output_ptr + offsets, output, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_low_dim_axis_parse_base_case1[grid]() + + check_axes_parse_res(act_res, ref_res) + + +def test_low_dim_axis_parse_empty_is_non_fatal(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_low_dim_axis_parse_empty_case(x_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + mask = block_start < n_elements + + _ = tl.load(x_ptr + block_start, mask=mask, other=0) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_low_dim_axis_parse_empty_case[grid]() + + check_axes_parse_res(act_res, ref_res) + + +def test_low_dim_axis_parse_empty_no_persistent_reduction_index_error(mock_autotuner): + import triton.backends.ascend.runtime + from triton.backends.ascend.runtime.autotuner import AutoTilingTuner + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_low_dim_axis_parse_guard_case(x_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + mask = block_start < n_elements + + _ = tl.load(x_ptr + block_start, mask=mask, other=0) + + with mock.patch.object(AutoTilingTuner, + "_autoparse_reduction_axes", new=_mock_single_reduction_axis), mock.patch.object( + AutoTilingTuner, "_autoparse_low_dim_axes", return_value=[]), mock.patch.object( + AutoTilingTuner, "_autoparse_split_params", + return_value={}), mock.patch.object(AutoTilingTuner, "_autoparse_tiling_params", + return_value={}): + act_res = triton_low_dim_axis_parse_guard_case[(1, )]() + + assert act_res["low_dim_axes"] == ["x"] + assert act_res["reduction_axes"] == ["x"] + + +def test_persistent_reduction_inner_axis_threshold(mock_autotuner): + import triton.backends.ascend.runtime + from triton.backends.ascend.runtime.autotuner import AutoTilingTuner + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_persistent_reduction_inner_axis_case(x_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + mask = block_start < n_elements + _ = tl.load(x_ptr + block_start, mask=mask, other=0) + + orig_apply_vv = AutoTilingTuner._apply_vv_axis_semantic_result + with mock.patch.object( + AutoTilingTuner, + "_apply_vv_axis_semantic_result", + new=_mock_apply_vv_with_low_dim_axes(orig_apply_vv, ["x"]), + ), mock.patch.object(AutoTilingTuner, + "_autoparse_reduction_axes", new=_mock_single_reduction_axis), mock.patch.object( + AutoTilingTuner, "_autoparse_low_dim_axes", return_value=["x"]), mock.patch.object( + AutoTilingTuner, "_autoparse_split_params", + return_value={}), mock.patch.object(AutoTilingTuner, "_autoparse_tiling_params", + return_value={}): + act_res = triton_persistent_reduction_inner_axis_case[(1, )](None, 1024) + assert act_res["persistent_reduction"] is True + + +def test_persistent_reduction_outer_axis_threshold(mock_autotuner): + import triton.backends.ascend.runtime + from triton.backends.ascend.runtime.autotuner import AutoTilingTuner + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_persistent_reduction_outer_axis_case(x_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + mask = block_start < n_elements + _ = tl.load(x_ptr + block_start, mask=mask, other=0) + + orig_apply_vv = AutoTilingTuner._apply_vv_axis_semantic_result + with mock.patch.object( + AutoTilingTuner, + "_apply_vv_axis_semantic_result", + new=_mock_apply_vv_with_low_dim_axes(orig_apply_vv, []), + ), mock.patch.object(AutoTilingTuner, + "_autoparse_reduction_axes", new=_mock_single_reduction_axis), mock.patch.object( + AutoTilingTuner, "_autoparse_low_dim_axes", return_value=[]), mock.patch.object( + AutoTilingTuner, "_autoparse_split_params", + return_value={}), mock.patch.object(AutoTilingTuner, "_autoparse_tiling_params", + return_value={}): + act_res = triton_persistent_reduction_outer_axis_case[(1, )](None, 64) + assert act_res["persistent_reduction"] is True + + +def test_persistent_reduction_outer_axis_over_threshold(mock_autotuner): + import triton.backends.ascend.runtime + from triton.backends.ascend.runtime.autotuner import AutoTilingTuner + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_persistent_reduction_outer_axis_over_threshold_case(x_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + mask = block_start < n_elements + _ = tl.load(x_ptr + block_start, mask=mask, other=0) + + orig_apply_vv = AutoTilingTuner._apply_vv_axis_semantic_result + with mock.patch.object( + AutoTilingTuner, + "_apply_vv_axis_semantic_result", + new=_mock_apply_vv_with_low_dim_axes(orig_apply_vv, []), + ), mock.patch.object(AutoTilingTuner, + "_autoparse_reduction_axes", new=_mock_single_reduction_axis), mock.patch.object( + AutoTilingTuner, "_autoparse_low_dim_axes", return_value=[]), mock.patch.object( + AutoTilingTuner, "_autoparse_split_params", + return_value={}), mock.patch.object(AutoTilingTuner, "_autoparse_tiling_params", + return_value={}): + act_res = triton_persistent_reduction_outer_axis_over_threshold_case[(1, )](None, 65) + assert act_res["persistent_reduction"] is False diff --git a/third_party/ascend/unittest/autotune_ut/test_mask_parse.py b/third_party/ascend/unittest/autotune_ut/test_mask_parse.py new file mode 100644 index 0000000000..c900593826 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_mask_parse.py @@ -0,0 +1,55 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +from test_common import check_axes_parse_res, mock_autotuner + + +@pytest.mark.parametrize("kernel_type", ["vector", "auto"]) +def test_mask_parse_kernel_type_vector_auto_consistency(mock_autotuner, kernel_type): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + hints={"kernel_type": kernel_type}, + ) + @triton.jit + def triton_mask_parse_kernel_type_vector_auto_consistency(x_ptr, y_ptr, output_ptr, n_elements, + BLOCK_SIZE: tl.constexpr, BLOCK_SUB: tl.constexpr): + block_start = tl.program_id(axis=0) * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SUB) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask, other=0.0) + y = tl.load(y_ptr + offsets, mask=mask, other=0.0) + tl.store(output_ptr + offsets, x + y, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_mask_parse_kernel_type_vector_auto_consistency[grid]() + check_axes_parse_res(act_res, ref_res) diff --git a/third_party/ascend/unittest/autotune_ut/test_no_tiling_axis_parse.py b/third_party/ascend/unittest/autotune_ut/test_no_tiling_axis_parse.py new file mode 100644 index 0000000000..8761309943 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_no_tiling_axis_parse.py @@ -0,0 +1,102 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import os +import shutil +import pytest +import torch +import torch_npu +import triton +import triton.backends.ascend.runtime +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +import test_common + +os.environ['TRITON_ALWAYS_COMPILE'] = '1' +os.environ['TRITON_AUTOTUNE_PARALLEL_COMPILE'] = '0' + + +def case_torch(x): + return torch.permute(x, (1, 0)) + + +@triton.autotune(configs=[], key=['xnumel', 'ynumel'], hints={ + "auto_gen_config": True, +}) +@triton.jit +def triton_permute_2d( + output_ptr, + x_ptr, + xnumel: tl.constexpr, + ynumel: tl.constexpr, + XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, +): + xpid = tl.program_id(0) + ypid = tl.program_id(1) + + x_off = xpid * XBLOCK + tl.arange(0, XBLOCK)[:, None] + y_off = ypid * YBLOCK + tl.arange(0, YBLOCK)[None, :] + mask = (x_off < xnumel) & (y_off < ynumel) + offs = y_off + x_off * ynumel + b = tl.load(x_ptr + offs, mask=mask) + ox_off = ypid * YBLOCK + tl.arange(0, YBLOCK)[:, None] + oy_off = xpid * XBLOCK + tl.arange(0, XBLOCK)[None, :] + o_mask = (ox_off < ynumel) & (oy_off < xnumel) + o_offs = oy_off + ox_off * xnumel + ret = tl.permute(b, (1, 0)) + tl.store(output_ptr + o_offs, ret, mask=o_mask) + + +def case_triton(x_cal, is_simt_only=False): + xnumel = x_cal.shape[0] + ynumel = x_cal.shape[1] + output = torch.randint(1, (ynumel, xnumel), dtype=x_cal.dtype, device=x_cal.device) + if is_simt_only: + (triton_permute_2d[lambda meta: (triton.cdiv(xnumel, meta['XBLOCK']), triton.cdiv(ynumel, meta['YBLOCK']), 1)]( + output, x_cal, xnumel, ynumel, compile_mode="simt_only")) + else: + (triton_permute_2d[lambda meta: + (triton.cdiv(xnumel, meta['XBLOCK']), triton.cdiv(ynumel, meta['YBLOCK']), 1)](output, x_cal, + xnumel, + ynumel)) + return output + + +@pytest.mark.parametrize('shape', [(1024, 32), (32, 8)]) +@pytest.mark.parametrize('dtype', ['bfloat16']) +def test_permute(shape, dtype): + x_cal = test_common.generate_tensor(shape, dtype).npu() + torch_output = case_torch(x_cal) + triton_output = case_triton(x_cal) + torch.testing.assert_close(torch_output, triton_output, rtol=1e-03, atol=1e-03, equal_nan=True) + + +@pytest.mark.skipif( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason="only support Ascend 950", +) +@pytest.mark.parametrize('shape', [(1024, 32)]) +@pytest.mark.parametrize('dtype', ['bfloat16']) +def test_permute_simt(shape, dtype): + x_cal = test_common.generate_tensor(shape, dtype).npu() + torch_output = case_torch(x_cal) + triton_output = case_triton(x_cal, True) + torch.testing.assert_close(torch_output, triton_output, rtol=1e-03, atol=1e-03, equal_nan=True) diff --git a/third_party/ascend/unittest/autotune_ut/test_reduce_simt.py b/third_party/ascend/unittest/autotune_ut/test_reduce_simt.py new file mode 100644 index 0000000000..6f631854e7 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_reduce_simt.py @@ -0,0 +1,76 @@ +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import os +import triton.backends.ascend.runtime + +os.environ["TRITON_PRINT_AUTOTUNING"] = "1" + +try: + from triton.backends.ascend.utils import is_compile_on_910_95 +except Exception: + + def is_compile_on_910_95(_arch): + return False + + +def get_grid(): + import triton.runtime.driver as driver + num_cores = driver.active.utils.get_aivector_core_num() + print(f"grid_size: {num_cores}") + return num_cores + + +@triton.autotune( + configs=[], + key=[], +) +@triton.jit +def triton_unk_reduce(in_ptr0, out_ptr0, y0_numel, x1_numel, X1BLOCK_SUB: tl.constexpr): + y0_offset = tl.program_id(0) + grid_size = tl.num_programs(0) + base_x1 = tl.arange(0, X1BLOCK_SUB) + loops_x1 = (x1_numel + X1BLOCK_SUB - 1) // X1BLOCK_SUB + + for y0 in range(y0_offset, y0_numel, grid_size): + _tmp8 = tl.full([X1BLOCK_SUB], 0, tl.float32) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1 + x1_mask = x1 < x1_numel + tmp0 = tl.load(in_ptr0 + x1_numel * y0 + x1, x1_mask, other=0.0) + _tmp8 += tmp0 + tmp8 = tl.sum(_tmp8, 0) + tl.store(out_ptr0 + y0, tmp8) + + +def torch_reduce(arg0): + return arg0.sum(dim=1) + + +@pytest.mark.skipif( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason="simt is support on Ascend 950", +) +@pytest.mark.parametrize( + "param_list", + [ + [128, 10000], + ], +) +def test_reduce(param_list): + y0_numel, x1_numel = param_list + arg0_1 = torch.randn(y0_numel, x1_numel, dtype=torch.float32, device="npu") + buf44 = torch.empty((y0_numel), dtype=torch.float32, device="npu") + + grid_size = get_grid() + triton_unk_reduce[(grid_size, 1, 1)]( + arg0_1, + buf44, + y0_numel, + x1_numel, + compile_mode='simt_only', + ) + torch_out = torch_reduce(arg0_1) + torch.testing.assert_close(buf44, torch_out, rtol=1e-04, atol=1e-04, equal_nan=True) diff --git a/third_party/ascend/unittest/autotune_ut/test_reduction_axes_parse.py b/third_party/ascend/unittest/autotune_ut/test_reduction_axes_parse.py new file mode 100644 index 0000000000..c33ce29e9e --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_reduction_axes_parse.py @@ -0,0 +1,256 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +from test_common import check_axes_parse_res, mock_autotuner + +import triton +import triton.language as tl + + +def assert_outward_semantic_axes_state(act_res, ref_res): + + def normalize_axes(axis_names, keys): + normalized = [] + for axis in axis_names: + if axis in keys: + normalized.append(axis) + elif isinstance(axis, str) and axis.startswith("r") and axis[1:] in keys: + normalized.append(axis[1:]) + else: + normalized.append(axis) + return normalized + + assert act_res["keys"] == ref_res["keys"] + assert act_res["split_params"] == ref_res["split_params"] + assert act_res["tiling_params"] == ref_res["tiling_params"] + assert act_res["low_dim_axes"] == ref_res["low_dim_axes"] + assert normalize_axes(act_res["reduction_axes"], + act_res["keys"]) == normalize_axes(ref_res["reduction_axes"], ref_res["keys"]) + + +def assert_vv_parser_semantic_axes_state(act_res, ref_res): + vv_parse_result = act_res["vv_parse_result_v2"] + assert vv_parse_result is not None + assert vv_parse_result.axis_length_exprs == ref_res["keys"] + assert vv_parse_result.split_params == ref_res["split_params"] + assert vv_parse_result.tiling_params == ref_res["tiling_params"] + assert vv_parse_result.low_dim_axes == ref_res["low_dim_axes"] + assert vv_parse_result.reduction_axes == ref_res["reduction_axes"] + + +def test_triton_max_last_dim_case1(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["x0_numel", "r1_numel"], + ) + @triton.jit + def triton_max_last_dim1( + in_ptr0, + out_ptr0, + x0_numel, + r1_numel, + X0BLOCK: tl.constexpr, + X0BLOCK_SUB: tl.constexpr, + R1BLOCK_SUB: tl.constexpr, + ): + x0_offset = tl.program_id(0) * X0BLOCK + base_x0 = tl.arange(0, X0BLOCK_SUB) + loops_x0 = (X0BLOCK + X0BLOCK_SUB - 1) // X0BLOCK_SUB + base_r1 = tl.arange(0, R1BLOCK_SUB) + loops_r1 = (r1_numel + R1BLOCK_SUB - 1) // R1BLOCK_SUB + for loop_x0 in range(loops_x0): + x0 = x0_offset + (loop_x0 * X0BLOCK_SUB) + base_x0[:, None] + x0_mask = x0 < min(X0BLOCK + x0_offset, x0_numel) + block_val = tl.full([X0BLOCK_SUB, R1BLOCK_SUB], float("-inf"), tl.float32) + for loop_r1 in range(loops_r1): + r1 = (loop_r1 * R1BLOCK_SUB) + base_r1[None, :] + r1_mask = r1 < r1_numel + tmp = tl.load(in_ptr0 + (r1 + r1_numel * x0), r1_mask & x0_mask, other=float("-inf")) + block_val = tl.maximum(block_val, tmp) + # Reduce along axis = 1 (the last dimension in this 2D tensor) + block_res = tl.max(block_val, axis=1)[:, None] # <- explicit positive axis index + tl.store(out_ptr0 + x0, block_res, x0_mask) + + ref_res = { + "keys": {"x": "x0_numel", "y": "r1_numel"}, + "split_params": {"x": "X0BLOCK"}, + "tiling_params": {"x": "X0BLOCK_SUB", "y": "R1BLOCK_SUB"}, + "low_dim_axes": ["y"], + "reduction_axes": ["y"], + } + grid = lambda meta: (meta["X0BLOCK"], ) + act_res = triton_max_last_dim1[grid]() + + assert_outward_semantic_axes_state(act_res, ref_res) + assert_vv_parser_semantic_axes_state(act_res, ref_res) + check_axes_parse_res(act_res, ref_res) + + +def test_triton_max_last_dim_case2(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["x0_numel", "r1_numel"], + ) + @triton.jit + def triton_max_last_dim2( + in_ptr0, + out_ptr0, + x0_numel, + r1_numel, + X0BLOCK: tl.constexpr, + X0BLOCK_SUB: tl.constexpr, + R1BLOCK_SUB: tl.constexpr, + ): + x0_offset = tl.program_id(0) * X0BLOCK + base_x0 = tl.arange(0, X0BLOCK_SUB) + loops_x0 = (X0BLOCK + X0BLOCK_SUB - 1) // X0BLOCK_SUB + base_r1 = tl.arange(0, R1BLOCK_SUB) + loops_r1 = (r1_numel + R1BLOCK_SUB - 1) // R1BLOCK_SUB + for loop_x0 in range(loops_x0): + x0 = x0_offset + (loop_x0 * X0BLOCK_SUB) + base_x0[:, None] + x0_mask = x0 < min(X0BLOCK + x0_offset, x0_numel) + block_val = tl.full([X0BLOCK_SUB, R1BLOCK_SUB], float("-inf"), tl.float32) + for loop_r1 in range(loops_r1): + r1 = (loop_r1 * R1BLOCK_SUB) + base_r1[None, :] + r1_mask = r1 < r1_numel + tmp = tl.load(in_ptr0 + (r1 + r1_numel * x0), r1_mask & x0_mask, other=float("-inf")) + block_val = tl.maximum(block_val, tmp) + # Reduce along axis=-1 (the last dimension, equivalent to axis=1 in 2D) + block_res = tl.max(block_val, axis=-1)[:, None] # <- negative axis index (last dim) + tl.store(out_ptr0 + x0, block_res, x0_mask) + + ref_res = { + "keys": {"x": "x0_numel", "y": "r1_numel"}, + "split_params": {"x": "X0BLOCK"}, + "tiling_params": {"x": "X0BLOCK_SUB", "y": "R1BLOCK_SUB"}, + "low_dim_axes": ["y"], + "reduction_axes": ["y"], + } + grid = lambda meta: (meta["X0BLOCK"], ) + act_res = triton_max_last_dim2[grid]() + + assert_outward_semantic_axes_state(act_res, ref_res) + assert_vv_parser_semantic_axes_state(act_res, ref_res) + check_axes_parse_res(act_res, ref_res) + + +def test_triton_max_last_dim_case3(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["x0_numel", "r1_numel"], + ) + @triton.jit + def triton_max_last_dim3( + in_ptr0, + out_ptr0, + x0_numel, + r1_numel, + X0BLOCK: tl.constexpr, + X0BLOCK_SUB: tl.constexpr, + R1BLOCK_SUB: tl.constexpr, + ): + x0_offset = tl.program_id(0) * X0BLOCK + base_x0 = tl.arange(0, X0BLOCK_SUB) + loops_x0 = (X0BLOCK + X0BLOCK_SUB - 1) // X0BLOCK_SUB + base_r1 = tl.arange(0, R1BLOCK_SUB) + loops_r1 = (r1_numel + R1BLOCK_SUB - 1) // R1BLOCK_SUB + for loop_x0 in range(loops_x0): + x0 = x0_offset + (loop_x0 * X0BLOCK_SUB) + base_x0[:, None] + x0_mask = x0 < min(X0BLOCK + x0_offset, x0_numel) + block_val = tl.full([X0BLOCK_SUB, R1BLOCK_SUB], float("-inf"), tl.float32) + for loop_r1 in range(loops_r1): + r1 = (loop_r1 * R1BLOCK_SUB) + base_r1[None, :] + r1_mask = r1 < r1_numel + tmp = tl.load(in_ptr0 + (r1 + r1_numel * x0), r1_mask & x0_mask, other=float("-inf")) + block_val = tl.maximum(block_val, tmp) + # Reduce along axis=1, passed as a positional argument (not keyword `axis=...`) + block_res = tl.max(block_val, 1)[:, None] # <- explicit positive axis index + tl.store(out_ptr0 + x0, block_res, x0_mask) + + ref_res = { + "keys": {"x": "x0_numel", "y": "r1_numel"}, + "split_params": {"x": "X0BLOCK"}, + "tiling_params": {"x": "X0BLOCK_SUB", "y": "R1BLOCK_SUB"}, + "low_dim_axes": ["y"], + "reduction_axes": ["y"], + } + grid = lambda meta: (meta["X0BLOCK"], ) + act_res = triton_max_last_dim3[grid]() + + assert_outward_semantic_axes_state(act_res, ref_res) + assert_vv_parser_semantic_axes_state(act_res, ref_res) + check_axes_parse_res(act_res, ref_res) + + +@pytest.mark.parametrize("kernel_type", ["vector", "auto"]) +def test_reduction_axes_parse_kernel_type_vector_auto_consistency(mock_autotuner, kernel_type): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["x0_numel", "r1_numel"], + hints={"kernel_type": kernel_type}, + ) + @triton.jit + def triton_reduction_axes_parse_kernel_type_vector_auto_consistency( + in_ptr0, + out_ptr0, + x0_numel, + r1_numel, + X0BLOCK: tl.constexpr, + X0BLOCK_SUB: tl.constexpr, + R1BLOCK_SUB: tl.constexpr, + ): + x0_offset = tl.program_id(0) * X0BLOCK + base_x0 = tl.arange(0, X0BLOCK_SUB) + loops_x0 = (X0BLOCK + X0BLOCK_SUB - 1) // X0BLOCK_SUB + base_r1 = tl.arange(0, R1BLOCK_SUB) + loops_r1 = (r1_numel + R1BLOCK_SUB - 1) // R1BLOCK_SUB + for loop_x0 in range(loops_x0): + x0 = x0_offset + (loop_x0 * X0BLOCK_SUB) + base_x0[:, None] + x0_mask = x0 < min(X0BLOCK + x0_offset, x0_numel) + block_val = tl.full([X0BLOCK_SUB, R1BLOCK_SUB], float("-inf"), tl.float32) + for loop_r1 in range(loops_r1): + r1 = (loop_r1 * R1BLOCK_SUB) + base_r1[None, :] + r1_mask = r1 < r1_numel + tmp = tl.load(in_ptr0 + (r1 + r1_numel * x0), r1_mask & x0_mask, other=float("-inf")) + block_val = tl.maximum(block_val, tmp) + block_res = tl.max(block_val, axis=1)[:, None] + tl.store(out_ptr0 + x0, block_res, x0_mask) + + ref_res = { + "keys": {"x": "x0_numel", "y": "r1_numel"}, + "split_params": {"x": "X0BLOCK"}, + "tiling_params": {"x": "X0BLOCK_SUB", "y": "R1BLOCK_SUB"}, + "low_dim_axes": ["y"], + "reduction_axes": ["y"], + } + grid = lambda meta: (meta["X0BLOCK"], ) + act_res = triton_reduction_axes_parse_kernel_type_vector_auto_consistency[grid]() + assert_outward_semantic_axes_state(act_res, ref_res) + assert_vv_parser_semantic_axes_state(act_res, ref_res) + check_axes_parse_res(act_res, ref_res) diff --git a/third_party/ascend/unittest/autotune_ut/test_split_axis_parse.py b/third_party/ascend/unittest/autotune_ut/test_split_axis_parse.py new file mode 100644 index 0000000000..1f28665d00 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_split_axis_parse.py @@ -0,0 +1,199 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest.mock as mock + +import pytest +from test_common import check_axes_parse_res, mock_autotuner + +import triton +import triton.language as tl + + +def test_split_axis_parse_base_case1(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_split_axis_parse_base_case1(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE # <- Separate assignment + + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + + tl.store(output_ptr + offsets, output, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_split_axis_parse_base_case1[grid]() + + check_axes_parse_res(act_res, ref_res) + + +def test_split_axis_parse_base_case2(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_split_axis_parse_base_case2(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + block_start = tl.program_id(axis=0) * BLOCK_SIZE # <- Computed inline but still named + + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + + tl.store(output_ptr + offsets, output, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_split_axis_parse_base_case2[grid]() + + check_axes_parse_res(act_res, ref_res) + + +def test_split_axis_parse_base_case3(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_split_axis_parse_base_case3(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + offsets = tl.program_id(axis=0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) # <- Fully fused + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + + tl.store(output_ptr + offsets, output, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_split_axis_parse_base_case3[grid]() + + check_axes_parse_res(act_res, ref_res) + + +def test_grid_stride_loop_block_only_tiling_semantics(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["N", "index_len"], + ) + @triton.jit + def triton_grid_stride_loop_block_only_tiling_semantics( + input_ptr, + output_ptr, + index_ptr, + N, + index_len, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + ): + pid_x = tl.program_id(axis=0) + pid_y = tl.program_id(axis=1) + grid_x = tl.num_programs(axis=0) + grid_y = tl.num_programs(axis=1) + for x in range(pid_x * BLOCK_M, index_len, grid_x * BLOCK_M): + row_offsets = x + tl.arange(0, BLOCK_M) + indices = tl.load(index_ptr + row_offsets, mask=row_offsets < index_len, other=0) + for y in range(pid_y * BLOCK_N, N, grid_y * BLOCK_N): + col_offsets = y + tl.arange(0, BLOCK_N) + col_mask = col_offsets < N + inp_offset = indices[:, None] * N + col_offsets[None, :] + out_offset = row_offsets[:, None] * N + col_offsets[None, :] + selected = tl.load(input_ptr + inp_offset, mask=col_mask[None, :], other=0.0) + tl.store(output_ptr + out_offset, selected, mask=col_mask[None, :]) + + ref_res = { + "keys": {"x": "N", "y": "index_len"}, + "split_params": {}, + "tiling_params": {"y": "BLOCK_M", "x": "BLOCK_N"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + act_res = triton_grid_stride_loop_block_only_tiling_semantics[(1, 1)]() + check_axes_parse_res(act_res, ref_res) + + +@pytest.mark.parametrize("kernel_type", ["vector", "auto"]) +def test_split_axis_parse_kernel_type_vector_auto_consistency(mock_autotuner, kernel_type): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + hints={"kernel_type": kernel_type}, + ) + @triton.jit + def triton_split_axis_parse_kernel_type_vector_auto_consistency(x_ptr, y_ptr, output_ptr, n_elements, + BLOCK_SIZE: tl.constexpr): + block_start = tl.program_id(axis=0) * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + tl.store(output_ptr + offsets, x + y, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_split_axis_parse_kernel_type_vector_auto_consistency[grid]() + check_axes_parse_res(act_res, ref_res) diff --git a/third_party/ascend/unittest/autotune_ut/test_tiling_axis_parse.py b/third_party/ascend/unittest/autotune_ut/test_tiling_axis_parse.py new file mode 100644 index 0000000000..71dd945af8 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_tiling_axis_parse.py @@ -0,0 +1,161 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +from test_common import check_axes_parse_res, mock_autotuner + +import triton +import triton.language as tl + + +def test_tiling_axis_parse_base_case1(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + ) + @triton.jit + def triton_tiling_axis_parse_base_case1(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr, + BLOCK_SUB: tl.constexpr): + offset = tl.program_id(axis=0) * BLOCK_SIZE + base = tl.arange(0, BLOCK_SUB) + loops = (BLOCK_SIZE + BLOCK_SUB - 1) // BLOCK_SUB # <- + for loop in range(loops): + offsets = offset + (loop * BLOCK_SUB) + base + mask = offsets < min(BLOCK_SIZE + offset, n_elements) + + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + + tl.store(output_ptr + offsets, output, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_tiling_axis_parse_base_case1[grid]() + + check_axes_parse_res(act_res, ref_res) + + +@pytest.mark.parametrize("kernel_type", ["vector", "auto"]) +def test_tiling_axis_parse_kernel_type_vector_auto_consistency(mock_autotuner, kernel_type): + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_elements"], + hints={"kernel_type": kernel_type}, + ) + @triton.jit + def triton_tiling_axis_parse_kernel_type_vector_auto_consistency(x_ptr, y_ptr, output_ptr, n_elements, + BLOCK_SIZE: tl.constexpr, BLOCK_SUB: tl.constexpr): + offset = tl.program_id(axis=0) * BLOCK_SIZE + base = tl.arange(0, BLOCK_SUB) + loops = (BLOCK_SIZE + BLOCK_SUB - 1) // BLOCK_SUB + for loop in range(loops): + offsets = offset + (loop * BLOCK_SUB) + base + mask = offsets < min(BLOCK_SIZE + offset, n_elements) + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + tl.store(output_ptr + offsets, x + y, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_tiling_axis_parse_kernel_type_vector_auto_consistency[grid]() + check_axes_parse_res(act_res, ref_res) + + +@pytest.mark.skip +def test_tiling_axis_parse_base_case2(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune(configs=[], key=["n_elements"]) + @triton.jit + def triton_tiling_axis_parse_base_case2(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr, + BLOCK_SUB: tl.constexpr): + offset = tl.program_id(axis=0) * BLOCK_SIZE + base = tl.arange(0, BLOCK_SUB) + for offset_sub in range(0, BLOCK_SIZE, BLOCK_SUB): + offsets = offset + offset_sub + base[:] # <- + mask = offsets < min(BLOCK_SIZE + offset, n_elements) + + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + + tl.store(output_ptr + offsets, output, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_tiling_axis_parse_base_case2[grid]() + + check_axes_parse_res(act_res, ref_res) + + +@pytest.mark.skip +def test_tiling_axis_parse_base_case3(mock_autotuner): + import triton.backends.ascend.runtime + + @triton.autotune(configs=[], key=["n_elements"]) + @triton.jit + def triton_tiling_axis_parse_base_case3(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr, + BLOCK_SUB: tl.constexpr): + offset = tl.program_id(axis=0) * BLOCK_SIZE + base = tl.arange(0, BLOCK_SUB)[:] # <- + for offset_sub in range(0, BLOCK_SIZE, BLOCK_SUB): + offsets = offset + offset_sub + base + mask = offsets < min(BLOCK_SIZE + offset, n_elements) + + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + + tl.store(output_ptr + offsets, output, mask=mask) + + ref_res = { + "keys": {"x": "n_elements"}, + "split_params": {"x": "BLOCK_SIZE"}, + "tiling_params": {"x": "BLOCK_SUB"}, + "low_dim_axes": ["x"], + "reduction_axes": [], + } + grid = lambda meta: (meta["BLOCK_SIZE"], ) + act_res = triton_tiling_axis_parse_base_case3[grid]() + + check_axes_parse_res(act_res, ref_res) diff --git a/third_party/ascend/unittest/autotune_ut/test_vector_parse_state_reset.py b/third_party/ascend/unittest/autotune_ut/test_vector_parse_state_reset.py new file mode 100644 index 0000000000..d130368963 --- /dev/null +++ b/third_party/ascend/unittest/autotune_ut/test_vector_parse_state_reset.py @@ -0,0 +1,107 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest.mock as mock + +import triton +import triton.language as tl + + +def _axis_bases(axes): + return [axis[1:] if isinstance(axis, str) and axis.startswith("r") else axis for axis in axes] + + +def _run_autoparse_and_generate(tuner, all_args): + tuner.nargs = {} + tuner.is_simt_mode = False + tuner.user_specified_multibuffer = None + tuner.user_specified_num_stages = None + tuner.user_specified_warps = None + dtype = all_args["x_ptr"].dtype + tuner._autoparse_axis_params(all_args) + tuner._gen_tile_configs({"x": all_args["n_cols"]}, dtype, all_args) + return { + "persistent_reduction": tuner.persistent_reduction, + "gen_configs": [dict(config.kwargs) for config in tuner.gen_configs], + "reduction_axes": list(tuner.reduction_axes or []), + "low_dim_axes": list(tuner.low_dim_axes or []), + "tiling_params": dict(tuner.tiling_params or {}), + } + + +class _FakeTensor: + + def __init__(self, dtype): + self.dtype = dtype + + +def test_vector_parse_state_resets_persistent_reduction_between_shapes(): + import torch + import triton.backends.ascend.runtime + + @triton.autotune( + configs=[], + key=["n_cols"], + hints={"auto_gen_config": True}, + ) + @triton.jit + def poly_norm_like_kernel( + x_ptr, + y_ptr, + n_cols, + BLOCK_SIZE: tl.constexpr, + ): + acc = tl.zeros([], dtype=tl.float32) + for start in range(0, n_cols, BLOCK_SIZE): + offsets = start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_cols + x = tl.load(x_ptr + offsets, mask=mask, other=0.0).to(tl.float32) + acc += tl.sum(x * x, axis=0) + scale = acc / n_cols + for start in range(0, n_cols, BLOCK_SIZE): + offsets = start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_cols + x = tl.load(x_ptr + offsets, mask=mask, other=0.0).to(tl.float32) + tl.store(y_ptr + offsets, x * scale, mask=mask) + + tuner = poly_norm_like_kernel + fake_x = _FakeTensor(torch.bfloat16) + fake_y = _FakeTensor(torch.bfloat16) + + with mock.patch( + "triton.backends.ascend.runtime.autotuner.get_byte_per_numel", + lambda dtype: 2, + ): + small = _run_autoparse_and_generate( + tuner, + {"x_ptr": fake_x, "y_ptr": fake_y, "n_cols": 1024}, + ) + large = _run_autoparse_and_generate( + tuner, + {"x_ptr": fake_x, "y_ptr": fake_y, "n_cols": 32768}, + ) + + assert small["persistent_reduction"] is True + assert large["tiling_params"] == {"x": "BLOCK_SIZE"} + assert large["low_dim_axes"] == ["x"] + assert _axis_bases(large["reduction_axes"]) == ["x"] + assert large["persistent_reduction"] is False + assert large["gen_configs"] + assert any(config.get("BLOCK_SIZE", 0) <= 8192 for config in large["gen_configs"]) diff --git a/third_party/ascend/unittest/conftest.py b/third_party/ascend/unittest/conftest.py new file mode 100644 index 0000000000..a284dcdec0 --- /dev/null +++ b/third_party/ascend/unittest/conftest.py @@ -0,0 +1,38 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch + + +@pytest.fixture(scope="session", autouse=True) +def assign_npu(worker_id): + npu_count = torch.npu.device_count() + if worker_id == "master": + npu_id = 0 + else: + idx = int(worker_id.replace("gw", "")) + npu_id = idx % npu_count + torch.npu.set_device(npu_id) + + +def pytest_addoption(parser): + parser.addoption("--kernel", action="append", default=None, + help="run only specified kernel(s); can be supplied multiple times") diff --git a/third_party/ascend/unittest/costmodel_ut/CMakeLists.txt b/third_party/ascend/unittest/costmodel_ut/CMakeLists.txt new file mode 100644 index 0000000000..28791e9128 --- /dev/null +++ b/third_party/ascend/unittest/costmodel_ut/CMakeLists.txt @@ -0,0 +1,32 @@ +include(${CMAKE_SOURCE_DIR}/cmake/AddTritonUnitTest.cmake) + +add_triton_ut( + NAME CostModelHardwareConfig + SRCS HardwareConfigTest.cpp + LIBS + AscendModelAnalysis +) + +add_triton_ut( + NAME CostModelPipelineScheduler + SRCS PipelineSchedulerTest.cpp + LIBS + AscendModelAnalysis + AscendModelIR +) + +add_triton_ut( + NAME CostModelPasses + SRCS PassesTest.cpp + LIBS + AscendModelTransforms + AscendModelAnalysis + AscendModelIR + MLIRArithDialect + MLIRFuncDialect + MLIRIR + MLIRParser + MLIRPass + MLIRSCFDialect + MLIRSupport +) diff --git a/third_party/ascend/unittest/costmodel_ut/HardwareConfigTest.cpp b/third_party/ascend/unittest/costmodel_ut/HardwareConfigTest.cpp new file mode 100644 index 0000000000..eddad04eff --- /dev/null +++ b/third_party/ascend/unittest/costmodel_ut/HardwareConfigTest.cpp @@ -0,0 +1,200 @@ +#include "AscendModel/HardwareConfig.h" + +#include + +#include +#include + +namespace { + +std::string get910BConfigPath() { + namespace fs = std::filesystem; + fs::path src = fs::path(__FILE__).parent_path().parent_path().parent_path(); + fs::path cfg = src / "costmodel" / "configs" / "ascend_910b.json"; + return cfg.string(); +} + +std::unique_ptr +loadConfigFromText(llvm::StringRef text) { + auto parsed = llvm::json::parse(text); + if (!parsed) + return nullptr; + return mlir::ascend::HardwareConfig::loadFromJSON(*parsed); +} + +} // namespace + +TEST(CostModelHardwareConfigTest, Default910BHasExpectedBasics) { + auto cfg = mlir::ascend::HardwareConfig::getDefault910B(); + ASSERT_NE(cfg, nullptr); + + EXPECT_GT(cfg->getClockFrequencyGHz(), 0.0); + EXPECT_GT(cfg->getCyclesPerMicrosecond(), 0); + EXPECT_GT(cfg->getCubeTFLOPS(), 0.0); + EXPECT_GT(cfg->getVectorTFLOPS(), 0.0); + + const auto *hbm = cfg->getMemorySpace("hbm"); + ASSERT_NE(hbm, nullptr); + EXPECT_GT(hbm->sizeBytes, 0u); + EXPECT_GT(hbm->bandwidthBytesPerCycle, 0.0); +} + +TEST(CostModelHardwareConfigTest, LoadFromCommitConfigFile) { + const std::string path = get910BConfigPath(); + + auto cfg = mlir::ascend::HardwareConfig::loadFromFile(path); + ASSERT_NE(cfg, nullptr) << "failed to load config from: " << path; + + std::string error; + EXPECT_TRUE(cfg->validate(error)) << error; + EXPECT_GT(cfg->getCubeTFLOPS(), 0.0); + EXPECT_GT(cfg->getVectorTFLOPS(), 0.0); + + int m = 0, n = 0, k = 0; + cfg->getCubeFractalSize(/*elementBits=*/16, m, n, k); + EXPECT_GT(m, 0); + EXPECT_GT(n, 0); + EXPECT_GT(k, 0); +} + +TEST(CostModelHardwareConfigTest, LoadGlobalConfigFromFile) { + const std::string path = get910BConfigPath(); + std::string error; + ASSERT_TRUE(mlir::ascend::loadHardwareConfigFromFile(path, error)) << error; + + mlir::ascend::HardwareConfig &global = mlir::ascend::getHardwareConfig(); + EXPECT_GT(global.getClockFrequencyGHz(), 0.0); + EXPECT_GT(global.getHBMBandwidthGBs(), 0.0); +} + +TEST(CostModelHardwareConfigTest, EstimationAPIsReturnNonNegative) { + auto cfg = mlir::ascend::HardwareConfig::getDefault910B(); + ASSERT_NE(cfg, nullptr); + + EXPECT_GE(cfg->estimateCubeCycles(/*M=*/128, /*N=*/128, /*K=*/64), 0); + EXPECT_GE(cfg->estimateVectorCycles(/*numElements=*/4096), 0); + EXPECT_GE(cfg->estimateMemoryCyclesWithLatency("hbm", /*bytes=*/8192), 0); +} + +TEST(CostModelHardwareConfigTest, ParsesCustomJsonAndExposesCoreParameters) { + auto cfg = loadConfigFromText(R"json( +{ + "name": "Tiny Test NPU", + "vendor": "UnitTest", + "version": "1.0", + "clock": {"frequency_ghz": 2.0}, + "memory_spaces": { + "hbm": {"type": "off_chip", "size_gb": 1, "bandwidth_gbps": 2000, "latency_cycles": 11}, + "ub": {"type": "on_chip_local", "size_kb": 64, "bandwidth_gbps": 1000, "latency_cycles": 3}, + "l0c": {"type": "register_file", "size_kb": 32, "bandwidth_gbps": 500, "latency_cycles": 1} + }, + "compute_units": { + "cube": { + "type": "matrix_engine", + "tflops_fp16": 64, + "tflops_fp32": 32, + "tflops_int8": 128, + "tile_m": 8, + "tile_n": 16, + "tile_k": 32, + "fractal_sizes": { + "fp16": {"m": 16, "n": 16, "k": 16}, + "fp32": {"m": 16, "n": 16, "k": 8}, + "int8": {"m": 16, "n": 16, "k": 32} + }, + "input_spaces": ["hbm", "ub"], + "output_space": "l0c" + }, + "vector": { + "type": "simd_engine", + "tflops_fp32": 8, + "width_elements": 64, + "width_bytes": 256, + "compute_space": "ub", + "supported_ops": ["add", "exp"], + "supported_dtypes": ["fp32"] + } + }, + "data_movers": { + "vector_mte2": { + "src_space": "hbm", + "dst_spaces": ["ub"], + "bandwidth_gbps": 1000, + "max_burst_bytes": 1024, + "alignment_bytes": 32 + } + }, + "pipeline": { + "vector_path": {"stages": ["vector_mte2", "vector"]}, + "parallelism": {"cube_path_and_vector_path": true} + }, + "calibration": { + "vector_op_cycles_per_vec_instruction": { + "simple_ops_add_sub_mul_etc": 2, + "exp": 7 + } + } +} +)json"); + ASSERT_NE(cfg, nullptr); + + std::string error; + EXPECT_TRUE(cfg->validate(error)) << error; + EXPECT_EQ(cfg->getName(), "Tiny Test NPU"); + EXPECT_EQ(cfg->getVendor(), "UnitTest"); + EXPECT_DOUBLE_EQ(cfg->getClockFrequencyGHz(), 2.0); + EXPECT_EQ(cfg->getCyclesPerMicrosecond(), 2000); + + const auto *hbm = cfg->getMemorySpace("hbm"); + ASSERT_NE(hbm, nullptr); + EXPECT_EQ(hbm->sizeBytes, 1ULL * 1024 * 1024 * 1024); + EXPECT_DOUBLE_EQ(hbm->bandwidthBytesPerCycle, 1000.0); + EXPECT_EQ(hbm->latencyCycles, 11); + + int m = 0, n = 0, k = 0; + cfg->getCubeTileSize(m, n, k); + EXPECT_EQ(m, 8); + EXPECT_EQ(n, 16); + EXPECT_EQ(k, 32); + cfg->getCubeFractalSize(/*elementBits=*/32, m, n, k); + EXPECT_EQ(m, 16); + EXPECT_EQ(n, 16); + EXPECT_EQ(k, 8); + cfg->getCubeFractalSize(/*elementBits=*/8, m, n, k); + EXPECT_EQ(k, 32); + + EXPECT_EQ(cfg->getVectorWidthElements(), 64); + EXPECT_EQ(cfg->getVectorOpCyclesPerInstruction("vadd"), 2); + EXPECT_EQ(cfg->getVectorOpCyclesPerInstruction("vexp"), 7); + EXPECT_TRUE(cfg->canRunInParallel("cube_path", "vector_path")); + EXPECT_EQ(cfg->estimateVectorCycles(/*numElements=*/129), 3); + EXPECT_EQ(cfg->estimateMemoryCycles("vector_mte2", /*bytes=*/2001), 5); + EXPECT_EQ(cfg->estimateMemoryCyclesWithLatency("hbm", /*bytes=*/2001), 14); +} + +TEST(CostModelHardwareConfigTest, RejectsInvalidConfigReferences) { + auto cfg = loadConfigFromText(R"json( +{ + "clock": {"frequency_ghz": 1.0}, + "memory_spaces": { + "hbm": {"type": "off_chip", "size_gb": 1, "bandwidth_gbps": 1000} + }, + "compute_units": { + "vector": {"type": "simd_engine", "tflops_fp32": 1, "width_elements": 16} + }, + "data_movers": { + "bad_mover": { + "src_space": "hbm", + "dst_spaces": ["missing_space"], + "bandwidth_gbps": 100 + } + } +} +)json"); + ASSERT_NE(cfg, nullptr); + + std::string error; + EXPECT_FALSE(cfg->validate(error)); + EXPECT_NE(error.find("bad_mover"), std::string::npos); + EXPECT_NE(error.find("missing_space"), std::string::npos); +} diff --git a/third_party/ascend/unittest/costmodel_ut/PassesTest.cpp b/third_party/ascend/unittest/costmodel_ut/PassesTest.cpp new file mode 100644 index 0000000000..6a76e7fd47 --- /dev/null +++ b/third_party/ascend/unittest/costmodel_ut/PassesTest.cpp @@ -0,0 +1,174 @@ +#include "AscendModel/Transforms/Passes.h" +#include "AscendModel/IR/AscendModelDialect.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Pass/PassManager.h" + +#include + +#include +#include + +using mlir::ModuleOp; +using mlir::Operation; +using mlir::OwningOpRef; +using mlir::Pass; +using mlir::PassManager; +using mlir::StringAttr; +using mlir::ascend::createAssignOpIDsPass; +using mlir::ascend::createEstimateCyclesPass; +using mlir::ascend::createPerfReportPass; +using mlir::ascend::createPipelineAnalysisPass; +using mlir::ascend::EstimateCyclesPassOptions; + +namespace { + +constexpr const char *kVectorModule = R"mlir( +module { + func.func @main(%arg0: tensor<4xf32>, %arg1: tensor<4xf32>) -> tensor<4xf32> { + %0 = ascend.vector_load %arg0 {bytes = 16 : i64} : tensor<4xf32> -> tensor<4xf32> + %1 = ascend.add %0, %arg1 : (tensor<4xf32>, tensor<4xf32>) -> tensor<4xf32> + ascend.vector_store %1 {bytes = 16 : i64} : tensor<4xf32> + return %1 : tensor<4xf32> + } +} +)mlir"; + +void registerDialects(mlir::MLIRContext &context) { + context.getOrLoadDialect(); + context.getOrLoadDialect(); + context.getOrLoadDialect(); + context.getOrLoadDialect(); +} + +OwningOpRef parseModule(mlir::MLIRContext &context, + llvm::StringRef source) { + registerDialects(context); + return mlir::parseSourceString(source, &context); +} + +template +bool runPasses(ModuleOp module, PassTs &&...passes) { + PassManager pm(module.getContext()); + (pm.addPass(std::forward(passes)), ...); + return mlir::succeeded(pm.run(module)); +} + +Operation *findFirstOp(ModuleOp module, llvm::StringRef name) { + Operation *result = nullptr; + module.walk([&](Operation *op) { + if (!result && op->getName().getStringRef() == name) + result = op; + }); + return result; +} + +int64_t getI64Attr(Operation *op, llvm::StringRef name) { + auto attr = op->getAttrOfType(name); + return attr ? attr.getInt() : -1; +} + +} // namespace + +TEST(CostModelPassesTest, AssignOpIDsPassAnnotatesAscendOpsOnly) { + mlir::MLIRContext context; + auto module = parseModule(context, R"mlir( +module { + func.func @main(%arg0: i32, %arg1: i32, %arg2: tensor<4xf32>) -> tensor<4xf32> { + %c0 = arith.addi %arg0, %arg1 : i32 + %0 = ascend.add %arg2, %arg2 : (tensor<4xf32>, tensor<4xf32>) -> tensor<4xf32> + return %0 : tensor<4xf32> + } +} +)mlir"); + ASSERT_TRUE(module); + + ASSERT_TRUE(runPasses(*module, createAssignOpIDsPass())); + + auto totalOps = module->getOperation()->getAttrOfType( + "ascend.total_ops"); + ASSERT_TRUE(totalOps); + EXPECT_EQ(totalOps.getInt(), 1); + + Operation *addOp = findFirstOp(*module, "ascend.add"); + ASSERT_NE(addOp, nullptr); + EXPECT_EQ(getI64Attr(addOp, "op_id"), 0); + + Operation *arithOp = findFirstOp(*module, "arith.addi"); + ASSERT_NE(arithOp, nullptr); + EXPECT_FALSE(arithOp->hasAttr("op_id")); +} + +TEST(CostModelPassesTest, EstimateCyclesAnnotatesComputeAndTransferOps) { + mlir::MLIRContext context; + auto module = parseModule(context, kVectorModule); + ASSERT_TRUE(module); + + ASSERT_TRUE(runPasses(*module, createEstimateCyclesPass())); + + Operation *loadOp = findFirstOp(*module, "ascend.vector_load"); + Operation *addOp = findFirstOp(*module, "ascend.add"); + Operation *storeOp = findFirstOp(*module, "ascend.vector_store"); + ASSERT_NE(loadOp, nullptr); + ASSERT_NE(addOp, nullptr); + ASSERT_NE(storeOp, nullptr); + + EXPECT_GT(getI64Attr(loadOp, "estimated_cycles"), 0); + EXPECT_GT(getI64Attr(addOp, "estimated_cycles"), 0); + EXPECT_GT(getI64Attr(storeOp, "estimated_cycles"), 0); + EXPECT_EQ(getI64Attr(loadOp, "bytes"), 16); + EXPECT_EQ(getI64Attr(storeOp, "bytes"), 16); + EXPECT_EQ(getI64Attr(addOp, "flops"), 4); + EXPECT_TRUE(loadOp->getAttrOfType("hw_unit")); + EXPECT_TRUE(addOp->getAttrOfType("hw_unit")); + EXPECT_TRUE(storeOp->getAttrOfType("hw_unit")); +} + +TEST(CostModelPassesTest, EstimateCyclesReportsInvalidArgBindings) { + mlir::MLIRContext context; + auto module = parseModule(context, kVectorModule); + ASSERT_TRUE(module); + + EstimateCyclesPassOptions options; + options.argBindingsStr = "arg0"; + + EXPECT_FALSE(runPasses(*module, createEstimateCyclesPass(options))); +} + +TEST(CostModelPassesTest, PipelineAnalysisSetsCycleSummaryAttrs) { + mlir::MLIRContext context; + auto module = parseModule(context, kVectorModule); + ASSERT_TRUE(module); + + ASSERT_TRUE(runPasses(*module, createAssignOpIDsPass(), + createEstimateCyclesPass(), + createPipelineAnalysisPass())); + + auto scheduled = module->getOperation()->getAttrOfType( + "ascend.scheduled_cycles_one_iter"); + auto roofline = module->getOperation()->getAttrOfType( + "ascend.roofline_cycles"); + auto simple = module->getOperation()->getAttrOfType( + "ascend.simple_sum_cycles"); + ASSERT_TRUE(scheduled); + ASSERT_TRUE(roofline); + ASSERT_TRUE(simple); + EXPECT_GT(scheduled.getInt(), 0); + EXPECT_GT(roofline.getInt(), 0); + EXPECT_GT(simple.getInt(), 0); +} + +TEST(CostModelPassesTest, PerfReportPassAcceptsEstimatedPipeline) { + mlir::MLIRContext context; + auto module = parseModule(context, kVectorModule); + ASSERT_TRUE(module); + + EXPECT_TRUE(runPasses(*module, createAssignOpIDsPass(), + createEstimateCyclesPass(), + createPipelineAnalysisPass(), createPerfReportPass())); +} diff --git a/third_party/ascend/unittest/costmodel_ut/PipelineSchedulerTest.cpp b/third_party/ascend/unittest/costmodel_ut/PipelineSchedulerTest.cpp new file mode 100644 index 0000000000..14f59b8ed1 --- /dev/null +++ b/third_party/ascend/unittest/costmodel_ut/PipelineSchedulerTest.cpp @@ -0,0 +1,150 @@ +#include "AscendModel/Analysis/PipelineAnalysis.h" +#include "AscendModel/HardwareConfig.h" +#include "AscendModel/IR/AscendModelDialect.h" + +#include + +#include + +using mlir::ascend::DependencyGraph; +using mlir::ascend::HWUnit; +using mlir::ascend::PipelineOp; +using mlir::ascend::PipelineScheduler; + +namespace { + +PipelineOp makeOp(int64_t id, HWUnit unit, int64_t duration) { + PipelineOp op; + op.opId = id; + op.hwUnit = unit; + op.duration = duration; + op.opName = "op" + std::to_string(id); + return op; +} + +} // namespace + +TEST(CostModelPipelineSchedulerTest, DifferentHardwareUnitsRunInParallel) { + PipelineScheduler scheduler; + scheduler.addOperation(makeOp(/*id=*/1, HWUnit::Cube, /*duration=*/100)); + scheduler.addOperation(makeOp(/*id=*/2, HWUnit::Vector, /*duration=*/80)); + + ASSERT_TRUE(scheduler.schedule()); + EXPECT_EQ(scheduler.getTotalCycles(), 100); + + const auto &ops = scheduler.getAllOps(); + ASSERT_EQ(ops.size(), 2u); + EXPECT_EQ(ops[0].startCycle, 0); + EXPECT_EQ(ops[1].startCycle, 0); + EXPECT_EQ(ops[0].endCycle, 100); + EXPECT_EQ(ops[1].endCycle, 80); +} + +TEST(CostModelPipelineSchedulerTest, SameHardwareUnitSerializesOperations) { + PipelineScheduler scheduler; + scheduler.addOperation(makeOp(/*id=*/1, HWUnit::Vector, /*duration=*/40)); + scheduler.addOperation(makeOp(/*id=*/2, HWUnit::Vector, /*duration=*/60)); + + ASSERT_TRUE(scheduler.schedule()); + EXPECT_EQ(scheduler.getTotalCycles(), 100); + + const auto &ops = scheduler.getAllOps(); + ASSERT_EQ(ops.size(), 2u); + EXPECT_TRUE((ops[0].endCycle <= ops[1].startCycle) || + (ops[1].endCycle <= ops[0].startCycle)); + EXPECT_EQ(ops[0].duration + ops[1].duration, 100); +} + +TEST(CostModelPipelineSchedulerTest, DependenciesDelayConsumersAcrossUnits) { + PipelineScheduler scheduler; + scheduler.addOperation(makeOp(/*id=*/1, HWUnit::Cube, /*duration=*/100)); + + PipelineOp consumer = makeOp(/*id=*/2, HWUnit::Vector, /*duration=*/30); + consumer.dependsOn.push_back(1); + scheduler.addOperation(consumer); + scheduler.addDependency(/*fromId=*/1, /*toId=*/2); + + ASSERT_TRUE(scheduler.schedule()); + EXPECT_EQ(scheduler.getTotalCycles(), 130); + + const auto &ops = scheduler.getAllOps(); + ASSERT_EQ(ops.size(), 2u); + EXPECT_EQ(ops[0].startCycle, 0); + EXPECT_EQ(ops[0].endCycle, 100); + EXPECT_EQ(ops[1].startCycle, 100); + EXPECT_EQ(ops[1].endCycle, 130); +} + +TEST(CostModelPipelineSchedulerTest, RejectsCyclicDependencyGraph) { + PipelineScheduler scheduler; + PipelineOp first = makeOp(/*id=*/1, HWUnit::Cube, /*duration=*/10); + first.dependsOn.push_back(2); + PipelineOp second = makeOp(/*id=*/2, HWUnit::Vector, /*duration=*/10); + second.dependsOn.push_back(1); + scheduler.addOperation(first); + scheduler.addOperation(second); + scheduler.addDependency(/*fromId=*/1, /*toId=*/2); + scheduler.addDependency(/*fromId=*/2, /*toId=*/1); + + EXPECT_FALSE(scheduler.schedule()); +} + +TEST(CostModelPipelineSchedulerTest, KernelCyclesApplyBarrierScalarAndWaves) { + auto config = mlir::ascend::HardwareConfig::getDefault910B(); + ASSERT_NE(config, nullptr); + + PipelineScheduler scheduler(config.get()); + scheduler.addOperation(makeOp(/*id=*/1, HWUnit::Vector, /*duration=*/100)); + ASSERT_TRUE(scheduler.schedule()); + + const int64_t expectedBarrierCycles = + 2 * config->getPipeBarrierCyclesPerIter(); + const int64_t expectedPerProgramCycles = + static_cast((100 + expectedBarrierCycles) * + (1.0 + config->getAIVScalarOverheadFactor())); + const int64_t expectedWaves = 3; // ceil(81 / 40) + + EXPECT_EQ(scheduler.getKernelCycles(/*numPrograms=*/81, + /*numParallelUnits=*/40, + /*numInnerIters=*/2), + expectedPerProgramCycles * expectedWaves); +} + +TEST(CostModelDependencyGraphTest, TopologicalOrderAndCycleDetection) { + DependencyGraph graph; + graph.addOp(/*opId=*/1, /*op=*/nullptr); + graph.addOp(/*opId=*/2, /*op=*/nullptr); + graph.addOp(/*opId=*/3, /*op=*/nullptr); + graph.addDependency(/*fromId=*/1, /*toId=*/2); + graph.addDependency(/*fromId=*/2, /*toId=*/3); + + EXPECT_FALSE(graph.hasCycle()); + auto deps = graph.getDependencies(/*opId=*/3); + ASSERT_EQ(deps.size(), 1u); + EXPECT_EQ(deps[0], 2); + EXPECT_EQ(graph.getTopologicalOrder().size(), 3u); + + graph.addDependency(/*fromId=*/3, /*toId=*/1); + EXPECT_TRUE(graph.hasCycle()); +} + +TEST(CostModelRooflineAnalyzerTest, ProducesReportAndJsonFromScheduledOps) { + PipelineScheduler scheduler; + scheduler.addOperation(makeOp(/*id=*/1, HWUnit::Vector, /*duration=*/40)); + scheduler.addOperation(makeOp(/*id=*/2, HWUnit::VecMTE2, /*duration=*/25)); + scheduler.addOperation(makeOp(/*id=*/3, HWUnit::MTE3, /*duration=*/10)); + ASSERT_TRUE(scheduler.schedule()); + + mlir::ascend::RooflineAnalyzer analyzer(scheduler); + auto report = analyzer.analyze(); + + EXPECT_EQ(report.totalCycles, 40); + EXPECT_EQ(report.unitBusyCycles[HWUnit::Vector], 40); + EXPECT_EQ(report.unitBusyCycles[HWUnit::VecMTE2], 25); + EXPECT_EQ(report.unitBusyCycles[HWUnit::MTE3], 10); + EXPECT_EQ(report.bottleneckUnit, HWUnit::Vector); + + std::string json = report.toJSON(); + EXPECT_NE(json.find("\"total_cycles\""), std::string::npos); + EXPECT_NE(json.find("\"unit_utilization\""), std::string::npos); +} diff --git a/third_party/ascend/unittest/costmodel_ut/test_costmodel_runtime.py b/third_party/ascend/unittest/costmodel_ut/test_costmodel_runtime.py new file mode 100644 index 0000000000..a92391f725 --- /dev/null +++ b/third_party/ascend/unittest/costmodel_ut/test_costmodel_runtime.py @@ -0,0 +1,240 @@ +import importlib.util +import builtins +import json +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +class DummyCacheManager: + + def __init__(self): + self.storage = {} + self.cache_dir = tempfile.TemporaryDirectory() + + def get_file(self, name): + return self.storage.get(name) + + def put(self, payload, name, binary=False): + if binary: + raise AssertionError("costmodel cache should be text json") + path = Path(self.cache_dir.name) / name + path.write_text(str(payload), encoding="utf-8") + self.storage[name] = str(path) + return str(path) + + def cleanup(self): + self.cache_dir.cleanup() + + def __del__(self): + self.cleanup() + + +class CostmodelRuntimeTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # Stub triton runtime cache before importing module under test. + triton_mod = types.ModuleType("triton") + runtime_mod = types.ModuleType("triton.runtime") + cache_mod = types.ModuleType("triton.runtime.cache") + + cls.bootstrap_cache = DummyCacheManager() + cache_mod.get_cache_manager = lambda _ns: cls.bootstrap_cache + cache_mod.triton_key = lambda: "bootstrap-key" + + runtime_mod.cache = cache_mod + triton_mod.runtime = runtime_mod + + sys.modules.setdefault("triton", triton_mod) + sys.modules.setdefault("triton.runtime", runtime_mod) + sys.modules.setdefault("triton.runtime.cache", cache_mod) + + repo_root = Path(__file__).resolve().parents[4] + module_path = repo_root / "third_party" / "ascend" / "backend" / "runtime" / "costmodel_runtime.py" + spec = importlib.util.spec_from_file_location("costmodel_runtime_under_test", module_path) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(module) + cls.cm = module + + def setUp(self): + self.cm._COSTMODEL_MEM_CACHE.clear() + + def test_parse_latency(self): + self.assertAlmostEqual(self.cm.parse_latency("Estimated Time: 3.25 us"), 3.25) + self.assertEqual(self.cm.parse_latency("noise"), float("inf")) + + def test_cache_namespace_variants(self): + with patch.object(self.cm, "_triton_key", None): + key_none = self.cm._costmodel_cache_namespace() + self.assertEqual(len(key_none), 64) + + with patch.object(self.cm, "_triton_key", lambda: "abc"): + key_ok = self.cm._costmodel_cache_namespace() + self.assertEqual(len(key_ok), 64) + + def _boom(): + raise RuntimeError("x") + + with patch.object(self.cm, "_triton_key", _boom): + key_err = self.cm._costmodel_cache_namespace() + self.assertEqual(len(key_err), 64) + + def test_store_and_load_costmodel_latency(self): + mgr = DummyCacheManager() + with patch.object(self.cm, "get_cache_manager", lambda _ns: mgr): + cache_key = "k1" + self.assertIsNone(self.cm.load_costmodel_latency(cache_key)) + + self.cm.store_costmodel_latency(cache_key, 7.5) + self.assertAlmostEqual(self.cm.load_costmodel_latency(cache_key), 7.5) + + self.cm._COSTMODEL_MEM_CACHE.clear() + self.assertAlmostEqual(self.cm.load_costmodel_latency(cache_key), 7.5) + + payload_path = Path(mgr.storage[f"{cache_key}.json"]) + self.assertAlmostEqual(float(json.loads(payload_path.read_text())["latency"]), 7.5) + + payload_path.write_text(json.dumps({"latency": "bad-float"}), encoding="utf-8") + self.cm._COSTMODEL_MEM_CACHE.clear() + self.assertIsNone(self.cm.load_costmodel_latency(cache_key)) + + def test_make_key_and_extra_args(self): + k1 = self.cm.make_costmodel_cache_key("ttir_a", ["-ascend-perf-model"]) + k2 = self.cm.make_costmodel_cache_key("ttir_a", ["-ascend-perf-model", "arg-bindings=a=1"]) + self.assertNotEqual(k1, k2) + + with patch.object(self.cm, "_resolve_default_hardware_config", lambda: "/tmp/ascend_910b.json"): + self.assertEqual( + self.cm._build_costmodel_extra_args("arg1=3", ""), + ["-ascend-perf-model", "arg-bindings=arg1=3"], + ) + self.assertEqual( + self.cm._build_costmodel_extra_args("", ""), + ["-ascend-perf-model", "hardware-config=/tmp/ascend_910b.json"], + ) + with patch.object(self.cm, "_resolve_default_hardware_config", lambda: ""): + self.assertEqual(self.cm._build_costmodel_extra_args("", ""), ["-ascend-perf-model"]) + + def test_run_costmodel_reads_file_and_adds_allow_unregistered_dialect(self): + calls = [] + + class AscendCapi: + + @staticmethod + def run_costmodel_inproc(mlir_text, args): + calls.append((mlir_text, tuple(args))) + return "Estimated Time: 1.0 us" + + fake_libtriton = types.SimpleNamespace(ascend=AscendCapi) + real_import = builtins.__import__ + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "triton._C.libtriton": + return fake_libtriton + return real_import(name, globals, locals, fromlist, level) + + with tempfile.NamedTemporaryFile("w", encoding="utf-8") as f: + f.write("module-from-file") + f.flush() + with patch("builtins.__import__", fake_import): + self.assertEqual(self.cm.run_costmodel(f.name, ["-ascend-perf-model"]), "Estimated Time: 1.0 us") + + self.assertEqual(calls[0][0], "module-from-file") + self.assertIn("-allow-unregistered-dialect", calls[0][1]) + + def test_run_costmodel_exception_paths(self): + + class GenericFailingCapi: + + @staticmethod + def run_costmodel_inproc(_mlir_text, _args): + raise RuntimeError("failed to parse input MLIR module") + + fake_libtriton = types.SimpleNamespace(ascend=GenericFailingCapi) + real_import = builtins.__import__ + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "triton._C.libtriton": + return fake_libtriton + return real_import(name, globals, locals, fromlist, level) + + with patch("builtins.__import__", fake_import): + self.assertIsNone(self.cm.run_costmodel("module-text", ["-ascend-perf-model"])) + + def test_normalize_item_and_eval_item(self): + cfg1 = object() + cfg2 = object() + + config, latency, pending = self.cm._normalize_costmodel_item( + {"config": cfg1, "ttir": "ttir1", "arg_bindings": "a=1", "hardware_config": "h1"}) + self.assertIs(config, cfg1) + self.assertIsNone(latency) + self.assertEqual(pending, (cfg1, "ttir1", "a=1", "h1")) + + config, latency, pending = self.cm._normalize_costmodel_item({"config": cfg2, "ttir": ""}) + self.assertIs(config, cfg2) + self.assertEqual(latency, float("inf")) + self.assertIsNone(pending) + + config, latency, pending = self.cm._normalize_costmodel_item(123) + self.assertIsNone(config) + self.assertEqual(latency, float("inf")) + self.assertIsNone(pending) + + with patch.object(self.cm, "load_costmodel_latency", lambda _k: 1.23): + cfg, t = self.cm._eval_one_costmodel_item((cfg1, "ttir1", "a=1", "h1")) + self.assertIs(cfg, cfg1) + self.assertAlmostEqual(t, 1.23) + + def test_eval_item_miss(self): + cfg1, cfg2 = object(), object() + calls = [] + + def fake_run(ttir_or_path, extra_args=None, dump_ir_on_error=False): + calls.append((ttir_or_path, tuple(extra_args or []))) + if "ttir1" in ttir_or_path: + return "Estimated Time: 9.9 us" + return None + + with patch.object(self.cm, "load_costmodel_latency", lambda _k: None), patch.object( + self.cm, "store_costmodel_latency", + lambda *_args, **_kwargs: None), patch.object(self.cm, "run_costmodel", fake_run): + out1 = self.cm._eval_one_costmodel_item((cfg1, "ttir1", "arg=1", "")) + out2 = self.cm._eval_one_costmodel_item((cfg2, "ttir2", "", "")) + + self.assertIs(out1[0], cfg1) + self.assertAlmostEqual(out1[1], 9.9) + self.assertIs(out2[0], cfg2) + self.assertEqual(out2[1], float("inf")) + self.assertEqual(len(calls), 2) + + def test_costmodel_bench_paths(self): + self.assertEqual(self.cm.costmodel_bench([]), (None, float("inf"))) + + cfg1, cfg2 = object(), object() + + with patch.object(self.cm, "_eval_one_costmodel_item", lambda _pending: (cfg1, 0.88)): + result = self.cm.costmodel_bench({"config": cfg1, "ttir": "t1"}) + self.assertIs(result[0], cfg1) + self.assertAlmostEqual(result[1], 0.88) + + result = self.cm.costmodel_bench({"config": cfg2, "ttir": ""}) + self.assertIs(result[0], cfg2) + self.assertEqual(result[1], float("inf")) + + def explode(*_args, **_kwargs): + raise RuntimeError("oops") + + with patch.object(self.cm, "_normalize_costmodel_item", explode): + fallback = self.cm.costmodel_bench({"config": cfg1, "ttir": "t1"}) + self.assertIs(fallback[0], cfg1) + self.assertEqual(fallback[1], float("inf")) + + +if __name__ == "__main__": + unittest.main() diff --git a/third_party/ascend/unittest/custom_op/builtin_ops_demo.py b/third_party/ascend/unittest/custom_op/builtin_ops_demo.py new file mode 100755 index 0000000000..7ea7725d15 --- /dev/null +++ b/third_party/ascend/unittest/custom_op/builtin_ops_demo.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import subprocess +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, ttir_to_linalg + + +@triton.jit +def my_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + index = tl.full([8], 0, tl.int32) + x = al.custom("__builtin_index_select", x_ptr, index, dim=0, bound=100, end_offset=(2, 2), start_offset=(0, 0), + src_stride=(4, 1), out=x) + tl.store(out_ptr + i, y, mask=i < n) + + +if __name__ == "__main__": + src = ASTSource(my_kernel, {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, {"BLOCK": 256}) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + options = NPUOptions() + try: + ttir = ast_to_ttir(my_kernel, src, context, options, {}, {}) + print("=== TTIR ===") + print(ttir) + metadata = { + **options.__dict__, + } + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + print("=== MLIR (linalg) ===") + print(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") diff --git a/third_party/ascend/unittest/custom_op/custom_op_demo.py b/third_party/ascend/unittest/custom_op/custom_op_demo.py new file mode 100755 index 0000000000..e3bbd5e412 --- /dev/null +++ b/third_party/ascend/unittest/custom_op/custom_op_demo.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +import subprocess +import os +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, ttir_to_linalg + + +@al.register_custom_op +class min_custom_op: + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_MTE2 + mode = al.MODE.SIMD + + symbol = 'min_custom_op_impl' + bitcode = os.path.abspath(__file__) + + +@al.register_custom_op +class simple_custom_op: + # name is optional, use class name by default. + name = 'simple_custom_op' + + # required attributes. + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + + symbol = 'simple_custom_op_impl' + bitcode = os.path.abspath(__file__) + + # __init__ method is optional, but it can be used for better user experience + # when provided. for example, you can validate arguments here. + def __init__(self, x, y, dim=0, out=None): + assert x.shape == y.shape, "x and y should have same shape" + assert isinstance(dim, int), "dim should be const integer" + assert out, "out is required" + + +@al.register_custom_op +class _example_custom_op: + name = 'example_custom_op' + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + + symbol = 'example_custom_op_impl' + bitcode = os.path.abspath(__file__) + + def __init__(self, src, index, offset: tl.int64, axis, out=None): + # support validate arguments in __init__ method. + assert isinstance(src, tl.tensor), "src should be tensor" + assert index.dtype.is_int(), "index should be integer tensor" + assert isinstance(offset, int), "offset should be integer" + assert isinstance(axis, int), "axis should be integer" + + # support multi-output by using tuple or list. + assert isinstance(out, tuple) and len(out) == 2, "out should be tuple of 2 items" + + # setup the symbol name of the function that will be called at runtime. + rank = len(index.shape) + self.symbol = f"{self.name}_{rank}d_{src.dtype.cname}_{index.dtype.cname}" + + # setup source and compile command if it is implemented by user source code. + self.source = f"workspace/example_custom_op_impl.cce" + self.compile = "bisheng -O2 -std=c++17 -o $@ -c $<" + + # dynamic set argument type. + self.arg_type['axis'] = index.dtype + + +@al.builtin +def example_op(src, index, offset, axis, _semantic=None): + # you can wrap a custom op as a builtin operation, + # output can be provided here to make it easy to use. + x = _semantic.full(src.shape, 0, tl.float32) + y = _semantic.full(index.shape, 0, tl.float32) + return al.custom_semantic(_example_custom_op.name, src, index, offset, axis, out=(x, y), _semantic=_semantic) + + +@triton.jit +def my_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + y = al.custom("min_custom_op", x, x_ptr, y_ptr + i, al.int64(0), (1, 2, 3), [4.1, 5.2], out=y) + y = al.custom("simple_custom_op", x, y, dim=1, out=y) + index = tl.full((2, 3), 0, tl.int64) + x, y = al.custom("example_custom_op", x, index, offset=1, axis=0, out=(x, y)) + result, _ = example_op(x, index, offset=2, axis=1) + tl.store(out_ptr + i, result, mask=i < n) + + +if __name__ == "__main__": + src = ASTSource(my_kernel, {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, {"BLOCK": 256}) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + options = NPUOptions() + try: + ttir = ast_to_ttir(my_kernel, src, context, options, {}, {}) + print("=== TTIR ===") + print(ttir) + metadata = { + **options.__dict__, + } + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + print("=== MLIR (linalg) ===") + print(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") diff --git a/third_party/ascend/unittest/custom_op/custom_op_extra_buffer_demo.py b/third_party/ascend/unittest/custom_op/custom_op_extra_buffer_demo.py new file mode 100755 index 0000000000..fe2dbe351b --- /dev/null +++ b/third_party/ascend/unittest/custom_op/custom_op_extra_buffer_demo.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Demo: declare scratch/extra buffers on a custom op via `extra_buffers` (dtype, size) +# and read back the sizes from lowered HIVM MLIR (`extra_buffers_sizes` attribute). + +from __future__ import annotations + +import os +import re +import subprocess + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, ttir_to_linalg + +# Scratch buffers requested by the custom kernel (element type + length in elements). +SCRATCH_SPEC = [ + (tl.float32, 1024), + (tl.bfloat16, 512), + (tl.int32, 256), +] + + +@al.register_custom_op +class demo_extra_buffer_op: + """Custom op that advertises extra device buffers for the NPU compiler / runtime.""" + + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + symbol = "demo_extra_buffer_op_impl" + bitcode = os.path.abspath(__file__) + + def __init__(self, x, out=None): + self.indexing_map = [al.affine_map.get_identity(1)] + self.extra_buffers = list(SCRATCH_SPEC) + + +@triton.jit +def kernel_extra_buf(x_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(out_ptr + i, mask=i < n) + r = al.custom("demo_extra_buffer_op", x, out=y) + tl.store(out_ptr + i, r, mask=i < n) + + +def compile_to_linalg_mlir(kernel, signature: dict, constants: dict) -> str | None: + src = ASTSource(kernel, signature, constants) + ctx = ir.context() + ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + options = NPUOptions() + try: + ttir = ast_to_ttir(kernel, src, ctx, options, {}, {}) + meta = {**options.__dict__} + return str(ttir_to_linalg(ttir, meta, options, named_ops=True)) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + return None + + +def extract_extra_buffer_sizes_from_mlir(mlir: str) -> list[int]: + """ + Parse `extra_buffers_sizes` from HIVM custom op text. + """ + # Parse [1024, 512, 256, ...] + m = re.search(r"extra_buffers_sizes\s*=\s*\[([^\]]+)\]", mlir) + if m: + raw = m.group(1).replace(" ", "") + return [int(x) for x in raw.split(",") if x] + + return [] + + +def main() -> None: + expected_sizes = [size for _, size in SCRATCH_SPEC] + print("Declared extra_buffers (dtype, element_count):") + for dt, sz in SCRATCH_SPEC: + print(f" {dt} -> {sz} elements") + + mlir = compile_to_linalg_mlir( + kernel_extra_buf, + {"x_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 128}, + ) + if not mlir: + print("Compilation failed.") + return + + parsed = extract_extra_buffer_sizes_from_mlir(mlir) + print("\nParsed extra_buffers_sizes from MLIR:", parsed) + if parsed == expected_sizes: + print("OK: MLIR sizes match the Python extra_buffers specification.") + elif parsed: + print("Note: parsed sizes differ from spec; inspect MLIR spelling below.") + else: + print("Could not parse extra_buffers_sizes automatically; " + "search the dump for 'extra_buffers_sizes'.") + + print("\n--- MLIR excerpt (lines containing hivm.hir.custom) ---") + for line in mlir.splitlines(): + if "hivm.hir.custom" in line and "demo_extra_buffer_op" in line: + print(line) + + +if __name__ == "__main__": + main() diff --git a/third_party/ascend/unittest/custom_op/custom_op_indexing_map_complex_demo.py b/third_party/ascend/unittest/custom_op/custom_op_indexing_map_complex_demo.py new file mode 100755 index 0000000000..11e6dcf3cf --- /dev/null +++ b/third_party/ascend/unittest/custom_op/custom_op_indexing_map_complex_demo.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +import os +import subprocess +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, ttir_to_linalg + + +def _make_indexing_maps(): + d0 = ascend_ir.affine_expr.get_dim(0) + d1 = ascend_ir.affine_expr.get_dim(1) + c8 = ascend_ir.affine_expr.get_constant(8) + + # Input maps use transpose and identity-like projections. + in0 = ascend_ir.affine_map.get(2, 0, [d1, d0]) + in1 = ascend_ir.affine_map.get(2, 0, [d0, d1]) + + # Output map models tiled coordinates. + out = ascend_ir.affine_map.get(2, 0, [d0.floordiv(c8), d1.mod(c8)]) + return [in0, in1, out] + + +@al.register_custom_op +class complex_indexing_map_custom_op: + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + symbol = "complex_indexing_map_custom" + # Fake path: this example checks IR lowering only. + bitcode = os.path.abspath(__file__) + + def __init__(self, x, y, out=None): + assert out is not None + self.indexing_map = _make_indexing_maps() + + +@triton.jit +def my_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + out = al.custom("complex_indexing_map_custom_op", x, y, out=x) + tl.store(out_ptr + i, out, mask=i < n) + + +if __name__ == "__main__": + src = ASTSource( + my_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + options = NPUOptions() + try: + ttir = ast_to_ttir(my_kernel, src, context, options, {}, {}) + print("=== TTIR ===") + print(ttir) + linalg = ttir_to_linalg(ttir, {**options.__dict__}, options, named_ops=True) + print("=== MLIR (linalg) ===") + print(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") diff --git a/third_party/ascend/unittest/custom_op/custom_op_indexing_map_compose_demo.py b/third_party/ascend/unittest/custom_op/custom_op_indexing_map_compose_demo.py new file mode 100755 index 0000000000..6d42c5b832 --- /dev/null +++ b/third_party/ascend/unittest/custom_op/custom_op_indexing_map_compose_demo.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +import os +import subprocess +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, ttir_to_linalg + + +def _compose_indexing_maps(): + d0 = ascend_ir.affine_expr.get_dim(0) + d1 = ascend_ir.affine_expr.get_dim(1) + c4 = ascend_ir.affine_expr.get_constant(4) + + # Base permutation map. + perm = ascend_ir.affine_map.get_permutation([1, 0]) + # Tile map (row-major tile decomposition). + tile = ascend_ir.affine_map.get(2, 0, [d0.floordiv(c4), d1.mod(c4)]) + # Compose tile with permutation to build a different output indexing. + out = tile.compose(perm) + + in0 = ascend_ir.affine_map.get_identity(2) + in1 = perm + return [in0, in1, out] + + +@al.register_custom_op +class compose_indexing_map_custom_op: + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + symbol = "compose_indexing_map_custom" + bitcode = os.path.abspath(__file__) + + def __init__(self, x, y, out=None): + assert out is not None + self.indexing_map = _compose_indexing_maps() + + +@triton.jit +def my_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + out = al.custom("compose_indexing_map_custom_op", x, y, out=y) + tl.store(out_ptr + i, out, mask=i < n) + + +if __name__ == "__main__": + src = ASTSource( + my_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + options = NPUOptions() + try: + ttir = ast_to_ttir(my_kernel, src, context, options, {}, {}) + print("=== TTIR ===") + print(ttir) + linalg = ttir_to_linalg(ttir, {**options.__dict__}, options, named_ops=True) + print("=== MLIR (linalg) ===") + print(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") diff --git a/third_party/ascend/unittest/custom_op/test_index_select.py b/third_party/ascend/unittest/custom_op/test_index_select.py new file mode 100755 index 0000000000..593b909379 --- /dev/null +++ b/third_party/ascend/unittest/custom_op/test_index_select.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import pytest +import torch +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al + + +@triton.jit +def builtin_index_select_kernel(src_ptr, index_ptr, out_ptr): + # Define 2x2 tile indices for output tensor + r = tl.arange(0, 2)[:, None] # Row indices: shape [2, 1] + c = tl.arange(0, 2)[None, :] # Column indices: shape [1, 2] + + # Load index tensor (shape [2]) from GM to UB + idx = tl.load(index_ptr + tl.arange(0, 2)) + # Initialize empty 2x2 output tile in UB (default value: 0) + dst = tl.full((2, 2), 0, dtype=tl.float32) + + # Invoke __builtin_index_select custom op to gather elements + out_tile = al.custom("__builtin_index_select", src_ptr, # Pointer to source tensor in GM + idx, # Index tensor (in UB) for gathering + dim=0, # Dimension to gather along + bound=4, # Upper bound for valid index values (out-of-bound check) + end_offset=(2, 2), # End offsets of each dimension for the index tensor + start_offset=(0, 0), # Start offsets of each dimension for the source tensor + src_stride=(4, 1), # Stride of each dimension for the source tensor in GM + out=dst # Output tensor (in UB) to store gathered elements + ) + + # Store the gathered tile from UB to output tensor in GM + tl.store(out_ptr + r * 2 + c, out_tile) + + +if __name__ == "__main__": + src = torch.tensor( + [[10., 11., 12., 13.], [20., 21., 22., 23.], [30., 31., 32., 33.], [40., 41., 42., 43.]], + device="npu", + dtype=torch.float32, + ) + index = torch.tensor([2, 0], device="npu", dtype=torch.int32) + out = torch.empty((2, 2), device="npu", dtype=torch.float32) + ref = torch.index_select(src, 0, index.to(torch.int64))[:, :2] + builtin_index_select_kernel[(1, )](src, index, out) + torch.testing.assert_close(out, ref) # ref: [[30., 31.], [10., 11.]] diff --git a/third_party/ascend/unittest/device_ut/test_common.py b/third_party/ascend/unittest/device_ut/test_common.py new file mode 100644 index 0000000000..3a38ae9b66 --- /dev/null +++ b/third_party/ascend/unittest/device_ut/test_common.py @@ -0,0 +1,223 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from typing import Optional +import torch +import torch_npu +import pytest +import functools +import re +import numpy as np + +_float_dtypes = ['float32', 'float16', 'bfloat16'] +_int_dtypes = ['int32', 'int64', 'int16', 'int8'] +_uint_dtypes = ['uint8', 'uint16', 'uint32', 'uint64'] +_all_dtypes_no_bool = _float_dtypes + _int_dtypes +_all_dtypes = _all_dtypes_no_bool + ['bool'] +_32bit_dtypes = ['float32', 'int32'] +_16bit_dtypes = ['float16', 'bfloat16', 'int16'] + + +def generate_numpy(shape, dtype, low=None, high=None): + if dtype in _int_dtypes + _uint_dtypes: + iinfo = np.iinfo(getattr(np, dtype)) + low = iinfo.min if low is None else max(low, iinfo.min) + high = iinfo.max if high is None else min(high, iinfo.max) + dty = getattr(np, dtype) + return np.random.randint(low, high, shape, dtype=dty) + elif dtype == 'float16' or dtype == 'float32': + return np.random.normal(0, 1, shape).astype(dtype) + elif dtype == 'bfloat16': + return (np.random.normal(0, 1, shape).astype('float32').view('uint32') & np.uint32(0xffff0000)).view('float32') + elif dtype == 'bool': + return np.random.randint(low=0, high=2, size=shape).astype(bool) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +def generate_tensor(shape, dtype): + if dtype == 'float32' or dtype == 'float16' or dtype == 'bfloat16': + return torch.randn(size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16': + return torch.randint(low=0, high=2000, size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'int8': + return torch.randint(low=0, high=127, size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'bool': + return torch.randint(low=0, high=2, size=shape).bool() + elif dtype == 'uint8': + return torch.randint(low=0, high=255, size=shape, dtype=torch.uint8) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +def get_triton_sig_typename(dtype): + if dtype == 'float32': + tyname = "*fp32" + elif dtype == 'int32': + tyname = "*i32" + elif dtype == 'int64': + tyname = "*i64" + elif dtype == 'float16': + tyname = "*fp16" + elif dtype == 'int16': + tyname = "*i16" + elif dtype == 'int8': + tyname = "*i8" + elif dtype == 'bool': + tyname = "*i1" + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + return tyname + + +# Relative error: abs(x_ref - x_cal) / abs(x_ref) +# Absolute error: abs(x_ref - x_cal) + + +# calculation type operators require different error range +# It is a stricter verification and not satisfied now, save it here +def validate_cal(dtype, y_cal, y_ref): + if dtype == 'float16': + if torch.mean(y_ref) < 0.001: + assert torch.abs(y_cal - y_ref) < 0.001, "|y_cal - y_ref| < 0.001 is required !" + else: + diff = torch.div(torch.abs(y_cal - y_ref), torch.abs(y_cal)) < 0.001 + # all true + assert diff.all(), "Relative error is less than 0.001 !" + if dtype == 'float32': + if torch.mean(y_ref) < 0.0001: + assert torch.abs(y_cal - y_ref) < 0.0001, "|y_cal - y_ref| < 0.0001 is required !" + else: + diff = torch.div(torch.abs(y_cal - y_ref), torch.abs(y_cal)) < 0.0001 + assert diff.all(), "Relative error is less than 0.001 !" + elif dtype == 'bfloat16': + diff = torch.div(torch.abs(y_cal - y_ref), torch.abs(y_cal)) < 0.001 + assert diff.all(), "Relative error is less than 0.001 !" + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16' or dtype == 'int8': + assert torch.equal(y_cal, y_ref) + elif dtype == 'uint8' or dtype == 'uint16' or dtype == 'uint32' or dtype == 'uint64': + assert torch.equal(y_cal, y_ref) + elif dtype == 'bool': + assert torch.equal(y_cal, y_ref) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +# moving and comparison ops require no precision error +def validate_cmp(dtype, y_cal, y_ref, overflow_mode: Optional[str] = None): + y_cal = y_cal.npu() + y_ref = y_ref.npu() + if overflow_mode == "saturate": + if dtype in ['float32', 'float16']: + min_value = -torch.finfo(dtype).min + max_value = torch.finfo(dtype).max + elif dtype in ['int32', 'int16', 'int8']: + min_value = torch.iinfo(dtype).min + max_value = torch.iinfo(dtype).max + elif dtype == 'bool': + min_value = 0 + max_value = 1 + else: + raise ValueError('Invalid parameter "dtype" is found : {}'.format(dtype)) + y_ref = torch.clamp(y_ref, min=min_value, max=max_value) + if dtype == 'float16': + torch.testing.assert_close(y_ref, y_cal, rtol=1e-03, atol=1e-03, equal_nan=True) + elif dtype == 'bfloat16': + torch.testing.assert_close(y_ref.to(torch.float32), y_cal.to(torch.float32), rtol=1e-03, atol=1e-03, + equal_nan=True) + elif dtype == 'float32': + torch.testing.assert_close(y_ref, y_cal, rtol=1e-04, atol=1e-04, equal_nan=True) + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16' or dtype == 'int8': + assert torch.equal(y_cal, y_ref) + elif dtype == 'uint8' or dtype == 'uint16' or dtype == 'uint32' or dtype == 'uint64': + assert torch.equal(y_cal, y_ref) + elif dtype == 'bool': + assert torch.equal(y_cal, y_ref) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +def validate_cmp_with_expection(dtype, y_cal, y_ref, expect): + if dtype == 'float32' or dtype == 'float16' or dtype == 'bfloat16': + if expect: + assert torch.allclose(y_ref, y_cal, rtol=1e-03, atol=1e-03, equal_nan=True) + else: + assert not torch.allclose(y_ref, y_cal, rtol=1e-03, atol=1e-03, equal_nan=True) + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16' or dtype == 'int8' \ + or dtype == 'uint8' or dtype == 'uint16' or dtype == 'uint32' or dtype == 'uint64': + if expect: + assert torch.equal(y_cal, y_ref) + else: + assert not torch.equal(y_cal, y_ref) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +# Use the following pytest fixture to run one test case by only single worker. +# Refer to https://pytest-xdist.readthedocs.io/en/stable/how-to.html#making-session-scoped-fixtures-execute-only-once +@pytest.fixture(scope="function") +def pytest_runonce(worker_id, request, cache): + if (cache.get(request.node.nodeid, "none")) == "none": + cache.set(request.node.nodeid, worker_id) + else: + file_name = f"pytest_{worker_id}.txt" + with open(file_name, 'a') as file: + file.write(f"{request.node.nodeid} is already processed by {worker_id}") + return True + yield True + cache.set(request.node.nodeid, "none") + + +def raises_with_match(expected_exception, match_pattern): + + def decorator(test_func): + + @functools.wraps(test_func) + def wrapper(*args, **kwargs): + with pytest.raises(expected_exception, match=match_pattern): + return test_func(*args, **kwargs) + + return wrapper + + return decorator + + +def capture_output(expected_output): + + def decorator(test_func): + + @functools.wraps(test_func) + def wrapper(*args, **kwargs): + capsys = kwargs.pop('capsys', None) + if capsys is None: + try: + capsys = pytest.fixture(capsys)() + except: + raise RuntimeError("This decorator requires pytest's capsys fixture") + test_func(capsys, *args, **kwargs) + captured = capsys.readouterr() + # pybind11::scoped_ostream_redirect captures std::cout with \x00 inserted + # for now, no idea how to eliminate \x00 from C++ side. + cleaned = re.sub(r"\x00", "", captured.out) + assert expected_output in cleaned + + return wrapper + + return decorator diff --git a/third_party/ascend/unittest/device_ut/test_device_print.py b/third_party/ascend/unittest/device_ut/test_device_print.py new file mode 100644 index 0000000000..29c5d2b17c --- /dev/null +++ b/third_party/ascend/unittest/device_ut/test_device_print.py @@ -0,0 +1,158 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest +import test_common +import os + +os.environ["TRITON_DEVICE_PRINT"] = "1" +os.environ["TRITON_ENABLE_TASKQUEUE"] = "0" +shape = (8, ) +XS = 8 +XVALS_INT = [ + 0, + torch.iinfo(torch.int8).min, + torch.iinfo(torch.int8).max, + torch.iinfo(torch.int16).min, + torch.iinfo(torch.int16).max, + torch.iinfo(torch.int32).min, + torch.iinfo(torch.int32).max, + torch.iinfo(torch.int32).max + 1 +] +XVALS_FP = [ + 0, + torch.finfo(torch.float32).eps, + torch.finfo(torch.float16).eps, + torch.finfo(torch.bfloat16).eps, + torch.finfo(torch.float32).max, + torch.finfo(torch.float16).max, + torch.finfo(torch.bfloat16).max, 1 +] + + +def torch_func(x0, x1): + res = x0 + x1 + return res + + +@triton.jit +def triton_kernel(out_ptr0, in_ptr0, in_ptr1, XBLOCK: tl.constexpr): + idx = tl.arange(0, XBLOCK) + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.load(in_ptr1 + idx) + tmp2 = tmp0 + tmp1 + tl.device_print("OUTPUT = ", tmp2) + tl.store(out_ptr0 + idx, tmp2) + + +def triton_func(x0, x1, XS): + out = torch.empty_like(x0) + triton_kernel[1, 1, 1](out, x0, x1, XS) + return out + + +@pytest.mark.skip(reason="waiting for bishengir-compile to support") +@pytest.mark.parametrize('sigtype', ['int64']) +def test_device_print_int64(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.parametrize('sigtype', ['int32']) +def test_device_print_int32(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.parametrize('sigtype', ['int16']) +def test_device_print_int16(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.parametrize('sigtype', ['int8']) +def test_device_print_int8(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.parametrize('sigtype', ['float32']) +def test_device_print_fp32(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_FP[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.parametrize('sigtype', ['float16']) +def test_device_print_fp16(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_FP[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.skip(reason="waiting for bishengir-compile to support") +@pytest.mark.parametrize('sigtype', ['bfloat16']) +def test_device_print_bf16(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_FP[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) diff --git a/third_party/ascend/unittest/device_ut/test_device_print_comprehensive.py b/third_party/ascend/unittest/device_ut/test_device_print_comprehensive.py new file mode 100644 index 0000000000..8cd8f14b12 --- /dev/null +++ b/third_party/ascend/unittest/device_ut/test_device_print_comprehensive.py @@ -0,0 +1,123 @@ +import os +import sys +import subprocess +import tempfile +import textwrap + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + +expected_prints = [ + "Offsets:", + "Mask:", + "Pointer offsets:", + "Loaded x:", + "Scalar factor:", + "Temp result (x * 2):", + "Final y (x * 2 + 1):", + "Positive mask:", + "Block ID:", + "Block start:", + "Valid elements in this block:", +] + + +def test_comprehensive_print(): + + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + temp_script = f.name + + f.write( + textwrap.dedent(f""" +import os +import sys +import subprocess +import tempfile +import textwrap + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + +os.environ["TRITON_DEVICE_PRINT"] = "1" + +@triton.jit +def comprehensive_print_kernel( + x_ptr, + y_ptr, + mask_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + tl.device_print("=====debug=====") + pid = tl.program_id(0) + block_start = pid * BLOCK_SIZE + + offsets = block_start + tl.arange(0, BLOCK_SIZE) + tl.device_print("Offsets: ", offsets) + + mask = offsets < n_elements + tl.device_print("Mask: ", mask) + + x_ptrs = x_ptr + offsets + tl.device_print("Pointer offsets: ", offsets) + + x = tl.load(x_ptrs, mask=mask, other=0.0) + tl.device_print("Loaded x: ", x) + + scalar_factor = 2.0 + tl.device_print("Scalar factor: ", scalar_factor) + + y_temp = x * scalar_factor + tl.device_print("Temp result (x * 2): ", y_temp) + + y = y_temp + 1.0 + tl.device_print("Final y (x * 2 + 1): ", y) + + positive_mask = y > 0.0 + tl.device_print("Positive mask: ", positive_mask) + + tl.device_print("Block ID: ", pid) + tl.device_print("Block start: ", block_start) + + y_ptrs = y_ptr + offsets + tl.store(y_ptrs, y, mask=mask) + mask_count = tl.sum(mask.to(tl.int32)) + tl.device_print("Valid elements in this block: ", mask_count) + + +def test_comprehensive_print(): + size = 16 + x = torch.randn(size).npu() + y = torch.zeros(size).npu() + mask = torch.ones(size, dtype=torch.bool).npu() + BLOCK_SIZE = 32 + + h = comprehensive_print_kernel[1,](x, y, mask, size, BLOCK_SIZE=BLOCK_SIZE) + + expected = x * 2.0 + 1.0 + torch.testing.assert_close(y, expected, rtol=1e-5, atol=1e-5) + + for i in range(12): + opStr = "call @triton_print_" + str(i) + assert opStr in h.asm["ttadapter"] + + print("passed!") + + +if __name__ == "__main__": + test_comprehensive_print() + """)) + + result = subprocess.run([sys.executable, temp_script], capture_output=True, text=True, env=os.environ.copy()) + + captured_output = result.stdout + "\n=== STDERR ===\n" + result.stderr + + assert "passed!" in captured_output + for prefix in expected_prints: + assert prefix in captured_output diff --git a/third_party/ascend/unittest/device_ut/test_device_print_script.py b/third_party/ascend/unittest/device_ut/test_device_print_script.py new file mode 100644 index 0000000000..15136f67cf --- /dev/null +++ b/third_party/ascend/unittest/device_ut/test_device_print_script.py @@ -0,0 +1,117 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import subprocess +import re +import pytest + + +# 执行test_device_print.py -> 生成 test_device_print.log, 以便执行后续验证 +def run_test_device_print_py(test_name, log_name): + testfile_path = os.path.join(os.getcwd(), test_name) + logfile_path = os.path.join(os.getcwd(), log_name) + + with open(logfile_path, 'w') as f: + try: + subprocess.run(["pytest", testfile_path], stdout=f, stderr=subprocess.STDOUT, check=True) + print(f"Run 【{test_name}】 successfully!") + except Exception as e: + print(f"Run 【{test_name}】 unsuccessfully: ", e) + + +def assert_close(expected_output, logfile): + # 读回日志内容 + with open(logfile, "r", encoding="utf-8") as f: + raw = f.read() + cleaned = re.sub(r"\x00", "", raw) + + # 在文件内容里做检查 + assert expected_output in cleaned, f"Expected '{expected_output}' not found in log file." + + +@pytest.mark.skip(reason="waiting for TA to support") +def test_device_print_int8(): + expected_output = "0,-128,127,0,-1,0,-1,0" + test_name = "test_device_print.py::test_device_print_int8[int8]" + log_name = "test_device_print_int8.log" + logfile = os.path.join(os.getcwd(), log_name) + run_test_device_print_py(test_name, logfile) + assert_close(expected_output, logfile) + + +@pytest.mark.skip(reason="waiting for TA to support") +def test_device_print_int16(): + expected_output = "0,-128,127,-32768,32767,0,-1,0" + test_name = "test_device_print.py::test_device_print_int16[int16]" + log_name = "test_device_print_int16.log" + logfile = os.path.join(os.getcwd(), log_name) + run_test_device_print_py(test_name, logfile) + assert_close(expected_output, logfile) + + +@pytest.mark.skip(reason="waiting for TA to support") +def test_device_print_int32(): + expected_output = "0,-128,127,-32768,32767,-2147483648,2147483647,-2147483648" + test_name = "test_device_print.py::test_device_print_int32[int32]" + log_name = "test_device_print_int32.log" + logfile = os.path.join(os.getcwd(), log_name) + run_test_device_print_py(test_name, logfile) + assert_close(expected_output, logfile) + + +@pytest.mark.skip(reason="waiting for compiler to support") +def test_device_print_int64(): + expected_output = "???" + test_name = "test_device_print.py::test_device_print_int64[int64]" + log_name = "test_device_print_int64.log" + logfile = os.path.join(os.getcwd(), log_name) + run_test_device_print_py(test_name, logfile) + assert_close(expected_output, logfile) + + +@pytest.mark.skip(reason="waiting for TA to support") +def test_device_print_fp16(): + expected_output = "0.000000,0.000000,0.000977,0.007812,inf,65504.000000,inf,1.000000" + test_name = "test_device_print.py::test_device_print_fp16[float16]" + log_name = "test_device_print_fp16.log" + logfile = os.path.join(os.getcwd(), log_name) + run_test_device_print_py(test_name, logfile) + assert_close(expected_output, logfile) + + +@pytest.mark.skip(reason="waiting for TA to support") +def test_device_print_fp32(): + expected_output = "0.000000,0.000000,0.000977,0.007812,340282346638528859811704183484516925440.000000,65504.000000,338953138925153547590470800371487866880.000000,1.000000" + test_name = "test_device_print.py::test_device_print_fp32[float32]" + log_name = "test_device_print_fp16.log" + logfile = os.path.join(os.getcwd(), log_name) + run_test_device_print_py(test_name, logfile) + assert_close(expected_output, logfile) + + +@pytest.mark.skip(reason="waiting for compiler to support") +def test_device_print_bf16(): + expected_output = "???" + test_name = "test_device_print.py::test_device_print_bf16[bfloat16]" + log_name = "test_device_print_bf16.log" + logfile = os.path.join(os.getcwd(), log_name) + run_test_device_print_py(test_name, logfile) + assert_close(expected_output, logfile) diff --git a/third_party/ascend/unittest/extra-info/data-dump/0/kernel_minimal_host.o b/third_party/ascend/unittest/extra-info/data-dump/0/kernel_minimal_host.o new file mode 100644 index 0000000000..7aad914748 Binary files /dev/null and b/third_party/ascend/unittest/extra-info/data-dump/0/kernel_minimal_host.o differ diff --git a/third_party/ascend/unittest/lit.cfg.py b/third_party/ascend/unittest/lit.cfg.py new file mode 100644 index 0000000000..0fc34a7fa7 --- /dev/null +++ b/third_party/ascend/unittest/lit.cfg.py @@ -0,0 +1,66 @@ +# -*- Python -*- + +import os +import platform +import re +import subprocess +import tempfile + +import lit.formats +import lit.util +from lit.llvm import llvm_config +from lit.llvm.subst import FindTool, ToolSubst + +# Configuration file for the 'lit' test runner + +# name: The name of this test suite +config.name = 'TRITON-ASCEND-MLIR' + +# Always use the internal lit shell: lit >= 23 rejects execute_external=True +# (removed entirely in LLVM 24), and none of the RUN lines need bash. +config.test_format = lit.formats.ShTest(False) + +# suffixes: A list of file extensions to treat as test files. +config.suffixes = ['.mlir'] + +# test_source_root: The root path where tests are located. +config.test_source_root = os.path.dirname(os.path.abspath(__file__)) + +# test_exec_root: The root path where tests should be run. +config.test_exec_root = os.path.join(config.triton_obj_root, 'test') +config.substitutions.append(('%PATH%', config.environment['PATH'])) + +llvm_config.with_system_environment(['HOME', 'INCLUDE', 'LIB', 'TMP', 'TEMP']) + +# excludes: A list of directories to exclude from the testsuite. The 'Inputs' +# subdirectories contain auxiliary inputs for various tests in their parent +# directories. +config.excludes = ['Inputs', 'Examples', 'CMakeLists.txt', 'README.txt', 'LICENSE.txt'] + +# test_source_root: The root path where tests are located. +config.test_source_root = os.path.dirname(__file__) + +# test_exec_root: The root path where tests should be run. +config.test_exec_root = os.path.join(config.triton_obj_root, 'test') +config.triton_tools_dir = os.path.join(config.triton_obj_root, 'bin') +config.filecheck_dir = config.llvm_tools_dir +# FileCheck -enable-var-scope is enabled by default in MLIR test +# This option avoids to accidentally reuse variable across -LABEL match, +# it can be explicitly opted-in by prefixing the variable name with $ +config.environment["FILECHECK_OPTS"] = "--enable-var-scope" + +tool_dirs = [config.triton_tools_dir, config.llvm_tools_dir, config.filecheck_dir] + +# Tweak the PATH to include the tools dir. +for d in tool_dirs: + llvm_config.with_environment('PATH', d, append_path=True) +tools = [ + 'triton-opt', + ToolSubst('%PYTHON', config.python_executable, unresolved='ignore'), +] + +llvm_config.add_tool_substitutions(tools, tool_dirs) + +llvm_config.with_environment('PYTHONPATH', [ + os.path.join(config.mlir_binary_dir, 'python_packages', 'triton'), +], append_path=True) diff --git a/third_party/ascend/unittest/lit.site.cfg.py.in b/third_party/ascend/unittest/lit.site.cfg.py.in new file mode 100644 index 0000000000..8bbb8e0224 --- /dev/null +++ b/third_party/ascend/unittest/lit.site.cfg.py.in @@ -0,0 +1,23 @@ +@LIT_SITE_CFG_IN_HEADER@ + +import sys + +config.triton_obj_root = "@triton_BINARY_DIR@" +config.llvm_src_root = "@LLVM_SOURCE_DIR@" +config.llvm_obj_root = "@LLVM_BINARY_DIR@" +config.llvm_tools_dir = "@LLVM_TOOLS_DIR@" +config.llvm_lib_dir = "@LLVM_LIBS_DIR@" +config.llvm_shlib_dir = "@CMAKE_LIBRARY_OUTPUT_DIRECTORY@" +config.llvm_shlib_ext = "@CMAKE_SHARED_LIBRARY_SUFFIX@" +config.llvm_exe_ext = "@EXEEXT@" +config.lit_tools_dir = "@LLVM_LIT_TOOLS_DIR@" +config.mlir_binary_dir = "@MLIR_BINARY_DIR@" +config.python_executable = "@Python3_EXECUTABLE@" +config.enable_bindings_python = @MLIR_ENABLE_BINDINGS_PYTHON@ + + +import lit.llvm +lit.llvm.initialize(lit_config, config) + +# Let the main config do the real work +lit_config.load_config(config, "@CMAKE_CURRENT_SOURCE_DIR@/lit.cfg.py") diff --git a/third_party/ascend/unittest/pytest_ut/ascend_codegen_fns.py b/third_party/ascend/unittest/pytest_ut/ascend_codegen_fns.py new file mode 100644 index 0000000000..567c8b6701 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/ascend_codegen_fns.py @@ -0,0 +1,50 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Shared ``codegen_fns`` for tests that drive ``ast_to_ttir`` directly. + +``CodeGenerator`` builds a plain ``ir.builder``; the ascend and buffer builder +methods (``get_target_attribute``, ``is_910_95``, ``_ascend_builder``, ...) are +bridged onto it by the backend's ``initialize_builder`` codegen function. Tests +that bypass ``AscendBackend`` and call ``ast_to_ttir`` themselves must supply +that function too, otherwise codegen fails with:: + + 'triton._C.libtriton.ir.builder' object has no attribute 'get_target_attribute' + +This module keeps that single source of truth. It deliberately does not import +``test_common``, which pulls in ``torch_npu``: these callers compile to MLIR +only and need no NPU runtime. +""" + +from triton.backends.ascend.compiler import _initialize_ascend_builder + +__all__ = ["ascend_codegen_fns"] + + +def ascend_codegen_fns(**extra): + """Return ``codegen_fns`` wired for the ascend backend. + + Keyword arguments are merged in, so a caller needing the address-space + helper writes:: + + ascend_codegen_fns(create_address_space=al.semantic.create_address_space) + """ + fns = {"initialize_builder": _initialize_ascend_builder} + fns.update(extra) + return fns diff --git a/third_party/ascend/unittest/pytest_ut/attn_cp_triton_kernel_3d.py b/third_party/ascend/unittest/pytest_ut/attn_cp_triton_kernel_3d.py new file mode 100644 index 0000000000..195458a936 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/attn_cp_triton_kernel_3d.py @@ -0,0 +1,182 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu + +#number of block per aiv core +NBLOCKS = 32 +#constants +TL_DTYPE_ATTN = tl.bfloat16 + + +@triton.jit +def tl_fn_forward_update(output_ptr0, output_ptr1, output_ptr2, prev_attn_out, prev_softmax_max, prev_softmax_sum, + cur_attn_out, cur_softmax_max, cur_softmax_sum, S: tl.constexpr, B: tl.constexpr, + H: tl.constexpr, N: tl.constexpr, E: tl.constexpr, CUR_SM_N_STRIDE: tl.constexpr, + PREV_SM_N_STRIDE: tl.constexpr, S_NBLOCKS: tl.constexpr, S_SUB: tl.constexpr, + BRC_SIZE: tl.constexpr): + + S_E_SUB_SIZE: tl.constexpr = S_SUB * E + D: tl.constexpr = H // N + S_BLOCK: tl.constexpr = (S + S_NBLOCKS - 1) // S_NBLOCKS + S_NSUB: tl.constexpr = (S_BLOCK + S_SUB - 1) // S_SUB + + D_SUB: tl.constexpr = E * BRC_SIZE + D_NLOOP: tl.constexpr = (D + D_SUB - 1) // D_SUB + + LOOP_COUNT: tl.constexpr = (S_BLOCK * B * N + S_SUB - 1) // S_SUB + block_idx = tl.program_id(0) + s_block_offset = block_idx % S_NBLOCKS * S_BLOCK + + for loop_index in range(LOOP_COUNT): + b = loop_index // (N * S_NSUB) + n = (loop_index // S_NSUB) % N + s_loop_offset = loop_index % S_NSUB * S_SUB + s = s_block_offset + s_loop_offset + tl.arange(0, S_SUB) #index on S axis + se_offset = E * (s_block_offset + s_loop_offset) + tl.arange(0, S_E_SUB_SIZE) + #assume no slice on N axis + offsets_prev = PREV_SM_N_STRIDE * (b * N + n) + se_offset + offsets_cur = CUR_SM_N_STRIDE * (b * N + n) + se_offset + mask0 = None + prev_softmax_max_local = tl.load(prev_softmax_max + offsets_prev, mask0) + cur_softmax_max_local = tl.load(cur_softmax_max + offsets_cur, mask0) + softmax_max = tl.maximum(prev_softmax_max_local, cur_softmax_max_local) + prev_scale = tl.exp(prev_softmax_max_local - softmax_max) + cur_scale = tl.exp(cur_softmax_max_local - softmax_max) + prev_softmax_sum_local = tl.load(prev_softmax_sum + offsets_prev, mask0) + cur_softmax_sum_local = tl.load(cur_softmax_sum + offsets_cur, mask0) + prev_softmax_sum_scaled = prev_softmax_sum_local * prev_scale + cur_softmax_sum_scaled = cur_softmax_sum_local * cur_scale + softmax_sum = prev_softmax_sum_scaled + cur_softmax_sum_scaled + prev_out_scale_local = prev_softmax_sum_scaled / softmax_sum + cur_out_scale_local = cur_softmax_sum_scaled / softmax_sum + prev_out_scale_out = prev_out_scale_local + cur_out_scale_out = cur_out_scale_local + mask1 = None + tl.store(output_ptr1 + offsets_cur, softmax_max, mask0) + tl.store(output_ptr2 + offsets_cur, softmax_sum, mask0) + prev_out_scale = prev_out_scale_local + prev_out_scale = prev_out_scale.reshape(S_SUB, 1, E) #(s_sub,8)->(1,1,s_sub,1,8) + cur_out_scale = cur_out_scale_local + cur_out_scale = cur_out_scale.reshape(S_SUB, 1, E) + for d_index in range(D_NLOOP): + # (s,b,h) -> (s,b,n*d) -> (s,b,n,d) + d = d_index * D_SUB + tl.arange(0, D_SUB) + offsets2 = s[:, None] * B * H + b * H + n * D + d[None, :] + mask2 = None + prev_attn_out_local = tl.load(prev_attn_out + offsets2, mask2) + cur_attn_out_local = tl.load(cur_attn_out + offsets2, mask2) + prev_attn_out_local = prev_attn_out_local.to(tl.float32) + cur_attn_out_local = cur_attn_out_local.to(tl.float32) + prev_attn_out_local = prev_attn_out_local.reshape(S_SUB, BRC_SIZE, E) + cur_attn_out_local = cur_attn_out_local.reshape(S_SUB, BRC_SIZE, E) + prev_out_scale_brc = prev_out_scale + prev_out_scale_brc = prev_out_scale_brc.broadcast_to(S_SUB, BRC_SIZE, E) + cur_out_scale_brc = cur_out_scale + cur_out_scale_brc = cur_out_scale_brc.broadcast_to(S_SUB, BRC_SIZE, E) + attn_out_local = prev_attn_out_local * prev_out_scale_brc + cur_attn_out_local * cur_out_scale_brc + attn_out_local = attn_out_local.reshape(S_SUB, D_SUB) + attn_out_local = attn_out_local.to(TL_DTYPE_ATTN) + tl.store(output_ptr0 + offsets2, attn_out_local, mask2) + + +#target_name = torch.npu.get_device_name(torch.npu.current_device()) +guards = {"dummy": None} + + +def forward_update_triton(prev_attn_out, prev_softmax_max, prev_softmax_sum, cur_attn_out, cur_softmax_max, + cur_softmax_sum): + + # size of sub_block in one block + S_SUB_SIZE = 64 + BROARDCAST_SIZE = 8 + + (S, B, H) = cur_attn_out.shape + N = prev_softmax_max.shape[1] + E = prev_softmax_max.shape[3] + D = S // H + D_SUB = E * BROARDCAST_SIZE + #(b,n,s,8) + PREV_SM_N_STRIDE = prev_softmax_max.stride()[1] + CUR_SM_N_STRIDE = cur_softmax_max.stride()[1] + + GUARD = (S % NBLOCKS == 0 and H % N == 0 and (H // N) % (D_SUB) == 0) + + if (not GUARD): + print(f"parameter does not meet compiling GUARD , fallback to eager foward_update \ + (S,H,N,D,D_SUB,NBLOCKS):{S},{H},{N},{D},{D_SUB},{NBLOCKS}") + + org_dtype = cur_attn_out.dtype + device_id = cur_attn_out.device.index + device = "npu:" + str(device_id) + + softmax_max = torch.empty_strided(cur_softmax_max.shape, cur_softmax_max.stride(), dtype=cur_softmax_max.dtype, + device=device) + softmax_sum = torch.empty_strided(cur_softmax_sum.shape, cur_softmax_sum.stride(), dtype=cur_softmax_max.dtype, + device=device) + attn_out = torch.empty_strided(cur_attn_out.shape, cur_attn_out.stride(), dtype=cur_attn_out.dtype, device=device) + + tl_fn_forward_update[NBLOCKS, 1, 1](attn_out, softmax_max, softmax_sum, prev_attn_out, prev_softmax_max, + prev_softmax_sum, cur_attn_out, cur_softmax_max, cur_softmax_sum, S=S, B=B, H=H, + N=N, E=E, CUR_SM_N_STRIDE=CUR_SM_N_STRIDE, PREV_SM_N_STRIDE=PREV_SM_N_STRIDE, + S_NBLOCKS=NBLOCKS, S_SUB=S_SUB_SIZE, BRC_SIZE=BROARDCAST_SIZE, debug=True) + + return attn_out, softmax_max, softmax_sum + + +@triton.jit +def tl_fn_backward_update(dq, dk, dv, cur_dq, cur_dk, cur_dv, qnumel: tl.constexpr, knumel: tl.constexpr, + XBLOCK: tl.constexpr, SIMD_SIZE: tl.constexpr): + + block_idx = tl.program_id(0) + block_offset = block_idx * XBLOCK + LOOP_COUNT: tl.constexpr = (XBLOCK + SIMD_SIZE - 1) // SIMD_SIZE + for loop_index in range(LOOP_COUNT): + loop_offset = block_offset + loop_index * SIMD_SIZE + tl.arange(0, SIMD_SIZE) + mask0 = loop_offset < qnumel + tmp0 = tl.load(dq + loop_offset, mask0).to(tl.float32) + tmp1 = tl.load(cur_dq + loop_offset, mask0).to(tl.float32) + tmp0 = tmp1 + tmp0 + tl.store(dq + loop_offset, tmp0.to(tl.bfloat16), mask0) + + mask1 = loop_offset < knumel + tmp2 = tl.load(dk + loop_offset, mask1).to(tl.float32) + tmp3 = tl.load(cur_dk + loop_offset, mask1).to(tl.float32) + tmp2 = tmp2 + tmp3 + + tmp4 = tl.load(dv + loop_offset, mask1).to(tl.float32) + tmp5 = tl.load(cur_dv + loop_offset, mask1).to(tl.float32) + tmp4 = tmp4 + tmp5 + + tl.store(dk + loop_offset, tmp2.to(tl.bfloat16), mask1) + tl.store(dv + loop_offset, tmp4.to(tl.bfloat16), mask1) + + +def backward_update_triton(dq, dk, dv, cur_dq, cur_dk, cur_dv): + # parameters need auto-tune. + SIMD_SIZE = 4 * 1024 + + xblock = max(dq.numel(), dk.numel()) // NBLOCKS + + tl_fn_backward_update[40, 1, 1](dq, dk, dv, cur_dq, cur_dk, cur_dv, dq.numel(), dk.numel(), xblock, SIMD_SIZE, + debug=True) diff --git a/third_party/ascend/unittest/pytest_ut/attn_cp_triton_kernel_3d_la.py b/third_party/ascend/unittest/pytest_ut/attn_cp_triton_kernel_3d_la.py new file mode 100644 index 0000000000..6617584ae5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/attn_cp_triton_kernel_3d_la.py @@ -0,0 +1,100 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu + +#number of block per aiv core +NBLOCKS = 32 +# size of sub_block in one block +# reduce the size from 64 to 32 due to UB overflow +S_SUB_SIZE = 32 + + +@triton.jit +def tl_fn_forward_update_la(prev_attn_out, prev_softmax_log_max_sum, cur_attn_out, cur_softmax_log_max_sum, + B: tl.constexpr, N: tl.constexpr, S: tl.constexpr, D: tl.constexpr, + PREV_ATTN_NSTRIDE: tl.constexpr, PREV_SOFTMAX_NSTRIDE: tl.constexpr, + CUR_ATTN_NSTRIDE: tl.constexpr, CUR_SOFTMAX_NSTRIDE: tl.constexpr, S_NBLOCKS: tl.constexpr, + S_SUB: tl.constexpr): + + S_BLOCK: tl.constexpr = (S + S_NBLOCKS - 1) // S_NBLOCKS + S_NSUB: tl.constexpr = (S_BLOCK + S_SUB - 1) // S_SUB + LOOP_COUNT: tl.constexpr = (S_BLOCK * B * N + S_SUB - 1) // S_SUB + block_idx = tl.program_id(0) + + s_block_start = block_idx * S_BLOCK + # assuming S stride is D, if D is not contiguous, need to use 2-d offset + SIMD_SIZE: tl.constexpr = S_SUB * D + + for loop_index in range(LOOP_COUNT): + b = loop_index // (N * S_NSUB) + n = (loop_index // S_NSUB) % N + s_loop_start = (loop_index % S_NSUB) * S_SUB + s = s_block_start + s_loop_start + + sd_offsets = D * s + tl.arange(0, SIMD_SIZE) + s1_offsets = s + tl.arange(0, S_SUB) + + mask0 = None + softmax_offsets = PREV_SOFTMAX_NSTRIDE * (b * N + n) + s1_offsets + prev_softmax_local = tl.load(prev_softmax_log_max_sum + softmax_offsets, mask0) + offsets = CUR_SOFTMAX_NSTRIDE * (b * N + n) + s1_offsets + cur_softmax_local = tl.load(cur_softmax_log_max_sum + offsets, mask0) + + attn_offsets = PREV_ATTN_NSTRIDE * (b * N + n) + sd_offsets + prev_attn_local = tl.load(prev_attn_out + attn_offsets, mask0) + offsets = CUR_ATTN_NSTRIDE * (b * N + n) + sd_offsets + cur_attn_local = tl.load(cur_attn_out + offsets, mask0) + + tmp0 = tl.exp(cur_softmax_local) + tmp1 = tl.exp(prev_softmax_local) + softmax_log_max_sum = tl.log(tmp0 + tmp1) + tmp2 = (prev_softmax_local - softmax_log_max_sum).reshape(S_SUB, 1).broadcast_to(S_SUB, D) + tmp3 = (cur_softmax_local - softmax_log_max_sum).reshape(S_SUB, 1).broadcast_to(S_SUB, D) + + attn_out = tl.exp(tmp2) * prev_attn_local.reshape(S_SUB, D) + (tl.exp(tmp3) * cur_attn_local.reshape(S_SUB, D)) + mask1 = None + tl.store(prev_softmax_log_max_sum + softmax_offsets, softmax_log_max_sum, mask1) + tl.store(prev_attn_out + attn_offsets, attn_out.reshape(SIMD_SIZE, ), mask1) + + +#target_name = torch.npu.get_device_name(torch.npu.current_device()) +guards = {"dummy": None} + + +def forward_update_triton(prev_attn_out, prev_softmax_log_max_sum, cur_attn_out, cur_softmax_log_max_sum): + (B, N, S, D) = cur_attn_out.shape + #shape is (b,n,s,d) + PREV_ATTN_NSTRIDE = prev_attn_out.stride()[1] + PREV_SOFTMAX_NSTRIDE = prev_softmax_log_max_sum.stride()[1] + CUR_ATTN_NSTRIDE = cur_attn_out.stride()[1] + CUR_SOFTMAX_NSTRIDE = cur_softmax_log_max_sum.stride()[1] + + device_id = cur_attn_out.device.index + device = "npu:" + str(device_id) + + tl_fn_forward_update_la[NBLOCKS, 1, + 1](prev_attn_out, prev_softmax_log_max_sum, cur_attn_out, cur_softmax_log_max_sum, B=B, N=N, + S=S, D=D, PREV_ATTN_NSTRIDE=PREV_ATTN_NSTRIDE, PREV_SOFTMAX_NSTRIDE=PREV_SOFTMAX_NSTRIDE, + CUR_ATTN_NSTRIDE=CUR_ATTN_NSTRIDE, CUR_SOFTMAX_NSTRIDE=CUR_SOFTMAX_NSTRIDE, + S_NBLOCKS=NBLOCKS, S_SUB=S_SUB_SIZE, debug=True) diff --git a/third_party/ascend/unittest/pytest_ut/conftest.py b/third_party/ascend/unittest/pytest_ut/conftest.py new file mode 100644 index 0000000000..b94250e41e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/conftest.py @@ -0,0 +1,52 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + + +def pytest_configure(config): + config.addinivalue_line("markers", "backend(name): select the runtime backend for a test module") + + +@pytest.fixture(scope="module", autouse=True) +def assign_npu(request, worker_id): + marker = request.node.get_closest_marker("backend") + if marker: + backend = marker.args[0] + else: + backend = "torch_npu" + if backend == "torch_npu": + import torch + npu_count = torch.npu.device_count() + if worker_id == "master": + npu_id = 0 + else: + idx = int(worker_id.replace("gw", "")) + npu_id = idx % npu_count + torch.npu.set_device(npu_id) + elif backend == "mindspore": + import mindspore + npu_count = mindspore.device_context.ascend.device_count() + if worker_id == "master": + npu_id = 0 + else: + idx = int(worker_id.replace("gw", "")) + npu_id = idx % npu_count + mindspore.set_device("Ascend", npu_id) diff --git a/third_party/ascend/unittest/pytest_ut/test_01_vector_add.py b/third_party/ascend/unittest/pytest_ut/test_01_vector_add.py new file mode 100644 index 0000000000..8d59474dfb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_01_vector_add.py @@ -0,0 +1,86 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +""" +Vector Addition - Pytest Version +""" + +import torch +import torch_npu + +import triton +import triton.language as tl + + +@triton.jit +def add_kernel(x_ptr, # *Pointer* to first input vector. + y_ptr, # *Pointer* to second input vector. + output_ptr, # *Pointer* to output vector. + n_elements, # Size of the vector. + BLOCK_SIZE: tl.constexpr, # Number of elements each program should process. + # NOTE: `constexpr` so it can be used as a shape value. + ): + # There are multiple 'programs' processing different data. We identify which program + # we are here: + pid = tl.program_id(axis=0) # We use a 1D launch grid so axis is 0. + # This program will process inputs that are offset from the initial data. + # For instance, if you had a vector of length 256 and block_size of 64, the programs + # would each access the elements [0:64, 64:128, 128:192, 192:256]. + # Note that offsets is a list of pointers: + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + # Create a mask to guard memory operations against out-of-bounds accesses. + mask = offsets < n_elements + # Load x and y from DRAM, masking out any extra elements in case the input is not a + # multiple of the block size. + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + # Write x + y back to DRAM. + tl.store(output_ptr + offsets, output, mask=mask) + + +# %% +# Let's also declare a helper function to (1) allocate the `z` tensor +# and (2) enqueue the above kernel with appropriate grid/block sizes: + + +def add(x: torch.Tensor, y: torch.Tensor): + output = torch.empty_like(x) + n_elements = output.numel() + + def grid(meta): + return (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + add_kernel[grid](x, y, output, n_elements, BLOCK_SIZE=1024) + return output + + +# %% +# We can now use the above function to compute the element-wise sum of two `torch.tensor` objects and test its correctness: +def test_vector_addition(): + torch.manual_seed(0) + size = 98432 + x = torch.rand(size, device='npu') + y = torch.rand(size, device='npu') + output_torch = x + y + output_triton = add(x, y) + torch.testing.assert_close(output_triton, output_torch) diff --git a/third_party/ascend/unittest/pytest_ut/test_02_fused_softmax.py b/third_party/ascend/unittest/pytest_ut/test_02_fused_softmax.py new file mode 100644 index 0000000000..91fde18a95 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_02_fused_softmax.py @@ -0,0 +1,121 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Fused Softmax +============= +""" + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + + +def naive_softmax(x): + """Compute row-wise softmax of X using native pytorch + + We subtract the maximum element in order to avoid overflows. Softmax is invariant to + this shift. + """ + # read MN elements ; write M elements + x_max = x.max(dim=1)[0] + # read MN + M elements ; write MN elements + z = x - x_max[:, None] + # read MN elements ; write MN elements + numerator = torch.exp(z) + # read MN elements ; write M elements + denominator = numerator.sum(dim=1) + # read MN + M elements ; write MN elements + ret = numerator / denominator[:, None] + # in total: read 5MN + 2M elements ; wrote 3MN + 2M elements + return ret + + +@triton.jit +def softmax_kernel(output_ptr, input_ptr, input_row_stride, output_row_stride, n_rows, n_cols, + BLOCK_SIZE: tl.constexpr): + # starting row of the program + row_start = tl.program_id(0) + row_step = tl.num_programs(0) + for row_idx in tl.range(row_start, n_rows, row_step): + # The stride represents how much we need to increase the pointer to advance 1 row + row_start_ptr = input_ptr + row_idx * input_row_stride + # The block size is the next power of two greater than n_cols, so we can fit each + # row in a single block + col_offsets = tl.arange(0, BLOCK_SIZE) + input_ptrs = row_start_ptr + col_offsets + # Load the row into SRAM, using a mask since BLOCK_SIZE may be > than n_cols + mask = col_offsets < n_cols + row = tl.load(input_ptrs, mask=mask, other=-float('inf')) + # Subtract maximum for numerical stability + row_minus_max = row - tl.max(row, axis=0) + + numerator = tl.exp(row_minus_max) + denominator = tl.sum(numerator, axis=0) + softmax_output = numerator / denominator + # Write back output to DRAM + output_row_start_ptr = output_ptr + row_idx * output_row_stride + output_ptrs = output_row_start_ptr + col_offsets + tl.store(output_ptrs, softmax_output, mask=mask) + + +kernels = {} + + +def softmax(x): + n_rows, n_cols = x.shape + + # The block size of each loop iteration is the smallest power of two greater than the number of columns in `x` + BLOCK_SIZE = triton.next_power_of_2(n_cols) + + # Allocate output + y = torch.empty_like(x) + + # pre-compile kernel to get register usage and compute thread occupancy. + kernel, num_programs = kernels.get(BLOCK_SIZE, (None, 0)) + if kernel is None: + num_programs = 32 + kernel = softmax_kernel + kernels[BLOCK_SIZE] = (kernel, num_programs) + + num_programs = min(num_programs, n_rows) + + # Create a number of persistent programs. + kernel[(num_programs, 1, 1)](y, x, x.stride(0), y.stride(0), n_rows, n_cols, BLOCK_SIZE) + return y + + +@pytest.mark.parametrize( + "shape", + [ + (1823, 781), + (128, 257), + ], +) +@pytest.mark.parametrize("dtype", [torch.float16, torch.float32]) +def test_fused_softmax(shape, dtype): + torch.manual_seed(0) + x = torch.randn(shape, dtype=dtype, device="npu") + + y_triton = softmax(x) + y_torch = torch.softmax(x, axis=1) + + torch.testing.assert_close(y_triton, y_torch, atol=1e-4, rtol=1e-4) diff --git a/third_party/ascend/unittest/pytest_ut/test_03_matrix_multiplication.py b/third_party/ascend/unittest/pytest_ut/test_03_matrix_multiplication.py new file mode 100644 index 0000000000..ad0593e220 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_03_matrix_multiplication.py @@ -0,0 +1,211 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Matrix Multiplication +=============== +""" + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import triton.language.extra.cann.extension as extension + +DEV = "npu" + + +def get_autotune_config(): + return [ + triton.Config({"BLOCK_SIZE_M": 128, "BLOCK_SIZE_N": 128, "BLOCK_SIZE_K": 128}), + triton.Config({"BLOCK_SIZE_M": 64, "BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 64}), + ] + + +@triton.autotune( + configs=get_autotune_config(), + key=["M", "N", "K"], +) +@triton.jit +def matmul_kernel( + # Pointers to matrices + a_ptr, b_ptr, c_ptr, + # Matrix dimensions + M, N, K, + # The stride variables represent how much to increase the ptr by when moving by 1 + # element in a particular dimension. E.g. `stride_am` is how much to increase `a_ptr` + # by to get the element one row down (A has M rows). + stride_am, stride_ak, # + stride_bk, stride_bn, # + stride_cm, stride_cn, + # Meta-parameters + BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, # + ACTIVATION: tl.constexpr, # +): + """Kernel for computing the matmul C = A x B. + A has shape (M, K), B has shape (K, N) and C has shape (M, N) + """ + GROUP_SIZE_M: tl.constexpr = 1 + # ----------------------------------------------------------- + # Map program ids `pid` to the block of C it should compute. + # This is done in a grouped ordering to promote L2 data reuse. + # See above `L2 Cache Optimizations` section for details. + pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + # ---------------------------------------------------------- + # Create pointers for the first blocks of A and B. + # We will advance this pointer as we move in the K direction + # and accumulate + # `a_ptrs` is a block of [BLOCK_SIZE_M, BLOCK_SIZE_K] pointers + # `b_ptrs` is a block of [BLOCK_SIZE_K, BLOCK_SIZE_N] pointers + # See above `Pointer Arithmetic` section for details + offs_am = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + offs_k = tl.arange(0, BLOCK_SIZE_K) + a_ptrs_base = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) + b_ptrs_base = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) + msk_m = offs_am < M + msk_n = offs_bn < N + # ----------------------------------------------------------- + # Iterate to compute a block of the C matrix. + # We accumulate into a `[BLOCK_SIZE_M, BLOCK_SIZE_N]` block + # of fp32 values for higher accuracy. + # `accumulator` will be converted back to fp16 after the loop. + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)): + # Load the next block of A and B, generate a mask by checking the K dimension. + # If it is out of bounds, set it to 0. + a_ptrs = a_ptrs_base + k * BLOCK_SIZE_K * stride_ak + b_ptrs = b_ptrs_base + k * BLOCK_SIZE_K * stride_bk + a = tl.load( + a_ptrs, + mask=msk_m[:, None] and (offs_k[None, :] < K - k * BLOCK_SIZE_K), + other=0.0, + ) + b = tl.load( + b_ptrs, + mask=msk_n[None, :] and (offs_k[:, None] < K - k * BLOCK_SIZE_K), + other=0.0, + ) + # We accumulate along the K dimension. + accumulator = tl.dot(a, b, accumulator) + # You can fuse arbitrary activation functions here + # while the accumulator is still in FP32! + # Original vector operations + # # ----------------------------------------------------------- + # # Write back the block of the output matrix C with masks. + # Comment out the following lines to enable split the workload to two vector cores + SUB_BLK_M: tl.constexpr = BLOCK_SIZE_M // 2 + for s in range(0, 2): + vec_sub_blk = extension.extract_slice(accumulator, (s * SUB_BLK_M, 0), (SUB_BLK_M, BLOCK_SIZE_N), (1, 1)) + if ACTIVATION == "leaky_relu_custom": + vec_sub_blk = leaky_relu_custom(vec_sub_blk) + c_sub_blk = vec_sub_blk.to(tl.float16) + # ----------------------------------------------------------- + # Write back the block of the output matrix C with masks. + offs_cm = pid_m * BLOCK_SIZE_M + s * SUB_BLK_M + tl.arange(0, SUB_BLK_M) + offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] + c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N) + tl.store(c_ptrs, c_sub_blk, mask=c_mask) + + +# We can fuse `leaky_relu_custom` by providing it as an `ACTIVATION` meta-parameter in `matmul_kernel`. +@triton.jit +def leaky_relu_custom(x): + return tl.where(x >= 0, x, 0.01 * x) + 1.0 + + +def torch_matmul(a, b, activation=""): + c = torch.matmul(a, b) + if activation == "leaky_relu_custom": + c = torch.where(c >= 0, c, 0.01 * c) + 1.0 + return c + + +# %% +# We can now create a convenience wrapper function that only takes two input tensors, +# and (1) checks any shape constraint; (2) allocates the output; (3) launches the above kernel. + + +def matmul(a, b, activation=""): + # Check constraints. + assert a.shape[1] == b.shape[0], "Incompatible dimensions" + assert a.is_contiguous(), "Matrix A must be contiguous" + M, K = a.shape + K, N = b.shape + # Allocates output. + c = torch.empty((M, N), device=a.device, dtype=torch.float16) + + # 1D launch kernel where each block gets its own program. + + def grid(META): + return (triton.cdiv(M, META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]), ) + + matmul_kernel[grid]( + a, b, c, # + M, N, K, # + a.stride(0), a.stride(1), # + b.stride(0), b.stride(1), # + c.stride(0), c.stride(1), # + ACTIVATION=activation, # + ) + return c + + +# %% +# Unit Test +# --------- +# +# We can test our custom matrix multiplication operation against a native torch implementation. +@pytest.mark.parametrize( + "shape", + [ + (512, 512, 512), + (256, 384, 128), + ], +) +@pytest.mark.parametrize( + "activation", + [ + "", + pytest.param("leaky_relu_custom", + marks=pytest.mark.skip(reason="temporarily skip leaky_relu_custom ub overflow case")), + ], +) +def test_matrix_multiplication(shape, activation): + m, k, n = shape + torch.manual_seed(0) + + a = torch.randn((m, k), device=DEV, dtype=torch.float16) + b = torch.randn((k, n), device=DEV, dtype=torch.float16) + + triton_output = matmul(a, b, activation) + torch_output = torch_matmul(a, b, activation) + + torch.testing.assert_close(triton_output, torch_output, atol=1e-3, rtol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_04_low_memory_dropout.py b/third_party/ascend/unittest/pytest_ut/test_04_low_memory_dropout.py new file mode 100644 index 0000000000..f615608e7a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_04_low_memory_dropout.py @@ -0,0 +1,134 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Low-Memory Dropout +================== +""" + +import pytest +import torch +import torch_npu + +import triton +import triton.language as tl + +DEV = "npu" + + +@triton.jit +def _dropout( + x_ptr, # pointer to the input + x_keep_ptr, # pointer to a mask of 0s and 1s + output_ptr, # pointer to the output + n_elements, # number of elements in the `x` tensor + p, # probability that an element of `x` is changed to zero + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + # Load data + x = tl.load(x_ptr + offsets, mask=mask) + x_keep = tl.load(x_keep_ptr + offsets, mask=mask) + # The line below is the crucial part, described in the paragraph above! + output = tl.where(x_keep != 0, x / (1 - p), 0.0) + # Write-back output + tl.store(output_ptr + offsets, output, mask=mask) + + +def dropout(x, x_keep, p): + output = torch.empty_like(x) + assert x.is_contiguous() + n_elements = x.numel() + + def grid(meta): + return (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + _dropout[grid](x, x_keep, output, n_elements, p, BLOCK_SIZE=1024) + return output + + +@triton.jit +def _seeded_dropout( + x_ptr, + output_ptr, + n_elements, + p, + seed, + BLOCK_SIZE: tl.constexpr, +): + # compute memory offsets of elements handled by this instance + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + # load data from x + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + # randomly prune it + random = tl.rand(seed, offsets) + x_keep = random > p + # write-back + output = tl.where(x_keep, x / (1 - p), 0.0) + tl.store(output_ptr + offsets, output, mask=mask) + + +def seeded_dropout(x, p, seed): + output = torch.empty_like(x) + assert x.is_contiguous() + n_elements = x.numel() + + def grid(meta): + return (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + _seeded_dropout[grid](x, output, n_elements, p, seed, BLOCK_SIZE=1024) + return output + + +@pytest.mark.parametrize("shape,p", [((10, ), 0.5), ((256, ), 0.5), ((513, ), 0.2), ((32, 64), 0.35)]) +def test_dropout_matches_reference(shape, p): + torch.manual_seed(0) + x = torch.randn(size=shape, device=DEV, dtype=torch.float32) + x_keep = (torch.rand(size=shape, device=DEV) > p).to(torch.int32) + + output = dropout(x, x_keep=x_keep, p=p) + expected = torch.where(x_keep != 0, x / (1 - p), torch.zeros_like(x)) + + torch.testing.assert_close(output, expected, atol=1e-6, rtol=0) + + +@pytest.mark.parametrize("shape,p,seed", [((10, ), 0.5, 123), ((256, ), 0.5, 123), ((513, ), 0.2, 7), + ((32, 64), 0.35, 999)]) +def test_seeded_dropout_is_deterministic(shape, p, seed): + torch.manual_seed(0) + x = torch.randn(size=shape, device=DEV, dtype=torch.float32) + + output = seeded_dropout(x, p=p, seed=seed) + output_same_seed = seeded_dropout(x, p=p, seed=seed) + output_different_seed = seeded_dropout(x, p=p, seed=512) + + torch.testing.assert_close(output, output_same_seed, atol=1e-6, rtol=0) + + assert output.shape == x.shape + assert output.dtype == x.dtype + assert torch.count_nonzero(output != output_different_seed).item() > 0 + assert torch.count_nonzero(output).item() <= x.numel() diff --git a/third_party/ascend/unittest/pytest_ut/test_05_layer_norm.py b/third_party/ascend/unittest/pytest_ut/test_05_layer_norm.py new file mode 100644 index 0000000000..84e3268b9b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_05_layer_norm.py @@ -0,0 +1,119 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Layer Normalization +============= +""" + +import pytest +import torch +import triton +import triton.language as tl +import torch_npu + + +@triton.jit +def _layer_norm_fwd_fused( + X, # pointer to the input + Y, # pointer to the output + W, # pointer to the weights + B, # pointer to the biases + Mean, # pointer to the mean + Rstd, # pointer to the 1/std + stride, # how much to increase the pointer when moving by 1 row + N, # number of columns in X + eps, # epsilon to avoid division by zero + BLOCK_SIZE: tl.constexpr, +): + # Map the program id to the row of X and Y it should compute. + row = tl.program_id(0) + Y += row * stride + X += row * stride + # Compute mean + mean = 0 + _mean = tl.zeros([BLOCK_SIZE], dtype=tl.float32) + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + a = tl.load(X + cols, mask=cols < N, other=0.).to(tl.float32) + _mean += a + mean = tl.sum(_mean, axis=0) / N + # Compute variance + _var = tl.zeros([BLOCK_SIZE], dtype=tl.float32) + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + x = tl.load(X + cols, mask=cols < N, other=0.).to(tl.float32) + x = tl.where(cols < N, x - mean, 0.) + _var += x * x + var = tl.sum(_var, axis=0) / N + rstd = 1 / tl.sqrt(var + eps) + # Write mean / rstd + tl.store(Mean + row, mean) + tl.store(Rstd + row, rstd) + # Normalize and apply linear transformation + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + mask = cols < N + w = tl.load(W + cols, mask=mask) + b = tl.load(B + cols, mask=mask) + x = tl.load(X + cols, mask=mask, other=0.).to(tl.float32) + x_hat = (x - mean) * rstd + y = x_hat * w + b + # Write output + tl.store(Y + cols, y, mask=mask) + + +@torch.inference_mode() +def layer_norm(x, normalized_shape, weight, bias, eps=1e-5): + # allocate output + y = torch.empty_like(x) + # reshape input data into 2D tensor + x_arg = x.reshape(-1, x.shape[-1]) + M, N = x_arg.shape + mean = torch.empty((M, ), dtype=torch.float32, device=x.device) + rstd = torch.empty((M, ), dtype=torch.float32, device=x.device) + # Less than 64KB per feature: enqueue fused kernel + MAX_FUSED_SIZE = 65536 // x.element_size() + BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) + # heuristics for number of warps + num_warps = min(max(BLOCK_SIZE // 256, 1), 8) + # enqueue kernel + kernel = _layer_norm_fwd_fused[(M, )]( # + x_arg, y, weight, bias, mean, rstd, # + x_arg.stride(0), N, eps, # + BLOCK_SIZE=BLOCK_SIZE, num_warps=num_warps, num_ctas=1) + return y + + +@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32]) +def test_layer_norm(dtype): + M, N = 128, 128 + eps = 1e-5 + device = 'npu' + + x_shape = (M, N) + w_shape = (x_shape[-1], ) + weight = torch.rand(w_shape, dtype=dtype, device=device) + bias = torch.rand(w_shape, dtype=dtype, device=device) + x = -2.3 + 0.5 * torch.randn(x_shape, dtype=dtype, device=device) + + y_tri = layer_norm(x, w_shape, weight, bias, eps) + y_ref = torch.nn.functional.layer_norm(x, w_shape, weight, bias, eps).to(dtype) + + assert torch.allclose(y_tri, y_ref, atol=1e-2, rtol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_06_fused_attention.py b/third_party/ascend/unittest/pytest_ut/test_06_fused_attention.py new file mode 100644 index 0000000000..8f83d6a730 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_06_fused_attention.py @@ -0,0 +1,340 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Fused Attention +=============== + +This is a Triton implementation of the Flash Attention v2 algorithm from Tri Dao (https://tridao.me/publications/flash2/flash2.pdf) + +Credits: OpenAI kernel team + +Extra Credits: + +* Original flash attention paper (https://arxiv.org/abs/2205.14135) +* Rabe and Staats (https://arxiv.org/pdf/2112.05682v2.pdf) + +""" + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import triton.language.extra.cann.extension as extension + +DEVICE = "npu" + + +@triton.jit +def _attn_fwd_inner(acc_ptr, l_i, m_i, q, # Accumulator, local l, local m, query vector + K_block_ptr, V_block_ptr, # Key and value block pointers for current stage + start_m, qk_scale, # Starting position of current query block, qk scale factor + BLOCK_M: tl.constexpr, HEAD_DIM: tl.constexpr, BLOCK_N: tl.constexpr, # Block size constants + STAGE: tl.constexpr, offs_m: tl.constexpr, + offs_n: tl.constexpr, # Current stage flag, m and n offset indices + N_CTX: tl.constexpr, + fp8_v: tl.constexpr): # Total context length, whether to enable FP8 for value precision + # Set the processing range [lo, hi) for the current stage (in column block units) + # Causal attention, as the name implies, restricts the flow of information during computation, + # only allowing the model to see the current and previous positions. + # In other words, the output at the current position can only depend on the input at or before this position, + # and cannot access information from future positions. + # Causal attention ensures sequential order and prevents "leakage of future information." + # But the following logic will also be triggered + if STAGE == 1: + # Stage 1: process all tokens before the query block + tl.static_assert(BLOCK_M >= BLOCK_N) + lo, hi = 0, start_m * BLOCK_M + elif STAGE == 2: + # Stage 2: process the current query block + tl.static_assert(BLOCK_M >= BLOCK_N) + lo, hi = start_m * BLOCK_M, (start_m + 1) * BLOCK_M + lo = tl.multiple_of(lo, BLOCK_M) # Align starting position + # causal = False (no need for masking) + else: + lo, hi = 0, N_CTX # Process the entire context + + # Adjust K and V block pointers to the starting position `lo` + K_block_ptr = tl.advance(K_block_ptr, (lo, 0)) # K is [HEAD_DIM, N_CTX], shift along the second dim by lo + V_block_ptr = tl.advance(V_block_ptr, (lo, 0)) # V is [N_CTX, HEAD_DIM], shift along the first dim by lo + + # Index mapping for the accumulator , used for slicing when HEAD_DIM >= 256 + row = tl.arange(0, BLOCK_M)[:, None] + col_head_dim = tl.arange(0, HEAD_DIM)[None, :] + block2d_acc = row * HEAD_DIM + col_head_dim + + # Iterate over all k, v blocks in the current stage and accumulate the output + for start_n in range(lo, hi, BLOCK_N): # Process BLOCK_N columns at a time + start_n = tl.multiple_of(start_n, BLOCK_N) # Align column start position + # -- Compute qk ---- + k = tl.load(K_block_ptr) + # Modify K + trans_k = tl.trans(k) + qk = tl.dot(q, trans_k) + # Apply causal mask for STAGE 2 + if STAGE == 2: + mask = offs_m[:, None] >= (start_n + offs_n[None, :]) # Construct upper triangular mask + qk = qk * qk_scale + tl.where(mask, 0, -1.0e6) # Set invalid positions to -∞ + m_ij = tl.maximum(m_i, tl.max(qk, 1)) # Update m_ij = max(m_i, max(qk)) + qk -= m_ij[:, None] # Subtract max for softmax stability + else: + qk = qk * qk_scale + m_ij = tl.maximum(m_i, tl.max(qk, 1)) # Scaled max + qk = qk - m_ij[:, None] # Stabilize + + # Softmax weights p = exp(qk) + p = tl.math.exp(qk) + + # Convert softmax weight type depending on FP8 usage + if fp8_v: + p_cast = p.to(tl.float8e5) # Convert to FP8 format (save memory) + else: + p_cast = p.to(k.dtype) + + v = tl.load(V_block_ptr) # Load corresponding V block + pv = tl.dot(p_cast, v) + l_ij = tl.sum(p, 1) # Softmax denominator (sum of each row) + # -- Update m_i and l_i + alpha = tl.math.exp(m_i - m_ij) # Update factor: exp difference between old and new max + l_i = l_i * alpha + l_ij # Update softmax denominator + # -- Update output accumulator -- + if HEAD_DIM < 256: + acc_ptr = acc_ptr * alpha[:, None] + acc_ptr = tl.dot(p_cast, v, acc_ptr) + else: + # 1. Load current slice of accumulator + acc = tl.load(acc_ptr + block2d_acc) + # 2. Update in slices (split by 1/4 of BLOCK_M to avoid ub overflow) + for i in range(4): + # Calculate start/end rows for current slice + offset = i * (BLOCK_M // 4) + # Extract slice data + acc_i = extension.extract_slice(acc, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + alpha_i = extension.extract_slice(alpha, [offset], [BLOCK_M // 4], [1]) + pv_i = extension.extract_slice(pv, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + # Incrementally update slice: acc = acc * alpha + pv + acc_i = acc_i * alpha_i[:, None] + pv_i + # Write updated slice back to accumulator + acc = extension.insert_slice(acc, acc_i, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + # 3. updated accumulator + tl.store(acc_ptr + block2d_acc, acc) + + m_i = m_ij # Update current block max + # Advance V and K block pointers to next BLOCK_N range + V_block_ptr = tl.advance(V_block_ptr, (BLOCK_N, 0)) + K_block_ptr = tl.advance(K_block_ptr, (BLOCK_N, 0)) + # Return accumulated output acc_ptr, softmax denominator l_i, and max value m_i + return acc_ptr, l_i, m_i + + +@triton.jit +def _attn_fwd(Q, K, V, M, Out, acc, sm_scale, stride_qz: tl.constexpr, stride_qh: tl.constexpr, stride_qm: tl.constexpr, + stride_qk: tl.constexpr, stride_kz: tl.constexpr, stride_kh: tl.constexpr, stride_kn: tl.constexpr, + stride_kk: tl.constexpr, stride_vz: tl.constexpr, stride_vh: tl.constexpr, stride_vn: tl.constexpr, + stride_vk: tl.constexpr, stride_oz: tl.constexpr, stride_oh: tl.constexpr, stride_om: tl.constexpr, + stride_on: tl.constexpr, Z: tl.constexpr, H: tl.constexpr, N_CTX: tl.constexpr, HEAD_DIM: tl.constexpr, + BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, STAGE: tl.constexpr): + # Total number of blocks in sequence dimension (M) + NUM_BLOCKS_M = N_CTX // BLOCK_M + # Total tasks = number of sequence blocks × batch size (Z) × number of attention heads (H) + NUM_BLOCKS = NUM_BLOCKS_M * Z * H + + # Current M-dimension block index + pid = tl.program_id(0) + + for block_idx in range(pid, NUM_BLOCKS, 20): + task_hz_idx = block_idx // NUM_BLOCKS_M + task_m_idx = block_idx % NUM_BLOCKS_M + off_z = task_hz_idx // H + off_h = task_hz_idx % H + qvk_offset = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh + # Create block pointers for Q, K, V, Output + Q_block_ptr = tl.make_block_ptr( + base=Q + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_qm, stride_qk), + offsets=(task_m_idx * BLOCK_M, 0), + block_shape=(BLOCK_M, HEAD_DIM), + order=(1, 0), + ) + V_block_ptr = tl.make_block_ptr( + base=V + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_vn, stride_vk), + offsets=(0, 0), + block_shape=(BLOCK_N, HEAD_DIM), + order=(1, 0), + ) + K_block_ptr = tl.make_block_ptr( + base=K + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_kn, stride_kk), + offsets=(0, 0), + block_shape=(BLOCK_N, HEAD_DIM), + order=(1, 0), + ) + O_block_ptr = tl.make_block_ptr( + base=Out + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_om, stride_on), + offsets=(task_m_idx * BLOCK_M, 0), + block_shape=(BLOCK_M, HEAD_DIM), + order=(1, 0), + ) + # Initialize offsets + offs_m = task_m_idx * BLOCK_M + tl.arange(0, BLOCK_M) + offs_n = tl.arange(0, BLOCK_N) + + m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf") + l_i = tl.zeros([BLOCK_M], dtype=tl.float32) + 1.0 + + # Initialize accumulator + if HEAD_DIM < 256: + acc_ptr = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32) + else: + acc_offset = (off_z.to(tl.int64) * stride_qz // stride_qm * HEAD_DIM + + off_h.to(tl.int64) * stride_qh // stride_qm * HEAD_DIM + task_m_idx * BLOCK_M * HEAD_DIM) + acc_ptr = acc + acc_offset + + # load q: it will stay in SRAM throughout + q = tl.load(Q_block_ptr) + + # stage 1: off-band + # For causal = True, STAGE = 3 and _attn_fwd_inner gets 1 as its STAGE + # For causal = False, STAGE = 1, and _attn_fwd_inner gets 3 as its STAGE + if STAGE & 1: + acc_ptr, l_i, m_i = _attn_fwd_inner(acc_ptr, l_i, m_i, q, K_block_ptr, V_block_ptr, # + task_m_idx, sm_scale, # + BLOCK_M, HEAD_DIM, BLOCK_N, # + 4 - STAGE, offs_m, offs_n, N_CTX, V.dtype.element_ty == tl.float8e5 # + ) + # stage 2: on-band + if STAGE & 2: + # barrier makes it easier for compielr to schedule the + # two loops independently + acc_ptr, l_i, m_i = _attn_fwd_inner(acc_ptr, l_i, m_i, q, K_block_ptr, V_block_ptr, # + task_m_idx, sm_scale, # + BLOCK_M, HEAD_DIM, BLOCK_N, # + 2, offs_m, offs_n, N_CTX, V.dtype.element_ty == tl.float8e5 # + ) + + m_i += tl.math.log(l_i) + if HEAD_DIM < 256: + accumulator = acc_ptr / l_i[:, None] + else: + row = tl.arange(0, BLOCK_M)[:, None] + col_head_dim = tl.arange(0, HEAD_DIM)[None, :] + block2d_acc = row * HEAD_DIM + col_head_dim + accumulator = tl.load(acc_ptr + block2d_acc) + accumulator = accumulator / l_i[:, None] + + m_ptrs = M + task_hz_idx * N_CTX + offs_m + + tl.store(m_ptrs, m_i) + tl.store(O_block_ptr, accumulator.to(Out.type.element_ty)) + + +class _attention(torch.autograd.Function): + + @staticmethod + def forward(ctx, q, k, v, causal, sm_scale, BM, BN): + """ + Forward computation interface: + Args: + ctx: Context object + q: Query tensor (Q), shape [Z, H, N_CTX, HEAD_DIM] + k: Key tensor (K), shape [Z, H, N_CTX, HEAD_DIM] + v: Value tensor (V), shape [Z, H, N_CTX, HEAD_DIM] + causal: Whether to enable causal attention + sm_scale: Scaling factor for QK product + BM: Q block size (BLOCK_M) + BN: K/V block size (BLOCK_N) + Returns: + o: Attention output tensor, shape [Z, H, N_CTX, HEAD_DIM] + """ + # shape constraints + HEAD_DIM_Q, HEAD_DIM_K = q.shape[-1], k.shape[-1] + # when v is in float8_e5m2 it is transposed. + HEAD_DIM_V = v.shape[-1] + assert HEAD_DIM_Q == HEAD_DIM_K and HEAD_DIM_K == HEAD_DIM_V + assert HEAD_DIM_K in {16, 32, 64, 128, 256} + + out = torch.empty_like(q) + stage = 3 if causal else 1 + extra_kern_args = {} + + # Number of NPU cores (adjust based on hardware) + num_cores = 20 + acc = torch.zeros((q.shape[0], q.shape[1], q.shape[2], HEAD_DIM_K), dtype=torch.float32, device=q.device) + M = torch.empty((q.shape[0], q.shape[1], q.shape[2]), device=q.device, dtype=torch.float32) + + _attn_fwd[(num_cores, )](q, k, v, M, out, acc, sm_scale, q.stride(0), q.stride(1), q.stride(2), q.stride(3), + k.stride(0), k.stride(1), k.stride(2), k.stride(3), v.stride(0), v.stride(1), + v.stride(2), v.stride(3), out.stride(0), out.stride(1), out.stride(2), out.stride(3), + q.shape[0], q.shape[1], N_CTX=q.shape[2], HEAD_DIM=HEAD_DIM_K, BLOCK_M=BM, BLOCK_N=BN, + STAGE=stage, **extra_kern_args) + + ctx.save_for_backward(q, k, v, out, M) + ctx.sm_scale = sm_scale + ctx.HEAD_DIM = HEAD_DIM_K + ctx.causal = causal + return out + + +attention = _attention.apply + + +# ==================== Pytest Test ==================== +@pytest.mark.parametrize("Z, H, N_CTX, HEAD_DIM, causal, dtype, BM, BN", [ + (1, 1, 128, 128, False, torch.float16, 32, 128), + (1, 1, 128, 128, False, torch.bfloat16, 64, 128), + (1, 2, 256, 256, False, torch.bfloat16, 32, 256), + (2, 2, 128, 256, False, torch.float16, 64, 128), + (4, 32, 64, 64, False, torch.float16, 32, 64), + (4, 32, 1024, 64, False, torch.bfloat16, 64, 128), + (4, 32, 4096, 64, False, torch.float16, 128, 128), +]) +def test_attention_fused(Z, H, N_CTX, HEAD_DIM, causal, dtype, BM, BN): + if N_CTX % BM != 0 or N_CTX % BN != 0 or HEAD_DIM % 16 != 0: + pytest.skip("Skipping non-divisible case") + + torch.manual_seed(20) + q = torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_() + k = torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_() + v = torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_() + + sm_scale = 0.5 + tri_out = attention(q, k, v, causal, sm_scale, BM, BN) + ref_out = torch_npu.npu_fusion_attention( + q, + k, + v, + H, + padding_mask=None, + atten_mask=None, + scale=sm_scale, + keep_prob=1.0, + input_layout="BNSD", + pre_tockens=65535, + next_tockens=65535, + sparse_mode=0, + )[0] + + torch.testing.assert_close(ref_out, tri_out, atol=1e-2, rtol=1e-2, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_07_extern_functions.py b/third_party/ascend/unittest/pytest_ut/test_07_extern_functions.py new file mode 100644 index 0000000000..01e41209c7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_07_extern_functions.py @@ -0,0 +1,83 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Libdevice (`tl.extra.libdevice`) function +============================== +""" + +import pytest +import torch +import torch_npu + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +from triton.backends.ascend.compiler import get_libdevice + +DEV = "npu" + + +@triton.jit +def asin_kernel( + x_ptr, + y_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + x = libdevice.asin(x) + tl.store(y_ptr + offsets, x, mask=mask) + + +extern_libs = {'libdevice': get_libdevice()} + + +def run_asin_case(size, use_extern_libs): + torch.manual_seed(0) + x = torch.rand(size, device=DEV) + output_triton = torch.empty_like(x) + output_torch = torch.asin(x) + assert x.device.type == DEV and output_triton.device.type == DEV + + n_elements = output_torch.numel() + + def grid(meta): + return (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + launch_kwargs = {"BLOCK_SIZE": 1024} + if use_extern_libs: + launch_kwargs["extern_libs"] = extern_libs + + asin_kernel[grid](x, output_triton, n_elements, **launch_kwargs) + torch.testing.assert_close(output_torch, output_triton, rtol=1e-4, atol=1e-4) + + +@pytest.mark.parametrize("size", [98432, 1024]) +def test_asin_kernel_matches_torch(size): + run_asin_case(size=size, use_extern_libs=False) + + +@pytest.mark.parametrize("size", [98432, 1024]) +def test_asin_kernel_matches_torch_with_extern_libs(size): + run_asin_case(size=size, use_extern_libs=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_08_grouped_gemm.py b/third_party/ascend/unittest/pytest_ut/test_08_grouped_gemm.py new file mode 100644 index 0000000000..830f9e57d6 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_08_grouped_gemm.py @@ -0,0 +1,287 @@ +# Copyright (c) 2023 NVIDIA Corporation & Affiliates. All rights reserved. +# Copyright (c) Huawei Technologies Co., Ltd. 2025. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Group GEMM +============================ +""" + +import pytest +import torch +import torch_npu + +import triton +import triton.language as tl +import triton.runtime.driver as driver + +DEV = "npu" + + +def get_npu_properties(): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device) + + +NUM_CORES = get_npu_properties()["num_aicore"] + + +@triton.autotune( + configs=[ + triton.Config({ + 'BLOCK_SIZE_M': 128, + 'BLOCK_SIZE_N': 128, + 'BLOCK_SIZE_K': 32, + 'NUM_SM': NUM_CORES, + }), + triton.Config({ + 'BLOCK_SIZE_M': 128, + 'BLOCK_SIZE_N': 128, + 'BLOCK_SIZE_K': 32, + 'NUM_SM': NUM_CORES, + }), + triton.Config({ + 'BLOCK_SIZE_M': 64, + 'BLOCK_SIZE_N': 64, + 'BLOCK_SIZE_K': 32, + 'NUM_SM': NUM_CORES, + }), + triton.Config({ + 'BLOCK_SIZE_M': 64, + 'BLOCK_SIZE_N': 64, + 'BLOCK_SIZE_K': 32, + 'NUM_SM': NUM_CORES, + }), + ], + key=['group_size'], +) +@triton.jit +def grouped_matmul_kernel( + group_a_ptrs, + group_b_ptrs, + group_c_ptrs, + group_gemm_sizes, + g_lds, + group_size, + NUM_SM: tl.constexpr, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, +): + tile_idx = tl.program_id(0) + last_problem_end = 0 + for g in range(group_size): + gm = tl.load(group_gemm_sizes + g * 3) + gn = tl.load(group_gemm_sizes + g * 3 + 1) + gk = tl.load(group_gemm_sizes + g * 3 + 2) + num_m_tiles = tl.cdiv(gm, BLOCK_SIZE_M) + num_n_tiles = tl.cdiv(gn, BLOCK_SIZE_N) + num_tiles = num_m_tiles * num_n_tiles + while (tile_idx >= last_problem_end and tile_idx < last_problem_end + num_tiles): + k = gk + lda = tl.load(g_lds + g * 3) + ldb = tl.load(g_lds + g * 3 + 1) + ldc = tl.load(g_lds + g * 3 + 2) + a_ptr = tl.load(group_a_ptrs + g).to(tl.pointer_type(tl.float16)) + b_ptr = tl.load(group_b_ptrs + g).to(tl.pointer_type(tl.float16)) + c_ptr = tl.load(group_c_ptrs + g).to(tl.pointer_type(tl.float16)) + tile_idx_in_gemm = tile_idx - last_problem_end + tile_m_idx = tile_idx_in_gemm // num_n_tiles + tile_n_idx = tile_idx_in_gemm % num_n_tiles + + offs_am = tile_m_idx * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_bn = tile_n_idx * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + offs_k = tl.arange(0, BLOCK_SIZE_K) + a_ptrs = a_ptr + offs_am[:, None] * lda + offs_k[None, :] + b_ptrs = b_ptr + offs_k[:, None] * ldb + offs_bn[None, :] + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + for _ in range(0, tl.cdiv(k, BLOCK_SIZE_K)): + tl.multiple_of(a_ptrs, [16, 16]) + tl.multiple_of(b_ptrs, [16, 16]) + a = tl.load(a_ptrs) + b = tl.load(b_ptrs) + accumulator += tl.dot(a, b) + a_ptrs += BLOCK_SIZE_K + b_ptrs += BLOCK_SIZE_K * ldb + c = accumulator.to(tl.float16) + + offs_cm = tile_m_idx * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_cn = tile_n_idx * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + c_ptrs = c_ptr + ldc * offs_cm[:, None] + offs_cn[None, :] + + tl.store(c_ptrs, c) + tile_idx += NUM_SM + + last_problem_end = last_problem_end + num_tiles + + +def group_gemm_fn(group_A, group_B): + device = torch.device(DEV) + assert len(group_A) == len(group_B) + group_size = len(group_A) + + A_addrs = [] + B_addrs = [] + C_addrs = [] + g_sizes = [] + g_lds = [] + group_C = [] + for i in range(group_size): + A = group_A[i] + B = group_B[i] + assert A.shape[1] == B.shape[0] + M, K = A.shape + K, N = B.shape + C = torch.empty((M, N), device=device, dtype=A.dtype) + group_C.append(C) + A_addrs.append(A.data_ptr()) + B_addrs.append(B.data_ptr()) + C_addrs.append(C.data_ptr()) + g_sizes += [M, N, K] + g_lds += [A.stride(0), B.stride(0), C.stride(0)] + + d_a_ptrs = torch.tensor(A_addrs, device=device) + d_b_ptrs = torch.tensor(B_addrs, device=device) + d_c_ptrs = torch.tensor(C_addrs, device=device) + d_g_sizes = torch.tensor(g_sizes, dtype=torch.int32, device=device) + d_g_lds = torch.tensor(g_lds, dtype=torch.int32, device=device) + + def grid(meta): + return (meta['NUM_SM'], ) + + grouped_matmul_kernel[grid]( + d_a_ptrs, + d_b_ptrs, + d_c_ptrs, + d_g_sizes, + d_g_lds, + group_size, + ) + + return group_C + + +def build_group_inputs(group_m, group_n, group_k): + assert len(group_m) == len(group_n) + assert len(group_n) == len(group_k) + + group_A = [] + group_B = [] + for m, n, k in zip(group_m, group_n, group_k): + group_A.append(torch.rand((m, k), device=DEV, dtype=torch.float16)) + group_B.append(torch.rand((k, n), device=DEV, dtype=torch.float16)) + return group_A, group_B + + +def run_group_gemm_case(group_m, group_n, group_k): + group_A, group_B = build_group_inputs(group_m, group_n, group_k) + + tri_out = group_gemm_fn(group_A, group_B) + ref_out = [torch.matmul(a, b) for a, b in zip(group_A, group_B)] + + assert len(tri_out) == len(ref_out) + for tri_tensor, ref_tensor, m, n in zip(tri_out, ref_out, group_m, group_n): + assert tri_tensor.shape == (m, n) + assert tri_tensor.dtype == torch.float16 + torch.testing.assert_close(ref_tensor, tri_tensor, atol=1e-2, rtol=1e-3) + + +@pytest.mark.parametrize( + "group_m,group_n,group_k", + [([1024, 512, 256, 128], [1024, 512, 256, 128], [1024, 512, 256, 128])], +) +def test_grouped_gemm_tutorial_example(group_m, group_n, group_k): + run_group_gemm_case( + group_m=group_m, + group_n=group_n, + group_k=group_k, + ) + + +def triton_perf_fn(a_ptrs, b_ptrs, c_ptrs, sizes, lds, group_size): + + def grid(meta): + return (meta['NUM_SM'], ) + + grouped_matmul_kernel[grid]( + a_ptrs, + b_ptrs, + c_ptrs, + sizes, + lds, + group_size, + ) + + +def torch_perf_fn(group_A, group_B): + for a, b in zip(group_A, group_B): + torch.matmul(a, b) + + +def run_benchmark_case(N, provider): + group_size = 4 + group_A = [] + group_B = [] + group_C = [] + A_addrs = [] + B_addrs = [] + C_addrs = [] + g_sizes = [] + g_lds = [] + for _ in range(group_size): + A = torch.rand((N, N), device=DEV, dtype=torch.float16) + B = torch.rand((N, N), device=DEV, dtype=torch.float16) + C = torch.empty((N, N), device=DEV, dtype=torch.float16) + group_A.append(A) + group_B.append(B) + group_C.append(C) + A_addrs.append(A.data_ptr()) + B_addrs.append(B.data_ptr()) + C_addrs.append(C.data_ptr()) + g_sizes += [N, N, N] + g_lds += [N, N, N] + + d_a_ptrs = torch.tensor(A_addrs, device=DEV) + d_b_ptrs = torch.tensor(B_addrs, device=DEV) + d_c_ptrs = torch.tensor(C_addrs, device=DEV) + d_g_sizes = torch.tensor(g_sizes, dtype=torch.int32, device=DEV) + d_g_lds = torch.tensor(g_lds, dtype=torch.int32, device=DEV) + + quantiles = [0.5, 0.2, 0.8] + + def bench_torch(): + torch_perf_fn(group_A, group_B) + + def bench_triton(): + triton_perf_fn(d_a_ptrs, d_b_ptrs, d_c_ptrs, d_g_sizes, d_g_lds, group_size) + + if provider == 'torch': + ms, min_ms, max_ms = triton.testing.do_bench(bench_torch, quantiles=quantiles) + if provider == 'triton': + ms, min_ms, max_ms = triton.testing.do_bench(bench_triton, quantiles=quantiles) + + assert ms >= 0 + assert min_ms >= 0 + assert max_ms >= 0 + + +@pytest.mark.parametrize("N", [2**i for i in range(7, 11)]) +@pytest.mark.parametrize("provider", ["torch", "triton"]) +def test_grouped_gemm_benchmark_cases(N, provider): + run_benchmark_case(N=N, provider=provider) diff --git a/third_party/ascend/unittest/pytest_ut/test_09_persistent_matmul.py b/third_party/ascend/unittest/pytest_ut/test_09_persistent_matmul.py new file mode 100644 index 0000000000..f73a3fc46f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_09_persistent_matmul.py @@ -0,0 +1,325 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Persistent Matmul +===================== +""" + +import time + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import triton.runtime.driver as driver + +DEV = "npu" +DTYPE = torch.float16 + + +def get_npu_properties(): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device) + + +def get_num_compute_cores(): + return get_npu_properties()["num_aicore"] + + +def _matmul_launch_metadata(grid, kernel, args): + ret = {} + M, N, K = args["M"], args["N"], args["K"] + bytes_per_elem = args["c_ptr"].element_size() + ret["name"] = f"{kernel.name} [M={M}, N={N}, K={K}]" + ret[f"flops{bytes_per_elem * 8}"] = 2.0 * M * N * K + ret["bytes"] = bytes_per_elem * (M * K + N * K + M * N) + return ret + + +@triton.jit(launch_metadata=_matmul_launch_metadata) +def matmul_kernel( + a_ptr, + b_ptr, + c_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_cn, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + GROUP_SIZE_M: tl.constexpr, +): + pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + (pid % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + start_m = pid_m * BLOCK_SIZE_M + start_n = pid_n * BLOCK_SIZE_N + + offs_am = start_m + tl.arange(0, BLOCK_SIZE_M) + offs_bn = start_n + tl.arange(0, BLOCK_SIZE_N) + offs_am = tl.where(offs_am < M, offs_am, 0) + offs_bn = tl.where(offs_bn < N, offs_bn, 0) + + offs_am = tl.max_contiguous(tl.multiple_of(offs_am, BLOCK_SIZE_M), BLOCK_SIZE_M) + offs_bn = tl.max_contiguous(tl.multiple_of(offs_bn, BLOCK_SIZE_N), BLOCK_SIZE_N) + offs_k = tl.arange(0, BLOCK_SIZE_K) + a_ptrs = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) + b_ptrs = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) + + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + + for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)): + a = tl.load(a_ptrs, mask=offs_k[None, :] < K - k * BLOCK_SIZE_K, other=0.0) + b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_SIZE_K, other=0.0) + accumulator = tl.dot(a, b, accumulator) + a_ptrs += BLOCK_SIZE_K * stride_ak + b_ptrs += BLOCK_SIZE_K * stride_bk + + c = accumulator.to(tl.float16) + offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] + c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N) + tl.store(c_ptrs, c, mask=c_mask) + + +@triton.jit(launch_metadata=_matmul_launch_metadata) +def matmul_kernel_persistent( + a_ptr, + b_ptr, + c_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_cn, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + GROUP_SIZE_M: tl.constexpr, + NUM_SMS: tl.constexpr, +): + start_pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + k_tiles = tl.cdiv(K, BLOCK_SIZE_K) + num_tiles = num_pid_m * num_pid_n + + tiles_per_sm = num_tiles // NUM_SMS + if start_pid < num_tiles % NUM_SMS: + tiles_per_sm += 1 + + tile_id = start_pid - NUM_SMS + ki = -1 + offs_k_for_mask = tl.arange(0, BLOCK_SIZE_K) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + + pid_m = 0 + pid_n = 0 + offs_am = tl.arange(0, BLOCK_SIZE_M) + offs_bn = tl.arange(0, BLOCK_SIZE_N) + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + + for _ in range(0, k_tiles * tiles_per_sm): + ki = tl.where(ki == k_tiles - 1, 0, ki + 1) + if ki == 0: + tile_id += NUM_SMS + group_id = tile_id // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + (tile_id % group_size_m) + pid_n = (tile_id % num_pid_in_group) // group_size_m + + start_m = pid_m * BLOCK_SIZE_M + start_n = pid_n * BLOCK_SIZE_N + offs_am = start_m + tl.arange(0, BLOCK_SIZE_M) + offs_bn = start_n + tl.arange(0, BLOCK_SIZE_N) + offs_am = tl.where(offs_am < M, offs_am, 0) + offs_bn = tl.where(offs_bn < N, offs_bn, 0) + offs_am = tl.max_contiguous(tl.multiple_of(offs_am, BLOCK_SIZE_M), BLOCK_SIZE_M) + offs_bn = tl.max_contiguous(tl.multiple_of(offs_bn, BLOCK_SIZE_N), BLOCK_SIZE_N) + + offs_k = ki * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K) + a_ptrs = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) + b_ptrs = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) + + a = tl.load(a_ptrs, mask=offs_k_for_mask[None, :] < K - ki * BLOCK_SIZE_K, other=0.0) + b = tl.load(b_ptrs, mask=offs_k_for_mask[:, None] < K - ki * BLOCK_SIZE_K, other=0.0) + accumulator = tl.dot(a, b, accumulator) + + if ki == k_tiles - 1: + offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M) + offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] + c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N) + c = accumulator.to(tl.float16) + tl.store(c_ptrs, c, mask=c_mask) + accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + + +def get_configs(dtype): + return { + torch.float16: { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 256, + "BLOCK_SIZE_K": 64, + "GROUP_SIZE_M": 8, + "num_stages": 3, + "num_warps": 8, + } + }[dtype] + + +def matmul(a, b): + assert a.shape[1] == b.shape[0], "Incompatible dimensions" + assert a.dtype == b.dtype, "Incompatible dtypes" + M, K = a.shape + _, N = b.shape + configs = get_configs(a.dtype) + c = torch.empty((M, N), device=a.device, dtype=a.dtype) + + def grid(meta): + return (triton.cdiv(M, meta["BLOCK_SIZE_M"]) * triton.cdiv(N, meta["BLOCK_SIZE_N"]), ) + + matmul_kernel[grid]( + a, + b, + c, + M, + N, + K, + a.stride(0), + a.stride(1), + b.stride(0), + b.stride(1), + c.stride(0), + c.stride(1), + BLOCK_SIZE_M=configs["BLOCK_SIZE_M"], + BLOCK_SIZE_N=configs["BLOCK_SIZE_N"], + BLOCK_SIZE_K=configs["BLOCK_SIZE_K"], + GROUP_SIZE_M=configs["GROUP_SIZE_M"], + num_stages=configs["num_stages"], + num_warps=configs["num_warps"], + ) + return c + + +def matmul_persistent(a, b): + assert a.shape[1] == b.shape[0], "Incompatible dimensions" + assert a.dtype == b.dtype, "Incompatible dtypes" + num_sms = get_num_compute_cores() + M, K = a.shape + _, N = b.shape + configs = get_configs(a.dtype) + c = torch.empty((M, N), device=a.device, dtype=a.dtype) + + def grid(meta): + return (min(num_sms, triton.cdiv(M, meta["BLOCK_SIZE_M"]) * triton.cdiv(N, meta["BLOCK_SIZE_N"])), ) + + matmul_kernel_persistent[grid]( + a, + b, + c, + M, + N, + K, + a.stride(0), + a.stride(1), + b.stride(0), + b.stride(1), + c.stride(0), + c.stride(1), + BLOCK_SIZE_M=configs["BLOCK_SIZE_M"], + BLOCK_SIZE_N=configs["BLOCK_SIZE_N"], + BLOCK_SIZE_K=configs["BLOCK_SIZE_K"], + GROUP_SIZE_M=configs["GROUP_SIZE_M"], + NUM_SMS=num_sms, + num_stages=configs["num_stages"], + num_warps=configs["num_warps"], + ) + return c + + +def torch_matmul(a, b): + return torch.matmul(a, b) + + +def bench(K, reps=10): + M = 8192 + N = 8192 + a = torch.randn((M, K), device=DEV, dtype=DTYPE) + b = torch.randn((K, N), device=DEV, dtype=DTYPE) + + for _ in range(reps): + _ = torch_matmul(a, b) + time.sleep(0.01) + for _ in range(reps): + _ = matmul(a, b) + time.sleep(0.01) + for _ in range(reps): + _ = matmul_persistent(a, b) + time.sleep(0.01) + + +def validate(M, N, K): + a = torch.randn((M, K), device=DEV, dtype=DTYPE) + b = torch.randn((K, N), device=DEV, dtype=DTYPE) + + torch_result = torch_matmul(a, b) + naive_result = matmul(a, b) + persistent_result = matmul_persistent(a, b) + return torch_result, naive_result, persistent_result + + +@pytest.mark.skip(reason="temporarily skip persistent matmul validate cases until UB overflow issue is fixed") +@pytest.mark.parametrize( + "M,N,K", + [ + (32, 32, 32), + (8192, 8192, 512), + ], +) +def test_persistent_matmul_validate_cases(M, N, K): + torch.manual_seed(0) + torch_result, naive_result, persistent_result = validate(M, N, K) + + torch.testing.assert_close(naive_result, torch_result, atol=1.0, rtol=0) + torch.testing.assert_close(persistent_result, torch_result, atol=1.0, rtol=0) + torch.testing.assert_close(naive_result, persistent_result, atol=1.0, rtol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_10_gather_sorted.py b/third_party/ascend/unittest/pytest_ut/test_10_gather_sorted.py new file mode 100644 index 0000000000..8fcc3f6b9e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_10_gather_sorted.py @@ -0,0 +1,201 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Gather sorted +=============== +This is an example only for npu. +""" + +import pytest +import torch +import torch_npu +import triton +import triton.runtime.driver as driver +import triton.language as tl + + +# get device properties of npu +def get_npu_properties(): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device) + + +# a torch-version gather_sorted benchmark +def torch_gather_sorted(embeddings, sorted_idxes, aux_idxes): + # make the result tensor + res = torch.empty((aux_idxes.shape[0], embeddings.shape[-1]), dtype=embeddings.dtype, device=embeddings.device) + + # scatter embeddings + res[aux_idxes] = embeddings[sorted_idxes] + + return res + + +# triton-version gather_sorted's kernel +@triton.jit +def gather_sorted_kernel(embeddings_ptr, sorted_indices_ptr, aux_indices_ptr, res_ptr, rows, cols, + DEFAULT_VALUE: tl.constexpr, BIG_CORE_NUM: tl.constexpr, BIG_ROW_BLOCK_SIZE: tl.constexpr, + COL_BLOCK_SIZE: tl.constexpr, COL_BLOCK_SIZE_SUB: tl.constexpr): + SMALL_ROW_BLOCK_SIZE = BIG_ROW_BLOCK_SIZE - 1 + + emb_dtype = embeddings_ptr.type.element_ty + default_value = tl.cast(DEFAULT_VALUE, dtype=emb_dtype) + + core_idx = tl.program_id(0) + # compute the the size and start index of block + row_block_size = BIG_ROW_BLOCK_SIZE if (core_idx < BIG_CORE_NUM) else SMALL_ROW_BLOCK_SIZE + row_start_idx = (core_idx * BIG_ROW_BLOCK_SIZE) if (core_idx < BIG_CORE_NUM) else ( + BIG_CORE_NUM * BIG_ROW_BLOCK_SIZE + (core_idx - BIG_CORE_NUM) * SMALL_ROW_BLOCK_SIZE) + + # this version has 3-buffers, initilize for buffers + row_block_size_0 = tl.cdiv(row_block_size, 3) + remain_row_block_size = row_block_size - row_block_size_0 + row_block_size_1 = tl.cdiv(remain_row_block_size, 2) + row_block_size_2 = remain_row_block_size - row_block_size_1 + + row_start_idx_0 = row_start_idx + row_start_idx_1 = row_start_idx + row_block_size_0 + row_start_idx_2 = row_start_idx + row_block_size_0 + row_block_size_1 + + # process blocks witn shape (row_block_size, COL_BLOCK_SIZE_SUB) one by one + for col_idx in tl.range(0, COL_BLOCK_SIZE, COL_BLOCK_SIZE_SUB): + + embedding_0 = tl.full((COL_BLOCK_SIZE_SUB, ), default_value, dtype=emb_dtype) + embedding_1 = embedding_0 + 0 + embedding_2 = embedding_0 + 0 + + emb_offsets = col_idx + tl.arange(0, COL_BLOCK_SIZE_SUB) + emb_mask = emb_offsets < cols + + prev_embedding_idx_0 = tl.cast(-1, dtype=tl.int32) + prev_embedding_idx_1 = tl.cast(-1, dtype=tl.int32) + prev_embedding_idx_2 = tl.cast(-1, dtype=tl.int32) + for row_idx in tl.range(row_start_idx_0, row_start_idx_1): + # process the first buffer + embedding_idx_0 = tl.load(sorted_indices_ptr + row_idx) + res_idx_0 = tl.load(aux_indices_ptr + row_idx) + + if (embedding_idx_0 != 0) and (embedding_idx_0 != prev_embedding_idx_0): + embedding_0 = tl.load(embeddings_ptr + embedding_idx_0 * cols + emb_offsets, emb_mask) + tl.store(res_ptr + res_idx_0 * cols + emb_offsets, embedding_0, emb_mask) + else: + tl.store(res_ptr + res_idx_0 * cols + emb_offsets, embedding_0, emb_mask) + + prev_embedding_idx_0 = embedding_idx_0 + + # process the second buffer + if (row_idx + row_block_size_0) < (row_start_idx_1 + row_block_size_1): + embedding_idx_1 = tl.load(sorted_indices_ptr + row_idx + row_block_size_0) + res_idx_1 = tl.load(aux_indices_ptr + row_idx + row_block_size_0) + + if (embedding_idx_1 != 0) and (embedding_idx_1 != prev_embedding_idx_1): + embedding_1 = tl.load(embeddings_ptr + embedding_idx_1 * cols + emb_offsets, emb_mask) + tl.store(res_ptr + res_idx_1 * cols + emb_offsets, embedding_1, emb_mask) + else: + tl.store(res_ptr + res_idx_1 * cols + emb_offsets, embedding_1, emb_mask) + + prev_embedding_idx_1 = embedding_idx_1 + + # process the third buffer + if (row_idx + row_block_size_0 + row_block_size_1) < (row_start_idx_2 + row_block_size_2): + embedding_idx_2 = tl.load(sorted_indices_ptr + row_idx + row_block_size_0 + row_block_size_1) + res_idx_2 = tl.load(aux_indices_ptr + row_idx + row_block_size_0 + row_block_size_1) + + if (embedding_idx_2 != 0) and (embedding_idx_2 != prev_embedding_idx_2): + embedding_2 = tl.load(embeddings_ptr + embedding_idx_2 * cols + emb_offsets, emb_mask) + tl.store(res_ptr + res_idx_2 * cols + emb_offsets, embedding_2, emb_mask) + else: + tl.store(res_ptr + res_idx_2 * cols + emb_offsets, embedding_2, emb_mask) + + prev_embedding_idx_2 = embedding_idx_2 + + +# triton-version gather_sorted's host +def triton_gather_sorted(embeddings: torch.Tensor, sorted_indices: torch.Tensor, aux_indices: torch.Tensor, + default_value=1.0): + # constant settings for npu + ALIGNED = 32 + USE_SIZE = 96 * 1024 + CORE_NUM = get_npu_properties()["num_vectorcore"] + + n_rows = sorted_indices.shape[0] + n_cols = embeddings.shape[1] + # make the result tensor + output = torch.empty(n_rows, n_cols, dtype=embeddings.dtype, device=embeddings.device) + + # when writing an npu kernel using triton, + # you should note that the difference between BLOCK_SIZE and BLOCK_SIZE_SUB + # BLOCK_SIZE specifies the size of data that are processed in one program + col_size_aligned = triton.cdiv(embeddings.shape[-1] * embeddings.element_size(), + ALIGNED) * ALIGNED // embeddings.element_size() + # the data are scattered to multiple programs, which can not be even + # some process more data, some process less + big_row_block_size = triton.cdiv(n_rows, CORE_NUM) + big_core_num = CORE_NUM - ((big_row_block_size * CORE_NUM) - n_rows) + col_block_size = col_size_aligned + # BLOCK_SIZE_SUB specifies the size of data that are processed in one loop of a program + col_block_size_sub = min(1024, col_size_aligned) + + grid = (min(n_rows, CORE_NUM), triton.cdiv(n_cols, col_block_size)) + # launch the kernel + gather_sorted_kernel[grid](embeddings, sorted_indices, aux_indices, output, n_rows, n_cols, default_value, + BIG_CORE_NUM=big_core_num, BIG_ROW_BLOCK_SIZE=big_row_block_size, + COL_BLOCK_SIZE=col_block_size, COL_BLOCK_SIZE_SUB=col_block_size_sub) + + return output + + +# genreate the desired inputs +def generate_inputs(index_shape, table_shape, dtype): + sorted_indices = torch.randint(1, table_shape[0], index_shape, dtype=torch.int32).npu() + mask = torch.rand_like(sorted_indices, dtype=torch.float).npu() < 0.2 + + # make sorted_indices + sorted_indices[mask] = 0 + sorted_indices, _ = torch.sort(sorted_indices) + counts = torch.bincount(sorted_indices) + _, _indices = torch.sort(counts[sorted_indices], descending=True, stable=True) + sorted_indices = sorted_indices[_indices] + + # make aux_indicess + aux_indices = torch.arange(0, index_shape[0], dtype=torch.int32).npu() + _indices = torch.randperm(aux_indices.size(0)) + aux_indices = aux_indices[_indices] + + # make table, the first contains only 1.0 + table = torch.randn(table_shape, dtype=dtype).npu() + table[0] = 1.0 + + return table, sorted_indices, aux_indices + + +# ==================== Pytest Test ==================== +@pytest.mark.parametrize("table_rows", [500, 1000]) +@pytest.mark.parametrize("table_cols", [16, 17, 31, 32, 63, 64, 128, 256, 819, 512, 1024, 8192, 1001, 2003, 17000]) +@pytest.mark.parametrize("index_num", [19, 123, 4321, 54321, 100, 200, 819, 500, 700, 1000]) +def test_gather_sorted(table_rows, table_cols, index_num): + table, sorted_indices, aux_indices = generate_inputs((index_num, ), (table_rows, table_cols), torch.float) + + expect = torch_gather_sorted(table, sorted_indices, aux_indices).cpu() + torch.npu.synchronize() + actual = triton_gather_sorted(table, sorted_indices, aux_indices).cpu() + torch.npu.synchronize() + + torch.testing.assert_close(actual, expect) diff --git a/third_party/ascend/unittest/pytest_ut/test_11_rab_time.py b/third_party/ascend/unittest/pytest_ut/test_11_rab_time.py new file mode 100644 index 0000000000..6603a2a457 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_11_rab_time.py @@ -0,0 +1,351 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Relative Attention Bias Timestamps +=============== +""" + +import math +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import triton.runtime.driver as driver + +NUM_BUCKETS = 128 +BUCKET_DIVISOR = 0.301 + + +# get device properties of npu +def get_npu_properties(): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device) + + +def create_pos_w(train_len: int, num_layers: int) -> torch.Tensor: + return torch.arange(0, 2 * train_len + 1).unsqueeze(1).repeat(1, num_layers) + + +def create_past_valid_lens(bs: int, past_len: int) -> torch.Tensor: + return torch.randint(0, past_len, (bs, )) + + +def create_timestamps(train_len: int, candidate_len: int, past_valid_lens: torch.Tensor) -> torch.Tensor: + bs = past_valid_lens.size(0) + timestamps = torch.zeros(bs, train_len + candidate_len // 2) + for i, valid_len in enumerate(past_valid_lens): + if valid_len > 0: + timestamps[i, :valid_len] = torch.arange(1, valid_len.int() + 1) + + if candidate_len <= 0: + return timestamps + timestamps[:, -candidate_len // 2:] = train_len + 1 + + return timestamps + + +def create_timestamps_weights(num_layers: int): + return (torch.arange(0, NUM_BUCKETS + 1).repeat(num_layers).reshape(NUM_BUCKETS + 1, num_layers)) + + +def create_rab_time_grad(num_layers: int, batchsize: int, s: int): + return torch.rand(num_layers, batchsize, s, s) * 1e-4 + + +def create_bucket_timestamps(batchsize: int, s: int): + result = torch.arange(batchsize * s) % NUM_BUCKETS + result = result.unsqueeze(-1).repeat(1, 1, s) + return result + + +@triton.jit +def rab_time_forward_kernel( + inp, + out, + index, + index_len: tl.constexpr, + inp_row_stride: tl.constexpr, + clamp_max: tl.constexpr, + bucketization_divisor: tl.constexpr, + BLOCK_SIZE: tl.constexpr, + COL_BLOCK_SIZE: tl.constexpr, +): + pid0 = tl.program_id(axis=0) + pid1 = tl.program_id(axis=1) + + col_iter_num = tl.cdiv(BLOCK_SIZE, COL_BLOCK_SIZE) + + for col_idx in tl.range(0, col_iter_num): + cols_offsets = (pid0 * BLOCK_SIZE + col_idx * COL_BLOCK_SIZE + tl.arange(0, COL_BLOCK_SIZE)) + cols_mask = cols_offsets < index_len + + out_mask = cols_offsets < index_len + + index_val = tl.load(index + cols_offsets, mask=cols_mask, other=0.0) + index_val = tl.abs(index_val) + index_val = tl.minimum(tl.maximum(index_val, 1.0), clamp_max) + index_val = tl.log(index_val) + index_val = index_val / bucketization_divisor + index_val = tl.cast(index_val, tl.int64) + + inp_val = tl.load(inp + pid1 * inp_row_stride + tl.arange(0, inp_row_stride)) + out_val = tl.gather(inp_val, index_val, 0) + + tl.store(out + pid1 * index_len + cols_offsets, out_val, mask=out_mask) + + +def get_outer_loop_num(num_layers, index_len): + sub_num_layers = num_layers + while sub_num_layers * index_len >= 2**31 - 1: + sub_num_layers = sub_num_layers // 2 + outer_loop_num = (num_layers + sub_num_layers - 1) // sub_num_layers + remain_layers = num_layers % sub_num_layers + return outer_loop_num, sub_num_layers, remain_layers + + +def rab_time_forward_triton(ts_w, timestamps, bucketization_divisor): + ts_w_trans = ts_w.t().contiguous() + + bs, seq_len = timestamps.shape + infer_len = 2 * seq_len + num_layers = ts_w.shape[1] + num_buckets = ts_w.shape[0] - 1 + + timestamps_expanded = timestamps.unsqueeze(-1).repeat(1, 1, 2) + timestamps_expanded = timestamps_expanded.reshape(bs, infer_len, 1) - timestamps_expanded.reshape(bs, 1, infer_len) + + timestamps_expanded = timestamps_expanded.view(-1) + timestamps_expanded = timestamps_expanded.contiguous() + + clamp_max = torch.exp(torch.tensor(num_buckets * bucketization_divisor)).item() + index_len = bs * infer_len * infer_len + + out = torch.empty((num_layers, index_len), dtype=ts_w.dtype, device=ts_w.device) + outer_loop_num, sub_num_layers, remain_layers = get_outer_loop_num(num_layers, index_len) + + CORE_NUM = get_npu_properties()["num_vectorcore"] + BLOCK_SIZE = math.ceil(index_len / CORE_NUM) + COL_BLOCK_SIZE = 8 * 1024 + + curr_layers = sub_num_layers + for i in range(outer_loop_num): + if i == outer_loop_num - 1 and remain_layers != 0: + curr_layers = remain_layers + + def grid(meta): + return (triton.cdiv(index_len, meta["BLOCK_SIZE"]), curr_layers) + + rab_time_forward_kernel[grid]( + ts_w_trans[i * sub_num_layers], + out[i * sub_num_layers], + timestamps_expanded, + index_len, + num_buckets + 1, + clamp_max, + bucketization_divisor, + BLOCK_SIZE, + COL_BLOCK_SIZE, + ) + + out = out.view(num_layers, bs, infer_len, infer_len) + + return out + + +@triton.jit +def rab_time_backward_kernel(inp, src, index, index_len, BLOCK_SIZE: tl.constexpr, COL_BLOCK_SIZE: tl.constexpr): + pid0 = tl.program_id(axis=0) + total_col_num = (BLOCK_SIZE if pid0 * BLOCK_SIZE + BLOCK_SIZE < index_len else index_len - pid0 * BLOCK_SIZE) + COL_BLOCK_SIZE = min(COL_BLOCK_SIZE, total_col_num) + col_iter_num = (total_col_num + COL_BLOCK_SIZE - 1) // COL_BLOCK_SIZE + + for col_idx in tl.range(0, col_iter_num): + base_idx = 0 + base_idx = base_idx.to(index.dtype.element_ty) + + col_start_offset = col_idx * COL_BLOCK_SIZE + + acc_result = 0.0 + acc_result = acc_result.to(inp.dtype.element_ty) + cur_col_num = (COL_BLOCK_SIZE if col_start_offset + COL_BLOCK_SIZE < total_col_num else total_col_num - + col_start_offset) + + for cur_idx in range(0, cur_col_num): + cur_offset = pid0 * BLOCK_SIZE + col_start_offset + cur_idx + + src_val = tl.load(src + cur_offset) + new_idx = tl.load(index + cur_offset) + + if base_idx == new_idx: + acc_result += src_val + else: + tl.atomic_add(inp + base_idx, acc_result) + + base_idx = new_idx + acc_result = 0.0 + acc_result = acc_result.to(inp.dtype.element_ty) + acc_result += src_val + + tl.atomic_add(inp + base_idx, acc_result) + + +def rab_time_backward_triton(rab_time_grad: torch.Tensor, bucket_timestamps: torch.Tensor): + num_layers, b, s, _ = rab_time_grad.shape + tsw_grad = torch.zeros(num_layers, NUM_BUCKETS, dtype=torch.float32).to(rab_time_grad.device) + + bucket_timestamps_expand = (bucket_timestamps.reshape(b, s // 2, 1, s // 2, + 1).repeat(1, 1, 2, 1, 2).reshape(b, s, + s).to(torch.int64)).view(-1) + + index_len = bucket_timestamps_expand.numel() + + rab_time_grad_f32 = rab_time_grad.to(torch.float32) + sorted_bucket_timestamps_expand, sorted_idx = torch.sort(bucket_timestamps_expand.view(-1)) + + torch.npu.synchronize() + + def grid(meta): + return (triton.cdiv(index_len, meta["BLOCK_SIZE"]), ) + + CORE_NUM = get_npu_properties()["num_vectorcore"] + BLOCK_SIZE = math.ceil(index_len / CORE_NUM) + + COL_BLOCK_SIZE = 8 * 1024 + + for layer_idx in range(num_layers): + curr_sorted_grad_f32 = rab_time_grad_f32[layer_idx].view(-1)[sorted_idx] + rab_time_backward_kernel[grid]( + tsw_grad[layer_idx], + curr_sorted_grad_f32, + sorted_bucket_timestamps_expand, + index_len, + BLOCK_SIZE, + COL_BLOCK_SIZE, + ) + + return tsw_grad + + +def rab_time_forward_golden(ts_w: torch.Tensor, timestamps: torch.Tensor, bucketization_divisor: float) -> torch.Tensor: + """ + torch realization of rab time forward for reference. + """ + infer_len = timestamps.shape[1] * 2 + bs = timestamps.shape[0] + num_layers = ts_w.shape[1] + + timestamps = timestamps.unsqueeze(-1).repeat(1, 1, 2) + diff_timestamps = timestamps.reshape(bs, infer_len, 1) - timestamps.reshape(bs, 1, infer_len) + + clamp_max = torch.exp(torch.tensor(NUM_BUCKETS * BUCKET_DIVISOR)) + diff_timestamps = (torch.log(torch.abs(diff_timestamps).clamp(1, clamp_max)) / bucketization_divisor) + bucket_timestamps = diff_timestamps.long() + bucket_timestamps = bucket_timestamps.view(-1) + result = torch.index_select(ts_w, dim=0, index=bucket_timestamps) + + result = result.t() + + result = result.view(num_layers, bs, infer_len, infer_len) + return result + + +def rab_time_backward_golden(rab_time_grad: torch.Tensor, bucket_timestamps: torch.Tensor): + """ + torch realization of rab time backward for reference. + """ + num_layers, b, s, _ = rab_time_grad.shape + tsw_grad = torch.zeros(num_layers, NUM_BUCKETS, dtype=torch.float32).to(rab_time_grad.device) + + bucket_timestamps_expand = (bucket_timestamps.reshape(b, s // 2, 1, s // 2, + 1).repeat(1, 1, 2, 1, 2).reshape(b, s, s).to(torch.int64)) + for n, grad in enumerate(rab_time_grad.to(torch.float32)): + tsw_grad[n] = tsw_grad[n].scatter_add(src=grad.view(-1), index=bucket_timestamps_expand.view(-1), dim=0) + return tsw_grad + + +def run_rab_time_forward_case(num_layers, train_len, candidate_len, bs, dtype): + past_valid_lens = create_past_valid_lens(bs, train_len).to(torch.int32) + timestamps = create_timestamps(train_len, candidate_len, past_valid_lens).to(torch.int32) + timestamps_weights = create_timestamps_weights(num_layers).to(dtype) + timestamps = timestamps.npu() + timestamps_weights = timestamps_weights.npu() + + torch_npu.npu.synchronize() + + # triton output + rab_time_out_triton = rab_time_forward_triton( + ts_w=timestamps_weights, + timestamps=timestamps, + bucketization_divisor=BUCKET_DIVISOR, + ) + torch_npu.npu.synchronize() + + # pytorch output + rab_time_out_golden = rab_time_forward_golden( + ts_w=timestamps_weights, + timestamps=timestamps, + bucketization_divisor=BUCKET_DIVISOR, + ) + torch_npu.npu.synchronize() + + torch.testing.assert_close(rab_time_out_triton, rab_time_out_golden) + + +def run_rab_time_backward_case(num_layers: int, batchsize: int, s: int, dtype: torch.dtype): + grad = create_rab_time_grad(num_layers, batchsize, s).to(dtype).npu() + bucket_timestamps = (create_bucket_timestamps(batchsize, s // 2).to(torch.int32).npu()) + + torch_npu.npu.synchronize() + + golden_result = (rab_time_backward_golden(grad, bucket_timestamps).to(torch.float32).cpu()) + op_result = (rab_time_backward_triton(grad, bucket_timestamps).to(torch.float32).cpu()) + + loss = 1e-4 if dtype == torch.float32 else 1e-3 + torch.testing.assert_close(op_result, golden_result, rtol=loss, atol=loss) + + +@pytest.mark.parametrize( + "num_layers, train_len, candidate_len, batch_size, dtype", + [ + pytest.param( + 8, + 500, + 500, + 4, + torch.float32, + marks=pytest.mark.skip(reason="temporarily skip UB overflow case"), + ), + ], +) +def test_rab_time_forward(num_layers, train_len, candidate_len, batch_size, dtype): + torch.manual_seed(0) + run_rab_time_forward_case(num_layers, train_len, candidate_len, batch_size, dtype) + + +@pytest.mark.parametrize( + "num_layers, batch_size, seq_len, dtype", + [ + (8, 4, 1500, torch.float32), + ], +) +def test_rab_time_backward(num_layers, batch_size, seq_len, dtype): + torch.manual_seed(0) + run_rab_time_backward_case(num_layers, batch_size, seq_len, dtype) diff --git a/third_party/ascend/unittest/pytest_ut/test_12_hstu_attention.py b/third_party/ascend/unittest/pytest_ut/test_12_hstu_attention.py new file mode 100644 index 0000000000..f4fe9b523d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_12_hstu_attention.py @@ -0,0 +1,933 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +HSTU Attention +=============== +""" + +from dataclasses import dataclass +from typing import List, Optional, Tuple +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import triton.runtime.driver as driver +import numpy as np +import torch.nn.functional as F + +DEVICE = "npu" +BLOCK_FWD = 64 +BLOCK_BWD = 32 + + +@dataclass +class JaggedData: + grad: torch.Tensor + q: torch.Tensor + k: torch.Tensor + v: torch.Tensor + bias: torch.Tensor + mask: torch.Tensor + max_seq_len: int + seq_offset: np.ndarray + + +def get_npu_properties(coreType): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device)[coreType] + + +@triton.jit +def _hstu_attn_fwd_one_block( + q, + k_block_ptr, + v_block_ptr, + bias_block_ptr, + alpha, + MAX_SEQ_LEN, + CAUSAL: tl.constexpr, + HAS_BIAS: tl.constexpr, + mask_block, +): + k = tl.load(k_block_ptr) + qk = tl.dot(q, tl.trans(k)) * alpha + if HAS_BIAS: + rel_attn_bias = tl.load(bias_block_ptr) + qk = qk + rel_attn_bias + silu = qk / (1.0 + tl.exp(-qk)) * (1.0 / MAX_SEQ_LEN) + if CAUSAL: + silu = tl.where(mask_block != 0, silu, 0) + v = tl.load(v_block_ptr) + silu = silu.to(v.dtype) + return tl.dot(silu, v) + + +@triton.jit +def _hstu_attn_fwd_compute( # noqa C901 + Q, + K, + V, + seq_offsets, + Out, + stride_qm: tl.constexpr, + stride_qh: tl.constexpr, + stride_kn: tl.constexpr, + stride_kh: tl.constexpr, + stride_vn: tl.constexpr, + stride_vh: tl.constexpr, + stride_om: tl.constexpr, + stride_oh: tl.constexpr, + alpha, + head_num, + MAX_SEQ_LEN, + off_batch, + off_head, + start_m, + seq_start, + seq_len, + CAUSAL: tl.constexpr, + HAS_BIAS: tl.constexpr, + BLOCK_D_Q: tl.constexpr, + BLOCK_D_V: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + mask_block, + bias, +): + off_head = off_head.to(tl.int64) + off_seq = seq_start.to(tl.int64) + start_m = start_m.to(tl.int32) + + # initialize offsets + q_offset = off_seq * stride_qm + off_head * stride_qh + k_offset = off_seq * stride_kn + off_head * stride_kh + v_offset = off_seq * stride_vn + off_head * stride_kh + + Q_block_ptr = tl.make_block_ptr( + base=Q + q_offset, + shape=(seq_len, BLOCK_D_Q), + strides=(stride_qm, 1), + offsets=(start_m, 0), + block_shape=(BLOCK_M, BLOCK_D_Q), + order=(1, 0), + ) + k_block_ptr = tl.make_block_ptr( + base=K + k_offset, + shape=(seq_len, BLOCK_D_Q), + strides=(stride_kn, 1), + offsets=(0, 0), + block_shape=(BLOCK_N, BLOCK_D_Q), + order=(1, 0), + ) + v_block_ptr = tl.make_block_ptr( + base=V + v_offset, + shape=(seq_len, BLOCK_D_V), + strides=(stride_vn, 1), + offsets=(0, 0), + block_shape=(BLOCK_N, BLOCK_D_V), + order=(1, 0), + ) + q = tl.load(Q_block_ptr) + + acc = tl.zeros([BLOCK_M, BLOCK_D_V], dtype=tl.float32) + if CAUSAL: + low = 0 + high = start_m + BLOCK_M + else: + low = 0 + high = seq_len + + bias_block_ptr = None + if HAS_BIAS: + bias_block_ptr = tl.make_block_ptr( + base=bias + off_batch * head_num * MAX_SEQ_LEN * MAX_SEQ_LEN + off_head * MAX_SEQ_LEN * MAX_SEQ_LEN, + shape=(MAX_SEQ_LEN, MAX_SEQ_LEN), + strides=(MAX_SEQ_LEN, 1), + offsets=(start_m, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + + for start_n in range(low, high, BLOCK_N): + acc += _hstu_attn_fwd_one_block( + q=q, + k_block_ptr=k_block_ptr, + v_block_ptr=v_block_ptr, + bias_block_ptr=bias_block_ptr, + alpha=alpha, + MAX_SEQ_LEN=MAX_SEQ_LEN, + CAUSAL=CAUSAL and start_m == start_n, + HAS_BIAS=HAS_BIAS, + mask_block=mask_block, + ) + k_block_ptr = tl.advance(k_block_ptr, (BLOCK_N, 0)) + v_block_ptr = tl.advance(v_block_ptr, (BLOCK_N, 0)) + if HAS_BIAS: + bias_block_ptr = tl.advance(bias_block_ptr, (0, BLOCK_N)) + + # rematerialize offsets to save registers + offs_v_d = tl.arange(0, BLOCK_D_V) + off_o = Out + off_seq * stride_om + off_head * stride_oh + offs_m = start_m + tl.arange(0, BLOCK_M) + out_ptrs = off_o + offs_m[:, None] * stride_om + offs_v_d[None, :] + tl.store(out_ptrs, acc, mask=(offs_m < seq_len)[:, None]) + + +@triton.jit +def _hstu_attn_fwd( # noqa C901 + Q, + K, + V, + seq_offsets, + Out, + stride_qm: tl.constexpr, + stride_qh: tl.constexpr, + stride_kn: tl.constexpr, + stride_kh: tl.constexpr, + stride_vn: tl.constexpr, + stride_vh: tl.constexpr, + stride_om: tl.constexpr, + stride_oh: tl.constexpr, + alpha: tl.constexpr, + batch: tl.constexpr, + head_num: tl.constexpr, + MAX_SEQ_LEN: tl.constexpr, + head_dim: tl.constexpr, + CAUSAL: tl.constexpr, + HAS_BIAS: tl.constexpr, + CORE_NUM: tl.constexpr, + tasks: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + mask, + bias, +): + core_id = tl.program_id(0) + cur_batch = 0 + mask_block = None + if CAUSAL and mask is not None: + mask_ptr = tl.make_block_ptr( + base=mask, + shape=(MAX_SEQ_LEN, MAX_SEQ_LEN), + strides=(MAX_SEQ_LEN, 1), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_M), + order=(1, 0), + ) + mask_block = tl.load(mask_ptr) + for col in range(core_id, tasks, CORE_NUM): + seq_end = tl.load(seq_offsets + cur_batch + 1) + start_m = col * BLOCK_M + while start_m >= seq_end * head_num // 2: + cur_batch += 1 + seq_end = tl.load(seq_offsets + cur_batch + 1) + seq_start = tl.load(seq_offsets + cur_batch) + seq_len = seq_end - seq_start + off_batch = cur_batch + off_head = (start_m - seq_start * head_num // 2) // (seq_len // 2) + start_m_1 = (start_m - seq_start * head_num // 2) % (seq_len // 2) + start_m_2 = seq_len - start_m_1 - BLOCK_M + _hstu_attn_fwd_compute( + Q, + K, + V, + seq_offsets, + Out, + stride_qm, + stride_qh, + stride_kn, + stride_kh, + stride_vn, + stride_vh, + stride_om, + stride_oh, + alpha, + head_num, + MAX_SEQ_LEN, + off_batch, + off_head, + start_m_1, + seq_start, + seq_len, + CAUSAL, + HAS_BIAS, + head_dim, + head_dim, + BLOCK_M, + BLOCK_N, + mask_block=mask_block, + bias=bias, + ) + _hstu_attn_fwd_compute( + Q, + K, + V, + seq_offsets, + Out, + stride_qm, + stride_qh, + stride_kn, + stride_kh, + stride_vn, + stride_vh, + stride_om, + stride_oh, + alpha, + head_num, + MAX_SEQ_LEN, + off_batch, + off_head, + start_m_2, + seq_start, + seq_len, + CAUSAL, + HAS_BIAS, + head_dim, + head_dim, + BLOCK_M, + BLOCK_N, + mask_block=mask_block, + bias=bias, + ) + + +@triton.jit +def _hstu_attn_bwd_one_block( # noqa C901 + start_m, + offs_n, + offs_m, + q_ptrs, + dq_ptrs, + mask_n, + do_ptrs, + dk, + dv, + k, + v, + pos_offs_n, + seq_len, + max_ids, + stride_qm, + stride_dom, + stride_dqm, + alpha, + MAX_SEQ_LEN, + CAUSAL: tl.constexpr, + HAS_BIAS: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + bias_block_ptr, +): + pos_offs_m = offs_m + start_m + mask_m = pos_offs_m < seq_len + # recompute qk and silu + q = tl.load( + q_ptrs + start_m * stride_qm, + mask=mask_m[:, None], + other=0.0, + ) + q_trans = tl.trans(q) + qk_trans = tl.dot(k, q_trans) * alpha + if HAS_BIAS: + rel_attn_bias = tl.load(bias_block_ptr) + qk_trans = qk_trans + tl.trans(rel_attn_bias) + sig_trans = 1.0 / (1.0 + tl.exp(-qk_trans)) + silu_trans = qk_trans * sig_trans * (1.0 / MAX_SEQ_LEN) + if CAUSAL: + invalid_mask_trans = pos_offs_m[None, :] == offs_n[:, None] + pos_offs_m_minus_n = pos_offs_m[None, :] - pos_offs_n[:, None] + invalid_mask_trans = invalid_mask_trans | (pos_offs_m_minus_n > 0) + silu_trans = tl.where(invalid_mask_trans, silu_trans, 0) + silu_trans = silu_trans.to(k.dtype) + # compute dv + do = tl.load( + do_ptrs + start_m * stride_dom, + mask=mask_m[:, None], + other=0.0, + ) + dv += tl.dot(silu_trans, do) + # compute dk and dq (dqk = do * v^T dk = dqk^T * q dq = dqk * k) + dqk_trans = tl.dot(v, tl.trans(do)) + dqk_trans = dqk_trans * sig_trans * (1 + qk_trans * (1 - sig_trans)) * (1.0 / MAX_SEQ_LEN) + if CAUSAL: + dqk_trans = tl.where(invalid_mask_trans, dqk_trans, 0) + dqk_trans = dqk_trans.to(k.dtype) + dq = tl.load( + dq_ptrs + start_m * stride_dqm, + mask=mask_m[:, None], + other=0.0, + ) + dq += tl.dot(tl.trans(dqk_trans), k) * alpha + tl.store( + dq_ptrs + start_m * stride_dqm, + dq, + mask=mask_m[:, None], + ) + # Note: the factor `alpha` is delayed until the end of the function to reduce the cost + dk += tl.dot(dqk_trans, q) + return dk, dv + + +@triton.jit +def _hstu_attn_bwd_one_col_block( # noqa C901 + start_n, + seq_len, + Q, + K, + V, + DOut, + DQ, + DK, + DV, + stride_qm, + stride_kn, + stride_vn, + stride_dom, + stride_dqm, + stride_dkn, + stride_dvn, + alpha, + MAX_SEQ_LEN, + CAUSAL: tl.constexpr, + HAS_BIAS: tl.constexpr, + BLOCK_D_Q: tl.constexpr, + BLOCK_D_V: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + bias, +): + # Work on the subsequence dv[start_n, start_n + BLOCK_N, :] + if CAUSAL: + low = start_n + high = seq_len + else: + low = 0 + high = seq_len + + # initialize row/col offsets + offs_m = tl.arange(0, BLOCK_M) + offs_qk_d = tl.arange(0, BLOCK_D_Q) + offs_v_d = tl.arange(0, BLOCK_D_V) + offs_n = start_n + tl.arange(0, BLOCK_N) + + dq_ptrs = DQ + (offs_m[:, None] * stride_dqm + offs_qk_d[None, :]) + dk = tl.zeros([BLOCK_N, BLOCK_D_Q], dtype=tl.float32) + dv = tl.zeros([BLOCK_N, BLOCK_D_V], dtype=tl.float32) + + mask_n = offs_n < seq_len + q_ptrs = Q + (offs_m[:, None] * stride_qm + offs_qk_d[None, :]) + do_ptrs = DOut + (offs_m[:, None] * stride_dom + offs_v_d[None, :]) + k_ptrs = K + (offs_n[:, None] * stride_kn + offs_qk_d[None, :]) + v_ptrs = V + (offs_n[:, None] * stride_vn + offs_v_d[None, :]) + k = tl.load(k_ptrs, mask=mask_n[:, None], other=0.0) + v = tl.load(v_ptrs, mask=mask_n[:, None], other=0.0) + + max_ids = seq_len + pos_offs_n = offs_n + # loop over rows + for start_m in tl.range(low, high, BLOCK_M): + bias_block_ptr = None + if HAS_BIAS: + bias_block_ptr = tl.make_block_ptr( + base=bias, + shape=(MAX_SEQ_LEN, MAX_SEQ_LEN), + strides=(MAX_SEQ_LEN, 1), + offsets=(start_m, start_n), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + start_m = tl.multiple_of(start_m, BLOCK_M) + dk, dv = _hstu_attn_bwd_one_block( + start_m=start_m, + offs_n=offs_n, + offs_m=offs_m, + q_ptrs=q_ptrs, + dq_ptrs=dq_ptrs, + mask_n=mask_n, + do_ptrs=do_ptrs, + dk=dk, + dv=dv, + k=k, + v=v, + pos_offs_n=pos_offs_n, + seq_len=seq_len, + max_ids=max_ids, + stride_qm=stride_qm, + stride_dom=stride_dom, + stride_dqm=stride_dqm, + alpha=alpha, + MAX_SEQ_LEN=MAX_SEQ_LEN, + CAUSAL=CAUSAL, + HAS_BIAS=HAS_BIAS, + BLOCK_M=BLOCK_M, + BLOCK_N=BLOCK_N, + bias_block_ptr=bias_block_ptr, + ) + # write-back + dk = dk * alpha + dv_ptrs = DV + (offs_n[:, None] * stride_dvn + offs_v_d[None, :]) + dk_ptrs = DK + (offs_n[:, None] * stride_dkn + offs_qk_d[None, :]) + tl.store(dv_ptrs, dv.to(k.dtype), mask=mask_n[:, None]) + tl.store(dk_ptrs, dk.to(k.dtype), mask=mask_n[:, None]) + + +@triton.jit +def _hstu_attn_bwd( # noqa C901 + Q, + K, + V, + Grad, + DQ, + DK, + DV, + stride_qm: tl.constexpr, + stride_qh: tl.constexpr, + stride_kn: tl.constexpr, + stride_kh: tl.constexpr, + stride_vn: tl.constexpr, + stride_vh: tl.constexpr, + stride_dom: tl.constexpr, + stride_doh: tl.constexpr, + seq_offsets, + alpha: tl.constexpr, + batch: tl.constexpr, + head_num: tl.constexpr, + MAX_SEQ_LEN: tl.constexpr, + head_dim: tl.constexpr, + CAUSAL: tl.constexpr, + HAS_BIAS: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + bias, +): + off = tl.program_id(0) + off_batch = off // head_num + off_head = off % head_num + off_head = off_head.to(tl.int64) + seq_start = tl.load(seq_offsets + off_batch).to(tl.int64) + seq_end = tl.load(seq_offsets + off_batch + 1) + seq_len = (seq_end - seq_start).to(tl.int32) + # offset pointers for batch/head + q_offset = seq_start * stride_qm + off_head * stride_qh + k_offset = seq_start * stride_kn + off_head * stride_kh + v_offset = seq_start * stride_vn + off_head * stride_vh + grad_offset = seq_start * stride_dom + off_head * stride_doh + bias_offset = off_batch * head_num * MAX_SEQ_LEN * MAX_SEQ_LEN + off_head * MAX_SEQ_LEN * MAX_SEQ_LEN + for start_n in range(0, seq_len, BLOCK_N): + _hstu_attn_bwd_one_col_block( + start_n=start_n, + seq_len=seq_len, + Q=Q + q_offset, + K=K + k_offset, + V=V + v_offset, + DOut=Grad + grad_offset, + DQ=DQ + q_offset, + DK=DK + k_offset, + DV=DV + v_offset, + stride_qm=stride_qm, + stride_kn=stride_kn, + stride_vn=stride_vn, + stride_dom=stride_dom, + stride_dqm=stride_qm, + stride_dkn=stride_kn, + stride_dvn=stride_vn, + alpha=alpha, + MAX_SEQ_LEN=MAX_SEQ_LEN, + CAUSAL=CAUSAL, + HAS_BIAS=HAS_BIAS, + BLOCK_D_Q=head_dim, + BLOCK_D_V=head_dim, + BLOCK_M=BLOCK_M, + BLOCK_N=BLOCK_N, + bias=bias + bias_offset if HAS_BIAS else bias, + ) + + +def triton_hstu_attention_fwd( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + seq_offsets: torch.Tensor, + max_seq_len: int, + alpha: float, + causal: bool, + mask: torch.Tensor, + bias: Optional[torch.Tensor] = None, +) -> torch.Tensor: + batch = seq_offsets.numel() - 1 + total_seq, head_num, head_dim = q.shape + out = torch.empty_like(v) + BLOCK_M = BLOCK_FWD + BLOCK_N = BLOCK_FWD + if total_seq == 0: + print("error") + return out + has_bias = bias is not None + core_num = get_npu_properties('num_aicore') + tasks = total_seq * head_num // BLOCK_M // 2 + grid = (core_num, 1, 1) + _hstu_attn_fwd[grid]( + q, + k, + v, + seq_offsets, + out, + q.stride(0), + q.stride(1), + k.stride(0), + k.stride(1), + v.stride(0), + v.stride(1), + out.stride(0), + out.stride(1), + alpha, + batch, + head_num, + max_seq_len, + head_dim, + causal, + has_bias, + core_num, + tasks, + BLOCK_M, + BLOCK_N, + mask, + bias, + ) + return out + + +def triton_hstu_attention_bwd( + grad: torch.Tensor, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + seq_offsets: torch.Tensor, + max_seq_len: int, + alpha: float, + causal: bool, + bias: Optional[torch.Tensor] = None, +) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + dq = torch.zeros_like(q) + dk = torch.zeros_like(k) + dv = torch.zeros_like(v) + if grad.shape[0] == 0: + return dq, dk, dv + batch = seq_offsets.numel() - 1 + _, head_num, head_dim = q.shape + has_bias = bias is not None + grid = ( + batch * head_num, + 1, + ) + _hstu_attn_bwd[grid]( + q, + k, + v, + grad, + dq, + dk, + dv, + q.stride(0), + q.stride(1), + k.stride(0), + k.stride(1), + v.stride(0), + v.stride(1), + grad.stride(0), + grad.stride(1), + seq_offsets, + alpha, + batch, + head_num, + max_seq_len, + head_dim, + causal, + has_bias, + BLOCK_BWD, + BLOCK_BWD, + bias, + ) + return dq, dk, dv + + +def jagged_data_gen(batch_size, max_seq_len, num_heads, attention_dim, dataType) -> JaggedData: + seq_array = np.arange(256, max_seq_len + 1, 256) + seq_lens = np.random.choice(seq_array, size=batch_size) + if not np.isin(max_seq_len, seq_lens): + seq_lens[np.random.randint(0, batch_size)] = max_seq_len + seq_lens_tensor = torch.from_numpy(seq_lens).to(dtype=torch.int64) + seq_offset = torch.concat(( + torch.zeros((1, ), dtype=torch.int64, device=seq_lens_tensor.device), + torch.cumsum(seq_lens_tensor, axis=0), + )).numpy() + max_seq_len = np.max(seq_lens) + total_seqs = np.sum(seq_lens) + grad = torch.rand((int(total_seqs), num_heads, attention_dim), dtype=dataType) + q = torch.rand((int(total_seqs), num_heads, attention_dim), dtype=dataType) + k = torch.rand((int(total_seqs), num_heads, attention_dim), dtype=dataType) + v = torch.rand((int(total_seqs), num_heads, attention_dim), dtype=dataType) + + bias = torch.empty(batch_size, num_heads, max_seq_len, max_seq_len, dtype=dataType).uniform_(-1, 1) + mask = 1 - torch.triu(torch.ones(batch_size, num_heads, max_seq_len, max_seq_len), diagonal=1).to(torch.float32) + return JaggedData( + grad=grad, + q=q, + k=k, + v=v, + bias=bias, + mask=mask, + max_seq_len=max_seq_len, + seq_offset=seq_offset, + ) + + +def dense_to_jagged(q, dense_tensor, seq_lens): + tensor = torch.zeros_like(q) + offset = 0 + for batch_id, seq_len in enumerate(seq_lens): + tensor[offset:offset + seq_len, :, :] = dense_tensor[batch_id, 0:seq_len, :, :] + offset = offset + seq_len + return tensor + + +def jagged_to_dense(jagged_tensor, seq_lens, head_nums, atten_dim): + need_pad_seq = [] + offset = 0 + for _, seq_len in enumerate(seq_lens): + src_tensor = jagged_tensor[offset:offset + seq_len, :, :].reshape(seq_len, head_nums, atten_dim) + need_pad_seq.append(src_tensor) + offset = offset + seq_len + + dense_tensor = torch.nn.utils.rnn.pad_sequence(need_pad_seq, batch_first=True) + return dense_tensor + + +def gloden_fwd(q, k, v, mask, alpha, seq_offset, attnBias, max_seq_len, enable_mask, enableBias, dataType): + head_nums = q.shape[1] + head_dim = q.shape[2] + batch_size = attnBias.shape[0] + seq_lens = np.zeros((batch_size, )).astype(np.int64) + for batch_id in range(batch_size): + seq_lens[batch_id] = seq_offset[batch_id + 1] - seq_offset[batch_id] + q_dens = jagged_to_dense(q, seq_lens, head_nums, head_dim).to(dataType) + k_dens = jagged_to_dense(k, seq_lens, head_nums, head_dim).to(dataType) + v_dens = jagged_to_dense(v, seq_lens, head_nums, head_dim).to(dataType) + q_dens = q_dens.permute(0, 2, 1, 3) + k_dens = k_dens.permute(0, 2, 3, 1) + v_dens = v_dens.permute(0, 2, 1, 3) + + qk_attn = torch.matmul(q_dens, k_dens) * alpha + qk_attn = qk_attn.to(torch.float32) + attnBias = attnBias.to(torch.float32) + mask = mask.to(torch.float32) + if enableBias: + qk_attn = qk_attn + attnBias + silu = F.silu(qk_attn) * (1 / max_seq_len) + if enable_mask: + silu = silu * mask + silu = silu.to(dataType) + atten_output = torch.matmul(silu, v_dens) + + atten_output = atten_output.permute(0, 2, 1, 3) + atten_output = dense_to_jagged(q, atten_output, seq_lens) + return atten_output.to(dataType) + + +def run_fwd_case(batch_size, max_seq_len, num_heads, attention_dim, data_type): + alpha = 1 + jagged_data = jagged_data_gen(batch_size, max_seq_len, num_heads, attention_dim, data_type) + # golden 输出 + golden_output = gloden_fwd( + jagged_data.q, + jagged_data.k, + jagged_data.v, + jagged_data.mask, + alpha, + jagged_data.seq_offset, + jagged_data.bias, + jagged_data.max_seq_len, + True, + False, + data_type, + ) + # triton 输出 + seq_offsets = torch.tensor(jagged_data.seq_offset, dtype=torch.int64, device=DEVICE) + triton_output = triton_hstu_attention_fwd( + q=jagged_data.q.npu(), + k=jagged_data.k.npu(), + v=jagged_data.v.npu(), + seq_offsets=seq_offsets, + max_seq_len=int(jagged_data.max_seq_len), + alpha=alpha, + causal=True, + mask=jagged_data.mask.npu(), + ) + loss = 1e-4 + if data_type == torch.float16: + loss = 1e-3 + elif data_type == torch.bfloat16: + loss = 1e-2 + torch.testing.assert_close(triton_output.cpu(), golden_output.cpu(), atol=loss, rtol=loss) + + +def golden_bwd(grad, q, k, v, bias, mask, max_seq_len, seq_offset, enable_mask, silu_scale, enable_bias, data_type): + + def jagged_to_dense_bwd(jagged_tensor, seq_lens, max_seq_len, head_num, head_dim): + batch_size = len(seq_lens) + dense_tensor = torch.zeros(batch_size, max_seq_len, head_num, head_dim, dtype=jagged_tensor.dtype) + + offset = 0 + for batch_id, seq_len in enumerate(seq_lens): + dense_tensor[batch_id, :seq_len, :, :] = jagged_tensor[offset:offset + seq_len, :, :] + offset = offset + seq_len + + return dense_tensor + + def dense_to_jagged_bwd(jagged_tensor, dense_tensor, seq_lens): + tensor = torch.zeros_like(jagged_tensor) + + offset = 0 + for batch_id, seq_len in enumerate(seq_lens): + tensor[offset:offset + seq_len, :, :] = dense_tensor[batch_id, 0:seq_len, :, :] + offset = offset + seq_len + + return tensor + + q = q.cpu() + k = k.cpu() + v = v.cpu() + grad = grad.cpu() + head_nums = grad.shape[1] + head_dim = grad.shape[2] + batch_size = bias.shape[0] + seq_lens = np.zeros((batch_size, )).astype(np.int64) + for batch_id in range(batch_size): + seq_lens[batch_id] = seq_offset[batch_id + 1] - seq_offset[batch_id] + grad_dens = jagged_to_dense_bwd(grad, seq_lens, max_seq_len, head_nums, head_dim).to(data_type) + q_dens = jagged_to_dense_bwd(q, seq_lens, max_seq_len, head_nums, head_dim).to(data_type) + k_dens = jagged_to_dense_bwd(k, seq_lens, max_seq_len, head_nums, head_dim).to(data_type) + v_dens = jagged_to_dense_bwd(v, seq_lens, max_seq_len, head_nums, head_dim).to(data_type) + actual_seq_lens = torch.from_numpy(seq_lens).reshape(batch_size, 1, 1, 1).to(data_type) + actual_seq_lens = torch.broadcast_to(actual_seq_lens, bias.shape) + qk = torch.matmul(q_dens.permute(0, 2, 1, 3), k_dens.permute(0, 2, 3, 1)) + gv = torch.matmul(grad_dens.permute(0, 2, 1, 3), v_dens.permute(0, 2, 3, 1)) + qk = qk.float() + gv = gv.float() + bias = bias.float() + if enable_mask: + mask = mask.to(data_type) + mask = mask.float() + if enable_bias: + bias = bias.to(data_type) + bias = bias.float() + qkb = qk + bias + else: + qkb = qk + real_silu_scale = 1 / max_seq_len if silu_scale == 0.0 else silu_scale + + if enable_mask: + score = F.silu(qkb) * real_silu_scale * mask + else: + score = F.silu(qkb) * real_silu_scale + score = score.to(data_type) + v_grad_dens = torch.matmul(score.permute(0, 1, 3, 2), grad_dens.permute(0, 2, 1, 3)).permute(0, 2, 1, 3) + if enable_mask: + bias_grad = gv * real_silu_scale * mask * F.sigmoid(qkb) * (1 + qkb * (1 - F.sigmoid(qkb))) + else: + bias_grad = gv * real_silu_scale * F.sigmoid(qkb) * (1 + qkb * (1 - F.sigmoid(qkb))) + bias_grad = bias_grad.to(data_type) + k_grad_dens = torch.matmul(bias_grad.permute(0, 1, 3, 2), q_dens.permute(0, 2, 1, 3)).permute(0, 2, 1, 3) + q_grad_dens = torch.matmul(bias_grad, k_dens.permute(0, 2, 1, 3)).permute(0, 2, 1, 3) + bias_grad = bias_grad.cpu() + q_grad_dens = q_grad_dens.cpu() + q_grad = dense_to_jagged_bwd(q, q_grad_dens, seq_lens) + k_grad_dens = k_grad_dens.cpu() + k_grad = dense_to_jagged_bwd(k, k_grad_dens, seq_lens) + v_grad_dens = v_grad_dens.cpu() + v_grad = dense_to_jagged_bwd(v, v_grad_dens, seq_lens) + torch.npu.synchronize() + return q_grad, k_grad, v_grad, bias_grad + + +def run_bwd_case(batch_size, max_seq_len, num_heads, attention_dim, data_type): + alpha = 1 + jagged_data = jagged_data_gen(batch_size, max_seq_len, num_heads, attention_dim, data_type) + # golden 输出 + q_grad_golden, k_grad_golden, v_grad_golden, _ = golden_bwd( + jagged_data.grad, + jagged_data.q, + jagged_data.k, + jagged_data.v, + jagged_data.bias, + jagged_data.mask, + jagged_data.max_seq_len, + jagged_data.seq_offset, + True, + 0, + False, + data_type, + ) + + # triton 输出 + seq_offsets = torch.tensor(jagged_data.seq_offset, dtype=torch.int64, device=DEVICE) + dq, dk, dv = triton_hstu_attention_bwd( + grad=jagged_data.grad.npu(), + q=jagged_data.q.npu(), + k=jagged_data.k.npu(), + v=jagged_data.v.npu(), + seq_offsets=seq_offsets, + max_seq_len=int(jagged_data.max_seq_len), + alpha=alpha, + causal=True, + ) + loss = 1e-4 + if data_type == torch.float16: + loss = 1e-3 + elif data_type == torch.bfloat16: + loss = 1e-2 + torch.testing.assert_close(dq.cpu(), q_grad_golden.cpu(), atol=loss, rtol=loss) + torch.testing.assert_close(dk.cpu(), k_grad_golden.cpu(), atol=loss, rtol=loss) + torch.testing.assert_close(dv.cpu(), v_grad_golden.cpu(), atol=loss, rtol=loss) + + +@pytest.mark.parametrize( + "batch_size, max_seq_len, num_heads, attention_dim, data_type", + [ + (2, 1024, 2, 32, torch.float32), + ], +) +def test_hstu_attention_fwd(batch_size, max_seq_len, num_heads, attention_dim, data_type): + np.random.seed(0) + torch.manual_seed(0) + run_fwd_case(batch_size, max_seq_len, num_heads, attention_dim, data_type) + + +@pytest.mark.parametrize( + "batch_size, max_seq_len, num_heads, attention_dim, data_type", + [ + (2, 1024, 2, 32, torch.float32), + ], +) +def test_hstu_attention_bwd(batch_size, max_seq_len, num_heads, attention_dim, data_type): + np.random.seed(0) + torch.manual_seed(0) + run_bwd_case(batch_size, max_seq_len, num_heads, attention_dim, data_type) diff --git a/third_party/ascend/unittest/pytest_ut/test_13_matrix_multiplication_optimized.py b/third_party/ascend/unittest/pytest_ut/test_13_matrix_multiplication_optimized.py new file mode 100644 index 0000000000..27df8cf162 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_13_matrix_multiplication_optimized.py @@ -0,0 +1,216 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import triton.runtime.driver as driver +import triton.language.extra.cann.extension as extension + + +# get device properties of npu +def get_npu_properties(): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device) + + +@triton.autotune( + configs=[ + triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 4}), + triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 5}), + triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 6}), + triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 7}), + triton.Config({"BLOCK_M": 128, "BLOCK_N": 256, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 8}), + triton.Config({"BLOCK_M": 256, "BLOCK_N": 128, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 4}), + triton.Config({"BLOCK_M": 256, "BLOCK_N": 128, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 5}), + triton.Config({"BLOCK_M": 256, "BLOCK_N": 128, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 6}), + triton.Config({"BLOCK_M": 256, "BLOCK_N": 128, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 7}), + triton.Config({"BLOCK_M": 256, "BLOCK_N": 128, "BLOCK_K": 256, "BLOCK_TRESHHOLD": 8}), + ], key=["M", "N", "K"]) +@triton.jit +def matmul_kernel( + mat_a, + mat_b, + mat_c, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, + num_cores: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, + BLOCK_TRESHHOLD: tl.constexpr, +): + pid = tl.program_id(axis=0) + task_m_idx = 0 + task_n_idx = 0 + ''' + 水平分核方式每个任务块编号如下 + [0, 1, 2, 3, 4, 5, 6, 7] + [8, 9, 10, 11, 12, 13, 14, 15] + [16, 17, 18, 19, 20, 21, 22, 23] + [24, 25, 26, 27, 28, 29, 30, 31] + [32, 33, 34, 35, 36, 37, 38, 39] + [40, 41, 42, 43, 44, 45, 46, 47] + [48, 49, 50, 51, 52, 53, 54, 55] + [56, 57, 58, 59, 60, 61, 62, 63] + 0核处理 0 20 40 60 4块任务 + 1核处理 1 21 41 61 4块任务 + 2核处理 2 22 42 62 4块任务 + ... + 19核处理 19 39 59 3块任务 + + 大shape下如果使用传统水平分核方式,会有如下问题 + 1:同一时间大量核心需要访问同一块左矩阵内存,产生Bank冲突,导致硬件访问效率降低 + 2:当完成一整行mat_c运算时,已经将所有右矩阵数据全部使用上,右矩阵较大时会超过L2Cache的容量上限, + 从而导致L2Cache的搬入及换出,此后每行运算都会或多或少产生CacheMiss,导致L2Cche命中率较低,影响 + 算子执行效率 + 此处使用8 * 8对角线分核方式可以按8 * 8的方块沿对角线方向分核计算,可以很大程度优化上面两点。 + + 此处以8*8对角线分核为例,实际以BLOCK_TRESHHOLD为tune参数选择最优的阈值 + 8 * 8 对角线分核方式中,每8 * 8分格内任务块编号如下 + [0, 8, 16, 24, 32, 40, 48, 56] + [57, 1, 9, 17, 25, 33, 41, 49] + [50, 58, 2, 10, 18, 26, 34, 42] + [43, 51, 59, 3, 11, 19, 27, 35] + [36, 44, 52, 60, 4, 12, 20, 28] + [29, 37, 45, 53, 61, 5, 13, 21] + [22, 30, 38, 46, 54, 62, 6, 14] + [15, 23, 31, 39, 47, 55, 63, 7] + + M轴方向超过8个基本块时,使用对角线分核可以明显减小Bank冲突 + 当右矩阵大小超过L2Cache大小时,采取对角线分核可以提升L2Cache利用率 + 所以当矩阵在M和N方向均超过8块时使能对角线分核即可有优化,当右矩阵大小超过L2Cache大小时优化效果尤为明显 + ''' + NUM_BLOCKS_M = triton.cdiv(M, BLOCK_M) + NUM_BLOCKS_N = triton.cdiv(N, BLOCK_N) + NUM_BLOCKS = NUM_BLOCKS_M * NUM_BLOCKS_N + # 当任务量较多时,可以使能对角线分核策略进行优化 + if NUM_BLOCKS_M >= BLOCK_TRESHHOLD and NUM_BLOCKS_N >= BLOCK_TRESHHOLD: + for block_idx in range(pid, NUM_BLOCKS, num_cores): + # 8 * 8 对角线分核代码实现 + curThresholdM = BLOCK_TRESHHOLD if block_idx < ( + NUM_BLOCKS_M // BLOCK_TRESHHOLD * BLOCK_TRESHHOLD) * NUM_BLOCKS_N else NUM_BLOCKS_M % BLOCK_TRESHHOLD + curThresholdM_thresholdN = curThresholdM * BLOCK_TRESHHOLD + curThresholdN = BLOCK_TRESHHOLD if block_idx % (NUM_BLOCKS_N * BLOCK_TRESHHOLD) < ( + curThresholdM * + NUM_BLOCKS_N) // curThresholdM_thresholdN * curThresholdM_thresholdN else NUM_BLOCKS_N % BLOCK_TRESHHOLD + localRelativeBlock = block_idx % (BLOCK_TRESHHOLD * NUM_BLOCKS_N) % (BLOCK_TRESHHOLD * curThresholdM) + task_m_idx = localRelativeBlock % curThresholdM + block_idx // (BLOCK_TRESHHOLD * + NUM_BLOCKS_N) * BLOCK_TRESHHOLD + # 求最小公倍数,方便求基本块的坐标 + x, y = curThresholdM, curThresholdN if curThresholdM > curThresholdN else curThresholdN, curThresholdM + while y != 0: + x, y = y, x % y + lcm = curThresholdM * curThresholdN // x + task_n_idx = (localRelativeBlock + (localRelativeBlock // lcm)) % curThresholdN + block_idx % ( + BLOCK_TRESHHOLD * NUM_BLOCKS_N) // curThresholdM_thresholdN * BLOCK_TRESHHOLD + + m_start = task_m_idx * BLOCK_M + n_start = task_n_idx * BLOCK_N + + mat_c_block = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + for k_start in range(0, K, BLOCK_K): + mat_a_offset = ( + (m_start + tl.arange(0, BLOCK_M)) * K)[:, None] + (k_start + tl.arange(0, BLOCK_K))[None, :] + mat_a_mask = ((m_start + tl.arange(0, BLOCK_M)) < M)[:, None] & ( + (k_start + tl.arange(0, BLOCK_K)) < K)[None, :] + mat_a_block = tl.load(mat_a + mat_a_offset, mask=mat_a_mask, other=0.0) + extension.compile_hint(mat_a_block, "dot_pad_only_k") + mat_b_offset = ( + (k_start + tl.arange(0, BLOCK_K)) * N)[:, None] + (n_start + tl.arange(0, BLOCK_N))[None, :] + mat_b_mask = ((k_start + tl.arange(0, BLOCK_K)) < K)[:, None] & ( + (n_start + tl.arange(0, BLOCK_N)) < N)[None, :] + mat_b_block = tl.load(mat_b + mat_b_offset, mask=mat_b_mask, other=0.0) + extension.compile_hint(mat_b_block, "dot_pad_only_k") + mat_c_block = tl.dot(mat_a_block, mat_b_block, mat_c_block) + mat_c_offset = ((m_start + tl.arange(0, BLOCK_M)) * N)[:, None] + (n_start + tl.arange(0, BLOCK_N))[None, :] + mat_c_mask = ((m_start + tl.arange(0, BLOCK_M)) < M)[:, None] & ( + (n_start + tl.arange(0, BLOCK_N)) < N)[None, :] + tl.store(mat_c + mat_c_offset, mat_c_block.to(tl.bfloat16), mask=mat_c_mask) + else: + # 传统顺序分核 + for block_idx in range(pid, NUM_BLOCKS, num_cores): + task_m_idx = block_idx // NUM_BLOCKS_N + task_n_idx = block_idx % NUM_BLOCKS_N + m_start = task_m_idx * BLOCK_M + n_start = task_n_idx * BLOCK_N + + mat_c_block = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + for k_start in range(0, K, BLOCK_K): + mat_a_offset = ( + (m_start + tl.arange(0, BLOCK_M)) * K)[:, None] + (k_start + tl.arange(0, BLOCK_K))[None, :] + mat_a_mask = ((m_start + tl.arange(0, BLOCK_M)) < M)[:, None] & ( + (k_start + tl.arange(0, BLOCK_K)) < K)[None, :] + mat_a_block = tl.load(mat_a + mat_a_offset, mask=mat_a_mask, other=0.0) + extension.compile_hint(mat_a_block, "dot_pad_only_k") + mat_b_offset = ( + (k_start + tl.arange(0, BLOCK_K)) * N)[:, None] + (n_start + tl.arange(0, BLOCK_N))[None, :] + mat_b_mask = ((k_start + tl.arange(0, BLOCK_K)) < K)[:, None] & ( + (n_start + tl.arange(0, BLOCK_N)) < N)[None, :] + mat_b_block = tl.load(mat_b + mat_b_offset, mask=mat_b_mask, other=0.0) + extension.compile_hint(mat_b_block, "dot_pad_only_k") + mat_c_block = tl.dot(mat_a_block, mat_b_block, mat_c_block) + mat_c_offset = ((m_start + tl.arange(0, BLOCK_M)) * N)[:, None] + (n_start + tl.arange(0, BLOCK_N))[None, :] + mat_c_mask = ((m_start + tl.arange(0, BLOCK_M)) < M)[:, None] & ( + (n_start + tl.arange(0, BLOCK_N)) < N)[None, :] + tl.store(mat_c + mat_c_offset, mat_c_block.to(tl.bfloat16), mask=mat_c_mask) + + +def triton_matmul( + mat_a, + mat_b, +): + m = mat_a.shape[0] + k = mat_a.shape[1] + n = mat_b.shape[1] + mat_c = torch.empty(m, n, dtype=mat_a.dtype, device=mat_a.device) + ''' + NPU芯片更加亲和512B对齐场景,如下分块通用性能较好,可以使用autotune选取最优 + BLOCK_M = 128 + BLOCK_N = 256 + BLOCK_K = 256 + ''' + + num_cores = get_npu_properties()["num_aicore"] + + matmul_kernel[(num_cores, )](mat_a, mat_b, mat_c, m, n, k, num_cores) + return mat_c + + +# ==================== Pytest Test ==================== +def test_matmul_extension(): + M = 2048 + K = 7168 + N = 16384 + + mat_a = torch.randn([M, K], dtype=torch.bfloat16, device="npu") + mat_b = torch.randn([K, N], dtype=torch.bfloat16, device="npu") + + result = triton_matmul(mat_a, mat_b) + golden = torch.matmul(mat_a, mat_b) + + mask = golden.abs() < 1.0 + tmpatol = tmprtol = 2**-6 + + torch.testing.assert_close(result[mask], golden[mask], atol=tmpatol, rtol=0) + torch.testing.assert_close(result[~mask], golden[~mask], atol=0, rtol=tmprtol) diff --git a/third_party/ascend/unittest/pytest_ut/test_14_accuracy_comparison.py b/third_party/ascend/unittest/pytest_ut/test_14_accuracy_comparison.py new file mode 100644 index 0000000000..bb7720ec4a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_14_accuracy_comparison.py @@ -0,0 +1,144 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + + +def run_add(x0, x1): + """ + 测试 Triton 实现的向量加法与 PyTorch 的结果,精度比对是否一致。 + + 步骤: + 1. 使用 PyTorch 计算参考结果(torch_ref) + 2. 使用 Triton 编写 kernel 并计算结果(triton_cal) + 3. 调用 accuracy_comparison 进行精度比对 + """ + + # 1. 使用 PyTorch 作为参考实现(golden truth) + def torch_func(x0, x1): + res = x0 + x1 + return res + + # 2. 定义 Triton kernel(在 NPU/GPU 上执行) + @triton.jit + def triton_kernel_add(out_ptr0, # 输出指针:结果存储位置 + in_ptr0, # 输入指针0:x0 的起始地址 + in_ptr1, # 输入指针1:x1 的起始地址 + XS: tl.constexpr # constexpr 参数:向量长度,在编译时确定 + ): + # 生成 [0, 1, 2, ..., XS-1] 的索引数组 + idx = tl.arange(0, XS) + # 从 in_ptr0 + idx 处加载 x0 的值 + tmp0 = tl.load(in_ptr0 + idx) + # 从 in_ptr1 + idx 处加载 x1 的值 + tmp1 = tl.load(in_ptr1 + idx) + # 执行加法 + tmp2 = tmp0 + tmp1 + # 将结果写入 out_ptr0 + idx + tl.store(out_ptr0 + idx, tmp2) + + # 3. Triton 封装函数:调用 kernel 并返回结果 + def triton_func(x0, x1): + y0 = torch.empty_like(x0) # 创建与输入形状、dtype 相同的输出张量 + # 启动 kernel:grid = [1, 1, 1] 表示仅使用一个 block + # 注意:XS 必须作为参数传入,因为它是 tl.constexpr 类型 + triton_kernel_add[1, 1, 1](y0, x0, x1, XS=x0.numel()) + return y0 + + # 4. 获取参考结果和 Triton 计算结果 + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1) + + # 5. 精度比对 + accuracy_comparison(triton_cal, torch_ref) + + # 6. 打印成功信息 + print( + f"== dtype:{triton_cal.dtype} == The accuracy comparison between triton_result and torch_result was successful." + ) + + +def accuracy_comparison(y_cal, y_ref): + """ + 精度比对函数:根据数据类型选择合适的比对策略。 + + 不同数据类型的处理策略: + - 浮点类型(float16/32, bfloat16):使用 torch.testing.assert_close,设置相对/绝对误差容限 + - 整数类型(int8/16/32/64):要求完全相等(torch.equal) + - 布尔类型(bool):CPU 上严格比较(避免设备差异) + """ + # 检查输出数据类型是否一致 + assert y_cal.dtype == y_ref.dtype, f"dtype mismatch: {y_cal.dtype} vs {y_ref.dtype}" + tensor_dtype = y_cal.dtype + + # 将张量移动到 NPU(假设测试在 NPU 上进行) + y_cal = y_cal.npu() + y_ref = y_ref.npu() + + # 根据数据类型选择不同的比对方式 + if tensor_dtype == torch.float16: + # float16 精度较低,允许稍大误差 + torch.testing.assert_close(y_ref, y_cal, rtol=1e-3, atol=1e-3, equal_nan=True) + elif tensor_dtype == torch.bfloat16: + # bfloat16 精度更低,建议转为 float32 再比较 + torch.testing.assert_close(y_ref.to(torch.float32), y_cal.to(torch.float32), rtol=1e-3, atol=1e-3, + equal_nan=True) + elif tensor_dtype == torch.float32: + # float32 精度较高,使用更严格的容差 + torch.testing.assert_close(y_ref, y_cal, rtol=1e-4, atol=1e-4, equal_nan=True) + elif tensor_dtype in [torch.int64, torch.int32, torch.int16, torch.int8, torch.uint32]: + # 整数类型应完全相等 + assert torch.equal(y_cal, y_ref), f"Integer tensors are not equal for dtype {tensor_dtype}" + elif tensor_dtype == torch.bool: + # 布尔类型建议在 CPU 上比较,避免设备间布尔表示差异 + assert torch.equal(y_cal.cpu(), y_ref.cpu()), "Boolean tensors are not equal" + else: + raise ValueError(f'Invalid or unsupported tensor dtype: {tensor_dtype}') + + +# ==================== Pytest Test ==================== +@pytest.mark.parametrize("dtype_name, dtype, low, high", [ + ("fp32", torch.float32, 0, 1), + ("fp16", torch.float16, 0, 1), + ("bf16", torch.bfloat16, 0, 1), + ("i64", torch.int64, 1, 100), + ("i32", torch.int32, 1, 100), + ("i16", torch.int16, 1, 100), + ("i8", torch.int8, 1, 100), + ("i1", torch.bool, 0, 2), +]) +def test_all_dtypes(dtype_name, dtype, low, high): + N = 1024 + if dtype == torch.bool: + x0 = torch.randint(low=low, high=high, size=(N, )).bool().npu() + x1 = torch.randint(low=low, high=high, size=(N, )).bool().npu() + elif dtype.is_floating_point: + x0 = torch.rand((N, ), dtype=dtype).npu() + x1 = torch.rand((N, ), dtype=dtype).npu() + else: + x0 = torch.randint(low=low, high=high, size=(N, ), dtype=dtype).npu() + x1 = torch.randint(low=low, high=high, size=(N, ), dtype=dtype).npu() + + print(f"Running test for {dtype_name}...") + run_add(x0, x1) diff --git a/third_party/ascend/unittest/pytest_ut/test_15_demo_autotune.py b/third_party/ascend/unittest/pytest_ut/test_15_demo_autotune.py new file mode 100644 index 0000000000..a63a0b7ec3 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_15_demo_autotune.py @@ -0,0 +1,88 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Autotune +============= +""" +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + + +# Return a set of different kernel configurations for autotune +def get_autotune_config(): + return [ + triton.Config({'XS': 1 * 128, 'multibuffer': True}), + triton.Config({'XS': 12 * 1024, 'multibuffer': True}), + triton.Config({'XS': 12 * 1024, 'multibuffer': False}), + triton.Config({'XS': 8 * 1024, 'multibuffer': True}), + ] + + +# Use @autotune decorator to automatically select the best kernel configuration +@triton.autotune( + configs=get_autotune_config(), + key=["numel"], +) +@triton.jit +def triton_calc_kernel(out_ptr0, in_ptr0, in_ptr1, numel, + XS: tl.constexpr # Block size controlling how many elements each thread block processes + ): + pid = tl.program_id(0) + idx = pid * XS + tl.arange(0, XS) + msk = idx < numel + for i in range(10000): + tmp0 = tl.load(in_ptr0 + idx, mask=msk, other=0.0) + tmp1 = tl.load(in_ptr1 + idx, mask=msk, other=0.0) + tmp2 = tl.math.exp(tmp0) + tmp1 + i + tl.store(out_ptr0 + idx, tmp2, mask=msk) + + +# Function to call the Triton kernel with autotuned configuration +def triton_calc_func(x0, x1): + n = x0.numel() + y0 = torch.empty_like(x0) + + def grid(meta): + return (triton.cdiv(n, meta["XS"]), 1, 1) + + triton_calc_kernel[grid](y0, x0, x1, n) + return y0 + + +# Reference implementation using PyTorch for correctness check +def torch_calc_func(x0, x1): + return torch.exp(x0) + x1 + 10000 - 1 + + +# ==================== Pytest Test ==================== +def test_triton_autotune(): + DEV = "npu" + DTYPE = torch.float32 + N = 192 * 1024 + x0 = torch.randn((N, ), dtype=DTYPE, device=DEV) + x1 = torch.randn((N, ), dtype=DTYPE, device=DEV) + + torch_ref = torch_calc_func(x0, x1) + triton_cal = triton_calc_func(x0, x1) + + torch.testing.assert_close(triton_cal, torch_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_16_profiler.py b/third_party/ascend/unittest/pytest_ut/test_16_profiler.py new file mode 100644 index 0000000000..393969cf63 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_16_profiler.py @@ -0,0 +1,116 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + + +def profiler_wrapper(fn, *args): + result_path = "./result_profiling" + skip_first = 10 + wait = 0 + warmup = 3 + active = 30 + repeat = 1 + stream = torch.npu.current_stream() + experimental_config = torch_npu.profiler._ExperimentalConfig( + aic_metrics=torch_npu.profiler.AiCMetrics.PipeUtilization, + profiler_level=torch_npu.profiler.ProfilerLevel.Level1, l2_cache=False, data_simplification=False) + with torch_npu.profiler.profile( + activities=[torch_npu.profiler.ProfilerActivity.CPU, torch_npu.profiler.ProfilerActivity.NPU], + schedule=torch_npu.profiler.schedule(wait=wait, warmup=warmup, active=active, repeat=repeat, + skip_first=skip_first), + on_trace_ready=torch_npu.profiler.tensorboard_trace_handler(result_path), record_shapes=True, + profile_memory=False, with_stack=False, with_flops=False, with_modules=False, + experimental_config=experimental_config) as prof: + stream.synchronize() + for _ in range(skip_first + (wait + warmup + active) * repeat): + fn(*args) + prof.step() + stream.synchronize() + + +@triton.jit +def triton_kernel_add(out_ptr0, in_ptr0, in_ptr1, XS: tl.constexpr): + idx = tl.arange(0, XS) + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.load(in_ptr1 + idx) + tmp2 = tmp0 + tmp1 + tl.store(out_ptr0 + idx, tmp2) + + +@triton.jit +def triton_kernel_or(out_ptr0, in_ptr0, in_ptr1, XS: tl.constexpr): + idx = tl.arange(0, XS) + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.load(in_ptr1 + idx) + tmp2 = tmp0 | tmp1 + tl.store(out_ptr0 + idx, tmp2) + + +def triton_add_func(x0, x1, N): + y0 = torch.empty_like(x0) + triton_kernel_add[1, 1, 1](y0, x0, x1, N) + return y0 + + +def triton_or_func(x0, x1, N): + y0 = torch.empty_like(x0) + triton_kernel_or[1, 1, 1](y0, x0, x1, N) + return y0 + + +# ==================== Pytest Test ==================== +@pytest.mark.parametrize("dtype, low, high", [ + (torch.float32, 0, 1), + (torch.float16, 0, 1), + (torch.bfloat16, 0, 1), + (torch.int64, 1, 100), + (torch.int32, 1, 100), + (torch.int16, 1, 100), + (torch.int8, 1, 100), + (torch.bool, 0, 2), +]) +def test_elementwise_ops(dtype, low, high): + N = 1024 + test_case_is_inductor = False + + if dtype == torch.bool: + x0 = torch.randint(low=low, high=high, size=(N, )).bool().npu() + x1 = torch.randint(low=low, high=high, size=(N, )).bool().npu() + triton_cal = triton_or_func(x0, x1, N) + ref = x0 | x1 + else: + if dtype.is_floating_point: + x0 = torch.rand((N, ), dtype=dtype).npu() + x1 = torch.rand((N, ), dtype=dtype).npu() + else: + x0 = torch.randint(low=low, high=high, size=(N, ), dtype=dtype).npu() + x1 = torch.randint(low=low, high=high, size=(N, ), dtype=dtype).npu() + + triton_cal = triton_add_func(x0, x1, N) + ref = x0 + x1 + + torch.testing.assert_close(triton_cal, ref) + + def wrapper(): + _ = triton_add_func(x0, x1, N) if dtype != torch.bool else triton_or_func(x0, x1, N) + + profiler_wrapper(wrapper) diff --git a/third_party/ascend/unittest/pytest_ut/test_17_demo_libentry.py b/third_party/ascend/unittest/pytest_ut/test_17_demo_libentry.py new file mode 100644 index 0000000000..a6172552f4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_17_demo_libentry.py @@ -0,0 +1,131 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import time + +import pytest +import torch +import torch_npu + +import triton +import triton.language as tl +from triton.runtime.libentry import libentry + +DEV = "npu" +DTYPE = torch.float32 +SEQ_LEN = 2 * 1024 +device = torch.npu.current_device() +stream = torch.npu.current_stream(device) + + +def benchmark(func): + warmup = 10 + repeat = 100 + + def wrapper(*args, **kwargs): + # + for _ in range(warmup): + result = func(*args, **kwargs) + stream.synchronize() + # + start_time = time.perf_counter_ns() + for _ in range(repeat): + result = func(*args, **kwargs) + stream.synchronize() + end_time = time.perf_counter_ns() + # + start_time = start_time * 1e-3 + end_time = end_time * 1e-3 + elapsed_time = (end_time - start_time) / repeat + return (result, elapsed_time) + + return wrapper + + +@libentry() +@triton.jit +def softmax_kernel( + output_ptr, + input_ptr, + input_row_stride, + output_row_stride, + n_rows, + n_cols, + XBLOCK: tl.constexpr, + XBLOCK_SUB: tl.constexpr, + RBLOCK: tl.constexpr, +): + pid = tl.program_id(0) + row_start = pid * XBLOCK + rblk_idx = tl.arange(0, XBLOCK_SUB) + col_idx = tl.arange(0, RBLOCK) + for row_idx in tl.range(0, XBLOCK, XBLOCK_SUB): + row_offsets = row_start + row_idx + rblk_idx[:, None] + col_offsets = col_idx[None, :] + xmask = row_offsets < n_rows + ymask = col_offsets < n_cols + mask = xmask & ymask + input_idx = row_offsets * input_row_stride + col_offsets + input_ptrs = input_ptr + input_idx + row = tl.load(input_ptrs, mask=mask, other=-float('inf')) + row_minus_max = row - tl.max(row, axis=1).reshape(XBLOCK_SUB, 1) + numerator = tl.exp(row_minus_max) + denominator = tl.sum(numerator, axis=1).reshape(XBLOCK_SUB, 1) + softmax_output = numerator / denominator + output_ptrs = output_ptr + (row_offsets * output_row_stride + col_offsets) + tl.store(output_ptrs, softmax_output, mask=mask) + + +@benchmark +def torch_func(x0: torch.Tensor): + m = torch.nn.Softmax(dim=1) + return m(x0) + + +@benchmark +def triton_func(y0: torch.Tensor, x0: torch.Tensor): + n_rows, n_cols = x0.shape + ncore = 40 + xs = (n_rows + ncore - 1) // ncore + xss = min(xs, 5) + softmax_kernel[(ncore, 1, 1)]( + y0, + x0, + x0.stride(0), + y0.stride(0), + n_rows, + n_cols, + XBLOCK=xs, + XBLOCK_SUB=xss, + RBLOCK=n_cols, + ) + return y0 + + +@pytest.mark.parametrize("batch", [1000 * x for x in range(1, 16 + 1)]) +def test_demo_libentry_softmax(batch): + torch.manual_seed(0) + x = torch.rand((batch, SEQ_LEN), dtype=DTYPE, device=DEV) + y = torch.empty_like(x) + + torch_out, _ = torch_func(x) + triton_out, _ = triton_func(y, x) + + torch.testing.assert_close(triton_out, torch_out) diff --git a/third_party/ascend/unittest/pytest_ut/test_18_gather.py b/third_party/ascend/unittest/pytest_ut/test_18_gather.py new file mode 100644 index 0000000000..f8a4c70898 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_18_gather.py @@ -0,0 +1,139 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Gather +=============== +This is an example only for npu. +""" + +import pytest +import torch +import torch_npu +import triton +import triton.runtime.driver as driver +import triton.language as tl + + +# get device properties of npu +def get_npu_properties(): + device = torch.npu.current_device() + return driver.active.utils.get_device_properties(device) + + +# a torch-version gather benchmark +def torch_gather(embeddings, idxes, default_value=0.0): + # make the result tensor + res = torch.empty((idxes.shape[0], embeddings.shape[-1]), dtype=embeddings.dtype, device=embeddings.device) + + # scatter embeddings + res[idxes >= 0] = embeddings[idxes[idxes >= 0]] + # set default values + res[idxes < 0] = default_value + + return res + + +# triton-version gather's kernel +@triton.jit +def gather_kernel(embeddings_ptr, idxes_ptr, res_ptr, rows, cols, DEFAULT_VALUE: tl.constexpr, + BIG_CORE_NUM: tl.constexpr, BIG_ROW_BLOCK_SIZE: tl.constexpr, COL_BLOCK_SIZE: tl.constexpr, + COL_BLOCK_SIZE_SUB: tl.constexpr): + SMALL_ROW_BLOCK_SIZE = BIG_ROW_BLOCK_SIZE - 1 + + embedding_dtype = embeddings_ptr.type.element_ty + default_value = tl.cast(DEFAULT_VALUE, dtype=embedding_dtype) + default_embedding = tl.full((COL_BLOCK_SIZE_SUB, ), default_value, dtype=embedding_dtype) + + core_idx = tl.program_id(0) + # compute the the size and start index of block + row_block_size = BIG_ROW_BLOCK_SIZE if (core_idx < BIG_CORE_NUM) else SMALL_ROW_BLOCK_SIZE + row_start_idx = (core_idx * BIG_ROW_BLOCK_SIZE) if (core_idx < BIG_CORE_NUM) else ( + BIG_CORE_NUM * BIG_ROW_BLOCK_SIZE + (core_idx - BIG_CORE_NUM) * SMALL_ROW_BLOCK_SIZE) + + # process blocks witn shape (row_block_size, COL_BLOCK_SIZE_SUB) one by one + for col_idx in tl.range(0, COL_BLOCK_SIZE, COL_BLOCK_SIZE_SUB): + emb_col_offsets = col_idx + tl.arange(0, COL_BLOCK_SIZE_SUB) + emb_col_mask = emb_col_offsets < cols + + for row_idx in tl.range(row_start_idx, min(row_start_idx + row_block_size, rows)): + idx_val = tl.load(idxes_ptr + row_idx) + + write_row_offset = row_idx * cols + write_emb_mask = emb_col_mask + + if idx_val >= 0: + read_row_offset = idx_val * cols + read_emb_mask = emb_col_mask + # read embedding + embedding = tl.load(embeddings_ptr + read_row_offset + emb_col_offsets, mask=read_emb_mask) + tl.store(res_ptr + write_row_offset + emb_col_offsets, embedding, write_emb_mask) + else: + # set default values + tl.store(res_ptr + write_row_offset + emb_col_offsets, default_embedding, write_emb_mask) + + +# triton-version gather's host +def triton_gather(embeddings: torch.Tensor, indices: torch.Tensor, default_value=0.0): + # constant settings for npu + USE_SIZE = 96 * 1024 + CORE_NUM = get_npu_properties()["num_vectorcore"] + + n_rows = indices.shape[0] + n_cols = embeddings.shape[1] + # make the result tensor + output = torch.empty(n_rows, n_cols, dtype=embeddings.dtype, device=embeddings.device) + + # when writing an npu kernel using triton, + # you should note that the difference between BLOCK_SIZE and BLOCK_SIZE_SUB + # BLOCK_SIZE specifies the size of data that are processed in one program + col_size_aligned = triton.cdiv(embeddings.shape[-1] * embeddings.element_size(), + 32) * 32 // embeddings.element_size() + # the data are scattered to multiple programs, which can not be even + # some process more data, some process less + big_row_block_size = triton.cdiv(n_rows, CORE_NUM) + big_core_num = CORE_NUM - ((big_row_block_size * CORE_NUM) - n_rows) + col_block_size = col_size_aligned + + # BLOCK_SIZE_SUB specifies the size of data that are processed in one loop of a program + max_col_block_size_sub = USE_SIZE // embeddings.element_size() // 2 + col_block_size_sub = min(col_size_aligned, max_col_block_size_sub) + + grid = (min(n_rows, CORE_NUM), triton.cdiv(n_cols, col_block_size)) + # launch the kernel + gather_kernel[grid](embeddings, indices, output, n_rows, n_cols, default_value, BIG_CORE_NUM=big_core_num, + BIG_ROW_BLOCK_SIZE=big_row_block_size, COL_BLOCK_SIZE=col_block_size, + COL_BLOCK_SIZE_SUB=col_block_size_sub) + + return output + + +# ==================== Pytest Test ==================== +@pytest.mark.parametrize("n_rows", [500, 1000]) +@pytest.mark.parametrize("n_cols", [16, 17, 31, 32, 63, 64, 128, 256, 819, 512, 1024, 8192, 1001, 2003, 17000]) +@pytest.mark.parametrize("index_num", [19, 123, 4321, 54321, 100, 200, 819, 500, 700, 1000]) +def test_gather(n_rows, n_cols, index_num): + indices = torch.randint(0, n_rows, (index_num, ), dtype=torch.int32).npu() + embeddings = torch.randn(n_rows, n_cols, dtype=torch.float).npu() + + expect = torch_gather(embeddings, indices).cpu() + actual = triton_gather(embeddings, indices).cpu() + torch.npu.synchronize() + + torch.testing.assert_close(actual, expect) diff --git a/third_party/ascend/unittest/pytest_ut/test_2d_permute.py b/third_party/ascend/unittest/pytest_ut/test_2d_permute.py new file mode 100644 index 0000000000..24fcee34d2 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_2d_permute.py @@ -0,0 +1,54 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl + +import torch +import torch_npu + + +def fn(x): + return x.t() + + +@triton.jit +def triton_2d_permute(output_ptr, input_ptr, X: tl.constexpr, Y: tl.constexpr): + xindex = tl.arange(0, X * Y) + input_local = tl.load(input_ptr + xindex) + output_local = input_local.reshape(X, Y).trans().reshape(X * Y) + tl.store(output_ptr + xindex, output_local) + + +@pytest.mark.parametrize('X', [32, 64, 256]) +@pytest.mark.parametrize('Y', [16, 32]) +def test_cases(X, Y): + + x = torch.randn((X, Y)).npu() + output1 = fn(x) + output2 = torch.randn(output1.shape, dtype=output1.dtype).npu() + + triton_2d_permute[1, 1, 1](output2, x, X, Y, debug=True) + print(output1) + print(output2) + + torch.testing.assert_close(output1, output2, rtol=1e-3, atol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_3Dgrid.py b/third_party/ascend/unittest/pytest_ut/test_3Dgrid.py new file mode 100644 index 0000000000..b779cee73b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_3Dgrid.py @@ -0,0 +1,91 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest + +BLOCK: tl.constexpr = 32 + + +@triton.jit +def triton_(in_ptr0, out_ptr0, x0_numel, r1_numel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr, + block_id_threshold: tl.constexpr, XBLOCK1: tl.constexpr, num_core: tl.constexpr): + RBLOCK: tl.constexpr = 64 + + block_idx = tl.program_id(0) * tl.num_programs(1) * tl.num_programs(2) + tl.program_id(1) * tl.num_programs( + 2) + tl.program_id(2) + if (block_idx < block_id_threshold): + offset = block_idx * XBLOCK + loops1 = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB # 32+23 / 24 = 2 + upper = offset + XBLOCK + else: + offset = block_id_threshold * XBLOCK + (block_idx - + block_id_threshold) * XBLOCK1 #pid=34 offset = 9*32 + (34-9)*24 = 888 + loops1 = (XBLOCK1 + XBLOCK_SUB - 1) // XBLOCK_SUB #1 + if (block_idx == num_core - 1): + upper = x0_numel + else: + upper = offset + XBLOCK1 # 912 + + base1 = tl.arange(0, XBLOCK_SUB) + base2 = tl.arange(0, RBLOCK) + loops2: tl.constexpr = (r1_numel + RBLOCK - 1) // RBLOCK + for loop1 in range(loops1): + x = offset + (loop1 * XBLOCK_SUB) + base1 + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1[None, :] + x0 = offset + (loop1 * XBLOCK_SUB) + base1[:, None] + xmask = x0 < upper + r1_prime = base2[:, None] + rindex = base2 + r1 = base2[None, :] + rmask = r1 < r1_numel + tmp0 = tl.load(in_ptr0 + (r1 + (64 * x0)), rmask & xmask, other=0.0) + + tmp1 = tl.reshape(tmp0, [XBLOCK_SUB, RBLOCK]) + tmp2_tmp = tl.sum(tmp1, 1) + tmp2 = tmp2_tmp.reshape(XBLOCK_SUB, 1) + + tl.store(out_ptr0 + (x0), tmp2, xmask) + + +guards = {"dummy": None} + + +# @pytest.mark.skip(reason="multi-process error, to be fixed.") +@pytest.mark.parametrize("size", [(1025, 64)]) +def test_3dgrid(size): + b = torch.randn((size), dtype=torch.float32).npu() + c = torch.sum(b, dim=1) + + ret = torch.randn((size[0]), dtype=torch.float32).npu() + + triton_[5, 2, 4](b, ret, size[0], size[1], XBLOCK=32, XBLOCK_SUB=16, block_id_threshold=9, XBLOCK1=24, num_core=40, + debug=True) + print(c[0:8]) + print(ret[0:8]) + torch.testing.assert_close(c, ret) + print("test 3D launch passed") + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/third_party/ascend/unittest/pytest_ut/test_abs.py b/third_party/ascend/unittest/pytest_ut/test_abs.py new file mode 100644 index 0000000000..bb5964bc9c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_abs.py @@ -0,0 +1,69 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + + +def torch_pointwise(x0): + if x0.dtype in [torch.uint8, torch.uint16, torch.uint32, torch.uint64]: + return x0 + res = torch.abs(x0) + return res + + +@triton.jit +def triton_abs(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp2 = tl.abs(tmp0) + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (2, 4096, 8), 32, 2048, 64], + # ['bfloat16', (2, 4096, 8), 32, 2048, 64], + ['float32', (2, 4096, 8), 32, 2048, 64], + ['int8', (2, 4096, 8), 32, 2048, 64], + # ['int16', (2, 4096, 8), 32, 2048, 64], + # ['int32', (2, 4096, 8), 32, 2048, 64], + # ['int64', (2, 4096, 8), 32, 2048, 64], + ['uint8', (2, 4096, 8), 32, 2048, 64], + # ['uint16', (2, 4096, 8), 32, 2048, 64], + # ['uint32', (2, 4096, 8), 32, 2048, 64], + # ['uint64', (2, 4096, 8), 32, 2048, 64], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + np_x0 = test_common.generate_numpy(shape, dtype) + x0 = torch.from_numpy(np_x0).to(eval('torch.' + dtype)).npu() + y_ref = torch_pointwise(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_abs[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_abs_2.py b/third_party/ascend/unittest/pytest_ut/test_abs_2.py new file mode 100644 index 0000000000..4f4df6c757 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_abs_2.py @@ -0,0 +1,62 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def torch_abs(x0): + res = torch.abs(x0) + return res + + +@triton.jit +def triton_abs(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.abs(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (4, 4), 4, 4, 4], + ['float32', (4, 4), 4, 4, 4], +]) +def test_abs(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype) + y_ref = torch_abs(x0) + tyname = test_common.get_triton_sig_typename(dtype) + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + x0 = x0.npu() + triton_abs[ncore, 1, 1](x0, y_cal, xblock, xblock_sub, debug=True) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_acos.py b/third_party/ascend/unittest/pytest_ut/test_acos.py new file mode 100644 index 0000000000..ed0c5bedfd --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_acos.py @@ -0,0 +1,87 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +@triton.jit +def triton_acos(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.acos(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', ['float32', 'float16', 'bfloat16']) +def test_asinh_special(param_list): + dtype = param_list + x0 = torch.linspace(-1.0 + 1e-6, 1.0 - 1e-6, 256, dtype=eval("torch." + dtype)).npu() + + y_ref = torch.acos(x0) + y_cal = torch.zeros_like(y_ref) + triton_acos[1, 1, 1](x0, y_cal, x0.shape[0], x0.shape[0]) + bf16_tolerance = 1.0 / 128 + if dtype == 'bfloat16': + torch.testing.assert_close(y_ref, y_cal, rtol=bf16_tolerance, atol=bf16_tolerance) + else: + torch.testing.assert_close(y_ref, y_cal, rtol=1e-3, atol=1e-3) + + +@triton.jit +def acos_kernel(in_ptr0, out_ptr0, n_elements, BLOCK_SIZE: tl.constexpr): + offsets = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(in_ptr0 + offsets, mask=mask) + x = x.to(tl.float32) + y = libdevice.acos(x) + tl.store(out_ptr0 + offsets, y, mask=mask) + + +@pytest.mark.acos +@pytest.mark.parametrize('shape', [(), (1, ), (1024, 1024), (20, 320, 15), (16, 128, 64, 60), (16, 7, 57, 32, 29)]) +@pytest.mark.parametrize('dtype', [torch.float16, torch.float32, torch.bfloat16]) +def test_accuracy_acos(shape, dtype): + inp = torch.randn(shape, dtype=dtype, device='npu') + ref_out = torch.acos(inp) + res_out = torch.zeros_like(ref_out) + n_elements = inp.numel() + grid = (triton.cdiv(n_elements, 1024), ) + acos_kernel[grid](inp, res_out, n_elements, BLOCK_SIZE=1024) + print("ref_out:", ref_out) + print("res_out:", res_out) + if dtype == torch.bfloat16: + bf16_tolerance = 1.0 / 128 + torch.testing.assert_close(res_out, ref_out, rtol=bf16_tolerance, atol=bf16_tolerance, equal_nan=True) + elif dtype == torch.float16: + torch.testing.assert_close(res_out, ref_out, rtol=1e-3, atol=1e-4, equal_nan=True) + else: + torch.testing.assert_close(res_out, ref_out, rtol=1e-6, atol=1e-4, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_acosh.py b/third_party/ascend/unittest/pytest_ut/test_acosh.py new file mode 100644 index 0000000000..78705b33b0 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_acosh.py @@ -0,0 +1,73 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +def torch_acosh(x0): + return torch.acosh(x0) + + +@triton.jit +def triton_acosh(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.acosh(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['bfloat16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_acosh_common(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu().abs() + 1.0 + y_ref = torch_acosh(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_acosh[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['bfloat16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_acosh_special(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = -test_common.generate_tensor(shape, dtype).npu().abs() + 1.0 + y_ref = torch_acosh(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_acosh[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_add.py b/third_party/ascend/unittest/pytest_ut/test_add.py new file mode 100644 index 0000000000..f90c850884 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_add.py @@ -0,0 +1,87 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 + x1 + return res + + +@triton.jit +def triton_add(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 + tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + ['uint8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_add[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_all_blocks_parallel(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_add[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_auto_blockify(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_add[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub, auto_blockify_size=ncore) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_address_check.py b/third_party/ascend/unittest/pytest_ut/test_address_check.py new file mode 100644 index 0000000000..cbc06b1d2b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_address_check.py @@ -0,0 +1,68 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest + + +@triton.jit +def simple_kernel(x_ptr, y_ptr, output_ptr, n_elements): + pid = tl.program_id(axis=0) + offsets = pid * 1024 + tl.arange(0, 1024) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + ret = x + y + tl.store(output_ptr + offsets, ret, mask=mask) + + +def test_npu_tensor_should_success(): + print("Test the NPU tensor. The NPU tensor should be passed and executed properly.") + + size = 1024 + x_npu = torch.rand(size, device='npu') + y_npu = torch.rand(size, device='npu') + output = torch.empty(size, device='npu') + + simple_kernel[(1, )](x_npu, y_npu, output, size) + + expected = x_npu + y_npu + actual = output + + torch.testing.assert_close(expected, actual, rtol=1e-03, atol=1e-03) + + +@pytest.mark.skip(reason="The launcher is optimized.and the CPU tensor check is removed.") +def test_cpu_tensor_should_fail(): + print("Test the CPU tensor. An address check exception should be raised.") + size = 1024 + x_cpu = torch.rand(size, device='cpu') + y_cpu = torch.rand(size, device='cpu') + output = torch.empty(size, device='npu') + + with pytest.raises(ValueError) as exc_info: + simple_kernel[(1, )](x_cpu, y_cpu, output, size) + + error_msg = str(exc_info.value) + assert "cannot be accessed from Triton (cpu tensor?)" in error_msg, \ + f"Expected error message to contain CPU tensor rejection hint, but got: {error_msg}" diff --git a/third_party/ascend/unittest/pytest_ut/test_advance.py b/third_party/ascend/unittest/pytest_ut/test_advance.py new file mode 100644 index 0000000000..d76f0f3278 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_advance.py @@ -0,0 +1,182 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, y_ptr, z_ptr, output_ptr1, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + # idx = tl.arange(0,XB*YB*ZB) + block_ptr_in = tl.make_block_ptr( + base=x_ptr, + shape=(XB, YB, ZB), + strides=(YB * ZB, ZB, 1), + offsets=(9, 6, 5), + block_shape=(XB, YB, ZB), + order=(2, 1, 0), + ) + bbptr = tl.advance(block_ptr_in, (-9, -6, -5)) + # XB,YB,1 + X = tl.load(bbptr) + # X = tl.load(x_ptr + idx) + # Y = tl.load(y_ptr + idx) + + # xx=tl.view(X,(ZB*YB,XB)) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + block_ptr_out = tl.make_block_ptr( + base=output_ptr, + shape=(XB, YB, ZB), + strides=(YB * ZB, ZB, 1), + offsets=(0, 0, 0), + block_shape=(XB, YB, ZB), + order=(2, 1, 0), + ) + tl.store(block_ptr_out, X) + # tl.store(output_ptr + tl.arange(0,ZB*YB)[:,None]*XB+xidx[None,:], xx) + # tl.store(output_ptr + xidx[:,None]*YB+yidx[None,:], yy) + + +@triton.jit +def fn_npu_2d(output_ptr, x_ptr, y_ptr, z_ptr, output_ptr1, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xoffset = tl.program_id(0) + block_ptr_in = tl.make_block_ptr( + base=x_ptr, + shape=(XB, YB), + strides=(YB, 1), + offsets=(6 + xoffset, 5), + block_shape=(XB, YB), + order=(1, 0), + ) + bbptr = tl.advance(block_ptr_in, (-6, -5)) + # XB,YB,1 + X = tl.load(bbptr) + + block_ptr_out = tl.make_block_ptr( + base=output_ptr, + shape=(XB, YB), + strides=(YB, 1), + offsets=(xoffset, 0), + block_shape=(XB, YB), + order=(1, 0), + ) + tl.store(block_ptr_out, X) + + +@triton.jit +def fn_npu_3d(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + block_ptr_in = tl.make_block_ptr( + base=x_ptr, + shape=(XB, YB, ZB), + strides=(YB * ZB, ZB, 1), + offsets=(0, 0, 0), + block_shape=(XB, YB, 2), + order=(2, 1, 0), + ) + + block_ptr_out = tl.make_block_ptr( + base=output_ptr, + shape=(XB, YB, ZB), + strides=(YB * ZB, ZB, 1), + offsets=(0, 0, 0), + block_shape=(XB, YB, 2), + order=(2, 1, 0), + ) + + for _ in range(ZB // 2): + X = tl.load(block_ptr_in, boundary_check=(0, 1, 2)) + tl.store(block_ptr_out, X, boundary_check=(0, 1, 2)) + block_ptr_in = tl.advance(block_ptr_in, (0, 0, 2)) + block_ptr_out = tl.advance(block_ptr_out, (0, 0, 2)) + + +@pytest.mark.parametrize('dtype', ["int32", "float32", "int16"]) +@pytest.mark.parametrize('shape', [(33, 9, 6), (8, 8, 4)]) +def test_advance_with_boundary_check(dtype, shape): + x = torch.randint(low=-128, high=128, size=shape, dtype=eval('torch.' + dtype)).npu() + + output = torch.randint(1, shape, dtype=eval('torch.' + dtype)).npu() + a = x + + fn_npu_3d[1, 1, 1](output, x, XB=shape[0], YB=shape[1], ZB=shape[2]) + + torch.testing.assert_close(output, a) + + +@pytest.mark.parametrize('dtype', ["int32", "float32", "int16"]) +@pytest.mark.parametrize('shape', [(1, 3), (3, 1), (1, 13), (13, 1)]) +def test_advance_supplement(dtype, shape): + x = torch.randint(low=-128, high=128, size=shape, dtype=eval('torch.' + dtype)).npu() + y = torch.randint(low=-128, high=128, size=shape, dtype=eval('torch.' + dtype)).npu() + z = torch.randint(low=-128, high=128, size=shape, dtype=eval('torch.' + dtype)).npu() + + output = torch.randint(1, shape, dtype=eval('torch.' + dtype)).npu() + output1 = output + + a = x + + fn_npu_2d[1, 1, 1](output, x, y, z, output1, XB=shape[0], YB=shape[1], ZB=1) + + torch.testing.assert_close(output, a) + + +paras = [ + ('*fp32', eval('torch.float32'), 2, 256, 16), + ('*fp32', eval('torch.float32'), 8, 8, 4), + ('*fp16', eval('torch.float16'), 2, 256, 16), + ('*fp16', eval('torch.float16'), 8, 8, 4), + ('*i8', eval('torch.int8'), 2, 256, 16), + ('*i8', eval('torch.int8'), 8, 8, 4), +] + + +@pytest.mark.parametrize('para_type,data_type,XB,YB,ZB', paras) +def test_npu(para_type, data_type, XB, YB, ZB): + + x = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + y = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + z = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + + print(f"shape = {x.shape}") + print(x.dtype) + + output = torch.randint(1, (XB, YB, ZB), dtype=data_type).npu() + output1 = output + print(f"output.dtype={output.dtype}") + + a = x + print(a) + fn_npu_[1, 1, 1](output, x, y, z, output1, XB=XB, YB=YB, ZB=ZB, debug=True) + print(output) + torch.testing.assert_close(output, a) + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/third_party/ascend/unittest/pytest_ut/test_advance_ptr.py b/third_party/ascend/unittest/pytest_ut/test_advance_ptr.py new file mode 100644 index 0000000000..f9052cd28e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_advance_ptr.py @@ -0,0 +1,51 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest + + +@triton.jit +def fn_npu_3d(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + block_ptr_in = tl.make_block_ptr(base=x_ptr, shape=(XB, YB, ZB), strides=(YB * ZB, ZB, 1), offsets=(0, 0, 0), + block_shape=(XB, YB, 2), order=(2, 1, 0)) + block_ptr_out = tl.make_block_ptr(base=output_ptr, shape=(XB, YB, ZB), strides=(YB * ZB, ZB, 1), offsets=(0, 0, 0), + block_shape=(XB, YB, 2), order=(2, 1, 0)) + pid = tl.program_id(axis=0) # pid=0,1 BLOCK_SIZE_N=8 + for _ in range(ZB // 2): + X = tl.load(block_ptr_in, boundary_check=(0, 1, 2)) + tl.store(block_ptr_out, X, boundary_check=(0, 1, 2)) + block_ptr_in = tl.advance(block_ptr_in, (0, 0, 2)) + block_ptr_out = tl.advance(block_ptr_out, (0, 0, 2)) + + +@pytest.mark.parametrize('dtype', ["int32", "float32", "int16"]) +@pytest.mark.parametrize('shape', [(33, 9, 6), (8, 8, 4)]) +def test_advance_with_boundary_check(dtype, shape): + x = torch.randint(low=-128, high=128, size=shape, dtype=eval('torch.' + dtype)).npu() + output = torch.randint(1, shape, dtype=eval('torch.' + dtype)).npu() + expected = x + output = torch.randint(1, shape, dtype=eval('torch.' + dtype)).npu() + fn_npu_3d[1, 1, 1](output, x, XB=shape[0], YB=shape[1], ZB=shape[2]) + torch.testing.assert_close(output, expected) diff --git a/third_party/ascend/unittest/pytest_ut/test_affine_map_binding.py b/third_party/ascend/unittest/pytest_ut/test_affine_map_binding.py new file mode 100644 index 0000000000..b46f389eb9 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_affine_map_binding.py @@ -0,0 +1,114 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +import triton.language.extra.cann.extension as al +import pytest + + +def test_extension_reexports_affine_bindings(): + assert al.affine_map is ascend_ir.affine_map + assert al.affine_expr is ascend_ir.affine_expr + assert al.affine_constant_expr is ascend_ir.affine_constant_expr + assert al.affine_dim_expr is ascend_ir.affine_dim_expr + assert al.affine_symbol_expr is ascend_ir.affine_symbol_expr + assert al.affine_binary_op_expr is ascend_ir.affine_binary_op_expr + assert al.AffineMap is al.affine_map + assert al.AffineExpr is al.affine_expr + assert al.AffineConstantExpr is al.affine_constant_expr + assert al.AffineDimExpr is al.affine_dim_expr + assert al.AffineSymbolExpr is al.affine_symbol_expr + assert al.AffineBinaryOpExpr is al.affine_binary_op_expr + + +def test_make_affine_map(): + with ir.context() as ctx: + ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + + d0 = ascend_ir.affine_expr.get_dim(0) + d1 = ascend_ir.affine_expr.get_dim(1) + c2 = ascend_ir.affine_expr.get_constant(2) + + expr = (d0 + c2) * d1 + assert "d0" in str(expr) and "d1" in str(expr) + assert not expr.is_pure_affine() + assert hash(expr) == hash(expr) + assert d0 == ascend_ir.affine_expr.get_dim(0) + assert c2 == ascend_ir.affine_expr.get_constant(2) + assert isinstance(c2, ascend_ir.affine_expr) + assert isinstance(d0, ascend_ir.affine_expr) + + identity_map = ascend_ir.affine_map.get_identity(2) + transpose_map = ascend_ir.affine_map.get(2, 0, [1, 0]) + transpose_map_by_expr = ascend_ir.affine_map.get(2, 0, [d1, d0]) + sum_map = ascend_ir.affine_map.get(2, 0, [d0 + d1, d1]) + const_map = ascend_ir.affine_map.get_constant(7) + minor_identity_map = ascend_ir.affine_map.get_minor_identity(3, 2) + + assert identity_map.is_identity() + assert identity_map.is_permutation() + assert identity_map.get_num_dims() == 2 + assert identity_map.get_num_symbols() == 0 + assert identity_map.get_num_results() == 2 + assert str(identity_map) == "(d0, d1) -> (d0, d1)" + + assert not transpose_map.is_identity() + assert transpose_map.is_permutation() + assert str(transpose_map) == "(d0, d1) -> (d1, d0)" + assert str(transpose_map_by_expr) == "(d0, d1) -> (d1, d0)" + assert str(sum_map) == "(d0, d1) -> (d0 + d1, d1)" + assert transpose_map.to_dict() == { + "num_dims": 2, + "num_symbols": 0, + "results": [1, 0], + } + assert str(sum_map.get_sub_map([1])) == "(d0, d1) -> (d1)" + assert str(sum_map.compose(transpose_map)) == "(d0, d1) -> (d0 + d1, d0)" + assert str(transpose_map.inverse_permutation()) == "(d0, d1) -> (d1, d0)" + assert transpose_map == transpose_map_by_expr + assert hash(transpose_map) == hash(transpose_map) + assert [str(r) for r in sum_map.get_results()] == ["d0 + d1", "d1"] + assert const_map.is_single_constant() + assert const_map.get_constant_result() == 7 + assert str(minor_identity_map) == "(d0, d1, d2) -> (d1, d2)" + + +def test_build_buffer_type_with_affine_map(): + with ir.context() as ctx: + ir.load_dialects(ctx) + ascend_ir.load_dialects(ctx) + builder = ascend_ir.ascendnpu_ir_builder(ctx) + + transpose_map = ascend_ir.affine_map.get(2, 0, [1, 0]) + ub_attr = builder.get_target_attribute(ascend_ir.AddressSpace.UB) + + buffer_ty = builder.get_buffer_ty_with_affine_map([8, 16], builder.get_float_ty(), transpose_map, ub_attr) + + assert "memref<8x16xf32" in str(buffer_ty) + assert "affine_map<(d0, d1) -> (d1, d0)>" in str(buffer_ty) + assert "ub" in str(buffer_ty) + + +if __name__ == '__main__': + test_build_buffer_type_with_affine_map() + test_extension_reexports_affine_bindings() + test_make_affine_map() diff --git a/third_party/ascend/unittest/pytest_ut/test_alloc.py b/third_party/ascend/unittest/pytest_ut/test_alloc.py new file mode 100644 index 0000000000..7b9e25c102 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_alloc.py @@ -0,0 +1,97 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os + +import pytest +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {"create_address_space": al.semantic.create_address_space}, + {}) + return str(module) + + +# ============== Kernel definitions ============== + + +@triton.jit +def allocate_local_buffer(XBLOCK: tl.constexpr): + # this statement has no effect, just to test the builder + bl.alloc(tl.float32, [XBLOCK]) + bl.alloc(tl.float32, [XBLOCK, XBLOCK], al.ascend_address_space.UB) + bl.alloc(tl.float32, [XBLOCK, XBLOCK], al.ascend_address_space.L1) + bl.alloc(tl.float32, [XBLOCK, XBLOCK], al.ascend_address_space.L0A) + bl.alloc(tl.float32, [XBLOCK, XBLOCK], al.ascend_address_space.L0B) + bl.alloc(tl.float32, [XBLOCK, XBLOCK], al.ascend_address_space.L0C) + bl.alloc(tl.float32, [XBLOCK, XBLOCK], al.ascend_address_space.UB, is_mem_unique=True) + + +@triton.jit +def allocate_to_smem_buffer(x_ptr, XBLOCK: tl.constexpr): + x_l1_keep = bl.alloc(tl.float32, [XBLOCK], al.ascend_address_space.L1) + for i in range(XBLOCK): + offsets = tl.arange(0, XBLOCK) + x = tl.load(x_ptr + offsets, mask=offsets < XBLOCK) + bl.to_buffer(tensor=x, bind_buffer=x_l1_keep) + tl.store(x_ptr + offsets, x, mask=offsets < XBLOCK) + + +def test_allocate_local_buffer(): + """Test allocating local buffers in different address spaces.""" + mlir = compile_kernel(allocate_to_smem_buffer, {"x_ptr": "*fp32"}, {"XBLOCK": 256}) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +# ============== Main for manual testing ============== + +if __name__ == "__main__": + print("=" * 60) + print("Test 1: Nested Scopes") + print("=" * 60) + mlir = compile_kernel(allocate_local_buffer, {}, {"XBLOCK": 256}) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_alloc_annotation.py b/third_party/ascend/unittest/pytest_ut/test_alloc_annotation.py new file mode 100644 index 0000000000..2c693d2d2e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_alloc_annotation.py @@ -0,0 +1,157 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Tests for the annotation bl.alloc puts on a buffer.""" +import os + +import pytest +import torch +import torch_npu # noqa: F401 +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.utils import is_compile_on_910_95 + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Compile a kernel to TTIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +def mark_attrs(mlir): + """Attribute dict of every annotation.mark in mlir.""" + return [ + line.split("{", 1)[1].split("}", 1)[0] + for line in mlir.splitlines() + if "annotation.mark" in line and "{" in line + ] + + +@triton.jit +def alloc_unused_buffer(x_ptr, y_ptr, XBLOCK: tl.constexpr): + offsets = tl.arange(0, XBLOCK) + x = tl.load(x_ptr + offsets) + bl.alloc(tl.float32, [XBLOCK], al.ascend_address_space.UB) + tl.store(y_ptr + offsets, x * 2.0) + + +@triton.jit +def alloc_unique_unused_buffer(x_ptr, y_ptr, XBLOCK: tl.constexpr): + offsets = tl.arange(0, XBLOCK) + x = tl.load(x_ptr + offsets) + bl.alloc(tl.float32, [XBLOCK], al.ascend_address_space.UB, is_mem_unique=True) + tl.store(y_ptr + offsets, x + 1.0) + + +@triton.jit +def alloc_ub_as_fixpipe_dst(a_ptr, b_ptr, y_ptr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr): + offs_m = tl.arange(0, M)[:, None] + offs_n = tl.arange(0, N)[None, :] + offs_k = tl.arange(0, K) + a = tl.load(a_ptr + offs_m * K + offs_k[None, :]) + b = tl.load(b_ptr + offs_k[:, None] * N + offs_n) + acc = tl.dot(a, b) + ub = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + al.fixpipe(acc, ub, al.FixpipeDMAMode.NZ2ND, al.FixpipeDualDstMode.NO_DUAL) + tl.store(y_ptr + offs_m * N + offs_n, bl.to_tensor(ub)) + + +@triton.jit +def alloc_unique_ub_as_fixpipe_dst(a_ptr, b_ptr, y_ptr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr): + offs_m = tl.arange(0, M)[:, None] + offs_n = tl.arange(0, N)[None, :] + offs_k = tl.arange(0, K) + a = tl.load(a_ptr + offs_m * K + offs_k[None, :]) + b = tl.load(b_ptr + offs_k[:, None] * N + offs_n) + acc = tl.dot(a, b) + ub = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB, is_mem_unique=True) + al.fixpipe(acc, ub, al.FixpipeDMAMode.NZ2ND, al.FixpipeDualDstMode.NO_DUAL) + tl.store(y_ptr + offs_m * N + offs_n, bl.to_tensor(ub)) + + +def _run_fixpipe_kernel(kernel, m, n, k): + """Run kernel on NPU, return (result, expected).""" + torch.manual_seed(0) + a = torch.randn(m, k, dtype=torch.float16, device="npu") + b = torch.randn(k, n, dtype=torch.float16, device="npu") + y = torch.zeros(m, n, dtype=torch.float32, device="npu") + + kernel[(1, )](a, b, y, M=m, N=n, K=k) + torch.npu.synchronize() + return y.cpu(), a.cpu().float() @ b.cpu().float() + + +def _check_fixpipe_result(kernel, what): + got, expected = _run_fixpipe_kernel(kernel, 64, 64, 64) + max_diff = float((got - expected).abs().max()) + assert max_diff < 0.5, f"{what} mismatch: max_abs_diff={max_diff:.3e} {got[0, :4]} != {expected[0, :4]}" + + +@pytest.mark.skipif( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason="only support in Ascend 950", +) +def test_alloc_as_fixpipe_dst_on_npu(): + _check_fixpipe_result(alloc_ub_as_fixpipe_dst, "fixpipe into bl.alloc buffer") + + +@pytest.mark.skipif( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason="only support in Ascend 950", +) +def test_unique_alloc_as_fixpipe_dst_on_npu(): + _check_fixpipe_result(alloc_unique_ub_as_fixpipe_dst, "fixpipe into mem_unique buffer") + + +def test_alloc_does_not_annotate_memory_effects(): + signature = {"x_ptr": "*fp32", "y_ptr": "*fp32"} + consts = {"XBLOCK": 1024} + + mlir = compile_kernel(alloc_unused_buffer, signature, consts) + assert not any("effects" in a for a in mark_attrs(mlir)), \ + "bl.alloc must not annotate write/read effects:\n" + mlir + + mlir = compile_kernel(alloc_unique_unused_buffer, signature, consts) + attrs = mark_attrs(mlir) + assert any("mem_unique" in a for a in attrs), "is_mem_unique must stay annotated:\n" + mlir + assert not any("effects" in a for a in attrs), \ + "bl.alloc must not annotate write/read effects:\n" + mlir diff --git a/third_party/ascend/unittest/pytest_ut/test_and.py b/third_party/ascend/unittest/pytest_ut/test_and.py new file mode 100644 index 0000000000..c9fdb44898 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_and.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_and(x0, x1): + res = x0 & x1 + return res + + +@triton.jit +def triton_and(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index) + tmp1 = tl.load(in_ptr1 + x_index) + tmp2 = tmp0 & tmp1 + tl.store(out_ptr0 + x_index, tmp2) + + +@pytest.mark.parametrize('param_list', [ + ['int32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_and(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_and(x0, x1) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_and[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_annotations.py b/third_party/ascend/unittest/pytest_ut/test_annotations.py new file mode 100644 index 0000000000..840f9010b8 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_annotations.py @@ -0,0 +1,75 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# Copyright 2018-2020 Philippe Tillet +# Copyright 2020-2022 OpenAI +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import annotations +import torch +import triton +import triton.language as tl +import pytest +from triton.backends.ascend.utils import is_compile_on_910_95 + + +def annotated_function(return_type=None, **arg_types): + """A decorator to add annotations to a function.""" + + def decorator(func): + func.__annotations__ = {**arg_types, 'return': return_type} + return func + + return decorator + + +# Test integer annotations +@pytest.mark.parametrize(("signed", "width"), [ + (signed, width) for signed in [False, True]\ + for width in [8, 16, 32, 64] +] + [(False, 1)] + ) +def test_int_annotation(signed, width, device="npu"): + if not signed and width in (16, 32, 64) and not is_compile_on_910_95( + triton.runtime.driver.active.get_current_target().arch): + pytest.skip("tl.store only supports uint16/uint32/uint64 on Ascend 950 (A5); unsupported on A2/A3.") + + @triton.jit + @annotated_function(X=torch.tensor, v=f"tl.{'' if signed else 'u'}int{width}") + def _kernel(X, v): + tl.store(X, v) + + h = _kernel[(1, )](torch.empty(1, device=device), 3) + pfx = 'si' if signed else 'ui' + assert f'%v: i{width}' in h.asm["ttir"] + assert f'arith.{pfx}tofp' in h.asm["ttir"] + + +# Test that unknown annotations do not emit an error +def test_unknown_annotation(device="npu"): + + @triton.jit + def _kernel(X: torch.Tensor, N: int, BLOCK_SIZE: tl.constexpr): + pass + + x = torch.empty(1, device=device) + _kernel[(1, )](x, x.shape[0], 32) + try: + _kernel[(1, )](x.shape[0], x.shape[0], 32) + except AttributeError: + pass diff --git a/third_party/ascend/unittest/pytest_ut/test_arange.py b/third_party/ascend/unittest/pytest_ut/test_arange.py new file mode 100644 index 0000000000..5c049bec80 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_arange.py @@ -0,0 +1,113 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import math +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import test_common + + +def torch_arange(start, end): + TRITON_MAX_TENSOR_NUMEL = 1048576 + if end < start: + raise ValueError("arange's end argument must be greater than the start argument") + if end - start > TRITON_MAX_TENSOR_NUMEL: + raise ValueError( + f"end - start must be less than or equal to TRITON_MAX_TENSOR_NUMEL = {TRITON_MAX_TENSOR_NUMEL}") + return torch.arange(start, end) + + +def torch_arange_access(start, end): + z = torch.zeros([end], dtype=torch.int32).npu() + v = torch.arange(start, end).npu() + z[start:end] = v + return z + + +@triton.jit +def triton_arange(z, BLOCK: tl.constexpr, START: tl.constexpr, END: tl.constexpr): + off = tl.arange(0, BLOCK) + val = tl.arange(START, END) + tl.store(z + off, val) + + +@triton.jit +def triton_arange_access(z, BLOCK: tl.constexpr, START: tl.constexpr, END: tl.constexpr): + off = tl.arange(START, END) + val = tl.arange(START, END) + tl.store(z + off, val) + + +@pytest.mark.parametrize('param_list', [ + [0, 128], + [7, 128], + [128, 1024], +]) +def test_case(param_list): + start, end = param_list + shape = [end - start] + block = end - start + dtype = 'int32' + + y_ref = torch_arange(start, end) + y_cal = torch.zeros(shape, dtype=torch.int32).npu() + + triton_arange[(1, )](y_cal, START=start, END=end, BLOCK=block) + + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + [0, 128], + [7, 128], + [128, 1024], +]) +def test_case_access(param_list): + start, end = param_list + shape = [end] + block = end - start + dtype = 'int32' + + y_ref = torch_arange_access(start, end) + y_cal = torch.zeros(shape, dtype=torch.int32).npu() + + triton_arange_access[(1, )](y_cal, START=start, END=end, BLOCK=block) + + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('invalid_param_list', [ + [0, 10000000], + [1024, 128], +]) +def test_arange_invalid_range(invalid_param_list): + start, end = invalid_param_list + shape = [end - start] + block = end - start + flag = False + try: + y_cal = torch.zeros(shape, dtype=torch.int32).npu() + triton_arange[(1, )](y_cal, START=start, END=end, BLOCK=block) + except Exception as e: + flag = True + assert flag diff --git a/third_party/ascend/unittest/pytest_ut/test_arch.py b/third_party/ascend/unittest/pytest_ut/test_arch.py new file mode 100644 index 0000000000..b7ab25429c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_arch.py @@ -0,0 +1,95 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import pytest +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + arch = "Ascend950" + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +@triton.jit +def copy( + A_ptr, + A1_ptr, + M: tl.constexpr, + N: tl.constexpr, +): + offs_a = tl.arange(0, M)[:, None] + offs_b = tl.arange(0, N)[None, :] + + offs_c = (offs_a) * M + (offs_b) + a_ptr = A_ptr + offs_c + a_val = tl.load(a_ptr) + a1_ptr = A1_ptr + offs_c + a1_val = tl.load(a1_ptr) + + add = tl.add(a_val, a1_val) + + add_ub = bl.to_buffer(add, al.ascend_address_space.UB) + A_l1 = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L1) + al.copy(add_ub, A_l1) + + +def test_arch(): + print("=" * 60) + print("Test 1: copy ") + print("=" * 60) + mlir = compile_kernel( + copy, + {"A_ptr": "*fp32", "A1_ptr": "*fp32"}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) + + +# ============== Main for manual testing ============== +if __name__ == "__main__": + test_arch() diff --git a/third_party/ascend/unittest/pytest_ut/test_argmax.py b/third_party/ascend/unittest/pytest_ut/test_argmax.py new file mode 100644 index 0000000000..3d84f2829d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_argmax.py @@ -0,0 +1,65 @@ +import logging +import math +import pytest +import torch +import torch_npu +import numpy as np +import triton +import triton.language as tl + +import test_common + + +def torch_argmax(x0, dim, keepdim): + x0 = x0 if x0.device == "cpu" else x0.cpu() + return torch.argmax(x0, dim=dim, keepdim=keepdim).npu() + + +@triton.jit +def triton_argmax_1d(in_ptr0, out_ptr1, xnumel, XBLOCK: tl.constexpr): + xoffset = tl.program_id(0) + tl.arange(0, XBLOCK) + tmp0 = tl.load(in_ptr0 + xoffset, None) + tmp4 = tl.argmax(tmp0, 0) + tl.store(out_ptr1, tmp4, None) + + +@pytest.mark.parametrize('shape', [(128, ), (256, ), (37, ), (741, )]) +@pytest.mark.parametrize('dtype', ['int32', 'float32', 'uint8', 'int8']) +def test_argmax_1d(dtype, shape): + x0 = test_common.generate_tensor(shape, dtype).npu() + triton_res = torch.empty(1, dtype=torch.int32).npu() + numel = shape[0] + triton_argmax_1d[(1, )](x0, triton_res, numel, numel) + torch_res = torch_argmax(x0, dim=0, keepdim=True) + test_common.validate_cmp("int32", triton_res, torch_res) + + +@triton.jit +def triton_argmax_2d(in_ptr0, out_ptr0, dim: tl.constexpr, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, + NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + mmask = mblk_idx < M + nmask = nblk_idx < N + mask = (mmask[:, None]) & (nmask[None, :]) + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + x = tl.load(in_ptr0 + idx, mask=mask, other=float('-inf')) + tmp4 = tl.argmax(x, dim) + if dim == 0: + tl.store(out_ptr0 + tl.arange(0, N), tmp4, None) + else: + tl.store(out_ptr0 + tl.arange(0, M), tmp4, None) + + +@pytest.mark.parametrize('shape', [(37, 125), (29, 4), (7, 31)]) +@pytest.mark.parametrize('dtype', ['int32', 'float32', 'uint8', 'int8']) +@pytest.mark.parametrize('dim', [0, 1]) +def test_argmax_2d(dtype, shape, dim): + shapex, shapey = shape + x0 = test_common.generate_tensor(shape, dtype).npu() + triton_res = torch.empty([ + shape[1 - dim], + ], dtype=torch.int32).npu() + triton_argmax_2d[(1, 1)](x0, triton_res, dim, shapex, shapey, shapex, shapey) + torch_res = torch_argmax(x0, dim=dim, keepdim=False) + test_common.validate_cmp("int32", triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_argmin.py b/third_party/ascend/unittest/pytest_ut/test_argmin.py new file mode 100644 index 0000000000..16be10a5f4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_argmin.py @@ -0,0 +1,65 @@ +import logging +import math +import pytest +import torch +import torch_npu +import numpy as np +import triton +import triton.language as tl + +import test_common + + +def torch_argmin(x0, dim, keepdim): + x0 = x0 if x0.device == "cpu" else x0.cpu() + return torch.argmin(x0, dim=dim, keepdim=keepdim).npu() + + +@triton.jit +def triton_argmin_1d(in_ptr0, out_ptr1, xnumel, XBLOCK: tl.constexpr): + xoffset = tl.program_id(0) + tl.arange(0, XBLOCK) + tmp0 = tl.load(in_ptr0 + xoffset, None) + tmp4 = tl.argmin(tmp0, 0) + tl.store(out_ptr1, tmp4, None) + + +@pytest.mark.parametrize('shape', [(128, ), (256, ), (37, ), (741, )]) +@pytest.mark.parametrize('dtype', ['int32', 'float32', 'uint8', 'int8']) +def test_argmin_1d(dtype, shape): + x0 = test_common.generate_tensor(shape, dtype).npu() + triton_res = torch.empty(1, dtype=torch.int32).npu() + numel = shape[0] + triton_argmin_1d[(1, )](x0, triton_res, numel, numel) + torch_res = torch_argmin(x0, dim=0, keepdim=True) + test_common.validate_cmp("int32", triton_res, torch_res) + + +@triton.jit +def triton_argmin_2d(in_ptr0, out_ptr0, dim: tl.constexpr, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, + NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + mmask = mblk_idx < M + nmask = nblk_idx < N + mask = (mmask[:, None]) & (nmask[None, :]) + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + x = tl.load(in_ptr0 + idx, mask=mask, other=float('inf')) + tmp4 = tl.argmin(x, dim) + if dim == 0: + tl.store(out_ptr0 + tl.arange(0, N), tmp4, None) + else: + tl.store(out_ptr0 + tl.arange(0, M), tmp4, None) + + +@pytest.mark.parametrize('shape', [(37, 125), (29, 4), (7, 31)]) +@pytest.mark.parametrize('dtype', ['int32', 'float32', 'uint8', 'int8']) +@pytest.mark.parametrize('dim', [0, 1]) +def test_argmin_2d(dtype, shape, dim): + shapex, shapey = shape + x0 = test_common.generate_tensor(shape, dtype).npu() + triton_res = torch.empty([ + shape[1 - dim], + ], dtype=torch.int32).npu() + triton_argmin_2d[(1, 1)](x0, triton_res, dim, shapex, shapey, shapex, shapey) + torch_res = torch_argmin(x0, dim=dim, keepdim=False) + test_common.validate_cmp("int32", triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_ascend_barrier.py b/third_party/ascend/unittest/pytest_ut/test_ascend_barrier.py new file mode 100755 index 0000000000..c8a525078f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ascend_barrier.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +import os + +import pytest +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +# ============== Kernel definitions ============== + + +@triton.jit +def kernel_debug_barrier(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + """Test debug barrier.""" + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + with al.scope(core_mode="vector"): + al.debug_barrier(al.SYNC_IN_VF.VV_ALL) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + result = x + y + tl.store(out_ptr + i, result, mask=i < n) + + +# ============== Pytest tests ============== + + +def test_debug_barrier(): + """Test debug barrier generates.""" + mlir = compile_kernel( + kernel_debug_barrier, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert "annotation.mark" in mlir and "VV_ALL" in mlir + + +# ============== Main for manual testing ============== + +if __name__ == "__main__": + print("=" * 60) + print("Test: debug barrier") + print("=" * 60) + mlir = compile_kernel( + kernel_debug_barrier, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_ascend_version.py b/third_party/ascend/unittest/pytest_ut/test_ascend_version.py new file mode 100644 index 0000000000..d131f87603 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ascend_version.py @@ -0,0 +1,28 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton.backends.ascend as ascend + + +def test_triton_and_ascend_version_api(): + assert hasattr(ascend, "__version__") + triton_ascend_version = ascend.__version__ + assert isinstance(triton_ascend_version, str) + assert triton_ascend_version diff --git a/third_party/ascend/unittest/pytest_ut/test_asin.py b/third_party/ascend/unittest/pytest_ut/test_asin.py new file mode 100644 index 0000000000..8d9d7249ed --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_asin.py @@ -0,0 +1,90 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import torch +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + + +@triton.jit +def asin_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + + y = libdevice.asin(x) + + tl.store(y_ptr + offsets, y, mask=mask) + + +@pytest.mark.parametrize('shape', [ + (12, 16), +]) +@pytest.mark.parametrize('dtype', ['float32']) +def test_asin(shape, dtype): + n_elements = shape[0] * shape[1] + + x = test_common.generate_tensor(shape, dtype).npu() + + # Ensure to include some boundary cases + x[0, 0] = 0.0 + x[0, 1] = 0.5 + x[0, 2] = -0.5 + x[0, 3] = 1.0 + x[0, 4] = -1.0 + x[0, 5] = 0.707 # sin(π/4) + x[0, 6] = 0.866 # sin(π/3) + + # Add some out-of-range values + x[0, 7] = 1.1 + x[0, 8] = -1.1 + + y = torch.empty_like(x) + + BLOCK_SIZE = 192 + grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + asin_kernel[grid](x, y, n_elements, BLOCK_SIZE=BLOCK_SIZE) + + expected = torch.asin(x) + + # Check the accuracy for values within the effective range. + valid_mask = (x >= -1) & (x <= 1) + + if torch.any(valid_mask): + valid_y = y[valid_mask] + valid_expected = expected[valid_mask] + + torch.testing.assert_close(valid_y, valid_expected, rtol=1e-3, atol=1e-3) + + # Check if values outside the range return NaN + invalid_mask = (x < -1) | (x > 1) + if torch.any(invalid_mask): + invalid_y = y[invalid_mask] + assert torch.all(torch.isnan(invalid_y)), "Invalid inputs should return NaN" + + print("✓ ASIN test PASSED!") diff --git a/third_party/ascend/unittest/pytest_ut/test_asinh.py b/third_party/ascend/unittest/pytest_ut/test_asinh.py new file mode 100644 index 0000000000..38865e92be --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_asinh.py @@ -0,0 +1,58 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +@triton.jit +def triton_asinh(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.asinh(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', ['float32', 'float16', 'bfloat16']) +def test_asinh_special(param_list): + dtype = param_list + x_near = torch.linspace(1.0 + 1e-6, 2.0, 500, dtype=eval("torch." + dtype)).npu() + x_far = torch.linspace(2.0, 1e4, 300, dtype=eval("torch." + dtype)).npu() + x0 = torch.cat([x_near, x_far], dim=0) + x_special = torch.tensor([1.0, 1.0 + 1e-8, 2.0, 1000.0, 1e4], dtype=eval("torch." + dtype)).npu() + x0 = torch.cat([x0, x_special], dim=0) + + if dtype == 'float16': + x0 = torch.clamp(x0, max=240) + y_ref = torch.asinh(x0) + y_cal = torch.zeros_like(y_ref) + triton_asinh[1, 1, 1](x0, y_cal, x0.shape[0], x0.shape[0]) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_asm.py b/third_party/ascend/unittest/pytest_ut/test_asm.py new file mode 100644 index 0000000000..ce4c7dca25 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_asm.py @@ -0,0 +1,96 @@ +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + + +def torch_add(x, y): + res = x + y + return res + + +@triton.jit +def triton_asm_add( + x_ptr, + y_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = tl.inline_asm_elementwise( + asm=""" + ADD.s64 $0, $1, $2 + """, + constraints=("=l,l,l"), + args=[x, y], + dtype=tl.int64, + is_pure=True, + pack=1, + ) + tl.store(output_ptr + offsets, output, mask=mask) + + +@pytest.mark.parametrize('param_list', [ + ['int64', 4096, 1024], +]) +def test_case(param_list): + dtype, length, block_size = param_list + ncore = length // block_size + x = test_common.generate_tensor((length, ), dtype).npu() + y = test_common.generate_tensor((length, ), dtype).npu() + res_ref = torch_add(x, y) + res_cal = torch.zeros((length, ), dtype=eval('torch.' + dtype)).npu() + triton_asm_add[(ncore, )](x, y, res_cal, length, BLOCK_SIZE=block_size) + test_common.validate_cmp(dtype, res_cal, res_ref) + + +@triton.jit +def triton_asm_add_2d( + x_ptr, + y_ptr, + output_ptr, + M, + N, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + pid = tl.program_id(axis=0) + row_offsets = pid * BLOCK_M + tl.arange(0, BLOCK_M) + col_offsets = tl.arange(0, BLOCK_N) + offsets = row_offsets[:, None] * N + col_offsets[None, :] + mask = (row_offsets[:, None] < M) & (col_offsets[None, :] < N) + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = tl.inline_asm_elementwise( + asm=""" + ADD.s64 $0, $1, $2 + """, + constraints=("=l,l,l"), + args=[x, y], + dtype=tl.int64, + is_pure=True, + pack=1, + ) + tl.store(output_ptr + offsets, output, mask=mask) + + +@pytest.mark.parametrize('param_list', [ + ['int64', 64, 32, 16, 32], +]) +def test_case_2d(param_list): + dtype, M, N, block_m, block_n = param_list + ncore = M // block_m + x = test_common.generate_tensor((M, N), dtype).npu() + y = test_common.generate_tensor((M, N), dtype).npu() + res_ref = torch_add(x, y) + res_cal = torch.zeros((M, N), dtype=eval('torch.' + dtype)).npu() + triton_asm_add_2d[(ncore, )](x, y, res_cal, M, N, BLOCK_M=block_m, BLOCK_N=block_n) + test_common.validate_cmp(dtype, res_cal, res_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_asm_scalar.py b/third_party/ascend/unittest/pytest_ut/test_asm_scalar.py new file mode 100644 index 0000000000..23b8f6553c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_asm_scalar.py @@ -0,0 +1,32 @@ +import torch +import torch_npu +import triton +import triton.language as tl +import pytest + + +@triton.jit +def triton_asm_time(output_ptr, ): + y = tl.inline_asm_elementwise( + asm=""" + MOV $0, SYS_CNT + """, + constraints="=l", + args=[], + dtype=(tl.int64), + is_pure=False, + pack=1, + ) + tl.store(output_ptr, y) + + +@pytest.mark.parametrize( + "param_list", + [[ + "int64", + ]], +) +def test_case(param_list): + (dtype, ) = param_list + res_cal = torch.zeros((1, ), dtype=eval("torch." + dtype)).npu() + triton_asm_time[(1, )](res_cal, ) diff --git a/third_party/ascend/unittest/pytest_ut/test_associative_scan.py b/third_party/ascend/unittest/pytest_ut/test_associative_scan.py new file mode 100644 index 0000000000..248d08cba4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_associative_scan.py @@ -0,0 +1,179 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import math +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + +import test_common + + +def torch_func(x, dim, reverse): + if reverse: + x = torch.flip(x, [dim]) + res = torch.cumsum(x, dim=dim) + return res + + +def combine_fn_test_torch(a, b, combine_fn): + return torch.maximum(a, b) + + +def torch_func_scan(x: torch.Tensor, dim: int, combine_fn='maximum', reverse=False): + """ + PyTorch implements associative_scan, with semantics fully aligned with Triton. + """ + dim = dim % x.ndim + + if reverse: + x = x.flip(dim) + + N = x.size(dim) + tensors = torch.unbind(x, dim=dim) + + outputs = [] + carry = tensors[0] + outputs.append(carry) + + for i in range(1, N): + carry = combine_fn_test_torch(tensors[i], carry, combine_fn) + outputs.append(carry) + + output = torch.stack(outputs, dim=dim) + + if reverse: + output = output.flip(dim) + + return output + + +@triton.jit +def combine_fn_test(a, b): + return tl.maximum(a, b) + + +@triton.jit +def triton_kernel_1d_scan( + out_ptr0, + in_ptr0, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + XBLOCK: tl.constexpr, +): + tl.static_assert(numel_x == XBLOCK, "numel_x must be equal to XBLOCK in this kernel") + idx = tl.arange(0, XBLOCK) + x = tl.load(in_ptr0 + idx) + ret = tl.associative_scan(x, axis=dim, reverse=reverse, combine_fn=combine_fn_test) + tl.store(out_ptr0 + idx, ret) + + +@triton.jit +def triton_kernel_2d_scan( + out_ptr0, + in_ptr0, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + numel_r: tl.constexpr, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, +): + tl.static_assert(numel_x == XBLOCK, "numel_x must be equal to XBLOCK in this kernel") + tl.static_assert(numel_r == RBLOCK, "numel_r must be equal to RBLOCK in this kernel") + idx_x = tl.arange(0, XBLOCK) + idx_r = tl.arange(0, RBLOCK) + idx = idx_x[:, None] * numel_r + idx_r[None, :] + x = tl.load(in_ptr0 + idx) + ret = tl.associative_scan(x, axis=dim, reverse=reverse, combine_fn=combine_fn_test) + tl.store(out_ptr0 + idx, ret) + + +@triton.jit +def triton_kernel_3d_scan( + out_ptr0, + in_ptr0, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + numel_r: tl.constexpr, + numel_z: tl.constexpr, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, + ZBLOCK: tl.constexpr, +): + tl.static_assert(numel_x == XBLOCK, "numel_x must be equal to XBLOCK in this kernel") + tl.static_assert(numel_r == RBLOCK, "numel_r must be equal to RBLOCK in this kernel") + tl.static_assert(numel_z == ZBLOCK, "numel_z must be equal to ZBLOCK in this kernel") + idx_x = tl.arange(0, XBLOCK) + idx_r = tl.arange(0, RBLOCK) + idx_z = tl.arange(0, ZBLOCK) + idx = idx_x[:, None, None] * numel_r * numel_z + idx_r[None, :, None] * numel_z + idx_z[None, None, :] + x = tl.load(in_ptr0 + idx) + ret = tl.associative_scan(x, axis=dim, reverse=reverse, combine_fn=combine_fn_test) + tl.store(out_ptr0 + idx, ret) + + +def triton_func_scan(x, dim, reverse): + res = torch.empty_like(x) + print(f"res.dtype = {res.dtype}") + shape = x.size() + if len(shape) == 1: + if dim >= 1: + pytest.skip("dim >= 1 for 1D tensor, skipping.") + triton_kernel_1d_scan[1, 1, 1](res, x, dim, reverse, x.shape[0], x.shape[0]) + elif len(shape) == 2: + if dim >= 2: + pytest.skip("dim >= 2 for 2D tensor, skipping.") + triton_kernel_2d_scan[1, 1, 1](res, x, dim, reverse, x.shape[0], x.shape[1], x.shape[0], x.shape[1]) + elif len(shape) == 3: + if dim >= 3: + pytest.skip("dim >= 3 for 3D tensor, skipping.") + triton_kernel_3d_scan[1, 1, 1](res, x, dim, reverse, x.shape[0], x.shape[1], x.shape[2], x.shape[0], x.shape[1], + x.shape[2]) + else: + pytest.skip(f"This testcase unsupported tensor dimension: {len(shape)}") + + return res + + +@pytest.mark.parametrize("dtype", ['int32', 'float32']) +@pytest.mark.parametrize("shape", [(128, ), (8, 4), (128, 4, 16)]) +@pytest.mark.parametrize("dim", [0, 1, 2]) +@pytest.mark.parametrize("combine_fn", [ + 'maximum', +]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_scan(dtype, shape, dim, combine_fn, reverse): + torch.manual_seed(0) + x = test_common.generate_tensor(shape=shape, dtype=dtype) + x_gold = x + cpu_res = torch_func_scan(x_gold, dim, combine_fn, reverse) + print(f"cpu_res: {cpu_res}") + + x_npu = x.npu() + triton_res = triton_func_scan(x_npu, dim, reverse) + print(f"triton_res: {triton_res}") + + test_common.validate_cmp(dtype, triton_res, cpu_res) + print(f"Validate PASS") diff --git a/third_party/ascend/unittest/pytest_ut/test_associative_scan_multi_input.py b/third_party/ascend/unittest/pytest_ut/test_associative_scan_multi_input.py new file mode 100644 index 0000000000..6fb8813dc4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_associative_scan_multi_input.py @@ -0,0 +1,157 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import triton +import triton.language as tl +import pytest + + +def combine_fn_test_torch_add(a, b): + return a + b + + +def torch_func_scan(x: torch.Tensor, dim: int, reverse: bool): + """ + PyTorch implements associative_scan, with semantics fully aligned with Triton. + """ + dim = dim % x.ndim + + if reverse: + x = x.flip(dim) + + N = x.size(dim) + tensors = torch.unbind(x, dim=dim) + + outputs = [] + carry = tensors[0] + outputs.append(carry) + + for i in range(1, N): + carry = combine_fn_test_torch_add(tensors[i], carry) + outputs.append(carry) + + output = torch.stack(outputs, dim=dim) + + if reverse: + output = output.flip(dim) + + return output + + +@triton.jit +def add_combine_fn(a_value, a_index, b_value, b_index): + new_val = a_value + b_value + new_idx = a_index + b_index + return (new_val, new_idx) + + +@triton.jit +def prefix_scan_last_dim_kernel( + vals_ptr, + idx_ptr, + out_vals_ptr, + out_idxs_ptr, + axis_size, + total_slices, + BLOCK_SIZE: tl.constexpr, + reverse: tl.constexpr, +): + slice_id = tl.program_id(0) + if slice_id >= total_slices: + return + + offs = tl.arange(0, BLOCK_SIZE) + mask = offs < axis_size + base = slice_id * axis_size + + vals = tl.load(vals_ptr + base + offs, mask=mask, other=0.0) + idxs = tl.load(idx_ptr + base + offs, mask=mask, other=0) + + pre_vals, pre_idxs = tl.associative_scan((vals, idxs), axis=0, combine_fn=add_combine_fn, reverse=reverse) + + tl.store(out_vals_ptr + base + offs, pre_vals, mask=mask) + tl.store(out_idxs_ptr + base + offs, pre_idxs, mask=mask) + + +def multi_input_prefix_sum(values: torch.Tensor, index: torch.Tensor, axis=0, reverse=False): + assert values.shape == index.shape + assert values.device == index.device + rank = values.ndim + if axis < 0: + axis += rank + assert 0 <= axis < rank + + # 1. change to make axis the last dim + order = list(range(rank)) + if axis != rank - 1: + order[axis], order[-1] = order[-1], order[axis] + inv_order = [0] * rank + for i, o in enumerate(order): + inv_order[o] = i + + vals_p = values.permute(order).contiguous() + idxs_p = index.permute(order).contiguous() + + shape_p = vals_p.shape + axis_size = shape_p[-1] + total_slices = 1 + for d in shape_p[:-1]: + total_slices *= d + + out_vals_p = torch.empty_like(vals_p) + out_idxs_p = torch.empty_like(idxs_p) + + BLOCK_SIZE = 1 << (axis_size - 1).bit_length() + + prefix_scan_last_dim_kernel[(total_slices, )](vals_p, idxs_p, out_vals_p, out_idxs_p, axis_size, total_slices, + BLOCK_SIZE=BLOCK_SIZE, reverse=reverse) + + # 2. permute back + out_vals = out_vals_p.permute(inv_order) + out_idxs = out_idxs_p.permute(inv_order) + return out_vals, out_idxs + + +@pytest.mark.parametrize("shape, axis", [ + ((10, ), 0), + ((4, 4), 0), + ((4, 4), 1), + ((2, 10, 5), 0), + ((2, 10, 5), 1), + ((2, 10, 5), 2), +]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_multi_input_prefix_sum(shape, axis, reverse): + torch.manual_seed(0) + device = "npu" + + values = torch.randn(shape, device=device, dtype=torch.float32) + index = torch.arange(values.numel(), device=device, dtype=torch.int32).reshape(shape) + + triton_vals, triton_idxs = multi_input_prefix_sum(values, index, axis=axis, reverse=reverse) + + torch_vals = torch_func_scan(values, axis, reverse) + torch_idxs = torch_func_scan(index, axis, reverse) + + assert torch.allclose(triton_vals, torch_vals, rtol=1e-3, atol=1e-3), \ + f"数值不匹配!shape={shape}, axis={axis}\nTriton: {triton_vals}\nPyTorch: {torch_vals}" + assert torch.equal(triton_idxs, torch_idxs), \ + f"索引不匹配!shape={shape}, axis={axis}\nTriton: {triton_idxs}\nPyTorch: {torch_idxs}" diff --git a/third_party/ascend/unittest/pytest_ut/test_assume.py b/third_party/ascend/unittest/pytest_ut/test_assume.py new file mode 100644 index 0000000000..b18c0f2db5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_assume.py @@ -0,0 +1,51 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common + + +@triton.jit +def triton_assume(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + tl.assume((XBLOCK & (XBLOCK - 1)) == 0) + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + num = tl.sum(tmp0) + tl.assume(num > 0) + tmp2 = tmp0 + tl.store(out_ptr0 + (xindex), tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_assume(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = x0 + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_assume[(ncore, )](x0, y_cal, x0.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_assume1.py b/third_party/ascend/unittest/pytest_ut/test_assume1.py new file mode 100644 index 0000000000..05d5630092 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_assume1.py @@ -0,0 +1,32 @@ +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + +from triton._internal_testing import (is_interpreter) + + +@triton.jit +def assume(out_ptr, N: tl.constexpr, BLOCK_N: tl.constexpr): + current_size = N - tl.program_id(0) * BLOCK_N + tl.assume(current_size >= BLOCK_N) + if current_size >= BLOCK_N: + tl.store(out_ptr + tl.program_id(0), current_size) + else: + tl.store(out_ptr + tl.program_id(0), current_size + 101024) + + +@pytest.mark.parametrize('dtype', ["float32"]) +def test_assume(dtype): + NBLOCKS = 1024 // 128 + BLOCK_N = 128 + N = 1024 + output = torch.zeros(NBLOCKS, device='npu') + pgm = assume[(NBLOCKS, )](output, N=N, BLOCK_N=BLOCK_N) + + if is_interpreter(): + return + + assert 'llvm.intr.assume' in pgm.asm['ttadapter'] diff --git a/third_party/ascend/unittest/pytest_ut/test_atan.py b/third_party/ascend/unittest/pytest_ut/test_atan.py new file mode 100644 index 0000000000..eca6b31f07 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atan.py @@ -0,0 +1,101 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +import triton.language.extra.cann.libdevice as libdevice + + +def standard_unary(x0, dtype): + res = torch.atan(x0) + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = libdevice.atan(x) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + # (torch.int8, 'int8'), + # (torch.int16, 'int16'), + # (torch.int32, 'int32'), + # (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_atan2.py b/third_party/ascend/unittest/pytest_ut/test_atan2.py new file mode 100644 index 0000000000..1abf385039 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atan2.py @@ -0,0 +1,134 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +import triton.language.extra.cann.libdevice as libdevice + + +def standard_unary(x0, y0, dtype): + res = torch.atan2(y0, x0) + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = libdevice.atan2(y, x) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_atan2_common(dtype, sigtype, N, NUMEL): + if N == 0: + return + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + y0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, y0, dtype) + x0 = x0.npu() + y0 = y0.npu() + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, y0, out, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(sigtype, out, ans) + + +input_vals = [ + (0.0, 1.0), + (0.0, -1.0), + (-1.0, -0.0), + (1.0, -0.0), +] + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes[0:2]) +@pytest.mark.parametrize('X,Y', input_vals) +def test_atan2_special(dtype, sigtype, N, NUMEL, X, Y): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = torch.full((N, ), X, dtype=eval(f'torch.{sigtype}')) + y0 = torch.full((N, ), Y, dtype=eval(f'torch.{sigtype}')) + + ans = standard_unary(x0, y0, dtype) + x0 = x0.npu() + y0 = y0.npu() + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, y0, out, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(sigtype, out, ans) + # -0.0 and +0.0 compare equal, so the sign of a zero result is checked + # separately: atan2 must keep the sign of a negative zero y. + assert torch.equal(torch.signbit(out.cpu()), torch.signbit(ans.cpu())) diff --git a/third_party/ascend/unittest/pytest_ut/test_atanh.py b/third_party/ascend/unittest/pytest_ut/test_atanh.py new file mode 100644 index 0000000000..a8d8ad3fe1 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atanh.py @@ -0,0 +1,77 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +def torch_atanh(x0): + return torch.atanh(x0) + + +@triton.jit +def triton_atanh(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.atanh(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['bfloat16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_atanh_common(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu().clamp(-0.99, 0.99) + y_ref = torch_atanh(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_atanh[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +input_vals = [-1, 1] + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['bfloat16', (2, 4096, 8), 2, 32768, 1024], +]) +@pytest.mark.parametrize('input_val', input_vals) +def test_atanh_special(param_list, input_val): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = torch.full(shape, input_val, dtype=eval('torch.' + dtype)).npu() + y_ref = torch_atanh(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_atanh[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_add.py b/third_party/ascend/unittest/pytest_ut/test_atomic_add.py new file mode 100644 index 0000000000..004cdb63ae --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_add.py @@ -0,0 +1,203 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import torch +import torch_npu + + +@triton.jit +def atomic_add(in_ptr0, out_ptr0, out_ptr1, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.atomic_add(out_ptr0 + (x1), tmp0, xmask) + tl.store(out_ptr1 + (x1), tmp1, xmask) + + +@triton.jit +def atomic_add_supply(in_ptr0, out_ptr0, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.atomic_add(out_ptr0 + (x1), tmp0, xmask) + + +@triton.jit +def atomic_add_for_load_offset(index_ptr, in_ptr0, out_ptr0): + index = tl.atomic_add(index_ptr, 1) + val = tl.load(in_ptr0 + index) + tl.store(out_ptr0, val) + + +@triton.jit +def atomic_add_for_store_offset(index_ptr, out_ptr0): + index = tl.atomic_add(index_ptr, 1) + tl.store(out_ptr0 + index, 1) + + +@pytest.mark.parametrize('param_list', [ + ['int64', (256, 32), 2], + ['int32', (32, 32), 2], + ['float32', (32, 32), 2], + ['float16', (64, 64), 4], + ['bfloat16', (64, 64), 4], + ['float32', (128, 128), 8], + ['float16', (128, 128), 16], + ['float32', (32768, 16), 32], +]) +def test_atomic_add(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] / ncore + split_size = shape[0] // ncore + x0_value = 3 + x0 = torch.full(shape, x0_value, dtype=eval(f'torch.{dtype}')).npu() + if dtype == 'int64': + x1 = torch.randint(-10**15, 10**15, (split_size, shape[1]), dtype=eval(f'torch.{dtype}')).npu() + else: + x1 = torch.full((split_size, shape[1]), 2, dtype=eval(f'torch.{dtype}')).npu() + y = torch.full((split_size, shape[1]), -10, dtype=eval(f'torch.{dtype}')).npu() + + y_ref = x1 + 0 + x1_ref = x1 + ncore * x0_value + + n_elements = shape[0] * shape[1] + atomic_add[ncore, 1, 1](x0, x1, y, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, x1, x1_ref) + + +@pytest.mark.parametrize('param_list', [ + ['int32', (32, 32), 1], + ['float32', (32, 32), 1], + ['float16', (64, 64), 1], +]) +def test_atomic_add_return_value(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] / ncore + split_size = shape[0] // ncore + x0_value = 3 + x0 = torch.full(shape, x0_value, dtype=eval(f'torch.{dtype}')).npu() + x1 = torch.full((split_size, shape[1]), 2, dtype=eval(f'torch.{dtype}')).npu() + y = torch.full((split_size, shape[1]), -10, dtype=eval(f'torch.{dtype}')).npu() + + y_ref = x1 + 0 + x1_ref = x1 + ncore * x0_value + + n_elements = shape[0] * shape[1] + atomic_add[ncore, 1, 1](x0, x1, y, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, x1, x1_ref) + test_common.validate_cmp(dtype, y, y_ref) + + +@triton.jit +def atomic_add_2d(in_ptr0, out_ptr0, out_ptr1, numel_0, numel_1, BLOCK_SIZE_0: tl.constexpr, + BLOCK_SIZE_1: tl.constexpr): + pid = tl.program_id(0) + idx0_in = pid * BLOCK_SIZE_0 + tl.arange(0, BLOCK_SIZE_0)[:, None] + idx0_out = tl.arange(0, BLOCK_SIZE_0)[:, None] + idx1 = tl.arange(0, BLOCK_SIZE_1)[None, :] + idx_in = idx0_in * BLOCK_SIZE_1 + idx1 + idx_out = idx0_out * BLOCK_SIZE_1 + idx1 + msk_in = (idx0_in < numel_0) & (idx1 < numel_1) + msk_out = (idx0_out < numel_0) & (idx1 < numel_1) + tmp0 = tl.load(in_ptr0 + idx_in, msk_in) + tmp1 = tl.atomic_add(out_ptr0 + idx_out, tmp0, msk_out) + tl.store(out_ptr1 + idx_out, tmp1, msk_out) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (32, 32), 2], +]) +def test_atomic_add_2d(param_list): + dtype, shape, ncore = param_list + split_size = shape[0] // ncore + block_size_0 = split_size + block_size_1 = shape[1] + x0_value = 3 + x0 = torch.full(shape, x0_value, dtype=eval('torch.float32')).npu() + x1 = torch.full((split_size, shape[1]), 2, dtype=eval('torch.float32')).npu() + y = torch.full((split_size, shape[1]), -10, dtype=eval('torch.float32')).npu() + + y_ref = x1 + 0 + x1_ref = x1 + ncore * x0_value + + atomic_add_2d[ncore, 1, 1](x0, x1, y, shape[0], shape[1], BLOCK_SIZE_0=block_size_0, BLOCK_SIZE_1=block_size_1) + test_common.validate_cmp(dtype, x1, x1_ref) + + +@pytest.mark.parametrize('shape', [(3, 1), (13, 1), (32, 1), (256, 1)]) +@pytest.mark.parametrize('dtype', ['float32']) +def test_atomic_add_2d_supply(dtype, shape): + ncore = 1 + block_size = shape[0] * shape[1] / ncore + split_size = shape[0] // ncore + x0_value = 3 + x0 = torch.full(shape, x0_value, dtype=eval('torch.' + dtype)).npu() + x1 = torch.full((split_size, shape[1]), 2, dtype=eval('torch.' + dtype)).npu() + + y_ref = x1 + 0 + x1_ref = x1 + ncore * x0_value + + n_elements = shape[0] * shape[1] + atomic_add_supply[shape[0], 1, 1](x0, x1, n_elements, BLOCK_SIZE=shape[1]) + test_common.validate_cmp(dtype, x1, x1_ref) + + +def test_atomic_add_for_load_offset(): + index = torch.tensor([1]).npu() + input_tensor = torch.zeros(5).npu() + output = torch.tensor([1]).npu() + index_ref = index.clone() + index_ref += 1 + output_ref = output.clone() + output_ref = input_tensor[index] + + atomic_add_for_load_offset[(1, )](index, input_tensor, output) + torch.equal(index, index_ref) + torch.equal(output, output_ref) + + +def test_atomic_add_for_store_offset(): + index = torch.tensor([1]).npu() + output = torch.zeros(5).npu() + index_ref = index.clone() + index_ref += 1 + output_ref = output.clone() + output_ref[index] = 1 + + atomic_add_for_store_offset[(1, )](index, output) + torch.equal(index, index_ref) + torch.equal(output, output_ref) + + +if __name__ == "__main__": + param_list = ['float32', (32, 32), 2] + test_atomic_add_2d(param_list) diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_and.py b/third_party/ascend/unittest/pytest_ut/test_atomic_and.py new file mode 100644 index 0000000000..60388e63f8 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_and.py @@ -0,0 +1,65 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import torch +import torch_npu + + +@triton.jit +def atomic_and(in_ptr0, out_ptr0, out_ptr1, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.atomic_and(out_ptr0 + (x1), tmp0, xmask) + tl.store(out_ptr1 + (x1), tmp1, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['int64', (32, 32), 2], + ['int32', (32, 32), 2], +]) +def test_atomic_and(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] // ncore + split_size = shape[0] // ncore + + val = torch.randint(low=0, high=10, size=shape, dtype=eval(f'torch.{dtype}')).npu() + + pointer = torch.randint(low=0, high=10, size=(split_size, shape[1]), dtype=eval(f'torch.{dtype}')).npu() + pointer_old = torch.full_like(pointer, -10).npu() + pointer_ref = pointer.clone() + + for i in range(ncore - 1): + pointer_ref &= val[(i * split_size):((i + 1) * split_size)] + + pointer_ref_last = pointer_ref.clone() + pointer_ref &= val[((ncore - 1) * split_size):(ncore * split_size)] + + n_elements = shape[0] * shape[1] + atomic_and[ncore, 1, 1](val, pointer, pointer_old, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, pointer, pointer_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_cas.py b/third_party/ascend/unittest/pytest_ut/test_atomic_cas.py new file mode 100644 index 0000000000..511658390d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_cas.py @@ -0,0 +1,188 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import torch +import torch_npu + +types_all = [ + (torch.float32, 'float32'), +] + + +def ceil_div(a, b): + return (a + b - 1) // b + + +@triton.jit +def atomic_cas(in_ptr0, in_ptr1, out_ptr0, out_ptr1, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + val = tl.load(in_ptr0 + (x0), xmask) + cmp = tl.load(in_ptr1 + (x0), xmask) + tmp1 = tl.atomic_cas(out_ptr0 + (x1), cmp, val) + tl.store(out_ptr1 + (x1), tmp1, xmask) + + +@triton.jit +def atomic_cas_with_full( + ptr, + out, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(0) + x = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = x < n_elements + + cmp = tl.full((BLOCK_SIZE, ), 2.0, tl.float32) + val = tl.full((BLOCK_SIZE, ), 1.0, tl.float32) + + old = tl.atomic_cas(ptr + x, cmp, val) # in_ptr(origin 2) -> ref: 1 X + tl.store(out + x, old, mask=mask) # out(origin 1) -> ref: old in_ptr(2) √ + + +@triton.jit +def atomic_cas_without_full( + ptr, + cmp_ptr, + val_ptr, + out_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(0) + x = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = x < n_elements + + cmp = tl.load(cmp_ptr + x, mask) # 2 + val = tl.load(val_ptr + x, mask) # 1 + + old = tl.atomic_cas(ptr + x, cmp, val) # old : 2 + tl.store(out_ptr + x, old, mask=mask) + + +@pytest.mark.parametrize('param_list', [ + ['int16', (8, 8), 2], + ['int32', (32, 32), 6], + ['int64', (32, 32), 2], + ['float32', (32, 32), 2], + ['float16', (64, 64), 4], + ['float32', (128, 128), 8], + ['float16', (128, 128), 16], +]) +def test_atomic_cas(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] // ncore + split_size = shape[0] // ncore + + import random + cmp_val = [random.randint(0, 10) for _ in range(ncore)] + + cmp = torch.ones(split_size, shape[1], dtype=eval(f'torch.{dtype}')).to().npu() * cmp_val[0] + for i in range(1, ncore): + append = torch.ones(split_size, shape[1], dtype=eval(f'torch.{dtype}')).to().npu() * cmp_val[i] + cmp = torch.cat([cmp, append], dim=0) + + val = torch.randint(low=0, high=10, size=shape, dtype=eval(f'torch.{dtype}')).npu() + + # Each program writes a distinct offset; no cross-program racing on the same address. + # shape[0] may not divide ncore evenly: only the first ncore*split_size rows are covered. + covered = ncore * split_size + pointer = torch.randint(low=0, high=10, size=shape, dtype=eval(f'torch.{dtype}')).npu() + pointer_old = torch.full_like(pointer, -10).npu() + pointer_ref = pointer.clone() + pointer_ref[:covered] = torch.where(pointer[:covered] == cmp, val[:covered], pointer[:covered]) + + n_elements = shape[0] * shape[1] + atomic_cas[ncore, 1, 1](val, cmp, pointer, pointer_old, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, pointer, pointer_ref) + + +@pytest.mark.parametrize('param_list', [ + ['int16', (8, 8), 1], + ['int32', (32, 32), 1], + ['float32', (32, 32), 1], +]) +def test_atomic_cas_return_value(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] // ncore + split_size = shape[0] // ncore + + import random + cmp_val = [random.randint(0, 10) for _ in range(ncore)] + + cmp = torch.ones(split_size, shape[1], dtype=eval(f'torch.{dtype}')).to().npu() * cmp_val[0] + for i in range(1, ncore): + append = torch.ones(split_size, shape[1], dtype=eval(f'torch.{dtype}')).to().npu() * cmp_val[i] + cmp = torch.cat([cmp, append], dim=0) + + val = torch.randint(low=0, high=10, size=shape, dtype=eval(f'torch.{dtype}')).npu() + + covered = ncore * split_size + pointer = torch.randint(low=0, high=10, size=shape, dtype=eval(f'torch.{dtype}')).npu() + pointer_old_ref = pointer.clone() + pointer_old = torch.full_like(pointer, -10).npu() + pointer_ref = pointer.clone() + pointer_ref[:covered] = torch.where(pointer[:covered] == cmp, val[:covered], pointer[:covered]) + + n_elements = shape[0] * shape[1] + atomic_cas[ncore, 1, 1](val, cmp, pointer, pointer_old, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, pointer, pointer_ref) + test_common.validate_cmp(dtype, pointer_old, pointer_old_ref) + + +@pytest.mark.parametrize('dtype,sigtype', types_all) +@pytest.mark.parametrize('n_elements, BLOCK_SIZE', [(4096, 256)]) +@pytest.mark.skip(reason="full tensor has problem, skipped") +def test_atomic_cas_with_full(n_elements, BLOCK_SIZE, dtype, sigtype): + in_ptr = torch.full((n_elements, ), 2, dtype=dtype).npu() + out_ptr = torch.empty_like(in_ptr) + + grid = (ceil_div(n_elements, BLOCK_SIZE), 1, 1) + atomic_cas_with_full[grid](in_ptr, out_ptr, n_elements, BLOCK_SIZE=BLOCK_SIZE) + + # old should be all 2 (for in-range) + torch.testing.assert_close(out_ptr, torch.full_like(out_ptr, 2.0)) + + # final ptr should be all 1 + torch.testing.assert_close(in_ptr, torch.ones_like(in_ptr)) + + +@pytest.mark.parametrize('dtype,sigtype', types_all) +@pytest.mark.parametrize('n_elements, BLOCK_SIZE', [(4096, 256)]) +def test_atomic_cas_without_full(n_elements, BLOCK_SIZE, dtype, sigtype): + in_ptr = torch.full((n_elements, ), 2, dtype=dtype).npu() + cmp_ptr = torch.full((n_elements, ), 2, dtype=dtype).npu() + val_ptr = torch.full((n_elements, ), 1, dtype=dtype).npu() + out_ptr = torch.full((n_elements, ), 1, dtype=dtype).npu() # ref: in_ptr + + grid = (ceil_div(n_elements, BLOCK_SIZE), 1, 1) + atomic_cas_without_full[grid](in_ptr, cmp_ptr, val_ptr, out_ptr, n_elements, BLOCK_SIZE=BLOCK_SIZE) + + torch.testing.assert_close(in_ptr, torch.full_like(in_ptr, 1.0)) + torch.testing.assert_close(out_ptr, torch.full_like(out_ptr, 2.0)) diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_max.py b/third_party/ascend/unittest/pytest_ut/test_atomic_max.py new file mode 100644 index 0000000000..501c5863c2 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_max.py @@ -0,0 +1,137 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import torch +import torch_npu +import numpy as np + + +@triton.jit +def triton_test_fn_atomic_max_dma(in_ptr0, out_ptr0, out_ptr1, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0)) + # only set mask of atomic_max + tl.atomic_max(out_ptr0 + (x1), tmp0, xmask) + + +@triton.jit +def triton_test_fn_atomic_max_dma_supply(in_ptr0, out_ptr0, n_elements: tl.constexpr, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.atomic_max(out_ptr0 + (x1), tmp0, xmask) + + +@triton.jit +def float_scalar_atomic_max_kernel( + value_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + offsets = tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + values = tl.load(value_ptr + offsets, mask=mask, other=-1.0e9) + block_max = tl.max(values, axis=0) + tl.atomic_max(output_ptr, block_max) + + +# torch.max do not support int +@pytest.mark.parametrize('param_list', [ + ['int32', (32, 32), 2], + ['int32', (128, 128), 8], + ['int32', (32768, 16), 32], + ['int64', (32, 32), 2], + ['int64', (128, 128), 8], + ['int64', (8192, 16), 32], +]) +def test_atomic_max(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] / ncore + split_size = shape[0] // ncore + # old size: (32768, 256) + # tensor of (1024, 256) is too big, and it will lead to failure in the backend + # so here we make it smaller + x0 = test_common.generate_tensor(shape, dtype) + x1 = test_common.generate_tensor((split_size, shape[1]), dtype) + y = test_common.generate_tensor((split_size, shape[1]), dtype) + + merged_tensor = torch.cat((x0, x1), dim=0) + chunks = torch.stack(torch.chunk(merged_tensor, ncore + 1, dim=0)) + x1_ref = torch.max(chunks, dim=0)[0] + x0 = x0.npu() + x1 = x1.npu() + y = y.npu() + + n_elements = shape[0] * shape[1] + triton_test_fn_atomic_max_dma[ncore, 1, 1](x0, x1, y, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, x1, x1_ref) + + +@pytest.mark.parametrize('shape', [(3, 1), (13, 1), (32, 1), (256, 1)]) +@pytest.mark.parametrize('dtype', ['int32']) +def test_atomic_max_2d_supply(dtype, shape): + # old size: (32768, 256) + # tensor of (1024, 256) is too big, and it will lead to failure in the backend + # so here we make it smaller + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + + x1_ref = torch.maximum(x0, x1) + + n_elements = shape[0] * shape[1] + triton_test_fn_atomic_max_dma_supply[shape[0], 1, 1](x0, x1, n_elements, BLOCK_SIZE=shape[1]) + test_common.validate_cmp(dtype, x1, x1_ref) + + +def test_float_scalar_atomic_max(): + n_elements = 257 + block_size = 1024 + + values_cpu = torch.linspace(-4.0, 4.0, n_elements, dtype=torch.float32, device="cpu") + values = values_cpu.npu() + output = torch.full((1, ), float("-inf"), dtype=torch.float32, device="npu") + + float_scalar_atomic_max_kernel[(1, )]( + values, + output, + n_elements, + BLOCK_SIZE=block_size, + ) + + expected = values_cpu.max().reshape(1) + torch.testing.assert_close(output.cpu(), expected, rtol=0, atol=0) + + +# if __name__ == "__main__": +# test_atomic_max(['int32', (8, 8), 2]) diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_min.py b/third_party/ascend/unittest/pytest_ut/test_atomic_min.py new file mode 100644 index 0000000000..7385b555c7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_min.py @@ -0,0 +1,92 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import torch +import torch_npu +import numpy as np + + +@triton.jit +def triton_test_fn_atomic_min_dma(in_ptr0, out_ptr0, out_ptr1, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0)) + tl.atomic_min(out_ptr0 + (x1), tmp0, xmask) + + +@triton.jit +def triton_test_fn_atomic_min_dma_supply(in_ptr0, out_ptr0, n_elements: tl.constexpr, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.atomic_min(out_ptr0 + (x1), tmp0, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['int32', (32, 32), 2], + ['int32', (128, 128), 8], + ['int32', (32768, 16), 32], + ['int64', (32, 32), 2], + ['int64', (128, 128), 8], + ['int64', (8192, 16), 32], +]) +def test_atomic_min(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] / ncore + split_size = shape[0] // ncore + x0 = test_common.generate_tensor(shape, dtype) + x1 = test_common.generate_tensor((split_size, shape[1]), dtype) + y = test_common.generate_tensor((split_size, shape[1]), dtype) + + merged_tensor = torch.cat((x0, x1), dim=0) + chunks = torch.stack(torch.chunk(merged_tensor, ncore + 1, dim=0)) + x1_ref = torch.min(chunks, dim=0)[0] + x0 = x0.npu() + x1 = x1.npu() + y = y.npu() + + n_elements = shape[0] * shape[1] + triton_test_fn_atomic_min_dma[ncore, 1, 1](x0, x1, y, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, x1, x1_ref) + + +@pytest.mark.parametrize('shape', [(3, 1), (13, 1), (32, 1), (256, 1)]) +@pytest.mark.parametrize('dtype', ['int32']) +def test_atomic_min_2d_supply(dtype, shape): + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + + x1_ref = torch.minimum(x0, x1) + + n_elements = shape[0] * shape[1] + triton_test_fn_atomic_min_dma_supply[shape[0], 1, 1](x0, x1, n_elements, BLOCK_SIZE=shape[1]) + test_common.validate_cmp(dtype, x1, x1_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_or.py b/third_party/ascend/unittest/pytest_ut/test_atomic_or.py new file mode 100644 index 0000000000..5b549ceb68 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_or.py @@ -0,0 +1,65 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import torch +import torch_npu + + +@triton.jit +def atomic_or(in_ptr0, out_ptr0, out_ptr1, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.atomic_or(out_ptr0 + (x1), tmp0, xmask) + tl.store(out_ptr1 + (x1), tmp1, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['int64', (32, 32), 2], + ['int32', (32, 32), 2], +]) +def test_atomic_or(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] // ncore + split_size = shape[0] // ncore + + val = torch.randint(low=0, high=10, size=shape, dtype=eval(f'torch.{dtype}')).npu() + + pointer = torch.randint(low=0, high=10, size=(split_size, shape[1]), dtype=eval(f'torch.{dtype}')).npu() + pointer_old = torch.full_like(pointer, -10).npu() + pointer_ref = pointer.clone() + + for i in range(ncore - 1): + pointer_ref |= val[(i * split_size):((i + 1) * split_size)] + + pointer_ref_last = pointer_ref.clone() + pointer_ref |= val[((ncore - 1) * split_size):(ncore * split_size)] + + n_elements = shape[0] * shape[1] + atomic_or[ncore, 1, 1](val, pointer, pointer_old, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, pointer, pointer_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_rmw_useanalysis.py b/third_party/ascend/unittest/pytest_ut/test_atomic_rmw_useanalysis.py new file mode 100644 index 0000000000..09a1fc2498 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_rmw_useanalysis.py @@ -0,0 +1,70 @@ +import torch +import triton +import triton.language as tl + + +@triton.jit +def atomic_rmw_useanalysis_kernel( + input_ptr, + output_ptr, + m_ptr, + d_ptr, + N: tl.constexpr, + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(0) + base_idx = pid * 8 + + term1 = 15.0 * 15.0 + term2 = 8.0 * (7.0 - base_idx) + + delta = term1 + term2 + sqrt_delta = tl.sqrt(delta) + + task_idx = tl.ceil((15.0 - sqrt_delta) / 2.0) + task_idx_i32 = task_idx.to(tl.int32) + + block_start = task_idx_i32 * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < N + + data = tl.load(input_ptr + offsets, mask=mask, other=0.0) + m_val = tl.load(m_ptr + offsets, mask=mask, other=0.0) + d_val = tl.load(d_ptr + offsets, mask=mask, other=0.0) + + scaled = data - m_val + p = tl.exp(scaled) + + result = p * (data * 2.0 - d_val) + + output_offsets = offsets + tl.atomic_add(output_ptr + output_offsets, result, mask=mask) + + +def test_atomic_rmw_useanalysis(): + DEVICE = "npu" + N = 1024 + BLOCK_SIZE = 128 + + torch.manual_seed(42) + input_data = torch.randn(N, dtype=torch.float32, device=DEVICE) + m_data = torch.randn(N, dtype=torch.float32, device=DEVICE) + d_data = torch.randn(N, dtype=torch.float32, device=DEVICE) + output_data = torch.zeros(N, dtype=torch.float32, device=DEVICE) + + grid = (8, ) + + atomic_rmw_useanalysis_kernel[grid]( + input_data, + output_data, + m_data, + d_data, + N=N, + BLOCK_SIZE=BLOCK_SIZE, + ) + output_sum = output_data.abs().sum().item() + + if output_sum == 0: + raise AssertionError("UseAnalysis bug detected: atomic_rmw dependencies were erased") + else: + print(" AtomicRMW UseAnalysis is working correctly.") diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_xchg.py b/third_party/ascend/unittest/pytest_ut/test_atomic_xchg.py new file mode 100644 index 0000000000..dc33d321f9 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_xchg.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import torch +import torch_npu + + +@triton.jit +def atomic_xchg(in_ptr0, out_ptr0, out_ptr1, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.atomic_xchg(out_ptr0 + (x1), tmp0, xmask) + tl.store(out_ptr1 + (x0), tmp1, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['int32', (32, 32), 2], + ['float32', (16, 16), 2], + ['float32', (32768, 16), 32], +]) +def test_atomic_xchg(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] // ncore + split_size = shape[0] // ncore + + val = torch.randint(low=0, high=10, size=shape, dtype=eval(f'torch.{dtype}')).npu() + + # Each program writes a distinct offset; no cross-program racing on the same address. + pointer = torch.randint(low=0, high=10, size=shape, dtype=eval(f'torch.{dtype}')).npu() + pointer_ref = val.clone() + pointer_old = torch.full_like(val, -10).npu() + + n_elements = shape[0] * shape[1] + atomic_xchg[ncore, 1, 1](val, pointer, pointer_old, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, pointer, pointer_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_atomic_xor.py b/third_party/ascend/unittest/pytest_ut/test_atomic_xor.py new file mode 100644 index 0000000000..e31e5f5a41 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_atomic_xor.py @@ -0,0 +1,71 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import torch +import torch_npu + + +@triton.jit +def atomic_xor(in_ptr0, out_ptr0, out_ptr1, n_elements, BLOCK_SIZE: tl.constexpr): + xoffset = tl.program_id(0) * BLOCK_SIZE + xindex = xoffset + tl.arange(0, BLOCK_SIZE)[:] + yindex = tl.arange(0, BLOCK_SIZE)[:] + xmask = xindex < n_elements + x0 = xindex + x1 = yindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.atomic_xor(out_ptr0 + (x1), tmp0, xmask) + tl.store(out_ptr1 + (x1), tmp1, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['int64', (32, 32), 2], + ['int32', (32, 32), 2], +]) +def test_atomic_xor(param_list): + dtype, shape, ncore = param_list + block_size = shape[0] * shape[1] // ncore + split_size = shape[0] // ncore + + # 初始化原始值 val,全为 0b0011(十进制 3) + val_value = 3 + val = torch.full(shape, val_value, dtype=eval(f'torch.{dtype}')).npu() + + # 每个线程使用不同输入值 x1,全为 0b0101(十进制 5) + pointer_value = 5 + pointer = torch.full((split_size, shape[1]), pointer_value, dtype=eval(f'torch.{dtype}')).npu() + pointer_old = torch.full_like(pointer, -10) + + # 原子异或后:val ^= pointer 每个线程执行一次 + # 因为异或操作具有可逆性和对称性,参考更新次数 + # 所以参考值为 val_value ^ pointer_value ^ pointer_value ^ ...(ncore 次) + pointer_result = pointer_value + for _ in range(ncore): + pointer_result ^= val_value + + pointer_ref = torch.full_like(pointer, pointer_result) + + n_elements = shape[0] * shape[1] + atomic_xor[ncore, 1, 1](val, pointer, pointer_old, n_elements, BLOCK_SIZE=split_size * shape[1]) + test_common.validate_cmp(dtype, pointer, pointer_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_attn_cp.py b/third_party/ascend/unittest/pytest_ut/test_attn_cp.py new file mode 100644 index 0000000000..6a83bd5e50 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_attn_cp.py @@ -0,0 +1,500 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import time +import torch +import torch_npu +import pytest +import sys + +sys.path.append("../../../") + +from attn_cp_triton_kernel_3d import forward_update_triton, backward_update_triton +from attn_cp_triton_kernel_3d_la import forward_update_triton as forward_update_triton_la + + +def collect_time(model, example_inputs, times: int = 1): + stream = torch.npu.current_stream() + warmup = 1 + stream.synchronize() + for i in range(times + warmup): + out = model(*example_inputs) + if i < warmup: + stream.synchronize() + t0 = time.perf_counter() + else: + t1 = time.perf_counter() + stream.synchronize() + t1 = time.perf_counter() + # GC the result after timing + assert out is not None + return (t1 - t0) / times + + +def print_performance(fn, args=(), times=10, repeat=10, baseline=1.0): + stream = torch.npu.current_stream() + + start = time.perf_counter() + + stream.synchronize() + + for _ in range(repeat * times): + fn(*args) + + stream.synchronize() + + end = time.perf_counter() + took = (end - start) / (times * repeat) + print(f"{took:.6f}") + return took + + +def profile_test(fn, fn_triton, args=(), name="gen_fn", times=10, repeat=10): + print(f"--------------------profiling {name} for {times * repeat} times--------------------") + stream = torch.npu.current_stream() + + experimental_config = torch_npu.profiler._ExperimentalConfig( + aic_metrics=torch_npu.profiler.AiCMetrics.PipeUtilization, + profiler_level=torch_npu.profiler.ProfilerLevel.Level1, + ) + prof = torch_npu.profiler.profile( + activities=[ + # torch_npu.profiler.ProfilerActivity.CPU, + torch_npu.profiler.ProfilerActivity.NPU + ], record_shapes=False, profile_memory=False, with_stack=False, + schedule=torch_npu.profiler.schedule(wait=0, warmup=1, active=100, repeat=1, skip_first=10), + on_trace_ready=torch_npu.profiler.tensorboard_trace_handler("./result_dir"), + experimental_config=experimental_config) + + stream.synchronize() + prof.start() + + for _ in range(times * repeat): + fn_triton(*args) + prof.step() + + prof.stop() + + +def benchmark_test(fn, fn_triton, args=(), name="gen_fn", times=10, repeat=10, profile=False): + print(f"--------------------benchmark_{name} for {times * repeat} times--------------------") + stream = torch.npu.current_stream() + + experimental_config = torch_npu.profiler._ExperimentalConfig( + aic_metrics=torch_npu.profiler.AiCMetrics.PipeUtilization, + profiler_level=torch_npu.profiler.ProfilerLevel.Level1, + ) + prof = torch_npu.profiler.profile( + activities=[ + # torch_npu.profiler.ProfilerActivity.CPU, + torch_npu.profiler.ProfilerActivity.NPU + ], record_shapes=False, profile_memory=False, with_stack=False, + schedule=torch_npu.profiler.schedule(wait=0, warmup=0, active=100, repeat=1, skip_first=10), + on_trace_ready=torch_npu.profiler.tensorboard_trace_handler("./result_dir"), + experimental_config=experimental_config) + + stream.synchronize() + prof.start() + start = time.perf_counter() + for _ in range(times * repeat): + fn_triton(*args) + if profile: + prof.step() + stream.synchronize() + end = time.perf_counter() + time_compiled = (end - start) / (times * repeat) + print(f"{time_compiled:.6f}") + + print(f"Runing eager {name} for {times * repeat} times") + start = time.perf_counter() + for _ in range(times * repeat): + fn(*args) + if profile: + prof.step() + stream.synchronize() + end = time.perf_counter() + time_eager = (end - start) / (times * repeat) + print(f"{time_eager:.6f}") + + time_eager *= 1000000 + time_compiled *= 1000000 + print( + f"Accelerated: {(time_eager - time_compiled) / time_compiled * 100:.4f}% eager takes {time_eager:.3f} us, triton takes {time_compiled:.3f} us" + ) + + return time_eager, time_compiled + + +from einops import rearrange + + +def trans_BNSD2SBH(x): + """Trans data layout from BNSD to SBH""" + return rearrange(x, 'b n s d -> s b (n d)').contiguous() + + +def broadcast_and_trans_BNSD2SBH(x, h): + """broadcast and trans a tensor from [b, n, s, 8] to [s, b, h]""" + n = x.shape[1] + d = h // n + # [b, n, s, 8] -> [b, n, s, d] + new_x = x[..., 0].unsqueeze(3) + new_x = new_x.repeat(1, 1, 1, d) + return trans_BNSD2SBH(new_x) + + +def forward_update(prev_attn_out, prev_softmax_max, prev_softmax_sum, cur_attn_out, cur_softmax_max, cur_softmax_sum): + # update softmax_max + org_dtype = prev_attn_out.dtype + softmax_max = torch.maximum(prev_softmax_max, cur_softmax_max) + prev_scale = torch.exp(prev_softmax_max - softmax_max) + cur_scale = torch.exp(cur_softmax_max - softmax_max) + # update softmax_sum + prev_softmax_sum_scaled = prev_softmax_sum * prev_scale + cur_softmax_sum_scaled = cur_softmax_sum * cur_scale + softmax_sum = prev_softmax_sum_scaled + cur_softmax_sum_scaled + # out updating scale + prev_out_scale = prev_softmax_sum_scaled / softmax_sum + cur_out_scale = cur_softmax_sum_scaled / softmax_sum + # [b, n, s, 8] -> [s, b, h] + prev_out_scale_sbh = broadcast_and_trans_BNSD2SBH(prev_out_scale, prev_attn_out.shape[-1]) + cur_out_scale_sbh = broadcast_and_trans_BNSD2SBH(cur_out_scale, prev_attn_out.shape[-1]) + # update output + attn_out = prev_attn_out * prev_out_scale_sbh + cur_attn_out * cur_out_scale_sbh + attn_out = attn_out.to(org_dtype) + return attn_out, softmax_max, softmax_sum + + +def prove_forward_update(): + + def data_validation(prev_softmax_max, cur_softmax_max, prev_softmax_sum, cur_softmax_sum, prev_attn_out, + cur_attn_out): + + (tt_attn_out, tt_softmax_max, tt_softmax_sum) = forward_update_triton(prev_attn_out, prev_softmax_max, + prev_softmax_sum, cur_attn_out, + cur_softmax_max, cur_softmax_sum) + + (attn_out, softmax_max, softmax_sum) = forward_update(prev_attn_out, prev_softmax_max, prev_softmax_sum, + cur_attn_out, cur_softmax_max, cur_softmax_sum) + + try: + assert torch.equal(softmax_max, tt_softmax_max) + print("max comparition passed.") + assert torch.equal(softmax_sum, tt_softmax_sum) + print("sum comparition passed.") + torch.testing.assert_close(attn_out, tt_attn_out) + print("atten comparition passed.") + + except Exception as e: + print(e) + print("comparison not passed") + print( + f"proving finished, attn shape:{prev_attn_out.shape}, stride:{prev_attn_out.stride(), cur_attn_out.stride()}, softmax shape:{prev_softmax_sum.shape}, stride:{prev_softmax_sum.stride(), cur_softmax_sum.stride()}" + ) + + (S, B, H, N) = (4096, 1, 6144, 48) + DS = 2 * S + DTYPE_ATTN = torch.bfloat16 + DTYPE = torch.float32 + F32_BLK_SIZE = 8 + for i in range(1): + print("prove_forward_update round:", i) + prev_attn_out = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + prev_softmax_max = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + prev_softmax_sum = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_attn_out = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + cur_softmax_max = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_softmax_sum = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + + prev_attn_out_s = prev_attn_out.view(2, S, B, H)[1] + prev_softmax_max_s = prev_softmax_max.view(B, N, 2, S, 8)[:, :, 1, :, :] + prev_softmax_sum_s = prev_softmax_sum.view(B, N, 2, S, 8)[:, :, 1, :, :] + cur_attn_out_s = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + cur_softmax_max_s = torch.rand((B, N, S), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_softmax_sum_s = torch.rand((B, N, S), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + + print("--------------------prove_forward_update:2S----------------------") + data_validation(prev_softmax_max, cur_softmax_max, prev_softmax_sum, cur_softmax_sum, prev_attn_out, + cur_attn_out) + + print("--------------------prove_forward_update:1S---------------------- ") + data_validation(prev_softmax_max_s, cur_softmax_max_s, prev_softmax_sum_s, cur_softmax_sum_s, prev_attn_out_s, + cur_attn_out_s) + + +def benchmark_forward_update(): + (S, B, H, N) = (4096, 1, 6144, 48) + DS = 2 * S + DTYPE_ATTN = torch.bfloat16 + DTYPE = torch.float32 + F32_BLK_SIZE = 8 + prev_attn_out = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + prev_softmax_max = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + prev_softmax_sum = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_attn_out = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + cur_softmax_max = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_softmax_sum = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + + prev_attn_out_s = prev_attn_out.view(2, S, B, H)[1] + prev_softmax_max_s = prev_softmax_max.view(B, N, 2, S, 8)[:, :, 1, :, :] + prev_softmax_sum_s = prev_softmax_sum.view(B, N, 2, S, 8)[:, :, 1, :, :] + cur_attn_out_s = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + cur_softmax_max_s = torch.rand((B, N, S), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_softmax_sum_s = torch.rand((B, N, S), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + + benchmark_test( + forward_update, forward_update_triton, + args=(prev_attn_out, prev_softmax_max, prev_softmax_sum, cur_attn_out, cur_softmax_max, cur_softmax_sum), + name="forward_update_2s", times=10, repeat=10) + + benchmark_test( + forward_update, forward_update_triton, + args=(prev_attn_out, prev_softmax_max, prev_softmax_sum, cur_attn_out, cur_softmax_max, cur_softmax_sum), + name="forward_update_s", times=10, repeat=10) + + +def profile_forward_update(): + (S, B, H, N) = (4096, 1, 6144, 48) + # (S, B, H, N) = (2048,1,1536,12) + DS = 2 * S + DTYPE_ATTN = torch.bfloat16 + DTYPE = torch.float32 + F32_BLK_SIZE = 8 + prev_attn_out = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + prev_softmax_max = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + prev_softmax_sum = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_attn_out = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + cur_softmax_max = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_softmax_sum = torch.rand((B, N, DS), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + + prev_attn_out_s = prev_attn_out.view(2, S, B, H)[1] + prev_softmax_max_s = prev_softmax_max.view(B, N, 2, S, 8)[:, :, 1, :, :] + prev_softmax_sum_s = prev_softmax_sum.view(B, N, 2, S, 8)[:, :, 1, :, :] + cur_attn_out_s = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + cur_softmax_max_s = torch.rand((B, N, S), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + cur_softmax_sum_s = torch.rand((B, N, S), dtype=DTYPE).npu().unsqueeze(3).repeat(1, 1, 1, F32_BLK_SIZE) + + profile_test( + forward_update, forward_update_triton, + args=(prev_attn_out, prev_softmax_max, prev_softmax_sum, cur_attn_out, cur_softmax_max, cur_softmax_sum), + name="forward_update_2s", times=10, repeat=10) + + profile_test( + forward_update, forward_update_triton, + args=(prev_attn_out, prev_softmax_max, prev_softmax_sum, cur_attn_out, cur_softmax_max, cur_softmax_sum), + name="forward_update_s", times=10, repeat=10) + + +def backward_update(dq, dk, dv, cur_dq, cur_dk, cur_dv, i=7, rank=1): + cp_size = 8 + if i >= cp_size - rank - 1: + if i == cp_size - 1: + cur_dq = cur_dq.view(dq.shape) + cur_dk = cur_dk.view(dk.shape) + cur_dv = cur_dv.view(dv.shape) + dq.add_(cur_dq) + dk.add_(cur_dk) + dv.add_(cur_dv) + else: + cur_dq = cur_dq.view(dq.shape) + dq.add_(cur_dq) + dk[0].add_(cur_dk) + dv[0].add_(cur_dv) + else: + dq[1].add_(cur_dq) + cur_dk = cur_dk.view(dk.shape) # [2s, b, h] -> [2, s, b, h] + cur_dv = cur_dv.view(dv.shape) + dk.add_(cur_dk) + dv.add_(cur_dv) + + +def prove_backward_update(): + (S, B, H) = (16384, 1, 1536) + DS = 2 * S + DTYPE_ATTN = torch.bfloat16 + DTYPE = torch.float32 + + d_dq = torch.randn((2, S, B, H), dtype=DTYPE_ATTN).npu() + cur_dq = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + d_cur_dq = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + + d_dk = torch.randn((2, S, B, H), dtype=DTYPE_ATTN).npu() + cur_dk = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + d_cur_dk = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + + d_dv = torch.randn((2, S, B, H), dtype=DTYPE_ATTN).npu() + cur_dv = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + d_cur_dv = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + + def data_validate(dq, dk, dv, cur_dq, cur_dk, cur_dv, i, rank): + dq_c = dq.detach().clone() + dk_c = dk.detach().clone() + dv_c = dv.detach().clone() + + if (i == 7): + backward_update(dq, dk, dv, cur_dq, cur_dk, cur_dv, i, rank) + backward_update_triton(dq_c, dk_c, dv_c, cur_dq, cur_dk, cur_dv) + elif (i == 6): + backward_update(dq, dk, dv, cur_dq, cur_dk, cur_dv, i, rank) + backward_update_triton(dq_c, dk_c[0], dv_c[0], cur_dq, cur_dk, cur_dv) + else: + backward_update(dq, dk, dv, cur_dq, cur_dk, cur_dv, i, rank) + backward_update_triton(dq_c[1], dk_c, dv_c, cur_dq, cur_dk, cur_dv) + + torch.testing.assert_close(dq, dq_c) + print("dq comparison passed") + torch.testing.assert_close(dk, dk_c) + print("dk comparison passed") + torch.testing.assert_close(dv, dv_c) + print("passed comparison ") + + print("--------------------prove_backward_update case 0----------------------") + data_validate(d_dq, d_dk, d_dv, d_cur_dq, d_cur_dk, d_cur_dv, 7, 1) + print("--------------------prove_backward_update case 1----------------------") + data_validate(d_dq, d_dk, d_dv, d_cur_dq, cur_dk, cur_dv, 6, 1) + print("--------------------prove_backward_update case 2----------------------") + data_validate(d_dq, d_dk, d_dv, cur_dq, d_cur_dk, d_cur_dv, 5, 1) + + +def benchmark_backward_update(): + (S, B, H) = (16384, 1, 1536) + DS = 2 * S + DTYPE_ATTN = torch.bfloat16 + DTYPE = torch.float32 + + d_dq = torch.randn((2, S, B, H), dtype=DTYPE_ATTN).npu() + cur_dq = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + d_cur_dq = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + + d_dk = torch.randn((2, S, B, H), dtype=DTYPE_ATTN).npu() + cur_dk = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + d_cur_dk = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + + d_dv = torch.randn((2, S, B, H), dtype=DTYPE_ATTN).npu() + cur_dv = torch.randn((S, B, H), dtype=DTYPE_ATTN).npu() + d_cur_dv = torch.randn((DS, B, H), dtype=DTYPE_ATTN).npu() + # benchmark case 0 + benchmark_test(backward_update, backward_update_triton, args=(d_dq, d_dk, d_dv, d_cur_dq, d_cur_dk, d_cur_dv), + name="backward_update_0") + + +def forward_update_la(prev_attn_out, prev_softmax_log_max_sum, cur_attn_out, cur_softmax_log_max_sum): + if prev_attn_out is None: + return cur_attn_out, cur_softmax_log_max_sum + softmax_log_max_sum = torch.log(torch.exp(cur_softmax_log_max_sum) + torch.exp(prev_softmax_log_max_sum)) + attn_out = torch.exp(prev_softmax_log_max_sum - softmax_log_max_sum) * prev_attn_out + torch.exp( + cur_softmax_log_max_sum - softmax_log_max_sum) * cur_attn_out + return attn_out, softmax_log_max_sum + + +# simuldate origin code :call foward_update then call copy +def forward_update_copy(prev_attn_out, prev_softmax_log_max_sum, cur_attn_out, cur_softmax_log_max_sum): + attn_out, softmax = forward_update_la(prev_attn_out, prev_softmax_log_max_sum, cur_attn_out, + cur_softmax_log_max_sum) + prev_attn_out.copy_(attn_out) + prev_softmax_log_max_sum.copy_(softmax) + + +def prove_forward_update_la(): + + def data_validation(prev_attn_out, prev_softmax_sum, cur_attn_out, cur_softmax_sum): + + (attn_out, softmax_sum) = forward_update_la(prev_attn_out, prev_softmax_sum, cur_attn_out, cur_softmax_sum) + forward_update_triton_la(prev_attn_out, prev_softmax_sum, cur_attn_out, cur_softmax_sum) + + try: + torch.testing.assert_close(softmax_sum, prev_softmax_sum) + print("softmax comparition passed.") + torch.testing.assert_close(attn_out, prev_attn_out) + print("attn comparition passed.") + + except Exception as e: + print(e) + print("comparison not passed") + + print( + f"proving finished, attn shape:{prev_attn_out.shape}, stride:{prev_attn_out.stride(), cur_attn_out.stride()}, softmax shape:{prev_softmax_sum.shape}, stride:{prev_softmax_sum.stride(), cur_softmax_sum.stride()}" + ) + + (S, B, N, D) = (4096, 1, 48, 128) + DS = 2 * S + DTYPE_ATTN = torch.float32 + DTYPE = torch.float32 + + for i in range(1): + print("round:", i) + attn_out = torch.randn((B, N, DS, D), dtype=DTYPE_ATTN).npu() + softmax_sum = torch.rand((B, N, DS, 1), dtype=DTYPE).npu() + + cur_attn_out = torch.randn((B, N, DS, D), dtype=DTYPE_ATTN).npu() + cur_softmax_sum = torch.rand((B, N, DS, 1), dtype=DTYPE).npu() + + cur_attn_out_s = torch.randn((B, N, S, D), dtype=DTYPE_ATTN).npu() + cur_softmax_sum_s = torch.rand((B, N, S, 1), dtype=DTYPE).npu() + + print("---------------------prove_forward_updat_la 2S-------------------------------") + data_validation(attn_out, softmax_sum, cur_attn_out, cur_softmax_sum) + + print("--------------------prove_forward_update_la 1S------------------------------") + # [b, n, 2s, d] -> [b, n, 2, s, d] + attn_out_s = attn_out.view(*attn_out.shape[:2], 2, attn_out.shape[2] // 2, attn_out.shape[-1]) + # [b, n, 2s, 1] -> [b, n, 2, s, 1] + softmax_sum_s = softmax_sum.view(*softmax_sum.shape[:2], 2, softmax_sum.shape[2] // 2, softmax_sum.shape[-1]) + + data_validation(attn_out_s[:, :, 1], softmax_sum_s[:, :, 1], cur_attn_out_s, cur_softmax_sum_s) + + +def benchmark_forward_update_la(): + (S, B, N, D) = (4096, 1, 48, 128) + DS = 2 * S + DTYPE_ATTN = torch.float32 + DTYPE = torch.float32 + + attn_out = torch.randn((B, N, DS, D), dtype=DTYPE_ATTN).npu() + softmax_sum = torch.rand((B, N, DS, 1), dtype=DTYPE).npu() + cur_attn_out = torch.randn((B, N, DS, D), dtype=DTYPE_ATTN).npu() + cur_softmax_sum = torch.rand((B, N, DS, 1), dtype=DTYPE).npu() + cur_attn_out_s = torch.randn((B, N, S, D), dtype=DTYPE_ATTN).npu() + cur_softmax_sum_s = torch.rand((B, N, S, 1), dtype=DTYPE).npu() + attn_out_s = attn_out.view(*attn_out.shape[:2], 2, attn_out.shape[2] // 2, attn_out.shape[-1]) + softmax_sum_s = softmax_sum.view(*softmax_sum.shape[:2], 2, softmax_sum.shape[2] // 2, softmax_sum.shape[-1]) + + benchmark_test(forward_update_copy, forward_update_triton_la, + args=(attn_out_s[:, :, 1], softmax_sum_s[:, :, 1], cur_attn_out_s, cur_softmax_sum_s), + name="forward_update_la", profile=False, repeat=1000) + + +@pytest.mark.skip(reason="attn_cp") +def test_prove_forward_update(): + prove_forward_update() + + +@pytest.mark.skip(reason="attn_cp") +def test_prove_forward_update_la(): + prove_forward_update_la() + + +@pytest.mark.skip(reason="attn_cp") +def test_prove_backward_update(): + prove_backward_update() + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/third_party/ascend/unittest/pytest_ut/test_backend_npu_utils_build_flags.py b/third_party/ascend/unittest/pytest_ut/test_backend_npu_utils_build_flags.py new file mode 100644 index 0000000000..236c5e4384 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_backend_npu_utils_build_flags.py @@ -0,0 +1,285 @@ +import builtins +import hashlib +import importlib.util +import os +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +pytestmark = pytest.mark.backend("none") + +DEFAULT_UTILS_PATH = (Path(__file__).resolve().parents[2] / "backend" / "utils.py") +DEFAULT_REGISTER_PATH = (Path(__file__).resolve().parents[2] / "backend" / "backend_register.py") +DEFAULT_DRIVER_PATH = (Path(__file__).resolve().parents[2] / "backend" / "driver.py") + + +def _get_utils_path(): + override = os.environ.get("TRITON_ASCEND_UTILS_UNDER_TEST") + if override: + return Path(override) + return DEFAULT_UTILS_PATH + + +def _load_utils_module(): + utils_path = _get_utils_path() + spec = importlib.util.spec_from_file_location("repo_backend_utils", utils_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _load_register_module(): + spec = importlib.util.spec_from_file_location("repo_backend_register", DEFAULT_REGISTER_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _load_driver_module(): + spec = importlib.util.spec_from_file_location("repo_backend_driver", DEFAULT_DRIVER_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _guard_torch_npu_import(monkeypatch): + monkeypatch.delitem(sys.modules, "torch_npu", raising=False) + real_import = builtins.__import__ + + def guarded_import(name, *args, **kwargs): + if name == "torch_npu" or name.startswith("torch_npu."): + raise AssertionError(f"unexpected import of {name}") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", guarded_import) + + +def _assert_npu_utils_uses_special_flags(utils, monkeypatch, tmp_path): + monkeypatch.setattr(utils, "_get_cxx", lambda: "c++") + monkeypatch.setattr(utils, "_get_ascend_path", lambda: str(tmp_path / "ascend")) + monkeypatch.setattr(utils.pybind11, "get_include", lambda: "/pybind11") + monkeypatch.setattr(utils.sysconfig, "get_config_var", lambda name: ".so") + monkeypatch.setattr(utils.sysconfig, "get_default_scheme", lambda: "posix_prefix", raising=False) + monkeypatch.setattr(utils.sysconfig, "get_paths", lambda scheme=None: {"include": "/pyinclude"}) + + calls = [] + + def fake_get_backend_func(name, *args, **kwargs): + calls.append((name, args, kwargs)) + if name == "get_cc_cmd_npu_utils": + return ["-DUSE_TORCH_NPU"] + if name == "get_cc_cmd": + return ["-ldl"] + return [] + + monkeypatch.setattr(utils, "get_backend_func", fake_get_backend_func) + monkeypatch.setattr( + utils.subprocess, + "run", + lambda *args, **kwargs: SimpleNamespace(returncode=0, stderr=""), + ) + + src_path = tmp_path / "npu_utils.cpp" + src_path.write_text("int main() { return 0; }\n") + + so_path = utils._build_npu_ext("npu_utils", str(src_path), kernel_launcher="torch") + + assert so_path.endswith(".so") + assert any(name == "get_cc_cmd_npu_utils" for name, _, _ in calls) + assert not any(name == "get_cc_cmd" for name, _, _ in calls) + + +def test_npu_utils_build_uses_special_flags(monkeypatch, tmp_path): + utils = _load_utils_module() + _assert_npu_utils_uses_special_flags(utils, monkeypatch, tmp_path) + + +def test_get_backend_func_uses_torch_npu_without_import(monkeypatch): + utils = _load_utils_module() + monkeypatch.delenv("TRITON_BACKEND", raising=False) + monkeypatch.setattr(utils, "backend_policy", None) + _guard_torch_npu_import(monkeypatch) + + calls = [] + + def fake_execute_func(category, method, *args, **kwargs): + calls.append((category, method, args, kwargs)) + return "ok" + + monkeypatch.setattr( + utils.backend_strategy_registry, + "execute_func", + fake_execute_func, + ) + + assert utils.get_backend_func("get_cc_cmd_npu_utils") == "ok" + assert calls == [("torch_npu", "get_cc_cmd_npu_utils", (), {})] + assert "torch_npu" not in sys.modules + + +def test_get_cc_cmd_npu_utils_resolves_torch_npu_path_without_import(monkeypatch, tmp_path): + register = _load_register_module() + _guard_torch_npu_import(monkeypatch) + monkeypatch.setattr(register, "get_torch_cxx_abi", lambda: 1) + + torch_path = tmp_path / "torch_pkg" + torch_npu_path = tmp_path / "torch_npu_pkg" + + def fake_find_spec(name): + if name == "torch": + return SimpleNamespace( + origin=str(torch_path / "__init__.py"), + submodule_search_locations=[str(torch_path)], + ) + if name == "torch_npu": + return SimpleNamespace( + origin=str(torch_npu_path / "__init__.py"), + submodule_search_locations=[str(torch_npu_path)], + ) + return None + + monkeypatch.setattr(register.importlib.util, "find_spec", fake_find_spec) + + cc_cmd = register.get_cc_cmd_npu_utils() + + assert f"-I{torch_path / 'include'}" in cc_cmd + assert f"-I{torch_npu_path / 'include'}" in cc_cmd + assert f"-L{torch_npu_path / 'lib'}" in cc_cmd + assert "torch_npu" not in sys.modules + + +def test_npu_utils_initialization_refreshes_build_path_without_loading(monkeypatch, tmp_path): + driver = _load_driver_module() + producer_cache = tmp_path / "producer" + consumer_cache = tmp_path / "consumer" + build_calls = [] + + def fake_build(npu_utils): + cache_root = os.environ["TRITON_CACHE_DIR"] + build_calls.append((npu_utils, cache_root)) + return str(Path(cache_root) / "npu_utils.so") + + monkeypatch.setattr(driver.NPUUtils, "_build_or_get_cached_so", fake_build) + + monkeypatch.setenv("TRITON_CACHE_DIR", str(producer_cache)) + npu_utils = driver.NPUUtils() + assert npu_utils._cache_path == str(producer_cache / "npu_utils.so") + assert npu_utils.npu_utils_mod is None + + monkeypatch.setenv("TRITON_CACHE_DIR", str(consumer_cache)) + assert driver.NPUUtils() is npu_utils + assert npu_utils._cache_path == str(consumer_cache / "npu_utils.so") + assert npu_utils.npu_utils_mod is None + assert build_calls == [ + (npu_utils, str(producer_cache)), + (npu_utils, str(consumer_cache)), + ] + + +def test_npu_utils_load_binary_requires_explicit_mix_mode(monkeypatch, tmp_path): + driver = _load_driver_module() + cached_so = tmp_path / "npu_utils.so" + monkeypatch.setattr(driver.NPUUtils, "_build_or_get_cached_so", lambda self: str(cached_so)) + + calls = [] + expected = ("module", "function", 0, 0, 1) + fake_mod = SimpleNamespace(load_kernel_binary=lambda *args: calls.append(args) or expected, ) + npu_utils = driver.NPUUtils() + monkeypatch.setattr(npu_utils, "_load_mod", lambda: fake_mod) + + assert npu_utils.load_binary("vector_add_kernel", b"kernel", 1, 0, "aiv") == expected + assert calls == [("vector_add_kernel", b"kernel", 1, 0, "aiv")] + with pytest.raises(TypeError, match="missing 1 required positional argument: 'mix_mode'"): + npu_utils.load_binary("vector_add_kernel", b"kernel", 1, 0) + + +def test_npu_utils_initialization_builds_and_caches_shared_object(monkeypatch, tmp_path): + driver = _load_driver_module() + cache_dir = tmp_path / "cache" + cache_dir.mkdir() + cached_so = cache_dir / "npu_utils.so" + built_so = tmp_path / "built_npu_utils.so" + built_so.write_bytes(b"built") + + class FakeCache: + + def __init__(self): + self.get_file_calls = 0 + self.put_calls = 0 + + def get_file(self, filename): + self.get_file_calls += 1 + return None + + def put(self, data, filename, binary=True): + self.put_calls += 1 + assert data == b"built" + assert filename == "npu_utils.so" + assert binary is True + cached_so.write_bytes(data) + return str(cached_so) + + fake_cache = FakeCache() + monkeypatch.setattr(driver, "get_cache_manager", lambda key: fake_cache) + monkeypatch.setattr(driver, "_build_npu_ext", lambda *args, **kwargs: str(built_so)) + + npu_utils = driver.NPUUtils() + + assert npu_utils._cache_path == str(cached_so) + assert npu_utils.npu_utils_mod is None + assert cached_so.read_bytes() == b"built" + assert fake_cache.get_file_calls == 1 + assert fake_cache.put_calls == 1 + + +def test_npu_utils_cache_key_uses_cann_torch_npu_version_and_source(monkeypatch, tmp_path): + driver = _load_driver_module() + _guard_torch_npu_import(monkeypatch) + cached_so = tmp_path / "npu_utils.so" + cached_so.write_bytes(b"cached") + captured_keys = [] + version_calls = [] + + class FakeCache: + + def get_file(self, filename): + assert filename == "npu_utils.so" + return str(cached_so) + + monkeypatch.setattr(driver, "get_cann_version", lambda: (9, 0, 0)) + monkeypatch.setattr( + driver.importlib.metadata, + "version", + lambda name: version_calls.append(name) or "2.7.1.post5.dev20260622", + ) + monkeypatch.setattr(driver, "get_cache_manager", lambda key: captured_keys.append(key) or FakeCache()) + + npu_utils = driver.NPUUtils() + source = (DEFAULT_DRIVER_PATH.parent / "npu_utils.cpp").read_text() + expected_key = hashlib.md5("\0".join(["9.0.0", "2.7.1.post5.dev20260622", source]).encode("utf-8")).hexdigest() + + assert npu_utils.get_so_path() == str(cached_so) + assert version_calls == ["torch_npu"] + assert captured_keys == [expected_key] + assert "torch_npu" not in sys.modules + + +@pytest.mark.parametrize( + ("arch", "raw_ub_kib", "graph_budget_bytes"), + ( + ("Ascend910B1", 192, 192 * 1024 * 80 // 100), + ("Ascend910_9581", 256, 256 * 1024 * 80 // 100), + ("Ascend950A3", 256, 256 * 1024 * 80 // 100), + ("", 0, 0), + ("unknown-arch", 0, 0), + (None, 0, 0), + ), +) +def test_graph_ub_budget_resolves_from_explicit_arch(arch, raw_ub_kib, graph_budget_bytes): + """The compiler-side resolver must not depend on the active NPU device.""" + utils = _load_utils_module() + + assert utils.ub_size_in_kbytes_for_arch(arch) == raw_ub_kib + assert utils.graph_ub_budget_bytes_for_arch(arch) == graph_budget_bytes diff --git a/third_party/ascend/unittest/pytest_ut/test_bind_buffer.py b/third_party/ascend/unittest/pytest_ut/test_bind_buffer.py new file mode 100644 index 0000000000..cefecb9e39 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_bind_buffer.py @@ -0,0 +1,68 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +#!/usr/bin/env python3 +import os + +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +@triton.jit +def bind_buffer(): + alloc = bl.alloc(tl.float32, [32, 32], al.ascend_address_space.UB) + tensor = tl.full((32, 32), 0, dtype=tl.float32) + bl.to_buffer(tensor, bind_buffer=alloc) + + +# ============== Main for manual testing ============== + +if __name__ == "__main__": + mlir = compile_kernel(bind_buffer, {}, {}) + assert len(mlir) > 0 + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_block_ptr.py b/third_party/ascend/unittest/pytest_ut/test_block_ptr.py new file mode 100644 index 0000000000..e8132a4508 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_block_ptr.py @@ -0,0 +1,195 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, y_ptr, z_ptr, output_ptr1, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + block_ptr_in = tl.make_block_ptr( + base=x_ptr, + shape=(XB, YB, ZB), + strides=(YB * ZB, ZB, 1), + offsets=(0, 0, 0), + block_shape=(XB, YB, ZB), + order=(2, 1, 0), + ) + X = tl.load(block_ptr_in) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + block_ptr_out = tl.make_block_ptr( + base=output_ptr, + shape=(XB, YB, ZB), + strides=(YB * ZB, ZB, 1), + offsets=(0, 0, 0), + block_shape=(XB, YB, ZB), + order=(2, 1, 0), + ) + tl.store(block_ptr_out, X) + + +paras = [ + ('*fp32', eval('torch.float32'), 2, 256, 16), + ('*fp32', eval('torch.float32'), 8, 8, 4), + ('*fp16', eval('torch.float16'), 2, 256, 16), + ('*fp16', eval('torch.float16'), 8, 8, 4), + ('*i8', eval('torch.int8'), 2, 256, 16), + ('*i8', eval('torch.int8'), 8, 8, 4), +] + + +@pytest.mark.parametrize('para_type,data_type,XB,YB,ZB', paras) +def test_npu(para_type, data_type, XB, YB, ZB): + + x = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + y = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + z = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + + print(f"shape = {x.shape}") + print(x.dtype) + + output = torch.randint(1, (XB, YB, ZB), dtype=data_type).npu() + output1 = output + print(f"output.dtype={output.dtype}") + + a = x + print(a) + fn_npu_[1, 1, 1](output, x, y, z, output1, XB=XB, YB=YB, ZB=ZB, debug=True) + print(output) + torch.testing.assert_close(output, a) + + +@triton.jit +def dma_block_ptr( + input_ptr, + output_ptr, + scale_ptr, + batch_size, + cu_seqlens_ptr, + stride_i_m, + stride_i_n, + stride_o_m, + stride_o_n, + stride_s_b, + HEAD_DIM, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, +): + n_progs = tl.num_programs(0) + pid = tl.program_id(0) + + cu_num_blocks = 0 + for bid in range(batch_size): + start_loc = tl.load(cu_seqlens_ptr + bid) + end_loc = tl.load(cu_seqlens_ptr + bid + 1) + scale = tl.load(scale_ptr + bid * stride_s_b) + + len_loc = end_loc - start_loc + prev_num_blocks = cu_num_blocks + new_num_blocks = tl.cdiv(len_loc, BLOCK_SIZE_M).to(tl.int32) + i_block_ptr_bbase = tl.make_block_ptr( + input_ptr + start_loc * stride_i_m, + shape=(len_loc, HEAD_DIM), + strides=(stride_i_m, stride_i_n), + offsets=(0, 0), + block_shape=(BLOCK_SIZE_M, BLOCK_SIZE_N), + order=(1, 0), + ) + o_block_ptr_bbase = tl.make_block_ptr( + output_ptr + start_loc * stride_o_m, + shape=(len_loc, HEAD_DIM), + strides=(stride_o_m, stride_o_n), + offsets=(0, 0), + block_shape=(BLOCK_SIZE_M, BLOCK_SIZE_N), + order=(1, 0), + ) + cu_num_blocks += new_num_blocks + for m_id in range((prev_num_blocks + pid) % n_progs, new_num_blocks, n_progs): + i_block_ptr = tl.advance(i_block_ptr_bbase, (m_id * BLOCK_SIZE_M, 0)) + o_block_ptr = tl.advance(o_block_ptr_bbase, (m_id * BLOCK_SIZE_M, 0)) + i_tile = tl.load(i_block_ptr, boundary_check=[0, 1], padding_option="zero") + o_tile = i_tile.to(tl.float32) * scale + tl.store(o_block_ptr, o_tile.to(i_tile.dtype), boundary_check=[0, 1]) + + +def ref_func(inputs, scale, cu_lens): + outputs = torch.zeros_like(inputs) + bsz = cu_lens.size(0) - 1 + for bid in range(bsz): + tmp = inputs[cu_lens[bid]:cu_lens[bid + 1]].to(torch.float32) * scale[bid] + outputs[cu_lens[bid]:cu_lens[bid + 1]] = tmp.to(outputs.dtype) + return outputs + + +def tt_func(inputs, scale, cu_lens): + bsz = cu_lens.size(0) - 1 + outputs = torch.zeros_like(inputs) + head_dim = inputs.size(-1) + assert head_dim <= 1024 + BLOCK_SIZE_N = 1024 + BLOCK_SIZE_M = 4 + dma_block_ptr[ + 20, + ]( + inputs, + outputs, + scale, + bsz, + cu_lens, + inputs.stride(0), + inputs.stride(1), + outputs.stride(0), + outputs.stride(1), + scale.stride(0), + head_dim, + BLOCK_SIZE_M=BLOCK_SIZE_M, + BLOCK_SIZE_N=BLOCK_SIZE_N, + ) + return outputs + + +@pytest.mark.parametrize('param_list', [ + [8, 1024, 1024, True], + [8, 1024, 1024, False], +]) +def test_func(param_list): + bsz, max_len, max_n, test_align = param_list + lens = torch.randint(max_len // 2, max_len, (bsz, ), dtype=torch.int32, device="npu") + n = torch.randint(max_n // 2, max_n, (1, ), dtype=torch.int32, device="npu")[0].item() + if test_align: + lens = (lens + 1023) // 1024 * 1024 + n = (n + 1023) // 1024 * 1024 + cu_lens = torch.cumsum(lens, dim=0) + cu_lens = torch.cat([torch.zeros(1, dtype=torch.int32, device="npu"), cu_lens], dim=0) + inputs = torch.randn(cu_lens[-1], n, dtype=torch.float16, device="npu") + scale = torch.randn(bsz, dtype=torch.float32, device="npu") + ref_output = ref_func(inputs, scale, cu_lens) + tt_output = tt_func(inputs, scale, cu_lens) + torch.testing.assert_close(ref_output, tt_output) diff --git a/third_party/ascend/unittest/pytest_ut/test_bool_masked_store_cache.py b/third_party/ascend/unittest/pytest_ut/test_bool_masked_store_cache.py new file mode 100644 index 0000000000..65c0fbe942 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_bool_masked_store_cache.py @@ -0,0 +1,54 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + + +@triton.jit +def bool_masked_store_cache_kernel(x_ptr, y_ptr, mask_ptr): + offsets = tl.program_id(0) + tl.arange(0, 1) + + mask = tl.load(mask_ptr + offsets) != 0 + value = tl.load(x_ptr + offsets) + + tl.store( + y_ptr + offsets, + value, + mask=mask, + cache_modifier=".cg", + eviction_policy="evict_first", + ) + + +@pytest.mark.functiontest +def test_bool_masked_store_with_cache_modifier_and_eviction_policy(): + x = torch.tensor([True], dtype=torch.bool).npu() + y = torch.tensor([False], dtype=torch.bool).npu() + mask = torch.tensor([True], dtype=torch.bool).npu() + + bool_masked_store_cache_kernel[(1, )](x, y, mask) + + torch.npu.synchronize() + + assert y.cpu().item() is True diff --git a/third_party/ascend/unittest/pytest_ut/test_boundary_check.py b/third_party/ascend/unittest/pytest_ut/test_boundary_check.py new file mode 100644 index 0000000000..4e6e8d2eeb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_boundary_check.py @@ -0,0 +1,375 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import triton +import triton.language as tl +import pytest + + +# ========== Test 1: Static base address + boundary_check ========== +@triton.jit +def static_base_boundary_check_kernel( + out_ptr, + in_ptr, + BLOCK_SIZE: tl.constexpr, +): + ptr = tl.make_block_ptr(base=in_ptr, shape=(BLOCK_SIZE * 2, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE, ), order=(0, )) + data = tl.load(ptr, boundary_check=(0, ), padding_option="zero") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def ref_static_base(in_tensor, BLOCK_SIZE): + return in_tensor[:BLOCK_SIZE].sum().item() + + +def test_static_base(): + BLOCK_SIZE = 64 + in_tensor = torch.randn(BLOCK_SIZE * 2, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + static_base_boundary_check_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + BLOCK_SIZE=BLOCK_SIZE, + ) + expected = ref_static_base(in_tensor.cpu(), BLOCK_SIZE) + assert torch.allclose(out_tensor.cpu(), torch.tensor(expected, device='cpu'), atol=1e-4) + + +# ========== Test 2: Simple dynamic base address + boundary_check ========== +@triton.jit +def simple_dynamic_base_boundary_check_kernel( + out_ptr, + in_ptr, + offset: tl.int32, + BLOCK_SIZE: tl.constexpr, +): + base = in_ptr + offset + ptr = tl.make_block_ptr(base=base, shape=(BLOCK_SIZE * 2, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE, ), order=(0, )) + data = tl.load(ptr, boundary_check=(0, ), padding_option="zero") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def test_simple_dynamic_base(): + BLOCK_SIZE = 64 + offset = 32 + in_tensor = torch.randn(BLOCK_SIZE * 4, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + simple_dynamic_base_boundary_check_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + offset=offset, + BLOCK_SIZE=BLOCK_SIZE, + ) + expected = in_tensor.cpu()[offset:offset + BLOCK_SIZE].sum().item() + assert torch.allclose(out_tensor.cpu(), torch.tensor(expected, device='cpu'), atol=1e-4) + + +# ========== Test 3: Nested loop + dynamic base address + advance + boundary_check ========== +@triton.jit +def nested_dynamic_advance_boundary_kernel( + out_ptr, + in_ptr, + stride_in: tl.int32, + OUTER_LOOP: tl.constexpr, + INNER_LOOP: tl.constexpr, + BLOCK_SIZE: tl.constexpr, +): + """ + Smallest reproducible code: The dynamic base address is in the outer loop, + and tl.advance is in the inner loop, where there is a boundary_check. + """ + for i in range(OUTER_LOOP): + base = in_ptr + i * stride_in + ptr = tl.make_block_ptr(base=base, shape=(INNER_LOOP * BLOCK_SIZE, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE, ), order=(0, )) + for j in range(INNER_LOOP): + cur_ptr = tl.advance(ptr, (j * BLOCK_SIZE, )) + data = tl.load(cur_ptr, boundary_check=(0, ), padding_option="zero") + result = tl.sum(data) + tl.store(out_ptr + i * INNER_LOOP + j, result) + + +def ref_nested_dynamic(in_tensor, OUTER_LOOP, INNER_LOOP, BLOCK_SIZE): + """ + PyTorch equivalent implementation: + - Treat in_tensor as a tensor of shape [OUTER_LOOP, INNER_LOOP * BLOCK_SIZE] + - For each (i, j) block: take the BLOCK_SIZE elements starting from j*BLOCK_SIZE in the i-th row and sum them up. + - Note: There is boundary_check + zero padding, but there is no out-of-bound access in this case, so no special handling is needed. + """ + reshaped = in_tensor[:OUTER_LOOP * INNER_LOOP * BLOCK_SIZE].view(OUTER_LOOP, INNER_LOOP * BLOCK_SIZE) + blocks = reshaped.unfold(1, BLOCK_SIZE, BLOCK_SIZE) + return blocks.sum(dim=-1).flatten() + + +def test_nested_dynamic(): + BLOCK_SIZE = 8 + OUTER_LOOP = 2 + INNER_LOOP = 2 + in_tensor = torch.randn(OUTER_LOOP * INNER_LOOP * BLOCK_SIZE * 2, dtype=torch.float32).npu() + out_tensor = torch.zeros(OUTER_LOOP * INNER_LOOP, dtype=torch.float32).npu() + nested_dynamic_advance_boundary_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + stride_in=INNER_LOOP * BLOCK_SIZE, + OUTER_LOOP=OUTER_LOOP, + INNER_LOOP=INNER_LOOP, + BLOCK_SIZE=BLOCK_SIZE, + ) + ref = ref_nested_dynamic(in_tensor.cpu(), OUTER_LOOP, INNER_LOOP, BLOCK_SIZE) + assert torch.allclose(out_tensor.cpu(), ref, atol=1e-4) + + +# ========== Test 4: Explicit out-of-bounds access + zero padding + boundary_check ========== +@triton.jit +def out_of_bound_zero_padding_kernel( + out_ptr, + in_ptr, + BLOCK_SIZE: tl.constexpr, +): + ptr = tl.make_block_ptr(base=in_ptr, shape=(BLOCK_SIZE, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE * 2, ), order=(0, )) + data = tl.load(ptr, boundary_check=(0, ), padding_option="zero") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def test_out_of_bound(): + BLOCK_SIZE = 64 + in_tensor = torch.randn(BLOCK_SIZE, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + out_of_bound_zero_padding_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + BLOCK_SIZE=BLOCK_SIZE, + ) + expected = in_tensor.cpu().sum().item() + assert torch.allclose(out_tensor.cpu(), torch.tensor(expected, device='cpu'), atol=1e-4) + + +# ========== Test 5:padding_option = NAN + boundary_check========== +@triton.jit +def nan_padding_kernel( + out_ptr, + in_ptr, + BLOCK_SIZE: tl.constexpr, +): + ptr = tl.make_block_ptr(base=in_ptr, shape=(BLOCK_SIZE, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE * 2, ), order=(0, )) + data = tl.load(ptr, boundary_check=(0, ), padding_option="nan") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def test_nan_padding(): + BLOCK_SIZE = 64 + in_tensor = torch.randn(BLOCK_SIZE, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + try: + nan_padding_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + BLOCK_SIZE=BLOCK_SIZE, + ) + assert torch.isnan(out_tensor.cpu()).any() + except Exception as e: + print(f"Warning: NAN padding test may not be supported: {e}") + + +# ========== Test 6:Multi-layer advance + boundary_check ========== +@triton.jit +def multi_advance_kernel( + out_ptr, + in_ptr, + BLOCK_SIZE: tl.constexpr, +): + base = in_ptr + ptr0 = tl.make_block_ptr(base=base, shape=(BLOCK_SIZE * 4, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE, ), order=(0, )) + ptr1 = tl.advance(ptr0, (BLOCK_SIZE, )) + ptr2 = tl.advance(ptr1, (BLOCK_SIZE, )) + data = tl.load(ptr2, boundary_check=(0, ), padding_option="zero") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def test_multi_advance(): + BLOCK_SIZE = 64 + in_tensor = torch.randn(BLOCK_SIZE * 4, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + multi_advance_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + BLOCK_SIZE=BLOCK_SIZE, + ) + expected = in_tensor.cpu()[2 * BLOCK_SIZE:3 * BLOCK_SIZE].sum().item() + assert torch.allclose(out_tensor.cpu(), torch.tensor(expected, device='cpu'), atol=1e-4) + + +# ========== Test 7:Complex base address calculation + boundary_check ========== +@triton.jit +def complex_base_calculation_kernel( + out_ptr, + in_ptr, + offset1: tl.int32, + offset2: tl.int32, + scale: tl.int32, + BLOCK_SIZE: tl.constexpr, +): + base = in_ptr + offset1 * scale + offset2 + ptr = tl.make_block_ptr(base=base, shape=(BLOCK_SIZE * 2, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE, ), order=(0, )) + data = tl.load(ptr, boundary_check=(0, ), padding_option="zero") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def test_complex_base(): + BLOCK_SIZE = 64 + offset1, offset2, scale = 2, 16, 32 + total_offset = offset1 * scale + offset2 + in_tensor = torch.randn(total_offset + BLOCK_SIZE * 2, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + complex_base_calculation_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + offset1=offset1, + offset2=offset2, + scale=scale, + BLOCK_SIZE=BLOCK_SIZE, + ) + expected = in_tensor.cpu()[total_offset:total_offset + BLOCK_SIZE].sum().item() + assert torch.allclose(out_tensor.cpu(), torch.tensor(expected, device='cpu'), atol=1e-4) + + +# ========== Test 8: padding_option="" (no padding, in-bounds) + boundary_check ========== +@triton.jit +def no_padding_in_bounds_kernel( + out_ptr, + in_ptr, + BLOCK_SIZE: tl.constexpr, +): + ptr = tl.make_block_ptr(base=in_ptr, shape=(BLOCK_SIZE * 2, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE, ), order=(0, )) + data = tl.load(ptr, boundary_check=(0, ), padding_option="") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def test_no_padding_in_bounds(): + """ + boundary_check enabled but all accesses are in-bounds (BLOCK_SIZE out of BLOCK_SIZE*2). + With padding_option="", the result should still be correct since no out-of-bounds access occurs. + """ + BLOCK_SIZE = 64 + in_tensor = torch.randn(BLOCK_SIZE * 2, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + no_padding_in_bounds_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + BLOCK_SIZE=BLOCK_SIZE, + ) + expected = in_tensor.cpu()[:BLOCK_SIZE].sum().item() + assert torch.allclose(out_tensor.cpu(), torch.tensor(expected, device='cpu'), atol=1e-4) + + +# ========== Test 9: padding_option="" (no padding, out-of-bounds) + boundary_check ========== +@triton.jit +def no_padding_out_of_bounds_kernel( + out_ptr, + in_ptr, + BLOCK_SIZE: tl.constexpr, +): + ptr = tl.make_block_ptr(base=in_ptr, shape=(BLOCK_SIZE, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE * 2, ), order=(0, )) + data = tl.load(ptr, boundary_check=(0, ), padding_option="") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def test_no_padding_out_of_bounds(): + """ + boundary_check enabled with actual out-of-bounds access (BLOCK_SIZE*2 load from BLOCK_SIZE data). + With padding_option="", out-of-bounds values are undefined — the kernel should still compile and + not crash. + """ + BLOCK_SIZE = 64 + in_tensor = torch.randn(BLOCK_SIZE, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + # Should compile and run without error + no_padding_out_of_bounds_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + BLOCK_SIZE=BLOCK_SIZE, + ) + + +# ========== Test 10: padding_option="" with dynamic base + boundary_check ========== +@triton.jit +def no_padding_dynamic_base_kernel( + out_ptr, + in_ptr, + offset: tl.int32, + BLOCK_SIZE: tl.constexpr, +): + base = in_ptr + offset + ptr = tl.make_block_ptr(base=base, shape=(BLOCK_SIZE * 2, ), strides=(1, ), offsets=(0, ), + block_shape=(BLOCK_SIZE, ), order=(0, )) + data = tl.load(ptr, boundary_check=(0, ), padding_option="") + result = tl.sum(data) + tl.store(out_ptr, result) + + +def test_no_padding_dynamic_base(): + """ + Same as test_no_padding_in_bounds but with a dynamic base address (offset). + All accesses are in-bounds, so results should match the reference. + """ + BLOCK_SIZE = 64 + offset = 32 + in_tensor = torch.randn(BLOCK_SIZE * 4, dtype=torch.float32).npu() + out_tensor = torch.zeros(1, dtype=torch.float32).npu() + no_padding_dynamic_base_kernel[(1, )]( + out_ptr=out_tensor, + in_ptr=in_tensor, + offset=offset, + BLOCK_SIZE=BLOCK_SIZE, + ) + expected = in_tensor.cpu()[offset:offset + BLOCK_SIZE].sum().item() + assert torch.allclose(out_tensor.cpu(), torch.tensor(expected, device='cpu'), atol=1e-4) + + +if __name__ == "__main__": + print("Running all boundary_check tests...") + test_static_base() + test_simple_dynamic_base() + test_nested_dynamic() + test_out_of_bound() + test_nan_padding() + test_multi_advance() + test_complex_base() + test_no_padding_in_bounds() + test_no_padding_out_of_bounds() + test_no_padding_dynamic_base() + print("All tests completed successfully!") diff --git a/third_party/ascend/unittest/pytest_ut/test_broadcast.py b/third_party/ascend/unittest/pytest_ut/test_broadcast.py new file mode 100644 index 0000000000..10b17adc52 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_broadcast.py @@ -0,0 +1,57 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu + +import pytest + +import test_common + +NBLOCKS = 1 +XS = tl.constexpr(2) +YS = tl.constexpr(4) +ZS = tl.constexpr(8) +NUMEL = tl.constexpr(XS * ZS) + + +@triton.jit +def fn_broadcast(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + idx = tl.arange(0, 1)[:, None, None] * N * M + mblk_idx[None, :, None] * N + nblk_idx[None, None, :] + odx = lblk_idx[:, None, None] * N * M + mblk_idx[None, :, None] * N + nblk_idx[None, None, :] + x = tl.load(in_ptr0 + idx) + x1 = tl.load(out_ptr0 + odx) + ret, _ = tl.broadcast(x, x1) + tl.store(out_ptr0 + odx, ret) + + +@pytest.mark.parametrize('dtype', ["bfloat16"]) +def test_broadcast_alltype(dtype): + input = test_common.generate_tensor((1, YS, ZS), dtype).npu() + ans = input.repeat(XS, 1, 1) + output = torch.zeros((XS, YS, ZS), dtype=eval('torch.' + dtype)).npu() + fn_broadcast[1, 1, 1](input, output, XS, YS, ZS) + test_common.validate_cmp(dtype, ans, output) diff --git a/third_party/ascend/unittest/pytest_ut/test_broadcast_op.py b/third_party/ascend/unittest/pytest_ut/test_broadcast_op.py new file mode 100644 index 0000000000..924964e5f7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_broadcast_op.py @@ -0,0 +1,54 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu + +import pytest + +import test_common + +NBLOCKS = 1 +XS = tl.constexpr(2) +YS = tl.constexpr(4) +ZS = tl.constexpr(8) +NUMEL = tl.constexpr(XS * ZS) + + +@triton.jit +def fn_broadcast_to(output_ptr, input_ptr, length): + col_offsets = tl.arange(0, NUMEL) + input = tl.load(input_ptr + col_offsets) + result = input.reshape((XS, 1, ZS)).broadcast_to((XS, YS, ZS)).reshape((XS * YS * ZS)) + brc_col_offsets = tl.arange(0, NUMEL * YS) + tl.store(output_ptr + brc_col_offsets, result) + + +@pytest.mark.parametrize('dtype', ["float32"]) +def test_broadcast_to(dtype): + length = NUMEL + dtype = eval(f"torch.{dtype}") + x = torch.randn((XS, 1, ZS), dtype=dtype).npu() + output = torch.randn((XS, YS, ZS), dtype=dtype).npu() + fn_broadcast_to[NBLOCKS, 1, 1](output, x, length, debug=True) + assert (torch.equal(output, x.repeat(1, YS, 1))) diff --git a/third_party/ascend/unittest/pytest_ut/test_buffer_language.py b/third_party/ascend/unittest/pytest_ut/test_buffer_language.py new file mode 100644 index 0000000000..73c263fbe3 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_buffer_language.py @@ -0,0 +1,227 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + arch = "Ascend910_95" + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {"create_address_space": al.semantic.create_address_space}, + {}) + return str(module) + + +# Test cases for core.py - buffer_type class +@triton.jit +def kernel_buffer_type_operations(M: tl.constexpr, N: tl.constexpr): + # Test buffer allocation (which creates buffer_type instances) + buf_ub = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + buf_l1 = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L1) + + +# Test cases for core.py - buffer class +@triton.jit +def kernel_buffer_class_operations(M: tl.constexpr, N: tl.constexpr): + # Allocate a buffer + buf = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + + # Test __str__ method implicitly + # Test subview method + sub = buf.subview([0, 0], [M // 2, N // 2], [1, 1]) + + # Test to_tensor method + tensor = buf.to_tensor(writable=True) + + +# Test cases for core.py - alloc function +@triton.jit +def kernel_alloc_operations(M: tl.constexpr, N: tl.constexpr): + # Test alloc with different address spaces + buf_ub = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + buf_l1 = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L1) + buf_l0a = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L0A) + buf_l0b = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L0B) + buf_l0c = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L0C) + + # Test alloc with mem_unique flag + buf_unique = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB, is_mem_unique=True) + + # Test alloc with different data types + buf_float16 = bl.alloc(tl.float16, [M, N], al.ascend_address_space.UB) + buf_int32 = bl.alloc(tl.int32, [M, N], al.ascend_address_space.UB) + buf_int16 = bl.alloc(tl.int16, [M, N], al.ascend_address_space.UB) + + +# Test cases for core.py - to_buffer function +@triton.jit +def kernel_to_buffer_operations(M: tl.constexpr, N: tl.constexpr): + # Create tensors of different data types + x_float32 = tl.full([M, N], 0.0, dtype=tl.float32) + x_float16 = tl.full([M, N], 0.0, dtype=tl.float16) + x_int32 = tl.full([M, N], 0, dtype=tl.int32) + + # Test to_buffer with different address spaces + buf_ub = bl.to_buffer(x_float32, al.ascend_address_space.UB) + buf_l1 = bl.to_buffer(x_float32, al.ascend_address_space.L1) + + # Test to_buffer with bind_buffer + buf_bind = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + buf_bound = bl.to_buffer(x_float32, al.ascend_address_space.UB, bind_buffer=buf_bind) + + +# Test cases for core.py - to_tensor function +@triton.jit +def kernel_to_tensor_operations(M: tl.constexpr, N: tl.constexpr): + # Allocate buffers of different data types + buf_float32 = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + buf_float16 = bl.alloc(tl.float16, [M, N], al.ascend_address_space.UB) + + # Test to_tensor with default parameters + tensor_float32 = bl.to_tensor(buf_float32, writable=True) + + # Test to_tensor with writable=False + tensor_readonly = bl.to_tensor(buf_float32, writable=False) + + # Test to_tensor with target shape (must be different from source shape) + tensor_reshaped = bl.to_tensor(buf_float32, writable=True, target_shape=[M * 2, N * 2]) + + +# Test cases for core.py - subview function +@triton.jit +def kernel_subview_operations(M: tl.constexpr, N: tl.constexpr): + # Allocate a buffer + buf = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + + # Test subview function with different parameters + sub1 = bl.subview(buf, [0, 0], [M // 2, N // 2], [1, 1]) + sub2 = bl.subview(buf, [M // 2, 0], [M // 2, N], [1, 1]) + sub3 = bl.subview(buf, [0, N // 2], [M, N // 2], [1, 1]) + + # Test subview with different offset types + # Integer offsets + sub_int = bl.subview(buf, [0, 0], [M // 2, N // 2], [1, 1]) + + +# Test function definitions +def test_buffer_type_operations(): + print("=" * 60) + print("Test 1: Buffer Type Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_buffer_type_operations, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_buffer_class_operations(): + print("=" * 60) + print("Test 2: Buffer Class Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_buffer_class_operations, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_alloc_operations(): + print("=" * 60) + print("Test 3: Alloc Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_alloc_operations, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_to_buffer_operations(): + print("=" * 60) + print("Test 4: to_buffer Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_to_buffer_operations, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_to_tensor_operations(): + print("=" * 60) + print("Test 5: to_tensor Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_to_tensor_operations, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_subview_operations(): + print("=" * 60) + print("Test 6: Subview Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_subview_operations, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +# ============== Main for manual testing ============== +if __name__ == "__main__": + test_buffer_type_operations() + test_buffer_class_operations() + test_alloc_operations() + test_to_buffer_operations() + test_to_tensor_operations() + test_subview_operations() diff --git a/third_party/ascend/unittest/pytest_ut/test_cann_extension.py b/third_party/ascend/unittest/pytest_ut/test_cann_extension.py new file mode 100644 index 0000000000..a2346c6598 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cann_extension.py @@ -0,0 +1,386 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import pytest +import triton +import torch +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.language.extra.cann.extension import index_put, gather_out_to_ub, scatter_ub_to_out +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.compiler.errors import MLIRCompilationError +from triton.backends.ascend import _apply_ascend_patch + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + +_apply_ascend_patch() + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + arch = "Ascend910_9589" + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {"create_address_space": al.semantic.create_address_space}, + {}) + return str(module) + + +# Test cases for core.py +@triton.jit +def kernel_core_functions(M: tl.constexpr, N: tl.constexpr): + # Test buffer allocation with different address spaces + buf_ub = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + buf_l1 = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L1) + buf_l0a = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L0A) + buf_l0b = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L0B) + buf_l0c = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L0C) + + # Test int64 + int64_val = al.int64(1234567890123456789) + + # Test sub_vec_id and sub_vec_num + vec_id = al.sub_vec_id() + vec_num = al.sub_vec_num() + + # Test debug_barrier + al.debug_barrier(al.SYNC_IN_VF.VV_ALL) + + # Test fixpipe (simple usage) + data = tl.full([M, N], 0.0, dtype=tl.float32) + result_fixpipe = al.fixpipe(data, buf_ub) + + +# Test cases for math_ops.py +@triton.jit +def kernel_math_ops(M: tl.constexpr, N: tl.constexpr): + # Test atan2 + x = tl.full([M, N], 1.0, dtype=tl.float32) + y = tl.full([M, N], 1.0, dtype=tl.float32) + result_atan2 = al.atan2(y, x) + + # Test isfinited + result_isfinited = al.isfinited(x) + + # Test finitef + result_finitef = al.finitef(x) + + +# Test cases for vec_ops.py +@triton.jit +def kernel_vec_ops(M: tl.constexpr, N: tl.constexpr): + # Test insert_slice + data = tl.full([M, N], 0.0, dtype=tl.float32) + slice_data = tl.full([M // 2, N // 2], 1.0, dtype=tl.float32) + result_insert = al.insert_slice(data, slice_data, [0, 0], [M // 2, N // 2], [1, 1]) + + # Test extract_slice + result_extract = al.extract_slice(data, [0, 0], [M // 2, N // 2], [1, 1]) + + # Test get_element + result_get = al.get_element(data, [0, 0]) + + # Test cast + data_int = tl.full([M, N], 1, dtype=tl.int32) + result_cast = al.cast(data_int, tl.float32) + + +# Test cases for aux_ops.py +@triton.jit +def kernel_aux_ops(M: tl.constexpr, N: tl.constexpr): + # Test parallel + data = tl.full([M, N], 0.0, dtype=tl.float32) + for _ in al.parallel(M): + pass + + # Test compile_hint + al.compile_hint(data, "core_mode") + + # Test multibuffer + al.multibuffer(data, 2) + + +# Test cases for scope.py +@triton.jit +def kernel_scope(M: tl.constexpr, N: tl.constexpr): + # Test scope with core_mode + data = tl.full([M, N], 0.0, dtype=tl.float32) + with al.scope(core_mode="vector"): + result = data + 1.0 + + +# Test cases for sync operations +@triton.jit +def kernel_sync_operations(M: tl.constexpr, N: tl.constexpr): + # Test sync_block_all + al.sync_block_all("all", 0) + + # Test sync_block_set and sync_block_wait + al.sync_block_set("cube", "vector", 0) + al.sync_block_wait("cube", "vector", 0) + + +# Test cases for custom_op.py +@triton.jit +def kernel_custom_op(M: tl.constexpr, N: tl.constexpr): + # Test custom op functionality (placeholder) + data = tl.full([M, N], 0.0, dtype=tl.float32) + # Actual custom op usage would depend on specific ops + + +@triton.jit +def kernel_index_put_simple(value_ptr, index_ptr, dst_ptr): + # index tile shape: [2] + index_local = tl.arange(0, 2) + x1_local = tl.arange(0, 2)[None, :] # shape=(1,2) + + index_tile = tl.load(index_ptr + index_local) + value_tile = tl.load(value_ptr + index_local[:, None] * 2 + x1_local) + + index_put(ptr=dst_ptr, index=index_tile, value=value_tile, dim=0, index_boundary=4, end_offset=(2, 2), + start_offset=(0, 0), dst_stride=(2, 1)) + + +@triton.jit +def simple_gather_kernel(src_ptr, index_ptr, out_ptr): + # index tile shape: [2,2] + y0_local = tl.arange(0, 2)[:, None] # [0,1] rows + x1_local = tl.arange(0, 2)[None, :] # [0,1] cols + mask = (y0_local < 2) & (x1_local < 2) + + # Load index tile to UB + index = tl.load(index_ptr + y0_local * 2 + x1_local, mask) + + # Call gather_out_to_ub: gather values from src along dim=0 + gathered = gather_out_to_ub(src=src_ptr, index=index, index_boundary=4, dim=0, src_stride=(2, 1), end_offset=(2, 2), + start_offset=(0, 0)) + + tl.store(out_ptr + y0_local * 2 + x1_local, gathered, mask) + + +@triton.jit +def simple_scatter_kernel(value_ptr, index_ptr, dst_ptr): + # index tile shape: [2,2] + y0_local = tl.arange(0, 2)[:, None] # [0,1] rows + x1_local = tl.arange(0, 2)[None, :] # [0,1] cols + mask = (y0_local < 2) & (x1_local < 2) + + value = tl.load(value_ptr + y0_local * 2 + x1_local, mask) + index = tl.load(index_ptr + y0_local * 2 + x1_local, mask) + + scatter_ub_to_out(ptr=dst_ptr, value=value, index=index, index_boundary=4, dim=0, dst_stride=(2, 1), + end_offset=(2, 2), start_offset=(0, 0)) + + +@triton.jit +def simple_scatter_kernel(value_ptr, index_ptr, dst_ptr): + # index tile shape: [2,2] + y0_local = tl.arange(0, 2)[:, None] # [0,1] rows + x1_local = tl.arange(0, 2)[None, :] # [0,1] cols + mask = (y0_local < 2) & (x1_local < 2) + + value = tl.load(value_ptr + y0_local * 2 + x1_local, mask) + index = tl.load(index_ptr + y0_local * 2 + x1_local, mask) + + scatter_ub_to_out(ptr=dst_ptr, value=value, index=index, index_boundary=4, dim=0, dst_stride=(2, 1), + end_offset=(2, 2), start_offset=(0, 0)) + + +# Test function definitions +def test_core_functions(): + print("=" * 60) + print("Test 1: Core Functions") + print("=" * 60) + mlir = compile_kernel( + kernel_core_functions, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_math_ops(): + print("=" * 60) + print("Test 2: Math Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_math_ops, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_mem_ops(): + print("=" * 60) + print("Test 3: Memory Operations (EXPECTED TO FAIL)") + print("=" * 60) + + # 构造输入(必须放在测试内,防止模块加载时报错) + value = torch.randn(3, 3, device='npu').half() + index = torch.tensor([0, 1], device='npu') + dst = torch.zeros(4, 2, device='npu').half() + + # ✅ 核心:把编译+运行的代码包在这里,pytest 才能捕获 + with pytest.warns(FutureWarning, match="index_put"), pytest.raises(MLIRCompilationError): + # 这一行会触发编译,直接抛出你要的错误 + kernel_index_put_simple[(1, )](value, index, dst) + + print("✅ 成功捕获预期的 MLIRCompilationError 错误!测试通过!") + + +def test_mem2_ops(): + print("=" * 60) + print("Test 3: Memory Operations (EXPECTED TO FAIL)") + print("=" * 60) + + # 构造输入(必须放在测试内,防止模块加载时报错) + src = torch.tensor([[1., 2.], [3., 4.], [5., 6.], [7., 8.]], device='npu') + index = torch.tensor([[0, 1], [2, 3]], device='npu') + out = torch.empty((2, 2), device='npu', dtype=torch.float32) + + with pytest.warns(FutureWarning, match="gather_out_to_ub"), pytest.raises(MLIRCompilationError): + simple_gather_kernel[(1, )](src, index, out) + + print("✅ 成功捕获预期的 MLIRCompilationError 错误!测试通过!") + + +def test_mem3_ops(): + print("=" * 60) + print("Test 3: Memory Operations (EXPECTED TO FAIL)") + print("=" * 60) + + # 构造输入(必须放在测试内,防止模块加载时报错) + dst = torch.zeros((4, 2), device='npu', dtype=torch.float32) + value = torch.tensor([[1., 2.], [3., 4.]], device='npu') + index = torch.tensor([[1, 2], [3, 0]], device='npu') + + with pytest.warns(FutureWarning, match="scatter_ub_to_out"), pytest.raises(MLIRCompilationError): + simple_scatter_kernel[(1, )](value, index, dst) + + print("✅ 成功捕获预期的 MLIRCompilationError 错误!测试通过!") + + +def test_mem4_ops(): + print("=" * 60) + print("Test 3: Memory Operations (EXPECTED TO FAIL)") + print("=" * 60) + + # 构造输入(必须放在测试内,防止模块加载时报错) + dst = torch.zeros((4, 2), device='npu', dtype=torch.float32) + value = torch.tensor([[1., 2.], [3., 4.]], device='npu') + index = torch.tensor([[1, 2], [3, 0]], device='npu') + + with pytest.warns(FutureWarning, match="scatter_ub_to_out"), pytest.raises(MLIRCompilationError): + simple_scatter_kernel[(1, )](value, index, dst) + + print("✅ 成功捕获预期的 MLIRCompilationError 错误!测试通过!") + + +def test_vec_ops(): + print("=" * 60) + print("Test 4: Vector Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_vec_ops, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_aux_ops(): + print("=" * 60) + print("Test 5: Auxiliary Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_aux_ops, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_scope(): + print("=" * 60) + print("Test 6: Scope Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_scope, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_sync_operations(): + print("=" * 60) + print("Test 7: Sync Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_sync_operations, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +def test_custom_op(): + print("=" * 60) + print("Test 8: Custom Operations") + print("=" * 60) + mlir = compile_kernel( + kernel_custom_op, + {}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + + +# ============== Main for manual testing ============== +if __name__ == "__main__": + test_core_functions() + test_math_ops() + test_mem_ops() + test_mem2_ops() + test_mem3_ops() + test_mem4_ops() + test_vec_ops() + test_aux_ops() + test_scope() + test_sync_operations() + test_custom_op() diff --git a/third_party/ascend/unittest/pytest_ut/test_cannonicalize_tl_where.py b/third_party/ascend/unittest/pytest_ut/test_cannonicalize_tl_where.py new file mode 100644 index 0000000000..e4941382b7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cannonicalize_tl_where.py @@ -0,0 +1,124 @@ +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +types_all = [ + (torch.float32, 'float32'), +] + +shapes_common = [(128, 256), (127, 256), (127, 16), (129, 256), (77, 1024), (69, 512), (512, 512)] + +block_size = [128, 256, 1024] + + +def ceil_div(a, b): + return (a + b - 1) // b + + +def profiler_wrapper(fn, *args): + result_path = "./result_profiling_tl_where" + skip_first = 10 + wait = 0 + warmup = 3 + active = 30 + repeat = 1 + stream = torch.npu.current_stream() + experimental_config = torch_npu.profiler._ExperimentalConfig( + aic_metrics=torch_npu.profiler.AiCMetrics.PipeUtilization, + profiler_level=torch_npu.profiler.ProfilerLevel.Level1, l2_cache=False, data_simplification=False) + with torch_npu.profiler.profile( + activities=[torch_npu.profiler.ProfilerActivity.CPU, torch_npu.profiler.ProfilerActivity.NPU], + schedule=torch_npu.profiler.schedule(wait=wait, warmup=warmup, active=active, repeat=repeat, + skip_first=skip_first), + on_trace_ready=torch_npu.profiler.tensorboard_trace_handler(result_path), record_shapes=True, + profile_memory=False, with_stack=False, with_flops=False, with_modules=False, + experimental_config=experimental_config) as prof: + stream.synchronize() + for _ in range(skip_first + (wait + warmup + active) * repeat): + fn(*args) + prof.step() + stream.synchronize() + + +@triton.jit +def tl_where_kernel( + in_ptr, + output_ptr, + N: tl.constexpr, + M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, +): + pid = tl.program_id(axis=0) + offset = tl.multiple_of(pid * BLOCK_SIZE_N, N) + x1 = (offset + tl.arange(0, BLOCK_SIZE_N)) // N + mask1 = tl.where(x1 < M, 1, 0).to(tl.int1) + data = tl.load(in_ptr + x1 * N, mask=mask1, other=0) + x2 = pid * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + tl.store(output_ptr + x2, data) + + +def torch_tl_where(in_tensor): + M = in_tensor.shape[0] // 2 + N = in_tensor.shape[1] + + output = torch.zeros_like(in_tensor) + + first_elements = in_tensor[:M, 0:1] + output[:M] = first_elements.expand(-1, N) + + return output + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('M, N', shapes_common) +@pytest.mark.parametrize('BLOCK_SIZE_N', block_size) +def test_tl_where(M, N, BLOCK_SIZE_N, dtype, sigtype): + + in_tensor = torch.randn(2 * M, N, dtype=dtype).npu() + + triton_output = torch.zeros_like(in_tensor) + + grid = (ceil_div(2 * M * N, BLOCK_SIZE_N), ) + + tl_where_kernel[grid](in_tensor, triton_output, N=N, M=M, BLOCK_SIZE_N=BLOCK_SIZE_N) + + torch_output = torch_tl_where(in_tensor.clone()) + assert torch.allclose(triton_output, torch_output, rtol=1e-5, atol=1e-8) + + +def triton_tl_where(in_tensor, BLOCK_SIZE): + M = in_tensor.shape[0] // 2 + N = in_tensor.shape[1] + + triton_output = torch.zeros_like(in_tensor) + grid = (ceil_div(2 * M * N, BLOCK_SIZE), ) + + tl_where_kernel[grid](in_tensor, triton_output, N=N, M=M, BLOCK_SIZE_N=BLOCK_SIZE) + + +def profile_performance_test(M, N, dtype, BLOCK_SIZE): + print(f"\nDetailed performance analysis: M={M}, N={N}, dtype={dtype}, block_size={BLOCK_SIZE}") + + in_tensor = torch.randn(2 * M, N, dtype=dtype).npu() + + def wrapper_func(x): + triton_tl_where(x, BLOCK_SIZE=BLOCK_SIZE) + + # Run performance analysis + profiler_wrapper(wrapper_func, in_tensor) + + +if __name__ == "__main__": + + # Optional: Run detailed profiler test (specific configuration) + profile_performance_test(512, 512, torch.float32, BLOCK_SIZE=1024) + + print("\n" + "=" * 80) + print("Test completed!") + print(f"Detailed performance analysis results saved in: ./result_profiling_tl_where/") + print("=" * 80) diff --git a/third_party/ascend/unittest/pytest_ut/test_cast.py b/third_party/ascend/unittest/pytest_ut/test_cast.py new file mode 100644 index 0000000000..55924575f2 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cast.py @@ -0,0 +1,48 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al + +import torch +import torch_npu +import test_common + +# --------------- +# test cast op +# --------------- + + +@triton.jit +def cast_kernel(x_ptr, out_ptr, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(x_ptr + offs) + y = al.cast(x, tl.float32) + tl.store(out_ptr + offs, y) + + +def test_cast(): + size = 64 + x = test_common.generate_tensor((size, ), 'int32').npu() + torch_ref = x.to(torch.float32) + triton_res = torch.zeros(size, dtype=torch.float32).npu() + cast_kernel[(1, )](x, triton_res, size) + assert (torch_ref == triton_res).all(), (torch_ref, triton_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_cast_full.py b/third_party/ascend/unittest/pytest_ut/test_cast_full.py new file mode 100644 index 0000000000..d3463f913b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cast_full.py @@ -0,0 +1,229 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + +Dimensions = tuple[int, int, int] + + +@triton.jit +def cast_to_bool(output_ptr, x_ptr, dims: Dimensions, overflow_mode: tl.constexpr): + xidx = tl.arange(0, dims.XB) + yidx = tl.arange(0, dims.YB) + zidx = tl.arange(0, dims.ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + overflow_mode = "trunc" if overflow_mode == 0 else "saturate" + ret = tl.cast(X, dtype=tl.int1, overflow_mode=overflow_mode) + + tl.store(output_ptr + idx, ret) + + +@triton.jit +def cast_to_i8(output_ptr, x_ptr, dims: Dimensions, overflow_mode: tl.constexpr): + xidx = tl.arange(0, dims.XB) + yidx = tl.arange(0, dims.YB) + zidx = tl.arange(0, dims.ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + overflow_mode = "trunc" if overflow_mode == 0 else "saturate" + ret = tl.cast(X, dtype=tl.int8, overflow_mode=overflow_mode) + + tl.store(output_ptr + idx, ret) + + +def cast_to_i16(output_ptr, x_ptr, dims: Dimensions, overflow_mode: tl.constexpr): + xidx = tl.arange(0, dims.XB) + yidx = tl.arange(0, dims.YB) + zidx = tl.arange(0, dims.ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + overflow_mode = "trunc" if overflow_mode == 0 else "saturate" + ret = tl.cast(X, dtype=tl.int16, overflow_mode=overflow_mode) + + tl.store(output_ptr + idx, ret) + + +@triton.jit +def cast_to_i32(output_ptr, x_ptr, dims: Dimensions, overflow_mode: tl.constexpr): + xidx = tl.arange(0, dims.XB) + yidx = tl.arange(0, dims.YB) + zidx = tl.arange(0, dims.ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + overflow_mode = "trunc" if overflow_mode == 0 else "saturate" + ret = tl.cast(X, dtype=tl.int32, overflow_mode=overflow_mode) + + tl.store(output_ptr + idx, ret) + + +@triton.jit +def cast_to_i64(output_ptr, x_ptr, dims: Dimensions, overflow_mode: tl.constexpr): + xidx = tl.arange(0, dims.XB) + yidx = tl.arange(0, dims.YB) + zidx = tl.arange(0, dims.ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + overflow_mode = "trunc" if overflow_mode == 0 else "saturate" + ret = tl.cast(X, dtype=tl.int64, overflow_mode=overflow_mode) + + tl.store(output_ptr + idx, ret) + + +@triton.jit +def cast_to_fp32(output_ptr, x_ptr, dims: Dimensions, overflow_mode: tl.constexpr): + xidx = tl.arange(0, dims.XB) + yidx = tl.arange(0, dims.YB) + zidx = tl.arange(0, dims.ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + overflow_mode = "trunc" if overflow_mode == 0 else "saturate" + ret = tl.cast(X, dtype=tl.float32, overflow_mode=overflow_mode) + + tl.store(output_ptr + idx, ret) + + +@triton.jit +def cast_to_fp16(output_ptr, x_ptr, dims: Dimensions, overflow_mode: tl.constexpr): + xidx = tl.arange(0, dims.XB) + yidx = tl.arange(0, dims.YB) + zidx = tl.arange(0, dims.ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + overflow_mode = "trunc" if overflow_mode == 0 else "saturate" + ret = tl.cast(X, dtype=tl.float16, overflow_mode=overflow_mode) + + tl.store(output_ptr + idx, ret) + + +@triton.jit +def cast_to_bf16(output_ptr, x_ptr, dims: Dimensions, overflow_mode: tl.constexpr): + xidx = tl.arange(0, dims.XB) + yidx = tl.arange(0, dims.YB) + zidx = tl.arange(0, dims.ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + overflow_mode = "trunc" if overflow_mode == 0 else "saturate" + ret = tl.cast(X, dtype=tl.bfloat16, overflow_mode=overflow_mode) + + tl.store(output_ptr + idx, ret) + + +import numpy as np + + +def cast_npu(para_type, data_type, to_para, to_dtype, XB, YB, ZB, overflow_mode): + + print(f"TESTING: cast from {para_type} to {to_para} in shape ({XB}, {YB}, {ZB})") + + if para_type == "*i1": + x = torch.randint(low=0, high=2, size=(XB, YB, ZB), dtype=data_type).npu() + elif (para_type == "*i8" or para_type == "*i16" or para_type == "*i32" or para_type == "*64"): + x = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + elif para_type == "*i16": + x = torch.randint(low=-32768, high=32768, size=(XB, YB, ZB), dtype=data_type).npu() + elif para_type == "*i32": + x = torch.randint(low=-65536, high=65536, size=(XB, YB, ZB), dtype=data_type).npu() + elif para_type == "*i64": + x = torch.randint(low=-65536, high=65536, size=(XB, YB, ZB), dtype=data_type).npu() + else: # float + x = torch.randn((XB, YB, ZB), dtype=data_type).npu() + + if to_para == "*i1": + triton_func = cast_to_bool + cmp_type = "bool" + elif to_para == "*i8": + triton_func = cast_to_i8 + cmp_type = "int8" + elif to_para == "*i16": + triton_func = cast_to_i16 + cmp_type = "int16" + elif to_para == "*i32": + triton_func = cast_to_i32 + cmp_type = "int32" + elif to_para == "*i64": + triton_func = cast_to_i64 + cmp_type = "int64" + elif to_para == "*fp16": + triton_func = cast_to_fp16 + cmp_type = "float16" + elif to_para == "*fp32": + triton_func = cast_to_fp32 + cmp_type = "float32" + elif to_para == "*bf16": + triton_func = cast_to_bf16 + cmp_type = "bfloat16" + + output = torch.randint(1, (XB, YB, ZB), dtype=to_dtype).npu() + + a = x.to(to_dtype) + dims = Dimensions(XB=XB, YB=YB, ZB=ZB) + + triton_func[1, 1, 1](output, x, dims, overflow_mode) + + test_common.validate_cmp(cmp_type, a, output, overflow_mode) + + +def test_cast_high_priority_dtype(): + + typelist = [ + (torch.int8, "*i8"), + (torch.float32, "*fp32"), + (torch.float16, "*fp16"), + ] + + overflow_mode = [ + 0, # "trunc", + 1, # "saturate", + ] + + shapes = [(8, 32, 32)] + ContinueList = [] + for src in typelist: + for dst in typelist: + if src != dst and (src[1], dst[1]) not in ContinueList: + for shape in shapes: + for mode in overflow_mode: + ( + src[1], + src[0], + dst[1], + dst[0], + shape[0], + shape[1], + shape[2], + mode, + ) + + print("test_cast_full passed") diff --git a/third_party/ascend/unittest/pytest_ut/test_cat.py b/third_party/ascend/unittest/pytest_ut/test_cat.py new file mode 100644 index 0000000000..a4af78ebd9 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cat.py @@ -0,0 +1,142 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common +import math + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, y_ptr, XB: tl.constexpr): + + idx = tl.arange(0, XB) + X = tl.load(x_ptr + idx) + Y = tl.load(y_ptr + idx) + + ret = tl.cat(X, Y, can_reorder=True) + + oidx = tl.arange(0, XB * 2) + + tl.store(output_ptr + oidx, ret) + + +# The CAT operator in the Triton community also does not support boolean types. +@pytest.mark.parametrize('shape', [(32, ), (741, )]) #triton only support 1D cat +@pytest.mark.parametrize('dtype', [ + 'float32', +]) +def test_cat(shape, dtype): + m = shape[0] + x = torch.full((m, ), 100, dtype=eval("torch." + dtype)).npu() + y = torch.full((m, ), 30, dtype=eval("torch." + dtype)).npu() + + output = torch.randint(1, (m * 2, ), dtype=eval("torch." + dtype)).npu() + + ans = torch.cat((x, y), dim=0) + + fn_npu_[1, 1, 1](output, x, y, m) + + test_common.validate_cmp(dtype, ans, output) + + +@triton.jit +def cat_pointer_load_kernel( + left_ptr, + right_ptr, + output_ptr, + BLOCK_SIZE: tl.constexpr, + MASKED: tl.constexpr, +): + offsets = tl.arange(0, BLOCK_SIZE) + pointers = tl.cat(left_ptr + offsets, right_ptr + offsets, can_reorder=True) + output_offsets = tl.arange(0, BLOCK_SIZE * 2) + + if MASKED: + mask = (output_offsets != 2) & (output_offsets != BLOCK_SIZE + 3) + other = output_offsets.to(tl.float32) + 1000.0 + values = tl.load(pointers, mask=mask, other=other) + else: + values = tl.load(pointers) + + tl.store(output_ptr + output_offsets, values) + + +@triton.jit +def cat_pointer_store_kernel( + left_ptr, + right_ptr, + BLOCK_SIZE: tl.constexpr, + MASKED: tl.constexpr, +): + offsets = tl.arange(0, BLOCK_SIZE) + pointers = tl.cat(left_ptr + offsets, right_ptr + offsets, can_reorder=True) + input_offsets = tl.arange(0, BLOCK_SIZE * 2) + values = input_offsets.to(tl.float32) + 200.0 + + if MASKED: + mask = (input_offsets != 1) & (input_offsets != BLOCK_SIZE + 5) + tl.store(pointers, values, mask=mask) + else: + tl.store(pointers, values) + + +@pytest.mark.parametrize("masked", [False, True], ids=["unmasked", "masked_tensor_other"]) +def test_cat_pointer_load(masked): + block_size = 16 + total_size = block_size * 2 + left = torch.arange(block_size, dtype=torch.float32, device="npu") + 10.0 + right = torch.arange(block_size, dtype=torch.float32, device="npu") + 100.0 + output = torch.empty(total_size, dtype=torch.float32, device="npu") + + cat_pointer_load_kernel[(1, )](left, right, output, BLOCK_SIZE=block_size, MASKED=masked) + + expected = torch.cat((left, right)) + if masked: + offsets = torch.arange(total_size, dtype=torch.int32, device="npu") + mask = (offsets != 2) & (offsets != block_size + 3) + other = offsets.to(torch.float32) + 1000.0 + expected = torch.where(mask, expected, other) + + torch.testing.assert_close(output, expected, rtol=0, atol=0) + + +@pytest.mark.parametrize("masked", [False, True], ids=["unmasked", "masked"]) +def test_cat_pointer_store(masked): + block_size = 16 + total_size = block_size * 2 + sentinel = -1.0 + left = torch.full((block_size, ), sentinel, dtype=torch.float32, device="npu") + right = torch.full((block_size, ), sentinel, dtype=torch.float32, device="npu") + + cat_pointer_store_kernel[(1, )](left, right, BLOCK_SIZE=block_size, MASKED=masked) + + offsets = torch.arange(total_size, dtype=torch.int32, device="npu") + values = offsets.to(torch.float32) + 200.0 + if masked: + mask = (offsets != 1) & (offsets != block_size + 5) + values = torch.where(mask, values, torch.full_like(values, sentinel)) + + torch.testing.assert_close(left, values[:block_size], rtol=0, atol=0) + torch.testing.assert_close(right, values[block_size:], rtol=0, atol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_cat_dim.py b/third_party/ascend/unittest/pytest_ut/test_cat_dim.py new file mode 100644 index 0000000000..c0d6690e95 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cat_dim.py @@ -0,0 +1,119 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest + + +@triton.jit +def fn3_dim0(output_ptr, x1_ptr, x2_ptr, x3_ptr, x1_shape: tl.constexpr, x2_shape: tl.constexpr, + x3_shape: tl.constexpr): + idx_start = 0 + x1_idx = tl.arange(0, x1_shape) + X1 = tl.load(x1_ptr + x1_idx) + tl.store(output_ptr + x1_idx, X1) + + idx_start += x1_shape + x2_idx = tl.arange(0, x2_shape) + X2 = tl.load(x2_ptr + x2_idx) + tl.store(output_ptr + idx_start + x2_idx, X2) + + idx_start += x2_shape + x3_idx = tl.arange(0, x3_shape) + X3 = tl.load(x3_ptr + x3_idx) + tl.store(output_ptr + idx_start + x3_idx, X3) + + +@triton.jit +def fn4_dim1(output_ptr, x0_ptr, x1_ptr, x2_ptr, x3_ptr, dim0_len: tl.constexpr, x0_len: tl.constexpr, + x1_len: tl.constexpr, x2_len: tl.constexpr, x3_len: tl.constexpr): + + total_dim1_len = x0_len + x1_len + x2_len + x3_len + x0 = tl.load(x0_ptr + tl.arange(0, dim0_len * x0_len)) + x0 = x0.reshape(dim0_len, x0_len) + x1 = tl.load(x1_ptr + tl.arange(0, dim0_len * x1_len)) + x1 = x1.reshape(dim0_len, x1_len) + x2 = tl.load(x2_ptr + tl.arange(0, dim0_len * x2_len)) + x2 = x2.reshape(dim0_len, x2_len) + x3 = tl.load(x3_ptr + tl.arange(0, dim0_len * x3_len)) + x3 = x3.reshape(dim0_len, x3_len) + # (86,5), (86,36), (86,5) (86,4) + #torch.arange(0,86)[:,None] * 50 + torch.arange(0,36) + idx_start = 0 + nidx0 = (tl.arange(0, dim0_len)[:, None] * total_dim1_len + idx_start) + tl.arange(0, x0_len) + tl.store(output_ptr + nidx0, x0) + + idx_start += x0_len + nidx1 = (tl.arange(0, dim0_len)[:, None] * total_dim1_len + idx_start) + tl.arange(0, x1_len) + tl.store(output_ptr + nidx1, x1) + + idx_start += x1_len + nidx2 = (tl.arange(0, dim0_len)[:, None] * total_dim1_len + idx_start) + tl.arange(0, x2_len) + tl.store(output_ptr + nidx2, x2) + + idx_start += x2_len + nidx3 = (tl.arange(0, dim0_len)[:, None] * total_dim1_len + idx_start) + tl.arange(0, x3_len) + tl.store(output_ptr + nidx3, x3) + + +def cat_dim0(data_type): + x1_shape = (86, 48) + x2_shape = (8, 48) + x3_shape = (16, 48) + + x1 = torch.rand(x1_shape, dtype=data_type).npu() + x2 = torch.rand(x2_shape, dtype=data_type).npu() + x3 = torch.rand(x3_shape, dtype=data_type).npu() + res = torch.zeros((x1_shape[0] + x2_shape[0] + x3_shape[0], 48), dtype=data_type).npu() + fn3_dim0[(1, 1, 1)](res, x1, x2, x3, x1_shape[0] * x1_shape[1], x2_shape[0] * x2_shape[1], + x3_shape[0] * x3_shape[1]) + + res_ref = torch.cat((x1, x2, x3), dim=0) + assert torch.allclose(res_ref, res, rtol=1e-03, atol=1e-03, equal_nan=True) + + +def cat_dim1(data_type): + #data_type = torch.float16 + x0_shape = (86, 5) + x1_shape = (86, 36) + x2_shape = (86, 5) + x3_shape = (86, 4) + + dim = 1 + x0 = torch.rand(x0_shape, dtype=data_type).npu() + x1 = torch.rand(x1_shape, dtype=data_type).npu() + x2 = torch.rand(x2_shape, dtype=data_type).npu() + x3 = torch.rand(x3_shape, dtype=data_type).npu() + res = torch.zeros((86, x0_shape[dim] + x1_shape[dim] + x2_shape[dim] + x3_shape[dim]), dtype=data_type).npu() + fn4_dim1[(1, 1, 1)](res, x0, x1, x2, x3, 86, x0_shape[1], x1_shape[1], x2_shape[1], x3_shape[1]) + #print("res_tri=", res) + + res_ref = torch.cat((x0, x1, x2, x3), dim=1) + #print("res_ref=", res_ref) + assert torch.allclose(res_ref, res, rtol=1e-03, atol=1e-03, equal_nan=True) + + +def test_cat(): + cat_dim0(torch.float16) + cat_dim1(torch.float16) diff --git a/third_party/ascend/unittest/pytest_ut/test_cat_help_func.py b/third_party/ascend/unittest/pytest_ut/test_cat_help_func.py new file mode 100644 index 0000000000..4b102b9335 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cat_help_func.py @@ -0,0 +1,728 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging +import random +import pytest + +import triton +import triton.language as tl +import torch +import test_common +import numpy as np +import triton.language.extra.cann.extension as extension + + +def gen_1d_cat_shapes(min_val=1, max_val=4096): + shape1 = random.randint(min_val, max_val) + shape2 = random.randint(min_val, max_val) + return (shape1, ), (shape2, ), 0 + + +def gen_2d_cat_shapes(dim=0, min_val=1, max_val=4096): + if dim == 0: + common_col = random.randint(min_val, max_val) + row1 = random.randint(min_val, max_val) + row2 = random.randint(min_val, max_val) + shape1 = (row1, common_col) + shape2 = (row2, common_col) + elif dim == 1: + common_row = random.randint(min_val, max_val) + col1 = random.randint(min_val, max_val) + col2 = random.randint(min_val, max_val) + shape1 = (common_row, col1) + shape2 = (common_row, col2) + else: + raise ValueError("2d shape only support dim=0 or dim=1") + return shape1, shape2, dim + + +def gen_3d_cat_shapes(dim=0, min_val=1, max_val=4096): + if dim not in [0, 1, 2]: + raise ValueError("3d shape only support dim=0/1/2") + + if dim == 0: + common_d1 = random.randint(min_val, max_val) + common_d2 = random.randint(min_val, max_val) + d0_1 = random.randint(min_val, max_val) + d0_2 = random.randint(min_val, max_val) + shape1 = (d0_1, common_d1, common_d2) + shape2 = (d0_2, common_d1, common_d2) + + elif dim == 1: + common_d0 = random.randint(min_val, max_val) + common_d2 = random.randint(min_val, max_val) + d1_1 = random.randint(min_val, max_val) + d1_2 = random.randint(min_val, max_val) + shape1 = (common_d0, d1_1, common_d2) + shape2 = (common_d0, d1_2, common_d2) + + else: # dim == 2 + common_d0 = random.randint(min_val, max_val) + common_d1 = random.randint(min_val, max_val) + d2_1 = random.randint(min_val, max_val) + d2_2 = random.randint(min_val, max_val) + shape1 = (common_d0, common_d1, d2_1) + shape2 = (common_d0, common_d1, d2_2) + + return shape1, shape2, dim + + +def gen_100_cat_shapes(num_groups=100, mix_ratio=(0.3, 0.3, 0.4), min_val=1, max_val=4096): + + shape_list = [] + num_1d = int(num_groups * mix_ratio[0]) + num_2d = int(num_groups * mix_ratio[1]) + num_3d = num_groups - num_1d - num_2d + + for _ in range(num_1d): + shape_list.append(gen_1d_cat_shapes(min_val, max_val)) + + for _ in range(num_2d): + dim = random.choice([0, 1]) + shape_list.append(gen_2d_cat_shapes(dim, min_val, max_val)) + + for _ in range(num_3d): + dim = random.choice([0, 1, 2]) + shape_list.append(gen_3d_cat_shapes(dim, min_val, max_val)) + + random.shuffle(shape_list) + return shape_list + + +full_shape = gen_100_cat_shapes(num_groups=100, mix_ratio=(0.3, 0.4, 0.3), min_val=1, max_val=4096) + + +@triton.jit +def _cat_helper_func_2D_1( + in_ptr0, + in_ptr1, + out_ptr0, + in0_x: tl.constexpr, + in1_x: tl.constexpr, + y0_numel, + x1_numel, + Y0BLOCK: tl.constexpr, + Y0BLOCK_SUB: tl.constexpr, +): + y0_offset = tl.program_id(0) * Y0BLOCK_SUB + base_y0 = tl.arange(0, Y0BLOCK_SUB) + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + base_input0_x1 = tl.arange(0, in0_x)[None, :] + base_input1_x1 = tl.arange(0, in1_x)[None, :] + x1 = tl.arange(0, in0_x + in1_x)[None, :] + + for loop in range(loops_y0): + y0 = y0_offset + (loop * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + x1_mask = x1 < x1_numel + tmp0 = tl.load(in_ptr0 + (base_input0_x1 + in0_x * y0), y0_mask) + tmp1 = tl.load(in_ptr1 + (base_input1_x1 + in1_x * y0), y0_mask) + tmp2 = tl.zeros((Y0BLOCK_SUB, in0_x + in1_x), dtype=tmp0.dtype) + tmp3 = extension.insert_slice(tmp2, tmp0, [0, 0], [Y0BLOCK_SUB, in0_x], [1, 1]) + tmp4 = extension.insert_slice(tmp3, tmp1, [0, in0_x], [Y0BLOCK_SUB, in1_x], [1, 1]) + tl.store(out_ptr0 + (x1 + (in0_x + in1_x) * y0), tmp4, x1_mask & y0_mask) + + +@triton.jit +def triton_unk_fused_cat_dim0_sameshape(output_ptr, x_ptr, y_ptr, y0_numel, x1_numel, Y0BLOCK: tl.constexpr, + Y0BLOCK_SUB: tl.constexpr, X1BLOCK_SUB: tl.constexpr): + y0_offset = tl.program_id(0) * Y0BLOCK + base_y0 = tl.arange(0, Y0BLOCK_SUB) + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + base_x1 = tl.arange(0, X1BLOCK_SUB) + loops_x1 = (x1_numel + X1BLOCK_SUB - 1) // X1BLOCK_SUB + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < x1_numel + + tmp0 = tl.load(x_ptr + (x1 + x1_numel * y0), x1_mask & y0_mask) + tmp8 = tl.load(y_ptr + (x1 + x1_numel * y0), x1_mask & y0_mask) + tmp10 = tl.zeros((2 * Y0BLOCK_SUB, X1BLOCK_SUB), dtype=tmp0.dtype) + tmp11 = extension.insert_slice(tmp10, tmp0, [0, 0], [Y0BLOCK_SUB, X1BLOCK_SUB], [1, 1]) + tmp12 = extension.insert_slice(tmp11, tmp8, [Y0BLOCK_SUB, 0], [Y0BLOCK_SUB, X1BLOCK_SUB], [1, 1]) + tmp13 = tl.reshape(tmp12, (2, Y0BLOCK_SUB, X1BLOCK_SUB)) + + new_base_x2 = tl.arange(0, X1BLOCK_SUB) + new_x2 = (loop_x1 * X1BLOCK_SUB) + new_base_x2[None, None, :] + new_base_y1 = tl.arange(0, Y0BLOCK_SUB) + new_y1 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + new_base_y1[None, :, None] + new_z0 = tl.arange(0, 2)[:, None, None] + new_x2_mask = new_x2 < x1_numel + new_y1_mask = new_y1 < y0_numel + tl.store(output_ptr + (new_x2 + x1_numel * (new_y1 + y0_numel * new_z0)), tmp13, new_x2_mask & new_y1_mask) + + +@triton.jit +def triton_unk_fused_cat_dim0_diffshape(output_ptr, x_ptr, y_ptr, y0_numel, y1_numel, x1_numel, YBLOCK: tl.constexpr, + YBLOCK_2: tl.constexpr, YBLOCK_SUB: tl.constexpr, X1BLOCK_SUB: tl.constexpr): + y0_offset = tl.program_id(0) * YBLOCK + base_y0 = tl.arange(0, YBLOCK_SUB) + loops_y0 = (YBLOCK + YBLOCK_SUB - 1) // YBLOCK_SUB + base_x1 = tl.arange(0, X1BLOCK_SUB) + loops_x1 = (x1_numel + X1BLOCK_SUB - 1) // X1BLOCK_SUB + min_numel = 0 + max_numel = 0 + clone_numel = 0 + if y0_numel < y1_numel: + min_numel = y0_numel + max_numel = y1_numel + clone_numel = y1_numel - y0_numel + else: + min_numel = y1_numel + max_numel = y0_numel + clone_numel = y0_numel - y1_numel + + for loops_y in range(loops_y0): + y0 = y0_offset + (loops_y * YBLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(YBLOCK + y0_offset, min_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < x1_numel + + tmp0 = tl.load(x_ptr + (x1 + x1_numel * y0), x1_mask & y0_mask) + tmp8 = tl.load(y_ptr + (x1 + x1_numel * y0), x1_mask & y0_mask) + tmp10 = tl.zeros((2 * YBLOCK_SUB, X1BLOCK_SUB), dtype=tmp0.dtype) + tmp11 = extension.insert_slice(tmp10, tmp0, [0, 0], [YBLOCK_SUB, X1BLOCK_SUB], [1, 1]) + tmp12 = extension.insert_slice(tmp11, tmp8, [YBLOCK_SUB, 0], [YBLOCK_SUB, X1BLOCK_SUB], [1, 1]) + tmp13 = tl.reshape(tmp12, (2, YBLOCK_SUB, X1BLOCK_SUB)) + + new_base_x2 = tl.arange(0, X1BLOCK_SUB) + new_x2 = (loop_x1 * X1BLOCK_SUB) + new_base_x2[None, None, :] + new_base_y1 = tl.arange(0, YBLOCK_SUB) + new_y1 = y0_offset + (loops_y * YBLOCK_SUB) + new_base_y1[None, :, None] + new_z0 = tl.arange(0, 2)[:, None, None] + new_x2_mask = new_x2 < x1_numel + new_y1_mask = new_y1 < min_numel + tl.store(output_ptr + (new_x2 + x1_numel * new_y1 + x1_numel * y0_numel * new_z0), tmp13, + new_x2_mask & new_y1_mask) + + loops_y1 = (YBLOCK_2 + YBLOCK_SUB - 1) // YBLOCK_SUB + y2_offset = tl.program_id(0) * YBLOCK_2 + min_numel + if y0_numel < y1_numel: + for loops_y1 in range(loops_y1): + y0 = y2_offset + (loops_y1 * YBLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(YBLOCK_2 + y2_offset, y1_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < x1_numel + + tmp8 = tl.load(y_ptr + (x1 + x1_numel * y0), x1_mask & y0_mask) + new_base_x2 = tl.arange(0, X1BLOCK_SUB) + new_x2 = (loop_x1 * X1BLOCK_SUB) + new_base_x2[None, :] + new_base_y1 = tl.arange(0, YBLOCK_SUB) + new_y1 = y2_offset + y0_numel + (loops_y1 * YBLOCK_SUB) + new_base_y1[:, None] + sum_numel = y0_numel + y1_numel + new_x2_mask = new_x2 < x1_numel + new_y1_mask = new_y1 < sum_numel + tl.store(output_ptr + (new_x2 + x1_numel * new_y1), tmp8, new_x2_mask & new_y1_mask) + else: + for loops_y1 in range(loops_y1): + y0 = y2_offset + (loops_y1 * YBLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(YBLOCK_2 + y2_offset, y0_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < x1_numel + + tmp8 = tl.load(x_ptr + (x1 + x1_numel * y0), x1_mask & y0_mask) + new_base_x2 = tl.arange(0, X1BLOCK_SUB) + new_x2 = (loop_x1 * X1BLOCK_SUB) + new_base_x2[None, :] + new_base_y1 = tl.arange(0, YBLOCK_SUB) + new_y1 = y2_offset + (loops_y1 * YBLOCK_SUB) + new_base_y1[:, None] + new_x2_mask = new_x2 < x1_numel + new_y1_mask = new_y1 < y0_numel + tl.store(output_ptr + (new_x2 + x1_numel * new_y1), tmp8, new_x2_mask & new_y1_mask) + + +@triton.jit +def triton_unk_fused_cat_dim1_sameshape(output_ptr, x_ptr, y_ptr, y0_numel, x1_numel, Y0BLOCK: tl.constexpr, + Y0BLOCK_SUB: tl.constexpr, X1BLOCK_SUB: tl.constexpr): + y0_offset = tl.program_id(0) * Y0BLOCK + base_y0 = tl.arange(0, Y0BLOCK_SUB) + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + base_x1 = tl.arange(0, X1BLOCK_SUB) + loops_x1 = (x1_numel + X1BLOCK_SUB - 1) // X1BLOCK_SUB + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < x1_numel + + tmp0 = tl.load(x_ptr + (x1 + x1_numel * y0), x1_mask & y0_mask) + tmp8 = tl.load(y_ptr + (x1 + x1_numel * y0), x1_mask & y0_mask) + tmp10 = tl.zeros((Y0BLOCK_SUB, 2 * X1BLOCK_SUB), dtype=tmp0.dtype) + tmp11 = extension.insert_slice(tmp10, tmp0, [0, 0], [Y0BLOCK_SUB, X1BLOCK_SUB], [1, 1]) + tmp12 = extension.insert_slice(tmp11, tmp8, [0, X1BLOCK_SUB], [Y0BLOCK_SUB, X1BLOCK_SUB], [1, 1]) + tmp13 = tl.reshape(tmp12, (Y0BLOCK_SUB, 2, X1BLOCK_SUB)) + + new_base_x2 = tl.arange(0, X1BLOCK_SUB) + new_x2 = (loop_x1 * X1BLOCK_SUB) + new_base_x2[None, None, :] + new_base_y1 = tl.arange(0, Y0BLOCK_SUB) + new_y1 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + new_base_y1[:, None, None] + new_z0 = tl.arange(0, 2)[None, :, None] + new_x2_mask = new_x2 < x1_numel + new_y1_mask = new_y1 < y0_numel + tl.store(output_ptr + (new_x2 + 2 * x1_numel * new_y1 + x1_numel * new_z0), tmp13, + new_x2_mask & new_y1_mask) + + +@triton.jit +def triton_unk_fused_cat_dim1_diffshape(output_ptr, x_ptr, y_ptr, y0_numel, x0_numel, x1_numel, Y0BLOCK: tl.constexpr, + Y0BLOCK_SUB: tl.constexpr, XBLOCK_SUB: tl.constexpr): + y0_offset = tl.program_id(0) * Y0BLOCK + base_y0 = tl.arange(0, Y0BLOCK_SUB) + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + base_x = tl.arange(0, XBLOCK_SUB) + min_numel = 0 + max_numel = 0 + clone_numel = 0 + if x0_numel < x1_numel: + min_numel = x0_numel + max_numel = x1_numel + clone_numel = x1_numel - x0_numel + else: + min_numel = x1_numel + max_numel = x0_numel + clone_numel = x0_numel - x1_numel + loops_x = (min_numel + XBLOCK_SUB - 1) // XBLOCK_SUB + loops_x2 = (clone_numel + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + for loop_x in range(loops_x): + x = (loop_x * XBLOCK_SUB) + base_x[None, :] + x_mask = x < min_numel + + tmp0 = tl.load(x_ptr + (x + x0_numel * y0), x_mask & y0_mask) + tmp8 = tl.load(y_ptr + (x + x1_numel * y0), x_mask & y0_mask) + tmp10 = tl.zeros((Y0BLOCK_SUB, 2 * XBLOCK_SUB), dtype=tmp0.dtype) + tmp11 = extension.insert_slice(tmp10, tmp0, [0, 0], [Y0BLOCK_SUB, XBLOCK_SUB], [1, 1]) + tmp12 = extension.insert_slice(tmp11, tmp8, [0, XBLOCK_SUB], [Y0BLOCK_SUB, XBLOCK_SUB], [1, 1]) + tmp13 = tl.reshape(tmp12, (Y0BLOCK_SUB, 2, XBLOCK_SUB)) + + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = (loop_x * XBLOCK_SUB) + new_base_x2[None, None, :] + new_base_y1 = tl.arange(0, Y0BLOCK_SUB) + new_y1 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + new_base_y1[:, None, None] + new_z0 = tl.arange(0, 2)[None, :, None] + new_x2_mask = new_x2 < min_numel + new_y1_mask = new_y1 < y0_numel + sum_numel = x0_numel + x1_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_y1 + x0_numel * new_z0), tmp13, new_x2_mask & new_y1_mask) + + if x0_numel < x1_numel: + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + for loop_x2 in range(loops_x2): + x = (loop_x2 * XBLOCK_SUB) + base_x[None, :] + min_numel + x_mask = x < x1_numel + + tmp8 = tl.load(y_ptr + (x + x1_numel * y0), x_mask & y0_mask) + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = x0_numel + min_numel + (loop_x2 * XBLOCK_SUB) + new_base_x2[None, :] + new_base_y1 = tl.arange(0, Y0BLOCK_SUB) + new_y1 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + new_base_y1[:, None] + sum_numel = x0_numel + x1_numel + new_x2_mask = new_x2 < sum_numel + new_y1_mask = new_y1 < y0_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_y1), tmp8, new_x2_mask & new_y1_mask) + else: + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + for loop_x2 in range(loops_x2): + x = (loop_x2 * XBLOCK_SUB) + base_x[None, :] + min_numel + x_mask = x < x0_numel + + tmp8 = tl.load(x_ptr + (x + x0_numel * y0), x_mask & y0_mask) + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = min_numel + (loop_x2 * XBLOCK_SUB) + new_base_x2[None, :] + new_base_y1 = tl.arange(0, Y0BLOCK_SUB) + new_y1 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + new_base_y1[:, None] + sum_numel = x0_numel + x1_numel + new_x2_mask = new_x2 < x0_numel + new_y1_mask = new_y1 < y0_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_y1), tmp8, new_x2_mask & new_y1_mask) + + +@triton.jit +def triton_unk_fused_cat_3d_dim0(output_ptr, x_ptr, y_ptr, z0_numel, z1_numel, y1_numel, x1_numel, ZBLOCK: tl.constexpr, + ZBLOCK_2: tl.constexpr, ZBLOCK_SUB: tl.constexpr, X1BLOCK_SUB: tl.constexpr): + z0_offset = tl.program_id(0) * ZBLOCK + base_z0 = tl.arange(0, ZBLOCK_SUB) + loops_z0 = (ZBLOCK + ZBLOCK_SUB - 1) // ZBLOCK_SUB + xy_numel = x1_numel * y1_numel + base_x1 = tl.arange(0, X1BLOCK_SUB) + loops_x1 = (xy_numel + X1BLOCK_SUB - 1) // X1BLOCK_SUB + min_numel = 0 + max_numel = 0 + clone_numel = 0 + if z0_numel < z1_numel: + min_numel = z0_numel + max_numel = z1_numel + clone_numel = z1_numel - z0_numel + else: + min_numel = z1_numel + max_numel = z0_numel + clone_numel = z0_numel - z1_numel + + for loops_z in range(loops_z0): + z0 = z0_offset + (loops_z * ZBLOCK_SUB) + base_z0[:, None] + z0_mask = z0 < min(ZBLOCK + z0_offset, min_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < xy_numel + + tmp0 = tl.load(x_ptr + (x1 + xy_numel * z0), x1_mask & z0_mask) + tmp8 = tl.load(y_ptr + (x1 + xy_numel * z0), x1_mask & z0_mask) + tmp10 = tl.zeros((2 * ZBLOCK_SUB, X1BLOCK_SUB), dtype=tmp0.dtype) + tmp11 = extension.insert_slice(tmp10, tmp0, [0, 0], [ZBLOCK_SUB, X1BLOCK_SUB], [1, 1]) + tmp12 = extension.insert_slice(tmp11, tmp8, [ZBLOCK_SUB, 0], [ZBLOCK_SUB, X1BLOCK_SUB], [1, 1]) + tmp13 = tl.reshape(tmp12, (2, ZBLOCK_SUB, X1BLOCK_SUB)) + + new_base_x2 = tl.arange(0, X1BLOCK_SUB) + new_x2 = (loop_x1 * X1BLOCK_SUB) + new_base_x2[None, None, :] + new_base_z1 = tl.arange(0, ZBLOCK_SUB) + new_z1 = z0_offset + (loops_z * ZBLOCK_SUB) + new_base_z1[None, :, None] + new_z0 = tl.arange(0, 2)[:, None, None] + new_x2_mask = new_x2 < xy_numel + new_z1_mask = new_z1 < min_numel + tl.store(output_ptr + (new_x2 + xy_numel * new_z1 + xy_numel * z0_numel * new_z0), tmp13, + new_x2_mask & new_z1_mask) + + loops_z1 = (ZBLOCK_2 + ZBLOCK_SUB - 1) // ZBLOCK_SUB + z2_offset = tl.program_id(0) * ZBLOCK_2 + min_numel + if z0_numel < z1_numel: + for loops_z1 in range(loops_z1): + z0 = z2_offset + (loops_z1 * ZBLOCK_SUB) + base_z0[:, None] + z0_mask = z0 < min(ZBLOCK_2 + z2_offset, z1_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < xy_numel + + tmp8 = tl.load(y_ptr + (x1 + xy_numel * z0), x1_mask & z0_mask) + new_base_x2 = tl.arange(0, X1BLOCK_SUB) + new_x2 = (loop_x1 * X1BLOCK_SUB) + new_base_x2[None, :] + new_base_z1 = tl.arange(0, ZBLOCK_SUB) + new_z1 = z2_offset + z0_numel + (loops_z1 * ZBLOCK_SUB) + new_base_z1[:, None] + sum_numel = z0_numel + z1_numel + new_x2_mask = new_x2 < xy_numel + new_z1_mask = new_z1 < sum_numel + tl.store(output_ptr + (new_x2 + xy_numel * new_z1), tmp8, new_x2_mask & new_z1_mask) + else: + for loops_z1 in range(loops_z1): + z0 = z2_offset + (loops_z1 * ZBLOCK_SUB) + base_z0[:, None] + z0_mask = z0 < min(ZBLOCK_2 + z2_offset, z0_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < xy_numel + + tmp8 = tl.load(x_ptr + (x1 + xy_numel * z0), x1_mask & z0_mask) + new_base_x2 = tl.arange(0, X1BLOCK_SUB) + new_x2 = (loop_x1 * X1BLOCK_SUB) + new_base_x2[None, :] + new_base_z1 = tl.arange(0, ZBLOCK_SUB) + new_z1 = z2_offset + (loops_z1 * ZBLOCK_SUB) + new_base_z1[:, None] + new_x2_mask = new_x2 < xy_numel + new_z1_mask = new_z1 < z0_numel + tl.store(output_ptr + (new_x2 + xy_numel * new_z1), tmp8, new_x2_mask & new_z1_mask) + + +@triton.jit +def triton_unk_fused_cat_3d_dim1(output_ptr, x_ptr, y_ptr, z0_numel, y0_numel, y1_numel, x0_numel, + Z0BLOCK: tl.constexpr, Z0BLOCK_SUB: tl.constexpr, XBLOCK_SUB: tl.constexpr): + z0_offset = tl.program_id(0) * Z0BLOCK + base_z0 = tl.arange(0, Z0BLOCK_SUB) + loops_z0 = (Z0BLOCK + Z0BLOCK_SUB - 1) // Z0BLOCK_SUB + base_x = tl.arange(0, XBLOCK_SUB) + min_numel = 0 + max_numel = 0 + clone_numel = 0 + if y0_numel < y1_numel: + min_numel = y0_numel * x0_numel + max_numel = y1_numel * x0_numel + clone_numel = (y1_numel - y0_numel) * x0_numel + else: + min_numel = y1_numel * x0_numel + max_numel = y0_numel * x0_numel + clone_numel = (y0_numel - y1_numel) * x0_numel + loops_x = (min_numel + XBLOCK_SUB - 1) // XBLOCK_SUB + loops_x2 = (clone_numel + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop_z0 in range(loops_z0): + z0 = z0_offset + (loop_z0 * Z0BLOCK_SUB) + base_z0[:, None] + z0_mask = z0 < min(Z0BLOCK + z0_offset, z0_numel) + for loop_x in range(loops_x): + x = (loop_x * XBLOCK_SUB) + base_x[None, :] + x_mask = x < min_numel + + tmp0 = tl.load(x_ptr + (x + x0_numel * y0_numel * z0), x_mask & z0_mask) + tmp8 = tl.load(y_ptr + (x + x0_numel * y1_numel * z0), x_mask & z0_mask) + tmp10 = tl.zeros((Z0BLOCK_SUB, 2 * XBLOCK_SUB), dtype=tmp0.dtype) + tmp11 = extension.insert_slice(tmp10, tmp0, [0, 0], [Z0BLOCK_SUB, XBLOCK_SUB], [1, 1]) + tmp12 = extension.insert_slice(tmp11, tmp8, [0, XBLOCK_SUB], [Z0BLOCK_SUB, XBLOCK_SUB], [1, 1]) + tmp13 = tl.reshape(tmp12, (Z0BLOCK_SUB, 2, XBLOCK_SUB)) + + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = (loop_x * XBLOCK_SUB) + new_base_x2[None, None, :] + new_base_z1 = tl.arange(0, Z0BLOCK_SUB) + new_z1 = z0_offset + (loop_z0 * Z0BLOCK_SUB) + new_base_z1[:, None, None] + new_z0 = tl.arange(0, 2)[None, :, None] + new_x2_mask = new_x2 < min_numel + new_z1_mask = new_z1 < z0_numel + sum_numel = min_numel + max_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_z1 + x0_numel * y0_numel * new_z0), tmp13, + new_x2_mask & new_z1_mask) + + if y0_numel == y1_numel: + return + + if y0_numel < y1_numel: + for loop_z0 in range(loops_z0): + z0 = z0_offset + (loop_z0 * Z0BLOCK_SUB) + base_z0[:, None] + z0_mask = z0 < min(Z0BLOCK + z0_offset, z0_numel) + for loop_x2 in range(loops_x2): + x = (loop_x2 * XBLOCK_SUB) + base_x[None, :] + min_numel + x_mask = x < y1_numel * x0_numel + + tmp8 = tl.load(y_ptr + (x + x0_numel * y1_numel * z0), x_mask & z0_mask) + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = x0_numel * y0_numel + min_numel + (loop_x2 * XBLOCK_SUB) + new_base_x2[None, :] + new_base_z1 = tl.arange(0, Z0BLOCK_SUB) + new_z1 = z0_offset + (loop_z0 * Z0BLOCK_SUB) + new_base_z1[:, None] + sum_numel = min_numel + max_numel + new_x2_mask = new_x2 < sum_numel + new_z1_mask = new_z1 < z0_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_z1), tmp8, new_x2_mask & new_z1_mask) + else: + for loop_z0 in range(loops_z0): + z0 = z0_offset + (loop_z0 * Z0BLOCK_SUB) + base_z0[:, None] + z0_mask = z0 < min(Z0BLOCK + z0_offset, z0_numel) + for loop_x2 in range(loops_x2): + x = (loop_x2 * XBLOCK_SUB) + base_x[None, :] + min_numel + x_mask = x < x0_numel * y0_numel + + tmp8 = tl.load(x_ptr + (x + x0_numel * y0_numel * z0), x_mask & z0_mask) + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = min_numel + (loop_x2 * XBLOCK_SUB) + new_base_x2[None, :] + new_base_z1 = tl.arange(0, Z0BLOCK_SUB) + new_z1 = z0_offset + (loop_z0 * Z0BLOCK_SUB) + new_base_z1[:, None] + sum_numel = min_numel + max_numel + new_x2_mask = new_x2 < x0_numel * y0_numel + new_z1_mask = new_z1 < z0_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_z1), tmp8, new_x2_mask & new_z1_mask) + + +@triton.jit +def triton_unk_fused_cat_3d_dim2(output_ptr, x_ptr, y_ptr, z0_numel, y0_numel, x0_numel, x1_numel, + Y0BLOCK: tl.constexpr, Y0BLOCK_SUB: tl.constexpr, XBLOCK_SUB: tl.constexpr): + y0_offset = tl.program_id(0) * Y0BLOCK + base_y0 = tl.arange(0, Y0BLOCK_SUB) + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + base_x = tl.arange(0, XBLOCK_SUB) + min_numel = 0 + max_numel = 0 + clone_numel = 0 + zy_numel = z0_numel * y0_numel + if x0_numel < x1_numel: + min_numel = x0_numel + max_numel = x1_numel + clone_numel = x1_numel - x0_numel + else: + min_numel = x1_numel + max_numel = x0_numel + clone_numel = x0_numel - x1_numel + loops_x = (min_numel + XBLOCK_SUB - 1) // XBLOCK_SUB + loops_x2 = (clone_numel + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, zy_numel) + for loop_x in range(loops_x): + x = (loop_x * XBLOCK_SUB) + base_x[None, :] + x_mask = x < min_numel + + tmp0 = tl.load(x_ptr + (x + x0_numel * y0), x_mask & y0_mask) + tmp8 = tl.load(y_ptr + (x + x1_numel * y0), x_mask & y0_mask) + tmp10 = tl.zeros((Y0BLOCK_SUB, 2 * XBLOCK_SUB), dtype=tmp0.dtype) + tmp11 = extension.insert_slice(tmp10, tmp0, [0, 0], [Y0BLOCK_SUB, XBLOCK_SUB], [1, 1]) + tmp12 = extension.insert_slice(tmp11, tmp8, [0, XBLOCK_SUB], [Y0BLOCK_SUB, XBLOCK_SUB], [1, 1]) + tmp13 = tl.reshape(tmp12, (Y0BLOCK_SUB, 2, XBLOCK_SUB)) + + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = (loop_x * XBLOCK_SUB) + new_base_x2[None, None, :] + new_base_y1 = tl.arange(0, Y0BLOCK_SUB) + new_y1 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + new_base_y1[:, None, None] + new_z0 = tl.arange(0, 2)[None, :, None] + new_x2_mask = new_x2 < min_numel + new_y1_mask = new_y1 < zy_numel + sum_numel = x0_numel + x1_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_y1 + x0_numel * new_z0), tmp13, new_x2_mask & new_y1_mask) + + if x0_numel == x1_numel: + return + + if x0_numel < x1_numel: + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, zy_numel) + for loop_x2 in range(loops_x2): + x = (loop_x2 * XBLOCK_SUB) + base_x[None, :] + min_numel + x_mask = x < x1_numel + + tmp8 = tl.load(y_ptr + (x + x1_numel * y0), x_mask & y0_mask) + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = x0_numel + min_numel + (loop_x2 * XBLOCK_SUB) + new_base_x2[None, :] + new_base_y1 = tl.arange(0, Y0BLOCK_SUB) + new_y1 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + new_base_y1[:, None] + sum_numel = x0_numel + x1_numel + new_x2_mask = new_x2 < sum_numel + new_y1_mask = new_y1 < zy_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_y1), tmp8, new_x2_mask & new_y1_mask) + else: + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, zy_numel) + for loop_x2 in range(loops_x2): + x = (loop_x2 * XBLOCK_SUB) + base_x[None, :] + min_numel + x_mask = x < x0_numel + + tmp8 = tl.load(x_ptr + (x + x0_numel * y0), x_mask & y0_mask) + new_base_x2 = tl.arange(0, XBLOCK_SUB) + new_x2 = min_numel + (loop_x2 * XBLOCK_SUB) + new_base_x2[None, :] + new_base_y1 = tl.arange(0, Y0BLOCK_SUB) + new_y1 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + new_base_y1[:, None] + sum_numel = x0_numel + x1_numel + new_x2_mask = new_x2 < x0_numel + new_y1_mask = new_y1 < zy_numel + tl.store(output_ptr + (new_x2 + sum_numel * new_y1), tmp8, new_x2_mask & new_y1_mask) + + +testlist = [ + # ===================== 1D场景(15组,dim=0) ===================== + ((3, ), (3, ), 0), + ((7, ), (9, ), 0), + ((13, ), (11, ), 0), + ((2047, ), (2047, ), 0), + ((2701, ), (3003, ), 0), + ((4093, ), (3095, ), 0), + + # ===================== 2D场景(20组,dim0/dim1) ===================== + # dim0(行拼接,列维度一致) + ((3, 5), (3, 5), 0), + ((1005, 300), (2007, 300), 0), + ((1307, 400), (309, 400), 0), + ((303, 500), (303, 500), 0), + # dim1(列拼接,行维度一致) + ((7, 9), (7, 9), 1), + ((100, 1001), (100, 2003), 1), + ((200, 2005), (200, 207), 1), + ((300, 707), (300, 707), 1), + + # ===================== 3D场景(15组,dim0/dim1/dim2) ===================== + # dim0(第0维拼接,d1/d2一致) + ((378, 200, 300), (101, 200, 300), 0), + ((378, 70, 50), (601, 70, 50), 0), + # dim1(第1维拼接,d0/d2一致) + ((100, 452, 300), (100, 201, 300), 1), + ((65, 1735, 57), (65, 2001, 57), 1), + # dim2(第2维拼接,d0/d1一致) + ((87, 200, 387), (87, 200, 501), 2), + ((20, 337, 543), (20, 337, 401), 2), +] + + +@pytest.mark.parametrize('testlists', testlist) +@pytest.mark.parametrize('dtype', ['bool', 'int8', 'int16', 'int32', 'int64', 'float16', 'float32', 'bfloat16']) +def test_cat_bigshape(testlists, dtype): + torch_dtype = eval('torch.' + dtype) + np_x0 = test_common.generate_numpy(testlists[0], dtype) + np_x1 = test_common.generate_numpy(testlists[1], dtype) + cat_dim = testlists[2] + + x0 = torch.from_numpy(np_x0).to(torch_dtype).npu() + x1 = torch.from_numpy(np_x1).to(torch_dtype).npu() + + if len(x0.shape) > 3: + pytest.skip("dim > 3 for 3D+ tensor, skipping.") + + torch_res = torch.cat([x0, x1], dim=cat_dim) + triton_res = torch.zeros_like(torch_res) + num_core = 32 + if len(x0.shape) == 3: + if cat_dim == 0: + ZBLOCK = (min(x0.shape[0], x1.shape[0]) + num_core - 1) // num_core + ZBLOCK_2 = (max(x0.shape[0], x1.shape[0]) - min(x0.shape[0], x1.shape[0]) + num_core - 1) // num_core + triton_unk_fused_cat_3d_dim0[num_core, 1, 1](triton_res, x0, x1, x0.shape[0], x1.shape[0], x0.shape[1], + x0.shape[2], ZBLOCK, ZBLOCK_2, 1, 256) + elif cat_dim == 1: + Z0BLOCK = (x0.shape[0] + num_core - 1) // num_core + triton_unk_fused_cat_3d_dim1[num_core, 1, 1](triton_res, x0, x1, x0.shape[0], x0.shape[1], x1.shape[1], + x1.shape[2], Z0BLOCK, 1, 256) + else: + Y0BLOCK = (x0.shape[0] * x0.shape[1] + num_core - 1) // num_core + triton_unk_fused_cat_3d_dim2[num_core, 1, 1](triton_res, x0, x1, x0.shape[0], x0.shape[1], x0.shape[2], + x1.shape[2], Y0BLOCK, 1, 256) + test_common.validate_cmp(dtype, torch_res, triton_res) + return + numel_large = torch_res.numel() > 512 and len(x0.shape) < 3 + if numel_large or (cat_dim == 0 and len(x0.shape) == 2): + squeeze_flag = False + if len(x0.shape) == 1: + squeeze_flag = True + x0 = torch.unsqueeze(x0, dim=0) + x1 = torch.unsqueeze(x1, dim=0) + triton_res = torch.unsqueeze(triton_res, dim=0) + cat_dim = 1 + if cat_dim == 1: + Y0BLOCK = (x0.shape[0] + num_core - 1) // num_core + if x0.shape[1] == x1.shape[1]: + triton_unk_fused_cat_dim1_sameshape[num_core, 1, 1](triton_res, x0, x1, x0.shape[0], x0.shape[1], + Y0BLOCK, 1, 256) + else: + triton_unk_fused_cat_dim1_diffshape[num_core, 1, 1](triton_res, x0, x1, x0.shape[0], x0.shape[1], + x1.shape[1], Y0BLOCK, 1, 256) + else: + if x0.shape[0] == x1.shape[0]: + Y0BLOCK = (x0.shape[0] + num_core - 1) // num_core + triton_unk_fused_cat_dim0_sameshape[num_core, 1, 1](triton_res, x0, x1, x0.shape[0], x0.shape[1], + Y0BLOCK, 1, 256) + else: + YBLOCK = (min(x0.shape[0], x1.shape[0]) + num_core - 1) // num_core + YBLOCK_2 = (max(x0.shape[0], x1.shape[0]) - min(x0.shape[0], x1.shape[0]) + num_core - 1) // num_core + triton_unk_fused_cat_dim0_diffshape[num_core, 1, 1](triton_res, x0, x1, x0.shape[0], x1.shape[0], + x1.shape[1], YBLOCK, YBLOCK_2, 1, 256) + if squeeze_flag: + triton_res = triton_res.squeeze() + else: + squeeze_flag = False + if len(x0.shape) == 1: + squeeze_flag = True + x0 = torch.unsqueeze(x0, dim=0) + x1 = torch.unsqueeze(x1, dim=0) + triton_res = torch.unsqueeze(triton_res, dim=0) + _cat_helper_func_2D_1[num_core, 1, 1](x0, x1, triton_res, x0.shape[1], x1.shape[1], x0.shape[0], + x0.shape[1] + x1.shape[1], 256, 16) + if squeeze_flag: + triton_res = triton_res.squeeze() + + test_common.validate_cmp(dtype, torch_res, triton_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_cdiv.py b/third_party/ascend/unittest/pytest_ut/test_cdiv.py new file mode 100644 index 0000000000..071e8764a9 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cdiv.py @@ -0,0 +1,110 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_cdiv(x0, x1, dtype_x): + if dtype_x in [torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64]: + return torch.div(x0, x1, rounding_mode='floor') + (x0 % x1 != 0).to(torch.int) + else: + if dtype_x in ["float16", "bfloat16"]: + x0 = x0.to(torch.float32) + x1 = x1.to(torch.float32) + return torch.ceil(x0 / x1).to(eval("torch." + dtype_x)) + + +def torch_cdiv_special(x0, x1, dtype_x): + if dtype_x in [torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64]: + return torch.div(x0, x1, rounding_mode='floor') + (x0 % x1 != 0).to(torch.int) + else: + if dtype_x in ["float16", "bfloat16"]: + x0 = x0.to(torch.float32) + return torch.ceil(x0 / x1).to(eval("torch." + dtype_x)) + + +@triton.jit +def triton_cdiv(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = tl.cdiv(XBLOCK, XBLOCK_SUB) + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index, None) + tmp1 = tl.load(in_ptr1 + x_index, None) + tmp2 = tl.cdiv(tmp0, tmp1) + tl.store(out_ptr0 + x_index, tmp2, None) + + +@triton.jit +def triton_cdiv_special(in_ptr0, div, out_ptr0, XBLOCK, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = tl.cdiv(XBLOCK, XBLOCK_SUB) + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index, None) + tmp2 = tl.cdiv(tmp0, div) + tl.store(out_ptr0 + x_index, tmp2, None) + + +param_lists = [ + # ['int8', (4096,), 1, 4096, 4096], + # ['int16', (4096,), 1, 4096, 4096], + ['int32', (4096, ), 1, 4096, 4096], + # ['int64', (4096,), 1, 4096, 4096], +] + + +@pytest.mark.parametrize('param_list', param_lists) +def test_cdiv(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + torch_dtype = eval('torch.' + dtype) + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_cdiv(x0, x1, dtype) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_cdiv[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) + + +@pytest.mark.parametrize('param_list', param_lists) +def test_cdiv_special(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + torch_dtype = eval('torch.' + dtype) + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = 2 + # torch结果 + torch_res = torch_cdiv_special(x0, x1, dtype) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_cdiv_special[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_ceil.py b/third_party/ascend/unittest/pytest_ut/test_ceil.py new file mode 100644 index 0000000000..e0836ef25a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ceil.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def torch_ceil(x0): + res = torch.ceil(x0) + return res + + +@triton.jit +def triton_ceil(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.ceil(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 32, 2048, 64], + # float64 is not exercised here: test_common's tensor/compare helpers don't support it. +]) +def test_ceil(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + np_x0 = test_common.generate_numpy(shape, dtype) + x0 = torch.from_numpy(np_x0).to(eval('torch.' + dtype)).npu() + y_ref = torch_ceil(x0) + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + x0 = x0.npu() + triton_ceil[ncore, 1, 1](x0, y_cal, xblock, xblock_sub, debug=True) + y_ref = y_ref.npu() + test_common.validate_cmp_with_expection(dtype, y_cal, y_ref, True) diff --git a/third_party/ascend/unittest/pytest_ut/test_celoss_indices.py b/third_party/ascend/unittest/pytest_ut/test_celoss_indices.py new file mode 100644 index 0000000000..da9f76e796 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_celoss_indices.py @@ -0,0 +1,112 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import triton +import triton.language as tl + + +@triton.jit +def celoss_indices_kernel( + inp_ptr, + tgt_ptr, + w_ptr, + out_ptr, + w_tgt_ptr, + ignore_index, + C, + D, + BLOCK_C: tl.constexpr, + BLOCK_D: tl.constexpr, +): + pid_d = tl.program_id(0).to(tl.int64) + pid_n = tl.program_id(1).to(tl.int64) + offset_d = pid_d * BLOCK_D + tl.arange(0, BLOCK_D).to(tl.int64) + + tgt_ptrs = tgt_ptr + pid_n * D + offset_d + tgt_mask = offset_d < D + tgt = tl.load(tgt_ptrs, mask=tgt_mask, other=0) + + ignore_mask = not (tgt == ignore_index) and tgt_mask + + tmp_max = tl.zeros([BLOCK_C, BLOCK_D], dtype=tl.float32) + tmp_sum = tl.zeros([BLOCK_C, BLOCK_D], dtype=tl.float32) + + for off in range(0, C, BLOCK_C): + offset_c = off + tl.arange(0, BLOCK_C) + inp_ptrs = inp_ptr + pid_n * C * D + offset_c[:, None] * D + offset_d[None, :] + inp_mask = offset_c[:, None] < C and offset_d[None, :] < D + inp = tl.load(inp_ptrs, inp_mask, other=-float("inf")).to(tl.float32) + cur_max = tl.maximum(tmp_max, inp) + cur_exp = tl.exp(inp - cur_max) + tmp_sum = tmp_sum * tl.exp(tmp_max - cur_max) + cur_exp + tmp_max = cur_max + + final_max = tl.max(tmp_max, axis=0) + tmp_sum = tmp_sum * tl.exp(tmp_max - final_max[None, :]) + final_sum = tl.log(tl.sum(tmp_sum, axis=0)) + inp_tgt_ptrs = inp_ptr + pid_n * C * D + tgt * D + offset_d + inp_tgt = tl.load(inp_tgt_ptrs, mask=tgt_mask, other=-float("inf")).to(tl.float32) + + out = final_sum + final_max - inp_tgt + w_tgt_ptrs = w_tgt_ptr + pid_n * D + offset_d + + if w_ptr is None: + w_tgt = ignore_mask + else: + w_tgt = tl.load(w_ptr + tgt, mask=tgt_mask, other=0).to(tl.float32) + w_tgt = tl.where(ignore_mask, w_tgt, 0) + + tl.store(w_tgt_ptrs, w_tgt, mask=tgt_mask) + out *= w_tgt + out_ptrs = out_ptr + pid_n * D + offset_d + tl.store(out_ptrs, out, mask=tgt_mask) + + +def test_celoss_indices_kernel(shape=(1, 2)): + device = "npu" + dtype = torch.float16 + ignore_index = -100 + BLOCK_C = 256 + BLOCK_D = 1 + + N, C = shape + D = 1 + + inp = torch.randn(shape, dtype=dtype, device=device) + tgt = torch.randint(0, C, (N, ), dtype=torch.int64, device=device) + wgt = torch.randn(C, dtype=dtype, device=device) + + out_triton = torch.empty((N * D, ), dtype=torch.float32, device=device) + w_tgt_triton = torch.empty((N * D, ), dtype=torch.float32, device=device) + + grid = (triton.cdiv(D, BLOCK_D), N) + celoss_indices_kernel[grid]( + inp, + tgt, + wgt, + out_triton, + w_tgt_triton, + ignore_index, + C, + D, + BLOCK_C=BLOCK_C, + BLOCK_D=BLOCK_D, + ) diff --git a/third_party/ascend/unittest/pytest_ut/test_chunk_coalescing.py b/third_party/ascend/unittest/pytest_ut/test_chunk_coalescing.py new file mode 100644 index 0000000000..0e25c037d4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_chunk_coalescing.py @@ -0,0 +1,127 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import triton +import triton.language as tl + +import test_common + + +@triton.jit +def kernel_chunk_coalescing_axis1(src, dst, N: tl.constexpr, BLOCK: tl.constexpr): + batch = tl.program_id(0) + tile = tl.program_id(1) + offsets = tile * BLOCK + tl.arange(0, BLOCK) + mask = offsets < N + base = batch * N + values = tl.load(src + base + offsets, mask=mask, other=0.0) + tl.store(dst + base + offsets, values, mask=mask) + + +@triton.jit +def kernel_chunk_coalescing_axis0(src, dst, N: tl.constexpr, BLOCK: tl.constexpr): + tile = tl.program_id(0) + offsets = tile * BLOCK + tl.arange(0, BLOCK) + mask = offsets < N + values = tl.load(src + offsets, mask=mask, other=0.0) + tl.store(dst + offsets, values, mask=mask) + + +@triton.jit +def kernel_chunk_coalescing_axis2(src, dst, N: tl.constexpr, BLOCK: tl.constexpr): + tile = tl.program_id(2) + offsets = tile * BLOCK + tl.arange(0, BLOCK) + mask = offsets < N + values = tl.load(src + offsets, mask=mask, other=0.0) + tl.store(dst + offsets, values, mask=mask) + + +@pytest.mark.parametrize("dtype", ["float32", "float16"]) +def test_chunk_coalescing_grid_axis1_e2e(dtype): + batch = 2 + block = 16 + num_tiles = 32 + n = block * num_tiles + src = test_common.generate_tensor((batch, n), dtype).npu() + dst = torch.empty_like(src) + + kernel_chunk_coalescing_axis1[(batch, num_tiles)]( + src, + dst, + n, + BLOCK=block, + ) + + assert torch.equal(dst.cpu(), src.cpu()) + + +def test_chunk_coalescing_tail_e2e(): + """A non-divisible tail remains a masked, conservative no-op candidate.""" + batch = 2 + block = 16 + num_tiles = 32 + n = block * num_tiles - 3 + src = test_common.generate_tensor((batch, n), "float32").npu() + dst = torch.empty_like(src) + + kernel_chunk_coalescing_axis1[(batch, num_tiles)]( + src, + dst, + n, + BLOCK=block, + ) + + assert torch.equal(dst.cpu(), src.cpu()) + + +def test_chunk_coalescing_grid_axis0_e2e(): + block = 16 + num_tiles = 32 + n = block * num_tiles + src = test_common.generate_tensor((n, ), "float32").npu() + dst = torch.empty_like(src) + + kernel_chunk_coalescing_axis0[(num_tiles, )]( + src, + dst, + n, + BLOCK=block, + ) + + assert torch.equal(dst.cpu(), src.cpu()) + + +def test_chunk_coalescing_grid_axis2_e2e(): + block = 16 + num_tiles = 32 + n = block * num_tiles + src = test_common.generate_tensor((n, ), "float32").npu() + dst = torch.empty_like(src) + + kernel_chunk_coalescing_axis2[(1, 1, num_tiles)]( + src, + dst, + n, + BLOCK=block, + ) + + assert torch.equal(dst.cpu(), src.cpu()) diff --git a/third_party/ascend/unittest/pytest_ut/test_clamp.py b/third_party/ascend/unittest/pytest_ut/test_clamp.py new file mode 100644 index 0000000000..426bbd01db --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_clamp.py @@ -0,0 +1,62 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time +import torch +import torch_npu +import test_common + + +def torch_clamp_float(x0): + res = torch.clamp(x0, 0.0, 100.0) + return res + + +@triton.jit +def triton_clamp_float(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.clamp(tmp0, 0.0, 100.0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + # int原生不支持 + ['float16', (4, 4), 4, 4, 4], + ['float32', (4, 4), 4, 4, 4], +]) +def test_clamp(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype) + y_ref = torch_clamp_float(x0) + tyname = test_common.get_triton_sig_typename(dtype) + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + x0 = x0.npu() + triton_clamp_float[ncore, 1, 1](x0, y_cal, xblock, xblock_sub, debug=True) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_common.py b/third_party/ascend/unittest/pytest_ut/test_common.py new file mode 100644 index 0000000000..3b1a4d2130 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_common.py @@ -0,0 +1,347 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from typing import Optional +import torch +import torch_npu +import pytest +import functools +import re +import numpy as np + +_float_dtypes = ['float32', 'float16', 'bfloat16'] +_int_dtypes = ['int32', 'int64', 'int16', 'int8'] +_uint_dtypes = ['uint8', 'uint16', 'uint32', 'uint64'] +_all_dtypes_no_bool = _float_dtypes + _int_dtypes +_all_dtypes = _all_dtypes_no_bool + ['bool'] +_32bit_dtypes = ['float32', 'int32'] +_16bit_dtypes = ['float16', 'bfloat16', 'int16'] + + +def generate_numpy(shape, dtype, low=None, high=None): + if dtype in _int_dtypes + _uint_dtypes: + iinfo = np.iinfo(getattr(np, dtype)) + low = iinfo.min if low is None else max(low, iinfo.min) + high = iinfo.max if high is None else min(high, iinfo.max) + dty = getattr(np, dtype) + return np.random.randint(low, high, shape, dtype=dty) + elif dtype == 'float16' or dtype == 'float32': + return np.random.normal(0, 1, shape).astype(dtype) + elif dtype == 'bfloat16': + return (np.random.normal(0, 1, shape).astype('float32').view('uint32') & np.uint32(0xffff0000)).view('float32') + elif dtype == 'bool': + return np.random.randint(low=0, high=2, size=shape).astype(bool) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +def generate_tensor(shape, dtype): + if dtype == 'float32' or dtype == 'float16' or dtype == 'bfloat16': + return torch.randn(size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16': + return torch.randint(low=0, high=2000, size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'int8': + return torch.randint(low=0, high=127, size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'bool': + return torch.randint(low=0, high=2, size=shape).bool() + elif dtype == 'uint8': + return torch.randint(low=0, high=255, size=shape, dtype=torch.uint8) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +def generate_tensor_fp8( + shape, dtype, seed=None, data_range=None, + special_ratio=0.05, # Ratio of inf/-inf/nan values (only takes effect when dtype is float type) + precision_ratio=0.35, # Ratio of precision small floats (only takes effect when dtype is float type) + extreme_ratio=0.1, # Ratio of extreme value region (20% for min~min+10, max-10~max, 80% for min and max themselves) (no effect when dtype is bool) + zero_ratio=0, # Extra ratio of zeros (no effect when dtype is bool) + bool_true_ratio=0.5, # Ratio of True (only takes effect when dtype is bool) +): + middle_range_dict = { + 'fp8e4m3': [-50, 50], + 'fp8e5m2': [-500, 500], + 'fp8e5b16': [-500, 500], + } + + float_precision_dict = { + 'fp8e4m3': [-1, 1], + 'fp8e5m2': [-1, 1], + 'fp8e5b16': [-1, 1], + } + + extreme_range_dict = { + 'fp8e4m3': [-448, 448], + 'fp8e5m2': [-57344, 57344], + 'fp8e5b16': [-57344, 57344], + } + + ddtype_dict = { + 'fp8e4m3': torch.float8_e4m3fn, + 'fp8e5m2': torch.float8_e5m2, + 'fp8e5b16': None, + } + + if seed is not None: + torch.manual_seed(seed) + np.random.seed(seed) + + if dtype not in ddtype_dict: + raise ValueError(f"Unsupported dtype: {dtype}") + + min_val, max_val = extreme_range_dict[dtype] + total = np.prod(shape) + + left_range = (min_val, min_val + 10) + right_range = (max_val - 10, max_val) + + if data_range == None: + mid_range = middle_range_dict[dtype] + else: + mid_range = data_range + + # Process float types + if dtype in ('float16', 'float32', 'bfloat16', 'fp8e4m3', 'fp8e5m2'): + + # Specified distribution probability and handling when exceeding 100% + if (extreme_ratio + precision_ratio + zero_ratio + special_ratio > 1): + raise ValueError(f"Total ratio over 100%, unable to allocate sampling:{extreme_ratio + precision_ratio}") + + # Read dictionary to get precision small float data range + precision_range = float_precision_dict[dtype] + + # Generate initial 1D zero data, float32 to prevent overflow + data = np.zeros(total, dtype=np.float32) + + # Calculate counts for each interval (extreme region cumulative: near min, exact min, near max, exact max order) + left_count = int(total * extreme_ratio / 10) + left_accumulate_count = int(total * extreme_ratio / 2) + left_right_count = int(total * extreme_ratio * 3 / 5) + left_right_accmulate_count = int(total * extreme_ratio) + precision_count = int(total * precision_ratio) + zero_count = int(total * zero_ratio) + special_count = int(total * special_ratio) + + # Generate shuffled index mapping array + indices = np.random.choice(total, size=total, replace=False) + + # Partition interval indices by counts + left_idx = indices[:left_count] + left_min_idx = indices[left_count:left_accumulate_count] + right_idx = indices[left_accumulate_count:left_right_count] + right_max_idx = indices[left_right_count:left_right_accmulate_count] + precision_idx = indices[left_right_accmulate_count:left_right_accmulate_count + precision_count] + zero_idx = indices[left_right_accmulate_count + precision_count:left_right_accmulate_count + precision_count + + zero_count] + special_idx = indices[left_right_accmulate_count + precision_count + zero_count:left_right_accmulate_count + + precision_count + zero_count + special_count] + mid_idx = indices[left_right_accmulate_count + precision_count + zero_count + special_count:] + + # Generate data by interval indices + data[left_idx] = np.random.uniform(left_range[0], left_range[1], size=len(left_idx)) + data[left_min_idx] = min_val + data[right_idx] = np.random.uniform(right_range[0], right_range[1], size=len(right_idx)) + data[right_max_idx] = max_val + data[precision_idx] = np.random.uniform(precision_range[0], precision_range[1], size=len(precision_idx)) + data[zero_idx] = 0 + for idx in special_idx: + r = np.random.rand() + if r < 1 / 3: + data[idx] = float('inf') + elif r < 2 / 3: + data[idx] = float('-inf') + else: + data[idx] = float('nan') + data[mid_idx] = np.random.uniform(mid_range[0], mid_range[1], size=len(mid_idx)) + + tensor = torch.from_numpy(data.reshape(shape)).to(ddtype_dict[dtype]) + + else: + raise ValueError(f"Unsupported dtype: {dtype}") + + return tensor + + +def get_triton_sig_typename(dtype): + if dtype == 'float32': + tyname = "*fp32" + elif dtype == 'int32': + tyname = "*i32" + elif dtype == 'int64': + tyname = "*i64" + elif dtype == 'float16': + tyname = "*fp16" + elif dtype == 'int16': + tyname = "*i16" + elif dtype == 'int8': + tyname = "*i8" + elif dtype == 'bool': + tyname = "*i1" + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + return tyname + + +# Relative error: abs(x_ref - x_cal) / abs(x_ref) +# Absolute error: abs(x_ref - x_cal) + + +# calculation type operators require different error range +# It is a stricter verification and not satisfied now, save it here +def validate_cal(dtype, y_cal, y_ref): + if dtype == 'float16': + if torch.mean(y_ref) < 0.001: + assert torch.abs(y_cal - y_ref) < 0.001, "|y_cal - y_ref| < 0.001 is required !" + else: + diff = torch.div(torch.abs(y_cal - y_ref), torch.abs(y_cal)) < 0.001 + # all true + assert diff.all(), "Relative error is less than 0.001 !" + if dtype == 'float32': + if torch.mean(y_ref) < 0.0001: + assert torch.abs(y_cal - y_ref) < 0.0001, "|y_cal - y_ref| < 0.0001 is required !" + else: + diff = torch.div(torch.abs(y_cal - y_ref), torch.abs(y_cal)) < 0.0001 + assert diff.all(), "Relative error is less than 0.001 !" + elif dtype == 'bfloat16': + diff = torch.div(torch.abs(y_cal - y_ref), torch.abs(y_cal)) < 0.001 + assert diff.all(), "Relative error is less than 0.001 !" + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16' or dtype == 'int8': + assert torch.equal(y_cal, y_ref) + elif dtype == 'uint8' or dtype == 'uint16' or dtype == 'uint32' or dtype == 'uint64': + assert torch.equal(y_cal, y_ref) + elif dtype == 'bool': + assert torch.equal(y_cal, y_ref) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +# moving and comparison ops require no precision error +def validate_cmp(dtype, y_cal, y_ref, overflow_mode: Optional[str] = None): + y_cal = y_cal.npu() + y_ref = y_ref.npu() + if overflow_mode == "saturate": + if dtype in ['float32', 'float16']: + min_value = -torch.finfo(dtype).min + max_value = torch.finfo(dtype).max + elif dtype in ['int32', 'int16', 'int8']: + min_value = torch.iinfo(dtype).min + max_value = torch.iinfo(dtype).max + elif dtype == 'bool': + min_value = 0 + max_value = 1 + else: + raise ValueError('Invalid parameter "dtype" is found : {}'.format(dtype)) + y_ref = torch.clamp(y_ref, min=min_value, max=max_value) + if dtype == 'float16': + torch.testing.assert_close(y_ref, y_cal, rtol=1e-03, atol=1e-03, equal_nan=True) + elif dtype == 'bfloat16': + torch.testing.assert_close(y_ref.to(torch.float32), y_cal.to(torch.float32), rtol=1e-03, atol=1e-03, + equal_nan=True) + elif dtype == 'float32': + torch.testing.assert_close(y_ref, y_cal, rtol=1e-04, atol=1e-04, equal_nan=True) + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16' or dtype == 'int8': + assert torch.equal(y_cal, y_ref) + elif dtype == 'uint8' or dtype == 'uint16' or dtype == 'uint32' or dtype == 'uint64': + assert torch.equal(y_cal, y_ref) + elif dtype == 'bool': + assert torch.equal(y_cal, y_ref) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +def validate_cmp_with_expection(dtype, y_cal, y_ref, expect): + if dtype == 'float32' or dtype == 'float16' or dtype == 'bfloat16': + if expect: + assert torch.allclose(y_ref, y_cal, rtol=1e-03, atol=1e-03, equal_nan=True) + else: + assert not torch.allclose(y_ref, y_cal, rtol=1e-03, atol=1e-03, equal_nan=True) + elif dtype == 'int32' or dtype == 'int64' or dtype == 'int16' or dtype == 'int8' \ + or dtype == 'uint8' or dtype == 'uint16' or dtype == 'uint32' or dtype == 'uint64': + if expect: + assert torch.equal(y_cal, y_ref) + else: + assert not torch.equal(y_cal, y_ref) + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + + +# Use the following pytest fixture to run one test case by only single worker. +# Refer to https://pytest-xdist.readthedocs.io/en/stable/how-to.html#making-session-scoped-fixtures-execute-only-once +@pytest.fixture(scope="function") +def pytest_runonce(worker_id, request, cache): + if (cache.get(request.node.nodeid, "none")) == "none": + cache.set(request.node.nodeid, worker_id) + else: + file_name = f"pytest_{worker_id}.txt" + with open(file_name, 'a') as file: + file.write(f"{request.node.nodeid} is already processed by {worker_id}") + return True + yield True + cache.set(request.node.nodeid, "none") + + +def raises_with_match(expected_exception, match_pattern): + + def decorator(test_func): + + @functools.wraps(test_func) + def wrapper(*args, **kwargs): + with pytest.raises(expected_exception, match=match_pattern): + return test_func(*args, **kwargs) + + return wrapper + + return decorator + + +def capture_output(expected_output): + + def decorator(test_func): + + @functools.wraps(test_func) + def wrapper(*args, **kwargs): + capsys = kwargs.pop('capsys', None) + if capsys is None: + try: + capsys = pytest.fixture(capsys)() + except: + raise RuntimeError("This decorator requires pytest's capsys fixture") + test_func(capsys, *args, **kwargs) + captured = capsys.readouterr() + # pybind11::scoped_ostream_redirect captures std::cout with \x00 inserted + # for now, no idea how to eliminate \x00 from C++ side. + cleaned = re.sub(r"\x00", "", captured.out) + assert expected_output in cleaned + + return wrapper + + return decorator + + +def pytest_collection_modifyitems(config, items): + keep_items = [] + for item in items: + if not hasattr(item, "callspec"): + continue + mark_obj = pytest.mark.case_info(level="P0", type="Functional") + item.add_marker(mark_obj) + #if not any(m.name == "skip" for m in item.own_markers): + # keep_items.append(item) + #items[:] = keep_items diff --git a/third_party/ascend/unittest/pytest_ut/test_compile_hint.py b/third_party/ascend/unittest/pytest_ut/test_compile_hint.py new file mode 100644 index 0000000000..82e624943c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_compile_hint.py @@ -0,0 +1,69 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +import pytest +import test_common + +# eg: pytest -v test.py::test_compile_hint +############################# + + +@triton.jit +def triton_compile_hint(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + al.compile_hint(tmp0, "hint_a") + al.multibuffer(tmp0, 2) + tmp2 = tmp0 + al.compile_hint(tmp2, "hint_b", 42) + al.compile_hint(tmp2, "hint_c", True) + al.compile_hint(tmp2, "hint_d", [XBLOCK, XBLOCK_SUB]) + tl.store(out_ptr0 + (xindex), tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_compile_hint(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = x0 + y_cal = test_common.generate_tensor(shape, dtype).npu() + h = triton_compile_hint[(ncore, )](x0, y_cal, x0.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + # Numeric correctness alone doesn't prove the hints reached the compiler: + # compile_hint/multibuffer don't change kernel semantics, so a wrong or + # dropped hint would still produce a numerically correct result. Check + # the lowered IR actually carries each annotation.mark with its value. + code_str = h.asm["ttadapter"] + assert code_str.count("annotation.mark") == 5 + assert "{hint_a}" in code_str + assert "{hivm.multi_buffer = 2 : i32}" in code_str + assert "{hint_b = 42 : i32}" in code_str + assert "{hint_c = true}" in code_str + assert f"{{hint_d = [{xblock}, {xblock_sub}]}}" in code_str diff --git a/third_party/ascend/unittest/pytest_ut/test_compiler.py b/third_party/ascend/unittest/pytest_ut/test_compiler.py new file mode 100644 index 0000000000..55d3e14a23 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_compiler.py @@ -0,0 +1,282 @@ +import json +import os +import sys +import warnings +from unittest.mock import MagicMock + +import pytest + +import triton +import triton.backends.ascend.compiler as compiler +from triton.backends.ascend import utils +from triton.backends.compiler import GPUTarget + +pytestmark = pytest.mark.backend("native") + + +@pytest.fixture(autouse=True) +def reset_option_warnings(monkeypatch): + monkeypatch.setattr(utils, "_WARNED_DEPRECATED_NPU_OPTIONS", set()) + + +@pytest.fixture(params=["Ascend910B1", "Ascend910_9581"]) +def backend(request): + return compiler.AscendBackend(GPUTarget("npu", request.param, 32)) + + +def _make_torch_npu_mock(cfg_dir): + """Create a mock torch_npu module whose __file__ is in cfg_dir.""" + mock = MagicMock() + mock.__file__ = os.path.join(cfg_dir, "__init__.py") + return mock + + +def _write_acl_config(cfg_dir, config): + """Write acl_default.json into cfg_dir.""" + cfg_path = os.path.join(cfg_dir, "acl_default.json") + with open(cfg_path, "w") as f: + json.dump(config, f) + return cfg_path + + +def test_get_simt_stack_limit_default(monkeypatch): + """Without torch_npu installed, should return default 1152.""" + monkeypatch.setitem(sys.modules, "torch_npu", None) + result = compiler.get_simt_stack_limit() + assert result == 1152 + + +def test_get_simt_stack_limit_from_config(monkeypatch, tmp_path): + """When acl_default.json has valid simt_stack_size, return it.""" + _write_acl_config(str(tmp_path), {"StackSize": {"simt_stack_size": 2048}}) + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert result == 2048 + + +def test_get_simt_stack_limit_no_StackSize_key(monkeypatch, tmp_path): + """When acl_default.json has no StackSize key, return default 1152.""" + _write_acl_config(str(tmp_path), {"OtherConfig": {"foo": 1}}) + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert result == 1152 + + +def test_get_simt_stack_limit_no_simt_stack_size_key(monkeypatch, tmp_path): + """When StackSize exists but simt_stack_size is absent, return default 1152.""" + _write_acl_config(str(tmp_path), {"StackSize": {"other_field": 100}}) + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert result == 1152 + + +def test_get_simt_stack_limit_empty_StackSize(monkeypatch, tmp_path): + """When StackSize is an empty dict, return default 1152.""" + _write_acl_config(str(tmp_path), {"StackSize": {}}) + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert result == 1152 + + +def test_get_simt_stack_limit_empty_config(monkeypatch, tmp_path): + """When acl_default.json is an empty dict, return default 1152.""" + _write_acl_config(str(tmp_path), {}) + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert result == 1152 + + +def test_get_simt_stack_limit_file_not_found(monkeypatch, tmp_path): + """When acl_default.json does not exist, return default 1152.""" + # Do not write acl_default.json; only mock torch_npu.__file__ + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert result == 1152 + + +def test_get_simt_stack_limit_invalid_json(monkeypatch, tmp_path): + """When acl_default.json is invalid JSON, return default 1152.""" + cfg_path = os.path.join(str(tmp_path), "acl_default.json") + with open(cfg_path, "w") as f: + f.write("{invalid json content}") + + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert result == 1152 + + +def test_get_simt_stack_limit_from_config_returns_int(monkeypatch, tmp_path): + """Return value from config should be an integer.""" + _write_acl_config(str(tmp_path), {"StackSize": {"simt_stack_size": 2048}}) + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert isinstance(result, int) + assert result == 2048 + + +def test_get_simt_stack_limit_config_overrides_default(monkeypatch, tmp_path): + """Config value should take precedence over default 1152.""" + _write_acl_config(str(tmp_path), {"StackSize": {"simt_stack_size": 9999}}) + mock_npu = _make_torch_npu_mock(str(tmp_path)) + monkeypatch.setitem(sys.modules, "torch_npu", mock_npu) + + result = compiler.get_simt_stack_limit() + assert result == 9999 + assert result != 1152 + + +def test_empty_options_expose_all_legacy_names(backend): + options = backend.parse_options({}) + + assert utils._DEPRECATED_NPU_OPTIONS <= options.__dict__.keys() + assert options.__dict__["arch"] == options.arch == backend.target.arch + assert options.__dict__["warp_size"] == 32 + assert options.__dict__["use_bytecode"] is True + assert options.__dict__["enable_vf_fusion"] is None + + +def test_active_options_keep_explicit_values(backend): + raw = {"enable_mixed_cv": True, "enable_dynamic_cv_pipeline": False, "num_warps": 16} + + options = backend.parse_options(raw) + + assert options.enable_mixed_cv is True + assert options.enable_dynamic_cv_pipeline is False + assert options.num_warps == 16 + + +_IGNORED_OPTIONS = sorted(utils._DEPRECATED_NPU_OPTIONS - utils._DEPRECATED_NPU_OPTION_ROUTES.keys() - + utils._DEPRECATED_NPU_OPTION_ALIASES.keys() - {"stream"}) + + +@pytest.mark.parametrize("name", _IGNORED_OPTIONS) +def test_ignored_values_preserve_defaults_and_input(backend, name): + baseline = backend.parse_options({}) + raw = {name: object()} + original = dict(raw) + + with pytest.warns(FutureWarning, match=name): + options = backend.parse_options(raw) + + assert raw == original + assert options.__dict__ == baseline.__dict__ + assert options.hash() == baseline.hash() + + +@pytest.mark.parametrize("value", [None, "", "--unsupported-bisheng-option"]) +def test_legacy_bisheng_options_launch_uses_backend_defaults(backend, value): + + @triton.jit + def kernel(): + pass + + baseline = backend.parse_options({}) + kwargs = {"bisheng_options": value} + + with pytest.warns(FutureWarning, match="bisheng_options.*backend-managed"): + options, signature, constexprs, attrs = kernel._pack_args(backend, kwargs, {}, [], None) + + assert kwargs == {"bisheng_options": value} + assert options.bisheng_options == "-cce-link-aicore-ll-module " + compiler.get_libdevice() + assert options.__dict__ == baseline.__dict__ + assert options.hash() == baseline.hash() + assert signature == constexprs == attrs == {} + + +@pytest.mark.parametrize("stream", [None, 0, object()], ids=["none", "zero", "unused-object"]) +def test_legacy_stream_is_consumed_without_losing_option_discovery(backend, stream): + baseline = backend.parse_options({"num_warps": 16}) + raw = {"stream": stream, "num_warps": 16, "kernel_arg": 42} + + with pytest.warns(FutureWarning, match="stream"): + options = backend.parse_options(raw) + + assert raw == {"num_warps": 16, "kernel_arg": 42} + assert options.__dict__["stream"] is None + assert options.__dict__ == baseline.__dict__ + assert options.hash() == baseline.hash() + + +@pytest.mark.parametrize( + "raw, expected", + [ + ({"force_simt_only": True, "compile_mode": "simd"}, {"compile_mode": "simt_only"}), + ({"force_simt_only": True, "force_simt_template": True}, {"compile_mode": "simt_only"}), + ({"force_simt_template": True}, {"compile_mode": "simd_simt_template"}), + ({"force_simt_only": False, "compile_mode": "simd"}, {"compile_mode": "simd"}), + ({"intra_cache_num": 4}, {"buf_slot_num_of_veccore": 4}), + ({"inter_cache_num": 3}, {"buf_slot_num_of_crosscore": 3}), + ({"load_cache_num": 2}, {"buf_slot_num_of_gm": 2}), + ({"intra_cache_num": 4, "buf_slot_num_of_veccore": 6}, {"buf_slot_num_of_veccore": 6}), + ({"enable_bishengir_simt_optimization": 101, "compile_mode": "simt_only" + }, {"simt_optimization_mode": 101, "compile_mode": "simt_only"}), + ({"enable_bishengir_simt_optimization": 101, "simt_optimization_mode": 100, "compile_mode": "simt_only" + }, {"simt_optimization_mode": 100, "compile_mode": "simt_only"}), + ], +) +def test_legacy_routes_preserve_precedence_and_input(raw, expected): + backend = compiler.AscendBackend(GPUTarget("npu", "Ascend910_9581", 32)) + original = dict(raw) + baseline = backend.parse_options(expected) + + with pytest.warns(FutureWarning): + options = backend.parse_options(raw) + + assert raw == original + assert options.__dict__ == baseline.__dict__ + assert options.hash() == baseline.hash() + + +def test_simt_alias_obeys_target_and_mode_restrictions(backend): + raw = {"enable_bishengir_simt_optimization": 101} + + with pytest.warns((FutureWarning, UserWarning)) as caught: + options = backend.parse_options(raw) + + assert any("only takes effect" in str(warning.message) for warning in caught) + assert options.simt_optimization_mode == 0 + assert raw == {"enable_bishengir_simt_optimization": 101} + + +def test_internal_grid_value_survives_serialization_and_reparse(backend): + raw = {"grid_num_tiles": utils._InternalNPUOptionInt(8)} + options = backend.parse_options(raw) + serialized = json.loads(json.dumps(options.__dict__)) + # JITFunction.preload restores tuple-valued options after JSON decoding. + restored = {name: tuple(value) if isinstance(value, list) else value for name, value in serialized.items()} + original = dict(restored) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + reparsed = backend.parse_options(restored) + + assert not caught + assert restored == original + assert reparsed.grid_num_tiles == options.grid_num_tiles == 8 + assert reparsed.__dict__ == options.__dict__ + assert reparsed.hash() == options.hash() + + +def test_unknown_options_remain_available_for_jit_validation(backend): + raw = {"unknown_compile_option": True} + + options = backend.parse_options(raw) + + assert raw == {"unknown_compile_option": True} + assert "unknown_compile_option" not in options.__dict__ diff --git a/third_party/ascend/unittest/pytest_ut/test_compiler_layout_memory_contract.py b/third_party/ascend/unittest/pytest_ut/test_compiler_layout_memory_contract.py new file mode 100644 index 0000000000..2a6a6b0ed7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_compiler_layout_memory_contract.py @@ -0,0 +1,1052 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +"""Source-level contracts for the layout / memory-access compiler closure. + +These tests intentionally load ``backend/compiler.py`` from this checkout. +The installed Triton package can point at another worktree, so importing +``triton.backends.ascend.compiler`` directly would not validate the source +being changed here. +""" + +import ast +import importlib.util +import itertools +import json +import sys +import types +import warnings +from collections import defaultdict +from pathlib import Path +from types import SimpleNamespace + +import pytest + +pytestmark = pytest.mark.backend("none") + +_UNSET = object() + + +def _stub_ub_size_in_kbytes_for_arch(arch): + """Mirror the raw UB architecture table for the compiler import shim. + + The table itself is covered by the source-loaded backend-utils test. This + local shim keeps this compiler-only contract independent of the installed + Ascend package while retaining meaningful architecture-budget assertions. + """ + if not isinstance(arch, str) or not arch: + return 0 + if arch.startswith(("Ascend910_95", "Ascend950")): + return 256 + if arch.startswith(( + "Ascend910A", + "Ascend910B", + "Ascend910D", + "Ascend910_93", + "Ascend310B", + )): + return 192 + return 0 + + +def _stub_graph_ub_budget_bytes_for_arch(arch): + return _stub_ub_size_in_kbytes_for_arch(arch) * 1024 * 80 // 100 + + +class _FakeModule: + + def __init__(self, events): + self.context = object() + self._events = events + self._string_count = 0 + + def __str__(self): + self._events.append(f"str:{self._string_count}") + self._string_count += 1 + return "module {}" + + +class _FakePassManager: + + def __init__(self, events): + self._events = events + + def enable_debug(self): + self._events.append("enable_debug") + + def run(self, _module, _pipeline_name): + self._events.append("run_row") + + +@pytest.fixture(scope="module") +def compiler_module(): + """Load this checkout's compiler without depending on installed Ascend utils. + + The test invokes only ``ttir_to_npubin`` and replaces all external tool + interactions below. A tiny import-time shim keeps the source-level + contract test runnable when the installed Triton wheel predates an import + added by this checkout (for example ``_enable_msdebug``). + """ + compiler_path = Path(__file__).resolve().parents[2] / "backend" / "compiler.py" + module_name = "triton.backends.ascend.compiler_layout_memory_contract_under_test" + utils_name = "triton.backends.ascend.utils" + driver_name = "triton.backends.ascend.driver" + cache_name = "triton.runtime.cache" + + def return_false(*_args, **_kwargs): + return False + + def remove_deprecated_npu_options(options, *, in_place=False): + normalized = options if in_place else dict(options) + normalized.pop("compile_on_910_95", None) + return normalized + + utils_stub = types.ModuleType(utils_name) + utils_path = compiler_path.with_name("utils.py") + utils_tree = ast.parse(utils_path.read_text()) + mode_helper = next(node for node in utils_tree.body + if isinstance(node, ast.FunctionDef) and node.name == "_multibuffer_mode_to_tuple") + exec(compile(ast.Module(body=[mode_helper], type_ignores=[]), str(utils_path), "exec"), utils_stub.__dict__) + for name in ( + "_check_bishengir_api_change", + "_check_bishengir_able_save_ir", + "_check_bishengir_is_regbased", + "_enable_print_ub_bits", + "_enable_dump_memory_info", + "_enable_msdebug", + "_is_ascend_sanitizer_enabled", + "_is_debug_line_info_disabled", + "_is_auto_map_parallel_blocks_enabled", + "force_disable_ffts", + "is_cann_version_at_least", + ): + setattr(utils_stub, name, return_false) + for name in ( + "_get_kernel_target", + "_get_triton_adapter_opt_path", + "_get_triton_mlir_opt_path", + "_get_triton_opt_path", + "_get_bishengir_opt_path", + ): + setattr(utils_stub, name, lambda *_args, **_kwargs: "") + utils_stub._get_npucompiler_path = lambda *_args, **_kwargs: ("", {}) + utils_stub._get_auto_blockify_blacklist_reasons = lambda *_args, **_kwargs: [] + utils_stub._warn_auto_blockify_disabled = lambda *_args, **_kwargs: None + utils_stub._remove_deprecated_npu_options = remove_deprecated_npu_options + utils_stub._warn_deprecated_npu_option = lambda name: warnings.warn( + f"Ascend compile option '{name}' is deprecated and ignored.", FutureWarning) + utils_stub._warn_deprecated_ascend_env_vars = lambda: None + utils_stub.downgrade_llir = lambda llir: llir + utils_stub.get_cann_version_file_hash = lambda: "" + utils_stub.graph_ub_budget_bytes_for_arch = _stub_graph_ub_budget_bytes_for_arch + utils_stub.ub_size_in_kbytes_for_arch = _stub_ub_size_in_kbytes_for_arch + + class UnusedNPUUtils: + pass + + driver_stub = types.ModuleType(driver_name) + driver_stub.NPUUtils = UnusedNPUUtils + + cache_stub = types.ModuleType(cache_name) + cache_stub._base32 = lambda value: str(value) + dump_dir = Path("/fake/dump") + cache_stub.get_dump_manager = lambda *_args, **_kwargs: SimpleNamespace( + cache_dir=str(dump_dir), + _make_path=lambda filename: str(dump_dir / filename), + put=lambda *_args, **_kwargs: None, + ) + + # Initialize the installed Triton package before temporarily replacing its + # cache module below. Loading compiler.py starts from triton._C; doing it + # in the reverse order would make unrelated backend imports see the tiny + # test stub instead of the runtime cache API they require. + importlib.import_module("triton") + + previous_utils = sys.modules.get(utils_name) + previous_driver = sys.modules.get(driver_name) + previous_cache = sys.modules.get(cache_name) + sys.modules[utils_name] = utils_stub + sys.modules[driver_name] = driver_stub + sys.modules[cache_name] = cache_stub + sys.modules.pop(module_name, None) + debug_line_rewriter_name = "triton.backends.ascend.debug_line_rewriter" + previous_debug_line_rewriter = sys.modules.get(debug_line_rewriter_name) + sys.modules.pop(debug_line_rewriter_name, None) + try: + debug_line_rewriter_path = compiler_path.with_name("debug_line_rewriter.py") + debug_line_rewriter_spec = importlib.util.spec_from_file_location(debug_line_rewriter_name, + debug_line_rewriter_path) + debug_line_rewriter = importlib.util.module_from_spec(debug_line_rewriter_spec) + assert debug_line_rewriter_spec is not None and debug_line_rewriter_spec.loader is not None + sys.modules[debug_line_rewriter_name] = debug_line_rewriter + debug_line_rewriter_spec.loader.exec_module(debug_line_rewriter) + spec = importlib.util.spec_from_file_location(module_name, compiler_path) + module = importlib.util.module_from_spec(spec) + assert spec is not None and spec.loader is not None + sys.modules[module_name] = module + spec.loader.exec_module(module) + finally: + if previous_debug_line_rewriter is None: + sys.modules.pop(debug_line_rewriter_name, None) + else: + sys.modules[debug_line_rewriter_name] = previous_debug_line_rewriter + if previous_utils is None: + sys.modules.pop(utils_name, None) + else: + sys.modules[utils_name] = previous_utils + if previous_driver is None: + sys.modules.pop(driver_name, None) + else: + sys.modules[driver_name] = previous_driver + if previous_cache is None: + sys.modules.pop(cache_name, None) + else: + sys.modules[cache_name] = previous_cache + return module + + +def _parse_options(compiler, arch, opts=None): + backend = compiler.AscendBackend(SimpleNamespace(backend="npu", arch=arch)) + return backend.parse_options({} if opts is None else opts) + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +@pytest.mark.parametrize( + ("arch", "requested_capacity", "expected_capacity"), + ( + ("Ascend910B1", _UNSET, 192 * 1024 * 80 // 100), + ("Ascend910B1", None, 192 * 1024 * 80 // 100), + ("Ascend910_9581", None, 256 * 1024 * 80 // 100), + ("Ascend950A3", None, 256 * 1024 * 80 // 100), + ("Ascend910B1", 0, 0), + ("Ascend910B1", 4096, 4096), + ("Ascend910B1", 192 * 1024 * 80 // 100 + 1, 192 * 1024 * 80 // 100), + ("Ascend910_9581", 256 * 1024 * 80 // 100 + 1, 256 * 1024 * 80 // 100), + ("unknown-arch", None, 0), + ), +) +def test_npu_options_normalizes_graph_ub_budget(compiler_module, arch, requested_capacity, expected_capacity): + """Direct NPUOptions users receive the same final integer as JIT users.""" + kwargs = {"arch": arch} + if requested_capacity is not _UNSET: + kwargs["graph_optimize_ub_capacity_bytes"] = requested_capacity + + options = compiler_module.NPUOptions(**kwargs) + + assert options.graph_optimize_ub_capacity_bytes == expected_capacity + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +@pytest.mark.parametrize( + ("arch", "requested_capacity", "expected_capacity"), + ( + ("Ascend910B1", _UNSET, 192 * 1024 * 80 // 100), + ("Ascend910B1", None, 192 * 1024 * 80 // 100), + ("Ascend910_9581", None, 256 * 1024 * 80 // 100), + ("Ascend950A3", None, 256 * 1024 * 80 // 100), + ("Ascend910B1", 0, 0), + ("Ascend910B1", 4096, 4096), + ("Ascend910B1", 192 * 1024 * 80 // 100 + 1, 192 * 1024 * 80 // 100), + ), +) +def test_parse_options_normalizes_graph_ub_budget(compiler_module, arch, requested_capacity, expected_capacity): + opts = {} + if requested_capacity is not _UNSET: + opts["graph_optimize_ub_capacity_bytes"] = requested_capacity + + options = _parse_options(compiler_module, arch, opts) + + assert options.arch == arch + assert not hasattr(options, "_arch") + assert options.graph_optimize_ub_capacity_bytes == expected_capacity + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_normalized_graph_ub_budget_contributes_to_npu_hash(compiler_module): + auto = compiler_module.NPUOptions(arch="Ascend910B1") + explicit_none = compiler_module.NPUOptions(arch="Ascend910B1", graph_optimize_ub_capacity_bytes=None) + disabled = compiler_module.NPUOptions(arch="Ascend910B1", graph_optimize_ub_capacity_bytes=0) + small = compiler_module.NPUOptions(arch="Ascend910B1", graph_optimize_ub_capacity_bytes=4096) + clamped = compiler_module.NPUOptions(arch="Ascend910B1", + graph_optimize_ub_capacity_bytes=192 * 1024 * 80 // 100 + 1) + + assert auto.__dict__["graph_optimize_ub_capacity_bytes"] == 192 * 1024 * 80 // 100 + assert explicit_none.graph_optimize_ub_capacity_bytes == 192 * 1024 * 80 // 100 + assert clamped.graph_optimize_ub_capacity_bytes == 192 * 1024 * 80 // 100 + assert auto.hash() == explicit_none.hash() == clamped.hash() + assert auto.hash() != disabled.hash() + assert auto.hash() != small.hash() + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +@pytest.mark.parametrize( + ("requested_capacity", "error_type"), + ( + (-1, ValueError), + (True, TypeError), + (1.5, TypeError), + ), +) +def test_npu_options_rejects_invalid_graph_ub_budget_requests(compiler_module, requested_capacity, error_type): + with pytest.raises(error_type): + compiler_module.NPUOptions( + arch="Ascend910B1", + graph_optimize_ub_capacity_bytes=requested_capacity, + ) + + +def _make_opt( + *, + is_pure_simt, + superblock_factor=0, + simt_optimization_mode=0, + shared_mem_dynamic_size=None, + disable_fma=False, + compile_on_910_95=False, +): + return SimpleNamespace( + is_pure_simt=is_pure_simt, + num_warps=4, + warp_size=32, + simt_optimization_mode=simt_optimization_mode, + shared_mem_dynamic_size=shared_mem_dynamic_size, + disable_fma=disable_fma, + superblock_factor=superblock_factor, + compile_on_910_95=compile_on_910_95, + ) + + +def _make_program_grid_contract(*transforms): + return { + "version": 2, + "extent_source": "runtime_original_grid", + "hidden_extent_axes": [0, 1], + "hidden_argument_order": ["originalGridX", "originalGridY"], + "hidden_argument_types": ["i32", "i32"], + "transforms": list(transforms), + } + + +def _run_ttir_to_npubin( + compiler, + monkeypatch, + *, + is_pure_simt=True, + auto_map_enabled=False, + has_blacklist_op=False, + row_coalescing_applied=False, + superblock_factor=0, + common_options=(), + simt_optimization_mode=0, + resolved_simt_stack_limit=1152, + shared_mem_dynamic_size=None, + disable_fma=False, +): + events = [] + commands = [] + module = _FakeModule(events) + pass_manager = _FakePassManager(events) + + def parse_ttir_metadata(_ttir, metadata): + events.append("parse") + return { + **metadata, + "has_auto_blockify_blacklist_op": has_blacklist_op, + "mix_mode": "aiv", + "row_coalescing_applied": row_coalescing_applied, + } + + def export_coalesce_metadata(_mod, _metadata, *, require_row_contract=False): + events.append(f"export:{require_row_contract}") + + def run_bisheng(command, **_kwargs): + commands.append(list(command)) + output = Path(command[command.index("-o") + 1] + ".o") + output.write_bytes(b"npubin") + return SimpleNamespace(returncode=0, stdout=b"", stderr=b"") + + monkeypatch.setattr( + compiler, + "ir", + SimpleNamespace(pass_manager=lambda _context: (events.append("pass_manager") or pass_manager)), + ) + monkeypatch.setattr(compiler, "_parse_ttir_metadata", parse_ttir_metadata) + monkeypatch.setattr(compiler, "_export_coalesce_metadata", export_coalesce_metadata) + monkeypatch.setattr( + compiler, + "get_common_bishengir_compile_options", + lambda _metadata: list(common_options), + ) + monkeypatch.setattr(compiler, "_get_npucompiler_path", lambda: ("/fake/bisheng", {})) + monkeypatch.setattr( + compiler, + "_is_auto_map_parallel_blocks_enabled", + lambda: auto_map_enabled, + ) + + # Keep this argv matrix independent of the host torch_npu configuration + # while checking that Pure-SIMT uses the backend-resolved stack limit. + def get_simt_stack_limit(): + return resolved_simt_stack_limit + + monkeypatch.setattr(compiler, "get_simt_stack_limit", get_simt_stack_limit) + monkeypatch.setattr(compiler.subprocess, "run", run_bisheng) + + result = compiler.ttir_to_npubin( + module, + {"bisheng_options": None}, + _make_opt( + is_pure_simt=is_pure_simt, + superblock_factor=superblock_factor, + simt_optimization_mode=simt_optimization_mode, + shared_mem_dynamic_size=shared_mem_dynamic_size, + disable_fma=disable_fma, + ), + ) + assert result == b"npubin" + assert len(commands) == 1 + return events, commands[0] + + +def test_simt_stack_limit_is_not_a_compile_option(compiler_module): + assert "simt_stack_limit" not in compiler_module.NPUOptions.__dataclass_fields__ + assert "simt_stack_limit" not in _parse_options(compiler_module, "Ascend910_9581").__dict__ + with pytest.raises(TypeError, match="simt_stack_limit"): + compiler_module.NPUOptions(arch="Ascend910_9581", simt_stack_limit=8192) + + +def _run_linalg_to_npubin(compiler, monkeypatch, function_name, has_blacklist_op): + commands = [] + parsed_metadata = defaultdict( + lambda: None, { + "hash": "layout-memory-contract-test", + "target": SimpleNamespace(arch="Ascend910B"), + "program_grid_transforms": None, + "program_grid_mapping_applied": False, + "row_coalescing_applied": False, + "has_auto_blockify_blacklist_op": has_blacklist_op, + "auto_blockify_enabled": not has_blacklist_op, + "ptsm_cap_authorized": False, + "mix_mode": "aiv", + }) + + class FakeNPUUtils: + + def has_device_limit(self): + return False + + def get_arch(self): + return "Ascend910B" + + def parse_linalg_metadata(linalg, _metadata): + return linalg, parsed_metadata + + def run_bisheng(command, **_kwargs): + commands.append(list(command)) + Path(command[command.index("-o") + 1] + "_reloc.o").write_bytes(b"npubin") + return SimpleNamespace(returncode=0, stdout=b"", stderr=b"") + + monkeypatch.setattr(compiler, "_parse_linalg_metadata", parse_linalg_metadata) + monkeypatch.setattr(compiler, "get_common_bishengir_compile_options", lambda _metadata: []) + monkeypatch.setattr(compiler, "get_auto_bind_sub_block_option", lambda _metadata: False) + monkeypatch.setattr(compiler, "NPUUtils", FakeNPUUtils) + monkeypatch.setattr(compiler, "_get_npucompiler_path", lambda: ("/fake/bishengir-compile", {})) + monkeypatch.setattr(compiler, "_is_auto_map_parallel_blocks_enabled", lambda: True) + monkeypatch.setattr(compiler.subprocess, "run", run_bisheng) + + result = getattr(compiler, function_name)( + "module {}", + {}, + SimpleNamespace(debug=False, target_arch="Ascend950PR", is_pure_simt=False), + ) + assert result == b"npubin" + assert len(commands) == 1 + return commands[0] + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_export_coalesce_metadata_removes_attrs_and_marks_row(compiler_module, monkeypatch): + removed = [] + + def get_int_attr(module, name): + return module.attrs.get(name) + + def remove_attr(module, name): + removed.append(name) + module.attrs.pop(name, None) + + monkeypatch.setattr( + compiler_module, + "ascend", + SimpleNamespace(ir=SimpleNamespace( + get_int_attr=get_int_attr, + remove_attr=remove_attr, + )), + ) + + coalesced = SimpleNamespace(attrs={ + "hacc.coalesce_factor": 4, + "hacc.coalesce_axis": 2, + "hacc.coalesce_grid_ceil_div": 1, + }) + metadata = {} + compiler_module._export_coalesce_metadata(coalesced, metadata) + + assert metadata == { + "coalesce_factor": 4, + "coalesce_axis": 2, + "coalesce_grid_ceil_div": True, + "row_coalescing_applied": True, + } + assert coalesced.attrs == {} + assert removed == [ + "hacc.coalesce_factor", + "hacc.coalesce_axis", + "hacc.coalesce_grid_ceil_div", + ] + + uncoalesced = SimpleNamespace(attrs={}) + uncoalesced_metadata = {} + compiler_module._export_coalesce_metadata(uncoalesced, uncoalesced_metadata) + assert uncoalesced_metadata == { + "coalesce_factor": 1, + "coalesce_axis": -1, + "coalesce_grid_ceil_div": False, + "row_coalescing_applied": False, + } + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_export_coalesce_metadata_rejects_partial_row_contract(compiler_module, monkeypatch): + + def get_int_attr(module, name): + return module.attrs.get(name) + + def remove_attr(module, name): + module.attrs.pop(name, None) + + monkeypatch.setattr( + compiler_module, + "ascend", + SimpleNamespace(ir=SimpleNamespace( + get_int_attr=get_int_attr, + remove_attr=remove_attr, + )), + ) + + with pytest.raises(RuntimeError, match="launch contract"): + compiler_module._export_coalesce_metadata( + SimpleNamespace(attrs={"hacc.coalesce_factor": 4}), + {}, + require_row_contract=True, + ) + + with pytest.raises(RuntimeError, match="RowCoalescing"): + compiler_module._export_coalesce_metadata( + SimpleNamespace(attrs={ + "hacc.coalesce_factor": 4, + "hacc.coalesce_axis": 0, + }), + {}, + require_row_contract=True, + ) + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_ttir_to_npubin_exports_make_ttir_row_contract_only_for_pure_simt(compiler_module, monkeypatch): + events, _command = _run_ttir_to_npubin( + compiler_module, + monkeypatch, + is_pure_simt=True, + ) + assert events == [ + "str:0", + "parse", + "export:True", + "str:1", + ] + + with monkeypatch.context() as pure_simt_off: + events, _command = _run_ttir_to_npubin( + compiler_module, + pure_simt_off, + is_pure_simt=False, + ) + assert events == ["str:0", "parse"] + + +def _run_make_ttir_with_recorded_graph_options(compiler, monkeypatch, options): + events = [] + module = _FakeModule(events) + pass_manager = _FakePassManager(events) + graph_calls = [] + + def record(name): + return lambda _pm, *args, **kwargs: events.append((name, args, kwargs)) + + monkeypatch.setattr( + compiler, + "ir", + SimpleNamespace(pass_manager=lambda _context: pass_manager), + ) + monkeypatch.setattr( + compiler, + "passes", + SimpleNamespace( + common=SimpleNamespace( + add_inliner=record("inliner"), + add_canonicalizer=record("canonicalizer"), + add_cse=record("cse"), + add_licm=record("licm"), + add_symbol_dce=record("symbol_dce"), + ), + ttir=SimpleNamespace( + add_rewrite_tensor_descriptor_to_pointer=record("rewrite_tensor_descriptor_to_pointer"), + add_combine=record("combine"), + add_reorder_broadcast=record("reorder_broadcast"), + add_loop_unroll=record("loop_unroll"), + ), + ), + ) + monkeypatch.setattr( + compiler, + "ascend", + SimpleNamespace(passes=SimpleNamespace(ttir=SimpleNamespace( + add_graph_optimize=lambda _pm, **kwargs: graph_calls.append(kwargs)))), + ) + + assert compiler.make_ttir(module, {}, options) is module + return events, graph_calls + + +def test_make_ttir_passes_canonical_compile_mode_to_graph_optimize(compiler_module, monkeypatch): + options = SimpleNamespace( + enable_graph_optimize=True, + target_arch="Ascend910B1", + compile_mode="simt_only", + debug=False, + ) + + events, graph_calls = _run_make_ttir_with_recorded_graph_options(compiler_module, monkeypatch, options) + + assert graph_calls == [{ + "ub_capacity_bytes": 192 * 1024 * 80 // 100, + "compile_mode": "simt_only", + }] + assert events[-1] == "run_row" + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_npu_options_keep_graph_remark_compatibility_default(compiler_module): + """The legacy graph-remarks name remains discoverable with a fixed default.""" + options = compiler_module.NPUOptions(arch="Ascend910B1") + assert options.__dict__["graph_optimize_emit_remarks"] is False + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +@pytest.mark.parametrize( + ("arch", "expected_capacity"), + ( + ("Ascend910B1", 192 * 1024 * 80 // 100), + ("Ascend910_9581", 256 * 1024 * 80 // 100), + ("Ascend950A3", 256 * 1024 * 80 // 100), + ("unknown-arch", 0), + ), +) +def test_make_ttir_forwards_normalized_graph_ub_budget(compiler_module, monkeypatch, arch, expected_capacity): + options = compiler_module.NPUOptions(arch=arch) + + events, graph_calls = _run_make_ttir_with_recorded_graph_options(compiler_module, monkeypatch, options) + + assert graph_calls[0]["ub_capacity_bytes"] == expected_capacity + assert events[-1] == "run_row" + + +def test_ttir_to_npubin_auto_blockify_argv_matrix(compiler_module, monkeypatch): + common_options = ["--common-before-pure-simt", "--common-after-pure-simt"] + pure_simt_prefix = [ + "--enable-hivm-compile=false", + "--enable-triton-ir-compile", + "--pure-simt", + "--num-warps=4", + "--threads-per-warp=32", + "--simt-optimization-mode=1000017", + "--simt-stack-limit=64", + "--shared-mem-dynamic-size=4096", + "--disable-fma", + ] + auto_blockify_flag = "--enable-auto-blockify-loop" + + for env_enabled, blacklisted, row_applied, superblock in itertools.product( + (False, True), + (False, True), + (False, True), + (0, 7), + ): + with monkeypatch.context() as case_monkeypatch: + _events, command = _run_ttir_to_npubin( + compiler_module, + case_monkeypatch, + auto_map_enabled=env_enabled, + has_blacklist_op=blacklisted, + row_coalescing_applied=row_applied, + superblock_factor=superblock, + common_options=common_options, + simt_optimization_mode=1000017, + resolved_simt_stack_limit=64, + shared_mem_dynamic_size=4096, + disable_fma=True, + ) + + second_injection = env_enabled and not row_applied + case = f"E={env_enabled}, B={blacklisted}, R={row_applied}, superblock={superblock}" + + expected_options = [*common_options, *pure_simt_prefix] + if second_injection: + expected_options.append(auto_blockify_flag) + if superblock > 0: + expected_options.append(f"--super-block-factor={superblock}") + + assert command[0] == "/fake/bisheng", case + assert Path(command[1]).name == "kernel.ttir.mlir", case + assert command[2:-2] == expected_options, case + assert command[-2] == "-o", case + assert Path(command[-1]).name == "kernel", case + + +@pytest.mark.parametrize( + ("auto_map_enabled", "blacklisted", "row_applied", "transforms", "expected_auto", "expected_ptsm"), + ( + (False, False, False, None, False, False), + (True, False, False, None, True, False), + (True, False, True, None, True, False), + (True, True, False, None, False, False), + (True, False, False, ((0, 1, 16, False, False), ), True, False), + (True, False, False, ((0, 0, 64, True, True), ), False, True), + (True, False, False, ((0, 1, 16, False, False), (1, 0, 4, True, True)), False, True), + ), +) +def test_finalize_program_launch_policy_uses_linalg_ptsm_gate( + compiler_module, + monkeypatch, + auto_map_enabled, + blacklisted, + row_applied, + transforms, + expected_auto, + expected_ptsm, +): + contract = None + if transforms is not None: + contract = _make_program_grid_contract(*(dict( + order=order, + kind="ceil_div", + axis=axis, + factor=factor, + persistent_coverage=persistent, + grid_stride_abi_verified=grid_stride, + ) for order, axis, factor, persistent, grid_stride in transforms)) + metadata = { + "program_grid_transforms": contract, + "program_grid_mapping_applied": contract is not None, + "row_coalescing_applied": row_applied, + "has_auto_blockify_blacklist_op": blacklisted, + "mix_mode": "aiv", + } + monkeypatch.setattr(compiler_module, "_is_auto_map_parallel_blocks_enabled", lambda: auto_map_enabled) + + compiler_module._finalize_program_launch_policy(metadata, SimpleNamespace(is_pure_simt=False)) + + assert metadata["auto_blockify_enabled"] is expected_auto + assert metadata["ptsm_cap_authorized"] is expected_ptsm + + +@pytest.mark.parametrize( + ("auto_map_enabled", "blacklisted", "row_applied", "expected_auto"), + ( + (False, False, False, False), + (False, True, False, False), + (True, False, False, True), + (True, True, False, True), + (True, False, True, False), + (True, True, True, False), + ), +) +def test_finalize_program_launch_policy_pure_simt_ignores_blacklist( + compiler_module, + monkeypatch, + auto_map_enabled, + blacklisted, + row_applied, + expected_auto, +): + metadata = { + "program_grid_transforms": None, + "program_grid_mapping_applied": False, + "row_coalescing_applied": row_applied, + "has_auto_blockify_blacklist_op": blacklisted, + "mix_mode": "aiv", + } + monkeypatch.setattr(compiler_module, "_is_auto_map_parallel_blocks_enabled", lambda: auto_map_enabled) + + compiler_module._finalize_program_launch_policy(metadata, SimpleNamespace(is_pure_simt=True)) + + assert metadata["auto_blockify_enabled"] is expected_auto + assert metadata["ptsm_cap_authorized"] is False + + +@pytest.mark.parametrize( + "function_name", + ( + "linalg_to_bin_enable_npu_compile_910_95", + "linalg_to_bin_enable_npu_compile_A2_A3", + ), +) +@pytest.mark.parametrize("has_blacklist_op", (False, True)) +def test_non_pure_simt_linalg_compilers_keep_blacklist_auto_blockify_gate( + compiler_module, + monkeypatch, + function_name, + has_blacklist_op, +): + command = _run_linalg_to_npubin( + compiler_module, + monkeypatch, + function_name, + has_blacklist_op, + ) + + assert ("--enable-auto-blockify-loop" in command) is not has_blacklist_op + assert "--pure-simt" not in command + + +def test_default_compile_mode_keeps_the_91095_layout_memory_gate_prepared(compiler_module): + """The canonical default is portable and enables the Ascend 950 template gate.""" + + a2_default = compiler_module.NPUOptions(arch="Ascend910B1") + assert a2_default.compile_on_910_95 is False + assert a2_default.compile_mode == "simd_simt_template" + assert a2_default.is_pure_simt is False + + a5_default = compiler_module.NPUOptions(arch="Ascend910_9589") + assert a5_default.compile_on_910_95 is True + assert a5_default.compile_mode == "simd_simt_template" + assert a5_default.is_pure_simt is False + + with pytest.warns(FutureWarning, match="compile_on_910_95"): + ignored_legacy_value = compiler_module.NPUOptions( + arch="Ascend910_9589", + compile_on_910_95=False, + ) + assert ignored_legacy_value.compile_on_910_95 is True + + canonical = compiler_module.NPUOptions( + arch="Ascend910_9589", + compile_mode="simd_simt_template", + ) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + alias = compiler_module.NPUOptions( + arch="Ascend910_9589", + compile_mode="unstructured_in_simt", + ) + assert not caught + assert alias.compile_mode == canonical.compile_mode == "simd_simt_template" + assert alias.hash() == canonical.hash() + + with pytest.raises(ValueError, match=r"invalid compile_mode='simt_template'"): + compiler_module.NPUOptions(arch="Ascend910_9589", compile_mode="simt_template") + + explicit_simd = compiler_module.NPUOptions(arch="Ascend910_9589", compile_mode="simd") + assert explicit_simd.compile_mode == "simd" + assert explicit_simd.is_pure_simt is False + + explicit_template = compiler_module.NPUOptions( + arch="Ascend910_9589", + compile_mode="simd_simt_template", + ) + assert explicit_template.compile_mode == "simd_simt_template" + assert explicit_template.is_pure_simt is False + + explicit_only = compiler_module.NPUOptions(arch="Ascend910_9589", compile_mode="simt_only") + assert explicit_only.compile_mode == "simt_only" + assert explicit_only.is_pure_simt is True + + # Legacy spellings remain discoverable while compile_mode controls lowering. + assert explicit_only.__dict__["force_simt_only"] is False + assert explicit_template.__dict__["force_simt_template"] is False + + +@pytest.mark.parametrize("arch", ["Ascend910B4", "Ascend910_9391", "Ascend950PR"]) +@pytest.mark.parametrize("mode", [ + {"gm": 4, "l1": 2, "l0c": 1, "ub": 2}, + {"future": 0, "gm": -1}, + {}, +]) +def test_multibuffer_mode_preserves_values_and_metadata(compiler_module, arch, mode): + options = _parse_options(compiler_module, arch, {"multibuffer_mode": mode}) + expected = tuple(sorted(mode.items())) + assert options.multibuffer_mode == expected + assert options.num_stages is None + assert options.multibuffer is True + metadata = dict(options.__dict__, multibuffer_mode=json.loads(json.dumps(options.multibuffer_mode))) + restored = _parse_options(compiler_module, arch, metadata) + assert restored.multibuffer_mode == expected + assert restored.hash() == options.hash() + assert restored.hash() != _parse_options(compiler_module, arch).hash() + + +@pytest.mark.parametrize("mode", [ + "[(gm,2)]", + True, + 2, + {"gm": "2"}, + {1: 2}, + {"gm": 2.0}, + {"gm": True}, + ("gm", 2), + (("gm", ), ), + (("gm", 2, 3), ), + ((1, 2), ), + (("gm", 2.0), ), + (("gm", True), ), +]) +def test_multibuffer_mode_requires_string_keys_and_integer_counts(compiler_module, mode): + with pytest.raises(TypeError, match="multibuffer_mode must be a dict"): + compiler_module.NPUOptions(multibuffer_mode=mode) + + +@pytest.mark.parametrize("name,value", [ + ("limit_auto_multi_buffer_only_for_local_buffer", False), + ("limit_auto_multi_buffer_of_local_buffer", "no-l0c"), + ("limit_auto_multi_buffer_buffer", "only-vector"), +]) +@pytest.mark.parametrize("mode", [None, {"ub": 2}]) +def test_multibuffer_legacy_options_are_preserved_without_ta_warnings(compiler_module, name, value, mode): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + options = compiler_module.NPUOptions(multibuffer_mode=mode, **{name: value}) + assert not caught + assert getattr(options, name) == value + assert options.multibuffer is True + + +def _capture_multibuffer_command(compiler, monkeypatch, is_a5, requested, derived=None): + options = _parse_options(compiler, "Ascend950PR" if is_a5 else "Ascend910B4", requested) + metadata = dict(options.__dict__, hash="layout-memory-contract-test", mix_mode="mix", bitcodes=None, + auto_blockify_enabled=False) + metadata.update(derived or {}) + monkeypatch.setattr(compiler, "_parse_linalg_metadata", lambda source, meta: (source, meta)) + monkeypatch.setattr(compiler, "_finalize_program_launch_policy", lambda *_args: None) + monkeypatch.setattr(compiler, "get_common_bishengir_compile_options", lambda _meta: []) + monkeypatch.setattr(compiler, "get_auto_bind_sub_block_option", lambda _meta: True) + monkeypatch.setattr(compiler, "_get_npucompiler_path", lambda: ("/fake/compiler", {})) + monkeypatch.setattr(compiler, "NPUUtils", + lambda: SimpleNamespace(has_device_limit=lambda: False, get_arch=lambda: options.target_arch)) + monkeypatch.delenv("TRITON_ENABLE_LIBDEVICE", raising=False) + + class CommandCaptured(BaseException): + pass + + commands = [] + + def capture(command, **_kwargs): + commands.append(command) + raise CommandCaptured + + monkeypatch.setattr(compiler.subprocess, "run", capture) + compile_fn = (compiler.linalg_to_bin_enable_npu_compile_910_95 + if is_a5 else compiler.linalg_to_bin_enable_npu_compile_A2_A3) + with pytest.raises(CommandCaptured): + compile_fn("module {}", metadata, options) + assert len(commands) == 1 + return commands[0] + + +@pytest.mark.parametrize("is_a5", [False, True]) +@pytest.mark.parametrize("requested,enabled", [({}, True), ({"num_stages": 1}, False), ({"num_stages": 3}, True), + ({"multibuffer": False}, False)]) +def test_multibuffer_legacy_command_stays_on_old_interface(compiler_module, monkeypatch, is_a5, requested, enabled): + command = _capture_multibuffer_command(compiler_module, monkeypatch, is_a5, requested) + assert not any(arg.startswith("--multibuffer-mode=") for arg in command) + assert f"--enable-auto-multi-buffer={enabled}" in command + assert ("--limit-auto-multi-buffer-of-local-buffer=no-limit" in command) == is_a5 + + +@pytest.mark.parametrize("is_a5", [False, True]) +def test_multibuffer_mode_is_forwarded_alongside_existing_defaults(compiler_module, monkeypatch, is_a5): + mode = {"ub": 2, "gm": 4, "l0c": 1, "l1": 2} + command = _capture_multibuffer_command(compiler_module, monkeypatch, is_a5, {"multibuffer_mode": mode}) + assert command.count("--multibuffer-mode=[(gm,4),(l0c,1),(l1,2),(ub,2)]") == 1 + assert "--enable-auto-multi-buffer=True" in command + assert ("--limit-auto-multi-buffer-of-local-buffer=no-limit" in command) == is_a5 + + +@pytest.mark.parametrize("is_a5", [False, True]) +@pytest.mark.parametrize("switch", [False, True]) +def test_multibuffer_explicit_old_options_are_still_forwarded(compiler_module, monkeypatch, is_a5, switch): + requested = { + "multibuffer_mode": {"gm": 4, "l1": 2, "l0c": 2, "ub": 2}, + "multibuffer": switch, + "limit_auto_multi_buffer_only_for_local_buffer": True, + "limit_auto_multi_buffer_of_local_buffer": "no-l0c", + "limit_auto_multi_buffer_buffer": "only-vector", + "set_workspace_multibuffer": 0, + } + command = _capture_multibuffer_command(compiler_module, monkeypatch, is_a5, requested) + assert "--multibuffer-mode=[(gm,4),(l0c,2),(l1,2),(ub,2)]" in command + assert f"--enable-auto-multi-buffer={switch}" in command + assert "--limit-auto-multi-buffer-only-for-local-buffer=True" in command + assert "--limit-auto-multi-buffer-of-local-buffer=no-l0c" in command + assert "--set-workspace-multibuffer=0" in command + if is_a5: + assert "--limit-auto-multi-buffer-buffer=only-vector" in command + + +def test_multibuffer_keeps_dynamic_cv_workspace_constraint(compiler_module, monkeypatch): + command = _capture_multibuffer_command(compiler_module, monkeypatch, True, + {"multibuffer_mode": {"gm": 4, "l1": 2, "l0c": 1, "ub": 2}}, + derived={"set_workspace_multibuffer": 0}) + assert "--set-workspace-multibuffer=0" in command + + +@pytest.mark.parametrize("is_a5", [False, True]) +def test_multibuffer_value_semantics_are_left_to_npuir(compiler_module, monkeypatch, is_a5): + # Level names and count ranges are vendor decisions; partial modes are forwarded as supplied. + command = _capture_multibuffer_command(compiler_module, monkeypatch, is_a5, + {"multibuffer_mode": {"future": 0, "ub": -1}}) + assert "--multibuffer-mode=[(future,0),(ub,-1)]" in command + + +@pytest.mark.parametrize("arch", ["Ascend910B4", "Ascend910_9391", "Ascend950PR"]) +@pytest.mark.parametrize("stages", [0, 1, 2, 3]) +@pytest.mark.parametrize("mode", [{"gm": 2, "l1": 2, "l0c": 2, "ub": 2}, {}]) +def test_multibuffer_mode_rejects_num_stages_even_when_equivalent(compiler_module, arch, stages, mode): + with pytest.raises(ValueError, match="num_stages and multibuffer_mode cannot be specified together"): + _parse_options(compiler_module, arch, {"multibuffer_mode": mode, "num_stages": stages}) + + +def test_multibuffer_default_num_stages_does_not_conflict(compiler_module): + assert compiler_module.NPUOptions().num_stages == 2 + assert compiler_module.NPUOptions(num_stages=1).num_stages == 1 + options = compiler_module.NPUOptions(multibuffer_mode={"ub": 2}, num_stages=None) + assert options.num_stages is None + + +@pytest.mark.parametrize("arch", ["Ascend910B4", "Ascend950PR"]) +def test_multibuffer_dictionary_order_does_not_change_compiler_hash(compiler_module, arch): + mode = {"ub": 2, "gm": 4, "l1": 2} + options = _parse_options(compiler_module, arch, {"multibuffer_mode": mode}) + reordered = _parse_options(compiler_module, arch, {"multibuffer_mode": dict(reversed(list(mode.items())))}) + assert reordered.multibuffer_mode == options.multibuffer_mode + assert reordered.hash() == options.hash() + mode["ub"] = 8 + assert dict(options.multibuffer_mode)["ub"] == 2 diff --git a/third_party/ascend/unittest/pytest_ut/test_complex_mask.py b/third_party/ascend/unittest/pytest_ut/test_complex_mask.py new file mode 100644 index 0000000000..d58fe02df6 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_complex_mask.py @@ -0,0 +1,70 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def copy(x): + return x.clone() + + +@triton.jit +def copy_kernel(in_ptr, out_ptr, N: tl.constexpr, NUMEL): + idx_block = tl.arange(0, N) + is_valid = N <= NUMEL + x = tl.load(in_ptr + idx_block, mask=idx_block < N) + mask_i1 = is_valid[None, None] & (idx_block < N)[None, :] + tl.store(out_ptr + idx_block[None, :], x[None, :], mask=mask_i1) + + +@triton.jit +def permute_copy_kernel(in_ptr, out_ptr, M: tl.constexpr, N: tl.constexpr, NUMEL): + idx_block_n = tl.arange(0, N) + idx_block_m = tl.arange(0, M) + idx_block = idx_block_m[:, None] + idx_block_n[None, :] * M + is_valid = N <= NUMEL + x = tl.load(in_ptr + idx_block, mask=(idx_block_m[:, None] < M) & (idx_block_n[None, :] < N)) + mask_i1 = (is_valid[None, None, None]) & (idx_block_m[None, :, None] < M) & (idx_block_n[None, None, :] < N) + tl.store(out_ptr + idx_block[None, :], x[None, :], mask=mask_i1) + + +def test_complex_mask_copy(): + N = 1024 + x = torch.randn(N, dtype=torch.float32).npu() + y = torch.empty_like(x).npu() + copy_kernel[(1, )](x, y, N=N, NUMEL=N) + torch.testing.assert_close(x, y) + + +@pytest.mark.skip(reason="The case is failed, skipping for now. Will be fixed in future.") +def test_complex_mask_permute_copy(): + M = 4 + N = 32 + x = torch.randn(M * N, dtype=torch.float32).npu() + y = torch.empty_like(x).npu() + permute_copy_kernel[(1, )](x, y, M=M, N=N, NUMEL=M * N) + torch.testing.assert_close(x, y) diff --git a/third_party/ascend/unittest/pytest_ut/test_continuous_mask_subi_load.py b/third_party/ascend/unittest/pytest_ut/test_continuous_mask_subi_load.py new file mode 100644 index 0000000000..d287453f1f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_continuous_mask_subi_load.py @@ -0,0 +1,58 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import pytest +import torch +import torch_npu # noqa: F401 +import triton +import triton.language as tl + + +@triton.jit +def masked_load_below_base(x_ptr, o_ptr, BIG: tl.constexpr, BLOCK: tl.constexpr): + offs = tl.arange(0, BLOCK) + idx = offs - BIG + mask = idx >= 0 + v = tl.load(x_ptr + idx, mask=mask, other=0.0) + tl.store(o_ptr + offs, v) + + +def _expected(block: int, big: int) -> torch.Tensor: + # Valid lanes load ones from x; masked-out lanes get other=0. + out = torch.zeros(block, dtype=torch.float32) + if big < block: + out[big:] = 1.0 + return out + + +@pytest.mark.parametrize("big,block", [ + (2, 256), +]) +def test_continuous_head_mask_subi_load(big, block): + """ + Usage: + PYTORCH_NPU_ALLOC_CONF=expandable_segments:True pytest test_continuous_mask_subi_load.py + """ + x = torch.ones(100000, dtype=torch.float32, device="npu") + o = torch.full((block, ), -1.0, dtype=torch.float32, device="npu") + + masked_load_below_base[(1, )](x, o, big, block) + torch.npu.synchronize() + + torch.testing.assert_close(o.cpu(), _expected(block, big).cpu(), rtol=0, atol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_control_flow.py b/third_party/ascend/unittest/pytest_ut/test_control_flow.py new file mode 100644 index 0000000000..94b6358cb5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_control_flow.py @@ -0,0 +1,826 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl + + +def _assert_close(actual, expected, *, atol=1e-3, rtol=1e-3): + torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol) + + +@triton.jit +def _cf_maybe_loop(cond, x): + if cond == 0: + return x + acc = x + for _ in range(10): + acc += 1 + return acc + + +@triton.jit +def _cf_with_scf_for_kernel(out_ptr, arg0, arg1): + pid = tl.program_id(0) + val = _cf_maybe_loop(arg0, arg1) + tl.store(out_ptr + pid, val * 10) + + +def test_control_flow_cf_to_scf_with_scf_for(): + for arg0, arg1 in [(0, 3), (1, 3), (7, 0)]: + out = torch.zeros((1, ), dtype=torch.int32, device="npu") + _cf_with_scf_for_kernel[(1, )](out, arg0, arg1) + val = arg1 if arg0 == 0 else arg1 + 10 + assert int(out[0].item()) == val * 10 + + +@triton.jit +def _cf_helper_func(a, b, c): + if a == 0: + return b * 9 + if a == 1: + return c * 9 + return 0 + + +@triton.jit +def _cf_with_call_kernel(out_ptr, arg0, arg1, arg2, arg3): + pid = tl.program_id(0) + if arg0 != 0: + tl.store(out_ptr + pid, 0) + return + result = _cf_helper_func(arg1, arg2, arg3) * 2 + tl.store(out_ptr + pid, result) + + +def test_control_flow_cf_to_scf_with_call(): + cases = [ + (1, 0, 5, 7, 0), + (0, 0, 5, 7, 90), + (0, 1, 5, 7, 126), + (0, 2, 5, 7, 0), + ] + for arg0, arg1, arg2, arg3, expected in cases: + out = torch.zeros((1, ), dtype=torch.int32, device="npu") + _cf_with_call_kernel[(1, )](out, arg0, arg1, arg2, arg3) + assert int(out[0].item()) == expected + + +@triton.jit +def _if_else_same_ptr_kernel(in_a_ptr, in_b_ptr, out_ptr, flag, n_elements, block: tl.constexpr): + pid = tl.program_id(0) + offs = pid * block + tl.arange(0, block) + mask = offs < n_elements + a = tl.load(in_a_ptr + offs, mask=mask) + b = tl.load(in_b_ptr + offs, mask=mask) + if flag != 0: + tl.store(out_ptr + offs, a, mask=mask) + else: + tl.store(out_ptr + offs, b, mask=mask) + + +def test_control_flow_driver_if_else_same_tensor_ptr(): + n_elements, block = 128, 32 + a = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + b = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + for flag in [1, 0]: + out = torch.zeros((n_elements, ), dtype=torch.float32, device="npu") + _if_else_same_ptr_kernel[(triton.cdiv(n_elements, block), )](a, b, out, flag, n_elements, block) + _assert_close(out, a if flag != 0 else b) + + +@triton.jit +def _if_else_diff_advance_kernel(in_ptr, out_ptr, flag, n_elements, block: tl.constexpr): + in_bp = tl.make_block_ptr( + base=in_ptr, + shape=(1, n_elements), + strides=(n_elements, 1), + offsets=(0, 0), + block_shape=(1, block), + order=(1, 0), + ) + if flag != 0: + in_bp = tl.advance(in_bp, (0, 0)) + else: + in_bp = tl.advance(in_bp, (0, block)) + val = tl.load(in_bp) + out_bp = tl.make_block_ptr( + base=out_ptr, + shape=(1, block), + strides=(block, 1), + offsets=(0, 0), + block_shape=(1, block), + order=(1, 0), + ) + tl.store(out_bp, val) + + +def test_control_flow_driver_if_else_diff_block_ptr_advance(): + n_elements, block = 64, 32 + x = torch.randn((1, n_elements), dtype=torch.float32, device="npu") + for flag in [1, 0]: + out = torch.empty((1, block), dtype=torch.float32, device="npu") + _if_else_diff_advance_kernel[(1, )](x, out, flag, n_elements, block) + expected = x[:, 0:block] if flag != 0 else x[:, block:2 * block] + _assert_close(out, expected) + + +@triton.jit +def _while_block_ptr_advance_kernel(in_ptr, out_ptr, n_iters, block: tl.constexpr): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(n_iters * block, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + acc = tl.zeros([block], tl.float32) + i = 0 + while i < n_iters: + acc += tl.load(bp) + bp = tl.advance(bp, (block, )) + i = i + 1 + tl.store(out_ptr + tl.arange(0, block), acc) + + +def test_control_flow_driver_while_block_ptr_advance_const(): + n_iters, block = 4, 16 + x = torch.randn((n_iters * block, ), dtype=torch.float32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _while_block_ptr_advance_kernel[(1, )](x, out, n_iters, block) + _assert_close(out, x.reshape(n_iters, block).sum(dim=0)) + + +@triton.jit +def _for_dynamic_advance_kernel(in_ptr, out_ptr, n_elements, block: tl.constexpr, n_loops: tl.constexpr): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(n_elements, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + acc = tl.zeros([block], tl.float32) + for i in range(0, n_loops): + bp = tl.advance(bp, (i, )) + acc += tl.load(bp) + tl.store(out_ptr + tl.arange(0, block), acc) + + +def test_control_flow_driver_for_block_ptr_dynamic_advance(): + block, n_loops = 16, 4 + n_elements = block + (n_loops * (n_loops - 1)) // 2 + x = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _for_dynamic_advance_kernel[(1, )](x, out, n_elements, block, n_loops) + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for i in range(n_loops): + pos += i + expected += x[pos:pos + block] + _assert_close(out, expected) + + +@triton.jit +def _for_advance_baseline_kernel(in_ptr, out_ptr, m_size, n_size, block_n: tl.constexpr, n_blocks: tl.constexpr): + row = tl.program_id(0) + in_bp = tl.make_block_ptr( + base=in_ptr, + shape=(m_size, n_size), + strides=(n_size, 1), + offsets=(row, 0), + block_shape=(1, block_n), + order=(1, 0), + ) + acc = tl.zeros([1, block_n], tl.float32) + for _ in range(0, n_blocks): + acc += tl.load(in_bp) + in_bp = tl.advance(in_bp, (0, block_n)) + tl.store(out_ptr + row, tl.sum(acc)) + + +def test_control_flow_regression_for_block_ptr_advance_baseline(): + m_size, n_size, block_n = 4, 64, 16 + x = torch.randn((m_size, n_size), dtype=torch.float32, device="npu") + out = torch.empty((m_size, ), dtype=torch.float32, device="npu") + _for_advance_baseline_kernel[(m_size, )](x, out, m_size, n_size, block_n, n_size // block_n) + _assert_close(out, x.sum(dim=1)) + + +@triton.jit +def _for_if_advance_kernel(in_ptr, out_ptr, m_size, n_size, block_n: tl.constexpr, n_blocks: tl.constexpr): + row = tl.program_id(0) + in_bp = tl.make_block_ptr( + base=in_ptr, + shape=(m_size, n_size), + strides=(n_size, 1), + offsets=(row, 0), + block_shape=(1, block_n), + order=(1, 0), + ) + acc = tl.zeros([1, block_n], tl.float32) + for i in range(0, n_blocks): + if i > 0: + in_bp = tl.advance(in_bp, (0, block_n)) + acc += tl.load(in_bp) + tl.store(out_ptr + row, tl.sum(acc)) + + +def test_control_flow_regression_for_if_advance(): + m_size, n_size, block_n = 2, 32, 8 + x = torch.randn((m_size, n_size), dtype=torch.float32, device="npu") + out = torch.empty((m_size, ), dtype=torch.float32, device="npu") + _for_if_advance_kernel[(m_size, )](x, out, m_size, n_size, block_n, n_size // block_n) + _assert_close(out, x.sum(dim=1)) + + +@triton.jit +def _for_dynamic_ub_kernel(in_ptr, out_ptr, m_size, n_size, n_blocks, block_n: tl.constexpr): + row = tl.program_id(0) + in_bp = tl.make_block_ptr( + base=in_ptr, + shape=(m_size, n_size), + strides=(n_size, 1), + offsets=(row, 0), + block_shape=(1, block_n), + order=(1, 0), + ) + acc = tl.zeros([1, block_n], tl.float32) + for _ in range(0, n_blocks): + acc += tl.load(in_bp) + in_bp = tl.advance(in_bp, (0, block_n)) + tl.store(out_ptr + row, tl.sum(acc)) + + +def test_control_flow_regression_for_block_ptr_dynamic_upper_bound(): + m_size, n_size, block_n = 2, 64, 16 + n_blocks = n_size // block_n + x = torch.randn((m_size, n_size), dtype=torch.float32, device="npu") + out = torch.empty((m_size, ), dtype=torch.float32, device="npu") + _for_dynamic_ub_kernel[(m_size, )](x, out, m_size, n_size, n_blocks, block_n) + _assert_close(out, x.sum(dim=1)) + + +@triton.jit +def _while_tensor_ptr_addptr_kernel(in_ptr, out_ptr, n_iters, block: tl.constexpr): + offs = tl.arange(0, block) + ptrs = in_ptr + offs + acc = tl.zeros([block], tl.float32) + i = 0 + while i < n_iters: + acc += tl.load(ptrs) + ptrs = ptrs + block + i = i + 1 + tl.store(out_ptr + offs, acc) + + +def test_control_flow_stress_while_tensor_ptr_addptr_const(): + n_iters, block = 4, 16 + x = torch.randn((n_iters * block, ), dtype=torch.float32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _while_tensor_ptr_addptr_kernel[(1, )](x, out, n_iters, block) + _assert_close(out, x.reshape(n_iters, block).sum(dim=0)) + + +@triton.jit +def _for_affine_advance_kernel( + in_ptr, + out_ptr, + n_elements, + block: tl.constexpr, + n_loops: tl.constexpr, + coeff_a: tl.constexpr, + coeff_b: tl.constexpr, +): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(n_elements, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + acc = tl.zeros([block], tl.float32) + for i in range(0, n_loops): + bp = tl.advance(bp, (coeff_a * i + coeff_b, )) + acc += tl.load(bp) + tl.store(out_ptr + tl.arange(0, block), acc) + + +def test_control_flow_stress_for_block_ptr_affine_advance(): + block, n_loops, coeff_a, coeff_b = 16, 4, 2, 3 + max_pos = sum(coeff_a * i + coeff_b for i in range(n_loops)) + n_elements = block + max_pos + x = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _for_affine_advance_kernel[(1, )](x, out, n_elements, block, n_loops, coeff_a, coeff_b) + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for i in range(n_loops): + pos += coeff_a * i + coeff_b + expected += x[pos:pos + block] + _assert_close(out, expected) + + +@triton.jit +def _nested_for_while_kernel(in_ptr, out_ptr, m_size, n_size, block: tl.constexpr, n_inner: tl.constexpr): + offs = tl.arange(0, block) + acc = tl.zeros([block], tl.float32) + for row in range(0, m_size): + bp = tl.make_block_ptr( + base=in_ptr + row * n_size, + shape=(n_size, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + row_acc = tl.zeros([block], tl.float32) + j = 0 + while j < n_inner: + row_acc += tl.load(bp) + bp = tl.advance(bp, (block, )) + j = j + 1 + acc += row_acc + tl.store(out_ptr + offs, acc) + + +def test_control_flow_stress_nested_for_while_block_ptr(): + m_size, block, n_inner = 3, 16, 4 + n_size = block * n_inner + x = torch.randn((m_size, n_size), dtype=torch.float32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _nested_for_while_kernel[(1, )](x, out, m_size, n_size, block, n_inner) + _assert_close(out, x.reshape(m_size, n_inner, block).sum(dim=(0, 1))) + + +@triton.jit +def _while_no_factual_iv_kernel(in_ptr, step_ptr, out_ptr, n_iters, block: tl.constexpr): + offs = tl.arange(0, block) + acc = tl.zeros([block], tl.float32) + ptrs = in_ptr + offs + i = 0 + k = 0 + while k < n_iters: + acc += tl.load(ptrs) + ptrs = ptrs + block + step = tl.load(step_ptr + k) + i = i + step + k = k + 1 + tl.store(out_ptr + offs, acc) + + +def test_control_flow_stress_while_without_factual_iv(): + n_iters, block = 4, 16 + x = torch.arange(0, n_iters * block, dtype=torch.float32, device="npu") + steps = torch.tensor([1, 2, 1, 3], dtype=torch.int32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _while_no_factual_iv_kernel[(1, )](x, steps, out, n_iters, block) + _assert_close(out, x.reshape(n_iters, block).sum(dim=0)) + + +@triton.jit +def _while_if_else_block_ptr_advance_kernel(in_ptr, out_ptr, n_iters, block: tl.constexpr): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(n_iters * block, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + acc = tl.zeros([block], tl.float32) + i = 0 + while i < n_iters: + if i == 1: + bp = tl.advance(bp, (3, )) + else: + bp = tl.advance(bp, (1, )) + acc += tl.load(bp) + bp = tl.advance(bp, (2, )) + i = i + 1 + tl.store(out_ptr + tl.arange(0, block), acc) + + +def test_control_flow_stress_while_if_else_block_ptr_advance(): + n_iters, block = 4, 16 + x = torch.randn((n_iters * block, ), dtype=torch.float32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _while_if_else_block_ptr_advance_kernel[(1, )](x, out, n_iters, block) + + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for i in range(n_iters): + pos += 3 if i == 1 else 1 + expected += x[pos:pos + block] + pos += 2 + _assert_close(out, expected) + + +@triton.jit +def _for_if_else_block_ptr_load_after_post_advance_kernel( + in_ptr, + out_ptr, + flag, + n_elements, + block: tl.constexpr, + n_loops: tl.constexpr, +): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(n_elements, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + acc = tl.zeros([block], tl.float32) + for _ in range(0, n_loops): + if flag != 0: + bp = tl.advance(bp, (1, )) + else: + bp = tl.advance(bp, (3, )) + bp = tl.advance(bp, (2, )) + acc += tl.load(bp) + tl.store(out_ptr + tl.arange(0, block), acc) + + +def test_control_flow_stress_for_if_else_block_ptr_load_after_post_advance(): + block, n_loops = 16, 4 + n_elements = block + n_loops * 5 + x = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + + for flag in [1, 0]: + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _for_if_else_block_ptr_load_after_post_advance_kernel[(1, )](x, out, flag, n_elements, block, n_loops) + + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for _ in range(n_loops): + pos += 1 if flag != 0 else 3 + pos += 2 + expected += x[pos:pos + block] + _assert_close(out, expected) + + +@triton.jit +def _for_if_else_tensor_ptr_load_after_post_addptr_kernel( + in_ptr, + out_ptr, + flag, + block: tl.constexpr, + n_loops: tl.constexpr, +): + offs = tl.arange(0, block) + step_then = tl.zeros([block], tl.int32) + 1 + step_else = tl.zeros([block], tl.int32) + 3 + step_after = tl.zeros([block], tl.int32) + 2 + ptrs = in_ptr + offs + acc = tl.zeros([block], tl.float32) + for _ in range(0, n_loops): + if flag != 0: + ptrs = ptrs + step_then + else: + ptrs = ptrs + step_else + ptrs = ptrs + step_after + acc += tl.load(ptrs) + tl.store(out_ptr + offs, acc) + + +def test_control_flow_stress_for_if_else_tensor_ptr_load_after_post_addptr(): + block, n_loops = 16, 4 + n_elements = block + n_loops * 5 + x = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + + for flag in [1, 0]: + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _for_if_else_tensor_ptr_load_after_post_addptr_kernel[(1, )](x, out, flag, block, n_loops) + + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for _ in range(n_loops): + pos += 1 if flag != 0 else 3 + pos += 2 + expected += x[pos:pos + block] + _assert_close(out, expected) + + +@triton.jit +def _for_nested_if_block_ptr_load_after_repeated_advance_kernel( + in_ptr, + out_ptr, + flag0, + flag1, + n_elements, + block: tl.constexpr, + n_loops: tl.constexpr, +): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(n_elements, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + acc = tl.zeros([block], tl.float32) + for _ in range(0, n_loops): + if flag0 != 0: + bp = tl.advance(bp, (1, )) + if flag1 != 0: + bp = tl.advance(bp, (2, )) + else: + bp = tl.advance(bp, (4, )) + else: + bp = tl.advance(bp, (3, )) + if flag1 != 0: + bp = tl.advance(bp, (1, )) + else: + bp = tl.advance(bp, (2, )) + bp = tl.advance(bp, (1, )) + acc += tl.load(bp) + tl.store(out_ptr + tl.arange(0, block), acc) + + +def test_control_flow_stress_for_nested_if_block_ptr_load_after_repeated_advance(): + block, n_loops = 16, 4 + n_elements = block + n_loops * 6 + x = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + + for flag0, flag1 in [(1, 1), (1, 0), (0, 1), (0, 0)]: + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _for_nested_if_block_ptr_load_after_repeated_advance_kernel[(1, )](x, out, flag0, flag1, n_elements, block, + n_loops) + + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for _ in range(n_loops): + if flag0 != 0: + pos += 1 + pos += 2 if flag1 != 0 else 4 + else: + pos += 3 + pos += 1 if flag1 != 0 else 2 + pos += 1 + expected += x[pos:pos + block] + _assert_close(out, expected) + + +@triton.jit +def _for_while_if_block_ptr_load_after_nested_advance_kernel( + in_ptr, + out_ptr, + flag, + n_elements, + inner_iters, + block: tl.constexpr, + outer_loops: tl.constexpr, +): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(n_elements, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + acc = tl.zeros([block], tl.float32) + for _ in range(0, outer_loops): + j = 0 + while j < inner_iters: + if flag != 0: + bp = tl.advance(bp, (1, )) + else: + bp = tl.advance(bp, (2, )) + j = j + 1 + bp = tl.advance(bp, (3, )) + acc += tl.load(bp) + tl.store(out_ptr + tl.arange(0, block), acc) + + +def test_control_flow_stress_for_while_if_block_ptr_load_after_nested_advance(): + block, outer_loops, inner_iters = 16, 3, 2 + n_elements = block + outer_loops * (inner_iters * 2 + 3) + x = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + + for flag in [1, 0]: + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _for_while_if_block_ptr_load_after_nested_advance_kernel[(1, )](x, out, flag, n_elements, inner_iters, block, + outer_loops) + + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for _ in range(outer_loops): + for _ in range(inner_iters): + pos += 1 if flag != 0 else 2 + pos += 3 + expected += x[pos:pos + block] + _assert_close(out, expected) + + +@triton.jit +def _for_2d_multi_advance_kernel( + in_ptr, + out_ptr, + m_size, + n_size, + block_m: tl.constexpr, + block_n: tl.constexpr, + n_loops: tl.constexpr, +): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(m_size, n_size), + strides=(n_size, 1), + offsets=(0, 0), + block_shape=(block_m, block_n), + order=(1, 0), + ) + acc = tl.zeros([block_m, block_n], tl.float32) + for i in range(0, n_loops): + bp = tl.advance(bp, (1, i)) + acc += tl.load(bp) + out_bp = tl.make_block_ptr( + base=out_ptr, + shape=(block_m, block_n), + strides=(block_n, 1), + offsets=(0, 0), + block_shape=(block_m, block_n), + order=(1, 0), + ) + tl.store(out_bp, acc) + + +def test_control_flow_for_block_ptr_2d_dynamic_multi_advance(): + block_m, block_n, n_loops = 2, 8, 4 + m_size = block_m + n_loops + n_size = block_n + (n_loops * (n_loops - 1)) // 2 + x = torch.randn((m_size, n_size), dtype=torch.float32, device="npu") + out = torch.empty((block_m, block_n), dtype=torch.float32, device="npu") + _for_2d_multi_advance_kernel[(1, )](x, out, m_size, n_size, block_m, block_n, n_loops) + + row = 0 + col = 0 + expected = torch.zeros((block_m, block_n), dtype=torch.float32, device="npu") + for i in range(n_loops): + row += 1 + col += i + expected += x[row:row + block_m, col:col + block_n] + _assert_close(out, expected) + + +@triton.jit +def _while_chained_block_ptr_advance_kernel( + in_ptr, + out_ptr, + n_iters, + block: tl.constexpr, +): + bp = tl.make_block_ptr( + base=in_ptr, + shape=(n_iters * block + 1, ), + strides=(1, ), + offsets=(0, ), + block_shape=(block, ), + order=(0, ), + ) + acc = tl.zeros([block], tl.float32) + i = 0 + while i < n_iters: + bp = tl.advance(bp, (1, )) + acc += tl.load(bp) + bp = tl.advance(bp, (block - 1, )) + i = i + 1 + tl.store(out_ptr + tl.arange(0, block), acc) + + +def test_control_flow_while_block_ptr_chained_advance(): + n_iters, block = 4, 16 + x = torch.randn((n_iters * block + 1, ), dtype=torch.float32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _while_chained_block_ptr_advance_kernel[(1, )](x, out, n_iters, block) + + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for _ in range(n_iters): + pos += 1 + expected += x[pos:pos + block] + pos += block - 1 + _assert_close(out, expected) + + +@triton.jit +def _while_tensor_ptr_dynamic_step_kernel( + in_ptr, + step_ptr, + out_ptr, + n_iters, + block: tl.constexpr, +): + offs = tl.arange(0, block) + ptrs = in_ptr + offs + acc = tl.zeros([block], tl.float32) + i = 0 + while i < n_iters: + step = tl.load(step_ptr + i) + step_vec = step + tl.zeros([block], tl.int32) + ptrs = ptrs + step_vec + acc += tl.load(ptrs) + i = i + 1 + tl.store(out_ptr + offs, acc) + + +def test_control_flow_while_tensor_ptr_dynamic_step(): + block = 16 + steps_host = [1, 3, 2, 4] + n_iters = len(steps_host) + n_elements = block + sum(steps_host) + x = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + steps = torch.tensor(steps_host, dtype=torch.int32, device="npu") + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _while_tensor_ptr_dynamic_step_kernel[(1, )](x, steps, out, n_iters, block) + + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for step in steps_host: + pos += step + expected += x[pos:pos + block] + _assert_close(out, expected) + + +@triton.jit +def _for_nested_if_tensor_ptr_load_after_repeated_addptr_kernel( + in_ptr, + out_ptr, + flag0, + flag1, + block: tl.constexpr, + n_loops: tl.constexpr, +): + offs = tl.arange(0, block) + step1 = tl.zeros([block], tl.int32) + 1 + step2 = tl.zeros([block], tl.int32) + 2 + step3 = tl.zeros([block], tl.int32) + 3 + step4 = tl.zeros([block], tl.int32) + 4 + ptrs = in_ptr + offs + acc = tl.zeros([block], tl.float32) + for _ in range(0, n_loops): + if flag0 != 0: + ptrs = ptrs + step1 + if flag1 != 0: + ptrs = ptrs + step2 + else: + ptrs = ptrs + step4 + else: + ptrs = ptrs + step3 + if flag1 != 0: + ptrs = ptrs + step1 + else: + ptrs = ptrs + step2 + ptrs = ptrs + step1 + acc += tl.load(ptrs) + tl.store(out_ptr + offs, acc) + + +def test_control_flow_stress_for_nested_if_tensor_ptr_load_after_repeated_addptr(): + block, n_loops = 16, 4 + n_elements = block + n_loops * 6 + x = torch.randn((n_elements, ), dtype=torch.float32, device="npu") + + for flag0, flag1 in [(1, 1), (1, 0), (0, 1), (0, 0)]: + out = torch.empty((block, ), dtype=torch.float32, device="npu") + _for_nested_if_tensor_ptr_load_after_repeated_addptr_kernel[(1, )](x, out, flag0, flag1, block, n_loops) + + pos = 0 + expected = torch.zeros((block, ), dtype=torch.float32, device="npu") + for _ in range(n_loops): + if flag0 != 0: + pos += 1 + pos += 2 if flag1 != 0 else 4 + else: + pos += 3 + pos += 1 if flag1 != 0 else 2 + pos += 1 + expected += x[pos:pos + block] + _assert_close(out, expected) diff --git a/third_party/ascend/unittest/pytest_ut/test_control_flow_pointer_boundary.py b/third_party/ascend/unittest/pytest_ut/test_control_flow_pointer_boundary.py new file mode 100644 index 0000000000..7e967c0f18 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_control_flow_pointer_boundary.py @@ -0,0 +1,428 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import re + +import pytest +import torch +import torch_npu # noqa: F401 +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al + +BLOCK = 32 +N = 128 + + +@triton.jit +def block_ptr_if_descriptor_kernel(a, b, out, choose_ptr, descriptor_ptr, BLOCK: tl.constexpr): + choose = tl.load(choose_ptr) + size_a = tl.load(descriptor_ptr) + size_b = tl.load(descriptor_ptr + 1) + stride_a = tl.load(descriptor_ptr + 2) + stride_b = tl.load(descriptor_ptr + 3) + # Block-pointer offsets are required to use Triton's 32-bit index type. + # Keep the descriptor storage wide, but narrow the values at the frontend + # boundary instead of changing the product's make_block_ptr contract. + offset_a = tl.load(descriptor_ptr + 4).to(tl.int32) + offset_b = tl.load(descriptor_ptr + 5).to(tl.int32) + if choose != 0: + pointer = tl.make_block_ptr( + base=a + 3, + shape=(size_a, ), + strides=(stride_a, ), + offsets=(offset_a, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + else: + pointer = tl.make_block_ptr( + base=b + 11, + shape=(size_b, ), + strides=(stride_b, ), + offsets=(offset_b, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + value = tl.load(pointer, boundary_check=(0, ), padding_option="zero") + tl.store(out + tl.arange(0, BLOCK), value) + + +@triton.jit +def block_ptr_for_descriptor_kernel(a, b, out, scalar_out, steps_ptr, descriptor_ptr, BLOCK: tl.constexpr): + steps = tl.load(steps_ptr) + size_a = tl.load(descriptor_ptr) + size_b = tl.load(descriptor_ptr + 1) + stride_a = tl.load(descriptor_ptr + 2) + stride_b = tl.load(descriptor_ptr + 3) + pointer = tl.make_block_ptr( + base=a, + shape=(size_a, ), + strides=(stride_a, ), + offsets=(1, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + ordinary_result = 17 + for i in tl.range(0, steps): + if (i & 1) == 0: + pointer = tl.advance(pointer, (2, )) + else: + pointer = tl.make_block_ptr( + base=b, + shape=(size_b, ), + strides=(stride_b, ), + offsets=(i + 3, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + ordinary_result = ordinary_result + i + 1 + value = tl.load(pointer, boundary_check=(0, ), padding_option="zero") + tl.store(out + tl.arange(0, BLOCK), value) + tl.store(scalar_out, ordinary_result) + + +@triton.jit +def block_ptr_while_descriptor_kernel(a, b, out, steps_ptr, switch_at_ptr, n: tl.constexpr, BLOCK: tl.constexpr): + steps = tl.load(steps_ptr) + switch_at = tl.load(switch_at_ptr) + pointer = tl.make_block_ptr( + base=a, + shape=(n, ), + strides=(1, ), + offsets=(0, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + i = 0 + while i < steps: + if i == switch_at: + pointer = tl.make_block_ptr( + base=b, + shape=(n - 3, ), + strides=(1, ), + offsets=(1, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + else: + pointer = tl.advance(pointer, (2, )) + i += 1 + value = tl.load(pointer, boundary_check=(0, ), padding_option="zero") + tl.store(out + tl.arange(0, BLOCK), value) + + +@triton.jit +def scalar_base_tensor_ptr_loop_kernel(x, out, steps_ptr, n: tl.constexpr, BLOCK: tl.constexpr): + steps = tl.load(steps_ptr) + lane = tl.arange(0, BLOCK) + pointers = x + 3 + lane + for _ in tl.range(0, steps): + pointers = pointers + 2 + index = 3 + lane + 2 * steps + value = tl.load(pointers, mask=index < n, other=0.0) + tl.store(out + lane, value) + + +@triton.jit +def dense_splat_tensor_ptr_loop_kernel(x, out, steps_ptr, n: tl.constexpr, BLOCK: tl.constexpr): + steps = tl.load(steps_ptr) + lane = tl.arange(0, BLOCK) + # Keep the loop delta tensor-valued so TTIR materializes a dense splat. + delta = tl.full((BLOCK, ), BLOCK, tl.int32) + pointers = x + lane + for _ in tl.range(0, steps): + pointers = pointers + delta + index = lane + BLOCK * steps + value = tl.load(pointers, mask=index < n, other=0.0) + tl.store(out + lane, value) + + +@triton.jit +def dense_splat_affine_rank1_loop_kernel(x, out, steps_ptr, n: tl.constexpr, BLOCK: tl.constexpr, STRIDE: tl.constexpr): + steps = tl.load(steps_ptr) + lane = tl.arange(0, BLOCK) + scale = tl.full((BLOCK, ), STRIDE, tl.int32) + pointers = x + lane * scale + for _ in tl.range(0, steps): + pointers += 1 + index = lane * STRIDE + steps + value = tl.load(pointers, mask=index < n, other=0.0) + tl.store(out + lane, value) + + +@triton.jit +def dense_splat_affine_rank2_loop_kernel(x, out, steps_ptr, n: tl.constexpr, BM: tl.constexpr, BN: tl.constexpr, + ROW_STRIDE: tl.constexpr): + steps = tl.load(steps_ptr) + row = tl.arange(0, BM)[:, None] + col = tl.arange(0, BN)[None, :] + scale = tl.full((BM, BN), ROW_STRIDE, tl.int32) + offsets = row * scale + col + pointers = x + offsets + for _ in tl.range(0, steps): + pointers += 1 + value = tl.load(pointers, mask=offsets + steps < n, other=0.0) + tl.store(out + row * BN + col, value) + + +@triton.jit +def broadcasted_pointer_rank2_loop_kernel(x, out, steps_ptr, BM: tl.constexpr, BN: tl.constexpr, + ROW_STRIDE: tl.constexpr): + steps = tl.load(steps_ptr) + row = tl.arange(0, BM)[:, None] + col = tl.arange(0, BN)[None, :] + # Match grouped matmul's construction order: first create a lower-rank + # pointer tensor, then broadcast it while adding the second axis. + row_pointers = x + row * ROW_STRIDE + pointers = row_pointers + col + for _ in tl.range(0, steps): + pointers += 1 + value = tl.load(pointers) + tl.store(out + row * BN + col, value) + + +@triton.jit +def opaque_tensor_ptr_loop_kernel(a, b, out, steps_ptr, n: tl.constexpr, BLOCK: tl.constexpr): + steps = tl.load(steps_ptr) + lane = tl.arange(0, BLOCK) + pointers = tl.where((lane & 1) == 0, a + lane, b + lane) + for _ in tl.range(0, steps): + pointers = pointers + 1 + index = lane + steps + value = tl.load(pointers, mask=index < n, other=0.0) + tl.store(out + lane, value) + + +@triton.jit +def scope_block_ptr_kernel(x, out, delta_ptr, n: tl.constexpr, BLOCK: tl.constexpr): + delta = tl.load(delta_ptr).to(tl.int32) + pointer = tl.make_block_ptr( + base=x, + shape=(n, ), + strides=(1, ), + offsets=(1, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + with al.scope(core_mode="vector", disable_auto_sync=True): + pointer = tl.advance(pointer, (delta, )) + value = tl.load(pointer, boundary_check=(0, ), padding_option="zero") + tl.store(out + tl.arange(0, BLOCK), value) + + +@triton.jit +def scope_tensor_ptr_kernel(x, out, delta_ptr, n: tl.constexpr, BLOCK: tl.constexpr): + lane = tl.arange(0, BLOCK) + delta = tl.load(delta_ptr) + pointers = x + lane + with al.scope(core_mode="vector"): + pointers = pointers + delta + value = tl.load(pointers, mask=lane + delta < n, other=0.0) + tl.store(out + lane, value) + + +def _inputs(): + a_cpu = torch.arange(2048, dtype=torch.float32) + b_cpu = 100000.0 + torch.arange(2048, dtype=torch.float32) * 3.0 + return a_cpu, b_cpu, a_cpu.npu(), b_cpu.npu() + + +def _device_i32(value): + return torch.tensor([value], dtype=torch.int32, device="npu") + + +def _slice(source, base_offset, logical_size, stride, offset): + expected = torch.zeros(BLOCK, dtype=source.dtype) + for lane in range(BLOCK): + logical_index = offset + lane + if 0 <= logical_index < logical_size: + expected[lane] = source[base_offset + logical_index * stride] + return expected + + +def _assert_output(actual, expected): + torch.npu.synchronize() + torch.testing.assert_close(actual.cpu(), expected, rtol=0, atol=0) + + +@pytest.mark.parametrize("choose", [0, 1]) +def test_block_ptr_if_carries_complete_dynamic_descriptor(choose): + a_cpu, b_cpu, a, b = _inputs() + descriptor = torch.tensor([40, 29, 2, 3, 3, 5], dtype=torch.int64, device="npu") + choose_ptr = _device_i32(choose) + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + + block_ptr_if_descriptor_kernel[(1, )](a, b, out, choose_ptr, descriptor, BLOCK=BLOCK) + + if choose: + expected = _slice(a_cpu, 3, 40, 2, 3) + else: + expected = _slice(b_cpu, 11, 29, 3, 5) + _assert_output(out, expected) + + +@pytest.mark.parametrize("steps", [0, 2, 5]) +def test_block_ptr_for_carries_changing_descriptor_and_ordinary_result(steps): + a_cpu, b_cpu, a, b = _inputs() + descriptor = torch.tensor([60, 55, 2, 3], dtype=torch.int64, device="npu") + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + scalar_out = torch.empty(1, dtype=torch.int32, device="npu") + + block_ptr_for_descriptor_kernel[(1, )](a, b, out, scalar_out, _device_i32(steps), descriptor, BLOCK=BLOCK) + + source, logical_size, stride, offset = a_cpu, 60, 2, 1 + for i in range(steps): + if (i & 1) == 0: + offset += 2 + else: + source, logical_size, stride, offset = b_cpu, 55, 3, i + 3 + _assert_output(out, _slice(source, 0, logical_size, stride, offset)) + assert scalar_out.cpu().item() == 17 + steps * (steps + 1) // 2 + + +@pytest.mark.parametrize("steps,switch_at", [(0, -1), (4, -1), (4, 2)]) +def test_block_ptr_while_carries_descriptor(steps, switch_at): + a_cpu, b_cpu, a, b = _inputs() + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + + block_ptr_while_descriptor_kernel[(1, )](a, b, out, _device_i32(steps), _device_i32(switch_at), n=N, BLOCK=BLOCK) + + source, logical_size, offset = a_cpu, N, 0 + for i in range(steps): + if i == switch_at: + source, logical_size, offset = b_cpu, N - 3, 1 + else: + offset += 2 + _assert_output(out, _slice(source, 0, logical_size, 1, offset)) + + +@pytest.mark.parametrize("steps", [0, 2, 4]) +def test_scalar_base_tensor_pointer_loop(steps): + a_cpu, _, a, _ = _inputs() + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + + scalar_base_tensor_ptr_loop_kernel[(1, )](a, out, _device_i32(steps), n=N, BLOCK=BLOCK) + + _assert_output(out, _slice(a_cpu, 0, N, 1, 3 + 2 * steps)) + + +@pytest.mark.parametrize("steps", [0, 1, 3]) +def test_dense_splat_tensor_pointer_loop(steps): + a_cpu, _, a, _ = _inputs() + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + + dense_splat_tensor_ptr_loop_kernel[(1, )](a, out, _device_i32(steps), n=N, BLOCK=BLOCK) + + _assert_output(out, _slice(a_cpu, 0, N, 1, BLOCK * steps)) + + +@pytest.mark.parametrize("stride,steps", [(16, 0), (16, 3), (256, 0), (256, 3)]) +def test_dense_splat_affine_rank1_loop(stride, steps): + n = 8192 + source_cpu = torch.arange(n, dtype=torch.float32) + source = source_cpu.npu() + out = torch.empty(16, dtype=torch.float32, device="npu") + + dense_splat_affine_rank1_loop_kernel[(1, )](source, out, _device_i32(steps), n=n, BLOCK=16, STRIDE=stride) + + expected = source_cpu[torch.arange(16) * stride + steps] + _assert_output(out, expected) + + +@pytest.mark.parametrize("steps", [0, 2]) +def test_dense_splat_affine_rank2_loop(steps): + bm, bn, row_stride = 8, 16, 64 + n = 1024 + source_cpu = torch.arange(n, dtype=torch.float32) + source = source_cpu.npu() + out = torch.empty((bm, bn), dtype=torch.float32, device="npu") + + dense_splat_affine_rank2_loop_kernel[(1, )](source, out, _device_i32(steps), n=n, BM=bm, BN=bn, + ROW_STRIDE=row_stride) + + rows = torch.arange(bm)[:, None] + cols = torch.arange(bn)[None, :] + expected = source_cpu[rows * row_stride + cols + steps] + _assert_output(out, expected) + + +@pytest.mark.parametrize("steps", [0, 2]) +def test_broadcasted_pointer_rank2_loop(steps): + bm, bn, row_stride = 8, 16, 64 + n = 1024 + source_cpu = torch.arange(n, dtype=torch.float32) + source = source_cpu.npu() + out = torch.empty((bm, bn), dtype=torch.float32, device="npu") + + compiled = broadcasted_pointer_rank2_loop_kernel[(1, )]( + source, + out, + _device_i32(steps), + BM=bm, + BN=bn, + ROW_STRIDE=row_stride, + ) + + rows = torch.arange(bm)[:, None] + cols = torch.arange(bn)[None, :] + expected = source_cpu[rows * row_stride + cols + steps] + _assert_output(out, expected) + + # Before descriptor propagation, the pointer broadcast becomes an opaque + # tensor<8x16xi32> loop carrier even though both axes are affine. + adapter = compiled.asm["ttadapter"] + assert not re.search(r"scf\.for[^\n]*-> \(tensor<8x16xi32>\)", adapter) + assert re.search(r"scf\.for[^\n]*-> \(i32\)", adapter) + + +@pytest.mark.parametrize("steps", [0, 2, 4]) +def test_opaque_tensor_pointer_loop(steps): + a_cpu, b_cpu, a, b = _inputs() + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + + opaque_tensor_ptr_loop_kernel[(1, )](a, b, out, _device_i32(steps), n=N, BLOCK=BLOCK) + + expected = torch.zeros(BLOCK, dtype=torch.float32) + for lane in range(BLOCK): + index = lane + steps + if index < N: + expected[lane] = a_cpu[index] if (lane & 1) == 0 else b_cpu[index] + _assert_output(out, expected) + + +@pytest.mark.parametrize("delta", [0, 3, 37]) +def test_scope_block_pointer_result(delta): + a_cpu, _, a, _ = _inputs() + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + + scope_block_ptr_kernel[(1, )](a, out, _device_i32(delta), n=N, BLOCK=BLOCK) + + _assert_output(out, _slice(a_cpu, 0, N, 1, 1 + delta)) + + +@pytest.mark.parametrize("delta", [0, 2, 5]) +def test_scope_tensor_pointer_result(delta): + a_cpu, _, a, _ = _inputs() + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + + scope_tensor_ptr_kernel[(1, )](a, out, _device_i32(delta), n=N, BLOCK=BLOCK) + + _assert_output(out, _slice(a_cpu, 0, N, 1, delta)) diff --git a/third_party/ascend/unittest/pytest_ut/test_copy.py b/third_party/ascend/unittest/pytest_ut/test_copy.py new file mode 100644 index 0000000000..862700f12a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_copy.py @@ -0,0 +1,98 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import pytest +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + arch = "Ascend910_95" + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +@triton.jit +def copy( + A_ptr, + A1_ptr, + M: tl.constexpr, + N: tl.constexpr, +): + offs_a = tl.arange(0, M)[:, None] + offs_b = tl.arange(0, N)[None, :] + + offs_c = (offs_a) * M + (offs_b) + a_ptr = A_ptr + offs_c + a_val = tl.load(a_ptr) + a1_ptr = A1_ptr + offs_c + a1_val = tl.load(a1_ptr) + + add = tl.add(a_val, a1_val) + add_ub = bl.to_buffer(add, al.ascend_address_space.UB) + + A_l1 = bl.alloc(tl.float32, [M, N], al.ascend_address_space.L1) + al.copy_from_ub_to_l1(add_ub, A_l1) + + A_ub = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + al.copy(add_ub, A_ub) + + +def test_copy(): + print("=" * 60) + print("Test 1: copy ") + print("=" * 60) + mlir = compile_kernel( + copy, + {"A_ptr": "*fp32", "A1_ptr": "*fp32"}, + {"M": 16, "N": 16}, + ) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) + + +# ============== Main for manual testing ============== +if __name__ == "__main__": + test_copy() diff --git a/third_party/ascend/unittest/pytest_ut/test_copysign.py b/third_party/ascend/unittest/pytest_ut/test_copysign.py new file mode 100644 index 0000000000..40aa235f2a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_copysign.py @@ -0,0 +1,108 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import torch +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + + +@triton.jit +def copysign_kernel(x_ptr, y_ptr, z_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + + z = libdevice.copysign(x, y) + + tl.store(z_ptr + offsets, z, mask=mask) + + +@pytest.mark.parametrize('shape', [ + (12, 16), +]) +@pytest.mark.parametrize('dtype', ['float32']) +def test_copysign(shape, dtype): + n_elements = shape[0] * shape[1] + x = test_common.generate_tensor(shape, dtype).npu() + y = test_common.generate_tensor(shape, dtype).npu() + + # Ensure to include some boundary cases + x[0, 0] = 3.14 + y[0, 0] = 1.0 # The result should be 3.14 + + x[0, 1] = 3.14 + y[0, 1] = -1.0 # The result should be -3.14 + + x[0, 2] = -3.14 + y[0, 2] = 1.0 # The result should be 3.14 + + x[0, 3] = -3.14 + y[0, 3] = -1.0 # The result should be -3.14 + + x[0, 4] = 0.0 + y[0, 4] = -1.0 # The result should be -0.0 + + x[0, 5] = 0.0 + y[0, 5] = 1.0 # The result should be 0.0 + + x[0, 6] = 3.14 + y[0, 6] = 0.0 # The result should be 3.14 + + x[0, 7] = 3.14 + y[0, 7] = -0.0 # The result should be -3.14 + + x[0, 8] = -3.14 + y[0, 8] = 0.0 # The result should be 3.14 + + x[0, 9] = -3.14 + y[0, 9] = -0.0 # The result should be -3.14 + + x[0, 10] = 0.0 + y[0, 10] = 0.0 # The result should be 0.0 + + x[0, 11] = 0.0 + y[0, 11] = -0.0 # The result should be -0.0 + + x[0, 12] = -0.0 + y[0, 12] = 0.0 # The result should be 0.0 + + x[0, 13] = -0.0 + y[0, 13] = -0.0 # The result should be -0.0 + + z = torch.empty_like(x) + + BLOCK_SIZE = 192 + grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + copysign_kernel[grid](x, y, z, n_elements, BLOCK_SIZE=BLOCK_SIZE) + + expected = torch.copysign(x, y) + + torch.testing.assert_close(z, expected, rtol=1e-3, atol=1e-3) + + print("✓ COPYSIGN test PASSED!") diff --git a/third_party/ascend/unittest/pytest_ut/test_cos.py b/third_party/ascend/unittest/pytest_ut/test_cos.py new file mode 100644 index 0000000000..7c88852df5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cos.py @@ -0,0 +1,99 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = torch.cos(x0) + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = tl.cos(x) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + # (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + # (torch.int8, 'int8'), + # (torch.int16, 'int16'), + # (torch.int32, 'int32'), + # (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_cos_2.py b/third_party/ascend/unittest/pytest_ut/test_cos_2.py new file mode 100644 index 0000000000..b7de878120 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cos_2.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def torch_cos(x0): + res = torch.cos(x0) + return res + + +@triton.jit +def triton_cos(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.cos(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 32, 2048, 64], +]) +def test_cos(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype) + y_ref = torch_cos(x0) + tyname = test_common.get_triton_sig_typename(dtype) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + x0 = x0.npu() + triton_cos[ncore, 1, 1](x0, y_cal, xblock, xblock_sub, debug=True) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_cosh.py b/third_party/ascend/unittest/pytest_ut/test_cosh.py new file mode 100644 index 0000000000..ee35268b6a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cosh.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +@triton.jit +def triton_cosh(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.cosh(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', ['float32', 'float16', 'bfloat16']) +def test_cosh_special(param_list): + dtype = param_list + x_near_zero = torch.linspace(-1.0, 1.0, 256, dtype=eval("torch." + dtype)).npu() + x_medium_neg = torch.linspace(-10.0, -1.0, 128, dtype=eval("torch." + dtype)).npu() + x_medium_pos = torch.linspace(1.0, 10.0, 128, dtype=eval("torch." + dtype)).npu() + + x_large_neg = torch.linspace(-1e4, -10.0, 64, dtype=eval("torch." + dtype)).npu() + x_large_pos = torch.linspace(10.0, 1e4, 64, dtype=eval("torch." + dtype)).npu() + + x0 = torch.cat([x_near_zero, x_medium_neg, x_medium_pos, x_large_neg, x_large_pos], dim=0) + + y_ref = torch.cosh(x0) + y_cal = torch.zeros_like(y_ref) + triton_cosh[1, 1, 1](x0, y_cal, x0.shape[0], x0.shape[0]) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_cummax.py b/third_party/ascend/unittest/pytest_ut/test_cummax.py new file mode 100644 index 0000000000..70cdaf59f5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cummax.py @@ -0,0 +1,264 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + +from test_common import generate_tensor, validate_cmp + + +def torch_cummax(x, dim, reverse): + if reverse: + x = torch.flip(x, [dim]) + values, _ = torch.cummax(x, dim=dim) + if reverse: + values = torch.flip(values, [dim]) + return values + + +# --------------------------------------------------------------------------- +# Triton combine functions +# --------------------------------------------------------------------------- +@triton.jit +def _cummax_combine(a, b): + return tl.maximum(a, b, propagate_nan=tl.PropagateNan.ALL) + + +# --------------------------------------------------------------------------- +# 1D kernel +# --------------------------------------------------------------------------- +@triton.jit +def cummax_kernel_1d( + out_ptr, + in_ptr, + reverse: tl.constexpr, + XBLOCK: tl.constexpr, +): + idx = tl.arange(0, XBLOCK) + x = tl.load(in_ptr + idx) + ret = tl.associative_scan(x, axis=0, combine_fn=_cummax_combine, reverse=reverse) + tl.store(out_ptr + idx, ret) + + +# --------------------------------------------------------------------------- +# 2D kernel +# --------------------------------------------------------------------------- +@triton.jit +def cummax_kernel_2d( + out_ptr, + in_ptr, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + numel_r: tl.constexpr, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, +): + idx_x = tl.arange(0, XBLOCK) + idx_r = tl.arange(0, RBLOCK) + idx = idx_x[:, None] * numel_r + idx_r[None, :] + x = tl.load(in_ptr + idx) + ret = tl.associative_scan(x, axis=dim, combine_fn=_cummax_combine, reverse=reverse) + tl.store(out_ptr + idx, ret) + + +# --------------------------------------------------------------------------- +# 3D kernel +# --------------------------------------------------------------------------- +@triton.jit +def cummax_kernel_3d( + out_ptr, + in_ptr, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + numel_r: tl.constexpr, + numel_z: tl.constexpr, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, + ZBLOCK: tl.constexpr, +): + idx_x = tl.arange(0, XBLOCK) + idx_r = tl.arange(0, RBLOCK) + idx_z = tl.arange(0, ZBLOCK) + idx = (idx_x[:, None, None] * numel_r * numel_z + idx_r[None, :, None] * numel_z + idx_z[None, None, :]) + x = tl.load(in_ptr + idx) + ret = tl.associative_scan(x, axis=dim, combine_fn=_cummax_combine, reverse=reverse) + tl.store(out_ptr + idx, ret) + + +# --------------------------------------------------------------------------- +# Dispatch helper +# --------------------------------------------------------------------------- +def triton_cummax(x, dim, reverse): + res = torch.empty_like(x) + shape = x.shape + ndim = len(shape) + if ndim == 1: + cummax_kernel_1d[1, 1, 1](res, x, reverse, shape[0]) + elif ndim == 2: + cummax_kernel_2d[1, 1, 1]( + res, + x, + dim, + reverse, + shape[0], + shape[1], + shape[0], + shape[1], + ) + elif ndim == 3: + cummax_kernel_3d[1, 1, 1]( + res, + x, + dim, + reverse, + shape[0], + shape[1], + shape[2], + shape[0], + shape[1], + shape[2], + ) + else: + pytest.skip(f"Unsupported tensor dimension: {ndim}") + return res + + +# --------------------------------------------------------------------------- +# Test parameters +# --------------------------------------------------------------------------- +float_dtypes = ["float32", "float16"] +int_dtypes = ["int32", "int16"] + +shapes_1d = [(64, ), (128, ), (7, )] +shapes_2d = [(7, 23), (16, 32), (3, 5)] +shapes_3d = [(4, 8, 16), (3, 5, 7)] + + +# --------------------------------------------------------------------------- +# 1D tests +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", float_dtypes + int_dtypes) +@pytest.mark.parametrize("shape", shapes_1d) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummax_1d(dtype, shape, reverse): + x = generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_cummax(x, dim=0, reverse=reverse) + torch_ref = torch_cummax(x, dim=0, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# 2D tests +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", float_dtypes + int_dtypes) +@pytest.mark.parametrize("shape", shapes_2d) +@pytest.mark.parametrize("dim", [0, 1]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummax_2d(dtype, shape, dim, reverse): + x = generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_cummax(x, dim=dim, reverse=reverse) + torch_ref = torch_cummax(x, dim=dim, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# 3D tests +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", float_dtypes + int_dtypes) +@pytest.mark.parametrize("shape", shapes_3d) +@pytest.mark.parametrize("dim", [0, 1]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummax_3d(dtype, shape, dim, reverse): + x = generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_cummax(x, dim=dim, reverse=reverse) + torch_ref = torch_cummax(x, dim=dim, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# Edge-case: constant input (all elements equal) +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", float_dtypes + int_dtypes) +@pytest.mark.parametrize("dim", [0, 1]) +def test_cummax_constant(dtype, dim): + torch_dtype = eval("torch." + dtype) + x = torch.full((8, 16), 42, dtype=torch_dtype).npu() + triton_cal = triton_cummax(x, dim=dim, reverse=False) + torch_ref = torch_cummax(x, dim=dim, reverse=False) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# Edge-case: monotonically increasing / decreasing +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummax_monotonic_increasing(dtype, reverse): + torch_dtype = eval("torch." + dtype) + x = torch.arange(1, 33, dtype=torch_dtype).reshape(4, 8).npu() + triton_cal = triton_cummax(x, dim=1, reverse=reverse) + torch_ref = torch_cummax(x, dim=1, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummax_monotonic_decreasing(dtype, reverse): + torch_dtype = eval("torch." + dtype) + x = torch.arange(32, 0, -1, dtype=torch_dtype).reshape(4, 8).npu() + triton_cal = triton_cummax(x, dim=1, reverse=reverse) + torch_ref = torch_cummax(x, dim=1, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# Edge-case: negative values +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +def test_cummax_negative_values(dtype): + torch_dtype = eval("torch." + dtype) + x = torch.tensor([[-3, -1, -4, -1, -5, -9, -2, -6]], dtype=torch_dtype).npu() + triton_cal = triton_cummax(x, dim=1, reverse=False) + torch_ref = torch_cummax(x, dim=1, reverse=False) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# Edge-case: single row / single column +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +def test_cummax_single_row(dtype): + x = generate_tensor(shape=(1, 32), dtype=dtype).npu() + triton_cal = triton_cummax(x, dim=1, reverse=False) + torch_ref = torch_cummax(x, dim=1, reverse=False) + validate_cmp(dtype, torch_ref, triton_cal) + + +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +def test_cummax_single_col(dtype): + x = generate_tensor(shape=(32, 1), dtype=dtype).npu() + triton_cal = triton_cummax(x, dim=0, reverse=False) + torch_ref = torch_cummax(x, dim=0, reverse=False) + validate_cmp(dtype, torch_ref, triton_cal) diff --git a/third_party/ascend/unittest/pytest_ut/test_cummin.py b/third_party/ascend/unittest/pytest_ut/test_cummin.py new file mode 100644 index 0000000000..4033b8a4ca --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cummin.py @@ -0,0 +1,264 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + +from test_common import generate_tensor, validate_cmp + + +def torch_cummin(x, dim, reverse): + if reverse: + x = torch.flip(x, [dim]) + values, _ = torch.cummin(x, dim=dim) + if reverse: + values = torch.flip(values, [dim]) + return values + + +# --------------------------------------------------------------------------- +# Triton combine functions +# --------------------------------------------------------------------------- +@triton.jit +def _cummin_combine(a, b): + return tl.minimum(a, b, propagate_nan=tl.PropagateNan.ALL) + + +# --------------------------------------------------------------------------- +# 1D kernel +# --------------------------------------------------------------------------- +@triton.jit +def cummin_kernel_1d( + out_ptr, + in_ptr, + reverse: tl.constexpr, + XBLOCK: tl.constexpr, +): + idx = tl.arange(0, XBLOCK) + x = tl.load(in_ptr + idx) + ret = tl.associative_scan(x, axis=0, combine_fn=_cummin_combine, reverse=reverse) + tl.store(out_ptr + idx, ret) + + +# --------------------------------------------------------------------------- +# 2D kernel +# --------------------------------------------------------------------------- +@triton.jit +def cummin_kernel_2d( + out_ptr, + in_ptr, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + numel_r: tl.constexpr, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, +): + idx_x = tl.arange(0, XBLOCK) + idx_r = tl.arange(0, RBLOCK) + idx = idx_x[:, None] * numel_r + idx_r[None, :] + x = tl.load(in_ptr + idx) + ret = tl.associative_scan(x, axis=dim, combine_fn=_cummin_combine, reverse=reverse) + tl.store(out_ptr + idx, ret) + + +# --------------------------------------------------------------------------- +# 3D kernel +# --------------------------------------------------------------------------- +@triton.jit +def cummin_kernel_3d( + out_ptr, + in_ptr, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + numel_r: tl.constexpr, + numel_z: tl.constexpr, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, + ZBLOCK: tl.constexpr, +): + idx_x = tl.arange(0, XBLOCK) + idx_r = tl.arange(0, RBLOCK) + idx_z = tl.arange(0, ZBLOCK) + idx = (idx_x[:, None, None] * numel_r * numel_z + idx_r[None, :, None] * numel_z + idx_z[None, None, :]) + x = tl.load(in_ptr + idx) + ret = tl.associative_scan(x, axis=dim, combine_fn=_cummin_combine, reverse=reverse) + tl.store(out_ptr + idx, ret) + + +# --------------------------------------------------------------------------- +# Dispatch helper +# --------------------------------------------------------------------------- +def triton_cummin(x, dim, reverse): + res = torch.empty_like(x) + shape = x.shape + ndim = len(shape) + if ndim == 1: + cummin_kernel_1d[1, 1, 1](res, x, reverse, shape[0]) + elif ndim == 2: + cummin_kernel_2d[1, 1, 1]( + res, + x, + dim, + reverse, + shape[0], + shape[1], + shape[0], + shape[1], + ) + elif ndim == 3: + cummin_kernel_3d[1, 1, 1]( + res, + x, + dim, + reverse, + shape[0], + shape[1], + shape[2], + shape[0], + shape[1], + shape[2], + ) + else: + pytest.skip(f"Unsupported tensor dimension: {ndim}") + return res + + +# --------------------------------------------------------------------------- +# Test parameters +# --------------------------------------------------------------------------- +float_dtypes = ["float32", "float16"] +int_dtypes = ["int32", "int16"] + +shapes_1d = [(64, ), (128, ), (7, )] +shapes_2d = [(7, 23), (16, 32), (3, 5)] +shapes_3d = [(4, 8, 16), (3, 5, 7)] + + +# --------------------------------------------------------------------------- +# 1D tests +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", float_dtypes + int_dtypes) +@pytest.mark.parametrize("shape", shapes_1d) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummin_1d(dtype, shape, reverse): + x = generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_cummin(x, dim=0, reverse=reverse) + torch_ref = torch_cummin(x, dim=0, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# 2D tests +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", float_dtypes + int_dtypes) +@pytest.mark.parametrize("shape", shapes_2d) +@pytest.mark.parametrize("dim", [0, 1]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummin_2d(dtype, shape, dim, reverse): + x = generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_cummin(x, dim=dim, reverse=reverse) + torch_ref = torch_cummin(x, dim=dim, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# 3D tests +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", float_dtypes + int_dtypes) +@pytest.mark.parametrize("shape", shapes_3d) +@pytest.mark.parametrize("dim", [0, 1]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummin_3d(dtype, shape, dim, reverse): + x = generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_cummin(x, dim=dim, reverse=reverse) + torch_ref = torch_cummin(x, dim=dim, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# Edge-case: constant input (all elements equal) +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", float_dtypes + int_dtypes) +@pytest.mark.parametrize("dim", [0, 1]) +def test_cummin_constant(dtype, dim): + torch_dtype = eval("torch." + dtype) + x = torch.full((8, 16), 42, dtype=torch_dtype).npu() + triton_cal = triton_cummin(x, dim=dim, reverse=False) + torch_ref = torch_cummin(x, dim=dim, reverse=False) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# Edge-case: monotonically increasing / decreasing +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummin_monotonic_increasing(dtype, reverse): + torch_dtype = eval("torch." + dtype) + x = torch.arange(1, 33, dtype=torch_dtype).reshape(4, 8).npu() + triton_cal = triton_cummin(x, dim=1, reverse=reverse) + torch_ref = torch_cummin(x, dim=1, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cummin_monotonic_decreasing(dtype, reverse): + torch_dtype = eval("torch." + dtype) + x = torch.arange(32, 0, -1, dtype=torch_dtype).reshape(4, 8).npu() + triton_cal = triton_cummin(x, dim=1, reverse=reverse) + torch_ref = torch_cummin(x, dim=1, reverse=reverse) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# Edge-case: negative values +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +def test_cummin_negative_values(dtype): + torch_dtype = eval("torch." + dtype) + x = torch.tensor([[-3, -1, -4, -1, -5, -9, -2, -6]], dtype=torch_dtype).npu() + triton_cal = triton_cummin(x, dim=1, reverse=False) + torch_ref = torch_cummin(x, dim=1, reverse=False) + validate_cmp(dtype, torch_ref, triton_cal) + + +# --------------------------------------------------------------------------- +# Edge-case: single row / single column +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +def test_cummin_single_row(dtype): + x = generate_tensor(shape=(1, 32), dtype=dtype).npu() + triton_cal = triton_cummin(x, dim=1, reverse=False) + torch_ref = torch_cummin(x, dim=1, reverse=False) + validate_cmp(dtype, torch_ref, triton_cal) + + +@pytest.mark.parametrize("dtype", ["float32", "int32"]) +def test_cummin_single_col(dtype): + x = generate_tensor(shape=(32, 1), dtype=dtype).npu() + triton_cal = triton_cummin(x, dim=0, reverse=False) + torch_ref = torch_cummin(x, dim=0, reverse=False) + validate_cmp(dtype, torch_ref, triton_cal) diff --git a/third_party/ascend/unittest/pytest_ut/test_cumprod.py b/third_party/ascend/unittest/pytest_ut/test_cumprod.py new file mode 100644 index 0000000000..c632442011 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cumprod.py @@ -0,0 +1,207 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.runtime.libentry import libentry +from triton.backends.ascend.utils import is_compile_on_910_95 + +from test_common import _all_dtypes_no_bool, _uint_dtypes, validate_cmp + + +def torch_func(x, dim, reverse): + is_bf16 = x.dtype == torch.bfloat16 + if is_bf16: + x = x.to(torch.float32) + if reverse: + x = torch.flip(x, [dim]) + res = torch.cumprod(x, dim=dim) + if is_bf16: + res = res.to(torch.bfloat16) + if reverse: + res = torch.flip(res, [dim]) + return res + + +@libentry() +@triton.jit +def triton_kernel_1d( + out_ptr0, + in_ptr0, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + XBLOCK: tl.constexpr, +): + tl.static_assert(numel_x == XBLOCK, "numel_x must be equal to XBLOCK in this kernel") + idx = tl.arange(0, XBLOCK) + x = tl.load(in_ptr0 + idx) + ret = tl.cumprod(x, axis=dim, reverse=reverse) + tl.store(out_ptr0 + idx, ret) + + +def triton_func_1d(x, dim, reverse): + res = torch.empty_like(x) + triton_kernel_1d[1, 1, 1](res, x, dim, reverse, x.shape[0], x.shape[0]) + return res + + +@libentry() +@triton.jit +def triton_kernel_2d( + out_ptr0, + in_ptr0, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + numel_r: tl.constexpr, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, +): + tl.static_assert(numel_x == XBLOCK, "numel_x must be equal to XBLOCK in this kernel") + tl.static_assert(numel_r == RBLOCK, "numel_r must be equal to RBLOCK in this kernel") + idx_x = tl.arange(0, XBLOCK) + idx_r = tl.arange(0, RBLOCK) + idx = idx_x[:, None] * numel_r + idx_r[None, :] + x = tl.load(in_ptr0 + idx) + ret = tl.cumprod(x, axis=dim, reverse=reverse) + tl.store(out_ptr0 + idx, ret) + + +def triton_func_2d(x, dim, reverse): + res = torch.empty_like(x) + triton_kernel_2d[1, 1, 1](res, x, dim, reverse, x.shape[0], x.shape[1], x.shape[0], x.shape[1]) + return res + + +# --------------------------------------------------------------------------- +# 5D kernel +# dim is the axis passed to tl.cumprod (0‥4), relative to the 5D block. +# The kernel is launched with a single program (grid=[1,1,1]) and loads the +# entire tensor in one shot, so every constexpr size must be a power-of-two +# (Triton tl.arange requirement). +# --------------------------------------------------------------------------- +@libentry() +@triton.jit +def triton_kernel_5d( + out_ptr0, + in_ptr0, + dim: tl.constexpr, + reverse: tl.constexpr, + S0: tl.constexpr, + S1: tl.constexpr, + S2: tl.constexpr, + S3: tl.constexpr, + S4: tl.constexpr, +): + # Build a 5D index grid, keeping each dimension independent so that + # tl.cumprod can operate along the correct axis without any flattening. + i0 = tl.arange(0, S0) + i1 = tl.arange(0, S1) + i2 = tl.arange(0, S2) + i3 = tl.arange(0, S3) + i4 = tl.arange(0, S4) + + # Broadcast to a full [S0, S1, S2, S3, S4] index tensor. + # Each reshape adds the missing axes so broadcasting produces the right shape. + idx = (i0[:, None, None, None, None] * (S1 * S2 * S3 * S4) + i1[None, :, None, None, None] * (S2 * S3 * S4) + + i2[None, None, :, None, None] * (S3 * S4) + i3[None, None, None, :, None] * S4 + + i4[None, None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + + ret = tl.cumprod(x, axis=dim, reverse=reverse) + + tl.store(out_ptr0 + idx, ret) + + +def triton_func_5d(x, dim, reverse): + assert x.ndim == 5, "triton_func_5d expects a 5-D tensor" + res = torch.empty_like(x) + S0, S1, S2, S3, S4 = x.shape + triton_kernel_5d[1, 1, 1]( + res, + x, + dim, + reverse, + S0, + S1, + S2, + S3, + S4, + ) + return res + + +def cumprod_generate_tensor(shape, dtype): + if dtype in ('float32', 'float16', 'bfloat16'): + return torch.rand(size=shape, dtype=eval('torch.' + dtype)) + elif dtype in ('int32', 'int64', 'int16'): + return torch.randint(low=0, high=10, size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'int8': + return torch.randint(low=0, high=10, size=shape, dtype=eval('torch.' + dtype)) + elif dtype in ('uint8', 'uint32', 'uint64', 'uint16'): + return torch.randint(low=0, high=10, size=shape, dtype=eval('torch.' + dtype)) + elif dtype == 'bool': + return torch.randint(low=0, high=2, size=shape, dtype=eval('torch.' + dtype)) + else: + raise ValueError(f"Unsupported dtype: {dtype}") + + +not_support_dtype = {'int8', 'bool'} +support_dtypes = (_all_dtypes_no_bool + _uint_dtypes) if is_compile_on_910_95( + triton.runtime.driver.active.get_current_target().arch) else \ + [dtype for dtype in _all_dtypes_no_bool if dtype not in not_support_dtype] + + +@pytest.mark.parametrize("dtype", support_dtypes) +@pytest.mark.parametrize("shape", [(1024, )]) +@pytest.mark.parametrize("dim", [0]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cumprod_1d(dtype, shape, dim, reverse): + x0 = cumprod_generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_func_1d(x0, dim, reverse) + torch_ref = torch_func(x0, dim, reverse).to(eval("torch." + dtype)) + validate_cmp(dtype, torch_ref, triton_cal) + + +@pytest.mark.parametrize("dtype", support_dtypes) +@pytest.mark.parametrize("shape", [(7, 23)]) +@pytest.mark.parametrize("dim", [0, 1]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cumprod_2d(dtype, shape, dim, reverse): + x0 = cumprod_generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_func_2d(x0, dim, reverse) + torch_ref = torch_func(x0, dim, reverse).to(eval("torch." + dtype)) + validate_cmp(dtype, torch_ref, triton_cal) + + +@pytest.mark.parametrize("dtype", support_dtypes) +@pytest.mark.parametrize("shape", [(2, 4, 4, 4, 4)]) +@pytest.mark.parametrize("dim", [0, 1, 4]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cumprod_5d(dtype, shape, dim, reverse): + x0 = cumprod_generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_func_5d(x0, dim, reverse) + torch_ref = torch_func(x0, dim, reverse).to(eval("torch." + dtype)) + validate_cmp(dtype, torch_ref, triton_cal) diff --git a/third_party/ascend/unittest/pytest_ut/test_cumsum.py b/third_party/ascend/unittest/pytest_ut/test_cumsum.py new file mode 100644 index 0000000000..c9196b6bf7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cumsum.py @@ -0,0 +1,84 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.runtime.libentry import libentry + +from test_common import _all_dtypes_no_bool, generate_tensor, validate_cmp + + +def torch_func(x, dim, reverse): + if reverse: + x = torch.flip(x, [dim]) + res = torch.cumsum(x, dim=dim) + if reverse: + res = torch.flip(res, [dim]) + return res + + +@libentry() +@triton.jit +def triton_kernel( + out_ptr0, + in_ptr0, + dim: tl.constexpr, + reverse: tl.constexpr, + numel_x: tl.constexpr, + numel_r: tl.constexpr, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, +): + tl.static_assert(numel_x == XBLOCK, "numel_x must be equal to XBLOCK in this kernel") + tl.static_assert(numel_r == RBLOCK, "numel_r must be equal to RBLOCK in this kernel") + idx_x = tl.arange(0, XBLOCK) + idx_r = tl.arange(0, RBLOCK) + idx = idx_x[:, None] * numel_r + idx_r[None, :] + x = tl.load(in_ptr0 + idx) + ret = tl.cumsum(x, axis=dim, reverse=reverse) + tl.store(out_ptr0 + idx, ret) + + +def triton_func(x, dim, reverse): + res = torch.empty_like(x) + triton_kernel[1, 1, 1](res, x, dim, reverse, x.shape[0], x.shape[1], x.shape[0], x.shape[1]) + return res + + +# dtype=int8, reverse=True not support; +not_support_dtype = {'int8', 'bool'} +support_dtypes = [dtype for dtype in _all_dtypes_no_bool if dtype not in not_support_dtype] + + +@pytest.mark.parametrize("dtype", support_dtypes) +@pytest.mark.parametrize("shape", [(7, 23)]) +@pytest.mark.parametrize("dim", [0, 1]) +@pytest.mark.parametrize("reverse", [False, True]) +def test_cumsum(dtype, shape, dim, reverse): + x0 = generate_tensor(shape=shape, dtype=dtype).npu() + triton_cal = triton_func(x0, dim, reverse) + torch_dtype = eval('torch.' + dtype) + if torch_dtype == torch.float16 or torch_dtype == torch.float32: + x0 = x0.to(torch.float32) + torch_ref = torch_func(x0, dim, reverse).to(torch_dtype) + validate_cmp(dtype, torch_ref, triton_cal) diff --git a/third_party/ascend/unittest/pytest_ut/test_custom.py b/third_party/ascend/unittest/pytest_ut/test_custom.py new file mode 100755 index 0000000000..97c2a0ffe9 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_custom.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +import subprocess +import os +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, ttir_to_linalg +import pytest + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + try: + options = NPUOptions() + ttir = ast_to_ttir(kernel, src, context, options, {}, {}) + metadata = { + **options.__dict__, + } + linalg = ttir_to_linalg(ttir, metadata, options, named_ops=True) + return str(linalg) + except subprocess.CalledProcessError as ex: + print(ex.stdout.decode()) + print(ex.stderr.decode()) + print("failed") + return None + + +# ============== Kernel definitions ============== + + +@al.register_custom_op +class my_custom_op: + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + symbol = "my_custom_func" + # fake path, this test only check Triton successfully lowered to MLIR + bitcode = os.path.abspath(__file__) + iterator_types = [ + al.IteratorType.Parallel, + al.IteratorType.Broadcast, + al.IteratorType.Transpose, + al.IteratorType.Reduction, + al.IteratorType.Interleave, + al.IteratorType.Deinterleave, + al.IteratorType.Inverse, + al.IteratorType.Pad, + al.IteratorType.Concat, + al.IteratorType.Gather, + al.IteratorType.Cumulative, + al.IteratorType.Opaque, + ] + + def __init__(self, x, ptr1, ptr2, offset: tl.int64, other, out=None): + # Add optional custom-op attribute: ArrayAttr. + self.indexing_map = [al.affine_map.get_identity(1)] + + # Tag ptr2 as an argument that should be aligned at dimension 1. + # Tag 2nd argument that should be aligned at dimension 0. + self.align_dim = {"ptr2": 1, 1: 0} + + +@triton.jit +def my_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + result = al.custom("my_custom_op", x, x_ptr, y_ptr + i, (1, 2, 3), [4.1, 5.2], out=y) + a = 123 + result = al.custom("my_custom_op", x, x_ptr, y_ptr, (a, n), (1.2, 3.4), out=result) + tl.store(out_ptr + i, result, mask=i < n) + + +@al.register_custom_op +class my_custom_op_extra_buf: + """Custom op declaring extra_buffers with several scalar Triton dtypes.""" + + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + symbol = "my_extra_buf_func" + bitcode = os.path.abspath(__file__) + + def __init__(self, x, out=None): + self.indexing_map = [al.affine_map.get_identity(1)] + self.extra_buffers = [ + (tl.bfloat16, 256), + (tl.float64, 424242), + (tl.int8, 11), + (tl.float16, 22), + (tl.int32, 33), + ] + + +@al.register_custom_op +class my_custom_op_extra_buf_single_buf: + """Custom op declaring extra_buffers with single buf.""" + + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + symbol = "my_extra_buf_func_single_buf" + bitcode = os.path.abspath(__file__) + + def __init__(self, x, out=None): + self.indexing_map = [al.affine_map.get_identity(1)] + self.extra_buffers = (tl.bfloat16, 256) + + +@triton.jit +def kernel_extra_buf(x_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(out_ptr + i, mask=i < n) + r = al.custom("my_custom_op_extra_buf", x, out=y) + tl.store(out_ptr + i, r, mask=i < n) + + +@triton.jit +def kernel_extra_buf_single_buf(x_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(out_ptr + i, mask=i < n) + r = al.custom("my_custom_op_extra_buf_single_buf", x, out=y) + tl.store(out_ptr + i, r, mask=i < n) + + +@al.register_custom_op +class my_custom_op_extra_buf_wide: + """Cover more integer widths and unsigned dtypes in extra_buffers_types.""" + + core = al.CORE.VECTOR + pipe = al.PIPE.PIPE_V + mode = al.MODE.SIMT + symbol = "my_extra_buf_wide_func" + bitcode = os.path.abspath(__file__) + + def __init__(self, x, out=None): + self.indexing_map = [al.affine_map.get_identity(1)] + self.extra_buffers = [ + (tl.int16, 1001), + (tl.uint16, 1002), + (tl.int64, 1003), + (tl.uint32, 1004), + (tl.uint8, 1005), + ] + + +@triton.jit +def kernel_extra_buf_wide(x_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(out_ptr + i, mask=i < n) + r = al.custom("my_custom_op_extra_buf_wide", x, out=y) + tl.store(out_ptr + i, r, mask=i < n) + + +# ============== Pytest tests ============== + + +def test_custom_op(): + """Test custom op compile to linalg MLIR.""" + mlir = compile_kernel(my_kernel, {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}) + assert mlir and len(mlir) > 0 + assert "func.func @my_kernel(" in mlir + assert "hivm.hir.custom" in mlir + for line in mlir.splitlines(): + if "hivm.hir.custom" in line: + # custom op name + assert '"my_custom_op"' in line + # All tt.ptr converted to memref. + assert "tt.ptr" not in line + # Required attributes are set. + assert "hivm.pipe = #hivm.pipe" in line + assert "hivm.tcore_type = #hivm.tcore_type" in line + assert "hivm.vf_mode = #hivm.vf_mode" in line + # Optional indexing map attribute should be attached. + assert "indexing_map = [" in line + # Tagged argument alignment info is attached as integer operand attr. + assert "align_dim = 1" in line + assert "align_dim = 0" in line + # All offset converted to int64. + assert 'i64, ' in line + assert 'i32, ' not in line + assert "iterator_types" in line + for iterator_name in ( + "parallel", + "broadcast", + "transpose", + "reduction", + "interleave", + "deinterleave", + "inverse", + "pad", + "concat", + "gather", + "cumulative", + "opaque", + ): + assert iterator_name in line + + +def _custom_lines(mlir: str, op_name: str): + # Match the MLIR string attribute exactly (avoid `my_custom_op` matching + # `my_custom_op_extra_buf`). + quoted = f'"{op_name}"' + return [line for line in mlir.splitlines() if "hivm.hir.custom" in line and quoted in line] + + +def test_custom_op_extra_buffers_mixed_scalar_types(): + """extra_buffers_types must preserve bf16/f64/i8/f16/i32 (not all lowered to f32).""" + mlir = compile_kernel( + kernel_extra_buf, + {"x_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert mlir and len(mlir) > 0 + lines = _custom_lines(mlir, "my_custom_op_extra_buf") + assert lines, "expected at least one hivm.hir.custom line for my_custom_op_extra_buf" + line = lines[0] + assert "extra_buffers_types" in line + assert "extra_buffers_sizes" in line + assert "bf16" in line + assert "f64" in line + assert "i8" in line + assert "f16" in line + assert "i32" in line + assert "424242" in line + + +def test_custom_op_extra_buffers_single_buffer(): + mlir = compile_kernel( + kernel_extra_buf_single_buf, + {"x_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert mlir and len(mlir) > 0 + lines = _custom_lines(mlir, "my_custom_op_extra_buf_single_buf") + assert lines, "expected at least one hivm.hir.custom line for my_custom_op_extra_buf_single_buf" + line = lines[0] + assert "extra_buffers_types" in line + assert "extra_buffers_sizes" in line + assert "f32" in line + + +def test_custom_op_extra_buffers_integer_variants(): + """extra_buffers accept int16/uint16/int64/uint32/uint8 (IR uses i* storage types).""" + mlir = compile_kernel( + kernel_extra_buf_wide, + {"x_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert mlir and len(mlir) > 0 + lines = _custom_lines(mlir, "my_custom_op_extra_buf_wide") + assert lines + line = lines[0] + assert "extra_buffers_types" in line + assert "extra_buffers_sizes" in line + assert "i16" in line + assert "i64" in line + assert "i32" in line + assert "i8" in line + assert "1001" in line and "1005" in line + + +def test_custom_op_without_extra_buffers_has_no_extra_buffer_attrs(): + """Ops that do not set extra_buffers should not emit extra_buffers_* attributes.""" + mlir = compile_kernel( + my_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert mlir + for line in _custom_lines(mlir, "my_custom_op"): + assert "extra_buffers_types" not in line + assert "extra_buffers_sizes" not in line + + +# ============== Main for manual testing ============== + +if __name__ == "__main__": + test_custom_op() + test_custom_op_without_extra_buffers_has_no_extra_buffer_attrs() + test_custom_op_extra_buffers_integer_variants() + test_custom_op_extra_buffers_mixed_scalar_types() + test_custom_op_extra_buffers_single_buffer() + mlir = compile_kernel(my_kernel, {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_custom_macro.py b/third_party/ascend/unittest/pytest_ut/test_custom_macro.py new file mode 100755 index 0000000000..5381f68818 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_custom_macro.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +import os + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, ttir_to_linalg + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel function to MLIR in linalg dialect.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + options = NPUOptions() + ttir = ast_to_ttir(kernel, src, context, options, {}, {}) + metadata = {**options.__dict__} + return str(ttir_to_linalg(ttir, metadata, options, named_ops=True)) + + +@al.register_custom_op +class my_custom_macro_op: + core = al.CORE.VECTOR + pipe = (al.PIPE.PIPE_MTE2, al.PIPE.PIPE_V) + mode = al.MODE.SIMD + symbol = "my_custom_macro_func" + bitcode = os.path.abspath(__file__) + + def __init__(self, x, out=None): + self.indexing_map = [al.affine_map.get_identity(1)] + + +@triton.jit +def my_macro_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + result = al.custom("my_custom_macro_op", x, out=y) + tl.store(out_ptr + i, result, mask=i < n) + + +@al.register_custom_op +class my_custom_macro_sync_op: + core = al.CORE.VECTOR + pipe = [al.PIPE.PIPE_MTE2, al.PIPE.PIPE_V] + mode = al.MODE.SIMD + symbol = "my_custom_macro_sync_func" + bitcode = os.path.abspath(__file__) + sync_event_slots = [ + al.SyncEventSlot( + set_pipe=al.PIPE.PIPE_MTE2, + wait_pipe=al.PIPE.PIPE_MTE1, + sync=al.SYNC_HINT.WAIT, + event=al.EVENT_ID.EVENT_ID1, + ), + (al.PIPE.PIPE_M, al.PIPE.PIPE_MTE2, al.SYNC_HINT.SET), + ] + + def __init__(self, x, out=None): + self.indexing_map = [al.affine_map.get_identity(1)] + + +@triton.jit +def my_macro_sync_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + result = al.custom( + "my_custom_macro_sync_op", + x, + out=y, + ) + tl.store(out_ptr + i, result, mask=i < n) + + +def _custom_macro_lines(mlir: str, op_name: str): + quoted = f'"{op_name}"' + return [line for line in mlir.splitlines() if "hivm.hir.custom_macro" in line and quoted in line] + + +def test_custom_macro_op_lowers_to_custom_macro(): + mlir = compile_kernel( + my_macro_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert mlir and len(mlir) > 0 + assert "func.func @my_macro_kernel(" in mlir + lines = _custom_macro_lines(mlir, "my_custom_macro_op") + assert lines, "expected hivm.hir.custom_macro for my_custom_macro_op" + for line in lines: + assert "hivm.hir.custom_macro" in line + assert "hivm.hir.custom " not in line + assert "hivm.pipe_in = #hivm.pipe" in line + assert "hivm.pipe_out = #hivm.pipe" in line + assert "hivm.tcore_type = #hivm.tcore_type" in line + assert "hivm.vf_mode = #hivm.vf_mode" in line + assert "indexing_map = [" in line + assert "hivm.pipe = #hivm.pipe" not in line + + +def test_custom_macro_op_sync_event_slots(): + mlir = compile_kernel( + my_macro_sync_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert mlir and len(mlir) > 0 + lines = _custom_macro_lines(mlir, "my_custom_macro_sync_op") + assert lines, "expected hivm.hir.custom_macro for my_custom_macro_sync_op" + line = lines[0] + assert "hivm.pipe_in = #hivm.pipe" in line + assert "hivm.pipe_out = #hivm.pipe" in line + assert "hivm.tcore_type = #hivm.tcore_type" in line + assert "hivm.vf_mode = #hivm.vf_mode" in line + assert "sync_event_slots = [" in line + assert "#hivm.sync_event_slot<#hivm.pipe, #hivm.pipe, wait, >" in line + assert "#hivm.sync_event_slot<#hivm.pipe, #hivm.pipe, set>" in line + assert "ins(%" in line and ": tensor<256xf32>" in line + assert "outs(%" in line + + +def test_custom_macro_op_does_not_emit_single_pipe_attr(): + mlir = compile_kernel( + my_macro_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert mlir + for line in _custom_macro_lines(mlir, "my_custom_macro_op"): + assert "hivm.pipe = #hivm.pipe" not in line + + +if __name__ == "__main__": + test_custom_macro_op_lowers_to_custom_macro() + test_custom_macro_op_sync_event_slots() + test_custom_macro_op_does_not_emit_single_pipe_attr() + mlir = compile_kernel( + my_macro_sync_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + print(f"Generated MLIR ({len(mlir)} chars):\n") + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_cyl_bessel_i0.py b/third_party/ascend/unittest/pytest_ut/test_cyl_bessel_i0.py new file mode 100644 index 0000000000..5eba4e9690 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_cyl_bessel_i0.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu +import numpy as np + + +def torch_modified_bessel_i0(x0): + return torch.special.modified_bessel_i0(x0) + + +@triton.jit +def triton_modified_bessel_i0(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.cyl_bessel_i0(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_modified_bessel_i0(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + np_x0 = test_common.generate_numpy(shape, dtype) + y_ref = np.i0(np_x0) + y_ref = torch.from_numpy(y_ref).to(eval('torch.' + dtype)) + x0 = torch.from_numpy(np_x0).to(eval('torch.' + dtype)).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_modified_bessel_i0[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_debug_barrier.py b/third_party/ascend/unittest/pytest_ut/test_debug_barrier.py new file mode 100644 index 0000000000..98e9266bd8 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_debug_barrier.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 - x1 + return res + + +@triton.jit +def triton_sub(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 - tmp1 + tl.debug_barrier() + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_sub[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_debug_line_rewriter.py b/third_party/ascend/unittest/pytest_ut/test_debug_line_rewriter.py new file mode 100644 index 0000000000..99e258cb98 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_debug_line_rewriter.py @@ -0,0 +1,497 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Unit tests for debug_line_rewriter. + +No real .npubin is needed: minimal ELF + DWARF-4 line programs are synthesized +in memory, so the full parse -> plan -> patch -> verify path is exercised, and +each demotion rule is tested directly against synthetic rows. +""" + +import os +import struct +import sys +import tempfile +import unittest +from unittest import mock + + +def _import_rewriter(): + """Import debug_line_rewriter, preferring the in-tree source so the unit + test validates the repo code regardless of whether triton-ascend has been + (re)installed. Falls back to the installed backend if the source isn't + found next to the test.""" + import importlib + + here = os.path.dirname(os.path.abspath(__file__)) + # pytest_ut/ -> ../../backend/debug_line_rewriter.py ; "." covers in-place runs + for rel in ("../../backend", "."): + candidate = os.path.normpath(os.path.join(here, rel)) + if candidate not in sys.path: + sys.path.insert(0, candidate) + try: + return importlib.import_module("debug_line_rewriter") + except Exception: + return importlib.import_module("triton.backends.ascend.debug_line_rewriter") + + +dlr = _import_rewriter() +import logging as _logging + +dlr.log.setLevel(_logging.CRITICAL) # silence rewrite-skipped logs during tests + +rewrite_debug_line = dlr.rewrite_debug_line +rewrite_debug_line_blob = dlr.rewrite_debug_line_blob +ENV_FLAG = dlr.ENV_FLAG +DW_LNS_ADVANCE_LINE = dlr.DW_LNS_ADVANCE_LINE +DW_LNS_COPY = dlr.DW_LNS_COPY +DW_LNS_FIXED_ADVANCE_PC = dlr.DW_LNS_FIXED_ADVANCE_PC +DW_LNS_NEGATE_STMT = dlr.DW_LNS_NEGATE_STMT +DW_LNS_SET_BASIC_BLOCK = dlr.DW_LNS_SET_BASIC_BLOCK +DW_LNS_SET_COLUMN = dlr.DW_LNS_SET_COLUMN +DW_LNS_SET_FILE = dlr.DW_LNS_SET_FILE +DW_LNE_END_SEQUENCE = dlr.DW_LNE_END_SEQUENCE +DW_LNE_SET_ADDRESS = dlr.DW_LNE_SET_ADDRESS + +# pytest_ut/conftest.py declares an autouse, module-scoped `assign_npu` fixture +# that depends on `worker_id` (pytest-xdist) and binds an NPU device. This suite +# needs no NPU, so override it with a no-op for this module: the file then runs +# under a bare `pytest ` (no xdist) and in NPU-less CI, while the normal +# xdist harness and `python -m unittest` are unaffected. Guarded so unittest +# runs even when pytest is not installed. +try: + import pytest as _pytest + + @_pytest.fixture(scope="module", autouse=True) + def assign_npu(): # noqa: D401 - shadows conftest fixture of the same name + yield +except ImportError: + pass + +# ── builders ────────────────────────────────────────────────────────────────── + + +def enc_uleb(n): + out = bytearray() + while True: + b = n & 0x7F + n >>= 7 + out.append(b | 0x80 if n else b) + if not n: + return bytes(out) + + +def enc_sleb(n): + out = bytearray() + more = True + while more: + b = n & 0x7F + n >>= 7 + if (n == 0 and not b & 0x40) or (n == -1 and b & 0x40): + more = False + else: + b |= 0x80 + out.append(b) + return bytes(out) + + +def build_debug_line(rows, files=("user_test.py", "internal")): + """Build a DWARF-4 .debug_line section mimicking bishengir's encoding: + one set_address, then per row set_file/set_column/negate_stmt/advance_line/ + fixed_advance_pc/copy. `rows` is a list of dicts {line,file,col,is_stmt}.""" + SOL = bytes([0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1]) # std opcode arg counts 1..12 + pro = bytearray() + pro += bytes([1]) # minimum_instruction_length + pro += bytes([1]) # maximum_operations_per_instruction + pro += bytes([1]) # default_is_stmt + pro += struct.pack("b", -5) # line_base + pro += bytes([14]) # line_range + pro += bytes([13]) # opcode_base + pro += SOL + pro += bytes([0]) # directory table terminator (empty) + for name in files: + pro += name.encode() + b"\0" + enc_uleb(0) + enc_uleb(0) + enc_uleb(0) + pro += bytes([0]) # file table terminator + + prog = bytearray() + prog += bytes([0]) + enc_uleb(1 + 8) + bytes([DW_LNE_SET_ADDRESS]) + struct.pack(" line 2, async for -> line 3 + self.assertEqual(dlr._loop_header_lines(f.name), {2, 3}) + + def test_source_length(self): + self.assertEqual(dlr._source_length(self.tmp.name), 12) + + def test_user_file_indices(self): + base = os.path.basename(self.tmp.name) + files = ["", base, "internal", "/x/site-packages/triton/standard.py"] + self.assertEqual(dlr._user_file_indices(files, self.tmp.name), {1}) + + def test_auto_detect_source_prefers_user(self): + files = ["", "/opt/py/site-packages/triton/standard.py", self.tmp.name] + self.assertEqual(dlr._auto_detect_source(files), self.tmp.name) + + def test_auto_detect_source_none(self): + files = ["", "/opt/py/site-packages/triton/standard.py", "/nope/missing.py"] + self.assertIsNone(dlr._auto_detect_source(files)) + + +# ── demotion rules (pure, synthetic rows) ────────────────────────────────────── + + +class TestPlanDemotions(unittest.TestCase): + + def plan(self, rows, protected=frozenset(), user_files={1}, src_lines=None): + return dlr._plan_demotions([rows], protected, user_files, src_lines) + + def test_scatter_dedup(self): + rows = [make_row(8), make_row(8), make_row(8)] + demote, kept, counts = self.plan(rows) + self.assertEqual(len(demote), 2) + self.assertEqual(kept, [8]) + self.assertEqual(counts["dup"], 2) + + def test_loop_header_kept_every_occurrence(self): + rows = [make_row(10), make_row(11), make_row(10), make_row(10)] + demote, kept, counts = self.plan(rows, protected={10}) + self.assertEqual(demote, []) + self.assertEqual(kept, [10, 11, 10, 10]) + + def test_foreign_file_demoted(self): + rows = [make_row(5, file=2), make_row(7, file=1)] + demote, kept, counts = self.plan(rows, user_files={1}) + self.assertEqual(counts["foreign"], 1) + self.assertEqual(kept, [7]) + + def test_line_zero_demoted(self): + rows = [make_row(0, file=1), make_row(7)] + demote, kept, counts = self.plan(rows) + self.assertEqual(counts["line0"], 1) + self.assertEqual(kept, [7]) + + def test_over_source_length_demoted(self): + rows = [make_row(306), make_row(7)] + demote, kept, counts = self.plan(rows, src_lines=43) + self.assertEqual(counts["over"], 1) + self.assertEqual(kept, [7]) + + def test_over_disabled_without_src_lines(self): + rows = [make_row(306), make_row(7)] + demote, kept, counts = self.plan(rows, src_lines=None) + self.assertEqual(counts["over"], 0) + self.assertEqual(kept, [306, 7]) + + def test_is_stmt_zero_ignored(self): + rows = [make_row(7), make_row(0, file=2, is_stmt=False), make_row(8)] + demote, kept, counts = self.plan(rows) + self.assertEqual(kept, [7, 8]) + self.assertEqual(len(demote), 0) + + def test_end_sequence_ignored(self): + rows = [make_row(7), make_row(0, end=True)] + demote, kept, counts = self.plan(rows) + self.assertEqual(kept, [7]) + + def test_special_opcode_skipped_not_patched(self): + # a row that WOULD be demoted (over-len) but is emitted by a special + # opcode -> survives unpatched, counted as special_skip, NOT in demote. + rows = [make_row(7), make_row(306, kind="special")] + demote, kept, counts = self.plan(rows, src_lines=43) + self.assertEqual(len(demote), 0) + self.assertEqual(counts["special_skip"], 1) + self.assertIn(306, kept) + + def test_dedup_is_per_sequence(self): + # line 8 appears once in each of two sequences -> both kept (first per seq). + demote, kept, counts = dlr._plan_demotions([[make_row(8)], [make_row(8)]], frozenset(), {1}, None) + self.assertEqual(kept, [8, 8]) + self.assertEqual(len(demote), 0) + + +# ── simulate (hand-crafted program) ──────────────────────────────────────────── + + +class TestSimulate(unittest.TestCase): + + def test_rows_recovered(self): + rows = [ + dict(line=7, file=1, col=10, is_stmt=True), + dict(line=0, file=2, col=0, is_stmt=False), + dict(line=8, file=1, col=5, is_stmt=True), + ] + dl = build_debug_line(rows) + hdr = dlr._parse_header(dl, little=True) + self.assertEqual(hdr["version"], 4) + self.assertEqual(hdr["opcode_base"], 13) + self.assertEqual(hdr["file_names"], ["", "user_test.py", "internal"]) + seqs = dlr._simulate(dl, hdr) + self.assertEqual(len(seqs), 1) + emitted = [(r["line"], r["file"], r["is_stmt"]) for r in seqs[0] if not r["end_sequence"]] + self.assertEqual(emitted, [(7, 1, True), (0, 2, False), (8, 1, True)]) + # every emitted row carries an opcode offset and kind + for r in seqs[0]: + self.assertIn(r["emit_kind"], ("copy", "end")) + + +# ── end-to-end blob rewrite ───────────────────────────────────────────────────── + + +class TestRewriteBlob(unittest.TestCase): + + def setUp(self): + self.src = tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) + self.src.write(SRC_12_LINES_LOOP_AT_10) + self.src.close() + self.addCleanup(os.unlink, self.src.name) + base = os.path.basename(self.src.name) + + # scatter (8 x3), loop header (10 x2 protected), phantom (99 over-len), + # line 0 in user file, foreign file row (5 in file 2). + self.rows = [ + dict(line=7, file=1, col=10, is_stmt=True), + dict(line=0, file=2, col=0, is_stmt=False), + dict(line=8, file=1, col=5, is_stmt=True), + dict(line=8, file=1, col=7, is_stmt=True), + dict(line=10, file=1, col=3, is_stmt=True), + dict(line=11, file=1, col=3, is_stmt=True), + dict(line=10, file=1, col=3, is_stmt=True), + dict(line=8, file=1, col=9, is_stmt=True), + dict(line=99, file=1, col=0, is_stmt=True), + dict(line=0, file=1, col=0, is_stmt=True), + dict(line=5, file=2, col=0, is_stmt=True), + ] + self.blob = build_elf(build_debug_line(self.rows, files=(base, "internal"))) + + def test_plan_and_patch(self): + new, res = rewrite_debug_line_blob(self.blob, src_path=self.src.name) + self.assertTrue(res.changed) + self.assertEqual(res.before, [7, 8, 8, 10, 11, 10, 8, 99, 0, 5]) + self.assertEqual(res.after, [7, 8, 10, 11, 10]) + self.assertEqual(res.demoted, 5) + self.assertEqual(res.counts["dup"], 2) + self.assertEqual(res.counts["over"], 1) + self.assertEqual(res.counts["line0"], 1) + self.assertEqual(res.counts["foreign"], 1) + self.assertNotEqual(new, self.blob) + + def test_patched_bytes_reparse_to_kept(self): + new, res = rewrite_debug_line_blob(self.blob, src_path=self.src.name) + secs, little = dlr._read_sections(__import__("io").BytesIO(new)) + hdr = dlr._parse_header(secs[".debug_line"]["data"], little) + survivors = dlr._surviving_is_stmt_lines(secs[".debug_line"]["data"], hdr) + self.assertEqual(survivors, res.after) + + def test_only_copy_bytes_changed_to_basic_block(self): + new, _ = rewrite_debug_line_blob(self.blob, src_path=self.src.name) + diff = [i for i in range(len(self.blob)) if self.blob[i] != new[i]] + # every changed byte was Copy(0x01) and became set_basic_block(0x07) + for i in diff: + self.assertEqual(self.blob[i], DW_LNS_COPY) + self.assertEqual(new[i], DW_LNS_SET_BASIC_BLOCK) + self.assertEqual(len(diff), 5) + self.assertEqual(len(new), len(self.blob)) # length preserving + + def test_idempotent(self): + once, _ = rewrite_debug_line_blob(self.blob, src_path=self.src.name) + twice, res2 = rewrite_debug_line_blob(once, src_path=self.src.name) + self.assertFalse(res2.changed) + self.assertEqual(twice, once) + + def test_noop_when_clean(self): + base = os.path.basename(self.src.name) + rows = [dict(line=7, file=1, col=0, is_stmt=True), dict(line=8, file=1, col=0, is_stmt=True)] + blob = build_elf(build_debug_line(rows, files=(base, "internal"))) + new, res = rewrite_debug_line_blob(blob, src_path=self.src.name) + self.assertFalse(res.changed) + self.assertEqual(new, blob) + + def test_wrong_source_does_not_wipe_all_stops(self): + # source basename matches no file in the table -> foreign rule disabled, + # so a wrong --src can't demote every row (only the genuine dup goes). + rows = [ + dict(line=7, file=1, col=0, is_stmt=True), + dict(line=8, file=1, col=0, is_stmt=True), + dict(line=8, file=1, col=2, is_stmt=True) + ] + blob = build_elf(build_debug_line(rows, files=("user_test.py", "internal"))) + new, res = rewrite_debug_line_blob(blob, src_path="/nonexistent/other.py") + self.assertEqual(res.counts["foreign"], 0) + self.assertEqual(res.after, [7, 8]) + + +# ── pipeline entry (env gate, polymorphism, safety) ──────────────────────────── + + +class TestPipelineEntry(unittest.TestCase): + + def setUp(self): + self.src = tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) + self.src.write(SRC_12_LINES_LOOP_AT_10) + self.src.close() + self.addCleanup(os.unlink, self.src.name) + base = os.path.basename(self.src.name) + self.rows = [ + dict(line=7, file=1, col=10, is_stmt=True), + dict(line=8, file=1, col=5, is_stmt=True), + dict(line=8, file=1, col=7, is_stmt=True), # dup -> demote + ] + self.blob = build_elf(build_debug_line(self.rows, files=(base, "internal"))) + + def test_disabled_is_identity(self): + with mock.patch.dict(os.environ, {}, clear=False): + os.environ.pop(ENV_FLAG, None) + out = rewrite_debug_line(self.blob, metadata=None) + self.assertEqual(out, self.blob) + + def test_enabled_patches_bytes(self): + with mock.patch.dict(os.environ, {ENV_FLAG: "1"}): + # provide source via metadata so it doesn't depend on cwd + md = {"src_path": self.src.name} + out = rewrite_debug_line(self.blob, metadata=md) + self.assertNotEqual(out, self.blob) + self.assertIsInstance(out, (bytes, bytearray)) + + def test_garbage_blob_safe(self): + with mock.patch.dict(os.environ, {ENV_FLAG: "1"}): + junk = b"not an elf at all" * 8 + out = rewrite_debug_line(junk, metadata=None) + self.assertEqual(out, junk) # returned unchanged, no exception + + def test_path_artifact_patched_in_place(self): + f = tempfile.NamedTemporaryFile("wb", suffix=".npubin", delete=False) + f.write(self.blob) + f.close() + self.addCleanup(os.unlink, f.name) + with mock.patch.dict(os.environ, {ENV_FLAG: "1"}): + md = {"src_path": self.src.name} + ret = rewrite_debug_line(f.name, metadata=md) + self.assertEqual(ret, f.name) + with open(f.name, "rb") as h: + patched = h.read() + self.assertNotEqual(patched, self.blob) + self.assertEqual(len(patched), len(self.blob)) + + def test_env_truthy_variants(self): + for val, expect_change in [("1", True), ("true", True), ("TRUE", True), ("on", True), ("0", False), ("", False), + ("false", False)]: + with mock.patch.dict(os.environ, {ENV_FLAG: val}): + md = {"src_path": self.src.name} + out = rewrite_debug_line(self.blob, metadata=md) + if expect_change: + self.assertNotEqual(out, self.blob, f"val={val!r}") + else: + self.assertEqual(out, self.blob, f"val={val!r}") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/third_party/ascend/unittest/pytest_ut/test_debug_triton_opt_cmd.py b/third_party/ascend/unittest/pytest_ut/test_debug_triton_opt_cmd.py new file mode 100644 index 0000000000..71f1ab0de1 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_debug_triton_opt_cmd.py @@ -0,0 +1,158 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Test that ttir_to_linalg prints the triton-opt command line when opt.debug=True. +Does not require NPU hardware. +""" + +import os +from unittest.mock import patch, MagicMock +import pytest + +MOD = "triton.backends.ascend.compiler" + +FAKE_TOOL = "/fake/triton-opt" +FAKE_PIPELINE = "builtin.module(some-pass,another-pass)" + + +def _make_metadata(): + return { + "compile_on_910_95": False, + "auto_blockify_size": 1, + "enable_dynamic_cv_pipeline": False, + "hash": "deadbeef", + } + + +def _make_opt(debug): + opt = MagicMock() + opt.debug = debug + return opt + + +def _run_ttir_to_linalg(debug, capsys): + """ + Patch all MLIR machinery so ttir_to_linalg can run without hardware, + then return the captured stdout. + + The dump manager is mocked as a test double that mirrors the real + ``FileCacheManager(dump=True)``: its ``cache_dir`` and ``_make_path`` + are anchored at ``$TRITON_DUMP_DIR`` (or the default ``~/.triton/dump`` + when the env var is unset) joined with the base32-encoded hash. + """ + from triton.backends.ascend.compiler import ttir_to_linalg + from triton.runtime.cache import _base32 + + pm_mock = MagicMock() + pm_mock.get_pipeline_str.return_value = FAKE_PIPELINE + + ir_mock = MagicMock() + ir_mock.pass_manager.return_value = pm_mock + + # Mirror get_dump_manager(hash_key) -> FileCacheManager(_base32(key), dump=True): + # cache_dir = knobs.cache.dump_dir / _base32(key) + # where knobs.cache.dump_dir is $TRITON_DUMP_DIR or ~/.triton/dump. + dump_dir_root = os.environ.get("TRITON_DUMP_DIR") \ + or os.path.join(os.path.expanduser("~"), ".triton", "dump") + expected_cache_dir = os.path.join(dump_dir_root, _base32("deadbeef")) + + dump_manager_mock = MagicMock() + dump_manager_mock.cache_dir = expected_cache_dir + dump_manager_mock._make_path.side_effect = \ + lambda filename: os.path.join(expected_cache_dir, filename) + + with patch(f"{MOD}._get_triton_opt_path", return_value=FAKE_TOOL), \ + patch(f"{MOD}._get_triton_adapter_opt_path", return_value="/fake/triton-adapter-opt"), \ + patch(f"{MOD}._is_auto_map_parallel_blocks_enabled", return_value=False), \ + patch(f"{MOD}.ir", ir_mock), \ + patch(f"{MOD}.ascend", MagicMock()), \ + patch(f"{MOD}.passes", MagicMock()), \ + patch(f"{MOD}.get_dump_manager", return_value=dump_manager_mock): + ttir_to_linalg(MagicMock(), _make_metadata(), _make_opt(debug)) + + return capsys.readouterr().out + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_debug_print_contains_tool_path(capsys): + out = _run_ttir_to_linalg(debug=True, capsys=capsys) + assert FAKE_TOOL in out + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_debug_print_contains_pass_pipeline(capsys): + out = _run_ttir_to_linalg(debug=True, capsys=capsys) + assert f"--pass-pipeline={FAKE_PIPELINE}" in out + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_debug_print_contains_debug_info_flag(capsys): + out = _run_ttir_to_linalg(debug=True, capsys=capsys) + assert "--mlir-print-debuginfo" in out + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_debug_print_contains_output_flag(capsys): + out = _run_ttir_to_linalg(debug=True, capsys=capsys) + assert " -o " in out + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_debug_print_has_prefix(capsys): + out = _run_ttir_to_linalg(debug=True, capsys=capsys) + assert "[DEBUG] cmd list:" in out + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_no_debug_print_when_debug_is_false(capsys): + out = _run_ttir_to_linalg(debug=False, capsys=capsys) + assert "[DEBUG] cmd list:" not in out + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_debug_print_uses_dump_dir_when_set(capsys, monkeypatch): + """When TRITON_DUMP_DIR is set, debug output should show paths under that dir.""" + from triton.runtime.cache import _base32 + monkeypatch.setenv("TRITON_DUMP_DIR", "/my/dump/dir") + out = _run_ttir_to_linalg(debug=True, capsys=capsys) + # The actual hash is "deadbeef" which becomes base32 "32W353Y" + hash_dir = _base32("deadbeef") + # Should contain the dump dir path with base32-encoded hash, not /tmp + assert "/my/dump/dir" in out + assert "/tmp" not in out + # Should contain the base32-encoded hash-based subdirectory + assert hash_dir in out + assert "kernel.ttir.mlir" in out + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_debug_print_uses_default_dump_dir_when_unset(capsys, monkeypatch): + """When TRITON_DUMP_DIR is not set, debug output should show the default + dump dir (~/.triton/dump), not /tmp.""" + monkeypatch.delenv("TRITON_DUMP_DIR", raising=False) + out = _run_ttir_to_linalg(debug=True, capsys=capsys) + default_dump_dir = os.path.join(os.path.expanduser("~"), ".triton", "dump") + # Should contain the default dump dir, not /tmp + assert default_dump_dir in out + assert "/tmp" not in out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/third_party/ascend/unittest/pytest_ut/test_device_assert.py b/third_party/ascend/unittest/pytest_ut/test_device_assert.py new file mode 100644 index 0000000000..0cec9d74cd --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_device_assert.py @@ -0,0 +1,69 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir( + kernel, + src, + context, + Options(), + {"create_address_space": al.semantic.create_address_space}, + {}, + ) + return str(module) + + +@triton.jit +def kernel_device_assert_with_mask(x_ptr, XBLOCK: tl.constexpr): + offsets = tl.arange(0, XBLOCK) + x = tl.load(x_ptr + offsets) + cond = x > 0 + mask = offsets < XBLOCK + tl.device_assert(cond, msg="x must be positive", mask=mask) + + +def test_device_assert_with_mask(): + mlir = compile_kernel(kernel_device_assert_with_mask, {"x_ptr": "*fp32"}, {"XBLOCK": 16}) + assert "device_assert" in mlir or "hivm.hir.device_assert" in mlir, "device_assert with mask not found in MLIR" diff --git a/third_party/ascend/unittest/pytest_ut/test_discrete_mask_atomic.py b/third_party/ascend/unittest/pytest_ut/test_discrete_mask_atomic.py new file mode 100644 index 0000000000..c0527c7dc4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_discrete_mask_atomic.py @@ -0,0 +1,48 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import triton +import triton.language as tl +import torch_npu +import pytest + + +@triton.jit +def single_disc_mask_atomic_add_kernel( + in_ptr, + BLOCK_N: tl.constexpr, +): + col_offs = tl.arange(0, BLOCK_N) + disc_mask = (col_offs * 2) < BLOCK_N + ptr_in = in_ptr + col_offs + tl.atomic_add(ptr_in, 1, mask=disc_mask) + + +@pytest.mark.parametrize("BLOCK_N", [8]) +def test_single_discrete_mask_atomic_add(BLOCK_N): + in_tensor = torch.arange(BLOCK_N, dtype=torch.float16, device='npu') + expected = in_tensor.clone() + single_disc_mask_atomic_add_kernel[(1, )](in_tensor, BLOCK_N=BLOCK_N) + + half = BLOCK_N // 2 + expected[:half] += 1 + assert torch.allclose(in_tensor, expected), \ + f"Expected:\n{expected.cpu()}\nGot:\n{in_tensor.cpu()}" diff --git a/third_party/ascend/unittest/pytest_ut/test_discrete_mask_loadstore.py b/third_party/ascend/unittest/pytest_ut/test_discrete_mask_loadstore.py new file mode 100644 index 0000000000..cb03ff1bec --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_discrete_mask_loadstore.py @@ -0,0 +1,408 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Discrete mask access conversion test suite +# +# Test matrix (mask type x operation type): +# +# | mask type | load only | store only | load + store | +# |---------------------------------|-----------|------------|--------------| +# | single discrete mask | (A) | (B) | - | +# | single continuous mask | (C) | (D) | - | +# | continuous & discrete 2-way | (E) | (F) | (G) | +# | continuous & discrete 4-way | - | - | (H) | +# | broadcast(cont & disc) 2-D AND | (I) | - | (J) | +# +# ============================================================================= + +import torch +import triton +import triton.language as tl +import torch_npu +import pytest + + +# ============================================================================= +# (A) Single discrete mask -- load only +# ============================================================================= +@triton.jit +def single_disc_mask_load_kernel( + in_ptr, + out_ptr, + BLOCK_N: tl.constexpr, +): + col_offs = tl.arange(0, BLOCK_N) + disc_mask = (col_offs * 2) < BLOCK_N + ptr_in = in_ptr + col_offs + ptr_out = out_ptr + col_offs + data = tl.load(ptr_in, mask=disc_mask, other=0.0) + tl.store(ptr_out, data) + + +@pytest.mark.parametrize("BLOCK_N", [8]) +def test_single_discrete_mask_load(BLOCK_N): + in_tensor = torch.arange(BLOCK_N, dtype=torch.float16, device='npu') + out_tensor = torch.empty(BLOCK_N, dtype=torch.float16, device='npu') + + single_disc_mask_load_kernel[(1, )](in_tensor, out_tensor, BLOCK_N=BLOCK_N) + + half = BLOCK_N // 2 + expected = torch.zeros(BLOCK_N, dtype=torch.float16, device='npu') + expected[:half] = in_tensor[:half] + assert torch.allclose(out_tensor, expected), \ + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}" + + +# ============================================================================= +# (B) Single discrete mask -- store only +# ============================================================================= +@triton.jit +def single_disc_mask_store_kernel( + in_ptr, + out_ptr, + BLOCK_N: tl.constexpr, +): + col_offs = tl.arange(0, BLOCK_N) + disc_mask = (col_offs * 2) < BLOCK_N + ptr_in = in_ptr + col_offs + ptr_out = out_ptr + col_offs + data = tl.load(ptr_in) + tl.store(ptr_out, data, mask=disc_mask) + + +@pytest.mark.parametrize("BLOCK_N", [8]) +def test_single_discrete_mask_store(BLOCK_N): + in_tensor = torch.arange(BLOCK_N, dtype=torch.float16, device='npu') + out_tensor = torch.full((BLOCK_N, ), -1.0, dtype=torch.float16, device='npu') + + single_disc_mask_store_kernel[(1, )](in_tensor, out_tensor, BLOCK_N=BLOCK_N) + + half = BLOCK_N // 2 + expected = torch.full((BLOCK_N, ), -1.0, dtype=torch.float16, device='npu') + expected[:half] = in_tensor[:half] + assert torch.allclose(out_tensor, expected), \ + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}" + + +# ============================================================================= +# (C) Single continuous mask -- load only +# ============================================================================= +@triton.jit +def single_cont_mask_load_kernel( + in_ptr, + out_ptr, + M: tl.constexpr, + BLOCK_M: tl.constexpr, +): + row_offs = tl.arange(0, BLOCK_M) + cont_mask = row_offs < M # Continuous mask + ptr_in = in_ptr + row_offs + ptr_out = out_ptr + row_offs + data = tl.load(ptr_in, mask=cont_mask, other=0.0) + tl.store(ptr_out, data, mask=cont_mask) + + +@pytest.mark.parametrize("M,BLOCK_M", [(6, 8)]) +def test_single_continuous_mask_load(M, BLOCK_M): + in_tensor = torch.arange(BLOCK_M, dtype=torch.float16, device='npu') + out_tensor = torch.full((BLOCK_M, ), -1.0, dtype=torch.float16, device='npu') + + single_cont_mask_load_kernel[(1, )](in_tensor, out_tensor, M=M, BLOCK_M=BLOCK_M) + + expected = torch.full((BLOCK_M, ), -1.0, dtype=torch.float16, device='npu') + expected[:M] = in_tensor[:M] + assert torch.allclose(out_tensor, expected), \ + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}" + + +# ============================================================================= +# (D) Single continuous mask -- store only +# ============================================================================= +@triton.jit +def single_cont_mask_store_kernel( + in_ptr, + out_ptr, + M: tl.constexpr, + BLOCK_M: tl.constexpr, +): + row_offs = tl.arange(0, BLOCK_M) + cont_mask = row_offs < M + ptr_in = in_ptr + row_offs + ptr_out = out_ptr + row_offs + data = tl.load(ptr_in) + tl.store(ptr_out, data, mask=cont_mask) + + +@pytest.mark.parametrize("M,BLOCK_M", [(6, 8)]) +def test_single_continuous_mask_store(M, BLOCK_M): + in_tensor = torch.arange(BLOCK_M, dtype=torch.float16, device='npu') + out_tensor = torch.full((BLOCK_M, ), -1.0, dtype=torch.float16, device='npu') + single_cont_mask_store_kernel[(1, )](in_tensor, out_tensor, M=M, BLOCK_M=BLOCK_M) + expected = torch.full((BLOCK_M, ), -1.0, dtype=torch.float16, device='npu') + expected[:M] = in_tensor[:M] + assert torch.allclose(out_tensor, expected), \ + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}" + + +# ============================================================================= +# (E) Continuous & discrete 2-way AND -- load only +# ============================================================================= +@triton.jit +def cont_disc_combined_mask_load_kernel( + in_ptr, + out_ptr, + M: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + row_offs = tl.arange(0, BLOCK_M) + col_offs = tl.arange(0, BLOCK_N) + # Continuous mask + row_boundary = row_offs < M + # Discrete mask + col_stride = (col_offs * 2) < BLOCK_N + mask = row_boundary[:, None] & col_stride[None, :] + ptr_in = in_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + ptr_out = out_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + data = tl.load(ptr_in, mask=mask, other=0.0) + tl.store(ptr_out, data) + + +@pytest.mark.parametrize("M,BLOCK_M,BLOCK_N", [(6, 8, 8)]) +def test_cont_disc_combined_mask_load(M, BLOCK_M, BLOCK_N): + in_tensor = torch.ones((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + out_tensor = torch.empty((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + + cont_disc_combined_mask_load_kernel[(1, )](in_tensor, out_tensor, M=M, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N) + + half_n = BLOCK_N // 2 + expected = torch.zeros((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + expected[:M, :half_n] = 1.0 + assert torch.allclose(out_tensor, expected), \ + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}" + + +# ============================================================================= +# (F) Continuous & discrete 2-way AND -- store only +# ============================================================================= +@triton.jit +def cont_disc_combined_mask_store_kernel( + in_ptr, + out_ptr, + M: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + row_offs = tl.arange(0, BLOCK_M) + col_offs = tl.arange(0, BLOCK_N) + row_boundary = row_offs < M # continuous -> contLeaf + col_stride = (col_offs * 2) < BLOCK_N # discrete -> discLeaf + mask = row_boundary[:, None] & col_stride[None, :] + ptr_in = in_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + ptr_out = out_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + data = tl.load(ptr_in) + tl.store(ptr_out, data, mask=mask) + + +@pytest.mark.parametrize("M,BLOCK_M,BLOCK_N", [(6, 8, 8)]) +def test_cont_disc_combined_mask_store(M, BLOCK_M, BLOCK_N): + in_tensor = torch.ones((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + out_tensor = torch.full((BLOCK_M, BLOCK_N), -1.0, dtype=torch.float16, device='npu') + cont_disc_combined_mask_store_kernel[(1, )](in_tensor, out_tensor, M=M, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N) + half_n = BLOCK_N // 2 + expected = torch.full((BLOCK_M, BLOCK_N), -1.0, dtype=torch.float16, device='npu') + expected[:M, :half_n] = 1.0 + assert torch.allclose(out_tensor, expected), \ + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}" + + +# ============================================================================= +# (G) Continuous & discrete 2-way AND -- load + store (complex interleave, original) +# ============================================================================= +@triton.jit +def interleave_cont_disc_mask_kernel( + in_ptr, + out_ptr, + M: tl.constexpr, + N: tl.constexpr, +): + pid = tl.program_id(0) + col_offs = tl.arange(0, N) + even_col_offs = tl.arange(0, N // 2) * 2 + even_col_mask = even_col_offs < N # discrete: cmpi(muli(range,2), N) + row_offs = tl.arange(0, M) + row_mask = row_offs < M # continuous: cmpi(range_M, M) + in_even_ptr = in_ptr + row_offs[:, None] * N + even_col_offs[None, :] + in_odd_ptr = in_ptr + row_offs[:, None] * N + even_col_offs[None, :] + 1 + even_data = tl.load(in_even_ptr, mask=row_mask[:, None] & even_col_mask[None, :], other=0.0) + odd_data = tl.load(in_odd_ptr) + rotated_data = tl.interleave(-odd_data, even_data) + out_ptr = out_ptr + row_offs[:, None] * N + col_offs[None, :] + tl.store(out_ptr, rotated_data) + + +@pytest.mark.skip(reason="not supported after the NPUIR is updated in April, and will be fixed later") +@pytest.mark.parametrize("M", [4]) +@pytest.mark.parametrize("N", [8]) +def test_discrete_mask_load_store(M, N): + """Regression test: mask=row_mask & even_col_mask (continuous & discrete 2-way)""" + input_tensor = torch.arange(M * N, dtype=torch.float16, device='npu').reshape(M, N) + output_tensor = torch.empty_like(input_tensor) + interleave_cont_disc_mask_kernel[(1, )](input_tensor, output_tensor, M=M, N=N) + even_cols = input_tensor[:, 0::2] + odd_cols = input_tensor[:, 1::2] + ref_output = torch.empty_like(input_tensor) + ref_output[:, 0::2] = -odd_cols + ref_output[:, 1::2] = even_cols + assert torch.allclose(output_tensor.float(), ref_output.float()) + + +# ============================================================================= +# (H) Continuous & discrete 4-way AND -- load + store +# ============================================================================= +@triton.jit +def multi_cont_disc_mask_load_store_kernel( + in_ptr, + out_ptr, + M: tl.constexpr, + N: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + row_offs = tl.arange(0, BLOCK_M) + col_offs = tl.arange(0, BLOCK_N) + + row_boundary = row_offs < M # continuous mask + col_boundary = col_offs < N # continuous mask + row_stride = (row_offs * 2) < BLOCK_M # discrete mask + col_stride = (col_offs * 2) < BLOCK_N # discrete mask + + mask = (row_boundary[:, None] & col_boundary[None, :] & row_stride[:, None] & col_stride[None, :]) + + ptr_in = in_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + ptr_out = out_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + + data = tl.load(ptr_in, mask=mask, other=0.0) + result = data + 1.0 + tl.store(ptr_out, result, mask=mask) + + +@pytest.mark.parametrize("M,N,BLOCK_M,BLOCK_N", [ + (6, 6, 8, 8), +]) +def test_multi_cont_disc_mask_load_store(M, N, BLOCK_M, BLOCK_N): + in_tensor = torch.ones((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + out_tensor = torch.zeros((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + + multi_cont_disc_mask_load_store_kernel[(1, )](in_tensor, out_tensor, M=M, N=N, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N) + + half_m = BLOCK_M // 2 # = 4 + half_n = BLOCK_N // 2 # = 4 + expected = torch.zeros((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + expected[:half_m, :half_n] = 2.0 + + assert torch.allclose(out_tensor, expected), (f"BLOCK=({BLOCK_M},{BLOCK_N}), valid=({M},{N})\n" + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}") + + +# ============================================================================= +# (I) broadcast(continuous & discrete) 2-D AND -- load only +# ============================================================================= +@triton.jit +def broadcast_cont_disc_2d_load_kernel( + in_ptr, + out_ptr, + M: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + row_offs = tl.arange(0, BLOCK_M) + col_offs = tl.arange(0, BLOCK_N) + + row_boundary = row_offs < M + row_disc = (row_offs * 2) < BLOCK_M + mask = row_boundary[:, None] & row_disc[:, None] & (col_offs < BLOCK_N)[None, :] + + ptr_in = in_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + ptr_out = out_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + + data = tl.load(ptr_in, mask=mask, other=0.0) + tl.store(ptr_out, data) + + +@pytest.mark.parametrize("M,BLOCK_M,BLOCK_N", [(3, 4, 8)]) +def test_broadcast_cont_disc_2d_load(M, BLOCK_M, BLOCK_N): + in_tensor = torch.ones((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + out_tensor = torch.empty((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + + broadcast_cont_disc_2d_load_kernel[(1, )](in_tensor, out_tensor, M=M, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N) + + disc_true_rows = BLOCK_M // 2 + both_true_rows = min(M, disc_true_rows) + + expected = torch.zeros((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + expected[:both_true_rows, :] = 1.0 + + assert torch.allclose(out_tensor, expected), (f"M={M}, BLOCK_M={BLOCK_M}, BLOCK_N={BLOCK_N}\n" + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}") + + +# ============================================================================= +# (J) broadcast(continuous & discrete) 2-D AND -- load + store +# ============================================================================= +@triton.jit +def broadcast_cont_disc_2d_load_store_kernel( + in_ptr, + out_ptr, + M: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + row_offs = tl.arange(0, BLOCK_M) + col_offs = tl.arange(0, BLOCK_N) + + row_boundary = row_offs < M + row_disc = (row_offs * 2) < BLOCK_M + + combined = row_boundary[:, None] & row_disc[:, None] & (col_offs < BLOCK_N)[None, :] + + ptr_in = in_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + ptr_out = out_ptr + row_offs[:, None] * BLOCK_N + col_offs[None, :] + + data = tl.load(ptr_in, mask=combined, other=0.0) + tl.store(ptr_out, data, mask=combined) + + +@pytest.mark.parametrize("M,BLOCK_M,BLOCK_N", [(3, 4, 8)]) +def test_broadcast_cont_disc_2d_load_store(M, BLOCK_M, BLOCK_N): + in_tensor = torch.ones((BLOCK_M, BLOCK_N), dtype=torch.float16, device='npu') + out_tensor = torch.full((BLOCK_M, BLOCK_N), -1.0, dtype=torch.float16, device='npu') + + broadcast_cont_disc_2d_load_store_kernel[(1, )](in_tensor, out_tensor, M=M, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N) + + disc_true_rows = BLOCK_M // 2 + both_true_rows = min(M, disc_true_rows) + + expected = torch.full((BLOCK_M, BLOCK_N), -1.0, dtype=torch.float16, device='npu') + expected[:both_true_rows, :] = 1.0 + + assert torch.allclose(out_tensor, expected), (f"M={M}, BLOCK_M={BLOCK_M}, BLOCK_N={BLOCK_N}\n" + f"Expected:\n{expected.cpu()}\nGot:\n{out_tensor.cpu()}") diff --git a/third_party/ascend/unittest/pytest_ut/test_discrete_mask_tail_block_mte_oob.py b/third_party/ascend/unittest/pytest_ut/test_discrete_mask_tail_block_mte_oob.py new file mode 100644 index 0000000000..d8129d2677 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_discrete_mask_tail_block_mte_oob.py @@ -0,0 +1,238 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# MTE (Memory Tag Extension) OOB regression test for DiscreteMaskAccessConversionPass +# +# This file verifies that DiscreteMaskAccessConversionPass correctly bounds +# global-memory accesses when the load/store mask is a combined discrete mask +# +# Test strategy +# ------------- +# The test engineers this condition in four steps: +# Step 1 — probe: Trigger a fresh 2 MB NPU segment; measure its size. +# Step 2 — pre_fill: Fill the segment with small tensors until the remaining +# free space is in [IN_BYTES, TARGET_FREE]. +# Step 3 — in_tensor: Allocate the test tensor; it lands at the segment tail +# with only ~7680 bytes gap to the boundary. +# Step 4 — kernel: Run the kernel + synchronize. Before the fix the +# OOB read (24576 bytes) crosses the boundary → MTE. +# After the fix the copy is bounded to IN_BYTES → no MTE. +# +# Memory layout at the time of the kernel call (before fix): +# +# ┌──────────────────────────── 2 MB segment ────────────────────────────────┐ +# │ probe(512 B) │←────── pre_fill (~2025 KB) ──────→│ in_tensor(8192 B) │gap│ +# └──────────────────────────────────────────────────────────────────────────┘ +# ↑ segment end +# ├──── OOB_BYTES (24576 B) ─────→ +# crosses boundary → MTE ✓ +# +# ============================================================================= + +import math +import torch +import triton +import triton.language as tl +import torch_npu +import pytest + + +@triton.jit +def cont_disc_oob_inplace_2d_kernel( + ptr, + M, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + pid_m = tl.program_id(0) + row_offs = tl.arange(0, BLOCK_M) + col_offs = tl.arange(0, BLOCK_N) + + # Continuous bound (contMask): marks the M valid rows in this tile. + row_boundary = row_offs < (M - pid_m * BLOCK_M) + row_disc = (row_offs * 2) < BLOCK_M + combined = row_boundary[:, None] & row_disc[:, None] & (col_offs < BLOCK_N)[None, :] + + row_start = pid_m * BLOCK_M + ptr_2d = ptr + (row_start + row_offs[:, None]) * BLOCK_N + col_offs[None, :] + + # load triggers DiscreteMaskAccessConversionPass. + # Before fix: copy size = BLOCK_M × BLOCK_N × 2 bytes = 32768 bytes (OOB). + # After fix: copy size = M × BLOCK_N × 2 bytes = 8192 bytes (safe). + data = tl.load(ptr_2d, mask=combined, other=0.0) + tl.store(ptr_2d, data, mask=row_boundary[:, None]) + + +# ============================================================================= +# Memory setup helper +# ============================================================================= +def _fill_segment_to_boundary(dtype, device, in_bytes, target_free, chunk_max_bytes): + """Allocate a fresh NPU segment and fill it so that only ~target_free bytes remain. + + Returns + ------- + pre_fillers : list of torch.Tensor + All tensors allocated (probe + fill chunks). The caller is responsible + for deleting them in `finally`. + pool_free_after_fill : int + Segment free space after filling, in bytes. + seg_size : int + Total size of the triggered segment, in bytes. + """ + elem_size = torch.finfo(dtype).bits // 8 + + # --- Step 1: probe — trigger a fresh 2 MB small-alloc segment ---------- + pool0 = torch.npu.memory_reserved(0) + alloc0 = torch.npu.memory_allocated(0) + + probe = torch.empty(1, dtype=dtype, device=device) + + pool1 = torch.npu.memory_reserved(0) + alloc1 = torch.npu.memory_allocated(0) + + seg_size = pool1 - pool0 # should be 2 MB = 2097152 bytes + probe_actual = alloc1 - alloc0 # NPU 512-byte aligned → 512 bytes + + print(f"\n[mte] Step 1: probe") + print(f"[mte] segment_size = {seg_size} bytes ({seg_size // 1024} KB)") + print(f"[mte] probe_actual = {probe_actual} bytes") + print(f"[mte] pool_free = {seg_size - probe_actual} bytes") + + # --- Step 2: pre_fill — leave only [in_bytes, target_free] bytes free --- + # Chunks are kept ≤ chunk_max_bytes to stay in the small-alloc pool and + # avoid opening a new segment via the large-alloc path. + pre_fillers = [probe] + + for chunk in [ + chunk_max_bytes, chunk_max_bytes // 2, chunk_max_bytes // 4, chunk_max_bytes // 8, 32 * 1024, 16 * 1024, + 8 * 1024, 4 * 1024, 2 * 1024, 1024, 512 + ]: + while True: + free = torch.npu.memory_reserved(0) - torch.npu.memory_allocated(0) + if free <= in_bytes: + break # not enough room even for in_tensor; stop + if free <= target_free: + break # already in target range; try smaller chunk + if free <= target_free + chunk: + break # this chunk would overshoot; try smaller chunk + try: + t = torch.empty(chunk // elem_size, dtype=dtype, device=device) + pre_fillers.append(t) + except RuntimeError: + break # segment exhausted; try smaller chunk + + pool_free_after_fill = torch.npu.memory_reserved(0) - torch.npu.memory_allocated(0) + pre_bytes = sum(t.numel() * elem_size for t in pre_fillers) + print(f"\n[mte] Step 2: pre_fill") + print(f"[mte] tensors = {len(pre_fillers)}, total = {pre_bytes} bytes ({pre_bytes // 1024} KB)") + print(f"[mte] pool_free = {pool_free_after_fill} bytes (target [{in_bytes}, {target_free}] bytes)") + + return pre_fillers, pool_free_after_fill, seg_size + + +# ============================================================================= +# Test: MTE OOB via segment-boundary placement +# ============================================================================= +@pytest.mark.parametrize("BLOCK_M,BLOCK_N,M", [ + (4, 4096, 1), +]) +def test_mte_segment_boundary_oob(BLOCK_M, BLOCK_N, M): + """Regression: combined discrete mask load causes OOB on tail blocks. + + Verifies that DiscreteMaskAccessConversionPass correctly bounds + the memory copy to M rows (the contiguous range), not BLOCK_M rows (the full tile). + + Test outcome: + - Before fix: RuntimeError (MTE OOB) — the test would fail. + - After fix: no exception — the test passes. + """ + dtype = torch.float16 + device = 'npu' + elem_size = 2 # float16 + + in_bytes = M * BLOCK_N * elem_size # 8192 bytes + oob_bytes = (BLOCK_M - M) * BLOCK_N * elem_size # 24576 bytes + # TARGET_FREE: midpoint between in_bytes and oob_bytes. + # Ensures in_tensor fits AND gap < oob_bytes so OOB crosses segment boundary. + target_free = (in_bytes + oob_bytes) // 2 # 16384 bytes + chunk_max_bytes = 512 * 1024 # 512 KB + + print(f"\n[mte] BLOCK_M={BLOCK_M} BLOCK_N={BLOCK_N} M={M}") + print(f"[mte] in_bytes = {in_bytes} bytes (in_tensor: {M}×{BLOCK_N}×{elem_size})") + print(f"[mte] oob_bytes = {oob_bytes} bytes (unfixed copy: {BLOCK_M}×{BLOCK_N}×{elem_size} - in_bytes)") + print(f"[mte] target_free = {target_free} bytes (must satisfy in_bytes < target_free < oob_bytes)") + + torch.npu.empty_cache() + + pre_fillers = [] + in_tensor = None + + try: + pre_fillers, pool_free_after_fill, _ = _fill_segment_to_boundary(dtype, device, in_bytes, target_free, + chunk_max_bytes) + except Exception as exc: + torch.npu.empty_cache() + pytest.skip(f"Memory layout setup failed (allocator behaviour may differ): {exc}") + + # Verify pre_fill achieved the required free-space window. + if not (in_bytes <= pool_free_after_fill <= target_free): + for t in reversed(pre_fillers): + del t + torch.npu.empty_cache() + pytest.skip(f"pre_fill did not reach target range [{in_bytes}, {target_free}] bytes; " + f"got {pool_free_after_fill} bytes. " + f"Skipping MTE check (NPU allocator behaviour may differ).") + + try: + # Step 3: allocate in_tensor — lands at the very end of the segment. + # NPU 512-byte alignment means the allocator consumes + # in_bytes + 512 = 8704 bytes, leaving gap ≈ target_free - 8704 = 7680 bytes. + in_tensor = torch.ones(M * BLOCK_N, dtype=dtype, device=device).view(M, BLOCK_N) + + gap = torch.npu.memory_reserved(0) - torch.npu.memory_allocated(0) + print(f"\n[mte] Step 3: in_tensor") + print(f"[mte] address = [{in_tensor.data_ptr():#x}, {in_tensor.data_ptr() + in_bytes:#x})") + print(f"[mte] gap = {gap} bytes (in_tensor end → segment end)") + + if oob_bytes <= gap: + pytest.skip(f"gap ({gap} bytes) >= oob_bytes ({oob_bytes} bytes): " + f"OOB would not cross the segment boundary. " + f"Skipping MTE check.") + print(f"[mte] oob_bytes({oob_bytes} B) > gap({gap} B) → MTE expected if unfixed ✓") + + # Step 4: run kernel + num_pids_m = math.ceil(M / BLOCK_M) + print(f"\n[mte] Step 4: kernel (grid=({num_pids_m},))") + cont_disc_oob_inplace_2d_kernel[(num_pids_m, )](in_tensor, M=M, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N) + torch.npu.synchronize() + print("[mte] PASSED: fix is effective, no OOB.") + + except RuntimeError as exc: + pytest.fail(f"MTE OOB triggered — DiscreteMaskAccessConversionPass fix " + f"may not be applied or is incomplete.\nError: {exc}") + + finally: + if in_tensor is not None: + del in_tensor + for t in reversed(pre_fillers): + del t + torch.npu.empty_cache() + print("[mte] Memory released.") diff --git a/third_party/ascend/unittest/pytest_ut/test_discrete_overlap_mask.py b/third_party/ascend/unittest/pytest_ut/test_discrete_overlap_mask.py new file mode 100644 index 0000000000..93890faa21 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_discrete_overlap_mask.py @@ -0,0 +1,234 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + +# --------------------------------------------------------------------------- +# Fixed Constants +# --------------------------------------------------------------------------- +_M_ROWS = 16 # Rows per program +_OFFS = 8 # Write window offset for two programs +_HALF = 12 # Mask threshold +_NUM_C = 24 # Rows of matrix C (= OFFS + M_ROWS, ensure pid=1 write window not out of bounds) + +assert _OFFS < _HALF < _M_ROWS, "OFFS < HALF < M_ROWS to ensure True/False on both sides" +assert _NUM_C >= _OFFS + _M_ROWS, "NUM_C must accommodate upper bound of pid=1 write window" + + +# --------------------------------------------------------------------------- +# Triton Kernel +# --------------------------------------------------------------------------- +@triton.jit +def _copy_matrix_kernel( + A_ptr, + idx_ptr, + C_ptr, + idx_stride, + A_row_stride, + A_col_stride, + C_row_stride, + C_col_stride, + BLOCK_N: tl.constexpr, + HALF: tl.constexpr, +): + """ + Discrete memory access + overlapping write window + runtime mask. + + pid=0 write window: rows [0, 15], mask=True when idx < HALF + pid=1 write window: rows [8, 23], mask=True when idx >= HALF + Overlap region : rows [8, 15] -> triggers load-select-store RMW + """ + program_id = tl.program_id(axis=0).to(tl.int64) + N_id = tl.program_id(axis=1).to(tl.int64) + + OFFS: tl.constexpr = 8 + M_ROWS: tl.constexpr = 16 + + N_BLOCK = N_id * BLOCK_N + tl.arange(0, BLOCK_N) # shape: (BLOCK_N,) + M_BLOCK = tl.arange(0, M_ROWS) # shape: (M_ROWS,) + + # Discrete row indices (loaded at runtime -> mask cannot be statically analyzed) + idx = tl.load(idx_ptr + program_id * idx_stride + M_BLOCK) + + # Runtime mask (generates scf.if -> compiler converts to load-select-store) + if program_id == 0: + mask = idx < HALF + else: + mask = idx >= HALF + + val = tl.load( + A_ptr + idx[:, None] * A_row_stride + tl.arange(0, BLOCK_N)[None, :] * A_col_stride, + mask=mask[:, None], + ) + + # Write to C (mask=False rows rely on load-select-store to preserve original values) + tl.store( + C_ptr + (OFFS * program_id + M_BLOCK[:, None]) * C_row_stride + N_BLOCK[None, :] * C_col_stride, + val, + mask=mask[:, None], + ) + # C (24 × N) Program 0 Program 1 + # Row 0~7 ──────────── write value (mask=True) ── Not involved + # Row 8~11 ──────────── write value (mask=True) ── Not written (mask=False, overwritten by P0 to 0) + # Row 12~15 ──────────── Not written (mask=False) ── write value (mask=True) + # Row 16~23 ──────────── Not involved ── write value (mask=True) + + +# --------------------------------------------------------------------------- +# Helper: Construct discrete index vector +# --------------------------------------------------------------------------- +def _make_idx(device: str) -> torch.Tensor: + """ + Construct 2x16 index matrix that meets mask distribution requirements. + + pid=0 row (idx0): + First HALF=12 values ∈ [0, HALF) -> mask=True + Last 4 values ∈ [HALF, M_ROWS) -> mask=False + pid=1 row (idx1): + First 4 values ∈ [OFFS, OFFS+4) -> mask=False + Last HALF=12 values ∈ [HALF, HALF*2) -> mask=True + """ + + def shuffle_quads(lst: list) -> list: + """Reverse each group of 4 elements (ignore if less than 4).""" + out = lst[:] + for i in range(0, len(out) - 3, 4): + out[i], out[i + 1], out[i + 2], out[i + 3] = \ + out[i + 3], out[i + 2], out[i + 1], out[i] + return out + + num_false = _M_ROWS - _HALF # = 4 + + seg0_true = shuffle_quads(list(range(0, _HALF))) # 12 values, < 12 + seg0_false = shuffle_quads(list(range(_HALF, _HALF + num_false))) # 4 values, >= 12 + idx0 = seg0_true + seg0_false # Total length 16 + + seg1_false = shuffle_quads(list(range(_OFFS, _OFFS + num_false))) # 4 values, < 12 + seg1_true = shuffle_quads(list(range(_HALF, _HALF + _HALF))) # 12 values, >= 12 + idx1 = seg1_false + seg1_true # Total length 16 + + assert len(idx0) == _M_ROWS, f"idx0 length error: {len(idx0)}" + assert len(idx1) == _M_ROWS, f"idx1 length error: {len(idx1)}" + assert all(v < _HALF for v in seg0_true), "pid=0 True segment should all be < HALF" + assert all(v >= _HALF for v in seg0_false), "pid=0 False segment should all be >= HALF" + assert all(v < _HALF for v in seg1_false), "pid=1 False segment should all be < HALF" + assert all(v >= _HALF for v in seg1_true), "pid=1 True segment should all be >= HALF" + + return torch.tensor([idx0, idx1], dtype=torch.int32, device=device) + + +# --------------------------------------------------------------------------- +# Dtype Mapping +# --------------------------------------------------------------------------- +_DTYPE_MAP = { + 'int32': torch.int32, + 'float32': torch.float32, + 'float16': torch.float16, + 'int16': torch.int16, +} + + +# --------------------------------------------------------------------------- +# Single Execution + Verification +# --------------------------------------------------------------------------- +def _run_once(BLOCK_N: int, dtype_str: str) -> None: + """ + Execute kernel once and verify results. + + Expectations: + C[0:HALF, :] all 0 -- pid=0 writes rows [0,HALF) of A (all 0) + C[HALF:NUM_C, :] all 1 -- pid=1 writes rows [HALF, NUM_C) of A (all 1) + """ + dev = 'npu' + td = _DTYPE_MAP[dtype_str] + zero_val = 0.0 if dtype_str.startswith('float') else 0 + one_val = 1.0 if dtype_str.startswith('float') else 1 + + # A: First HALF rows all 0, last HALF rows all 1 + A = torch.zeros((_NUM_C, BLOCK_N), dtype=td, device=dev) + A[_HALF:, :] = one_val + + idx = _make_idx(dev) + + # C: Fill all with 2 + C = torch.full((_NUM_C, BLOCK_N), 2, dtype=td, device=dev) + + grid = (2, 1) + _copy_matrix_kernel[grid]( + A_ptr=A, + idx_ptr=idx, + C_ptr=C, + idx_stride=idx.stride(0), + A_row_stride=A.stride(0), + A_col_stride=A.stride(1), + C_row_stride=C.stride(0), + C_col_stride=C.stride(1), + BLOCK_N=BLOCK_N, + HALF=_HALF, + ) + + # Verification + assert torch.all(C[:_HALF] == zero_val), ( + f"[dtype={dtype_str}, BLOCK_N={BLOCK_N}] " + f"C[:HALF] should all be {zero_val}, actual unique values: {C[:_HALF].unique().tolist()}") + assert torch.all(C[_HALF:] == one_val), ( + f"[dtype={dtype_str}, BLOCK_N={BLOCK_N}] " + f"C[HALF:] should all be {one_val}, actual unique values: {C[_HALF:].unique().tolist()}") + + +@pytest.mark.parametrize("param_list", [ + # --- int32 --- + (16, 'int32'), + (32, 'int32'), + (64, 'int32'), + # --- float32 --- + (16, 'float32'), + (32, 'float32'), + (64, 'float32'), +]) +def test_discrete_overlap_mask(param_list): + """ + Verify no precision issues in discrete access + overlapping write window + runtime mask scenario. + + Race condition errors are probabilistic. Each parameter combination is executed 10 times + to fully cover concurrent timing scenarios. + If sync_block_lock fix is effective, all 10 runs pass; if race condition exists, assertion failure + occurs with high probability. + """ + BLOCK_N, dtype_str = param_list + for _ in range(10): + _run_once(BLOCK_N, dtype_str) + + +if __name__ == "__main__": + configs = [ + (32, 'int32'), + (32, 'float32'), + ] + for BLOCK_N, dtype_str in configs: + print(f"Testing BLOCK_N={BLOCK_N}, dtype={dtype_str} ...", end=" ", flush=True) + for _ in range(10): + _run_once(BLOCK_N, dtype_str) + print("PASS (10 rounds)") + print("All tests passed.") diff --git a/third_party/ascend/unittest/pytest_ut/test_div.py b/third_party/ascend/unittest/pytest_ut/test_div.py new file mode 100644 index 0000000000..a145b32fbf --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_div.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 / x1 + return res + + +@triton.jit +def triton_div(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 / tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + if dtype == 'int8': + dtype = 'float32' + x1 = x1.masked_fill(x1 == 0, 1) + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_div[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_dot.py b/third_party/ascend/unittest/pytest_ut/test_dot.py new file mode 100644 index 0000000000..5f9beeb3ba --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_dot.py @@ -0,0 +1,278 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import test_common +from unittest.mock import MagicMock +from triton.language.semantic import TritonSemantic + + +@pytest.fixture(scope="function") +def restore_npu_hf32_setting(): + original_allow_hf32 = torch_npu.npu.matmul.allow_hf32 + try: + torch_npu.npu.matmul.allow_hf32 = True + yield + finally: + torch_npu.npu.matmul.allow_hf32 = original_allow_hf32 + + +def torch_dot_None(x0, x1): + res = torch.matmul(x0, x1) + return res + + +@triton.jit +def triton_dot_2_None(output_ptr, x_ptr, y_ptr, B: tl.constexpr, C: tl.constexpr, D: tl.constexpr): + bidx = tl.arange(0, B) + cidx = tl.arange(0, C) + didx = tl.arange(0, D) + + x_mask = (bidx[:, None] < B) & (cidx[None, :] < C) + y_mask = (cidx[:, None] < C) & (didx[None, :] < D) + out_mask = (bidx[:, None] < B) & (didx[None, :] < D) + Xidx = bidx[:, None] * C + cidx[None, :] + Yidx = cidx[:, None] * D + didx[None, :] + X = tl.load(x_ptr + Xidx, mask=x_mask, other=0.0) + Y = tl.load(y_ptr + Yidx, mask=y_mask, other=0.0) + ret = tl.dot(X, Y, input_precision="hf32") + oidx = bidx[:, None] * D + didx[None, :] + tl.store(output_ptr + oidx, ret, mask=out_mask) + + +@triton.jit +def triton_dot_2_allow_tf32(output_ptr, x_ptr, y_ptr, B: tl.constexpr, C: tl.constexpr, D: tl.constexpr): + bidx = tl.arange(0, B) + cidx = tl.arange(0, C) + didx = tl.arange(0, D) + + x_mask = (bidx[:, None] < B) & (cidx[None, :] < C) + y_mask = (cidx[:, None] < C) & (didx[None, :] < D) + out_mask = (bidx[:, None] < B) & (didx[None, :] < D) + Xidx = bidx[:, None] * C + cidx[None, :] + Yidx = cidx[:, None] * D + didx[None, :] + X = tl.load(x_ptr + Xidx, mask=x_mask, other=0.0) + Y = tl.load(y_ptr + Yidx, mask=y_mask, other=0.0) + # when allow_tf32=True; input_precision=tf32 + # After removing invasive modifications,input_precision needs to be passed. + ret = tl.dot(X, Y, input_precision="tf32") + oidx = bidx[:, None] * D + didx[None, :] + tl.store(output_ptr + oidx, ret, mask=out_mask) + + +@triton.jit +def triton_dot_2_input_tf32(output_ptr, x_ptr, y_ptr, B: tl.constexpr, C: tl.constexpr, D: tl.constexpr): + bidx = tl.arange(0, B) + cidx = tl.arange(0, C) + didx = tl.arange(0, D) + + x_mask = (bidx[:, None] < B) & (cidx[None, :] < C) + y_mask = (cidx[:, None] < C) & (didx[None, :] < D) + out_mask = (bidx[:, None] < B) & (didx[None, :] < D) + Xidx = bidx[:, None] * C + cidx[None, :] + Yidx = cidx[:, None] * D + didx[None, :] + X = tl.load(x_ptr + Xidx, mask=x_mask, other=0.0) + Y = tl.load(y_ptr + Yidx, mask=y_mask, other=0.0) + ret = tl.dot(X, Y, input_precision="tf32") + oidx = bidx[:, None] * D + didx[None, :] + tl.store(output_ptr + oidx, ret, mask=out_mask) + + +@triton.jit +def triton_dot_2_ignore_tf32(output_ptr, x_ptr, y_ptr, B: tl.constexpr, C: tl.constexpr, D: tl.constexpr): + bidx = tl.arange(0, B) + cidx = tl.arange(0, C) + didx = tl.arange(0, D) + + x_mask = (bidx[:, None] < B) & (cidx[None, :] < C) + y_mask = (cidx[:, None] < C) & (didx[None, :] < D) + out_mask = (bidx[:, None] < B) & (didx[None, :] < D) + Xidx = bidx[:, None] * C + cidx[None, :] + Yidx = cidx[:, None] * D + didx[None, :] + X = tl.load(x_ptr + Xidx, mask=x_mask, other=0.0) + Y = tl.load(y_ptr + Yidx, mask=y_mask, other=0.0) + ret = tl.dot(X, Y, input_precision="hf32") + oidx = bidx[:, None] * D + didx[None, :] + tl.store(output_ptr + oidx, ret, mask=out_mask) + + +testlist1 = [ + (10, 13, 35, 39), +] + +testlist2 = [(16, 32, 16)] + +typelist = [ + 'float32', +] + + +@pytest.mark.parametrize("B, C, D", testlist2) +@pytest.mark.parametrize("sigtype", typelist) +def test_dot_2(restore_npu_hf32_setting, sigtype, B, C, D): + x = test_common.generate_tensor((B, C), sigtype).npu() + y = test_common.generate_tensor((C, D), sigtype).npu() + z_ref = torch_dot_None(x, y).to(torch.float32) + z = torch.zeros((B, D), dtype=torch.float32).npu() + triton_dot_2_None[1, 1, 1](z, x, y, B, C, D) + torch.testing.assert_close(z, z_ref, rtol=3e-02, atol=3e-02, equal_nan=True) + + +@pytest.mark.parametrize("B, C, D", testlist2) +@pytest.mark.parametrize("sigtype", typelist) +def test_dot_2_allow_tf32(restore_npu_hf32_setting, sigtype, B, C, D): + x = test_common.generate_tensor((B, C), sigtype).npu() + y = test_common.generate_tensor((C, D), sigtype).npu() + z_ref = torch_dot_None(x, y).to(torch.float32) + z = torch.zeros((B, D), dtype=torch.float32).npu() + triton_dot_2_allow_tf32[1, 1, 1](z, x, y, B, C, D) + torch.testing.assert_close(z, z_ref, rtol=3e-02, atol=3e-02, equal_nan=True) + + +@pytest.mark.parametrize("B, C, D", testlist2) +@pytest.mark.parametrize("sigtype", typelist) +def test_dot_2_input_tf32(restore_npu_hf32_setting, sigtype, B, C, D): + x = test_common.generate_tensor((B, C), sigtype).npu() + y = test_common.generate_tensor((C, D), sigtype).npu() + z_ref = torch_dot_None(x, y).to(torch.float32) + z = torch.zeros((B, D), dtype=torch.float32).npu() + triton_dot_2_input_tf32[1, 1, 1](z, x, y, B, C, D) + torch.testing.assert_close(z, z_ref, rtol=3e-02, atol=3e-02, equal_nan=True) + + +@pytest.mark.parametrize("B, C, D", testlist2) +@pytest.mark.parametrize("sigtype", typelist) +def test_dot_2_ignore_tf32(sigtype, B, C, D): + input_type = "bfloat16" + x = test_common.generate_tensor((B, C), input_type).npu() + y = test_common.generate_tensor((C, D), input_type).npu() + z = torch.zeros((B, D), dtype=torch.float32).npu() + + original_allow_hf32 = torch_npu.npu.matmul.allow_hf32 + try: + torch_npu.npu.matmul.allow_hf32 = False + z_ref = torch_dot_None(x.to(torch.float32), y.to(torch.float32)).to(torch.float32) + + finally: + torch_npu.npu.matmul.allow_hf32 = original_allow_hf32 + + triton_dot_2_ignore_tf32[1, 1, 1](z, x, y, B, C, D) + torch.testing.assert_close(z, z_ref, rtol=3e-02, atol=3e-02, equal_nan=True) + + +# ============================================================================= +# Unit tests for the dot monkey-patch (third_party/ascend/backend/__init__.py) +# ============================================================================= + + +def _make_mock_builder(): + builder = MagicMock() + builder.options.default_dot_input_precision = "ieee" + builder.options.allowed_dot_input_precisions = ("ieee", "hf32") + builder.options.max_num_imprecise_acc_default = 0 + builder.codegen_fns = {"min_dot_size": MagicMock(return_value=(1, 1, 1))} + builder.create_dot = MagicMock(return_value=MagicMock()) + builder.create_splat = MagicMock(return_value=MagicMock()) + builder.get_fp32 = MagicMock(return_value=MagicMock()) + builder.get_fp16 = MagicMock(return_value=MagicMock()) + builder.get_fp64 = MagicMock(return_value=MagicMock()) + builder.get_int32 = MagicMock(return_value=MagicMock()) + return builder + + +def _mock_tensor(dtype): + t = MagicMock() + t.dtype = dtype + t.handle = MagicMock() + dim = MagicMock() + dim.value = 64 + t.shape = [dim, dim] + type_mock = MagicMock() + type_mock.is_block = MagicMock(return_value=True) + type_mock.scalar = dtype + type_mock.shape = [64, 64] + t.type = type_mock + return t + + +class TestDotAscendPatch: + """Verify the monkey-patched TritonSemantic.dot is behaviourally + equivalent to the original intrusive code.""" + + @pytest.fixture(autouse=True) + def _setup(self): + if getattr(TritonSemantic, "_ascend_dot_patch_applied", False): + return + try: + from triton.backends.ascend import _apply_ascend_patch as _apply + except ImportError: + pytest.skip("Ascend backend unavailable") + _apply() + + @pytest.fixture + def semantic(self): + semantic = MagicMock(spec=TritonSemantic) + semantic.builder = _make_mock_builder() + semantic._str_to_dot_input_precision = (lambda ip: TritonSemantic._str_to_dot_input_precision(semantic, ip)) + semantic.tensor = MagicMock(return_value=MagicMock()) + semantic.cast = MagicMock(side_effect=lambda x, dtype: x) + semantic.bitcast = MagicMock(side_effect=lambda x, dtype: x) + return semantic + + # ---- HF32 guard ---- + + def test_hf32_fp32_inputs_keep_hf32(self, semantic): + """fp32 x fp32 + hf32 → hf32 is preserved.""" + patched = TritonSemantic.dot + mock_original = MagicMock(return_value=MagicMock()) + cell = patched.__closure__[0] + saved = cell.cell_contents + cell.cell_contents = mock_original + try: + lhs = _mock_tensor(tl.float32) + rhs = _mock_tensor(tl.float32) + patched(semantic, lhs, rhs, None, "hf32", None, tl.float32) + assert mock_original.call_args[0][4] == "hf32" + finally: + cell.cell_contents = saved + + # ---- max_num_imprecise_acc ---- + + def test_max_imprecise_explicit_forced_to_zero(self, semantic, capsys): + """Explicit max_num_imprecise_acc: warns, reaches create_dot as 0.""" + lhs = _mock_tensor(tl.float16) + rhs = _mock_tensor(tl.float16) + TritonSemantic.dot(semantic, lhs, rhs, None, "ieee", 16, tl.float32) + + assert "max_num_imprecise_acc" in capsys.readouterr().out + assert semantic.builder.create_dot.call_args[0][4] == 0 + + def test_max_imprecise_none_no_warning_and_zero(self, semantic, capsys): + """None (default): no warning, reaches create_dot as 0.""" + lhs = _mock_tensor(tl.float16) + rhs = _mock_tensor(tl.float16) + TritonSemantic.dot(semantic, lhs, rhs, None, "ieee", None, tl.float32) + + assert "max_num_imprecise_acc" not in capsys.readouterr().out + assert semantic.builder.create_dot.call_args[0][4] == 0 diff --git a/third_party/ascend/unittest/pytest_ut/test_dot_scaled.py b/third_party/ascend/unittest/pytest_ut/test_dot_scaled.py new file mode 100644 index 0000000000..2c7b1d4417 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_dot_scaled.py @@ -0,0 +1,224 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import contextlib +import itertools +import re +import math +import textwrap +import os +import inspect +import pathlib + +import numpy as np +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + +from numpy.random import RandomState +from triton.language.extra import libdevice + + +@pytest.mark.parametrize("M, N, K, rhs_scale, normal_type, acc_num, num_warps", + [(M, N, K, rhs_scale, normal_type, acc_num, 4) + for M, N, K in itertools.product([32, 64], [32, 64], [32]) + for rhs_scale in [False, True] + for normal_type in ["bf16", "fp16"] + for acc_num in [None, 1, 2]]) +def test_scaled_dot(M, N, K, rhs_scale, normal_type, num_warps, acc_num): + device = "npu" + + @triton.jit + def dot_scale_kernel(a_base, stride_a0, stride_a1, a_scale, b_base, stride_b0, stride_b1, b_scale, out, + BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr, type_a: tl.constexpr, + type_b: tl.constexpr, acc_num: tl.constexpr): + + PACKED_BLOCK_K_A: tl.constexpr = BLOCK_K + PACKED_BLOCK_K_B: tl.constexpr = BLOCK_K + a_ptr = a_base + tl.arange(0, BLOCK_M)[:, None] * stride_a0 + tl.arange(0, + PACKED_BLOCK_K_A)[None, :] * stride_a1 + b_ptr = b_base + tl.arange(0, PACKED_BLOCK_K_B)[:, None] * stride_b0 + tl.arange(0, + BLOCK_N)[None, :] * stride_b1 + + a = tl.load(a_ptr) + b = tl.load(b_ptr) + SCALE_BLOCK_K: tl.constexpr = BLOCK_K // 32 + accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + if a_scale is not None: + scale_a_ptr = a_scale + tl.arange(0, BLOCK_M)[:, None] * SCALE_BLOCK_K + tl.arange(0, + SCALE_BLOCK_K)[None, :] + a_scale = tl.load(scale_a_ptr) + if b_scale is not None: + scale_b_ptr = b_scale + tl.arange(0, BLOCK_N)[:, None] * SCALE_BLOCK_K + tl.arange(0, + SCALE_BLOCK_K)[None, :] + b_scale = tl.load(scale_b_ptr) + accumulator = tl.dot_scaled(a, a_scale, type_a, b, b_scale, type_b, acc=accumulator, fast_math=True, + out_dtype=tl.float32) + if acc_num is not None: + for _ in range(acc_num): + accumulator = tl.dot_scaled(a, a_scale, type_a, b, b_scale, type_b, acc=accumulator, fast_math=True, + out_dtype=tl.float32) + + out_ptr = out + tl.arange(0, BLOCK_M)[:, None] * BLOCK_N + tl.arange(0, BLOCK_N)[None, :] + tl.store(out_ptr, accumulator.to(a.dtype)) + + # The max exponent we use to initialize data in the x/y and associated scale tensor to avoid + # overflow when scaling. + comp_dtype_max_exp = 6 if normal_type == "fp16" else 15 + + torch.manual_seed(0) + + def make_arg(shape, ty): + if ty == "bf16" or ty == "fp16": + comp_dtype = torch.float16 if ty == "fp16" else torch.bfloat16 + ret = torch.randn(shape, dtype=comp_dtype, device=device) + # Clamp to avoid relative error issues + ret.clamp_(-2**comp_dtype_max_exp, 2**comp_dtype_max_exp - 1) + else: + ret = torch.randint(256, shape, dtype=torch.uint8, device=device) + return ret + + type_a = normal_type + type_b = type_a + + x = make_arg((M, K), type_a) + y = make_arg((K, N), type_b) + + min_scale, max_scale = (0, 142) if type_a == torch.bfloat16 else (124, 131) + scale_x = torch.randint(min_scale, max_scale, (M, K // 32), dtype=torch.uint8, device=device) + min_scale, max_scale = (0, 142) if type_b == torch.bfloat16 else (124, 131) + scale_y = torch.randint(min_scale, max_scale, (N, K // 32), dtype=torch.uint8, device=device) + + if not rhs_scale: + scale_y = None + + def golden_ref(x, scale_x, y, scale_y): + shape_expand_x = x.shape[-1] // scale_x.shape[-1] + if x.dtype == torch.bfloat16: + upscale_x = scale_x.repeat_interleave(shape_expand_x, dim=1).to(torch.int16) + upscale_x = (upscale_x << 7).view(torch.bfloat16) + else: + scale_fp32 = scale_x.repeat_interleave(shape_expand_x, dim=1).to(torch.int32) + scale_fp32 = (scale_fp32 << 23).view(torch.float32) + upscale_x = scale_fp32.to(torch.float16) + upscale_y = None + if scale_y is None: + upscale_y = torch.ones_like(y) + else: + scale_y = scale_y.T + shape_expand_y = y.shape[0] // scale_y.shape[0] + if y.dtype == torch.bfloat16: + upscale_y = scale_y.repeat_interleave(shape_expand_y, dim=0).to(torch.int16) + upscale_y = (upscale_y << 7).view(torch.bfloat16) + else: + scale_fp32 = scale_y.repeat_interleave(shape_expand_y, dim=0).to(torch.int32) + scale_fp32 = (scale_fp32 << 23).view(torch.float32) + upscale_y = scale_fp32.to(torch.float16) + ret = torch.matmul(x * upscale_x, y * upscale_y) + return ret + + kernel_kwargs = {"num_warps": num_warps} + z = x.new_empty((M, N), dtype=x.dtype) + pgm = dot_scale_kernel[(1, )](x, *x.stride(), scale_x, y, *y.stride(), scale_y, z, M, N, K, type_a, type_b, acc_num, + **kernel_kwargs) + z_ref = golden_ref(x, scale_x, y, scale_y) + if acc_num is not None: + z_ref = z_ref * (acc_num + 1) + + atol = 1e-5 + rtol = 1e-2 + torch.testing.assert_close(z, z_ref, atol=atol, rtol=rtol) + + +@pytest.mark.parametrize("normal_type", ["bf16", "fp16"]) +def test_scaled_dot_fast_math(normal_type): + device = "npu" + m = n = k = 32 + + @triton.jit + def dot_scale_kernel(a_base, stride_a0, stride_a1, a_scale, b_base, stride_b0, stride_b1, b_scale, out, + BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr, type_a: tl.constexpr, + type_b: tl.constexpr, fast_math: tl.constexpr): + + a_ptr = a_base + tl.arange(0, BLOCK_M)[:, None] * stride_a0 + tl.arange(0, BLOCK_K)[None, :] * stride_a1 + b_ptr = b_base + tl.arange(0, BLOCK_K)[:, None] * stride_b0 + tl.arange(0, BLOCK_N)[None, :] * stride_b1 + a = tl.load(a_ptr) + b = tl.load(b_ptr) + + scale_block_k: tl.constexpr = BLOCK_K // 32 + scale_a_ptr = a_scale + tl.arange(0, BLOCK_M)[:, None] * scale_block_k + tl.arange(0, scale_block_k)[None, :] + scale_b_ptr = b_scale + tl.arange(0, BLOCK_N)[:, None] * scale_block_k + tl.arange(0, scale_block_k)[None, :] + a_scale = tl.load(scale_a_ptr) + b_scale = tl.load(scale_b_ptr) + + out_tensor = tl.dot_scaled(a, a_scale, type_a, b, b_scale, type_b, fast_math=fast_math, out_dtype=tl.float32) + out_ptr = out + tl.arange(0, BLOCK_M)[:, None] * BLOCK_N + tl.arange(0, BLOCK_N)[None, :] + tl.store(out_ptr, out_tensor.to(a.dtype)) + + def make_scale_tensor(scale, data_dtype): + if data_dtype == torch.bfloat16: + scale_i16 = scale.to(torch.int16) + return (scale_i16 << 7).view(torch.bfloat16) + scale_fp32 = (scale.to(torch.int32) << 23).view(torch.float32) + return scale_fp32.to(torch.float16) + + def golden_ref(x, scale_x, y, scale_y, fast_math): + upscale_x = make_scale_tensor(scale_x.repeat_interleave(x.shape[1] // scale_x.shape[1], dim=1), x.dtype) + scale_y_t = scale_y.T + upscale_y = make_scale_tensor(scale_y_t.repeat_interleave(y.shape[0] // scale_y_t.shape[0], dim=0), y.dtype) + scaled_x = x * upscale_x + scaled_y = y * upscale_y + + if not fast_math: + lhs_nan_mask = scale_x.eq(255).repeat_interleave(x.shape[1] // scale_x.shape[1], dim=1) + rhs_nan_mask = scale_y_t.eq(255).repeat_interleave(y.shape[0] // scale_y_t.shape[0], dim=0) + scaled_x = torch.where(lhs_nan_mask, torch.full_like(scaled_x, float("nan")), scaled_x) + scaled_y = torch.where(rhs_nan_mask, torch.full_like(scaled_y, float("nan")), scaled_y) + + return torch.matmul(scaled_x, scaled_y) + + dtype = torch.float16 if normal_type == "fp16" else torch.bfloat16 + x = torch.full((m, k), 2.0, dtype=dtype, device=device) + y = torch.full((k, n), 3.0, dtype=dtype, device=device) + scale_x = torch.zeros((m, k // 32), dtype=torch.uint8, device=device) + scale_y = torch.zeros((n, k // 32), dtype=torch.uint8, device=device) + scale_x[3, 0] = 255 + scale_y[5, 0] = 255 + + def run_kernel(fast_math): + out = torch.empty((m, n), dtype=dtype, device=device) + dot_scale_kernel[(1, )](x, *x.stride(), scale_x, y, *y.stride(), scale_y, out, m, n, k, normal_type, + normal_type, fast_math, num_warps=4) + return out + + out_fast_math = run_kernel(True) + out_precise = run_kernel(False) + + ref_fast_math = golden_ref(x, scale_x, y, scale_y, fast_math=True) + ref_precise = golden_ref(x, scale_x, y, scale_y, fast_math=False) + + assert torch.isnan(out_precise[3, :]).all() + assert torch.isnan(out_precise[:, 5]).all() + assert not torch.isnan(out_precise[:3, :5]).any() + + torch.testing.assert_close(out_fast_math, ref_fast_math, atol=1e-3, rtol=1e-2, equal_nan=True) + torch.testing.assert_close(out_precise, ref_precise, atol=1e-3, rtol=1e-2, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_dot_scaled_fp4_fp8.py b/third_party/ascend/unittest/pytest_ut/test_dot_scaled_fp4_fp8.py new file mode 100644 index 0000000000..003c12384f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_dot_scaled_fp4_fp8.py @@ -0,0 +1,207 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import triton +import triton.language as tl +import numpy as np +import test_common +from triton.backends.ascend.utils import is_compile_on_910_95 + +# Chained dot_scaled: first matmul with zero init, second accumulates via acc=. +# Mirrors normalize-matmul.mlir @test_mmadmx_chain_no_elemwise_decompose: +# mmadmxL1(init=true, outs=empty) -> mmadmxL1(init=false, outs=first) +# No transpose. Result should equal 2 * dot_scaled(lhs, rhs, scales). + + +def ub_overflow_check(M, N, K): + bytes_of_dtype = 4 + M32 = ((M + 31) // 32) * 32 + N32 = ((N + 31) // 32) * 32 + cond1 = M32 * K * bytes_of_dtype <= 60 * 1024 + cond2 = K * N32 * bytes_of_dtype <= 60 * 1024 + cond3 = M32 * N32 * bytes_of_dtype <= 256 * 1024 + return cond1 and cond2 and cond3 + + +def torch_dot_scaled(a, b, sa, sb): + sa = torch.exp2(sa - 127).repeat_interleave(32, dim=1) + sb = torch.exp2(sb - 127).repeat_interleave(32, dim=1).T + return torch.matmul(a * sa, b * sb) + + +@triton.jit +def dot_scale_chain_kernel( + lhs_ptr, + lhs_scale_ptr, + rhs_ptr, + rhs_scale_ptr, + out_ptr, + lhs_format: tl.constexpr, + rhs_format: tl.constexpr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, +): + Midx = tl.arange(0, M) + Nidx = tl.arange(0, N) + Kidx = tl.arange(0, K) + K1idx = tl.arange(0, K // 32) + + lhs = tl.load(lhs_ptr + Midx[:, None] * K + Kidx[None, :]) + lhs_scale = tl.load(lhs_scale_ptr + Midx[:, None] * (K // 32) + K1idx[None, :]) + rhs = tl.load(rhs_ptr + Kidx[:, None] * N + Nidx[None, :]) + rhs_scale = tl.load(rhs_scale_ptr + Nidx[:, None] * (K // 32) + K1idx[None, :]) + + # First mmad: init=true equivalent (fresh accumulation). + first = tl.dot_scaled(lhs, lhs_scale, lhs_format, rhs, rhs_scale, rhs_format) + # Second mmad: init=false equivalent (accumulate into L0C). + result = tl.dot_scaled(lhs, lhs_scale, lhs_format, rhs, rhs_scale, rhs_format, acc=first) + tl.store(out_ptr + Midx[:, None] * N + Nidx[None, :], result) + + +testlist = [ + (1, 7, 64), + (15, 128, 64), + (128, 64, 64), + (189, 175, 64), + (1, 1, 128), + (35, 37, 128), + (62, 64, 192), + (31, 27, 256), +] + + +@pytest.mark.skipif( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason="only support in Ascend 950", +) +@pytest.mark.parametrize("dtype", ["fp8e4m3", "fp8e5m2"]) +@pytest.mark.parametrize("M, N, K", testlist) +def test_scaled_dot_fp8(M, N, K, dtype): + if not ub_overflow_check(M, N, K): + pytest.skip("UB overflow") + + lhs_format = dtype[3:] + rhs_format = lhs_format + + lhs = test_common.generate_tensor_fp8(shape=(M, K), dtype=dtype).npu() + rhs = test_common.generate_tensor_fp8(shape=(K, N), dtype=dtype).npu() + lhs_scale = torch.randint(low=110, high=128, size=(M, K // 32), dtype=torch.int8).npu() + rhs_scale = torch.randint(low=110, high=128, size=(N, K // 32), dtype=torch.int8).npu() + + lhs_f32 = lhs.cpu().to(torch.float32) + rhs_f32 = rhs.cpu().to(torch.float32) + lhs_scale_f32 = lhs_scale.cpu().to(torch.float32) + rhs_scale_f32 = rhs_scale.cpu().to(torch.float32) + + triton_res = torch.zeros((M, N), dtype=torch.float32).npu() + dot_scale_chain_kernel[(1, )](lhs, lhs_scale, rhs, rhs_scale, triton_res, lhs_format, rhs_format, M, N, K) + + single = torch_dot_scaled(lhs_f32, rhs_f32, lhs_scale_f32, rhs_scale_f32) + torch_ref = (single + single).to(torch.float32) + + if dtype == "fp8e4m3": + torch.testing.assert_close(triton_res.cpu(), torch_ref.cpu(), rtol=125e-03, atol=125e-03, equal_nan=True) + elif dtype == "fp8e5m2": + torch.testing.assert_close(triton_res.cpu(), torch_ref.cpu(), rtol=75e-02, atol=75e-02, equal_nan=True) + + +@triton.jit +def _dot_scaled_test_fp4( + a_ptr, + a_scale_ptr, + b_ptr, + b_scale_ptr, + c_ptr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, + K_PACKED: tl.constexpr, + K_SCALE: tl.constexpr, +): + # A: [M, K_PACKED] uint8 (packed e2m1), scale [M, K_SCALE] uint8 e8m0 + # B: [K_PACKED, N] uint8 (packed e2m1), scale [N, K_SCALE] uint8 e8m0 + # note: rhs is [K, N] not [N, K] for dot_scaled + m_offs = tl.arange(0, M) + n_offs = tl.arange(0, N) + k_packed_offs = tl.arange(0, K_PACKED) + scale_offs = tl.arange(0, K_SCALE) + + a = tl.load(a_ptr + m_offs[:, None] * K_PACKED + k_packed_offs[None, :]) + a_scale = tl.load(a_scale_ptr + m_offs[:, None] * K_SCALE + scale_offs[None, :]) + + b = tl.load(b_ptr + k_packed_offs[:, None] * N + n_offs[None, :]) + b_scale = tl.load(b_scale_ptr + n_offs[:, None] * K_SCALE + scale_offs[None, :]) + + acc = tl.dot_scaled(a, a_scale, "e2m1", b, b_scale, "e2m1") + + tl.store(c_ptr + m_offs[:, None] * N + n_offs[None, :], acc) + + +def dequant_e2m1(packed_uint8, scale_uint8, rows, K): + """Reference dequant for verification.""" + vals = torch.tensor([0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, -0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0], + device=packed_uint8.device, dtype=torch.float32) + low = vals[(packed_uint8 & 0x0F).long()] + high = vals[((packed_uint8 >> 4) & 0x0F).long()] + out = torch.zeros(rows, K, device=packed_uint8.device, dtype=torch.float32) + out[:, 0::2] = low + out[:, 1::2] = high + scale_f = torch.pow(2.0, scale_uint8.float() - 127.0) + scale_expanded = scale_f.repeat_interleave(32, dim=1)[:, :K] + return out * scale_expanded + + +@pytest.mark.skip(reason="to be supported in Ascend 950") +def test_dot_scaled_fp4(): + M, N, K = 16, 16, 64 + K_PACKED = K // 2 + K_SCALE = K // 32 + + a_packed = torch.randint(0, 256, (M, K_PACKED), device='npu', dtype=torch.uint8) + a_scale = torch.full((M, K_SCALE), 127, device='npu', dtype=torch.uint8) + + # B for dot_scaled: [K_PACKED, N] (K-major) + b_packed_nk = torch.randint(0, 256, (N, K_PACKED), device='npu', dtype=torch.uint8) + b_packed_kn = b_packed_nk.t().contiguous() # [K_PACKED, N] + b_scale = torch.full((N, K_SCALE), 127, device='npu', dtype=torch.uint8) + + c = torch.empty(M, N, device='npu', dtype=torch.float32) + + _dot_scaled_test_fp4[(1, )]( + a_packed, + a_scale, + b_packed_kn, + b_scale, + c, + M, + N, + K, + K_PACKED, + K_SCALE, + ) + + # Reference + a_deq = dequant_e2m1(a_packed, a_scale, M, K) + b_deq = dequant_e2m1(b_packed_nk, b_scale, N, K) + ref = a_deq @ b_deq.t() + torch.testing.assert_close(c, ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_downgrade.py b/third_party/ascend/unittest/pytest_ut/test_downgrade.py new file mode 100644 index 0000000000..810fdc7b92 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_downgrade.py @@ -0,0 +1,49 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +from triton.backends.ascend.utils import downgrade_llir, _downgrade_mem_attrs, _downgrade_stacksaverestore_intrinsics + + +@pytest.mark.parametrize("new_attr,legacy_attrs", [ + ("memory(none)", ["readnone"]), + ("memory(read)", ["readonly"]), + ("memory(write)", ["writeonly"]), + ("memory(readwrite)", []), + ("memory(argmem: read)", ["readonly", "argmemonly"]), + ("memory(argmem: read, inaccessiblemem: write)", ["inaccessiblemem_or_argmemonly"]), + ("memory(read, argmem: readwrite)", []), + ("memory(readwrite, argmem: none)", []), +]) +def test_mem_attrs(new_attr, legacy_attrs): + assert _downgrade_mem_attrs(new_attr).strip().split() == legacy_attrs + + +@pytest.mark.parametrize("new_intr,legacy_intr", [ + ("declare ptr @llvm.stacksave.p0()", "declare ptr @llvm.stacksave()"), + ("declare ptr addrspace(5) @llvm.stacksave.p5()", "declare ptr addrspace(5) @llvm.stacksave()"), + ("declare void @llvm.stackrestore.p0(ptr %ptr)", "declare void @llvm.stackrestore(ptr %ptr)"), + ("declare void @llvm.stackrestore.p5(ptr addrspace(5) %ptr)", + "declare void @llvm.stackrestore(ptr addrspace(5) %ptr)"), + ("%53 = call ptr @llvm.stacksave.p0()", "%53 = call ptr @llvm.stacksave()"), + ("call void @llvm.stackrestore.p0(ptr %53)", "call void @llvm.stackrestore(ptr %53)"), +]) +def test_stacksaverestore_intrinsics(new_intr, legacy_intr): + assert _downgrade_stacksaverestore_intrinsics(new_intr).strip() == legacy_intr diff --git a/third_party/ascend/unittest/pytest_ut/test_driver.py b/third_party/ascend/unittest/pytest_ut/test_driver.py new file mode 100644 index 0000000000..cff8eacfd5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_driver.py @@ -0,0 +1,167 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import types +from unittest.mock import MagicMock + +import pytest + +_MOCK_AICORE_NUM = 24 +_MOCK_AIVECTOR_NUM = _MOCK_AICORE_NUM * 2 + + +def _make_mock_npu_utils(): + """Create a mock with real _get_npu_device_limit_form_env and get_device_properties + bound, while get_device_aicore returns a fixed int. + + get_device_aicore cannot be bound via types.MethodType because it is decorated + with @functools.lru_cache(), whose wrapper object is not a descriptor and does + not produce a correct bound method. Using return_value avoids this issue.""" + try: + from triton.backends.ascend.driver import NPUUtils + except ImportError: + pytest.skip("Ascend backend not available") + mock = MagicMock() + # _get_npu_device_limit_form_env calls self.get_device_aicore(); configure it + # to return a fixed int instead of binding the real lru_cached method. + mock.get_device_core.return_value = _MOCK_AICORE_NUM, _MOCK_AIVECTOR_NUM + # Bind real methods (not lru_cached) so the actual parsing/property logic runs. + mock._get_npu_device_limit_form_env = types.MethodType(NPUUtils._get_npu_device_limit_form_env, mock) + mock.get_device_properties = types.MethodType(NPUUtils.get_device_properties, mock) + return mock, NPUUtils + + +@pytest.mark.parametrize( + "env_value,expected_aic,expected_aiv", + [ + # env not set -> return device defaults + (None, _MOCK_AICORE_NUM, _MOCK_AIVECTOR_NUM), + # valid values within device limit + ("12,24", 12, 24), + (f"{_MOCK_AICORE_NUM},{_MOCK_AIVECTOR_NUM}", _MOCK_AICORE_NUM, _MOCK_AIVECTOR_NUM), + ("1,2", 1, 2), + # leading/trailing spaces are stripped by .strip() + (" 12,24 ", 12, 24), + ("12 , 24 ", 12, 24), + ("12, 24", 12, 24), + ], +) +def test_npu_device_limit_env_var_valid(monkeypatch, env_value, expected_aic, expected_aiv): + """Test _get_npu_device_limit_form_env returns user-specified values for valid input.""" + mock_utils, NPUUtils = _make_mock_npu_utils() + + if env_value is None: + monkeypatch.delenv("NPU_DEVICE_LIMIT", raising=False) + else: + monkeypatch.setenv("NPU_DEVICE_LIMIT", env_value) + + aic, aiv = NPUUtils._get_npu_device_limit_form_env(mock_utils) + assert aic == expected_aic + assert aiv == expected_aiv + + +@pytest.mark.parametrize( + "env_value", + [ + # non-positive values + "0,48", + "24,0", + "0,0", + # exceeds device limit + f"{_MOCK_AICORE_NUM + 1}," + f"{_MOCK_AIVECTOR_NUM}", + f"{_MOCK_AICORE_NUM}," + f"{_MOCK_AIVECTOR_NUM + 1}", + "100,200", + # invalid format + "abc", + "12", # missing second value + "12,24,36", # too many values + "12.5,24", # float not matched + "", # empty string + "-1,48", # negative sign not matched + "2,6", + "2,5", + ], +) +def test_npu_device_limit_env_var_invalid(monkeypatch, env_value): + """Test _get_npu_device_limit_form_env raises ValueError for invalid input.""" + mock_utils, NPUUtils = _make_mock_npu_utils() + monkeypatch.setenv("NPU_DEVICE_LIMIT", env_value) + + with pytest.raises(ValueError, match="NPU_DEVICE_LIMIT"): + NPUUtils._get_npu_device_limit_form_env(mock_utils) + + +def test_npu_device_limit_get_device_properties(monkeypatch): + """Test get_device_properties respects NPU_DEVICE_LIMIT.""" + mock_utils, NPUUtils = _make_mock_npu_utils() + + monkeypatch.setenv("NPU_DEVICE_LIMIT", "8,16") + props = NPUUtils.get_device_properties(mock_utils, "npu") + assert props["num_aicore"] == 8 + assert props["num_vectorcore"] == 16 + assert props["max_shared_mem"] == 1 + + +def test_npu_device_limit_default_when_unset(monkeypatch): + """Test get_device_properties returns hardware defaults when env var is unset.""" + mock_utils, NPUUtils = _make_mock_npu_utils() + + monkeypatch.delenv("NPU_DEVICE_LIMIT", raising=False) + props = NPUUtils.get_device_properties(mock_utils, "npu") + assert props["num_aicore"] == _MOCK_AICORE_NUM + assert props["num_vectorcore"] == _MOCK_AIVECTOR_NUM + + +def test_npu_device_limit_propagates_error(monkeypatch): + """Test get_device_properties propagates ValueError from _get_npu_device_limit_form_env.""" + mock_utils, NPUUtils = _make_mock_npu_utils() + + monkeypatch.setenv("NPU_DEVICE_LIMIT", "0,48") + with pytest.raises(ValueError, match="non-positive"): + NPUUtils.get_device_properties(mock_utils, "npu") + + +def test_npu_device_limit_no_caching_of_env_var(monkeypatch): + """Test that get_device_properties re-reads the env var on every call. + + get_device_properties is not lru_cached, so changing the env var between + calls takes effect immediately.""" + mock_utils, NPUUtils = _make_mock_npu_utils() + + monkeypatch.setenv("NPU_DEVICE_LIMIT", "8,16") + props1 = NPUUtils.get_device_properties(mock_utils, "npu") + assert props1["num_aicore"] == 8 + + # Change env var - get_device_properties is not cached, so the new value takes effect + monkeypatch.setenv("NPU_DEVICE_LIMIT", "16,32") + props2 = NPUUtils.get_device_properties(mock_utils, "npu") + assert props2["num_aicore"] == 16 + + +def test_npu_device_limit_get_aicore_num(monkeypatch): + """Test get_aicore_num / get_aivector_core_num read from get_device_properties.""" + mock_utils, NPUUtils = _make_mock_npu_utils() + + monkeypatch.setenv("NPU_DEVICE_LIMIT", "8,16") + + assert NPUUtils.get_aicore_num(mock_utils) == 8 + assert NPUUtils.get_aivector_core_num(mock_utils) == 16 diff --git a/third_party/ascend/unittest/pytest_ut/test_elementwise_ceil.py b/third_party/ascend/unittest/pytest_ut/test_elementwise_ceil.py new file mode 100644 index 0000000000..690cc0bc81 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_elementwise_ceil.py @@ -0,0 +1,86 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time +import test_common +import os +import shutil + +import torch +import torch_npu + + +def standard_ceil(x0): + res = torch.ceil(x0) + return res + + +@triton.jit +def triton_ceil(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.ceil(x) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +types = [ + (torch.float32, 'float32'), +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + +ops = [ + ('ceil', triton_ceil, standard_ceil), +] + + +@pytest.mark.parametrize('opName, tritonOp, standOp', ops) +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_elementwise_common(opName, tritonOp, standOp, dtype, sigtype, N, NUMEL): + torch.manual_seed(0) + torch_npu.npu.utils.set_device(0) + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standOp(x0) + x0 = x0.npu() + + output = torch.zeros((N, ), dtype=dtype).npu() + tritonOp[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_elementwise_clip.py b/third_party/ascend/unittest/pytest_ut/test_elementwise_clip.py new file mode 100644 index 0000000000..5dce754aaf --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_elementwise_clip.py @@ -0,0 +1,88 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time +import test_common +import os +import shutil + +import torch +import torch_npu + + +def standard_clamp(x0): + res = torch.clamp(x0, min=-10, max=10) + return res + + +@triton.jit +def triton_clamp(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.clamp(x, -10, 10) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + (torch.bfloat16, 'bfloat16'), +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + +ops = [ + ('clamp', triton_clamp, standard_clamp), +] + + +@pytest.mark.parametrize('opName, tritonOp, standOp', ops) +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_elementwise_common(opName, tritonOp, standOp, dtype, sigtype, N, NUMEL): + torch.manual_seed(0) + torch_npu.npu.utils.set_device(0) + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standOp(x0) + x0 = x0.npu() + + output = torch.zeros((N, ), dtype=dtype).npu() + tritonOp[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_elementwise_f2i.py b/third_party/ascend/unittest/pytest_ut/test_elementwise_f2i.py new file mode 100644 index 0000000000..a5bf1b21e7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_elementwise_f2i.py @@ -0,0 +1,137 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time +import test_common +import os +import shutil + +import torch +import torch_npu + + +def standard_f2i32(x0): + res = x0.to(torch.int32) + return res + + +def standard_f2i8(x0): + res = x0.to(torch.int8) + return res + + +def standard_f2i16(x0): + res = x0.to(torch.int16) + return res + + +def standard_f2i64(x0): + res = x0.to(torch.int64) + return res + + +@triton.jit +def triton_f2i8(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.cast(x, tl.int8) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +@triton.jit +def triton_f2i16(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.cast(x, tl.int16) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +@triton.jit +def triton_f2i32(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.cast(x, tl.int32) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +@triton.jit +def triton_f2i64(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.cast(x, tl.int64) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + (torch.bfloat16, 'bfloat16'), +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), +] + +map_for_64_t = {37: 31} + +ops = [ + ('f2i8', triton_f2i8, standard_f2i8, 'int8'), + ('f2i16', triton_f2i16, standard_f2i16, 'int16'), + ('f2i32', triton_f2i32, standard_f2i32, 'int32'), + ('f2i64', triton_f2i64, standard_f2i64, 'int64'), +] + + +def continue_func(opName, d_type): + if 'f2i' in opName and 'int' in d_type: + return True + + +@pytest.mark.parametrize('opName, tritonOp, standOp, dst_sigtype', ops) +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_elementwise_common(opName, tritonOp, standOp, dst_sigtype, dtype, sigtype, N, NUMEL): + if continue_func(opName, sigtype): + return + + torch_npu.npu.utils.set_device(0) + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standOp(x0) + x0 = x0.npu() + + output = test_common.generate_tensor(shape=(N, ), dtype=dst_sigtype).npu() + tritonOp[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(dst_sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_elementwise_floor.py b/third_party/ascend/unittest/pytest_ut/test_elementwise_floor.py new file mode 100644 index 0000000000..c78b908710 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_elementwise_floor.py @@ -0,0 +1,86 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time +import test_common +import os +import shutil + +import torch +import torch_npu + + +def standard_floor(x0): + res = torch.floor(x0) + return res + + +@triton.jit +def triton_floor(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.floor(x) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +types = [ + (torch.float32, 'float32'), +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + +ops = [ + ('floor', triton_floor, standard_floor), +] + + +@pytest.mark.parametrize('opName, tritonOp, standOp', ops) +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_elementwise_common(opName, tritonOp, standOp, dtype, sigtype, N, NUMEL): + torch.manual_seed(0) + torch_npu.npu.utils.set_device(0) + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standOp(x0) + x0 = x0.npu() + + output = torch.zeros((N, ), dtype=dtype).npu() + tritonOp[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_elementwise_i2f.py b/third_party/ascend/unittest/pytest_ut/test_elementwise_i2f.py new file mode 100644 index 0000000000..d12738101a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_elementwise_i2f.py @@ -0,0 +1,123 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time +import test_common +import os +import shutil + +import torch +import torch_npu + + +def standard_i2f_float32(x0): + res = x0.to(torch.float32) + return res + + +def standard_i2f_float16(x0): + res = x0.to(torch.float16) + return res + + +def standard_i2f_bfloat16(x0): + res = x0.to(torch.bfloat16) + return res + + +@triton.jit +def triton_i2f_float32(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.cast(x, tl.float32) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +@triton.jit +def triton_i2f_float16(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.cast(x, tl.float16) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +@triton.jit +def triton_i2f_bfloat16(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.cast(x, tl.bfloat16) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +types = [ + # (torch.int8, 'int8'), # TO BE FIXED i8 -> f16、bf16 + # (torch.int16, 'int16'), # TO BE FIXED i16 -> f32、bf16 + (torch.int32, 'int32'), # TO BE FIXED i32 -> f16、bf16 + # (torch.int64, 'int64'), # TO BE FIXED i64 -> bf16 +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), +] + +map_for_64_t = {37: 31} + +ops = [ + # ('i2f16', triton_i2f_float16, standard_i2f_float16, 'float16'), + ('i2f32', triton_i2f_float32, standard_i2f_float32, 'float32'), + # ('i2fbf16', triton_i2f_bfloat16, standard_i2f_bfloat16, 'bfloat16'), +] + + +def continue_func(opName, d_type): + if 'i2f' in opName and 'float' in d_type: + return True + + +@pytest.mark.parametrize('opName, tritonOp, standOp, dst_sigtype', ops) +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_elementwise_common(opName, tritonOp, standOp, dst_sigtype, dtype, sigtype, N, NUMEL): + if continue_func(opName, sigtype): + return + + torch_npu.npu.utils.set_device(0) + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standOp(x0) + x0 = x0.npu() + + output = test_common.generate_tensor(shape=(N, ), dtype=dst_sigtype).npu() + tritonOp[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(dst_sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_elementwise_round.py b/third_party/ascend/unittest/pytest_ut/test_elementwise_round.py new file mode 100644 index 0000000000..2cd3acb6bc --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_elementwise_round.py @@ -0,0 +1,92 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common +import os +import shutil + +import torch +import torch_npu + + +def standard_round_to_nearest_neighbor_even(x0): # TO BE FIXED round to nearest neighbor even + res = torch.round(x0) + return res + + +def standard_round(x0): + res = torch.where(x0 >= 0, torch.floor(x0 + 0.5), torch.ceil(x0 - 0.5)) + return res + + +@triton.jit +def triton_round(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + mask = idx_block < N + x = tl.load(in_ptr0 + idx_block, mask=mask) + res = tl.where(x >= 0, tl.floor(x.to(tl.float32) + 0.5), tl.ceil(x.to(tl.float32) - 0.5)) + tl.store(out_ptr0 + idx_block, res, mask=mask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + (torch.bfloat16, 'bfloat16'), +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + +ops = [ + ('round', triton_round, standard_round), +] + + +@pytest.mark.parametrize('opName, tritonOp, standOp', ops) +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_elementwise_common(opName, tritonOp, standOp, dtype, sigtype, N, NUMEL): + torch.manual_seed(0) + torch_npu.npu.utils.set_device(0) + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standOp(x0) + x0 = x0.npu() + + output = torch.zeros((N, ), dtype=dtype).npu() + tritonOp[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_eq.py b/third_party/ascend/unittest/pytest_ut/test_eq.py new file mode 100644 index 0000000000..35ad5458c3 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_eq.py @@ -0,0 +1,79 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_binary(x0, y0): + res = x0 == y0 + return res + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x == y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + (torch.int8, 'int8'), + (torch.int16, 'int16'), + (torch.int32, 'int32'), + (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + y0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + ans = standard_binary(x0, y0) + out = torch.zeros((N, ), dtype=torch.bool).npu() + triton_elementwise_binary[1, 1, 1](x0, y0, out, N, NUMEL) + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_eq_2.py b/third_party/ascend/unittest/pytest_ut/test_eq_2.py new file mode 100644 index 0000000000..f99d537001 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_eq_2.py @@ -0,0 +1,76 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_eq(x0, x1): + return x0 == x1 + + +def torch_eq_from_np(x0, x1, dtype): + return torch.from_numpy(x0 == x1).to(eval('torch.' + dtype)) + + +@triton.jit +def triton_eq(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index, None) + tmp1 = tl.load(in_ptr1 + x_index, None) + tmp2 = tmp0 == tmp1 + tl.store(out_ptr0 + x_index, tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + # ['float16', (2, 4096, 8), 2, 32768, 1024], + # ['bfloat16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + # ['int16', (2, 4096, 8), 2, 32768, 1024], + # ['int32', (2, 4096, 8), 2, 32768, 1024], + # ['int64', (2, 4096, 8), 2, 32768, 1024], + ['uint8', (2, 4096, 8), 2, 32768, 1024], + # ['uint16', (2, 4096, 8), 2, 32768, 1024], + # ['uint32', (2, 4096, 8), 2, 32768, 1024], + # ['uint64', (2, 4096, 8), 2, 32768, 1024], +]) +def test_eq(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + np_x0 = test_common.generate_numpy(shape, dtype) + x0 = torch.from_numpy(np_x0).to(eval('torch.' + dtype)).npu() + np_x1 = test_common.generate_numpy(shape, dtype) + x1 = torch.from_numpy(np_x1).to(eval('torch.' + dtype)).npu() + # torch结果 + torch_res = torch_eq_from_np(np_x0, np_x1, dtype).npu() + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_eq[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_erfinv.py b/third_party/ascend/unittest/pytest_ut/test_erfinv.py new file mode 100644 index 0000000000..3ac29694e0 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_erfinv.py @@ -0,0 +1,149 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common +import triton.language.extra.cann.libdevice as libdevice + + +@triton.jit +def triton_erfinv(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = tl.load(in_ptr0 + xindex, xmask) + y = libdevice.erfinv(x0) + tl.store(out_ptr0 + xindex, y, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_erfinv_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + x[0][0][0] = 1 # erfinv(1) -> ∞ + x[0][0][1] = -1 # erfinv(-1) -> -∞ + + # Avoid numerical instability near ±1 + # Move values in (threshold, 1) to threshold and (-1, -threshold) to -threshold + threshold = 1 - 1.1e-4 + too_close_pos = (x > threshold) & (x < 1) + too_close_neg = (x < -threshold) & (x > -1) + x[too_close_pos] = threshold + x[too_close_neg] = -threshold + y_ref = torch.erfinv(x).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_erfinv[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_all_blocks_parallel(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + x[0][0][0] = 1 # erfinv(1) -> ∞ + x[0][0][1] = -1 # erfinv(-1) -> -∞ + + # Avoid numerical instability near ±1 + # Move values in (threshold, 1) to threshold and (-1, -threshold) to -threshold + threshold = 1 - 1.1e-4 + too_close_pos = (x > threshold) & (x < 1) + too_close_neg = (x < -threshold) & (x > -1) + x[too_close_pos] = threshold + x[too_close_neg] = -threshold + y_ref = torch.erfinv(x).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_erfinv[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_auto_blockify(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + x[0][0][0] = 1 # erfinv(1) -> ∞ + x[0][0][1] = -1 # erfinv(-1) -> -∞ + + # Avoid numerical instability near ±1 + # Move values in (threshold, 1) to threshold and (-1, -threshold) to -threshold + threshold = 1 - 1.1e-4 + too_close_pos = (x > threshold) & (x < 1) + too_close_neg = (x < -threshold) & (x > -1) + x[too_close_pos] = threshold + x[too_close_neg] = -threshold + y_ref = torch.erfinv(x).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_erfinv[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub, auto_blockify_size=ncore) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +def prepare_erfinv_input(x: torch.Tensor, dtype: str) -> torch.Tensor: + """Inject special values and clip regions near ±1 to avoid numerical instability.""" + # Inject ±1 at known positions + if x.numel() >= 2: + with torch.no_grad(): + x_flat = x.view(-1) + x_flat[0] = 1.0 # erfinv(1) → +inf + x_flat[1] = -1.0 # erfinv(-1) → -inf + + # Threshold to avoid instability near ±1 + threshold = 1.0 - 1.1e-4 + too_close_pos = (x > threshold) & (x < 1.0) + too_close_neg = (x < -threshold) & (x > -1.0) + + with torch.no_grad(): + x[too_close_pos] = threshold + x[too_close_neg] = -threshold + + return x + + +@pytest.mark.parametrize("dtype", ["float32"]) +@pytest.mark.parametrize("shape", [ + (2, 4096, 8), +]) +def test_erfinv_all_blocks_parallel(dtype, shape): + + XBLOCK = 1024 + XBLOCK_SUB = 128 + + x = test_common.generate_tensor(shape, dtype).npu() + x = prepare_erfinv_input(x, dtype) + + y_ref = torch.erfinv(x).npu() + y_cal = torch.zeros_like(x).npu() + numel = x.numel() + + def grid(meta): + return (triton.cdiv(numel, meta['XBLOCK']), ) + + triton_erfinv[grid](x, y_cal, numel, XBLOCK=XBLOCK, XBLOCK_SUB=XBLOCK_SUB) + + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_exp.py b/third_party/ascend/unittest/pytest_ut/test_exp.py new file mode 100644 index 0000000000..9b9afd0bb6 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_exp.py @@ -0,0 +1,89 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + + +def torch_pointwise(x0): + res = torch.exp(x0) + return res + + +@triton.jit +def triton_exp(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp2 = tl.exp(tmp0) + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_exp[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = tl.exp(x) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + + +@pytest.mark.parametrize('dtype, sigtype', [(torch.float32, 'float32'), (torch.float16, 'float16')]) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = torch_pointwise(x0) + x0 = x0.npu() + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_exp2.py b/third_party/ascend/unittest/pytest_ut/test_exp2.py new file mode 100644 index 0000000000..f45bdd13bd --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_exp2.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_exp2(x0): + res = torch.pow(2, x0, out=None) + return res + + +@triton.jit +def triton_exp2(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index, None) + tmp1 = tl.exp2(tmp0) + tl.store(out_ptr0 + x_index, tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_exp2(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_exp2(x0) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_exp2[ncore, 1, 1](x0, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_expand_dims.py b/third_party/ascend/unittest/pytest_ut/test_expand_dims.py new file mode 100644 index 0000000000..304ec9ff8d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_expand_dims.py @@ -0,0 +1,74 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + + ret = tl.expand_dims(X, 2) + + oidx = xidx[:, None, None, None] * YB * ZB + yidx[None, :, None, None] * ZB + tl.arange( + 0, 1)[None, None, :, None] + zidx[None, None, None, :] + + tl.store(output_ptr + oidx, ret) + + +paras = [ + ('*fp32', eval('torch.float32'), 2, 256, 16), + ('*fp32', eval('torch.float32'), 8, 8, 4), + ('*fp16', eval('torch.float16'), 2, 256, 16), + ('*fp16', eval('torch.float16'), 8, 8, 4), + ('*i8', eval('torch.int8'), 2, 256, 16), + ('*i8', eval('torch.int8'), 8, 8, 4), +] + + +@pytest.mark.parametrize('para_type,data_type,XB,YB,ZB', paras) +def test_npu(para_type, data_type, XB, YB, ZB): + + x = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + a = x.unsqueeze(2) + + print(f"shape = {x.shape}") + print(x.dtype) + print(a[0, 0:16, 0, 0]) + + output = torch.randint(1, (XB, YB, 1, ZB), dtype=data_type).npu() + + print(f"output.dtype={output.dtype}") + + fn_npu_[1, 1, 1](output, x, XB=XB, YB=YB, ZB=ZB, debug=True) + print(output[0, 0:16, 0, 0]) + + torch.testing.assert_close(output, a) diff --git a/third_party/ascend/unittest/pytest_ut/test_expm1.py b/third_party/ascend/unittest/pytest_ut/test_expm1.py new file mode 100644 index 0000000000..7a06320d61 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_expm1.py @@ -0,0 +1,62 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +def torch_expm1(x0): + return torch.expm1(x0) + + +@triton.jit +def triton_expm1(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.expm1(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.skip(reason="expm1 failed sometimes, wait for fix") +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_expm1(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0_ref = test_common.generate_tensor(shape, dtype) + # error gains when input is large; expm1 used to handle small input when exp(x) close to 1 + x0_ref[x0_ref > 1] = 1 + y_ref = torch_expm1(x0_ref) + + x0 = x0_ref.npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_expm1[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_extra_libdevice_module_map.py b/third_party/ascend/unittest/pytest_ut/test_extra_libdevice_module_map.py new file mode 100644 index 0000000000..de9f0afdc1 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_extra_libdevice_module_map.py @@ -0,0 +1,44 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import torch +import torch_npu # noqa: F401 +import triton +import triton.language as tl +from triton.language.extra import libdevice + + +@triton.jit +def _extra_libdevice_log1p_kernel(x_ptr, o_ptr, BLOCK: tl.constexpr): + offs = tl.arange(0, BLOCK) + x = tl.load(x_ptr + offs) + y = libdevice.log1p(x) + tl.store(o_ptr + offs, y) + + +def test_extra_libdevice_module_map_log1p(): + n = 32 + # Domain of log1p: x > -1. Use values near 0 for numerical stability. + x = torch.linspace(-0.5, 0.5, n, dtype=torch.float32, device="npu") + o = torch.empty(n, dtype=torch.float32, device="npu") + + _extra_libdevice_log1p_kernel[(1, )](x, o, BLOCK=n) + torch.npu.synchronize() + + torch.testing.assert_close(o.cpu(), torch.log1p(x.cpu()), rtol=1e-5, atol=1e-5) diff --git a/third_party/ascend/unittest/pytest_ut/test_extract_slice.py b/third_party/ascend/unittest/pytest_ut/test_extract_slice.py new file mode 100644 index 0000000000..a87fc47881 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_extract_slice.py @@ -0,0 +1,78 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al + +import pytest +import test_common + + +@triton.jit +def triton_kernel(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + out_sub = al.extract_slice(output, [block_start], [32], [1]) + out_idx = block_start + tl.arange(0, 32) + out_msk = out_idx < n_elements + tl.store(output_ptr + out_idx, out_sub, mask=out_msk) + + +def triton_func(x: torch.Tensor, y: torch.Tensor): + output = torch.empty_like(x) + n_elements = output.numel() + grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + triton_kernel[grid](x, y, output, n_elements, BLOCK_SIZE=1024) + return output + + +def test_extract_slice(): + size = 1024 + x = torch.rand(size, device='npu') + y = torch.rand(size, device='npu') + torch_ref = x + y + triton_cal = triton_func(x, y) + torch.testing.assert_close(triton_cal[:32], torch_ref[:32]) + + +@triton.jit +def extract_slice_kernel(x_ptr, out_ptr, N: tl.constexpr, OFF: tl.constexpr, SZ: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(x_ptr + offs) + sub = al.extract_slice(x, [OFF], [SZ], [1]) + tl.store(out_ptr + tl.arange(0, SZ), sub) + + +@pytest.mark.parametrize("dtype", ['int8', 'int16', 'int32', 'int64', 'float16', 'float32', 'bfloat16', 'uint8']) +def test_extract_slice_dtypes(dtype): + size, offset, sz = 64, 8, 16 + x = test_common.generate_tensor((size, ), dtype).npu() + out = torch.zeros(sz, dtype=eval('torch.' + dtype)).npu() + extract_slice_kernel[(1, )](x, out, size, offset, sz) + torch.testing.assert_close(out, x[offset:offset + sz]) diff --git a/third_party/ascend/unittest/pytest_ut/test_fast_dividef.py b/third_party/ascend/unittest/pytest_ut/test_fast_dividef.py new file mode 100644 index 0000000000..19cded059b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_fast_dividef.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common +import triton.language.extra.cann.libdevice as libdevice + + +def torch_pointwise(x0, x1): + res = x0 / x1 + return res + + +@triton.jit +def triton_fast_dividef(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = libdevice.fast_dividef(tmp0, tmp1) + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_fast_dividef[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_fast_expf.py b/third_party/ascend/unittest/pytest_ut/test_fast_expf.py new file mode 100644 index 0000000000..fc8217500e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_fast_expf.py @@ -0,0 +1,56 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common +import triton.language.extra.cann.libdevice as libdevice + + +def torch_pointwise(x0): + res = torch.exp(x0) + return res + + +@triton.jit +def triton_fast_expf(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp2 = libdevice.fast_expf(tmp0) + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_fast_expf[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_fdiv.py b/third_party/ascend/unittest/pytest_ut/test_fdiv.py new file mode 100644 index 0000000000..0714798a24 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_fdiv.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_fdiv(x0, x1): + res = x0 / x1 + return res + + +@triton.jit +def triton_fdiv(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + x_index = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + tmp0 = tl.load(in_ptr0 + x_index) + tmp1 = tl.load(in_ptr1 + x_index) + tmp2 = tl.fdiv(tmp0, tmp1) + tl.store(out_ptr0 + x_index, tmp2) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_fdiv(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_tmp = test_common.generate_tensor(shape, dtype) + y0 = y_tmp.masked_fill(y_tmp == 0, 1) + y0 = y0.npu() + + # torch结果 + y_ref = torch_fdiv(x0, y0).to(eval('torch.' + dtype)) + # triton结果 + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_fdiv[ncore, 1, 1](x0, y0, y_cal, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_fixpipe.py b/third_party/ascend/unittest/pytest_ut/test_fixpipe.py new file mode 100644 index 0000000000..d694e1d968 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_fixpipe.py @@ -0,0 +1,88 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import pytest +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend import _apply_ascend_patch + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + +_apply_ascend_patch() + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + arch = "Ascend910_9589" + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +@triton.jit +def fixpipe( + A_ptr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, +): + + row_matmul = tl.program_id(0) + + offs_i = tl.arange(0, tl.constexpr(M))[:, None] # [M,1] (row axis) + offs_k = tl.arange(0, K) # [K] + + a_ptrs = A_ptr + (row_matmul + offs_i) * K + offs_k[None, :] + a_vals = tl.load(a_ptrs) # [M, K] + + ub = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + al.fixpipe(a_vals, ub, dual_dst_mode=al.FixpipeDualDstMode.NO_DUAL) + + +@pytest.mark.parametrize("M, K, N", [(16, 16, 16)]) +def test_fixpipe(M, K, N): + mlir = compile_kernel( + fixpipe, + { + "A_ptr": "*fp32", + }, + {"M": M, "K": K, "N": N}, + ) + assert len(mlir) > 0 diff --git a/third_party/ascend/unittest/pytest_ut/test_fixpipe_tensor_dst.py b/third_party/ascend/unittest/pytest_ut/test_fixpipe_tensor_dst.py new file mode 100644 index 0000000000..18bb5a796c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_fixpipe_tensor_dst.py @@ -0,0 +1,186 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import pytest +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend import _apply_ascend_patch + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" +DEFAULT_A5_ARCH = "Ascend910_9589" +HACC_TARGET_ATTR = f'hacc.target = #hacc.target<"{DEFAULT_A5_ARCH}">' + +_apply_ascend_patch() + + +def make_options(arch): + + class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + Options.arch = arch + return Options() + + +def compile_kernel(kernel, signature, constants, *, arch=None): + """Compile to TTIR mlir; mirrors triton.compile frontend + ascend patch.""" + arch = arch or DEFAULT_A5_ARCH + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, make_options(arch), {}, {}) + return str(module) + + +@triton.jit +def fixpipe_without_dst( + A_ptr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, +): + row_matmul = tl.program_id(0) + + offs_i = tl.arange(0, tl.constexpr(M))[:, None] + offs_k = tl.arange(0, K) + + a_ptrs = A_ptr + (row_matmul + offs_i) * K + offs_k[None, :] + a_vals = tl.load(a_ptrs) + + result = al.fixpipe(a_vals, dual_dst_mode=al.FixpipeDualDstMode.NO_DUAL) + tl.store(a_ptrs, result) + + +@triton.jit +def fixpipe_with_buffer_dst( + A_ptr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, +): + row_matmul = tl.program_id(0) + + offs_i = tl.arange(0, tl.constexpr(M))[:, None] + offs_k = tl.arange(0, K) + + a_ptrs = A_ptr + (row_matmul + offs_i) * K + offs_k[None, :] + a_vals = tl.load(a_ptrs) + + ub = bl.alloc(tl.float32, [M, N], al.ascend_address_space.UB) + al.fixpipe(a_vals, ub, dual_dst_mode=al.FixpipeDualDstMode.NO_DUAL) + + +@pytest.mark.parametrize("M, K, N", [(16, 16, 16)]) +def test_fixpipe_without_dst(M, K, N): + mlir = compile_kernel( + fixpipe_without_dst, + {"A_ptr": "*fp32"}, + {"M": M, "K": K, "N": N}, + ) + assert len(mlir) > 0 + assert "hivm.hir.fixpipe" in mlir + assert HACC_TARGET_ATTR in mlir + assert HACC_TARGET_ATTR in mlir + + +@triton.jit +def fixpipe_row_split( + A_ptr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, +): + row_matmul = tl.program_id(0) + + offs_i = tl.arange(0, tl.constexpr(M))[:, None] + offs_k = tl.arange(0, K) + + a_ptrs = A_ptr + (row_matmul + offs_i) * K + offs_k[None, :] + a_vals = tl.load(a_ptrs) + + result = al.fixpipe(a_vals, dual_dst_mode=al.FixpipeDualDstMode.ROW_SPLIT) + + +@triton.jit +def fixpipe_column_split( + A_ptr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, +): + row_matmul = tl.program_id(0) + + offs_i = tl.arange(0, tl.constexpr(M))[:, None] + offs_k = tl.arange(0, K) + + a_ptrs = A_ptr + (row_matmul + offs_i) * K + offs_k[None, :] + a_vals = tl.load(a_ptrs) + + result = al.fixpipe(a_vals, dual_dst_mode=al.FixpipeDualDstMode.COLUMN_SPLIT) + + +@pytest.mark.parametrize("M, K, N", [(32, 16, 16)]) +def test_fixpipe_row_split(M, K, N): + mlir = compile_kernel( + fixpipe_row_split, + {"A_ptr": "*fp32"}, + {"M": M, "K": K, "N": N}, + ) + assert len(mlir) > 0 + assert "dual_dst_mode = " in mlir + assert HACC_TARGET_ATTR in mlir + + +@pytest.mark.parametrize("M, K, N", [(16, 16, 32)]) +def test_fixpipe_column_split(M, K, N): + mlir = compile_kernel( + fixpipe_column_split, + {"A_ptr": "*fp32"}, + {"M": M, "K": K, "N": N}, + ) + assert len(mlir) > 0 + assert "dual_dst_mode = " in mlir + assert HACC_TARGET_ATTR in mlir + + +@pytest.mark.parametrize("M, K, N", [(16, 16, 16)]) +def test_fixpipe_with_buffer_dst(M, K, N): + mlir = compile_kernel( + fixpipe_with_buffer_dst, + {"A_ptr": "*fp32"}, + {"M": M, "K": K, "N": N}, + ) + assert len(mlir) > 0 + assert "hivm.hir.fixpipe" in mlir + assert HACC_TARGET_ATTR in mlir diff --git a/third_party/ascend/unittest/pytest_ut/test_flip.py b/third_party/ascend/unittest/pytest_ut/test_flip.py new file mode 100644 index 0000000000..809d400f6b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_flip.py @@ -0,0 +1,50 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al + +import torch +import torch_npu +import pytest +import test_common + +# --------------- +# test flip op +# --------------- + + +@triton.jit +def flip_kernel(x_ptr, out_ptr, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(x_ptr + offs) + y = al.flip(x, dim=0) + tl.store(out_ptr + offs, y) + + +@pytest.mark.parametrize("dtype", ['int8', 'int16', 'int32', 'int64', 'float16', 'float32', 'bfloat16', 'uint8']) +def test_flip(dtype): + size = 64 + x = test_common.generate_tensor((size, ), dtype).npu() + torch_ref = torch.flip(x, dims=[0]) + triton_res = torch.zeros(size, dtype=eval('torch.' + dtype)).npu() + flip_kernel[(1, )](x, triton_res, size) + assert (torch_ref == triton_res).all(), (torch_ref, triton_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_float_indirect_atomic_min.py b/third_party/ascend/unittest/pytest_ut/test_float_indirect_atomic_min.py new file mode 100644 index 0000000000..25c2fef57b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_float_indirect_atomic_min.py @@ -0,0 +1,66 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl + + +@triton.jit +def float_indirect_atomic_min_kernel( + value_ptr, + index_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + offsets = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + + values = tl.load(value_ptr + offsets, mask=mask, other=0.0) + indices = tl.load(index_ptr + offsets, mask=mask, other=0) + tl.atomic_min(output_ptr + indices, values, mask=mask) + + +def test_float_indirect_atomic_min(): + n_elements = 257 + block_size = 128 + + values_cpu = torch.linspace(-4.0, 4.0, n_elements, dtype=torch.float32, device="cpu") + indices_cpu = torch.arange(n_elements - 1, -1, -1, dtype=torch.int64, device="cpu") + + values = values_cpu.npu() + indices = indices_cpu.npu() + output = torch.full((n_elements, ), float("inf"), dtype=torch.float32, device="npu") + + grid = (triton.cdiv(n_elements, block_size), ) + float_indirect_atomic_min_kernel[grid]( + values, + indices, + output, + n_elements, + BLOCK_SIZE=block_size, + ) + + expected = torch.full((n_elements, ), float("inf"), dtype=torch.float32, device="cpu") + expected[indices_cpu] = values_cpu + + torch.testing.assert_close(output.cpu(), expected, rtol=0, atol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_floor.py b/third_party/ascend/unittest/pytest_ut/test_floor.py new file mode 100644 index 0000000000..13a83dbd91 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_floor.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import test_common + + +def torch_floor(x0, x1): + res = x0 + torch.floor(x1) + return res + + +@triton.jit +def triton_floor(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + x_index = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = x_index < xnumel + tmp0 = tl.load(in_ptr0 + x_index, xmask) + tmp1 = tl.load(in_ptr1 + x_index, xmask) + tmp2 = tmp0 + tl.floor(tmp1) + tl.store(out_ptr0 + x_index, tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_floor(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + y_ref = torch_floor(x0, x1) + # triton结果 + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_floor[ncore, 1, 1](x0, x1, y_cal, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_floordiv.py b/third_party/ascend/unittest/pytest_ut/test_floordiv.py new file mode 100644 index 0000000000..1edc15866a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_floordiv.py @@ -0,0 +1,72 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_func(x0, x1): + res = x0 // x1 + return res + + +@triton.jit +def triton_kernel(out_ptr0, in_ptr0, in_ptr1, N: tl.constexpr): + idx = tl.arange(0, N) + x = tl.load(in_ptr0 + idx) + y = tl.load(in_ptr1 + idx) + ret = x // y + tl.store(out_ptr0 + idx, ret) + + +def triton_func(x0, x1, N): + out = torch.empty_like(x0) + triton_kernel[1, 1, 1](out, x0, x1, N) + return out + + +types = [ + "int32", + "bool", +] + +shapes = [ + 3, + 32, + 37, + 256, + 781, +] + + +@pytest.mark.parametrize("sigtype", types) +@pytest.mark.parametrize("N", shapes) +def test_floordiv(sigtype, N): + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x1 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x1 = x1.masked_fill(x1 == 0, 1) + + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, N) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_for_ptr.py b/third_party/ascend/unittest/pytest_ut/test_for_ptr.py new file mode 100644 index 0000000000..5dd2477d9a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_for_ptr.py @@ -0,0 +1,116 @@ +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +types_all = [ + (torch.float32, 'float32'), +] + +shapes_common = [(128, 256), (127, 256), (127, 16), (129, 256), (77, 1024), (69, 512)] + +block_size = [128, 256, 1024] + + +def ceil_div(a, b): + return (a + b - 1) // b + + +def profiler_wrapper(fn, *args): + result_path = "./result_profiling_for" + skip_first = 10 + wait = 0 + warmup = 3 + active = 30 + repeat = 1 + stream = torch.npu.current_stream() + experimental_config = torch_npu.profiler._ExperimentalConfig( + aic_metrics=torch_npu.profiler.AiCMetrics.PipeUtilization, + profiler_level=torch_npu.profiler.ProfilerLevel.Level1, l2_cache=False, data_simplification=False) + with torch_npu.profiler.profile( + activities=[torch_npu.profiler.ProfilerActivity.CPU, torch_npu.profiler.ProfilerActivity.NPU], + schedule=torch_npu.profiler.schedule(wait=wait, warmup=warmup, active=active, repeat=repeat, + skip_first=skip_first), + on_trace_ready=torch_npu.profiler.tensorboard_trace_handler(result_path), record_shapes=True, + profile_memory=False, with_stack=False, with_flops=False, with_modules=False, + experimental_config=experimental_config) as prof: + stream.synchronize() + for _ in range(skip_first + (wait + warmup + active) * repeat): + fn(*args) + prof.step() + stream.synchronize() + + +@triton.jit +def for_ptr_kernel( + in_ptr, + output_ptr, + N: tl.constexpr, + M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, +): + pid = tl.program_id(axis=0) + offset = 2 * pid * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + x_in = in_ptr + offset + x_out = output_ptr + offset + for _ in range(0, 2): + mask = (offset < M * N) + data = tl.load(x_in, mask=mask, other=0) + tl.store(x_out, data, mask) + x_in += BLOCK_SIZE_N + x_out += BLOCK_SIZE_N + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('M, N', shapes_common) +@pytest.mark.parametrize('BLOCK_SIZE_N', block_size) +def test_for_ptr(M, N, BLOCK_SIZE_N, dtype, sigtype): + + in_tensor = torch.randn(M, N, dtype=dtype).npu() + + triton_output = torch.zeros_like(in_tensor) + + grid = (ceil_div(2 * M * N, BLOCK_SIZE_N), ) + + for_ptr_kernel[grid](in_tensor, triton_output, N=N, M=M, BLOCK_SIZE_N=BLOCK_SIZE_N) + + assert torch.allclose(triton_output, in_tensor, rtol=1e-5, atol=1e-8) + + +def triton_lfor_ptr(in_tensor, BLOCK_SIZE): + M = in_tensor.shape[0] + N = in_tensor.shape[1] + + triton_output = torch.zeros_like(in_tensor) + grid = (ceil_div(2 * M * N, BLOCK_SIZE), ) + + for_ptr_kernel[grid](in_tensor, triton_output, N=N, M=M, BLOCK_SIZE_N=BLOCK_SIZE) + + +def profile_performance_test(M, N, dtype, BLOCK_SIZE): + print(f"\nDetailed performance analysis: M={M}, N={N}, dtype={dtype}, block_size={BLOCK_SIZE}") + + in_tensor = torch.randn(2 * M, N, dtype=dtype).npu() + + def wrapper_func(x): + triton_lfor_ptr(x, BLOCK_SIZE=BLOCK_SIZE) + + # Run performance analysis + profiler_wrapper(wrapper_func, in_tensor) + + +if __name__ == "__main__": + print("For Kernel Performance Test Suite") + print("Function: Broadcast first element") + + # Optional: Run detailed profiler test (specific configuration) + profile_performance_test(512, 512, torch.float32, BLOCK_SIZE=1024) + + print("\n" + "=" * 80) + print("Test completed!") + print(f"Detailed performance analysis results saved in: ./result_profiling_for/") + print("=" * 80) diff --git a/third_party/ascend/unittest/pytest_ut/test_full.py b/third_party/ascend/unittest/pytest_ut/test_full.py new file mode 100644 index 0000000000..a366ace2c1 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_full.py @@ -0,0 +1,95 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu +import pytest + + +@triton.jit +def fn_npu_f32(output_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + ret = tl.full((XB, YB, ZB), value=100, dtype=tl.float32) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_f16(output_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + ret = tl.full((XB, YB, ZB), value=100, dtype=tl.float16) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_i8(output_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + ret = tl.full((XB, YB, ZB), value=100, dtype=tl.int8) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +testlist = [ + (fn_npu_f32, 'float32', torch.float32, 2, 256, 16), + (fn_npu_f32, 'float32', torch.float32, 8, 8, 4), + (fn_npu_f16, 'float16', torch.float16, 2, 256, 16), + (fn_npu_f16, 'float16', torch.float16, 8, 8, 4), + (fn_npu_i8, 'int8', torch.int8, 2, 256, 16), + (fn_npu_i8, 'int8', torch.int8, 8, 8, 4), +] + + +@pytest.mark.parametrize('testfunc, sigtype, dtype, XB, YB, ZB', testlist) +def test_npu(testfunc, sigtype, dtype, XB, YB, ZB): + + x = torch.full((XB, YB, ZB), 100, dtype=dtype).npu() + + print(f"shape = {x.shape}") + print(x.dtype) + print(x[0, 0:16, 0]) + + output = torch.randint(1, (XB, YB, ZB), dtype=dtype).npu() + + print(f"output.dtype={output.dtype}") + + testfunc[1, 1, 1](output, XB, YB, ZB, debug=True) + print(output[0, 0:16, 0]) + + test_common.validate_cmp(sigtype, output, x) diff --git a/third_party/ascend/unittest/pytest_ut/test_full_row_copy_inlining.py b/third_party/ascend/unittest/pytest_ut/test_full_row_copy_inlining.py new file mode 100644 index 0000000000..b98524c192 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_full_row_copy_inlining.py @@ -0,0 +1,52 @@ +"""Only compiler-versioned A5 SIMD row copies opt into libcall inlining.""" +import subprocess +from types import SimpleNamespace + +import pytest + +from triton.backends.ascend import compiler + + +@pytest.mark.parametrize("marked,arch,mode,parallel,expected", [ + (True, "Ascend950PR_9579", "aiv", "simd", True), + (False, "Ascend950PR_9579", "aiv", "simd", False), + (True, "Ascend910B4", "aiv", "simd", False), + (True, "Ascend950PR_9579", "mix", "mix_simd_simt", False), + (True, "Ascend950PR_9579", "aiv", "simt", False), +]) +def test_full_row_copy_inlining_scope(monkeypatch, marked, arch, mode, parallel, expected): + monkeypatch.setattr(compiler, "_supports_full_row_copy_inlining", lambda _path: True) + metadata = dict(has_full_row_copy=marked, target=SimpleNamespace(arch=arch), mix_mode=mode, parallel_mode=parallel) + assert compiler._full_row_copy_compile_options( + metadata, "/compiler") == (["--enable-lib-call-no-inline=false"] if expected else []) + + +@pytest.mark.parametrize("status,output,supported", [(0, "--enable-lib-call-no-inline", True), (0, "", False), + (1, "--enable-lib-call-no-inline", False)]) +def test_compiler_option_probe(monkeypatch, status, output, supported): + compiler._supports_full_row_copy_inlining.cache_clear() + monkeypatch.setattr(compiler.subprocess, "run", + lambda *_args, **_kwargs: SimpleNamespace(returncode=status, stdout=output)) + assert compiler._supports_full_row_copy_inlining("/compiler") == supported + compiler._supports_full_row_copy_inlining.cache_clear() + + +def test_probe_failure_keeps_default(monkeypatch): + compiler._supports_full_row_copy_inlining.cache_clear() + + def fail(*_args, **_kwargs): + raise subprocess.TimeoutExpired("/compiler", 10) + + monkeypatch.setattr(compiler.subprocess, "run", fail) + metadata = dict(has_full_row_copy=True, target=SimpleNamespace(arch="Ascend950PR"), mix_mode="aiv", + parallel_mode="simd") + assert compiler._full_row_copy_compile_options(metadata, "/compiler") == [] + compiler._supports_full_row_copy_inlining.cache_clear() + + +@pytest.mark.parametrize("attrs,expected", [("tt.full_row_copy", True), ("tt.full_row_copy_extra", False), ("", False)]) +def test_marker_is_derived_from_ir(attrs, expected): + linalg = ('module attributes {' + attrs + '} { func.func @kernel() ' + 'attributes {mix_mode = "aiv", parallel_mode = "simd"} { return } }') + _, metadata = compiler._parse_linalg_metadata(linalg, {"has_full_row_copy": not expected}) + assert metadata["has_full_row_copy"] == expected diff --git a/third_party/ascend/unittest/pytest_ut/test_fusedattention.py b/third_party/ascend/unittest/pytest_ut/test_fusedattention.py new file mode 100644 index 0000000000..f884dd3dbe --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_fusedattention.py @@ -0,0 +1,354 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Fused Attention +=============== + +This is a Triton implementation of the Flash Attention v2 algorithm from Tri Dao (https://tridao.me/publications/flash2/flash2.pdf) + +Credits: OpenAI kernel team + +Extra Credits: + +* Original flash attention paper (https://arxiv.org/abs/2205.14135) +* Rabe and Staats (https://arxiv.org/pdf/2112.05682v2.pdf) + +""" + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import triton.language.extra.cann.extension as extension + +DEVICE = "npu" + + +@triton.jit +def _attn_fwd_inner(acc_ptr, l_i, m_i, q, # Accumulator, local l, local m, query vector + K_block_ptr, V_block_ptr, # Key and value block pointers for current stage + start_m, qk_scale, # Starting position of current query block, qk scale factor + BLOCK_M: tl.constexpr, HEAD_DIM: tl.constexpr, BLOCK_N: tl.constexpr, # Block size constants + STAGE: tl.constexpr, offs_m: tl.constexpr, + offs_n: tl.constexpr, # Current stage flag, m and n offset indices + N_CTX: tl.constexpr, + fp8_v: tl.constexpr): # Total context length, whether to enable FP8 for value precision + # Set the processing range [lo, hi) for the current stage (in column block units) + # causal = true + # stage = 1 + # Causal attention, as the name implies, restricts the flow of information during computation, + # only allowing the model to see the current and previous positions. + # In other words, the output at the current position can only depend on the input at or before this position, + # and cannot access information from future positions. + # Causal attention ensures sequential order and prevents "leakage of future information." + # But the following logic will also be triggered + if STAGE == 1: + # Stage 1: process all tokens before the query block + tl.static_assert(BLOCK_M >= BLOCK_N) + lo, hi = 0, start_m * BLOCK_M + elif STAGE == 2: + # Stage 2: process the current query block + tl.static_assert(BLOCK_M >= BLOCK_N) + lo, hi = start_m * BLOCK_M, (start_m + 1) * BLOCK_M + lo = tl.multiple_of(lo, BLOCK_M) # Align starting position + # causal = False (no need for masking) + else: + lo, hi = 0, N_CTX # Process the entire context + + # Adjust K and V block pointers to the starting position `lo` + K_block_ptr = tl.advance(K_block_ptr, (lo, 0)) # K is [HEAD_DIM, N_CTX], shift along the second dim by lo + V_block_ptr = tl.advance(V_block_ptr, (lo, 0)) # V is [N_CTX, HEAD_DIM], shift along the first dim by lo + + # Index mapping for the accumulator , used for slicing when HEAD_DIM >= 256 + row = tl.arange(0, BLOCK_M)[:, None] + col_head_dim = tl.arange(0, HEAD_DIM)[None, :] + block2d_acc = row * HEAD_DIM + col_head_dim + + # Iterate over all k, v blocks in the current stage and accumulate the output + for start_n in range(lo, hi, BLOCK_N): # Process BLOCK_N columns at a time + start_n = tl.multiple_of(start_n, BLOCK_N) # Align column start position + # -- Compute qk ---- + k = tl.load(K_block_ptr) + # Modify K + trans_k = tl.trans(k) + qk = tl.dot(q, trans_k) + # Apply causal mask for STAGE 2 + if STAGE == 2: + mask = offs_m[:, None] >= (start_n + offs_n[None, :]) # Construct upper triangular mask + qk = qk * qk_scale + tl.where(mask, 0, -1.0e6) # Set invalid positions to -∞ + m_ij = tl.maximum(m_i, tl.max(qk, 1)) # Update m_ij = max(m_i, max(qk)) + qk -= m_ij[:, None] # Subtract max for softmax stability + else: + qk = qk * qk_scale + m_ij = tl.maximum(m_i, tl.max(qk, 1)) # Scaled max + qk = qk - m_ij[:, None] # Stabilize + + # Softmax weights p = exp(qk) + p = tl.math.exp(qk) + + # Convert softmax weight type depending on FP8 usage + if fp8_v: + p_cast = p.to(tl.float8e5) # Convert to FP8 format (save memory) + else: + p_cast = p.to(k.dtype) + + v = tl.load(V_block_ptr) # Load corresponding V block + pv = tl.dot(p_cast, v) + l_ij = tl.sum(p, 1) # Softmax denominator (sum of each row) + # -- Update m_i and l_i + alpha = tl.math.exp(m_i - m_ij) # Update factor: exp difference between old and new max + l_i = l_i * alpha + l_ij # Update softmax denominator + # -- Update output accumulator -- + if HEAD_DIM < 256: + acc_ptr = acc_ptr * alpha[:, None] + acc_ptr = tl.dot(p_cast, v, acc_ptr) + else: + # 1. Load current slice of accumulator + acc = tl.load(acc_ptr + block2d_acc) + # 2. Update in slices (split by 1/4 of BLOCK_M to avoid ub overflow) + for i in range(4): + # Calculate start/end rows for current slice + offset = i * (BLOCK_M // 4) + # Extract slice data + acc_i = extension.extract_slice(acc, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + alpha_i = extension.extract_slice(alpha, [offset], [BLOCK_M // 4], [1]) + pv_i = extension.extract_slice(pv, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + # Incrementally update slice: acc = acc * alpha + pv + acc_i = acc_i * alpha_i[:, None] + pv_i + # Write updated slice back to accumulator + acc = extension.insert_slice(acc, acc_i, (offset, 0), (BLOCK_M // 4, HEAD_DIM), (1, 1)) + # 3. updated accumulator + tl.store(acc_ptr + block2d_acc, acc) + + m_i = m_ij # Update current block max + # Advance V and K block pointers to next BLOCK_N range + V_block_ptr = tl.advance(V_block_ptr, (BLOCK_N, 0)) + K_block_ptr = tl.advance(K_block_ptr, (BLOCK_N, 0)) + # Return accumulated output acc_ptr, softmax denominator l_i, and max value m_i + return acc_ptr, l_i, m_i + + +@triton.jit +def _attn_fwd(Q, K, V, M, Out, acc, sm_scale, stride_qz: tl.constexpr, stride_qh: tl.constexpr, stride_qm: tl.constexpr, + stride_qk: tl.constexpr, stride_kz: tl.constexpr, stride_kh: tl.constexpr, stride_kn: tl.constexpr, + stride_kk: tl.constexpr, stride_vz: tl.constexpr, stride_vh: tl.constexpr, stride_vn: tl.constexpr, + stride_vk: tl.constexpr, stride_oz: tl.constexpr, stride_oh: tl.constexpr, stride_om: tl.constexpr, + stride_on: tl.constexpr, Z: tl.constexpr, H: tl.constexpr, N_CTX: tl.constexpr, HEAD_DIM: tl.constexpr, + BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, STAGE: tl.constexpr): + # Total number of blocks in sequence dimension (M) + NUM_BLOCKS_M = N_CTX // BLOCK_M + # Total tasks = number of sequence blocks × batch size (Z) × number of attention heads (H) + NUM_BLOCKS = NUM_BLOCKS_M * Z * H + + # Current M-dimension block index + pid = tl.program_id(0) + + for block_idx in range(pid, NUM_BLOCKS, 20): + task_hz_idx = block_idx // NUM_BLOCKS_M + task_m_idx = block_idx % NUM_BLOCKS_M + off_z = task_hz_idx // H + off_h = task_hz_idx % H + qvk_offset = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh + # Create block pointers for Q, K, V, Output + Q_block_ptr = tl.make_block_ptr( + base=Q + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_qm, stride_qk), + offsets=(task_m_idx * BLOCK_M, 0), + block_shape=(BLOCK_M, HEAD_DIM), + order=(1, 0), + ) + V_block_ptr = tl.make_block_ptr( + base=V + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_vn, stride_vk), + offsets=(0, 0), + block_shape=(BLOCK_N, HEAD_DIM), + order=(1, 0), + ) + K_block_ptr = tl.make_block_ptr( + base=K + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_kn, stride_kk), + offsets=(0, 0), + block_shape=(BLOCK_N, HEAD_DIM), + order=(1, 0), + ) + O_block_ptr = tl.make_block_ptr( + base=Out + qvk_offset, + shape=(N_CTX, HEAD_DIM), + strides=(stride_om, stride_on), + offsets=(task_m_idx * BLOCK_M, 0), + block_shape=(BLOCK_M, HEAD_DIM), + order=(1, 0), + ) + # Initialize offsets + offs_m = task_m_idx * BLOCK_M + tl.arange(0, BLOCK_M) + offs_n = tl.arange(0, BLOCK_N) + + m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf") + l_i = tl.zeros([BLOCK_M], dtype=tl.float32) + 1.0 + + # Initialize accumulator + if HEAD_DIM < 256: + acc_ptr = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32) + else: + acc_offset = (off_z.to(tl.int64) * stride_qz // stride_qm * HEAD_DIM + + off_h.to(tl.int64) * stride_qh // stride_qm * HEAD_DIM + task_m_idx * BLOCK_M * HEAD_DIM) + acc_ptr = acc + acc_offset + + # load q: it will stay in SRAM throughout + q = tl.load(Q_block_ptr) + + # stage 1: off-band + # For causal = True, STAGE = 3 and _attn_fwd_inner gets 1 as its STAGE + # For causal = False, STAGE = 1, and _attn_fwd_inner gets 3 as its STAGE + if STAGE & 1: + acc_ptr, l_i, m_i = _attn_fwd_inner(acc_ptr, l_i, m_i, q, K_block_ptr, V_block_ptr, # + task_m_idx, sm_scale, # + BLOCK_M, HEAD_DIM, BLOCK_N, # + 4 - STAGE, offs_m, offs_n, N_CTX, V.dtype.element_ty == tl.float8e5 # + ) + # stage 2: on-band + if STAGE & 2: + # barrier makes it easier for compielr to schedule the + # two loops independently + acc_ptr, l_i, m_i = _attn_fwd_inner(acc_ptr, l_i, m_i, q, K_block_ptr, V_block_ptr, # + task_m_idx, sm_scale, # + BLOCK_M, HEAD_DIM, BLOCK_N, # + 2, offs_m, offs_n, N_CTX, V.dtype.element_ty == tl.float8e5 # + ) + + m_i += tl.math.log(l_i) + if HEAD_DIM < 256: + accumulator = acc_ptr / l_i[:, None] + else: + row = tl.arange(0, BLOCK_M)[:, None] + col_head_dim = tl.arange(0, HEAD_DIM)[None, :] + block2d_acc = row * HEAD_DIM + col_head_dim + accumulator = tl.load(acc_ptr + block2d_acc) + accumulator = accumulator / l_i[:, None] + + m_ptrs = M + task_hz_idx * N_CTX + offs_m + + tl.store(m_ptrs, m_i) + tl.store(O_block_ptr, accumulator.to(Out.type.element_ty)) + + +class _attention(torch.autograd.Function): + + @staticmethod + def forward(ctx, q, k, v, causal, sm_scale, BM, BN): + """ + Forward computation interface: + Args: + ctx: Context object + q: Query tensor (Q), shape [Z, H, N_CTX, HEAD_DIM] + k: Key tensor (K), shape [Z, H, N_CTX, HEAD_DIM] + v: Value tensor (V), shape [Z, H, N_CTX, HEAD_DIM] + causal: Whether to enable causal attention + sm_scale: Scaling factor for QK product + BM: Q block size (BLOCK_M) + BN: K/V block size (BLOCK_N) + Returns: + o: Attention output tensor, shape [Z, H, N_CTX, HEAD_DIM] + """ + # shape constraints + HEAD_DIM_Q, HEAD_DIM_K = q.shape[-1], k.shape[-1] + # when v is in float8_e5m2 it is transposed. + HEAD_DIM_V = v.shape[-1] + assert HEAD_DIM_Q == HEAD_DIM_K and HEAD_DIM_K == HEAD_DIM_V + assert HEAD_DIM_K in {16, 32, 64, 128, 256} + + o = torch.empty_like(q) + stage = 3 if causal else 1 + extra_kern_args = {} + + # Number of NPU cores (adjust based on hardware) + num_cores = 20 + acc = torch.zeros((q.shape[0], q.shape[1], q.shape[2], HEAD_DIM_K), dtype=torch.float32, device=q.device) + M = torch.empty((q.shape[0], q.shape[1], q.shape[2]), device=q.device, dtype=torch.float32) + + _attn_fwd[(num_cores, )](q, k, v, M, o, acc, sm_scale, q.stride(0), q.stride(1), q.stride(2), q.stride(3), + k.stride(0), k.stride(1), k.stride(2), k.stride(3), v.stride(0), v.stride(1), + v.stride(2), v.stride(3), o.stride(0), o.stride(1), o.stride(2), o.stride(3), + q.shape[0], q.shape[1], N_CTX=q.shape[2], HEAD_DIM=HEAD_DIM_K, BLOCK_M=BM, BLOCK_N=BN, + STAGE=stage, **extra_kern_args) + + ctx.save_for_backward(q, k, v, o, M) + ctx.sm_scale = sm_scale + ctx.HEAD_DIM = HEAD_DIM_K + ctx.causal = causal + return o + + +attention = _attention.apply + + +@pytest.mark.parametrize("Z, H, N_CTX, HEAD_DIM, causal, dtype, BM, BN", [ + (1, 1, 128, 128, False, torch.float16, 32, 128), + (1, 1, 128, 128, False, torch.bfloat16, 64, 128), + (1, 2, 256, 256, False, torch.bfloat16, 32, 256), + (2, 2, 128, 256, False, torch.float16, 64, 128), + (4, 32, 64, 64, False, torch.float16, 32, 64), + (4, 32, 1024, 64, False, torch.bfloat16, 64, 128), + (4, 32, 4096, 64, False, torch.float16, 128, 128), +]) +def test_op(Z, H, N_CTX, HEAD_DIM, causal, dtype, BM, BN): + # Filter out non-integer cases; N_CTX must be divisible by BM and BN, and HEAD_DIM must be divisible by 16. + if N_CTX % BM != 0 or N_CTX % BN != 0 or HEAD_DIM % 16 != 0: + pytest.skip("Skipping non-divisible case") + + torch.manual_seed(20) + q = (torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_()) + k = (torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_()) + v = (torch.empty((Z, H, N_CTX, HEAD_DIM), dtype=dtype, device=DEVICE).normal_(mean=0.0, std=0.5).requires_grad_()) + + sm_scale = 0.5 + + tri_out = attention(q, k, v, causal, sm_scale, BM, BN) + ref_out = torch_npu.npu_fusion_attention( + q, + k, + v, + H, + padding_mask=None, + atten_mask=None, + scale=sm_scale, + keep_prob=1.0, + input_layout="BNSD", + pre_tockens=65535, + next_tockens=65535, + sparse_mode=0, + )[0] + + torch.testing.assert_close(ref_out, tri_out, atol=1e-2, rtol=1e-2, equal_nan=True) + print(f"[PASSED] Attention shape:({Z}, {H}, {N_CTX}, {HEAD_DIM}), BM: {BM}, BN: {BN}, dtype: {dtype}") + + +if __name__ == "__main__": + test_op(1, 1, 128, 128, causal=False, dtype=torch.float16, BM=32, BN=128) + test_op(1, 1, 128, 128, causal=False, dtype=torch.bfloat16, BM=64, BN=128) + test_op(1, 2, 256, 256, causal=False, dtype=torch.bfloat16, BM=32, BN=256) + test_op(2, 2, 128, 256, causal=False, dtype=torch.float16, BM=64, BN=128) + test_op(4, 32, 64, 64, causal=False, dtype=torch.float16, BM=32, BN=64) + test_op(4, 32, 1024, 64, causal=False, dtype=torch.bfloat16, BM=64, BN=128) + test_op(4, 32, 4096, 64, causal=False, dtype=torch.float16, BM=128, BN=128) diff --git a/third_party/ascend/unittest/pytest_ut/test_gamma.py b/third_party/ascend/unittest/pytest_ut/test_gamma.py new file mode 100644 index 0000000000..df2ec0b014 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_gamma.py @@ -0,0 +1,85 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common +import triton.language.extra.cann.libdevice as libdevice +import numpy as np +from scipy.special import gamma + + +@triton.jit +def triton_gamma(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = tl.load(in_ptr0 + xindex, xmask) + y = libdevice.gamma(x0) + tl.store(out_ptr0 + xindex, y, xmask) + + +@pytest.mark.skip(reason="Wait for AscendNPU-IR support") +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_gamma_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = torch.abs(test_common.generate_tensor(shape, dtype)) + x_np = x.cpu().numpy() + x = x.npu() + y_ref = torch.from_numpy(gamma(x_np)).to(x.device).to(x.dtype).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_gamma[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.skip(reason="Wait for AscendNPU-IR support") +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_all_blocks_parallel(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = torch.abs(test_common.generate_tensor(shape, dtype)) + x_np = x.cpu().numpy() + x = x.npu() + y_ref = torch.from_numpy(gamma(x_np)).to(x.device).to(x.dtype).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_gamma[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.skip(reason="Wait for AscendNPU-IR support") +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 2048, 8), 2, 32768, 512], +]) +def test_auto_blockify(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = torch.abs(test_common.generate_tensor(shape, dtype)) + x_np = x.cpu().numpy() + x = x.npu() + y_ref = torch.from_numpy(gamma(x_np)).to(x.device).to(x.dtype).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_gamma[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub, auto_blockify_size=ncore) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_gather.py b/third_party/ascend/unittest/pytest_ut/test_gather.py new file mode 100644 index 0000000000..08946f8944 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_gather.py @@ -0,0 +1,73 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import triton.language.extra.cann.extension as extension +import numpy as np +import test_common +import pytest + + +@pytest.mark.skip(reason="waiting for the compiler to support.") +@pytest.mark.parametrize("src_shape, indices_shape, axis", [ + ([4, 4], [8, 4], 0), + ([4, 64], [4, 32], 1), + ([128, 64], [128, 128], 1), +]) +def test_gather(src_shape, indices_shape, axis): + + @triton.jit + def gather_kernel(src_ptr, idx_ptr, out_ptr, axis: tl.constexpr, src_dim0: tl.constexpr, src_dim1: tl.constexpr, + src_stride0: tl.constexpr, src_stride1: tl.constexpr, idx_dim0: tl.constexpr, + idx_dim1: tl.constexpr, idx_stride0: tl.constexpr, idx_stride1: tl.constexpr, + out_dim0: tl.constexpr, out_dim1: tl.constexpr, out_stride0: tl.constexpr, + out_stride1: tl.constexpr): + src_offs = (tl.arange(0, src_dim0)[:, None] * src_stride0 + tl.arange(0, src_dim1)[None, :] * src_stride1) + src = tl.load(src_ptr + src_offs) + + idx_offs = (tl.arange(0, idx_dim0)[:, None] * idx_stride0 + tl.arange(0, idx_dim1)[None, :] * idx_stride1) + idx = tl.load(idx_ptr + idx_offs) + + out = extension.gather(src, idx, axis) + + out_offs = (tl.arange(0, out_dim0)[:, None] * out_stride0 + tl.arange(0, out_dim1)[None, :] * out_stride1) + tl.store(out_ptr + out_offs, out) + + def triton_gather(src: torch.Tensor, axis: int, indices: torch.Tensor): + output = torch.empty(indices.shape, dtype=src.dtype, device=src.device) + gather_kernel[(1, )](src, indices, output, axis, src.shape[0], src.shape[1], + src.stride(0), src.stride(1), indices.shape[0], indices.shape[1], indices.stride(0), + indices.stride(1), output.shape[0], output.shape[1], output.stride(0), output.stride(1)) + return output + + DEV = "npu" + src = torch.randn(src_shape, device=DEV) + indices = torch.randint(0, src.shape[axis], indices_shape, device=DEV) + ref = torch.gather(src, axis, indices) + result = triton_gather(src, axis, indices) + torch.testing.assert_close(result, ref, rtol=0, atol=0) + + +if __name__ == "__main__": + test_gather([4, 64], [4, 32], 1) + print("success: test_gather") diff --git a/third_party/ascend/unittest/pytest_ut/test_gather_gemv.py b/third_party/ascend/unittest/pytest_ut/test_gather_gemv.py new file mode 100644 index 0000000000..42c837e17d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_gather_gemv.py @@ -0,0 +1,116 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import test_common + + +def torch_gather_gemv(weight, indices, vector): + """Reference: gather matrices from weight by indices (with negative clamping), + then do batched matrix-vector multiply. + + Args: + weight: (B, S, S) float16 + indices: (N,) int64, values in [-B, B-1] + vector: (S,) float16 + Returns: + result: (N, S) float16 + """ + B = weight.shape[0] + indices_clamped = torch.where(indices < 0, indices + B, indices) + gathered = weight[indices_clamped].float() # (N, S, S) + result = torch.matmul(gathered, vector.float().unsqueeze(-1)).squeeze(-1) + return result.to(torch.float16) + + +@triton.jit +def triton_gather_gemv_kernel( + in_ptr0, # indices: (N,) int64 + in_ptr1, # weight: (B, S, S) float16 + in_ptr2, # vector: (S,) float16 + out_ptr1, # output: (N*S,) float16 + xnumel, + rnumel, + XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr, +): + xoffset = tl.program_id(0).to(tl.int64) * XBLOCK + xindex = xoffset + tl.arange(0, XBLOCK)[:, None].to(tl.int64) + rbase = tl.arange(0, RBLOCK)[None, :].to(tl.int64) + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0 // rnumel), None, eviction_policy="evict_last") + _tmp11 = tl.full([XBLOCK, RBLOCK], 0, tl.float32) + for roffset in range(0, rnumel, RBLOCK): + rindex = roffset + rbase + r1 = rindex + tmp7 = tl.load(in_ptr2 + (r1), None, eviction_policy="evict_last").to(tl.float32) + tmp1 = tmp0 + 8 + tmp2 = tmp0 < 0 + tmp3 = tl.where(tmp2, tmp1, tmp0) + tmp4 = tl.load( + in_ptr1 + (r1 + (rnumel * (x0 % rnumel)) + (rnumel * rnumel * tmp3)), + None, + eviction_policy="evict_first", + ) + tmp5 = tmp4.to(tl.float32) + tmp8 = tmp7.to(tl.float32) + tmp9 = tmp5 * tmp8 + tmp10 = tl.broadcast_to(tmp9, [XBLOCK, RBLOCK]) + tmp12 = _tmp11 + tmp10 + _tmp11 = tmp12 + tmp11 = tl.sum(_tmp11, 1)[:, None] + tmp13 = tmp11.to(tl.float32) + tl.store(out_ptr1 + (x0), tmp13, None) + + +@pytest.mark.parametrize('S', [2048]) +@pytest.mark.parametrize('config', [(1, 2048), (64, 8)], ids=['xblock1_rblock2048', 'xblock64_rblock8']) +def test_gather_gemv(S, config): + XBLOCK, RBLOCK = config + B = 8 + N = 2 + + indices = torch.randint(0, B, (N, ), dtype=torch.int64).npu() + weight = test_common.generate_tensor((B, S, S), 'float16').npu() + vector = test_common.generate_tensor((S, ), 'float16').npu() + + xnumel = N * S + rnumel = S + out = torch.empty((xnumel, ), dtype=torch.float16).npu() + grid = (triton.cdiv(xnumel, XBLOCK), ) + triton_gather_gemv_kernel[grid]( + indices, + weight, + vector, + out, + xnumel, + rnumel, + XBLOCK=XBLOCK, + RBLOCK=RBLOCK, + num_stages=1, + num_warps=8, + ) + triton_res = out.view(N, S) + torch_res = torch_gather_gemv(weight, indices, vector) + test_common.validate_cmp("float16", triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_gather_simd.py b/third_party/ascend/unittest/pytest_ut/test_gather_simd.py new file mode 100644 index 0000000000..6225e4ef2c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_gather_simd.py @@ -0,0 +1,46 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Unit test for gather_2d_simd kernel. +""" + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest + +from triton.language.extra.kernels import gather_2d_simd + + +@pytest.mark.parametrize("M,N,K", [ + (32, 128, 64), +]) +def test_gather_2d_simd(M, N, K): + """Test gather_2d_simd with various tensor sizes.""" + src = torch.randn(M, N, dtype=torch.float32, device='npu') + indices = torch.randint(0, N, (M, K), dtype=torch.int32, device='npu') + output = torch.empty((M, K), dtype=src.dtype, device='npu') + + grid = (triton.cdiv(M, 32), ) + gather_2d_simd[grid](src, indices, output, M, N, K, XBLOCK=32, XBLOCK_SUB=4) + + ref = torch.gather(src, 1, indices.long()) + assert torch.allclose(output, ref, rtol=1e-5, atol=1e-5) diff --git a/third_party/ascend/unittest/pytest_ut/test_ge.py b/third_party/ascend/unittest/pytest_ut/test_ge.py new file mode 100644 index 0000000000..e86e6dc632 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ge.py @@ -0,0 +1,79 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_binary(x0, y0): + res = x0 >= y0 + return res + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x >= y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + (torch.int8, 'int8'), + (torch.int16, 'int16'), + (torch.int32, 'int32'), + (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + y0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + ans = standard_binary(x0, y0) + out = torch.zeros((N, ), dtype=torch.bool).npu() + triton_elementwise_binary[1, 1, 1](x0, y0, out, N, NUMEL) + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_ge_2.py b/third_party/ascend/unittest/pytest_ut/test_ge_2.py new file mode 100644 index 0000000000..c5e9b6ebc2 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ge_2.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_ge(x0, x1, dtype): + res = torch.where(torch.ge(x0, x1), torch.ones_like(x0), torch.zeros_like(x0)).to(eval('torch.' + dtype)) + return res + + +@triton.jit +def triton_ge(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 >= tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_ge(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_ge(x0, x1, dtype) + # triton结果 + triton_res = torch.empty_like(x0) + triton_ge[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_gelu.py b/third_party/ascend/unittest/pytest_ut/test_gelu.py new file mode 100644 index 0000000000..ff6cfe2286 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_gelu.py @@ -0,0 +1,99 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = x0 * 0.5 * (1.0 + torch.erf(x0 / torch.sqrt(torch.tensor(2.0)))) + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = x * 0.5 * (1.0 + tl.erf(x / tl.sqrt(2.0))) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + # (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + # (torch.int8, 'int8'), + # (torch.int16, 'int16'), + # (torch.int32, 'int32'), + # (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_get_active_torch_device.py b/third_party/ascend/unittest/pytest_ut/test_get_active_torch_device.py new file mode 100644 index 0000000000..b2cb1f0fc4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_get_active_torch_device.py @@ -0,0 +1,32 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import triton + + +@pytest.mark.backend("torch_npu") +def test_get_active_torch_device_returns_npu_device(): + device = triton.runtime.driver.active.get_active_torch_device() + + assert isinstance(device, torch.device) + assert device.type == "npu" + assert device.index == torch.npu.current_device() diff --git a/third_party/ascend/unittest/pytest_ut/test_get_element.py b/third_party/ascend/unittest/pytest_ut/test_get_element.py new file mode 100644 index 0000000000..4b57b6fb2f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_get_element.py @@ -0,0 +1,49 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al + +import torch +import torch_npu +import pytest +import test_common + +# --------------- +# test get_element op +# --------------- + + +@triton.jit +def get_element_kernel(x_ptr, out_ptr, N: tl.constexpr, IDX: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(x_ptr + offs) + v = al.get_element(x, [IDX]) + tl.store(out_ptr, v) + + +@pytest.mark.parametrize("dtype", ['int8', 'int16', 'int32', 'int64', 'float16', 'float32', 'bfloat16', 'uint8']) +def test_get_element(dtype): + size, idx = 64, 10 + x = test_common.generate_tensor((size, ), dtype).npu() + out = torch.zeros(1, dtype=eval('torch.' + dtype)).npu() + get_element_kernel[(1, )](x, out, size, idx) + assert out.item() == x[idx].item(), (out.item(), x[idx].item()) diff --git a/third_party/ascend/unittest/pytest_ut/test_graph_optimize.py b/third_party/ascend/unittest/pytest_ut/test_graph_optimize.py new file mode 100644 index 0000000000..add03e578c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_graph_optimize.py @@ -0,0 +1,916 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import json +import os +import subprocess + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + +import pytest +import triton +import triton.language as tl +from triton._C.libtriton import ascend, ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, make_ttir +from triton.backends.ascend.program_grid import ( + DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK, + INDEPENDENT_AXIS_TENSORIZE_RULE_BIT, + PERSISTENT_TASK_STRIP_MINING_RULE_BIT, +) +from triton.compiler.code_generator import ast_to_ttir +from triton.compiler.compiler import ASTSource +from triton.errors import TritonError + +pytestmark = pytest.mark.backend("none") + + +@triton.jit +def graph_optimize_kernel(x_ptr, y_ptr, BLOCK: tl.constexpr): + offsets = tl.arange(0, BLOCK) + value = tl.load(x_ptr + offsets) + tl.store(y_ptr + offsets, value) + + +@triton.jit +def graph_optimize_legacy_memory_isolation_kernel(x_ptr, y_ptr, BLOCK: tl.constexpr): + # This is deliberately a strided AddPtr load/store shape. It is eligible + # for the legacy T2L StridedLoadStoreRewrite, but must remain untouched by + # the early generic graph-optimize pass even when its default mask carries + # all seven rule identities. + offsets = tl.arange(0, BLOCK) * 3 + value = tl.load(x_ptr + offsets) + tl.store(y_ptr + offsets, value) + + +@triton.jit +def diagonal_shift_forward_kernel(x_ptr, y_ptr, BLOCK: tl.constexpr): + # Shift a cumulative sum left by one the way the segmented-cumsum kernels + # spell it: a sub-diagonal mask over a BLOCK x BLOCK matrix, reduced back + # to one dimension. DiagonalMaskRemoval must collapse it to a subtraction. + offsets = tl.arange(0, BLOCK) + x = tl.load(x_ptr + offsets) + cumulative = tl.cumsum(x, axis=0) + rows = tl.arange(0, BLOCK)[:, None] + cols = tl.arange(0, BLOCK)[None, :] + matrix = tl.where(rows == cols + 1, cumulative[None, :], 0.0) + tl.store(y_ptr + offsets, tl.sum(matrix, axis=1)) + + +@triton.jit +def diagonal_shift_reverse_kernel(x_ptr, y_ptr, BLOCK: tl.constexpr): + # The mirrored form: a reverse scan shifted right by a super-diagonal mask. + offsets = tl.arange(0, BLOCK) + x = tl.load(x_ptr + offsets) + cumulative = tl.cumsum(x, axis=0, reverse=True) + rows = tl.arange(0, BLOCK)[:, None] + cols = tl.arange(0, BLOCK)[None, :] + matrix = tl.where(rows == cols - 1, cumulative[None, :], 0.0) + tl.store(y_ptr + offsets, tl.sum(matrix, axis=1)) + + +@triton.jit +def wrapped_tile_copy_kernel(src_ptr, dst_ptr, n, BLOCK_K: tl.constexpr, BLOCK_N: tl.constexpr): + # The fused-MoE weight-tile shape: the column offset wraps on a runtime + # dimension, and the store mask already discards the lanes past it. + # ConvertModuloToMask must drop the wrap and mask the load instead. + pid = tl.program_id(0) + offs_n = pid * BLOCK_N + tl.arange(0, BLOCK_N) + offs_k = tl.arange(0, BLOCK_K) + wrapped = offs_n % n + tile = tl.load(src_ptr + (offs_k[:, None] * n + wrapped[None, :])) + tl.store( + dst_ptr + (offs_k[:, None] * n + offs_n[None, :]), + tile, + mask=offs_n[None, :] < n, + ) + + +@triton.jit +def constexpr_wrapped_tile_copy_kernel(src_ptr, dst_ptr, N: tl.constexpr, BLOCK_K: tl.constexpr, BLOCK_N: tl.constexpr): + # Same shape with a compile-time bound. TritonToStructured can keep this + # wrap and re-express it as a strided access, which is exactly equivalent, + # so ConvertModuloToMask must leave it alone. + pid = tl.program_id(0) + offs_n = pid * BLOCK_N + tl.arange(0, BLOCK_N) + offs_k = tl.arange(0, BLOCK_K) + wrapped = offs_n % N + tile = tl.load(src_ptr + (offs_k[:, None] * N + wrapped[None, :])) + tl.store( + dst_ptr + (offs_k[:, None] * N + offs_n[None, :]), + tile, + mask=offs_n[None, :] < N, + ) + + +@triton.jit +def overflow_assert_provenance_kernel(output_ptr, n, BLOCK: tl.constexpr): + offsets = tl.arange(0, BLOCK) + checked_offset = offsets * n + # Deliberately reuse the automatic message. This must remain a user + # assertion without the private auto-overflow provenance marker. + tl.device_assert( + checked_offset >= 0, + "int32 overflow detected for operation mul", + ) + tl.store(output_ptr + checked_offset, 0.0) + + +@triton.jit +def fused_swiglu_fast_sigmoid(x): + return tl.fdiv(1.0, 1.0 + tl.exp(-x)) + + +@triton.jit +def fused_swiglu_fast_silu(x): + dtype = x.type.element_ty + x = x.to(tl.float32) + return tl.fdiv(x, 1.0 + tl.exp(-x)).to(dtype) + + +@triton.jit +def fused_swiglu_fast_silu_bwd(dy, x): + dtype = x.type.element_ty + dy = dy.to(tl.float32) + x = x.to(tl.float32) + sigmoid = fused_swiglu_fast_sigmoid(x) + return (dy * sigmoid * (1 + x * (1 - sigmoid))).to(dtype) + + +@triton.jit +def fused_swiglu_bwd_b_graph_optimize_kernel( + dy_ptr, + g_ptr, + fc_ptr, + dg_ptr, + dfc_ptr, + db_g_ptr, + db_fc_ptr, + M, + N, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, +): + """Test-only fixed-config clone of fused_swiglu_bwd_b_kernel. + + Keep the original [column, row] pointer layout deliberately: it is the + implicit transpose that LoadStoreTranspose rewrites. The body retains the + three loads, two loop stores, binary fanout, mask/other, loop-carried + reductions, and two final reduction stores of the task kernel. + """ + dtype = dy_ptr.type.element_ty + col_idx = tl.program_id(axis=0) + col_off = col_idx * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + row_off = tl.arange(0, BLOCK_SIZE_M) + dy_ptrs = dy_ptr + (row_off[None, :] * N + col_off[:, None]) + g_ptrs = g_ptr + (row_off[None, :] * N + col_off[:, None]) + fc_ptrs = fc_ptr + (row_off[None, :] * N + col_off[:, None]) + dg_ptrs = dg_ptr + (row_off[None, :] * N + col_off[:, None]) + dfc_ptrs = dfc_ptr + (row_off[None, :] * N + col_off[:, None]) + sum_b_g = tl.zeros((BLOCK_SIZE_N, BLOCK_SIZE_M), dtype=tl.float32) + sum_b_fc = tl.zeros((BLOCK_SIZE_N, BLOCK_SIZE_M), dtype=tl.float32) + + for row_idx in range(0, tl.cdiv(M, BLOCK_SIZE_M)): + mask = (row_off[None, :] < M - row_idx * BLOCK_SIZE_M) & (col_off[:, None] < N) + dy = tl.load(dy_ptrs, mask=mask, other=0.0).to(tl.float32) + g = tl.load(g_ptrs, mask=mask, other=0.0) + fc = tl.load(fc_ptrs, mask=mask, other=0.0).to(tl.float32) + silu_g = fused_swiglu_fast_silu(g) + dg = (dy * fc).to(dtype) + dg = fused_swiglu_fast_silu_bwd(dg, g) + dfc = (dy * silu_g.to(tl.float32)).to(dtype) + sum_b_g += dg.to(tl.float32) + sum_b_fc += dfc.to(tl.float32) + tl.store(dg_ptrs, dg, mask=mask) + tl.store(dfc_ptrs, dfc, mask=mask) + dy_ptrs += BLOCK_SIZE_M * N + g_ptrs += BLOCK_SIZE_M * N + fc_ptrs += BLOCK_SIZE_M * N + dg_ptrs += BLOCK_SIZE_M * N + dfc_ptrs += BLOCK_SIZE_M * N + + tl.store(db_g_ptr + col_off, tl.sum(sum_b_g, 1), mask=col_off < N) + tl.store(db_fc_ptr + col_off, tl.sum(sum_b_fc, 1), mask=col_off < N) + + +def make_ast_ttir(options): + source = ASTSource( + graph_optimize_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32"}, + {"BLOCK": 16}, + ) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + return ast_to_ttir(graph_optimize_kernel, source, context, options, {}, {}) + + +def make_legacy_memory_isolation_ast_ttir(options): + source = ASTSource( + graph_optimize_legacy_memory_isolation_kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32"}, + {"BLOCK": 16}, + ) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + return ast_to_ttir( + graph_optimize_legacy_memory_isolation_kernel, + source, + context, + options, + {}, + {}, + ) + + +def make_fused_swiglu_ttir(options, block_m=256, block_n=32): + """Compile the original-layout fused-SwiGLU clone only through TTIR. + + This deliberately avoids the backend binary pipeline, so the structural + 256x32 regression remains runnable on a host where that production tile is + too large for the local NPU UB. The separate small-tiling test exercises + the complete on-device pipeline. + """ + source = ASTSource( + fused_swiglu_bwd_b_graph_optimize_kernel, + { + "dy_ptr": "*bf16", + "g_ptr": "*bf16", + "fc_ptr": "*bf16", + "dg_ptr": "*bf16", + "dfc_ptr": "*bf16", + "db_g_ptr": "*bf16", + "db_fc_ptr": "*bf16", + "M": "i32", + "N": "i32", + }, + {"BLOCK_SIZE_M": block_m, "BLOCK_SIZE_N": block_n}, + ) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir( + fused_swiglu_bwd_b_graph_optimize_kernel, + source, + context, + options, + {}, + {}, + ) + return make_ttir(module, {}, options) + + +def assert_ttir_text_reparseable(ttir, tmp_path, name): + path = tmp_path / f"{name}.ttir.mlir" + path.write_text(ttir) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + reparsed = ir.parse_mlir_module(str(path), context) + assert "tt.func" in str(reparsed) + + +def assert_reparseable(module, tmp_path, name): + assert_ttir_text_reparseable(str(module), tmp_path, name) + + +def test_graph_optimize_pass_accepts_zero_rule_mask(tmp_path): + module = make_ast_ttir(NPUOptions(arch="Ascend910_95")) + pm = ir.pass_manager(module.context) + ascend.passes.ttir.add_graph_optimize(pm, rule_mask=0) + pm.run(module, "") + + assert "tt.func" in str(module) + assert_reparseable(module, tmp_path, "zero-rule-mask") + + +def test_default_graph_mask_enables_iat_and_ptsm(): + expected_mask = 511 | INDEPENDENT_AXIS_TENSORIZE_RULE_BIT | PERSISTENT_TASK_STRIP_MINING_RULE_BIT + + assert DEFAULT_GRAPH_OPTIMIZATION_RULE_MASK == expected_mask + assert NPUOptions(arch="Ascend910_95").rule_mask == expected_mask + + +def test_default_generic_graph_mask_excludes_legacy_memory_compatibility(tmp_path, ): + """The 8/16/32/64 identities are enabled by default, not generic rules. + + The generic GraphOptimizePass runs at early TTIR. Row, Axis, Chunk, and + StridedLoadStoreRewrite retain their original compatibility-pass slots, so + this strided memory shape must not acquire either coalescing metadata or + an indirect-memory op merely because the default mask is 3071. + """ + options = NPUOptions(arch="Ascend910_95", enable_graph_optimize=True) + default_result = make_ttir( + make_legacy_memory_isolation_ast_ttir(options), + {}, + options, + ) + text = str(default_result) + + assert "hacc.coalesce_factor" not in text + assert "hacc.coalesce_axis" not in text + assert "hacc.coalesce_grid_ceil_div" not in text + assert "tt.indirect_load" not in text + assert "tt.indirect_store" not in text + assert_reparseable(default_result, tmp_path, "generic-legacy-memory-isolation") + + +def test_default_graph_mask_preserves_native_graph_rule_bundle(monkeypatch, tmp_path): + """Default 3071 retains native 1|2|4 behavior, including StoreCoalescing, + without running legacy rules. + + This input has the LoadStoreTranspose (bit 1) structural signature. The + default all-identity mask must retain that native rewrite. Together with + the legacy isolation test above, this catches either failure mode: + accidentally dropping native rules, or scheduling compatibility passes + from the early generic graph pass. + """ + monkeypatch.setenv("TRITON_DUMP_DIR", str(tmp_path / "dump")) + + default_options = NPUOptions( + arch="Ascend910_95", + enable_graph_optimize=True, + debug=True, + sanitize_overflow=True, + ) + + default_ttir = str(make_fused_swiglu_ttir(default_options)) + + # The default must perform the native transpose rewrite to [M, N]. + assert "tensor<256x32x!tt.ptr>" in default_ttir + assert "tensor<32x256x!tt.ptr>" not in default_ttir + assert "hacc.coalesce_factor" not in default_ttir + assert "tt.indirect_load" not in default_ttir + assert "tt.indirect_store" not in default_ttir + assert_ttir_text_reparseable( + default_ttir, + tmp_path, + "default-native-graph-rule-bundle", + ) + + +def test_make_ttir_supports_graph_optimize_toggle(monkeypatch, tmp_path): + monkeypatch.setenv("TRITON_ALWAYS_COMPILE", "1") + + for enabled in (False, True): + options = NPUOptions( + arch="Ascend910_95", + enable_graph_optimize=enabled, + ) + for attempt in range(2): + module = make_ast_ttir(options) + result = make_ttir(module, {}, options) + + assert result is module + assert "tt.func" in str(result) + assert_reparseable( + result, + tmp_path, + f"graph-optimize-{int(enabled)}-{attempt}", + ) + + +def test_auto_overflow_assert_has_frontend_provenance_marker(): + source = ASTSource( + overflow_assert_provenance_kernel, + {"output_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 16}, + ) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir( + overflow_assert_provenance_kernel, + source, + context, + NPUOptions( + arch="Ascend910_95", + debug=True, + sanitize_overflow=True, + ), + {}, + {}, + ) + overflow_asserts = [ + line for line in str(module).splitlines() if '"int32 overflow detected for operation mul"' in line + ] + assert any("tt.auto_overflow_assert" in line for line in overflow_asserts) + assert any("tt.auto_overflow_assert" not in line for line in overflow_asserts) + + +def test_fused_swiglu_bwd_b_256x32_graph_optimize_structure(monkeypatch, tmp_path): + """Keep the original production tile as a TTIR-only structural gate.""" + # make_ttir() writes its debug dump through Triton's standard dump manager; + # keep test artifacts scoped to pytest's disposable directory. + monkeypatch.setenv("TRITON_DUMP_DIR", str(tmp_path / "dump")) + ttirs = {} + for enabled in (False, True): + options = NPUOptions( + arch="Ascend910_95", + enable_graph_optimize=enabled, + debug=True, + sanitize_overflow=True, + ) + module = make_fused_swiglu_ttir(options) + ttir = str(module) + assert_ttir_text_reparseable(ttir, tmp_path, f"fused-swiglu-256x32-{int(enabled)}") + (tmp_path / f"fused-swiglu-256x32-{int(enabled)}.ttir").write_text(ttir) + ttirs[enabled] = ttir + + off_ttir = ttirs[False] + on_ttir = ttirs[True] + assert off_ttir.count("tt.assert") == on_ttir.count("tt.assert") == 20 + assert on_ttir.count("tt.auto_overflow_assert") == 20 + assert "tensor<32x256x!tt.ptr>" in off_ttir + assert "tensor<32x256x!tt.ptr>" not in on_ttir + assert "tensor<256x32x!tt.ptr>" in on_ttir + for old_guard_shape in ( + "tensor<1x256xi64>", + "tensor<32x256xi64>", + "tensor<1x256xi1>", + "tensor<32x256xi1>", + ): + assert old_guard_shape not in on_ttir + + # This is the original 256x32 target from the task's 189-line comparison: + # the rule must emit the natural-layout structure itself, without relying + # on a post graph-optimize canonicalize/CSE/DCE cleanup. + assert on_ttir.count("tt.expand_dims") == 2 + assert on_ttir.count("tt.splat") == 15 + assert on_ttir.count("arith.muli") == 8 + + +def _require_npu(): + torch = pytest.importorskip("torch") + pytest.importorskip("torch_npu", exc_type=ImportError) + if not hasattr(torch, "npu") or not torch.npu.is_available(): + pytest.skip("Ascend NPU is unavailable") + return torch + + +def test_graph_optimize_numerical_equivalence(monkeypatch, tmp_path): + """Exercise the complete JIT pipeline on hardware with cache bypassed.""" + torch = _require_npu() + monkeypatch.setenv("TRITON_ALWAYS_COMPILE", "1") + source = torch.arange(16, dtype=torch.float32, device="npu") + outputs = {} + + for enabled in (False, True): + repeated_outputs = [] + for attempt in range(2): + # JITFunction has its own in-memory cache, so clear it before each + # launch to ensure TRITON_ALWAYS_COMPILE reaches triton.compile(). + graph_optimize_kernel.device_caches.clear() + output = torch.empty_like(source) + compiled = graph_optimize_kernel[(1, )]( + source, + output, + BLOCK=16, + enable_graph_optimize=enabled, + ) + torch.npu.synchronize() + assert_ttir_text_reparseable( + compiled.asm["ttir"], + tmp_path, + f"hardware-{int(enabled)}-{attempt}", + ) + repeated_outputs.append(output.cpu()) + + torch.testing.assert_close(repeated_outputs[0], source.cpu()) + torch.testing.assert_close(repeated_outputs[1], repeated_outputs[0]) + outputs[enabled] = repeated_outputs[-1] + + torch.testing.assert_close(outputs[True], outputs[False]) + + +def test_fused_swiglu_bwd_b_small_tiling_graph_optimize_equivalence(monkeypatch, tmp_path): + """Run the task kernel shape with a CANN-friendly validation tile. + + The production kernel's 256x32 autotune configuration is intentionally not + changed. The current environment's frozen validation tile is 64x8 (1/16 + of that tile); 32x8 remains an off-only fallback for smaller devices. + """ + torch = _require_npu() + torch_npu = pytest.importorskip("torch_npu") + monkeypatch.setenv("TRITON_ALWAYS_COMPILE", "1") + torch.manual_seed(20260724) + + m, n = 128, 64 + dtype = torch.bfloat16 + dy = torch.randn((m, n), dtype=dtype, device="npu") + g = torch.randn((m, n), dtype=dtype, device="npu") + fc = torch.randn((m, n), dtype=dtype, device="npu") + + def eager_reference(): + dy_f32 = dy.float() + g_f32 = g.float() + fc_f32 = fc.float() + sigmoid = 1.0 / (1.0 + torch.exp(-g_f32)) + # The kernel explicitly rounds this intermediate to bf16 before the + # SiLU backward expression, so reproduce that rounding in the oracle. + dg = (dy_f32 * fc_f32).to(dtype).float() + dg = (dg * sigmoid * (1.0 + g_f32 * (1.0 - sigmoid))).to(dtype) + silu_g = (g_f32 / (1.0 + torch.exp(-g_f32))).to(dtype) + dfc = (dy_f32 * silu_g.float()).to(dtype) + return ( + dg, + dfc, + dg.float().sum(dim=0).to(dtype), + dfc.float().sum(dim=0).to(dtype), + ) + + expected = tuple(value.cpu() for value in eager_reference()) + summary = { + "M": m, + "N": n, + "dtype": str(dtype), + "rtol": 3e-2, + "atol": 1e-1, + "enable_graph_optimize": [False, True], + "debug": True, + "sanitize_overflow": True, + "torch_version": torch.__version__, + "torch_npu_version": getattr(torch_npu, "__version__", "unknown"), + "cann_version": torch_npu.npu.utils.get_cann_version(), + "candidates": [], + } + + def run_mode(enabled, block_m, block_n, attempt): + fused_swiglu_bwd_b_graph_optimize_kernel.device_caches.clear() + dg = torch.empty((m, n), dtype=dtype, device="npu") + dfc = torch.empty((m, n), dtype=dtype, device="npu") + db_g = torch.empty((n, ), dtype=dtype, device="npu") + db_fc = torch.empty((n, ), dtype=dtype, device="npu") + compiled = fused_swiglu_bwd_b_graph_optimize_kernel[(triton.cdiv(n, block_n), )]( + dy, + g, + fc, + dg, + dfc, + db_g, + db_fc, + m, + n, + BLOCK_SIZE_M=block_m, + BLOCK_SIZE_N=block_n, + num_warps=4, + num_stages=2, + debug=True, + sanitize_overflow=True, + enable_graph_optimize=enabled, + ) + torch.npu.synchronize() + ttir = compiled.asm["ttir"] + name = f"fused-swiglu-small-{int(enabled)}-{block_m}x{block_n}-{attempt}" + assert_ttir_text_reparseable(ttir, tmp_path, name) + (tmp_path / f"{name}.ttir").write_text(ttir) + return tuple(value.cpu() for value in (dg, dfc, db_g, db_fc)), ttir + + selected = None + outputs = {} + ttirs = {} + last_runtime_error = None + + def write_summary(): + (tmp_path / "fused-swiglu-small-tiling-summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True)) + + def run_repeated_mode(enabled, block_m, block_n): + repeated = [] + mode_ttir = [] + for attempt in range(2): + result, ttir = run_mode(enabled, block_m, block_n, attempt) + repeated.append(result) + mode_ttir.append(ttir) + for first, second in zip(repeated[0], repeated[1]): + torch.testing.assert_close(first, second, rtol=0, atol=0) + return repeated[-1], mode_ttir[-1] + + for block_m, block_n in ((64, 8), (32, 8)): + candidate = {"BLOCK_SIZE_M": block_m, "BLOCK_SIZE_N": block_n} + try: + off_output, off_ttir = run_repeated_mode(False, block_m, block_n) + except (RuntimeError, TritonError, subprocess.CalledProcessError) as error: + # Only a configuration that cannot run even with graph-optimize + # disabled is eligible for the smaller fallback tile. + candidate["status"] = "off-runtime-error" + candidate["error_type"] = type(error).__name__ + candidate["error"] = str(error) + summary["candidates"].append(candidate) + last_runtime_error = error + continue + + try: + on_output, on_ttir = run_repeated_mode(True, block_m, block_n) + except (RuntimeError, TritonError, subprocess.CalledProcessError) as error: + # An enabled-only failure is the regression under test, not a + # resource reason to silently reduce the validation coverage. + candidate["status"] = "on-runtime-error" + candidate["error_type"] = type(error).__name__ + candidate["error"] = str(error) + summary["candidates"].append(candidate) + write_summary() + raise AssertionError("graph-optimize failed for a tiling that succeeds with it disabled") from error + + selected = (block_m, block_n) + outputs = {False: off_output, True: on_output} + ttirs = {False: off_ttir, True: on_ttir} + candidate["status"] = "selected" + summary["candidates"].append(candidate) + break + + if selected is None: + write_summary() + raise RuntimeError("all fused_swiglu validation tilings failed") from last_runtime_error + + for index, (off, on, reference) in enumerate(zip(outputs[False], outputs[True], expected)): + torch.testing.assert_close(on, off, rtol=3e-2, atol=1e-1) + torch.testing.assert_close(on, reference, rtol=3e-2, atol=1e-1) + summary[f"output_{index}_max_abs_on_off"] = float((on.float() - off.float()).abs().max().item()) + summary[f"output_{index}_max_abs_eager"] = float((on.float() - reference.float()).abs().max().item()) + + block_m, block_n = selected + on_ttir = ttirs[True] + off_ttir = ttirs[False] + assert on_ttir.count("tt.assert") == off_ttir.count("tt.assert") + assert on_ttir.count("tt.assert") > 0 + assert "overflow detected for operation" in on_ttir + assert on_ttir.count("tt.auto_overflow_assert") == on_ttir.count("tt.assert") + old_pointer_type = "tensor<{}x{}x!tt.ptr".format(block_n, block_m) + new_pointer_type = "tensor<{}x{}x!tt.ptr".format(block_m, block_n) + assert old_pointer_type in off_ttir + assert old_pointer_type not in on_ttir + assert new_pointer_type in on_ttir + assert "tensor<1x{}xi64>".format(block_m) not in on_ttir + assert "tensor<{}x{}xi64>".format(block_n, block_m) not in on_ttir + assert "tensor<1x{}xi1>".format(block_m) not in on_ttir + assert "tensor<{}x{}xi1>".format(block_n, block_m) not in on_ttir + + summary["selected_tiling"] = { + "BLOCK_SIZE_M": block_m, + "BLOCK_SIZE_N": block_n, + "num_warps": 4, + "num_stages": 2, + } + summary["graph_optimize_assert_count"] = on_ttir.count("tt.assert") + summary["device"] = (torch.npu.get_device_name(torch.npu.current_device()) + if hasattr(torch.npu, "get_device_name") else "npu") + write_summary() + + +def make_diagonal_shift_ttir(kernel, options, block=16, rule_mask=None): + source = ASTSource( + kernel, + {"x_ptr": "*fp32", "y_ptr": "*fp32"}, + {"BLOCK": block}, + ) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, source, context, options, {}, {}) + module = make_ttir(module, {}, options) + if rule_mask is not None: + pm = ir.pass_manager(module.context) + ascend.passes.ttir.add_graph_optimize(pm, rule_mask=rule_mask) + pm.run(module, "") + return str(module) + + +@pytest.mark.parametrize( + "kernel", + (diagonal_shift_forward_kernel, diagonal_shift_reverse_kernel), + ids=("forward-left-shift", "reverse-right-shift"), +) +def test_diagonal_mask_removal_collapses_quadratic_shift(kernel): + """The 16x16 intermediate and its reduction give way to one subtraction.""" + options = NPUOptions(arch="Ascend910B1", enable_graph_optimize=True) + + ttir = make_diagonal_shift_ttir(kernel, options) + + assert "arith.subf" in ttir + assert "tt.reduce" not in ttir + assert "tt.scan" in ttir + # The quadratic intermediate is what the rule exists to remove. + assert "tensor<16x16xf32>" not in ttir + assert "tensor<16x16xi1>" not in ttir + + +@pytest.mark.parametrize( + "kernel", + (diagonal_shift_forward_kernel, diagonal_shift_reverse_kernel), + ids=("forward-left-shift", "reverse-right-shift"), +) +def test_diagonal_mask_removal_is_gated_by_its_rule_bit(kernel): + """Mask 127 is every other identity, so the pattern must survive intact.""" + options = NPUOptions(arch="Ascend910B1", enable_graph_optimize=False) + + ttir = make_diagonal_shift_ttir(kernel, options, rule_mask=127) + + assert "tt.reduce" in ttir + assert "tensor<16x16xf32>" in ttir + assert "arith.subf" not in ttir + + +def test_diagonal_mask_removal_applies_without_simt_route(): + """The rewrite is compute logic, so it must not depend on pure-SIMT mode. + + Its predecessor lived behind the 910_95 plus force-SIMT gate of + processStridedLoadStoreRewriteOperations and therefore never ran on other + targets. + """ + for arch, compile_mode in ( + ("Ascend910B1", "simd"), + ("Ascend910_9589", "simt_only"), + ): + options = NPUOptions( + arch=arch, + enable_graph_optimize=True, + compile_mode=compile_mode, + ) + + ttir = make_diagonal_shift_ttir(diagonal_shift_forward_kernel, options) + + assert "arith.subf" in ttir + assert "tt.reduce" not in ttir + + +def test_diagonal_mask_removal_numerical_equivalence(monkeypatch, tmp_path): + """Compare the default graph path against the unoptimized kernel on hardware. + + The float rewrite is not bit-exact, since scan[i] - x[i] only recovers + scan[i - 1] up to rounding, so this asserts closeness rather than equality. + """ + torch = _require_npu() + monkeypatch.setenv("TRITON_ALWAYS_COMPILE", "1") + source = torch.rand(16, dtype=torch.float32, device="npu") + expected = torch.cat([ + torch.zeros(1, dtype=torch.float32), + torch.cumsum(source.cpu(), dim=0)[:-1], + ]) + + outputs = {} + for enabled in (False, True): + diagonal_shift_forward_kernel.device_caches.clear() + output = torch.empty_like(source) + compiled = diagonal_shift_forward_kernel[(1, )]( + source, + output, + BLOCK=16, + enable_graph_optimize=enabled, + ) + torch.npu.synchronize() + assert_ttir_text_reparseable( + compiled.asm["ttir"], + tmp_path, + f"diagonal-{int(enabled)}", + ) + outputs[enabled] = output.cpu() + + assert "arith.subf" in compiled.asm["ttir"] + torch.testing.assert_close(outputs[False], expected, rtol=1e-5, atol=1e-6) + torch.testing.assert_close(outputs[True], outputs[False], rtol=1e-5, atol=1e-6) + + +def make_wrapped_tile_ttir(options, block_k=8, block_n=16, bound=None, rule_mask=None): + kernel = wrapped_tile_copy_kernel if bound is None else constexpr_wrapped_tile_copy_kernel + signature = {"src_ptr": "*fp32", "dst_ptr": "*fp32"} + constants = {"BLOCK_K": block_k, "BLOCK_N": block_n} + if bound is None: + signature["n"] = "i32" + else: + constants["N"] = bound + + source = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, source, context, options, {}, {}) + module = make_ttir(module, {}, options) + if rule_mask is not None: + pm = ir.pass_manager(module.context) + ascend.passes.ttir.add_graph_optimize(pm, rule_mask=rule_mask) + pm.run(module, "") + return str(module) + + +def test_convert_modulo_to_mask_linearizes_wrapped_tile(): + """The wrap gives way to a linear offset plus a boundary mask on the load. + + The point of the rewrite is the address form: TritonToStructured can turn a + linear masked access into one contiguous transfer, while it has no way to + keep a wrapped address contiguous. + """ + options = NPUOptions(arch="Ascend910B1", enable_graph_optimize=True) + + ttir = make_wrapped_tile_ttir(options) + + assert "arith.remsi" not in ttir + assert "arith.cmpi slt" in ttir + # The load carries a mask and a zero fill it did not have before. + assert "tt.load" in ttir + assert "arith.constant dense<0.000000e+00> : tensor<8x16xf32>" in ttir + + +def test_convert_modulo_to_mask_is_gated_by_its_rule_bit(): + """Mask 255 is every other identity, so the wrap must survive intact.""" + options = NPUOptions(arch="Ascend910B1", enable_graph_optimize=False) + + ttir = make_wrapped_tile_ttir(options, rule_mask=255) + + assert "arith.remsi" in ttir + + +def test_convert_modulo_to_mask_leaves_compile_time_bounds_alone(): + """A constant divisor belongs to TritonToStructured, which stays equivalent. + + visitOperandRem re-expresses such a wrap as a strided access instead of + discarding it, so claiming this candidate would trade an exact rewrite for + an approximate one. + """ + options = NPUOptions(arch="Ascend910B1", enable_graph_optimize=True) + + ttir = make_wrapped_tile_ttir(options, bound=20) + + assert "arith.remsi" in ttir + + +def test_convert_modulo_to_mask_applies_without_simt_route(): + """The rewrite is address logic, so it must not depend on pure-SIMT mode.""" + for arch, compile_mode in ( + ("Ascend910B1", "simd"), + ("Ascend910_9589", "simt_only"), + ): + options = NPUOptions( + arch=arch, + enable_graph_optimize=True, + compile_mode=compile_mode, + ) + + ttir = make_wrapped_tile_ttir(options) + + assert "arith.remsi" not in ttir + + +def test_convert_modulo_to_mask_numerical_equivalence(monkeypatch, tmp_path): + """The lanes the store keeps must read exactly what the wrap gave them. + + A bound that is not a multiple of the tile makes the last program a boundary + tile, which is the only place the wrap ever mattered. + """ + torch = _require_npu() + monkeypatch.setenv("TRITON_ALWAYS_COMPILE", "1") + block_k = 8 + block_n = 16 + bound = 20 + # The wrapped lanes now address past the last row instead of folding back + # into the tensor, so the allocation carries one tile of slack. Whether the + # lowered transfer actually stops at the mask is a TritonToStructured + # property, asserted structurally by the tests above. + source = torch.rand(block_k * bound + block_n, dtype=torch.float32, device="npu") + expected = source[:block_k * bound].cpu() + + outputs = {} + for enabled in (False, True): + wrapped_tile_copy_kernel.device_caches.clear() + output = torch.zeros(block_k * bound, dtype=torch.float32, device="npu") + grid = ((bound + block_n - 1) // block_n, ) + compiled = wrapped_tile_copy_kernel[grid]( + source, + output, + bound, + BLOCK_K=block_k, + BLOCK_N=block_n, + enable_graph_optimize=enabled, + ) + torch.npu.synchronize() + assert_ttir_text_reparseable( + compiled.asm["ttir"], + tmp_path, + f"wrapped-tile-{int(enabled)}", + ) + outputs[enabled] = output.cpu() + + assert "arith.remsi" not in compiled.asm["ttir"] + torch.testing.assert_close(outputs[False], expected, rtol=0, atol=0) + torch.testing.assert_close(outputs[True], outputs[False], rtol=0, atol=0) + + +def test_graph_optimize_options_contribute_to_npu_hash(): + assert (NPUOptions(enable_graph_optimize=False).hash() != NPUOptions(enable_graph_optimize=True).hash()) diff --git a/third_party/ascend/unittest/pytest_ut/test_gt.py b/third_party/ascend/unittest/pytest_ut/test_gt.py new file mode 100644 index 0000000000..e5d81e0584 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_gt.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_gt(x0, x1, dtype): + res = torch.where(torch.gt(x0, x1), torch.ones_like(x0), torch.zeros_like(x0)).to(eval('torch.' + dtype)) + return res + + +@triton.jit +def triton_gt(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 > tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_gt(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_gt(x0, x1, dtype) + # triton结果 + triton_res = torch.empty_like(x0) + triton_gt[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_hd_permute.py b/third_party/ascend/unittest/pytest_ut/test_hd_permute.py new file mode 100644 index 0000000000..c58e985b5d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_hd_permute.py @@ -0,0 +1,53 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu + +X_SIZE = tl.constexpr(4) +Y_SIZE = tl.constexpr(64) +Z_SIZE = tl.constexpr(32) +NUMEL = X_SIZE * Y_SIZE * Z_SIZE + + +def torch_permute(x): + return x.reshape((X_SIZE, Y_SIZE, Z_SIZE)).permute(1, 0, 2).reshape((X_SIZE * Y_SIZE * Z_SIZE)) + + +@triton.jit +def triton_permute(output_ptr, input_ptr): + x_index = tl.arange(0, X_SIZE * Y_SIZE * Z_SIZE) + input_local = tl.load(input_ptr + x_index) + output_local = input_local.reshape((X_SIZE, Y_SIZE, Z_SIZE)).permute(1, 0, 2).reshape((X_SIZE * Y_SIZE * Z_SIZE)) + tl.store(output_ptr + x_index, output_local) + + +def test_hd_permute(): + # 生成数据 + x = torch.randn(NUMEL).npu() + # torch结果 + torch_res = torch_permute(x) + # triton结果 + triton_res = torch.randn(torch_res.shape, dtype=torch_res.dtype).npu() + triton_permute[1, 1, 1](triton_res, x) + # 比较结果 + torch.testing.assert_close(triton_res, torch_res, rtol=1e-3, atol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_histogram.py b/third_party/ascend/unittest/pytest_ut/test_histogram.py new file mode 100644 index 0000000000..2e3bd28b51 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_histogram.py @@ -0,0 +1,106 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import math +import test_common +from triton.backends.ascend.utils import is_compile_on_910_95 + + +@triton.jit +def histogram_kernel(x_ptr, z_ptr, M: tl.constexpr, N: tl.constexpr): + offset1 = tl.arange(0, M) + offset2 = tl.arange(0, N) + x = tl.load(x_ptr + offset1) + z = tl.histogram(x, N) + tl.store(z_ptr + offset2, z) + + +@triton.jit +def histogram_mask_kernel(x_ptr, z_ptr, n_elements, N: tl.constexpr, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(0) + block_start = pid * BLOCK_SIZE + offset1 = tl.arange(0, BLOCK_SIZE) + offset2 = tl.arange(0, N) + x_idx = block_start + offset1 + mask = x_idx < n_elements + x = tl.load(x_ptr + x_idx) + z = tl.histogram(x, N, mask=mask) + tl.atomic_add(z_ptr + offset2, z) + + +@pytest.mark.parametrize("M", [2048]) +@pytest.mark.parametrize("N", [2]) +@pytest.mark.parametrize("ncore", [1]) +@pytest.mark.parametrize("dtype", ["int32", "int64"]) +def test_histogram(M, N, ncore, dtype): + torch.manual_seed(17) + x = torch.randint(low=0, high=N, size=(M, ), dtype=eval(f'torch.{dtype}')).npu() + # torch结果 + y_cal = torch.histc(x.float(), bins=N, min=0, max=N - 1) + # triton结果 + y_ref = torch.empty(N, dtype=eval(f'torch.{dtype}'), device="npu") + histogram_kernel[(ncore, )](x, y_ref, M=M, N=N) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize("M", [2048]) +@pytest.mark.parametrize("N", [2]) +@pytest.mark.parametrize("ncore", [1]) +@pytest.mark.parametrize("dtype", ["uint32", "uint64"]) +@pytest.mark.skipif( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason="uint32/uint64 are only supported on Ascend 950 (A5); A2/A3 load/histogram do not support them.", +) +def test_histogram_uint(M, N, ncore, dtype): + torch.manual_seed(17) + x_cpu = torch.randint(low=0, high=N, size=(M, ), dtype=eval(f'torch.{dtype}'), device="cpu") + x = x_cpu.to("npu") + # torch结果 + y_cal = torch.histc(x.float(), bins=N, min=0, max=N - 1) + y_cal = y_cal.to(eval(f'torch.{dtype}')) + # triton结果 + y_ref = torch.empty(N, dtype=eval(f'torch.{dtype}'), device="npu") + histogram_kernel[(ncore, )](x, y_ref, M=M, N=N) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.skip(reason="not supported before the NPUIR is updated, and will be fixed later.") +@pytest.mark.parametrize("M", [2048]) +@pytest.mark.parametrize("N", [4]) +@pytest.mark.parametrize("BLOCK_SIZE", [10]) +@pytest.mark.parametrize("dtype", ["int32", "int64"]) +def test_histogram_with_mask(M, N, BLOCK_SIZE, dtype): + torch.manual_seed(42) + + x = torch.randint(low=0, high=N, size=(M, ), dtype=eval(f'torch.{dtype}')).npu() + mask = torch.zeros((M, ), dtype=torch.bool) + mask[:M // 2] = True + + valid_x = x.cpu()[mask] + ref_count = torch.bincount(valid_x, minlength=N) + y_cal = ref_count.to(eval(f'torch.{dtype}')) + y_ref = torch.zeros(N, dtype=eval(f'torch.{dtype}'), device="npu") + grid = lambda meta: (triton.cdiv(M, meta['BLOCK_SIZE']), ) + histogram_mask_kernel[grid](x, y_ref, n_elements=M // 2, N=N, BLOCK_SIZE=BLOCK_SIZE) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_hoistbroadcast.py b/third_party/ascend/unittest/pytest_ut/test_hoistbroadcast.py new file mode 100644 index 0000000000..63d0b0523c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_hoistbroadcast.py @@ -0,0 +1,220 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu +import numpy as np + + +@triton.jit +def copy( + data_ptrs, + tgt_loc_ptr, + src_loc_ptr, + num_locs, + stride: tl.constexpr, + num_locs_upper: tl.constexpr, +): + bid = tl.program_id(0) + data_ptr = tl.load(data_ptrs + bid) + data_ptr = tl.cast(data_ptr, tl.pointer_type(tl.float16)) + num_locs_offset = tl.arange(0, num_locs_upper) + tgt_locs = tl.load(tgt_loc_ptr + num_locs_offset) + src_locs = tl.load(src_loc_ptr + num_locs_offset) + copy_offset = tl.arange(0, stride) + value = tl.load(data_ptr + src_locs[:, None] * stride + copy_offset[None, :]) + value += 1 + tl.store(data_ptr + tgt_locs[:, None] * stride + copy_offset[None, :], value) + + +@triton.jit +def copy_all_layer_kv_cache( + data_ptrs, + strides, + tgt_loc_ptr, + src_loc_ptr, + num_locs, + num_locs_upper: tl.constexpr, +): + BLOCK_SIZE: tl.constexpr = 128 + + bid = tl.program_id(0) + stride = tl.load(strides + bid) + + data_ptr = tl.load(data_ptrs + bid) + data_ptr = tl.cast(data_ptr, tl.pointer_type(tl.uint8)) + + num_locs_offset = tl.arange(0, num_locs_upper) + tgt_locs = tl.load(tgt_loc_ptr + num_locs_offset) + src_locs = tl.load(src_loc_ptr + num_locs_offset) + + num_loop = tl.cdiv(stride, BLOCK_SIZE) + for i in range(num_loop): + copy_offset = tl.arange(0, BLOCK_SIZE) + i * BLOCK_SIZE + mask = (num_locs_offset < num_locs)[:, None] & (copy_offset < stride)[None, :] + value = tl.load(data_ptr + src_locs[:, None] * stride + copy_offset[None, :], mask=mask) + value *= 1 + tl.store( + data_ptr + tgt_locs[:, None] * stride + copy_offset[None, :], + value, + mask=mask, + ) + + +@triton.jit +def copy_all_layer_kv_cache2( + data_ptrs, + strides, + tgt_loc_ptr, + src_loc_ptr, + num_locs, + num_locs_upper: tl.constexpr, +): + BLOCK_SIZE: tl.constexpr = 128 + + bid = tl.program_id(0) + stride = tl.load(strides + bid) + + data_ptr = tl.load(data_ptrs + bid) + data_ptr = tl.cast(data_ptr, tl.pointer_type(tl.uint8)) + + num_locs_offset = tl.arange(0, num_locs_upper) + tgt_locs = tl.load(tgt_loc_ptr + num_locs_offset) + src_locs = tl.load(src_loc_ptr + num_locs_offset) + + num_loop = tl.cdiv(stride, BLOCK_SIZE) + for i in range(num_loop): + copy_offset = tl.arange(0, BLOCK_SIZE) + i * BLOCK_SIZE + mask = (num_locs_offset < num_locs)[:, None] & (copy_offset < stride)[None, :] + value = tl.load(data_ptr + (src_locs[:, None] * stride + copy_offset[None, :]), mask=mask) + value *= 1 + tl.store( + data_ptr + (tgt_locs[:, None] * stride + copy_offset[None, :]), + value, + mask=mask, + ) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (10, 10), 'npu'], +]) +def test_copy(param_list): + dtype, shape, device = param_list + data = torch.zeros(shape, dtype=eval('torch.' + dtype), device=device) + data_ref = torch.zeros(shape, dtype=eval('torch.' + dtype), device=device) + + src_loc = torch.tensor([0], dtype=torch.int32, device=device) + tgt_loc = torch.tensor([0], dtype=torch.int32, device=device) + + data_ptr = torch.tensor([data.data_ptr()], dtype=torch.int64, device=device) + stride = shape[1] + copy[(1, )](data_ptr, tgt_loc, src_loc, 1, stride, 1) + data_ref[0, :] += 1 + test_common.validate_cmp(dtype, data, data_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', 3, 20, 16, 4, 16, 'npu'], +]) +def test_hoistbroadcast_compare(param_list): + dtype, layer_num, page_num, page_size, head_num, head_dim, device = param_list + kv_buffer = torch.randn((2, layer_num, page_num, page_size, head_num, head_dim), dtype=eval('torch.' + dtype), + device=device) + kv_buffer_ref = kv_buffer.clone() + k_buffer = kv_buffer[0] + v_buffer = kv_buffer[1] + k_buffer_ref = kv_buffer_ref[0] + v_buffer_ref = kv_buffer_ref[1] + + data_ptrs = torch.tensor([x.data_ptr() for x in [k_buffer]] + [x.data_ptr() for x in [v_buffer]], dtype=torch.int64, + device=device) + data_ptrs_ref = torch.tensor([x.data_ptr() for x in [k_buffer_ref]] + [x.data_ptr() for x in [v_buffer_ref]], + dtype=torch.int64, device=device) + + data_strides = torch.cat([ + torch.tensor([np.prod(x.shape[1:]) * x.dtype.itemsize for x in k_buffer], device=device), + torch.tensor([np.prod(x.shape[1:]) * x.dtype.itemsize for x in v_buffer], device=device) + ], dim=0) + data_strides_ref = data_strides.clone() + + src_loc = torch.tensor([0], dtype=torch.int32, device=device) + tgt_loc = torch.tensor([0], dtype=torch.int32, device=device) + + copy_all_layer_kv_cache[(len(data_ptrs), )](data_ptrs, data_strides, tgt_loc, src_loc, len(tgt_loc), 1) + copy_all_layer_kv_cache2[(len(data_ptrs_ref), )](data_ptrs_ref, data_strides_ref, tgt_loc, src_loc, len(tgt_loc), 1) + test_common.validate_cmp(dtype, kv_buffer, kv_buffer_ref) + + +def torch_pointwise(x0): + if x0.dtype != torch.uint32: + return torch.abs(x0) + else: + return torch.abs(x0.to(torch.float32)) + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, y_ptr, z_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr, XNUMEL: tl.constexpr, + YNUMEL: tl.constexpr, ZNUMEL: tl.constexpr): + xoffs = tl.program_id(0) * XB + yoffs = tl.program_id(1) * YB + zoffs = tl.program_id(2) * ZB + + xidx = tl.arange(0, XB) + xoffs + yidx = tl.arange(0, YB) + yoffs + zidx = tl.arange(0, ZB) + zoffs + + X = tl.load(x_ptr + xidx[:, None, None] * YNUMEL * ZNUMEL + yidx[None, :, None] * ZNUMEL + zidx[None, None, :]) + ret = tl.abs(X) + tl.store(output_ptr + xidx[:, None, None] * YNUMEL * ZNUMEL + yidx[None, :, None] * ZNUMEL + zidx[None, None, :], + ret) + + +@pytest.mark.parametrize('shape', [(8, 16, 16)]) +@pytest.mark.parametrize('dtype', ['float32']) +def test_case2(dtype, shape): + # 生成数据 + x = test_common.generate_tensor(shape, dtype).npu() + y = test_common.generate_tensor(shape, dtype).npu() + z = test_common.generate_tensor(shape, dtype).npu() + new_shape = shape + + output = torch.randint(1, new_shape, dtype=eval('torch.' + dtype)).npu() + output1 = output + + ans = torch_pointwise(x) + + XB = shape[0] + xnumel = shape[0] + YB = shape[1] + ynumel = shape[1] + ZB = shape[2] + znumel = shape[2] + + grid = (1, 1, 1) + + fn_npu_[grid](output, x, y, z, XB, YB, ZB, xnumel, ynumel, znumel) + + test_common.validate_cmp(dtype, ans, output) diff --git a/third_party/ascend/unittest/pytest_ut/test_hypot.py b/third_party/ascend/unittest/pytest_ut/test_hypot.py new file mode 100644 index 0000000000..9bb1e6b31c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_hypot.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +def torch_hypot(x0, x1): + return torch.hypot(x0, x1) + + +@triton.jit +def triton_hypot(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = libdevice.hypot(tmp0, tmp1) + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['bfloat16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_hypot(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_hypot(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_hypot[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_if_advance.py b/third_party/ascend/unittest/pytest_ut/test_if_advance.py new file mode 100644 index 0000000000..66af345270 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_if_advance.py @@ -0,0 +1,36 @@ +import torch +import torch_npu +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al + + +@triton.jit +def triton_if_advance_kernel(in_ptr0, in_ptr1, out_ptr, xnumel, ynumel, k_loops, XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr): + + K_block_ptr = tl.make_block_ptr(base=in_ptr0, shape=(xnumel, ynumel), strides=(ynumel, 1), offsets=(0, 0), + block_shape=(XBLOCK, YBLOCK), order=(1, 0)) + V_block_ptr = tl.make_block_ptr(base=in_ptr1, shape=(ynumel, xnumel), strides=(xnumel, 1), offsets=(0, 0), + block_shape=(YBLOCK, XBLOCK), order=(1, 0)) + O_block_ptr = tl.make_block_ptr(base=out_ptr, shape=(xnumel, xnumel), strides=(xnumel, 1), offsets=(0, 0), + block_shape=(XBLOCK, XBLOCK), order=(1, 0)) + res = tl.zeros([XBLOCK, XBLOCK], tl.float32) + for i in range(0, k_loops): + if i > 0: + K_block_ptr = tl.advance(K_block_ptr, (0, YBLOCK)) + V_block_ptr = tl.advance(V_block_ptr, (YBLOCK, 0)) + a = tl.load(K_block_ptr) + b = tl.load(V_block_ptr) + res = tl.dot(a, b, acc=res) + tl.store(O_block_ptr, res) + + +def test_if_advance(): + x = torch.randn((64, 256), dtype=torch.float32, device="npu") + y = torch.randn((256, 64), dtype=torch.float32, device="npu") + out_tri = torch.empty((64, 64), dtype=torch.float32, device="npu") + out_std = torch.empty((64, 64), dtype=torch.float32, device="npu") + torch.matmul(x, y, out=out_std) + triton_if_advance_kernel[1, 1, 1](x, y, out_tri, 64, 256, 4, 64, 64) + torch.testing.assert_close(out_std, out_tri, atol=1e-2, rtol=1e-2) diff --git a/third_party/ascend/unittest/pytest_ut/test_if_load.py b/third_party/ascend/unittest/pytest_ut/test_if_load.py new file mode 100644 index 0000000000..7932366cb4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_if_load.py @@ -0,0 +1,82 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +@triton.jit +def triton_if_load(in_ptr0, out_ptr0, XBLOCK: tl.constexpr): + base1 = tl.arange(0, XBLOCK) + index = base1 + if tl.program_id(0) == 0: + base1 = base1 * 1 + else: + base1 = base1 * 2 + tmp0 = tl.load(in_ptr0 + base1, base1 < XBLOCK, other=0.0) + tl.store(out_ptr0 + index, tmp0, None) + + +@triton.jit +def triton_for_if_load(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + base1 = tl.arange(0, XBLOCK_SUB) + index = base1 + loops = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for i in range(loops): + base1 = base1 + i * XBLOCK_SUB + index = index + i * XBLOCK_SUB + if tl.program_id(0) != 0: + base1 = base1 + 1 + + tmp0 = tl.load(in_ptr0 + base1, base1 < XBLOCK, other=0.0) + tl.store(out_ptr0 + index, tmp0, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (32, ), 32], +]) +def test_if_load(param_list): + dtype, shape, xblock = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = x0.clone() + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_if_load[(1, )](x0, y_cal, xblock) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (32, ), 32, 16], +]) +def test_if_load(param_list): + dtype, shape, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = x0.clone() + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_for_if_load[(1, )](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_if_tensor.py b/third_party/ascend/unittest/pytest_ut/test_if_tensor.py new file mode 100644 index 0000000000..7ceadb105e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_if_tensor.py @@ -0,0 +1,134 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import math +import torch +import triton +import triton.language as tl + +import pytest + + +@triton.jit +def if_tensor_kernel( + kv_start_idx, + output_ptr, +): + pid = tl.program_id(0) + if kv_start_idx: + value = tl.load(kv_start_idx + pid) + tl.store(output_ptr + pid, value) + + +def test_kernel(): + n = 8 + device = 'npu' + + kv_start_idx = torch.arange(n, dtype=torch.float32, device=device) + output1 = torch.zeros(n, dtype=torch.float32, device=device) + if_tensor_kernel[(n, )]( + kv_start_idx, + output1, + ) + + expected = torch.arange(n, dtype=torch.float32, device=device) + assert torch.allclose(output1, expected), f"Output {output1} != Expected {expected}" + print(f"RESULT: output1 = {output1}") + print("✅ Test passed!") + + +@triton.jit +def mul_if_block_kernel(value, value_stride0, value_stride1, output, output_stride0, output_stride1, lengths, bs, dim, + max_seq_len, DIM_SIZE: tl.constexpr, BLOCK_SIZE: tl.constexpr): + batch_idx = tl.program_id(0) + if batch_idx >= bs: + return + + need_reverse = tl.program_id(1) == 0 + + block_idx = tl.program_id(2) + block_start = block_idx * BLOCK_SIZE + + reverse_len = tl.load(lengths + batch_idx) + copy_len = max_seq_len - reverse_len + + value = value + batch_idx * value_stride0 + output = output + batch_idx * output_stride0 + + if need_reverse: + if block_start >= reverse_len: + return + value_block_ptr = tl.make_block_ptr(base=value, shape=(reverse_len, dim), strides=(value_stride0, 1), + offsets=(block_start, 0), block_shape=(BLOCK_SIZE, DIM_SIZE), order=(0, 1)) + + block_values = tl.load(value_block_ptr, boundary_check=(0, 1), padding_option="zero") + + output_block_ptr = tl.make_block_ptr(base=output + (reverse_len - 1) * output_stride1, shape=(reverse_len, dim), + strides=(output_stride0, 1), offsets=(block_start, 0), + block_shape=(BLOCK_SIZE, DIM_SIZE), order=(0, 1)) + tl.store(output_block_ptr, block_values, boundary_check=(0, 1)) + else: + if block_start >= copy_len: + return + value_block_ptr = tl.make_block_ptr(base=value + reverse_len * value_stride1, shape=(max_seq_len, dim), + strides=(value_stride0, 1), offsets=(block_start, 0), + block_shape=(BLOCK_SIZE, DIM_SIZE), order=(0, 1)) + + block_values = tl.load(value_block_ptr, boundary_check=(0, 1), padding_option="zero") + + output_block_ptr = tl.make_block_ptr(base=output + reverse_len * output_stride1, shape=(max_seq_len, dim), + strides=(output_stride0, 1), offsets=(block_start, 0), + block_shape=(BLOCK_SIZE, DIM_SIZE), order=(0, 1)) + tl.store(output_block_ptr, block_values, boundary_check=(0, 1)) + + +def ref_reverse(value: torch.Tensor, lengths: torch.Tensor) -> torch.Tensor: + bs, max_seq_len, dim = value.shape + out = value.clone() + for b in range(bs): + r = int(lengths[b]) + if r > 0: + out[b, :r] = value[b, :r].flip(0) + return out + + +@pytest.mark.parametrize("bs,max_seq_len,dim,BLOCK_SIZE", [ + (2, 32, 16, 8), +]) +def test_reverse_sequence_kernel(bs, max_seq_len, dim, BLOCK_SIZE): + device = "npu" + value = torch.randn(bs, max_seq_len, dim, device=device, dtype=torch.float32) + lengths = torch.tensor([0, max_seq_len // 2, max_seq_len] + [max_seq_len // 3] * max(0, bs - 3), device=device, + dtype=torch.int32)[:bs] + output = torch.empty_like(value) + + value_stride0 = value.stride(0) + value_stride1 = value.stride(1) + output_stride0 = output.stride(0) + output_stride1 = output.stride(1) + + DIM_SIZE = dim + grid = (bs, 2, math.ceil(max_seq_len / BLOCK_SIZE)) + mul_if_block_kernel[grid](value, value_stride0, value_stride1, output, output_stride0, output_stride1, lengths, bs, + dim, max_seq_len, DIM_SIZE=DIM_SIZE, BLOCK_SIZE=BLOCK_SIZE) + + +if __name__ == "__main__": + test_kernel() diff --git a/third_party/ascend/unittest/pytest_ut/test_implicit_atomic.py b/third_party/ascend/unittest/pytest_ut/test_implicit_atomic.py new file mode 100644 index 0000000000..b7a539f392 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_implicit_atomic.py @@ -0,0 +1,140 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import pytest +import triton +import test_common +import triton.language as tl + +types_all = [ + (torch.float32, 'float32'), +] + + +def ceil_div(a, b): + return (a + b - 1) // b + + +@triton.jit +def addptr_implicit_perm_atomic_add_2d( + ptr, + out, + ynumel, + xnumel, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + y = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK)[None, :] # [1, YB] + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] # [XB, 1] + + val = 1.0 + (x.to(tl.float32) * 0.01) + (y.to(tl.float32) * 0.001) # [XB, YB] + xmask = x < xnumel + ymask = y < ynumel + old = tl.atomic_add(ptr + (x + 4 * y), val, xmask & ymask) + + tl.store(out + (x + 4 * y), old) + + +@triton.jit +def addptr_implicit_perm_atomic_cas_2d( + ptr, + out, + cmp_ptr, + val_ptr, + ynumel, + xnumel, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + y = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK)[None, :] + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] + + xmask = x < xnumel + ymask = y < ynumel + mask = xmask & ymask + + offset = x + 4 * y + + cmp = tl.load(cmp_ptr + offset, mask=mask, other=0.0).to(tl.float32) + val = tl.load(val_ptr + offset, mask=mask, other=0.0).to(tl.float32) + + old = tl.atomic_cas(ptr + offset, cmp, val) + + tl.store(out + offset, old, mask=mask) + + +@pytest.mark.parametrize('dtype,sigtype', types_all) +@pytest.mark.parametrize('xnumel, ynumel, XBLOCK, YBLOCK', [(4, 512, 4, 64)]) +def test_addptr_implicit_perm_atomic_add_2d( + dtype, + sigtype, + xnumel, + ynumel, + XBLOCK, + YBLOCK, +): + in_ptr = torch.zeros((ynumel * 4, ), dtype=dtype).npu() + out_ptr = torch.ones_like(in_ptr) + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + addptr_implicit_perm_atomic_add_2d[grid](in_ptr, out_ptr, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK) + + y_idx = torch.arange(ynumel).unsqueeze(1).npu() + x_idx = torch.arange(xnumel).unsqueeze(0).npu() + idx = (x_idx + 4 * y_idx).reshape(-1) + torch.testing.assert_close(out_ptr[idx], torch.zeros_like(out_ptr[idx])) + + val_ref = (1.0 + 0.01 * x_idx.to(torch.float32) + 0.001 * y_idx.to(torch.float32)).reshape(-1) + torch.testing.assert_close(in_ptr[idx], val_ref, rtol=1e-5, atol=1e-5) + + +@pytest.mark.parametrize('dtype,sigtype', types_all) +@pytest.mark.parametrize('xnumel, ynumel, XBLOCK, YBLOCK', [(4, 512, 4, 64)]) +def test_addptr_implicit_perm_atomic_cas_2d( + dtype, + sigtype, + xnumel, + ynumel, + XBLOCK, + YBLOCK, +): + in_ptr = torch.full((ynumel * 4, ), 2, dtype=dtype).npu() + out_ptr = torch.full((ynumel * 4, ), 1, dtype=dtype).npu() + cmp_ptr = torch.full((ynumel * 4, ), 2, dtype=dtype).npu() + val_ptr = torch.full((ynumel * 4, ), 1, dtype=dtype).npu() + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + addptr_implicit_perm_atomic_cas_2d[grid](in_ptr, out_ptr, cmp_ptr, val_ptr, ynumel, xnumel, YBLOCK=YBLOCK, + XBLOCK=XBLOCK) + + y_idx = torch.arange(ynumel).unsqueeze(1).npu() + x_idx = torch.arange(xnumel).unsqueeze(0).npu() + idx = (x_idx + 4 * y_idx).reshape(-1) + + torch.testing.assert_close(out_ptr[idx], torch.full_like(out_ptr[idx], 2.0)) + + torch.testing.assert_close(in_ptr[idx], torch.ones_like(in_ptr[idx])) + + +if __name__ == '__main__': + case_2d = (4, 512, 4, 64) + test_addptr_implicit_perm_atomic_add_2d(*types_all[0], *case_2d) + test_addptr_implicit_perm_atomic_cas_2d(*types_all[0], *case_2d) diff --git a/third_party/ascend/unittest/pytest_ut/test_implicit_permute.py b/third_party/ascend/unittest/pytest_ut/test_implicit_permute.py new file mode 100644 index 0000000000..c5016fc665 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_implicit_permute.py @@ -0,0 +1,1338 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import pytest +import triton +import triton.language as tl +import test_common + +types_all = [ + (torch.float32, 'float32'), +] + +case_2d = [ + # X, Y, XBLOCK, YBLOCK + (512, 32, 4, 64), +] + +case_3d = [ + # X, Y, Z, XBLOCK, YBLOCK, ZBLOCK + (100, 40, 32, 10, 4, 4), +] + +case_4d = [ + # X, Y, Z, W, XBLOCK, YBLOCK, ZBLOCK, WBLOCK + (100, 80, 20, 16, 20, 4, 4, 4), +] + + +# ---------------------------------------------------------- +# Triton kernel: addptr with implicit permute kernels +# ---------------------------------------------------------- +@triton.jit +def addptr_implicit_perm_load_2d( + ptr, + out, + ynumel, + xnumel, + stride_y, + stride_x, + out_stride_x, + out_stride_y, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] + y = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK)[None, :] + mask = (x < xnumel) & (y < ynumel) + + offset = x * stride_x + y * stride_y + val = tl.load(ptr + offset, mask=mask) + + out_offset = x * out_stride_x + y * out_stride_y + tl.store(out + out_offset, val, mask=mask) + + +@triton.jit +def addptr_implicit_perm_store_2d( + ptr, + out, + ynumel, + xnumel, + in_stride_x, + in_stride_y, + stride_y, + stride_x, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] + y = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK)[None, :] + mask = (x < xnumel) & (y < ynumel) + + in_offset = x * in_stride_x + y * in_stride_y + val = tl.load(ptr + in_offset, mask=mask) + + out_offset = x * stride_x + y * stride_y + tl.store(out + out_offset, val, mask=mask) + + +@triton.jit +def addptr_implicit_perm_load_store_2d_static_stride(ptr, out, ynumel, xnumel, stride_y: tl.constexpr, + stride_x: tl.constexpr, YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr): + # logical indices (A^T view) + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] # [XBLOCK, 1] + y = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK)[None, :] # [1, YBLOCK] + mask = (x < xnumel) & (y < ynumel) + + # IMPORTANT: + # ptr is a row-major A, but we interpret it as A^T via stride + offset = x * stride_x + y * stride_y + + val = tl.load(ptr + offset, mask) + tl.store(out + offset, val, mask) + + +@triton.jit +def addptr_implicit_perm_load_store_2d( + ptr, + out, + ynumel, + xnumel, + stride_y, + stride_x, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + # logical indices (A^T view) + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] # [XBLOCK, 1] + y = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK)[None, :] # [1, YBLOCK] + + mask = (x < xnumel) & (y < ynumel) + + # IMPORTANT: + # ptr is a row-major A, but we interpret it as A^T via stride + offset = x * stride_x + y * stride_y + + val = tl.load(ptr + offset, mask=mask) + tl.store(out + offset, val, mask=mask) + + +@triton.jit +def addptr_implicit_perm_load_store_3d_static_stride( + ptr, + out, + znumel, + ynumel, + xnumel, + stride_z: tl.constexpr, + stride_y: tl.constexpr, + stride_x: tl.constexpr, + ZBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + pid_x = tl.program_id(0) + pid_y = tl.program_id(1) + pid_z = tl.program_id(2) + + x = pid_x * XBLOCK + tl.arange(0, XBLOCK)[:, None, None] + y = pid_y * YBLOCK + tl.arange(0, YBLOCK)[None, :, None] + z = pid_z * ZBLOCK + tl.arange(0, ZBLOCK)[None, None, :] + + mask = (x < xnumel) & (y < ynumel) & (z < znumel) + + offset = x * stride_x + y * stride_y + z * stride_z + val = tl.load(ptr + offset, mask=mask) + tl.store(out + offset, val, mask=mask) + + +@triton.jit +def addptr_implicit_perm_load_store_3d( + ptr, + out, + znumel, + ynumel, + xnumel, + stride_z, + stride_y, + stride_x, + ZBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + pid_x = tl.program_id(0) + pid_y = tl.program_id(1) + pid_z = tl.program_id(2) + + x = pid_x * XBLOCK + tl.arange(0, XBLOCK)[:, None, None] + y = pid_y * YBLOCK + tl.arange(0, YBLOCK)[None, :, None] + z = pid_z * ZBLOCK + tl.arange(0, ZBLOCK)[None, None, :] + + mask = (x < xnumel) & (y < ynumel) & (z < znumel) + + offset = x * stride_x + y * stride_y + z * stride_z + val = tl.load(ptr + offset, mask=mask) + tl.store(out + offset, val, mask=mask) + + +@triton.jit +def addptr_implicit_perm_load_store_4d_static_stride( + ptr, + out, + wnumel, + znumel, + ynumel, + xnumel, + stride_w: tl.constexpr, + stride_z: tl.constexpr, + stride_y: tl.constexpr, + stride_x: tl.constexpr, + WBLOCK: tl.constexpr, + ZBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + pid0 = tl.program_id(0) # covers (w, x) + pid1 = tl.program_id(1) # y + pid2 = tl.program_id(2) # z + + xblocks_per_w = (xnumel + XBLOCK - 1) // XBLOCK + + w_pid = pid0 // xblocks_per_w + x_pid = pid0 - w_pid * xblocks_per_w + + x0 = x_pid * XBLOCK + y0 = pid1 * YBLOCK + z0 = pid2 * ZBLOCK + w0 = w_pid * WBLOCK + + x = x0 + tl.arange(0, XBLOCK)[:, None, None, None] + y = y0 + tl.arange(0, YBLOCK)[None, :, None, None] + z = z0 + tl.arange(0, ZBLOCK)[None, None, :, None] + w = w0 + tl.arange(0, WBLOCK)[None, None, None, :] + + mask = (x < xnumel) & (y < ynumel) & (z < znumel) & (w < wnumel) + + offset = x * stride_x + y * stride_y + z * stride_z + w * stride_w + val = tl.load(ptr + offset, mask=mask, other=0.0) + tl.store(out + offset, val, mask=mask) + + +@triton.jit +def addptr_implicit_perm_load_store_4d( + ptr, + out, + wnumel, + znumel, + ynumel, + xnumel, + stride_w, + stride_z, + stride_y, + stride_x, + WBLOCK: tl.constexpr, + ZBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + pid0 = tl.program_id(0) # covers (w, x) + pid1 = tl.program_id(1) # y + pid2 = tl.program_id(2) # z + + xblocks_per_w = (xnumel + XBLOCK - 1) // XBLOCK + + w_pid = pid0 // xblocks_per_w + x_pid = pid0 - w_pid * xblocks_per_w + + x0 = x_pid * XBLOCK + y0 = pid1 * YBLOCK + z0 = pid2 * ZBLOCK + w0 = w_pid * WBLOCK + + x = x0 + tl.arange(0, XBLOCK)[:, None, None, None] + y = y0 + tl.arange(0, YBLOCK)[None, :, None, None] + z = z0 + tl.arange(0, ZBLOCK)[None, None, :, None] + w = w0 + tl.arange(0, WBLOCK)[None, None, None, :] + + mask = (x < xnumel) & (y < ynumel) & (z < znumel) & (w < wnumel) + + offset = x * stride_x + y * stride_y + z * stride_z + w * stride_w + val = tl.load(ptr + offset, mask=mask, other=0.0) + tl.store(out + offset, val, mask=mask) + + +# ---------------------------------------------------------- +# Triton kernel: make_tensor_ptr with implicit permute kernels +# ---------------------------------------------------------- +@triton.jit +def make_tensor_ptr_implicit_perm_load_2d( + ptr, + out, + ynumel, + xnumel, + STRIDE_Y, + STRIDE_X, + OUT_STRIDE_X, + OUT_STRIDE_Y, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + y0 = tl.program_id(1) * YBLOCK + x0 = tl.program_id(0) * XBLOCK + y = y0 + tl.arange(0, YBLOCK)[None, :] + x = x0 + tl.arange(0, XBLOCK)[:, None] + xmask = x < xnumel + ymask = y < ynumel + mask = xmask & ymask + + tptr = tl.make_block_ptr( + base=ptr, + shape=(xnumel, ynumel), + strides=(STRIDE_X, STRIDE_Y), + offsets=(x0, y0), + block_shape=(XBLOCK, YBLOCK), + order=(0, 1), + ) + + val = tl.load(tptr) + out_offset = x * OUT_STRIDE_X + y * OUT_STRIDE_Y + tl.store(out + out_offset, val, mask=mask) + + +@triton.jit +def make_tensor_ptr_implicit_perm_store_2d( + ptr, + out, + ynumel, + xnumel, + IN_STRIDE_X, + IN_STRIDE_Y, + STRIDE_Y, + STRIDE_X, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + y0 = tl.program_id(1) * YBLOCK + x0 = tl.program_id(0) * XBLOCK + y = y0 + tl.arange(0, YBLOCK)[None, :] + x = x0 + tl.arange(0, XBLOCK)[:, None] + xmask = x < xnumel + ymask = y < ynumel + mask = xmask & ymask + + in_offset = x * IN_STRIDE_X + y * IN_STRIDE_Y + val = tl.load(ptr + in_offset, mask=mask) + + tptr = tl.make_block_ptr( + base=out, + shape=(xnumel, ynumel), + strides=(STRIDE_X, STRIDE_Y), + offsets=(x0, y0), + block_shape=(XBLOCK, YBLOCK), + order=(0, 1), + ) + tl.store(tptr, val, boundary_check=(0, 1)) + + +@triton.jit +def make_tensor_ptr_implicit_perm_load_store_2d_static_stride(ptr, out, ynumel, xnumel, STRIDE_Y: tl.constexpr, + STRIDE_X: tl.constexpr, YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr): + y0 = tl.program_id(1) * YBLOCK + x0 = tl.program_id(0) * XBLOCK + y = y0 + tl.arange(0, YBLOCK)[None, :] # [1, YBLOCK] + x = x0 + tl.arange(0, XBLOCK)[:, None] # [XBLOCK, 1] + xmask = x < xnumel + ymask = y < ynumel + mask = xmask & ymask + + tptr = tl.make_block_ptr( + base=ptr, + shape=(xnumel, ynumel), + strides=(STRIDE_X, STRIDE_Y), + offsets=(x0, y0), + block_shape=(XBLOCK, YBLOCK), + order=(0, 1), + ) + + val = tl.load(tptr) + tl.store(out + (x * STRIDE_X + STRIDE_Y * y), val, mask=mask) + + +@triton.jit +def make_tensor_ptr_implicit_perm_load_store_3d_static_stride( + ptr, + out, + znumel, + ynumel, + xnumel, + STRIDE_Z: tl.constexpr, + STRIDE_Y: tl.constexpr, + STRIDE_X: tl.constexpr, + ZBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + pid_x = tl.program_id(0) + pid_y = tl.program_id(1) + pid_z = tl.program_id(2) + + x0 = pid_x * XBLOCK + y0 = pid_y * YBLOCK + z0 = pid_z * ZBLOCK + + x = x0 + tl.arange(0, XBLOCK)[:, None, None] + y = y0 + tl.arange(0, YBLOCK)[None, :, None] + z = z0 + tl.arange(0, ZBLOCK)[None, None, :] + + mask = (x < xnumel) & (y < ynumel) & (z < znumel) + + tptr = tl.make_block_ptr( + base=ptr, + shape=(xnumel, ynumel, znumel), + strides=(STRIDE_X, STRIDE_Y, STRIDE_Z), + offsets=(x0, y0, z0), + block_shape=(XBLOCK, YBLOCK, ZBLOCK), + order=(0, 1, 2), + ) + + val = tl.load(tptr) + tl.store(out + (x * STRIDE_X + y * STRIDE_Y + z * STRIDE_Z), val, mask=mask) + + +@triton.jit +def make_tensor_ptr_implicit_perm_load_store_3d( + ptr, + out, + znumel, + ynumel, + xnumel, + STRIDE_Z, + STRIDE_Y, + STRIDE_X, + ZBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + pid_x = tl.program_id(0) + pid_y = tl.program_id(1) + pid_z = tl.program_id(2) + + x0 = pid_x * XBLOCK + y0 = pid_y * YBLOCK + z0 = pid_z * ZBLOCK + + x = x0 + tl.arange(0, XBLOCK)[:, None, None] + y = y0 + tl.arange(0, YBLOCK)[None, :, None] + z = z0 + tl.arange(0, ZBLOCK)[None, None, :] + + mask = (x < xnumel) & (y < ynumel) & (z < znumel) + + tptr = tl.make_block_ptr( + base=ptr, + shape=(xnumel, ynumel, znumel), + strides=(STRIDE_X, STRIDE_Y, STRIDE_Z), + offsets=(x0, y0, z0), + block_shape=(XBLOCK, YBLOCK, ZBLOCK), + order=(0, 1, 2), + ) + + val = tl.load(tptr) + tl.store(out + (x * STRIDE_X + y * STRIDE_Y + z * STRIDE_Z), val, mask=mask) + + +@triton.jit +def make_tensor_ptr_implicit_perm_load_3d_static_stride( + ptr, + out, + znumel, # logical z (== X) + ynumel, # logical y (== Y) + xnumel, # logical x (== Z) + STRIDE_Z: tl.constexpr, + STRIDE_Y: tl.constexpr, + STRIDE_X: tl.constexpr, + # out is row-major with shape (xnumel, ynumel, znumel) + OUT_STRIDE0: tl.constexpr, # = ynumel*znumel + OUT_STRIDE1: tl.constexpr, # = znumel + OUT_STRIDE2: tl.constexpr, # = 1 + ZBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + pid_x = tl.program_id(0) + pid_y = tl.program_id(1) + pid_z = tl.program_id(2) + + x0 = pid_x * XBLOCK + y0 = pid_y * YBLOCK + z0 = pid_z * ZBLOCK + + x = x0 + tl.arange(0, XBLOCK)[:, None, None] + y = y0 + tl.arange(0, YBLOCK)[None, :, None] + z = z0 + tl.arange(0, ZBLOCK)[None, None, :] + + mask = (x < xnumel) & (y < ynumel) & (z < znumel) + + # load: implicit permute view + tptr = tl.make_block_ptr( + base=ptr, + shape=(xnumel, ynumel, znumel), + strides=(STRIDE_X, STRIDE_Y, STRIDE_Z), + offsets=(x0, y0, z0), + block_shape=(XBLOCK, YBLOCK, ZBLOCK), + order=(0, 1, 2), + ) + val = tl.load(tptr) + + # store: row-major output (no implicit permute) + out_offset = x * OUT_STRIDE0 + y * OUT_STRIDE1 + z * OUT_STRIDE2 + tl.store(out + out_offset, val, mask=mask) + + +# ---------------------------------------------------------- +# Triton kernel: advance with implicit permute kernels +# ---------------------------------------------------------- +@triton.jit +def advance_implicit_perm_load_store_2d_static_stride(ptr, out, ynumel, xnumel, STRIDE_Y: tl.constexpr, + STRIDE_X: tl.constexpr, YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr): + y0 = tl.program_id(1) * YBLOCK + x0 = tl.program_id(0) * XBLOCK + y = y0 + tl.arange(0, YBLOCK)[None, :] + x = x0 + tl.arange(0, XBLOCK)[:, None] + mask = (x < xnumel) & (y < ynumel) + + tptr = tl.make_block_ptr( + base=ptr, + shape=(xnumel, ynumel), + strides=(STRIDE_X, STRIDE_Y), + offsets=(0, 0), + block_shape=(XBLOCK, YBLOCK), + order=(0, 1), + ) + tptr2 = tl.advance(tptr, (x0, y0)) + val = tl.load(tptr2) + tl.store(out + (x * STRIDE_X + y * STRIDE_Y), val, mask=mask) + + +@triton.jit +def advance_implicit_perm_load_store_3d_static_stride( + ptr, + out, + znumel, + ynumel, + xnumel, + STRIDE_Z: tl.constexpr, + STRIDE_Y: tl.constexpr, + STRIDE_X: tl.constexpr, + ZBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, + XBLOCK: tl.constexpr, +): + pid_x = tl.program_id(0) + pid_y = tl.program_id(1) + pid_z = tl.program_id(2) + + x0 = pid_x * XBLOCK + y0 = pid_y * YBLOCK + z0 = pid_z * ZBLOCK + + x = x0 + tl.arange(0, XBLOCK)[:, None, None] + y = y0 + tl.arange(0, YBLOCK)[None, :, None] + z = z0 + tl.arange(0, ZBLOCK)[None, None, :] + + mask = (x < xnumel) & (y < ynumel) & (z < znumel) + + tptr0 = tl.make_block_ptr( + base=ptr, + shape=(xnumel, ynumel, znumel), + strides=(STRIDE_X, STRIDE_Y, STRIDE_Z), + offsets=(0, 0, 0), + block_shape=(XBLOCK, YBLOCK, ZBLOCK), + order=(0, 1, 2), + ) + tptr = tl.advance(tptr0, (x0, y0, z0)) + val = tl.load(tptr) + tl.store(out + (x * STRIDE_X + y * STRIDE_Y + z * STRIDE_Z), val, mask=mask) + + +# ---------------------------------------------------------- +# pytest case +# ---------------------------------------------------------- +def ceil_div(a, b): + return (a + b - 1) // b + + +def _assert_row_major_2d(A, X, Y): + assert tuple(A.shape) == (X, Y) + assert A.is_contiguous() + assert A.stride() == (Y, 1) + + +def _assert_row_major_3d(A, X, Y, Z): + # [X, Y, Z] contiguous -> stride = (Y*Z, Z, 1) + assert tuple(A.shape) == (X, Y, Z) + assert A.is_contiguous() + assert A.stride() == (Y * Z, Z, 1) + + +def _assert_row_major_4d(A, X, Y, Z, W): + # [X, Y, Z, W] contiguous -> stride = (Y*Z*W, Z*W, W, 1) + assert tuple(A.shape) == (X, Y, Z, W) + assert A.is_contiguous() + assert A.stride() == (Y * Z * W, Z * W, W, 1) + + +# ---------------------------------------------------------- +# pytest case: addptr kernels +# ---------------------------------------------------------- +@pytest.mark.parametrize("X, Y, XBLOCK, YBLOCK", case_2d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_addptr_implicit_perm_load_2d(X, Y, XBLOCK, YBLOCK, dtype, sigtype): + A = test_common.generate_tensor(shape=(X, Y), dtype=sigtype).npu() + _assert_row_major_2d(A, X, Y) + + xnumel = Y + ynumel = X + + STRIDE_X = 1 + STRIDE_Y = Y + + out = torch.empty((xnumel, ynumel), device="npu", dtype=A.dtype) + assert out.is_contiguous() + OUT_STRIDE_X = ynumel + OUT_STRIDE_Y = 1 + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + addptr_implicit_perm_load_2d[grid]( + A, + out, + ynumel, + xnumel, + STRIDE_Y, + STRIDE_X, + OUT_STRIDE_X, + OUT_STRIDE_Y, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + ref = A.T.contiguous() + torch.testing.assert_close(out, ref) + + +@pytest.mark.parametrize("X, Y, XBLOCK, YBLOCK", case_2d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_addptr_implicit_perm_store_2d(X, Y, XBLOCK, YBLOCK, dtype, sigtype): + """ + Test goal: + - Real memory layout: A[X, Y], row-major (stride = (Y, 1)) + - Load uses row-major semantics only + - Store uses implicit transpose semantics only + - Output shape: [Y, X] row-major + - Result: out == A.T + """ + A = test_common.generate_tensor(shape=(X, Y), dtype=sigtype).npu() + _assert_row_major_2d(A, X, Y) + + xnumel = X + ynumel = Y + + IN_STRIDE_X = Y + IN_STRIDE_Y = 1 + + STRIDE_X = 1 + STRIDE_Y = X + + out = torch.empty((Y, X), device="npu", dtype=A.dtype) + assert out.is_contiguous() + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + addptr_implicit_perm_store_2d[grid]( + A, + out, + ynumel, + xnumel, + IN_STRIDE_X, + IN_STRIDE_Y, + STRIDE_Y, + STRIDE_X, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + ref = A.T.contiguous() + torch.testing.assert_close(out, ref) + + +@pytest.mark.parametrize("X, Y, XBLOCK, YBLOCK", case_2d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_addptr_implicit_perm_load_store_2d_static_stride( + X, + Y, + XBLOCK, + YBLOCK, + dtype, + sigtype, +): + """ + Test goal: + - Real memory layout: A[X, Y], row-major (stride = (Y, 1)) + - Kernel view: A^T[Y, X], stride = (1, Y) + - Kernel does load+store with identical offsets + - Result must satisfy: out == in + """ + A = test_common.generate_tensor( + shape=(X, Y), + dtype=sigtype, + ).npu() + + _assert_row_major_2d(A, X, Y) + + out = torch.zeros_like(A) + + # A^T logical shape + xnumel = Y # cols of A + ynumel = X # rows of A + + # A^T logical stride + stride_x = 1 + stride_y = Y + + grid = ( + ceil_div(xnumel, XBLOCK), + ceil_div(ynumel, YBLOCK), + 1, + ) + + addptr_implicit_perm_load_store_2d_static_stride[grid]( + A, + out, + ynumel, + xnumel, + stride_y, + stride_x, + XBLOCK=XBLOCK, + YBLOCK=YBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, XBLOCK, YBLOCK", case_2d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_addptr_implicit_perm_load_store_2d( + X, + Y, + XBLOCK, + YBLOCK, + dtype, + sigtype, +): + """ + Same as static-stride version, but stride passed as runtime values. + """ + A = test_common.generate_tensor( + shape=(X, Y), + dtype=sigtype, + ).npu() + + _assert_row_major_2d(A, X, Y) + + out = torch.zeros_like(A) + + # A^T logical shape + xnumel = Y # cols of A + ynumel = X # rows of A + + # A^T logical stride + stride_x = 1 + stride_y = Y + + grid = ( + ceil_div(xnumel, XBLOCK), + ceil_div(ynumel, YBLOCK), + 1, + ) + + addptr_implicit_perm_load_store_2d[grid]( + A, + out, + ynumel, + xnumel, + stride_y, + stride_x, + XBLOCK=XBLOCK, + YBLOCK=YBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, Z, XBLOCK, YBLOCK, ZBLOCK", case_3d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_addptr_implicit_perm_load_store_3d_static_stride(X, Y, Z, XBLOCK, YBLOCK, ZBLOCK, dtype, sigtype): + """ + Test goal: + - Real memory layout: A[X, Y, Z], row-major (stride = (Y*Z, Z, 1)) + - Kernel view: treat as permuted logical coords via stride: + offset = x*1 + y*Z + z*(Y*Z) + i.e. (x,y,z) mapped to base index (z, y, x) + - Kernel does load+store with identical offsets + - Result must satisfy: out == in + """ + A = test_common.generate_tensor(shape=(X, Y, Z), dtype=sigtype).npu() + _assert_row_major_3d(A, X, Y, Z) + out = torch.zeros_like(A) + + # Logical shape for "A^(perm)" (x fastest) + xnumel = Z + ynumel = Y + znumel = X + + # Logical strides (in elements): (1, Z, Y*Z) + stride_x = 1 + stride_y = Z + stride_z = Y * Z + + grid = ( + ceil_div(xnumel, XBLOCK), + ceil_div(ynumel, YBLOCK), + ceil_div(znumel, ZBLOCK), + ) + + addptr_implicit_perm_load_store_3d_static_stride[grid]( + A, + out, + znumel, + ynumel, + xnumel, + stride_z, + stride_y, + stride_x, + ZBLOCK=ZBLOCK, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, Z, XBLOCK, YBLOCK, ZBLOCK", case_3d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_addptr_implicit_perm_load_store_3d(X, Y, Z, XBLOCK, YBLOCK, ZBLOCK, dtype, sigtype): + """ + Same as static-stride version, but stride passed as runtime values. + """ + A = test_common.generate_tensor(shape=(X, Y, Z), dtype=sigtype).npu() + _assert_row_major_3d(A, X, Y, Z) + out = torch.zeros_like(A) + + xnumel = Z + ynumel = Y + znumel = X + + stride_x = 1 + stride_y = Z + stride_z = Y * Z + + grid = ( + ceil_div(xnumel, XBLOCK), + ceil_div(ynumel, YBLOCK), + ceil_div(znumel, ZBLOCK), + ) + + addptr_implicit_perm_load_store_3d[grid]( + A, + out, + znumel, + ynumel, + xnumel, + stride_z, + stride_y, + stride_x, + ZBLOCK=ZBLOCK, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, Z, W, XBLOCK, YBLOCK, ZBLOCK, WBLOCK", case_4d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_addptr_implicit_perm_load_store_4d_static_stride(X, Y, Z, W, XBLOCK, YBLOCK, ZBLOCK, WBLOCK, dtype, sigtype): + """ + Test goal: + - Real memory layout: A[X, Y, Z, W], row-major (stride = (Y*Z*W, Z*W, W, 1)) + - Kernel view: treat as permuted logical coords via stride: + offset = x*1 + y*W + z*(Z*W) + w*(Y*Z*W) + i.e. (x,y,z,w) mapped to base index (w, z, y, x) + - Kernel does load+store with identical offsets + - Result must satisfy: out == in + """ + A = test_common.generate_tensor(shape=(X, Y, Z, W), dtype=sigtype).npu() + _assert_row_major_4d(A, X, Y, Z, W) + out = torch.zeros_like(A) + + # Logical shape (x fastest) + xnumel = W + ynumel = Z + znumel = Y + wnumel = X + + # Logical strides (in elements): (1, W, Z*W, Y*Z*W) + stride_x = 1 + stride_y = W + stride_z = Z * W + stride_w = Y * Z * W + + # Kernel maps pid0 over (w, x). It uses xblocks_per_w computed from xnumel. + xblocks_per_w = ceil_div(xnumel, XBLOCK) + grid0 = wnumel * xblocks_per_w + grid = ( + grid0, + ceil_div(ynumel, YBLOCK), + ceil_div(znumel, ZBLOCK), + ) + + addptr_implicit_perm_load_store_4d_static_stride[grid]( + A, + out, + wnumel, + znumel, + ynumel, + xnumel, + stride_w, + stride_z, + stride_y, + stride_x, + WBLOCK=WBLOCK, + ZBLOCK=ZBLOCK, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, Z, W, XBLOCK, YBLOCK, ZBLOCK, WBLOCK", case_4d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_addptr_implicit_perm_load_store_4d(X, Y, Z, W, XBLOCK, YBLOCK, ZBLOCK, WBLOCK, dtype, sigtype): + """ + Same as static-stride version, but stride passed as runtime values. + """ + A = test_common.generate_tensor(shape=(X, Y, Z, W), dtype=sigtype).npu() + _assert_row_major_4d(A, X, Y, Z, W) + out = torch.zeros_like(A) + + xnumel = W + ynumel = Z + znumel = Y + wnumel = X + + stride_x = 1 + stride_y = W + stride_z = Z * W + stride_w = Y * Z * W + + xblocks_per_w = ceil_div(xnumel, XBLOCK) + grid0 = wnumel * xblocks_per_w + grid = ( + grid0, + ceil_div(ynumel, YBLOCK), + ceil_div(znumel, ZBLOCK), + ) + + addptr_implicit_perm_load_store_4d[grid]( + A, + out, + wnumel, + znumel, + ynumel, + xnumel, + stride_w, + stride_z, + stride_y, + stride_x, + WBLOCK=WBLOCK, + ZBLOCK=ZBLOCK, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +# ---------------------------------------------------------- +# pytest case: make_tensor_ptr kernels +# ---------------------------------------------------------- +@pytest.mark.parametrize("X, Y, XBLOCK, YBLOCK", case_2d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_make_tensor_ptr_implicit_perm_load_2d(X, Y, XBLOCK, YBLOCK, dtype, sigtype): + """ + Test goal: + - Real memory layout: A[X, Y], row-major (stride = (Y, 1)) + - Kernel view (logical): shape = (Y, X), stride = (1, Y) (implicit transpose) + - Load via make_block_ptr with implicit permute stride + - Store with row-major output stride + - Output shape: [Y, X] row-major + - Result: out == A.T + """ + A = test_common.generate_tensor(shape=(X, Y), dtype=sigtype).npu() + _assert_row_major_2d(A, X, Y) + + xnumel = Y + ynumel = X + + STRIDE_X = 1 + STRIDE_Y = Y + + out = torch.empty((xnumel, ynumel), device="npu", dtype=A.dtype) + assert out.is_contiguous() + OUT_STRIDE_X = ynumel + OUT_STRIDE_Y = 1 + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + make_tensor_ptr_implicit_perm_load_2d[grid]( + A, + out, + ynumel, + xnumel, + STRIDE_Y, + STRIDE_X, + OUT_STRIDE_X, + OUT_STRIDE_Y, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + ref = A.T.contiguous() + torch.testing.assert_close(out, ref) + + +@pytest.mark.parametrize("X, Y, XBLOCK, YBLOCK", case_2d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_make_tensor_ptr_implicit_perm_store_2d(X, Y, XBLOCK, YBLOCK, dtype, sigtype): + """ + Test goal: + - Real memory layout: A[X, Y], row-major (stride = (Y, 1)) + - Load uses row-major semantics only + - Store via make_block_ptr with implicit transpose semantics only + - Output shape: [Y, X] row-major + - Result: out == A.T + """ + A = test_common.generate_tensor(shape=(X, Y), dtype=sigtype).npu() + _assert_row_major_2d(A, X, Y) + + xnumel = X + ynumel = Y + + IN_STRIDE_X = Y + IN_STRIDE_Y = 1 + + STRIDE_X = 1 + STRIDE_Y = X + + out = torch.empty((Y, X), device="npu", dtype=A.dtype) + assert out.is_contiguous() + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + make_tensor_ptr_implicit_perm_store_2d[grid]( + A, + out, + ynumel, + xnumel, + IN_STRIDE_X, + IN_STRIDE_Y, + STRIDE_Y, + STRIDE_X, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + ref = A.T.contiguous() + torch.testing.assert_close(out, ref) + + +@pytest.mark.parametrize("X, Y, XBLOCK, YBLOCK", case_2d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_make_tensor_ptr_implicit_perm_load_store_2d_static_stride(X, Y, XBLOCK, YBLOCK, dtype, sigtype): + """ + Test goal matches addptr_2d_static_stride, but uses tl.make_block_ptr + tl.load(tptr). + Real layout: A[X,Y] row-major stride=(Y,1) + Kernel view: A^T[Y,X] stride=(1,Y) + Store is by explicit linear offset with same logical stride. + """ + A = test_common.generate_tensor(shape=(X, Y), dtype=sigtype).npu() + _assert_row_major_2d(A, X, Y) + out = torch.zeros_like(A) + + xnumel = Y + ynumel = X + STRIDE_X = 1 + STRIDE_Y = Y + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + make_tensor_ptr_implicit_perm_load_store_2d_static_stride[grid]( + A, + out, + ynumel, + xnumel, + STRIDE_Y=STRIDE_Y, + STRIDE_X=STRIDE_X, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, Z, XBLOCK, YBLOCK, ZBLOCK", case_3d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_make_tensor_ptr_implicit_perm_load_store_3d_static_stride(X, Y, Z, XBLOCK, YBLOCK, ZBLOCK, dtype, sigtype): + """ + Real layout: A[X,Y,Z] row-major stride=(Y*Z, Z, 1) + Kernel view (logical): shape=(Z,Y,X), strides=(1, Z, Y*Z) + """ + A = test_common.generate_tensor(shape=(X, Y, Z), dtype=sigtype).npu() + _assert_row_major_3d(A, X, Y, Z) + out = torch.zeros_like(A) + + xnumel = Z + ynumel = Y + znumel = X + STRIDE_X = 1 + STRIDE_Y = Z + STRIDE_Z = Y * Z + + grid = ( + ceil_div(xnumel, XBLOCK), + ceil_div(ynumel, YBLOCK), + ceil_div(znumel, ZBLOCK), + ) + + make_tensor_ptr_implicit_perm_load_store_3d_static_stride[grid]( + A, + out, + znumel, + ynumel, + xnumel, + STRIDE_Z=STRIDE_Z, + STRIDE_Y=STRIDE_Y, + STRIDE_X=STRIDE_X, + ZBLOCK=ZBLOCK, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, Z, XBLOCK, YBLOCK, ZBLOCK", case_3d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_make_tensor_ptr_implicit_perm_load_store_3d(X, Y, Z, XBLOCK, YBLOCK, ZBLOCK, dtype, sigtype): + """ + Same as static stride but STRIDE_* passed at runtime. + """ + A = test_common.generate_tensor(shape=(X, Y, Z), dtype=sigtype).npu() + _assert_row_major_3d(A, X, Y, Z) + out = torch.zeros_like(A) + + xnumel = Z + ynumel = Y + znumel = X + STRIDE_X = 1 + STRIDE_Y = Z + STRIDE_Z = Y * Z + + grid = ( + ceil_div(xnumel, XBLOCK), + ceil_div(ynumel, YBLOCK), + ceil_div(znumel, ZBLOCK), + ) + + make_tensor_ptr_implicit_perm_load_store_3d[grid]( + A, + out, + znumel, + ynumel, + xnumel, + STRIDE_Z, + STRIDE_Y, + STRIDE_X, + ZBLOCK=ZBLOCK, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, Z, XBLOCK, YBLOCK, ZBLOCK", case_3d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_make_tensor_ptr_implicit_perm_load_3d_static_stride(X, Y, Z, XBLOCK, YBLOCK, ZBLOCK, dtype, sigtype): + A = test_common.generate_tensor(shape=(X, Y, Z), dtype=sigtype).npu() + _assert_row_major_3d(A, X, Y, Z) + + # logical/permuted shape + xnumel = Z + ynumel = Y + znumel = X + + # implicit-permute strides (elements) + STRIDE_X = 1 + STRIDE_Y = Z + STRIDE_Z = Y * Z + + # output is row-major of shape (Z, Y, X) + out = torch.empty((xnumel, ynumel, znumel), device="npu", dtype=A.dtype) + assert out.is_contiguous() + OUT_STRIDE0 = ynumel * znumel # Y*X + OUT_STRIDE1 = znumel # X + OUT_STRIDE2 = 1 + + grid = ( + ceil_div(xnumel, XBLOCK), + ceil_div(ynumel, YBLOCK), + ceil_div(znumel, ZBLOCK), + ) + + make_tensor_ptr_implicit_perm_load_3d_static_stride[grid]( + A, + out, + znumel, + ynumel, + xnumel, + STRIDE_Z=STRIDE_Z, + STRIDE_Y=STRIDE_Y, + STRIDE_X=STRIDE_X, + OUT_STRIDE0=OUT_STRIDE0, + OUT_STRIDE1=OUT_STRIDE1, + OUT_STRIDE2=OUT_STRIDE2, + ZBLOCK=ZBLOCK, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + # expected: out[x,y,z] == A[z,y,x] => out == A.permute(2,1,0) + ref = A.permute(2, 1, 0).contiguous() + torch.testing.assert_close(out, ref) + + +# ---------------------------------------------------------- +# pytest case: advance kernels +# ---------------------------------------------------------- +@pytest.mark.parametrize("X, Y, XBLOCK, YBLOCK", case_2d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_advance_implicit_perm_load_store_2d_static_stride(X, Y, XBLOCK, YBLOCK, dtype, sigtype): + """ + Same goal as addptr_2d_static_stride, but uses tl.make_block_ptr + tl.advance. + """ + A = test_common.generate_tensor(shape=(X, Y), dtype=sigtype).npu() + _assert_row_major_2d(A, X, Y) + out = torch.zeros_like(A) + + xnumel = Y + ynumel = X + STRIDE_X = 1 + STRIDE_Y = Y + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + advance_implicit_perm_load_store_2d_static_stride[grid]( + A, + out, + ynumel, + xnumel, + STRIDE_Y=STRIDE_Y, + STRIDE_X=STRIDE_X, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +@pytest.mark.parametrize("X, Y, Z, XBLOCK, YBLOCK, ZBLOCK", case_3d) +@pytest.mark.parametrize("dtype, sigtype", types_all) +def test_advance_implicit_perm_load_store_3d_static_stride(X, Y, Z, XBLOCK, YBLOCK, ZBLOCK, dtype, sigtype): + """ + Real layout: A[X,Y,Z] row-major stride=(Y*Z, Z, 1) + Kernel view (logical): shape=(Z,Y,X), strides=(1, Z, Y*Z) + """ + A = test_common.generate_tensor(shape=(X, Y, Z), dtype=sigtype).npu() + _assert_row_major_3d(A, X, Y, Z) + out = torch.zeros_like(A) + + xnumel = Z + ynumel = Y + znumel = X + STRIDE_X = 1 + STRIDE_Y = Z + STRIDE_Z = Y * Z + + grid = ( + ceil_div(xnumel, XBLOCK), + ceil_div(ynumel, YBLOCK), + ceil_div(znumel, ZBLOCK), + ) + + advance_implicit_perm_load_store_3d_static_stride[grid]( + A, + out, + znumel, + ynumel, + xnumel, + STRIDE_Z=STRIDE_Z, + STRIDE_Y=STRIDE_Y, + STRIDE_X=STRIDE_X, + ZBLOCK=ZBLOCK, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + torch.testing.assert_close(out, A) + + +if __name__ == "__main__": + test_addptr_implicit_perm_load_2d(*case_2d[0], *types_all[0]) + test_addptr_implicit_perm_store_2d(*case_2d[0], *types_all[0]) + test_addptr_implicit_perm_load_store_2d_static_stride(*case_2d[0], *types_all[0]) + test_addptr_implicit_perm_load_store_2d(*case_2d[0], *types_all[0]) + test_addptr_implicit_perm_load_store_3d_static_stride(*case_3d[0], *types_all[0]) + test_addptr_implicit_perm_load_store_3d(*case_3d[0], *types_all[0]) + test_addptr_implicit_perm_load_store_4d_static_stride(*case_4d[0], *types_all[0]) + test_addptr_implicit_perm_load_store_4d(*case_4d[0], *types_all[0]) + + test_make_tensor_ptr_implicit_perm_load_2d(*case_2d[0], *types_all[0]) + test_make_tensor_ptr_implicit_perm_store_2d(*case_2d[0], *types_all[0]) + test_make_tensor_ptr_implicit_perm_load_3d_static_stride(*case_3d[0], *types_all[0]) + test_make_tensor_ptr_implicit_perm_load_store_2d_static_stride(*case_2d[0], *types_all[0]) + test_make_tensor_ptr_implicit_perm_load_store_3d_static_stride(*case_3d[0], *types_all[0]) + test_make_tensor_ptr_implicit_perm_load_store_3d(*case_3d[0], *types_all[0]) + test_advance_implicit_perm_load_store_2d_static_stride(*case_2d[0], *types_all[0]) + test_advance_implicit_perm_load_store_3d_static_stride(*case_3d[0], *types_all[0]) diff --git a/third_party/ascend/unittest/pytest_ut/test_import_time.py b/third_party/ascend/unittest/pytest_ut/test_import_time.py new file mode 100644 index 0000000000..668c0774d1 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_import_time.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Regression test for triton import time. + +Guards against accidental regressions from PR #1200 (lazy init of +``is_compile_on_910_95`` and moving the check out of module import time). +Import is measured in a fresh subprocess so the measurement is not masked by +triton already being imported in the pytest worker process. +""" + +import subprocess +import sys + +import pytest + +pytestmark = pytest.mark.backend("native") + +# Upper limit (ms) for ``import triton``. The baseline after PR #1200 is +# ~240ms on the dev container; 1000ms gives ~4x headroom to absorb CI jitter +# while still catching real regressions (e.g. re-introducing eager init). +IMPORT_TIME_LIMIT_MS = 1000 + +# Measure only the import call; interpreter startup is excluded by reading the +# elapsed time from inside the subprocess. +_TIMING_SCRIPT = ("import time\n" + "t = time.perf_counter()\n" + "import triton\n" + "print((time.perf_counter() - t) * 1000)\n") + + +@pytest.mark.parametrize("run_id", range(3)) +def test_import_triton_time_under_limit(run_id): + result = subprocess.run( + [sys.executable, "-c", _TIMING_SCRIPT], + capture_output=True, + text=True, + check=True, + ) + elapsed_ms = float(result.stdout.strip()) + msg = (f"run {run_id}: import triton took {elapsed_ms:.1f} ms " + f"(limit {IMPORT_TIME_LIMIT_MS} ms)") + assert elapsed_ms < IMPORT_TIME_LIMIT_MS, msg diff --git a/third_party/ascend/unittest/pytest_ut/test_index_select.py b/third_party/ascend/unittest/pytest_ut/test_index_select.py new file mode 100644 index 0000000000..9be813be3a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_index_select.py @@ -0,0 +1,292 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as extension + +import torch +import torch_npu +import pytest +import test_common + +# ============================================================================ +# PyTorch Reference Implementation +# ============================================================================ + + +def torch_index_select(x0, dim, indices): + """PyTorch reference implementation using torch.index_select.""" + return torch.index_select(x0, dim, indices) + + +# ============================================================================ +# Triton Kernel Implementations +# ============================================================================ + + +@triton.jit +def index_select_manual_kernel(in_ptr, indices_ptr, out_ptr, dim, g_stride: tl.constexpr, indice_length: tl.constexpr, + g_block: tl.constexpr, g_block_sub: tl.constexpr, other_block: tl.constexpr): + """ + Manual implementation using extension.get_element and extension.insert_slice. + + This is a baseline implementation without using the index_select_simd intrinsic. + """ + g_begin = tl.program_id(0) * g_block + for goffs in range(0, g_block, g_block_sub): + g_idx = tl.arange(0, g_block_sub) + g_begin + goffs + g_mask = g_idx < indice_length + indices = tl.load(indices_ptr + g_idx, g_mask, other=0) + + for other_offset in range(0, g_stride, other_block): + tmp_buf = tl.zeros((g_block_sub, other_block), in_ptr.dtype.element_ty) + other_idx = tl.arange(0, other_block) + other_offset + other_mask = other_idx < g_stride + + # Manual gather: iterate over each index + for i in range(0, g_block_sub): + gather_offset = extension.get_element(indices, (i, )) * g_stride + val = tl.load(in_ptr + gather_offset + other_idx, other_mask) + tmp_buf = extension.insert_slice(tmp_buf, val[None, :], offsets=(i, 0), sizes=(1, other_block), + strides=(1, 1)) + + tl.store(out_ptr + g_idx[:, None] * g_stride + other_idx[None, :], tmp_buf, + g_mask[:, None] & other_mask[None, :]) + + +@triton.jit +def index_select_extension_kernel(in_ptr, indices_ptr, out_ptr, dim: tl.constexpr, other_numel: tl.constexpr, + g_stride: tl.constexpr, indice_length: tl.constexpr, g_block: tl.constexpr, + g_block_sub: tl.constexpr, other_block: tl.constexpr): + """ + Implementation using extension.index_select_simd intrinsic. + + This uses the hardware-accelerated SIMD index_select operation. + """ + g_begin = tl.program_id(0) * g_block + for goffs in range(0, g_block, g_block_sub): + g_idx = tl.arange(0, g_block_sub) + g_begin + goffs + g_mask = g_idx < indice_length + indices = tl.load(indices_ptr + g_idx, g_mask, other=0) + + for other_offset in range(0, g_stride, other_block): + other_idx = tl.arange(0, other_block) + other_offset + other_mask = other_idx < g_stride + + # Use extension index_select_simd + tmp_buf = extension.index_select_simd(src=in_ptr, dim=dim, index=indices, src_shape=(other_numel, g_stride), + src_offset=(-1, 0), read_shape=(-1, other_block)) + + tl.store(out_ptr + g_idx[:, None] * g_stride + other_idx[None, :], tmp_buf, + g_mask[:, None] & other_mask[None, :]) + + +@triton.jit +def index_select_auto_kernel(in_ptr, indices_ptr, out_ptr, dim: tl.constexpr, other_numel: tl.constexpr, + g_stride: tl.constexpr, indice_length: tl.constexpr, g_block: tl.constexpr, + g_block_sub: tl.constexpr, other_block: tl.constexpr): + """ + Auto-lowering implementation using standard tl.load with computed offsets. + + This lets the compiler automatically handle the index_select pattern. + """ + g_begin = tl.program_id(0) * g_block + for goffs in range(0, g_block, g_block_sub): + g_idx = tl.arange(0, g_block_sub) + g_begin + goffs + g_mask = g_idx < indice_length + indices = tl.load(indices_ptr + g_idx, g_mask, other=0) + + for other_offset in range(0, g_stride, other_block): + other_idx = tl.arange(0, other_block) + other_offset + other_mask = other_idx < g_stride + + # Auto-lowering: compute offsets and use standard load + src_offsets = indices[:, None] * g_stride + other_idx[None, :] + tmp_buf = tl.load(in_ptr + src_offsets, g_mask[:, None] & other_mask[None, :]) + + tl.store(out_ptr + g_idx[:, None] * g_stride + other_idx[None, :], tmp_buf, + g_mask[:, None] & other_mask[None, :]) + + +# ============================================================================ +# Host Functions +# ============================================================================ + + +def triton_index_select(x0, dim, indices, impl='extension', num_vec_core=48): + """ + Triton implementation of index_select. + + Args: + x0: Source tensor + dim: Dimension to select from + indices: Indices to select + impl: Implementation to use ('manual', 'extension', or 'auto') + num_vec_core: Number of vector cores to use + + Returns: + Output tensor with selected indices + """ + sz = list(x0.shape) + sz[dim] = len(indices) + out = torch.empty(tuple(sz), dtype=x0.dtype).npu() + + g_stride = x0.stride(dim) + indice_length = indices.numel() + g_block = (indice_length - 1) // num_vec_core + 1 + + # Calculate UB space allocation + enable_multi_buffer = True + ub_size = 125 * 1024 // (2 if enable_multi_buffer else 1) + other_block = g_stride + g_block_sub = ub_size // ( + # max memory consumption: arith.select + other (mask handling in auto) + x0.element_size() * g_stride * 3 + indices.element_size()) + if g_block_sub < 1: + other_block = (ub_size - indices.element_size()) // x0.element_size() + g_block_sub = 1 + + # Select kernel based on implementation + if impl == 'manual': + kernel = index_select_manual_kernel + kernel[num_vec_core, 1, 1](x0, indices, out, dim, g_stride=g_stride, indice_length=indice_length, + g_block=g_block, g_block_sub=g_block_sub, other_block=other_block, multibuffer=False) + elif impl == 'extension': + kernel = index_select_extension_kernel + kernel[num_vec_core, 1, + 1](x0, indices, out, dim, other_numel=sz[0], g_stride=g_stride, indice_length=indice_length, + g_block=g_block, g_block_sub=g_block_sub, other_block=other_block) + elif impl == 'auto': + kernel = index_select_auto_kernel + kernel[num_vec_core, 1, + 1](x0, indices, out, dim, other_numel=sz[0], g_stride=g_stride, indice_length=indice_length, + g_block=g_block, g_block_sub=g_block_sub, other_block=other_block) + else: + raise ValueError(f"Unknown implementation: {impl}") + + return out + + +# ============================================================================ +# Test Parameters +# ============================================================================ + +# Test cases with various shapes and sizes +# Format: (src_shape, dim, indice_shape, dtype) +INDEX_SELECT_TEST_CASES = [ + # Small scale tests (< 100us) + ((10, 16), 0, (1024, ), "float32"), # 27us fp32 + ((20669, 8), 0, (2048, ), "float32"), # 31us fp32 + ((1034, 16), 0, (48000, ), "float32"), # 38us fp32 + ((270098, 32), 0, (512, ), "float32"), # 40us fp32 + ((1000000, 8), 0, (21329, ), "float32"), # 44us fp32 + + # Medium scale tests (100us - 2000us) + ((22717, 8), 0, (278400, ), "float32"), # 76us fp32 + ((2000000, 32), 0, (270244, ), "float32"), # 155us fp32 + ((500000, 37), 0, (324344, ), "float32"), # 266us +- 10us fp32 + ((270610, 32), 0, (2928000, ), "float32"), # 553us fp32 + ((3200, 16), 0, (1971940, ), "float32"), # 1104us fp32 + ((3200, 1, 37), 0, (1022226, ), "float32"), # 1965us +- 65us fp32 + + # Large scale tests (> 2000us) + ((500000, 240), 0, (375144, ), "float32"), # 1984us +- 5us fp32 48core + ((323357, 37), 0, (1022226, ), "float32"), # 3500us +- 50us fp32 + ((480000, 16), 0, (3943880, ), "float32"), # 3636 +- 42us fp32 + ((480000, 32), 0, (3943880, ), "float32"), # 3678 +- 5us fp32 + ((480000, 64), 0, (3943880, ), "float32"), # 5392 +- 20us fp32 + ((378103, 240), 0, (1992337, ), "float32"), # 10000us fp32 + ((374035, 240), 0, (1971940, ), "float32"), # 10100us fp32 +] + +# ============================================================================ +# Test Cases +# ============================================================================ + + +@pytest.mark.parametrize("src_shape, dim, indice_shape, dtype", INDEX_SELECT_TEST_CASES) +def test_index_select_manual(src_shape, dim, indice_shape, dtype): + """Test manual implementation using extension.get_element and extension.insert_slice.""" + x0 = test_common.generate_tensor(shape=src_shape, dtype=dtype).npu() + indices = torch.randint(0, src_shape[dim], size=indice_shape, dtype=torch.int32).npu() + + torch_ref = torch_index_select(x0, dim, indices) + triton_cal = triton_index_select(x0, dim, indices, impl='manual', num_vec_core=40) + + assert torch.equal(torch_ref, triton_cal), \ + f"Manual implementation failed for shape={src_shape}, dim={dim}, indices={indice_shape}" + + +@pytest.mark.parametrize("src_shape, dim, indice_shape, dtype", INDEX_SELECT_TEST_CASES) +def test_index_select_extension(src_shape, dim, indice_shape, dtype): + """Test extension.index_select_simd implementation.""" + x0 = test_common.generate_tensor(shape=src_shape, dtype=dtype).npu() + indices = torch.randint(0, src_shape[dim], size=indice_shape, dtype=torch.int32).npu() + + torch_ref = torch_index_select(x0, dim, indices) + triton_cal = triton_index_select(x0, dim, indices, impl='extension', num_vec_core=48) + + test_common.validate_cmp(dtype, triton_cal, torch_ref) + + +@pytest.mark.interpreter +def test_index_select_extension_bool(): + src_shape = (64, 128) + indices = torch.tensor([0, 7, 31, 63], dtype=torch.int32).npu() + x0 = test_common.generate_tensor(src_shape, "bool").npu() + + out = torch.empty((len(indices), src_shape[1]), dtype=x0.dtype).npu() + index_select_extension_kernel[1, 1, 1]( + x0, + indices, + out, + 0, + other_numel=src_shape[0], + g_stride=src_shape[1], + indice_length=indices.numel(), + g_block=indices.numel(), + g_block_sub=indices.numel(), + other_block=src_shape[1], + ) + + torch_ref = torch_index_select(x0, 0, indices) + test_common.validate_cmp("bool", out, torch_ref) + + +@pytest.mark.parametrize("src_shape, dim, indice_shape, dtype", INDEX_SELECT_TEST_CASES) +def test_index_select_auto(src_shape, dim, indice_shape, dtype): + """Test auto-lowering implementation using standard tl.load.""" + x0 = test_common.generate_tensor(shape=src_shape, dtype=dtype).npu() + indices = torch.randint(0, src_shape[dim], size=indice_shape, dtype=torch.int32).npu() + + torch_ref = torch_index_select(x0, dim, indices) + triton_cal = triton_index_select(x0, dim, indices, impl='auto', num_vec_core=48) + + test_common.validate_cmp(dtype, triton_cal, torch_ref) + + +# Quick smoke test +if __name__ == "__main__": + test_index_select_extension((500000, 37), 0, (324344, ), "float32") + print("extension implementation passed") + + test_index_select_auto((500000, 37), 0, (324344, ), "float32") + print("auto-lowering implementation passed") diff --git a/third_party/ascend/unittest/pytest_ut/test_index_select_inductor.py b/third_party/ascend/unittest/pytest_ut/test_index_select_inductor.py new file mode 100644 index 0000000000..b66367b6eb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_index_select_inductor.py @@ -0,0 +1,91 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu + +import test_common + + +@triton.jit +def triton_unk_fused_embedding_0(in_ptr0, in_ptr1, out_ptr0, y0_numel, x1_numel, Y0BLOCK: tl.constexpr, + Y0BLOCK_SUB: tl.constexpr): + x1_numel = 64 + X1BLOCK_SUB: tl.constexpr = 64 + y0_offset = tl.program_id(0) * Y0BLOCK + base_y0 = tl.arange(0, Y0BLOCK_SUB) + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + base_x1 = tl.arange(0, X1BLOCK_SUB) + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + x1 = base_x1[None, :] + tmp0 = tl.load(in_ptr0 + (y0), y0_mask) + tmp6 = tl.load(in_ptr1 + (x1 + 64 * tmp0)) + tl.store(out_ptr0 + (x1 + 64 * y0), tmp6, y0_mask) + + +def torch_embedding_impl(in_ptr0, in_ptr1): + indices_flat = in_ptr0.flatten() # [6400] + vocab_size = 1353406 + out_flat = torch.embedding(in_ptr1, indices_flat) # [6400, 16] + out = out_flat.view(128, 50, 64) + return out + + +def test_kernel(): + arg0_1 = torch.randint(0, 1353406, (128, 50), device='npu', dtype=torch.int64) + arg1_1 = torch.randn((1353406, 64), device='npu', dtype=torch.float32) + buf0 = torch.empty((128, 50, 64), device='npu', dtype=torch.float32) + + y0_numel = 128 * 50 + x1_numel = 64 + Y0BLOCK = 256 + Y0BLOCK_SUB = 16 + + grid = (triton.cdiv(y0_numel, Y0BLOCK), ) + + print(f"Grid size: {grid}") + print(f"Total programs: {grid[0]}") + print(f"y0_numel: {y0_numel}, Y0BLOCK: {Y0BLOCK}") + + triton_unk_fused_embedding_0[grid]( + arg0_1, + arg1_1, + buf0, + y0_numel, + x1_numel, + Y0BLOCK=Y0BLOCK, + Y0BLOCK_SUB=Y0BLOCK_SUB, + ) + + print(f"Output shape: {buf0.shape}") + print(f"Output sample: {buf0[0, 0, :]}") + + expected = torch_embedding_impl(arg0_1, arg1_1) + + test_common.validate_cmp("float32", buf0, expected) + + +if __name__ == "__main__": + test_kernel() diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_add.py b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_add.py new file mode 100644 index 0000000000..93f920f720 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_add.py @@ -0,0 +1,684 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Indirect atomic_add test suite +# +# Test matrix (access pattern x tensor rank): +# +# | access pattern | 1-D | 2-D | 3-D | 4-D | 5-D | +# |-------------------------------------------------|------|------|------|------|------| +# | structured pointer + discrete mask | (A1) | (A2) | (A3) | (A4) | (A5) | +# | partial structured: high-dim disc + low-dim cont| - | (B2) | (B3) | (B4) | (B5) | +# | fully unstructured indirect offsets | (C1) | (C2) | (C3) | (C4) | (C5) | +# | loaded mask + masked-off out-of-bounds offsets | (D1) | - | - | - | - | +# | scalar-splat mask shared by load and atomic | (E1) | - | - | - | - | +# | scalar-splat mask + used atomic return value | (F1) | - | - | - | - | +# +# Notes: +# 1. Case A exercises the structured-pointer discrete-mask atomic_add path. +# 2. Case B exercises a single high-dimension discrete remap with the +# remaining lower dimensions kept contiguous. +# 3. Case C exercises fully unstructured indirect offsets. +# 4. Case D verifies that a loaded discrete mask guards loaded invalid offsets. +# 5. Case E verifies that a scalar-splat mask shared by an indirect load and +# atomic_add still guards inactive programs. +# 6. Case F verifies that the old-value copy and atomic store use the same +# scalar-splat-masked address range, including inactive invalid offsets. +# 7. Cases A-D and F validate both the final destination tensor and the atomic_add +# return value, which must be the old value observed at each access. +# ============================================================================= + +import math + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +_TARGET_ARCH = triton.runtime.driver.active.get_current_target().arch + +SUPPORTED_DTYPES = [ + ("int8", torch.int8), + ("int16", torch.int16), + ("int32", torch.int32), + ("int64", torch.int64), + ("float16", torch.float16), + ("float32", torch.float32), + ("bfloat16", torch.bfloat16), + ("uint32", torch.uint32), + ("uint64", torch.uint64), +] + +RANK_SHAPES = { + 1: (8, ), + 2: (4, 4), + 3: (2, 3, 4), + 4: (2, 2, 3, 4), + 5: (2, 2, 2, 3, 4), +} + +PARTIAL_STRUCTURED_SHAPES = { + 2: (2, 16), + 3: (2, 2, 8), + 4: (2, 2, 2, 8), + 5: (2, 2, 2, 2, 8), +} + +TEST_RANKS = [3] +TEST_DTYPE = [("int32", torch.int32), ("bfloat16", torch.bfloat16)] + + +@triton.jit +def structured_disc_mask_atomic_add_1d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + mask = (linear * 2) < D0 + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def loaded_mask_oob_offset_atomic_add_1d( + idx_ptr, + mask_ptr, + val_ptr, + out_ptr, + old_ptr, + BLOCK_SIZE: tl.constexpr, +): + linear = tl.arange(0, BLOCK_SIZE) + offset = tl.load(idx_ptr + linear) + mask = tl.load(mask_ptr + linear) != 0 + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + offset, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit(do_not_specialize=["numel"]) +def scalar_splat_mask_atomic_add_1d( + topk_ids_ptr, + tokens_cnts_ptr, + numel, + TOKENS_PER_THREAD: tl.constexpr, +): + pid = tl.program_id(0) + offsets = pid * TOKENS_PER_THREAD + tl.arange(0, TOKENS_PER_THREAD) + mask = offsets < numel + expert_id = tl.load(topk_ids_ptr + offsets, mask=mask, other=0) + tl.atomic_add(tokens_cnts_ptr + expert_id, 1, mask=mask) + + +@triton.jit(do_not_specialize=["numel"]) +def scalar_splat_mask_atomic_add_return_value_1d( + idx_ptr, + out_ptr, + old_ptr, + numel, +): + pid = tl.program_id(0) + offset = tl.load(idx_ptr + pid) + mask = pid < numel + old = tl.atomic_add(out_ptr + offset, 1, mask=mask) + tl.store(old_ptr + pid, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_add_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + mask = (linear * 2) < (D0 * D1) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_add_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + mask = (linear * 2) < (D0 * D1 * D2) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_add_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + mask = (linear * 2) < (D0 * D1 * D2 * D3) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_add_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + mask = (linear * 2) < (D0 * D1 * D2 * D3 * D4) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def fully_unstructured_atomic_add_1d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_add_2d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_add_3d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_add_4d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_add_5d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_add(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_add_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + inner_idx = tl.arange(0, D1)[None, :] + linear = i0 * D1 + inner_idx + value = tl.load(val_ptr + linear) + ptr = out_ptr + remapped_i0 * D1 + inner_idx + old = tl.atomic_add(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_add_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1)[:, :, None] * D2 + i2 + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((remapped_i0 * D1 + i1)[:, :, None] * D2 + i2) + old = tl.atomic_add(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_add_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3 + i3) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((((remapped_i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3) + i3) + old = tl.atomic_add(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_add_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = ((((i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4 + i4) + value = tl.load(val_ptr + linear) + ptr = out_ptr + (((((remapped_i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4) + i4) + old = tl.atomic_add(ptr, value) + tl.store(old_ptr + linear, old) + + +STRUCTURED_DISC_MASK_KERNELS = { + 1: structured_disc_mask_atomic_add_1d, + 2: structured_disc_mask_atomic_add_2d, + 3: structured_disc_mask_atomic_add_3d, + 4: structured_disc_mask_atomic_add_4d, + 5: structured_disc_mask_atomic_add_5d, +} + +PARTIAL_STRUCTURED_KERNELS = { + 2: partial_structured_atomic_add_2d, + 3: partial_structured_atomic_add_3d, + 4: partial_structured_atomic_add_4d, + 5: partial_structured_atomic_add_5d, +} + +FULLY_UNSTRUCTURED_KERNELS = { + 1: fully_unstructured_atomic_add_1d, + 2: fully_unstructured_atomic_add_2d, + 3: fully_unstructured_atomic_add_3d, + 4: fully_unstructured_atomic_add_4d, + 5: fully_unstructured_atomic_add_5d, +} + + +def _build_partial_structured_offsets(shape): + assert len(shape) >= 2 + first_dim = shape[0] + tail_shape = shape[1:] + tail_numel = math.prod(tail_shape) + remapped_first = ((torch.arange(first_dim, dtype=torch.int64) + 1) % first_dim) + first_offsets = remapped_first.reshape((first_dim, ) + (1, ) * len(tail_shape)) + tail_offsets = torch.arange(tail_numel, dtype=torch.int64).reshape((1, ) + tail_shape) + return first_offsets * tail_numel + tail_offsets, math.prod(shape) + + +def _build_fully_unstructured_offsets(shape): + numel = math.prod(shape) + output_numel = numel * 2 + base = torch.arange(numel, dtype=torch.int64) + offsets = (base % 2) * numel + (base // 2) + offsets = offsets.reshape(shape) + return offsets, output_numel + + +def _build_discrete_mask(shape): + numel = math.prod(shape) + return ((torch.arange(numel, dtype=torch.int64).reshape(shape) * 2) < numel) + + +def _build_value_tensor(shape, dtype): + return torch.ones(shape, dtype=dtype) + + +def _build_output_baseline(output_numel, dtype): + if dtype in (torch.uint32, torch.uint64): + return torch.arange(output_numel, dtype=torch.int64).to(dtype) + return torch.arange(output_numel, dtype=dtype) + + +def _simulate_atomic_add(base_output, offsets, values, mask=None): + compute_dtype = torch.int64 if base_output.dtype in (torch.uint32, torch.uint64) else base_output.dtype + expected_output = base_output.reshape(-1).clone().cpu().to(compute_dtype) + flat_offsets = offsets.reshape(-1).to(torch.int64).cpu() + flat_values = values.reshape(-1).cpu().to(compute_dtype) + flat_old = torch.zeros(flat_values.shape, dtype=compute_dtype) + if mask is None: + flat_mask = torch.ones(flat_offsets.shape, dtype=torch.bool) + else: + flat_mask = mask.reshape(-1).cpu() + + for idx in range(flat_offsets.numel()): + if not bool(flat_mask[idx]): + continue + offset = int(flat_offsets[idx].item()) + flat_old[idx] = expected_output[offset] + expected_output[offset] = expected_output[offset] + flat_values[idx] + + return expected_output.to(base_output.dtype), flat_old.reshape(values.shape).to(base_output.dtype) + + +def _assert_equal(actual, expected, dtype_name, rank, scenario): + actual_cpu = actual.cpu() + expected_cpu = expected.cpu() + assert torch.equal(actual_cpu, expected_cpu), (f"scenario={scenario}, dtype={dtype_name}, rank={rank}\n" + f"Expected:\n{expected_cpu}\nGot:\n{actual_cpu}") + + +def _launch_structured_discrete_mask(rank, values, output, old, shape): + kernel = STRUCTURED_DISC_MASK_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_partial_structured(rank, values, output, old, shape): + kernel = PARTIAL_STRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_fully_unstructured(rank, offsets, values, output, old, shape): + kernel = FULLY_UNSTRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](offsets, values, output, old, **kwargs) + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_add_structured_pointer_with_discrete_mask(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + values = _build_value_tensor(shape, torch_dtype).npu() + output_numel = math.prod(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_structured_discrete_mask(rank, values, output, old, shape) + + identity_offsets = torch.arange(output_numel, dtype=torch.int64).reshape(shape) + mask = _build_discrete_mask(shape) + expected_output, expected_old = _simulate_atomic_add( + baseline, + identity_offsets, + _build_value_tensor(shape, torch_dtype), + mask=mask, + ) + _assert_equal(output, expected_output, dtype_name, rank, "structured+discrete-mask/output") + _assert_equal(old, expected_old, dtype_name, rank, "structured+discrete-mask/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_add_partially_structured_indirect_offsets(dtype_name, torch_dtype, rank): + if rank == 1: + pytest.skip("Partially structured test is not applicable to 1-D tensors") + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = PARTIAL_STRUCTURED_SHAPES[rank] + offsets, output_numel = _build_partial_structured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_partial_structured(rank, values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_add( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "partially-structured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "partially-structured-indirect/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_add_fully_unstructured_indirect_offsets(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + offsets, output_numel = _build_fully_unstructured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_fully_unstructured(rank, offsets.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_add( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "fully-unstructured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "fully-unstructured-indirect/old") + + +def test_atomic_add_loaded_mask_skips_oob_offsets(): + if not is_compile_on_910_95(_TARGET_ARCH): + pytest.skip("masked indirect atomic template is only enabled on 910_95/950") + + block_size = 8 + invalid_offset = 1 << 30 + offsets = torch.tensor( + [0, invalid_offset, 1, invalid_offset, 2, invalid_offset, 3, invalid_offset], + dtype=torch.int64, + ) + mask = torch.tensor([1, 0, 1, 0, 1, 0, 1, 0], dtype=torch.int32) + values = torch.ones(block_size, dtype=torch.int32) + baseline = torch.tensor([10, 20, 30, 40], dtype=torch.int32) + old_sentinel = -777 + + output = baseline.clone().npu() + old = torch.full((block_size, ), old_sentinel, dtype=torch.int32).npu() + loaded_mask_oob_offset_atomic_add_1d[(1, )]( + offsets.npu(), + mask.npu(), + values.npu(), + output, + old, + BLOCK_SIZE=block_size, + ) + torch.npu.synchronize() + + expected_output = baseline + 1 + expected_old = torch.tensor( + [10, old_sentinel, 20, old_sentinel, 30, old_sentinel, 40, old_sentinel], + dtype=torch.int32, + ) + _assert_equal(output, expected_output, "int32", 1, "loaded-mask-oob-offset/output") + _assert_equal(old, expected_old, "int32", 1, "loaded-mask-oob-offset/old") + + +def test_atomic_add_scalar_splat_mask_skips_inactive_programs(): + numel = 3 + grid_size = 8 + tokens_per_thread = 1 + sentinel_expert = 3 + + # The inactive programs point to a valid sentinel bucket so the regression + # is detected deterministically without deliberately triggering an MTE OOB. + topk_ids = torch.tensor( + [0, 1, 2] + [sentinel_expert] * (grid_size - numel), + dtype=torch.int64, + ).npu() + tokens_cnts = torch.zeros(sentinel_expert + 1, dtype=torch.int32).npu() + + scalar_splat_mask_atomic_add_1d[(grid_size, )]( + topk_ids, + tokens_cnts, + numel, + TOKENS_PER_THREAD=tokens_per_thread, + ) + torch.npu.synchronize() + + expected = torch.tensor([1, 1, 1, 0], dtype=torch.int32) + assert torch.equal(tokens_cnts.cpu(), expected) + + +def test_atomic_add_scalar_splat_mask_return_value_skips_oob_offsets(): + numel = 3 + grid_size = 8 + invalid_offset = 1 << 30 + old_sentinel = -777 + + offsets = torch.tensor( + [0, 1, 2] + [invalid_offset] * (grid_size - numel), + dtype=torch.int64, + ).npu() + baseline = torch.tensor([10, 20, 30], dtype=torch.int32) + output = baseline.clone().npu() + old = torch.full((grid_size, ), old_sentinel, dtype=torch.int32).npu() + + scalar_splat_mask_atomic_add_return_value_1d[(grid_size, )]( + offsets, + output, + old, + numel, + ) + torch.npu.synchronize() + + expected_output = baseline + 1 + expected_old = torch.tensor( + [10, 20, 30] + [old_sentinel] * (grid_size - numel), + dtype=torch.int32, + ) + _assert_equal(output, expected_output, "int32", 1, "scalar-splat-mask-return-value/output") + _assert_equal(old, expected_old, "int32", 1, "scalar-splat-mask-return-value/old") diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_and.py b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_and.py new file mode 100644 index 0000000000..07b1a59de2 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_and.py @@ -0,0 +1,535 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Indirect atomic_and test suite +# +# Test matrix (access pattern x tensor rank): +# +# | access pattern | 1-D | 2-D | 3-D | 4-D | 5-D | +# |-------------------------------------------------|------|------|------|------|------| +# | structured pointer + discrete mask | (A1) | (A2) | (A3) | (A4) | (A5) | +# | partial structured: high-dim disc + low-dim cont| - | (B2) | (B3) | (B4) | (B5) | +# | fully unstructured indirect offsets | (C1) | (C2) | (C3) | (C4) | (C5) | +# +# Notes: +# 1. Case A exercises the structured-pointer discrete-mask atomic_and path. +# 2. Case B exercises a single high-dimension discrete remap with the +# remaining lower dimensions kept contiguous. +# 3. Case C exercises fully unstructured indirect offsets. +# 4. All cases validate both the final destination tensor and the atomic_and +# return value, which must be the old value observed at each access. +# ============================================================================= + +import math + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +_TARGET_ARCH = triton.runtime.driver.active.get_current_target().arch + +SUPPORTED_DTYPES = [ + ("int8", torch.int8), + ("int16", torch.int16), + ("int32", torch.int32), + ("int64", torch.int64), + ("uint32", torch.uint32), + ("uint64", torch.uint64), +] + +RANK_SHAPES = { + 1: (8, ), + 2: (4, 4), + 3: (2, 3, 4), + 4: (2, 2, 3, 4), + 5: (2, 2, 2, 3, 4), +} + +PARTIAL_STRUCTURED_SHAPES = { + 2: (2, 16), + 3: (2, 2, 8), + 4: (2, 2, 2, 8), + 5: (2, 2, 2, 2, 8), +} + +TEST_RANKS = [3] +TEST_DTYPE = [("int32", torch.int32)] + + +@triton.jit +def structured_disc_mask_atomic_and_1d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + mask = (linear * 2) < D0 + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_and_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + mask = (linear * 2) < (D0 * D1) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_and_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + mask = (linear * 2) < (D0 * D1 * D2) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_and_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + mask = (linear * 2) < (D0 * D1 * D2 * D3) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_and_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + mask = (linear * 2) < (D0 * D1 * D2 * D3 * D4) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def fully_unstructured_atomic_and_1d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_and_2d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_and_3d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_and_4d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_and_5d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_and(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_and_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + inner_idx = tl.arange(0, D1)[None, :] + linear = i0 * D1 + inner_idx + value = tl.load(val_ptr + linear) + ptr = out_ptr + remapped_i0 * D1 + inner_idx + old = tl.atomic_and(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_and_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1)[:, :, None] * D2 + i2 + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((remapped_i0 * D1 + i1)[:, :, None] * D2 + i2) + old = tl.atomic_and(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_and_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3 + i3) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((((remapped_i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3) + i3) + old = tl.atomic_and(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_and_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = ((((i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4 + i4) + value = tl.load(val_ptr + linear) + ptr = out_ptr + (((((remapped_i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4) + i4) + old = tl.atomic_and(ptr, value) + tl.store(old_ptr + linear, old) + + +STRUCTURED_DISC_MASK_KERNELS = { + 1: structured_disc_mask_atomic_and_1d, + 2: structured_disc_mask_atomic_and_2d, + 3: structured_disc_mask_atomic_and_3d, + 4: structured_disc_mask_atomic_and_4d, + 5: structured_disc_mask_atomic_and_5d, +} + +PARTIAL_STRUCTURED_KERNELS = { + 2: partial_structured_atomic_and_2d, + 3: partial_structured_atomic_and_3d, + 4: partial_structured_atomic_and_4d, + 5: partial_structured_atomic_and_5d, +} + +FULLY_UNSTRUCTURED_KERNELS = { + 1: fully_unstructured_atomic_and_1d, + 2: fully_unstructured_atomic_and_2d, + 3: fully_unstructured_atomic_and_3d, + 4: fully_unstructured_atomic_and_4d, + 5: fully_unstructured_atomic_and_5d, +} + + +def _build_partial_structured_offsets(shape): + assert len(shape) >= 2 + first_dim = shape[0] + tail_shape = shape[1:] + tail_numel = math.prod(tail_shape) + remapped_first = ((torch.arange(first_dim, dtype=torch.int64) + 1) % first_dim) + first_offsets = remapped_first.reshape((first_dim, ) + (1, ) * len(tail_shape)) + tail_offsets = torch.arange(tail_numel, dtype=torch.int64).reshape((1, ) + tail_shape) + return first_offsets * tail_numel + tail_offsets, math.prod(shape) + + +def _build_fully_unstructured_offsets(shape): + numel = math.prod(shape) + output_numel = numel * 2 + base = torch.arange(numel, dtype=torch.int64) + offsets = (base % 2) * numel + (base // 2) + offsets = offsets.reshape(shape) + return offsets, output_numel + + +def _build_discrete_mask(shape): + numel = math.prod(shape) + return ((torch.arange(numel, dtype=torch.int64).reshape(shape) * 2) < numel) + + +def _build_value_tensor(shape, dtype): + return torch.ones(shape, dtype=dtype) + + +def _build_output_baseline(output_numel, dtype): + if dtype in (torch.uint32, torch.uint64): + return torch.arange(output_numel, dtype=torch.int64).to(dtype) + return torch.arange(output_numel, dtype=dtype) + + +def _simulate_atomic_and(base_output, offsets, values, mask=None): + compute_dtype = torch.int64 if base_output.dtype in (torch.uint32, torch.uint64) else base_output.dtype + expected_output = base_output.reshape(-1).clone().cpu().to(compute_dtype) + flat_offsets = offsets.reshape(-1).to(torch.int64).cpu() + flat_values = values.reshape(-1).cpu().to(compute_dtype) + flat_old = torch.zeros(flat_values.shape, dtype=compute_dtype) + if mask is None: + flat_mask = torch.ones(flat_offsets.shape, dtype=torch.bool) + else: + flat_mask = mask.reshape(-1).cpu() + + for idx in range(flat_offsets.numel()): + if not bool(flat_mask[idx]): + continue + offset = int(flat_offsets[idx].item()) + flat_old[idx] = expected_output[offset] + expected_output[offset] = expected_output[offset] & flat_values[idx] + + return expected_output.to(base_output.dtype), flat_old.reshape(values.shape).to(base_output.dtype) + + +def _assert_equal(actual, expected, dtype_name, rank, scenario): + actual_cpu = actual.cpu() + expected_cpu = expected.cpu() + assert torch.equal(actual_cpu, expected_cpu), (f"scenario={scenario}, dtype={dtype_name}, rank={rank}\n" + f"Expected:\n{expected_cpu}\nGot:\n{actual_cpu}") + + +def _launch_structured_discrete_mask(rank, values, output, old, shape): + kernel = STRUCTURED_DISC_MASK_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_partial_structured(rank, values, output, old, shape): + kernel = PARTIAL_STRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_fully_unstructured(rank, offsets, values, output, old, shape): + kernel = FULLY_UNSTRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](offsets, values, output, old, **kwargs) + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_and_structured_pointer_with_discrete_mask(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + values = _build_value_tensor(shape, torch_dtype).npu() + output_numel = math.prod(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_structured_discrete_mask(rank, values, output, old, shape) + + identity_offsets = torch.arange(output_numel, dtype=torch.int64).reshape(shape) + mask = _build_discrete_mask(shape) + expected_output, expected_old = _simulate_atomic_and( + baseline, + identity_offsets, + _build_value_tensor(shape, torch_dtype), + mask=mask, + ) + _assert_equal(output, expected_output, dtype_name, rank, "structured+discrete-mask/output") + _assert_equal(old, expected_old, dtype_name, rank, "structured+discrete-mask/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_and_partially_structured_indirect_offsets(dtype_name, torch_dtype, rank): + if rank == 1: + pytest.skip("Partially structured test is not applicable to 1-D tensors") + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = PARTIAL_STRUCTURED_SHAPES[rank] + offsets, output_numel = _build_partial_structured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_partial_structured(rank, values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_and( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "partially-structured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "partially-structured-indirect/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_and_fully_unstructured_indirect_offsets(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + offsets, output_numel = _build_fully_unstructured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_fully_unstructured(rank, offsets.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_and( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "fully-unstructured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "fully-unstructured-indirect/old") diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_cas.py b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_cas.py new file mode 100644 index 0000000000..ca5c8559dc --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_cas.py @@ -0,0 +1,424 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Indirect atomic_cas test suite +# +# Test matrix (access pattern x tensor rank): +# +# | access pattern | 1-D | 2-D | 3-D | 4-D | 5-D | +# |-------------------------------------------------|------|------|------|------|------| +# | partial structured: high-dim disc + low-dim cont| - | (B2) | (B3) | (B4) | (B5) | +# | fully unstructured indirect offsets | (C1) | (C2) | (C3) | (C4) | (C5) | +# +# Notes: +# 1. Case B exercises a single high-dimension discrete remap with the +# remaining lower dimensions kept contiguous. +# 2. Case C exercises fully unstructured indirect offsets. +# 3. All cases validate both the final destination tensor and the atomic_cas +# return value, which must be the old value observed at each access. +# ============================================================================= + +import math + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +_TARGET_ARCH = triton.runtime.driver.active.get_current_target().arch + +SUPPORTED_DTYPES = [ + ("int8", torch.int8), + ("int16", torch.int16), + ("int32", torch.int32), + ("int64", torch.int64), + ("float16", torch.float16), + ("float32", torch.float32), + ("bfloat16", torch.bfloat16), + ("uint32", torch.uint32), + ("uint64", torch.uint64), +] + +RANK_SHAPES = { + 1: (8, ), + 2: (4, 4), + 3: (2, 3, 4), + 4: (2, 2, 3, 4), + 5: (2, 2, 2, 3, 4), +} + +PARTIAL_STRUCTURED_SHAPES = { + 2: (2, 16), + 3: (2, 2, 8), + 4: (2, 2, 2, 8), + 5: (2, 2, 2, 2, 8), +} + +TEST_RANKS = [3] +TEST_DTYPE = [("int32", torch.int32), ("bfloat16", torch.bfloat16)] + + +@triton.jit +def fully_unstructured_atomic_cas_1d( + idx_ptr, + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + offset = tl.load(idx_ptr + linear) + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_cas(out_ptr + offset, compare, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_cas_2d( + idx_ptr, + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + offset = tl.load(idx_ptr + linear) + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_cas(out_ptr + offset, compare, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_cas_3d( + idx_ptr, + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + offset = tl.load(idx_ptr + linear) + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_cas(out_ptr + offset, compare, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_cas_4d( + idx_ptr, + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + offset = tl.load(idx_ptr + linear) + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_cas(out_ptr + offset, compare, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_cas_5d( + idx_ptr, + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + offset = tl.load(idx_ptr + linear) + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_cas(out_ptr + offset, compare, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_cas_2d( + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + ptr = out_ptr + remapped_i0 * D1 + i1 + old = tl.atomic_cas(ptr, compare, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_cas_3d( + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1)[:, :, None] * D2 + i2 + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((remapped_i0 * D1 + i1)[:, :, None] * D2 + i2) + old = tl.atomic_cas(ptr, compare, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_cas_4d( + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3 + i3) + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((((remapped_i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3) + i3) + old = tl.atomic_cas(ptr, compare, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_cas_5d( + cmp_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = ((((i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4 + i4) + compare = tl.load(cmp_ptr + linear) + value = tl.load(val_ptr + linear) + ptr = out_ptr + (((((remapped_i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4) + i4) + old = tl.atomic_cas(ptr, compare, value) + tl.store(old_ptr + linear, old) + + +PARTIAL_STRUCTURED_KERNELS = { + 2: partial_structured_atomic_cas_2d, + 3: partial_structured_atomic_cas_3d, + 4: partial_structured_atomic_cas_4d, + 5: partial_structured_atomic_cas_5d, +} + +FULLY_UNSTRUCTURED_KERNELS = { + 1: fully_unstructured_atomic_cas_1d, + 2: fully_unstructured_atomic_cas_2d, + 3: fully_unstructured_atomic_cas_3d, + 4: fully_unstructured_atomic_cas_4d, + 5: fully_unstructured_atomic_cas_5d, +} + + +def _build_partial_structured_offsets(shape): + assert len(shape) >= 2 + first_dim = shape[0] + tail_shape = shape[1:] + tail_numel = math.prod(tail_shape) + remapped_first = ((torch.arange(first_dim, dtype=torch.int64) + 1) % first_dim) + first_offsets = remapped_first.reshape((first_dim, ) + (1, ) * len(tail_shape)) + tail_offsets = torch.arange(tail_numel, dtype=torch.int64).reshape((1, ) + tail_shape) + return first_offsets * tail_numel + tail_offsets, math.prod(shape) + + +def _build_fully_unstructured_offsets(shape): + numel = math.prod(shape) + output_numel = numel * 2 + base = torch.arange(numel, dtype=torch.int64) + offsets = (base % 2) * numel + (base // 2) + offsets = offsets.reshape(shape) + return offsets, output_numel + + +def _build_value_tensor(shape, dtype): + return torch.full(shape, 7, dtype=dtype) + + +def _build_output_baseline(output_numel, dtype): + if dtype in (torch.uint32, torch.uint64): + return torch.arange(output_numel, dtype=torch.int64).to(dtype) + return torch.arange(output_numel, dtype=dtype) + + +def _build_compare_tensor(offsets, baseline, dtype): + compute_dtype = torch.int64 if dtype in (torch.uint32, torch.uint64) else dtype + flat_offsets = offsets.reshape(-1).to(torch.int64) + base_flat = baseline.reshape(-1).to(compute_dtype) + compare = torch.empty(flat_offsets.numel(), dtype=compute_dtype) + for idx, offset in enumerate(flat_offsets.tolist()): + compare[idx] = base_flat[offset] if idx % 2 == 0 else base_flat[offset] + 1 + return compare.reshape(offsets.shape).to(dtype) + + +def _simulate_atomic_cas(base_output, offsets, compare, values): + compute_dtype = torch.int64 if base_output.dtype in (torch.uint32, torch.uint64) else base_output.dtype + expected_output = base_output.reshape(-1).clone().cpu().to(compute_dtype) + flat_offsets = offsets.reshape(-1).to(torch.int64).cpu() + flat_compare = compare.reshape(-1).cpu().to(compute_dtype) + flat_values = values.reshape(-1).cpu().to(compute_dtype) + flat_old = torch.zeros(flat_values.shape, dtype=compute_dtype) + + for idx in range(flat_offsets.numel()): + offset = int(flat_offsets[idx].item()) + flat_old[idx] = expected_output[offset] + if expected_output[offset] == flat_compare[idx]: + expected_output[offset] = flat_values[idx] + + return expected_output.to(base_output.dtype), flat_old.reshape(values.shape).to(base_output.dtype) + + +def _assert_equal(actual, expected, dtype_name, rank, scenario): + actual_cpu = actual.cpu() + expected_cpu = expected.cpu() + assert torch.equal(actual_cpu, expected_cpu), (f"scenario={scenario}, dtype={dtype_name}, rank={rank}\n" + f"Expected:\n{expected_cpu}\nGot:\n{actual_cpu}") + + +def _launch_partial_structured(rank, compare, values, output, old, shape): + kernel = PARTIAL_STRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](compare, values, output, old, **kwargs) + + +def _launch_fully_unstructured(rank, offsets, compare, values, output, old, shape): + kernel = FULLY_UNSTRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](offsets, compare, values, output, old, **kwargs) + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_cas_partially_structured_indirect_offsets(dtype_name, torch_dtype, rank): + if rank == 1: + pytest.skip("Partially structured test is not applicable to 1-D tensors") + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = PARTIAL_STRUCTURED_SHAPES[rank] + offsets, output_numel = _build_partial_structured_offsets(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + compare = _build_compare_tensor(offsets, baseline, torch_dtype) + values = _build_value_tensor(shape, torch_dtype).npu() + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_partial_structured(rank, compare.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_cas( + baseline, + offsets, + compare, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "partially-structured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "partially-structured-indirect/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_cas_fully_unstructured_indirect_offsets(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + offsets, output_numel = _build_fully_unstructured_offsets(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + compare = _build_compare_tensor(offsets, baseline, torch_dtype) + values = _build_value_tensor(shape, torch_dtype).npu() + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_fully_unstructured(rank, offsets.npu(), compare.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_cas( + baseline, + offsets, + compare, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "fully-unstructured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "fully-unstructured-indirect/old") diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_max.py b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_max.py new file mode 100644 index 0000000000..5c6c4a6a1b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_max.py @@ -0,0 +1,538 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Indirect atomic_max test suite +# +# Test matrix (access pattern x tensor rank): +# +# | access pattern | 1-D | 2-D | 3-D | 4-D | 5-D | +# |-------------------------------------------------|------|------|------|------|------| +# | structured pointer + discrete mask | (A1) | (A2) | (A3) | (A4) | (A5) | +# | partial structured: high-dim disc + low-dim cont| - | (B2) | (B3) | (B4) | (B5) | +# | fully unstructured indirect offsets | (C1) | (C2) | (C3) | (C4) | (C5) | +# +# Notes: +# 1. Case A exercises the structured-pointer discrete-mask atomic_max path. +# 2. Case B exercises a single high-dimension discrete remap with the +# remaining lower dimensions kept contiguous. +# 3. Case C exercises fully unstructured indirect offsets. +# 4. All cases validate both the final destination tensor and the atomic_max +# return value, which must be the old value observed at each access. +# ============================================================================= + +import math + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +_TARGET_ARCH = triton.runtime.driver.active.get_current_target().arch + +SUPPORTED_DTYPES = [ + ("int8", torch.int8), + ("int16", torch.int16), + ("int32", torch.int32), + ("int64", torch.int64), + ("float16", torch.float16), + ("float32", torch.float32), + ("bfloat16", torch.bfloat16), + ("uint32", torch.uint32), + ("uint64", torch.uint64), +] + +RANK_SHAPES = { + 1: (8, ), + 2: (4, 4), + 3: (2, 3, 4), + 4: (2, 2, 3, 4), + 5: (2, 2, 2, 3, 4), +} + +PARTIAL_STRUCTURED_SHAPES = { + 2: (2, 16), + 3: (2, 2, 8), + 4: (2, 2, 2, 8), + 5: (2, 2, 2, 2, 8), +} + +TEST_RANKS = [3] +TEST_DTYPE = [("int32", torch.int32)] + + +@triton.jit +def structured_disc_mask_atomic_max_1d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + mask = (linear * 2) < D0 + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_max_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + mask = (linear * 2) < (D0 * D1) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_max_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + mask = (linear * 2) < (D0 * D1 * D2) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_max_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + mask = (linear * 2) < (D0 * D1 * D2 * D3) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_max_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + mask = (linear * 2) < (D0 * D1 * D2 * D3 * D4) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def fully_unstructured_atomic_max_1d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_max_2d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_max_3d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_max_4d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_max_5d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_max(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_max_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + inner_idx = tl.arange(0, D1)[None, :] + linear = i0 * D1 + inner_idx + value = tl.load(val_ptr + linear) + ptr = out_ptr + remapped_i0 * D1 + inner_idx + old = tl.atomic_max(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_max_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1)[:, :, None] * D2 + i2 + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((remapped_i0 * D1 + i1)[:, :, None] * D2 + i2) + old = tl.atomic_max(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_max_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3 + i3) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((((remapped_i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3) + i3) + old = tl.atomic_max(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_max_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = ((((i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4 + i4) + value = tl.load(val_ptr + linear) + ptr = out_ptr + (((((remapped_i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4) + i4) + old = tl.atomic_max(ptr, value) + tl.store(old_ptr + linear, old) + + +STRUCTURED_DISC_MASK_KERNELS = { + 1: structured_disc_mask_atomic_max_1d, + 2: structured_disc_mask_atomic_max_2d, + 3: structured_disc_mask_atomic_max_3d, + 4: structured_disc_mask_atomic_max_4d, + 5: structured_disc_mask_atomic_max_5d, +} + +PARTIAL_STRUCTURED_KERNELS = { + 2: partial_structured_atomic_max_2d, + 3: partial_structured_atomic_max_3d, + 4: partial_structured_atomic_max_4d, + 5: partial_structured_atomic_max_5d, +} + +FULLY_UNSTRUCTURED_KERNELS = { + 1: fully_unstructured_atomic_max_1d, + 2: fully_unstructured_atomic_max_2d, + 3: fully_unstructured_atomic_max_3d, + 4: fully_unstructured_atomic_max_4d, + 5: fully_unstructured_atomic_max_5d, +} + + +def _build_partial_structured_offsets(shape): + assert len(shape) >= 2 + first_dim = shape[0] + tail_shape = shape[1:] + tail_numel = math.prod(tail_shape) + remapped_first = ((torch.arange(first_dim, dtype=torch.int64) + 1) % first_dim) + first_offsets = remapped_first.reshape((first_dim, ) + (1, ) * len(tail_shape)) + tail_offsets = torch.arange(tail_numel, dtype=torch.int64).reshape((1, ) + tail_shape) + return first_offsets * tail_numel + tail_offsets, math.prod(shape) + + +def _build_fully_unstructured_offsets(shape): + numel = math.prod(shape) + output_numel = numel * 2 + base = torch.arange(numel, dtype=torch.int64) + offsets = (base % 2) * numel + (base // 2) + offsets = offsets.reshape(shape) + return offsets, output_numel + + +def _build_discrete_mask(shape): + numel = math.prod(shape) + return ((torch.arange(numel, dtype=torch.int64).reshape(shape) * 2) < numel) + + +def _build_value_tensor(shape, dtype): + return torch.full(shape, math.prod(shape) + 7, dtype=dtype) + + +def _build_output_baseline(output_numel, dtype): + if dtype in (torch.uint32, torch.uint64): + return torch.arange(output_numel, dtype=torch.int64).to(dtype) + return torch.arange(output_numel, dtype=dtype) + + +def _simulate_atomic_max(base_output, offsets, values, mask=None): + compute_dtype = torch.int64 if base_output.dtype in (torch.uint32, torch.uint64) else base_output.dtype + expected_output = base_output.reshape(-1).clone().cpu().to(compute_dtype) + flat_offsets = offsets.reshape(-1).to(torch.int64).cpu() + flat_values = values.reshape(-1).cpu().to(compute_dtype) + flat_old = torch.zeros(flat_values.shape, dtype=compute_dtype) + if mask is None: + flat_mask = torch.ones(flat_offsets.shape, dtype=torch.bool) + else: + flat_mask = mask.reshape(-1).cpu() + + for idx in range(flat_offsets.numel()): + if not bool(flat_mask[idx]): + continue + offset = int(flat_offsets[idx].item()) + flat_old[idx] = expected_output[offset] + expected_output[offset] = torch.maximum(expected_output[offset], flat_values[idx]) + + return expected_output.to(base_output.dtype), flat_old.reshape(values.shape).to(base_output.dtype) + + +def _assert_equal(actual, expected, dtype_name, rank, scenario): + actual_cpu = actual.cpu() + expected_cpu = expected.cpu() + assert torch.equal(actual_cpu, expected_cpu), (f"scenario={scenario}, dtype={dtype_name}, rank={rank}\n" + f"Expected:\n{expected_cpu}\nGot:\n{actual_cpu}") + + +def _launch_structured_discrete_mask(rank, values, output, old, shape): + kernel = STRUCTURED_DISC_MASK_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_partial_structured(rank, values, output, old, shape): + kernel = PARTIAL_STRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_fully_unstructured(rank, offsets, values, output, old, shape): + kernel = FULLY_UNSTRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](offsets, values, output, old, **kwargs) + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_max_structured_pointer_with_discrete_mask(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + values = _build_value_tensor(shape, torch_dtype).npu() + output_numel = math.prod(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_structured_discrete_mask(rank, values, output, old, shape) + + identity_offsets = torch.arange(output_numel, dtype=torch.int64).reshape(shape) + mask = _build_discrete_mask(shape) + expected_output, expected_old = _simulate_atomic_max( + baseline, + identity_offsets, + _build_value_tensor(shape, torch_dtype), + mask=mask, + ) + _assert_equal(output, expected_output, dtype_name, rank, "structured+discrete-mask/output") + _assert_equal(old, expected_old, dtype_name, rank, "structured+discrete-mask/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_max_partially_structured_indirect_offsets(dtype_name, torch_dtype, rank): + if rank == 1: + pytest.skip("Partially structured test is not applicable to 1-D tensors") + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = PARTIAL_STRUCTURED_SHAPES[rank] + offsets, output_numel = _build_partial_structured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_partial_structured(rank, values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_max( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "partially-structured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "partially-structured-indirect/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_max_fully_unstructured_indirect_offsets(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + offsets, output_numel = _build_fully_unstructured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_fully_unstructured(rank, offsets.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_max( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "fully-unstructured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "fully-unstructured-indirect/old") diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_min.py b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_min.py new file mode 100644 index 0000000000..1f0a2b7943 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_min.py @@ -0,0 +1,538 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Indirect atomic_min test suite +# +# Test matrix (access pattern x tensor rank): +# +# | access pattern | 1-D | 2-D | 3-D | 4-D | 5-D | +# |-------------------------------------------------|------|------|------|------|------| +# | structured pointer + discrete mask | (A1) | (A2) | (A3) | (A4) | (A5) | +# | partial structured: high-dim disc + low-dim cont| - | (B2) | (B3) | (B4) | (B5) | +# | fully unstructured indirect offsets | (C1) | (C2) | (C3) | (C4) | (C5) | +# +# Notes: +# 1. Case A exercises the structured-pointer discrete-mask atomic_min path. +# 2. Case B exercises a single high-dimension discrete remap with the +# remaining lower dimensions kept contiguous. +# 3. Case C exercises fully unstructured indirect offsets. +# 4. All cases validate both the final destination tensor and the atomic_min +# return value, which must be the old value observed at each access. +# ============================================================================= + +import math + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +_TARGET_ARCH = triton.runtime.driver.active.get_current_target().arch + +SUPPORTED_DTYPES = [ + ("int8", torch.int8), + ("int16", torch.int16), + ("int32", torch.int32), + ("int64", torch.int64), + ("float16", torch.float16), + ("float32", torch.float32), + ("bfloat16", torch.bfloat16), + ("uint32", torch.uint32), + ("uint64", torch.uint64), +] + +RANK_SHAPES = { + 1: (8, ), + 2: (4, 4), + 3: (2, 3, 4), + 4: (2, 2, 3, 4), + 5: (2, 2, 2, 3, 4), +} + +PARTIAL_STRUCTURED_SHAPES = { + 2: (2, 16), + 3: (2, 2, 8), + 4: (2, 2, 2, 8), + 5: (2, 2, 2, 2, 8), +} + +TEST_RANKS = [3] +TEST_DTYPE = [("int32", torch.int32)] + + +@triton.jit +def structured_disc_mask_atomic_min_1d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + mask = (linear * 2) < D0 + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_min_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + mask = (linear * 2) < (D0 * D1) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_min_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + mask = (linear * 2) < (D0 * D1 * D2) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_min_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + mask = (linear * 2) < (D0 * D1 * D2 * D3) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_min_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + mask = (linear * 2) < (D0 * D1 * D2 * D3 * D4) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def fully_unstructured_atomic_min_1d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_min_2d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_min_3d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_min_4d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_min_5d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_min(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_min_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + inner_idx = tl.arange(0, D1)[None, :] + linear = i0 * D1 + inner_idx + value = tl.load(val_ptr + linear) + ptr = out_ptr + remapped_i0 * D1 + inner_idx + old = tl.atomic_min(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_min_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1)[:, :, None] * D2 + i2 + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((remapped_i0 * D1 + i1)[:, :, None] * D2 + i2) + old = tl.atomic_min(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_min_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3 + i3) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((((remapped_i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3) + i3) + old = tl.atomic_min(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_min_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = ((((i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4 + i4) + value = tl.load(val_ptr + linear) + ptr = out_ptr + (((((remapped_i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4) + i4) + old = tl.atomic_min(ptr, value) + tl.store(old_ptr + linear, old) + + +STRUCTURED_DISC_MASK_KERNELS = { + 1: structured_disc_mask_atomic_min_1d, + 2: structured_disc_mask_atomic_min_2d, + 3: structured_disc_mask_atomic_min_3d, + 4: structured_disc_mask_atomic_min_4d, + 5: structured_disc_mask_atomic_min_5d, +} + +PARTIAL_STRUCTURED_KERNELS = { + 2: partial_structured_atomic_min_2d, + 3: partial_structured_atomic_min_3d, + 4: partial_structured_atomic_min_4d, + 5: partial_structured_atomic_min_5d, +} + +FULLY_UNSTRUCTURED_KERNELS = { + 1: fully_unstructured_atomic_min_1d, + 2: fully_unstructured_atomic_min_2d, + 3: fully_unstructured_atomic_min_3d, + 4: fully_unstructured_atomic_min_4d, + 5: fully_unstructured_atomic_min_5d, +} + + +def _build_partial_structured_offsets(shape): + assert len(shape) >= 2 + first_dim = shape[0] + tail_shape = shape[1:] + tail_numel = math.prod(tail_shape) + remapped_first = ((torch.arange(first_dim, dtype=torch.int64) + 1) % first_dim) + first_offsets = remapped_first.reshape((first_dim, ) + (1, ) * len(tail_shape)) + tail_offsets = torch.arange(tail_numel, dtype=torch.int64).reshape((1, ) + tail_shape) + return first_offsets * tail_numel + tail_offsets, math.prod(shape) + + +def _build_fully_unstructured_offsets(shape): + numel = math.prod(shape) + output_numel = numel * 2 + base = torch.arange(numel, dtype=torch.int64) + offsets = (base % 2) * numel + (base // 2) + offsets = offsets.reshape(shape) + return offsets, output_numel + + +def _build_discrete_mask(shape): + numel = math.prod(shape) + return ((torch.arange(numel, dtype=torch.int64).reshape(shape) * 2) < numel) + + +def _build_value_tensor(shape, dtype): + return torch.ones(shape, dtype=dtype) + + +def _build_output_baseline(output_numel, dtype): + if dtype in (torch.uint32, torch.uint64): + return torch.arange(output_numel, dtype=torch.int64).to(dtype) + return torch.arange(output_numel, dtype=dtype) + + +def _simulate_atomic_min(base_output, offsets, values, mask=None): + compute_dtype = torch.int64 if base_output.dtype in (torch.uint32, torch.uint64) else base_output.dtype + expected_output = base_output.reshape(-1).clone().cpu().to(compute_dtype) + flat_offsets = offsets.reshape(-1).to(torch.int64).cpu() + flat_values = values.reshape(-1).cpu().to(compute_dtype) + flat_old = torch.zeros(flat_values.shape, dtype=compute_dtype) + if mask is None: + flat_mask = torch.ones(flat_offsets.shape, dtype=torch.bool) + else: + flat_mask = mask.reshape(-1).cpu() + + for idx in range(flat_offsets.numel()): + if not bool(flat_mask[idx]): + continue + offset = int(flat_offsets[idx].item()) + flat_old[idx] = expected_output[offset] + expected_output[offset] = torch.minimum(expected_output[offset], flat_values[idx]) + + return expected_output.to(base_output.dtype), flat_old.reshape(values.shape).to(base_output.dtype) + + +def _assert_equal(actual, expected, dtype_name, rank, scenario): + actual_cpu = actual.cpu() + expected_cpu = expected.cpu() + assert torch.equal(actual_cpu, expected_cpu), (f"scenario={scenario}, dtype={dtype_name}, rank={rank}\n" + f"Expected:\n{expected_cpu}\nGot:\n{actual_cpu}") + + +def _launch_structured_discrete_mask(rank, values, output, old, shape): + kernel = STRUCTURED_DISC_MASK_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_partial_structured(rank, values, output, old, shape): + kernel = PARTIAL_STRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_fully_unstructured(rank, offsets, values, output, old, shape): + kernel = FULLY_UNSTRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](offsets, values, output, old, **kwargs) + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_min_structured_pointer_with_discrete_mask(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + values = _build_value_tensor(shape, torch_dtype).npu() + output_numel = math.prod(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_structured_discrete_mask(rank, values, output, old, shape) + + identity_offsets = torch.arange(output_numel, dtype=torch.int64).reshape(shape) + mask = _build_discrete_mask(shape) + expected_output, expected_old = _simulate_atomic_min( + baseline, + identity_offsets, + _build_value_tensor(shape, torch_dtype), + mask=mask, + ) + _assert_equal(output, expected_output, dtype_name, rank, "structured+discrete-mask/output") + _assert_equal(old, expected_old, dtype_name, rank, "structured+discrete-mask/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_min_partially_structured_indirect_offsets(dtype_name, torch_dtype, rank): + if rank == 1: + pytest.skip("Partially structured test is not applicable to 1-D tensors") + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = PARTIAL_STRUCTURED_SHAPES[rank] + offsets, output_numel = _build_partial_structured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_partial_structured(rank, values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_min( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "partially-structured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "partially-structured-indirect/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_min_fully_unstructured_indirect_offsets(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + offsets, output_numel = _build_fully_unstructured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_fully_unstructured(rank, offsets.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_min( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "fully-unstructured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "fully-unstructured-indirect/old") diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_or.py b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_or.py new file mode 100644 index 0000000000..debf22305a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_or.py @@ -0,0 +1,535 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Indirect atomic_or test suite +# +# Test matrix (access pattern x tensor rank): +# +# | access pattern | 1-D | 2-D | 3-D | 4-D | 5-D | +# |-------------------------------------------------|------|------|------|------|------| +# | structured pointer + discrete mask | (A1) | (A2) | (A3) | (A4) | (A5) | +# | partial structured: high-dim disc + low-dim cont| - | (B2) | (B3) | (B4) | (B5) | +# | fully unstructured indirect offsets | (C1) | (C2) | (C3) | (C4) | (C5) | +# +# Notes: +# 1. Case A exercises the structured-pointer discrete-mask atomic_or path. +# 2. Case B exercises a single high-dimension discrete remap with the +# remaining lower dimensions kept contiguous. +# 3. Case C exercises fully unstructured indirect offsets. +# 4. All cases validate both the final destination tensor and the atomic_or +# return value, which must be the old value observed at each access. +# ============================================================================= + +import math + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +_TARGET_ARCH = triton.runtime.driver.active.get_current_target().arch + +SUPPORTED_DTYPES = [ + ("int8", torch.int8), + ("int16", torch.int16), + ("int32", torch.int32), + ("int64", torch.int64), + ("uint32", torch.uint32), + ("uint64", torch.uint64), +] + +RANK_SHAPES = { + 1: (8, ), + 2: (4, 4), + 3: (2, 3, 4), + 4: (2, 2, 3, 4), + 5: (2, 2, 2, 3, 4), +} + +PARTIAL_STRUCTURED_SHAPES = { + 2: (2, 16), + 3: (2, 2, 8), + 4: (2, 2, 2, 8), + 5: (2, 2, 2, 2, 8), +} + +TEST_RANKS = [3] +TEST_DTYPE = [("int32", torch.int32)] + + +@triton.jit +def structured_disc_mask_atomic_or_1d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + mask = (linear * 2) < D0 + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_or_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + mask = (linear * 2) < (D0 * D1) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_or_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + mask = (linear * 2) < (D0 * D1 * D2) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_or_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + mask = (linear * 2) < (D0 * D1 * D2 * D3) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_or_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + mask = (linear * 2) < (D0 * D1 * D2 * D3 * D4) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def fully_unstructured_atomic_or_1d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_or_2d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_or_3d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_or_4d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_or_5d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_or(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_or_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + inner_idx = tl.arange(0, D1)[None, :] + linear = i0 * D1 + inner_idx + value = tl.load(val_ptr + linear) + ptr = out_ptr + remapped_i0 * D1 + inner_idx + old = tl.atomic_or(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_or_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1)[:, :, None] * D2 + i2 + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((remapped_i0 * D1 + i1)[:, :, None] * D2 + i2) + old = tl.atomic_or(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_or_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3 + i3) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((((remapped_i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3) + i3) + old = tl.atomic_or(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_or_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = ((((i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4 + i4) + value = tl.load(val_ptr + linear) + ptr = out_ptr + (((((remapped_i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4) + i4) + old = tl.atomic_or(ptr, value) + tl.store(old_ptr + linear, old) + + +STRUCTURED_DISC_MASK_KERNELS = { + 1: structured_disc_mask_atomic_or_1d, + 2: structured_disc_mask_atomic_or_2d, + 3: structured_disc_mask_atomic_or_3d, + 4: structured_disc_mask_atomic_or_4d, + 5: structured_disc_mask_atomic_or_5d, +} + +PARTIAL_STRUCTURED_KERNELS = { + 2: partial_structured_atomic_or_2d, + 3: partial_structured_atomic_or_3d, + 4: partial_structured_atomic_or_4d, + 5: partial_structured_atomic_or_5d, +} + +FULLY_UNSTRUCTURED_KERNELS = { + 1: fully_unstructured_atomic_or_1d, + 2: fully_unstructured_atomic_or_2d, + 3: fully_unstructured_atomic_or_3d, + 4: fully_unstructured_atomic_or_4d, + 5: fully_unstructured_atomic_or_5d, +} + + +def _build_partial_structured_offsets(shape): + assert len(shape) >= 2 + first_dim = shape[0] + tail_shape = shape[1:] + tail_numel = math.prod(tail_shape) + remapped_first = ((torch.arange(first_dim, dtype=torch.int64) + 1) % first_dim) + first_offsets = remapped_first.reshape((first_dim, ) + (1, ) * len(tail_shape)) + tail_offsets = torch.arange(tail_numel, dtype=torch.int64).reshape((1, ) + tail_shape) + return first_offsets * tail_numel + tail_offsets, math.prod(shape) + + +def _build_fully_unstructured_offsets(shape): + numel = math.prod(shape) + output_numel = numel * 2 + base = torch.arange(numel, dtype=torch.int64) + offsets = (base % 2) * numel + (base // 2) + offsets = offsets.reshape(shape) + return offsets, output_numel + + +def _build_discrete_mask(shape): + numel = math.prod(shape) + return ((torch.arange(numel, dtype=torch.int64).reshape(shape) * 2) < numel) + + +def _build_value_tensor(shape, dtype): + return torch.ones(shape, dtype=dtype) + + +def _build_output_baseline(output_numel, dtype): + if dtype in (torch.uint32, torch.uint64): + return torch.arange(output_numel, dtype=torch.int64).to(dtype) + return torch.arange(output_numel, dtype=dtype) + + +def _simulate_atomic_or(base_output, offsets, values, mask=None): + compute_dtype = torch.int64 if base_output.dtype in (torch.uint32, torch.uint64) else base_output.dtype + expected_output = base_output.reshape(-1).clone().cpu().to(compute_dtype) + flat_offsets = offsets.reshape(-1).to(torch.int64).cpu() + flat_values = values.reshape(-1).cpu().to(compute_dtype) + flat_old = torch.zeros(flat_values.shape, dtype=compute_dtype) + if mask is None: + flat_mask = torch.ones(flat_offsets.shape, dtype=torch.bool) + else: + flat_mask = mask.reshape(-1).cpu() + + for idx in range(flat_offsets.numel()): + if not bool(flat_mask[idx]): + continue + offset = int(flat_offsets[idx].item()) + flat_old[idx] = expected_output[offset] + expected_output[offset] = expected_output[offset] | flat_values[idx] + + return expected_output.to(base_output.dtype), flat_old.reshape(values.shape).to(base_output.dtype) + + +def _assert_equal(actual, expected, dtype_name, rank, scenario): + actual_cpu = actual.cpu() + expected_cpu = expected.cpu() + assert torch.equal(actual_cpu, expected_cpu), (f"scenario={scenario}, dtype={dtype_name}, rank={rank}\n" + f"Expected:\n{expected_cpu}\nGot:\n{actual_cpu}") + + +def _launch_structured_discrete_mask(rank, values, output, old, shape): + kernel = STRUCTURED_DISC_MASK_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_partial_structured(rank, values, output, old, shape): + kernel = PARTIAL_STRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_fully_unstructured(rank, offsets, values, output, old, shape): + kernel = FULLY_UNSTRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](offsets, values, output, old, **kwargs) + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_or_structured_pointer_with_discrete_mask(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + values = _build_value_tensor(shape, torch_dtype).npu() + output_numel = math.prod(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_structured_discrete_mask(rank, values, output, old, shape) + + identity_offsets = torch.arange(output_numel, dtype=torch.int64).reshape(shape) + mask = _build_discrete_mask(shape) + expected_output, expected_old = _simulate_atomic_or( + baseline, + identity_offsets, + _build_value_tensor(shape, torch_dtype), + mask=mask, + ) + _assert_equal(output, expected_output, dtype_name, rank, "structured+discrete-mask/output") + _assert_equal(old, expected_old, dtype_name, rank, "structured+discrete-mask/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_or_partially_structured_indirect_offsets(dtype_name, torch_dtype, rank): + if rank == 1: + pytest.skip("Partially structured test is not applicable to 1-D tensors") + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = PARTIAL_STRUCTURED_SHAPES[rank] + offsets, output_numel = _build_partial_structured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_partial_structured(rank, values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_or( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "partially-structured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "partially-structured-indirect/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_or_fully_unstructured_indirect_offsets(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + offsets, output_numel = _build_fully_unstructured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_fully_unstructured(rank, offsets.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_or( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "fully-unstructured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "fully-unstructured-indirect/old") diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_xchg.py b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_xchg.py new file mode 100644 index 0000000000..978c2af4fe --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_xchg.py @@ -0,0 +1,530 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Indirect atomic_xchg test suite +# +# Test matrix (access pattern x tensor rank): +# +# | access pattern | 1-D | 2-D | 3-D | 4-D | 5-D | +# |-------------------------------------------------|------|------|------|------|------| +# | structured pointer + discrete mask | (A1) | (A2) | (A3) | (A4) | (A5) | +# | partial structured: high-dim disc + low-dim cont| - | (B2) | (B3) | (B4) | (B5) | +# | fully unstructured indirect offsets | (C1) | (C2) | (C3) | (C4) | (C5) | +# +# Notes: +# 1. Case A exercises the structured-pointer discrete-mask atomic_xchg path. +# 2. Case B exercises a single high-dimension discrete remap with the +# remaining lower dimensions kept contiguous. +# 3. Case C exercises fully unstructured indirect offsets. +# 4. All cases validate both the final destination tensor and the atomic_xchg +# return value, which must be the old value observed at each access. +# ============================================================================= + +import math + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +SUPPORTED_DTYPES = [ + ("int16", torch.int16), + ("int32", torch.int32), + ("int64", torch.int64), + ("float16", torch.float16), + ("float32", torch.float32), + ("bfloat16", torch.bfloat16), + ("uint32", torch.uint32), + ("uint64", torch.uint64), +] + +RANK_SHAPES = { + 1: (8, ), + 2: (4, 4), + 3: (2, 3, 4), + 4: (2, 2, 3, 4), + 5: (2, 2, 2, 3, 4), +} + +PARTIAL_STRUCTURED_SHAPES = { + 2: (2, 16), + 3: (2, 2, 8), + 4: (2, 2, 2, 8), + 5: (2, 2, 2, 2, 8), +} + +TEST_RANKS = [3] +TEST_DTYPE = [("int32", torch.int32)] + + +@triton.jit +def structured_disc_mask_atomic_xchg_1d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + mask = (linear * 2) < D0 + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_xchg_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + mask = (linear * 2) < (D0 * D1) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_xchg_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + mask = (linear * 2) < (D0 * D1 * D2) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_xchg_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + mask = (linear * 2) < (D0 * D1 * D2 * D3) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_xchg_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + mask = (linear * 2) < (D0 * D1 * D2 * D3 * D4) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def fully_unstructured_atomic_xchg_1d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_xchg_2d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_xchg_3d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_xchg_4d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_xchg_5d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xchg(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_xchg_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + inner_idx = tl.arange(0, D1)[None, :] + linear = i0 * D1 + inner_idx + value = tl.load(val_ptr + linear) + ptr = out_ptr + remapped_i0 * D1 + inner_idx + old = tl.atomic_xchg(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_xchg_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1)[:, :, None] * D2 + i2 + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((remapped_i0 * D1 + i1)[:, :, None] * D2 + i2) + old = tl.atomic_xchg(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_xchg_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3 + i3) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((((remapped_i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3) + i3) + old = tl.atomic_xchg(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_xchg_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = ((((i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4 + i4) + value = tl.load(val_ptr + linear) + ptr = out_ptr + (((((remapped_i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4) + i4) + old = tl.atomic_xchg(ptr, value) + tl.store(old_ptr + linear, old) + + +STRUCTURED_DISC_MASK_KERNELS = { + 1: structured_disc_mask_atomic_xchg_1d, + 2: structured_disc_mask_atomic_xchg_2d, + 3: structured_disc_mask_atomic_xchg_3d, + 4: structured_disc_mask_atomic_xchg_4d, + 5: structured_disc_mask_atomic_xchg_5d, +} + +PARTIAL_STRUCTURED_KERNELS = { + 2: partial_structured_atomic_xchg_2d, + 3: partial_structured_atomic_xchg_3d, + 4: partial_structured_atomic_xchg_4d, + 5: partial_structured_atomic_xchg_5d, +} + +FULLY_UNSTRUCTURED_KERNELS = { + 1: fully_unstructured_atomic_xchg_1d, + 2: fully_unstructured_atomic_xchg_2d, + 3: fully_unstructured_atomic_xchg_3d, + 4: fully_unstructured_atomic_xchg_4d, + 5: fully_unstructured_atomic_xchg_5d, +} + + +def _build_partial_structured_offsets(shape): + assert len(shape) >= 2 + first_dim = shape[0] + tail_shape = shape[1:] + tail_numel = math.prod(tail_shape) + remapped_first = ((torch.arange(first_dim, dtype=torch.int64) + 1) % first_dim) + first_offsets = remapped_first.reshape((first_dim, ) + (1, ) * len(tail_shape)) + tail_offsets = torch.arange(tail_numel, dtype=torch.int64).reshape((1, ) + tail_shape) + return first_offsets * tail_numel + tail_offsets, math.prod(shape) + + +def _build_fully_unstructured_offsets(shape): + numel = math.prod(shape) + output_numel = numel * 2 + base = torch.arange(numel, dtype=torch.int64) + offsets = (base % 2) * numel + (base // 2) + offsets = offsets.reshape(shape) + return offsets, output_numel + + +def _build_discrete_mask(shape): + numel = math.prod(shape) + return ((torch.arange(numel, dtype=torch.int64).reshape(shape) * 2) < numel) + + +def _build_value_tensor(shape, dtype): + return torch.ones(shape, dtype=dtype) + + +def _build_output_baseline(output_numel, dtype): + if dtype in (torch.uint32, torch.uint64): + return torch.arange(output_numel, dtype=torch.int64).to(dtype) + return torch.arange(output_numel, dtype=dtype) + + +def _simulate_atomic_xchg(base_output, offsets, values, mask=None): + compute_dtype = torch.int64 if base_output.dtype in (torch.uint32, torch.uint64) else base_output.dtype + expected_output = base_output.reshape(-1).clone().cpu().to(compute_dtype) + flat_offsets = offsets.reshape(-1).to(torch.int64).cpu() + flat_values = values.reshape(-1).cpu().to(compute_dtype) + flat_old = torch.zeros(flat_values.shape, dtype=compute_dtype) + if mask is None: + flat_mask = torch.ones(flat_offsets.shape, dtype=torch.bool) + else: + flat_mask = mask.reshape(-1).cpu() + + for idx in range(flat_offsets.numel()): + if not bool(flat_mask[idx]): + continue + offset = int(flat_offsets[idx].item()) + flat_old[idx] = expected_output[offset] + expected_output[offset] = flat_values[idx] + + return expected_output.to(base_output.dtype), flat_old.reshape(values.shape).to(base_output.dtype) + + +def _assert_equal(actual, expected, dtype_name, rank, scenario): + actual_cpu = actual.cpu() + expected_cpu = expected.cpu() + assert torch.equal(actual_cpu, expected_cpu), (f"scenario={scenario}, dtype={dtype_name}, rank={rank}\n" + f"Expected:\n{expected_cpu}\nGot:\n{actual_cpu}") + + +def _launch_structured_discrete_mask(rank, values, output, old, shape): + kernel = STRUCTURED_DISC_MASK_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_partial_structured(rank, values, output, old, shape): + kernel = PARTIAL_STRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_fully_unstructured(rank, offsets, values, output, old, shape): + kernel = FULLY_UNSTRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](offsets, values, output, old, **kwargs) + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_xchg_structured_pointer_with_discrete_mask(dtype_name, torch_dtype, rank): + shape = RANK_SHAPES[rank] + values = _build_value_tensor(shape, torch_dtype).npu() + output_numel = math.prod(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_structured_discrete_mask(rank, values, output, old, shape) + + identity_offsets = torch.arange(output_numel, dtype=torch.int64).reshape(shape) + mask = _build_discrete_mask(shape) + expected_output, expected_old = _simulate_atomic_xchg( + baseline, + identity_offsets, + _build_value_tensor(shape, torch_dtype), + mask=mask, + ) + _assert_equal(output, expected_output, dtype_name, rank, "structured+discrete-mask/output") + _assert_equal(old, expected_old, dtype_name, rank, "structured+discrete-mask/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_xchg_partially_structured_indirect_offsets(dtype_name, torch_dtype, rank): + if rank == 1: + pytest.skip("Partially structured test is not applicable to 1-D tensors") + shape = PARTIAL_STRUCTURED_SHAPES[rank] + offsets, output_numel = _build_partial_structured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_partial_structured(rank, values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_xchg( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "partially-structured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "partially-structured-indirect/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_xchg_fully_unstructured_indirect_offsets(dtype_name, torch_dtype, rank): + shape = RANK_SHAPES[rank] + offsets, output_numel = _build_fully_unstructured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_fully_unstructured(rank, offsets.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_xchg( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "fully-unstructured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "fully-unstructured-indirect/old") diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_xor.py b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_xor.py new file mode 100644 index 0000000000..9038b7407a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_atomic_xor.py @@ -0,0 +1,535 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# ============================================================================= +# Indirect atomic_xor test suite +# +# Test matrix (access pattern x tensor rank): +# +# | access pattern | 1-D | 2-D | 3-D | 4-D | 5-D | +# |-------------------------------------------------|------|------|------|------|------| +# | structured pointer + discrete mask | (A1) | (A2) | (A3) | (A4) | (A5) | +# | partial structured: high-dim disc + low-dim cont| - | (B2) | (B3) | (B4) | (B5) | +# | fully unstructured indirect offsets | (C1) | (C2) | (C3) | (C4) | (C5) | +# +# Notes: +# 1. Case A exercises the structured-pointer discrete-mask atomic_xor path. +# 2. Case B exercises a single high-dimension discrete remap with the +# remaining lower dimensions kept contiguous. +# 3. Case C exercises fully unstructured indirect offsets. +# 4. All cases validate both the final destination tensor and the atomic_xor +# return value, which must be the old value observed at each access. +# ============================================================================= + +import math + +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +_TARGET_ARCH = triton.runtime.driver.active.get_current_target().arch + +SUPPORTED_DTYPES = [ + ("int8", torch.int8), + ("int16", torch.int16), + ("int32", torch.int32), + ("int64", torch.int64), + ("uint32", torch.uint32), + ("uint64", torch.uint64), +] + +RANK_SHAPES = { + 1: (8, ), + 2: (4, 4), + 3: (2, 3, 4), + 4: (2, 2, 3, 4), + 5: (2, 2, 2, 3, 4), +} + +PARTIAL_STRUCTURED_SHAPES = { + 2: (2, 16), + 3: (2, 2, 8), + 4: (2, 2, 2, 8), + 5: (2, 2, 2, 2, 8), +} + +TEST_RANKS = [3] +TEST_DTYPE = [("int32", torch.int32)] + + +@triton.jit +def structured_disc_mask_atomic_xor_1d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + mask = (linear * 2) < D0 + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_xor_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + mask = (linear * 2) < (D0 * D1) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_xor_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + mask = (linear * 2) < (D0 * D1 * D2) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_xor_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + mask = (linear * 2) < (D0 * D1 * D2 * D3) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def structured_disc_mask_atomic_xor_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + mask = (linear * 2) < (D0 * D1 * D2 * D3 * D4) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + linear, value, mask=mask) + tl.store(old_ptr + linear, old, mask=mask) + + +@triton.jit +def fully_unstructured_atomic_xor_1d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, +): + i0 = tl.arange(0, D0) + linear = i0 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_xor_2d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + i1 = tl.arange(0, D1)[None, :] + linear = i0 * D1 + i1 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_xor_3d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1) * D2 + i2 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_xor_4d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = ((i0 * D1 + i1) * D2 + i2) * D3 + i3 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def fully_unstructured_atomic_xor_5d( + idx_ptr, + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None, None] + i1 = tl.arange(0, D1)[None, :, None, None, None] + i2 = tl.arange(0, D2)[None, None, :, None, None] + i3 = tl.arange(0, D3)[None, None, None, :, None] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2) * D3 + i3) * D4 + i4 + offset = tl.load(idx_ptr + linear) + value = tl.load(val_ptr + linear) + old = tl.atomic_xor(out_ptr + offset, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_xor_2d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + inner_idx = tl.arange(0, D1)[None, :] + linear = i0 * D1 + inner_idx + value = tl.load(val_ptr + linear) + ptr = out_ptr + remapped_i0 * D1 + inner_idx + old = tl.atomic_xor(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_xor_3d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :] + i2 = tl.arange(0, D2)[None, None, :] + linear = (i0 * D1 + i1)[:, :, None] * D2 + i2 + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((remapped_i0 * D1 + i1)[:, :, None] * D2 + i2) + old = tl.atomic_xor(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_xor_4d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None] + i2 = tl.arange(0, D2)[None, None, :] + i3 = tl.arange(0, D3)[None, None, None, :] + linear = (((i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3 + i3) + value = tl.load(val_ptr + linear) + ptr = out_ptr + ((((remapped_i0 * D1 + i1) * D2 + i2)[:, :, :, None] * D3) + i3) + old = tl.atomic_xor(ptr, value) + tl.store(old_ptr + linear, old) + + +@triton.jit +def partial_structured_atomic_xor_5d( + val_ptr, + out_ptr, + old_ptr, + D0: tl.constexpr, + D1: tl.constexpr, + D2: tl.constexpr, + D3: tl.constexpr, + D4: tl.constexpr, +): + i0 = tl.arange(0, D0)[:, None, None, None] + remapped_i0 = (i0 + 1) % D0 + i1 = tl.arange(0, D1)[None, :, None, None] + i2 = tl.arange(0, D2)[None, None, :, None] + i3 = tl.arange(0, D3)[None, None, None, :] + i4 = tl.arange(0, D4)[None, None, None, None, :] + linear = ((((i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4 + i4) + value = tl.load(val_ptr + linear) + ptr = out_ptr + (((((remapped_i0 * D1 + i1) * D2 + i2) * D3 + i3)[:, :, :, :, None] * D4) + i4) + old = tl.atomic_xor(ptr, value) + tl.store(old_ptr + linear, old) + + +STRUCTURED_DISC_MASK_KERNELS = { + 1: structured_disc_mask_atomic_xor_1d, + 2: structured_disc_mask_atomic_xor_2d, + 3: structured_disc_mask_atomic_xor_3d, + 4: structured_disc_mask_atomic_xor_4d, + 5: structured_disc_mask_atomic_xor_5d, +} + +PARTIAL_STRUCTURED_KERNELS = { + 2: partial_structured_atomic_xor_2d, + 3: partial_structured_atomic_xor_3d, + 4: partial_structured_atomic_xor_4d, + 5: partial_structured_atomic_xor_5d, +} + +FULLY_UNSTRUCTURED_KERNELS = { + 1: fully_unstructured_atomic_xor_1d, + 2: fully_unstructured_atomic_xor_2d, + 3: fully_unstructured_atomic_xor_3d, + 4: fully_unstructured_atomic_xor_4d, + 5: fully_unstructured_atomic_xor_5d, +} + + +def _build_partial_structured_offsets(shape): + assert len(shape) >= 2 + first_dim = shape[0] + tail_shape = shape[1:] + tail_numel = math.prod(tail_shape) + remapped_first = ((torch.arange(first_dim, dtype=torch.int64) + 1) % first_dim) + first_offsets = remapped_first.reshape((first_dim, ) + (1, ) * len(tail_shape)) + tail_offsets = torch.arange(tail_numel, dtype=torch.int64).reshape((1, ) + tail_shape) + return first_offsets * tail_numel + tail_offsets, math.prod(shape) + + +def _build_fully_unstructured_offsets(shape): + numel = math.prod(shape) + output_numel = numel * 2 + base = torch.arange(numel, dtype=torch.int64) + offsets = (base % 2) * numel + (base // 2) + offsets = offsets.reshape(shape) + return offsets, output_numel + + +def _build_discrete_mask(shape): + numel = math.prod(shape) + return ((torch.arange(numel, dtype=torch.int64).reshape(shape) * 2) < numel) + + +def _build_value_tensor(shape, dtype): + return torch.ones(shape, dtype=dtype) + + +def _build_output_baseline(output_numel, dtype): + if dtype in (torch.uint32, torch.uint64): + return torch.arange(output_numel, dtype=torch.int64).to(dtype) + return torch.arange(output_numel, dtype=dtype) + + +def _simulate_atomic_xor(base_output, offsets, values, mask=None): + compute_dtype = torch.int64 if base_output.dtype in (torch.uint32, torch.uint64) else base_output.dtype + expected_output = base_output.reshape(-1).clone().cpu().to(compute_dtype) + flat_offsets = offsets.reshape(-1).to(torch.int64).cpu() + flat_values = values.reshape(-1).cpu().to(compute_dtype) + flat_old = torch.zeros(flat_values.shape, dtype=compute_dtype) + if mask is None: + flat_mask = torch.ones(flat_offsets.shape, dtype=torch.bool) + else: + flat_mask = mask.reshape(-1).cpu() + + for idx in range(flat_offsets.numel()): + if not bool(flat_mask[idx]): + continue + offset = int(flat_offsets[idx].item()) + flat_old[idx] = expected_output[offset] + expected_output[offset] = expected_output[offset] ^ flat_values[idx] + + return expected_output.to(base_output.dtype), flat_old.reshape(values.shape).to(base_output.dtype) + + +def _assert_equal(actual, expected, dtype_name, rank, scenario): + actual_cpu = actual.cpu() + expected_cpu = expected.cpu() + assert torch.equal(actual_cpu, expected_cpu), (f"scenario={scenario}, dtype={dtype_name}, rank={rank}\n" + f"Expected:\n{expected_cpu}\nGot:\n{actual_cpu}") + + +def _launch_structured_discrete_mask(rank, values, output, old, shape): + kernel = STRUCTURED_DISC_MASK_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_partial_structured(rank, values, output, old, shape): + kernel = PARTIAL_STRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](values, output, old, **kwargs) + + +def _launch_fully_unstructured(rank, offsets, values, output, old, shape): + kernel = FULLY_UNSTRUCTURED_KERNELS[rank] + kwargs = {f"D{dim}": size for dim, size in enumerate(shape)} + kernel[(1, )](offsets, values, output, old, **kwargs) + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_xor_structured_pointer_with_discrete_mask(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + values = _build_value_tensor(shape, torch_dtype).npu() + output_numel = math.prod(shape) + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_structured_discrete_mask(rank, values, output, old, shape) + + identity_offsets = torch.arange(output_numel, dtype=torch.int64).reshape(shape) + mask = _build_discrete_mask(shape) + expected_output, expected_old = _simulate_atomic_xor( + baseline, + identity_offsets, + _build_value_tensor(shape, torch_dtype), + mask=mask, + ) + _assert_equal(output, expected_output, dtype_name, rank, "structured+discrete-mask/output") + _assert_equal(old, expected_old, dtype_name, rank, "structured+discrete-mask/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_xor_partially_structured_indirect_offsets(dtype_name, torch_dtype, rank): + if rank == 1: + pytest.skip("Partially structured test is not applicable to 1-D tensors") + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = PARTIAL_STRUCTURED_SHAPES[rank] + offsets, output_numel = _build_partial_structured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_partial_structured(rank, values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_xor( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "partially-structured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "partially-structured-indirect/old") + + +@pytest.mark.parametrize("dtype_name, torch_dtype", TEST_DTYPE) +@pytest.mark.parametrize("rank", TEST_RANKS) +def test_atomic_xor_fully_unstructured_indirect_offsets(dtype_name, torch_dtype, rank): + if not is_compile_on_910_95(_TARGET_ARCH) and torch_dtype in (torch.uint32, torch.uint64): + pytest.skip("uint32 and uint64 atomics are only supported on 950") + shape = RANK_SHAPES[rank] + offsets, output_numel = _build_fully_unstructured_offsets(shape) + values = _build_value_tensor(shape, torch_dtype).npu() + baseline = _build_output_baseline(output_numel, torch_dtype) + output = baseline.clone().npu() + old = torch.zeros(shape, dtype=torch_dtype).npu() + + _launch_fully_unstructured(rank, offsets.npu(), values, output, old, shape) + + expected_output, expected_old = _simulate_atomic_xor( + baseline, + offsets, + _build_value_tensor(shape, torch_dtype), + ) + _assert_equal(output, expected_output, dtype_name, rank, "fully-unstructured-indirect/output") + _assert_equal(old, expected_old, dtype_name, rank, "fully-unstructured-indirect/old") diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_load_pointer_cast_precise_size.py b/third_party/ascend/unittest/pytest_ut/test_indirect_load_pointer_cast_precise_size.py new file mode 100644 index 0000000000..0f8d5ae48a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_load_pointer_cast_precise_size.py @@ -0,0 +1,62 @@ +import torch +import torch_npu # noqa: F401 + +import triton +import triton.language as tl + + +@triton.jit +def _indirect_load_from_i64_ptr_kernel( + block_table_ptrs, + positions, + out, + BLOCK_SIZE: tl.constexpr, + BLOCK: tl.constexpr, +): + offsets = tl.arange(0, BLOCK) + + block_table_ptr = tl.load(block_table_ptrs).to(tl.pointer_type(tl.int32)) + + pos = tl.load(positions + offsets) + block_indices = pos // BLOCK_SIZE + + # Regression point: + # int64 runtime address -> pointer -> vector indirect load. + block_numbers = tl.load(block_table_ptr + block_indices) + + slot_ids = block_numbers * BLOCK_SIZE + pos % BLOCK_SIZE + tl.store(out + offsets, slot_ids.to(tl.int64)) + + +def test_indirect_load_pointer_cast_precise_size_e2e(): + block_size = 4 + block = 8 + + block_table_cpu = torch.tensor([10, 11, 12, 13], dtype=torch.int32) + positions_cpu = torch.tensor( + [0, 1, 4, 7, 8, 11, 12, 15], + dtype=torch.int32, + ) + + block_table = block_table_cpu.npu() + positions = positions_cpu.npu() + out = torch.empty((block, ), dtype=torch.int64, device="npu") + + block_table_ptrs = torch.tensor( + [block_table.data_ptr()], + dtype=torch.int64, + ).npu() + + _indirect_load_from_i64_ptr_kernel[(1, )]( + block_table_ptrs, + positions, + out, + BLOCK_SIZE=block_size, + BLOCK=block, + ) + + torch.npu.synchronize() + + expected = (block_table_cpu[positions_cpu // block_size] * block_size + positions_cpu % block_size).to(torch.int64) + + assert torch.equal(out.cpu(), expected.cpu()) diff --git a/third_party/ascend/unittest/pytest_ut/test_indirect_scalar_load_offset.py b/third_party/ascend/unittest/pytest_ut/test_indirect_scalar_load_offset.py new file mode 100644 index 0000000000..b9a34efccd --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_indirect_scalar_load_offset.py @@ -0,0 +1,98 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +@triton.jit +def gather_after_reduce_kernel( + logits_ptr, + topk_ids_ptr, + output_ptr, + logits_stride, + vocab_size, + BLOCK: tl.constexpr, +): + req_idx = tl.program_id(0) + + max_val = -float('inf') + for start in range(0, vocab_size, BLOCK): + offsets = start + tl.arange(0, BLOCK) + mask = offsets < vocab_size + vals = tl.load( + logits_ptr + req_idx * logits_stride + offsets, + mask=mask, + other=-float('inf'), + ) + block_max = tl.max(vals) + max_val = tl.maximum(max_val, block_max) + + topk_id = tl.load(topk_ids_ptr + req_idx + tl.arange(0, 1)) + val = tl.load(logits_ptr + req_idx * logits_stride + topk_id) + tl.store(output_ptr + req_idx + tl.arange(0, 1), val - max_val) + + +def torch_reference(logits, topk_ids): + num_rows = logits.shape[0] + output = torch.empty(num_rows, dtype=logits.dtype) + for i in range(num_rows): + max_val = logits[i].max() + output[i] = logits[i, topk_ids[i]] - max_val + return output + + +shapes = [ + (4, 128), + (8, 256), + (16, 1024), +] + + +@pytest.mark.parametrize('num_rows,vocab_size', shapes) +def test_gather_after_reduce(num_rows, vocab_size): + BLOCK = 128 + + logits_ref = test_common.generate_tensor(shape=(num_rows, vocab_size), dtype='float32') + logits = logits_ref.npu() + logits_flat = logits.reshape(-1) + + topk_ids_ref = torch.randint(0, vocab_size, (num_rows, ), dtype=torch.int64) + topk_ids = topk_ids_ref.npu() + + output = torch.empty(num_rows, dtype=torch.float32).npu() + + gather_after_reduce_kernel[(num_rows, )]( + logits_flat, + topk_ids, + output, + vocab_size, + vocab_size, + BLOCK=BLOCK, + ) + + output_ref = torch_reference(logits_ref, topk_ids_ref) + test_common.validate_cmp('float32', output, output_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_insert_slice.py b/third_party/ascend/unittest/pytest_ut/test_insert_slice.py new file mode 100644 index 0000000000..498149fb80 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_insert_slice.py @@ -0,0 +1,87 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +import pytest +import test_common + + +@triton.jit +def triton_kernel(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr, SLICE_OFFSET: tl.constexpr, + SLICE_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + x_sub = al.extract_slice(x, [block_start + SLICE_OFFSET], [SLICE_SIZE], [1]) + y_sub = al.extract_slice(y, [block_start + SLICE_OFFSET], [SLICE_SIZE], [1]) + output_sub = x_sub + y_sub + output = tl.load(output_ptr + offsets, mask=mask) + output = al.insert_slice(output, output_sub, [block_start + SLICE_OFFSET], [SLICE_SIZE], [1]) + tl.store(output_ptr + offsets, output, mask=mask) + + +def triton_func(x: torch.Tensor, y: torch.Tensor, slice_offset: int, slice_size: int): + output = torch.empty_like(x) + n_elements = output.numel() + grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + triton_kernel[grid](x, y, output, n_elements, BLOCK_SIZE=1024, SLICE_OFFSET=0, SLICE_SIZE=32) + return output + + +def test_extract_slice(): + size = 1024 + slice_offset = 0 + slice_size = 32 + x = torch.rand(size, device='npu') + y = torch.rand(size, device='npu') + torch_ref = x + y + triton_cal = triton_func(x, y, slice_offset, slice_size) + torch.testing.assert_close(triton_cal[slice_offset:slice_offset + slice_size], + torch_ref[slice_offset:slice_offset + slice_size]) + + +@triton.jit +def insert_slice_kernel(x_ptr, out_ptr, N: tl.constexpr, OFF: tl.constexpr, SZ: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(x_ptr + offs) + sub = al.extract_slice(x, [OFF], [SZ], [1]) + base = tl.zeros((N, ), dtype=x.dtype) + full = al.insert_slice(base, sub, [OFF], [SZ], [1]) + tl.store(out_ptr + offs, full) + + +@pytest.mark.parametrize("dtype", ['int8', 'int16', 'int32', 'int64', 'float16', 'float32', 'bfloat16', 'uint8']) +def test_insert_slice_dtypes(dtype): + size, offset, sz = 64, 8, 16 + x_cpu = test_common.generate_tensor((size, ), dtype) + expected = torch.zeros(size, dtype=eval('torch.' + dtype)) + expected[offset:offset + sz] = x_cpu[offset:offset + sz] + x = x_cpu.npu() + out = torch.zeros(size, dtype=eval('torch.' + dtype)).npu() + insert_slice_kernel[(1, )](x, out, size, offset, sz) + torch.testing.assert_close(out.cpu(), expected) diff --git a/third_party/ascend/unittest/pytest_ut/test_interleave.py b/third_party/ascend/unittest/pytest_ut/test_interleave.py new file mode 100644 index 0000000000..f78b397393 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_interleave.py @@ -0,0 +1,75 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, y_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + Y = tl.load(y_ptr + idx) + + ret = tl.interleave(X, Y) + + oidx = xidx[:, None, None] * YB * ZB * 2 + yidx[None, :, None] * ZB * 2 + tl.arange(0, 2 * ZB)[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@pytest.mark.parametrize('para_type,data_type,XB,YB,ZB', [ + ['float32', torch.float32, 2, 64, 16], + ['float32', torch.float32, 8, 8, 4], + ['float16', torch.float16, 2, 64, 16], + ['float16', torch.float16, 8, 8, 4], + ['int8', torch.int8, 2, 64, 32], + ['int8', torch.int8, 8, 8, 4], +]) +def test_interleave(para_type, data_type, XB, YB, ZB): + + x = torch.full((XB, YB, ZB), 100, dtype=data_type).npu() + y = torch.full((XB, YB, ZB), 30, dtype=data_type).npu() + + print(f"shape = {x.shape}") + print(x.dtype) + + output = torch.randint(1, (XB, YB, ZB * 2), dtype=data_type).npu() + output1 = output + print(f"output.dtype={output.dtype}") + + ans = torch.stack((x, y), dim=-1).reshape(XB, YB, ZB * 2) + print(ans) + print(ans.shape) + + fn_npu_[1, 1, 1](output, x, y, XB, YB, ZB) + print(output) + + test_common.validate_cmp(para_type, ans, output) diff --git a/third_party/ascend/unittest/pytest_ut/test_interleave_optimizaiton.py b/third_party/ascend/unittest/pytest_ut/test_interleave_optimizaiton.py new file mode 100644 index 0000000000..dfbd09112b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_interleave_optimizaiton.py @@ -0,0 +1,178 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + + +def torch_interleave_load(q, k, head_dim_half, bias): + d_indices = torch.arange(0, head_dim_half) + k[d_indices * 2 + bias] = q[d_indices * 2 + bias] + k[d_indices * 2 + 1 + bias] = -q[d_indices * 2 + 1 + bias] + return k + + +def torch_interleave_load_with_mask(q, k, head_dim_half, bias, numel): + d_indices = torch.arange(0, min(head_dim_half, numel)) + k[d_indices * 2 + bias] = q[d_indices * 2 + bias] + k[d_indices * 2 + 1 + bias] = -q[d_indices * 2 + 1 + bias] + return k + + +def torch_interleave_loadstore_with_mask(q, head_dim_half, bias, numel): + d_indices = torch.arange(0, min(head_dim_half, numel)) + # it's unneccessary since we store it back without edit: q[d_indices * 2 + bias] = q[d_indices * 2 + bias] + q[d_indices * 2 + 1 + bias] = -q[d_indices * 2 + 1 + bias] + return q + + +@triton.jit +def triton_interleave_load(q_ptr, k_ptr, head_dim_half: tl.constexpr, bias: tl.constexpr): + d_indices = tl.program_id(0) + tl.arange(0, head_dim_half) + q_base = q_ptr + bias + q_real = tl.load(q_base + d_indices * 2) + q_imag = tl.load(q_base + d_indices * 2 + 1) + new_q_real = q_real + new_q_imag = -q_imag + tl.store(k_ptr + d_indices * 2 + bias, new_q_real) + tl.store(k_ptr + d_indices * 2 + 1 + bias, new_q_imag) + + +@triton.jit +def triton_deinterleave_static_pair(q_ptr, out_ptr, second_offset: tl.constexpr, head_dim_half: tl.constexpr): + d_indices = tl.program_id(0) + tl.arange(0, head_dim_half) + first = tl.load(q_ptr + d_indices * 2 + 64) + second = tl.load(q_ptr + d_indices * 2 + second_offset) + tl.store(out_ptr + d_indices, first) + tl.store(out_ptr + d_indices + head_dim_half, second) + + +@triton.jit +def triton_interleave_load_runtime_bias(q_ptr, out_ptr, bias, head_dim_half: tl.constexpr): + d_indices = tl.program_id(0) + tl.arange(0, head_dim_half) + q_base = q_ptr + bias + q_real = tl.load(q_base + d_indices * 2) + q_imag = tl.load(q_base + d_indices * 2 + 1) + tl.store(out_ptr + d_indices, q_real) + tl.store(out_ptr + d_indices + head_dim_half, -q_imag) + + +@triton.jit +def triton_interleave_load_with_mask(q_ptr, k_ptr, head_dim_half: tl.constexpr, bias: tl.constexpr, + numel: tl.constexpr): + d_indices = tl.program_id(0) + tl.arange(0, head_dim_half) + mask = d_indices < numel + q_base = q_ptr + bias + q_real = tl.load(q_base + d_indices * 2, mask) + q_imag = tl.load(q_base + d_indices * 2 + 1, mask) + new_q_real = q_real + new_q_imag = -q_imag + tl.store(k_ptr + d_indices * 2 + bias, new_q_real, mask) + tl.store(k_ptr + d_indices * 2 + 1 + bias, new_q_imag, mask) + + +# when load and store are on the same pointer, sometimes we can only optimize the store with mask +@triton.jit +def triton_interleave_loadstore_with_mask(q_ptr, head_dim_half: tl.constexpr, bias: tl.constexpr, numel: tl.constexpr): + d_indices = tl.arange(0, head_dim_half) + mask = d_indices < numel + q_base = q_ptr + bias + q_real = tl.load(q_base + d_indices * 2, mask) + q_imag = tl.load(q_base + d_indices * 2 + 1, mask) + new_q_real = q_real + new_q_imag = -q_imag + tl.store(q_base + d_indices * 2, new_q_real, mask) + tl.store(q_base + d_indices * 2 + 1, new_q_imag, mask) + + +@pytest.mark.parametrize('para_type,data_type,head_dim_half,bias', [ + ['float32', torch.float32, 16, 4], + ['float32', torch.float32, 16, 64], + ['float32', torch.float32, 16, 65], +]) +def test_interleave(para_type, data_type, head_dim_half, bias): + length = bias + head_dim_half * 2 + q = torch.randn((length, ), dtype=data_type).npu() + k = torch.zeros_like(q, dtype=data_type).npu() + k_ref = torch.zeros_like(q, dtype=data_type).npu() + + triton_interleave_load[(1, )](q, k, head_dim_half, bias) + k_ref = torch_interleave_load(q, k_ref, head_dim_half, bias) + assert torch.allclose(k, k_ref) + + +@pytest.mark.parametrize("second_offset,expect_deinterleave", [(65, True), (66, False)]) +def test_static_deinterleave_pair_selection(second_offset, expect_deinterleave): + head_dim_half = 16 + q_cpu = torch.randn((64 + head_dim_half * 2 + 2, ), dtype=torch.float32) + expected = torch.cat((q_cpu[64::2][:head_dim_half], q_cpu[second_offset::2][:head_dim_half])) + q = q_cpu.npu() + out = torch.empty((head_dim_half * 2, ), dtype=torch.float32).npu() + + kernel = triton_deinterleave_static_pair[(1, )](q, out, second_offset, head_dim_half) + + assert torch.allclose(out.cpu(), expected) + assert ("tensor.extract_slice" in kernel.asm["ttadapter"]) == expect_deinterleave + + +def test_interleave_runtime_bias_uses_deinterleave(): + head_dim_half = 16 + bias = 64 + q = torch.randn((bias + head_dim_half * 2, ), dtype=torch.float32).npu() + out = torch.empty((head_dim_half * 2, ), dtype=torch.float32).npu() + + kernel = triton_interleave_load_runtime_bias[(1, )](q, out, bias, head_dim_half) + + expected = torch.cat((q[bias::2][:head_dim_half], -q[bias + 1::2][:head_dim_half])) + assert torch.allclose(out, expected) + assert kernel.asm["ttadapter"].count("tensor.extract_slice") == 2 + + +@pytest.mark.parametrize('para_type,data_type,head_dim_half,bias,numel', [ + ['float32', torch.float32, 16, 0, 8], + ['float32', torch.float32, 16, 64, 8], +]) +def test_interleave_with_mask(para_type, data_type, head_dim_half, bias, numel): + length = bias + head_dim_half * 2 + q = torch.randn((length, ), dtype=data_type).npu() + k = torch.zeros_like(q, dtype=data_type).npu() + k_ref = torch.zeros_like(q, dtype=data_type).npu() + + triton_interleave_load_with_mask[(1, )](q, k, head_dim_half, bias, numel) + k_ref = torch_interleave_load_with_mask(q, k_ref, head_dim_half, bias, numel) + assert torch.allclose(k, k_ref) + + +@pytest.mark.parametrize('para_type,data_type,head_dim_half,bias,numel', [ + ['float32', torch.float32, 16, 0, 8], + ['float32', torch.float32, 16, 64, 8], +]) +def test_interleave_loadstore_with_mask(para_type, data_type, head_dim_half, bias, numel): + length = bias + head_dim_half * 2 + q = torch.randn((length, ), dtype=data_type).npu() + q_ref = q.clone() + + triton_interleave_loadstore_with_mask[(1, )](q, head_dim_half, bias, numel) + q_ref = torch_interleave_loadstore_with_mask(q_ref, head_dim_half, bias, numel) + assert torch.allclose(q, q_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_inttoptr_offset_reset.py b/third_party/ascend/unittest/pytest_ut/test_inttoptr_offset_reset.py new file mode 100644 index 0000000000..7ad6d2b002 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_inttoptr_offset_reset.py @@ -0,0 +1,265 @@ +import re + +import pytest +import torch +import torch_npu # noqa: F401 + +import triton +import triton.language as tl +from triton.compiler.code_generator import ast_to_ttir +from triton.compiler.compiler import ASTSource +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir +from triton.backends.ascend.compiler import NPUOptions, ttir_to_linalg + + +@triton.jit +def _inttoptr_static_offset_kernel( + block_table_ptrs, + out, + BLOCK_SIZE: tl.constexpr, + BLOCK: tl.constexpr, +): + offsets = tl.arange(0, BLOCK) + + # Load a raw i64 address and cast to pointer — creates tt.int_to_ptr. + block_table_ptr = tl.load(block_table_ptrs).to(tl.pointer_type(tl.int32)) + + # Scalar addptr with a constant offset — creates a ReinterpretCastOp + # whose result type has a non-zero static offset in the strided layout. + block_table_ptr = block_table_ptr + 1 + + block_numbers = tl.load(block_table_ptr + offsets) + slot_ids = block_numbers.to(tl.int64) + tl.store(out + offsets, slot_ids) + + +@triton.jit +def _inttoptr_dyn_offset_kernel( + block_table_ptrs, + dyn_offset, + out, + BLOCK_SIZE: tl.constexpr, + BLOCK: tl.constexpr, +): + offsets = tl.arange(0, BLOCK) + + block_table_ptr = tl.load(block_table_ptrs).to(tl.pointer_type(tl.int32)) + + # Scalar addptr with a runtime offset — creates a ReinterpretCastOp + # whose result type has a dynamic offset (?) in the strided layout. + block_table_ptr = block_table_ptr + tl.load(dyn_offset) + + block_numbers = tl.load(block_table_ptr + offsets) + slot_ids = block_numbers.to(tl.int64) + tl.store(out + offsets, slot_ids) + + +@triton.jit +def _inttoptr_indirect_load_kernel( + block_table_ptrs, + offsets_ptr, + out, + BLOCK: tl.constexpr, +): + offsets = tl.arange(0, BLOCK) + offsets = tl.load(offsets_ptr + offsets) + raw_ptr = tl.load(block_table_ptrs).to(tl.pointer_type(tl.int32)) + raw_ptrs = tl.broadcast_to(raw_ptr, (BLOCK, )) + values = tl.load(raw_ptrs + offsets) + tl.store(out + offsets, values) + + +@triton.jit +def _inttoptr_branch_local_kernel(base_ptrs, flag_ptr, out): + """Use an int_to_ptr only inside each branch, without a loop backedge.""" + raw_addr = tl.load(base_ptrs) + flag = tl.load(flag_ptr) + if flag != 0: + ptr = raw_addr.to(tl.pointer_type(tl.int32)) + value = tl.load(ptr) + else: + ptr = raw_addr.to(tl.pointer_type(tl.int32)) + 4 + value = tl.load(ptr) + tl.store(out, value) + + +@triton.jit +def _inttoptr_for_carrier_kernel(base_ptrs, steps_ptr, out): + """Carry a complete scalar pointer through a for backedge.""" + raw_addr = tl.load(base_ptrs) + steps = tl.load(steps_ptr) + ptr = raw_addr.to(tl.pointer_type(tl.int32)) + for _ in tl.range(0, steps): + ptr = ptr + 1 + tl.store(out, tl.load(ptr)) + + +@triton.jit +def _inttoptr_while_carrier_kernel(base_ptrs, steps_ptr, out): + """Carry the complete i64 address through a while backedge.""" + raw_addr = tl.load(base_ptrs) + steps = tl.load(steps_ptr) + address = raw_addr + iteration = 0 + while iteration < steps: + # The loaded address is byte-addressed; one int32 element is four bytes. + address = address + 4 + iteration += 1 + ptr = address.to(tl.pointer_type(tl.int32)) + tl.store(out, tl.load(ptr)) + + +@triton.jit +def _inttoptr_nested_if_kernel(base_ptrs, steps_ptr, flag_ptr, out): + """Update a complete pointer carrier on both sides of a nested if.""" + raw_addr = tl.load(base_ptrs) + steps = tl.load(steps_ptr) + flag = tl.load(flag_ptr) + ptr = raw_addr.to(tl.pointer_type(tl.int32)) + for iteration in tl.range(0, steps): + if flag != 0: + ptr = ptr + 1 + else: + ptr = ptr + 2 + tl.store(out, tl.load(ptr)) + + +def _compile_to_adapter(kernel, signature, constants): + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + options = NPUOptions() + ttir = ast_to_ttir(kernel, src, context, options, {}, {}) + return str(ttir_to_linalg(ttir, {**options.__dict__}, options, named_ops=True)) + + +def test_inttoptr_static_offset_reset(): + """Static offset (constant 1) baked into base address.""" + block = 8 + + block_table_cpu = torch.arange(16, dtype=torch.int32) + block_table = block_table_cpu.npu() + out = torch.empty((block, ), dtype=torch.int64, device="npu") + + block_table_ptrs = torch.tensor([block_table.data_ptr()], dtype=torch.int64).npu() + + _inttoptr_static_offset_kernel[(1, )]( + block_table_ptrs, + out, + BLOCK_SIZE=4, + BLOCK=block, + ) + torch.npu.synchronize() + + # offset 1 => skip block_table[0] + expected = block_table_cpu[1:1 + block].to(torch.int64) + torch.testing.assert_close(out.cpu(), expected.cpu()) + + +def test_inttoptr_indirect_load_uses_scalar_view(): + """An opaque int_to_ptr tensor load keeps the one-element carrier view.""" + adapter = _compile_to_adapter( + _inttoptr_indirect_load_kernel, + {"block_table_ptrs": "*i64", "offsets_ptr": "*i32", "out": "*i32"}, + {"BLOCK": 8}, + ) + assert "hivm.hir.pointer_cast" in adapter + assert "annotation.mark" in adapter + assert "memref.reinterpret_cast" in adapter + assert re.search(r"memref<1xi32, strided<\[1\]>>", adapter) + + +def test_inttoptr_indirect_load_scalar_view_npu(): + """A runtime int_to_ptr base keeps its one-element view in the helper ABI.""" + source_cpu = torch.arange(16, dtype=torch.int32) + source = source_cpu.npu() + base_ptrs = torch.tensor([source.data_ptr()], dtype=torch.int64).npu() + offsets = torch.arange(8, dtype=torch.int32).npu() + out = torch.empty(8, dtype=torch.int32, device="npu") + + _inttoptr_indirect_load_kernel[(1, )](base_ptrs, offsets, out, BLOCK=8) + torch.npu.synchronize() + torch.testing.assert_close(out.cpu(), source_cpu[:8]) + + +@pytest.mark.parametrize("flag", [0, 1]) +def test_inttoptr_branch_local(flag): + source_cpu = torch.arange(16, dtype=torch.int32) + source = source_cpu.npu() + base_ptrs = torch.tensor([source.data_ptr()], dtype=torch.int64).npu() + flag_ptr = torch.tensor([flag], dtype=torch.int32).npu() + out = torch.empty(1, dtype=torch.int32, device="npu") + + _inttoptr_branch_local_kernel[(1, )](base_ptrs, flag_ptr, out) + torch.npu.synchronize() + expected = source_cpu[0 if flag else 4:1 if flag else 5] + torch.testing.assert_close(out.cpu(), expected) + + +@pytest.mark.parametrize("steps", [0, 1, 4]) +def test_inttoptr_for_carrier(steps): + source_cpu = torch.arange(32, dtype=torch.int32) + source = source_cpu.npu() + base_ptrs = torch.tensor([source.data_ptr()], dtype=torch.int64).npu() + steps_ptr = torch.tensor([steps], dtype=torch.int32).npu() + out = torch.empty(1, dtype=torch.int32, device="npu") + + _inttoptr_for_carrier_kernel[(1, )](base_ptrs, steps_ptr, out) + torch.npu.synchronize() + torch.testing.assert_close(out.cpu(), source_cpu[steps:steps + 1]) + + +@pytest.mark.parametrize("steps", [0, 1, 4]) +def test_inttoptr_while_carrier(steps): + source_cpu = torch.arange(32, dtype=torch.int32) + source = source_cpu.npu() + base_ptrs = torch.tensor([source.data_ptr()], dtype=torch.int64).npu() + steps_ptr = torch.tensor([steps], dtype=torch.int32).npu() + out = torch.empty(1, dtype=torch.int32, device="npu") + + _inttoptr_while_carrier_kernel[(1, )](base_ptrs, steps_ptr, out) + torch.npu.synchronize() + torch.testing.assert_close(out.cpu(), source_cpu[steps:steps + 1]) + + +@pytest.mark.parametrize("flag", [0, 1]) +@pytest.mark.parametrize("steps", [0, 1, 4]) +def test_inttoptr_nested_if(flag, steps): + source_cpu = torch.arange(32, dtype=torch.int32) + source = source_cpu.npu() + base_ptrs = torch.tensor([source.data_ptr()], dtype=torch.int64).npu() + steps_ptr = torch.tensor([steps], dtype=torch.int32).npu() + flag_ptr = torch.tensor([flag], dtype=torch.int32).npu() + out = torch.empty(1, dtype=torch.int32, device="npu") + + _inttoptr_nested_if_kernel[(1, )](base_ptrs, steps_ptr, flag_ptr, out) + torch.npu.synchronize() + offset = steps if flag else 2 * steps + torch.testing.assert_close(out.cpu(), source_cpu[offset:offset + 1]) + + +def test_inttoptr_dyn_offset_reset(): + """Dynamic offset (runtime value) baked into base address.""" + block = 8 + + block_table_cpu = torch.arange(16, dtype=torch.int32) + block_table = block_table_cpu.npu() + out = torch.empty((block, ), dtype=torch.int64, device="npu") + + block_table_ptrs = torch.tensor([block_table.data_ptr()], dtype=torch.int64).npu() + dyn_offset = torch.tensor([2], dtype=torch.int32).npu() + + _inttoptr_dyn_offset_kernel[(1, )]( + block_table_ptrs, + dyn_offset, + out, + BLOCK_SIZE=4, + BLOCK=block, + ) + torch.npu.synchronize() + + # dynamic offset 2 => skip block_table[0:2] + expected = block_table_cpu[2:2 + block].to(torch.int64) + torch.testing.assert_close(out.cpu(), expected.cpu()) diff --git a/third_party/ascend/unittest/pytest_ut/test_invert.py b/third_party/ascend/unittest/pytest_ut/test_invert.py new file mode 100644 index 0000000000..1612315b43 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_invert.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_invert(x0): + res = ~(x0) + return res + + +@triton.jit +def triton_invert(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp2 = ~tmp0 + tl.store(out_ptr0 + (xindex), tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['int8', (2, 4096, 8), 2, 32768, 1024], + ['int16', (2, 4096, 8), 2, 32768, 1024], + ['int32', (2, 4096, 8), 2, 32768, 1024], + ['int64', (2, 4096, 8), 2, 32768, 1024], + ['bool', (2, 4096, 8), 2, 32768, 1024], +]) +def test_invert(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_invert(x0) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_invert[ncore, 1, 1](x0, triton_res, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_isfinited.py b/third_party/ascend/unittest/pytest_ut/test_isfinited.py new file mode 100644 index 0000000000..4fa5be3afe --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_isfinited.py @@ -0,0 +1,135 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import torch +import torch_npu +import pytest +import test_common + +types = [ + "float32", + "float16", + "bfloat16", +] + +shapes = [ + # 3, + # 32, + 37, + # 256, + # 781, +] + + +@pytest.mark.parametrize("sigtype", types) +@pytest.mark.parametrize("N", shapes) +@pytest.mark.parametrize("val", ['1.0', 'nan', 'inf', '-inf']) +def test_isfinited(sigtype, N, val): + + def torch_func(x0): + res = torch.isfinite(x0) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = tl.arange(0, N) + x0 = tl.load(in_ptr0 + idx) + ret = libdevice.isfinited(x0) + tl.store(out_ptr0 + idx, ret) + + def triton_func(x0, N): + out = torch.zeros(x0.shape, dtype=torch.bool).npu() + triton_kernel[1, 1, 1](out, x0, N) + return out + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x0[1] = float(val) + + torch_ref = torch_func(x0) + triton_cal = triton_func(x0, N) + + test_common.validate_cmp("bool", triton_cal, torch_ref) + + +@pytest.mark.parametrize("N", shapes) +@pytest.mark.parametrize("val", ['1.0', 'nan', 'inf', '-inf']) +def test_finitef(N, val): + + def torch_func(x0): + res = torch.isfinite(x0) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = tl.arange(0, N) + x0 = tl.load(in_ptr0 + idx) + ret = libdevice.finitef(x0) + tl.store(out_ptr0 + idx, ret) + + def triton_func(x0, N): + out = torch.zeros(x0.shape, dtype=torch.bool).npu() + triton_kernel[1, 1, 1](out, x0, N) + return out + + x0 = test_common.generate_tensor(shape=(N, ), dtype='float32').npu() + x0[1] = float(val) + + torch_ref = torch_func(x0) + triton_cal = triton_func(x0, N) + test_common.validate_cmp("bool", triton_cal, torch_ref) + + +invalid_types = [ + "int32", +] + + +@pytest.mark.parametrize("sigtype", invalid_types) +@pytest.mark.parametrize("N", shapes) +@test_common.raises_with_match(triton.compiler.errors.CompilationError, "Expected dtype fp16/fp32/bf16, but got") +def test_isfinited_invalid_dtype(sigtype, N, monkeypatch): + monkeypatch.setenv('TRITON_FRONT_END_DEBUGGING', '1') + + def torch_func(x0): + res = torch.isfinite(x0) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = tl.arange(0, N) + x0 = tl.load(in_ptr0 + idx) + ret = libdevice.isfinited(x0) + tl.store(out_ptr0 + idx, ret) + + def triton_func(x0, N): + out = torch.zeros(x0.shape, dtype=torch.bool).npu() + triton_kernel[1, 1, 1](out, x0, N) + return out + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x0[1] = float('nan') + + torch_ref = torch_func(x0) + triton_cal = triton_func(x0, N) + test_common.validate_cmp("bool", triton_cal, torch_ref) + assert triton_cal[1] == True diff --git a/third_party/ascend/unittest/pytest_ut/test_isnan.py b/third_party/ascend/unittest/pytest_ut/test_isnan.py new file mode 100644 index 0000000000..dedf3d903b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_isnan.py @@ -0,0 +1,107 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + +types = [ + "float32", + "float16", + "bfloat16", +] + +shapes = [ + # 3, + # 32, + 37, + # 256, + # 781, +] + + +@pytest.mark.parametrize("sigtype", types) +@pytest.mark.parametrize("N", shapes) +def test_isnan(sigtype, N): + + def torch_func(x0): + res = torch.isnan(x0) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = tl.arange(0, N) + x0 = tl.load(in_ptr0 + idx) + ret = tl.extra.cann.libdevice.isnan(x0) + tl.store(out_ptr0 + idx, ret) + + def triton_func(x0, N): + out = torch.zeros(x0.shape, dtype=torch.bool).npu() + triton_kernel[1, 1, 1](out, x0, N) + return out + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x0[1] = float('nan') + + torch_ref = torch_func(x0) + triton_cal = triton_func(x0, N) + test_common.validate_cmp("bool", triton_cal, torch_ref) + assert triton_cal[1] == True + + +invalid_types = [ + "int32", +] + + +@pytest.mark.parametrize("sigtype", invalid_types) +@pytest.mark.parametrize("N", shapes) +def test_isnan_invalid_dtype(sigtype, N): + + def torch_func(x0): + res = torch.isnan(x0) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = tl.arange(0, N) + x0 = tl.load(in_ptr0 + idx) + ret = tl.extra.cann.libdevice.isnan(x0) + tl.store(out_ptr0 + idx, ret) + + def triton_func(x0, N): + out = torch.zeros(x0.shape, dtype=torch.bool).npu() + triton_kernel[1, 1, 1](out, x0, N) + return out + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x0[1] = float('nan') + flag = False + try: + torch_ref = torch_func(x0) + triton_cal = triton_func(x0, N) + test_common.validate_cmp("bool", triton_cal, torch_ref) + assert triton_cal[1] == True + except Exception as e: + flag = True + assert flag diff --git a/third_party/ascend/unittest/pytest_ut/test_join.py b/third_party/ascend/unittest/pytest_ut/test_join.py new file mode 100644 index 0000000000..293cf927ae --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_join.py @@ -0,0 +1,66 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, y_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + + idx = xidx[:, None] * YB + yidx[None, :] + + X = tl.load(x_ptr + idx) + Y = tl.load(y_ptr + idx) + + ret = tl.join(X, Y) + + oidx = xidx[:, None, None] * YB * 2 + yidx[None, :, None] * 2 + tl.arange(0, 2)[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@pytest.mark.parametrize('para_type,data_type,XB,YB,ZB', [ + ['float32', torch.float32, 4, 64, 4], + ['float32', torch.float32, 8, 8, 4], + ['float16', torch.float16, 4, 64, 4], + ['float16', torch.float16, 8, 8, 4], + ['int8', torch.int8, 4, 128, 4], + ['int8', torch.int8, 8, 8, 4], +]) +def test_join(para_type, data_type, XB, YB, ZB): + x = torch.full((XB, YB), 100, dtype=data_type).npu() + y = torch.full((XB, YB), 30, dtype=data_type).npu() + + ans = torch.stack((x, y), dim=-1) + print(ans) + + output = torch.randint(1, (XB, YB, 2), dtype=data_type).npu() + fn_npu_[1, 1, 1](output, x, y, XB, YB, ZB, debug=True) + + print(output) + test_common.validate_cmp(para_type, ans, output) diff --git a/third_party/ascend/unittest/pytest_ut/test_lanzcos.py b/third_party/ascend/unittest/pytest_ut/test_lanzcos.py new file mode 100644 index 0000000000..ce6b034297 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_lanzcos.py @@ -0,0 +1,246 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch, torch_npu +import triton +import triton.language as tl +import numpy as np +import math +import pytest + + +@triton.jit +def lanczos_resize_kernel( + img_src_ptr, + img_dst_ptr, + img_coeffs_ptr, + src_rows, + src_cols, + dst_rows, + dst_cols, + R_H, + R_W, + C, + stride_in_h, + stride_in_w, + stride_in_c, + stride_out_h, + stride_out_w, + stride_out_c, + BLOCK_SIZE: tl.constexpr, +): + block_id_c = tl.program_id(0) + block_id_h = tl.program_id(1) + block_id_w = tl.program_id(2) + dest_h_offs = block_id_h * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + dest_w_offs = block_id_w * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + dest_offs = (block_id_c[None, None] * stride_out_c + dest_h_offs[:, None] * stride_out_h + + dest_w_offs[None, :] * stride_out_w) + + RR_H = 1.0 / R_H + RR_W = 1.0 / R_W + + fy = (dest_h_offs + 0.5) * RR_H - 0.5 + sy = tl.floor(fy) + fx = (dest_w_offs + 0.5) * RR_W - 0.5 + sx = tl.floor(fx) + + idxY = tl.floor((fy - sy) * 24.999999).to(tl.int32) + idxX = tl.floor((fx - sx) * 24.999999).to(tl.int32) + tableIndex = idxY[:, None] * 25 + idxX[None, :] + res = tl.zeros((BLOCK_SIZE, BLOCK_SIZE), tl.float32) + + for ii in range(4): + for jj in range(4): + src_offsets = (block_id_c[None, None] * stride_in_c + (tl.clamp( + (sy + ii - 1), 0.0, (src_rows - 1).to(tl.float32)).to(tl.int32))[:, None] * stride_in_h + (tl.clamp( + (sx + jj - 1), 0.0, (src_cols - 1).to(tl.float32)).to(tl.int32))[None, :] * stride_in_w) + src_val = tl.load(img_src_ptr + src_offsets) + coeffs_offs = tableIndex[:, :] * 16 + (ii * 4 + jj)[None, None] + coeffs = tl.load(img_coeffs_ptr + coeffs_offs) + res = res + src_val * coeffs + dst_mask = (dest_h_offs[:, None] < dst_rows) & (dest_w_offs[None, :] < dst_cols) + res = tl.clamp(res, 0.0, 1.0) + tl.store(img_dst_ptr + dest_offs, res, mask=dst_mask) + + +def lanczos_resize_triton(img_src, img_dst, c_lanczosCoeffs, dst_rows, dst_cols): + N, C, src_rows, src_cols = img_src.shape + R_H = float(dst_rows) / src_rows + R_W = float(dst_cols) / src_cols + + stride_in_n, stride_in_c, stride_in_h, stride_in_w = img_src.stride() + stride_out_n, stride_out_c, stride_out_h, stride_out_w = img_dst.stride() + BLOCK_SIZE = 16 + grid = lambda meta: ( + C, + triton.cdiv(dst_rows, meta["BLOCK_SIZE"]), + triton.cdiv(dst_cols, meta["BLOCK_SIZE"]), + ) + lanczos_resize_kernel[grid]( + img_src, + img_dst, + c_lanczosCoeffs, + src_rows, + src_cols, + dst_rows, + dst_cols, + R_H, + R_W, + C, + stride_in_h, + stride_in_w, + stride_in_c, + stride_out_h, + stride_out_w, + stride_out_c, + BLOCK_SIZE=BLOCK_SIZE, + ) + return img_dst + + +def lanczos_resize_cpu(img_src, img_dst, img_coeffs, dst_rows, dst_cols): + N, C, src_rows, src_cols = img_src.shape + R_H = float(dst_rows) / src_rows + R_W = float(dst_cols) / src_cols + for i in range(dst_rows): + for j in range(dst_cols): + RR_H = 1.0 / R_H + RR_W = 1.0 / R_W + fy = (i + 0.5) * RR_H - 0.5 + sy = math.floor(fy) + fx = (j + 0.5) * RR_W - 0.5 + sx = math.floor(fx) + idxY = math.floor((fy - np.floor(fy)) * 24.999999) + idxX = math.floor((fx - np.floor(fx)) * 24.999999) + tableIndex = idxY * 25 + idxX + res = (0.0, 0.0, 0.0, 0.0) + for ii in range(4): + for jj in range(4): + idx_y = np.clip(sy + ii - 1, 0, src_rows - 1) + idx_x = np.clip(sx + jj - 1, 0, src_cols - 1) + src_val = img_src[0, :, idx_y, idx_x] + coeffs_offs = tableIndex * 16 + (ii * 4 + jj) + coeffs = img_coeffs[coeffs_offs] + res = res + src_val * coeffs + + img_dst[0, :, i, j] = np.clip(res, 0.0, 1.0) + + +@pytest.mark.parametrize("shapes", [ + [360, 640, 140, 280], +]) +def test_lanzcos(shapes): + c_lanczosCoeffs = torch.randn(10000, dtype=torch.float32, device="npu") / 4.0 + src_rows, src_cols, dst_rows, dst_cols = shapes + img_src = torch.randn(1, 4, src_rows, src_cols, dtype=torch.float32, device="npu") + img_dst = torch.zeros( + (1, img_src.shape[1], dst_rows, dst_cols), + dtype=img_src.dtype, + device=img_src.device, + ) + resized_image = lanczos_resize_triton(img_src, img_dst, c_lanczosCoeffs, dst_rows, dst_cols) + img_src_cpu = img_src.cpu().numpy() + img_dst_cpu = torch.zeros((1, img_src_cpu.shape[1], dst_rows, dst_cols), dtype=img_src.dtype, device="cpu").numpy() + lanczos_resize_cpu(img_src_cpu, img_dst_cpu, c_lanczosCoeffs.cpu().numpy(), dst_rows, dst_cols) + torch.testing.assert_close(resized_image.cpu(), torch.from_numpy(img_dst_cpu), atol=1.0 / 255, rtol=0) + + +def benchmark_test(fn_ref, fn_triton, ref_args=(), triton_args=(), name="gen_fn", times=10, repeat=10): + import time + + print(f"--------------------benchmark_{name} for {times * repeat} times--------------------") + stream = torch.npu.current_stream() + # warm_up + stream.synchronize() + for _ in range(10): + fn_triton(*triton_args) + stream.synchronize() + + start = time.perf_counter() + for _ in range(times * repeat): + fn_triton(*triton_args) + stream.synchronize() + end = time.perf_counter() + + time_compiled = (end - start) / (times * repeat) + time_compiled *= 1000000 + + # warm_up + stream.synchronize() + for _ in range(10): + std = fn_ref(*ref_args) + stream.synchronize() + + start = time.perf_counter() + for _ in range(times * repeat): + std = fn_ref(*ref_args) + stream.synchronize() + end = time.perf_counter() + time_eager = (end - start) / (times * repeat) + time_eager *= 1000000 + + accelerated = (time_eager - time_compiled) / time_compiled * 100 + print(f"Accelerated: {accelerated:.4f}% eager takes {time_eager:.3f} us, triton takes {time_compiled:.3f} us") + + return accelerated, time_eager, time_compiled + + +if __name__ == "__main__": + c_lanczosCoeffs = torch.randn(10000, dtype=torch.float32, device="npu") / 4.0 + + src_rows, src_cols = 360, 640 + dst_rows, dst_cols = 140, 280 + img_src = torch.randn(1, 4, src_rows, src_cols, dtype=torch.float32, device="npu") + + print("==========run npu===============") + img_dst = torch.zeros( + (1, img_src.shape[1], dst_rows, dst_cols), + dtype=img_src.dtype, + device=img_src.device, + ) + resized_image = lanczos_resize_triton(img_src, img_dst, c_lanczosCoeffs, dst_rows, dst_cols) + resized_cpu = resized_image.cpu().numpy() + print("==========run cpu===============") + img_src_cpu = img_src.cpu().numpy() + img_dst_cpu = torch.zeros((1, img_src_cpu.shape[1], dst_rows, dst_cols), dtype=img_src.dtype, device="cpu").numpy() + lanczos_resize_cpu(img_src_cpu, img_dst_cpu, c_lanczosCoeffs.cpu().numpy(), dst_rows, dst_cols) + + print("==========compare result===============") + diff = np.abs(resized_cpu - img_dst_cpu) + max_diff_value = np.max(diff) + print("max diff float = ", max_diff_value) + print("max diff * 255 int = ", int(max_diff_value * 255)) + torch.testing.assert_close(resized_image.cpu(), torch.from_numpy(img_dst_cpu), atol=1.0 / 255, rtol=0) + + print("==========profiling===============") + accelerate, eager_time, triton_time = benchmark_test( + lanczos_resize_cpu, + lanczos_resize_triton, + ref_args=( + img_src_cpu, + img_dst_cpu, + c_lanczosCoeffs.cpu().numpy(), + dst_rows, + dst_cols, + ), + triton_args=(img_src, img_dst, c_lanczosCoeffs, dst_rows, dst_cols), + name="lanzcos", + ) diff --git a/third_party/ascend/unittest/pytest_ut/test_launcher_empty_signature.py b/third_party/ascend/unittest/pytest_ut/test_launcher_empty_signature.py new file mode 100644 index 0000000000..7b9fc3cf3d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_launcher_empty_signature.py @@ -0,0 +1,56 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import pytest + +import triton +import triton.language as tl + + +@triton.jit +def _empty_kernel(): + return + + +@pytest.mark.interpreter +def test_launcher_empty_signature(): + grid = (1, ) + _empty_kernel[grid]() + assert True + + +# Regression test for commit 6365783a: +# When all kernel parameters are constexpr, the launcher generated invalid +# C++ code with a trailing comma in the _launch function signature because +# it checked len(signature) > 0 instead of len(arg_decls) > 0. +@triton.jit +def _all_constexpr_kernel(VAL: tl.constexpr): + tl.static_assert(VAL == 42) + pass + + +@pytest.mark.interpreter +def test_launcher_all_params_constexpr(): + grid = (1, ) + # All params are constexpr -> arg_decls is empty but signature is not. + # Before the fix this produced: void _launch(..., ) /* trailing comma */ + _all_constexpr_kernel[grid](VAL=42) + assert True diff --git a/third_party/ascend/unittest/pytest_ut/test_launcher_export_api.py b/third_party/ascend/unittest/pytest_ut/test_launcher_export_api.py new file mode 100644 index 0000000000..cae91be93e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_launcher_export_api.py @@ -0,0 +1,590 @@ +import importlib.util +import sys +from itertools import product +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "python")) + + +def _load_driver_module(): + driver_path = Path(__file__).resolve().parents[2] / "backend" / "driver.py" + spec = importlib.util.spec_from_file_location("ascend_driver_under_test", driver_path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +driver = _load_driver_module() + + +def _mock_backend_func(name, *args): + return f"/* {name}: {args} */" + + +def _make_metadata(): + return SimpleNamespace( + target=driver.GPUTarget("npu", "Ascend910B3", 0), + workspace_size=0, + lock_init_value=0, + sync_block_lock_layout=0, + bs_task_type=0, + mix_mode="aiv", + shared=0, + compile_on_910_95=False, + parallel_mode="", + is_pure_simt=False, + debug=False, + coalesce_factor=1, + coalesce_axis=-1, + coalesce_grid_ceil_div=False, + has_auto_blockify_blacklist_op=False, + row_coalescing_applied=False, + program_grid_transforms=None, + program_grid_mapping_applied=False, + auto_blockify_enabled=False, + ptsm_cap_authorized=False, + ) + + +def _make_program_grid_contract(*transforms): + return { + "version": 2, + "extent_source": "runtime_original_grid", + "hidden_extent_axes": [0, 1], + "hidden_argument_order": ["originalGridX", "originalGridY"], + "hidden_argument_types": ["i32", "i32"], + "transforms": list(transforms), + } + + +def _split_launch_functions(src): + c_abi_part, cpp_part = src.split("static void _launch(", maxsplit=1) + return c_abi_part, "static void _launch(" + cpp_part + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_make_launcher_exposes_triton_launch_kernel( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32", 2: "i32"}, + metadata=_make_metadata(), + ) + + assert 'void triton_launch_kernel(' in src + assert 'const void* const* kernel_args, const size_t* arg_sizes, int num_args' in src + assert 'std::vector> copied_kernel_args;' in src + assert 'std::vector launch_arg_sizes;' in src + assert 'std::vector launch_args(total_size, 0);' in src + assert 'memcpy(launch_args.data() + grid_offset, &gridX, sizeof(int32_t));' in src + _mock_ffts.assert_called_once_with("Ascend910B3") + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_make_launcher_resolves_npu_utils_from_active_cache_root( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + cache_key = "NPU_UTILS_CACHE_KEY" + + def make_utils(cache_root): + return SimpleNamespace( + get_aivector_core_num=lambda: 40, + get_aicore_num=lambda: 20, + get_so_path=lambda: f"{cache_root}/{cache_key}/npu_utils.so", + ) + + producer_utils = make_utils("/producer/cache") + consumer_utils = make_utils("/consumer/cache") + # make_launcher currently reads NPUUtils once for core counts and once for + # the shared-object path. + mock_npu_utils.side_effect = [producer_utils, producer_utils, consumer_utils, consumer_utils] + + producer_src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=_make_metadata(), + ) + consumer_src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=_make_metadata(), + ) + + assert producer_src == consumer_src + assert "/producer/cache" not in producer_src + assert "/consumer/cache" not in consumer_src + assert '#include ' in producer_src + assert 'const char* cache_root = std::getenv("TRITON_CACHE_DIR");' in producer_src + assert f'npu_utils_path = std::string(cache_root) + "/{cache_key}/npu_utils.so";' in producer_src + assert 'const char* triton_home = std::getenv("TRITON_HOME");' in producer_src + assert f'npu_utils_path = std::string(base) + "/.triton/cache/{cache_key}/npu_utils.so";' in producer_src + module_init = producer_src.split("PyMODINIT_FUNC PyInit___triton_launcher", maxsplit=1)[1] + assert "init_npu_utils();" in module_init + assert "set_npu_utils_path" not in producer_src + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_make_launcher_rejects_unmatched_coalescing_metadata( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + metadata = _make_metadata() + metadata.coalesce_factor = 16 + metadata.coalesce_axis = 1 + + with pytest.raises(RuntimeError, match="unmatched RowCoalescing"): + driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_make_launcher_uses_ceil_div_for_row_coalescing( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + metadata = _make_metadata() + metadata.coalesce_factor = 4 + metadata.coalesce_axis = 2 + metadata.coalesce_grid_ceil_div = True + metadata.row_coalescing_applied = True + metadata.auto_blockify_enabled = True + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + + assert src.count("gridZ = (gridZ + 4 - 1) / 4;") == 2 + assert "ChunkCoalescing: grid[2] not divisible" not in src + assert src.count("blockNum = std::min(blockNum, (uint32_t)40);") == 2 + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_make_launcher_allows_auto_blockify_with_iat_mapping( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + metadata = _make_metadata() + metadata.program_grid_mapping_applied = True + metadata.auto_blockify_enabled = True + metadata.program_grid_transforms = _make_program_grid_contract({ + "order": 0, + "kind": "ceil_div", + "axis": 1, + "factor": 16, + "persistent_coverage": False, + "grid_stride_abi_verified": False, + }) + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + + assert src.count("gridY = (uint32_t)(((uint64_t)originalGridY + 15u) / 16u);") == 2 + assert src.count("blockNum = std::min(blockNum, (uint32_t)40);") == 2 + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_make_launcher_enables_91095_simt_for_sls_mixed_parallel_mode( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + metadata = _make_metadata() + metadata.compile_on_910_95 = True + metadata.parallel_mode = "mix_simd_simt" + metadata.shared_mem_dynamic_size = 221184 + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + + assert src.count("cann_launch_kernel(func, blockNum") == 2 + assert src.count("cann_get_launch_kernel_cfg(221184)") == 2 + assert src.count("aclrtLaunchKernelWithHostArgs") == 1 + assert src.count("attrInfo.id = ACL_RT_LAUNCH_KERNEL_ATTR_DYN_UBUF_SIZE;") == 1 + c_abi_launch, cpp_launch = _split_launch_functions(src) + assert "static_cast(launch_args.data())" in c_abi_launch + assert "&args" in cpp_launch + assert "launch_args.size()" in c_abi_launch + assert "sizeof(args)" in cpp_launch + + +@pytest.mark.parametrize( + ("auto_blockify_enabled", "blacklisted"), + ( + (False, False), + (False, True), + (True, False), + (True, True), + ), +) +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_make_launcher_block_cap_follows_compiler_auto_blockify_decision( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, + auto_blockify_enabled, + blacklisted, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + cap = "blockNum = std::min(blockNum, (uint32_t)40);" + + for is_pure_simt, row_coalescing_applied in product( + (False, True), + (False, True), + ): + metadata = _make_metadata() + metadata.has_auto_blockify_blacklist_op = blacklisted + metadata.auto_blockify_enabled = auto_blockify_enabled + metadata.is_pure_simt = is_pure_simt + metadata.row_coalescing_applied = row_coalescing_applied + if row_coalescing_applied: + metadata.coalesce_factor = 4 + metadata.coalesce_axis = 0 + metadata.coalesce_grid_ceil_div = True + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + expected_per_launch_path = 1 if auto_blockify_enabled else 0 + c_abi_launch, cpp_launch = _split_launch_functions(src) + case = (f"P={is_pure_simt}, B={blacklisted}, R={row_coalescing_applied}, " + f"A={auto_blockify_enabled}") + assert c_abi_launch.count(cap) == expected_per_launch_path, case + assert cpp_launch.count(cap) == expected_per_launch_path, case + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_make_launcher_disables_block_cap_for_ptsm( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + metadata = _make_metadata() + metadata.program_grid_mapping_applied = True + metadata.ptsm_cap_authorized = True + metadata.program_grid_transforms = _make_program_grid_contract({ + "order": 0, + "kind": "ceil_div", + "axis": 0, + "factor": 64, + "persistent_coverage": True, + "grid_stride_abi_verified": True, + }) + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + + assert "blockNum = std::min(blockNum, (uint32_t)40);" not in src + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_merged_code_workspace_allocation_appears_in_both_paths( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + metadata = _make_metadata() + metadata.workspace_size = 1024 + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + + c_abi_launch, cpp_launch = _split_launch_functions(src) + + assert c_abi_launch.count("allocate_memory") == 1 + assert cpp_launch.count("allocate_memory") == 1 + assert c_abi_launch.count("workspace_handle_guard") == 1 + assert cpp_launch.count("workspace_handle_guard") == 1 + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_merged_code_sync_block_lock_appears_in_both_paths( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + metadata = _make_metadata() + metadata.sync_block_lock_layout = 2 + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + + c_abi_launch, cpp_launch = _split_launch_functions(src) + + assert c_abi_launch.count("/* allocate_sync_block_lock: ('syncBlockLockSize', 'stream') */") == 1 + assert cpp_launch.count("/* allocate_sync_block_lock: ('syncBlockLockSize', 'stream') */") == 1 + assert c_abi_launch.count("syncBlockLock_handle_guard") == 1 + assert cpp_launch.count("syncBlockLock_handle_guard") == 1 + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_merged_code_msprof_calls_in_both_paths( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32"}, + metadata=_make_metadata(), + ) + + c_abi_launch, cpp_launch = _split_launch_functions(src) + + assert c_abi_launch.count("beginTime = MsprofSysCycleTime();") == 1 + assert cpp_launch.count("beginTime = MsprofSysCycleTime();") == 1 + assert c_abi_launch.count("MsprofReportApi(false, &info);") == 1 + assert cpp_launch.count("MsprofReportApi(false, &info);") == 1 + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_merged_code_preamble_shared_variables_present( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32", 2: "i32"}, + metadata=_make_metadata(), + ) + + c_abi_launch, cpp_launch = _split_launch_functions(src) + + for section_name, section in [("triton_launch_kernel", c_abi_launch), ("_launch", cpp_launch)]: + assert "void* workspace_addr_ptr = nullptr;" in section, f"{section_name}: missing workspace_addr_ptr" + assert "void* workspace_handle = nullptr;" in section, f"{section_name}: missing workspace_handle" + assert "uint32_t blockNum4Workspace = gridX * gridY * gridZ;" in section, f"{section_name}: missing blockNum4Workspace" + assert "uint32_t blockNum = gridX * gridY * gridZ;" in section, f"{section_name}: missing blockNum" + assert "cann_error ret = CANN_SUCCESS;" in section, f"{section_name}: missing ret" + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_merged_code_taskqueue_mode_in_both_paths( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32"}, + metadata=_make_metadata(), + ) + + c_abi_launch, cpp_launch = _split_launch_functions(src) + + assert c_abi_launch.count("std::function launch_call") == 1 + assert cpp_launch.count("std::function launch_call") == 1 + # The dlsym helpers in cpp_npu_utils_dlopen reference "async_launch" multiple + # times (typedef, static decl, dlsym). Use the call site marker returned by + # the mocked get_backend_func to verify the actual call appears in both paths. + assert c_abi_launch.count("/* async_launch: ('launch_call',) */") == 1 + assert cpp_launch.count("/* async_launch: ('launch_call',) */") == 1 + assert c_abi_launch.count("return ret;") >= 1 + assert cpp_launch.count("return ret;") >= 1 + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_merged_code_grid_warning_in_both_paths( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + + with patch.dict("os.environ", {"TRITON_GRID_WARN_PRINT": "true"}): + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=_make_metadata(), + ) + + c_abi_launch, cpp_launch = _split_launch_functions(src) + + assert c_abi_launch.count("Grid %u > physical limit") == 1 + assert cpp_launch.count("Grid %u > physical limit") == 1 + + +@patch.object(driver, "NPUUtils") +@patch.object(driver, "force_disable_ffts", return_value=False) +@patch.object(driver, "is_ffts_supported", return_value=True) +@patch.object(driver, "get_backend_func", side_effect=_mock_backend_func) +def test_argument_packing_differs_between_launch_paths( + _mock_backend_func_patch, + _mock_ffts, + _mock_disable_ffts, + mock_npu_utils, +): + mock_npu_utils.return_value.get_aivector_core_num.return_value = 40 + mock_npu_utils.return_value.get_aicore_num.return_value = 20 + + src = driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32", 2: "i32"}, + metadata=_make_metadata(), + ) + + c_abi_launch, cpp_launch = _split_launch_functions(src) + + assert "std::vector launch_args(total_size, 0);" in c_abi_launch + assert "reserve_slot" in c_abi_launch + + assert "struct __attribute__((packed))" in cpp_launch + assert "void* ffts_addr __attribute__((aligned(8)));" in cpp_launch + assert "memcpy(launch_args.data()" not in cpp_launch + + +@patch("importlib.util.module_from_spec") +@patch("importlib.util.spec_from_file_location") +@patch.object(driver, "make_npu_launcher_stub", return_value="/tmp/fake_launcher.so") +@patch.object(driver, "make_launcher", return_value="// wrapper src") +@patch.object(driver, "generate_npu_header_src", return_value="// header src") +def test_npu_launcher_exposes_launcher_so_path( + mock_header_src, + mock_wrapper_src, + mock_launcher_stub, + mock_spec_from_file_location, + mock_module_from_spec, +): + fake_module = SimpleNamespace(launch=object()) + fake_spec = SimpleNamespace(loader=SimpleNamespace(exec_module=lambda module: None)) + mock_module_from_spec.return_value = fake_module + mock_spec_from_file_location.return_value = fake_spec + src = SimpleNamespace( + constants={"input_ptr": 1}, + signature={"input_ptr": "*fp32", "numel": "i32"}, + fn=SimpleNamespace(arg_names=["input_ptr", "numel"]), + ) + metadata = _make_metadata() + + launcher = driver.NPULauncher(src, metadata) + + assert launcher.so_launcher_path == "/tmp/fake_launcher.so" + assert mock_launcher_stub.call_count == 1 + assert launcher.get_launcher_so_path() == "/tmp/fake_launcher.so" + assert mock_header_src.call_count == 1 + assert mock_wrapper_src.call_count == 1 + assert mock_launcher_stub.call_count == 1 + mock_wrapper_src.assert_called_with( + {0: 1}, + {0: "*fp32", 1: "i32"}, + metadata, + ) + mock_launcher_stub.assert_called_with("// header src", "// wrapper src", False) diff --git a/third_party/ascend/unittest/pytest_ut/test_layernorm.py b/third_party/ascend/unittest/pytest_ut/test_layernorm.py new file mode 100644 index 0000000000..328bae5a0d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_layernorm.py @@ -0,0 +1,126 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import triton +import triton.language as tl +import torch_npu + + +@triton.jit +def _layer_norm_fwd_fused( + X, # pointer to the input + Y, # pointer to the output + W, # pointer to the weights + B, # pointer to the biases + Mean, # pointer to the mean + Rstd, # pointer to the 1/std + stride, # how much to increase the pointer when moving by 1 row + N, # number of columns in X + eps, # epsilon to avoid division by zero + BLOCK_SIZE: tl.constexpr, +): + # Map the program id to the row of X and Y it should compute. + row = tl.program_id(0) + Y += row * stride + X += row * stride + # Compute mean + mean = 0 + _mean = tl.zeros([BLOCK_SIZE], dtype=tl.float32) + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + a = tl.load(X + cols, mask=cols < N, other=0.).to(tl.float32) + _mean += a + mean = tl.sum(_mean, axis=0) / N + # Compute variance + _var = tl.zeros([BLOCK_SIZE], dtype=tl.float32) + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + x = tl.load(X + cols, mask=cols < N, other=0.).to(tl.float32) + x = tl.where(cols < N, x - mean, 0.) + _var += x * x + var = tl.sum(_var, axis=0) / N + rstd = 1 / tl.sqrt(var + eps) + # Write mean / rstd + tl.store(Mean + row, mean) + tl.store(Rstd + row, rstd) + # Normalize and apply linear transformation + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + mask = cols < N + w = tl.load(W + cols, mask=mask) + b = tl.load(B + cols, mask=mask) + x = tl.load(X + cols, mask=mask, other=0.).to(tl.float32) + x_hat = (x - mean) * rstd + y = x_hat * w + b + # Write output + tl.store(Y + cols, y, mask=mask) + + +@torch.inference_mode() +def layer_norm(x, normalized_shape, weight, bias, eps=1e-5): + # allocate output + y = torch.empty_like(x) + # reshape input data into 2D tensor + x_arg = x.reshape(-1, x.shape[-1]) + M, N = x_arg.shape + mean = torch.empty((M, ), dtype=torch.float32, device=x.device) + rstd = torch.empty((M, ), dtype=torch.float32, device=x.device) + # Less than 64KB per feature: enqueue fused kernel + MAX_FUSED_SIZE = 65536 // x.element_size() + BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) + # heuristics for number of warps + num_warps = min(max(BLOCK_SIZE // 256, 1), 8) + # enqueue kernel + kernel = _layer_norm_fwd_fused[(M, )]( # + x_arg, y, weight, bias, mean, rstd, # + x_arg.stride(0), N, eps, # + BLOCK_SIZE=BLOCK_SIZE, num_warps=num_warps, num_ctas=1) + # print(kernel.asm['ttir']) + return y + + +def _layer_norm(M, N, dtype, eps=1e-5, device='npu'): + # create data + x_shape = (M, N) + w_shape = (x_shape[-1], ) + weight = torch.rand(w_shape, dtype=dtype, device=device, requires_grad=True) + bias = torch.rand(w_shape, dtype=dtype, device=device, requires_grad=True) + x = -2.3 + 0.5 * torch.randn(x_shape, dtype=dtype, device=device) + dy = .1 * torch.randn_like(x) + x.requires_grad_(True) + # forward pass + y_tri = layer_norm(x, w_shape, weight, bias, eps) + y_ref = torch.nn.functional.layer_norm(x, w_shape, weight, bias, eps).to(dtype) + # compare + assert torch.allclose(y_tri, y_ref, atol=1e-2, rtol=0) + print(f"layernorm {M},{N} {dtype} passed") + + +def test_layernorm(): + _layer_norm(128, 128, torch.float16) + _layer_norm(128, 128, torch.bfloat16) + _layer_norm(128, 128, torch.float32) + + # _layer_norm(128, 3, torch.bfloat16) + # _layer_norm(128, 16, torch.bfloat16) + # _layer_norm(128, 37, torch.bfloat16) + # _layer_norm(128, 781, torch.bfloat16) diff --git a/third_party/ascend/unittest/pytest_ut/test_layout_memory_895_closure_differential.py b/third_party/ascend/unittest/pytest_ut/test_layout_memory_895_closure_differential.py new file mode 100644 index 0000000000..9f3b830947 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_layout_memory_895_closure_differential.py @@ -0,0 +1,791 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +"""895 closure differential tests for layout / memory-access migration. + +The migration deliberately keeps the original compiler, launcher, and +autotuner contracts. This test obtains those three 895 sources with +``git show`` and executes only the relevant AST closure with local fakes. It +does not import a baseline wheel or depend on a sibling checkout. A normal +checkout which no longer contains the baseline object simply skips this +optional historical differential test; release validation sets +``TRITON_REQUIRE_895_DIFFERENTIAL=1`` so a missing baseline is a failure +rather than a misleading green skip. +""" + +import ast +import copy +import itertools +import os +import subprocess +import tempfile +from pathlib import Path +from types import SimpleNamespace + +import pytest + +_BASELINE_COMMIT = "895c5fbe2b0e69349b76388e65fd8c3e79703bb9" +_REBASE_BASE_COMMIT = "e28d648cc79e270e6bd0baea5c24a285407faa7e" +_REQUIRE_BASELINE_ENV = "TRITON_REQUIRE_895_DIFFERENTIAL" +_SOURCE_PATHS = { + "compiler": "third_party/ascend/backend/compiler.py", + "driver": "third_party/ascend/backend/driver.py", + "autotuner": "third_party/ascend/backend/runtime/autotuner.py", +} + + +def _repo_root(): + return Path(__file__).resolve().parents[4] + + +def _source_text(path): + # compiler.py starts with a UTF-8 BOM in some revisions. Python's normal + # source loader accepts it, whereas ast.parse receives the decoded marker. + return path.read_text(encoding="utf-8-sig") + + +def _baseline_is_required(): + return os.environ.get(_REQUIRE_BASELINE_ENV, "").lower() in { + "1", + "true", + "yes", + } + + +@pytest.fixture(scope="module") +def source_pairs(): + """Return (895, target) source text for every closure under test. + + Keep the historical object optional for a normal shallow checkout. The + release-validation environment explicitly upgrades an unavailable object + to failure, so the differential cannot silently disappear from the + migration acceptance evidence. + """ + root = _repo_root() + pairs = {} + for name, relative_path in _SOURCE_PATHS.items(): + try: + result = subprocess.run( + ["git", "show", f"{_BASELINE_COMMIT}:{relative_path}"], + cwd=root, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + except OSError as error: + message = f"cannot invoke git for 895 closure differential: {error}" + if _baseline_is_required(): + pytest.fail(message) + pytest.skip(message) + if result.returncode != 0: + message = ("895 closure differential baseline is unavailable: " + f"{result.stderr.strip() or _BASELINE_COMMIT}") + if _baseline_is_required(): + pytest.fail(message) + pytest.skip(message) + pairs[name] = (result.stdout.lstrip("\ufeff"), _source_text(root / relative_path)) + return pairs + + +def _top_level_functions(source, *names): + tree = ast.parse(source) + by_name = {node.name: node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))} + missing = set(names) - set(by_name) + assert not missing, f"source no longer defines closure functions: {sorted(missing)}" + return [copy.deepcopy(by_name[name]) for name in names] + + +def _exec_functions(source, names, namespace): + module = ast.Module(body=_top_level_functions(source, *names), type_ignores=[]) + ast.fix_missing_locations(module) + exec(compile(module, "", "exec"), namespace) + return namespace + + +def _normalised_function_ast(source, name): + function = _top_level_functions(source, name)[0] + return ast.dump(function, include_attributes=False) + + +def _rebase_base_source(relative_path): + result = subprocess.run( + ["git", "show", f"{_REBASE_BASE_COMMIT}:{relative_path}"], + cwd=_repo_root(), + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + assert result.returncode == 0, result.stderr + return result.stdout.lstrip("\ufeff") + + +def _load_compiler_closure(source): + # The selected functions only need these imported names. Keeping a tiny + # namespace lets the test execute the exact source closure without loading + # the installed compiler package or an NPU toolchain. + subprocess_proxy = SimpleNamespace(CalledProcessError=subprocess.CalledProcessError, ) + namespace = { + "os": os, + "tempfile": tempfile, + "Path": Path, + "subprocess": subprocess_proxy, + "PROGRAM_GRID_TRANSFORMS_ATTR": "hacc.program_grid_transforms", + "ProgramGridContractError": RuntimeError, + "normalize_program_grid_transforms": lambda transforms: transforms, + "get_persistent_transform": lambda _transforms: None, + } + functions = ["_get_then_remove_rc", "_export_coalesce_metadata", "ttir_to_npubin"] + if "def _export_program_grid_metadata" in source: + functions[1:1] = [ + "_get_then_remove_program_grid_transforms", + "_export_program_grid_metadata", + "_finalize_program_launch_policy", + ] + _exec_functions(source, functions, namespace) + return namespace + + +def test_895_compiler_closure_ast_is_identical_outside_row_migration(source_pairs): + baseline_source, target_source = source_pairs["compiler"] + rebase_source = _rebase_base_source(_SOURCE_PATHS["compiler"]) + for name in ( + "_get_then_remove_rc", + "get_common_bishengir_compile_options", + ): + baseline = _normalised_function_ast(rebase_source, name) + target = _normalised_function_ast(target_source, name) + assert target == baseline, name + + baseline_metadata = _normalised_function_ast(baseline_source, "_parse_ttir_metadata") + target_metadata = _normalised_function_ast(target_source, "_parse_ttir_metadata") + assert target_metadata != baseline_metadata + assert "_get_auto_blockify_blacklist_reasons" in target_metadata + assert "attr='get'" in baseline_metadata + assert "attr='get'" not in target_metadata + + +class _FakeIrModule: + + def __init__(self, attrs=None): + self.context = object() + self.attrs = dict(attrs or {}) + self.stringify_calls = 0 + + def __str__(self): + self.stringify_calls += 1 + return "module {}" + + +class _FakePassManager: + + def __init__(self): + self.enable_debug_calls = 0 + self.run_calls = [] + + def enable_debug(self): + self.enable_debug_calls += 1 + + def run(self, _module, *pipeline_name): + self.run_calls.append(pipeline_name) + + +def _row_attrs(row_applied): + if not row_applied: + return {} + return { + "hacc.coalesce_factor": 4, + "hacc.coalesce_axis": 2, + "hacc.coalesce_grid_ceil_div": 1, + } + + +def _make_opt( + *, + superblock_factor, +): + return SimpleNamespace( + is_pure_simt=True, + num_warps=4, + warp_size=32, + simt_optimization_mode=1000017, + shared_mem_dynamic_size=4096, + disable_fma=True, + superblock_factor=superblock_factor, + compile_on_910_95=False, + ) + + +def _run_ttir_to_npubin( + closure, + *, + env_enabled, + blacklisted, + row_applied, + superblock_factor, +): + """Run the historical pure-SIMT tail with a fake pass manager/compiler.""" + pass_manager = _FakePassManager() + commands = [] + metadata_after_parse = [] + + def get_int_attr(module, name): + return module.attrs.get(name) + + def remove_attr(module, name): + module.attrs.pop(name, None) + + def parse_ttir_metadata(_ttir, metadata): + parsed = dict(metadata) + parsed.update({ + "has_auto_blockify_blacklist_op": blacklisted, + "mix_mode": "aiv", + }) + # Both the 895 baseline and the compatibility-restored target read + # this option. Keep it neutral for the argv differential below. + if "bisheng_options" in closure["ttir_to_npubin"].__code__.co_consts: + parsed["bisheng_options"] = None + metadata_after_parse.append(parsed) + return parsed + + def run_bisheng(command, **_kwargs): + commands.append(list(command)) + bin_file = Path(command[command.index("-o") + 1]) + bin_file.with_name(f"{bin_file.name}.o").write_bytes(b"npubin") + return SimpleNamespace(returncode=0, stdout=b"", stderr=b"") + + closure["ir"] = SimpleNamespace(pass_manager=lambda _context: pass_manager) + closure["ascend"] = SimpleNamespace( + ir=SimpleNamespace( + get_int_attr=get_int_attr, + get_program_grid_transforms=lambda _module: None, + remove_attr=remove_attr, + ), + passes=SimpleNamespace(ttir=SimpleNamespace(add_row_coalescing=lambda _pm: None), ), + ) + closure["_parse_ttir_metadata"] = parse_ttir_metadata + closure["get_common_bishengir_compile_options"] = lambda _metadata: [ + "--common-before-pure-simt", + "--common-after-pure-simt", + ] + closure["_get_npucompiler_path"] = lambda: ("bishengir-compile", {}) + closure["_is_auto_map_parallel_blocks_enabled"] = lambda: env_enabled + closure["get_simt_stack_limit"] = lambda: 64 + closure["subprocess"].run = run_bisheng + + result = closure["ttir_to_npubin"]( + _FakeIrModule(_row_attrs(row_applied)), + {}, + _make_opt(superblock_factor=superblock_factor, ), + ) + assert result == b"npubin" + assert len(commands) == 1 + assert len(metadata_after_parse) == 1 + return pass_manager, commands[0], metadata_after_parse[0] + + +def _normalise_command(command): + """Remove only temporary-directory entropy from an argv comparison.""" + return [ + command[0], + Path(command[1]).name, + *command[2:-2], + command[-2], + Path(command[-1]).name, + ] + + +def _export_coalesce_metadata(closure, attrs): + removed = [] + module = _FakeIrModule(attrs) + + def get_int_attr(current_module, name): + return current_module.attrs.get(name) + + def remove_attr(current_module, name): + removed.append(name) + current_module.attrs.pop(name, None) + + closure["ascend"] = SimpleNamespace(ir=SimpleNamespace(get_int_attr=get_int_attr, remove_attr=remove_attr), ) + metadata = {} + closure["_export_coalesce_metadata"](module, metadata) + return metadata, module.attrs, removed + + +def test_895_pure_simt_bisheng_argv_matrix_after_row_make_ttir_migration(source_pairs): + """The fixed-policy pure-SIMT argv cases retain no user block switch.""" + _baseline_source, target_source = source_pairs["compiler"] + common_prefix = [ + "--common-before-pure-simt", + "--common-after-pure-simt", + "--enable-hivm-compile=false", + "--enable-triton-ir-compile", + "--pure-simt", + "--num-warps=4", + "--threads-per-warp=32", + "--simt-optimization-mode=1000017", + "--simt-stack-limit=64", + "--shared-mem-dynamic-size=4096", + "--disable-fma", + ] + cases = itertools.product((False, True), # E: TRITON_ALL_BLOCKS_PARALLEL + (False, True), # B: blacklist result + (False, True), # R: Row pass result + (0, 7), # superblock factor + ) + + count = 0 + for env_enabled, blacklisted, row_applied, superblock in cases: + target_closure = _load_compiler_closure(target_source) + target_pm, target_command, _target_metadata = _run_ttir_to_npubin( + target_closure, + env_enabled=env_enabled, + blacklisted=blacklisted, + row_applied=row_applied, + superblock_factor=superblock, + ) + case = f"E={env_enabled}, B={blacklisted}, R={row_applied}, superblock={superblock}" + + expected_options = list(common_prefix) + auto_blockify = env_enabled and not row_applied + if auto_blockify: + expected_options.append("--enable-auto-blockify-loop") + if superblock > 0: + expected_options.append(f"--super-block-factor={superblock}") + assert _normalise_command(target_command) == [ + "bishengir-compile", + "kernel.ttir.mlir", + *expected_options, + "-o", + "kernel", + ], case + + assert target_pm.run_calls == [], case + count += 1 + + assert count == 16 + + +@pytest.mark.parametrize( + "name,attrs,expected", + ( + ( + "axis", + { + "hacc.coalesce_factor": 2, + "hacc.coalesce_axis": 0, + "hacc.coalesce_grid_ceil_div": 0, + }, + { + "coalesce_factor": 2, + "coalesce_axis": 0, + "coalesce_grid_ceil_div": False, + "row_coalescing_applied": True, + }, + ), + ( + "chunk", + { + "hacc.coalesce_factor": 16, + "hacc.coalesce_axis": 1, + "hacc.coalesce_grid_ceil_div": 0, + }, + { + "coalesce_factor": 16, + "coalesce_axis": 1, + "coalesce_grid_ceil_div": False, + "row_coalescing_applied": True, + }, + ), + ( + "row", + { + "hacc.coalesce_factor": 4, + "hacc.coalesce_axis": 2, + "hacc.coalesce_grid_ceil_div": 1, + }, + { + "coalesce_factor": 4, + "coalesce_axis": 2, + "coalesce_grid_ceil_div": True, + "row_coalescing_applied": True, + }, + ), + ), +) +def test_895_coalesce_attrs_export_identically(name, attrs, expected, source_pairs): + baseline_closure = _load_compiler_closure(source_pairs["compiler"][0]) + target_closure = _load_compiler_closure(source_pairs["compiler"][1]) + baseline = _export_coalesce_metadata(baseline_closure, attrs) + target = _export_coalesce_metadata(target_closure, attrs) + expected_removed = [ + "hacc.coalesce_factor", + "hacc.coalesce_axis", + "hacc.coalesce_grid_ceil_div", + ] + assert baseline == target == (expected, {}, expected_removed), name + + +class _FakeNPUUtils: + npu_utils_mod = SimpleNamespace(__file__="") + + def get_so_path(self): + return "/cache/895-closure/npu_utils.so" + + def get_aivector_core_num(self): + return 40 + + def get_aicore_num(self): + return 20 + + +def _module_string_constant(source, name): + tree = ast.parse(source) + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + if not any(isinstance(target, ast.Name) and target.id == name for target in node.targets): + continue + value = ast.literal_eval(node.value) + if isinstance(value, str): + return value + return "" + + +def _load_make_launcher(source): + state = {"auto_map_enabled": False} + namespace = { + "os": os, + "NPUUtils": _FakeNPUUtils, + "_BASE_ARGS_FORMAT": "iiiKKOOOO", + "_BASE_ARGS_FORMAT_LEN": len("iiiKKOOOO"), + "_CPP_DEVICE_POINTER": _module_string_constant(source, "_CPP_DEVICE_POINTER"), + "_CPP_MSPROF_EXTERN": _module_string_constant(source, "_CPP_MSPROF_EXTERN"), + "_CPP_MSPROF_CALLBACK": _module_string_constant(source, "_CPP_MSPROF_CALLBACK"), + "_CPP_MSPROF_BEFORE_LAUNCH": _module_string_constant(source, "_CPP_MSPROF_BEFORE_LAUNCH"), + "_CPP_ALIGN_LAUNCH_OFFSET": _module_string_constant(source, "_CPP_ALIGN_LAUNCH_OFFSET"), + "_CPP_GET_TENSOR_SHAPE": _module_string_constant(source, "_CPP_GET_TENSOR_SHAPE"), + "_is_auto_map_parallel_blocks_enabled": lambda: state["auto_map_enabled"], + "force_disable_ffts": lambda *_args: False, + "is_ffts_supported": lambda _arch: True, + "get_ascend_arch_from_env": lambda: "Ascend910B", + "get_backend_func": lambda name, *_args: f"/* {name} */", + "convert_sigtype_to_int": lambda _ty: 0, + "extract_device_print_code_from_cann": lambda: "", + } + _exec_functions(source, ("generate_npu_header_src", "ty_to_cpp", "make_launcher"), namespace) + return namespace["make_launcher"], state + + +def _make_metadata(*, factor, axis, ceil_div, blacklisted, row_applied, is_pure_simt=False): + return SimpleNamespace( + target=SimpleNamespace(arch="Ascend910B"), + workspace_size=0, + lock_init_value=0, + sync_block_lock_layout=0, + bs_task_type=0, + mix_mode="aiv", + shared=0, + compile_on_910_95=False, + parallel_mode="", + # The baseline closure still reads this retired field; the target + # closure reads is_pure_simt. Keep both in this historical test mock. + force_simt_only=is_pure_simt, + is_pure_simt=is_pure_simt, + debug=False, + shared_mem_dynamic_size=221184, + coalesce_factor=factor, + coalesce_axis=axis, + coalesce_grid_ceil_div=ceil_div, + has_auto_blockify_blacklist_op=blacklisted, + row_coalescing_applied=row_applied, + program_grid_transforms=None, + program_grid_mapping_applied=False, + auto_blockify_enabled=False, + ptsm_cap_authorized=False, + ) + + +def _launcher_paths(source): + return source.split("static void _launch(", maxsplit=1) + + +def _coalescing_fragment(path): + start = path.index("// coalescing: each program covers") + end = path.index("uint32_t blockNum4Workspace", start) + return path[start:end] + + +@pytest.mark.parametrize( + "name,factor,axis,ceil_div,assignment,guard", + ( + ( + "axis", + 2, + 0, + False, + "gridX = gridX / 2;", + "ChunkCoalescing: grid[0] not divisible by coalesce_factor 2", + ), + ( + "chunk", + 16, + 1, + False, + "gridY = gridY / 16;", + "ChunkCoalescing: grid[1] not divisible by coalesce_factor 16", + ), + ( + "row", + 4, + 2, + True, + "gridZ = (gridZ + 4 - 1) / 4;", + None, + ), + ), +) +def test_895_launcher_coalescing_and_block_cap_closure( + name, + factor, + axis, + ceil_div, + assignment, + guard, + source_pairs, +): + baseline_make_launcher, baseline_state = _load_make_launcher(source_pairs["driver"][0]) + target_make_launcher, target_state = _load_make_launcher(source_pairs["driver"][1]) + cap = "blockNum = std::min(blockNum, (uint32_t)40);" + + for env_enabled, is_pure_simt, blacklisted in itertools.product( + (False, True), + (False, True), + (False, True), + ): + row_applied = True + baseline_state["auto_map_enabled"] = env_enabled + target_state["auto_map_enabled"] = env_enabled + baseline_src = baseline_make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=_make_metadata( + factor=factor, + axis=axis, + ceil_div=ceil_div, + blacklisted=blacklisted, + row_applied=row_applied, + is_pure_simt=is_pure_simt, + ), + ) + target_src = target_make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=_make_metadata( + factor=factor, + axis=axis, + ceil_div=ceil_div, + blacklisted=blacklisted, + row_applied=row_applied, + is_pure_simt=is_pure_simt, + ), + ) + case = f"{name}: E={env_enabled}, P={is_pure_simt}, B={blacklisted}, R={row_applied}" + baseline_paths = _launcher_paths(baseline_src) + target_paths = _launcher_paths(target_src) + assert len(baseline_paths) == len(target_paths) == 2, case + expected_baseline_cap_count = 1 if env_enabled and not blacklisted else 0 + expected_target_cap_count = 1 if env_enabled and (is_pure_simt or not blacklisted) else 0 + for baseline_path, target_path in zip(baseline_paths, target_paths): + assert _coalescing_fragment(baseline_path) == _coalescing_fragment(target_path), case + assert baseline_path.count(assignment) == target_path.count(assignment) == 1, case + if guard is None: + assert "ChunkCoalescing: grid[2] not divisible" not in baseline_path, case + assert "ChunkCoalescing: grid[2] not divisible" not in target_path, case + else: + assert baseline_path.count(guard) == target_path.count(guard) == 1, case + assert baseline_path.count(cap) == expected_baseline_cap_count, case + assert target_path.count(cap) == expected_target_cap_count, case + + +def test_895_launcher_all_emittable_coalescing_metadata_cases(source_pairs): + baseline_make_launcher, baseline_state = _load_make_launcher(source_pairs["driver"][0]) + target_make_launcher, target_state = _load_make_launcher(source_pairs["driver"][1]) + grid_names = ("gridX", "gridY", "gridZ") + cap = "blockNum = std::min(blockNum, (uint32_t)40);" + families = ( + ("axis", (2, 3, 4, 8, 16), False), + ("chunk", (2, 4, 8, 16), False), + ("row", (2, 4, 8), True), + ) + + for family, factors, ceil_div in families: + for factor, axis, env_enabled, is_pure_simt, blacklisted in itertools.product( + factors, + (0, 1, 2), + (False, True), + (False, True), + (False, True), + ): + row_applied = True + baseline_state["auto_map_enabled"] = env_enabled + target_state["auto_map_enabled"] = env_enabled + metadata = _make_metadata( + factor=factor, + axis=axis, + ceil_div=ceil_div, + blacklisted=blacklisted, + row_applied=row_applied, + is_pure_simt=is_pure_simt, + ) + baseline_src = baseline_make_launcher(constants={}, signature={0: "*fp32", 1: "*fp32"}, metadata=metadata) + target_src = target_make_launcher(constants={}, signature={0: "*fp32", 1: "*fp32"}, metadata=metadata) + grid = grid_names[axis] + case = (f"{family}: H={factor}, axis={axis}, ceil={ceil_div}, " + f"E={env_enabled}, P={is_pure_simt}, B={blacklisted}, R={row_applied}") + expected_assignment = (f"{grid} = ({grid} + {factor} - 1) / {factor};" + if ceil_div else f"{grid} = {grid} / {factor};") + expected_baseline_cap_count = 1 if env_enabled and not blacklisted else 0 + expected_target_cap_count = 1 if env_enabled and (is_pure_simt or not blacklisted) else 0 + for baseline_path, target_path in zip(_launcher_paths(baseline_src), _launcher_paths(target_src)): + assert _coalescing_fragment(baseline_path) == _coalescing_fragment(target_path), case + assert baseline_path.count(expected_assignment) == 1, case + assert target_path.count(expected_assignment) == 1, case + if ceil_div: + assert f"grid[{axis}] not divisible by coalesce_factor" not in baseline_path, case + assert f"grid[{axis}] not divisible by coalesce_factor" not in target_path, case + else: + guard = (f"ChunkCoalescing: grid[{axis}] not divisible by " + f"coalesce_factor {factor}") + assert baseline_path.count(guard) == 1, case + assert target_path.count(guard) == 1, case + assert baseline_path.count(cap) == expected_baseline_cap_count, case + assert target_path.count(cap) == expected_target_cap_count, case + + +def test_895_launcher_keeps_mixed_simt_sls_marker_in_both_paths(source_pairs): + """SLS still selects the original 910_95 mixed-SIMT launch ABI.""" + baseline_make_launcher, _baseline_state = _load_make_launcher(source_pairs["driver"][0]) + target_make_launcher, _target_state = _load_make_launcher(source_pairs["driver"][1]) + + metadata = _make_metadata( + factor=1, + axis=-1, + ceil_div=False, + blacklisted=False, + row_applied=False, + ) + metadata.compile_on_910_95 = True + metadata.parallel_mode = "mix_simd_simt" + baseline_src = baseline_make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + target_src = target_make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ) + + baseline_paths = _launcher_paths(baseline_src) + target_paths = _launcher_paths(target_src) + + for baseline_path, target_path in zip(baseline_paths, target_paths): + # Baseline keeps the inlined RT launch ABI in each launch path. + assert baseline_path.count("rtKernelLaunchWithFlagV2") == 1 + assert baseline_path.count("rtArgsEx_t argsInfo") == 1 + assert "cfgInfo.localMemorySize = 221184;" in baseline_path + # Target routes both paths through the shared shim entry points; the + # 221184 literal is carried at the cfg acquisition call site. + assert target_path.count("cann_get_launch_kernel_cfg(221184)") == 1 + assert "cann_launch_kernel(func, blockNum" in target_path + + # The shim lives once in the header (first path) and carries both the + # ACL (9.1.0+) and RT (<9.1.0) launch implementations. + assert target_src.count("aclrtLaunchKernelWithHostArgs") == 1 + assert target_src.count("rtKernelLaunchWithFlagV2") == 1 + assert target_src.count("aclrtLaunchKernelAttr attrInfo") == 1 + assert target_src.count("rtArgsEx_t argsInfo") == 1 + + +def _load_inject_grid_num_tiles(source): + tree = ast.parse(source) + candidates = [ + node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name == "_inject_grid_num_tiles" + ] + assert len(candidates) == 1 + function = copy.deepcopy(candidates[0]) + # Execute the method as a stand-alone function. Supply a local equivalent + # of the backend's int marker so this verifies the source behavior rather + # than importing whichever Triton wheel happens to be installed. + function.decorator_list = [] + module = ast.Module(body=[function], type_ignores=[]) + ast.fix_missing_locations(module) + internal_npu_option_int = type("_InternalNPUOptionInt", (int, ), {}) + namespace = {"_InternalNPUOptionInt": internal_npu_option_int} + exec(compile(module, "", "exec"), namespace) + return namespace["_inject_grid_num_tiles"] + + +def test_895_grid_num_tiles_ast_closure_differential(source_pairs): + """Static, callable, and explicit-grid paths remain byte-for-byte semantic peers.""" + baseline_inject = _load_inject_grid_num_tiles(source_pairs["autotuner"][0]) + target_inject = _load_inject_grid_num_tiles(source_pairs["autotuner"][1]) + dynamic_grid = lambda _meta: (2, 16) + cases = ( + ("static", {"grid": (2, 16)}, {"grid": (2, 16), "grid_num_tiles": 16}), + ("callable", {"grid": dynamic_grid}, {"grid": dynamic_grid}), + ( + "explicit", + {"grid": (2, 16), "grid_num_tiles": 99}, + {"grid": (2, 16), "grid_num_tiles": 99}, + ), + ) + + for name, initial, expected in cases: + baseline_kwargs = dict(initial) + target_kwargs = dict(initial) + baseline_inject(baseline_kwargs) + target_inject(target_kwargs) + assert baseline_kwargs == expected, name + assert target_kwargs == expected, name + assert baseline_kwargs == target_kwargs, name + + +def _expected_row_rebase_core(baseline): + old_guard = " Block *pidBlock = seed.pid->getBlock();\n if (!pidBlock || !seed.workBlock)\n" + new_guard = " if (!seed.entryGuard || !seed.workBlock)\n" + old_insertion = (" if (Operation *validDef = seed.validCount.getDefiningOp())\n" + " rw.setInsertionPointAfter(validDef);\n" + " else\n" + " rw.setInsertionPointAfter(seed.pid);\n") + new_insertion = " rw.setInsertionPoint(seed.entryGuard);\n" + assert old_guard in baseline + assert old_insertion in baseline + return baseline.replace(old_guard, new_guard).replace(old_insertion, new_insertion) + + +def test_895_legacy_memory_access_core_sources_are_mechanical_relocations(source_pairs): + del source_pairs # Fixture makes a missing 895 object fail in strict mode. + root = _repo_root() + sources = { + "axis": "third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/StridedAxisCoalescing.cpp", + "chunk": "third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/ChunkCoalescing.cpp", + "sls": "third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/StridedLoadStoreRewrite.cpp", + "row": "third_party/ascend/lib/TritonToGraph/LegacyMemoryAccess/RowCoalescing.cpp", + } + for name, target_path in sources.items(): + baseline = _rebase_base_source(target_path) + target = _source_text(root / target_path) + expected = _expected_row_rebase_core(baseline) if name == "row" else baseline + assert target == expected, name diff --git a/third_party/ascend/unittest/pytest_ut/test_layout_memory_91095_native_e2e.py b/third_party/ascend/unittest/pytest_ut/test_layout_memory_91095_native_e2e.py new file mode 100644 index 0000000000..959a75c87e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_layout_memory_91095_native_e2e.py @@ -0,0 +1,415 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +"""Native 910_95 observability regressions for layout/memory compatibility. + +These are deliberately *not* 910B4 smoke tests. The four migrated legacy +optimizations keep their original compile-on-910_95 / pure-SIMT scheduling +slots, so value-only tests on a B4 cannot prove that the gate-on path ran. + +The observer below wraps only test-process call sites. It records the module +immediately before compiler metadata export and the launcher source generated +for the real JIT launch. Production compiler and launcher behavior is left +unchanged. Every test then launches on real hardware and checks both numerical +results and the pass-to-metadata-to-launcher handoff. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +import pytest + +try: + import triton + from triton.backends.ascend.utils import is_compile_on_910_95 + _target_arch = triton.runtime.driver.active.get_current_target().arch +except Exception: + _target_arch = "" + + def is_compile_on_910_95(_arch): + return False + + +# Do this before importing torch_npu, Triton, or defining JIT kernels. In +# particular, an Ascend 910B4 must collect this module as skipped rather than +# execute a value-only fallback and be mistaken for a 910_95 gate-on result. +if not is_compile_on_910_95(_target_arch): + pytest.skip( + "requires a detected Ascend 950 toolchain; 910B4 is not a " + "native gate-on substitute", + allow_module_level=True, + ) + +torch = pytest.importorskip("torch") +pytest.importorskip("torch_npu") + +import triton.language as tl +from triton.backends.ascend import compiler as ascend_compiler +from triton.backends.ascend import driver as ascend_driver + +pytestmark = pytest.mark.backend("torch_npu") + + +@dataclass +class _NativePipelineObserver: + """Capture only the native JIT handoff points needed by these regressions.""" + + pre_export_ir: list[str] = field(default_factory=list) + metadata_after_export: list[dict[str, Any]] = field(default_factory=list) + launcher_sources: list[str] = field(default_factory=list) + + @classmethod + def install(cls, monkeypatch: pytest.MonkeyPatch) -> "_NativePipelineObserver": + observer = cls() + export_metadata = ascend_compiler._export_coalesce_metadata + make_launcher = ascend_driver.make_launcher + + def observe_export(module, metadata, **kwargs): + # This is after Row or T2L's Axis/Chunk/SLS sequence but before + # hacc.coalesce_* is intentionally removed for the vendor compiler. + observer.pre_export_ir.append(str(module)) + result = export_metadata(module, metadata, **kwargs) + observer.metadata_after_export.append(dict(metadata)) + return result + + def observe_launcher(*args, **kwargs): + source = make_launcher(*args, **kwargs) + observer.launcher_sources.append(source) + return source + + monkeypatch.setattr(ascend_compiler, "_export_coalesce_metadata", observe_export) + monkeypatch.setattr(ascend_driver, "make_launcher", observe_launcher) + return observer + + def exported_ir_with(self, needle: str) -> str: + for ir_text in reversed(self.pre_export_ir): + if needle in ir_text: + return ir_text + raise AssertionError(f"did not observe {needle!r} before metadata export; captured " + f"{len(self.pre_export_ir)} module(s)") + + def launcher_with(self, needle: str) -> str: + for source in reversed(self.launcher_sources): + if needle in source: + return source + raise AssertionError(f"did not observe launcher fragment {needle!r}; generated " + f"{len(self.launcher_sources)} launcher(s)") + + +def _launch_with_observer(monkeypatch, kernel, grid, *args, **compile_options): + """Force one native compile + launch and retain the associated observables.""" + # ``force_simt_template`` is an existing T2L precondition and may be set by + # a test below. Never accept a compile_on_910_95 override here: that bit + # must continue to come exclusively from real device/toolchain detection. + assert "compile_on_910_95" not in compile_options + # JITFunction owns an in-process cache in addition to Triton's file cache. + # Clearing only this test kernel prevents a prior invocation from bypassing + # the observer, while TRITON_ALWAYS_COMPILE forces the compiler pipeline. + monkeypatch.setenv("TRITON_ALWAYS_COMPILE", "1") + kernel.device_caches.clear() + observer = _NativePipelineObserver.install(monkeypatch) + compiled = kernel[grid](*args, **compile_options) + torch.npu.synchronize() + assert observer.pre_export_ir, "the native compilation never exported metadata" + assert observer.launcher_sources, "the native launch never generated a launcher" + return compiled, observer + + +def _assert_real_91095_gate(compiled, *, pure_simt: bool) -> None: + """Do not let a manual mode override masquerade as device gate-on.""" + assert compiled.metadata.compile_on_910_95 is True + assert compiled.metadata.is_pure_simt is pure_simt + if not pure_simt: + assert compiled.metadata.compile_mode == "simd_simt_template" + + +@triton.jit +def _row_tail_copy(src, dst, n, BLOCK: tl.constexpr): + pid = tl.program_id(0) + if pid >= n: + return + offsets = pid + tl.arange(0, BLOCK) + mask = offsets < n + value = tl.load(src + offsets, mask=mask, other=0.0) + tl.store(dst + offsets, value, mask=mask) + + +@triton.jit +def _chunk_axis1_copy(src, dst, N: tl.constexpr, BLOCK: tl.constexpr): + batch = tl.program_id(0) + tile = tl.program_id(1) + offsets = tile * BLOCK + tl.arange(0, BLOCK) + mask = offsets < N + base = batch * N + value = tl.load(src + base + offsets, mask=mask, other=0.0) + tl.store(dst + base + offsets, value, mask=mask) + + +@triton.jit +def _chunk_axis1_extra_data_predicate(src, dst, N: tl.constexpr, BLOCK: tl.constexpr): + batch = tl.program_id(0) + tile = tl.program_id(1) + offsets = tile * BLOCK + tl.arange(0, BLOCK) + mask = offsets < N + base = batch * N + value = tl.load(src + base + offsets, mask=mask, other=0.0) + # A second pid-dependent compare that selects data instead of only feeding + # an assert. N - 1 is not a whole number of tiles, so it also cannot be + # mistaken for the seed mask. + value = tl.where(offsets < N - 1, value, 0.0) + tl.store(dst + base + offsets, value, mask=mask) + + +@triton.jit +def _chunk_axis2_copy(src, dst, N: tl.constexpr, BLOCK: tl.constexpr): + # Chunk chooses its seed from the greatest program-id axis. Keeping axes + # 0 and 1 at one makes this a real grid-Z case, rather than merely testing + # the driver's metadata-to-string branch in isolation. + tile = tl.program_id(2) + offsets = tile * BLOCK + tl.arange(0, BLOCK) + mask = offsets < N + value = tl.load(src + offsets, mask=mask, other=0.0) + tl.store(dst + offsets, value, mask=mask) + + +@triton.jit +def _sls_masked_stride4_gather(src, dst, N: tl.constexpr, BLOCK: tl.constexpr): + pid = tl.program_id(0) + offsets = pid * BLOCK + tl.arange(0, BLOCK) + mask = offsets < N + # Static power-of-two stride plus the one-tile mask is specifically the + # SLS indirect-load route; without the mask this stays on strided DMA. + value = tl.load(src + offsets * 4, mask=mask, other=0.0) + tl.store(dst + offsets, value, mask=mask) + + +def test_row_91095_native_metadata_launcher_and_ir(monkeypatch): + """Row remains pure-SIMT and carries ceil-div grid metadata to launch.""" + n = 19 # H=8 leaves a tail, so ceil-div is observable at the launcher. + src = torch.arange(n, dtype=torch.float32).npu() + dst = torch.full_like(src, -1) + + compiled, observer = _launch_with_observer( + monkeypatch, + _row_tail_copy, + (n, ), + src, + dst, + n, + BLOCK=16, + compile_mode="simt_only", + ) + + assert torch.equal(dst.cpu(), src.cpu()) + _assert_real_91095_gate(compiled, pure_simt=True) + assert compiled.metadata.coalesce_factor == 8 + assert compiled.metadata.coalesce_axis == 0 + assert compiled.metadata.coalesce_grid_ceil_div is True + assert compiled.metadata.row_coalescing_applied is True + + row_ir = observer.exported_ir_with("hacc.coalesce_factor = 8 : i32") + assert "hacc.coalesce_axis = 0 : i32" in row_ir + assert "hacc.coalesce_grid_ceil_div = 1 : i32" in row_ir + + launcher = observer.launcher_with("gridX = (gridX + 8 - 1) / 8;") + assert launcher.count("gridX = (gridX + 8 - 1) / 8;") == 2 + assert "ChunkCoalescing: grid[0] not divisible" not in launcher + + +def test_chunk_91095_native_metadata_launcher_and_ir(monkeypatch): + """Chunk's factor/axis metadata is consumed by exact grid division.""" + batch, block, num_tiles = 2, 16, 32 + n = block * num_tiles + src = torch.arange(batch * n, dtype=torch.float32).reshape(batch, n).npu() + dst = torch.empty_like(src) + + compiled, observer = _launch_with_observer( + monkeypatch, + _chunk_axis1_copy, + (batch, num_tiles), + src, + dst, + N=n, + BLOCK=block, + # This selects the legacy T2L template precondition only. It does + # not set compile_on_910_95; _assert_real_91095_gate below proves the + # device-derived gate remained true for the actual compiled kernel. + force_simt_template=True, + # Keep covering the uninstrumented form; the default instrumented one + # is covered by test_chunk_coalesces_through_overflow_sanitizer_91095. + sanitize_overflow=False, + ) + + assert torch.equal(dst.cpu(), src.cpu()) + _assert_real_91095_gate(compiled, pure_simt=False) + assert compiled.metadata.coalesce_factor == 16 + assert compiled.metadata.coalesce_axis == 1 + assert compiled.metadata.coalesce_grid_ceil_div is False + assert compiled.metadata.row_coalescing_applied is True + + chunk_ir = observer.exported_ir_with("hacc.coalesce_factor = 16 : i32") + assert "hacc.coalesce_axis = 1 : i32" in chunk_ir + assert "hacc.coalesce_grid_ceil_div" not in chunk_ir + + launcher = observer.launcher_with("gridY = gridY / 16;") + assert launcher.count("gridY = gridY / 16;") == 2 + assert launcher.count("ChunkCoalescing: grid[1] not divisible by coalesce_factor 16") == 2 + + +def test_chunk_axis2_91095_native_metadata_launcher_and_ir(monkeypatch): + """Chunk's real max-axis seed shrinks grid-Z with the floor-div ABI.""" + block, num_tiles = 16, 32 + n = block * num_tiles + src = torch.arange(n, dtype=torch.float32).npu() + dst = torch.empty_like(src) + + compiled, observer = _launch_with_observer( + monkeypatch, + _chunk_axis2_copy, + (1, 1, num_tiles), + src, + dst, + N=n, + BLOCK=block, + # Preserve the original T2L template prerequisite only; the hardware + # detection assertion below proves this is not a forged 910_95 result. + force_simt_template=True, + sanitize_overflow=False, + ) + + assert torch.equal(dst.cpu(), src.cpu()) + _assert_real_91095_gate(compiled, pure_simt=False) + assert compiled.metadata.coalesce_factor == 16 + assert compiled.metadata.coalesce_axis == 2 + assert compiled.metadata.coalesce_grid_ceil_div is False + assert compiled.metadata.row_coalescing_applied is True + + chunk_ir = observer.exported_ir_with("hacc.coalesce_factor = 16 : i32") + assert "hacc.coalesce_axis = 2 : i32" in chunk_ir + assert "hacc.coalesce_grid_ceil_div" not in chunk_ir + + launcher = observer.launcher_with("gridZ = gridZ / 16;") + # make_launcher emits both the stable ABI and local C++ packing paths. + assert launcher.count("gridZ = gridZ / 16;") == 2 + assert launcher.count("ChunkCoalescing: grid[2] not divisible by coalesce_factor 16") == 2 + + +def test_chunk_coalesces_through_overflow_sanitizer_91095(monkeypatch): + """The default overflow instrumentation no longer suppresses Chunk.""" + batch, block, num_tiles = 2, 16, 32 + n = block * num_tiles + src = torch.arange(batch * n, dtype=torch.float32).reshape(batch, n).npu() + dst = torch.empty_like(src) + + compiled, observer = _launch_with_observer( + monkeypatch, + _chunk_axis1_copy, + (batch, num_tiles), + src, + dst, + N=n, + BLOCK=block, + force_simt_template=True, + # Do not pass sanitize_overflow: the JIT default instruments every + # pid-derived index with compares that can only trap. + ) + + assert torch.equal(dst.cpu(), src.cpu()) + _assert_real_91095_gate(compiled, pure_simt=False) + assert compiled.metadata.sanitize_overflow is True + assert compiled.metadata.coalesce_factor == 16 + assert compiled.metadata.coalesce_axis == 1 + assert compiled.metadata.coalesce_grid_ceil_div is False + assert compiled.metadata.row_coalescing_applied is True + + chunk_ir = observer.exported_ir_with("hacc.coalesce_factor = 16 : i32") + assert "hacc.coalesce_axis = 1 : i32" in chunk_ir + assert "hacc.coalesce_grid_ceil_div" not in chunk_ir + + launcher = observer.launcher_with("gridY = gridY / 16;") + assert launcher.count("gridY = gridY / 16;") == 2 + assert launcher.count("ChunkCoalescing: grid[1] not divisible by coalesce_factor 16") == 2 + + +def test_chunk_rejects_data_reaching_pid_predicate_91095(monkeypatch): + """Only trap-only compares are waived; one that selects data must bail.""" + batch, block, num_tiles = 2, 16, 32 + n = block * num_tiles + src = torch.arange(batch * n, dtype=torch.float32).reshape(batch, n).npu() + dst = torch.empty_like(src) + + compiled, observer = _launch_with_observer( + monkeypatch, + _chunk_axis1_extra_data_predicate, + (batch, num_tiles), + src, + dst, + N=n, + BLOCK=block, + force_simt_template=True, + ) + + expected = src.clone() + expected[:, n - 1] = 0.0 + assert torch.equal(dst.cpu(), expected.cpu()) + _assert_real_91095_gate(compiled, pure_simt=False) + assert compiled.metadata.sanitize_overflow is True + assert compiled.metadata.coalesce_factor == 1 + assert compiled.metadata.coalesce_axis == -1 + assert compiled.metadata.coalesce_grid_ceil_div is False + assert compiled.metadata.row_coalescing_applied is False + assert all("hacc.coalesce_factor" not in ir_text for ir_text in observer.pre_export_ir) + assert all("gridY = gridY / 16;" not in launcher for launcher in observer.launcher_sources) + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_sls_91095_native_ir_metadata_and_mixed_simt_launcher(monkeypatch): + """SLS emits the masked indirect-load path and preserves its launch ABI.""" + n = 1024 + src = torch.arange(n * 4, dtype=torch.float16).npu() + dst = torch.empty(n, dtype=torch.float16).npu() + + compiled, observer = _launch_with_observer( + monkeypatch, + _sls_masked_stride4_gather, + (1, ), + src, + dst, + N=n, + BLOCK=n, + # Same as Chunk: retain the original template condition but never + # override the device-derived compile_on_910_95 gate. + force_simt_template=True, + ) + + assert torch.equal(dst.cpu(), src.cpu()[::4]) + _assert_real_91095_gate(compiled, pure_simt=False) + assert compiled.metadata.parallel_mode == "mix_simd_simt" + # This is the historical non-pure-SIMT branch in NPUOptions.__post_init__: + # 895 and the migrated source both set it to 221184. Read the real JIT + # metadata instead of supplying a test-local launcher metadata object. + assert compiled.metadata.shared_mem_dynamic_size == 221184 + assert compiled.metadata.coalesce_factor == 1 + assert compiled.metadata.coalesce_axis == -1 + assert compiled.metadata.row_coalescing_applied is False + + sls_ir = observer.exported_ir_with("triton_indirect_load") + assert "parallel_mode = \"mix_simd_simt\"" in sls_ir + + launcher = observer.launcher_with("rtKernelLaunchWithFlagV2") + # cfg setup is shared via the cann shim (once); both launch paths call it. + assert launcher.count("rtKernelLaunchWithFlagV2") == 1 + assert launcher.count("rtArgsEx_t argsInfo") == 1 + # The same historical value must reach cfgInfo via both generated launcher + # paths; the target is not allowed to silently pick a different ABI value. + assert launcher.count("cann_get_launch_kernel_cfg(221184)") == 2 diff --git a/third_party/ascend/unittest/pytest_ut/test_ldst.py b/third_party/ascend/unittest/pytest_ut/test_ldst.py new file mode 100644 index 0000000000..c5b989fa70 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ldst.py @@ -0,0 +1,449 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch, torch_npu +import triton +import triton.language as tl +import triton.language.math as tl_math +import pytest + + +def test_ldst_indirect_00(): + + @triton.jit + def triton_ldst_indirect_00_kernel(out_ptr0, in_ptr0, in_ptr1, OFFSET0: tl.constexpr, XS: tl.constexpr): + pid = tl.program_id(0) + offset1 = tl.load(in_ptr0 + OFFSET0) + idx_in1 = offset1 + pid * XS + tl.arange(0, XS) + tmp0 = tl.load(in_ptr1 + idx_in1, cache_modifier=".ca", eviction_policy="evict_first", volatile=True) + tmp1 = tl_math.exp(tmp0) + idx_out0 = pid * XS + tl.arange(0, XS) + tl.store(out_ptr0 + idx_out0, tmp1) + + def triton_ldst_indirect_00_func(x0, x1, s, xs): + n = x1.numel() + ns = n - s + assert ns == xs, "test only single core" + y0 = torch.empty((ns, ), dtype=x1.dtype, device=x1.device) + triton_ldst_indirect_00_kernel[ns // xs, 1, 1](y0, x0, x1, OFFSET0=s, XS=xs) + return y0 + + def torch_ldst_indirect_00_func(x0, x1, s): + offset = x0[s] + return torch.exp(x1[offset:]) + + DEV = "npu" + DTYPE = torch.float32 + offset = 0 + N0, N1 = 16, 16 + blocksize = 16 + assert N0 > offset, "offset must be < N0" + N1 = N1 + offset + x0 = torch.arange(0, N0, dtype=torch.int32, device=DEV) + x1 = torch.randn((N1, ), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_00_func(x0, x1, offset) + triton_cal = triton_ldst_indirect_00_func(x0, x1, offset, blocksize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_01(): + + @triton.jit + def triton_ldst_indirect_01_kernel(out_ptr0, in_ptr0, in_ptr1, OFFSET0: tl.constexpr, XS: tl.constexpr): + pid = tl.program_id(0) + offset1 = tl.load(in_ptr0 + OFFSET0) + idx_in1 = offset1 + pid * XS + tl.arange(0, XS) + tmp0 = tl.load(in_ptr1 + idx_in1) + tmp1 = tl_math.exp(tmp0) + idx_out0 = pid * XS + tl.arange(0, XS) + tl.store(out_ptr0 + idx_out0, tmp1) + + def triton_ldst_indirect_01_func(x0, x1, s, xs): + n = x1.numel() + ns = n - s + assert ns == xs, "test only single core" + y0 = torch.empty((ns, ), dtype=x1.dtype, device=x1.device) + triton_ldst_indirect_01_kernel[ns // xs, 1, 1](y0, x0, x1, OFFSET0=s, XS=xs) + return y0 + + def torch_ldst_indirect_01_func(x0, x1, s): + offset = x0[s] + return torch.exp(x1[offset:]) + + DEV = "npu" + DTYPE = torch.float32 + offset = 0 + N0, N1 = 16, 16 + blocksize = 16 + assert N0 > offset, "offset must be < N0" + N1 = N1 + offset + x0 = torch.arange(0, N0, device=DEV) # int64 + x1 = torch.randn((N1, ), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_01_func(x0, x1, offset) + triton_cal = triton_ldst_indirect_01_func(x0, x1, offset, blocksize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_02(): + + @triton.jit + def triton_ldst_indirect_02_kernel(out_ptr0, in_ptr0, in_ptr1, XS: tl.constexpr): + pid = tl.program_id(0) + for i in tl.range(0, XS): + tmp0 = tl.load(in_ptr0 + i) + tmp1 = tl.load(in_ptr1 + tmp0) + tmp2 = tl_math.exp(tmp1) + tl.store(out_ptr0 + i, tmp2) + + def triton_ldst_indirect_02_func(x0, x1, xs): + n0 = x0.numel() + assert n0 == xs, "test only single core" + y0 = torch.empty((n0, ), dtype=x1.dtype, device=x1.device) + triton_ldst_indirect_02_kernel[n0 // xs, 1, 1](y0, x0, x1, XS=xs) + return y0 + + def torch_ldst_indirect_02_func(x0, x1): + return torch.exp(x1[x0]) + + DEV = "npu" + DTYPE = torch.float32 + offset = 8 + N0, N1 = 16, 32 + blocksize = 16 + assert N1 >= N0 + offset, "N1 must be >= N0+offset" + assert N0 == blocksize, "N0 must be == blocksize" + x0 = offset + torch.arange(0, N0, device=DEV) # int64 + x1 = torch.randn((N1, ), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_02_func(x0, x1) + triton_cal = triton_ldst_indirect_02_func(x0, x1, blocksize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_03(): + + @triton.jit + def triton_ldst_indirect_03_kernel(out_ptr0, in_ptr0, in_ptr1, XS: tl.constexpr): + pid = tl.program_id(0) + in_idx0 = pid * XS + tl.arange(0, XS) + tmp0 = tl.load(in_ptr0 + in_idx0) + tmp1 = tl.load(in_ptr1 + tmp0) + tmp2 = tl_math.exp(tmp1) + out0_idx = pid * XS + tl.arange(0, XS) + tl.store(out_ptr0 + out0_idx, tmp2) + + def triton_ldst_indirect_03_func(x0, x1, xs): + n0 = x0.numel() + assert n0 == xs, "test only single core" + y0 = torch.empty((n0, ), dtype=x1.dtype, device=x1.device) + triton_ldst_indirect_03_kernel[n0 // xs, 1, 1](y0, x0, x1, XS=xs) + return y0 + + def torch_ldst_indirect_03_func(x0, x1): + return torch.exp(x1[x0]) + + DEV = "npu" + DTYPE = torch.float32 + offset = 8 + N0, N1 = 16, 32 + blocksize = 16 + assert N1 >= N0 + offset, "N1 must be >= N0+offset" + assert N0 == blocksize, "N0 must be == blocksize" + x0 = offset + torch.arange(0, N0, device=DEV) # int64 + x1 = torch.randn((N1, ), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_03_func(x0, x1) + triton_cal = triton_ldst_indirect_03_func(x0, x1, blocksize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_04(): + + @triton.jit + def triton_ldst_indirect_04_kernel(out_ptr0, in_ptr0, in_ptr1, XS: tl.constexpr): + pid = tl.program_id(0) + in_idx0 = pid * XS + tl.arange(0, XS) + tmp0 = tl.load(in_ptr0 + in_idx0) + tmp0min = tl.min(tmp0, axis=0) + tmp0max = tl.max(tmp0, axis=0) + tmp0 = tmp0 * 2.0 + tmp0 = tl.clamp(tmp0, tmp0min, tmp0max) + tmp0 = tmp0.to(tl.int32) + tmp1 = tl.load(in_ptr1 + tmp0) + tmp2 = tl_math.exp(tmp1) + out0_idx = pid * XS + tl.arange(0, XS) + tl.store(out_ptr0 + out0_idx, tmp2) + + def triton_ldst_indirect_04_func(x0, x1, xs): + n0 = x0.numel() + assert n0 == xs, "test only single core" + y0 = torch.empty((n0, ), dtype=x1.dtype, device=x1.device) + triton_ldst_indirect_04_kernel[n0 // xs, 1, 1](y0, x0, x1, XS=xs) + return y0 + + def torch_ldst_indirect_04_func(x0, x1): + x0min = torch.min(x0) + x0max = torch.max(x0) + idx = torch.clamp(x0 * 2, x0min, x0max) + return torch.exp(x1[idx.to(torch.int32)]) + + DEV = "npu" + DTYPE = torch.float32 + offset = 8 + N0, N1 = 16, 32 + blocksize = 16 + assert N1 >= N0 + offset, "N1 must be >= N0+offset" + assert N0 == blocksize, "N0 must be == blocksize" + x0 = offset + torch.arange(0, N0, dtype=torch.float32, device=DEV) + x1 = torch.randn((N1, ), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_04_func(x0, x1) + triton_cal = triton_ldst_indirect_04_func(x0, x1, blocksize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_05(): + + @triton.jit + def triton_ldst_indirect_05_kernel(out_ptr0, in_ptr1, in_ptr2, stride_in_r, XS: tl.constexpr, RS: tl.constexpr): + pid = tl.program_id(0) + in_idx0 = pid * XS + tl.arange(0, XS) + in_idx1 = tl.arange(0, RS) + tmp0 = tl.arange(0, XS) + tmp1 = tl.load(in_ptr1 + in_idx1) + in_idx2 = tmp0[:, None] * stride_in_r + tmp1[None, :] + tmp2 = tl.load(in_ptr2 + in_idx2) + tmp2 = tl_math.exp(tmp2) + out0_idx = in_idx0[:, None] * RS + in_idx1[None, :] + tl.store(out_ptr0 + out0_idx, tmp2) + + def triton_ldst_indirect_05_func(xc, x2, xs, rs): + nr = x2.size()[0] + nc = xc.numel() + stride_in_r = x2.stride()[0] + assert nr == xs, "test only single core" + y0 = torch.empty((nr, nc), dtype=x2.dtype, device=x2.device) + triton_ldst_indirect_05_kernel[nr // xs, 1, 1](y0, xc, x2, stride_in_r, XS=xs, RS=rs) + return y0 + + def torch_ldst_indirect_05_func(xr, xc, x2): + flatten_idx = (xr[:, None] * x2.stride()[0] + xc[None, :]).flatten() + extracted = x2.flatten()[flatten_idx].reshape([xr.numel(), xc.numel()]) + return torch.exp(extracted) + + DEV = "npu" + DTYPE = torch.float32 + offset = 8 + N0, N1 = 16, 32 + blocksize = 8 + lowdimsize = N0 + assert N1 >= N0 + offset, "N1 must be >= N0+offset" + assert N0 == lowdimsize, "N0 must be == lowdimsize" + xc = offset + torch.arange(0, N0, device=DEV) + xr = torch.arange(0, blocksize, device=DEV) + x2 = torch.randn((blocksize, N1), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_05_func(xr, xc, x2) + triton_cal = triton_ldst_indirect_05_func(xc, x2, blocksize, lowdimsize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_06(): + + @triton.jit + def triton_ldst_indirect_06_kernel(out_ptr0, in_ptr0, in_ptr1, in_ptr2, stride_in_r, XS: tl.constexpr, + RS: tl.constexpr): + pid = tl.program_id(0) + in_idx0 = pid * XS + tl.arange(0, XS) + in_idx1 = tl.arange(0, RS) + tmp0 = tl.load(in_ptr0 + in_idx0) + tmp1 = tl.load(in_ptr1 + in_idx1) + in_idx2 = tmp0[:, None] * stride_in_r + tmp1[None, :] + tmp2 = tl.load(in_ptr2 + in_idx2) + tmp2 = tl_math.exp(tmp2) + out0_idx = in_idx0[:, None] * RS + in_idx1[None, :] + tl.store(out_ptr0 + out0_idx, tmp2) + + def triton_ldst_indirect_06_func(xr, xc, x2, xs, rs): + nr = x2.size()[0] + nc = xc.numel() + stride_in_r = x2.stride()[0] + assert nr == xs, "test only single core" + y0 = torch.empty((nr, nc), dtype=x2.dtype, device=x2.device) + triton_ldst_indirect_06_kernel[nr // xs, 1, 1](y0, xr, xc, x2, stride_in_r, XS=xs, RS=rs) + return y0 + + def torch_ldst_indirect_06_func(xr, xc, x2): + flatten_idx = (xr[:, None] * x2.stride()[0] + xc[None, :]).flatten() + extracted = x2.flatten()[flatten_idx].reshape([xr.numel(), xc.numel()]) + return torch.exp(extracted) + + DEV = "npu" + DTYPE = torch.float32 + offset = 8 + N0, N1 = 16, 32 + blocksize = 4 + lowdimsize = N0 + assert N1 >= N0 + offset, "N1 must be >= N0+offset" + assert N0 == lowdimsize, "N0 must be == lowdimsize" + xc = offset + torch.arange(0, N0, device=DEV) + xr = torch.arange(0, blocksize, device=DEV) + x2 = torch.randn((blocksize, N1), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_06_func(xr, xc, x2) + triton_cal = triton_ldst_indirect_06_func(xr, xc, x2, blocksize, lowdimsize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_07(): + + @triton.jit + def triton_ldst_indirect_07_kernel(out_ptr0, in_ptr0, in_ptr1, in_ptr2, stride_in_r, XS: tl.constexpr, + RS: tl.constexpr): + pid = tl.program_id(0) + in_idx0 = pid * XS + tl.arange(0, XS) + in_idx1 = tl.arange(0, RS) + tmp0 = tl.load(in_ptr0 + in_idx0) + tmp1 = tl.load(in_ptr1 + in_idx1) + in_idx2 = tmp0[:, None] * stride_in_r + tmp1[None, :] + tmp2 = tl.load(in_ptr2 + in_idx2) + out0_idx = in_idx0[:, None] * RS + in_idx1[None, :] + tl.store(out_ptr0 + out0_idx, tmp2) + + def triton_ldst_indirect_07_func(xr, xc, x2, xs, rs): + nr = x2.size()[0] + nc = xc.numel() + stride_in_r = x2.stride()[0] + assert nr == xs, "test only single core" + y0 = torch.empty((nr, nc), dtype=x2.dtype, device=x2.device) + triton_ldst_indirect_07_kernel[nr // xs, 1, 1](y0, xr, xc, x2, stride_in_r, XS=xs, RS=rs) + return y0 + + def torch_ldst_indirect_07_func(xr, xc, x2): + flatten_idx = (xr[:, None] * x2.stride()[0] + xc[None, :]).flatten() + extracted = x2.flatten()[flatten_idx].reshape([xr.numel(), xc.numel()]) + return extracted + + DEV = "npu" + DTYPE = torch.float32 + offset = 8 + N0, N1 = 16, 32 + blocksize = 4 + lowdimsize = N0 + assert N1 >= N0 + offset, "N1 must be >= N0+offset" + assert N0 == lowdimsize, "N0 must be == lowdimsize" + xc = offset + torch.arange(0, N0, device=DEV) + xr = torch.arange(0, blocksize, device=DEV) + x2 = torch.randn((blocksize, N1), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_07_func(xr, xc, x2) + triton_cal = triton_ldst_indirect_07_func(xr, xc, x2, blocksize, lowdimsize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_08(): + + @triton.jit + def triton_ldst_indirect_08_kernel(out_ptr0, in_ptr_xc, in_ptr_x2, stride_in_r, OUT_COLS: tl.constexpr, + XS: tl.constexpr, RS: tl.constexpr): + pid = tl.program_id(0) + row_idx_full = pid * XS + tl.arange(0, XS) + col_pos = tl.arange(0, RS) + xc_vals = tl.load(in_ptr_xc + col_pos) + row_arange = tl.arange(0, XS) + gather_flat = row_arange[:, None] * stride_in_r + xc_vals[None, :] + vals = tl.load(in_ptr_x2 + gather_flat) + vals = tl_math.exp(vals) + out_flat = row_idx_full[:, None] * OUT_COLS + xc_vals[None, :] + tl.store(out_ptr0 + out_flat, vals) + + def triton_ldst_indirect_08_func(xc, x2, xs, rs): + nr = x2.size(0) + out_cols = x2.size(1) + stride_in_r = x2.stride(0) + assert nr == xs, "test only single core" + y0 = torch.zeros((nr, out_cols), dtype=x2.dtype, device=x2.device) + triton_ldst_indirect_08_kernel[nr // xs, 1, 1](y0, xc, x2, stride_in_r, OUT_COLS=out_cols, XS=xs, RS=rs) + return y0 + + def torch_ldst_indirect_08_func(xr, xc, x2): + out = torch.zeros((xr.numel(), x2.size(1)), dtype=x2.dtype, device=x2.device) + gathered = torch.exp(x2[xr[:, None], xc[None, :]]) + out.scatter_(1, xc.expand(xr.numel(), -1), gathered) + return out + + DEV = "npu" + DTYPE = torch.float32 + offset = 8 + N0, N1 = 16, 32 + blocksize = 8 + lowdimsize = N0 + assert N1 >= N0 + offset, "N1 must be >= N0+offset" + assert N0 == lowdimsize, "N0 must be == lowdimsize" + xc = offset + torch.arange(0, N0, device=DEV) + xr = torch.arange(0, blocksize, device=DEV) + x2 = torch.randn((blocksize, N1), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_08_func(xr, xc, x2) + triton_cal = triton_ldst_indirect_08_func(xc, x2, blocksize, lowdimsize) + torch.testing.assert_close(triton_cal, torch_ref) + + +def test_ldst_indirect_09(): + + @triton.jit + def triton_ldst_indirect_09_kernel(out_ptr0, in_ptr1, in_ptr2, stride_in_r, offset: tl.constexpr, XS: tl.constexpr, + RS: tl.constexpr): + pid = tl.program_id(0) + in_idx0 = tl.arange(0, XS) + in_idx1 = tl.arange(0, RS) + tmp0 = pid * XS + tl.load(in_ptr1 + in_idx0) + tmp1 = tl.arange(0, RS) + offset + in_idx2 = tmp0[:, None] * stride_in_r + tmp1[None, :] + tmp2 = tl.load(in_ptr2 + in_idx2) + tmp2 = tl_math.exp(tmp2) + out0_idx = pid * XS * RS + in_idx0[:, None] * RS + in_idx1[None, :] + tl.store(out_ptr0 + out0_idx, tmp2) + + def triton_ldst_indirect_09_func(xr, x2, offset, xs, rs): + nr = xr.numel() + nc = rs + stride_in_r = x2.stride()[0] + y0 = torch.empty((nr, nc), dtype=x2.dtype, device=x2.device) + triton_ldst_indirect_09_kernel[nr // xs, 1, 1](y0, xr, x2, stride_in_r, offset=offset, XS=xs, RS=rs) + return y0 + + def torch_ldst_indirect_09_func(xr, xc, x2): + flatten_idx = (xr[:, None] * x2.stride()[0] + xc[None, :]).flatten() + extracted = x2.flatten()[flatten_idx].reshape([xr.numel(), xc.numel()]) + return torch.exp(extracted) + + DEV = "npu" + DTYPE = torch.float32 + offset = 8 + N0, N1 = 16, 32 + blocksize = 8 + lowdimsize = N0 + assert N1 >= N0 + offset, "N1 must be >= N0+offset" + assert N0 == lowdimsize, "N0 must be == lowdimsize" + xc = offset + torch.arange(0, N0, device=DEV) + xr = torch.arange(0, blocksize, device=DEV) + x2 = torch.randn((blocksize, N1), dtype=DTYPE, device=DEV) + torch_ref = torch_ldst_indirect_09_func(xr, xc, x2) + triton_cal = triton_ldst_indirect_09_func(xr, x2, offset, blocksize, lowdimsize) + torch.testing.assert_close(triton_cal, torch_ref) + + +if __name__ == "__main__": + test_ldst_indirect_05() + print("success: test_ldst_indirect_05") diff --git a/third_party/ascend/unittest/pytest_ut/test_le.py b/third_party/ascend/unittest/pytest_ut/test_le.py new file mode 100644 index 0000000000..b03e6fa7e5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_le.py @@ -0,0 +1,79 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_binary(x0, y0): + res = x0 <= y0 + return res + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x <= y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + (torch.int8, 'int8'), + (torch.int16, 'int16'), + (torch.int32, 'int32'), + (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + y0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + ans = standard_binary(x0, y0) + out = torch.zeros((N, ), dtype=torch.bool).npu() + triton_elementwise_binary[1, 1, 1](x0, y0, out, N, NUMEL) + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_lgamma.py b/third_party/ascend/unittest/pytest_ut/test_lgamma.py new file mode 100644 index 0000000000..e09cbe7014 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_lgamma.py @@ -0,0 +1,113 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common +import triton.language.extra.cann.libdevice as libdevice + + +@triton.jit +def triton_lgamma(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = tl.load(in_ptr0 + xindex, xmask) + y = libdevice.lgamma(x0) + tl.store(out_ptr0 + xindex, y, xmask) + + +@pytest.mark.skip(reason="Wait for AscendNPU-IR support") +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_lgamma_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + + # Avoid numerical instability near negative integer + nearest_int = torch.round(x) + neg_mask = nearest_int <= -1 + threshold = torch.zeros_like(x) + if neg_mask.any(): + neg_ints = nearest_int[neg_mask] + threshold[neg_mask] = 5.75e-5 * (2.42**(-1 - neg_ints)) + mask = (torch.abs(x - nearest_int) < threshold) & (nearest_int <= -1) + if mask.any(): + x = torch.where(mask, nearest_int, x) + + y_ref = torch.lgamma(x).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_lgamma[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.skip(reason="Wait for AscendNPU-IR support") +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_all_blocks_parallel(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + + # Avoid numerical instability near negative integer + nearest_int = torch.round(x) + neg_mask = nearest_int <= -1 + threshold = torch.zeros_like(x) + if neg_mask.any(): + neg_ints = nearest_int[neg_mask] + threshold[neg_mask] = 5.75e-5 * (2.42**(-1 - neg_ints)) + mask = (torch.abs(x - nearest_int) < threshold) & (nearest_int <= -1) + if mask.any(): + x = torch.where(mask, nearest_int, x) + + y_ref = torch.lgamma(x).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_lgamma[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.skip(reason="Wait for AscendNPU-IR support") +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 2048, 8), 2, 32768, 512], +]) +def test_auto_blockify(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + + # Avoid numerical instability near negative integer + nearest_int = torch.round(x) + neg_mask = nearest_int <= -1 + threshold = torch.zeros_like(x) + if neg_mask.any(): + neg_ints = nearest_int[neg_mask] + threshold[neg_mask] = 5.75e-5 * (2.42**(-1 - neg_ints)) + mask = (torch.abs(x - nearest_int) < threshold) & (nearest_int <= -1) + if mask.any(): + x = torch.where(mask, nearest_int, x) + + y_ref = torch.lgamma(x).npu() + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_lgamma[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub, auto_blockify_size=ncore) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_linearize.py b/third_party/ascend/unittest/pytest_ut/test_linearize.py new file mode 100644 index 0000000000..21780f52f4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_linearize.py @@ -0,0 +1,602 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +num_cube_core = 20 +num_vector_core = 20 + + +def foo(a, d, shape): + y = a.reshape(shape) + y = y.permute(0, 2, 1) + d + return y + + +@triton.jit +def triton_gpu_revised(in_ptr0, in_ptr1, out_ptr0, ynumel, xnumel, YBLOCK: tl.constexpr, SHAPE0: tl.constexpr, + SHAPE1: tl.constexpr, SHAPE2: tl.constexpr, XBLOCK: tl.constexpr): + yoffset = tl.program_id(1) * (tl.program_id(2) + 1) * YBLOCK + yindex = yoffset + tl.arange(0, YBLOCK) + ymask = yindex < ynumel + xoffset = tl.program_id(0) * XBLOCK + xindex = xoffset + tl.arange(0, XBLOCK) + xmask = xindex < xnumel + x2 = xindex[:, None] + x2_1 = xindex[None, :] + y3 = yindex[:, None] + y0 = (yindex % SHAPE1)[None, :] + y1 = (yindex // SHAPE1)[None, :] + tmp0 = tl.load(in_ptr0 + (x2_1 + (SHAPE2 * y3)), (x2_1 < xnumel) & (y3 < ynumel)) + tmp1 = tl.load(in_ptr1 + (y0 + (SHAPE1*x2) + (SHAPE1*SHAPE2*y1)), \ + (xindex[:,None] < xnumel) & (yindex[None,:] < ynumel)) + # (x2 < xnumel) & (y0 < SHAPE1)) + # (xindex[:,None] < xnumel) & (yindex[None,:] < ynumel)) + tmp10 = tmp1.permute(1, 0) + tmp2 = tmp0 + tmp10 + tl.store(out_ptr0 + (x2_1 + (SHAPE2 * y3)), tmp2, (x2_1 < xnumel) & (y3 < ynumel)) + + +def biggest_divisor(num): + for i in range(2, num): + if num % i == 0: + return num // i + return num + + +def find_good_yblock(ynumel, xnumel, y_upper, dtype): + y = ynumel + x = xnumel + + align_numel = 4 if dtype == torch.int64 else 8 + ub_upper = 3900 if dtype == torch.int64 else 8000 + + # optimize block_dim + def get_block_dim(y, x): + return ((xnumel + x - 1) // x) * ((y_upper + y - 1) // y) + + count = 0 + while (get_block_dim(y, x) < num_vector_core and y > 8 and count < 20): + y_1 = biggest_divisor(y) + if get_block_dim(y_1, x) > num_vector_core: + break + y = y_1 + if get_block_dim(y, x) < num_vector_core and x > align_numel: + x = x // 2 + count = count + 1 + + # optimize block_size to avoid ub-overflow + while (y * x > ub_upper): + y_1 = biggest_divisor(y) + if y_1 == y or y_1 <= align_numel: + break + y = y_1 + + while (y * x > ub_upper and x > align_numel): + x_1 = x // 2 + if x_1 <= align_numel: + break + x = x_1 + + return (x, y) + + +def triton_foo(a, d, shape, dtype): + z, y, x = shape + out = torch.empty_strided((z, x, y), (x * y, 1, x), device='npu', dtype=dtype) + XBLOCK, YBLOCK = find_good_yblock(y, x, y * z, dtype=dtype) + print(f"XBLOCK={XBLOCK},YBLOCK={YBLOCK}, block_dim={((x + XBLOCK -1 )//XBLOCK) * (((y*z) + YBLOCK -1 ) // YBLOCK)}") + grid = ((x + XBLOCK - 1) // XBLOCK, ((y * z) + YBLOCK - 1) // YBLOCK, 1) + + triton_gpu_revised[grid](a, d, out, y * z, x, SHAPE0=z, SHAPE1=y, SHAPE2=x, YBLOCK=YBLOCK, XBLOCK=XBLOCK) + return out + + +types = [ + (torch.float32, 'float32'), +] + +shapes = [(8, 2048, 4)] + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('Z,Y,X', shapes) +def test_linearize(Z, Y, X, dtype, sigtype): + shape = (Z, Y, X) + print(f"start data validation on shape:{shape}") + a = test_common.generate_tensor(shape=(Z, Y * X), dtype=sigtype).npu() + d = test_common.generate_tensor(shape=(Z, X, Y), dtype=sigtype).npu() + r = triton_foo(a, d, shape, dtype) + r1 = foo(a, d, shape) + test_common.validate_cmp(sigtype, r1, r) + print(f"data validation passed") + + +# Test linearize offset handling with expert routing pattern +@triton.jit +def linearize_offset_kernel( + bias_ptr, + output_ptr, + experts_ids_ptr, + N: tl.constexpr, + EM: tl.constexpr, + stride_bias_e, + stride_bias_n, + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + GROUP_SIZE_M: tl.constexpr, +): + pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + off_experts = tl.load(experts_ids_ptr + pid_m).to(tl.int64) + offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)) % N + + if bias_ptr is not None: + bias = tl.load(bias_ptr + off_experts * stride_bias_e + offs_bn[None, :] * stride_bias_n) + tl.store(output_ptr + pid * 16 + tl.arange(0, 16), bias.reshape(16)) + + +def torch_linearize_offset(bias_ptr, experts_ids_ptr, N, EM, stride_bias_e, stride_bias_n, BLOCK_SIZE_M, BLOCK_SIZE_N, + GROUP_SIZE_M): + """PyTorch reference implementation for offset handling""" + output = torch.empty([16, 16], dtype=bias_ptr.dtype, device=bias_ptr.device) + + num_pid_m = (EM + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M + num_pid_n = (N + BLOCK_SIZE_N - 1) // BLOCK_SIZE_N + num_pid_in_group = GROUP_SIZE_M * num_pid_n + + for pid in range(16): + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + off_experts = experts_ids_ptr[pid_m].to(torch.int64).item() + offs_bn = torch.arange(pid_n * BLOCK_SIZE_N, + (pid_n + 1) * BLOCK_SIZE_N, dtype=torch.int64, device=bias_ptr.device) % N + + bias = bias_ptr[off_experts, offs_bn] + output[pid] = bias + + return output + + +@pytest.mark.parametrize('dtype,sigtype', [(torch.float32, 'float32')]) +def test_linearize_offset_handling(dtype, sigtype): + """ + Test linearization's handling of complex offset patterns. + This test simulates expert routing scenarios where offsets are computed + dynamically based on expert IDs, validating correct pointer arithmetic + and linearization in the compiler. + """ + print(f"Testing linearize offset handling with dtype={sigtype}") + + # Setup test data + num_experts = 4 + hidden_dim = 64 + bias_ptr = torch.arange(0, num_experts * hidden_dim, dtype=dtype).npu().reshape(num_experts, hidden_dim) + output_ptr = torch.empty([16, 16], dtype=dtype).npu() + experts_ids_ptr = torch.tensor([1, 2, 3, 1], dtype=torch.int32).npu() + + # Kernel parameters + N = 64 + EM = 64 + stride_bias_e = 64 + stride_bias_n = 1 + BLOCK_SIZE_M = 16 + BLOCK_SIZE_N = 16 + GROUP_SIZE_M = 4 + + # Run triton kernel + linearize_offset_kernel[(16, )](bias_ptr=bias_ptr, output_ptr=output_ptr, experts_ids_ptr=experts_ids_ptr, N=N, + EM=EM, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n, + BLOCK_SIZE_M=BLOCK_SIZE_M, BLOCK_SIZE_N=BLOCK_SIZE_N, GROUP_SIZE_M=GROUP_SIZE_M) + + # Compute reference result + expected = torch_linearize_offset(bias_ptr, experts_ids_ptr, N, EM, stride_bias_e, stride_bias_n, BLOCK_SIZE_M, + BLOCK_SIZE_N, GROUP_SIZE_M) + + # Validate results + test_common.validate_cmp(sigtype, expected, output_ptr) + print(f"Linearize offset handling test passed") + + +def torch_expand_dims_and_add(buffer, cache, buffer_stride, BLOCK, NUMEL): + for i in range(buffer.shape[0]): + tmp = buffer[i, 0, :buffer_stride] + accumulator = torch.zeros(2, buffer_stride).npu() + accumulator += tmp[None, :] + cache[i, 0, :] = accumulator.reshape(buffer_stride * 2) + + +@triton.jit +def expand_dims_and_add( + buffer_ptr, + cache_ptr, + buffer_stride: tl.constexpr, + BLOCK: tl.constexpr, + NUMEL: tl.constexpr, +): + pid = tl.program_id(0) + buffer_offset = pid * buffer_stride + buffer_index = (buffer_offset + tl.arange(0, buffer_stride)) % NUMEL + + cache_offset = pid * buffer_stride * 2 + cache_index = cache_offset + tl.arange(0, buffer_stride * 2) + + tmp = tl.load(buffer_ptr + buffer_index[None, :]) + accumulator = tl.zeros((2, buffer_stride), dtype=tl.float32) + accumulator += tmp + tl.store(cache_ptr + cache_index, accumulator.reshape(buffer_stride * 2)) + + +cache_shapes = [ + (5, 15), +] + + +@pytest.mark.parametrize("dtype, sigtype", types) +@pytest.mark.parametrize("batch_size, buffer_len", cache_shapes) +def test_expand_dims_and_add(batch_size, buffer_len, dtype, sigtype): + block = biggest_divisor(buffer_len) + cache_len = buffer_len * 2 + buffer = test_common.generate_tensor(shape=(batch_size, 1, buffer_len), dtype=sigtype).npu() + cache = torch.zeros(batch_size, 1, cache_len, dtype=dtype).npu() + cache_ref = torch.zeros(batch_size, 1, cache_len, dtype=dtype).npu() + numel = batch_size * buffer_len * 2 + torch_expand_dims_and_add(buffer, cache_ref, buffer_len, block, numel) + expand_dims_and_add[block, cache_len](buffer, cache, buffer_len, block, numel) + + test_common.validate_cmp(sigtype, cache, cache_ref) + + +def torch_save_cache_to_buffer(buffer, cache1, cache2, buffer_stride, cache_stride, BLOCK): + idx = torch.arange(0, cache_stride) + mask = ((idx // BLOCK) % 2 == 0) + max_len = min(buffer_stride, mask.shape[0]) + for i in range(buffer.shape[0]): + if i % 2 == 0: + tmp = cache1[i, 0, mask] + buffer[i, 0, :max_len] = tmp[:max_len] + else: + tmp = cache2[i, 0, mask] + buffer[i, 0, :max_len] = tmp[:max_len] + + +def torch_save_cache_to_buffer_with_offset(buffer, cache1, cache2, buffer_stride, cache_stride, BLOCK): + idx = torch.arange(0, cache_stride) + mask = ((idx // BLOCK) % 2 == 0) + max_len = min(buffer_stride, mask.shape[0]) + for i in range(buffer.shape[0]): + if i % 2 == 0: + tmp = cache1[i, 0, mask] + buffer[i, 0, :max_len] = tmp[:max_len] + else: + tmp = cache2[i, 0, ~mask] + buffer[i, 0, :max_len] = tmp[:max_len] + + +def torch_rearrange_and_combine_two_buffer( + buffer1, + buffer2, + cache, + buffer_stride, + NUM_BLOCK, + BLOCK, +): + for i in range(buffer1.shape[0]): + tmp1 = buffer1[i, 0, :] + tmp1 = tmp1.reshape(NUM_BLOCK, BLOCK) + tmp1 = tmp1.permute(1, 0) + tmp1 = tmp1.reshape(1, -1) + + tmp2 = buffer2[i, 0, :] + tmp2 = tmp2.reshape(NUM_BLOCK, BLOCK) + tmp2 = tmp2.permute(1, 0) + tmp2 = tmp2.reshape(1, -1) + + cache[i, 0, :buffer_stride] = tmp1 + cache[i, 0, buffer_stride:] = tmp2 + + +def torch_save_cache_to_buffer_with_mask(buffer, cache1, cache2, mask_int, buffer_stride, cache_stride, BLOCK, + MASK_NUM): + idx = torch.arange(0, cache_stride) + mask_idx = torch.arange(0, buffer_stride) + mask = ((idx // BLOCK) % 2 == 0) + max_len = min(buffer_stride, mask.shape[0]) + for i in range(buffer.shape[0]): + if i % 2 == 0: + tmp = cache1[i, 0, :] + tmp[~(idx < MASK_NUM)] = 0 + tmp = tmp[mask] + buffer[i, 0, :max_len] = tmp[:max_len] + else: + tmp = cache2[i, 0, mask] + tmp[~((mask_idx < MASK_NUM) & (mask_idx < mask_int[i]))] = 0 + buffer[i, 0, :max_len] = tmp[:max_len] + + +def torch_rearrange_cache_with_mask( + cache1, + cache2, + half_buffer_num, + buffer_len, + NUM_BLOCK, + BLOCK, +): + buffer_num = half_buffer_num * 2 + idx1 = torch.arange(0, half_buffer_num) + idx2 = torch.arange(0, buffer_len) + # all true + mask1 = idx1 < buffer_num + mask2 = idx2 < buffer_len + + for i in range(cache1.shape[0]): + for j in range(2): + mask = (mask1[j] & mask2) + tmp1 = cache1[i, j, :] + tmp1[~mask] = 0 + tmp1 = tmp1.reshape(NUM_BLOCK, BLOCK) + tmp1 = tmp1.permute(1, 0) + tmp1 = tmp1.reshape(1, -1) + + tmp2 = cache1[i, j + half_buffer_num, :] + tmp2[~mask] = 0 + tmp2 = tmp2.reshape(NUM_BLOCK, BLOCK) + tmp2 = tmp2.permute(1, 0) + tmp2 = tmp2.reshape(1, -1) + + cache2[i, j, :] = -tmp1 + cache2[i, j + half_buffer_num, :] = tmp2 + + +@triton.jit +def save_cache_to_buffer(buffer_ptr, cache_ptr1, cache_ptr2, buffer_stride: tl.constexpr, BLOCK: tl.constexpr): + pid_loc = tl.program_id(0) + buffer_offset = pid_loc * buffer_stride + buffer_index = buffer_offset + tl.arange(0, buffer_stride) + + cache_offset = pid_loc * buffer_stride + cache_index = cache_offset + tl.arange(0, buffer_stride) + cache_index_0 = cache_index // BLOCK + cache_index_1 = cache_index % BLOCK + + if pid_loc % 2 == 0: + tmp = tl.load(cache_ptr1 + (2 * BLOCK * cache_index_0 + cache_index_1)) + tl.store(buffer_ptr + buffer_index, tmp) + if pid_loc % 2 == 1: + tmp = tl.load(cache_ptr2 + (2 * BLOCK * cache_index_0 + cache_index_1)) + tl.store(buffer_ptr + buffer_index, tmp) + + +@triton.jit +def save_cache_to_buffer_with_offset(buffer_ptr, cache_ptr1, cache_ptr2, buffer_stride: tl.constexpr, + BLOCK: tl.constexpr): + pid_loc = tl.program_id(0) + buffer_offset = pid_loc * buffer_stride + buffer_index = buffer_offset + tl.arange(0, buffer_stride) + + cache_offset = pid_loc * buffer_stride + cache_index = cache_offset + tl.arange(0, buffer_stride) + cache_index_0 = cache_index // BLOCK + cache_index_1 = cache_index % BLOCK + + if pid_loc % 2 == 0: + tmp = tl.load(cache_ptr1 + (2 * BLOCK * cache_index_0 + cache_index_1)) + tl.store(buffer_ptr + buffer_index, tmp) + if pid_loc % 2 == 1: + tmp = tl.load(cache_ptr2 + BLOCK + (2 * BLOCK * cache_index_0 + cache_index_1)) + tl.store(buffer_ptr + buffer_index, tmp) + + +@triton.jit +def rearrange_and_combine_two_buffer( + buffer_ptr1, + buffer_ptr2, + cache_ptr, + buffer_stride: tl.constexpr, + NUM_BLOCK: tl.constexpr, + BLOCK: tl.constexpr, +): + pid_loc = tl.program_id(0) + buffer_offset = pid_loc * buffer_stride + buffer_index = buffer_offset + tl.arange(0, buffer_stride) + + tmp1 = tl.load(buffer_ptr1 + buffer_index) + tmp2 = tl.load(buffer_ptr2 + buffer_index) + + x_offset = pid_loc * 2 * BLOCK * buffer_stride + x_index = x_offset + tl.arange(0, buffer_stride) + + for i in range(2): + n_index = (x_index // BLOCK) + b_index = (x_index % BLOCK) + + if i % 2 == 0: + tl.store(cache_ptr + NUM_BLOCK * b_index + n_index, tmp1) + if i % 2 == 1: + tl.store(cache_ptr + NUM_BLOCK * b_index + n_index, tmp2) + + x_index = x_index + buffer_stride * BLOCK + + +@triton.jit +def save_cache_to_buffer_with_mask(buffer_ptr, cache_ptr1, cache_ptr2, mask_int_ptr, buffer_stride: tl.constexpr, + BLOCK: tl.constexpr, MASK_NUM: tl.constexpr): + pid_loc = tl.program_id(0) + + buffer_offset = pid_loc * buffer_stride + + buffer_index = tl.arange(0, buffer_stride) + index = buffer_offset + buffer_index + cache_index_0 = index // BLOCK + cache_index_1 = index % BLOCK + mask_int = tl.load(mask_int_ptr + pid_loc) + if pid_loc % 2 == 0: + tmp = tl.load(cache_ptr1 + (2*BLOCK*cache_index_0 + cache_index_1), \ + ((2*BLOCK*cache_index_0 + cache_index_1) < buffer_offset * 2 + MASK_NUM), other=0.0) + tl.store(buffer_ptr + index, tmp) + if pid_loc % 2 == 1: + tmp = tl.load(cache_ptr2 + (2*BLOCK*cache_index_0 + cache_index_1), \ + (buffer_index < MASK_NUM) & (buffer_index < mask_int), other=0.0) + tl.store(buffer_ptr + index, tmp) + + +@triton.jit +def rearrange_cache_with_mask( + cache_ptr1, + cache_ptr2, + half_buffer_num: tl.constexpr, + buffer_stride: tl.constexpr, + NUM_BLOCK: tl.constexpr, + BLOCK: tl.constexpr, +): + pid_loc = tl.program_id(0) + buffer_num = half_buffer_num * 2 + buffer_offset = pid_loc * buffer_stride * buffer_num + + index = tl.arange(0, buffer_stride) + buffer_index = buffer_offset + index + mask_index = index[None, :] + load_index = buffer_index[None, :] + + buffer_index2 = buffer_offset * BLOCK + index + n_index = (buffer_index2 // BLOCK) + b_index = (buffer_index2 % BLOCK) + store_index = NUM_BLOCK * b_index + n_index + store_index2 = store_index[None, :] + + buffer_num_offset = 0 + for i in range(2): + buffer_num_index = buffer_num_offset + tl.arange(0, half_buffer_num) + buffer_num_index2 = buffer_num_index[:, None] + + tmp1 = tl.load(cache_ptr1 + buffer_stride * buffer_num_index2 + load_index) + tmp1 = tmp1 * (2 * i - 1) + mask1 = buffer_num_index2 < buffer_num + mask2 = mask_index < buffer_stride + tl.store(cache_ptr2 + buffer_stride * buffer_num_index2 + store_index2, tmp1, mask1 & mask2) + # tl.store(cache_ptr2 + buffer_stride * buffer_num_index2 + store_index2, tmp1, True) + + buffer_num_offset = buffer_num_offset + half_buffer_num + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('batch_size,buffer_len', cache_shapes) +def test_linearize_jump_load(batch_size, buffer_len, dtype, sigtype): + block = biggest_divisor(buffer_len) + cache_len = buffer_len * 2 + buffer_ref = torch.zeros(batch_size, 1, buffer_len, dtype=dtype) + buffer = buffer_ref.npu() + cache1_ref = test_common.generate_tensor(shape=(batch_size, 1, cache_len), dtype=sigtype) + cache1 = cache1_ref.npu() + cache2_ref = test_common.generate_tensor(shape=(batch_size, 1, cache_len), dtype=sigtype) + cache2 = cache2_ref.npu() + + torch_save_cache_to_buffer(buffer_ref, cache1_ref, cache2_ref, buffer_len, cache_len, block) + save_cache_to_buffer[(batch_size, 1, 1)](buffer, cache1, cache2, buffer_len, block) + test_common.validate_cmp(sigtype, buffer, buffer_ref) + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('batch_size,buffer_len', cache_shapes) +def test_linearize_jump_load_with_offset(batch_size, buffer_len, dtype, sigtype): + block = biggest_divisor(buffer_len) + cache_len = buffer_len * 2 + buffer_ref = torch.zeros(batch_size, 1, buffer_len, dtype=dtype) + buffer = buffer_ref.npu() + cache1_ref = test_common.generate_tensor(shape=(batch_size, 1, cache_len), dtype=sigtype) + cache1 = cache1_ref.npu() + cache2_ref = test_common.generate_tensor(shape=(batch_size, 1, cache_len), dtype=sigtype) + cache2 = cache2_ref.npu() + + torch_save_cache_to_buffer_with_offset(buffer_ref, cache1_ref, cache2_ref, buffer_len, cache_len, block) + save_cache_to_buffer_with_offset[(batch_size, 1, 1)](buffer, cache1, cache2, buffer_len, block) + test_common.validate_cmp(sigtype, buffer, buffer_ref) + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('batch_size,buffer_len', cache_shapes) +def test_linearize_rearrange(batch_size, buffer_len, dtype, sigtype): + block = biggest_divisor(buffer_len) + num_block = int(buffer_len / block) + cache_len = buffer_len * 2 + buffer1_ref = test_common.generate_tensor(shape=(batch_size, 1, buffer_len), dtype=sigtype) + buffer1 = buffer1_ref.npu() + buffer2_ref = test_common.generate_tensor(shape=(batch_size, 1, buffer_len), dtype=sigtype) + buffer2 = buffer2_ref.npu() + cache_ref = torch.zeros(batch_size, 1, cache_len, dtype=dtype) + cache = cache_ref.npu() + + torch_rearrange_and_combine_two_buffer(buffer1_ref, buffer2_ref, cache_ref, buffer_len, num_block, block) + rearrange_and_combine_two_buffer[(batch_size, 1, 1)](buffer1, buffer2, cache, buffer_len, num_block, block) + test_common.validate_cmp(sigtype, cache, cache_ref) + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('batch_size,buffer_len', cache_shapes) +def test_linearize_jump_load_with_mask(batch_size, buffer_len, dtype, sigtype): + block = biggest_divisor(buffer_len) + cache_len = buffer_len * 2 + buffer_ref = torch.zeros(batch_size, 1, buffer_len, dtype=dtype) + buffer = buffer_ref.npu() + cache1_ref = test_common.generate_tensor(shape=(batch_size, 1, cache_len), dtype=sigtype) + cache1 = cache1_ref.npu() + cache2_ref = test_common.generate_tensor(shape=(batch_size, 1, cache_len), dtype=sigtype) + cache2 = cache2_ref.npu() + mask_ref = torch.arange(0, batch_size, dtype=torch.int64) * 2 + mask = mask_ref.npu() + mask_num = 16 + torch_save_cache_to_buffer_with_mask(buffer_ref, cache1_ref, cache2_ref, mask_ref, buffer_len, cache_len, block, + mask_num) + save_cache_to_buffer_with_mask[(batch_size, 1, 1)](buffer, cache1, cache2, mask, buffer_len, block, mask_num) + test_common.validate_cmp(sigtype, buffer, buffer_ref) + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('batch_size,buffer_len', cache_shapes) +def test_linearize_rearrange_with_mask(batch_size, buffer_len, dtype, sigtype): + block = biggest_divisor(buffer_len) + num_block = int(buffer_len / block) + cache1_ref = test_common.generate_tensor(shape=(batch_size, 4, buffer_len), dtype=sigtype) + cache1 = cache1_ref.npu() + cache2_ref = torch.zeros(batch_size, 4, buffer_len, dtype=dtype) + cache2 = cache2_ref.npu() + + torch_rearrange_cache_with_mask(cache1_ref, cache2_ref, 2, buffer_len, num_block, block) + rearrange_cache_with_mask[(batch_size, 1, 1)](cache1, cache2, 2, buffer_len, num_block, block) + test_common.validate_cmp(sigtype, cache2, cache2_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_linearize_fallback.py b/third_party/ascend/unittest/pytest_ut/test_linearize_fallback.py new file mode 100644 index 0000000000..841c2562d2 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_linearize_fallback.py @@ -0,0 +1,86 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def torch_save_cache_to_buffer_indirect(buffer, cache, loc, buffer_stride, cache_stride, BLOCK): + max_len = min(buffer_stride, loc.shape[0]) + for i in range(buffer.shape[0]): + index = (loc // BLOCK) * BLOCK + loc % BLOCK + tmp = cache[i, 0, index] + buffer[i, 0, :max_len] = tmp[:max_len] + + +@triton.jit +def save_cache_to_buffer_indirect(buffer_ptr, cache_ptr, loc_ptr, buffer_stride: tl.constexpr, BLOCK: tl.constexpr): + pid_loc = tl.program_id(0) + index = tl.arange(0, buffer_stride) + loc = tl.load(loc_ptr + index) + buffer_offset = pid_loc * buffer_stride + buffer_index = buffer_offset + index + + cache_offset = pid_loc * buffer_stride * 2 + cache_index = cache_offset + loc + cache_index_0 = cache_index // BLOCK + cache_index_1 = cache_index % BLOCK + + tmp = tl.load(cache_ptr + (BLOCK * cache_index_0 + cache_index_1)) + tl.store(buffer_ptr + buffer_index, tmp) + + +def biggest_divisor(num): + for i in range(2, num): + if num % i == 0: + return num // i + return num + + +types = [ + (torch.float32, 'float32'), +] + +cache_shapes = [ + (5, 15), +] + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('batch_size,buffer_len', cache_shapes) +def test_linearize_indirect_load(batch_size, buffer_len, dtype, sigtype): + block = biggest_divisor(buffer_len) + cache_len = buffer_len * 2 + buffer_ref = torch.zeros(batch_size, 1, buffer_len, dtype=dtype) + buffer = buffer_ref.npu() + cache_ref = test_common.generate_tensor(shape=(batch_size, 1, cache_len), dtype=sigtype) + cache = cache_ref.npu() + + loc_ref = torch.arange(0, buffer_len) + loc = loc_ref.npu() + torch_save_cache_to_buffer_indirect(buffer_ref, cache_ref, loc_ref, buffer_len, cache_len, block) + save_cache_to_buffer_indirect[(batch_size, 1, 1)](buffer, cache, loc, buffer_len, block) + test_common.validate_cmp(sigtype, buffer, buffer_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_linearize_mask.py b/third_party/ascend/unittest/pytest_ut/test_linearize_mask.py new file mode 100644 index 0000000000..7e35de4f7d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_linearize_mask.py @@ -0,0 +1,165 @@ +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +types_all = [ + (torch.float32, 'float32'), +] + +shapes_common = [(128, 256), (127, 256), (127, 16), (129, 256), (77, 1024), (69, 512)] + +block_size = [128, 256, 1024] + + +def ceil_div(a, b): + return (a + b - 1) // b + + +def profiler_wrapper(fn, *args): + result_path = "./result_profiling_broadcast" + skip_first = 10 + wait = 0 + warmup = 3 + active = 30 + repeat = 1 + stream = torch.npu.current_stream() + experimental_config = torch_npu.profiler._ExperimentalConfig( + aic_metrics=torch_npu.profiler.AiCMetrics.PipeUtilization, + profiler_level=torch_npu.profiler.ProfilerLevel.Level1, l2_cache=False, data_simplification=False) + with torch_npu.profiler.profile( + activities=[torch_npu.profiler.ProfilerActivity.CPU, torch_npu.profiler.ProfilerActivity.NPU], + schedule=torch_npu.profiler.schedule(wait=wait, warmup=warmup, active=active, repeat=repeat, + skip_first=skip_first), + on_trace_ready=torch_npu.profiler.tensorboard_trace_handler(result_path), record_shapes=True, + profile_memory=False, with_stack=False, with_flops=False, with_modules=False, + experimental_config=experimental_config) as prof: + stream.synchronize() + for _ in range(skip_first + (wait + warmup + active) * repeat): + fn(*args) + prof.step() + stream.synchronize() + + +@triton.jit +def linearize_mask_broadcast_kernel( + in_ptr, + output_ptr, + N: tl.constexpr, + M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, +): + pid = tl.program_id(axis=0) + offset = tl.multiple_of(pid * BLOCK_SIZE_N, N) + x1 = (offset + tl.arange(0, BLOCK_SIZE_N)) // N + mask1 = (x1 < M) + data = tl.load(in_ptr + x1 * N, mask=mask1, other=0) + x2 = pid * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + tl.store(output_ptr + x2, data) + + +def torch_linearize_mask_broadcast(in_tensor): + M = in_tensor.shape[0] // 2 + N = in_tensor.shape[1] + + output = torch.zeros_like(in_tensor) + + first_elements = in_tensor[:M, 0:1] + output[:M] = first_elements.expand(-1, N) + + return output + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('M, N', shapes_common) +@pytest.mark.parametrize('BLOCK_SIZE_N', block_size) +def test_linearize_mask_broadcast(M, N, BLOCK_SIZE_N, dtype, sigtype): + + in_tensor = torch.randn(2 * M, N, dtype=dtype).npu() + + triton_output = torch.zeros_like(in_tensor) + + grid = (ceil_div(2 * M * N, BLOCK_SIZE_N), ) + + linearize_mask_broadcast_kernel[grid](in_tensor, triton_output, N=N, M=M, BLOCK_SIZE_N=BLOCK_SIZE_N) + + torch_output = torch_linearize_mask_broadcast(in_tensor.clone()) + assert torch.allclose(triton_output, torch_output, rtol=1e-5, atol=1e-8) + + +def triton_linearize_mask_broadcast(in_tensor, BLOCK_SIZE): + M = in_tensor.shape[0] // 2 + N = in_tensor.shape[1] + + triton_output = torch.zeros_like(in_tensor) + grid = (ceil_div(2 * M * N, BLOCK_SIZE), ) + + linearize_mask_broadcast_kernel[grid](in_tensor, triton_output, N=N, M=M, BLOCK_SIZE_N=BLOCK_SIZE) + + +@triton.jit +def rem_kernel(in_ptr0, in_ptr1, out_ptr, N: tl.constexpr, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(0) + x = tl.arange(0, BLOCK_SIZE) + + base_offset = pid * BLOCK_SIZE + x + + rem_result = base_offset % 128 + mask = rem_result < 64 + + tmp0 = tl.load(in_ptr0 + base_offset, mask=mask, other=0.0) + tmp1 = tl.load(in_ptr1 + base_offset, mask=mask, other=0.0) + tmp2 = tmp0 + tmp1 + tl.store(out_ptr + base_offset, tmp2, mask=mask) + + +def test_linearize_mask_rem(): + N = 1024 + BLOCK_SIZE = 256 + dtype = 'float32' + shape = (N, ) + + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + triton_res = torch.zeros(shape).npu() + + grid = (ceil_div(N, BLOCK_SIZE), ) + rem_kernel[grid](x0, x1, triton_res, N, BLOCK_SIZE=BLOCK_SIZE) + + base_offsets = torch.arange(N).npu() + rem_results = base_offsets % 128 + mask_bool = rem_results < 64 + + torch_res = torch.zeros((N, )).npu() + torch_res[mask_bool] = x0[mask_bool] + x1[mask_bool] + + test_common.validate_cmp(dtype, triton_res, torch_res) + + +def profile_performance_test(M, N, dtype, BLOCK_SIZE): + print(f"\nDetailed performance analysis: M={M}, N={N}, dtype={dtype}, block_size={BLOCK_SIZE}") + + in_tensor = torch.randn(2 * M, N, dtype=dtype).npu() + + def wrapper_func(x): + triton_linearize_mask_broadcast(x, BLOCK_SIZE=BLOCK_SIZE) + + # Run performance analysis + profiler_wrapper(wrapper_func, in_tensor) + + +if __name__ == "__main__": + print("Broadcast Kernel Performance Test Suite") + print("Function: Broadcast first element of first M rows, set remaining M rows to zero") + + # Optional: Run detailed profiler test (specific configuration) + profile_performance_test(512, 512, torch.float32, BLOCK_SIZE=1024) + + print("\n" + "=" * 80) + print("Test completed!") + print(f"Detailed performance analysis results saved in: ./result_profiling_broadcast/") + print("=" * 80) diff --git a/third_party/ascend/unittest/pytest_ut/test_linearize_mask_fallback.py b/third_party/ascend/unittest/pytest_ut/test_linearize_mask_fallback.py new file mode 100644 index 0000000000..58fd5cee4f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_linearize_mask_fallback.py @@ -0,0 +1,127 @@ +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +types_all = [ + (torch.float32, 'float32'), +] + +shapes_common = [(128, 256), (127, 256), (127, 16), (129, 256), (77, 1024), (69, 512)] + +block_size = [128, 256, 1024] + + +def ceil_div(a, b): + return (a + b - 1) // b + + +def profiler_wrapper(fn, *args): + result_path = "./result_profiling_mask_fallback" + skip_first = 10 + wait = 0 + warmup = 3 + active = 30 + repeat = 1 + stream = torch.npu.current_stream() + experimental_config = torch_npu.profiler._ExperimentalConfig( + aic_metrics=torch_npu.profiler.AiCMetrics.PipeUtilization, + profiler_level=torch_npu.profiler.ProfilerLevel.Level1, l2_cache=False, data_simplification=False) + with torch_npu.profiler.profile( + activities=[torch_npu.profiler.ProfilerActivity.CPU, torch_npu.profiler.ProfilerActivity.NPU], + schedule=torch_npu.profiler.schedule(wait=wait, warmup=warmup, active=active, repeat=repeat, + skip_first=skip_first), + on_trace_ready=torch_npu.profiler.tensorboard_trace_handler(result_path), record_shapes=True, + profile_memory=False, with_stack=False, with_flops=False, with_modules=False, + experimental_config=experimental_config) as prof: + stream.synchronize() + for _ in range(skip_first + (wait + warmup + active) * repeat): + fn(*args) + prof.step() + stream.synchronize() + + +@triton.jit +def linearize_mask_broadcast_kernel( + in_ptr, + output_ptr, + N: tl.constexpr, + M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, +): + pid = tl.program_id(axis=0) + offset = tl.multiple_of(pid * BLOCK_SIZE_N, N) + x1 = (offset + tl.arange(0, BLOCK_SIZE_N)) + x2 = x1 // N + mask1 = (x1 < M * N) + data = tl.load(in_ptr + x2 * N, mask=mask1, other=0) + x3 = pid * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N) + tl.store(output_ptr + x3, data) + + +def torch_linearize_mask_broadcast(in_tensor): + M = in_tensor.shape[0] // 2 + N = in_tensor.shape[1] + + output = torch.zeros_like(in_tensor) + + first_elements = in_tensor[:M, 0:1] + output[:M] = first_elements.expand(-1, N) + + return output + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('M, N', shapes_common) +@pytest.mark.parametrize('BLOCK_SIZE_N', block_size) +def test_linearize_mask_broadcast(M, N, BLOCK_SIZE_N, dtype, sigtype): + + in_tensor = torch.randn(2 * M, N, dtype=dtype).npu() + + triton_output = torch.zeros_like(in_tensor) + + grid = (ceil_div(2 * M * N, BLOCK_SIZE_N), ) + + linearize_mask_broadcast_kernel[grid](in_tensor, triton_output, N=N, M=M, BLOCK_SIZE_N=BLOCK_SIZE_N) + + torch_output = torch_linearize_mask_broadcast(in_tensor.clone()) + assert torch.allclose(triton_output, torch_output, rtol=1e-5, atol=1e-8) + + +def triton_linearize_mask_broadcast(in_tensor, BLOCK_SIZE): + M = in_tensor.shape[0] // 2 + N = in_tensor.shape[1] + + triton_output = torch.zeros_like(in_tensor) + grid = (ceil_div(2 * M * N, BLOCK_SIZE), ) + + linearize_mask_broadcast_kernel[grid](in_tensor, triton_output, N=N, M=M, BLOCK_SIZE_N=BLOCK_SIZE) + + +def profile_performance_test(M, N, dtype, BLOCK_SIZE): + print(f"\nDetailed performance analysis: M={M}, N={N}, dtype={dtype}, block_size={BLOCK_SIZE}") + + in_tensor = torch.randn(2 * M, N, dtype=dtype).npu() + + def wrapper_func(x): + triton_linearize_mask_broadcast(x, BLOCK_SIZE=BLOCK_SIZE) + + # Run performance analysis + profiler_wrapper(wrapper_func, in_tensor) + + +if __name__ == "__main__": + print("mask fallback Kernel Performance Test Suite") + print("Function: Broadcast first element of first M rows, set remaining M rows to zero") + + # Optional: Run detailed profiler test (specific configuration) + profile_performance_test(512, 512, torch.float32, BLOCK_SIZE=1024) + + print("\n" + "=" * 80) + print("Test completed!") + print(f"Detailed performance analysis results saved in: ./result_profiling_mask_fallback/") + print("=" * 80) diff --git a/third_party/ascend/unittest/pytest_ut/test_linearize_permute.py b/third_party/ascend/unittest/pytest_ut/test_linearize_permute.py new file mode 100644 index 0000000000..8aaa295f52 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_linearize_permute.py @@ -0,0 +1,517 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +types_all = [ + (torch.float32, 'float32'), +] + +shapes_common = [ + (8, 2048, 4), +] + + +def foo(a, b, shape): + y = a.reshape(shape) + y = y.permute(0, 2, 1) + b + return y + + +def ceil_div(a, b): + return (a + b - 1) // b + + +@triton.jit +def triton_gpu(in_ptr0, in_ptr1, out_ptr, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr, + SHAPE0: tl.constexpr, SHAPE1: tl.constexpr, SHAPE2: tl.constexpr): + yoffset = tl.program_id(1) * (tl.program_id(2) + 1) * YBLOCK + yindex = yoffset + tl.arange(0, YBLOCK)[None, :] # (1, YBLOCK) + ymask = yindex < ynumel + xoffset = tl.program_id(0) * XBLOCK + xindex = xoffset + tl.arange(0, XBLOCK)[:, None] # (XBLOCK, 1) + xmask = xindex < xnumel + x2 = xindex # (XBLOCK, 1) + y3 = yindex # (1, YBLOCK) + y0 = yindex % SHAPE1 # (1, YBLOCK) + y1 = (yindex // SHAPE1) # (1, YBLOCK) + tmp0 = tl.load(in_ptr0 + (x2 + (SHAPE2 * y3)), xmask) # (XBLOCK, YBLOCK) + tmp1 = tl.load(in_ptr1 + (y0 + (SHAPE1 * x2) + (SHAPE1 * SHAPE2 * y1)), xmask) # (XBLOCK, YBLOCK) + tmp2 = tmp0 + tmp1 + tl.store(out_ptr + (x2 + (SHAPE2 * y3)), tmp2, xmask) + + +@triton.jit +def k_load_perm_select(ptr, out, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr): + y = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK)[None, :] + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] + xmask = x < xnumel + ymask = y < ynumel + bad_mask = xmask | ymask + val = tl.load(ptr + (x + 4 * y), bad_mask) + tl.store(out + (x + 4 * y), val, xmask & ymask) + + +@triton.jit +def k_store_perm_select(in_ptr, out_ptr, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr): + y = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK)[None, :] + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] + xmask = x < xnumel + ymask = y < ynumel + bad_mask = xmask | ymask + val = tl.load(in_ptr + (x + 4 * y), xmask & ymask) + tl.store(out_ptr + (x + 4 * y), val, bad_mask) + + +@triton.jit +def k_load_moddiv_noperm(in_ptr, out_ptr, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr, + SHAPE0: tl.constexpr, SHAPE1: tl.constexpr, SHAPE2: tl.constexpr): + pid_y = tl.program_id(1) + pid_x = tl.program_id(0) + + yindex = pid_y * YBLOCK + tl.arange(0, YBLOCK) # (YBLOCK,) + xindex = pid_x * XBLOCK + tl.arange(0, XBLOCK) # (XBLOCK,) + + # 2D tile:[YBLOCK, XBLOCK] + y = yindex[:, None] # (YBLOCK, 1) + x = xindex[None, :] # (1, XBLOCK) + + mask = y < ynumel + + z = (yindex // SHAPE1)[:, None] # (YBLOCK, 1) + y_ = (yindex % SHAPE1)[:, None] # (YBLOCK, 1) + y_linear = z * SHAPE1 + y_ # (YBLOCK, 1) + offset_load = x + SHAPE2 * y_linear # (YBLOCK, XBLOCK) + val = tl.load(in_ptr + offset_load, mask=mask) + + offset_store = x + SHAPE2 * y # (YBLOCK, XBLOCK) + tl.store(out_ptr + offset_store, val, mask=mask) + + +@triton.jit +def k_store_moddiv_noperm(in_ptr, out_ptr, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr, + SHAPE0: tl.constexpr, SHAPE1: tl.constexpr, SHAPE2: tl.constexpr): + pid_y = tl.program_id(1) + pid_x = tl.program_id(0) + + yindex = pid_y * YBLOCK + tl.arange(0, YBLOCK) # (YBLOCK,) + xindex = pid_x * XBLOCK + tl.arange(0, XBLOCK) # (XBLOCK,) + + y = yindex[:, None] # (YBLOCK, 1) + x = xindex[None, :] # (1, XBLOCK) + + mask = (y < ynumel) & (x < xnumel) + + offset_load = x + SHAPE2 * y # (YBLOCK, XBLOCK) + val = tl.load(in_ptr + offset_load, mask=mask) + + z = (yindex // SHAPE1)[:, None] # (YBLOCK, 1) + y_ = (yindex % SHAPE1)[:, None] # (YBLOCK, 1) + y_linear = z * SHAPE1 + y_ # (YBLOCK, 1) + offset_store = x + SHAPE2 * y_linear # (YBLOCK, XBLOCK) + tl.store(out_ptr + offset_store, val, mask=mask) + + +@triton.jit +def k_load_moddiv_perm(in_ptr, out_ptr, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr, + SHAPE0: tl.constexpr, SHAPE1: tl.constexpr, SHAPE2: tl.constexpr): + pid_y = tl.program_id(1) + pid_x = tl.program_id(0) + + yindex = pid_y * YBLOCK + tl.arange(0, YBLOCK) # (YBLOCK,) + xindex = pid_x * XBLOCK + tl.arange(0, XBLOCK) # (XBLOCK,) + + X = xindex[:, None] # (XBLOCK, 1) + Y = yindex[None, :] # (1, YBLOCK) + mask_load = (X < xnumel) & (Y < ynumel) # (XBLOCK, YBLOCK) + + z = (yindex // SHAPE1)[None, :] # (1, YBLOCK) + y_ = (yindex % SHAPE1)[None, :] # (1, YBLOCK) + offset_load = X + SHAPE2 * y_ + SHAPE2 * SHAPE1 * z # (XBLOCK, YBLOCK) + val = tl.load(in_ptr + offset_load, mask=mask_load) # (XBLOCK, YBLOCK) + + y2 = yindex[:, None] # (YBLOCK, 1) + x2 = xindex[None, :] # (1, XBLOCK) + mask_store = (y2 < ynumel) & (x2 < xnumel) # (YBLOCK, XBLOCK) + offset_store = x2 + SHAPE2 * y2 # (YBLOCK, XBLOCK) + + tl.store(out_ptr + offset_store, val.permute(1, 0), mask=mask_store) + + +@triton.jit +def k_store_moddiv_perm(in_ptr, out_ptr, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr, + SHAPE0: tl.constexpr, SHAPE1: tl.constexpr, SHAPE2: tl.constexpr): + pid_y = tl.program_id(1) + pid_x = tl.program_id(0) + + yindex = pid_y * YBLOCK + tl.arange(0, YBLOCK) # (YBLOCK,) + xindex = pid_x * XBLOCK + tl.arange(0, XBLOCK) # (XBLOCK,) + + y = yindex[:, None] # (YBLOCK, 1) + x = xindex[None, :] # (1, XBLOCK) + mask_load = (y < ynumel) & (x < xnumel) + + offset_load = x + SHAPE2 * y # (YBLOCK, XBLOCK) + val = tl.load(in_ptr + offset_load, mask=mask_load) # (YBLOCK, XBLOCK) + + X = xindex[:, None] # (XBLOCK, 1) + Y = yindex[None, :] # (1, YBLOCK) + mask_store = (X < xnumel) & (Y < ynumel) # (XBLOCK, YBLOCK) + + z = Y // SHAPE1 # (1, YBLOCK) + y_ = Y % SHAPE1 # (1, YBLOCK) + offset_store = X + SHAPE2 * y_ + SHAPE2 * SHAPE1 * z # (XBLOCK, YBLOCK) + + tl.store(out_ptr + offset_store, val.permute(1, 0), mask=mask_store) + + +@triton.jit +def k_load_store_moddiv_noperm(in_ptr, out_ptr, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr, + SHAPE0: tl.constexpr, SHAPE1: tl.constexpr, SHAPE2: tl.constexpr): + pid_y = tl.program_id(1) + pid_x = tl.program_id(0) + + yindex = pid_y * YBLOCK + tl.arange(0, YBLOCK) # (YBLOCK,) + xindex = pid_x * XBLOCK + tl.arange(0, XBLOCK) # (XBLOCK,) + + y = yindex[:, None] # (YBLOCK, 1) + x = xindex[None, :] # (1, XBLOCK) + + mask = (y < ynumel) & (x < xnumel) + + z = (yindex // SHAPE1)[:, None] # (YBLOCK, 1) + y_ = (yindex % SHAPE1)[:, None] # (YBLOCK, 1) + y_linear = z * SHAPE1 + y_ # (YBLOCK, 1) + + offset = x + SHAPE2 * y_linear # (YBLOCK, XBLOCK) + + val = tl.load(in_ptr + offset, mask=mask) + val = val + 2 + tl.store(out_ptr + offset, val, mask=mask) + + +@triton.jit +def k_load_store_moddiv_perm(in_ptr, out_ptr, ynumel, xnumel, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr, + SHAPE0: tl.constexpr, SHAPE1: tl.constexpr, SHAPE2: tl.constexpr): + # Program ID + pid_y = tl.program_id(1) + pid_x = tl.program_id(0) + + # Block index calculation + yindex = pid_y * YBLOCK + tl.arange(0, YBLOCK) # (YBLOCK,) + xindex = pid_x * XBLOCK + tl.arange(0, XBLOCK) # (XBLOCK,) + + # Broadcasting to 2D + x = xindex[:, None] # (XBLOCK, 1) + y = yindex[None, :] # (1, YBLOCK) + + # Valid mask + mask = (x < xnumel) & (y < ynumel) + + # Simulate linear index back to 3D + z = y // SHAPE1 # (1, YBLOCK) + y_ = y % SHAPE1 # (1, YBLOCK) + + # compute input offset (simulate [z, y_, x] access in strided format) + offset = x + SHAPE2 * y_ + SHAPE2 * SHAPE1 * z # (XBLOCK, YBLOCK) + + # load with implicit transpose (will be interpreted as [Y, X] then transposed) + val = tl.load(in_ptr + offset, mask=mask) + + # apply some dummy operation + val = val + 1 + + # store it back to out_ptr with same offset + tl.store(out_ptr + offset, val, mask=mask) + + +@triton.jit +def k_load_perm_scalar_ref(in_ptr, out_ptr, y1_numel, y0_numel, x2_numel, Y1BLOCK: tl.constexpr, Y0BLOCK: tl.constexpr, + Y0BLOCK_SUB: tl.constexpr, X2BLOCK_SUB: tl.constexpr): + pid_y1 = tl.program_id(0) + pid_y0 = tl.program_id(1) + + y1_scalar = pid_y1 * Y1BLOCK + + base_y0 = tl.arange(0, Y0BLOCK_SUB) + base_x2 = tl.arange(0, X2BLOCK_SUB) + + y0_offset = pid_y0 * Y0BLOCK + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + + for loop_y0 in range(loops_y0): + y0_0 = y0_offset + loop_y0 * Y0BLOCK_SUB + base_y0[:, None] # (Y0BLOCK_SUB, 1) + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[None, :] # (1, Y0BLOCK_SUB) + x2_1 = base_x2[None, :] # (1, X2BLOCK_SUB) + x2 = base_x2[:, None] # (X2BLOCK_SUB, 1) + y0_0_mask = y0_0 < min(Y0BLOCK + y0_offset, y0_numel) + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + val0 = tl.load(in_ptr + (y0 + 8 * x2 + 8 * 16 * y1_scalar), mask=y0_mask) + tl.store(out_ptr + (x2_1 + 16 * y0_0 + 8 * 16 * y1_scalar), val0.permute([1, 0]), mask=y0_0_mask) + + +@triton.jit +def k_load_perm_scalar(in_ptr, out_ptr, y1_numel, y0_numel, x2_numel, Y1BLOCK: tl.constexpr, Y0BLOCK: tl.constexpr, + Y0BLOCK_SUB: tl.constexpr, X2BLOCK_SUB: tl.constexpr): + pid_y1 = tl.program_id(0) + pid_y0 = tl.program_id(1) + + y1_scalar = pid_y1 * Y1BLOCK + + base_y0 = tl.arange(0, Y0BLOCK_SUB) + base_x2 = tl.arange(0, X2BLOCK_SUB) + + y0_offset = pid_y0 * Y0BLOCK + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + + for loop_y0 in range(loops_y0): + y0_0 = y0_offset + loop_y0 * Y0BLOCK_SUB + base_y0[:, None] # (Y0BLOCK_SUB, 1) + x2_1 = base_x2[None, :] # (1, X2BLOCK_SUB) + y0_0_mask = y0_0 < min(Y0BLOCK + y0_offset, y0_numel) + val0 = tl.load(in_ptr + (y0_0 + 8 * x2_1 + 8 * 16 * y1_scalar), mask=y0_0_mask) + tl.store(out_ptr + (x2_1 + 16 * y0_0 + 8 * 16 * y1_scalar), val0, mask=y0_0_mask) + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('Z, Y, X', shapes_common) +def test_triton_gpu_kernel(Z, Y, X, dtype, sigtype): + shape = (Z, Y, X) + + a = test_common.generate_tensor(shape=(Z, Y * X), dtype=sigtype).npu() + b = test_common.generate_tensor(shape=(Z, X, Y), dtype=sigtype).npu() + + # must set device='npu' for empty_strided, do not use out.npu() later + out = torch.empty_strided((Z, X, Y), (X * Y, 1, X), device='npu', dtype=dtype) + + out_ref = foo(a, b, shape) + + ynumel = Z * Y + xnumel = X + XBLOCK = X + YBLOCK = 64 + + if ynumel % YBLOCK != 0: + pytest.skip(f"ynumel:{ynumel} not divisible by YBLOCK:{YBLOCK}") + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + triton_gpu[grid](a, b, out, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK, SHAPE0=Z, SHAPE1=Y, SHAPE2=X) + + test_common.validate_cmp(sigtype, out_ref, out) + + +@pytest.mark.parametrize('dtype,sigtype', types_all) +@pytest.mark.parametrize('xnumel, ynumel, XBLOCK, YBLOCK', [(3, 513, 4, 64)]) +def test_k_load_perm_select(xnumel, ynumel, XBLOCK, YBLOCK, dtype, sigtype): + + in_ptr = test_common.generate_tensor(shape=(ynumel * 4, ), dtype=sigtype).npu() + out_ptr = torch.zeros_like(in_ptr) + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + k_load_perm_select[grid](in_ptr, out_ptr, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK) + + out_ref = torch.zeros_like(out_ptr) + y_idx = torch.arange(ynumel).unsqueeze(1).npu() # [ynumel, 1] + x_idx = torch.arange(xnumel).unsqueeze(0).npu() # [1, xnumel] + idx = (x_idx + 4 * y_idx).reshape(-1) # [ynumel * xnumel] + out_ref[idx] = in_ptr[idx] + torch.testing.assert_close(out_ptr[idx], out_ref[idx]) + + +@pytest.mark.parametrize('dtype,sigtype', types_all) +@pytest.mark.parametrize('xnumel, ynumel, XBLOCK, YBLOCK', [(3, 513, 4, 64)]) +def test_k_store_perm_select(xnumel, ynumel, XBLOCK, YBLOCK, dtype, sigtype): + in_ptr = test_common.generate_tensor(shape=(ynumel * 4, ), dtype=sigtype).npu() + out_ptr = torch.zeros_like(in_ptr) + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + k_store_perm_select[grid]( + in_ptr, + out_ptr, + ynumel, + xnumel, + YBLOCK=YBLOCK, + XBLOCK=XBLOCK, + ) + + out_ref = torch.zeros_like(out_ptr) + y_idx = torch.arange(ynumel).unsqueeze(1).npu() + x_idx = torch.arange(xnumel).unsqueeze(0).npu() + idx = (x_idx + 4 * y_idx).reshape(-1) + out_ref[idx] = in_ptr[idx] + torch.testing.assert_close(out_ptr[idx], out_ref[idx]) + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('Z, Y, X', shapes_common) +def test_k_load_moddiv_noperm(Z, Y, X, dtype, sigtype): + a = test_common.generate_tensor(shape=(Z, Y, X), dtype=sigtype).npu() + in_flat = a.contiguous().view(-1) + out = torch.zeros_like(in_flat) + + ynumel = Z * Y + xnumel = X + XBLOCK = X + YBLOCK = 64 + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + k_load_moddiv_noperm[grid](in_flat, out, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK, SHAPE0=Z, SHAPE1=Y, SHAPE2=X) + + torch.testing.assert_close(out, in_flat) + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('Z, Y, X', shapes_common) +def test_k_store_moddiv_noperm(Z, Y, X, dtype, sigtype): + a = test_common.generate_tensor(shape=(Z, Y, X), dtype=sigtype).npu() + in_flat = a.contiguous().view(-1) + out = torch.zeros_like(in_flat) + + ynumel = Z * Y + xnumel = X + XBLOCK = X + YBLOCK = 64 + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + k_store_moddiv_noperm[grid](in_flat, out, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK, SHAPE0=Z, SHAPE1=Y, + SHAPE2=X) + + torch.testing.assert_close(out, in_flat) + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('Z, Y, X', shapes_common) +def test_k_load_moddiv_perm(Z, Y, X, dtype, sigtype): + a = test_common.generate_tensor(shape=(Z, Y, X), dtype=sigtype).npu() + in_flat = a.contiguous().view(-1) + out = torch.zeros_like(in_flat) + + ynumel = Z * Y + xnumel = X + XBLOCK = X + YBLOCK = 64 + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + k_load_moddiv_perm[grid](in_flat, out, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK, SHAPE0=Z, SHAPE1=Y, SHAPE2=X) + + torch.testing.assert_close(out, in_flat) + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('Z, Y, X', shapes_common) +def test_k_store_moddiv_perm(Z, Y, X, dtype, sigtype): + a = test_common.generate_tensor(shape=(Z, Y, X), dtype=sigtype).npu() + in_flat = a.contiguous().view(-1) + out = torch.zeros_like(in_flat) + + ynumel = Z * Y + xnumel = X + XBLOCK = X + YBLOCK = 64 + + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + k_store_moddiv_perm[grid](in_flat, out, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK, SHAPE0=Z, SHAPE1=Y, SHAPE2=X) + + torch.testing.assert_close(out, in_flat) + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('Z, Y, X', shapes_common) +def test_k_load_store_moddiv_noperm(Z, Y, X, dtype, sigtype): + a = test_common.generate_tensor(shape=(Z, Y, X), dtype=sigtype).npu() + in_flat = a.contiguous().view(-1) + out = torch.empty_like(in_flat) + + ynumel = Z * Y + xnumel = X + XBLOCK = 4 + YBLOCK = 256 + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + k_load_store_moddiv_noperm[grid](in_flat, out, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK, SHAPE0=Z, SHAPE1=Y, + SHAPE2=X) + + ref = (a + 2).contiguous().view(-1) + torch.testing.assert_close(out, ref) + + +@pytest.mark.parametrize('dtype, sigtype', types_all) +@pytest.mark.parametrize('Z, Y, X', shapes_common) +def test_k_load_store_moddiv_perm(Z, Y, X, dtype, sigtype): + shape = (Z, Y, X) + numel = Z * Y * X + + a = test_common.generate_tensor(shape, dtype=sigtype).npu() + a_flat = a.contiguous().view(-1) + + out = torch.zeros_like(a_flat) + + ynumel = Z * Y + xnumel = X + XBLOCK = 4 + YBLOCK = 256 + grid = (ceil_div(xnumel, XBLOCK), ceil_div(ynumel, YBLOCK), 1) + + k_load_store_moddiv_perm[grid](a_flat, out, ynumel, xnumel, YBLOCK=YBLOCK, XBLOCK=XBLOCK, SHAPE0=Z, SHAPE1=Y, + SHAPE2=X) + + a_reshaped = a + 1 + out_ref = a_reshaped.contiguous().view(-1) + + torch.testing.assert_close(out, out_ref) + + +@pytest.mark.parametrize('dtype', [torch.float32]) +@pytest.mark.parametrize('y1_numel, y0_numel, x2_numel, Y1BLOCK, Y0BLOCK, Y0BLOCK_SUB, X2BLOCK_SUB', [ + (2, 8, 16, 1, 8, 4, 16), +]) +def test_k_load_perm_scalar(y1_numel, y0_numel, x2_numel, Y1BLOCK, Y0BLOCK, Y0BLOCK_SUB, X2BLOCK_SUB, dtype): + in_ptr = torch.randn((y0_numel * 8, x2_numel), dtype=dtype, device='npu') + + out_ptr_triton_ref = torch.zeros((y1_numel, y0_numel, x2_numel), dtype=dtype, device='npu') + out_ptr_triton = torch.zeros((y1_numel, y0_numel, x2_numel), dtype=dtype, device='npu') + + grid = (ceil_div(y1_numel, Y1BLOCK), ceil_div(y0_numel, Y0BLOCK)) + + k_load_perm_scalar_ref[grid](in_ptr=in_ptr, out_ptr=out_ptr_triton_ref, y1_numel=y1_numel, y0_numel=y0_numel, + x2_numel=x2_numel, Y1BLOCK=Y1BLOCK, Y0BLOCK=Y0BLOCK, Y0BLOCK_SUB=Y0BLOCK_SUB, + X2BLOCK_SUB=X2BLOCK_SUB) + + k_load_perm_scalar[grid]( + in_ptr=in_ptr, + out_ptr=out_ptr_triton, + y1_numel=y1_numel, + y0_numel=y0_numel, + x2_numel=x2_numel, + Y1BLOCK=Y1BLOCK, + Y0BLOCK=Y0BLOCK, + Y0BLOCK_SUB=Y0BLOCK_SUB, + X2BLOCK_SUB=X2BLOCK_SUB, + ) + + torch.testing.assert_close(out_ptr_triton, out_ptr_triton_ref, rtol=1e-5, atol=1e-6) diff --git a/third_party/ascend/unittest/pytest_ut/test_load.py b/third_party/ascend/unittest/pytest_ut/test_load.py new file mode 100644 index 0000000000..eccad4d8db --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_load.py @@ -0,0 +1,207 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + +# eg: pytest -v test.py::test_add +############################# + + +@triton.jit +def triton_load_store(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp2 = tmp0 + tl.store(out_ptr0 + (xindex), tmp2, xmask) + + +# require: all data (4d and 5d) can be placed into but without ub overflow +@triton.jit +def triton_load_store_multi_d(in_ptr0, out_ptr0, BLOCK_0: tl.constexpr, BLOCK_1: tl.constexpr, BLOCK_2: tl.constexpr, + BLOCK_3: tl.constexpr, BLOCK_4: tl.constexpr, SHAPE_0: tl.constexpr, + SHAPE_1: tl.constexpr, SHAPE_2: tl.constexpr, SHAPE_3: tl.constexpr, + SHAPE_4: tl.constexpr, STRIDE_0: tl.constexpr, STRIDE_1: tl.constexpr, + STRIDE_2: tl.constexpr, STRIDE_3: tl.constexpr, STRIDE_4: tl.constexpr): + offsets = tl.program_id(0) + + offsets = offsets + tl.arange(0, BLOCK_0) * STRIDE_0 + masks = tl.arange(0, BLOCK_0) < SHAPE_0 + if (BLOCK_1 * BLOCK_2 * BLOCK_3 * BLOCK_4) > 1: + offsets = offsets[:, None] + tl.arange(0, BLOCK_1)[None, :] * STRIDE_1 + masks = masks[:, None] & (tl.arange(0, BLOCK_1)[None, :] < SHAPE_1) + if (BLOCK_2 * BLOCK_3 * BLOCK_4) > 1: + offsets = offsets[:, :, None] + tl.arange(0, BLOCK_2)[None, None, :] * STRIDE_2 + masks = masks[:, :, None] & (tl.arange(0, BLOCK_2)[None, None, :] < SHAPE_2) + if (BLOCK_3 * BLOCK_4) > 1: + offsets = offsets[:, :, :, None] + tl.arange(0, BLOCK_3)[None, None, None, :] * STRIDE_3 + masks = masks[:, :, :, None] & (tl.arange(0, BLOCK_3)[None, None, None, :] < SHAPE_3) + if BLOCK_4 > 1: + offsets = offsets[:, :, :, :, None] + tl.arange(0, BLOCK_4)[None, None, None, None, :] * STRIDE_4 + masks = masks[:, :, :, :, None] & (tl.arange(0, BLOCK_4)[None, None, None, None, :] < SHAPE_4) + + tmp_in = tl.load(in_ptr0 + offsets, masks) + tmp_out = tmp_in + tl.store(out_ptr0 + offsets, tmp_out, masks) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + ['float32', (8, 8, 4), 2, 128, 64], + ['float16', (8, 8, 4), 2, 128, 64], + ['int8', (8, 8, 4), 2, 128, 64], + ['int8', (8, 7, 4), 2, 128, 64], +]) +def test_load_store(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = x0 + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_load_store[(ncore, )](x0, y_cal, x0.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (8, 4, 16, 16)], + ['float16', (8, 4, 16, 16)], + ['int8', (8, 4, 16, 16)], + ['float32', (8, 8, 4, 4)], + ['float16', (8, 8, 4, 4)], + ['int8', (8, 8, 4, 4)], + ['float32', (3, 8, 2, 16, 16)], + ['float16', (3, 8, 2, 16, 16)], + ['float32', (11, 8, 8, 4, 4)], + ['float16', (11, 8, 8, 4, 4)], + ['int8', (11, 8, 8, 4, 4)], +]) +def test_load_store_multi_d(param_list): + dtype, shape = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_expect = x0 + y_actual = test_common.generate_tensor(shape, dtype).npu() + + blocks = list(x0.size()) + shapes = list(x0.stride()) + while len(blocks) < 5: + blocks.append(1) + shapes.append(1) + triton_load_store_multi_d[(1, )](x0, y_actual, *blocks, *blocks, *shapes) + test_common.validate_cmp(dtype, y_actual, y_expect) + + +@triton.jit +def triton_load_other_mask(in_ptr, out_ptr, N, BLOCK: tl.constexpr): + offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + mask = offs < N + x = tl.load(in_ptr + offs, mask=mask, other=mask) + tl.store(out_ptr + offs, x, mask=mask) + + +@triton.jit +def triton_load_other_scalar(in_ptr, out_ptr, N, BLOCK: tl.constexpr): + offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + mask = offs < N + x = tl.load(in_ptr + offs, mask=mask, other=0.0) + tl.store(out_ptr + offs, x, mask=mask) + + +@triton.jit +def triton_load_other_tensor(in_ptr, out_ptr, other_ptr, N, BLOCK: tl.constexpr): + offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + mask = offs < N + other = tl.load(other_ptr + offs) + x = tl.load(in_ptr + offs, mask=mask, other=other) + tl.store(out_ptr + offs, x) + + +@triton.jit +def triton_load_other_tensor_discrete_mask(in_ptr, out_ptr, other_ptr, N, BLOCK: tl.constexpr): + offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + mask = (offs % 2) == 0 + other = tl.load(other_ptr + offs) + x = tl.load(in_ptr + offs, mask=mask, other=other) + tl.store(out_ptr + offs, x) + + +@pytest.mark.parametrize('param_list', [ + ['float32', 1000, 128], + ['int8', 1000, 128], +]) +def test_load_with_other_mask(param_list): + dtype, n, block = param_list + x = test_common.generate_tensor((n, ), dtype).npu() + out_mask = torch.empty(n, device='npu', dtype=getattr(torch, dtype)) + out_zero = torch.empty(n, device='npu', dtype=getattr(torch, dtype)) + grid = (triton.cdiv(n, block), ) + triton_load_other_mask[grid](x, out_mask, n, block) + triton_load_other_scalar[grid](x, out_zero, n, block) + test_common.validate_cmp(dtype, out_mask, x) + test_common.validate_cmp(dtype, out_zero, x) + test_common.validate_cmp(dtype, out_mask, out_zero) + + +@pytest.mark.parametrize('param_list', [ + ['float32', 1000, 128], + ['int8', 1000, 128], +]) +def test_load_with_other_tensor(param_list): + dtype, n, block = param_list + grid = (triton.cdiv(n, block), ) + total = grid[0] * block + torch_dtype = getattr(torch, dtype) + x = test_common.generate_tensor((n, ), dtype).npu() + x_pad = torch.zeros(total, device='npu', dtype=torch_dtype) + x_pad[:n] = x + other = test_common.generate_tensor((total, ), dtype).npu() + out = torch.empty(total, device='npu', dtype=torch_dtype) + triton_load_other_tensor[grid](x_pad, out, other, n, block) + ref = other.clone() + ref[:n] = x + test_common.validate_cmp(dtype, out, ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', 1000, 128], + ['int8', 1000, 128], +]) +def test_load_with_other_tensor_discrete_mask(param_list): + dtype, n, block = param_list + grid = (triton.cdiv(n, block), ) + total = grid[0] * block + torch_dtype = getattr(torch, dtype) + x = test_common.generate_tensor((total, ), dtype).npu() + other = test_common.generate_tensor((total, ), dtype).npu() + out = torch.empty(total, device='npu', dtype=torch_dtype) + triton_load_other_tensor_discrete_mask[grid](x, out, other, n, block) + offs = torch.arange(total, device='npu') + mask = (offs % 2) == 0 + ref = other.clone() + ref[mask] = x[mask] + test_common.validate_cmp(dtype, out, ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_load_broadcast.py b/third_party/ascend/unittest/pytest_ut/test_load_broadcast.py new file mode 100644 index 0000000000..6eed53f25b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_load_broadcast.py @@ -0,0 +1,144 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +@triton.jit +def kernel_load_diff_axis_broadcast( + X_ptr, + Out_ptr, + M, + XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, +): + offsetx = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK) # [XBLOCK] + offsety = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK) # [YBLOCK] + + ptr = X_ptr + offsety[None, :] + mask = (offsetx < M)[:, None] + + val = tl.load(ptr, mask, other=0.0) + offsets = offsetx[:, None] * YBLOCK + offsety[None, :] + tl.store(Out_ptr + offsets, val) + + +def torch_load_diff_axis_broadcast(X, M, XBLOCK, YBLOCK): + out = torch.zeros((XBLOCK, YBLOCK), dtype=X.dtype) + if M > 0: + out[:min(M, XBLOCK), :] = X[0, :] + return out + + +@pytest.mark.parametrize('param_list', [ + ['float32', 140, 256, 64], + ['float32', 1, 256, 64], + ['float32', 256, 256, 64], +]) +def test_load_diff_axis_broadcast_other(param_list): + dtype, M, XBLOCK, YBLOCK = param_list + x = test_common.generate_tensor((1, YBLOCK), dtype).npu() + out = torch.zeros((XBLOCK, YBLOCK), dtype=eval('torch.' + dtype)).npu() + ref = torch_load_diff_axis_broadcast(x.cpu(), M, XBLOCK, YBLOCK) + + kernel_load_diff_axis_broadcast[(1, 1)](x, out, M, XBLOCK, YBLOCK) + + test_common.validate_cmp(dtype, out, ref) + + +@triton.jit +def kernel_load_diff_axis_broadcast_no_other( + X_ptr, + Out_ptr, + M, + XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, +): + offsetx = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK) # [XBLOCK] + offsety = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK) # [YBLOCK] + + ptr = X_ptr + offsety[None, :] + mask = (offsetx < M)[:, None] + + val = tl.load(ptr, mask) + offsets = offsetx[:, None] * YBLOCK + offsety[None, :] + tl.store(Out_ptr + offsets, val) + + +@pytest.mark.parametrize('param_list', [ + ['float32', 140, 256, 64], + ['float32', 256, 256, 64], +]) +def test_load_diff_axis_broadcast_no_other(param_list): + dtype, M, XBLOCK, YBLOCK = param_list + x = test_common.generate_tensor((1, YBLOCK), dtype).npu() + out = torch.zeros((XBLOCK, YBLOCK), dtype=eval('torch.' + dtype)).npu() + ref = torch_load_diff_axis_broadcast(x.cpu(), M, XBLOCK, YBLOCK) + + kernel_load_diff_axis_broadcast_no_other[(1, 1)](x, out, M, XBLOCK, YBLOCK) + + test_common.validate_cmp(dtype, out, ref) + + +@triton.jit +def kernel_load_same_axis_broadcast( + X_ptr, + Out_ptr, + YM, + XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, +): + offsetx = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK) # [XBLOCK] + offsety = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK) # [YBLOCK] + + ptr = X_ptr + offsety[None, :] + mask = (offsety < YM)[None, :] + + val = tl.load(ptr, mask, other=0.0) + offsets = offsetx[:, None] * YBLOCK + offsety[None, :] + tl.store(Out_ptr + offsets, val) + + +def torch_load_same_axis_broadcast(X, YM, XBLOCK, YBLOCK): + out = torch.zeros((XBLOCK, YBLOCK), dtype=X.dtype) + out[:, :min(YM, YBLOCK)] = X[0, :min(YM, YBLOCK)] + return out + + +@pytest.mark.parametrize('param_list', [ + ['float32', 40, 256, 64], + ['float32', 64, 256, 64], +]) +def test_load_same_axis_broadcast(param_list): + dtype, YM, XBLOCK, YBLOCK = param_list + x = test_common.generate_tensor((1, YBLOCK), dtype).npu() + out = torch.zeros((XBLOCK, YBLOCK), dtype=eval('torch.' + dtype)).npu() + ref = torch_load_same_axis_broadcast(x.cpu(), YM, XBLOCK, YBLOCK) + + kernel_load_same_axis_broadcast[(1, 1)](x, out, YM, XBLOCK, YBLOCK) + + test_common.validate_cmp(dtype, out, ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_load_store.py b/third_party/ascend/unittest/pytest_ut/test_load_store.py new file mode 100644 index 0000000000..ea68eee469 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_load_store.py @@ -0,0 +1,150 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +@triton.jit +def triton_load_store(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + x_index = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = x_index < xnumel + tmp0 = tl.load(in_ptr0 + x_index, xmask) + tmp2 = tmp0 + tl.store(out_ptr0 + x_index, tmp2, xmask) + + +# require: all data (4d and 5d) can be placed into but without ub overflow +@triton.jit +def triton_load_store_multi_d(in_ptr0, out_ptr0, BLOCK_0: tl.constexpr, BLOCK_1: tl.constexpr, BLOCK_2: tl.constexpr, + BLOCK_3: tl.constexpr, BLOCK_4: tl.constexpr, SHAPE_0: tl.constexpr, + SHAPE_1: tl.constexpr, SHAPE_2: tl.constexpr, SHAPE_3: tl.constexpr, + SHAPE_4: tl.constexpr, STRIDE_0: tl.constexpr, STRIDE_1: tl.constexpr, + STRIDE_2: tl.constexpr, STRIDE_3: tl.constexpr, STRIDE_4: tl.constexpr): + offsets = tl.program_id(0) + + offsets = offsets + tl.arange(0, BLOCK_0) * STRIDE_0 + masks = tl.arange(0, BLOCK_0) < SHAPE_0 + if (BLOCK_1 * BLOCK_2 * BLOCK_3 * BLOCK_4) > 1: + offsets = offsets[:, None] + tl.arange(0, BLOCK_1)[None, :] * STRIDE_1 + masks = masks[:, None] & (tl.arange(0, BLOCK_1)[None, :] < SHAPE_1) + if (BLOCK_2 * BLOCK_3 * BLOCK_4) > 1: + offsets = offsets[:, :, None] + tl.arange(0, BLOCK_2)[None, None, :] * STRIDE_2 + masks = masks[:, :, None] & (tl.arange(0, BLOCK_2)[None, None, :] < SHAPE_2) + if (BLOCK_3 * BLOCK_4) > 1: + offsets = offsets[:, :, :, None] + tl.arange(0, BLOCK_3)[None, None, None, :] * STRIDE_3 + masks = masks[:, :, :, None] & (tl.arange(0, BLOCK_3)[None, None, None, :] < SHAPE_3) + if BLOCK_4 > 1: + offsets = offsets[:, :, :, :, None] + tl.arange(0, BLOCK_4)[None, None, None, None, :] * STRIDE_4 + masks = masks[:, :, :, :, None] & (tl.arange(0, BLOCK_4)[None, None, None, None, :] < SHAPE_4) + + tmp_in = tl.load(in_ptr0 + offsets, masks) + tmp_out = tmp_in + tl.store(out_ptr0 + offsets, tmp_out, masks) + + +@triton.jit +def triton_load_store_sle_mask(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + x_index = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = x_index <= xnumel + tmp0 = tl.load(in_ptr0 + x_index, xmask) + tmp2 = tmp0 + tl.store(out_ptr0 + x_index, tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + ['float32', (8, 8, 4), 2, 128, 64], + ['float16', (8, 8, 4), 2, 128, 64], + ['int8', (8, 8, 4), 2, 128, 64], +]) +def test_load_store(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + y_ref = x0 + # triton结果 + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_load_store[(ncore, )](x0, y_cal, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (8, 4, 16, 16)], + ['float16', (8, 4, 16, 16)], + ['int8', (8, 4, 16, 16)], + ['float32', (8, 8, 4, 4)], + ['float16', (8, 8, 4, 4)], + ['int8', (8, 8, 4, 4)], + ['float32', (3, 8, 2, 16, 16)], + ['float16', (3, 8, 2, 16, 16)], + ['float32', (11, 8, 8, 4, 4)], + ['float16', (11, 8, 8, 4, 4)], + ['int8', (11, 8, 8, 4, 4)], +]) +def test_load_store_multi_d(param_list): + # 生成数据 + dtype, shape = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + y_expect = x0 + y_actual = test_common.generate_tensor(shape, dtype).npu() + # triton结果 + blocks = list(x0.size()) + shapes = list(x0.stride()) + while len(blocks) < 5: + blocks.append(1) + shapes.append(1) + triton_load_store_multi_d[(1, )](x0, y_actual, *blocks, *blocks, *shapes) + # 比较结果 + test_common.validate_cmp(dtype, y_actual, y_expect) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + ['float32', (8, 8, 4), 2, 128, 64], + ['float16', (8, 8, 4), 2, 128, 64], + ['int8', (8, 8, 4), 2, 128, 64], +]) +def test_load_store_sle_mask(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + y_ref = x0 + # triton结果 + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_load_store_sle_mask[(ncore, )](x0, y_cal, x0.numel() - 1, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_load_strided.py b/third_party/ascend/unittest/pytest_ut/test_load_strided.py new file mode 100644 index 0000000000..75473aa6a0 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_load_strided.py @@ -0,0 +1,753 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Tests for SIMT IndirectLoad fast-path (TritonToLinalg / StridedLoadStoreRewrite). +# +# What each parameter row exercises against the V1 trigger condition +# (compileOn91095 + forceSimtTemplate, last-axis stride statically > 1, +# non-permuted layout, rank <= 5, not stride==2-even-size): +# +# * STRIDE > 2 / odd -> V1 should rewrite tt.load -> tt.indirect_load +# * STRIDE == 2, even size -> DeinterleaveStatusOptimization handles (V1 yields) +# * STRIDE == 2, odd size -> V1 should rewrite (deinterleave precondition fails) +# * Permuted layout -> ImplicitPermute handles (V1 must not touch) +# * Last-axis stride == 1 -> No rewrite (normal strided memref.copy) +# +# Verifies correctness only -- to confirm which path actually fired, dump IR +# with MLIR_ENABLE_DUMP=1 and grep for tt.indirect_load / tt.trans. + +import triton +import triton.language as tl +import torch +import pytest +import test_common + +try: + from triton.backends.ascend.utils import is_compile_on_910_95 +except Exception: + + def is_compile_on_910_95(_arch): + return False + + +a3_known_boundary_load_issue = pytest.mark.xfail( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason=("Known A3 baseline issue on release/3.2.2-dev: make_block_ptr " + "boundary_check + static power-of-two stride"), + strict=False, +) + +# --------------------------------------------------------------------------- +# 1D: out[i] = in[i * STRIDE] +# --------------------------------------------------------------------------- + + +@triton.jit +def kernel_1d_strided_compaction( + in_ptr, + out_ptr, + in_numel, + out_numel, + XBLOCK: tl.constexpr, + XBLOCK_SUB: tl.constexpr, + STRIDE: tl.constexpr, +): + pid = tl.program_id(0) + xoffset = pid * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB) + src_idx = xindex * STRIDE + # Two masks: store side bounded by out_numel, load side by in_numel. + store_mask = xindex < out_numel + load_mask = src_idx < in_numel + tmp = tl.load(in_ptr + src_idx, load_mask) + tl.store(out_ptr + xindex, tmp, store_mask) + + +def _ref_1d(src_cpu: torch.Tensor, stride: int, out_numel: int) -> torch.Tensor: + flat = src_cpu.flatten().contiguous() + return flat[::stride][:out_numel].contiguous() + + +@pytest.mark.parametrize("dtype,in_numel,stride,ncore,xblock,xblock_sub", [ + # ---- V1 命中: stride > 2 (deinterleave 不接) ---- + ("float32", 4096 * 16, 16, 2, 2048, 256), # stride=16 + ("float32", 4096 * 8, 8, 2, 2048, 256), # stride=8 + ("float32", 4096 * 4, 4, 2, 2048, 256), # stride=4 + ("float32", 4096 * 3, 3, 2, 2048, 256), # stride=3 (奇数 stride, block 仍 pow2) + ("float16", 4096 * 6, 6, 2, 2048, 256), + ("int8", 4096 * 7, 7, 2, 2048, 256), + # ---- Deinterleave 接管 (stride==2 + 偶数 block, V1 让路) ---- + # 注:无法在 Triton 中构造"奇数 size + stride==2"以测试 V1 这一支, + # 因为 tl.arange 要求 end-start 是 2 的幂. + ("float32", 4096 * 2, 2, 2, 2048, 256), + # ---- 不应改写 (stride==1) ---- + # TODO: 编译失败原因待排查; 已加 CSE+Canonicalize 收尾仍挂. + # pytest.param("float32", 4096, 1, 2, 2048, 256, + # marks=pytest.mark.skip("stride=1 V1 sub-step compile bug")), +]) +def test_1d_strided_compaction(dtype, in_numel, stride, ncore, xblock, xblock_sub): + out_numel = ncore * xblock + assert in_numel >= out_numel * stride, "in_numel must cover the strided range" + assert xblock % xblock_sub == 0 + + src = test_common.generate_tensor((in_numel, ), dtype).npu() + dst = test_common.generate_tensor((out_numel, ), dtype).npu() + + kernel_1d_strided_compaction[(ncore, )]( + src, + dst, + in_numel, + out_numel, + xblock, + xblock_sub, + stride, + ) + + ref = _ref_1d(src.cpu(), stride, out_numel) + actual = dst.cpu() + if dtype in ("float32", "float16"): + assert torch.allclose(actual, ref, atol=1e-2, rtol=1e-3), \ + f"max abs diff = {(actual.float() - ref.float()).abs().max().item()}" + else: + assert torch.equal(actual, ref), \ + f"mismatch count = {(actual != ref).sum().item()}" + + +# --------------------------------------------------------------------------- +# Multi-D: out[i0, i1, ..., in-1] = in_flat[sum_d i_d * STRIDE_d] +# Output is packed contiguous of shape `blocks`; input is flat with enough +# elements to cover the maximum offset. +# --------------------------------------------------------------------------- + + +@triton.jit +def kernel_multi_d_gather( + in_ptr, + out_ptr, + BLOCK_0: tl.constexpr, + BLOCK_1: tl.constexpr, + BLOCK_2: tl.constexpr, + BLOCK_3: tl.constexpr, + BLOCK_4: tl.constexpr, + STRIDE_0: tl.constexpr, + STRIDE_1: tl.constexpr, + STRIDE_2: tl.constexpr, + STRIDE_3: tl.constexpr, + STRIDE_4: tl.constexpr, +): + # Build in_off / out_off progressively. out_off is packed-contiguous so we + # can compare against a plain reshape of the reference gather. + in_off = tl.arange(0, BLOCK_0) * STRIDE_0 + out_off = tl.arange(0, BLOCK_0) + + if (BLOCK_1 * BLOCK_2 * BLOCK_3 * BLOCK_4) > 1: + in_off = in_off[:, None] + tl.arange(0, BLOCK_1)[None, :] * STRIDE_1 + out_off = out_off[:, None] * BLOCK_1 + tl.arange(0, BLOCK_1)[None, :] + if (BLOCK_2 * BLOCK_3 * BLOCK_4) > 1: + in_off = in_off[:, :, None] + tl.arange(0, BLOCK_2)[None, None, :] * STRIDE_2 + out_off = out_off[:, :, None] * BLOCK_2 + tl.arange(0, BLOCK_2)[None, None, :] + if (BLOCK_3 * BLOCK_4) > 1: + in_off = in_off[:, :, :, None] + tl.arange(0, BLOCK_3)[None, None, None, :] * STRIDE_3 + out_off = out_off[:, :, :, None] * BLOCK_3 + tl.arange(0, BLOCK_3)[None, None, None, :] + if BLOCK_4 > 1: + in_off = in_off[:, :, :, :, None] + tl.arange(0, BLOCK_4)[None, None, None, None, :] * STRIDE_4 + out_off = out_off[:, :, :, :, None] * BLOCK_4 + tl.arange(0, BLOCK_4)[None, None, None, None, :] + + tmp = tl.load(in_ptr + in_off) + tl.store(out_ptr + out_off, tmp) + + +def _ref_multi_d(src_flat_cpu: torch.Tensor, blocks, strides) -> torch.Tensor: + """reference[i0, ..., in-1] = src_flat[sum_d i_d * strides[d]]""" + coords = torch.meshgrid(*[torch.arange(b) for b in blocks], indexing="ij") + offsets = torch.zeros(blocks, dtype=torch.int64) + for d in range(len(blocks)): + offsets = offsets + coords[d].to(torch.int64) * int(strides[d]) + return src_flat_cpu[offsets] + + +@pytest.mark.parametrize("dtype,blocks,strides", [ + # ---- V1 hit: non-permuted, last-axis stride statically > 1, all blocks power-of-2 ---- + # 2D + ("float32", (4, 8), (8, 4)), # stride 4 + ("float32", (4, 8), (24, 3)), # stride 3 (odd) + # 3D + ("float16", (2, 4, 8), (32, 8, 4)), # stride 4 + ("float32", (4, 4, 8), (96, 24, 3)), # stride 3 (odd) + # 4D + ("float16", (2, 4, 4, 8), (128, 32, 8, 3)), # stride 3 + # 5D + ("float32", (2, 2, 2, 4, 8), (256, 128, 64, 16, 5)), # stride 5 (odd) + + # ---- Deinterleave handles (stride==2 + even last block) ---- + ("float16", (4, 4, 8), (32, 8, 2)), + + # ---- Permuted: ImplicitPermute handles, V1 must skip ---- + ("float32", (4, 8), (1, 4)), # strides ascending + ("float32", (4, 4, 8), (1, 4, 16)), # strictly ascending + + # ---- Normal contiguous (stride==1): no rewrite ---- + ("float32", (4, 8), (8, 1)), + ("float32", (4, 4, 8), (32, 8, 1)), +]) +def test_multi_d_gather(dtype, blocks, strides): + assert len(blocks) == len(strides) + assert len(blocks) <= 5 + + # Cover max input offset: (B_d - 1) * STRIDE_d sum + 1. + max_offset = sum((b - 1) * s for b, s in zip(blocks, strides)) + 1 + src = test_common.generate_tensor((max_offset, ), dtype).npu() + dst = test_common.generate_tensor(tuple(blocks), dtype).npu() + + padded_blocks = list(blocks) + [1] * (5 - len(blocks)) + padded_strides = list(strides) + [0] * (5 - len(strides)) + + kernel_multi_d_gather[(1, )]( + src, + dst, + *padded_blocks, + *padded_strides, + ) + + ref = _ref_multi_d(src.cpu(), blocks, strides) + actual = dst.cpu() + if dtype in ("float32", "float16"): + assert torch.allclose(actual, ref, atol=1e-2, rtol=1e-3), \ + f"max abs diff = {(actual.float() - ref.float()).abs().max().item()}" + else: + assert torch.equal(actual, ref), \ + f"mismatch count = {(actual != ref).sum().item()}" + + +# --------------------------------------------------------------------------- +# make_block_ptr (tt.make_tensor_ptr) strided load: +# Build a tl.make_block_ptr with non-default strides such that the low-dim +# stride is `stride_n`. When stride_n > 1 V1 should rewrite the load to +# tt.indirect_load; when stride_n == 1 V1 should bail. +# +# Sanity: this test verifies value correctness only. For "did V1 actually +# trigger?" see the FileCheck test under +# third_party/ascend/unittest/Conversion/950PR/TritonToLinalg/ +# indirect_load_rewrite.mlir +# --------------------------------------------------------------------------- + + +@triton.jit +def kernel_block_ptr_strided( + in_ptr, + out_ptr, + src_shape_m, + src_shape_n, + src_stride_m, + src_stride_n, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + in_block_ptr = tl.make_block_ptr( + base=in_ptr, + shape=(src_shape_m, src_shape_n), + strides=(src_stride_m, src_stride_n), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + out_block_ptr = tl.make_block_ptr( + base=out_ptr, + shape=(BLOCK_M, BLOCK_N), + strides=(BLOCK_N, 1), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + tile = tl.load(in_block_ptr) + tl.store(out_block_ptr, tile) + + +def _ref_block_ptr_strided(src_cpu_flat: torch.Tensor, block_m: int, block_n: int, stride_m: int, + stride_n: int) -> torch.Tensor: + coords_i, coords_j = torch.meshgrid(torch.arange(block_m), torch.arange(block_n), indexing="ij") + offsets = (coords_i.to(torch.int64) * int(stride_m) + coords_j.to(torch.int64) * int(stride_n)) + return src_cpu_flat[offsets] + + +@pytest.mark.parametrize("dtype,block_m,block_n,stride_m,stride_n", [ + # V1 命中: non-permuted, 低维 stride 静态 > 1 + ("float32", 4, 8, 32, 4), + ("float32", 4, 8, 24, 3), + ("float16", 8, 8, 64, 8), + ("int8", 16, 8, 56, 7), + # Deinterleave 接管 (stride_n == 2, 偶数 last block) + ("float32", 4, 8, 16, 2), + # V1 不动 (stride_n == 1, contiguous) + ("float32", 4, 8, 8, 1), + ("float16", 8, 16, 16, 1), +]) +def test_block_ptr_strided(dtype, block_m, block_n, stride_m, stride_n): + max_offset = (block_m - 1) * stride_m + (block_n - 1) * stride_n + 1 + src = test_common.generate_tensor((max_offset, ), dtype).npu() + dst = test_common.generate_tensor((block_m, block_n), dtype).npu() + + src_shape_m = block_m * stride_m + src_shape_n = stride_n * block_n + + kernel_block_ptr_strided[(1, )]( + src, + dst, + src_shape_m, + src_shape_n, + stride_m, + stride_n, + block_m, + block_n, + ) + + ref = _ref_block_ptr_strided(src.cpu(), block_m, block_n, stride_m, stride_n) + actual = dst.cpu() + if dtype in ("float32", "float16"): + assert torch.allclose(actual, ref, atol=1e-2, rtol=1e-3), \ + f"max abs diff = {(actual.float() - ref.float()).abs().max().item()}" + else: + assert torch.equal(actual, ref), \ + f"mismatch count = {(actual != ref).sum().item()}" + + +# --------------------------------------------------------------------------- +# V2: tt.store -> tt.indirect_store fast-path. Mirror of the load tests +# above, but the strided side is the OUTPUT (scatter) instead of the input. +# +# out[i*stride] = in[i] (1D AddPtr) +# out[i*stride_m + j*stride_n] = in[i, j] (2D block_ptr) +# --------------------------------------------------------------------------- + + +@triton.jit +def kernel_1d_strided_scatter( + in_ptr, + out_ptr, + in_numel, + out_numel, + XBLOCK: tl.constexpr, + XBLOCK_SUB: tl.constexpr, + STRIDE: tl.constexpr, +): + pid = tl.program_id(0) + xoffset = pid * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB) + dst_idx = xindex * STRIDE + load_mask = xindex < in_numel + store_mask = dst_idx < out_numel + tmp = tl.load(in_ptr + xindex, load_mask) + tl.store(out_ptr + dst_idx, tmp, store_mask) + + +def _ref_1d_scatter(src_cpu: torch.Tensor, stride: int, out_numel: int, dtype: str) -> torch.Tensor: + """out[i*stride] = src[i] for i in [0, src.numel()).""" + torch_dtype = eval("torch." + dtype) + out = torch.zeros(out_numel, dtype=torch_dtype) + flat = src_cpu.flatten().contiguous() + for i in range(flat.numel()): + idx = i * stride + if idx < out_numel: + out[idx] = flat[i] + return out + + +@pytest.mark.parametrize("dtype,in_numel,stride,ncore,xblock,xblock_sub", [ + # V2 命中: stride > 2 (deinterleave doesn't apply) + ("float32", 4096, 16, 2, 2048, 256), + ("float32", 4096, 8, 2, 2048, 256), + ("float32", 4096, 4, 2, 2048, 256), + ("float32", 4096, 3, 2, 2048, 256), + ("float16", 4096, 6, 2, 2048, 256), + ("int8", 4096, 7, 2, 2048, 256), + # Deinterleave 接管 (stride==2, 偶数 last block) + ("float32", 4096, 2, 2, 2048, 256), + # V2 不动 (stride == 1) + ("float32", 4096, 1, 2, 2048, 256), +]) +def test_1d_strided_scatter(dtype, in_numel, stride, ncore, xblock, xblock_sub): + out_numel = in_numel * stride + assert xblock % xblock_sub == 0 + assert ncore * xblock >= in_numel + src = test_common.generate_tensor((in_numel, ), dtype).npu() + dst = test_common.generate_tensor((out_numel, ), dtype).npu() + # Pre-zero dst so non-written positions are deterministic. + dst.zero_() + + kernel_1d_strided_scatter[(ncore, )]( + src, + dst, + in_numel, + out_numel, + xblock, + xblock_sub, + stride, + ) + + ref = _ref_1d_scatter(src.cpu(), stride, out_numel, dtype) + actual = dst.cpu() + if dtype in ("float32", "float16"): + assert torch.allclose(actual, ref, atol=1e-2, rtol=1e-3), \ + f"max abs diff = {(actual.float() - ref.float()).abs().max().item()}" + else: + assert torch.equal(actual, ref), \ + f"mismatch count = {(actual != ref).sum().item()}" + + +@triton.jit +def kernel_block_ptr_strided_scatter( + in_ptr, + out_ptr, + dst_shape_m, + dst_shape_n, + dst_stride_m, + dst_stride_n, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + # Contiguous in, strided out. + in_block_ptr = tl.make_block_ptr( + base=in_ptr, + shape=(BLOCK_M, BLOCK_N), + strides=(BLOCK_N, 1), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + out_block_ptr = tl.make_block_ptr( + base=out_ptr, + shape=(dst_shape_m, dst_shape_n), + strides=(dst_stride_m, dst_stride_n), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + tile = tl.load(in_block_ptr) + tl.store(out_block_ptr, tile) + + +def _ref_block_ptr_scatter(src_cpu: torch.Tensor, block_m: int, block_n: int, stride_m: int, stride_n: int, + out_numel: int, dtype: str) -> torch.Tensor: + """out_flat[i*stride_m + j*stride_n] = src[i, j]""" + torch_dtype = eval("torch." + dtype) + out = torch.zeros(out_numel, dtype=torch_dtype) + for i in range(block_m): + for j in range(block_n): + idx = i * stride_m + j * stride_n + if idx < out_numel: + out[idx] = src_cpu[i, j] + return out + + +@pytest.mark.parametrize("dtype,block_m,block_n,stride_m,stride_n", [ + # V2 命中: non-permuted, 低维 stride 静态 > 1 + ("float32", 4, 8, 32, 4), + ("float32", 4, 8, 24, 3), + ("float16", 8, 8, 64, 8), + ("int8", 16, 8, 56, 7), + # Deinterleave 接管 + ("float32", 4, 8, 16, 2), + # V2 不动 (contiguous) + ("float32", 4, 8, 8, 1), + ("float16", 8, 16, 16, 1), +]) +def test_block_ptr_strided_scatter(dtype, block_m, block_n, stride_m, stride_n): + src = test_common.generate_tensor((block_m, block_n), dtype).npu() + out_numel = (block_m - 1) * stride_m + (block_n - 1) * stride_n + 1 + # Round up to make a contiguous flat buffer of sufficient size. + out_numel = max(out_numel, block_m * stride_m) + dst = test_common.generate_tensor((out_numel, ), dtype).npu() + dst.zero_() + + dst_shape_m = block_m * stride_m + dst_shape_n = stride_n * block_n + + kernel_block_ptr_strided_scatter[(1, )]( + src, + dst, + dst_shape_m, + dst_shape_n, + stride_m, + stride_n, + block_m, + block_n, + ) + + ref = _ref_block_ptr_scatter(src.cpu(), block_m, block_n, stride_m, stride_n, out_numel, dtype) + actual = dst.cpu() + if dtype in ("float32", "float16"): + assert torch.allclose(actual, ref, atol=1e-2, rtol=1e-3), \ + f"max abs diff = {(actual.float() - ref.float()).abs().max().item()}" + else: + assert torch.equal(actual, ref), \ + f"mismatch count = {(actual != ref).sum().item()}" + + +# --------------------------------------------------------------------------- +# V1.5: make_block_ptr Load + boundary_check + strided (low-dim stride > 1). +# Block intentionally overflows the parent tensor; OOB lanes should get +# the padding value (0 for PAD_ZERO, NaN for PAD_NAN), in-bounds lanes +# should get the strided gather data. +# --------------------------------------------------------------------------- + + +@triton.jit +def kernel_block_ptr_boundary_load( + in_ptr, + out_ptr, + PARENT_M: tl.constexpr, + PARENT_N: tl.constexpr, + STRIDE_M: tl.constexpr, + STRIDE_N: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + PAD_NAN: tl.constexpr, +): + in_block_ptr = tl.make_block_ptr( + base=in_ptr, + shape=(PARENT_M, PARENT_N), + strides=(STRIDE_M, STRIDE_N), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + out_block_ptr = tl.make_block_ptr( + base=out_ptr, + shape=(BLOCK_M, BLOCK_N), + strides=(BLOCK_N, 1), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + if PAD_NAN: + tile = tl.load(in_block_ptr, boundary_check=(0, 1), padding_option="nan") + else: + tile = tl.load(in_block_ptr, boundary_check=(0, 1), padding_option="zero") + tl.store(out_block_ptr, tile) + + +def _ref_boundary_load(src_cpu_flat, parent_m, parent_n, stride_m, stride_n, block_m, block_n, pad_value, dtype): + torch_dtype = eval("torch." + dtype) + out = torch.full((block_m, block_n), pad_value, dtype=torch_dtype) + for i in range(block_m): + for j in range(block_n): + if i < parent_m and j < parent_n: + out[i, j] = src_cpu_flat[i * stride_m + j * stride_n] + return out + + +@pytest.mark.parametrize("dtype,parent_m,parent_n,stride_m,stride_n,block_m,block_n,pad_nan", [ + # V1.5 hit: PAD_ZERO, stride_n=4, block exceeds parent + pytest.param("float32", 5, 3, 12, 4, 8, 8, False, marks=a3_known_boundary_load_issue), + # V1.5 hit: PAD_ZERO, stride_n=3, block partially out-of-bounds + pytest.param("float32", 7, 5, 15, 3, 8, 8, False, marks=a3_known_boundary_load_issue), + # V1.5 hit: PAD_NAN (float) + pytest.param("float32", 5, 3, 12, 4, 8, 8, True, marks=a3_known_boundary_load_issue), + # V1.5 hit: float16 + pytest.param("float16", 5, 5, 20, 4, 8, 8, False, marks=a3_known_boundary_load_issue), + # OOB only on last axis: block fully fits axis 0 + pytest.param("float32", 8, 3, 12, 4, 8, 8, False, marks=a3_known_boundary_load_issue), +]) +def test_block_ptr_boundary_load(dtype, parent_m, parent_n, stride_m, stride_n, block_m, block_n, pad_nan): + # in buffer covers all valid (i, j) where i < parent_m, j < parent_n. + in_numel = (parent_m - 1) * stride_m + (parent_n - 1) * stride_n + 1 + src = test_common.generate_tensor((in_numel, ), dtype).npu() + dst = test_common.generate_tensor((block_m, block_n), dtype).npu() + + kernel_block_ptr_boundary_load[(1, )]( + src, + dst, + parent_m, + parent_n, + stride_m, + stride_n, + block_m, + block_n, + pad_nan, + ) + + pad_value = float("nan") if pad_nan else 0 + ref = _ref_boundary_load(src.cpu(), parent_m, parent_n, stride_m, stride_n, block_m, block_n, pad_value, dtype) + actual = dst.cpu() + if pad_nan: + # NaN locations both ref and actual; compare ignoring NaN via finite mask. + finite_mask = torch.isfinite(ref) + # In-bounds positions + assert torch.allclose(actual[finite_mask], ref[finite_mask], atol=1e-2, rtol=1e-3) + # OOB positions must be NaN in actual too + assert torch.isnan(actual[~finite_mask]).all(), \ + "expected NaN in OOB positions" + else: + if dtype in ("float32", "float16"): + assert torch.allclose(actual, ref, atol=1e-2, rtol=1e-3), \ + f"max abs diff = {(actual.float() - ref.float()).abs().max().item()}" + else: + assert torch.equal(actual, ref) + + +# --------------------------------------------------------------------------- +# V1.5: make_block_ptr Store + boundary_check + strided. +# Block intentionally overflows the parent; OOB positions should NOT be +# written; in-bounds positions should receive the strided values. +# --------------------------------------------------------------------------- + + +@triton.jit +def kernel_block_ptr_boundary_store( + in_ptr, + out_ptr, + PARENT_M: tl.constexpr, + PARENT_N: tl.constexpr, + STRIDE_M: tl.constexpr, + STRIDE_N: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + in_block_ptr = tl.make_block_ptr( + base=in_ptr, + shape=(BLOCK_M, BLOCK_N), + strides=(BLOCK_N, 1), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + out_block_ptr = tl.make_block_ptr( + base=out_ptr, + shape=(PARENT_M, PARENT_N), + strides=(STRIDE_M, STRIDE_N), + offsets=(0, 0), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + tile = tl.load(in_block_ptr) + tl.store(out_block_ptr, tile, boundary_check=(0, 1)) + + +# --------------------------------------------------------------------------- +# V1.5 regression: kernel pattern `tl.make_block_ptr(s + bos*H + i_h, ...)` +# where the base is a scalar AddPtr chain (NOT the raw function-arg ptr). +# Reproduces the chunk_local_cumsum 10x precision bug fixed 2026-05-28. +# Before the fix, the scalar AddPtr lowered to a size-1 reinterpret_cast view +# and per-element offsets indexed out of bounds. +# --------------------------------------------------------------------------- + + +@triton.jit +def kernel_chunk_local_cumsum( + s, + o, + T, + H: tl.constexpr, + BT: tl.constexpr, +): + i_t, i_bh = tl.program_id(0), tl.program_id(1) + i_b, i_h = i_bh // H, i_bh % H + bos = i_b * T + p_s = tl.make_block_ptr(s + bos * H + i_h, (T, ), (H, ), (i_t * BT, ), (BT, ), (0, )) + p_o = tl.make_block_ptr(o + bos * H + i_h, (T, ), (H, ), (i_t * BT, ), (BT, ), (0, )) + b_s = tl.load(p_s, boundary_check=(0, )).to(tl.float32) + b_o = tl.cumsum(b_s, axis=0) + tl.store(p_o, b_o.to(p_o.dtype.element_ty), boundary_check=(0, )) + + +@pytest.mark.parametrize("dtype,B,T,H,chunk_size", [ + ("float32", 4, 2048, 8, 128), + ("float32", 2, 512, 4, 64), + ("float16", 4, 1024, 8, 128), +]) +def test_chunk_local_cumsum_scalar_base(dtype, B, T, H, chunk_size): + """Verify scalar-AddPtr-base make_block_ptr path produces correct results.""" + torch_dtype = eval("torch." + dtype) + torch.manual_seed(42) + g = torch.randn(B, T, H, device="npu", dtype=torch_dtype) * 0.1 + out = torch.empty_like(g, dtype=torch.float32) + + NT = (T + chunk_size - 1) // chunk_size + grid = (NT, B * H) + kernel_chunk_local_cumsum[grid](g, out, T, H=H, BT=chunk_size) + + # Reference: per-chunk cumsum along the T axis for each (b, h). + ref = torch.zeros_like(g, dtype=torch.float32) + g_cpu = g.cpu() + for b in range(B): + for h in range(H): + for t_chunk in range(NT): + start = t_chunk * chunk_size + end = min(start + chunk_size, T) + sl = g_cpu[b, start:end, h].to(torch.float32) + ref[b, start:end, h] = torch.cumsum(sl, dim=0).to("npu") + + diff = (out - ref).abs().max().item() + atol = 1e-3 if dtype == "float16" else 1e-4 + assert diff < atol, f"max abs diff = {diff:.6e}" + + +@pytest.mark.skip(reason="The case is failed, skipping for now. Will be fixed in future.") +@pytest.mark.parametrize("dtype,parent_m,parent_n,stride_m,stride_n,block_m,block_n", [ + ("float32", 5, 3, 12, 4, 8, 8), + ("float32", 7, 5, 15, 3, 8, 8), + ("float16", 5, 5, 20, 4, 8, 8), +]) +def test_block_ptr_boundary_store(dtype, parent_m, parent_n, stride_m, stride_n, block_m, block_n): + src = test_common.generate_tensor((block_m, block_n), dtype).npu() + out_numel = (parent_m - 1) * stride_m + (parent_n - 1) * stride_n + 1 + # Generous round-up to guarantee no OOB writes are possible (even if V1.5 + # logic regresses we won't segfault). + out_numel = max(out_numel, block_m * stride_m + block_n * stride_n) + dst = test_common.generate_tensor((out_numel, ), dtype).npu() + # Pre-fill dst with a sentinel so spurious writes to OOB positions would + # be detectable (we only assert in-bounds correctness below; the sentinel + # itself is informational for future debug). + if dtype in ("float32", "float16"): + sentinel = float("inf") + elif dtype == "int8": + sentinel = -128 + else: + sentinel = 0 + dst.fill_(sentinel) + + kernel_block_ptr_boundary_store[(1, )]( + src, + dst, + parent_m, + parent_n, + stride_m, + stride_n, + block_m, + block_n, + ) + + actual_cpu = dst.cpu() + src_cpu = src.cpu() + # Check in-bounds positions got the strided write. + for i in range(parent_m): + for j in range(parent_n): + idx = i * stride_m + j * stride_n + if i < block_m and j < block_n: + expected = src_cpu[i, j] + got = actual_cpu[idx] + if dtype in ("float32", "float16"): + assert torch.isclose(got, expected, atol=1e-2, rtol=1e-3), \ + f"mismatch at parent[{i},{j}]: got {got} vs {expected}" + else: + assert got == expected, \ + f"mismatch at parent[{i},{j}]: got {got} vs {expected}" diff --git a/third_party/ascend/unittest/pytest_ut/test_log.py b/third_party/ascend/unittest/pytest_ut/test_log.py new file mode 100644 index 0000000000..3e6726bfd5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_log.py @@ -0,0 +1,68 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = torch.log(x0) + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = tl.math.log(x) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + + +@pytest.mark.parametrize('dtype,sigtype', [ + (torch.float32, 'float32'), +]) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_log10.py b/third_party/ascend/unittest/pytest_ut/test_log10.py new file mode 100644 index 0000000000..caa8bbcce6 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_log10.py @@ -0,0 +1,90 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import torch +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + + +@triton.jit +def log10_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + + y = libdevice.log10(x) + + tl.store(y_ptr + offsets, y, mask=mask) + + +@pytest.mark.parametrize('shape', [ + (12, 16), +]) +@pytest.mark.parametrize('dtype', ['float32']) +def test_log10(shape, dtype): + x = test_common.generate_tensor(shape, dtype).npu() + + x[0, 0] = 1.0 # log10(1) = 0 + x[0, 1] = 10.0 # log10(10) = 1 + x[0, 2] = 100.0 # log10(100) = 2 + x[0, 3] = 0.1 # log10(0.1) = -1 + x[0, 4] = 0.0 # log10(0) = -inf + x[0, 5] = -1.0 # log10(-1) = NaN + x[0, 6] = 2.0 # log10(2) ≈ 0.3010 + + y = torch.empty_like(x) + + BLOCK_SIZE = 192 + grid = lambda meta: (triton.cdiv(192, meta['BLOCK_SIZE']), ) + + log10_kernel[grid](x, y, 192, BLOCK_SIZE=BLOCK_SIZE) + + expected = torch.log10(x) + print(f"triton_ret = {y}") + print(f"triton_ret = {expected}") + + valid_mask = (x > 0) + + if torch.any(valid_mask): + valid_y = y[valid_mask] + valid_expected = expected[valid_mask] + + torch.testing.assert_close(valid_y, valid_expected, rtol=1e-3, atol=1e-3) + + # Check if negative values return NaN + negative_mask = (x < 0) + if torch.any(negative_mask): + negative_y = y[negative_mask] + assert torch.all(torch.isnan(negative_y)), "Negative inputs should return NaN" + + # Check if zero value returns -inf + zero_mask = (x == 0) + if torch.any(zero_mask): + zero_y = y[zero_mask] + assert torch.all(torch.isinf(zero_y) & (zero_y < 0)), "Zero inputs should return -inf" + + print("✓ LOG10 test PASSED!") diff --git a/third_party/ascend/unittest/pytest_ut/test_log1p.py b/third_party/ascend/unittest/pytest_ut/test_log1p.py new file mode 100644 index 0000000000..c066ea35ca --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_log1p.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import test_common +import triton.language.extra.cann.libdevice as libdevice + + +def torch_log1p(x0, x1): + res = x0 + torch.log1p(x1) + return res + + +@triton.jit +def triton_log1p(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + x_index = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = x_index < xnumel + tmp0 = tl.load(in_ptr0 + x_index, xmask) + tmp1 = tl.load(in_ptr1 + x_index, xmask) + tmp2 = tmp0 + libdevice.log1p(tmp1) + tl.store(out_ptr0 + x_index, tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_log1p(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_log1p(x0, x1) + # triton结果 + triton_res = test_common.generate_tensor(shape, dtype).npu() + triton_log1p[ncore, 1, 1](x0, x1, triton_res, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_log2.py b/third_party/ascend/unittest/pytest_ut/test_log2.py new file mode 100644 index 0000000000..37c912d623 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_log2.py @@ -0,0 +1,58 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import test_common + + +def torch_log2(x0): + res = torch.log2(x0) + return res + + +@triton.jit +def triton_log2(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_inedx = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_inedx, None) + tmp2 = tl.log2(tmp0) + tl.store(out_ptr0 + x_inedx, tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_log2(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_log2(x0) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_log2[ncore, 1, 1](x0, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_log_2.py b/third_party/ascend/unittest/pytest_ut/test_log_2.py new file mode 100644 index 0000000000..f45bdd13bd --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_log_2.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_exp2(x0): + res = torch.pow(2, x0, out=None) + return res + + +@triton.jit +def triton_exp2(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index, None) + tmp1 = tl.exp2(tmp0) + tl.store(out_ptr0 + x_index, tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_exp2(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_exp2(x0) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_exp2[ncore, 1, 1](x0, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_logical_and.py b/third_party/ascend/unittest/pytest_ut/test_logical_and.py new file mode 100644 index 0000000000..848a98af15 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_logical_and.py @@ -0,0 +1,77 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import numpy as np +import test_common + + +def torch_logical_and(x0, x1): + res = torch.logical_and(x0, x1) + return res + + +@triton.jit +def triton_logical_and(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index) + tmp1 = tl.load(in_ptr1 + x_index) + tmp2 = tmp0.logical_and(tmp1) + tl.store(out_ptr0 + x_index, tmp2) + + +@pytest.mark.parametrize('param_list', [ + ['bool', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + # ['int16', (2, 4096, 8), 2, 32768, 1024], + # ['int32', (2, 4096, 8), 2, 32768, 1024], + # ['int64', (2, 4096, 8), 2, 32768, 1024], + ['uint8', (2, 4096, 8), 2, 32768, 1024], + # ['uint16', (2, 4096, 8), 2, 32768, 1024], + # ['uint32', (2, 4096, 8), 2, 32768, 1024], + # ['uint64', (2, 4096, 8), 2, 32768, 1024], + # ['float16', (2, 4096, 8), 2, 32768, 1024], + # ['float32', (2, 4096, 8), 2, 32768, 1024], + # ['bfloat16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_and(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + torch_dtype = eval('torch.' + dtype) + np_x0 = test_common.generate_numpy(shape, dtype) + np_x1 = test_common.generate_numpy(shape, dtype) + x0 = torch.from_numpy(np_x0).to(torch_dtype).npu() + x1 = torch.from_numpy(np_x1).to(torch_dtype).npu() + # torch结果 + np_res = np.logical_and(np_x0, np_x1) + torch_res = torch.from_numpy(np_res).to(torch.bool) + # triton结果 + triton_res = torch.zeros(shape, dtype=torch.bool).npu() + triton_logical_and[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_logical_or.py b/third_party/ascend/unittest/pytest_ut/test_logical_or.py new file mode 100644 index 0000000000..baef1d2bfe --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_logical_or.py @@ -0,0 +1,77 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import numpy as np +import test_common + + +def torch_logical_or(x0, x1): + res = torch.logical_or(x0, x1) + return res + + +@triton.jit +def triton_logical_or(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index) + tmp1 = tl.load(in_ptr1 + x_index) + tmp2 = tmp0.logical_or(tmp1) + tl.store(out_ptr0 + x_index, tmp2) + + +@pytest.mark.parametrize('param_list', [ + ['bool', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + # ['int16', (2, 4096, 8), 2, 32768, 1024], + # ['int32', (2, 4096, 8), 2, 32768, 1024], + # ['int64', (2, 4096, 8), 2, 32768, 1024], + ['uint8', (2, 4096, 8), 2, 32768, 1024], + # ['uint16', (2, 4096, 8), 2, 32768, 1024], + # ['uint32', (2, 4096, 8), 2, 32768, 1024], + # ['uint64', (2, 4096, 8), 2, 32768, 1024], + # ['float16', (2, 4096, 8), 2, 32768, 1024], + # ['float32', (2, 4096, 8), 2, 32768, 1024], + # ['bfloat16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_logical_or(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + torch_dtype = eval('torch.' + dtype) + np_x0 = test_common.generate_numpy(shape, dtype) + np_x1 = test_common.generate_numpy(shape, dtype) + x0 = torch.from_numpy(np_x0).to(torch_dtype).npu() + x1 = torch.from_numpy(np_x1).to(torch_dtype).npu() + # torch结果 + np_res = np.logical_or(np_x0, np_x1) + torch_res = torch.from_numpy(np_res).to(torch.bool) + # triton结果 + triton_res = torch.zeros(shape, dtype=torch.bool).npu() + triton_logical_or[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_loop_carried_mask.py b/third_party/ascend/unittest/pytest_ut/test_loop_carried_mask.py new file mode 100644 index 0000000000..6213dd42f7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_loop_carried_mask.py @@ -0,0 +1,126 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu # noqa: F401 +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + + +@triton.jit +def loop_carried_mask(x, out, written, final_mask, limits, steps_ptr, initial_ptr, OBSERVE: tl.constexpr, + READ_BEFORE: tl.constexpr, REVERSE_AND: tl.constexpr, DISCRETE: tl.constexpr, + SUFFIX: tl.constexpr, BLOCK: tl.constexpr): + lane = tl.arange(0, BLOCK) + steps = tl.load(steps_ptr) + initial = tl.load(initial_ptr) + mask = lane < initial + acc = tl.zeros([BLOCK], dtype=tl.float32) + for i in tl.range(0, steps): + if READ_BEFORE: + acc += tl.load(x + lane, mask=mask, other=0.0) + limit = tl.load(limits + i) + if SUFFIX: + current = lane >= limit + else: + current = lane < limit + if DISCRETE: + current = current & ((lane & 1) == 0) + if REVERSE_AND: + mask = current & mask + else: + mask = mask & current + acc += tl.load(x + lane, mask=mask, other=0.0) + tl.store(written + lane, acc, mask=mask) + tl.store(out + lane, acc) + if OBSERVE: + tl.store(final_mask + lane, mask.to(tl.int32)) + + +def check_loop_mask(limits, initial, observe=False, read_before=False, reverse_and=False, discrete=False, suffix=False, + compile_mode="simd_simt_template", block=16): + lane = torch.arange(block) + host_x = (lane.float() + 1) / 4 + x = host_x.npu() + out = torch.empty_like(x) + written = torch.full_like(x, -777) + final_mask = torch.empty(block, dtype=torch.int32, device="npu") + device_limits = torch.tensor(limits or [0], dtype=torch.int32, device="npu") + steps_ptr = torch.tensor([len(limits)], dtype=torch.int32, device="npu") + initial_ptr = torch.tensor([initial], dtype=torch.int32, device="npu") + loop_carried_mask[(1, )](x, out, written, final_mask, device_limits, steps_ptr, initial_ptr, OBSERVE=observe, + READ_BEFORE=read_before, REVERSE_AND=reverse_and, DISCRETE=discrete, SUFFIX=suffix, + BLOCK=block, compile_mode=compile_mode) + torch.npu.synchronize() + + # Keep the original boolean recurrence as the independent reference. + mask = lane < initial + expected = torch.zeros(block, dtype=torch.float32) + expected_written = torch.full_like(expected, -777) + for limit in limits: + if read_before: + expected += torch.where(mask, host_x, 0.0) + current = lane >= limit if suffix else lane < limit + if discrete: + current &= (lane & 1) == 0 + mask &= current + expected += torch.where(mask, host_x, 0.0) + expected_written[mask] = expected[mask] + torch.testing.assert_close(out.cpu(), expected, rtol=0, atol=0) + torch.testing.assert_close(written.cpu(), expected_written, rtol=0, atol=0) + if observe: + torch.testing.assert_close(final_mask.cpu(), mask.int(), rtol=0, atol=0) + + +@pytest.mark.parametrize("compile_mode", ["simd", "simd_simt_template"]) +@pytest.mark.parametrize("limits,initial,observe,read_before,reverse_and", [ + pytest.param([], 7, True, False, False, id="zero-iterations"), + pytest.param([15], 16, False, False, False, id="one-iteration"), + pytest.param([15, 14, 13], 16, False, False, False, id="shrinking"), + pytest.param([4, 12], 16, False, False, False, id="shrink-then-expand"), + pytest.param([4, 12, 4], 16, False, False, False, id="alternating"), + pytest.param([-4, 12], 16, False, False, False, id="negative-bound"), + pytest.param([20, 4], 16, False, False, False, id="above-block"), + pytest.param([12, 8], 3, False, False, False, id="narrow-initial"), + pytest.param([12], 0, True, False, False, id="empty-initial"), + pytest.param(list(range(15, -2, -1)), 16, False, False, False, id="past-empty"), + pytest.param([4, 12], 16, True, False, False, id="final-mask"), + pytest.param([4, 12], 7, True, True, False, id="use-before-update"), + pytest.param([4, 12], 16, False, False, True, id="commuted-and"), +]) +def test_loop_prefix_mask(limits, initial, observe, read_before, reverse_and, compile_mode): + check_loop_mask(limits, initial, observe, read_before, reverse_and, compile_mode=compile_mode) + + +@pytest.mark.parametrize("observe", [False, True]) +@pytest.mark.parametrize("suffix", [False, True]) +def test_loop_runtime_mask(observe, suffix): + if not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch): + pytest.skip("runtime loop masks require masked indirect access on Ascend910_95/950") + + limits = [12, 4] if suffix else [4, 12] + check_loop_mask(limits, 16, observe=observe, discrete=not suffix, suffix=suffix) + + +@pytest.mark.parametrize("compile_mode", ["simd", "simd_simt_template"]) +def test_loop_singleton_mask(compile_mode): + check_loop_mask([0, 2, 0], 1, observe=True, suffix=True, compile_mode=compile_mode, block=1) diff --git a/third_party/ascend/unittest/pytest_ut/test_lshift.py b/third_party/ascend/unittest/pytest_ut/test_lshift.py new file mode 100644 index 0000000000..6df7e5ed08 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_lshift.py @@ -0,0 +1,99 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import time +import test_common +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = x0 << 2 + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + tmp = tl.cast(2, tl.int8) + ret = x << tmp + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + # (torch.float32, 'float32'), + # (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + (torch.int8, 'int8'), + # (torch.int16, 'int16'), + # (torch.int32, 'int32'), + # (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + # print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + # print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_lt.py b/third_party/ascend/unittest/pytest_ut/test_lt.py new file mode 100644 index 0000000000..ab790b05e5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_lt.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_lt(x0, x1): + return x0 < x1 + + +@triton.jit +def triton_lt(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index, None) + tmp1 = tl.load(in_ptr1 + x_index, None) + tmp2 = tmp0 < tmp1 + tl.store(out_ptr0 + x_index, tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (32, ), 1, 32, 32], +]) +def test_lt(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_lt(x0, x1).to(eval('torch.' + dtype)) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_lt[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_make_block_ptr_boundary_size.py b/third_party/ascend/unittest/pytest_ut/test_make_block_ptr_boundary_size.py new file mode 100644 index 0000000000..c8d61e2a9c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_make_block_ptr_boundary_size.py @@ -0,0 +1,173 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +""" +getBoundarySizes() reconstructs the per-axis in-bounds size by decomposing the +flat block offset with the full-shape strides. Three defects in that +decomposition are covered: + +an axis that is *not* boundary-checked used to leave its +contribution in the flat offset, so a checked trailing axis computed its +boundary from the un-reduced value and clipped it to 0, silently dropping +the loaded/stored block; + +a permuted layout whose logical axis order differs from physical stride order +must be decomposed in major-to-minor stride order. +""" + +import torch +import torch_npu + +import triton +import triton.language as tl + + +@triton.jit +def boundary_size_unchecked_axis_kernel(in_ptr, out_ptr, TOTAL_M, TOTAL_N, OFF_M, OFF_N, BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr): + """The leading axis is *not* boundary-checked while the trailing axis + is: its flat offset (OFF_M * TOTAL_N + OFF_N) must be reduced before the + trailing-axis boundary is derived from it.""" + bptr = tl.make_block_ptr( + base=in_ptr, + shape=(TOTAL_M, TOTAL_N), + strides=(TOTAL_N, 1), + offsets=(OFF_M, OFF_N), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + val = tl.load(bptr, boundary_check=(1, ), padding_option="zero") + + out = tl.make_block_ptr( + base=out_ptr, + shape=(TOTAL_M, TOTAL_N), + strides=(TOTAL_N, 1), + offsets=(OFF_M, OFF_N), + block_shape=(BLOCK_M, BLOCK_N), + order=(1, 0), + ) + tl.store(out, val, boundary_check=(1, )) + + +@triton.jit +def boundary_size_permuted_strides_kernel(in_ptr, out_ptr, size_0, size_1, size_2, BLOCK_0: tl.constexpr, + BLOCK_1: tl.constexpr, BLOCK_2: tl.constexpr): + """Copy a dynamically shaped, permuted tensor through block pointers. + + The input's logical strides are [1, 800, 20], so its physical stride order + is [0, 2, 1]. Runtime shapes make the lowering exercise + getBoundarySizes()'s linear-offset reconstruction rather than replacing it + with the original per-axis offsets. + """ + pid = tl.program_id(0) + num_tiles_1 = tl.cdiv(size_1, BLOCK_1) + num_tiles_2 = tl.cdiv(size_2, BLOCK_2) + + tile_2 = pid % num_tiles_2 + pid = pid // num_tiles_2 + tile_1 = pid % num_tiles_1 + tile_0 = pid // num_tiles_1 + + offset_0 = tile_0 * BLOCK_0 + offset_1 = tile_1 * BLOCK_1 + offset_2 = tile_2 * BLOCK_2 + + src = tl.make_block_ptr( + base=in_ptr, + shape=(size_0, size_1, size_2), + strides=(1, 800, 20), + offsets=(offset_0, offset_1, offset_2), + block_shape=(BLOCK_0, BLOCK_1, BLOCK_2), + order=(0, 2, 1), + ) + value = tl.load(src, boundary_check=(0, 1, 2), padding_option="zero") + + dst = tl.make_block_ptr( + base=out_ptr, + shape=(size_0, size_1, size_2), + strides=(1200, 40, 1), + offsets=(offset_0, offset_1, offset_2), + block_shape=(BLOCK_0, BLOCK_1, BLOCK_2), + order=(2, 1, 0), + ) + tl.store(dst, value, boundary_check=(0, 1, 2)) + + +def _check_boundary(actual: torch.Tensor, expected: torch.Tensor, tag: str): + torch.testing.assert_close( + actual, + expected, + rtol=0, + atol=0, + msg=(f"{tag}: make_block_ptr boundary size diverged from the " + "reference implementation"), + ) + + +def test_boundary_size_unchecked_axis(): + TOTAL_M, TOTAL_N = 8, 60 + BLOCK_M, BLOCK_N = 4, 32 + OFF_M, OFF_N = 2, 36 + + x = torch.arange(TOTAL_M * TOTAL_N, dtype=torch.float32, device="npu").reshape(TOTAL_M, TOTAL_N) + + out_kernel = torch.zeros((TOTAL_M, TOTAL_N), dtype=torch.float32, device="npu") + out_ref = torch.zeros_like(out_kernel) + + boundary_size_unchecked_axis_kernel[(1, )](x, out_kernel, TOTAL_M, TOTAL_N, OFF_M, OFF_N, BLOCK_M=BLOCK_M, + BLOCK_N=BLOCK_N) + torch.npu.synchronize() + + # 24 columns fit in bounds on the checked trailing axis; the 8 columns + # past the tail stay zero (zero padding on the load, masked store). + # With the un-reduced flat offset the boundary used to clip to 0 and the + # whole block was dropped. + out_ref[OFF_M:OFF_M + BLOCK_M, OFF_N:TOTAL_N] = \ + x[OFF_M:OFF_M + BLOCK_M, OFF_N:TOTAL_N] + + _check_boundary(out_kernel, out_ref, "unchecked_axis") + + +def test_boundary_size_permuted_strides(): + shape = (20, 30, 40) + block_shape = (1, 16, 64) + + # [30, 40, 20] contiguous has strides [800, 20, 1]. Moving its last + # axis to the front produces logical shape [20, 30, 40] and the target + # non-major-to-minor strides [1, 800, 20]. + src = torch.arange(20 * 30 * 40, dtype=torch.float32, device="npu").reshape(30, 40, 20).permute(2, 0, 1) + assert src.shape == shape + assert src.stride() == (1, 800, 20) + + actual = torch.zeros(shape, dtype=torch.float32, device="npu") + grid = (triton.cdiv(shape[0], block_shape[0]) * triton.cdiv(shape[1], block_shape[1]) * + triton.cdiv(shape[2], block_shape[2]), ) + boundary_size_permuted_strides_kernel[grid]( + src, + actual, + shape[0], + shape[1], + shape[2], + BLOCK_0=block_shape[0], + BLOCK_1=block_shape[1], + BLOCK_2=block_shape[2], + ) + torch.npu.synchronize() + + _check_boundary(actual, src, "permuted_strides") diff --git a/third_party/ascend/unittest/pytest_ut/test_make_tensor_ptr_zero_stride.py b/third_party/ascend/unittest/pytest_ut/test_make_tensor_ptr_zero_stride.py new file mode 100644 index 0000000000..428fa2f14f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_make_tensor_ptr_zero_stride.py @@ -0,0 +1,186 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu + +import triton +import triton.language as tl +import pytest + + +@triton.jit +def broadcast_load_ref_kernel(in_ptr, out_ptr, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr): + """Reference: load a (BLOCK_M, BLOCK_N, BLOCK_K) block where every element + is ``in_ptr[0]`` (broadcast of a single scalar to the block shape).""" + off_m = tl.arange(0, BLOCK_M)[:, None, None] + off_n = tl.arange(0, BLOCK_N)[None, :, None] + off_k = tl.arange(0, BLOCK_K)[None, None, :] + val = tl.load(in_ptr + off_m * 0 + off_n * 0 + off_k * 0) + tl.store(out_ptr + off_m * (BLOCK_N * BLOCK_K) + off_n * BLOCK_K + off_k, val) + + +@triton.jit +def broadcast_store_ref_kernel(in_ptr, out_ptr, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr): + """Reference: store ``in_ptr[0] + 1`` into every position of a + (BLOCK_M, BLOCK_N, BLOCK_K) block. Under broadcast-zero-stride semantics + every store site receives the same scalar; the last writer wins on the + single shared address.""" + off_m = tl.arange(0, BLOCK_M)[:, None, None] + off_n = tl.arange(0, BLOCK_N)[None, :, None] + off_k = tl.arange(0, BLOCK_K)[None, None, :] + val = tl.load(in_ptr) + 1.0 + tl.store(out_ptr + off_m * (BLOCK_N * BLOCK_K) + off_n * BLOCK_K + off_k, val) + + +@triton.jit +def broadcast_advance_ref_kernel(in_ptr, out_ptr, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr): + """Reference: advance with strides (0, 0, 0) keeps the address at + ``base``, so the loaded block is still ``in_ptr[0]`` broadcast.""" + off_m = tl.arange(0, BLOCK_M)[:, None, None] + off_n = tl.arange(0, BLOCK_N)[None, :, None] + off_k = tl.arange(0, BLOCK_K)[None, None, :] + val = tl.load(in_ptr) + tl.store(out_ptr + off_m * (BLOCK_N * BLOCK_K) + off_n * BLOCK_K + off_k, val) + + +@triton.jit +def broadcast_load_kernel(in_ptr, out_ptr, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr): + """Use ``make_block_ptr`` with all-zero strides for the load.""" + bptr = tl.make_block_ptr( + base=in_ptr, + shape=(BLOCK_M, BLOCK_N, BLOCK_K), + strides=(0, 0, 0), + offsets=(0, 0, 0), + block_shape=(BLOCK_M, BLOCK_N, BLOCK_K), + order=(0, 1, 2), + ) + val = tl.load(bptr, boundary_check=(0, 1, 2)) + + out = tl.make_block_ptr( + base=out_ptr, + shape=(BLOCK_M, BLOCK_N, BLOCK_K), + strides=(BLOCK_N * BLOCK_K, BLOCK_K, 1), + offsets=(0, 0, 0), + block_shape=(BLOCK_M, BLOCK_N, BLOCK_K), + order=(0, 1, 2), + ) + tl.store(out, val, boundary_check=(0, 1, 2)) + + +@triton.jit +def broadcast_store_kernel(in_ptr, out_ptr, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr): + """Use ``make_block_ptr`` with all-zero strides for the store.""" + scalar = tl.load(in_ptr) + + bptr = tl.make_block_ptr( + base=out_ptr, + shape=(BLOCK_M, BLOCK_N, BLOCK_K), + strides=(0, 0, 0), + offsets=(0, 0, 0), + block_shape=(BLOCK_M, BLOCK_N, BLOCK_K), + order=(0, 1, 2), + ) + tl.store(bptr, scalar + 1.0, boundary_check=(0, 1, 2)) + + +@triton.jit +def broadcast_advance_kernel(in_ptr, out_ptr, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr): + """Use ``make_block_ptr`` with all-zero strides followed by ``tl.advance`` + with non-zero offsets — the advanced pointer must still alias base.""" + bptr = tl.make_block_ptr( + base=in_ptr, + shape=(BLOCK_M, BLOCK_N, BLOCK_K), + strides=(0, 0, 0), + offsets=(0, 0, 0), + block_shape=(BLOCK_M, BLOCK_N, BLOCK_K), + order=(0, 1, 2), + ) + advanced = tl.advance(bptr, [2, 3, 4]) + val = tl.load(advanced, boundary_check=(0, 1, 2)) + + out = tl.make_block_ptr( + base=out_ptr, + shape=(BLOCK_M, BLOCK_N, BLOCK_K), + strides=(BLOCK_N * BLOCK_K, BLOCK_K, 1), + offsets=(0, 0, 0), + block_shape=(BLOCK_M, BLOCK_N, BLOCK_K), + order=(0, 1, 2), + ) + tl.store(out, val, boundary_check=(0, 1, 2)) + + +def _check_close(actual: torch.Tensor, expected: torch.Tensor, tag: str): + torch.testing.assert_close( + actual, + expected, + rtol=0, + atol=0, + msg=(f"{tag}: broadcast-zero-stride kernel diverged from the " + "reference implementation"), + ) + + +def test_zero_stride_make_block_ptr_load(): + BLOCK_M, BLOCK_N, BLOCK_K = 4, 16, 16 + + scalar = torch.tensor([3.14], dtype=torch.float32, device="npu") + + out_kernel = torch.zeros((BLOCK_M, BLOCK_N, BLOCK_K), dtype=torch.float32, device="npu") + out_ref = torch.zeros_like(out_kernel) + + broadcast_load_kernel[(1, )](scalar, out_kernel, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_K=BLOCK_K) + broadcast_load_ref_kernel[(1, )](scalar, out_ref, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_K=BLOCK_K) + torch.npu.synchronize() + + _check_close(out_kernel, out_ref, "load") + + +@pytest.mark.skip(reason="waiting for fix") +def test_zero_stride_make_block_ptr_store(): + + BLOCK_M, BLOCK_N, BLOCK_K = 4, 16, 16 + + scalar = torch.tensor([2.0], dtype=torch.float32, device="npu") + sink = torch.tensor([0.0], dtype=torch.float32, device="npu") + sink_ref = torch.tensor([0.0], dtype=torch.float32, device="npu") + + broadcast_store_kernel[(1, )](scalar, sink, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_K=BLOCK_K) + broadcast_store_ref_kernel[(1, )](scalar, sink_ref, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_K=BLOCK_K) + torch.npu.synchronize() + + _check_close(sink, sink_ref, "store") + + +@pytest.mark.skip(reason="waiting for fix") +def test_zero_stride_make_block_ptr_advance(): + + BLOCK_M, BLOCK_N, BLOCK_K = 4, 16, 16 + + scalar = torch.tensor([1.5], dtype=torch.float32, device="npu") + + out_kernel = torch.zeros((BLOCK_M, BLOCK_N, BLOCK_K), dtype=torch.float32, device="npu") + out_ref = torch.zeros_like(out_kernel) + + broadcast_advance_kernel[(1, )](scalar, out_kernel, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_K=BLOCK_K) + broadcast_advance_ref_kernel[(1, )](scalar, out_ref, BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_K=BLOCK_K) + torch.npu.synchronize() + + _check_close(out_kernel, out_ref, "advance") diff --git a/third_party/ascend/unittest/pytest_ut/test_makeblockptr_negative_padding.py b/third_party/ascend/unittest/pytest_ut/test_makeblockptr_negative_padding.py new file mode 100644 index 0000000000..4d339997ab --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_makeblockptr_negative_padding.py @@ -0,0 +1,136 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is herey_size granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest +import test_common + + +@triton.jit +def negative_padding_with_load_kernel( + input_ptr, + output_ptr, + x_offset: tl.constexpr, + y_offset: tl.constexpr, + x_size: tl.constexpr, + y_size: tl.constexpr, +): + in_ptr = tl.make_block_ptr( + base=input_ptr, + shape=(x_size, y_size), + strides=(y_size, 1), + offsets=(x_offset, y_offset), + block_shape=(x_size, y_size), + order=(1, 0), + ) + out_ptr = tl.make_block_ptr( + base=output_ptr, + shape=(x_size, y_size), + strides=(y_size, 1), + offsets=(0, 0), + block_shape=(x_size, y_size), + order=(1, 0), + ) + in_val = tl.load(in_ptr, boundary_check=(0, 1), padding_option="zero") + tl.store(out_ptr, in_val) + + +@triton.jit +def negative_padding_with_store_kernel( + input_ptr, + output_ptr, + x_offset: tl.constexpr, + y_offset: tl.constexpr, + x_size: tl.constexpr, + y_size: tl.constexpr, +): + in_ptr = tl.make_block_ptr( + base=input_ptr, + shape=(x_size, y_size), + strides=(y_size, 1), + offsets=(0, 0), + block_shape=(x_size, y_size), + order=(1, 0), + ) + out_ptr = tl.make_block_ptr( + base=output_ptr, + shape=(x_size, y_size), + strides=(y_size, 1), + offsets=(x_offset, y_offset), + block_shape=(x_size, y_size), + order=(1, 0), + ) + in_val = tl.load(in_ptr) + tl.store(out_ptr, in_val, boundary_check=(0, 1)) + + +@pytest.mark.parametrize('param_list', [(8, 8), (16, 16), (32, 32), (64, 64)]) +def test_makeblockptr_load_with_negative_padding(param_list): + shape = param_list + torch.manual_seed(1) + x_offset = torch.randint(shape[0], size=()).item() + # y_offset = torch.randint(shape[1], size=()).item() + y_offset = 0 + input_tensor = torch.arange(start=1, end=shape[0] * shape[1] + 1, dtype=torch.int32).view(shape).npu() + output = torch.zeros(shape, dtype=torch.int32).npu() + negative_padding_with_load_kernel[(1, )]( + input_tensor, + output, + -x_offset, + -y_offset, + shape[0], + shape[1], + ) + output_ref = torch.zeros((shape[0] + x_offset, shape[1] + y_offset), dtype=torch.int32).cpu() + output_subview = torch.narrow(output_ref, 0, x_offset, shape[0]) + output_subview = torch.narrow(output_subview, 1, y_offset, shape[1]) + output_subview.copy_(input_tensor) + output_ref = torch.narrow(output_ref, 0, 0, shape[0]) + output_ref = torch.narrow(output_ref, 1, 0, shape[1]) + test_common.validate_cmp("int32", output, output_ref) + + +@pytest.mark.parametrize('param_list', [(8, 8), (16, 16), (32, 32), (64, 64)]) +def test_makeblockptr_store_with_negative_padding(param_list): + shape = param_list + torch.manual_seed(1) + x_offset = torch.randint(shape[0], size=()).item() + # y_offset = torch.randint(shape[1], size=()).item() + y_offset = 0 + input_tensor = torch.arange(start=1, end=shape[0] * shape[1] + 1, dtype=torch.int32).view(shape).npu() + output = torch.zeros(shape, dtype=torch.int32).npu() + negative_padding_with_store_kernel[(1, )]( + input_tensor, + output, + -x_offset, + -y_offset, + shape[0], + shape[1], + ) + output_ref = torch.zeros(shape, dtype=torch.int32).cpu() + input_subview = torch.narrow(input_tensor, 0, x_offset, shape[0] - x_offset) + input_subview = torch.narrow(input_subview, 1, y_offset, shape[1] - y_offset) + output_subview = torch.narrow(output_ref, 0, 0, shape[0] - x_offset) + output_subview = torch.narrow(output_subview, 1, 0, shape[1] - y_offset) + output_subview.copy_(input_subview) + test_common.validate_cmp("int32", output, output_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_makeblockptr_permute.py b/third_party/ascend/unittest/pytest_ut/test_makeblockptr_permute.py new file mode 100644 index 0000000000..5b5ffe1de7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_makeblockptr_permute.py @@ -0,0 +1,115 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest +import test_common + + +@pytest.mark.parametrize('shape', [(1, 4, 2)]) +@pytest.mark.parametrize('permute_order', [(2, 0, 1)]) +def test_makeblockptr_order(shape, permute_order): + + @triton.jit + def triton_kernel( + in0_ptr: tl.tensor, # of tl.pointer_type + out0_ptr: tl.tensor, # of tl.pointer_type + in0_stride0: int, + in0_stride1: int, + in0_stride2: int, # strides for in0 + in0_stride_order0: tl.constexpr, + in0_stride_order1: tl.constexpr, + in0_stride_order2: tl.constexpr, # stride order for in0 + out0_stride0: int, + out0_stride1: int, + out0_stride2: int, # strides for out0 + out0_stride_order0: tl.constexpr, + out0_stride_order1: tl.constexpr, + out0_stride_order2: tl.constexpr, # stride order for out0 + s0: int, + s1: int, + s2: int, + tile_size0: tl.constexpr, + tile_size1: tl.constexpr, + tile_size2: tl.constexpr, + ): + tile_id0 = tl.program_id(axis=0) + tile_id1 = tl.program_id(axis=1) + tile_id2 = tl.program_id(axis=2) + offset0 = (tile_id0 * tile_size0).to(tl.int32) + offset1 = (tile_id1 * tile_size1).to(tl.int32) + offset2 = (tile_id2 * tile_size2).to(tl.int32) + in0_bptr = tl.make_block_ptr(in0_ptr, (s0, s1, s2), (in0_stride0, in0_stride1, in0_stride2), + (offset0, offset1, offset2), (tile_size0, tile_size1, tile_size2), + order=(in0_stride_order0, in0_stride_order1, in0_stride_order2)) + in0 = tl.load(in0_bptr, boundary_check=(in0_stride_order0, in0_stride_order1, + in0_stride_order2)).to(in0_ptr.type.element_ty) + + out0 = in0 + + out0_bptr = tl.make_block_ptr(out0_ptr, (s0, s1, s2), (out0_stride0, out0_stride1, out0_stride2), + (offset0, offset1, offset2), (tile_size0, tile_size1, tile_size2), + order=(out0_stride_order0, out0_stride_order1, out0_stride_order2)) + tl.store(out0_bptr, out0.to(out0_bptr.type.element_ty), + boundary_check=(out0_stride_order0, out0_stride_order1, out0_stride_order2)) + + def triton_func(in0: torch.Tensor, permute_order): + # in fact, it adjusts the layout metadata instead of doing a real permutation. + in0_permuted_tmp = in0.permute(permute_order) + in0_permuted_shape = in0_permuted_tmp.size() + in0_permuted_strides = in0_permuted_tmp.stride() + in0_stride_order = [len(permute_order) - 1 - i for i in permute_order] + shape = (in0_permuted_shape[0], in0_permuted_shape[1], in0_permuted_shape[2]) + tile_sizes = (shape[0], shape[1], shape[2]) + out0 = torch.empty(shape, dtype=in0.dtype, device=in0.device) + out0_strides = out0.stride() + out0_stride_order = [len(permute_order) - 1 - i for i in range(len(permute_order))] + grid = (shape[0] // tile_sizes[0], shape[1] // tile_sizes[1], shape[2] // tile_sizes[2]) + triton_kernel[grid]( + in0, + out0, + in0_permuted_strides[0], + in0_permuted_strides[1], + in0_permuted_strides[2], # stride for in0 + in0_stride_order[0], + in0_stride_order[1], + in0_stride_order[2], # stride order for in0 + out0_strides[0], + out0_strides[1], + out0_strides[2], # stride for out0 + out0_stride_order[0], + out0_stride_order[1], + out0_stride_order[2], # stride orderfor out0 + shape[0], + shape[1], + shape[2], # task indexing space + tile_size0=tile_sizes[0], + tile_size1=tile_sizes[1], + tile_size2=tile_sizes[2], + ) + return out0 + + x0 = torch.randint(0, 9, shape, dtype=torch.int32).npu() + torch_ref = torch.permute(x0, permute_order) + triton_cal = triton_func(x0, permute_order) + test_common.validate_cmp("int32", triton_cal, torch_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_map_elementwise_ops.py b/third_party/ascend/unittest/pytest_ut/test_map_elementwise_ops.py new file mode 100644 index 0000000000..3db37c6ceb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_map_elementwise_ops.py @@ -0,0 +1,387 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Coverage tests for tl.map_elementwise decomposition. +One test per op category / control-flow scenario. +""" +import torch +import triton +import triton.language as tl + +# ---- integer arith ---- + + +@triton.jit +def _add_i(a, b): + return tl.add(a, b, sanitize_overflow=False) + + +def test_map_arith_add_i(): + + @triton.jit + def kernel(X, Y, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + y = tl.load(Y + offs) + z = tl.map_elementwise(_add_i, x, y) + tl.store(Z + offs, z) + + N = 128 + x = torch.randint(-100, 100, (N, ), dtype=torch.int32, device='npu') + y = torch.randint(-100, 100, (N, ), dtype=torch.int32, device='npu') + z = torch.zeros(N, dtype=torch.int32, device='npu') + kernel[(1, )](x, y, z, N=N) + assert torch.equal(z, x + y) + + +# ---- float arith ---- + + +@triton.jit +def _mul_f(a, b): + return a * b + + +def test_map_arith_mul_f(): + + @triton.jit + def kernel(X, Y, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + y = tl.load(Y + offs) + z = tl.map_elementwise(_mul_f, x, y) + tl.store(Z + offs, z) + + N = 128 + x = torch.randn(N, dtype=torch.float32, device='npu') + y = torch.randn(N, dtype=torch.float32, device='npu') + z = torch.zeros(N, dtype=torch.float32, device='npu') + kernel[(1, )](x, y, z, N=N) + assert torch.allclose(z, x * y) + + +# ---- bitwise ---- + + +@triton.jit +def _bitwise(a, b): + return a & b, a | b, a ^ b + + +def test_map_arith_bitwise(): + + @triton.jit + def kernel(A, B, C, D, E, N: tl.constexpr): + offs = tl.arange(0, N) + a = tl.load(A + offs) + b = tl.load(B + offs) + c, d, e = tl.map_elementwise(_bitwise, a, b) + tl.store(C + offs, c) + tl.store(D + offs, d) + tl.store(E + offs, e) + + N = 128 + a = torch.randint(0, 255, (N, ), dtype=torch.int32, device='npu') + b = torch.randint(0, 255, (N, ), dtype=torch.int32, device='npu') + c, d, e = [torch.zeros(N, dtype=torch.int32, device='npu') for _ in range(3)] + kernel[(1, )](a, b, c, d, e, N=N) + assert torch.equal(c, a & b) and torch.equal(d, a | b) and torch.equal(e, a ^ b) + + +# ---- neg ---- + + +@triton.jit +def _neg(a): + return -a + + +def test_map_arith_neg_f(): + + @triton.jit + def kernel(X, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + z = tl.map_elementwise(_neg, x) + tl.store(Z + offs, z) + + N = 128 + x = torch.randn(N, dtype=torch.float32, device='npu') + z = torch.zeros(N, dtype=torch.float32, device='npu') + kernel[(1, )](x, z, N=N) + assert torch.allclose(z, -x) + + +# ---- comparison + external constant ---- + + +@triton.jit +def _gt_zero(a): + if a > 0: + return 1 + else: + return 0 + + +def test_map_cmp_i(): + + @triton.jit + def kernel(X, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + z = tl.map_elementwise(_gt_zero, x) + tl.store(Z + offs, z) + + N = 128 + x = torch.randint(-10, 10, (N, ), dtype=torch.int32, device='npu') + z = torch.zeros(N, dtype=torch.int32, device='npu') + kernel[(1, )](x, z, N=N) + expected = (x > 0).to(torch.int32) + assert torch.equal(z, expected) + + +# ---- type cast ---- + + +@triton.jit +def _cast(a): + return a.to(tl.float32) + + +def test_map_cast_sitofp(): + + @triton.jit + def kernel(X, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + z = tl.map_elementwise(_cast, x) + tl.store(Z + offs, z) + + N = 128 + x = torch.randint(-100, 100, (N, ), dtype=torch.int32, device='npu') + z = torch.zeros(N, dtype=torch.float32, device='npu') + kernel[(1, )](x, z, N=N) + assert torch.allclose(z, x.float()) + + +# ---- math (float) ---- + + +@triton.jit +def _exp(a): + return tl.exp(a) + + +def test_map_math_exp(): + + @triton.jit + def kernel(X, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + z = tl.map_elementwise(_exp, x) + tl.store(Z + offs, z) + + N = 128 + x = torch.randn(N, dtype=torch.float32, device='npu') * 0.5 + z = torch.zeros(N, dtype=torch.float32, device='npu') + kernel[(1, )](x, z, N=N) + assert torch.allclose(z, torch.exp(x), atol=1e-4) + + +# ---- math (integer) ---- + + +@triton.jit +def _abs_i(a): + return tl.abs(a) + + +def test_map_math_abs_i(): + + @triton.jit + def kernel(X, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + z = tl.map_elementwise(_abs_i, x) + tl.store(Z + offs, z) + + N = 128 + x = torch.randint(-100, 100, (N, ), dtype=torch.int32, device='npu') + z = torch.zeros(N, dtype=torch.int32, device='npu') + kernel[(1, )](x, z, N=N) + assert torch.equal(z, torch.abs(x)) + + +# ---- select ---- + + +@triton.jit +def _where_max(a, b): + return tl.where(a > b, a, b) + + +def test_map_select_direct(): + + @triton.jit + def kernel(X, Y, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + y = tl.load(Y + offs) + z = tl.map_elementwise(_where_max, x, y) + tl.store(Z + offs, z) + + N = 128 + x = torch.randint(0, 100, (N, ), dtype=torch.int32, device='npu') + y = torch.randint(0, 100, (N, ), dtype=torch.int32, device='npu') + z = torch.zeros(N, dtype=torch.int32, device='npu') + kernel[(1, )](x, y, z, N=N) + assert torch.equal(z, torch.maximum(x, y)) + + +# ---- 2D tensor ---- + + +@triton.jit +def _add(a, b): + return a + b + + +def test_map_2d(): + + @triton.jit + def kernel(X, Y, Z, ROWS: tl.constexpr, COLS: tl.constexpr): + offs = tl.arange(0, ROWS)[:, None] * COLS + tl.arange(0, COLS)[None, :] + x = tl.load(X + offs) + y = tl.load(Y + offs) + z = tl.map_elementwise(_add, x, y) + tl.store(Z + offs, z) + + rows, cols = 4, 8 + x = torch.randint(-100, 100, (rows, cols), dtype=torch.int32, device='npu') + y = torch.randint(-100, 100, (rows, cols), dtype=torch.int32, device='npu') + z = torch.zeros((rows, cols), dtype=torch.int32, device='npu') + kernel[(1, )](x, y, z, ROWS=rows, COLS=cols) + assert torch.equal(z, x + y) + + +# ---- for loop ---- + + +@triton.jit +def _accumulate(a): + result = 0 + for i in range(5): + result = result + a + return result + + +def test_map_for_loop(): + + @triton.jit + def kernel(X, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + z = tl.map_elementwise(_accumulate, x) + tl.store(Z + offs, z) + + N = 128 + x = torch.randint(-10, 10, (N, ), dtype=torch.int32, device='npu') + z = torch.zeros(N, dtype=torch.int32, device='npu') + kernel[(1, )](x, z, N=N) + assert torch.equal(z, x * 5) + + +# ---- community tests (from test_core.py) ---- + + +@triton.jit +def _compare(x, y): + if x < y: + return -1 + elif x == y: + return 0 + else: + return 1 + + +def test_map_elementwise(): + + @triton.jit + def kernel(X, Y, Z, BLOCK: tl.constexpr): + x = tl.load(X + tl.arange(0, BLOCK)) + y = tl.load(Y + tl.arange(0, BLOCK)) + z = tl.map_elementwise(_compare, x, y) + tl.store(Z + tl.arange(0, BLOCK), z) + + shape = (128, ) + x = torch.randint(-100, 100, shape, dtype=torch.int32, device='npu') + y = torch.randint(-100, 100, shape, dtype=torch.int32, device='npu') + z = torch.zeros(shape, dtype=torch.int32, device='npu') + kernel[(1, )](x, y, z, BLOCK=shape[0]) + expected = (x > y).int() - (y > x).int() + assert torch.equal(z, expected) + + +@triton.jit +def _divmod(a, b): + return a // b, a % b + + +def test_map_elementwise_multiple_outputs(): + + @triton.jit + def kernel(A, B, C, D, BLOCK: tl.constexpr): + a = tl.load(A + tl.arange(0, BLOCK)) + b = tl.load(B + tl.arange(0, BLOCK)) + c, d = tl.map_elementwise(_divmod, a, b) + tl.store(C + tl.arange(0, BLOCK), c) + tl.store(D + tl.arange(0, BLOCK), d) + + shape = (512, ) + A = torch.randint(1, 100, shape, dtype=torch.int32, device='npu') + B = torch.randint(1, 10, shape, dtype=torch.int32, device='npu') + C = torch.zeros(shape, dtype=torch.int32, device='npu') + D = torch.zeros(shape, dtype=torch.int32, device='npu') + kernel[(1, )](A, B, C, D, BLOCK=shape[0]) + assert torch.equal(C, A // B) and torch.equal(D, A % B) + + +@triton.jit +def _divmod_pack2(a0, a1, b0, b1): + return a0 // b0, a1 // b1, a0 % b0, a1 % b1 + + +def test_map_elementwise_pack(): + + @triton.jit + def kernel(A, B, C, D, BLOCK: tl.constexpr): + a = tl.load(A + tl.arange(0, BLOCK)) + b = tl.load(B + tl.arange(0, BLOCK)) + c, d = tl.map_elementwise(_divmod_pack2, a, b, pack=2) + tl.store(C + tl.arange(0, BLOCK), c) + tl.store(D + tl.arange(0, BLOCK), d) + + shape = (512, ) + A = torch.randint(1, 100, shape, dtype=torch.int32, device='npu') + B = torch.randint(1, 10, shape, dtype=torch.int32, device='npu') + C = torch.zeros(shape, dtype=torch.int32, device='npu') + D = torch.zeros(shape, dtype=torch.int32, device='npu') + kernel[(1, )](A, B, C, D, BLOCK=shape[0]) + assert torch.equal(C, A // B) and torch.equal(D, A % B) diff --git a/third_party/ascend/unittest/pytest_ut/test_mask_negative_dim_clamp.py b/third_party/ascend/unittest/pytest_ut/test_mask_negative_dim_clamp.py new file mode 100644 index 0000000000..1e88f82173 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_mask_negative_dim_clamp.py @@ -0,0 +1,91 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import triton +import triton.language as tl + + +@triton.jit +def kernel_minimal( + idx_ptr, + table_ptr, + out_ptr, + y0_numel, + x1_numel, + Y0BLOCK: tl.constexpr, + Y0BLOCK_SUB: tl.constexpr, + X1BLOCK_SUB: tl.constexpr, +): + y0_offset = tl.program_id(0) * Y0BLOCK + base_y0 = tl.arange(0, Y0BLOCK_SUB) + loops_y0 = (Y0BLOCK + Y0BLOCK_SUB - 1) // Y0BLOCK_SUB + base_x1 = tl.arange(0, X1BLOCK_SUB) + loops_x1 = (x1_numel + X1BLOCK_SUB - 1) // X1BLOCK_SUB + for loop_y0 in range(loops_y0): + y0 = y0_offset + (loop_y0 * Y0BLOCK_SUB) + base_y0[:, None] + y0_mask = y0 < min(Y0BLOCK + y0_offset, y0_numel) + for loop_x1 in range(loops_x1): + x1 = (loop_x1 * X1BLOCK_SUB) + base_x1[None, :] + x1_mask = x1 < x1_numel + + m_lo = (y0 >= 64) & (y0 < 128) & y0_mask + idx_lo = tl.load(idx_ptr + ((-64) + y0), m_lo, other=0) + + m_hi = (y0 >= 128) & y0_mask + idx_hi = tl.load(idx_ptr + ((-128) + y0), m_hi, other=0) + + val_lo = tl.load(table_ptr + (x1 + 2048 * idx_lo), x1_mask, other=0.0) + val_hi = tl.load(table_ptr + (x1 + 2048 * idx_hi), x1_mask, other=0.0) + + result = tl.where(m_hi, val_hi, tl.where(m_lo, val_lo, 0.0)) + tl.store(out_ptr + (x1 + 2048 * y0), result, x1_mask & y0_mask) + + +def _reference(idx, table): + ref = torch.zeros((192, 2048), device=table.device, dtype=table.dtype) + flat = idx.flatten() + ref[64:128] = table[flat[0:64]] + ref[128:192] = table[flat[0:64]] + return ref.unsqueeze(0) + + +def test(): + device = 'npu' + idx = torch.randint(0, 95, (1, 64), device=device, dtype=torch.int64) + table = torch.randn((96, 2048), device=device, dtype=torch.float32) + out = torch.empty((1, 192, 2048), device=device, dtype=torch.float32) + kernel_minimal[(36, )]( + idx, + table, + out, + 192, + 2048, + 6, + 4, + 2048, + multibuffer=False, + ) + torch.npu.synchronize() + torch.testing.assert_close(out, _reference(idx, table)) + + +if __name__ == "__main__": + test() diff --git a/third_party/ascend/unittest/pytest_ut/test_max_constancy.py b/third_party/ascend/unittest/pytest_ut/test_max_constancy.py new file mode 100644 index 0000000000..6798790144 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_max_constancy.py @@ -0,0 +1,27 @@ +import torch +import triton +import triton.language as tl +import triton.language.extra.cann.extension as extension +import pytest + + +@triton.jit +def compile_hint_kernel(input_ptr, output_ptr, n_elements: tl.constexpr, SIZE: tl.constexpr): + offsets = tl.arange(0, n_elements) + val = tl.load(input_ptr + offsets) + val = tl.max_constancy(val, SIZE) + tl.store(output_ptr + offsets, val) + + +@pytest.mark.parametrize('sigtype', [ + 'int32', + #'int64', 'int16', 'int8', + #'uint8', 'uint16', 'uint32', 'uint64', + #'float32', 'float16', 'bfloat16', 'bool' +]) +def test_compile_hint(sigtype): + n_elements = 10 + dtype = eval(f"torch.{sigtype}") + x = torch.ones((n_elements, ), dtype=dtype).npu() + y = torch.zeros((n_elements, ), dtype=dtype).npu() + compile_hint_kernel[(1, )](x, y, n_elements, 1) diff --git a/third_party/ascend/unittest/pytest_ut/test_max_contiguous.py b/third_party/ascend/unittest/pytest_ut/test_max_contiguous.py new file mode 100644 index 0000000000..c5c2268250 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_max_contiguous.py @@ -0,0 +1,26 @@ +import torch +import triton +import triton.language as tl +import pytest + + +@triton.jit +def compile_hint_kernel(input_ptr, output_ptr, n_elements: tl.constexpr, SIZE: tl.constexpr): + offsets = tl.arange(0, n_elements) + val = tl.load(input_ptr + offsets) + val = tl.max_contiguous(val, SIZE) + tl.store(output_ptr + offsets, val) + + +@pytest.mark.parametrize('sigtype', [ + 'int32', + #'int64', 'int16', 'int8', + #'uint8', 'uint16', 'uint32', 'uint64', + #'float32', 'float16', 'bfloat16', 'bool' +]) +def test_compile_hint(sigtype): + n_elements = 10 + dtype = eval(f"torch.{sigtype}") + x = torch.ones((n_elements, ), dtype=dtype).npu() + y = torch.zeros((n_elements, ), dtype=dtype).npu() + compile_hint_kernel[(1, )](x, y, n_elements, 1) diff --git a/third_party/ascend/unittest/pytest_ut/test_max_dim0.py b/third_party/ascend/unittest/pytest_ut/test_max_dim0.py new file mode 100644 index 0000000000..3ebcd9e225 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_max_dim0.py @@ -0,0 +1,130 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def standard_max(x0, dim, dtype): + # fix with aclnnMaxDim support list:[DT_FLOAT,DT_FLOAT16,DT_INT64,DT_BOOL,DT_BFLOAT16,]. + if x0.dtype == torch.int8: + x0 = x0.to(torch.int64) + res, index = torch.max(x0, dim) + return res.to(dtype) + + +@triton.jit +def triton_max_dim0(in_ptr0, out_ptr0, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + + mmask = mblk_idx < M + nmask = nblk_idx < N + + mask = (mmask[:, None]) & (nmask[None, :]) + + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + + x = tl.load(in_ptr0 + idx, mask=mask, other=-float('inf')) + + ret = tl.max(x, 0) + + tl.store(out_ptr0 + nblk_idx, ret, mask=nmask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16,'bfloat16'), TODO: waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), TODO: waiting for supporting or testing + # (torch.int32,'int32'), TODO: waiting for supporting or testing + # (torch.int64,'int64'), TODO: waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (57, 3, 64, 16), + (57, -32, 64, 32), + (57, 37, 64, 64), + (57, -256, 64, 256), + (57, 263, 64, 512), + (64, 3, 64, 16), + (64, -32, 64, 32), + (64, 37, 64, 64), + (64, -256, 64, 256), + (64, 263, 64, 512), + (3, 3, 8, 8), + (-32, 3, 32, 8), + (37, 3, 64, 8), + (-256, 3, 256, 8), + (263, 3, 512, 8), + (3, 1, 8, 8), + (-32, 1, 32, 8), + (37, 1, 64, 8), + (-256, 1, 256, 8), + (263, 1, 512, 8), +] + +map_for_64_t = {37: (31, 32), 263: (107, 128)} +map_for_32_t = {263: (137, 256)} + + +# @pytest.mark.parametrize('dtype, sigtype',[(torch.float32,'float32'),]) +@pytest.mark.parametrize('M, N, MNUMEL, NNUMEL', [(64, -32, 64, 32)]) +@pytest.mark.parametrize('dtype, sigtype', types) +# @pytest.mark.parametrize('M, N, MNUMEL, NNUMEL',shapes) +def test_max_dim0(dtype, sigtype, M, N, MNUMEL, NNUMEL): + + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M][0] if M in map_for_64_t else M + MNUMEL = map_for_64_t[M][1] if M in map_for_64_t else MNUMEL + N = map_for_64_t[N][0] if N in map_for_64_t else N + NNUMEL = map_for_64_t[N][1] if N in map_for_64_t else NNUMEL + + elif sigtype == 'float32' or sigtype == 'bfloat16' or sigtype == 'int32': + M = map_for_32_t[M][0] if M in map_for_32_t else M + MNUMEL = map_for_32_t[M][1] if M in map_for_32_t else MNUMEL + N = map_for_32_t[N][0] if N in map_for_32_t else N + NNUMEL = map_for_32_t[N][1] if N in map_for_32_t else NNUMEL + + print(f"max : ({M}, {N}) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(M, N), dtype=sigtype) + + ans = standard_max(x0, 0, dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((N, ), dtype=dtype).npu() + triton_max_dim0[1, 1, 1](x0, output, M=M, N=N, MNUMEL=MNUMEL, NNUMEL=NNUMEL) + print(output) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_max_dim1.py b/third_party/ascend/unittest/pytest_ut/test_max_dim1.py new file mode 100644 index 0000000000..77999b76fe --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_max_dim1.py @@ -0,0 +1,137 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def standard_max(x0, dim, dtype): + # fix with aclnnMaxDim support list:[DT_FLOAT,DT_FLOAT16,DT_INT64,DT_BOOL,DT_BFLOAT16,]. + if x0.dtype == torch.int8: + x0 = x0.to(torch.int64) + res, index = torch.max(x0, dim) + return res.to(dtype) + + +@triton.jit +def triton_max_dim1(in_ptr0, out_ptr0, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + + mmask = mblk_idx < M + nmask = nblk_idx < N + + mask = (mmask[:, None]) & (nmask[None, :]) + + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + + if in_ptr0.dtype == tl.int8: + padding = -128 + else: + padding = -float('inf') + + x = tl.load(in_ptr0 + idx, mask=mask, other=padding) + + ret = tl.max(x, 1) + + tl.store(out_ptr0 + mblk_idx, ret, mask=mmask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16,'bfloat16'), waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), waiting for supporting or testing + # (torch.int32,'int32'), waiting for supporting or testing + # (torch.int64,'int64'), waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (57, 3, 64, 16), + (57, -32, 64, 32), + (57, 37, 64, 64), + (57, -256, 64, 256), + (57, 263, 64, 512), + (64, 3, 64, 16), + (64, -32, 64, 32), + (64, 37, 64, 64), + (64, -256, 64, 256), + (64, 263, 64, 512), + (3, 3, 8, 8), + (-32, 3, 32, 8), + (37, 3, 64, 8), + (-256, 3, 256, 8), + (263, 3, 512, 8), + (3, 1, 8, 8), + (-32, 1, 32, 8), + (37, 1, 64, 8), + (-256, 1, 256, 8), + (263, 1, 512, 8), +] + +map_for_64_t = {37: (31, 32), 263: (107, 128)} +map_for_32_t = {263: (137, 256)} + + +@pytest.mark.parametrize('M, N, MNUMEL, NNUMEL', [ + (64, -32, 64, 32), +]) +# @pytest.mark.parametrize('M, N',[(263,3),(-256,3)]) +@pytest.mark.parametrize('dtype, sigtype', types) +# @pytest.mark.parametrize('M, N, MNUMEL, NNUMEL',shapes) +def test_max_dim1(dtype, sigtype, M, N, MNUMEL, NNUMEL): + + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M][0] if M in map_for_64_t else M + MNUMEL = map_for_64_t[M][1] if M in map_for_64_t else MNUMEL + N = map_for_64_t[N][0] if N in map_for_64_t else N + NNUMEL = map_for_64_t[N][1] if N in map_for_64_t else NNUMEL + + elif sigtype == 'float32' or sigtype == 'bfloat16' or sigtype == 'int32': + M = map_for_32_t[M][0] if M in map_for_32_t else M + MNUMEL = map_for_32_t[M][1] if M in map_for_32_t else MNUMEL + N = map_for_32_t[N][0] if N in map_for_32_t else N + NNUMEL = map_for_32_t[N][1] if N in map_for_32_t else NNUMEL + + print(f"max : ({M}, {N}) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(M, N), dtype=sigtype) + + ans = standard_max(x0, 1, dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((M, ), dtype=dtype).npu() + triton_max_dim1[1, 1, 1](x0, output, M=M, N=N, MNUMEL=MNUMEL, NNUMEL=NNUMEL) + print(output) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_max_propagate_nan.py b/third_party/ascend/unittest/pytest_ut/test_max_propagate_nan.py new file mode 100644 index 0000000000..708a9fb2e6 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_max_propagate_nan.py @@ -0,0 +1,54 @@ +import torch +import torch_npu +import triton +import triton.language as tl +import pytest + + +def test_max_default_behavior(): + + @triton.jit + def func(in_ptr, out_ptr): + a = tl.load(in_ptr + tl.arange(0, 8)[:, None] * 8 + tl.arange(0, 8)[None, :]) + a = tl.max(a, 0) + tl.store(out_ptr + tl.arange(0, 8), a) + + a = torch.randn((8, 8), device="npu") + std = a.max(0)[0] + ans = torch.zeros((8, ), dtype=torch.float32, device="npu") + func[1, 1, 1](a, ans) + torch.testing.assert_close(std, ans) + + +def test_max_propagate_nan_all(): + + @triton.jit + def func(in_ptr, out_ptr): + a = tl.load(in_ptr + tl.arange(0, 8)[:, None] * 8 + tl.arange(0, 8)[None, :]) + a = tl.max(a, 0, propagate_nan=tl.PropagateNan.ALL) + tl.store(out_ptr + tl.arange(0, 8), a) + + a = torch.randn((8, 8), device="npu") + std = a.max(0)[0] + ans = torch.zeros((8, ), dtype=torch.float32, device="npu") + func[1, 1, 1](a, ans) + torch.testing.assert_close(std, ans) + + +def test_max_propagate_nan_via_subfunction(): + + @triton.jit + def sub_max(x, dim: tl.constexpr, propagatenan: tl.constexpr): + return tl.max(x, dim, propagate_nan=propagatenan) + + @triton.jit + def func(in_ptr, out_ptr): + a = tl.load(in_ptr + tl.arange(0, 8)[:, None] * 8 + tl.arange(0, 8)[None, :]) + res = sub_max(a, 0, tl.PropagateNan.ALL) + tl.store(out_ptr + tl.arange(0, 8), res) + + a = torch.randn((8, 8), device="npu") + std = a.max(0)[0] + ans = torch.zeros((8, ), dtype=torch.float32, device="npu") + func[1, 1, 1](a, ans) + torch.testing.assert_close(std, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_max_vector.py b/third_party/ascend/unittest/pytest_ut/test_max_vector.py new file mode 100644 index 0000000000..5e3d8f8fa9 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_max_vector.py @@ -0,0 +1,89 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_(x0, dtype): + res, index = torch.max(x0, 0, keepdim=True) + return res + + +@triton.jit +def triton_max_vector(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + + if in_ptr0.dtype == tl.int8: + padding = -128 + else: + padding = -float('inf') + + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N, other=padding) + ret = tl.max(x, 0) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < 1) + + +types = [ + (torch.float32, 'float32'), + # (torch.float16,'float16'), TODO : fix reduceConverter bug + # (torch.bfloat16,'bfloat16'), waiting for supporting or testing + # (torch.int8,'int8'), TODO : fix compiler bug + # (torch.int16,'int16'), waiting for supporting or testing + # (torch.int32,'int32'), waiting for supporting or testing + # (torch.int64,'int64'), waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.skip(reason="randomly failed") +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_reduce_dim0_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_(x0, dtype) + x0 = x0.npu() + + output = torch.zeros((1, ), dtype=dtype).npu() + triton_max_vector[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_maximum.py b/third_party/ascend/unittest/pytest_ut/test_maximum.py new file mode 100644 index 0000000000..abdd398e14 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_maximum.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import test_common + + +def torch_maximum(x0, x1): + res = torch.maximum(x0, x1) + return res + + +@triton.jit +def triton_maximum(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + x_index = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = x_index < xnumel + tmp0 = tl.load(in_ptr0 + x_index, xmask) + tmp1 = tl.load(in_ptr1 + x_index, xmask) + tmp2 = tl.maximum(tmp0, tmp1) + tl.store(out_ptr0 + x_index, tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_maximum(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_maximum(x0, x1) + # triton结果 + triton_res = test_common.generate_tensor(shape, dtype).npu() + triton_maximum[ncore, 1, 1](x0, x1, triton_res, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_mean_dim0.py b/third_party/ascend/unittest/pytest_ut/test_mean_dim0.py new file mode 100644 index 0000000000..f317ca7f74 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_mean_dim0.py @@ -0,0 +1,145 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def standard_mean(x0, dim, dtype): + res = torch.mean(x0, dim, dtype=dtype) + return res + + +@triton.jit +def triton_mean_dim0(in_ptr0, out_ptr0, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + + mmask = mblk_idx < M + nmask = nblk_idx < N + + mask = (mmask[:, None]) & (nmask[None, :]) + + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + + x = tl.load(in_ptr0 + idx, mask=mask, other=0) + + if x.dtype == tl.bfloat16: + ret = (tl.sum(x.to(tl.float32), 0) / M).to(tl.bfloat16) + elif x.dtype == tl.float16: + ret = tl.sum(x, 0) / M + else: + ret = tl.sum(x.to(tl.float32), 0) / M + + tl.store(out_ptr0 + nblk_idx, ret, mask=nmask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16,'bfloat16'), TODO: waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), TODO: waiting for supporting or testing + # (torch.int32,'int32'), TODO: waiting for supporting or testing + # (torch.int64,'int64'), TODO: waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (57, 3, 64, 16), + (57, -32, 64, 32), + (57, 37, 64, 64), + (57, -256, 64, 256), + (57, 263, 64, 512), + (64, 3, 64, 16), + (64, -32, 64, 32), + (64, 37, 64, 64), + (64, -256, 64, 256), + (64, 263, 64, 512), + (3, 3, 8, 8), + (-32, 3, 32, 8), + (37, 3, 64, 8), + (-256, 3, 256, 8), + (263, 3, 512, 8), + (3, 1, 8, 8), + (-32, 1, 32, 8), + (37, 1, 64, 8), + (-256, 1, 256, 8), + (263, 1, 512, 8), +] + +map_for_64_t = {37: (31, 32)} +map_for_32_t = {263: (137, 256)} + + +# @pytest.mark.parametrize('dtype, sigtype',[(torch.float32,'float32'),]) +@pytest.mark.parametrize('M, N, MNUMEL, NNUMEL', [ + (57, 3, 64, 16), + (64, -32, 64, 32), + (37, 3, 64, 8), + (263, 1, 512, 8), + (-256, 3, 256, 8), +]) +@pytest.mark.parametrize('dtype, sigtype', types) +# @pytest.mark.parametrize('M, N, MNUMEL, NNUMEL',shapes) +def test_mean_dim0(dtype, sigtype, M, N, MNUMEL, NNUMEL): + + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M][0] if M in map_for_64_t else M + MNUMEL = map_for_64_t[M][1] if M in map_for_64_t else MNUMEL + N = map_for_64_t[N][0] if N in map_for_64_t else N + NNUMEL = map_for_64_t[N][1] if N in map_for_64_t else NNUMEL + + res_dtype = dtype + res_sigtype = sigtype + should_cast_to_fp32 = ['int8', 'int16', 'int32', 'int64', 'float32', 'bfloat16'] + + if sigtype in should_cast_to_fp32: + M = map_for_32_t[M][0] if M in map_for_32_t else M + MNUMEL = map_for_32_t[M][1] if M in map_for_32_t else MNUMEL + N = map_for_32_t[N][0] if N in map_for_32_t else N + NNUMEL = map_for_32_t[N][1] if N in map_for_32_t else NNUMEL + if sigtype != 'bfloat16': + res_dtype = torch.float32 + res_sigtype = 'float32' + + print(f"sum : ({M}, {N}) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(M, N), dtype=sigtype) + + ans = standard_mean(x0, 0, res_dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((N, ), dtype=res_dtype).npu() + triton_mean_dim0[1, 1, 1](x0, output, M=M, N=N, MNUMEL=MNUMEL, NNUMEL=NNUMEL, debug=True) + print(output) + + test_common.validate_cmp(res_sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_mean_dim1.py b/third_party/ascend/unittest/pytest_ut/test_mean_dim1.py new file mode 100644 index 0000000000..e79f09c4ee --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_mean_dim1.py @@ -0,0 +1,145 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def standard_mean(x0, dim, dtype): + res = torch.mean(x0, dim, dtype=dtype) + return res + + +@triton.jit +def triton_mean_dim1(in_ptr0, out_ptr0, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + + mmask = mblk_idx < M + nmask = nblk_idx < N + + mask = (mmask[:, None]) & (nmask[None, :]) + + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + + x = tl.load(in_ptr0 + idx, mask=mask, other=0) + + if x.dtype == tl.bfloat16: + ret = (tl.sum(x.to(tl.float32), 1) / N).to(tl.bfloat16) + elif x.dtype == tl.float16: + ret = tl.sum(x, 1) / N + else: + ret = tl.sum(x.to(tl.float32), 1) / N + + tl.store(out_ptr0 + mblk_idx, ret, mask=mmask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16,'bfloat16'), TODO: waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), TODO: waiting for supporting or testing + # (torch.int32,'int32'), TODO: waiting for supporting or testing + # (torch.int64,'int64'), TODO: waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (57, 3, 64, 16), + (57, -32, 64, 32), + (57, 37, 64, 64), + (57, -256, 64, 256), + (57, 263, 64, 512), + (64, 3, 64, 16), + (64, -32, 64, 32), + (64, 37, 64, 64), + (64, -256, 64, 256), + (64, 263, 64, 512), + (3, 3, 8, 8), + (-32, 3, 32, 8), + (37, 3, 64, 8), + (-256, 3, 256, 8), + (263, 3, 512, 8), + (3, 1, 8, 8), + (-32, 1, 32, 8), + (37, 1, 64, 8), + (-256, 1, 256, 8), + (263, 1, 512, 8), +] + +map_for_64_t = {37: (31, 32)} +map_for_32_t = {263: (137, 256)} + + +# @pytest.mark.parametrize('dtype, sigtype',[(torch.float32,'float32'),]) +@pytest.mark.parametrize('M, N, MNUMEL, NNUMEL', [ + (57, 3, 64, 16), + (64, -32, 64, 32), + (37, 3, 64, 8), + (263, 1, 512, 8), + (-256, 3, 256, 8), +]) +@pytest.mark.parametrize('dtype, sigtype', types) +# @pytest.mark.parametrize('M, N, MNUMEL, NNUMEL',shapes) +def test_mean_dim1(dtype, sigtype, M, N, MNUMEL, NNUMEL): + + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M][0] if M in map_for_64_t else M + MNUMEL = map_for_64_t[M][1] if M in map_for_64_t else MNUMEL + N = map_for_64_t[N][0] if N in map_for_64_t else N + NNUMEL = map_for_64_t[N][1] if N in map_for_64_t else NNUMEL + + res_dtype = dtype + res_sigtype = sigtype + should_cast_to_fp32 = ['int8', 'int16', 'int32', 'int64', 'float32', 'bfloat16'] + + if sigtype in should_cast_to_fp32: + M = map_for_32_t[M][0] if M in map_for_32_t else M + MNUMEL = map_for_32_t[M][1] if M in map_for_32_t else MNUMEL + N = map_for_32_t[N][0] if N in map_for_32_t else N + NNUMEL = map_for_32_t[N][1] if N in map_for_32_t else NNUMEL + if sigtype != 'bfloat16': + res_dtype = torch.float32 + res_sigtype = 'float32' + + print(f"sum : ({M}, {N}) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(M, N), dtype=sigtype) + + ans = standard_mean(x0, 1, res_dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((M, ), dtype=res_dtype).npu() + triton_mean_dim1[1, 1, 1](x0, output, M=M, N=N, MNUMEL=MNUMEL, NNUMEL=NNUMEL, debug=True) + print(output) + + test_common.validate_cmp(res_sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_mean_vector.py b/third_party/ascend/unittest/pytest_ut/test_mean_vector.py new file mode 100644 index 0000000000..7c40a2b5cf --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_mean_vector.py @@ -0,0 +1,105 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def standard_mean(x0, dim, dtype): + res = torch.mean(x0, dim, keepdim=True, dtype=dtype) + return res + + +@triton.jit +def triton_mean_dim0(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N, other=0) + + if x.dtype == tl.bfloat16: + ret = (tl.sum(x.to(tl.float32), 0) / N).to(tl.bfloat16) + elif x.dtype == tl.float16: + ret = tl.sum(x, 0) / N + else: + ret = tl.sum(x.to(tl.float32), 0) / N + + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < 1) + + +types = [ + (torch.float32, 'float32'), + # (torch.float16,'float16'), TODO: should fix reduceConverter's bug + # (torch.bfloat16,'bfloat16'), TODO: waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), TODO: waiting for supporting or testing + # (torch.int32,'int32'), TODO: waiting for supporting or testing + # (torch.int64,'int64'), TODO: waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: (31, 32)} + + +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_mean_dim0(dtype, sigtype, N, NUMEL): + + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N][0] if N in map_for_64_t else N + NUMEL = map_for_64_t[N][1] if N in map_for_64_t else NUMEL + + res_dtype = dtype + res_sigtype = sigtype + should_cast_to_fp32 = ['int8', 'int16', 'int32', 'int64', 'float32'] + + if sigtype in should_cast_to_fp32: + res_dtype = torch.float32 + res_sigtype = 'float32' + + print(f"sum : ({N},) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_mean(x0, 0, res_dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((1, ), dtype=res_dtype).npu() + triton_mean_dim0[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + print(output) + + test_common.validate_cmp(res_sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_merge_concat_load_buffer.py b/third_party/ascend/unittest/pytest_ut/test_merge_concat_load_buffer.py new file mode 100644 index 0000000000..eeac2ad690 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_merge_concat_load_buffer.py @@ -0,0 +1,176 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""End-to-end coverage for the merge-concat-load-buffer pass. + +Two masked tl.load results that are concatenated lower to two full-tile UB +allocs, two linalg.fill ops and an extract_slice/insert_slice pair. The pass +folds the two allocs into one and drops the fills when it can prove the copies +cover every later read. + +The kernels below reproduce that shape so the optimised code is exercised on +device. Padding correctness is the interesting part: if the pass ever dropped a +fill it could not prove dead, the `other` value would be replaced by whatever +was left in UB, so `test_cat_masked_load_padding_visible` stores the whole tile +instead of masking the tail away. + +The pipeline only enables the pass on 910_95/950, so on other targets these +tests cover the unoptimised lowering of the same kernels rather than the pass. +""" + +import pytest +import torch +import torch_npu + +import triton +import triton.language as tl + +import test_common + + +@triton.jit +def fused_cat_masked_load_kernel(in_ptr0, in_ptr1, out_ptr, y_numel, HALF: tl.constexpr, YBLOCK: tl.constexpr, + OTHER: tl.constexpr, MASK_STORE: tl.constexpr): + """Concatenate two row-masked loads along the column axis. + + This mirrors what torch-inductor emits for a `cat` of two tensors: one + full-width tile whose left half is loaded from `in_ptr0` and whose right + half is loaded from `in_ptr1` at a negative bias. + """ + yoffset = tl.program_id(0) * YBLOCK + row = (yoffset + tl.arange(0, YBLOCK))[:, None] + col = tl.arange(0, 2 * HALF)[None, :] + + row_mask = row < y_numel + from_left = col < HALF + + left = tl.load(in_ptr0 + row * HALF + col, mask=row_mask & from_left, other=OTHER) + right = tl.load(in_ptr1 + row * HALF + (col - HALF), mask=row_mask & (col >= HALF), other=OTHER) + result = tl.where(from_left, left, right) + + if MASK_STORE: + tl.store(out_ptr + row * (2 * HALF) + col, result, mask=row_mask) + else: + tl.store(out_ptr + row * (2 * HALF) + col, result) + + +@triton.jit +def cat_masked_load_1d_kernel(x_ptr, y_ptr, out_ptr, n_valid, BLOCK: tl.constexpr, OTHER: tl.constexpr): + idx = tl.arange(0, BLOCK) + mask = idx < n_valid + + x = tl.load(x_ptr + idx, mask=mask, other=OTHER) + y = tl.load(y_ptr + idx, mask=mask, other=OTHER) + result = tl.cat(x, y, can_reorder=True) + + tl.store(out_ptr + tl.arange(0, 2 * BLOCK), result) + + +def _reference(x0, x1, rows, half, y_valid, tail): + """Expected output tile: valid rows carry the concat, the rest carry `tail`.""" + ref = torch.full((rows, 2 * half), tail, dtype=x0.dtype) + ref[:y_valid, :half] = x0[:y_valid] + ref[:y_valid, half:] = x1[:y_valid] + return ref + + +# y_numel is deliberately not a multiple of YBLOCK in most cases so the tail +# tile is partially masked and the padding path is actually taken. +@pytest.mark.parametrize('dtype', ['float32', 'float16', 'int32']) +@pytest.mark.parametrize('y_numel, yblock, half', [ + (32, 32, 256), + (30, 32, 256), + (70, 32, 128), + (1, 32, 64), + (100, 16, 32), +]) +def test_cat_masked_load_masked_store(dtype, y_numel, yblock, half): + """Concat of two masked loads, tail rows masked away on store.""" + torch_dtype = eval('torch.' + dtype) + n_prog = triton.cdiv(y_numel, yblock) + rows = n_prog * yblock + sentinel = -1 + + x0 = test_common.generate_tensor((rows, half), dtype) + x1 = test_common.generate_tensor((rows, half), dtype) + out = torch.full((rows, 2 * half), sentinel, dtype=torch_dtype) + + # Tail rows are never stored, so they keep the sentinel the buffer held. + ref = _reference(x0, x1, rows, half, y_valid=y_numel, tail=sentinel) + + out_npu = out.npu() + fused_cat_masked_load_kernel[n_prog, 1, 1](x0.npu(), x1.npu(), out_npu, y_numel, HALF=half, YBLOCK=yblock, OTHER=0, + MASK_STORE=True) + + test_common.validate_cmp(dtype, out_npu, ref) + + +@pytest.mark.parametrize('dtype', ['float32', 'float16']) +@pytest.mark.parametrize('y_numel, yblock, half', [ + (30, 32, 256), + (70, 32, 128), + (1, 32, 64), +]) +def test_cat_masked_load_padding_visible(dtype, y_numel, yblock, half): + """Same concat, but the whole tile is stored so `other` padding is observed. + + This is the case where the pass must not drop the fill it cannot prove + dead: rows in [y_numel, rows) are never written by a copy, so their value + comes purely from the padding. + """ + torch_dtype = eval('torch.' + dtype) + n_prog = triton.cdiv(y_numel, yblock) + rows = n_prog * yblock + other = -7.5 + + x0 = test_common.generate_tensor((rows, half), dtype) + x1 = test_common.generate_tensor((rows, half), dtype) + out = torch.zeros((rows, 2 * half), dtype=torch_dtype) + + ref = _reference(x0, x1, rows, half, y_valid=y_numel, tail=other) + + out_npu = out.npu() + fused_cat_masked_load_kernel[n_prog, 1, 1](x0.npu(), x1.npu(), out_npu, y_numel, HALF=half, YBLOCK=yblock, + OTHER=other, MASK_STORE=False) + + test_common.validate_cmp(dtype, out_npu, ref) + + +@pytest.mark.parametrize('dtype', ['float32', 'float16']) +@pytest.mark.parametrize('block, n_valid', [ + (64, 64), + (64, 40), + (128, 1), +]) +def test_cat_masked_load_1d(dtype, block, n_valid): + """1-D tl.cat of two masked loads; the padding is always observable.""" + torch_dtype = eval('torch.' + dtype) + other = -3.25 + + x = test_common.generate_tensor((block, ), dtype) + y = test_common.generate_tensor((block, ), dtype) + + keep = torch.arange(block) < n_valid + pad = torch.full((block, ), other, dtype=torch_dtype) + ref = torch.cat((torch.where(keep, x, pad), torch.where(keep, y, pad))) + + out_npu = torch.zeros((2 * block, ), dtype=torch_dtype).npu() + cat_masked_load_1d_kernel[1, 1, 1](x.npu(), y.npu(), out_npu, n_valid, BLOCK=block, OTHER=other) + + test_common.validate_cmp(dtype, out_npu, ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_min_dim0.py b/third_party/ascend/unittest/pytest_ut/test_min_dim0.py new file mode 100644 index 0000000000..41125de925 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_min_dim0.py @@ -0,0 +1,152 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +@pytest.mark.skip(reason="to be supported by bishengir-compile") +def test_min_dim0_3d(): + + def torch_func(x, dim): + res = torch.min(x, dim) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N0: tl.constexpr, N1: tl.constexpr, N2: tl.constexpr): + idx0 = tl.arange(0, N0) + idx1 = tl.arange(0, N1) + idx2 = tl.arange(0, N2) + in_idx = idx2[None, None, :] + idx1[None, :, None] * N2 + idx0[:, None, None] * N2 * N1 + tmp0 = tl.load(in_ptr0 + in_idx) + tmp1 = tl.min(tmp0, 0) + out_idx = idx2[None, :] + idx1[:, None] * N2 + tl.store(out_ptr0 + out_idx, tmp1) + + def triton_func(x0, dim): + N0, N1, N2 = x0.size() + y0 = test_common.generate_tensor(shape=(N1, N2), dtype="float32").npu() + triton_kernel[1, 1, 1](y0, x0, N0, N1, N2) + return y0 + + dim = 0 + N0, N1, N2 = 1, 22, 13 + x0 = test_common.generate_tensor(shape=(N0, N1, N2), dtype="float32").npu() + torch_ref = torch_func(x0, dim) + triton_cal = triton_func(x0, dim) + test_common.validate_cmp("float32", triton_cal, torch_ref) + + +def standard_min(x0, dim, dtype): + # fix with aclnnMinDim support list:[DT_FLOAT,DT_FLOAT16,DT_INT64,DT_BOOL,DT_BFLOAT16,]. + if x0.dtype == torch.int8: + x0 = x0.to(torch.int64) + res, index = torch.min(x0, dim) + return res.to(dtype) + + +@triton.jit +def triton_min_dim0(in_ptr0, out_ptr0, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + + mmask = mblk_idx < M + nmask = nblk_idx < N + + mask = (mmask[:, None]) & (nmask[None, :]) + + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + if in_ptr0.dtype == tl.int8: + padding = 127 + else: + padding = float('inf') + x = tl.load(in_ptr0 + idx, mask=mask, other=padding) + + ret = tl.min(x, 0) + + tl.store(out_ptr0 + nblk_idx, ret, mask=nmask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16,'bfloat16'), TODO: waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), TODO: waiting for supporting or testing + # (torch.int32,'int32'), TODO: waiting for supporting or testing + # (torch.int64,'int64'), TODO: waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +#shapes=[ +# (57,3,64,16), (57,-32,64,32), (57,37,64,64), (57,-256,64,256), (57,263,64,512), +# (64,3,64,16), (64,-32,64,32), (64,37,64,64), (64,-256,64,256), (64,263,64,512), +# (3,3,8,8), (-32,3,32,8), (37,3,64,8), (-256,3,256,8), (263,3,512,8), +# (3,1,8,8), (-32,1,32,8), (37,1,64,8), (-256,1,256,8), (263,1,512,8), +#] +shapes = [ + (64, -32, 64, 32), +] + +map_for_64_t = {37: (31, 32), 263: (107, 128)} +map_for_32_t = {263: (137, 256)} + + +# @pytest.mark.parametrize('dtype, sigtype',[(torch.float32,'float32'),]) +@pytest.mark.parametrize('M, N, MNUMEL, NNUMEL', [(64, -32, 64, 32)]) +@pytest.mark.parametrize('dtype, sigtype', types) +# @pytest.mark.parametrize('M, N, MNUMEL, NNUMEL',shapes) +def test_min_dim0(dtype, sigtype, M, N, MNUMEL, NNUMEL): + + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M][0] if M in map_for_64_t else M + MNUMEL = map_for_64_t[M][1] if M in map_for_64_t else MNUMEL + N = map_for_64_t[N][0] if N in map_for_64_t else N + NNUMEL = map_for_64_t[N][1] if N in map_for_64_t else NNUMEL + + elif sigtype == 'float32' or sigtype == 'bfloat16' or sigtype == 'int32': + M = map_for_32_t[M][0] if M in map_for_32_t else M + MNUMEL = map_for_32_t[M][1] if M in map_for_32_t else MNUMEL + N = map_for_32_t[N][0] if N in map_for_32_t else N + NNUMEL = map_for_32_t[N][1] if N in map_for_32_t else NNUMEL + + print(f"min : ({M}, {N}) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(M, N), dtype=sigtype) + + ans = standard_min(x0, 0, dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((N, ), dtype=dtype).npu() + triton_min_dim0[1, 1, 1](x0, output, M=M, N=N, MNUMEL=MNUMEL, NNUMEL=NNUMEL) + print(output) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_min_dim1.py b/third_party/ascend/unittest/pytest_ut/test_min_dim1.py new file mode 100644 index 0000000000..b2567e18d4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_min_dim1.py @@ -0,0 +1,135 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def standard_min(x0, dim, dtype): + # fix with aclnnMinDim support list:[DT_FLOAT,DT_FLOAT16,DT_INT64,DT_BOOL,DT_BFLOAT16,]. + if x0.dtype == torch.int8: + x0 = x0.to(torch.int64) + res, index = torch.min(x0, dim) + return res.to(dtype) + + +@triton.jit +def triton_min_dim1(in_ptr0, out_ptr0, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + + mmask = mblk_idx < M + nmask = nblk_idx < N + + mask = (mmask[:, None]) & (nmask[None, :]) + + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + if in_ptr0.dtype == tl.int8: + padding = 127 + else: + padding = float('inf') + x = tl.load(in_ptr0 + idx, mask=mask, other=padding) + + ret = tl.min(x, 1) + + tl.store(out_ptr0 + mblk_idx, ret, mask=mmask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16,'bfloat16'), waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), waiting for supporting or testing + # (torch.int32,'int32'), waiting for supporting or testing + # (torch.int64,'int64'), waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (57, 3, 64, 16), + (57, -32, 64, 32), + (57, 37, 64, 64), + (57, -256, 64, 256), + (57, 263, 64, 512), + (64, 3, 64, 16), + (64, -32, 64, 32), + (64, 37, 64, 64), + (64, -256, 64, 256), + (64, 263, 64, 512), + (3, 3, 8, 8), + (-32, 3, 32, 8), + (37, 3, 64, 8), + (-256, 3, 256, 8), + (263, 3, 512, 8), + (3, 1, 8, 8), + (-32, 1, 32, 8), + (37, 1, 64, 8), + (-256, 1, 256, 8), + (263, 1, 512, 8), +] + +map_for_64_t = {37: (31, 32), 263: (107, 128)} +map_for_32_t = {263: (137, 256)} + + +@pytest.mark.parametrize('M, N, MNUMEL, NNUMEL', [ + (64, -32, 64, 32), +]) +# @pytest.mark.parametrize('M, N',[(263,3),(-256,3)]) +@pytest.mark.parametrize('dtype, sigtype', types) +# @pytest.mark.parametrize('M, N, MNUMEL, NNUMEL',shapes) +def test_min_dim1(dtype, sigtype, M, N, MNUMEL, NNUMEL): + + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M][0] if M in map_for_64_t else M + MNUMEL = map_for_64_t[M][1] if M in map_for_64_t else MNUMEL + N = map_for_64_t[N][0] if N in map_for_64_t else N + NNUMEL = map_for_64_t[N][1] if N in map_for_64_t else NNUMEL + + elif sigtype == 'float32' or sigtype == 'bfloat16' or sigtype == 'int32': + M = map_for_32_t[M][0] if M in map_for_32_t else M + MNUMEL = map_for_32_t[M][1] if M in map_for_32_t else MNUMEL + N = map_for_32_t[N][0] if N in map_for_32_t else N + NNUMEL = map_for_32_t[N][1] if N in map_for_32_t else NNUMEL + + print(f"min : ({M}, {N}) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(M, N), dtype=sigtype) + + ans = standard_min(x0, 1, dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((M, ), dtype=dtype).npu() + triton_min_dim1[1, 1, 1](x0, output, M=M, N=N, MNUMEL=MNUMEL, NNUMEL=NNUMEL) + print(output) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_min_vector.py b/third_party/ascend/unittest/pytest_ut/test_min_vector.py new file mode 100644 index 0000000000..7a0a6c9255 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_min_vector.py @@ -0,0 +1,88 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import test_common +import torch +import torch_npu + + +def standard_(x0, dtype): + res, index = torch.min(x0, 0, keepdim=True) + return res + + +@triton.jit +def triton_min_vector(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + if in_ptr0.dtype == tl.int8: + padding = 127 + else: + padding = float('inf') + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N, other=padding) + + ret = tl.min(x, 0) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < 1) + + +types = [ + (torch.float32, 'float32'), + # (torch.float16,'float16'), TODO : fix reduceConverter bug + # (torch.bfloat16,'bfloat16'), waiting for supporting or testing + # (torch.int8,'int8'), TODO : fix compiler bug + # (torch.int16,'int16'), waiting for supporting or testing + # (torch.int32,'int32'), waiting for supporting or testing + # (torch.int64,'int64'), waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.skip(reason="randomly failed") +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_reduce_dim0_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_(x0, dtype) + x0 = x0.npu() + + output = torch.zeros((1, ), dtype=dtype).npu() + triton_min_vector[1, 1, 1](x0, output, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_minimum.py b/third_party/ascend/unittest/pytest_ut/test_minimum.py new file mode 100644 index 0000000000..8029cbfa6d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_minimum.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import test_common + + +def torch_minimum(x0, x1): + res = torch.minimum(x0, x1) + return res + + +@triton.jit +def triton_minimum(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + x_index = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = x_index < xnumel + tmp0 = tl.load(in_ptr0 + x_index, xmask) + tmp1 = tl.load(in_ptr1 + x_index, xmask) + tmp2 = tl.minimum(tmp0, tmp1) + tl.store(out_ptr0 + x_index, tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_minimum(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + y_ref = torch_minimum(x0, x1) + # triton结果 + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_minimum[ncore, 1, 1](x0, x1, y_cal, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_mixed_axis_load_mask.py b/third_party/ascend/unittest/pytest_ut/test_mixed_axis_load_mask.py new file mode 100644 index 0000000000..ca09d5d257 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_mixed_axis_load_mask.py @@ -0,0 +1,176 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu # noqa: F401 +import triton +import triton.language as tl + + +@triton.jit +def _mixed_axis_bounds(input_ptr, output_ptr, rows, lower, upper, TENSOR_OTHER: tl.constexpr): + x = tl.arange(0, 3)[:, None] + r = tl.arange(0, 32)[None, :] + # A nonstructured row and a structured, strided column. Only the masked + # interval has storage; a nonzero lower bound makes preceding addresses + # invalid too. Empty intervals must perform no reads. + ptr = input_ptr + (x % 2) + (r - lower) * 16 + mask = (x < rows) & (r >= lower) & (r < upper) + other = (x * 100 + r).to(tl.float32) if TENSOR_OTHER else -7.0 + value = tl.load(ptr, mask, other=other) + tl.store(output_ptr + x * 32 + r, value) + + +@pytest.mark.parametrize("lower,upper", [(0, 32), (0, 20), (7, 23), (20, 7), (32, 32)]) +@pytest.mark.parametrize("rows", [0, 2, 3]) +@pytest.mark.parametrize("tensor_other", [False, True]) +def test_mixed_axis_bounds(lower, upper, rows, tensor_other): + source = torch.arange(max(upper - lower, 1) * 16, dtype=torch.float32).reshape(-1, 16) + x = torch.arange(3)[:, None] + r = torch.arange(32)[None, :] + expected = (x * 100 + r).float() if tensor_other else torch.full((3, 32), -7.0) + for row in range(rows): + if upper > lower: + expected[row, lower:upper] = source[:, row % 2] + output = torch.empty((3, 32), dtype=torch.float32, device="npu") + _mixed_axis_bounds[(1, )](source.npu(), output, rows, lower, upper, tensor_other, multibuffer=False) + torch.npu.synchronize() + torch.testing.assert_close(output.cpu(), expected, rtol=0, atol=0) + + +@triton.jit +def _mixed_axis_variance(input_ptr, output_ptr, xnumel, rnumel, WIDTH: tl.constexpr, XBLOCK: tl.constexpr, + RBLOCK: tl.constexpr): + x = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK)[:, None] + xmask = x < xnumel + rbase = tl.arange(0, RBLOCK)[None, :] + accum = tl.full((XBLOCK, RBLOCK), 0, tl.float32) + for start in range(0, rnumel, RBLOCK): + r = start + rbase + mask = xmask & (r < rnumel) + offset = x % WIDTH + WIDTH * r + WIDTH * rnumel * (x // WIDTH) + value = tl.load(input_ptr + offset, mask, other=0.0) + accum = tl.where(mask, accum + value, accum) + mean = tl.sum(accum, 1)[:, None] / rnumel + variance = tl.full((XBLOCK, RBLOCK), 0, tl.float32) + for start in range(0, rnumel, RBLOCK): + r = start + rbase + mask = xmask & (r < rnumel) + offset = x % WIDTH + WIDTH * r + WIDTH * rnumel * (x // WIDTH) + value = tl.load(input_ptr + offset, mask, other=0.0) + diff = value - mean + variance = tl.where(mask, variance + diff * diff, variance) + result = tl.sum(variance, 1) / rnumel + tl.store(output_ptr + tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK), result, + tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK) < xnumel) + + +@pytest.mark.parametrize("rnumel", [16, 32, 48, 64]) +def test_mixed_axis_reduction_tail(rnumel): + # XBLOCK=3 crosses WIDTH=8 boundaries and leaves one row in the final tile. + torch.manual_seed(0) + source = torch.randn((2, rnumel, 8), dtype=torch.float32) + expected = source.var(dim=1, correction=0).flatten() + output = torch.empty(16, dtype=torch.float32, device="npu") + _mixed_axis_variance[(triton.cdiv(16, 3), )](source.npu(), output, 16, rnumel, 8, 3, 32, multibuffer=False) + torch.npu.synchronize() + torch.testing.assert_close(output.cpu(), expected, rtol=1e-5, atol=1e-5) + + +@triton.jit +def _two_structured_axes(input_ptr, output_ptr, slo, shi, rlo, rhi, rstride): + x = tl.arange(0, 3)[:, None, None] + s = tl.arange(0, 4)[None, :, None] + r = tl.arange(0, 32)[None, None, :] + ptr = input_ptr + x % 2 + (s - slo) * rstride * 16 + (r - rlo) * 16 + mask = (s >= slo) & (s < shi) & (r >= rlo) & (r < rhi) + value = tl.load(ptr, mask, other=-7.0) + tl.store(output_ptr + x * 128 + s * 32 + r, value) + + +@pytest.mark.parametrize("slo,shi,rlo,rhi", [(0, 3, 0, 20), (1, 3, 7, 23), (3, 1, 0, 32)]) +def test_two_structured_axes(slo, shi, rlo, rhi): + ns, nr = max(shi - slo, 1), max(rhi - rlo, 1) + source = torch.arange(ns * nr * 16, dtype=torch.float32).reshape(ns, nr, 16) + expected = torch.full((3, 4, 32), -7.0) + if shi > slo and rhi > rlo: + for row in range(3): + expected[row, slo:shi, rlo:rhi] = source[:, :, row % 2] + output = torch.empty_like(expected, device="npu") + _two_structured_axes[(1, )](source.npu(), output, slo, shi, rlo, rhi, nr, multibuffer=False) + torch.npu.synchronize() + torch.testing.assert_close(output.cpu(), expected, rtol=0, atol=0) + + +@triton.jit +def _mixed_axis_column_loop(input_ptr, output_ptr, rows, width, out_stride, TENSOR_OTHER: tl.constexpr): + x = tl.program_id(0) * 32 + tl.arange(0, 32)[:, None] + rbase = tl.arange(0, 64)[None, :] + for start in range(0, width, 64): + r = start + rbase + # Irregular rows, contiguous columns and a runtime column tail. The + # row predicate is loop-covered; the column predicate must survive. + mask = (x < rows) & (r < width) + other = (x * 1000 + r).to(tl.float32) if TENSOR_OTHER else -7.0 + value = tl.load(input_ptr + (x % 5) * width + r, mask, other=other) + tl.store(output_ptr + x * out_stride + r, value) + + +@pytest.mark.parametrize("width", [63, 64, 65, 130, 512]) +@pytest.mark.parametrize("rows", [0, 1, 33]) +@pytest.mark.parametrize("tensor_other", [False, True]) +@pytest.mark.parametrize("multibuffer", [False, True]) +def test_mixed_axis_column_loop(width, rows, tensor_other, multibuffer): + nr, nc = max(triton.cdiv(rows, 32), 1) * 32, triton.cdiv(width, 64) * 64 + source = torch.arange(5 * width, dtype=torch.float32).reshape(5, width) + x, r = torch.arange(nr)[:, None], torch.arange(nc)[None, :] + expected = (x * 1000 + r).float() if tensor_other else torch.full((nr, nc), -7.0) + expected[:rows, :width] = source[torch.arange(rows) % 5] + output = torch.empty((nr, nc), dtype=torch.float32, device="npu") + _mixed_axis_column_loop[(nr // 32, )](source.npu(), output, rows, width, nc, tensor_other, multibuffer=multibuffer) + torch.npu.synchronize() + torch.testing.assert_close(output.cpu(), expected, rtol=0, atol=0) + + +@triton.jit +def _mixed_axis_row_gate(input_ptr, output_ptr, lower, upper, width, gate, EQUALITY: tl.constexpr): + x = tl.arange(0, 8)[:, None] + r = tl.arange(0, 32)[None, :] + row_mask = (x == lower) if EQUALITY else ((x >= lower) & (x < upper)) + mask = row_mask & (r < width) & gate + value = tl.load(input_ptr + (x % 2) * width + r, mask, other=-7.0) + tl.store(output_ptr + x * 32 + r, value) + + +@pytest.mark.parametrize("lower,upper,equality", [(-5, 3, False), (2, 7, False), (7, 2, False), (-5, 8, True), + (3, 8, True)]) +@pytest.mark.parametrize("gate", [False, True]) +def test_mixed_axis_row_gate(lower, upper, equality, gate): + width = 20 + source = torch.arange(2 * width, dtype=torch.float32).reshape(2, width) + x = torch.arange(8) + valid = ((x == lower) if equality else ((x >= lower) & (x < upper))) & gate + expected = torch.full((8, 32), -7.0) + expected[valid, :width] = source[x[valid] % 2] + output = torch.empty_like(expected, device="npu") + _mixed_axis_row_gate[(1, )](source.npu(), output, lower, upper, width, gate, equality, multibuffer=False) + torch.npu.synchronize() + torch.testing.assert_close(output.cpu(), expected, rtol=0, atol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_mod.py b/third_party/ascend/unittest/pytest_ut/test_mod.py new file mode 100644 index 0000000000..fc91c60afb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_mod.py @@ -0,0 +1,80 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_pointwise(x0, x1, dtype): + if dtype == 'float16': + x0 = x0.to(torch.float32) + x1 = x1.to(torch.float32) + elif dtype == 'float32': + x0 = x0.to(torch.float64) + x1 = x1.to(torch.float64) + res = torch.div(x0, x1, rounding_mode="trunc") + res = x0 - x1 * res + return res + + +@triton.jit +def triton_mod(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 % tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + if dtype == 'int8': + x0 = torch.randint(low=1, high=127, size=shape, dtype=eval('torch.' + dtype)).npu() + x1 = torch.randint(low=1, high=127, size=shape, dtype=eval('torch.' + dtype)).npu() + else: + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1, dtype) + if dtype == "float16": + y_ref = y_ref.to(torch.float16) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_mod[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + #test_common.validate_cmp(dtype, y_cal, y_ref.npu()) + if dtype == 'int8': + torch.equal(y_cal, y_ref) + else: + res = torch.isclose(y_cal, y_ref, rtol=1e-3, atol=1e-3, equal_nan=True) + if not res.all(): + max_diff = torch.max((y_ref - y_cal)).item() + raise ValueError(f"Tensors are not close, diff is {max_diff}") diff --git a/third_party/ascend/unittest/pytest_ut/test_mul.py b/third_party/ascend/unittest/pytest_ut/test_mul.py new file mode 100644 index 0000000000..feb98bb668 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_mul.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 * x1 + return res + + +@triton.jit +def triton_mul(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 * tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + ['uint8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_mul[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_mul_reduce.py b/third_party/ascend/unittest/pytest_ut/test_mul_reduce.py new file mode 100644 index 0000000000..5ca4246331 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_mul_reduce.py @@ -0,0 +1,55 @@ +import pytest +import torch +import torch_npu +import triton +import triton.language as tl +import numpy as np + + +@triton.jit +def minimum(a, b): + ret = tl.minimum(a, b, tl.PropagateNan.ALL) + if a.dtype == tl.bfloat16: + ret = ret.to(tl.bfloat16) + return ret + + +@triton.jit +def triton_pw_rdc5d(in_ptr0, in_ptr1, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + x0 = tl.load(in_ptr0 + idx) + x1 = tl.load(in_ptr1 + idx) + ret0 = x0 * x1 + ret = tl.reduce(ret0, 4, minimum, keep_dims=True) + zblk_idx = tl.arange(0, 1) + odx = (lblk_idx[:, None, None, None, None] * K * N * M + mblk_idx[None, :, None, None, None] * K * N + + nblk_idx[None, None, :, None, None] * K + kblk_idx[None, None, None, :, None] + + zblk_idx[None, None, None, None, :]) + tl.store(out_ptr0 + odx, ret) + + +@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16]) +@pytest.mark.parametrize("shape", [(16, 1, 1, 1, 1)]) # L=16, others=1 +def test_pw_rdc5d(dtype, shape): + L, M, N, K, Z = shape + a = torch.randn(*shape, dtype=dtype, device='npu') + b = torch.randn(*shape, dtype=dtype, device='npu') + out = torch.empty(*shape, dtype=dtype, device='npu') + + expected = (a * b).to(dtype) + + triton_pw_rdc5d[(1, )](a, b, out, L=L, M=M, N=N, K=K, Z=Z) + + torch.testing.assert_close(out.cpu(), expected.cpu(), rtol=1e-3, atol=1e-3) + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/third_party/ascend/unittest/pytest_ut/test_multi_return.py b/third_party/ascend/unittest/pytest_ut/test_multi_return.py new file mode 100644 index 0000000000..1935e5225b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_multi_return.py @@ -0,0 +1,599 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from typing import Optional +import pytest +import torch +import torch_npu +import triton +from triton.language.math import tanh +import triton.language as tl + +device = 'npu' + + +@triton.jit +def triton_multi_return_cross_entropy_case( + X_ptr, + Y_ptr, + X_stride, + Y_stride, + n_cols, + ignore_index, + BLOCK_SIZE: tl.constexpr, +): + program_id = tl.program_id(0).to(tl.int64) + + Y_ptr += program_id * Y_stride + y = tl.load(Y_ptr) + X_ptr += program_id * X_stride + + if y == ignore_index: + for i in range(0, n_cols, BLOCK_SIZE): + X_offsets = i + tl.arange(0, BLOCK_SIZE) + tl.store(X_ptr + X_offsets, 0.0, mask=X_offsets < n_cols) + return + else: + for i in range(0, n_cols, BLOCK_SIZE): + X_offsets = i + tl.arange(0, BLOCK_SIZE) + tl.store(X_ptr + X_offsets, 0.0, mask=X_offsets < n_cols) + + +@pytest.mark.parametrize('param_list', [ + [1024, 152064, 11480, 'bfloat16', 'int32', 'float32'], +]) +def test_case(param_list): + N, M, V, dtype_1, dtype_2, dtype_3 = param_list + logits_chunk = torch.rand([N, M], device=device, dtype=eval('torch.' + dtype_1)) + target_chunk = torch.rand(N, device=device, dtype=eval('torch.' + dtype_2)) + loss_1d_slice = torch.rand(N, device=device, dtype=eval('torch.' + dtype_3)) + ce_weight = None + z_loss_1d_slice = None + softcap = None + return_z_loss = False + BLOCK_SIZE = N + triton_multi_return_cross_entropy_case[(N, )](X_ptr=logits_chunk, X_stride=logits_chunk.stride(-2), + Y_ptr=target_chunk, Y_stride=target_chunk.stride(-1), + n_cols=logits_chunk.stride(-2), ignore_index=-100, + BLOCK_SIZE=BLOCK_SIZE, num_warps=32) + + +@triton.jit +def liger_cross_entropy_kernel( + X_ptr, + X_stride, + Y_ptr, + Y_stride, + weight_ptr, + loss_ptr, + z_loss_ptr, + loss_stride, + n_cols, + n_non_ignore, + sum_non_ignore_weight, + weight_sum, + ignore_index, + lse_square_scale: tl.constexpr, + label_smoothing: tl.constexpr, + reduction: tl.constexpr, # set it as constexpr since reduction is always known at compile time + softcap, + RETURN_Z_LOSS: tl.constexpr, + BLOCK_SIZE: tl.constexpr, + HAS_WEIGHT: tl.constexpr, + HAS_SOFTCAPPING: tl.constexpr, +): + """ + This kernel computes both cross entropy loss and the gradient of the input. + + Parameters: + X_ptr: Pointer to input tensor. + X_stride (int): The stride of the input tensor. + Y_ptr: Pointer to target tensor. + Y_stride (int): The stride of the target tensor. + weight_ptr: Pointer to weight tensor. + loss_ptr: Pointer to tensor to store the loss. + z_loss_ptr: Pointer to tensor to store the z loss. No operation if RETURN_Z_LOSS is 0. + loss_stride (int): The stride of the loss tensor. + n_cols (int): The number of columns in the input tensor. + n_non_ignore (float): The number of non-ignored elements in the batch. + sum_non_ignore_weight (float): The sum of non-ignored target's weights in the batch. + weight_sum (float): The sum of weight tensor. + ignore_index (int): The index to ignore in the target. + label_smoothing (float): The amount of smoothing when computing the loss, where 0.0 means no smoothing. + lse_square_scale (float): The scaler of (logsumexp(_input)) ^ 2 adding to the loss for the stability of training. + reduction (str): The string for the reduction to apply + softcap (float): The upper threshold for scaling logits to the range (-softcap, +softcap). + RETURN_Z_LOSS (int): The boolean value to decide whether storing z loss to z_loss_ptr or not. It must be 0 or 1. + BLOCK_SIZE (int): The block size for Triton operations. + HAS_WEIGHT (bool): The boolean value to determine whether assigning weight to each of the classes. + HAS_SOFTCAPPING (bool): The boolean value to determine whether applying soft-capping or not. + """ + + # If B*T*V is too large, program_id * stride will overflow out of int32, so we convert to int64 + program_id = tl.program_id(0).to(tl.int64) + + # 1. Load Y_ptr first because if the target is ignore_index, we can return right away + Y_ptr += program_id * Y_stride + y = tl.load(Y_ptr) + + # 2. locate the start index + X_ptr += program_id * X_stride + + if y == ignore_index: + # set all X_ptr as 0 + for i in range(0, n_cols, BLOCK_SIZE): + X_offsets = i + tl.arange(0, BLOCK_SIZE) + tl.store(X_ptr + X_offsets, 0.0, mask=X_offsets < n_cols) + return + + loss_ptr += program_id * loss_stride + if RETURN_Z_LOSS: + z_loss_ptr += program_id * loss_stride + + if HAS_WEIGHT: + weight_y = tl.load(weight_ptr + y).cast(tl.float32) + + # Online softmax: 2 loads + 1 store (compared with 3 loads + 1 store for the safe softmax) + + # 3. [Online softmax] first pass: find max + sum + m = float("-inf") # m is the max value. use the notation from the paper + d = 0.0 # d is the sum. use the notation from the paper + ori_X_y = tl.load(X_ptr + y).cast(tl.float32) # we need to store the original value of X_y for the loss calculation + if HAS_SOFTCAPPING: + ori_X_y = softcap * tanh(ori_X_y / softcap) + + # Label smoothing is a general case of normal cross entropy + scaled_x_sum = 0.0 + eps = label_smoothing / n_cols + + for i in range(0, n_cols, BLOCK_SIZE): + X_offsets = i + tl.arange(0, BLOCK_SIZE) + X_block = tl.load( + X_ptr + X_offsets, + mask=X_offsets < n_cols, + other=float("-inf"), + # Ensure float32 precision for softmax calculation + ).cast(tl.float32) + if HAS_SOFTCAPPING: + X_block = softcap * tanh(X_block / softcap) + block_max = tl.max(X_block) + if label_smoothing > 0: + # scale X beforehand to avoid overflow + if HAS_WEIGHT: + weight_block = tl.load(weight_ptr + X_offsets, mask=X_offsets < n_cols) + scaled_x_sum += tl.sum(tl.where(X_offsets < n_cols, -eps * X_block * weight_block, 0.0)) + else: + scaled_x_sum += tl.sum(tl.where(X_offsets < n_cols, -eps * X_block, 0.0)) + m_new = tl.maximum(m, block_max) + d = d * tl.exp(m - m_new) + tl.sum(tl.exp(X_block - m_new)) + m = m_new + + # log (sum(e^(X_i))) = log (sum(e ^ (max(X) * e ^ (X_i - max(X))))) + # = log (e^(max(X)) * sum(e ^ (X_i - max(X)))) + # = max(X) + log (sum(e ^ (X_i - max(X)))) = m + log d + lse = m + tl.log(d) + + # 4. [Online Softmax] Second pass: compute gradients + for i in range(0, n_cols, BLOCK_SIZE): + X_offsets = i + tl.arange(0, BLOCK_SIZE) + X_block = tl.load( + X_ptr + X_offsets, + mask=X_offsets < n_cols, + other=float("-inf"), + # Ensure float32 precision for softmax calculation + ).cast(tl.float32) + if HAS_SOFTCAPPING: + intermediate = tanh(X_block / softcap) + X_block = softcap * intermediate + + if not HAS_WEIGHT: + X_block = tl.exp(X_block - m) / d + # derivative of z-loss: 2 * lse_square_scale * lse * softmax(x_i) + X_block += 2 * lse_square_scale * lse * X_block + # smoothing term + X_block += -eps + # special handle dx_y + X_block = tl.where(X_offsets != y, X_block, X_block - (1 - label_smoothing)) + # reduction scale + if reduction == "mean": + X_block = X_block / n_non_ignore + else: + weight_block = tl.load(weight_ptr + X_offsets, mask=X_offsets < n_cols) + softmax_X = tl.exp(X_block - m) / d + # derivative of original_loss + dloss_ori = (1 - label_smoothing) * softmax_X + # specially handle dx_y + dloss_ori = tl.where(X_offsets != y, dloss_ori, dloss_ori - (1 - label_smoothing)) + dloss_ori = dloss_ori * weight_y + # derivative of smooth_loss + dloss_smooth = eps * (-weight_block + softmax_X * weight_sum) + # derivative of z-loss + dz_loss = 2 * lse_square_scale * lse * softmax_X + # reduction scale + if reduction == "mean": + dloss_ori = dloss_ori / sum_non_ignore_weight + dloss_smooth = dloss_smooth / sum_non_ignore_weight + dz_loss = dz_loss / n_non_ignore + # derivative of total_loss + X_block = dloss_ori + dloss_smooth + dz_loss + + # chain rule softcapping + # d(softcap * tanh(x / softcap)) = (1 - tanh^2(x / softcap)) + if HAS_SOFTCAPPING: + X_block = X_block * (1 - intermediate * intermediate) + + tl.store(X_ptr + X_offsets, X_block, mask=X_offsets < n_cols) + + # We need tl.debug_barrier() to ensure the new result of X_ptr is written as mentioned in + tl.debug_barrier() + + # 5. Calculate the loss + + # loss = log (softmax(X_y)) = log ((e ^ (X_y - max(X)) / sum(e ^ (X - max(X)))) + # = (X_y - max(X)) - log(sum(e ^ (X - max(X)))) + # = X_y - m - log d = X_y - lse + # sum(e ^ (X - max(X))) must >= 1 because the max term is e ^ 0 = 1 + # So we can safely calculate log (softmax(X_y)) without overflow + loss = lse - ori_X_y + if HAS_WEIGHT: + loss = weight_y * loss + + # Original loss = H(q, p), with label smoothing regularization = H(q', p) and (label_smoothing / V) = eps + # H(q', p) = (1 - label_smoothing) * H(q, p) + label_smoothing * H(u, p) + # = (1 - label_smoothing) * H(q, p) + eps * sum(logsoftmax(x_i)) + # By using m (global max of xi) and d (sum of e^(xi-m)), we can simplify as: + # = (1 - label_smoothing) * H(q, p) + (sum(-eps * x_i) + label_smoothing * (m + logd)) + if label_smoothing > 0: + if HAS_WEIGHT: + smooth_loss = scaled_x_sum + eps * lse * weight_sum + else: + smooth_loss = scaled_x_sum + label_smoothing * lse + loss = loss * (1 - label_smoothing) + smooth_loss + + # An auxiliary loss, z_loss + z_loss = lse_square_scale * lse * lse + # Normalize the loss by the number of non-ignored elements if reduction is "mean" + if reduction == "mean": + if HAS_WEIGHT: + loss = loss / sum_non_ignore_weight + else: + loss = loss / n_non_ignore + z_loss = z_loss / n_non_ignore + loss += z_loss + + tl.store(loss_ptr, loss) + if RETURN_Z_LOSS: + tl.store(z_loss_ptr, z_loss) + + +@triton.jit +def element_mul_kernel( + X_ptr, + X_stride, + grad_output_ptr, + n_cols, + BLOCK_SIZE: tl.constexpr, +): + """ + This function multiplies each element of the tensor pointed by X_ptr with the value pointed by grad_output_ptr. + The multiplication is performed in-place on the tensor pointed by X_ptr. + + Parameters: + X_ptr: Pointer to the input tensor. + X_stride (int): The stride of the input tensor. + grad_output_ptr: Pointer to the gradient output value. + n_cols (int): The number of columns in the input tensor. + BLOCK_SIZE (int): The block size for Triton operations. + """ + + # Get the program ID and convert it to int64 to avoid overflow + program_id = tl.program_id(0).to(tl.int64) + + # Locate the start index + X_ptr += program_id * X_stride + + # Load the gradient output value + grad_output = tl.load(grad_output_ptr) + + # Perform the element-wise multiplication + for i in range(0, n_cols, BLOCK_SIZE): + X_offsets = i + tl.arange(0, BLOCK_SIZE) + X_block = tl.load(X_ptr + X_offsets, mask=X_offsets < n_cols) + tl.store(X_ptr + X_offsets, X_block * grad_output, mask=X_offsets < n_cols) + + +MAX_FUSED_SIZE = 65536 // 2 + + +def cross_entropy_forward( + _input, + target, + weight, + ignore_index, + lse_square_scale, + label_smoothing, + reduction, + softcap, + return_z_loss, +): + assert isinstance(return_z_loss, bool), f"return_z_loss must be True or False. Got: {return_z_loss}" + + BT, V = _input.shape + n_rows = BT + + BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(V)) + # unreduced loss + loss_1d = torch.zeros(n_rows, dtype=_input.dtype, device=_input.device) + z_loss_1d = torch.zeros(n_rows, dtype=_input.dtype, device=_input.device) if return_z_loss else None + + target_mask = target != ignore_index + n_non_ignore = target_mask.sum().item() + assert (target * target_mask).max() < _input.shape[-1], ( + f"Target {target.max()} is out of bounds. Expected < {_input.shape[-1]}") + assert (target * target_mask).min() >= 0, f"Target {target.min()} is out of bounds. Expected >= 0" + sum_non_ignore_weight = n_non_ignore + weight_sum = 0.0 + if weight is not None: + assert weight.shape[0] == V, f"If given, weight has to be a Tensor of size V. Got: {weight.shape}" + assert torch.is_floating_point(weight), ( + f"If given, weight has to be a Tensor of floating point dtype. Got: {weight.dtype}") + sum_non_ignore_weight = torch.gather(weight, dim=0, index=target.masked_select(target_mask)).sum().item() + weight_sum = weight.sum().item() + # ensure weight is contiguous + if weight.stride(-1) != 1: + weight = weight.contiguous() + + # ensure _input and target are contiguous in the last dimension + if _input.stride(-1) != 1: + _input = _input.contiguous() + if target.stride(-1) != 1: + target = target.contiguous() + + # Here we use a trick to store X_ptr gradient in X_ptr so we can save memory + liger_cross_entropy_kernel[(n_rows, )]( + X_ptr=_input, + X_stride=_input.stride(-2), + Y_ptr=target, + Y_stride=target.stride(-1), # always 1 + weight_ptr=weight, # dummy if None + loss_ptr=loss_1d, + z_loss_ptr=z_loss_1d, + loss_stride=loss_1d.stride(-1), # always 1 + n_cols=V, + n_non_ignore=n_non_ignore, + sum_non_ignore_weight=sum_non_ignore_weight, + ignore_index=ignore_index, + weight_sum=weight_sum, + lse_square_scale=lse_square_scale, + label_smoothing=label_smoothing, + reduction=reduction, + softcap=softcap, + RETURN_Z_LOSS=return_z_loss, + BLOCK_SIZE=BLOCK_SIZE, + HAS_WEIGHT=True if weight is not None else False, + HAS_SOFTCAPPING=True if softcap is not None else False, + ) + + if reduction == "none": + loss = loss_1d + z_loss = z_loss_1d if return_z_loss else None + else: + loss = torch.sum(loss_1d) + z_loss = torch.sum(z_loss_1d) if return_z_loss else None + + return loss, z_loss, _input + + +def cross_entropy_backward(_input, grad_output): + # If cross entropy is the last layer, grad_output is 1.0. Skip the mul to save time + if torch.equal(grad_output, torch.tensor(1.0, device=grad_output.device)): + pass + # If reduction is 'none' + elif grad_output.ndim > 0: + _input = _input * grad_output.unsqueeze(dim=1) + # If reduction is ['mean', 'sum'], grad_output is just a scalar + # We use a Triton kernel instead of a PyTorch operation because modifying inputs in-place + # for gradient storage and backward multiple times causes anomalies with PyTorch but not with Triton. + else: + BT, V = _input.shape + n_rows = BT + BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(V)) + + element_mul_kernel[(n_rows, )]( + _input, + _input.stride(-2), + grad_output, + V, + BLOCK_SIZE=BLOCK_SIZE, + ) + + return _input + + +class LigerCrossEntropyFunction(torch.autograd.Function): + """ + This class implements a custom autograd function for the Liger Cross Entropy loss. + It overrides the forward and backward methods of the torch.autograd.Function class. + """ + + @staticmethod + def forward( + ctx, + _input: torch.Tensor, + target: torch.Tensor, + weight: Optional[torch.FloatTensor], + ignore_index: int = -100, + lse_square_scale: float = 0.0, + label_smoothing: float = 0.0, + reduction: str = "mean", + softcap: Optional[float] = None, + return_z_loss: bool = False, + ): + """ + The forward pass of the Liger Cross Entropy loss. + + Parameters: + ctx : The context object. + _input (tensor): The input tensor of shape (BT, V) where B is batch size, T is sequence length, V is vocab size. + target (tensor): The target tensor of shape (BT) where each value is in [0, V-1]. + weight(Tensor, optional): a manual rescaling weight given to each class. If given, has to be a Tensor of size V and floating point dtype + ignore_index (int): The index to ignore in the target. + lse_square_scale (float): The scaler of (logsumexp(_input)) ^ 2 adding to the loss for the stability of training. + label_smoothing (float): The amount of smoothing when computing the loss, where 0.0 means no smoothing. + reduction (str): The reduction to apply to the output: "none" | "mean | "sum". + softcap (Optional[float]): The upper threshold for scaling logits to the range (-softcap, +softcap). + return_z_loss (bool): When `return_z_loss` is `True`, returns (loss, z_loss) instead of (loss, None). Default: `False` + + Returns: + tuple: A tuple with the compouted losses with respect to loss and z loss. The elements are tensors or None. + """ + loss, z_loss, _input = cross_entropy_forward( + _input, + target, + weight, + ignore_index, + lse_square_scale, + label_smoothing, + reduction, + softcap, + return_z_loss, + ) + # If we don't detach the _input tensor, the memory will double + # Not sure why but seems that there will be a time both grad and value exist but in different location + ctx.save_for_backward(_input.detach()) + ctx.return_z_loss = return_z_loss + + return loss, z_loss + + @staticmethod + def backward(ctx, grad_output, grad_ouput2): + """ + The backward pass of the Liger Cross Entropy loss. + + Parameters: + ctx : The context object with saved tensors. + grad_output (tensor): The tensor containing the gradient of the loss with respect to the output. + grad_output2 (tenosr): No use. + Returns: + tuple: A tuple with the gradients with respect to the inputs. The elements are tensors or None. + """ + if ctx.return_z_loss: + del grad_ouput2 # z_loss is only for logging + + (_input, ) = ctx.saved_tensors + _input = cross_entropy_backward(_input, grad_output) + return ( + _input, + None, + None, + None, + None, + None, + None, + None, + None, + ) + + +def liger_cross_entropy( + input, + target, + weight=None, + size_average=None, + ignore_index: int = -100, + reduce=None, + reduction: str = "mean", + label_smoothing: float = 0.0, + lse_square_scale: float = 0.0, + softcap: Optional[float] = None, + return_z_loss: bool = False, +): + loss, z_loss = LigerCrossEntropyFunction.apply( + input, + target, + weight, + ignore_index, + lse_square_scale, + label_smoothing, + reduction, + softcap, + return_z_loss, + ) + if not return_z_loss: + return loss + return loss, z_loss + + +def _test_correctness_functional( + B, + T, + V, + scalar, + dtype, + atol, + rtol, +): + _input = torch.randn(B * T, V, device=device, dtype=dtype) * scalar + + x1 = _input.clone().requires_grad_(True) + x2 = _input.clone().requires_grad_(True) + + target = torch.randint(0, V, (B * T, ), device=device, dtype=torch.long) + + y1, y1_z = liger_cross_entropy( + x1, + target, + None, + ignore_index=0, + lse_square_scale=1e-4, + label_smoothing=0.1, + reduction="mean", + softcap=30.0, + return_z_loss=True, + ) + y2, y2_z = LigerCrossEntropyFunction.apply(x2, target, None, 0, 1e-4, 0.1, "mean", 30.0, True) + + assert torch.allclose(y1, y2, atol=atol, rtol=rtol) + assert torch.allclose(y1_z, y2_z, atol=atol, rtol=rtol) + + grad = torch.randn_like(y2) + + y1.backward(grad) + y2.backward(grad) + + assert torch.allclose(x1.grad, x2.grad, atol=atol, rtol=rtol) + + +@pytest.mark.parametrize( + "B, T, V", + [ + (2, 512, 4096), + ], +) +@pytest.mark.parametrize( + "scalar, dtype, atol, rtol", + [ + (1.0, torch.bfloat16, 1e-8, 5e-2), + (1.0, torch.float32, 1e-8, 1e-6), + (1.0, torch.int32, 0, 0), + ], +) +def test_correctness_functional(B, T, V, scalar, dtype, atol, rtol): + _test_correctness_functional(B, T, V, scalar, dtype, atol, rtol) diff --git a/third_party/ascend/unittest/pytest_ut/test_multibuffer.py b/third_party/ascend/unittest/pytest_ut/test_multibuffer.py new file mode 100644 index 0000000000..726b1303fb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_multibuffer.py @@ -0,0 +1,74 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os + +import pytest +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +@triton.jit +def multibuffer(XBLOCK: tl.constexpr): + buf = bl.alloc(tl.float32, [XBLOCK, XBLOCK], al.ascend_address_space.UB) + al.multibuffer(buf, 2) + + +def test_multibuffer(): + print("=" * 60) + print("Test 1: test_alloc_ub_multibuffer") + print("=" * 60) + mlir = compile_kernel(multibuffer, {}, {"XBLOCK": 256}) + print(f"Generated MLIR ({len(mlir)} chars):\n") + print(mlir) + + +# ============== Main for manual testing ============== +if __name__ == "__main__": + test_multibuffer() diff --git a/third_party/ascend/unittest/pytest_ut/test_multibuffer_autotune.py b/third_party/ascend/unittest/pytest_ut/test_multibuffer_autotune.py new file mode 100644 index 0000000000..36940b0861 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_multibuffer_autotune.py @@ -0,0 +1,302 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +"""Exercise structured modes through Config, JSON caches and autotune without a device.""" +import ast +import itertools +import json +import sys +from pathlib import Path +from types import ModuleType, SimpleNamespace +from typing import Any, Dict, List, Optional + +import pytest + +pytestmark = pytest.mark.backend("none") +ROOT = Path(__file__).resolve().parents[4] +BACKEND = ROOT / "third_party" / "ascend" / "backend" +MODE = {"gm": 4, "l1": 2, "l0c": 1, "ub": 2} +MODE_PAIRS = tuple(sorted(MODE.items())) + + +def _load_definitions(path, names, namespace): + tree = ast.parse(path.read_text()) + nodes = [ + node for node in tree.body if getattr(node, "name", None) in names or (isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id in names for target in node.targets)) + ] + exec(compile(ast.Module(body=nodes, type_ignores=[]), str(path), "exec"), namespace) + + +@pytest.fixture +def tuning(monkeypatch): + namespace = dict(Any=Any, Dict=Dict, List=List, Optional=Optional, itertools=itertools, + _remove_deprecated_npu_options=lambda value: dict(value)) + _load_definitions(ROOT / "python/triton/runtime/autotuner.py", {"Config"}, namespace) + utils = ModuleType("triton.backends.ascend.utils") + _load_definitions(BACKEND / "utils.py", {"_multibuffer_mode_to_tuple"}, utils.__dict__) + monkeypatch.setitem(sys.modules, utils.__name__, utils) + namespace["_multibuffer_mode_to_tuple"] = utils._multibuffer_mode_to_tuple + runtime = ModuleType("triton.runtime.autotuner") + runtime.Config = namespace["Config"] + monkeypatch.setitem(sys.modules, runtime.__name__, runtime) + _load_definitions(BACKEND / "runtime/__init__.py", {"_patch_config_init"}, namespace) + namespace["_patch_config_init"]() + names = { + "_ALL_PARAMS", + "_DEFAULTS", + "_VALID_VALUES", + "_CUBE_PARAMS", + "_MIXCV_PARAMS", + "_VECTOR_PARAMS", + "_VALIDATION_RULES", + "_check_boolean_list", + "_check_string_in_set", + "_check_int_in_set", + "_check_multibuffer_mode_list", + "BaseAutotuner", + "CubeAutotuner", + "MixcvAutotuner", + "VectorAutotuner", + "get_max_configs", + "_normalize_config_hints", + "_multibuffer_to_num_stages", + "_DEFAULT_HINT_NUM_STAGES", + "_clone_config_with_kwargs", + "_expand_configs_with_hints", + "_validate_config_hint_values", + "_DEFAULT_COMPILE_MODE", + } + _load_definitions(BACKEND / "runtime/autotuner.py", names, namespace) + namespace["_get_constexpr_candidates_from_fn"] = lambda _fn: [] + namespace["get_byte_per_numel"] = lambda _dtype: 4 + tree = ast.parse((BACKEND / "runtime/autotuner.py").read_text()) + tuner = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "AutoTilingTuner") + generate = next(node for node in tuner.body if getattr(node, "name", None) == "generate_key_and_configs") + exec(compile(ast.Module(body=[generate], type_ignores=[]), str(BACKEND / "runtime/autotuner.py"), "exec"), + namespace) + return SimpleNamespace(**namespace) + + +def test_multibuffer_dictionary_config_survives_json_cache(tuning): + config = tuning.Config({"multibuffer_mode": MODE}) + assert config.num_stages is None + assert "num_stages" not in config.all_kwargs() + assert config.kwargs["multibuffer_mode"] == MODE_PAIRS + restored = tuning.Config(**json.loads(json.dumps(config.__dict__))) + assert restored == config + assert hash(restored) == hash(config) + + +@pytest.mark.parametrize("name", ["CubeAutotuner", "MixcvAutotuner", "VectorAutotuner"]) +def test_multibuffer_mode_presets_keep_existing_options(tuning, name): + generator = getattr(tuning, name) + modes = [MODE, {"future": 0}] + configs = generator.get_configs(multibuffer_mode=modes) + assert configs and {config.kwargs["multibuffer_mode"] + for config in configs} == {tuple(sorted(mode.items())) + for mode in modes} + assert all(config.num_stages is None for config in configs) + baseline = {tuple(sorted(config.kwargs.items())) for config in generator.get_configs()} + assert { + tuple(sorted((key, value) + for key, value in config.kwargs.items() + if key != "multibuffer_mode")) + for config in configs + } == baseline + + +def test_multibuffer_legacy_presets_keep_their_defaults(tuning): + configs = tuning.MixcvAutotuner.get_configs() + assert len(configs) == 8 + assert all(config.num_stages == 2 and "multibuffer_mode" not in config.kwargs for config in configs) + assert {config.kwargs["set_workspace_multibuffer"] for config in configs} == {2, 4} + assert all(config.kwargs["limit_auto_multi_buffer_of_local_buffer"] == "no-l0c" for config in configs) + assert all(config.kwargs["limit_auto_multi_buffer_only_for_local_buffer"] is False for config in configs) + + +def test_multibuffer_explicit_legacy_preset_values_survive(tuning): + configs = tuning.MixcvAutotuner.get_configs(multibuffer_mode=[MODE], set_workspace_multibuffer=[2, 4], + limit_auto_multi_buffer_of_local_buffer=["no-l0c"]) + assert configs and {config.kwargs["set_workspace_multibuffer"] for config in configs} == {2, 4} + assert all(config.kwargs["limit_auto_multi_buffer_of_local_buffer"] == "no-l0c" for config in configs) + + +@pytest.mark.parametrize("mode_in_base", [False, True]) +def test_multibuffer_max_configs_keep_existing_options(tuning, mode_in_base): + base = tuning.Config({"multibuffer_mode": MODE} if mode_in_base else {}, num_stages=None) + kwargs = {} if mode_in_base else {"multibuffer_mode": [MODE]} + configs = tuning.get_max_configs(base, kernel_type="mixcv", **kwargs) + assert configs and all(config.kwargs["multibuffer_mode"] == MODE_PAIRS for config in configs) + assert all(config.num_stages is None for config in configs) + baseline = tuning.get_max_configs(tuning.Config({}, num_stages=2), kernel_type="mixcv") + assert { + tuple(sorted((key, value) + for key, value in config.kwargs.items() + if key != "multibuffer_mode")) + for config in configs + } == {tuple(sorted(config.kwargs.items())) + for config in baseline} + + +def test_multibuffer_max_configs_preserve_explicit_old_values(tuning): + base = tuning.Config({"multibuffer_mode": MODE, "limit_auto_multi_buffer_of_local_buffer": "no-l0c"}) + configs = tuning.get_max_configs(base, kernel_type="mixcv", set_workspace_multibuffer=[2, 4]) + assert {config.kwargs["set_workspace_multibuffer"] for config in configs} == {2, 4} + assert all(config.kwargs["limit_auto_multi_buffer_of_local_buffer"] == "no-l0c" for config in configs) + + +@pytest.mark.parametrize("mode_in_base", [False, True]) +def test_multibuffer_hints_preserve_explicit_switch(tuning, mode_in_base): + hints = {"multibuffer": [False, True]} + if not mode_in_base: + hints["multibuffer_mode"] = [MODE] + assert tuning._normalize_config_hints(hints, inject_default_num_stages=True, + has_multibuffer_mode=mode_in_base) == hints + + +def test_multibuffer_legacy_hints_keep_stage_expansion(tuning): + assert tuning._normalize_config_hints({"multibuffer": [False, True]}) == {"num_stages": [1, 2]} + + +@pytest.mark.parametrize("value,valid", [ + ([MODE], True), + ([list(MODE_PAIRS)], True), + ([{"future": -1}], True), + ([{}], True), + (["[(gm,2)]"], False), + ([{"gm": 2.0}], False), + ([{"gm": True}], False), + ([{1: 2}], False), + ([None], False), + ([], False), +]) +def test_multibuffer_tuning_values_require_string_keys_and_integer_counts(tuning, value, valid): + assert tuning._VALIDATION_RULES["multibuffer_mode"]["check"](value, "multibuffer_mode") is valid + + +def test_multibuffer_dictionary_is_copied_for_config_hashing(tuning): + mode = dict(MODE) + config = tuning.Config({"multibuffer_mode": mode}) + before = hash(config) + mode["gm"] = 99 + assert config.kwargs["multibuffer_mode"] == MODE_PAIRS + assert hash(config) == before + + +@pytest.mark.parametrize("stages", [0, 1, 2, 3]) +@pytest.mark.parametrize("mode", [MODE, {}]) +def test_multibuffer_config_rejects_num_stages(tuning, stages, mode): + with pytest.raises(ValueError, match="num_stages and multibuffer_mode cannot be specified together"): + tuning.Config({"multibuffer_mode": mode}, num_stages=stages) + + +def test_multibuffer_config_default_and_explicit_none(tuning): + assert tuning.Config({}).num_stages == 3 + assert tuning.Config({}, num_stages=None).num_stages is None + assert tuning.Config({"multibuffer_mode": MODE}, num_stages=None).num_stages is None + + +@pytest.mark.parametrize("name", ["CubeAutotuner", "MixcvAutotuner", "VectorAutotuner"]) +def test_multibuffer_presets_reject_explicit_num_stages(tuning, name): + with pytest.raises(ValueError, match="num_stages and multibuffer_mode cannot be specified together"): + getattr(tuning, name).get_configs(multibuffer_mode=[MODE], num_stages=[2]) + + +@pytest.mark.parametrize("mode_in_base", [False, True]) +def test_multibuffer_max_configs_reject_explicit_num_stages(tuning, mode_in_base): + if mode_in_base: + base = tuning.Config({"multibuffer_mode": MODE}) + kwargs = {"num_stages": [2]} + else: + base = tuning.Config({}, num_stages=2) + kwargs = {"multibuffer_mode": [MODE]} + with pytest.raises(ValueError, match="num_stages and multibuffer_mode cannot be specified together"): + tuning.get_max_configs(base, **kwargs) + + +@pytest.mark.parametrize("mode_in_base", [False, True]) +def test_multibuffer_hints_reject_explicit_num_stages(tuning, mode_in_base): + hints = {"num_stages": [2]} + if not mode_in_base: + hints["multibuffer_mode"] = [MODE] + with pytest.raises(ValueError, match="num_stages and multibuffer_mode cannot be specified together"): + tuning._normalize_config_hints(hints, has_multibuffer_mode=mode_in_base) + + +def _auto_tuner(tuning, hints=None, user_configs=(), generated_configs=()): + tuner = SimpleNamespace( + arg_names=["x"], + keys=[], + fn=object(), + cache={}, + axis_arg_names={}, + _raw_config_hints=hints or {}, + user_configs=list(user_configs), + gen_configs=[], + auto_gen_config=True, + _autoparse_axis_params=lambda _args: None, + ) + tuner._gen_tile_configs = lambda *_args: setattr(tuner, "gen_configs", list(generated_configs)) + return tuner + + +@pytest.mark.parametrize("mode_in_hints", [False, True]) +@pytest.mark.parametrize("has_generated_config", [False, True]) +def test_multibuffer_auto_configs_drop_generated_stage_defaults(tuning, mode_in_hints, has_generated_config): + hints = {"multibuffer_mode": [MODE]} if mode_in_hints else {} + launch = {} if mode_in_hints else {"multibuffer_mode": MODE} + generated = [tuning.Config({}, num_stages=1)] if has_generated_config else [] + tuner = _auto_tuner(tuning, hints=hints, generated_configs=generated) + tuning.generate_key_and_configs(tuner, SimpleNamespace(dtype="float32"), **launch) + assert tuner.configs + for config in tuner.configs: + options = dict(config.all_kwargs(), **launch) + assert dict(options["multibuffer_mode"]) == MODE + assert "num_stages" not in options + + +@pytest.mark.parametrize("source", ["launch", "hints", "config_mode", "config_stages"]) +def test_multibuffer_auto_configs_reject_explicit_conflicts_before_generation(tuning, source): + launch, hints, configs = {}, {}, [] + if source == "launch": + launch = {"num_stages": 2, "multibuffer_mode": MODE} + elif source == "hints": + hints = {"num_stages": [2], "multibuffer_mode": [MODE]} + elif source == "config_mode": + configs = [tuning.Config({"multibuffer_mode": MODE})] + launch = {"num_stages": 2} + else: + configs = [tuning.Config({}, num_stages=2)] + launch = {"multibuffer_mode": MODE} + tuner = _auto_tuner(tuning, hints=hints, user_configs=configs) + + def unexpected_generation(*_args): + raise AssertionError("Conflicting options reached tile generation") + + tuner._gen_tile_configs = unexpected_generation + with pytest.raises(ValueError, match="num_stages and multibuffer_mode cannot be specified together"): + tuning.generate_key_and_configs(tuner, SimpleNamespace(dtype="float32"), **launch) + + +def test_multibuffer_launch_mode_prevents_default_stage_hints(tuning): + tuner = _auto_tuner(tuning, hints={"enable_ubuf_saving": [True]}, user_configs=[tuning.Config({}, num_stages=None)]) + tuning.generate_key_and_configs(tuner, SimpleNamespace(dtype="float32"), multibuffer_mode=MODE) + assert "num_stages" not in tuner.config_hints + assert all(config.num_stages is None for config in tuner.configs) + + +def test_multibuffer_dictionary_order_does_not_change_config_hash(tuning): + config = tuning.Config({"multibuffer_mode": MODE}) + reordered = tuning.Config({"multibuffer_mode": dict(reversed(list(MODE.items())))}) + assert reordered == config + assert hash(reordered) == hash(config) + assert {config: "cached"}[reordered] == "cached" + + +def test_multibuffer_dictionary_order_does_not_change_autotune_key(tuning): + tuner = _auto_tuner(tuning) + arg = SimpleNamespace(dtype="float32") + key = tuning.generate_key_and_configs(tuner, arg, multibuffer_mode=MODE) + reordered = tuning.generate_key_and_configs(tuner, arg, multibuffer_mode=dict(reversed(list(MODE.items())))) + assert {key: "cached"}[reordered] == "cached" + changed = tuning.generate_key_and_configs(tuner, arg, multibuffer_mode={**MODE, "ub": 8}) + assert changed != key diff --git a/third_party/ascend/unittest/pytest_ut/test_multiple_of.py b/third_party/ascend/unittest/pytest_ut/test_multiple_of.py new file mode 100644 index 0000000000..6813ee8bc4 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_multiple_of.py @@ -0,0 +1,26 @@ +import torch +import triton +import triton.language as tl +import pytest + + +@triton.jit +def compile_hint_kernel(input_ptr, output_ptr, n_elements: tl.constexpr, SIZE: tl.constexpr): + offsets = tl.arange(0, n_elements) + val = tl.load(input_ptr + offsets) + val = tl.multiple_of(val, SIZE) + tl.store(output_ptr + offsets, val) + + +@pytest.mark.parametrize('sigtype', [ + 'int32', + #'int64', 'int16', 'int8', + #'uint8', 'uint16', 'uint32', 'uint64', + #'float32', 'float16', 'bfloat16', 'bool' +]) +def test_compile_hint(sigtype): + n_elements = 10 + dtype = eval(f"torch.{sigtype}") + x = torch.ones((n_elements, ), dtype=dtype).npu() + y = torch.zeros((n_elements, ), dtype=dtype).npu() + compile_hint_kernel[(1, )](x, y, n_elements, 1) diff --git a/third_party/ascend/unittest/pytest_ut/test_ne.py b/third_party/ascend/unittest/pytest_ut/test_ne.py new file mode 100644 index 0000000000..00ecc5b137 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ne.py @@ -0,0 +1,76 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_ne(x0, x1): + return x0 != x1 + + +def torch_ne_from_np(x0, x1, dtype): + return torch.from_numpy(x0 != x1).to(eval('torch.' + dtype)) + + +@triton.jit +def triton_ne(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index, None) + tmp1 = tl.load(in_ptr1 + x_index, None) + tmp2 = tmp0 != tmp1 + tl.store(out_ptr0 + x_index, tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + # ['float16', (2, 4096, 8), 2, 32768, 1024], + # ['bfloat16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + # ['int16', (2, 4096, 8), 2, 32768, 1024], + # ['int32', (2, 4096, 8), 2, 32768, 1024], + # ['int64', (2, 4096, 8), 2, 32768, 1024], + ['uint8', (2, 4096, 8), 2, 32768, 1024], + # ['uint16', (2, 4096, 8), 2, 32768, 1024], + # ['uint32', (2, 4096, 8), 2, 32768, 1024], + # ['uint64', (2, 4096, 8), 2, 32768, 1024], +]) +def test_ne(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + np_x0 = test_common.generate_numpy(shape, dtype) + x0 = torch.from_numpy(np_x0).to(eval('torch.' + dtype)).npu() + np_x1 = test_common.generate_numpy(shape, dtype) + x1 = torch.from_numpy(np_x1).to(eval('torch.' + dtype)).npu() + # torch结果 + torch_res = torch_ne_from_np(np_x0, np_x1, dtype).npu() + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_ne[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_nearbyint.py b/third_party/ascend/unittest/pytest_ut/test_nearbyint.py new file mode 100644 index 0000000000..f02b896d7c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_nearbyint.py @@ -0,0 +1,69 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import torch +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + + +@triton.jit +def nearbyint_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + + y = libdevice.nearbyint(x) + + tl.store(y_ptr + offsets, y, mask=mask) + + +@pytest.mark.parametrize('shape', [ + (12, 16), +]) +@pytest.mark.parametrize('dtype', ['float32']) +def test_nearbyint(shape, dtype): + n_elements = shape[0] * shape[1] + x = test_common.generate_tensor(shape, dtype).npu() + + # Ensure some boundary cases are included + x[0, 0] = 0.0 + x[0, 1] = 3.14 # Should be rounded to 3.0 + x[0, 2] = -2.71 # Should be rounded to -3.0 + x[0, 3] = 5.0 # Integer, should remain unchanged + x[0, 4] = -3.0 # Negative integer, should remain unchanged + x[0, 5] = 2.5 # Should be rounded to 3.0 + x[0, 6] = -1.5 # Should be rounded to -2.0 + + y = torch.empty_like(x) + + BLOCK_SIZE = 192 + grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + nearbyint_kernel[grid](x, y, n_elements, BLOCK_SIZE=BLOCK_SIZE) + + expected = torch.round(x) + test_common.validate_cmp('float32', y, expected) diff --git a/third_party/ascend/unittest/pytest_ut/test_nearest.py b/third_party/ascend/unittest/pytest_ut/test_nearest.py new file mode 100644 index 0000000000..3fb5b4b429 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_nearest.py @@ -0,0 +1,116 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import math +import numpy as np +import pytest + + +@triton.jit +def nearest_resize_kernel(img_src_ptr, img_dst_ptr, src_rows, src_cols, dst_rows, dst_cols, RR_H, RR_W, C, stride_in_h, + stride_in_w, stride_in_c, stride_out_h, stride_out_w, stride_out_c, BLOCK_SIZE: tl.constexpr): + #RR_H和RR_W分别为高和宽的缩放比例 + block_id_c = tl.program_id(0) + block_id_h = tl.program_id(1) + block_id_w = tl.program_id(2) + dest_h_offs = (block_id_h * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)) + dest_w_offs = (block_id_w * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)) + dest_offs = (block_id_c[None, None] * stride_out_c + dest_h_offs[:, None] * stride_out_h + + dest_w_offs[None, :] * stride_out_w) + #根据output image的坐标值(dest_h_offs, dest_w_offs)计算input image的坐标值(sy, sx) + fy = dest_h_offs * RR_H + sy = tl.floor(fy) + fx = dest_w_offs * RR_W + sx = tl.floor(fx) + + src_offsets = (block_id_c[None, None] * stride_in_c + + tl.minimum(tl.maximum(sy, 0), src_rows - 1)[:, None].to(tl.int32) * stride_in_h + + tl.minimum(tl.maximum(sx, 0), src_cols - 1)[None, :].to(tl.int32) * stride_in_w) + src_val = tl.load(img_src_ptr + src_offsets) + dst_mask = (dest_h_offs[:, None] < dst_rows) & (dest_w_offs[None, :] < dst_cols) + tl.store(img_dst_ptr + dest_offs, src_val, mask=dst_mask) + + +def triton_kernel(img_src, img_dst): + N, C, src_rows, src_cols = img_src.shape + _, _, dst_rows, dst_cols = img_dst.shape + R_H = float(dst_rows) / src_rows + R_W = float(dst_cols) / src_cols + RR_H = 1.0 / R_H + RR_W = 1.0 / R_W + stride_in_n, stride_in_c, stride_in_h, stride_in_w = img_src.stride() + stride_out_n, stride_out_c, stride_out_h, stride_out_w = img_dst.stride() + bs = 16 + grid = lambda meta: ( + C, + triton.cdiv(dst_rows, meta["BLOCK_SIZE"]), + triton.cdiv(dst_cols, meta["BLOCK_SIZE"]), + ) + nearest_resize_kernel[grid](img_src, img_dst, src_rows, src_cols, dst_rows, dst_cols, RR_H, RR_W, C, stride_in_h, + stride_in_w, stride_in_c, stride_out_h, stride_out_w, stride_out_c, bs) + return img_dst + + +def nearest_resize_cpu(img_src, img_dst): + N, C, src_rows, src_cols = img_src.shape + _, _, dst_rows, dst_cols = img_dst.shape + #RR_H和RR_W分别为高和宽的缩放比例 + RR_H = src_rows / float(dst_rows) + RR_W = src_cols / float(dst_cols) + #根据output image的坐标值(i,j)计算input image的坐标值(sy, sx) + for i in range(dst_rows): + for j in range(dst_cols): + fy = (i * RR_H) + sy = math.floor(fy) + fx = (j * RR_W) + sx = math.floor(fx) + src_val = img_src[0, :, np.clip(sy, 0, src_rows - 1), np.clip(sx, 0, src_cols - 1)] + img_dst[0, :, i, j] = src_val + return img_dst + + +@pytest.mark.parametrize("shapes", [ + [360, 640, 140, 280], +]) +def test_nearest(shapes): + src_rows, src_cols, dst_rows, dst_cols = shapes + img_src = torch.rand(1, 4, src_rows, src_cols, dtype=torch.float32, device='npu') + img_dst = torch.zeros((1, img_src.shape[1], dst_rows, dst_cols), dtype=img_src.dtype, device=img_src.device) + torch_ref = nearest_resize_cpu(img_src.cpu(), img_dst.cpu()) + triton_cal = triton_kernel(img_src, img_dst) + torch.testing.assert_close(torch_ref.npu(), triton_cal) + + +if __name__ == "__main__": + src_rows, src_cols = 360, 640 + dst_rows, dst_cols = 140, 280 + img_src = torch.rand(1, 4, src_rows, src_cols, dtype=torch.float32, device='npu') + img_dst = torch.zeros((1, img_src.shape[1], dst_rows, dst_cols), dtype=img_src.dtype, device=img_src.device) + + assert img_src.shape[ + 0] == 1, "currently supports only shape[0] == 1 which does not change the functionality of thie case" + torch_ref = nearest_resize_cpu(img_src.cpu(), img_dst.cpu()) + triton_cal = triton_kernel(img_src, img_dst) + torch.testing.assert_close(torch_ref.npu(), triton_cal) + print("success") diff --git a/third_party/ascend/unittest/pytest_ut/test_neg.py b/third_party/ascend/unittest/pytest_ut/test_neg.py new file mode 100644 index 0000000000..a32a5039d8 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_neg.py @@ -0,0 +1,58 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import time +import torch +import torch_npu +import test_common + + +def torch_neg(x0): + res = -x0 + return res + + +@triton.jit +def triton_neg(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1 = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = -tmp0 + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (8, 8), 8, 8, 8], + ['float32', (8, 8), 8, 8, 8], + ['int8', (2, 4096, 8), 32, 2048, 64], +]) +def test_neg(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_neg(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_neg[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_neg_index.py b/third_party/ascend/unittest/pytest_ut/test_neg_index.py new file mode 100644 index 0000000000..774dec4cc3 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_neg_index.py @@ -0,0 +1,52 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import time +import torch +import torch_npu +import test_common + + +@triton.jit +def triton_neg_index_load_kernel(in_ptr, out_ptr, BLOCK_SIZE: tl.constexpr, NEG_INDEX: tl.constexpr): + offset = tl.arange(0, BLOCK_SIZE) + mask = offset >= NEG_INDEX + tmp = tl.load(in_ptr + ((-NEG_INDEX) + offset), mask, other=0.0) + tl.store(out_ptr + offset, tmp) + + +def triton_neg_index_load(in_tensor, index): + out_tensor = torch.zeros(in_tensor.shape, device=in_tensor.device, dtype=in_tensor.dtype) + triton_neg_index_load_kernel[(1, )](in_tensor, out_tensor, in_tensor.numel(), index) + return out_tensor + + +def torch_neg_index_load(in_tensor, index): + out = torch.zeros(in_tensor.shape, device=in_tensor.device, dtype=in_tensor.dtype) + out[index:] = in_tensor[:index] + return out + + +def test_neg_index_load(): + input_data = torch.arange(12, device="npu", dtype=torch.float32) + triton_out = triton_neg_index_load(input_data, 6) + torch_out = torch_neg_index_load(input_data, 6) diff --git a/third_party/ascend/unittest/pytest_ut/test_negative_mask_dim.py b/third_party/ascend/unittest/pytest_ut/test_negative_mask_dim.py new file mode 100644 index 0000000000..55ea0cb752 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_negative_mask_dim.py @@ -0,0 +1,48 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl + +import torch +import torch_npu + + +@triton.jit +def triton_negative_mask_dim(in_ptr0, out_ptr0, XBLOCK: tl.constexpr): + index = tl.arange(0, XBLOCK) + mask = (index < 1) & (index + 1 >= XBLOCK) + tmp0 = tl.load(in_ptr0 + index, mask, other=0.0) + tl.store(out_ptr0 + index, tmp0, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (32, ), 32], +]) +def test_negative_mask_dim(param_list): + dtype, shape, xblock = param_list + x0 = torch.ones(shape, dtype=eval('torch.' + dtype)).npu() + y_ref = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + + y_cal = torch.ones(shape, dtype=eval('torch.' + dtype)).npu() + triton_negative_mask_dim[(1, )](x0, y_cal, xblock) + assert torch.allclose(y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_nextafter.py b/third_party/ascend/unittest/pytest_ut/test_nextafter.py new file mode 100644 index 0000000000..007f74d9dc --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_nextafter.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu +import numpy as np + + +def torch_nextafter(x0, x1): + return torch.nextafter(x0, x1) + + +@triton.jit +def triton_nextafter(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = libdevice.nextafter(tmp0, tmp1) + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_nextafter(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0_ref = test_common.generate_tensor(shape, dtype) + x1_ref = test_common.generate_tensor(shape, dtype) + x0 = x0_ref.npu() + x1 = x1_ref.npu() + y_ref = torch_nextafter(x0_ref, x1_ref) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_nextafter[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_not.py b/third_party/ascend/unittest/pytest_ut/test_not.py new file mode 100644 index 0000000000..694afd64f0 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_not.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import torch +import torch_npu +import test_common + + +def torch_not(x0): + res = torch.bitwise_not(x0) + return res + + +@triton.jit +def triton_not(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp2 = not (tmp0.to(tl.int1)) + tl.store(out_ptr0 + (xindex), tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['bool', (2, 4096, 8), 2, 32768, 1024], +]) +def test_not(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_not(x0) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_not[ncore, 1, 1](x0, triton_res, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_npu_indexing.py b/third_party/ascend/unittest/pytest_ut/test_npu_indexing.py new file mode 100644 index 0000000000..d1abbc3aab --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_npu_indexing.py @@ -0,0 +1,143 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import time + + +def foo(a, b, c): + Z, Y, X, R = (1, 1, 64, 64) + y = a + b + y = y.sum(-1) + y = y.unsqueeze(3) + y = y.broadcast_to(Z, Y, X, R) + b + y = c + y.permute(0, 1, 3, 2) + return y + + +@triton.jit +def triton_foo( + in_ptr0, + in_ptr1, + in_ptr2, + out_ptr0, + BLOCK1: tl.constexpr, + BLOCK1_SUB: tl.constexpr, + BLOCK2: tl.constexpr, + Z: tl.constexpr, + Y: tl.constexpr, + X: tl.constexpr, + R: tl.constexpr, + Z_STRIDE: tl.constexpr, + Y_STRIDE: tl.constexpr, + X_STRIDE: tl.constexpr, + R_STRIDE: tl.constexpr, + Z_STRIDE1: tl.constexpr, + Y_STRIDE1: tl.constexpr, + X_STRIDE1: tl.constexpr, + R_STRIDE1: tl.constexpr, +): + offset: tl.constexpr = tl.program_id(0) * BLOCK1 + base1 = tl.arange(0, BLOCK1_SUB) + base2 = tl.arange(0, BLOCK2) + nsub: tl.constexpr = BLOCK1 // BLOCK1_SUB + # loops1 : tl.constexpr = nsub * Y * Z + loops1: tl.constexpr = nsub + loops2: tl.constexpr = R // BLOCK2 + + for z in range(Z): + for y in range(Y): + for loop1 in range(loops1): + # y = (loop1 // nsub) % Y + # z = loop1 // nsub // Y + # off1 = (loop1 % nsub) + off1 = loop1 + x = offset + (off1 * BLOCK1_SUB) + base1[:, None] + x1 = offset + (off1 * BLOCK1_SUB) + base1[None, :] + _tmp4 = tl.full([BLOCK1_SUB, BLOCK2], 0, tl.float32) + for loop2 in range(loops2): + r = loop2 * BLOCK2 + base2[None, :] + tmp0 = tl.load(in_ptr0 + (R_STRIDE * r + (X_STRIDE * x) + (Y_STRIDE * y) + (Z_STRIDE * z)), None) + tmp1 = tl.load(in_ptr1 + (R_STRIDE * r + (X_STRIDE * x) + (Y_STRIDE * y) + (Z_STRIDE * z)), None) + tmp2 = tmp0 + tmp1 + _tmp4 = _tmp4 + tmp2 + tmp4 = tl.sum(_tmp4, 1)[:, None] + tmp5 = tmp4.reshape(BLOCK1_SUB, 1).broadcast_to(BLOCK1_SUB, BLOCK2) + + for loop2 in range(loops2): + r = loop2 * BLOCK2 + base2[None, :] + tmp6 = tl.load(in_ptr1 + (R_STRIDE * r + (X_STRIDE * x) + (Y_STRIDE * y) + (Z_STRIDE * z)), None) + tmp7 = tmp6 + tmp5 + r1 = loop2 * BLOCK2 + base2[:, None] + tmp8 = tl.load(in_ptr2 + (R_STRIDE1 * x1 + (X_STRIDE1 * r1) + (Y_STRIDE1 * y) + (Z_STRIDE1 * z)), + None) + + tmp9 = tmp8.reshape(BLOCK2, BLOCK1_SUB) + tmp7.reshape(BLOCK1_SUB, BLOCK2).permute(1, 0) + tl.store(out_ptr0 + (R_STRIDE1 * x1 + (X_STRIDE1 * r1) + (Y_STRIDE1 * y) + (Z_STRIDE1 * z)), tmp9, + None) + + +def foo_triton_wrapper(a, b, c): + NBLOCKS = 1 + BLOCK1 = a.shape[2] // NBLOCKS + BLOCK1_SUB = 64 + BLOCK2 = 64 + + value = torch.empty_strided((c.shape[0], c.shape[1], c.shape[2], c.shape[3]), + (c.stride()[0], c.stride()[1], c.stride()[2], c.stride()[3]), + dtype=torch.float32).npu() + + triton_foo[NBLOCKS, 1, 1]( + a, + b, + c, + value, + BLOCK1, + BLOCK1_SUB, + BLOCK2, + a.shape[0], + a.shape[1], + a.shape[2], + a.shape[3], + a.stride()[0], + a.stride()[1], + a.stride()[2], + a.stride()[3], + c.stride()[0], + c.stride()[1], + c.stride()[2], + c.stride()[3], + ) + return value + + +def test_npu_indexing(): + Z, Y, X, R = (1, 1, 64, 64) + a = torch.randn((Z, Y, X, R), dtype=torch.float32).npu() + b = torch.randn((Z, Y, X, R), dtype=torch.float32).npu() + c = torch.randn((Z, Y, R, X), dtype=torch.float32).npu() + r = foo_triton_wrapper(a, b, c) + r1 = foo(a, b, c) + print(r[0, 0, 0:8, 0:8]) + print(r1[0, 0, 0:8, 0:8]) + torch.testing.assert_close(r, r1) diff --git a/third_party/ascend/unittest/pytest_ut/test_npu_indexing2.py b/third_party/ascend/unittest/pytest_ut/test_npu_indexing2.py new file mode 100644 index 0000000000..e36cb8403f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_npu_indexing2.py @@ -0,0 +1,92 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import time + + +def foo(a, b, c): + y = a + b + c + y = y.sum(dim=1) + return y + + +@triton.jit +def triton_codegen2(in_ptr0, in_ptr1, in_ptr2, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr, + RBLOCK: tl.constexpr): + ynumel = 8 + rnumel = 2048 + xnumel = 1024 + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + base2 = tl.arange(0, RBLOCK) + loops2: tl.constexpr = rnumel // RBLOCK + for y in range(ynumel): + y0 = y + for loop1 in range(loops1): + x = offset + (loop1 * XBLOCK_SUB) + base1 + x1 = offset + (loop1 * XBLOCK_SUB) + base1[None, :] + _tmp6 = tl.full([XBLOCK_SUB, RBLOCK], 0, tl.float32) + for loop2 in range(loops2): + r2 = loop2 * RBLOCK + base2[:, None] + tmp0 = tl.load(in_ptr0 + (x1 + (1024 * r2) + (2097152 * y0)), None, eviction_policy='evict_last') + tmp1 = tl.load(in_ptr1 + (x1 + (1024 * r2) + (2097152 * y0)), None, eviction_policy='evict_last') + tmp3 = tl.load(in_ptr2 + (x1 + (1024 * r2) + (2097152 * y0)), None, eviction_policy='evict_last') + tmp2 = tmp0 + tmp1 + tmp4 = tmp2 + tmp3 + tmp5 = tl.reshape(tmp4, [RBLOCK, XBLOCK_SUB]) + tmp7 = _tmp6 + tmp5 + _tmp6 = tmp7 + tmp6 = tl.sum(_tmp6, 0).reshape(XBLOCK_SUB) + + tl.store(out_ptr0 + (x + (1024 * y0)), tmp6, None) + + +def foo_triton_wrapper(a, b, c): + NBLOCKS = 8 + BLOCK1 = a.shape[2] // NBLOCKS + BLOCK1_SUB = 64 + BLOCK2 = 64 + + value = torch.empty_strided((c.shape[0], c.shape[2]), (c.shape[2], 1), dtype=torch.float32).npu() + + triton_codegen2[NBLOCKS, 1, 1](a, b, c, value, BLOCK1, BLOCK1_SUB, BLOCK2) + + return value + + +def test_npu_indexing2(): + + Y, X, R = (8, 2048, 1024) + a = torch.randn((Y, X, R), dtype=torch.float32).npu() + b = torch.randn((Y, X, R), dtype=torch.float32).npu() + c = torch.randn((Y, X, R), dtype=torch.float32).npu() + r = foo_triton_wrapper(a, b, c) + r1 = foo(a, b, c) + print(r[ + 0:8, + 0:8, + ]) + print(r1[0:8, 0:8]) + torch.testing.assert_close(r, r1, rtol=1e-3, atol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_or.py b/third_party/ascend/unittest/pytest_ut/test_or.py new file mode 100644 index 0000000000..5a03d301c5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_or.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import test_common + + +def torch_or(x0, x1): + res = x0 | x1 + return res + + +@triton.jit +def triton_or(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x_index = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + x_index, None) + tmp1 = tl.load(in_ptr1 + x_index, None) + tmp2 = tmp0 | tmp1 + tl.store(out_ptr0 + x_index, tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['int32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_or(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_or(x0, x1) + # triton结果 + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_or[ncore, 1, 1](x0, x1, triton_res, xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_paged_kvcache_krope.py b/third_party/ascend/unittest/pytest_ut/test_paged_kvcache_krope.py new file mode 100644 index 0000000000..ebc419b4fa --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_paged_kvcache_krope.py @@ -0,0 +1,72 @@ +import torch +import triton +import triton.language as tl +import pytest + + +@triton.jit +def rope_like_load_kernel( + Kv_cache, + Req_to_tokens, + output_ptr, + stride_kv: tl.constexpr, + HEAD_DIM_V: tl.constexpr, + HEAD_DIM: tl.constexpr, + PAGE_SIZE: tl.constexpr, + BLOCK_N: tl.constexpr, + ROPE_DIM: tl.constexpr, +): + + offs_d_kpe = tl.arange(HEAD_DIM_V, HEAD_DIM) + offs_n = tl.arange(0, BLOCK_N) + + kv_page_number = tl.load(Req_to_tokens + offs_n // PAGE_SIZE) + kv_loc = kv_page_number * PAGE_SIZE + offs_n % PAGE_SIZE + + offs_k_pe = kv_loc[None, :] * stride_kv + offs_d_kpe[:, None] + + k_pe = tl.load(Kv_cache + offs_k_pe) + + offs_out = offs_n[:, None] * ROPE_DIM + tl.arange(0, ROPE_DIM)[None, :] + tl.store(output_ptr + offs_out, tl.trans(k_pe)) + + +def test_bubbleup_extract_nonzero_offset(): + device = "npu" + + PAGE_SIZE = 2 + BLOCK_N = 4 + head_dim = 32 + head_dim_v = 24 + rope_dim = head_dim - head_dim_v + num_pages = BLOCK_N // PAGE_SIZE + + req_to_tokens = torch.arange(num_pages, dtype=torch.int32, device=device) + total_tokens = num_pages * PAGE_SIZE + kv_cache = torch.zeros(total_tokens, head_dim, dtype=torch.float32, device=device) + for token_id in range(total_tokens): + kv_cache[token_id, :head_dim_v] = (torch.arange(head_dim_v, dtype=torch.float32) + token_id * 100) + kv_cache[token_id, head_dim_v:] = (torch.arange(head_dim_v, head_dim, dtype=torch.float32) + token_id * 1000) + output = torch.zeros(BLOCK_N, rope_dim, dtype=torch.float32, device=device) + + rope_like_load_kernel[(1, )]( + kv_cache.flatten(), + req_to_tokens, + output.flatten(), + stride_kv=head_dim, + HEAD_DIM_V=head_dim_v, + HEAD_DIM=head_dim, + PAGE_SIZE=PAGE_SIZE, + BLOCK_N=BLOCK_N, + ROPE_DIM=rope_dim, + ) + + expected = torch.zeros(BLOCK_N, rope_dim, dtype=torch.float32, device=device) + for token_id in range(BLOCK_N): + expected[token_id] = (torch.arange(head_dim_v, head_dim, dtype=torch.float32) + token_id * 1000) + + buggy = torch.zeros(BLOCK_N, rope_dim, dtype=torch.float32, device=device) + for token_id in range(BLOCK_N): + buggy[token_id] = (torch.arange(rope_dim, dtype=torch.float32) + token_id * 100) + + assert torch.allclose(output, expected, atol=1e-5) diff --git a/third_party/ascend/unittest/pytest_ut/test_parallel.py b/third_party/ascend/unittest/pytest_ut/test_parallel.py new file mode 100644 index 0000000000..72889eb49b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_parallel.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu +from torch.testing import assert_close + +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al + + +@triton.jit +def parallel_kernel(x_ptr, out_ptr, M: tl.constexpr, N: tl.constexpr): + # Load the full [M, N] block + offs_m = tl.arange(0, M) + offs_n = tl.arange(0, N) + block = tl.load(x_ptr + offs_m[:, None] * N + offs_n[None, :]) + + SUB_M: tl.constexpr = M // 2 + for s in al.parallel(0, 2): + sub = al.extract_slice(block, (s * SUB_M, 0), (SUB_M, N), (1, 1)) + sub = sub * 2.0 + offs_sub_m = s * SUB_M + tl.arange(0, SUB_M) + out_ptrs = out_ptr + offs_sub_m[:, None] * N + offs_n[None, :] + tl.store(out_ptrs, sub) + + +@pytest.mark.parametrize("M, N", [(128, 128)]) +def test_parallel(M, N): + x = torch.randn((M, N), device="npu", dtype=torch.float32) + out = torch.empty((M, N), device="npu", dtype=torch.float32) + h = parallel_kernel[(1, )](x, out, M=M, N=N) + torch.npu.synchronize() + assert_close(out, x * 2, rtol=1e-3, atol=1e-3) + code_str = h.asm["ttadapter"] + count = code_str.count("hivm.parallel_loop") + assert count == 1 + + +if __name__ == "__main__": + test_parallel(128, 128) + print("test_parallel PASSED!") diff --git a/third_party/ascend/unittest/pytest_ut/test_permute.py b/third_party/ascend/unittest/pytest_ut/test_permute.py new file mode 100644 index 0000000000..9850b26579 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_permute.py @@ -0,0 +1,127 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import time + + +@triton.jit +def triton_foo( + in_ptr0, + in_ptr1, + in_ptr2, + out_ptr0, + BLOCK1: tl.constexpr, + BLOCK1_SUB: tl.constexpr, + BLOCK2: tl.constexpr, + X: tl.constexpr, + Y: tl.constexpr, + Z: tl.constexpr, + R: tl.constexpr, + Z_STRIDE: tl.constexpr, + Y_STRIDE: tl.constexpr, + X_STRIDE: tl.constexpr, + R_STRIDE: tl.constexpr, + X_STRIDE1: tl.constexpr, + Y_STRIDE1: tl.constexpr, + Z_STRIDE1: tl.constexpr, + R_STRIDE1: tl.constexpr, +): + offset: tl.constexpr = tl.program_id(0) * BLOCK1 + base1 = tl.arange(0, BLOCK1_SUB) + base2 = tl.arange(0, BLOCK2) + nsub: tl.constexpr = BLOCK1 // BLOCK1_SUB + # loops1 : tl.constexpr = nsub * Y * Z + loops1: tl.constexpr = nsub + loops2: tl.constexpr = R // BLOCK2 + + for z in range(Z): + for y in range(Y): + for loop1 in range(loops1): + off1 = loop1 + x = offset + (off1 * BLOCK1_SUB) + base1[:, None] + x1 = offset + (off1 * BLOCK1_SUB) + base1[None, :] + + for loop2 in range(loops2): + r = loop2 * BLOCK2 + base2[None, :] + r1 = loop2 * BLOCK2 + base2[:, None] + tmp0 = tl.load(in_ptr0 + ((R_STRIDE * r) + (X_STRIDE * x) + (Y_STRIDE * y) + (Z_STRIDE * z)), None) + tmp1 = tl.load(in_ptr1 + ((R_STRIDE * r) + (X_STRIDE * x) + (Y_STRIDE * y) + (Z_STRIDE * z)), None) + tmp2 = tmp0 + tmp1 + + tmp8 = tl.load(in_ptr2 + (R_STRIDE1 * r + X_STRIDE1 * x + (Y_STRIDE1 * y) + (Z_STRIDE1 * z)), None) + tmp9 = tmp8 + tmp2 + tl.store(out_ptr0 + (R_STRIDE1 * r + X_STRIDE1 * x + (Y_STRIDE1 * y) + (Z_STRIDE1 * z)), tmp9, None) + + +def foo_triton_wrapper(a, b, c): + NBLOCKS = 32 if c.shape[0] >= 256 else 1 + BLOCK1 = c.shape[0] // NBLOCKS + BLOCK1_SUB = BLOCK1 if BLOCK1 < 64 else 64 + BLOCK2 = c.shape[3] if c.shape[3] < 64 else 64 + + value = torch.empty_strided((c.shape[0], c.shape[1], c.shape[2], c.shape[3]), + (c.stride()[0], c.stride()[1], c.stride()[2], c.stride()[3]), + dtype=torch.float32).npu() + + triton_foo[NBLOCKS, 1, 1]( + a, + b, + c, + value, + BLOCK1, + BLOCK1_SUB, + BLOCK2, + c.shape[0], + c.shape[1], + c.shape[2], + c.shape[3], + a.stride()[0], + a.stride()[1], + a.stride()[2], + a.stride()[3], + c.stride()[0], + c.stride()[1], + c.stride()[2], + c.stride()[3], + ) + return value + + +def foo(a, b, c): + y = a + b + y = c + y.permute(2, 1, 0, 3) + return y + + +def test_permute_handwritten(): + + Z, Y, X, R = (1, 12, 4096, 8) + a = torch.randn((Z, Y, X, R), dtype=torch.float32).npu() + b = torch.randn((Z, Y, X, R), dtype=torch.float32).npu() + c = torch.randn((X, Y, Z, R), dtype=torch.float32).npu() + r = foo_triton_wrapper(a, b, c) + r1 = foo(a, b, c) + print(r[0, 0, 0:8, 0:8]) + print(r1[0, 0, 0:8, 0:8]) + torch.testing.assert_close(r, r1, rtol=1e-3, atol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_permute_full.py b/third_party/ascend/unittest/pytest_ut/test_permute_full.py new file mode 100644 index 0000000000..e717b8a1c3 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_permute_full.py @@ -0,0 +1,171 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_021(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + # XB,YB,1 + X = tl.load(x_ptr + idx) + + ret = tl.permute(X, (0, 2, 1)) + + oidx = xidx[:, None, None] * YB * ZB + zidx[None, :, None] * YB + yidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_102(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + # XB,YB,1 + X = tl.load(x_ptr + idx) + + ret = tl.permute(X, (1, 0, 2)) + + oidx = yidx[:, None, None] * XB * ZB + xidx[None, :, None] * ZB + zidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_210(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + # XB,YB,1 + X = tl.load(x_ptr + idx) + + ret = tl.permute(X, (2, 1, 0)) + + oidx = zidx[:, None, None] * YB * XB + yidx[None, :, None] * XB + xidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_201(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + # XB,YB,1 + X = tl.load(x_ptr + idx) + + ret = tl.permute(X, (2, 0, 1)) + + oidx = zidx[:, None, None] * YB * XB + xidx[None, :, None] * YB + yidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_120(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + # XB,YB,1 + X = tl.load(x_ptr + idx) + + ret = tl.permute(X, (1, 2, 0)) + + oidx = yidx[:, None, None] * ZB * XB + zidx[None, :, None] * XB + xidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@pytest.mark.parametrize('para_type,data_type,XB,YB,ZB', [ + # ['float32',eval('torch.float32'),2,4,3], + ['float32', eval('torch.float32'), 2, 4, 8], + # ['float32',eval('torch.float32'),2,4,37], + ['float32', eval('torch.float32'), 2, 4, 64], + # ['float32',eval('torch.float32'),2,4,781], + + # ['float16',eval('torch.float16'),2,4,3], + ['float16', eval('torch.float16'), 2, 4, 8], + # ['float16',eval('torch.float16'),2,4,37], + ['float16', eval('torch.float16'), 2, 4, 64], + # ['float16',eval('torch.float16'),2,4,781], + + # ['int8',eval('torch.int8'),2,4,3], + ['int8', eval('torch.int8'), 2, 4, 8], + # ['int8',eval('torch.int8'),2,4,37], + ['int8', eval('torch.int8'), 2, 4, 64], + # ['int8',eval('torch.int8'),2,4,781], +]) +def test_permute(para_type, data_type, XB, YB, ZB): + + x = torch.randint(low=0, high=2, size=(XB, YB, ZB), dtype=data_type).npu() + + output = torch.randint(1, (XB, ZB, YB), dtype=data_type).npu() + torch_021 = torch.permute(x, (0, 2, 1)) + fn_npu_021[1, 1, 1](output, x, XB, YB, ZB) + torch.testing.assert_close(output, torch_021) + + print(" test permute 021 passed") + + output = torch.randint(1, (YB, XB, ZB), dtype=data_type).npu() + torch_102 = torch.permute(x, (1, 0, 2)) + fn_npu_102[1, 1, 1](output, x, XB, YB, ZB) + torch.testing.assert_close(output, torch_102) + + print(" test permute 102 passed") + + output = torch.randint(1, (ZB, XB, YB), dtype=data_type).npu() + torch_201 = torch.permute(x, (2, 0, 1)) + fn_npu_201[1, 1, 1](output, x, XB, YB, ZB) + torch.testing.assert_close(output, torch_201) + + print(" test permute 201 passed") + + output = torch.randint(1, (ZB, YB, XB), dtype=data_type).npu() + torch_210 = torch.permute(x, (2, 1, 0)) + fn_npu_210[1, 1, 1](output, x, XB, YB, ZB) + torch.testing.assert_close(output, torch_210) + + print(" test permute 210 passed") + + output = torch.randint(1, (YB, ZB, XB), dtype=data_type).npu() + torch_120 = torch.permute(x, (1, 2, 0)) + fn_npu_120[1, 1, 1](output, x, XB, YB, ZB) + torch.testing.assert_close(output, torch_120) + + print(" test permute 120 passed") diff --git a/third_party/ascend/unittest/pytest_ut/test_permute_reshape.py b/third_party/ascend/unittest/pytest_ut/test_permute_reshape.py new file mode 100644 index 0000000000..65b5775b31 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_permute_reshape.py @@ -0,0 +1,91 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import time + + +@triton.jit +def triton_foo( + in_ptr0, + in_ptr1, + in_ptr2, + out_ptr0, + BLOCK1: tl.constexpr, + BLOCK1_SUB: tl.constexpr, + BLOCK2: tl.constexpr, + S: tl.constexpr, + N: tl.constexpr, + D: tl.constexpr, +): + offset: tl.constexpr = tl.program_id(0) * BLOCK1 + base1 = tl.arange(0, BLOCK1_SUB) + base2 = tl.arange(0, BLOCK2) + loops1: tl.constexpr = BLOCK1 // BLOCK1_SUB + loops2: tl.constexpr = D // BLOCK2 + + for loop1 in range(loops1): + off1 = loop1 + s = offset + (off1 * BLOCK1_SUB) + base1[:, None] + for n in range(N): + for loop2 in range(loops2): + d = loop2 * BLOCK2 + base2[None, :] + tmp0 = tl.load(in_ptr0 + ((32768 * n) + (8 * s) + d), None) + tmp1 = tl.load(in_ptr1 + ((32768 * n) + (8 * s) + d), None) + tmp2 = tmp0 + tmp1 + + tmp3 = tl.load(in_ptr2 + ((8 * n) + d + (96 * s)), None) + tmp9 = tmp3 + tmp2 + tl.store(out_ptr0 + ((8 * n) + d + (96 * s)), tmp9, None) + + +def foo_triton_wrapper(a, b, c): + NBLOCKS = 32 if a.shape[2] >= 256 else 1 + BLOCK1 = a.shape[2] // NBLOCKS + BLOCK1_SUB = BLOCK1 if BLOCK1 < 64 else 64 + BLOCK2 = a.shape[3] if a.shape[3] < 64 else 64 + + value = torch.empty_strided((c.shape[0], c.shape[1], c.shape[2]), (c.stride()[0], c.stride()[1], c.stride()[2]), + dtype=torch.float32).npu() + triton_foo[NBLOCKS, 1, 1](a, b, c, value, BLOCK1, BLOCK1_SUB, BLOCK2, a.shape[2], a.shape[1], a.shape[3]) + + return value + + +def foo(a, b, c): + B, N, S, D = (1, 12, 4096, 8) + y = a + b + y = c + y.permute(2, 0, 1, 3).reshape(S, B, N * D) + return y + + +def test_permute_reshape(): + B, N, S, D = (1, 12, 4096, 8) + a = torch.randn((B, N, S, D), dtype=torch.float32).npu() + b = torch.randn((B, N, S, D), dtype=torch.float32).npu() + c = torch.randn((S, B, N * D), dtype=torch.float32).npu() + r = foo_triton_wrapper(a, b, c) + r1 = foo(a, b, c) + print(r[0:8, 0, 0:8]) + print(r1[0:8, 0, 0:8]) + torch.testing.assert_close(r, r1, rtol=1e-3, atol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_permuted_boundary_check.py b/third_party/ascend/unittest/pytest_ut/test_permuted_boundary_check.py new file mode 100644 index 0000000000..c232f62de0 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_permuted_boundary_check.py @@ -0,0 +1,38 @@ +import torch +import torch_npu +import triton +import triton.language as tl +import pytest + + +@triton.jit +def zj_fa_fwd_pattern(in_ptr0, in_ptr1, out_ptr, M, K, N, MBLOCK: tl.constexpr, NBLOCK: tl.constexpr, + KBLOCK: tl.constexpr): + a_ptr = tl.make_block_ptr(base=in_ptr0, shape=(M, K), # 8, 3 + strides=(K, 1), offsets=(0, 0), block_shape=(MBLOCK, KBLOCK), order=(1, 0)) + + b_ptr = tl.make_block_ptr(base=in_ptr1, shape=(K, N), # 3, 8 + strides=(1, K), offsets=(0, 0), block_shape=(KBLOCK, NBLOCK), order=(0, 1)) + + c_ptr = tl.make_block_ptr(base=out_ptr, shape=(M, N), strides=(1, M), offsets=(0, 0), block_shape=(MBLOCK, NBLOCK), + order=(0, 1)) + + a = tl.load(a_ptr, boundary_check=(0, ), padding_option="zero") + b = tl.load(b_ptr, boundary_check=(0, ), padding_option="zero") + c = tl.dot(a, b) + tl.store(c_ptr, c, boundary_check=(0, 1)) + + +def test_permute_boundary_check(): + M = 8 + K = 3 + N = 8 + MBLOCK = 8 + NBLOCK = 8 + KBLOCK = 4 + a = torch.randn((M, K), device="npu") # 8, 3 + b = torch.randn((N, K), device="npu") # 8, 3 + c = torch.empty((N, M), device="npu") + zj_fa_fwd_pattern[(1, 1, 1)](a, b, c, M, K, N, MBLOCK, NBLOCK, KBLOCK) + std = a @ b.T + torch.testing.assert_close(std, c.T, atol=1e-2, rtol=1e-2) diff --git a/third_party/ascend/unittest/pytest_ut/test_pointer_bitcast_byte_buffer.py b/third_party/ascend/unittest/pytest_ut/test_pointer_bitcast_byte_buffer.py new file mode 100644 index 0000000000..893a3df62d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_pointer_bitcast_byte_buffer.py @@ -0,0 +1,268 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import struct + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest + + +def _write_u32_le(buffer, byte_offsets, values): + for byte_offset, value in zip(byte_offsets, values): + for byte_index, byte_value in enumerate(struct.pack("> 16 + buffer[byte_offset + index * 2] = bf16_bits & 0xFF + buffer[byte_offset + index * 2 + 1] = bf16_bits >> 8 + + +@triton.jit +def _paged_scale_load( + kv_ptr, + block_tables_ptr, + out_ptr, + n_positions, + stride_kvblk, + stride_kvpos, + stride_kvbyte, + DIM: tl.constexpr, + CACHE_BLOCK_SIZE: tl.constexpr, + BLOCK: tl.constexpr, +): + positions = tl.arange(0, BLOCK) + mask = positions < n_positions + logical_block = positions // CACHE_BLOCK_SIZE + intra_block_pos = positions % CACHE_BLOCK_SIZE + physical_block = tl.load(block_tables_ptr + logical_block, mask=mask, other=0) + kv_base = (physical_block * stride_kvblk + intra_block_pos * stride_kvpos) + scale_addr = kv_base + DIM * stride_kvbyte + scale_ptr = (kv_ptr + scale_addr).to(tl.pointer_type(tl.uint32, 1), bitcast=True) + scale_u32 = tl.load(scale_ptr, mask=mask, other=0) + tl.store(out_ptr + positions, scale_u32.to(tl.int32), mask=mask) + + +@triton.jit +def _multi_addptr_bf16_store( + values_ptr, + cache_ptr, + block_idx, + token_pos, + block_stride, + N: tl.constexpr, + TOKEN_BYTES: tl.constexpr, + BF16_OFFSET: tl.constexpr, + BLOCK: tl.constexpr, +): + lanes = tl.arange(0, BLOCK) + block_base = cache_ptr + block_idx * tl.multiple_of(block_stride, 16) + token_base = block_base + token_pos * TOKEN_BYTES + bf16_ptr = (token_base + BF16_OFFSET).to(tl.pointer_type(tl.bfloat16)) + mask = lanes < N + values = tl.load(values_ptr + lanes, mask=mask, other=0.0) + tl.store(bf16_ptr + lanes, values, mask=mask) + + +@triton.jit +def _loop_bf16_load( + src, + out, + count, + NOPE_DIM: tl.constexpr, + TOKEN_BYTES: tl.constexpr, + ROPE_DIM: tl.constexpr, + BLOCK: tl.constexpr, + NUM_WORKERS: tl.constexpr, +): + worker = tl.program_id(0) + lanes = tl.arange(0, BLOCK) + mask = lanes < ROPE_DIM + for token_idx in range(worker, count, NUM_WORKERS): + token_data = src + token_idx * TOKEN_BYTES + bf16_ptr = (token_data + NOPE_DIM).to(tl.pointer_type(tl.bfloat16)) + values = tl.load(bf16_ptr + lanes, mask=mask, other=0.0) + tl.store(out + token_idx * ROPE_DIM + lanes, values, mask=mask) + + +@triton.jit +def _tensor_multi_addptr_bf16_load( + cache_ptr, + block_ids_ptr, + positions_ptr, + out_ptr, + block_stride, + token_bytes, + BF16_OFFSET: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + cols = tl.arange(0, BLOCK_N) + block_ids = tl.load(block_ids_ptr + cols) + positions = tl.load(positions_ptr + cols) + + block_offsets = block_ids.to(tl.int64) * block_stride + block_ptrs = cache_ptr + block_offsets + token_ptrs = block_ptrs + positions * token_bytes + rope_bytes = token_ptrs + BF16_OFFSET + rope_ptrs = rope_bytes.to(tl.pointer_type(tl.bfloat16), bitcast=True) + + rows = tl.arange(0, BLOCK_M) + values = tl.load(rope_ptrs[None, :] + rows[:, None]) + output_offsets = rows[:, None] * BLOCK_N + cols[None, :] + tl.store(out_ptr + output_offsets, values) + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_pointer_bitcast_paged_scale_tensor_offset(): + cache_block_size = 4 + dim = 8 + record_bytes = dim + 4 + stride_kvpos = record_bytes + stride_kvblk = cache_block_size * record_bytes + n_positions = 8 + block_table = [2, 0] + values = [100 + index for index in range(n_positions)] + + host = torch.zeros(3 * stride_kvblk, dtype=torch.uint8) + byte_offsets = [] + for position in range(n_positions): + logical_block = position // cache_block_size + intra_block_pos = position % cache_block_size + physical_block = block_table[logical_block] + byte_offsets.append(physical_block * stride_kvblk + intra_block_pos * stride_kvpos + dim) + _write_u32_le(host, byte_offsets, values) + + src = host.npu() + block_tables = torch.tensor(block_table, dtype=torch.int32).npu() + out = torch.empty((n_positions, ), dtype=torch.int32, device="npu") + _paged_scale_load[(1, )]( + src, + block_tables, + out, + n_positions, + stride_kvblk, + stride_kvpos, + 1, + DIM=dim, + CACHE_BLOCK_SIZE=cache_block_size, + BLOCK=n_positions, + ) + + torch.testing.assert_close(out.cpu(), torch.tensor(values, dtype=torch.int32), rtol=0, atol=0) + + +def test_pointer_bitcast_scalar_multi_addptr_store(): + block_idx = 1 + token_pos = 2 + block_stride = 2048 + token_bytes = 576 + bf16_offset = 448 + values = [float(index + 1) for index in range(16)] + byte_offset = (block_idx * block_stride + token_pos * token_bytes + bf16_offset) + + expected = torch.zeros(byte_offset + len(values) * 2 + 64, dtype=torch.uint8) + _write_bf16_le(expected, byte_offset, values) + cache = torch.zeros_like(expected).npu() + source = torch.tensor(values, dtype=torch.float32).to(torch.bfloat16).npu() + _multi_addptr_bf16_store[(1, )]( + source, + cache, + block_idx, + token_pos, + block_stride, + N=len(values), + TOKEN_BYTES=token_bytes, + BF16_OFFSET=bf16_offset, + BLOCK=32, + ) + + torch.testing.assert_close(cache.cpu(), expected, rtol=0, atol=0) + + +def test_pointer_bitcast_inside_dynamic_loop(): + token_count = 5 + nope_dim = 448 + rope_dim = 8 + token_bytes = 576 + num_workers = 2 + host = torch.zeros(token_count * token_bytes, dtype=torch.uint8) + expected_values = [] + for token_idx in range(token_count): + values = [float(token_idx * rope_dim + index + 1) for index in range(rope_dim)] + expected_values.extend(values) + _write_bf16_le(host, token_idx * token_bytes + nope_dim, values) + + src = host.npu() + out = torch.empty((token_count * rope_dim, ), dtype=torch.bfloat16, device="npu") + _loop_bf16_load[(num_workers, )]( + src, + out, + token_count, + NOPE_DIM=nope_dim, + TOKEN_BYTES=token_bytes, + ROPE_DIM=rope_dim, + BLOCK=16, + NUM_WORKERS=num_workers, + ) + + expected = torch.tensor(expected_values, dtype=torch.float32).to(torch.bfloat16) + torch.testing.assert_close(out.cpu(), expected, rtol=0, atol=0) + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +def test_pointer_bitcast_tensor_multi_addptr_broadcast_load(): + block_m = 4 + block_n = 32 + block_stride = 2048 + token_bytes = 576 + bf16_offset = 448 + + block_ids = torch.arange(block_n, dtype=torch.int32) + positions = torch.arange(block_n, dtype=torch.int32) % 3 + host = torch.zeros(block_n * block_stride, dtype=torch.uint8) + expected = torch.empty((block_m, block_n), dtype=torch.bfloat16) + for col in range(block_n): + values = [float(col * block_m + row + 1) for row in range(block_m)] + byte_offset = (block_ids[col].item() * block_stride + positions[col].item() * token_bytes + bf16_offset) + _write_bf16_le(host, byte_offset, values) + expected[:, col] = torch.tensor(values, dtype=torch.bfloat16) + + cache = host.npu() + out = torch.empty((block_m, block_n), dtype=torch.bfloat16, device="npu") + _tensor_multi_addptr_bf16_load[(1, )]( + cache, + block_ids.npu(), + positions.npu(), + out, + block_stride, + token_bytes, + BF16_OFFSET=bf16_offset, + BLOCK_M=block_m, + BLOCK_N=block_n, + ) + + torch.testing.assert_close(out.cpu(), expected, rtol=0, atol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_pointer_reshape.py b/third_party/ascend/unittest/pytest_ut/test_pointer_reshape.py new file mode 100644 index 0000000000..76bd4d32ef --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_pointer_reshape.py @@ -0,0 +1,112 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Regression for common-base pointer reshape across masked loads and stores.""" +import pytest +import torch +import torch_npu +import triton +import triton.language as tl + +# Remove once NPUIR fixes arange_2d S/V synchronization and packed i1 scalar +# extraction on A2/A3. The offset error can also leave later tests failing with +# device exceptions, so skip the entire family until both backend fixes land. +pytestmark = pytest.mark.skipif( + triton.runtime.driver.active.get_current_target().arch.startswith(("Ascend910B", "Ascend910_93")), + reason="NPUIR A2/A3: arange_2d synchronization and packed-mask scalar extraction are incorrect", +) + + +@triton.jit +def _masked_pointer_reshape(src, gathered, masked_out, n, WIDE: tl.constexpr, BLOCK: tl.constexpr): + lanes = tl.arange(0, BLOCK) + logical = (lanes * 7) % BLOCK + offsets = logical - BLOCK // 2 + if WIDE: + offsets = offsets.to(tl.int64) + offsets_2d = tl.reshape(offsets, (4, BLOCK // 4)) + # The scalar base displacement and negative per-lane offsets must cancel. + src_ptrs = (src + BLOCK // 2) + offsets_2d + src_flat = tl.reshape(src_ptrs, (BLOCK, )) + valid = logical < n + values = tl.load(src_flat, mask=valid, other=-7) + tl.store(gathered + lanes, values) + + dst_ptrs = masked_out + lanes + dst_2d = tl.reshape(dst_ptrs, (4, BLOCK // 4)) + tl.store(dst_2d, tl.reshape(values, (4, BLOCK // 4)), mask=tl.reshape(valid, (4, BLOCK // 4))) + + +@pytest.mark.parametrize("n", [0, 17, 32]) +@pytest.mark.parametrize("wide", [False, True]) +@pytest.mark.parametrize("dtype", [torch.float32, torch.int32]) +def test_masked_pointer_reshape(n, wide, dtype): + block = 32 + # Masked-off source lanes contain distinct poison values, and masked-off + # output lanes must keep the sentinel rather than storing the load's other. + src_cpu = torch.arange(block, dtype=dtype) * 3 + 5 + src_cpu[n:] = -1000 + src = src_cpu.npu() + gathered = torch.empty(block, dtype=dtype, device="npu") + masked_out = torch.full((block, ), 123, dtype=dtype, device="npu") + _masked_pointer_reshape[(1, )](src, gathered, masked_out, n, WIDE=wide, BLOCK=block) + + indices = (torch.arange(block) * 7) % block + valid = indices < n + expected = torch.where(valid, src_cpu[indices], -7) + expected_out = torch.where(valid, expected, 123) + torch.testing.assert_close(gathered.cpu(), expected, rtol=0, atol=0) + torch.testing.assert_close(masked_out.cpu(), expected_out, rtol=0, atol=0) + + +@triton.jit +def _common_base_reshape_variants(src, out, shift, steps, ROW_STRIDE: tl.constexpr, BROADCAST: tl.constexpr): + rows = tl.arange(0, 4) + cols = tl.arange(0, 8) + if BROADCAST: + ptrs = (src + shift) + rows[:, None].to(tl.int64) + ptrs = tl.broadcast_to(ptrs, (4, 8)) + else: + offsets = rows[:, None] * ROW_STRIDE + cols[None, :] + ptrs = (src + shift) + offsets.to(tl.int64) + ptrs = ptrs + 2 + for _ in range(steps): + ptrs = ptrs + 1 + flat = tl.reshape(ptrs, (32, )) + values = tl.load(flat) + dst = tl.reshape(out + tl.arange(0, 32), (2, 16)) + tl.store(dst, tl.reshape(values, (2, 16))) + + +@pytest.mark.parametrize("row_stride,broadcast", [(8, False), (16, False), (0, True)]) +@pytest.mark.parametrize("steps", [0, 3]) +@pytest.mark.parametrize("dtype", [torch.float32, torch.int32]) +def test_common_base_reshape_variants(row_stride, broadcast, steps, dtype): + shift = 7 + src_cpu = torch.arange(80, dtype=dtype) * 3 + 5 + src = src_cpu.npu() + out = torch.empty(32, dtype=dtype, device="npu") + _common_base_reshape_variants[(1, )](src, out, shift, steps, ROW_STRIDE=row_stride, BROADCAST=broadcast) + rows = torch.arange(4)[:, None] + if broadcast: + offsets = rows.expand(4, 8) + else: + offsets = rows * row_stride + torch.arange(8)[None, :] + indices = (offsets + shift + 2 + steps).reshape(-1) + torch.testing.assert_close(out.cpu(), src_cpu[indices], rtol=0, atol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_pointer_type.py b/third_party/ascend/unittest/pytest_ut/test_pointer_type.py new file mode 100644 index 0000000000..7641b25d66 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_pointer_type.py @@ -0,0 +1,49 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def kernel(ans_ptr, x_ptr): + val = tl.load(x_ptr) + output_ptr = tl.load(ans_ptr) + output_ptr = output_ptr.to(tl.pointer_type(val.dtype)) + tl.store(output_ptr, val) + + +@pytest.mark.parametrize("literal, dtype_str", + [[0, eval('torch.int8')], [0, eval('torch.int16')], [0, eval('torch.int32')], + [0, eval('torch.int64')], [0, eval('torch.float16')], [0, eval('torch.float32')]]) +def test_pointer_type(literal, dtype_str): + x = torch.randint(low=0, high=5, size=(1, ), dtype=dtype_str).npu() + output = torch.zeros((1, ), dtype=dtype_str).npu() + ans = [] + ans.append(output.data_ptr()) + ans_tensor = torch.tensor(ans).npu() + kernel[(1, )](ans_tensor, x) + assert torch.isclose(x, output) + print("Pointer type convert successful") diff --git a/third_party/ascend/unittest/pytest_ut/test_pow.py b/third_party/ascend/unittest/pytest_ut/test_pow.py new file mode 100644 index 0000000000..310aa9afa7 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_pow.py @@ -0,0 +1,121 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +from triton.language.extra.cann.libdevice import pow +import torch +import torch_npu +import pytest +import test_common + +shapes = [ + # 3, + # 32, + 37, + # 256, + # 781, +] + + +@pytest.mark.parametrize("sigtype", ["float32"]) +@pytest.mark.parametrize("N", shapes) +def test_pow_vv(sigtype, N): + + def torch_func(x0, x1): + res = torch.pow(x0, x1) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, in_ptr1, N: tl.constexpr): + idx = tl.arange(0, N) + x0 = tl.load(in_ptr0 + idx) + x1 = tl.load(in_ptr1 + idx) + ret = pow(x0, x1) + tl.store(out_ptr0 + idx, ret) + + def triton_func(x0, x1, N): + out = torch.empty_like(x0) + triton_kernel[1, 1, 1](out, x0, x1, N) + return out + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x1 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + + triton_cal = triton_func(x0, x1, N) + torch_ref = torch_func(x0, x1) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.parametrize("sigtype", ["float32"]) +@pytest.mark.parametrize("N", shapes) +def test_pow_vs_dynamic(sigtype, N): + + def torch_func(x0, x1): + res = torch.pow(x0, x1) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, in_ptr1, N: tl.constexpr): + idx = tl.arange(0, N) + x0 = tl.load(in_ptr0 + idx) + x1 = tl.load(in_ptr1) + ret = pow(x0, x1) + tl.store(out_ptr0 + idx, ret) + + def triton_func(x0, x1, N): + out = torch.empty_like(x0) + triton_kernel[1, 1, 1](out, x0, x1, N) + return out + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x1 = test_common.generate_tensor(shape=(1, ), dtype=sigtype).npu() + + triton_cal = triton_func(x0, x1, N) + torch_ref = torch_func(x0, x1) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.parametrize("sigtype", ["float32"]) +@pytest.mark.parametrize("N", shapes) +def test_pow_vs_const(sigtype, N): + + def torch_func(x0, x1): + res = torch.pow(x0, x1) + return res + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, x1: tl.constexpr, N: tl.constexpr): + idx = tl.arange(0, N) + x0 = tl.load(in_ptr0 + idx) + ret = pow(x0, x1) + tl.store(out_ptr0 + idx, ret) + + def triton_func(x0, x1, N): + out = torch.empty_like(x0) + triton_kernel[1, 1, 1](out, x0, x1, N) + return out + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x1 = test_common.generate_tensor(shape=(1, ), dtype=sigtype).item() + + triton_cal = triton_func(x0, x1, N) + torch_ref = torch_func(x0, x1) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_precise_div.py b/third_party/ascend/unittest/pytest_ut/test_precise_div.py new file mode 100644 index 0000000000..c40afb552e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_precise_div.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl + +import torch +import torch_npu +import test_common + + +def torch_divRn(x0, x1): + return x0 / x1 + + +@triton.jit +def triton_divRn(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tl.div_rn(tmp0, tmp1) + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 32, 2048, 64], +]) +def test_divRn(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype) + x2 = x1.masked_fill(x1 == 0, 1) + x2 = x2.npu() + y_ref = torch_divRn(x0, x2) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_divRn[ncore, 1, 1](x0, x2, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_precise_sqrt.py b/third_party/ascend/unittest/pytest_ut/test_precise_sqrt.py new file mode 100644 index 0000000000..1aaa97cceb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_precise_sqrt.py @@ -0,0 +1,55 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl + +torch.set_printoptions(precision=10) + + +@triton.jit +def sqrtrn_kernel(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + id = tl.program_id(axis=0) + start = id * BLOCK_SIZE + offsets = start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + + output = x + tl.sqrt_rn(y) + tl.store(output_ptr + offsets, output, mask=mask) + + +def sqrtrn(x: torch.Tensor, y: torch.Tensor): + output = torch.empty_like(y) + grid = lambda meta: (triton.cdiv(output.numel(), meta['BLOCK_SIZE']), ) + sqrtrn_kernel[grid](x, y, output, output.numel(), BLOCK_SIZE=512) + return output + + +def test_sqrtrn_fp32(): + size = 10240 + x = torch.abs(torch.randn(size, device='npu', dtype=torch.float32)) + y = torch.abs(torch.randn(size, device='npu', dtype=torch.float32)) + ref = x + torch.sqrt(y) + cal = sqrtrn(x, y) + torch.testing.assert_close(cal, ref, rtol=1e-06, atol=1e-06, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_ptr_add.py b/third_party/ascend/unittest/pytest_ut/test_ptr_add.py new file mode 100644 index 0000000000..bb9f685640 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ptr_add.py @@ -0,0 +1,64 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest + + +def ptr_add(device): + + @triton.jit + def wrap_stacked(a_ptr, c_ptr, M: tl.constexpr, N: tl.constexpr, stride_am: tl.constexpr, stride_an: tl.constexpr, + stride_cm: tl.constexpr, stride_cn: tl.constexpr, BLOCK_SIZE_K: tl.constexpr): + offs_am = (2 + tl.arange(0, 4)) % M + offs_an = tl.arange(0, 4) + a_ptrs = a_ptr + (offs_am[:, None] * stride_am + offs_an[None, :] * stride_an) + + offs_cm = tl.arange(0, 4) + offs_cn = tl.arange(0, 4) + c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :] + for k in range(0, 2): + a = tl.load(a_ptrs) + tl.store(c_ptrs, a) + a_ptrs += BLOCK_SIZE_K * stride_an + c_ptrs += BLOCK_SIZE_K * stride_an + + M = 4 + N = 8 + AA = torch.arange(0, M * N, dtype=torch.float32).npu() + A = AA.reshape((M, N)) + out = torch.full((M, N), 88888, dtype=torch.float32).npu() + grid = lambda meta: (1, ) + + wrap_stacked[grid](A, out, M, N, A.stride(0), A.stride(1), out.stride(0), out.stride(1), BLOCK_SIZE_K=4) + + # Expected output copied from running triton on NPU + expected_out = torch.tensor([[16, 17, 18, 19, 20, 21, 22, 23], [24, 25, 26, 27, 28, 29, 30, 31], + [0, 1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14, 15]], ).npu() + + assert torch.equal(expected_out.int(), out.int()) + + +def test_ptr_add(): + device = 'npu' + ptr_add(device) diff --git a/third_party/ascend/unittest/pytest_ut/test_rand.py b/third_party/ascend/unittest/pytest_ut/test_rand.py new file mode 100644 index 0000000000..0e6becea6f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_rand.py @@ -0,0 +1,88 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import test_common +import math + + +@triton.jit +def kernel_rand(x_ptr, n_rounds: tl.constexpr, N: tl.constexpr, XBLOCK: tl.constexpr): + block_offset = tl.program_id(0) * XBLOCK + block_size = XBLOCK if block_offset + XBLOCK <= N else N - block_offset + for inner_idx in range(block_size): + global_offset = block_offset + inner_idx + rand_vals = tl.rand(5, 10 + global_offset, n_rounds) # 对每个索引生成一个随机数 + tl.store(x_ptr + global_offset, rand_vals) # 存储随机数 + + +@triton.jit +def kernel_randn(x_ptr, n_rounds: tl.constexpr, N: tl.constexpr, XBLOCK: tl.constexpr): + block_offset = tl.program_id(0) * XBLOCK + block_size = XBLOCK if block_offset + XBLOCK <= N else N - block_offset + for inner_idx in range(block_size): + global_offset = block_offset + inner_idx + rand_vals = tl.randn(5, 10 + global_offset, n_rounds) # 对每个索引生成一个随机数 + tl.store(x_ptr + global_offset, rand_vals) # 存储随机数 + + +@triton.jit +def kernel_randint(x_ptr, n_rounds: tl.constexpr, N: tl.constexpr, XBLOCK: tl.constexpr): + block_offset = tl.program_id(0) * XBLOCK + block_size = XBLOCK if block_offset + XBLOCK <= N else N - block_offset + for inner_idx in range(block_size): + global_offset = block_offset + inner_idx + rand_vals = tl.randint(5, 10 + global_offset, n_rounds) # 对每个索引生成一个随机数 + tl.store(x_ptr + global_offset, rand_vals.to(tl.int32)) # 存储随机数(store 不支持 uint32) + + +@triton.jit +def kernel_randint4x(x_ptr, n_rounds: tl.constexpr, N: tl.constexpr, XBLOCK: tl.constexpr): + block_offset = tl.program_id(0) * XBLOCK + indices = tl.arange(0, 4) + block_size = XBLOCK if block_offset + XBLOCK <= N else N - block_offset + for inner_idx in range(0, block_size, step=4): + global_offset = block_offset + inner_idx + rand_vals, _, _, _ = tl.randint4x(5, 10 + global_offset, n_rounds) # 对每个索引生成一个随机数 + mask = (global_offset + indices) < N + tl.store(x_ptr + global_offset + indices, rand_vals.to(tl.int32), mask) # 存储随机数 + + +shapes = [(1, 3)] + + +@pytest.mark.parametrize('shape', shapes) +def test_case(shape): + y_calf = torch.zeros(shape, dtype=eval('torch.float32')).npu() + + numel = y_calf.numel() + ncore = 1 if numel < 32 else 32 + xblock = math.ceil(numel / ncore) + + kernel_rand[ncore, 1, 1](y_calf, 10, numel, xblock) + kernel_randn[ncore, 1, 1](y_calf, 10, numel, xblock) + + y_cali = torch.zeros(shape, dtype=eval('torch.int32')).npu() + + kernel_randint[ncore, 1, 1](y_cali, 10, numel, xblock) + kernel_randint4x[ncore, 1, 1](y_cali, 10, numel, xblock) diff --git a/third_party/ascend/unittest/pytest_ut/test_range.py b/third_party/ascend/unittest/pytest_ut/test_range.py new file mode 100644 index 0000000000..2544f77a39 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_range.py @@ -0,0 +1,89 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +@triton.jit +def triton_add(in_ptr0, in_ptr1, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + idx = lblk_idx[:, None, None] * N * M + mblk_idx[None, :, None] * N + nblk_idx[None, None, :] + x0 = tl.load(in_ptr0 + idx) + x1 = tl.load(in_ptr1 + idx) + ret = x0 + x1 + for i in tl.range(2, 5, 2): + ret = ret + x1 + for i in tl.static_range(2, 10, 3): + ret = ret + x0 + odx = lblk_idx[:, None, None] * N * M + mblk_idx[None, :, None] * N + nblk_idx[None, None, :] + tl.store(out_ptr0 + odx, ret) + + +testlist = [ + (3, 5, 8), +] + + +def get_torch_typename(dtype): + if dtype == 'float32': + tyname = torch.float32 + elif dtype == 'int32': + tyname = torch.int32 + elif dtype == 'int64': + tyname = torch.int64 + elif dtype == 'float16': + tyname = torch.float16 + elif dtype == 'bfloat16': + tyname = torch.bfloat16 + elif dtype == 'int16': + tyname = torch.int16 + elif dtype == 'int8': + tyname = torch.int8 + elif dtype == 'bool': + tyname = torch.bool + else: + raise ValueError('Invalid parameter \"dtype\" is found : {}'.format(dtype)) + return tyname + + +typelist = ['int8', 'int16', 'int32', 'int64'] + + +@pytest.mark.parametrize('L, M, N', testlist) +@pytest.mark.parametrize('sigtype', typelist) +def test_add(sigtype, L, M, N): + dtype = get_torch_typename(sigtype) + shape = (L, M, N) + x0 = test_common.generate_tensor(shape=(L, M, N), dtype=sigtype).npu() + x1 = test_common.generate_tensor(shape=(L, M, N), dtype=sigtype).npu() + y_ref = x0 + x1 + x1 + x1 + x0 + x0 + x0 + output = torch.zeros(shape, dtype=dtype).npu() + triton_add[1, 1, 1](x0, x1, output, L, M, N) + test_common.validate_cmp(sigtype, output, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_ravel.py b/third_party/ascend/unittest/pytest_ut/test_ravel.py new file mode 100644 index 0000000000..4d41dd7019 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_ravel.py @@ -0,0 +1,71 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr, can_reorder: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + + ret = tl.ravel(X, can_reorder=can_reorder) + + oidx = tl.arange(0, XB * YB * ZB) + tl.store(output_ptr + oidx, ret) + + +testlist = [ + ('float32', torch.float32, 2, 256, 16), + ('float32', torch.float32, 8, 8, 4), + ('float16', torch.float16, 2, 256, 16), + ('float16', torch.float16, 8, 8, 4), + ('int8', torch.int8, 2, 256, 16), + ('int8', torch.int8, 8, 8, 4), +] + + +@pytest.mark.parametrize('sigtype, dtype, XB, YB, ZB', testlist) +@pytest.mark.parametrize('can_reorder', [False, True]) +def test_ravel(sigtype, dtype, XB, YB, ZB, can_reorder): + + x = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=dtype).npu() + ans = torch.ravel(x) + + print(ans[0:16]) + + output = torch.randint(1, (XB * YB * ZB, ), dtype=dtype).npu() + + fn_npu_[1, 1, 1](output, x, XB, YB, ZB, can_reorder) + + print(output[0:16]) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_reduce_count_vector.py b/third_party/ascend/unittest/pytest_ut/test_reduce_count_vector.py new file mode 100644 index 0000000000..b4ebd2df46 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_reduce_count_vector.py @@ -0,0 +1,151 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time +import test_common + +import torch +import torch_npu + + +def standard_count(x0, cmp_val, dim): + res = (x0 == cmp_val).sum(dim=dim) + return res + + +def standard_gt(x0, cmp_val, dim): + res = (x0 > cmp_val).sum(dim=dim) + return res + + +def standard_lt(x0, cmp_val, dim): + res = (x0 < cmp_val).sum(dim=dim) + return res + + +@triton.jit +def triton_count(in_ptr0, out_ptr0, cmp_val, dim: tl.constexpr, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, N) + x = tl.load(in_ptr0 + idx_block) + + tmp3 = (x == cmp_val) + # tmp3 bool -> tl.float32 + tmp4 = tmp3.to(tl.float32) + res = tl.sum(tmp4, dim) + + tl.store(out_ptr0 + idx_block, res) + + +@triton.jit +def triton_gt(in_ptr0, out_ptr0, cmp_val, dim: tl.constexpr, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, N) + x = tl.load(in_ptr0 + idx_block) + + tmp3 = (x > cmp_val) + # tmp3 bool -> tl.float32 + tmp4 = tmp3.to(tl.float32) + res = tl.sum(tmp4, dim) + + tl.store(out_ptr0 + idx_block, res) + + +@triton.jit +def triton_lt(in_ptr0, out_ptr0, cmp_val, dim: tl.constexpr, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, N) + x = tl.load(in_ptr0 + idx_block) + + tmp3 = (x < cmp_val) + # tmp3 bool -> tl.float32 + tmp4 = tmp3.to(tl.float32) + res = tl.sum(tmp4, dim) + + tl.store(out_ptr0 + idx_block, res) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + (torch.bfloat16, 'bfloat16'), + (torch.int8, 'int8'), + (torch.int16, 'int16'), + (torch.int32, 'int32'), + (torch.int64, 'int64'), +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (32, 32), +] + +map_for_64_t = {37: 31} + +CPM_VAL_INT = 8 +CPM_VAL_FLOAT = 0.5 + +# TO BE FIXED with mask +ops = [ + ('counti', triton_count, standard_count, CPM_VAL_INT), + ('countf', triton_gt, standard_gt, CPM_VAL_FLOAT), + ('countf', triton_lt, standard_lt, CPM_VAL_FLOAT), +] + + +def judge_continue(opName, sigtype): + if opName == 'counti' and 'int' in sigtype: + return False + if opName == 'countf' and 'float' in sigtype: + return False + return True + + +@pytest.mark.parametrize('opName, tritonOp, standOp, cmp_val', ops) +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_reduce_count_vector(opName, tritonOp, standOp, cmp_val, dtype, sigtype, N, NUMEL): + if judge_continue(opName, sigtype): + return + torch.manual_seed(0) + torch_npu.npu.utils.set_device(0) + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + N = map_for_64_t[N] if N in map_for_64_t else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + ans = standOp(x0, cmp_val, 0) + x0 = x0.npu() + + output = torch.tensor(0, dtype=torch.float32).npu() + tritonOp[1, 1, 1](x0, output, cmp_val, dim=0, N=N, NUMEL=NUMEL, debug=True) + output = output.cpu().to(torch.int32) + # print(f'x0:{x0}\ntriton:{output}\ntorch:{ans}') + assert torch.equal(output, ans) + + +if __name__ == "__main__": + dtype = torch.float32 + sigtype = 'float32' + allshape = [(3, 32)] + for shape in allshape: + test_reduce_count_vector('countf', triton_lt, standard_lt, CPM_VAL_FLOAT, dtype, sigtype, shape[0], shape[1]) diff --git a/third_party/ascend/unittest/pytest_ut/test_reduce_maximum.py b/third_party/ascend/unittest/pytest_ut/test_reduce_maximum.py new file mode 100644 index 0000000000..ec29ccbbfe --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_reduce_maximum.py @@ -0,0 +1,308 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def maximum(a, b): + ret = tl.maximum(a, b, tl.PropagateNan.ALL) + # 经过测试发现,tl.maximum仅在输入类型为bfloat16时,输出的结果会转变为float32,从而导致编译报错。在GPU上测试发现,和NPU上错误的现象一致。 + # 因此此处针对输入类型为bfloat16的情况,对输出进行了类型转换来规避该错误引起的编译报错。 + if a.dtype == tl.bfloat16: + ret = ret.to(tl.bfloat16) + return ret + + +@triton.jit +def triton_max_5d_dim024(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + idx = lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + \ + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + zblk_idx[ + None, None, None, None, :] + odx = mblk_idx[:, None] * K + kblk_idx[None, :] + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 4, maximum) + ret1 = tl.reduce(ret, 2, maximum) + ret2 = tl.reduce(ret1, 0, maximum) + tl.store(out_ptr0 + odx, ret2) + + +@triton.jit +def triton_max_5d_dim13(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None] * N * Z + nblk_idx[None, :, None] * Z + zblk_idx[None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret_k = tl.reduce(x, 3, maximum) # [L, M, N, Z] + ret_m = tl.reduce(ret_k, 1, maximum) # [L, N, Z] + tl.store(out_ptr0 + odx, ret_m) + + +@triton.jit +def triton_max_5d_dim0(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (mblk_idx[:, None, None, None] * N * K * Z + nblk_idx[None, :, None, None] * K * Z + + kblk_idx[None, None, :, None] * Z + zblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 0, maximum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_max_5d_dim1(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None, None] * N * K * Z + nblk_idx[None, :, None, None] * K * Z + + kblk_idx[None, None, :, None] * Z + zblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 1, maximum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_max_5d_dim2(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None, None] * M * K * Z + mblk_idx[None, :, None, None] * K * Z + + kblk_idx[None, None, :, None] * Z + zblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 2, maximum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_max_5d_dim3(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None, None] * M * N * Z + mblk_idx[None, :, None, None] * N * Z + + nblk_idx[None, None, :, None] * Z + zblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 3, maximum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_max_5d_dim4(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None, None] * M * N * K + mblk_idx[None, :, None, None] * N * K + + nblk_idx[None, None, :, None] * K + kblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 4, maximum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_max_5d_all(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret1 = tl.reduce(x, 4, maximum) + ret2 = tl.reduce(ret1, 3, maximum) + ret3 = tl.reduce(ret2, 2, maximum) + ret4 = tl.reduce(ret3, 1, maximum) + ret5 = tl.reduce(ret4, 0, maximum) + tl.store(out_ptr0, ret5) + + +testlist = [ + (triton_max_5d_dim024, (1, 1, 1, 1, 1), "dim024"), + (triton_max_5d_dim024, (2, 2, 2, 2, 2), "dim024"), + (triton_max_5d_dim024, (3, 11, 1, 3, 42), "dim024"), + (triton_max_5d_dim13, (1, 1, 1, 1, 1024), "dim13"), + (triton_max_5d_dim0, (2, 2, 2, 2, 2), "dim0"), + (triton_max_5d_dim1, (2, 2, 2, 2, 2), "dim1"), + (triton_max_5d_dim2, (2, 2, 2, 2, 2), "dim2"), + (triton_max_5d_dim3, (2, 2, 2, 2, 2), "dim3"), + (triton_max_5d_dim4, (2, 2, 2, 2, 2), "dim4"), + (triton_max_5d_all, (3, 11, 1, 3, 42), "all"), +] + +typelist = ['int8', 'int16', 'int32', 'int64', 'float16', 'float32', 'bfloat16', 'bool'] + +ids = [ + "{}-{}-{}".format(testfunc.__name__, "-".join(map(str, shape)), dim_name) for testfunc, shape, dim_name in testlist +] + + +@pytest.mark.parametrize('testfunc, shape, dim_name', testlist, ids=ids) +@pytest.mark.parametrize('dtype', typelist) +def test_max(testfunc, dtype, shape, dim_name): + x0 = test_common.generate_tensor(shape=shape, dtype=dtype).npu() + + if dim_name == "dim024": + if 'int' in dtype: + ans, _ = torch.max(x0.to(torch.int64), 4) + ans, _ = torch.max(ans.to(torch.int64), 2) + ans, _ = torch.max(ans.to(torch.int64), 0) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.max(x0, 4) + ans, _ = torch.max(ans, 2) + ans, _ = torch.max(ans, 0) + output = torch.zeros((shape[1], ) + (shape[3], ), dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim13": + if 'int' in dtype: + ans, _ = torch.max(x0.to(torch.int64), 3) + ans, _ = torch.max(ans.to(torch.int64), 1) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.max(x0, 3) + ans, _ = torch.max(ans, 1) + output = torch.zeros((shape[0], ) + (shape[2], ) + (shape[4], ), dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim0": + if 'int' in dtype: + ans, _ = torch.max(x0.to(torch.int64), 0) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.max(x0, 0) + output = torch.zeros((shape[1], ) + (shape[2], ) + (shape[3], ) + (shape[4], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim1": + if 'int' in dtype: + ans, _ = torch.max(x0.to(torch.int64), 1) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.max(x0, 1) + output = torch.zeros((shape[0], ) + (shape[2], ) + (shape[3], ) + (shape[4], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim2": + if 'int' in dtype: + ans, _ = torch.max(x0.to(torch.int64), 2) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.max(x0, 2) + output = torch.zeros((shape[0], ) + (shape[1], ) + (shape[3], ) + (shape[4], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim3": + if 'int' in dtype: + ans, _ = torch.max(x0.to(torch.int64), 3) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.max(x0, 3) + output = torch.zeros((shape[0], ) + (shape[1], ) + (shape[2], ) + (shape[4], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim4": + if 'int' in dtype: + ans, _ = torch.max(x0.to(torch.int64), 4) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.max(x0, 4) + output = torch.zeros((shape[0], ) + (shape[1], ) + (shape[2], ) + (shape[3], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "all": + if 'int' in dtype: + ans = torch.max(x0.to(torch.int64)) + ans = torch.tensor([ans], dtype=eval('torch.' + dtype)) + else: + ans = torch.tensor([torch.max(x0)], dtype=eval('torch.' + dtype)) + output = torch.zeros((1, ), dtype=eval('torch.' + dtype)).npu() + + testfunc[(1, )](x0, output, *shape) + + test_common.validate_cmp(dtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_reduce_mean.py b/third_party/ascend/unittest/pytest_ut/test_reduce_mean.py new file mode 100644 index 0000000000..f22cd47c39 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_reduce_mean.py @@ -0,0 +1,89 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common +import numpy as np + + +def numpy_mean_pr(x0, x1): + res = np.mean(x0, axis=-1) + x1 + return res + + +@triton.jit +def triton_mean_pr(out_ptr0, in_ptr0, in_ptr1, xnumel, rnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr, + RBLOCK: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + rindex = tl.arange(0, RBLOCK)[None, :] + rmask = rindex < rnumel + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB) + xmask = xindex[:, None] < xnumel + x0 = xindex + r1 = rindex + tmp0 = tl.load(in_ptr0 + (r1 + (RBLOCK * x0[:, None])), xmask & rmask) + tmp4 = tl.load(in_ptr1 + (x0), xindex < xnumel) + tmp1 = tl.reshape(tmp0, [XBLOCK_SUB, RBLOCK]) + tmp3 = tl.sum(tmp1, 1) / RBLOCK + tmp5 = tmp3 + tmp4 + tl.store(out_ptr0 + (xindex), tmp5, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (8, 8, 4), 8, 2], + ['float32', (8, 8, 64), 8, 2], + ['float32', (8, 8, 1024), 8, 2], + ['float16', (8, 8, 4), 8, 2], + ['float16', (8, 8, 64), 8, 2], + ['float16', (8, 8, 1024), 8, 2], + ['int8', (8, 8, 4), 8, 2], + ['int8', (8, 8, 64), 8, 2], + pytest.param( + ['int8', (8, 8, 1024), 8, 2], + marks=pytest.mark.skip( + reason="tl.sum does not support int8 reduction with RBLOCK=1024, and will be fixed later"), + ), +]) +def test_mean_pr(param_list): + dtype, shape, ncore, xblock_sub = param_list + import math + numel = math.prod(shape) + xblock = numel // shape[-1] // ncore + rblock = shape[-1] + assert (ncore * xblock * shape[-1] == numel) + xn1 = np.random.randn(shape[0], shape[1], shape[2]).astype(eval('np.' + dtype)) + xn2 = np.random.randn(shape[0], shape[1]).astype(eval('np.' + dtype)) + x0 = torch.tensor(xn1).npu() + x1 = torch.tensor(xn2).npu() + y_ref = numpy_mean_pr(xn1, xn2) + if dtype == 'int8': + y_cal = test_common.generate_tensor(shape[:-1], 'float32').npu() + else: + y_cal = test_common.generate_tensor(shape[:-1], dtype).npu() + triton_mean_pr[ncore, 1, 1](y_cal, x0, x1, x1.numel(), rblock, xblock, xblock_sub, rblock) + if dtype == 'int8': + torch.allclose(torch.tensor(y_ref.astype(np.float32)).npu(), y_cal, rtol=1e-03, atol=1e-03, equal_nan=True) + else: + torch.allclose(torch.tensor(y_ref).npu(), y_cal, rtol=1e-03, atol=1e-03, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_reduce_min_4_keepdim_True_with_index_op.py b/third_party/ascend/unittest/pytest_ut/test_reduce_min_4_keepdim_True_with_index_op.py new file mode 100644 index 0000000000..26ffbe0c51 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_reduce_min_4_keepdim_True_with_index_op.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +import time + +import pytest +import torch +import torch_npu + +import triton +import triton.language as tl +import test_common + + +@triton.jit +def promote_to_tensor(x): + # Addition promotes to tensor for us + return x + tl.zeros((1, ), tl.int1) + + +@triton.jit +def minimum_with_index(a_value, a_index, b_value, b_index): + mask = a_value < b_value + equal = a_value == b_value + if promote_to_tensor(a_value).dtype.is_floating(): + a_isnan = a_value != a_value + b_isnan = b_value != b_value + mask |= a_isnan and not b_isnan + # Consider NaNs as equal + equal |= a_isnan and b_isnan + # Prefer lowest index if values are equal + mask |= equal & (a_index < b_index) + return tl.where(mask, a_value, b_value), tl.where(mask, a_index, b_index) + + +@triton.jit +def triton_min_5d_dim4_keepdim(in_ptr0, in_ptr1, out_ptr0, out_ptr1, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, + K: tl.constexpr, Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + idx = lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + \ + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + zblk_idx[None, None, None, None, :] + x = tl.load(in_ptr0 + idx) + x1 = tl.load(in_ptr1 + idx) + ret, ret1 = tl.reduce((x, x1), 4, minimum_with_index, keep_dims=True) + zblk_idx = tl.arange(0, 1) + odx = lblk_idx[:, None, None, None, None] * K * N * M + mblk_idx[None, :, None, None, None] * K * N + \ + nblk_idx[None, None, :, None, None] * K + kblk_idx[None, None, None, :, None] \ + + zblk_idx[None, None, None, None, :] + tl.store(out_ptr0 + odx, ret) + tl.store(out_ptr1 + odx, ret1) + + +testlist = [ + # 5D + (triton_min_5d_dim4_keepdim, (1, 1, 1, 1, 1)), + (triton_min_5d_dim4_keepdim, (2, 2, 2, 2, 2)), + (triton_min_5d_dim4_keepdim, (9, 3, 2, 4, 17)), + (triton_min_5d_dim4_keepdim, (3, 11, 1, 3, 42)), + (triton_min_5d_dim4_keepdim, (2, 51, 3, 13, 1)), + (triton_min_5d_dim4_keepdim, (129, 1, 5, 1, 4)), + (triton_min_5d_dim4_keepdim, (203, 1, 2, 2, 3)), + (triton_min_5d_dim4_keepdim, (512, 1, 1, 1, 1)), + (triton_min_5d_dim4_keepdim, (3, 1, 1, 2, 600)), + (triton_min_5d_dim4_keepdim, (1, 1, 1, 1, 1024)), + (triton_min_5d_dim4_keepdim, (15, 2, 2, 2, 54)), + (triton_min_5d_dim4_keepdim, (2, 91, 4, 2, 4)), + (triton_min_5d_dim4_keepdim, (1, 1, 3, 2, 600)), + (triton_min_5d_dim4_keepdim, (5, 2, 4, 1, 26)), + (triton_min_5d_dim4_keepdim, (2, 2, 2, 4, 8)), +] + +typelist = ['int8', 'int16', 'int32', 'int64', 'float16', 'float32', 'bfloat16', 'bool'] + +ids = ["{}-{}".format(testfunc.__name__, "-".join(map(str, shape))) for testfunc, shape in testlist] + + +@pytest.mark.parametrize('testfunc, shape', testlist, ids=ids) +@pytest.mark.parametrize('sigtype', typelist) +def test_min_dim4_keepdim(testfunc, sigtype, shape): + dtype = eval('torch.' + sigtype) + x0 = torch.randn(shape).to(dtype).npu() + + x1 = torch.arange(x0.numel()).view(x0.shape).npu().to(torch.int32) + if 'int' in sigtype: + ans, ans1 = torch.min(x0.to(torch.int64), 4) + ans = ans.to(dtype) + else: + ans, ans1 = torch.min(x0, 4) + output = torch.zeros(shape[0:4], dtype=dtype).npu() + output1 = torch.zeros(shape[0:4], dtype=torch.int32).npu() + testfunc[(1, )](x0, x1, output, output1, *shape, debug=True) + test_common.validate_cmp(sigtype, output, ans) + test_common.validate_cmp('int32', output1, ans1) diff --git a/third_party/ascend/unittest/pytest_ut/test_reduce_minimum.py b/third_party/ascend/unittest/pytest_ut/test_reduce_minimum.py new file mode 100644 index 0000000000..f472eef67e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_reduce_minimum.py @@ -0,0 +1,308 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def minimum(a, b): + ret = tl.minimum(a, b, tl.PropagateNan.ALL) + # 经过测试发现,tl.minimum仅在输入类型为bfloat16时,输出的结果会转变为float32,从而导致编译报错。在GPU上测试发现,和NPU上错误的现象一致。 + # 因此此处针对输入类型为bfloat16的情况,对输出进行了类型转换来规避该错误引起的编译报错。 + if a.dtype == tl.bfloat16: + ret = ret.to(tl.bfloat16) + return ret + + +@triton.jit +def triton_min_5d_dim024(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + idx = lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + \ + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + zblk_idx[ + None, None, None, None, :] + odx = mblk_idx[:, None] * K + kblk_idx[None, :] + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 4, minimum) + ret1 = tl.reduce(ret, 2, minimum) + ret2 = tl.reduce(ret1, 0, minimum) + tl.store(out_ptr0 + odx, ret2) + + +@triton.jit +def triton_min_5d_dim13(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None] * N * Z + nblk_idx[None, :, None] * Z + zblk_idx[None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret_k = tl.reduce(x, 3, minimum) # [L, M, N, Z] + ret_m = tl.reduce(ret_k, 1, minimum) # [L, N, Z] + tl.store(out_ptr0 + odx, ret_m) + + +@triton.jit +def triton_min_5d_dim0(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (mblk_idx[:, None, None, None] * N * K * Z + nblk_idx[None, :, None, None] * K * Z + + kblk_idx[None, None, :, None] * Z + zblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 0, minimum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_min_5d_dim1(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None, None] * N * K * Z + nblk_idx[None, :, None, None] * K * Z + + kblk_idx[None, None, :, None] * Z + zblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 1, minimum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_min_5d_dim2(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None, None] * M * K * Z + mblk_idx[None, :, None, None] * K * Z + + kblk_idx[None, None, :, None] * Z + zblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 2, minimum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_min_5d_dim3(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None, None] * M * N * Z + mblk_idx[None, :, None, None] * N * Z + + nblk_idx[None, None, :, None] * Z + zblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 3, minimum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_min_5d_dim4(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + odx = (lblk_idx[:, None, None, None] * M * N * K + mblk_idx[None, :, None, None] * N * K + + nblk_idx[None, None, :, None] * K + kblk_idx[None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret = tl.reduce(x, 4, minimum) + tl.store(out_ptr0 + odx, ret) + + +@triton.jit +def triton_min_5d_all(in_ptr0, out_ptr0, L: tl.constexpr, M: tl.constexpr, N: tl.constexpr, K: tl.constexpr, + Z: tl.constexpr): + lblk_idx = tl.arange(0, L) + mblk_idx = tl.arange(0, M) + nblk_idx = tl.arange(0, N) + kblk_idx = tl.arange(0, K) + zblk_idx = tl.arange(0, Z) + + idx = (lblk_idx[:, None, None, None, None] * Z * K * N * M + mblk_idx[None, :, None, None, None] * Z * K * N + + nblk_idx[None, None, :, None, None] * Z * K + kblk_idx[None, None, None, :, None] * Z + + zblk_idx[None, None, None, None, :]) + + x = tl.load(in_ptr0 + idx) + ret1 = tl.reduce(x, 4, minimum) + ret2 = tl.reduce(ret1, 3, minimum) + ret3 = tl.reduce(ret2, 2, minimum) + ret4 = tl.reduce(ret3, 1, minimum) + ret5 = tl.reduce(ret4, 0, minimum) + tl.store(out_ptr0, ret5) + + +testlist = [ + (triton_min_5d_dim024, (1, 1, 1, 1, 1), "dim024"), + (triton_min_5d_dim024, (2, 2, 2, 2, 2), "dim024"), + (triton_min_5d_dim024, (3, 11, 1, 3, 42), "dim024"), + (triton_min_5d_dim13, (1, 1, 1, 1, 1024), "dim13"), + (triton_min_5d_dim0, (2, 2, 2, 2, 2), "dim0"), + (triton_min_5d_dim1, (2, 2, 2, 2, 2), "dim1"), + (triton_min_5d_dim2, (2, 2, 2, 2, 2), "dim2"), + (triton_min_5d_dim3, (2, 2, 2, 2, 2), "dim3"), + (triton_min_5d_dim4, (2, 2, 2, 2, 2), "dim4"), + (triton_min_5d_all, (3, 11, 1, 3, 42), "all"), +] + +typelist = ['int8', 'int16', 'int32', 'int64', 'float16', 'float32', 'bfloat16', 'bool'] + +ids = [ + "{}-{}-{}".format(testfunc.__name__, "-".join(map(str, shape)), dim_name) for testfunc, shape, dim_name in testlist +] + + +@pytest.mark.parametrize('testfunc, shape, dim_name', testlist, ids=ids) +@pytest.mark.parametrize('dtype', typelist) +def test_min(testfunc, dtype, shape, dim_name): + x0 = test_common.generate_tensor(shape=shape, dtype=dtype).npu() + + if dim_name == "dim024": + if 'int' in dtype: + ans, _ = torch.min(x0.to(torch.int64), 4) + ans, _ = torch.min(ans.to(torch.int64), 2) + ans, _ = torch.min(ans.to(torch.int64), 0) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.min(x0, 4) + ans, _ = torch.min(ans, 2) + ans, _ = torch.min(ans, 0) + output = torch.zeros((shape[1], ) + (shape[3], ), dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim13": + if 'int' in dtype: + ans, _ = torch.min(x0.to(torch.int64), 3) + ans, _ = torch.min(ans.to(torch.int64), 1) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.min(x0, 3) + ans, _ = torch.min(ans, 1) + output = torch.zeros((shape[0], ) + (shape[2], ) + (shape[4], ), dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim0": + if 'int' in dtype: + ans, _ = torch.min(x0.to(torch.int64), 0) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.min(x0, 0) + output = torch.zeros((shape[1], ) + (shape[2], ) + (shape[3], ) + (shape[4], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim1": + if 'int' in dtype: + ans, _ = torch.min(x0.to(torch.int64), 1) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.min(x0, 1) + output = torch.zeros((shape[0], ) + (shape[2], ) + (shape[3], ) + (shape[4], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim2": + if 'int' in dtype: + ans, _ = torch.min(x0.to(torch.int64), 2) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.min(x0, 2) + output = torch.zeros((shape[0], ) + (shape[1], ) + (shape[3], ) + (shape[4], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim3": + if 'int' in dtype: + ans, _ = torch.min(x0.to(torch.int64), 3) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.min(x0, 3) + output = torch.zeros((shape[0], ) + (shape[1], ) + (shape[2], ) + (shape[4], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "dim4": + if 'int' in dtype: + ans, _ = torch.min(x0.to(torch.int64), 4) + ans = ans.to(dtype=eval('torch.' + dtype)) + else: + ans, _ = torch.min(x0, 4) + output = torch.zeros((shape[0], ) + (shape[1], ) + (shape[2], ) + (shape[3], ), + dtype=eval('torch.' + dtype)).npu() + + elif dim_name == "all": + if 'int' in dtype: + ans = torch.min(x0.to(torch.int64)) + ans = torch.tensor([ans], dtype=eval('torch.' + dtype)) + else: + ans = torch.tensor([torch.min(x0)], dtype=eval('torch.' + dtype)) + output = torch.zeros((1, ), dtype=eval('torch.' + dtype)).npu() + + testfunc[(1, )](x0, output, *shape) + + test_common.validate_cmp(dtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_reduce_sum.py b/third_party/ascend/unittest/pytest_ut/test_reduce_sum.py new file mode 100644 index 0000000000..a435124417 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_reduce_sum.py @@ -0,0 +1,90 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import numpy as np + +import os +import sys + +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.append(parent_dir) +import test_common + + +# PR: Pointiwise-Reduction pattern, reduction in last axis +def numpy_sum_pr(x0, x1): + res = np.sum(x0, axis=-1) + x1 + return res + + +@triton.jit +def triton_sum_pr(out_ptr0, in_ptr0, in_ptr1, xnumel, rnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr, + RBLOCK: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + rindex = tl.arange(0, RBLOCK)[None, :] + rmask = rindex < rnumel + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB) + xmask = xindex[:, None] < xnumel + x0 = xindex + r1 = rindex + tmp0 = tl.load(in_ptr0 + (r1 + (RBLOCK * x0[:, None])), xmask & rmask) + tmp4 = tl.load(in_ptr1 + (x0), xindex < xnumel) + tmp1 = tl.reshape(tmp0, [XBLOCK_SUB, RBLOCK]) + tmp3 = tl.sum(tmp1, 1) + tmp5 = tmp3 + tmp4 + tl.store(out_ptr0 + (xindex), tmp5, None) + + +# fp16 use numpy +@pytest.mark.parametrize('param_list', [ + # ['float32', (8, 8, 4), 8, 2], + ['float32', (8, 8, 64), 8, 2], + ['float32', (8, 8, 512), 8, 2], + ['float16', (8, 8, 4), 8, 2], + ['float16', (8, 8, 64), 8, 2], + ['float16', (8, 8, 512), 8, 2], + ['int8', (8, 8, 4), 8, 2], + ['int8', (8, 8, 64), 8, 2], + ['int8', (8, 8, 512), 8, 2], +]) +def test_sum_pr(param_list): + dtype, shape, ncore, xblock_sub = param_list + import math + numel = math.prod(shape) + xblock = numel // shape[-1] // ncore + rblock = shape[-1] + assert (ncore * xblock * shape[-1] == numel) + xn1 = np.random.randn(shape[0], shape[1], shape[2]).astype(eval('np.' + dtype)) + xn2 = np.random.randn(shape[0], shape[1]).astype(eval('np.' + dtype)) + x0 = torch.tensor(xn1).npu() + x1 = torch.tensor(xn2).npu() + if dtype == 'int8': + y_ref = numpy_sum_pr(xn1, xn2).astype(np.int8) + else: + y_ref = numpy_sum_pr(xn1, xn2) + y_cal = test_common.generate_tensor(shape[:-1], dtype).npu() + triton_sum_pr[ncore, 1, 1](y_cal, x0, x1, x1.numel(), rblock, xblock, xblock_sub, rblock) + test_common.validate_cmp(dtype, y_cal, torch.tensor(y_ref).npu()) diff --git a/third_party/ascend/unittest/pytest_ut/test_relu.py b/third_party/ascend/unittest/pytest_ut/test_relu.py new file mode 100644 index 0000000000..63e9bd9714 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_relu.py @@ -0,0 +1,61 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import test_common +import triton.language.extra.cann.libdevice as libdevice + + +def torch_relu(x0, x1): + res = x0 + torch.relu(x1) + return res + + +@triton.jit +def triton_relu(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + x_index = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = x_index < xnumel + tmp0 = tl.load(in_ptr0 + x_index, xmask) + tmp1 = tl.load(in_ptr1 + x_index, xmask) + tmp2 = tmp0 + libdevice.relu(tmp1) + tl.store(out_ptr0 + x_index, tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], +]) +def test_relu(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + torch_res = torch_relu(x0, x1) + # triton结果 + triton_res = test_common.generate_tensor(shape, dtype).npu() + triton_relu[ncore, 1, 1](x0, x1, triton_res, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_reshape.py b/third_party/ascend/unittest/pytest_ut/test_reshape.py new file mode 100644 index 0000000000..11778d3c7d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_reshape.py @@ -0,0 +1,70 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + + ret = tl.reshape(X, (ZB, XB * YB)) + + oidx = tl.arange(0, ZB)[:, None] * XB * YB + tl.arange(0, XB * YB)[None, :] + + tl.store(output_ptr + oidx, ret) + + +testlist = [ + ('float32', torch.float32, 2, 256, 16), + ('float32', torch.float32, 8, 8, 4), + ('float16', torch.float16, 2, 256, 16), + ('float16', torch.float16, 8, 8, 4), + ('int8', torch.int8, 2, 256, 16), + ('int8', torch.int8, 8, 8, 4), +] + + +@pytest.mark.parametrize('sigtype, dtype, XB, YB, ZB', testlist) +def test_ravel(sigtype, dtype, XB, YB, ZB): + + x = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=dtype).npu() + ans = torch.reshape(x, (ZB, XB * YB)) + + print(ans[0, 0:16]) + + output = torch.randint(1, (ZB, XB * YB), dtype=dtype).npu() + + fn_npu_[1, 1, 1](output, x, XB, YB, ZB) + print(output[0, 0:16]) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_resize_performance.py b/third_party/ascend/unittest/pytest_ut/test_resize_performance.py new file mode 100644 index 0000000000..6478e2f6b5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_resize_performance.py @@ -0,0 +1,102 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch, torch_npu +import triton +import triton.language as tl +import numpy as np +import math +import pytest + + +@triton.jit +def nearest_resize_kernel_col_tile(img_src_ptr, img_dst_ptr, src_rows: tl.constexpr, src_cols: tl.constexpr, + dst_rows: tl.constexpr, dst_cols: tl.constexpr, RR_H: tl.constexpr, + RR_W: tl.constexpr, stride_in_h: tl.constexpr, stride_in_w: tl.constexpr, + stride_in_c: tl.constexpr, stride_out_h: tl.constexpr, stride_out_w: tl.constexpr, + stride_out_c: tl.constexpr, BLOCK_SIZE: tl.constexpr): + #RR_H和RR_W分别为高和宽的缩放比例 + block_id_c = tl.program_id(0) + block_id_h = tl.program_id(1) + block_id_w = tl.program_id(2) + + dest_w_offs = (block_id_w * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)) + + dest_offs = (block_id_c[None, None] * stride_out_c + block_id_h[None, None] * stride_out_h + + dest_w_offs[None, :] * stride_out_w) + + fx = dest_w_offs * RR_W + sx = tl.floor(fx) + + new_col = block_id_h * RR_H + src_offsets = (block_id_c[None, None] * stride_in_c + new_col[None, None].to(tl.int32) * stride_in_h + + tl.clamp(sx, 0, src_cols - 1)[None, :].to(tl.int32) * stride_in_w) + src_val = tl.load(img_src_ptr + src_offsets) + dst_mask = dest_w_offs[None, :] < dst_cols + tl.store(img_dst_ptr + dest_offs, src_val, mask=dst_mask) + + +def nearest_resize_cpu(img_src, img_dst, dst_rows, dst_cols): + N, C, src_rows, src_cols = img_src.shape + # 2,4 64, 32 + #RR_H和RR_W分别为高和宽的缩放比例 + RR_H = src_rows / float(dst_rows) + RR_W = src_cols / float(dst_cols) + print("RR_H RR_W", RR_H, RR_W) + # 2, 2 + #根据output image的坐标值(i,j)计算input image的坐标值(sy, sx) + for i in range(dst_rows): #32 + for j in range(dst_cols): #16 + # fy = i * 2 = 0/1/..31 * 2 = 0/2/4...62 + fy = (i * RR_H) + sy = math.floor(fy) + # fx = j * 2 = 0/1/2..14 * 2 = 0/2/4...28 + fx = (j * RR_W) + sx = math.floor(fx) + src_val = img_src[0, :, np.clip(sy, 0, src_rows - 1), np.clip(sx, 0, src_cols - 1)] + # img_dst[0, :, i, j] 表示取批量中第 0 张图像、第 i 行第 j 列位置上的所有通道像素值 + img_dst[0, :, i, j] = src_val + + +def test_nearest_resize(): + n, c, h, w = 1, 4, 64, 64 + img_src = torch.randint(0, 255, size=(n, c, h, w)) + dst_rows = h // 2 + dst_cols = w // 2 + img_dst_cpu = torch.randint(0, 255, size=(n, c, dst_rows, dst_cols)) + nearest_resize_cpu(img_src, img_dst_cpu, dst_rows, dst_cols) + + # call triton kernel + img_src = img_src.npu() + RR_H = h / float(dst_rows) + RR_W = w / float(dst_cols) + img_dst_npu = torch.randint(0, 255, size=(n, c, dst_rows, dst_cols)).npu() + stride_in_h = h + stride_in_w = 1 + stride_in_c = h * w + stride_out_h = dst_rows + stride_out_w = 1 + stride_out_c = dst_rows * dst_cols + BLOCK_SIZE = 32 + # best performance + nearest_resize_kernel_col_tile[(4, 32, 1)](img_src, img_dst_npu, h, w, dst_rows, dst_cols, RR_H, RR_W, stride_in_h, + stride_in_w, stride_in_c, stride_out_h, stride_out_w, stride_out_c, + BLOCK_SIZE) + assert torch.equal(img_dst_cpu.cpu(), img_dst_npu.cpu()) diff --git a/third_party/ascend/unittest/pytest_ut/test_rint.py b/third_party/ascend/unittest/pytest_ut/test_rint.py new file mode 100644 index 0000000000..23b1bb3f13 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_rint.py @@ -0,0 +1,65 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice + +import time + +import torch +import torch_npu +import test_common + + +@triton.jit +def triton_rint(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + for loop1 in range(0, XBLOCK, XBLOCK_SUB): + x0 = offset + loop1 + tl.arange(0, XBLOCK_SUB) + xmask = x0 < xnumel + tmp0 = tl.load(in_ptr0 + x0, mask=xmask) + tmp1 = libdevice.rint(tmp0) + tl.store(out_ptr0 + x0, tmp1, mask=xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 32, 2048, 64], +]) +def test_rint(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch.round(x) + y_cal = test_common.generate_tensor(x.shape, dtype).npu() + triton_rint[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub, debug=True) + test_common.validate_cmp_with_expection(dtype, y_cal, y_ref, True) + + +@pytest.mark.parametrize('dtype', [ + 'float32', +]) +def test_rint_half(dtype): + x0 = torch.tensor([-2.5, -1.5, -0.5, 0, 0.5, 1.5, 2.5], dtype=eval('torch.' + dtype)).npu() + y_ref = torch.round(x0) + y_cal = test_common.generate_tensor(x0.shape, dtype).npu() + triton_rint[32, 1, 1](x0, y_cal, x0.numel(), 2048, 64, debug=True) + test_common.validate_cmp_with_expection(dtype, y_cal, y_ref, True) diff --git a/third_party/ascend/unittest/pytest_ut/test_rms_norm.py b/third_party/ascend/unittest/pytest_ut/test_rms_norm.py new file mode 100644 index 0000000000..95e1baf02e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_rms_norm.py @@ -0,0 +1,142 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import triton +import triton.language as tl +import torch_npu + + +@triton.jit +def _rms_norm_fwd_fused( + X, # pointer to the input + Y, # pointer to the output + W, # pointer to the weights + stride, # how much to increase the pointer when moving by 1 row + N, # number of columns in X + eps, # epsilon to avoid division by zero + BLOCK_SIZE: tl.constexpr, +): + # Map the program id to the row of X and Y it should compute. + row = tl.program_id(0) + Y += row * stride + X += row * stride + # Compute variance + _var = tl.zeros([BLOCK_SIZE], dtype=tl.float32) + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + x = tl.load(X + cols, mask=cols < N, other=0.0).to(tl.float32) + _var += x * x + var = tl.sum(_var, axis=0) / N + rstd = 1 / tl.sqrt(var + eps) + # Normalize and apply linear transformation + for off in range(0, N, BLOCK_SIZE): + cols = off + tl.arange(0, BLOCK_SIZE) + mask = cols < N + w = tl.load(W + cols, mask=mask).to(tl.float32) + x = tl.load(X + cols, mask=mask, other=0.0).to(tl.float32) + x_hat = x * rstd + y = x_hat * w + # Write output + tl.store(Y + cols, y.to(tl.float16), mask=mask) + + +# have to change the block_size +@torch.inference_mode() +def rms_norm(x, weight, eps, out=None): + # allocate output, tl.store save y in tl.float16 + y = torch.empty_like(x, dtype=torch.float16) if out is None else out + # reshape input data into 2D tensor + x_arg = x.view(-1, x.shape[-1]) + M, N = x_arg.shape + # Less than 64KB per feature: enqueue fused kernel + MAX_FUSED_SIZE = 65536 // x.element_size() + BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) + if N > BLOCK_SIZE: + raise RuntimeError("This layer norm doesn't support feature dim >= 64KB.") + # heuristics for number of warps + num_warps = min(max(BLOCK_SIZE // 256, 1), 8) + BLOCK_SIZE = 128 * 2 * 2 * 2 * 2 * 2 * 2 + num_warps = 8 + # enqueue kernel + kernel = _rms_norm_fwd_fused[(M, )](x_arg, y, weight, x_arg.stride(0), N, eps, BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps) + return y, kernel + + +def _rms_norm(shape, datatype): + x = torch.randn(shape[0], shape[1], dtype=datatype, device="npu") + weight = torch.randn(shape[1], dtype=datatype, device="npu") + y, kernel = rms_norm(x, weight, eps=1e-5) + eps1 = 1e-5 + if datatype == torch.bfloat16 or datatype == torch.float16: + x = x.to(torch.float32) + rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + eps1) # 计算均方根 + x_norm = x / rms # 标准化 + y_ref = weight * x_norm + y_ref = y_ref.to(torch.float16) + torch.testing.assert_close(y_ref, y, rtol=1e-3, atol=1e-3) + + +def test_cases(): + _rms_norm((16, 1024), torch.float16) + _rms_norm((16, 1024), torch.float32) + _rms_norm((16, 1024), torch.bfloat16) + _rms_norm((128, 3), torch.bfloat16) + _rms_norm((128, 16), torch.bfloat16) + _rms_norm((128, 37), torch.bfloat16) + _rms_norm((128, 64), torch.bfloat16) + _rms_norm((128, 781), torch.bfloat16) + _rms_norm((128, 781), torch.bfloat16) + _rms_norm((16, 1024), torch.float16) + _rms_norm((16, 1024), torch.float32) + _rms_norm((16, 1024), torch.bfloat16) + + _rms_norm((128, 128), torch.float16) + _rms_norm((128, 128), torch.float32) + _rms_norm((128, 128), torch.bfloat16) + + _rms_norm((1, 128), torch.float16) + _rms_norm((1, 128), torch.float32) + _rms_norm((1, 128), torch.bfloat16) + + _rms_norm((65535, 128), torch.float16) + _rms_norm((65535, 128), torch.float32) + _rms_norm((65535, 128), torch.bfloat16) + + _rms_norm((128, 3), torch.float16) + _rms_norm((128, 3), torch.float32) + _rms_norm((128, 3), torch.bfloat16) + + _rms_norm((128, 16), torch.float16) + _rms_norm((128, 16), torch.float32) + _rms_norm((128, 16), torch.bfloat16) + + _rms_norm((128, 37), torch.float16) + _rms_norm((128, 37), torch.float32) + _rms_norm((128, 37), torch.bfloat16) + + _rms_norm((128, 64), torch.float16) + _rms_norm((128, 64), torch.float32) + _rms_norm((128, 64), torch.bfloat16) + + _rms_norm((128, 781), torch.float16) + _rms_norm((128, 781), torch.float32) + _rms_norm((128, 781), torch.bfloat16) diff --git a/third_party/ascend/unittest/pytest_ut/test_rotary_embedding.py b/third_party/ascend/unittest/pytest_ut/test_rotary_embedding.py new file mode 100644 index 0000000000..7a5f37c225 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_rotary_embedding.py @@ -0,0 +1,175 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Rotary embedding kernel implemented by Triton. + +GPT-NeoX style +""" + +import torch +import torch_npu +import triton +import triton.language as tl + + +@triton.jit +def rotary_embedding_kernel( + state, # [num_tokens, head_num, head_dim] + cos, # [num_tokens, 1, head_dim // 2] + sin, # [num_tokens, 1, head_dim // 2] + stride_state_n, + stride_state_h, + stride_state_d, + stride_cos_n, + stride_cos_d, + # stride_sin_n, + # stride_sin_d, + num_tokens, + num_heads, + BLOCK_N: tl.constexpr, + BLOCK_H: tl.constexpr, + BLOCK_D: tl.constexpr, +): + token_index = tl.program_id(0) + token_range = token_index * BLOCK_N + tl.arange(0, BLOCK_N) + head_index = tl.program_id(1) + head_range = head_index * BLOCK_H + tl.arange(0, BLOCK_H) + + dim_range_x = tl.arange(0, BLOCK_D // 2) + dim_range_y = tl.arange(BLOCK_D // 2, BLOCK_D) + + state_x_offset = (token_range[:, None, None] * stride_state_n + head_range[None, :, None] * stride_state_h + + dim_range_x[None, None, :] * stride_state_d) + state_y_offset = (token_range[:, None, None] * stride_state_n + head_range[None, :, None] * stride_state_h + + dim_range_y[None, None, :] * stride_state_d) + + cos_sim_offset = (token_range[:, None, None] * stride_cos_n + dim_range_x[None, None, :] * stride_cos_d) + + state_x = tl.load( + state + state_x_offset, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + other=0.0, + ) + state_y = tl.load( + state + state_y_offset, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + other=0.0, + ) + + cos_loaded = tl.load( + cos + cos_sim_offset, + mask=token_range[:, None, None] < num_tokens, + other=0.0, + ) + sin_loaded = tl.load( + sin + cos_sim_offset, + mask=token_range[:, None, None] < num_tokens, + other=0.0, + ) + + out_x = state_x * cos_loaded - state_y * sin_loaded + out_y = state_x * sin_loaded + state_y * cos_loaded + + tl.store( + state + state_x_offset, + out_x, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + ) + tl.store( + state + state_y_offset, + out_y, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + ) + + +@torch.inference_mode() +def rotary_embedding(state, cos, sin): + num_tokens = state.shape[0] + num_heads = state.shape[1] + head_dim = state.shape[2] + + #BLOCK_N = 32 + BLOCK_N = 16 + BLOCK_H = 4 + grid = ( + triton.cdiv(num_tokens, BLOCK_N), + triton.cdiv(num_heads, BLOCK_H), + ) + if head_dim >= 128: + num_warps = 8 + else: + num_warps = 4 + + kernel = rotary_embedding_kernel[grid]( + state, + cos, + sin, + state.stride(0), + state.stride(1), + state.stride(2), + cos.stride(0), + cos.stride(2), + # sin.stride(0), + # sin.stride(2), + num_tokens, + num_heads, + BLOCK_N=BLOCK_N, + BLOCK_H=BLOCK_H, + BLOCK_D=head_dim, + num_warps=num_warps, + num_stages=1, + ) + return + + +def torch_rotary_embedding(state, cos, sin): + _, _, dim = state.shape + state_x = state[:, :, 0:dim // 2] + state_y = state[:, :, dim // 2:dim] + out_x = state_x * cos - state_y * sin + out_y = state_x * sin + state_y * cos + return torch.cat((out_x, out_y), dim=-1) + + +def rotary_emb(tokens, heads, headdim, dtype): + tokens_num = tokens + num_heads = heads + head_dim = headdim + max_positions = 1024 + + # torch.float16 has floating point problem in Triton 2.0.0 + # But it works fine in Triton 2.1.0 + state = torch.randn((tokens_num, num_heads, head_dim), dtype=dtype, device="npu") + cos_shape = (tokens_num, 1, head_dim // 2) + cos = -1.2 + 0.5 * torch.randn(cos_shape, dtype=dtype, device="npu") + sin = -2.0 + 0.5 * torch.randn(cos_shape, dtype=dtype, device="npu") + # forward pass + torch_result = torch_rotary_embedding(state, cos, sin) + rotary_embedding(state, cos, sin) + triton_result = state # state is modified in-place + torch.testing.assert_close(torch_result, triton_result, rtol=1e-3, atol=1e-3) + + +def test_cases(): + rotary_emb(256, 96, 128, torch.float16) + rotary_emb(256, 96, 128, torch.float32) diff --git a/third_party/ascend/unittest/pytest_ut/test_rotatry_gpt.py b/third_party/ascend/unittest/pytest_ut/test_rotatry_gpt.py new file mode 100644 index 0000000000..5a8bdac7f3 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_rotatry_gpt.py @@ -0,0 +1,186 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Rotary embedding kernel implemented by Triton. +GPT-J style +""" + +import torch +import torch_npu +import triton +import triton.language as tl + + +@triton.jit +def rotary_embedding_kernel( + state, # [num_tokens, head_num, head_dim] + cos, # [num_tokens, 1, head_dim // 2] + sin, # [num_tokens, 1, head_dim // 2] + stride_state_n, + stride_state_h, + stride_state_d, + stride_cos_n, + stride_cos_d, + # stride_sin_n, + # stride_sin_d, + num_tokens, + num_heads, + BLOCK_N: tl.constexpr, + BLOCK_H: tl.constexpr, + BLOCK_D: tl.constexpr, +): + token_index = tl.program_id(0) + token_range = token_index * BLOCK_N + tl.arange(0, BLOCK_N) + head_index = tl.program_id(1) + head_range = head_index * BLOCK_H + tl.arange(0, BLOCK_H) + + dim_range = tl.arange(0, BLOCK_D // 2) + dim_range_x = dim_range * 2 + dim_range_y = dim_range * 2 + 1 + + # tl.device_print("dim x", dim_range_x) + # tl.device_print("dim y", dim_range_y) + + state_x_offset = (token_range[:, None, None] * stride_state_n + head_range[None, :, None] * stride_state_h + + dim_range_x[None, None, :] * stride_state_d) + + state_y_offset = (token_range[:, None, None] * stride_state_n + head_range[None, :, None] * stride_state_h + + dim_range_y[None, None, :] * stride_state_d) + + cos_sim_offset = (token_range[:, None, None] * stride_cos_n + dim_range[None, None, :] * stride_cos_d) + + state_x = tl.load( + state + state_x_offset, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + other=0.0, + ) + state_y = tl.load( + state + state_y_offset, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + other=0.0, + ) + + cos_loaded = tl.load( + cos + cos_sim_offset, + mask=token_range[:, None, None] < num_tokens, + other=0.0, + ) + sin_loaded = tl.load( + sin + cos_sim_offset, + mask=token_range[:, None, None] < num_tokens, + other=0.0, + ) + + out_x = state_x * cos_loaded - state_y * sin_loaded + out_y = state_x * sin_loaded + state_y * cos_loaded + + tl.store( + state + state_x_offset, + out_x, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + ) + tl.store( + state + state_y_offset, + out_y, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + ) + + +@torch.inference_mode() +def rotary_embedding(state, cos, sin): + num_tokens = state.shape[0] + num_heads = state.shape[1] + head_dim = state.shape[2] + + BLOCK_N = 8 + BLOCK_H = 4 + grid = ( + triton.cdiv(num_tokens, BLOCK_N), + triton.cdiv(num_heads, BLOCK_H), + ) + if head_dim >= 128: + num_warps = 8 + else: + num_warps = 4 + + kernel = rotary_embedding_kernel[grid]( + state, + cos, + sin, + state.stride(0), + state.stride(1), + state.stride(2), + cos.stride(0), + cos.stride(2), + # sin.stride(0), + # sin.stride(2), + num_tokens, + num_heads, + BLOCK_N=BLOCK_N, + BLOCK_H=BLOCK_H, + BLOCK_D=head_dim, + num_warps=num_warps, + num_stages=1, + ) + # print(kernel.asm['ttir']) + return + + +def torch_rotary_embedding(state, cos, sin): + _, _, dim = state.shape + state_x = state[:, :, 0:dim:2] + state_y = state[:, :, 1:dim:2] + out_x = state_x * cos - state_y * sin + out_y = state_x * sin + state_y * cos + out = torch.empty_like(state).npu() + out[:, :, 0:dim:2] = out_x + out[:, :, 1:dim:2] = out_y + return out + + +def _rotary_emb(dtype): + tokens_num = 256 + num_heads = 96 + head_dim = 128 + max_positions = 1024 + + # torch.float16 has floating point problem in Triton 2.0.0 + # But it works fine in Triton 2.1.0 + state = torch.randn((tokens_num, num_heads, head_dim), dtype=dtype, device="npu") + cos_shape = (tokens_num, 1, head_dim // 2) + cos = -1.2 + 0.5 * torch.randn(cos_shape, dtype=dtype, device="npu") + sin = -2.0 + 0.5 * torch.randn(cos_shape, dtype=dtype, device="npu") + # forward pass + torch_result = torch_rotary_embedding(state, cos, sin) + rotary_embedding(state, cos, sin) + triton_result = state # state is modified in-place + # print(torch_result[1][0]) + # print(triton_result[1][0]) + # Note: This test is not accurate enough. + assert torch.allclose(torch_result, triton_result, atol=1e-2, rtol=1e-7) + + +def test_rotary_emb(): + _rotary_emb(torch.float16) + _rotary_emb(torch.float32) diff --git a/third_party/ascend/unittest/pytest_ut/test_rotaty_embedding_gpt.py b/third_party/ascend/unittest/pytest_ut/test_rotaty_embedding_gpt.py new file mode 100644 index 0000000000..5f73042f78 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_rotaty_embedding_gpt.py @@ -0,0 +1,174 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Rotary embedding kernel implemented by Triton. + +GPT-NeoX style +""" + +import torch +import torch_npu +import triton +import triton.language as tl + + +@triton.jit +def rotary_embedding_kernel( + state, # [num_tokens, head_num, head_dim] + cos, # [num_tokens, 1, head_dim // 2] + sin, # [num_tokens, 1, head_dim // 2] + stride_state_n, + stride_state_h, + stride_state_d, + stride_cos_n, + stride_cos_d, + # stride_sin_n, + # stride_sin_d, + num_tokens, + num_heads, + BLOCK_N: tl.constexpr, + BLOCK_H: tl.constexpr, + BLOCK_D: tl.constexpr, +): + token_index = tl.program_id(0) + token_range = token_index * BLOCK_N + tl.arange(0, BLOCK_N) + head_index = tl.program_id(1) + head_range = head_index * BLOCK_H + tl.arange(0, BLOCK_H) + + dim_range_x = tl.arange(0, BLOCK_D // 2) + dim_range_y = tl.arange(BLOCK_D // 2, BLOCK_D) + + state_x_offset = (token_range[:, None, None] * stride_state_n + head_range[None, :, None] * stride_state_h + + dim_range_x[None, None, :] * stride_state_d) + state_y_offset = (token_range[:, None, None] * stride_state_n + head_range[None, :, None] * stride_state_h + + dim_range_y[None, None, :] * stride_state_d) + + cos_sim_offset = (token_range[:, None, None] * stride_cos_n + dim_range_x[None, None, :] * stride_cos_d) + + state_x = tl.load( + state + state_x_offset, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + other=0.0, + ) + state_y = tl.load( + state + state_y_offset, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + other=0.0, + ) + + cos_loaded = tl.load( + cos + cos_sim_offset, + mask=token_range[:, None, None] < num_tokens, + other=0.0, + ) + sin_loaded = tl.load( + sin + cos_sim_offset, + mask=token_range[:, None, None] < num_tokens, + other=0.0, + ) + + out_x = state_x * cos_loaded - state_y * sin_loaded + out_y = state_x * sin_loaded + state_y * cos_loaded + + tl.store( + state + state_x_offset, + out_x, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + ) + tl.store( + state + state_y_offset, + out_y, + mask=(token_range[:, None, None] < num_tokens) + & (head_range[None, :, None] < num_heads), + ) + + +@torch.inference_mode() +def rotary_embedding(state, cos, sin): + num_tokens = state.shape[0] + num_heads = state.shape[1] + head_dim = state.shape[2] + + BLOCK_N = 16 + BLOCK_H = 4 + grid = ( + triton.cdiv(num_tokens, BLOCK_N), + triton.cdiv(num_heads, BLOCK_H), + ) + if head_dim >= 128: + num_warps = 8 + else: + num_warps = 4 + + kernel = rotary_embedding_kernel[grid]( + state, + cos, + sin, + state.stride(0), + state.stride(1), + state.stride(2), + cos.stride(0), + cos.stride(2), + # sin.stride(0), + # sin.stride(2), + num_tokens, + num_heads, + BLOCK_N=BLOCK_N, + BLOCK_H=BLOCK_H, + BLOCK_D=head_dim, + num_warps=num_warps, + num_stages=1, + ) + return + + +def torch_rotary_embedding(state, cos, sin): + _, _, dim = state.shape + state_x = state[:, :, 0:dim // 2] + state_y = state[:, :, dim // 2:dim] + out_x = state_x * cos - state_y * sin + out_y = state_x * sin + state_y * cos + return torch.cat((out_x, out_y), dim=-1) + + +def rotary_emb(tokens, heads, headdim, dtype): + tokens_num = tokens + num_heads = heads + head_dim = headdim + #max_positions = 1024 + + # torch.float16 has floating point problem in Triton 2.0.0 + # But it works fine in Triton 2.1.0 + state = torch.randn((tokens_num, num_heads, head_dim), dtype=dtype, device="npu") + cos_shape = (tokens_num, 1, head_dim // 2) + cos = -1.2 + 0.5 * torch.randn(cos_shape, dtype=dtype, device="npu") + sin = -2.0 + 0.5 * torch.randn(cos_shape, dtype=dtype, device="npu") + # forward pass + torch_result = torch_rotary_embedding(state, cos, sin) + rotary_embedding(state, cos, sin) + triton_result = state # state is modified in-place + torch.testing.assert_close(torch_result, triton_result, rtol=1e-3, atol=1e-3) + + +def test_cases(): + rotary_emb(256, 96, 128, torch.float16) + rotary_emb(256, 96, 128, torch.float32) diff --git a/third_party/ascend/unittest/pytest_ut/test_row_coalescing_compat.py b/third_party/ascend/unittest/pytest_ut/test_row_coalescing_compat.py new file mode 100644 index 0000000000..3cfc9f44ed --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_row_coalescing_compat.py @@ -0,0 +1,352 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +"""IR contracts for the pure-SIMT Row graph-optimization rule. + +Row is selected by graph-rule bit 8 and ``compile_mode="simt_only"``. The +test enters through the public graph-optimization binding, which is the same +pass scheduled by ``make_ttir()``. +""" + +import pytest + +from triton._C.libtriton import ascend, ir +from triton._C.libtriton.ascend import ir as ascend_ir + +if not hasattr(ascend.passes.ttir, "add_graph_optimize"): + pytest.skip( + "requires the TritonAscend build containing graph optimization", + allow_module_level=True, + ) + + +def _row_module( + name, + width, + *, + axis="x", + reads_num_programs=False, + pid_derived_outside_work=False, + bound_before_pid=False, + multiply_pid_in_work=False, + has_direct_call=False, + body="copy", +): + num_programs = (f" %num_programs = tt.get_num_programs {axis} : i32\n" if reads_num_programs else "") + pre_load = "" + pre_pid = "" + helper = "" + if has_direct_call: + # Keep the call outside the work region so this test exercises the + # V1 entry-closure gate rather than merely Row's liftable-op filter. + pre_pid = " tt.call @row_helper() : () -> ()\n" + helper = """ tt.func private @row_helper() { + tt.return + } +""" + pid_prefix = "" + pid_in_work = "%pid" + if pid_derived_outside_work: + pid_prefix = """ %one = arith.constant 1 : i32 + %pid_base = arith.addi %pid, %one : i32 +""" + pid_in_work = "%pid_base" + work_pid_prefix = "" + if multiply_pid_in_work: + work_pid_prefix = f""" %block = arith.constant {width} : i32 + %pid_scaled = arith.muli %pid, %block : i32 +""" + pid_in_work = "%pid_scaled" + bound_prefix = "" + count_definition = " %count = tt.load %valid : !tt.ptr\n" + guard_count = "%count" + if bound_before_pid: + bound_prefix = " %bound = arith.constant 32768 : i32\n" + count_definition = "" + guard_count = "%bound" + load = f" %value = tt.load %src_ptr : tensor<{width}x!tt.ptr>" + post_load = "" + store_value = "%value" + store_mask = "" + + if body == "masked_copy": + pre_load = f""" %count_splat = tt.splat %count : i32 -> tensor<{width}xi32> + %input_mask = arith.cmpi slt, %offsets, %count_splat : tensor<{width}xi32> + %zero = arith.constant dense<0.000000e+00> : tensor<{width}xf32> +""" + load = f" %value = tt.load %src_ptr, %input_mask, %zero : tensor<{width}x!tt.ptr>" + store_mask = ", %input_mask" + elif body == "reduce": + post_load = f""" %reduced = "tt.reduce"(%value) <{{axis = 0 : i32}}> ({{ + ^bb0(%a: f32, %b: f32): + %sum = arith.addf %a, %b : f32 + tt.reduce.return %sum : f32 + }}) : (tensor<{width}xf32>) -> f32 + %out = tt.splat %reduced : f32 -> tensor<{width}xf32> +""" + store_value = "%out" + elif body == "scan": + post_load = f""" %out = "tt.scan"(%value) <{{axis = 0 : i32, reverse = false}}> ({{ + ^bb0(%a: f32, %b: f32): + %sum = arith.addf %a, %b : f32 + tt.scan.return %sum : f32 + }}) : (tensor<{width}xf32>) -> tensor<{width}xf32> +""" + store_value = "%out" + elif body == "for": + post_load = f""" %for_lb = arith.constant 0 : index + %for_step = arith.constant 1 : index + %for_ub = arith.constant 2 : index + %out = scf.for %i = %for_lb to %for_ub step %for_step iter_args(%acc = %value) -> (tensor<{width}xf32>) {{ + %one = arith.constant dense<1.000000e+00> : tensor<{width}xf32> + %next = arith.addf %acc, %one : tensor<{width}xf32> + scf.yield %next : tensor<{width}xf32> + }} +""" + store_value = "%out" + elif body == "for_pid_lower_bound": + post_load = f""" %pid_index = arith.index_cast %pid : i32 to index + %for_step = arith.constant 1 : index + %for_ub = arith.constant 2 : index + %out = scf.for %i = %pid_index to %for_ub step %for_step iter_args(%acc = %value) -> (tensor<{width}xf32>) {{ + scf.yield %acc : tensor<{width}xf32> + }} +""" + store_value = "%out" + elif body == "non_whitelisted_y_num_programs": + post_load = " %not_whitelisted = tt.get_num_programs y : i32\n" + elif body != "copy": + raise ValueError(f"unsupported row test body: {body}") + + return f""" +module {{ + tt.func public @{name}(%src: !tt.ptr, %dst: !tt.ptr, %valid: !tt.ptr) {{ +{pre_pid}{bound_prefix} %pid = tt.get_program_id {axis} : i32 +{num_programs}{pid_prefix}{count_definition} %past_end = arith.cmpi sge, %pid, {guard_count} : i32 + cf.cond_br %past_end, ^bb_return, ^bb_work + ^bb_return: + tt.return + ^bb_work: +{work_pid_prefix} %pid_splat = tt.splat {pid_in_work} : i32 -> tensor<{width}xi32> + %range = tt.make_range {{end = {width} : i32, start = 0 : i32}} : tensor<{width}xi32> + %offsets = arith.addi %pid_splat, %range : tensor<{width}xi32> + %src_splat = tt.splat %src : !tt.ptr -> tensor<{width}x!tt.ptr> + %src_ptr = tt.addptr %src_splat, %offsets : tensor<{width}x!tt.ptr>, tensor<{width}xi32> +{pre_load}{load} +{post_load} %dst_splat = tt.splat %dst : !tt.ptr -> tensor<{width}x!tt.ptr> + %dst_ptr = tt.addptr %dst_splat, %offsets : tensor<{width}x!tt.ptr>, tensor<{width}xi32> + tt.store %dst_ptr, {store_value}{store_mask} : tensor<{width}x!tt.ptr> + tt.return + }} +{helper}}} +""" + + +def _run_row(text, tmp_path, *, compile_mode="simt_only", rule_mask=8): + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + path = tmp_path / "row_coalescing_compat.mlir" + path.write_text(text) + module = ir.parse_mlir_module(str(path), context) + pm = ir.pass_manager(context) + ascend.passes.ttir.add_graph_optimize( + pm, + rule_mask=rule_mask, + compile_mode=compile_mode, + ) + pm.run(module, "row-coalescing-graph-rule-test") + return str(module) + + +def _assert_row_hit(text, *, factor=8, axis=0): + assert f"hacc.coalesce_factor = {factor} : i32" in text + assert f"hacc.coalesce_axis = {axis} : i32" in text + assert "hacc.coalesce_grid_ceil_div = 1 : i32" in text + + +def _assert_row_bailout(text): + assert "hacc.coalesce_factor" not in text + assert "hacc.coalesce_axis" not in text + assert "hacc.coalesce_grid_ceil_div" not in text + assert "cf.cond_br" in text + + +@pytest.mark.parametrize( + ("width", "factor"), + ((16, 8), (32, 4), (1024, 2)), +) +def test_row_coalescing_graph_rule_preserves_h_selection(width, factor, tmp_path): + text = _run_row(_row_module(f"row_h{factor}", width), tmp_path) + + assert f"hacc.coalesce_factor = {factor} : i32" in text + assert "hacc.coalesce_axis = 0 : i32" in text + assert "hacc.coalesce_grid_ceil_div = 1 : i32" in text + assert f"tensor<{factor}x{width}xf32>" in text + assert "cf.cond_br" not in text + + +def test_row_coalescing_scaffold_follows_pid_when_bound_is_hoisted(tmp_path): + text = _run_row( + _row_module( + "row_bound_before_pid", + 16, + bound_before_pid=True, + multiply_pid_in_work=True, + ), + tmp_path, + ) + + _assert_row_hit(text) + assert text.index("arith.constant 32768") < text.index("tt.get_program_id") + assert text.index("tt.get_program_id") < text.index("tt.make_range") + assert "cf.cond_br" not in text + + +def test_row_coalescing_rejects_visible_num_programs_on_row_axis(tmp_path): + text = _run_row( + _row_module("row_reads_num_programs", 16, reads_num_programs=True), + tmp_path, + ) + + _assert_row_bailout(text) + + +def test_row_coalescing_adds_tail_mask_to_masked_load_and_store(tmp_path): + text = _run_row(_row_module("row_tail_mask", 16, body="masked_copy"), tmp_path) + + _assert_row_hit(text) + assert "cf.cond_br" not in text + assert "arith.cmpi slt" in text + assert text.count("arith.andi") >= 2 + assert text.count("tensor<8x16xi1>") >= 4 + + lifted_loads = [line for line in text.splitlines() if "tt.load" in line and "tensor<8x16x!tt.ptr>" in line] + lifted_stores = [line for line in text.splitlines() if "tt.store" in line and "tensor<8x16x!tt.ptr>" in line] + assert len(lifted_loads) == 1 and lifted_loads[0].count(",") >= 2 + assert len(lifted_stores) == 1 and lifted_stores[0].count(",") >= 2 + + +def test_row_coalescing_lifts_reduce_along_original_row_axis(tmp_path): + text = _run_row(_row_module("row_reduce", 16, body="reduce"), tmp_path) + + _assert_row_hit(text) + assert '"tt.reduce"' in text + assert "<{axis = 1 : i32}>" in text + assert "(tensor<8x16xf32>) -> tensor<8xf32>" in text + assert "tensor<8x1xf32> -> tensor<8x16xf32>" in text + + +def test_row_coalescing_lifts_scan_along_original_row_axis(tmp_path): + text = _run_row(_row_module("row_scan", 16, body="scan"), tmp_path) + + _assert_row_hit(text) + assert '"tt.scan"' in text + assert "<{axis = 1 : i32, reverse = false}>" in text + assert "(tensor<8x16xf32>) -> tensor<8x16xf32>" in text + + +def test_row_coalescing_clones_scf_for_with_lifted_iter_args(tmp_path): + text = _run_row(_row_module("row_for", 16, body="for"), tmp_path) + + _assert_row_hit(text) + assert "scf.for" in text + assert "iter_args" in text + assert "-> (tensor<8x16xf32>)" in text + assert "scf.yield" in text + + +def test_row_coalescing_declines_late_sandbox_failure_without_committing(tmp_path): + text = _run_row( + _row_module("row_late_sandbox_decline", 16, body="for_pid_lower_bound"), + tmp_path, + ) + + _assert_row_bailout(text) + assert "arith.index_cast" in text + assert "scf.for" in text + + +def test_row_coalescing_rejects_non_whitelisted_y_num_programs(tmp_path): + text = _run_row( + _row_module( + "row_non_whitelisted_y_num_programs", + 16, + body="non_whitelisted_y_num_programs", + ), + tmp_path, + ) + + _assert_row_bailout(text) + assert "tt.get_num_programs y" in text + + +def test_row_coalescing_preserves_nonzero_row_axis(tmp_path): + text = _run_row(_row_module("row_axis_y", 16, axis="y"), tmp_path) + + _assert_row_hit(text, axis=1) + assert "tt.get_program_id y" in text + + +def test_row_coalescing_requires_force_simt_only_and_rule_bit(tmp_path): + source = _row_module("row_force_gate", 16) + + mode_disabled = _run_row(source, tmp_path, compile_mode="simd") + mask_disabled = _run_row(source, tmp_path, rule_mask=7) + + _assert_row_bailout(mode_disabled) + _assert_row_bailout(mask_disabled) + + +def test_row_coalescing_rejects_pid_value_defined_before_work_block(tmp_path): + text = _run_row( + _row_module("row_pid_derived_before_work", 16, pid_derived_outside_work=True), + tmp_path, + ) + + _assert_row_bailout(text) + # The Python MLIR printer may renumber SSA values, so check the preserved + # scalar add structurally rather than relying on its textual value name. + assert text.count("arith.addi") >= 2 + + +def test_row_coalescing_rejects_direct_call_even_outside_work_region(tmp_path): + text = _run_row(_row_module("row_with_call", 16, has_direct_call=True), tmp_path) + + _assert_row_bailout(text) + assert "tt.call @row_helper()" in text + + +def test_row_coalescing_requires_one_public_entry_but_ignores_unused_private_funcs(tmp_path, ): + source = _row_module("row_one_public", 16) + multiple_public = source.replace( + "\n}\n", + """ + tt.func public @another_entry() { + tt.return + } +} +""", + 1, + ) + with_unused_private = source.replace( + "\n}\n", + """ + tt.func private @unused_helper() { + tt.return + } +} +""", + 1, + ) + + _assert_row_bailout(_run_row(multiple_public, tmp_path)) + _assert_row_hit(_run_row(with_unused_private, tmp_path)) diff --git a/third_party/ascend/unittest/pytest_ut/test_row_coalescing_e2e.py b/third_party/ascend/unittest/pytest_ut/test_row_coalescing_e2e.py new file mode 100644 index 0000000000..dca975b47c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_row_coalescing_e2e.py @@ -0,0 +1,58 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +"""NPU regression for the pure-SIMT Row compatibility scheduling slot.""" + +import pytest +import torch +import triton +import triton.language as tl +from triton.backends.ascend.utils import is_compile_on_910_95 + +pytestmark = pytest.mark.skipif( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason="Row pure-SIMT binary validation requires an Ascend 950 toolchain", +) + + +@triton.jit +def row_coalescing_tail_copy(src, dst, n, BLOCK: tl.constexpr): + # Keep the canonical scalar row guard that RowCoalescing matches. The + # runtime ``n`` makes the final group partial, so the pass must combine its + # generated row-tail mask with these load/store masks. + pid = tl.program_id(0) + if pid >= n: + return + offsets = pid + tl.arange(0, BLOCK) + mask = offsets < n + value = tl.load(src + offsets, mask=mask, other=0.0) + tl.store(dst + offsets, value, mask=mask) + + +@pytest.mark.parametrize("dtype", (torch.float32, torch.float16)) +def test_row_coalescing_tail_pure_simt_e2e(dtype): + # 19 is intentionally not divisible by the expected H=8 for a 16-element + # base tile. The compatibility pass must use the original ceil-div launch + # contract and suppress stores for rows past ``n``. + n = 19 + src = torch.arange(n, dtype=dtype).npu() + dst = torch.full_like(src, -1) + + row_coalescing_tail_copy[(n, )]( + src, + dst, + n, + BLOCK=16, + compile_mode="simt_only", + ) + torch.npu.synchronize() + + assert torch.equal(dst.cpu(), src.cpu()) diff --git a/third_party/ascend/unittest/pytest_ut/test_rshift.py b/third_party/ascend/unittest/pytest_ut/test_rshift.py new file mode 100644 index 0000000000..f24676572b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_rshift.py @@ -0,0 +1,99 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import time +import test_common +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = x0 >> 2 + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + tmp = tl.cast(2, tl.int8) + ret = x >> tmp + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + # (torch.float32, 'float32'), + # (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + (torch.int8, 'int8'), + # (torch.int16, 'int16'), + # (torch.int32, 'int32'), + # (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + # print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + # print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_rsqrt.py b/third_party/ascend/unittest/pytest_ut/test_rsqrt.py new file mode 100644 index 0000000000..f6be226a50 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_rsqrt.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import numpy as np +import pytest +import test_common + + +def numpy_rsqrt(x0, x1): + res = x0 + 1.0 / (np.sqrt(x1)) + return res + + +@triton.jit +def triton_rsqrt(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.load(in_ptr1 + (x0), xmask) + tmp2 = tmp0 + tl.rsqrt(tmp1) + tl.store(out_ptr0 + (xindex), tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_rsqrt(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = np.abs(np.random.randn(shape[0], shape[1], shape[2])).astype(eval('np.' + dtype)) + x1 = np.abs(np.random.randn(shape[0], shape[1], shape[2])).astype(eval('np.' + dtype)) + x0_npu = torch.tensor(x0).npu() + x1_npu = torch.tensor(x1).npu() + # numpy结果 + numpy_res = numpy_rsqrt(x0, x1) + # triton结果 + triton_res = test_common.generate_tensor(shape, dtype).npu() + triton_rsqrt[ncore, 1, 1](x0_npu, x1_npu, triton_res, x0_npu.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, triton_res, torch.tensor(numpy_res).npu()) diff --git a/third_party/ascend/unittest/pytest_ut/test_runtime_utils.py b/third_party/ascend/unittest/pytest_ut/test_runtime_utils.py new file mode 100644 index 0000000000..2a2211bc30 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_runtime_utils.py @@ -0,0 +1,122 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import logging +import warnings + +import pytest +import torch + +from triton.backends.ascend import utils +from triton.backends.ascend.runtime import utils as runtime_utils + + +@pytest.fixture(autouse=True) +def _reset_deprecated_npu_option_warnings(monkeypatch): + monkeypatch.setattr(utils, "_WARNED_DEPRECATED_NPU_OPTIONS", set()) + + +def test_get_logger(): + logger = utils.get_logger("test_utils", "INFO") + assert logger.level == logging.INFO + + +def test_get_ascend_arch_from_env_is_deprecated(monkeypatch): + monkeypatch.setattr(utils, "_WARNED_DEPRECATED_ASCEND_ENV_VARS", set(), raising=False) + monkeypatch.setenv("TRITON_ASCEND_ARCH", "Ascend910_9599") + with pytest.warns(FutureWarning, match=r"TRITON_ASCEND_ARCH.*will be removed.*GPUTarget.arch"): + result = utils.get_ascend_arch_from_env() + assert result == "" + + +def test_deprecated_ascend_env_var_warns_only_once_per_process(monkeypatch): + name = "TRITON_REGISTER_TENSOR_MSPROF" + monkeypatch.setattr(utils, "_WARNED_DEPRECATED_ASCEND_ENV_VARS", set(), raising=False) + monkeypatch.delenv(name, raising=False) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + utils._warn_deprecated_ascend_env_var(name) + monkeypatch.setenv(name, "1") + utils._warn_deprecated_ascend_env_var(name) + utils._warn_deprecated_ascend_env_var(name) + + assert len(caught) == 1 + + +@pytest.mark.parametrize( + ("legacy_option", "compile_mode"), + [ + pytest.param("force_simt_only", "simt_only", id="simt-only"), + pytest.param("force_simt_template", "simd_simt_template", id="simt-template"), + ], +) +def test_deprecated_simt_option_routes_to_compile_mode(legacy_option, compile_mode): + options = {legacy_option: True} + + with pytest.warns(FutureWarning, match=rf"{legacy_option}.*compile_mode={compile_mode!r}"): + normalized = utils._remove_deprecated_npu_options(options) + + assert normalized == {"compile_mode": compile_mode} + assert options == {legacy_option: True} + + +@pytest.mark.parametrize( + ("options", "expected_compile_mode"), + [ + pytest.param({"compile_mode": "simd", "force_simt_only": True}, "simt_only", id="simt-only"), + pytest.param( + {"compile_mode": "simt_only", "force_simt_template": True}, + "simd_simt_template", + id="simt-template", + ), + pytest.param( + {"compile_mode": "simd", "force_simt_only": True, "force_simt_template": True}, + "simt_only", + id="simt-only-over-template", + ), + ], +) +def test_deprecated_simt_force_option_takes_precedence_over_compile_mode(options, expected_compile_mode): + original = dict(options) + + with pytest.warns(FutureWarning): + normalized = utils._remove_deprecated_npu_options(options) + + assert normalized == {"compile_mode": expected_compile_mode} + assert options == original + + +def test_deprecated_simt_option_is_routed_once_during_in_place_normalization(): + options = {"force_simt_only": True} + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + first = utils._remove_deprecated_npu_options(options, in_place=True) + second = utils._remove_deprecated_npu_options(options, in_place=True) + + assert len(caught) == 1 + assert first is second is options + assert options == {"compile_mode": "simt_only"} + + +def test_get_byte_per_numel_supports_unsigned_integer_dtypes(): + assert runtime_utils.get_byte_per_numel(torch.uint16) == 2 + assert runtime_utils.get_byte_per_numel(torch.uint32) == 4 + assert runtime_utils.get_byte_per_numel(torch.uint64) == 8 diff --git a/third_party/ascend/unittest/pytest_ut/test_scalarPointer.py b/third_party/ascend/unittest/pytest_ut/test_scalarPointer.py new file mode 100644 index 0000000000..bbe7f65f74 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_scalarPointer.py @@ -0,0 +1,178 @@ +import pytest +import os +import sys +import math + +import pytest +import os +import sys +import math + +current_file = os.path.abspath(__file__) +vllm_root_path = os.path.dirname(os.path.dirname(os.path.dirname(current_file))) +sys.path.insert(0, vllm_root_path) + +#from vllm.model_executor.layers.mamba.ops.ssd_state_passing import _state_passing_fwd_kernel +from einops import rearrange +import torch +import torch_npu +import triton +import triton.language as tl + + +@triton.autotune( + configs=[ + triton.Config({'BLOCK_SIZE': 64}), + ], + key=['dim'], +) +@triton.jit +def _state_passing_fwd_kernel( + # Pointers to matrices + states_ptr, + out_ptr, + final_states_ptr, + dA_cs_ptr, + initstates_ptr, + seq_idx_ptr, + # Matrix dimensions + dim, + nchunks, + seqlen, + chunk_size, + # Strides + stride_states_batch, + stride_states_chunk, + stride_states_head, + stride_states_dim, + stride_out_batch, + stride_out_chunk, + stride_out_head, + stride_out_dim, + stride_final_states_batch, + stride_final_states_head, + stride_final_states_dim, + stride_dA_cs_batch, + stride_dA_cs_chunk, + stride_dA_cs_head, + stride_initstates_batch, + stride_initstates_head, + stride_initstates_dim, + stride_seq_idx_batch, + stride_seq_idx_seqlen, + # Meta-parameters + HAS_INITSTATES: tl.constexpr, + HAS_SEQ_IDX: tl.constexpr, + IS_CONT_BATCHED: tl.constexpr, + BLOCK_SIZE: tl.constexpr, +): + pid_b = tl.program_id(axis=1) + pid_h = tl.program_id(axis=2) + pid_m = tl.program_id(axis=0) + states_ptr += pid_b * stride_states_batch + pid_h * stride_states_head + dA_cs_ptr += pid_b * stride_dA_cs_batch + pid_h * stride_dA_cs_head + out_ptr += pid_b * stride_out_batch + pid_h * stride_out_head + final_states_ptr += pid_b * stride_final_states_batch + pid_h * stride_final_states_head + + offs_m = pid_m * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + states_ptrs = states_ptr + offs_m * stride_states_dim + out_ptrs = out_ptr + offs_m * stride_out_dim + final_states_ptrs = final_states_ptr + offs_m * stride_final_states_dim + + # - states will be the past state of the sequence that continues on the current check + if not HAS_INITSTATES: + states = tl.zeros((BLOCK_SIZE, ), dtype=tl.float32) + + tl.store(out_ptrs, states, mask=offs_m < dim) + out_ptrs += stride_out_chunk + for c in range(nchunks): + new_states = tl.load(states_ptrs, mask=offs_m < dim, other=0.0).to(tl.float32) + #dA_cs = tl.load(dA_cs_ptr + tl.arange(0,1)).to(tl.float32) + dA_cs = tl.load(dA_cs_ptr).to(tl.float32) + scale = tl.exp(dA_cs) + states = scale * states + new_states + if c < nchunks - 1: + tl.store(out_ptrs, states, mask=offs_m < dim) + else: + tl.store(final_states_ptrs, states, mask=offs_m < dim) + states_ptrs += stride_states_chunk + dA_cs_ptr += stride_dA_cs_chunk + out_ptrs += stride_out_chunk + + +def _state_passing_fwd( + states, + dA_chunk_cumsum, + initial_states=None, + seq_idx=None, + chunk_size=None, + out_dtype=None, + is_cont_batched=False, +): + batch, nchunks, nheads, dim = states.shape + assert dA_chunk_cumsum.shape == (batch, nheads, nchunks) + if initial_states is not None: + if is_cont_batched: + # - if cu_seqlens is provided, then the initial states + # are used for continuous batching. In which case we + # require seq_idx to be provided + assert seq_idx is not None, "" + else: + # - this is the regular batching case, where initial + # states are used are for each example of the batch. + assert initial_states.shape == (batch, nheads, dim) + + if seq_idx is not None: + assert chunk_size is not None + seqlen = seq_idx.shape[-1] + assert seq_idx.shape == (batch, seqlen) + out_dtype = states.dtype if out_dtype is None else out_dtype + out = torch.empty((batch, nchunks, nheads, dim), device=states.device, dtype=out_dtype) + final_states = torch.empty((batch, nheads, dim), device=states.device, dtype=torch.float32) + grid = lambda META: (triton.cdiv(dim, META['BLOCK_SIZE']), batch, nheads) + # with torch.cuda.device(states.device.index): + _state_passing_fwd_kernel[grid]( + states, + out, + final_states, + dA_chunk_cumsum, + initial_states, + seq_idx, + dim, + nchunks, + seqlen if seq_idx is not None else 0, + chunk_size if seq_idx is not None else 0, + states.stride(0), + states.stride(1), + states.stride(2), + states.stride(3), + out.stride(0), + out.stride(1), + out.stride(2), + out.stride(3), + final_states.stride(0), + final_states.stride(1), + final_states.stride(2), + dA_chunk_cumsum.stride(0), + dA_chunk_cumsum.stride(2), + dA_chunk_cumsum.stride(1), + *((initial_states.stride(0), initial_states.stride(1), + initial_states.stride(2)) if initial_states is not None else (0, 0, 0)), + *((seq_idx.stride(0), seq_idx.stride(1)) if seq_idx is not None else (0, 0)), + HAS_INITSTATES=initial_states is not None, + HAS_SEQ_IDX=seq_idx is not None, + IS_CONT_BATCHED=is_cont_batched, + ) + return out, final_states + + +@pytest.mark.perf(repeat=17) +def test_state_passing_fwd(): + states = torch.randn((1, 7, 3, 5, 5), dtype=torch.float32).npu() + dA_cumsum = torch.randn((1, 3, 7, 17), dtype=torch.float32).npu() + initial_states = None + cu_seqlens = None + states, final_states = _state_passing_fwd( + rearrange(states, "... p n -> ... (p n)"), dA_cumsum[:, :, :, -1], + initial_states=rearrange(initial_states, "... p n -> ... (p n)") if initial_states is not None else None, + seq_idx=None, chunk_size=17, out_dtype=torch.float32, is_cont_batched=cu_seqlens is not None) diff --git a/third_party/ascend/unittest/pytest_ut/test_scalar_calc.py b/third_party/ascend/unittest/pytest_ut/test_scalar_calc.py new file mode 100644 index 0000000000..53517eb4eb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_scalar_calc.py @@ -0,0 +1,694 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice + +import pytest +import test_common + + +### add +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_add_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tmp0 + 2.0 + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = y + 2.0 + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### sub +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_sub_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tmp0 - 2.0 + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = y - 2.0 + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### mul +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_mul_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tmp0 * 2.0 + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = y * 2.0 + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### div +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_div_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tmp0 / 2.0 + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = y / 2.0 + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### remf +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_remf_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tmp0 % 2.0 + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = y - 2.0 * torch.div(y, 2.0, rounding_mode="trunc") + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### negf +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_negf_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = -tmp0 + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = -y + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### cmpf +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_cmpf_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = (tmp0 > 0.5).to(tmp0.dtype) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = (y > 0.5).to(y.dtype) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### ceil +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_ceil_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.math.ceil(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.ceil(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### floor +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_floor_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.math.floor(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.floor(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### maximum(propagate_nan == tl.PropagateNan.ALL) +# setting propagate_nan=tl.PropagateNan.ALL to generate arith::MaximumFOp +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_maximum_nanall_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + tl.static_assert(N > 1) + tmp0 = tl.load(in_ptr0 + 0) + tmp1 = tl.load(in_ptr0 + 1) + tmp1 = tl.maximum(tmp0, tmp1, propagate_nan=tl.PropagateNan.ALL) + tl.store(out_ptr0 + 0, tmp1) + + def torch_func(x0): + y0 = x0[0] + y1 = x0[1] + y = torch.maximum(y0, y1) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### maximum(propagate_nan == tl.PropagateNan.NONE) +# setting propagate_nan=tl.PropagateNan.NONE to generate arith::MaxNumFOp +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_maximum_nannone_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + tl.static_assert(N > 1) + tmp0 = tl.load(in_ptr0 + 0) + tmp1 = tl.load(in_ptr0 + 1) + tmp1 = tl.maximum(tmp0, tmp1, propagate_nan=tl.PropagateNan.ALL) + tl.store(out_ptr0 + 0, tmp1) + + def torch_func(x0): + y0 = x0[0] + y1 = x0[1] + y = torch.fmax(y0, y1) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### minimum(propagate_nan == tl.PropagateNan.ALL) +# setting propagate_nan=tl.PropagateNan.ALL to generate arith::MinimumFOp +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_minimum_nanall_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + tl.static_assert(N > 1) + tmp0 = tl.load(in_ptr0 + 0) + tmp1 = tl.load(in_ptr0 + 1) + tmp1 = tl.minimum(tmp0, tmp1, propagate_nan=tl.PropagateNan.ALL) + tl.store(out_ptr0 + 0, tmp1) + + def torch_func(x0): + y0 = x0[0] + y1 = x0[1] + y = torch.minimum(y0, y1) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### minimum(propagate_nan == tl.PropagateNan.NONE) +# setting propagate_nan=tl.PropagateNan.NONE to generate arith::MinNumFOp +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_minimum_nannone_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + tl.static_assert(N > 1) + tmp0 = tl.load(in_ptr0 + 0) + tmp1 = tl.load(in_ptr0 + 1) + tmp1 = tl.minimum(tmp0, tmp1, propagate_nan=tl.PropagateNan.NONE) + tl.store(out_ptr0 + 0, tmp1) + + def torch_func(x0): + y0 = x0[0] + y1 = x0[1] + y = torch.fmin(y0, y1) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### extf +@pytest.mark.parametrize('param_list', [['float16', 'float32', 16]]) +def test_scalar_extf_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tmp0.to(tl.float32) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = y.to(torch.float32) + return torch.tensor(y) + + src_dtype, dst_dtype, N = param_list + x0 = test_common.generate_tensor((N, ), src_dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dst_dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dst_dtype, y_cal[0], y_ref) + + +### truncf +@pytest.mark.parametrize('param_list', [['float32', 'float16', 16]]) +def test_scalar_truncf_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tmp0.to(tl.float16) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = y.to(torch.float16) + return torch.tensor(y) + + src_dtype, dst_dtype, N = param_list + x0 = test_common.generate_tensor((N, ), src_dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dst_dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dst_dtype, y_cal[0], y_ref) + + +### exp +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_exp_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.math.exp(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.exp(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### exp2 +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_exp_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.math.exp2(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.exp2(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### log +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_log_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp0 = tl.abs(tmp0) + tmp1 = tl.log(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.abs(y) + y = torch.log(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### log2 +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_log2_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp0 = tl.abs(tmp0) + tmp1 = tl.log2(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.abs(y) + y = torch.log2(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### sin +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_sin_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.sin(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.sin(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### cos +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_cos_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.cos(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.cos(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### abs +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_abs_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.abs(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.abs(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### erf +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_erf_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.erf(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.erf(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### sqrt +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_sqrt_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp0 = tl.abs(tmp0) + tmp1 = tl.math.sqrt(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.abs(y) + y = torch.sqrt(y) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### rsqrt +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_rsqrt_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp0 = tl.abs(tmp0) + tmp1 = tl.math.rsqrt(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.abs(y) + y = torch.rsqrt(y) + return y.clone().detach() + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### tanh +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_tanh_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + idx = 0 + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = libdevice.tanh(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def torch_func(x0): + y = x0[0] + y = torch.tanh(y) + return y.clone().detach() + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) + + +### sum +@pytest.mark.parametrize('param_list', [['float32', 16]]) +def test_scalar_sum_calc(param_list): + + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, N: tl.constexpr): + tmp0 = tl.load(in_ptr0 + tl.arange(0, N)) + tmp1 = tl.sum(tmp0, 0) + tl.store(out_ptr0 + 0, tmp1) + + def torch_func(x0): + y = torch.sum(x0, 0) + return torch.tensor(y) + + dtype, N = param_list + x0 = test_common.generate_tensor((N, ), dtype).npu() + y_ref = torch_func(x0) + y_cal = test_common.generate_tensor((1, ), dtype).npu() + triton_kernel[1, 1, 1](y_cal, x0, N=N) + test_common.validate_cmp(dtype, y_cal[0], y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_scalar_masked_load.py b/third_party/ascend/unittest/pytest_ut/test_scalar_masked_load.py new file mode 100644 index 0000000000..eddb175958 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_scalar_masked_load.py @@ -0,0 +1,51 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu # noqa: F401 +import triton +import triton.language as tl + + +@triton.jit(do_not_specialize=["active_count"]) +def scalar_masked_load_kernel(src, dst, active_count, WITH_OTHER: tl.constexpr): + pid = tl.program_id(0) + active = pid < active_count + if WITH_OTHER: + value = tl.load(src + pid, mask=active, other=0) + tl.store(dst + pid, value) + else: + value = tl.load(src + pid, mask=active) + # The inactive load result is unspecified; leave the destination alone. + tl.store(dst + pid, value, mask=active) + + +@pytest.mark.parametrize("dtype", [torch.int8, torch.int32, torch.float32, torch.bool]) +@pytest.mark.parametrize("active_count", [0, 5, 32]) +@pytest.mark.parametrize("with_other", [False, True]) +def test_scalar_masked_load(dtype, active_count, with_other): + source = (torch.arange(32) % 7 - 3).to(dtype) + src = source.npu() + dst = torch.full((32, ), 1, dtype=dtype, device="npu") + scalar_masked_load_kernel[(32, )](src, dst, active_count, with_other) + expected = torch.full((32, ), 0 if with_other else 1, dtype=dtype) + expected[:active_count] = source[:active_count] + torch.testing.assert_close(dst.cpu(), expected, rtol=0, atol=0) diff --git a/third_party/ascend/unittest/pytest_ut/test_scalar_pointer_control_flow.py b/third_party/ascend/unittest/pytest_ut/test_scalar_pointer_control_flow.py new file mode 100644 index 0000000000..be85719376 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_scalar_pointer_control_flow.py @@ -0,0 +1,341 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import torch +import torch_npu # noqa: F401 +import triton +import triton.language as tl + +BLOCK = 16 +INPUT_SIZE = 256 + + +@triton.jit +def scalar_pointer_if_kernel(x, out, predicate_ptr): + predicate = tl.load(predicate_ptr) + if predicate != 0: + pointer = x + 3 + else: + pointer = x + 11 + tl.store(out, tl.load(pointer)) + + +@triton.jit +def scalar_pointer_for_kernel(x, out, steps_ptr): + steps = tl.load(steps_ptr) + pointer = x + 2 + for _ in tl.range(0, steps): + pointer = pointer + 3 + tl.store(out, tl.load(pointer)) + + +@triton.jit +def scalar_pointer_while_kernel(x, out, steps_ptr): + steps = tl.load(steps_ptr) + pointer = x + 1 + iteration = 0 + while iteration < steps: + pointer = pointer + 2 + iteration += 1 + tl.store(out, tl.load(pointer)) + + +@triton.jit +def scalar_pointer_while_from_function_args_kernel(x, out, steps_ptr): + """Keep function-argument bases outside the loop and carry only offsets.""" + steps = tl.load(steps_ptr) + input_pointer = x + output_pointer = out + iteration = 0 + while iteration < steps: + input_pointer = input_pointer + 2 + output_pointer = output_pointer + 3 + iteration += 1 + tl.store(output_pointer, tl.load(input_pointer)) + + +@triton.jit +def scalar_pointer_while_if_backedge_kernel(x0, x1, out, steps_ptr, switch_at_ptr): + """Select a new scalar-pointer base on one while-loop backedge.""" + steps = tl.load(steps_ptr) + switch_at = tl.load(switch_at_ptr) + pointer = x0 + 1 + iteration = 0 + while iteration < steps: + if iteration == switch_at: + pointer = x1 + 7 + else: + pointer = pointer + 2 + iteration += 1 + tl.store(out, tl.load(pointer)) + + +@triton.jit +def scalar_pointer_nested_if_for_kernel(x, out, predicate_ptr, steps_ptr): + predicate = tl.load(predicate_ptr) + steps = tl.load(steps_ptr) + pointer = x + for iteration in tl.range(0, steps): + if (iteration & 1) == predicate: + pointer = pointer + 2 + else: + pointer = pointer + 1 + tl.store(out, tl.load(pointer)) + + +@triton.jit +def scalar_pointer_select_loop_kernel(x0, x1, out, predicate_ptr, steps_ptr): + predicate = tl.load(predicate_ptr) + steps = tl.load(steps_ptr) + pointer = tl.where(predicate != 0, x0 + 4, x1 + 7) + for _ in tl.range(0, steps): + pointer = pointer + 2 + tl.store(out, tl.load(pointer)) + + +@triton.jit +def scalar_pointer_if_with_tensor_mask_kernel(x0, x1, out, predicate_ptr, limit_ptr, BLOCK: tl.constexpr): + """Return a scalar pointer and an independently computed tensor mask.""" + predicate = tl.load(predicate_ptr) + limit = tl.load(limit_ptr) + lane = tl.arange(0, BLOCK) + if predicate != 0: + pointer = x0 + 3 + valid = lane < limit + else: + pointer = x1 + 7 + valid = lane < BLOCK - 2 + value = tl.load(pointer + lane, mask=valid, other=-1.0) + tl.store(out + lane, value) + + +@triton.jit +def tensor_pointer_loop_kernel(tensor_source, tensor_out, steps_ptr, BLOCK: tl.constexpr): + """Exercise one tensor-of-pointers loop carrier across several iterations.""" + steps = tl.load(steps_ptr) + lane = tl.arange(0, BLOCK) + tensor_pointer = tensor_source + lane + 6 + for _ in tl.range(0, steps): + tensor_pointer += 2 + tl.store(tensor_out + lane, tl.load(tensor_pointer)) + + +@triton.jit +def descriptor_boundary_scalar_kernel(x0, x1, out, steps_ptr, switch_at_ptr, N: tl.constexpr, BLOCK: tl.constexpr): + steps = tl.load(steps_ptr) + switch_at = tl.load(switch_at_ptr) + pointer = tl.make_block_ptr( + base=x0, + shape=(N, ), + strides=(1, ), + offsets=(0, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + for iteration in tl.range(0, steps): + if iteration == switch_at: + pointer = tl.make_block_ptr( + base=x1, + shape=(N, ), + strides=(1, ), + offsets=(4, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + else: + pointer = tl.advance(pointer, (1, )) + value = tl.load(pointer, boundary_check=(0, ), padding_option="zero") + tl.store(out + tl.arange(0, BLOCK), value) + + +def _inputs(): + x0_cpu = torch.arange(INPUT_SIZE, dtype=torch.float32) + x1_cpu = 1000.0 + 2.0 * torch.arange(INPUT_SIZE, dtype=torch.float32) + return x0_cpu, x1_cpu, x0_cpu.npu(), x1_cpu.npu() + + +def _device_i32(value): + return torch.tensor([value], dtype=torch.int32, device="npu") + + +def _assert_output(actual, expected): + torch.npu.synchronize() + torch.testing.assert_close(actual.cpu(), expected, rtol=0, atol=0) + + +@pytest.mark.parametrize("predicate", [0, 1]) +def test_scalar_pointer_if_yield(predicate): + x0_cpu, _, x0, _ = _inputs() + out = torch.empty(1, dtype=torch.float32, device="npu") + + scalar_pointer_if_kernel[(1, )](x0, out, _device_i32(predicate)) + + expected_index = 3 if predicate else 11 + _assert_output(out, x0_cpu[expected_index:expected_index + 1]) + + +@pytest.mark.parametrize("steps", [0, 1, 4]) +def test_scalar_pointer_for_carried(steps): + x0_cpu, _, x0, _ = _inputs() + out = torch.empty(1, dtype=torch.float32, device="npu") + + scalar_pointer_for_kernel[(1, )](x0, out, _device_i32(steps)) + + expected_index = 2 + 3 * steps + _assert_output(out, x0_cpu[expected_index:expected_index + 1]) + + +@pytest.mark.parametrize("steps", [0, 1, 5]) +def test_scalar_pointer_while_carried(steps): + x0_cpu, _, x0, _ = _inputs() + out = torch.empty(1, dtype=torch.float32, device="npu") + + scalar_pointer_while_kernel[(1, )](x0, out, _device_i32(steps)) + + expected_index = 1 + 2 * steps + _assert_output(out, x0_cpu[expected_index:expected_index + 1]) + + +@pytest.mark.parametrize("steps", [0, 1, 5]) +def test_scalar_pointer_while_from_function_args(steps): + x0_cpu, _, x0, _ = _inputs() + out_cpu = torch.zeros(INPUT_SIZE, dtype=torch.float32) + out = out_cpu.npu() + + scalar_pointer_while_from_function_args_kernel[(1, )](x0, out, _device_i32(steps)) + + expected_input_index = 2 * steps + expected_output_index = 3 * steps + expected = torch.zeros_like(out_cpu) + expected[expected_output_index] = x0_cpu[expected_input_index] + _assert_output(out, expected) + + +@pytest.mark.parametrize("steps,switch_at", [(0, 0), (1, 0), (4, 1), (4, 7)]) +def test_scalar_pointer_while_if_backedge(steps, switch_at): + x0_cpu, x1_cpu, x0, x1 = _inputs() + out = torch.empty(1, dtype=torch.float32, device="npu") + + scalar_pointer_while_if_backedge_kernel[(1, )]( + x0, + x1, + out, + _device_i32(steps), + _device_i32(switch_at), + ) + + if switch_at < steps: + expected = x1_cpu[7 + 2 * (steps - switch_at - 1):] + else: + expected = x0_cpu[1 + 2 * steps:] + _assert_output(out, expected[:1]) + + +@pytest.mark.parametrize("predicate", [0, 1]) +@pytest.mark.parametrize("steps", [0, 1, 4]) +def test_scalar_pointer_nested_if_for(predicate, steps): + x0_cpu, _, x0, _ = _inputs() + out = torch.empty(1, dtype=torch.float32, device="npu") + + scalar_pointer_nested_if_for_kernel[(1, )](x0, out, _device_i32(predicate), _device_i32(steps)) + + expected_index = sum(2 if (iteration & 1) == predicate else 1 for iteration in range(steps)) + _assert_output(out, x0_cpu[expected_index:expected_index + 1]) + + +@pytest.mark.parametrize("predicate", [0, 1]) +@pytest.mark.parametrize("steps", [0, 3]) +def test_scalar_pointer_select_into_loop(predicate, steps): + x0_cpu, x1_cpu, x0, x1 = _inputs() + out = torch.empty(1, dtype=torch.float32, device="npu") + + scalar_pointer_select_loop_kernel[(1, )](x0, x1, out, _device_i32(predicate), _device_i32(steps)) + + source = x0_cpu if predicate else x1_cpu + expected_index = (4 if predicate else 7) + 2 * steps + _assert_output(out, source[expected_index:expected_index + 1]) + + +@pytest.mark.parametrize("predicate", [0, 1]) +def test_scalar_pointer_if_with_tensor_mask(predicate): + """Check mixed scalar-pointer/tensor-mask results across one scf.if.""" + block = 8 + limit = 5 + x0_cpu, x1_cpu, x0, x1 = _inputs() + out = torch.empty(block, dtype=torch.float32, device="npu") + + scalar_pointer_if_with_tensor_mask_kernel[(1, )]( + x0, + x1, + out, + _device_i32(predicate), + _device_i32(limit), + BLOCK=block, + ) + + source = x0_cpu if predicate else x1_cpu + offset = 3 if predicate else 7 + valid_count = limit if predicate else block - 2 + expected = torch.full((block, ), -1.0, dtype=torch.float32) + expected[:valid_count] = source[offset:offset + valid_count] + _assert_output(out, expected) + + +@pytest.mark.parametrize("steps", [0, 1, 3]) +def test_tensor_pointer_loop(steps): + """Check a tensor-of-pointers backedge through a real NPU load/store.""" + block = 8 + length = 64 + source_cpu = torch.arange(length, dtype=torch.float32) + source = source_cpu.npu() + tensor_output = torch.empty(block, dtype=torch.float32, device="npu") + steps_device = _device_i32(steps) + + tensor_pointer_loop_kernel[(1, )](source, tensor_output, steps_device, BLOCK=block) + + tensor_start = 6 + 2 * steps + _assert_output(tensor_output, source_cpu[tensor_start:tensor_start + block]) + + +@pytest.mark.parametrize("steps,switch_at", [(0, -1), (3, -1), (3, 1)]) +def test_descriptor_boundary_scalar_regression(steps, switch_at): + x0_cpu, x1_cpu, x0, x1 = _inputs() + out = torch.empty(BLOCK, dtype=torch.float32, device="npu") + + descriptor_boundary_scalar_kernel[(1, )]( + x0, + x1, + out, + _device_i32(steps), + _device_i32(switch_at), + N=INPUT_SIZE, + BLOCK=BLOCK, + ) + + source = x0_cpu + offset = 0 + for iteration in range(steps): + if iteration == switch_at: + source = x1_cpu + offset = 4 + else: + offset += 1 + _assert_output(out, source[offset:offset + BLOCK]) diff --git a/third_party/ascend/unittest/pytest_ut/test_scope.py b/third_party/ascend/unittest/pytest_ut/test_scope.py new file mode 100755 index 0000000000..dfd9688d5c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_scope.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +import os + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + +import pytest +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +# ============== Kernel definitions ============== + + +@triton.jit +def kernel_nested_scope(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + """Test nested scopes.""" + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + with al.scope(core_mode="vector"): + with al.scope(core_mode="vector"): + with al.scope(core_mode="cube"): + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + result = x + y + tl.store(out_ptr + i, result, mask=i < n) + + +@triton.jit +def kernel_scope_escape(x_ptr, out_ptr, n, BLOCK: tl.constexpr): + """Test variable defined inside scope, used outside.""" + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + with al.scope(core_mode="vector"): + x = tl.load(x_ptr + i, mask=i < n) + # Use x outside of the scope + a = x + 1.0 + tl.store(out_ptr + i, a, mask=i < n) + + +@triton.jit +def kernel_scope_cube(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + """Test cube core mode.""" + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + with al.scope(core_mode="cube"): + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + result = x + y + tl.store(out_ptr + i, result, mask=i < n) + + +@triton.jit +def kernel_scope_vector(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + """Test vector core mode.""" + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + with al.scope(core_mode="vector"): + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + result = x + y + tl.store(out_ptr + i, result, mask=i < n) + + +@triton.jit +def kernel_scope_disable_auto_sync(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr): + """Test disable auto sync.""" + i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + with al.scope(core_mode="vector", disable_auto_sync=True): + x = tl.load(x_ptr + i, mask=i < n) + y = tl.load(y_ptr + i, mask=i < n) + result = x + y + tl.store(out_ptr + i, result, mask=i < n) + + +# ============== Pytest tests ============== + + +def test_nested_scope(): + """Test nested scopes compile successfully.""" + mlir = compile_kernel(kernel_nested_scope, {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}) + assert "scope.scope" in mlir + assert len(mlir) > 0 + + +def test_scope_escape(): + """Test variable escaping from scope.""" + mlir = compile_kernel(kernel_scope_escape, {"x_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, {"BLOCK": 256}) + assert "scope.scope" in mlir + assert len(mlir) > 0 + + +def test_scope_cube_mode(): + """Test cube core mode generates correct attributes.""" + mlir = compile_kernel(kernel_scope_cube, {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}) + assert "scope.scope" in mlir + # Check for cube core type attribute + assert "hivm.tcore_type" in mlir or "CUBE" in mlir.upper() + + +def test_scope_vector_mode(): + """Test vector core mode generates correct attributes.""" + mlir = compile_kernel(kernel_scope_vector, {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}) + assert "scope.scope" in mlir + # Check for vector core type attribute + assert "hivm.tcore_type" in mlir or "VECTOR" in mlir.upper() + + +def test_scope_disable_auto_sync(): + """Test disable auto sync generates correct attributes.""" + mlir = compile_kernel( + kernel_scope_disable_auto_sync, + {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}, + ) + assert "scope.scope" in mlir + # Check for disable auto sync attribute + assert "hivm.disable_auto_sync" in mlir + + +# ============== Main for manual testing ============== + +if __name__ == "__main__": + print("=" * 60) + print("Test 1: Nested Scopes") + print("=" * 60) + mlir = compile_kernel(kernel_nested_scope, {"x_ptr": "*fp32", "y_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, + {"BLOCK": 256}) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) + + print("\n" + "=" * 60) + print("Test 2: Scope Escape") + print("=" * 60) + mlir = compile_kernel(kernel_scope_escape, {"x_ptr": "*fp32", "out_ptr": "*fp32", "n": "i32"}, {"BLOCK": 256}) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_scope_buffer.py b/third_party/ascend/unittest/pytest_ut/test_scope_buffer.py new file mode 100644 index 0000000000..1c5ba07809 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_scope_buffer.py @@ -0,0 +1,98 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir( + kernel, + src, + context, + Options(), + {"create_address_space": al.semantic.create_address_space}, + {}, + ) + return str(module) + + +@triton.jit +def helper_use_q(q, XBLOCK: tl.constexpr): + tmp = q + 1.0 + tl.device_assert(tmp.shape[0] == XBLOCK if isinstance(tmp.shape[0], tl.constexpr) else True) + + +@triton.jit +def helper_use_buffer(q_l1, XBLOCK: tl.constexpr): + q = bl.to_tensor(q_l1) + helper_use_q(q, XBLOCK) + + +@triton.jit +def kernel_buffer_arg(XBLOCK: tl.constexpr): + q_l1 = bl.alloc(tl.float32, [XBLOCK], al.ascend_address_space.L1) + helper_use_buffer(q_l1, XBLOCK) + + +@triton.jit +def kernel_scope_for_if_call_buffer(x_ptr, XBLOCK: tl.constexpr): + q_l1 = bl.alloc(tl.float32, [XBLOCK], al.ascend_address_space.L1) + offsets = tl.arange(0, XBLOCK) + with al.scope(core_mode="vector"): + for skv_loop_idx in range(2): + if skv_loop_idx == 0: + q = tl.load(x_ptr + offsets, mask=offsets < XBLOCK) + bl.to_buffer(tensor=q, bind_buffer=q_l1) + else: + q = bl.to_tensor(q_l1) + helper_use_buffer(q_l1, XBLOCK) + + +def test_buffer_argument_to_jit_function(): + mlir = compile_kernel(kernel_buffer_arg, {}, {"XBLOCK": 16}) + assert len(mlir) > 0 + + +def test_buffer_helper_call_inside_scope_loop_if(): + mlir = compile_kernel(kernel_scope_for_if_call_buffer, {"x_ptr": "*fp32"}, {"XBLOCK": 16}) + assert "scf.for" in mlir + assert "scope.scope" in mlir diff --git a/third_party/ascend/unittest/pytest_ut/test_select_analysis.py b/third_party/ascend/unittest/pytest_ut/test_select_analysis.py new file mode 100644 index 0000000000..57d8c3e24b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_select_analysis.py @@ -0,0 +1,438 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def kernel_cal_select_mask_bool( + Output_ptr, + Indices_ptr, + numel: tl.constexpr, + BLOCK: tl.constexpr, +): + offs = tl.arange(0, BLOCK) + index = tl.load(Indices_ptr) + + true_tensor = tl.arange(0, BLOCK) < numel + false_tensor = tl.arange(0, BLOCK) >= numel + mask = offs < index + res = tl.where(mask, true_tensor, false_tensor) + tl.store(Output_ptr + offs, res) + + +@triton.jit +def kernel_cal_select_mask_one_dim_static_offset(QK_ptr, Other_ptr, Output_ptr, BLOCK: tl.constexpr, + start_zero: tl.constexpr): + offs = tl.arange(0, BLOCK) + + qk_ub = tl.load(QK_ptr + offs) + other = tl.load(Other_ptr + offs) + + if start_zero: + mask = offs < 1 + else: + mask = offs >= 1 + + res = tl.where(mask, qk_ub, other) + tl.store(Output_ptr + offs, res) + + +@triton.jit +def kernel_cal_select_mask_one_dim_dyn_offset(QK_ptr, Other_ptr, Output_ptr, Indices_ptr, BLOCK: tl.constexpr, + start_zero: tl.constexpr): + offs = tl.arange(0, BLOCK) + index = tl.load(Indices_ptr) + + qk_ub = tl.load(QK_ptr + offs) + other = tl.load(Other_ptr + offs) + + if start_zero: + mask = offs < index + else: + mask = offs >= index + + res = tl.where(mask, qk_ub, other) + tl.store(Output_ptr + offs, res) + + +@triton.jit +def kernel_cal_select_mask_one_dim_two_dyn_offset( + QK_ptr, + Other_ptr, + Output_ptr, + Indices_ptr, + BLOCK: tl.constexpr, +): + offs = tl.arange(0, BLOCK) + minIndex = tl.load(Indices_ptr) + maxIndex = tl.load(Indices_ptr + 1) + + qk_ub = tl.load(QK_ptr + offs) + other = tl.load(Other_ptr + offs) + + mask = (offs < maxIndex) and (offs >= minIndex) + res = tl.where(mask, qk_ub, other) + tl.store(Output_ptr + offs, res) + + +@triton.jit +def kernel_cal_select_mask_two_dim_static_offset( + QK_ptr, + Other_ptr, + Output_ptr, + BLOCK: tl.constexpr, + start_zero: tl.constexpr, +): + rows = tl.arange(0, BLOCK) + cols = tl.arange(0, BLOCK) + offs = rows[:, None] * BLOCK + cols[None, :] + + qk_ub = tl.load(QK_ptr + offs) + other = tl.load(Other_ptr + offs) + if start_zero: + mask_rows = rows < 1 + mask_cols = cols < 1 + else: + mask_rows = rows >= 1 + mask_cols = cols >= 1 + + res = tl.where(mask_rows[:, None] & mask_cols[None, :], qk_ub, other) + tl.store(Output_ptr + offs, res) + + +@triton.jit +def kernel_cal_select_mask_two_dim_one_dyn_offset( + QK_ptr, + Other_ptr, + Output_ptr, + Indices_ptr, + BLOCK: tl.constexpr, + start_zero: tl.constexpr, +): + rows = tl.arange(0, BLOCK) + cols = tl.arange(0, BLOCK) + offs = rows[:, None] * BLOCK + cols[None, :] + minIndex = tl.load(Indices_ptr) + maxIndex = tl.load(Indices_ptr + 1) + + qk_ub = tl.load(QK_ptr + offs) + other = tl.load(Other_ptr + offs) + if start_zero: + mask_rows = rows < 1 + else: + mask_rows = rows >= 1 + + mask_cols = (cols < maxIndex) & (cols >= minIndex) + + res = tl.where(mask_rows[:, None] & mask_cols[None, :], qk_ub, other) + tl.store(Output_ptr + offs, res) + + +@triton.jit +def kernel_cal_select_mask_two_dim_all_dyn_offset( + QK_ptr, + Other_ptr, + Output_ptr, + Indices_ptr, + BLOCK: tl.constexpr, +): + rows = tl.arange(0, BLOCK) + cols = tl.arange(0, BLOCK) + offs = rows[:, None] * BLOCK + cols[None, :] + min_index1 = tl.load(Indices_ptr) + max_index1 = tl.load(Indices_ptr + 1) + min_index2 = tl.load(Indices_ptr + 2) + max_index2 = tl.load(Indices_ptr + 3) + + qk_ub = tl.load(QK_ptr + offs) + other = tl.load(Other_ptr + offs) + + mask_rows = (rows < max_index1) & (rows >= min_index1) + mask_cols = (cols < max_index2) & (cols >= min_index2) + + res = tl.where(mask_rows[:, None] & mask_cols[None, :], qk_ub, other) + tl.store(Output_ptr + offs, res) + + +@triton.jit +def kernel_cal_select_mask( + QK_ptr, + Other_ptr, + Output_ptr, + Indices_ptr, + stride_qk: tl.constexpr, + numel: tl.constexpr, + BLOCK: tl.constexpr, +): + rows = tl.arange(0, BLOCK) * stride_qk + cols = tl.arange(0, BLOCK) + offs = rows[:, None] + cols[None, :] + row_indices = tl.load(Indices_ptr) + col_indices = tl.load(Indices_ptr + 1) + + qk_ub = tl.load(QK_ptr + offs) + other = tl.load(Other_ptr + offs) + mask_rows = rows < row_indices * stride_qk + mask_cols = cols < col_indices + + res = tl.where(mask_rows[:, None] & mask_cols[None, :], qk_ub, other) + tl.store(Output_ptr + offs, res) + + +def torch_cal_select_mask_bool( + index: torch.Tensor, + numel, + BLOCK, +): + offs = torch.arange(0, BLOCK) + true_tensor = torch.arange(0, BLOCK) < numel + false_tensor = torch.arange(0, BLOCK) >= numel + mask = offs < index + + res = torch.where(mask, true_tensor, false_tensor) + return res + + +def torch_cal_select_mask_one_dim_static_offset( + QK: torch.Tensor, + Other: torch.Tensor, + start_zero, +): + Output = Other.clone() + if start_zero: + Output[:1] = QK[:1] + else: + Output[1:] = QK[1:] + return Output + + +def torch_cal_select_mask_one_dim_dyn_offset( + QK: torch.Tensor, + Other: torch.Tensor, + Indices: torch.Tensor, + start_zero, +): + limit_idx = Indices[0].item() + Output = Other.clone() + if start_zero: + Output[:limit_idx] = QK[:limit_idx] + else: + Output[limit_idx:] = QK[limit_idx:] + return Output + + +def torch_cal_select_mask_one_dim_two_dyn_offset( + QK: torch.Tensor, + Other: torch.Tensor, + Indices: torch.Tensor, +): + min_idx = Indices[0].item() + max_idx = Indices[1].item() + Output = Other.clone() + Output[min_idx:max_idx] = QK[min_idx:max_idx] + + return Output + + +def torch_cal_select_mask_two_dim_static_offset( + QK: torch.Tensor, + Other: torch.Tensor, + start_zero, +): + Output = Other.clone() + if start_zero: + Output[:1, :1] = QK[:1, :1] + else: + Output[1:, 1:] = QK[1:, 1:] + + return Output + + +def torch_cal_select_mask_two_dim_one_dyn_offset( + QK: torch.Tensor, + Other: torch.Tensor, + Indices: torch.Tensor, + start_zero, +): + min_idx = Indices[0].item() + max_idx = Indices[1].item() + Output = Other.clone() + if start_zero: + Output[:1, min_idx:max_idx] = QK[:1, min_idx:max_idx] + else: + Output[1:, min_idx:max_idx] = QK[1:, min_idx:max_idx] + + return Output + + +def torch_cal_select_mask_two_dim_all_dyn_offset( + QK: torch.Tensor, + Other: torch.Tensor, + Indices: torch.Tensor, +): + min_idx1 = Indices[0].item() + max_idx1 = Indices[1].item() + min_idx2 = Indices[2].item() + max_idx2 = Indices[3].item() + Output = Other.clone() + Output[min_idx1:max_idx1, min_idx2:max_idx2] = QK[min_idx1:max_idx1, min_idx2:max_idx2] + + return Output + + +def torch_cal_select_mask( + QK: torch.Tensor, + Other: torch.Tensor, + Indices: torch.Tensor, +): + row_limit_idx = Indices[0].item() + col_limit_idx = Indices[1].item() + Output = Other.clone() + Output[:row_limit_idx, :col_limit_idx] = QK[:row_limit_idx, :col_limit_idx] + return Output + + +@pytest.mark.parametrize('param_list', [['bool', 64, 63]]) +def test_select_analysis_bool(param_list): + dtype, SEQ_LEN, index = param_list + assert dtype == 'bool' + qk_cal = torch.empty(SEQ_LEN).npu() + indices = torch.tensor([index]).npu() + qk_ref = torch_cal_select_mask_bool(index, SEQ_LEN, SEQ_LEN) + kernel_cal_select_mask_bool[(1, )](qk_cal, indices, SEQ_LEN, SEQ_LEN) + test_common.validate_cmp(dtype, qk_cal, qk_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', 64, True], + ['float32', 64, False], +]) +def test_select_analysis_one_dim_static_offset(param_list): + dtype, SEQ_LEN, start_zero = param_list + assert dtype != 'bool' + qk = torch.rand([SEQ_LEN], dtype=eval('torch.' + dtype), device='npu') + qk_cal = torch.empty_like(qk).npu() + other = torch.zeros_like(qk).npu() + qk_ref = torch_cal_select_mask_one_dim_static_offset(qk, other, start_zero) + kernel_cal_select_mask_one_dim_static_offset[(1, )](qk, other, qk_cal, SEQ_LEN, start_zero) + test_common.validate_cmp(dtype, qk_cal, qk_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', 64, 62, True], + ['float32', 64, 62, False], +]) +def test_select_analysis_one_dim_dyn_offset(param_list): + dtype, SEQ_LEN, index, start_zero = param_list + assert dtype != 'bool' + qk = torch.rand([SEQ_LEN], dtype=eval('torch.' + dtype), device='npu') + qk_cal = torch.empty_like(qk).npu() + other = torch.zeros_like(qk).npu() + indices_tensor = torch.tensor([index]).npu() + qk_ref = torch_cal_select_mask_one_dim_dyn_offset(qk, other, indices_tensor, start_zero) + kernel_cal_select_mask_one_dim_dyn_offset[(1, )](qk, other, qk_cal, indices_tensor, SEQ_LEN, start_zero) + test_common.validate_cmp(dtype, qk_cal, qk_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', 64, 1, 62], + ['float32', 64, 1, 62], +]) +def test_select_analysis_one_dim_two_dyn_offset(param_list): + dtype, SEQ_LEN, minIndex, maxIndex = param_list + assert dtype != 'bool' + qk = torch.rand([SEQ_LEN], dtype=eval('torch.' + dtype), device='npu') + qk_cal = torch.empty_like(qk).npu() + other = torch.zeros_like(qk).npu() + indices_tensor = torch.tensor([minIndex, maxIndex]).npu() + qk_ref = torch_cal_select_mask_one_dim_two_dyn_offset(qk, other, indices_tensor) + kernel_cal_select_mask_one_dim_two_dyn_offset[(1, )](qk, other, qk_cal, indices_tensor, SEQ_LEN) + test_common.validate_cmp(dtype, qk_cal, qk_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', 64, True], + ['float32', 64, False], +]) +def test_select_analysis_two_dim_static_offset(param_list): + dtype, SEQ_LEN, start_zero = param_list + assert dtype != 'bool' + qk = torch.rand([SEQ_LEN, SEQ_LEN], dtype=eval('torch.' + dtype), device='npu') + qk_cal = torch.empty_like(qk).npu() + other = torch.zeros_like(qk).npu() + qk_ref = torch_cal_select_mask_two_dim_static_offset(qk, other, start_zero) + kernel_cal_select_mask_two_dim_static_offset[(1, )](qk, other, qk_cal, SEQ_LEN, start_zero) + test_common.validate_cmp(dtype, qk_cal, qk_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', 64, 1, 62, True], + ['float32', 64, 1, 62, False], +]) +def test_select_analysis_two_dim_one_dyn_offset(param_list): + dtype, SEQ_LEN, minIndex, maxIndex, start_zero = param_list + assert dtype != 'bool' + + qk = torch.rand([SEQ_LEN, SEQ_LEN], dtype=eval('torch.' + dtype), device='npu') + qk_cal = torch.empty_like(qk).npu() + other = torch.zeros_like(qk).npu() + indices_tensor = torch.tensor([minIndex, maxIndex]).npu() + qk_ref = torch_cal_select_mask_two_dim_one_dyn_offset(qk, other, indices_tensor, start_zero) + kernel_cal_select_mask_two_dim_one_dyn_offset[(1, )](qk, other, qk_cal, indices_tensor, SEQ_LEN, start_zero) + test_common.validate_cmp(dtype, qk_cal, qk_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', 64, 2, 62, 1, 63], + ['float32', 64, 2, 62, 1, 63], +]) +def test_select_analysis_two_dim_all_dyn_offset(param_list): + dtype, SEQ_LEN, minIndex1, maxIndex1, minIndex2, maxIndex2 = param_list + assert dtype != 'bool' + + qk = torch.rand([SEQ_LEN, SEQ_LEN], dtype=eval('torch.' + dtype), device='npu') + qk_cal = torch.empty_like(qk).npu() + other = torch.zeros_like(qk).npu() + indices_tensor = torch.tensor([minIndex1, maxIndex1, minIndex2, maxIndex2]).npu() + qk_ref = torch_cal_select_mask_two_dim_all_dyn_offset(qk, other, indices_tensor) + kernel_cal_select_mask_two_dim_all_dyn_offset[(1, )](qk, other, qk_cal, indices_tensor, SEQ_LEN) + test_common.validate_cmp(dtype, qk_cal, qk_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', 64, 63, 62], + ['float32', 64, 63, 62], +]) +def test_select_analysis(param_list): + dtype, SEQ_LEN, indice_x, indice_y = param_list + assert dtype != 'bool' + qk = torch.rand([SEQ_LEN, SEQ_LEN], dtype=eval('torch.' + dtype), device='npu') + qk_cal = torch.empty_like(qk).npu() + other = torch.zeros_like(qk).npu() + indices_tensor = torch.tensor([indice_x, indice_y]).npu() + qk_ref = torch_cal_select_mask(qk, other, indices_tensor) + kernel_cal_select_mask[(1, )](qk, other, qk_cal, indices_tensor, qk.stride(0), SEQ_LEN * SEQ_LEN, SEQ_LEN) + test_common.validate_cmp(dtype, qk_cal, qk_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_divsiop.py b/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_divsiop.py new file mode 100644 index 0000000000..12e2166723 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_divsiop.py @@ -0,0 +1,84 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_divsiop_select_analysis(offs_num, divnum, maxindex, index, query): + idx = query[index] + offs_m = torch.arange(offs_num, dtype=torch.int32).npu() + query_pos = offs_m // divnum - idx + mask = query_pos < maxindex + + tensor0 = torch.tensor(0, dtype=torch.int32).npu() + tensor1 = torch.tensor(1, dtype=torch.int32).npu() + result = torch.where(mask, tensor1, tensor0).npu() + return result + + +@triton.jit +def divsiop_select_analysis_kernel1(index, out_ptr, query, offs_num: tl.constexpr, divnum: tl.constexpr, + maxindex: tl.constexpr): + idx = tl.load(query + index) + offs_m = tl.arange(0, offs_num) + + query_pos = offs_m // divnum - idx + + mask = query_pos < maxindex + query_mask = tl.where(mask, 1, 0).to(tl.int1) + tl.store(out_ptr + tl.arange(0, offs_num), query_mask) + + +@triton.jit +def divsiop_select_analysis_kernel2(index, out_ptr, query, offs_num: tl.constexpr, divnum: tl.constexpr, + maxindex: tl.constexpr): + idx = tl.load(query + index) + offs_m = tl.arange(0, offs_num) + + query_pos = -idx + offs_m // divnum + mask = query_pos < maxindex + + query_mask = tl.where(mask, 1, 0).to(tl.int1) + tl.store(out_ptr + tl.arange(0, offs_num), query_mask) + + +@pytest.mark.parametrize('param_list', [[16, 4, 2, index] for index in range(0, 4)]) +def test_divsiop_select_analysis1(param_list): + offs_num, divnum, maxindex, index = param_list + query = torch.tensor(range(0, divnum)).npu() + y_ref = torch_divsiop_select_analysis(offs_num, divnum, maxindex, index, query).npu() + y_cal = torch.full((offs_num, ), 2, dtype=torch.int32).npu() + divsiop_select_analysis_kernel1[(1, )](index, y_cal, query, offs_num, divnum, maxindex) + test_common.validate_cmp('int32', y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [[16, 4, 2, index] for index in range(0, 4)]) +def test_divsiop_select_analysis2(param_list): + offs_num, divnum, maxindex, index = param_list + query = torch.tensor(range(0, divnum)).npu() + y_ref = torch_divsiop_select_analysis(offs_num, divnum, maxindex, index, query).npu() + y_cal = torch.full((offs_num, ), 2, dtype=torch.int32).npu() + divsiop_select_analysis_kernel2[(1, )](index, y_cal, query, offs_num, divnum, maxindex) + test_common.validate_cmp('int32', y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_invert.py b/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_invert.py new file mode 100644 index 0000000000..64abd9b3cb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_invert.py @@ -0,0 +1,153 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def cal_atten_mask_kernel( + QK_ptr, + Indices_ptr, + stride_qk_m, + stride_qk_n, + stride_ik, + SEQ_LEN: tl.constexpr, + sparse_block_size: tl.constexpr, + BLOCK_SBS: tl.constexpr, + TOPK_BASE: tl.constexpr, +): + pid_m = tl.program_id(axis=0) + pid_n = tl.program_id(axis=1) + + idx_sub_sbs = pid_n + cur_s1 = pid_m * BLOCK_SBS + cur_s2 = cur_s1 + BLOCK_SBS + + if cur_s1 >= SEQ_LEN: + return + + beg_sbs = idx_sub_sbs * BLOCK_SBS // sparse_block_size + end_sbs = ((idx_sub_sbs + 1) * BLOCK_SBS) // sparse_block_size + + valid_col_end = cur_s1 + (cur_s2 - cur_s1) + + offs_m = cur_s1 + tl.arange(0, BLOCK_SBS) + offs_n_base = idx_sub_sbs * BLOCK_SBS + offs_n = offs_n_base + tl.arange(0, BLOCK_SBS) + + mask_m = offs_m < SEQ_LEN + mask_n = offs_n < SEQ_LEN + mask_load = mask_m[:, None] & mask_n[None, :] + + qk_ub = tl.load(QK_ptr + offs_m[:, None] * stride_qk_m + offs_n[None, :] * stride_qk_n, mask=mask_load, other=0.0) + + for idx_k in range(beg_sbs, end_sbs): + idx_s2 = tl.load(Indices_ptr + TOPK_BASE + idx_k * stride_ik) + if idx_s2 != -1 and idx_s2 * sparse_block_size > valid_col_end: + idx_lower_sbs = idx_k * sparse_block_size - \ + idx_sub_sbs * BLOCK_SBS + idx_higher_sbs = (idx_k + 1) * sparse_block_size - \ + idx_sub_sbs * BLOCK_SBS + mask_lower_sbs = tl.arange(0, BLOCK_SBS) >= idx_lower_sbs + mask_higher_sbs = tl.arange(0, BLOCK_SBS) < idx_higher_sbs + qk_ub = tl.where((mask_lower_sbs & mask_higher_sbs)[None, :], float("-inf"), qk_ub) + + tl.store(QK_ptr + offs_m[:, None] * stride_qk_m + offs_n[None, :] * stride_qk_n, qk_ub, mask=mask_load) + + +def launch_cal_atten_mask(qk_tensor, indices_tensor, sparse_block_size=64, block_sbs=128): + """ + qk_tensor: (SEQ_LEN, SEQ_LEN) + indices_tensor: (K,) / (BATCH, K, ...) + """ + assert qk_tensor.is_contiguous() + M, N = qk_tensor.shape + + stride_qk_m = qk_tensor.stride(0) + stride_qk_n = qk_tensor.stride(1) + + stride_ik = 1 + topk_base = 0 + + grid = (triton.cdiv(M, block_sbs), triton.cdiv(N, block_sbs)) + cal_atten_mask_kernel[grid]( + qk_tensor, + indices_tensor, + stride_qk_m, + stride_qk_n, + stride_ik, + SEQ_LEN=M, + sparse_block_size=sparse_block_size, + BLOCK_SBS=block_sbs, + TOPK_BASE=topk_base, + ) + return qk_tensor + + +def torch_cal_atten_mask( + qk, + indices, + sparse_block_size, + block_sbs, + topk_base=0, +): + device = qk.device + dtype = qk.dtype + M, N = qk.shape + + row_ids = torch.arange(M, device=device).unsqueeze(1) + col_ids = torch.arange(N, device=device).unsqueeze(0) + + k_idx_global = col_ids // sparse_block_size + lookup_idx = k_idx_global + topk_base + max_valid_idx = indices.numel() - 1 + + valid_lookup = (lookup_idx >= 0) & (lookup_idx <= max_valid_idx) + safe_lookup_idx = lookup_idx.clamp(0, max_valid_idx) + idx_s2_map = indices.gather(0, safe_lookup_idx.squeeze(0)).unsqueeze(0) + idx_s2_map = torch.where(valid_lookup, idx_s2_map, torch.tensor(-1, device=device)) + + row_block_ends = ((row_ids // block_sbs) + 1) * block_sbs + row_block_ends = torch.min(row_block_ends, torch.tensor(N, device=device)) + + start_pos_k_map = idx_s2_map * sparse_block_size + cond_valid = (idx_s2_map != -1) + cond_exceed = (start_pos_k_map > row_block_ends) + final_mask = cond_valid & cond_exceed + + qk_out = torch.where(final_mask, torch.tensor(float("-inf"), dtype=dtype, device=device), qk) + return qk_out + + +@pytest.mark.parametrize('param_list', [['float32', 1024, 128, 64]]) +def test_divsiop_select_analysis1(param_list): + dtype, SEQ_LEN, BLOCK_SBS, SPARSE_BLOCK = param_list + qk = torch.zeros((SEQ_LEN, SEQ_LEN), dtype=eval('torch.' + dtype), device='npu') + K_SIZE = 20 + indices = torch.full((K_SIZE, ), -1, dtype=torch.int32, device='npu') + indices[10] = 20 + qk_ref = torch_cal_atten_mask(qk.clone(), indices, sparse_block_size=SPARSE_BLOCK, block_sbs=BLOCK_SBS) + qk_cal = launch_cal_atten_mask(qk, indices, sparse_block_size=SPARSE_BLOCK, block_sbs=BLOCK_SBS) + test_common.validate_cmp(dtype, qk_cal, qk_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_iterarg.py b/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_iterarg.py new file mode 100644 index 0000000000..95dcce8b43 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_select_analysis_for_iterarg.py @@ -0,0 +1,173 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def kernel_where_blocked_iter_arg( + X_ptr, + Other_ptr, + Out_ptr, + N, + BLOCK: tl.constexpr, +): + offs = tl.arange(0, BLOCK) + range_val = offs + + for start in range(0, N, BLOCK): + x = tl.load(X_ptr + range_val) + other = tl.load(Other_ptr + range_val) + mask = range_val < N + res = tl.where(mask, x, other) + tl.store(Out_ptr + range_val, res) + range_val = range_val + BLOCK + + +def torch_where_blocked_iter_arg(X, Other, N, BLOCK): + Out = torch.zeros_like(X) + range_val = 0 + for start in range(0, N, BLOCK): + end = min(range_val + BLOCK, N) + Out[range_val:end] = X[range_val:end] + if end < range_val + BLOCK: + Out[end:range_val + BLOCK] = Other[end:range_val + BLOCK] + range_val = range_val + BLOCK + return Out + + +@pytest.mark.parametrize('param_list', [ + ['float32', 300, 64], + ['float32', 50, 64], + ['float32', 192, 64], +]) +def test_select_analysis_where_iterarg(param_list): + dtype, N, BLOCK = param_list + x = torch.rand(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + other = torch.rand(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + out = torch.zeros(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + ref = torch_where_blocked_iter_arg(x, other, N, BLOCK) + + kernel_where_blocked_iter_arg[(1, )](x, other, out, N, BLOCK) + + test_common.validate_cmp(dtype, out, ref) + + +@triton.jit +def kernel_where_blocked_iter_arg_neg_step( + X_ptr, + Other_ptr, + Out_ptr, + N, + LB: tl.constexpr, + BLOCK: tl.constexpr, +): + offs = tl.arange(0, BLOCK) + range_val = offs + + for start in range(LB, N, -BLOCK): + x = tl.load(X_ptr + range_val) + other = tl.load(Other_ptr + range_val) + mask = range_val < N + res = tl.where(mask, x, other) + tl.store(Out_ptr + range_val, res) + range_val = range_val + BLOCK + + +def torch_where_blocked_iter_arg_neg_step(X, Other, N, LB, BLOCK): + Out = torch.zeros_like(X) + range_val = 0 + for start in range(LB, N, -BLOCK): + end = min(range_val + BLOCK, N) + Out[range_val:end] = X[range_val:end] + if end < range_val + BLOCK: + Out[end:range_val + BLOCK] = Other[end:range_val + BLOCK] + range_val = range_val + BLOCK + return Out + + +@pytest.mark.parametrize('param_list', [ + ['float32', 140, 320, 64], + ['float32', 192, 320, 64], +]) +def test_select_analysis_where_iterarg_neg_step(param_list): + dtype, N, LB, BLOCK = param_list + x = torch.rand(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + other = torch.rand(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + out = torch.zeros(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + ref = torch_where_blocked_iter_arg_neg_step(x, other, N, LB, BLOCK) + + kernel_where_blocked_iter_arg_neg_step[(1, )](x, other, out, N, LB, BLOCK) + + test_common.validate_cmp(dtype, out, ref) + + +@triton.jit +def kernel_where_blocked_iter_arg_nonzero_lb( + X_ptr, + Other_ptr, + Out_ptr, + N, + START: tl.constexpr, + BLOCK: tl.constexpr, +): + offs = tl.arange(0, BLOCK) + range_val = offs + + for start in range(START, N, BLOCK): + x = tl.load(X_ptr + range_val) + other = tl.load(Other_ptr + range_val) + mask = range_val < N + res = tl.where(mask, x, other) + tl.store(Out_ptr + range_val, res) + range_val = range_val + BLOCK + + +def torch_where_blocked_iter_arg_nonzero_lb(X, Other, N, START, BLOCK): + Out = torch.zeros_like(X) + range_val = 0 + for start in range(START, N, BLOCK): + end = min(range_val + BLOCK, N) + Out[range_val:end] = X[range_val:end] + if end < range_val + BLOCK: + Out[end:range_val + BLOCK] = Other[end:range_val + BLOCK] + range_val = range_val + BLOCK + return Out + + +@pytest.mark.parametrize('param_list', [ + ['float32', 300, 16, 64], + ['float32', 256, 128, 64], +]) +def test_select_analysis_where_iterarg_nonzero_lb(param_list): + dtype, N, START, BLOCK = param_list + x = torch.rand(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + other = torch.rand(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + out = torch.zeros(N + BLOCK, dtype=eval('torch.' + dtype), device='npu') + ref = torch_where_blocked_iter_arg_nonzero_lb(x, other, N, START, BLOCK) + + kernel_where_blocked_iter_arg_nonzero_lb[(1, )](x, other, out, N, START, BLOCK) + + test_common.validate_cmp(dtype, out, ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_sigmoid.py b/third_party/ascend/unittest/pytest_ut/test_sigmoid.py new file mode 100644 index 0000000000..f2d141dc80 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sigmoid.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import pytest +import test_common + + +def torch_sigmoid(x0, x1): + res = x0 + torch.sigmoid(x1) + return res + + +@triton.jit +def triton_sigmoid(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.load(in_ptr1 + (x0), xmask) + tmp2 = tmp0 + tl.sigmoid(tmp1) + tl.store(out_ptr0 + (xindex), tmp2, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_sigmoid(param_list): + # 生成数据 + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + # torch结果 + y_ref = torch_sigmoid(x0, x1) + # triton结果 + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_sigmoid[ncore, 1, 1](x0, x1, y_cal, x0.numel(), xblock, xblock_sub) + # 比较结果 + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_signbit.py b/third_party/ascend/unittest/pytest_ut/test_signbit.py new file mode 100644 index 0000000000..65d518fa6e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_signbit.py @@ -0,0 +1,77 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common +import triton.language.extra.cann.libdevice as libdevice + + +@triton.jit +def triton_signbit(in_ptr0, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = tl.load(in_ptr0 + xindex, xmask) + y = libdevice.signbit(x0) + tl.store(out_ptr0 + xindex, y, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_signbit_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch.signbit(x).npu() + y_cal = torch.zeros(shape).bool().npu() + triton_signbit[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub) + test_common.validate_cmp('bool', y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_all_blocks_parallel(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch.signbit(x).npu() + y_cal = torch.zeros(shape).bool().npu() + triton_signbit[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub) + test_common.validate_cmp('bool', y_cal, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_auto_blockify(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch.signbit(x).npu() + y_cal = torch.zeros(shape).bool().npu() + triton_signbit[ncore, 1, 1](x, y_cal, x.numel(), xblock, xblock_sub, auto_blockify_size=ncore) + test_common.validate_cmp('bool', y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_silu.py b/third_party/ascend/unittest/pytest_ut/test_silu.py new file mode 100644 index 0000000000..757fbcae3e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_silu.py @@ -0,0 +1,68 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = x0 * (1 / (1 + torch.exp(-x0))) + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = x * (1 / (1 + tl.math.exp(-x))) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + + +@pytest.mark.parametrize('dtype, sigtype', [ + (torch.float32, 'float32'), +]) +@pytest.mark.parametrize('N, NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_simplify_iterargs.py b/third_party/ascend/unittest/pytest_ut/test_simplify_iterargs.py new file mode 100644 index 0000000000..d1dabaf669 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_simplify_iterargs.py @@ -0,0 +1,78 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +@triton.jit +def triton_fn_expanddims(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, YBLOCK: tl.constexpr, YBLOCK_SUB: tl.constexpr): + base1 = tl.arange(0, XBLOCK)[:, None] + base2 = tl.arange(0, YBLOCK_SUB)[None, :] + loops1: tl.constexpr = YBLOCK // YBLOCK_SUB # assume it's divisible + for _ in range(loops1): + x0 = base1 * YBLOCK + base2 + base2 = base2 + YBLOCK_SUB + tmp0 = tl.load(in_ptr0 + (x0), None) + tl.store(out_ptr0 + (x0), tmp0, None) + + +@triton.jit +def triton_fn_broadcast(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, YBLOCK: tl.constexpr, YBLOCK_SUB: tl.constexpr): + base1 = tl.arange(0, XBLOCK)[:, None] + base2 = tl.arange(0, YBLOCK_SUB)[None, :] + base2 = base2.broadcast_to((XBLOCK, YBLOCK_SUB)) + loops1: tl.constexpr = YBLOCK // YBLOCK_SUB # assume it's divisible + for _ in range(loops1): + x0 = base1 * YBLOCK + base2 + base2 = base2 + YBLOCK_SUB + tmp0 = tl.load(in_ptr0 + (x0), None) + tl.store(out_ptr0 + (x0), tmp0, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (128, 128), 128, 128, 32], +]) +def test_expanddims(param_list): + dtype, shape, xblock, yblock, yblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_fn_expanddims[(1, )](x0, y_cal, xblock, yblock, yblock_sub) + test_common.validate_cmp(dtype, y_cal, x0) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (128, 128), 128, 128, 32], +]) +def test_broadcast(param_list): + dtype, shape, xblock, yblock, yblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_fn_broadcast[(1, )](x0, y_cal, xblock, yblock, yblock_sub) + test_common.validate_cmp(dtype, y_cal, x0) diff --git a/third_party/ascend/unittest/pytest_ut/test_sin.py b/third_party/ascend/unittest/pytest_ut/test_sin.py new file mode 100644 index 0000000000..e17ee71b7f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sin.py @@ -0,0 +1,99 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = torch.sin(x0) + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = tl.sin(x) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + # (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + # (torch.int8, 'int8'), + # (torch.int16, 'int16'), + # (torch.int32, 'int32'), + # (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwise_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_sinh.py b/third_party/ascend/unittest/pytest_ut/test_sinh.py new file mode 100644 index 0000000000..81227bd868 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sinh.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +@triton.jit +def triton_sinh(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.sinh(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', ['float32', 'float16', 'bfloat16']) +def test_sinh_special(param_list): + dtype = param_list + x_near_zero = torch.linspace(-1.0, 1.0, 256, dtype=eval("torch." + dtype)).npu() + x_medium_neg = torch.linspace(-10.0, -1.0, 128, dtype=eval("torch." + dtype)).npu() + x_medium_pos = torch.linspace(1.0, 10.0, 128, dtype=eval("torch." + dtype)).npu() + + x_large_neg = torch.linspace(-1e4, -10.0, 64, dtype=eval("torch." + dtype)).npu() + x_large_pos = torch.linspace(10.0, 1e4, 64, dtype=eval("torch." + dtype)).npu() + + x0 = torch.cat([x_near_zero, x_medium_neg, x_medium_pos, x_large_neg, x_large_pos], dim=0) + + y_ref = torch.sinh(x0) + y_cal = torch.zeros_like(y_ref) + triton_sinh[1, 1, 1](x0, y_cal, x0.shape[0], x0.shape[0]) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_sink_broadcast.py b/third_party/ascend/unittest/pytest_ut/test_sink_broadcast.py new file mode 100644 index 0000000000..99b347246d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sink_broadcast.py @@ -0,0 +1,89 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +@triton.jit +def triton_sink_broadcast1(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, YBLOCK: tl.constexpr): + base1 = tl.arange(0, XBLOCK)[:, None] * YBLOCK + base2 = tl.arange(0, YBLOCK)[None, :] + base1 = base1.broadcast_to((XBLOCK, YBLOCK)) + tmp0 = tl.load(in_ptr0 + base1, None) + index = base1 + base2 + tl.store(out_ptr0 + index, tmp0, None) + + +@triton.jit +def triton_sink_broadcast2(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, YBLOCK: tl.constexpr): + base1 = tl.arange(0, XBLOCK)[:, None] * YBLOCK + base2 = tl.arange(0, YBLOCK)[None, :] + base1 = base1.broadcast_to((XBLOCK, YBLOCK)) + tmp0 = tl.load(in_ptr0 + base1, base1 < XBLOCK * YBLOCK, other=0.0) + index = base1 + base2 + tl.store(out_ptr0 + index, tmp0, index < XBLOCK * YBLOCK) + + +@triton.jit +def triton_sink_broadcast3(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, YBLOCK: tl.constexpr): + base1 = (tl.arange(0, XBLOCK) * YBLOCK)[:, None] + base2 = tl.arange(0, YBLOCK)[None, :] + base1 = base1.broadcast_to((XBLOCK, YBLOCK)) + tmp0 = tl.load(in_ptr0 + base1 + base2, (base1 + base2) < XBLOCK * YBLOCK, other=0.0) + index = base1 + base2 + tl.store(out_ptr0 + index, tmp0, index < XBLOCK * YBLOCK) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (32, 32), 32, 32], +]) +def test_sink_broadcast(param_list): + dtype, shape, xblock, yblock = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = x0.clone() + y_ref = y_ref[:, 0].unsqueeze(1).expand(-1, x0.size(1)) + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + y_cal2 = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_sink_broadcast1[(1, )](x0, y_cal, xblock, yblock) + triton_sink_broadcast2[(1, )](x0, y_cal2, xblock, yblock) + test_common.validate_cmp(dtype, y_cal, y_ref) + test_common.validate_cmp(dtype, y_cal2, y_ref) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (32, 32), 32, 32], +]) +def test_sink_broadcast3(param_list): + dtype, shape, xblock, yblock = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = x0.clone() + + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_sink_broadcast3[(1, )](x0, y_cal, xblock, yblock) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_softmax.py b/third_party/ascend/unittest/pytest_ut/test_softmax.py new file mode 100644 index 0000000000..b5db90e5d0 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_softmax.py @@ -0,0 +1,172 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import test_common +import pytest +from triton.compiler.errors import CompilationError + + +def naive_softmax(x): + # read MN elements ; write M elements + x_max = x.max(dim=1)[0] + # read MN + M elements ; write MN elements + z = x - x_max[:, None] + # read MN elements ; write MN elements + numerator = torch.exp(z) + # read MN elements ; write M elements + denominator = numerator.sum(dim=1) + # read MN + M elements ; write MN elements + ret = numerator / denominator[:, None] + # in total: read 5MN + 2M elements ; wrote 3MN + 2M elements + return ret + + +@triton.jit +def softmax_kernel(output_ptr, input_ptr, input_row_stride, output_row_stride, n_rows, n_cols, + BLOCK_SIZE: tl.constexpr): + # starting row of the program + row_start = tl.program_id(0) + row_step = tl.num_programs(0) + for row_idx in tl.range(row_start, n_rows, row_step): + # The stride represents how much we need to increase the pointer to advance 1 row + row_start_ptr = input_ptr + row_idx * input_row_stride + # The block size is the next power of two greater than n_cols, so we can fit each + # row in a single block + col_offsets = tl.arange(0, BLOCK_SIZE) + input_ptrs = row_start_ptr + col_offsets + # Load the row into SRAM, using a mask since BLOCK_SIZE may be > than n_cols + mask = col_offsets < n_cols + row = tl.load(input_ptrs, mask=mask, other=-float('inf')) + softmax_output = tl.softmax(row) + # Write back output to DRAM + output_row_start_ptr = output_ptr + row_idx * output_row_stride + output_ptrs = output_row_start_ptr + col_offsets + tl.store(output_ptrs, softmax_output, mask=mask) + + +kernels = {} + + +def softmax(x, stream): + n_rows, n_cols = x.shape + + BLOCK_SIZE = triton.next_power_of_2(n_cols) + + y = torch.empty_like(x) + + kernel, num_programs = kernels.get(BLOCK_SIZE, (None, 0)) + if kernel is None: + num_programs = 32 + kernel = softmax_kernel + kernels[BLOCK_SIZE] = (kernel, num_programs) + + num_programs = min(num_programs, n_rows) + + # Create a number of persistent programs. + kernel[(num_programs, 1, 1)](y, x, x.stride(0), y.stride(0), n_rows, n_cols, BLOCK_SIZE) + return y + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + (torch.bfloat16, 'bfloat16'), +] + +shapes = [ + (1823, 781), + (1823, 2), + (1823, 4), + (1823, -32), + (1823, -100), + (1823, -256), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.skip(reason="randomly failed") +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('M, N', shapes) +def test_softmax(dtype, sigtype, M, N): + torch_npu.npu.utils.set_device(0) + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M] if M in map_for_64_t else M + N = map_for_64_t[N] if N in map_for_64_t else N + + device = torch.npu.current_device() + stream = torch.npu.current_stream(device).npu_stream + torch.manual_seed(0) + x = torch.randn(M, N, dtype=dtype, device='npu') + y_triton = softmax(x, stream) + y_torch = torch.softmax(x, axis=1) + test_common.validate_cmp(sigtype, y_triton, y_torch) + + +@triton.jit +def softmax_dim_kernel(input_ptr, output_ptr, M: tl.constexpr, N: tl.constexpr, dim: tl.constexpr, + keep_dims: tl.constexpr): + row_offsets = tl.arange(0, M) + col_offsets = tl.arange(0, N) + idx = row_offsets[:, None] * N + col_offsets[None, :] + x = tl.load(input_ptr + idx) + ret = tl.softmax(x, dim=dim, keep_dims=keep_dims) + tl.store(output_ptr + idx, ret) + + +dim_keep_dims_cases = [ + (0, False), + (0, True), + (1, True), + (-1, True), + (-2, False), +] + + +@pytest.mark.parametrize('dtype, sigtype', types) +@pytest.mark.parametrize('dim, keep_dims', dim_keep_dims_cases) +def test_softmax_dim_keep_dims(dtype, sigtype, dim, keep_dims): + torch_npu.npu.utils.set_device(0) + torch.manual_seed(0) + M, N = 32, 128 + x = torch.randn(M, N, dtype=dtype, device='npu') + + y_triton = torch.empty_like(x) + softmax_dim_kernel[(1, )](x, y_triton, M, N, dim, keep_dims) + + y_torch = torch.softmax(x, dim=dim) + test_common.validate_cmp(sigtype, y_triton, y_torch) + + +def test_softmax_dim1_without_keep_dims_broadcast_fails(): + torch_npu.npu.utils.set_device(0) + M, N = 32, 128 + x = torch.randn(M, N, dtype=torch.float32, device='npu') + y = torch.empty_like(x) + + with pytest.raises(CompilationError): + softmax_dim_kernel[(1, )](x, y, M, N, 1, False) diff --git a/third_party/ascend/unittest/pytest_ut/test_sort.py b/third_party/ascend/unittest/pytest_ut/test_sort.py new file mode 100644 index 0000000000..4a998afb6e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sort.py @@ -0,0 +1,127 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import pytest +import numpy as np +import torch +import triton.language as tl +import triton.language.extra.cann.extension as al +import test_common + +# --------------- +# test sort op +# --------------- + + +@triton.jit +def sort_kernel_2d(X, Z, N: tl.constexpr, M: tl.constexpr, descending: tl.constexpr): + offx = tl.arange(0, M) + offy = tl.arange(0, N) * M + off2d = offx[None, :] + offy[:, None] + x = tl.load(X + off2d) + x = al.sort(x, descending=descending, dim=1) + tl.store(Z + off2d, x) + + +@pytest.mark.interpreter +@pytest.mark.parametrize("shape", [(256, 16)]) +@pytest.mark.parametrize("descending", [False, True]) +@pytest.mark.parametrize("dtype", ['int8', 'int16', 'float16', 'float32', 'bfloat16', 'bool']) +def test_sort_2d(shape, descending, dtype): + if dtype == "bool": + x = test_common.generate_tensor(shape, dtype) + np_sorted = np.sort(x) + if descending: + np_sorted = np_sorted[:, ::-1].copy() + torch_res = torch.from_numpy(np_sorted).npu() + else: + x = test_common.generate_tensor(shape, dtype).npu() + torch_res = torch.sort(x, descending=descending)[0] + + x = x.npu() + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + N = x.shape[0] + M = x.shape[1] + sort_kernel_2d[(1, )](x, triton_res, N, M, descending) + assert (torch_res == triton_res).all(), (torch_res, triton_res) + + +@triton.jit +def sort_kernel_3d(X, Z, D0: tl.constexpr, D1: tl.constexpr, D2: tl.constexpr, descending: tl.constexpr): + off2 = tl.arange(0, D2) + off1 = tl.arange(0, D1) * D2 + off0 = tl.arange(0, D0) * D1 * D2 + + off = off2[None, None, :] + off1[None, :, None] + off0[:, None, None] + x = tl.load(X + off) + + x = al.sort(x, descending=descending, dim=2) + + tl.store(Z + off, x) + + +@pytest.mark.interpreter +@pytest.mark.parametrize("shape", [(8, 4, 16)]) +@pytest.mark.parametrize("descending", [False, True]) +@pytest.mark.parametrize("dtype", ['int8', 'int16', 'float16', 'float32', 'bfloat16', 'bool']) +def test_sort_3d(shape, descending, dtype): + if dtype == "bool": + x = test_common.generate_tensor(shape, dtype) + np_sorted = np.sort(x) + if descending: + np_sorted = np_sorted[:, :, ::-1].copy() + torch_res = torch.from_numpy(np_sorted).npu() + else: + x = test_common.generate_tensor(shape, dtype).npu() + torch_res = torch.sort(x, descending=descending)[0] + + x = x.npu() + triton_res = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + D0 = x.shape[0] + D1 = x.shape[1] + D2 = x.shape[2] + sort_kernel_3d[(1, )](x, triton_res, D0, D1, D2, descending) + assert (torch_res == triton_res).all(), (torch_res, triton_res) + + +@triton.jit +def sort_kernel_1d(X, Z, M: tl.constexpr, descending: tl.constexpr): + off = tl.arange(0, M) + x = tl.load(X + off) + x = al.sort(x, descending=descending, dim=0) + tl.store(Z + off, x) + + +@triton.jit +def sort_call_kernel(X, Z, M: tl.constexpr, descending: tl.constexpr): + sort_kernel_1d(X, Z, M, descending) + + +def test_sort_call(): + shape = (8, ) + descending = True + x = torch.randn(size=shape, dtype=torch.float32).npu() + triton_res = torch.empty_like(x) + M = x.shape[0] + sort_call_kernel[(1, )](x, triton_res, M, descending) + + torch_res = torch.sort(x, descending=descending)[0] + assert (torch_res == triton_res).all(), (torch_res, triton_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_specialization_cache_npu.py b/third_party/ascend/unittest/pytest_ut/test_specialization_cache_npu.py new file mode 100644 index 0000000000..094da5e393 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_specialization_cache_npu.py @@ -0,0 +1,206 @@ +"""NPU integration tests for Ascend specialization and compile options. + +Run this file only with a configured CANN/TorchNPU environment, for example:: + + ASCEND_RT_VISIBLE_DEVICES=0 pytest -q test_specialization_cache_npu.py + +The device-independent unit suite covers SIMT-only and exact cache-key policy; +this file intentionally repeats the three acceptance paths end to end with +real compilation counts and NPU output checks. Current 910B hardware does not +advertise the SIMT execution mode. + +Legacy option cases also check argument handling through JIT and Inductor. +""" + +import pytest +import torch +import torch_npu # noqa: F401 # registers the NPU backend with PyTorch + +import triton +import triton.language as tl + + +@triton.jit +def _add_value(src_ptr, dst_ptr, n, value, BLOCK: tl.constexpr): + offsets = tl.arange(0, BLOCK) + mask = offsets < n + src = tl.load(src_ptr + offsets, mask=mask, other=0) + tl.store(dst_ptr + offsets, src + value, mask=mask) + + +@triton.jit +def _add_annotated_value(src_ptr, dst_ptr, n, value: tl.int32, BLOCK: tl.constexpr): + offsets = tl.arange(0, BLOCK) + mask = offsets < n + src = tl.load(src_ptr + offsets, mask=mask, other=0) + tl.store(dst_ptr + offsets, src + value, mask=mask) + + +def _clear_kernel_cache(kernel=_add_value): + device = torch.npu.current_device() + kernel.device_caches[device][0].clear() + + +def _run_case(n, src, dst, mode): + _add_value[(1, )](src, dst, n, 3, BLOCK=32, compile_mode=mode) + torch.npu.synchronize() + expected = src[:n] + 3 + torch.testing.assert_close(dst[:n], expected) + + +def _run_annotated_case(n, value, src, dst, mode): + _add_annotated_value[(1, )](src, dst, n, value, BLOCK=32, compile_mode=mode) + torch.npu.synchronize() + expected = src[:n] + value + torch.testing.assert_close(dst[:n], expected) + + +def _assert_aligned_and_unaligned_pair(first, second): + assert {first.data_ptr() % 16, second.data_ptr() % 16} == {0, first.element_size()} + + +def _pointer_views(src_base, dst_base, offsets, changed_pointer): + src_offsets = offsets if changed_pointer in {"src", "both"} else (0, 0) + dst_offsets = offsets if changed_pointer in {"dst", "both"} else (0, 0) + src = tuple(src_base[offset:offset + 16] for offset in src_offsets) + dst = tuple(dst_base[offset:offset + 16] for offset in dst_offsets) + + for name, views in (("src", src), ("dst", dst)): + if changed_pointer in {name, "both"}: + _assert_aligned_and_unaligned_pair(*views) + else: + assert views[0].data_ptr() == views[1].data_ptr() + + return src, dst + + +@pytest.fixture(autouse=True) +def _reset_cache_hook(): + _clear_kernel_cache() + _clear_kernel_cache(_add_annotated_value) + old_hook = triton.knobs.runtime.jit_cache_hook + try: + yield + finally: + triton.knobs.runtime.jit_cache_hook = old_hook + _clear_kernel_cache() + _clear_kernel_cache(_add_annotated_value) + + +@pytest.mark.parametrize("mode", ["simd"]) +@pytest.mark.parametrize("values", [(16, 17), (17, 16)]) +def test_simd_integer_alignment_reuses_compilation(mode, values): + src = torch.arange(32, dtype=torch.int32, device="npu") + dst = torch.empty_like(src) + compile_count = 0 + + def count_compile(**_kwargs): + nonlocal compile_count + compile_count += 1 + + triton.knobs.runtime.jit_cache_hook = count_compile + _run_case(values[0], src, dst, mode) + _run_case(values[1], src, dst, mode) + + assert compile_count == 1 + + +@pytest.mark.parametrize("mode", ["simd"]) +@pytest.mark.parametrize("offsets", [(0, 1), (1, 0)], ids=["aligned-first", "unaligned-first"]) +@pytest.mark.parametrize("changed_pointer", ["src", "dst", "both"], ids=["src-only", "dst-only", "src-and-dst"]) +def test_simd_pointer_alignment_reuses_compilation(mode, offsets, changed_pointer): + src_base = torch.arange(17, dtype=torch.int32, device="npu") + dst_base = torch.empty_like(src_base) + compile_count = 0 + + def count_compile(**_kwargs): + nonlocal compile_count + compile_count += 1 + + triton.knobs.runtime.jit_cache_hook = count_compile + src, dst = _pointer_views(src_base, dst_base, offsets, changed_pointer) + _run_case(16, src[0], dst[0], mode) + _run_case(16, src[1], dst[1], mode) + + assert compile_count == 1 + + +@pytest.mark.parametrize("mode", ["simd"]) +@pytest.mark.parametrize("values", [(1, 2), (2, 1)]) +def test_simd_integer_one_still_recompiles(mode, values): + src = torch.arange(32, dtype=torch.int32, device="npu") + dst = torch.empty_like(src) + compile_count = 0 + + def count_compile(**_kwargs): + nonlocal compile_count + compile_count += 1 + + triton.knobs.runtime.jit_cache_hook = count_compile + _run_case(values[0], src, dst, mode) + _run_case(values[1], src, dst, mode) + + assert compile_count == 2 + + +@pytest.mark.parametrize("mode", ["simd"]) +@pytest.mark.parametrize("values", [(1, 2), (2, 1)]) +def test_simd_annotated_integer_one_is_constexpr_and_recompiles(mode, values): + src = torch.arange(32, dtype=torch.int32, device="npu") + dst = torch.empty_like(src) + compile_records = [] + + def record_compile(**kwargs): + compile_info = kwargs["compile"] + compile_records.append((compile_info["signature"], compile_info["constants"])) + + triton.knobs.runtime.jit_cache_hook = record_compile + _run_annotated_case(32, values[0], src, dst, mode) + _run_annotated_case(32, values[1], src, dst, mode) + + assert len(compile_records) == 2 + for value, (signature, constants) in zip(values, compile_records): + if value == 1: + assert signature["value"] == "constexpr" + assert constants[(3, )] == 1 + else: + assert signature["value"] == "i32" + assert (3, ) not in constants + + +@pytest.mark.parametrize("compile_graph", [False, True], ids=["jit", "inductor"]) +@pytest.mark.parametrize( + "name, value", + [("use_bytecode", False), ("warp_size", 1), ("stream", 0), ("enable_vf_fusion", True), ("enable_vf_fusion", False)], +) +def test_legacy_option_is_not_a_kernel_argument(compile_graph, name, value): + if compile_graph: + from torch.utils._triton import has_triton_package + + # Older PyTorch checks triton_key, which Triton 3.6 no longer exposes. + if not has_triton_package(): + pytest.skip(f"PyTorch {torch.__version__} cannot recognize Triton {triton.__version__} for Inductor") + # Register NPU support before Dynamo checks whether Triton is available. + import torch_npu._inductor # noqa: F401 + + from torch._inductor.codecache import CacheBase + + # Some TorchNPU builds still import triton_key when constructing cache keys. + try: + CacheBase.get_system() + except ImportError as error: + if error.name != "triton.compiler.compiler" or "triton_key" not in str(error): + raise + pytest.skip(f"TorchNPU {torch_npu.__version__} requires the legacy Triton triton_key API") + + legacy_options = {name: value} + + def add_one(x): + result = torch.empty_like(x) + _add_value[(1, )](x, result, x.numel(), 1, BLOCK=32, **legacy_options) + return result + + run = torch.compile(add_one, fullgraph=True) if compile_graph else add_one + x = torch.arange(17, device="npu", dtype=torch.float32) + + torch.testing.assert_close(run(x), x + 1) diff --git a/third_party/ascend/unittest/pytest_ut/test_specialization_cache_unit.py b/third_party/ascend/unittest/pytest_ut/test_specialization_cache_unit.py new file mode 100644 index 0000000000..98102df38e --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_specialization_cache_unit.py @@ -0,0 +1,251 @@ +"""Device-independent unit tests for Ascend specialization cache keys. + +These tests exercise binder/native-specializer policy without CANN or an NPU. +The NPU integration suite separately verifies compilation counts and outputs. +""" + +import inspect + +import pytest +import torch +from triton.backends.ascend.compiler import AscendBackend +from triton.backends.compiler import GPUTarget +from triton.runtime.jit import KernelParam, compute_cache_key, create_function_from_signature + +pytestmark = pytest.mark.backend("native") + + +class PointerArg: + dtype = torch.float32 + + def __init__(self, address): + self.address = address + + def data_ptr(self): + return self.address + + +class CountingAscendBackend(AscendBackend): + + def __init__(self): + super().__init__(GPUTarget("npu", "Ascend910B", 32)) + self.alignment_specialization_calls = 0 + + def use_alignment_specialization(self, options): + self.alignment_specialization_calls += 1 + return super().use_alignment_specialization(options) + + +def make_binder(backend=None, do_not_specialize_on_alignment=False): + + def kernel(value, pointer): + pass + + signature = inspect.signature(kernel) + params = [ + KernelParam(i, param, False, do_not_specialize_on_alignment) + for i, param in enumerate(signature.parameters.values()) + ] + if backend is None: + backend = AscendBackend(GPUTarget("npu", "Ascend910B", 32)) + return create_function_from_signature(signature, params, backend) + + +def bind_and_key(binder, cache, value, address, options): + _, specialization, raw_options = binder(value, PointerArg(address), **options) + return specialization, compute_cache_key(cache, specialization, raw_options) + + +SIMD_OPTIONS = [ + pytest.param({"compile_mode": "simd"}, id="simd"), + pytest.param({"compile_mode": "unstructured_in_simt"}, id="unstructured-in-simt"), + pytest.param({"force_simt_template": True}, id="force-simt-template"), + pytest.param({}, id="default-simd-simt-template"), +] + +SIMT_OPTIONS = [ + pytest.param({"compile_mode": "simt_only"}, id="simt-only"), + pytest.param({"force_simt_only": True}, id="force-simt-only"), + pytest.param({"compile_mode": "simd", "force_simt_only": True}, id="force-simt-overrides-mode"), +] + + +def test_alignment_policy_is_evaluated_once_per_binder_call(): + backend = CountingAscendBackend() + binder = make_binder(backend) + + binder(16, PointerArg(0x1000), compile_mode="simd") + assert backend.alignment_specialization_calls == 1 + + binder(17, PointerArg(0x1004), compile_mode="simt_only") + assert backend.alignment_specialization_calls == 2 + + +def test_alignment_policy_is_not_evaluated_when_unused(): + backend = CountingAscendBackend() + binder = make_binder(backend, do_not_specialize_on_alignment=True) + + binder(16, PointerArg(0x1000), compile_mode="simd") + + assert backend.alignment_specialization_calls == 0 + + +def test_alignment_policy_is_not_evaluated_for_non_specialized_annotation(): + + def kernel(value): + pass + + kernel.__annotations__["value"] = "fp32" + signature = inspect.signature(kernel) + params = [KernelParam(0, next(iter(signature.parameters.values())), False, False)] + backend = CountingAscendBackend() + binder = create_function_from_signature(signature, params, backend) + + _, specialization, _ = binder(1.25, compile_mode="simd") + + assert specialization == [("fp32", None)] + assert backend.alignment_specialization_calls == 0 + + +def test_alignment_policy_is_not_evaluated_when_value_specialization_is_disabled(): + + def kernel(value): + pass + + signature = inspect.signature(kernel) + params = [KernelParam(0, next(iter(signature.parameters.values())), True, False)] + backend = CountingAscendBackend() + binder = create_function_from_signature(signature, params, backend) + + _, specialization, _ = binder(16, compile_mode="simd") + + assert specialization == [("i32", None)] + assert backend.alignment_specialization_calls == 0 + + +@pytest.mark.parametrize("options", SIMD_OPTIONS) +@pytest.mark.parametrize("values", [(16, 17), (17, 16)], ids=["aligned-first", "unaligned-first"]) +def test_simd_integer_alignment_reuses_cache_key(options, values): + binder = make_binder() + cache = {} + first, first_key = bind_and_key(binder, cache, values[0], 0x1004, options) + second, second_key = bind_and_key(binder, cache, values[1], 0x1004, options) + + assert first_key == second_key + assert first[0][1] == second[0][1] == "" + + +@pytest.mark.parametrize("options", [ + pytest.param({"compile_mode": "simd"}, id="simd"), + pytest.param({"compile_mode": "simd_simt_template"}, id="simd-simt-template"), +]) +@pytest.mark.parametrize( + "values, expected_type", + [ + pytest.param((-16, -15), "i32", id="negative-i32"), + pytest.param((1 << 32, (1 << 32) + 1), "i64", id="i64"), + pytest.param((1 << 63, (1 << 63) + 1), "u64", id="u64"), + ], +) +def test_simd_integer_alignment_reuses_cache_key_across_integer_types(options, values, expected_type): + binder = make_binder() + cache = {} + first, first_key = bind_and_key(binder, cache, values[0], 0x1004, options) + second, second_key = bind_and_key(binder, cache, values[1], 0x1004, options) + + assert first_key == second_key + assert first[0] == second[0] == (expected_type, "") + + +@pytest.mark.parametrize("options", SIMD_OPTIONS) +@pytest.mark.parametrize("addresses", [(0x1000, 0x1004), (0x1004, 0x1000)], ids=["aligned-first", "unaligned-first"]) +def test_simd_pointer_alignment_reuses_cache_key(options, addresses): + binder = make_binder() + cache = {} + first, first_key = bind_and_key(binder, cache, 17, addresses[0], options) + second, second_key = bind_and_key(binder, cache, 17, addresses[1], options) + + assert first_key == second_key + assert first[1][1] == second[1][1] == "" + + +@pytest.mark.parametrize("options", SIMT_OPTIONS) +@pytest.mark.parametrize("values", [(16, 17), (17, 16)], ids=["aligned-first", "unaligned-first"]) +def test_simt_integer_alignment_keeps_distinct_cache_keys(options, values): + binder = make_binder() + cache = {} + first, first_key = bind_and_key(binder, cache, values[0], 0x1004, options) + second, second_key = bind_and_key(binder, cache, values[1], 0x1004, options) + + assert first_key != second_key + assert {first[0][1], second[0][1]} == {"", "D"} + + +@pytest.mark.parametrize("options", SIMT_OPTIONS) +@pytest.mark.parametrize("addresses", [(0x1000, 0x1004), (0x1004, 0x1000)], ids=["aligned-first", "unaligned-first"]) +def test_simt_pointer_alignment_keeps_distinct_cache_keys(options, addresses): + binder = make_binder() + cache = {} + first, first_key = bind_and_key(binder, cache, 17, addresses[0], options) + second, second_key = bind_and_key(binder, cache, 17, addresses[1], options) + + assert first_key != second_key + assert {first[1][1], second[1][1]} == {"", "D"} + + +@pytest.mark.parametrize("options", SIMD_OPTIONS + SIMT_OPTIONS) +@pytest.mark.parametrize("values", [(1, 2), (2, 1)], ids=["one-first", "one-second"]) +def test_integer_one_keeps_distinct_cache_key(options, values): + binder = make_binder() + cache = {} + first, first_key = bind_and_key(binder, cache, values[0], 0x1004, options) + second, second_key = bind_and_key(binder, cache, values[1], 0x1004, options) + + assert first_key != second_key + assert {first[0][0], second[0][0]} == {"constexpr", "i32"} + + +@pytest.mark.parametrize("options", SIMD_OPTIONS + SIMT_OPTIONS) +def test_annotated_integer_one_keeps_constexpr_specialization(options): + + def kernel(value): + pass + + annotation = "i32" + kernel.__annotations__["value"] = annotation + signature = inspect.signature(kernel) + params = [KernelParam(0, next(iter(signature.parameters.values())), False, False)] + backend = AscendBackend(GPUTarget("npu", "Ascend910B", 32)) + binder = create_function_from_signature(signature, params, backend) + cache = {} + + _, one_specialization, one_options = binder(1, **options) + _, two_specialization, two_options = binder(2, **options) + + assert one_specialization == [("constexpr", 1)] + assert two_specialization == [(annotation, "")] + assert compute_cache_key(cache, one_specialization, + one_options) != compute_cache_key(cache, two_specialization, two_options) + + +@pytest.mark.parametrize("options", [SIMD_OPTIONS[0], *SIMT_OPTIONS]) +@pytest.mark.parametrize("values", [(1, 2), (16, 17)], ids=["value-specialization", "alignment-specialization"]) +def test_annotated_integer_do_not_specialize_keeps_legacy_cache_key(options, values): + + def kernel(value): + pass + + kernel.__annotations__["value"] = "i32" + signature = inspect.signature(kernel) + params = [KernelParam(0, next(iter(signature.parameters.values())), True, False)] + backend = AscendBackend(GPUTarget("npu", "Ascend910B", 32)) + binder = create_function_from_signature(signature, params, backend) + cache = {} + + _, first_specialization, first_options = binder(values[0], **options) + first_key = compute_cache_key(cache, first_specialization, first_options) + _, second_specialization, second_options = binder(values[1], **options) + second_key = compute_cache_key(cache, second_specialization, second_options) + + assert first_specialization == second_specialization == [("i32", None)] + assert first_key == second_key diff --git a/third_party/ascend/unittest/pytest_ut/test_split.py b/third_party/ascend/unittest/pytest_ut/test_split.py new file mode 100644 index 0000000000..96af5366b5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_split.py @@ -0,0 +1,72 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl + +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, output_ptr1, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + + xx, yy = tl.split(X) + + oidx = xidx[:, None] * YB + yidx[None, :] + + tl.store(output_ptr + oidx, xx) + tl.store(output_ptr1 + oidx, yy) + + +@pytest.mark.parametrize('para_type,data_type,XB,YB,ZB', [ + ['float32', torch.float32, 8, 8, 2], + ['float16', torch.float16, 8, 8, 2], + ['int8', torch.int8, 8, 128, 2], + ['int8', torch.int8, 8, 8, 2], +]) +def test_split(para_type, data_type, XB, YB, ZB): + + x = torch.randint(low=-128, high=128, size=(XB, YB, ZB), dtype=data_type).npu() + + a, b = torch.split(x, 1, dim=-1) + a = a.reshape(XB, YB) + b = b.reshape(XB, YB) + print(a) + print(b) + + output = torch.randint(1, (XB, YB), dtype=data_type).npu() + output1 = torch.randint(1, (XB, YB), dtype=data_type).npu() + fn_npu_[1, 1, 1](output, x, output1, XB, YB, ZB, debug=True) + + print(output) + print(output1) + + test_common.validate_cmp(para_type, a, output) + test_common.validate_cmp(para_type, b, output1) diff --git a/third_party/ascend/unittest/pytest_ut/test_sqrt.py b/third_party/ascend/unittest/pytest_ut/test_sqrt.py new file mode 100644 index 0000000000..f0926d2a9c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sqrt.py @@ -0,0 +1,94 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = torch.sqrt(x0) + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = tl.math.sqrt(x) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + # float64 is not exercised here: test_common's tensor/compare helpers don't support it. +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_static_print_and_assert.py b/third_party/ascend/unittest/pytest_ut/test_static_print_and_assert.py new file mode 100644 index 0000000000..132245122c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_static_print_and_assert.py @@ -0,0 +1,187 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest +import test_common +import functools +import os +import re + +shape = (8, ) +XS = 8 +XVALS_INT = [ + 0, -128, # torch.iinfo(torch.int8).min + 127, # torch.iinfo(torch.int8).max + -32768, # torch.iinfo(torch.int16).min + 32767, # torch.iinfo(torch.int16).max + -2147483648, # torch.iinfo(torch.int32).min + 2147483647, # torch.iinfo(torch.int32).max + 9223372036854775807 +] # torch.iinfo(torch.int64).max + +XVALS_FP = [ + 0.0000000000e+00, # 0 + 1.1921000009e-07, # torch.finfo(torch.float32).eps + 9.7655999707e-04, # torch.finfo(torch.float16).eps + 7.8125000000e-03, # torch.finfo(torch.bfloat16).eps + 3.4027999388e+38, # torch.finfo(torch.float32).max + 6.5504000000e+04, # torch.finfo(torch.float16).max + 3.3894999515e+38, # torch.finfo(torch.bfloat16).max + 1.0000000000e+00 +] # 1 + + +def torch_func(x0, x1): + res = x0 + x1 + return res + + +@triton.jit +def triton_kernel(out_ptr0, in_ptr0, in_ptr1, XBLOCK: tl.constexpr, print_data_ptr: tl.constexpr, + assert_data_ptr: tl.constexpr): + idx = tl.arange(0, XBLOCK) + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.load(in_ptr1 + idx) + tmp2 = tmp0 + tmp1 + tl.static_print(print_data_ptr) + tl.static_assert(assert_data_ptr == assert_data_ptr, "assert_data should equal assert_data") + tl.store(out_ptr0 + idx, tmp2) + + +def triton_func(x0, x1, XS, print_data_ptr, assert_data_ptr): + out = torch.empty_like(x0) + triton_kernel[1, 1, 1](out, x0, x1, XS, print_data_ptr, assert_data_ptr) + return out + + +@pytest.mark.skip(reason="waiting for TA to support") +@pytest.mark.parametrize('sigtype', ['int8']) +@test_common.capture_output("-128") +def test_static_print_int8(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS, -128, XVALS_INT[0]) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.skip(reason="waiting for TA to support") +@pytest.mark.parametrize('sigtype', ['int16']) +@test_common.capture_output("-32768") +def test_static_print_int16(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS, -32768, XVALS_INT[2]) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.skip(reason="waiting for TA to support") +@pytest.mark.parametrize('sigtype', ['int32']) +@test_common.capture_output("-2147483648") +def test_static_print_int32(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS, -2147483648, XVALS_INT[4]) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.skip(reason="waiting for TA to support") +@pytest.mark.parametrize('sigtype', ['int64']) +@test_common.capture_output("9223372036854775807") +def test_static_print_int64(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS, 9223372036854775807, XVALS_INT[-1]) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.skip(reason="waiting for TA to support") +@pytest.mark.parametrize('sigtype', ['float16']) +@test_common.capture_output("1.1921000009e-07") +def test_static_print_float16(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_FP[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS, 1.1921000009e-07, XVALS_FP[1]) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.skip(reason="waiting for TA to support") +@pytest.mark.parametrize('sigtype', ['float32']) +@test_common.capture_output("0.0078125") +def test_static_print_float32(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_FP[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS, 7.8125000000e-03, XVALS_FP[0]) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.skip(reason="waiting for TA to support") +@pytest.mark.parametrize('sigtype', ['bfloat16']) +@test_common.capture_output("0.00097655999707") +def test_static_print_bfloat16(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_FP[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS, 9.7655999707e-04, XVALS_FP[2]) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) + + +@pytest.mark.skip(reason="waiting for TA to support") +@pytest.mark.parametrize('sigtype', ['int8']) +@test_common.capture_output("True") +def test_static_print_bool(capsys, sigtype): + dtype = eval(f"torch.{sigtype}") + x0 = torch.zeros(shape, dtype=dtype).npu() + x1 = torch.ones(shape, dtype=dtype).npu() + for i in range(x1.numel()): + x1[i] = XVALS_INT[i] + torch_ref = torch_func(x0, x1) + triton_cal = triton_func(x0, x1, XS, True, True) + test_common.validate_cmp(sigtype, triton_cal, torch_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_store_broadcast.py b/third_party/ascend/unittest/pytest_ut/test_store_broadcast.py new file mode 100644 index 0000000000..716b2b4c90 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_store_broadcast.py @@ -0,0 +1,200 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + + +@triton.jit +def zero_stride_store_broadcast_kernel( + ptr, + M: tl.constexpr, + N: tl.constexpr, + STRIDE_M: tl.constexpr, + STRIDE_N: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, +): + offs_m = tl.arange(0, BLOCK_M)[:, None] + offs_n = tl.arange(0, BLOCK_N)[None, :] + mask = (offs_m < M) & (offs_n < N) + offsets = offs_m * STRIDE_M + offs_n * STRIDE_N + + # Every logical row aliases the same backing row, so all writers store the + # same value. This makes the end-to-end result deterministic while keeping + # the full-rank zero-stride pointer broadcast that StoreConverter must defer. + # Match the triggering FlagGems path: 0.0 is a DSL scalar, represented as + # a uniform full-rank TTIR value, so every aliased row writes the same value. + tl.store(ptr + offsets, 0.0, mask=mask) + + +def test_zero_stride_pointer_broadcast_store(): + base = torch.full((1, 4), 7.0, dtype=torch.float32, device="npu") + expanded = base.expand(3, 4) + original_stride = expanded.stride() + original_data_ptr = expanded.data_ptr() + + zero_stride_store_broadcast_kernel[(1, 1)]( + expanded, + M=expanded.size(0), + N=expanded.size(1), + STRIDE_M=expanded.stride(0), + STRIDE_N=expanded.stride(1), + BLOCK_M=16, + BLOCK_N=64, + ) + torch.npu.synchronize() + + torch.testing.assert_close(base, torch.zeros_like(base)) + torch.testing.assert_close(expanded, torch.zeros_like(expanded)) + assert expanded.stride() == original_stride == (0, 1) + assert expanded.data_ptr() == original_data_ptr + + +@triton.jit +def kernel_store_diff_axis_broadcast( + X_ptr, + Out_ptr, + M, + XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, +): + offsetx = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK) # [XBLOCK] + offsety = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK) # [YBLOCK] + + val = tl.load(X_ptr + offsety[None, :]) # [1, YBLOCK] + val = tl.broadcast_to(val, (XBLOCK, YBLOCK)) # [XBLOCK, YBLOCK] + + ptr = Out_ptr + offsety[None, :] # [1, YBLOCK] + ptr = tl.broadcast_to(ptr, (XBLOCK, YBLOCK)) # [XBLOCK, YBLOCK] + mask = (offsetx < M)[:, None] + + tl.store(ptr, val, mask) + + +def torch_store_diff_axis_broadcast(X, M, XBLOCK, YBLOCK): + out = torch.zeros((XBLOCK, YBLOCK), dtype=X.dtype) + if M > 0: + out[0, :] = X[:YBLOCK] + return out + + +@pytest.mark.parametrize('param_list', [ + ['float32', 140, 256, 64], + ['float32', 0, 256, 64], + ['float32', 256, 256, 64], +]) +def test_store_diff_axis_broadcast(param_list): + dtype, M, XBLOCK, YBLOCK = param_list + x = test_common.generate_tensor((YBLOCK, ), dtype).npu() + out = torch.zeros((XBLOCK, YBLOCK), dtype=eval('torch.' + dtype)).npu() + ref = torch_store_diff_axis_broadcast(x.cpu(), M, XBLOCK, YBLOCK) + + kernel_store_diff_axis_broadcast[(1, 1)](x, out, M, XBLOCK, YBLOCK) + + test_common.validate_cmp(dtype, out, ref) + + +@triton.jit +def kernel_store_same_axis_broadcast( + X_ptr, + Out_ptr, + YM, + XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, +): + offsetx = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK) # [XBLOCK] + offsety = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK) # [YBLOCK] + + val = tl.load(X_ptr + offsety[None, :]) # [1, YBLOCK] + val = tl.broadcast_to(val, (XBLOCK, YBLOCK)) # [XBLOCK, YBLOCK] + + ptr = Out_ptr + offsety[None, :] # [1, YBLOCK] + ptr = tl.broadcast_to(ptr, (XBLOCK, YBLOCK)) # [XBLOCK, YBLOCK] + mask = (offsety < YM)[None, :] + + tl.store(ptr, val, mask) + + +def torch_store_same_axis_broadcast(X, YM, XBLOCK, YBLOCK): + out = torch.zeros((XBLOCK, YBLOCK), dtype=X.dtype) + out[0, :min(YM, YBLOCK)] = X[:min(YM, YBLOCK)] + return out + + +@pytest.mark.parametrize('param_list', [ + ['float32', 40, 256, 64], + ['float32', 64, 256, 64], +]) +def test_store_same_axis_broadcast(param_list): + dtype, YM, XBLOCK, YBLOCK = param_list + x = test_common.generate_tensor((YBLOCK, ), dtype).npu() + out = torch.zeros((XBLOCK, YBLOCK), dtype=eval('torch.' + dtype)).npu() + ref = torch_store_same_axis_broadcast(x.cpu(), YM, XBLOCK, YBLOCK) + + kernel_store_same_axis_broadcast[(1, 1)](x, out, YM, XBLOCK, YBLOCK) + + test_common.validate_cmp(dtype, out, ref) + + +@triton.jit +def kernel_store_no_mask_broadcast( + X_ptr, + Out_ptr, + XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr, +): + offsetx = tl.program_id(0) * XBLOCK + tl.arange(0, XBLOCK) # [XBLOCK] + offsety = tl.program_id(1) * YBLOCK + tl.arange(0, YBLOCK) # [YBLOCK] + + val = tl.load(X_ptr + offsety[None, :]) # [1, YBLOCK] + val = tl.broadcast_to(val, (XBLOCK, YBLOCK)) # [XBLOCK, YBLOCK] + + ptr = Out_ptr + offsety[None, :] # [1, YBLOCK] + ptr = tl.broadcast_to(ptr, (XBLOCK, YBLOCK)) # [XBLOCK, YBLOCK] + + tl.store(ptr, val) + + +def torch_store_no_mask_broadcast(X, XBLOCK, YBLOCK): + out = torch.zeros((XBLOCK, YBLOCK), dtype=X.dtype) + out[0, :] = X[:YBLOCK] + return out + + +@pytest.mark.parametrize('param_list', [ + ['float32', 256, 64], + ['float32', 128, 32], +]) +def test_store_no_mask_broadcast(param_list): + dtype, XBLOCK, YBLOCK = param_list + x = test_common.generate_tensor((YBLOCK, ), dtype).npu() + out = torch.zeros((XBLOCK, YBLOCK), dtype=eval('torch.' + dtype)).npu() + ref = torch_store_no_mask_broadcast(x.cpu(), XBLOCK, YBLOCK) + + kernel_store_no_mask_broadcast[(1, 1)](x, out, XBLOCK, YBLOCK) + + test_common.validate_cmp(dtype, out, ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_store_new_mask.py b/third_party/ascend/unittest/pytest_ut/test_store_new_mask.py new file mode 100644 index 0000000000..2a7f717897 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_store_new_mask.py @@ -0,0 +1,120 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import itertools +import random, time + + +@triton.jit +def triton_store_new_mask( + in_ptr0, + out_ptr0, + mask_ptr, + cache_modifier: tl.constexpr, + eviction_policy: tl.constexpr, + BLOCK_0: tl.constexpr, + BLOCK_1: tl.constexpr, + BLOCK_2: tl.constexpr, + BLOCK_3: tl.constexpr, + BLOCK_4: tl.constexpr, + BLOCK_5: tl.constexpr, + BLOCK_6: tl.constexpr, + BLOCK_7: tl.constexpr, + SHAPE_0: tl.constexpr, + SHAPE_1: tl.constexpr, + SHAPE_2: tl.constexpr, + SHAPE_3: tl.constexpr, + SHAPE_4: tl.constexpr, + SHAPE_5: tl.constexpr, + SHAPE_6: tl.constexpr, + SHAPE_7: tl.constexpr, + STRIDE_0: tl.constexpr, + STRIDE_1: tl.constexpr, + STRIDE_2: tl.constexpr, + STRIDE_3: tl.constexpr, + STRIDE_4: tl.constexpr, + STRIDE_5: tl.constexpr, + STRIDE_6: tl.constexpr, + STRIDE_7: tl.constexpr, +): + offsets = tl.program_id(0) + + offsets = offsets + tl.arange(0, BLOCK_0) * STRIDE_0 + masks = tl.arange(0, BLOCK_0) < SHAPE_0 + + if (BLOCK_1 * BLOCK_2 * BLOCK_3 * BLOCK_4 * BLOCK_5 * BLOCK_6 * BLOCK_7) > 1: + offsets = offsets[:, None] + tl.arange(0, BLOCK_1)[None, :] * STRIDE_1 + masks = masks[:, None] & (tl.arange(0, BLOCK_1)[None, :] < SHAPE_1) + if (BLOCK_2 * BLOCK_3 * BLOCK_4 * BLOCK_5 * BLOCK_6 * BLOCK_7) > 1: + offsets = offsets[:, :, None] + tl.arange(0, BLOCK_2)[None, None, :] * STRIDE_2 + masks = masks[:, :, None] & (tl.arange(0, BLOCK_2)[None, None, :] < SHAPE_2) + if (BLOCK_3 * BLOCK_4 * BLOCK_5 * BLOCK_6 * BLOCK_7) > 1: + offsets = offsets[:, :, :, None] + tl.arange(0, BLOCK_3)[None, None, None, :] * STRIDE_3 + masks = masks[:, :, :, None] & (tl.arange(0, BLOCK_3)[None, None, None, :] < SHAPE_3) + if (BLOCK_4 * BLOCK_5 * BLOCK_6 * BLOCK_7) > 1: + offsets = offsets[:, :, :, :, None] + tl.arange(0, BLOCK_4)[None, None, None, None, :] * STRIDE_4 + masks = masks[:, :, :, :, None] & (tl.arange(0, BLOCK_4)[None, None, None, None, :] < SHAPE_4) + if (BLOCK_5 * BLOCK_6 * BLOCK_7) > 1: + offsets = offsets[:, :, :, :, :, None] + tl.arange(0, BLOCK_5)[None, None, None, None, None, :] * STRIDE_5 + masks = masks[:, :, :, :, :, None] & (tl.arange(0, BLOCK_5)[None, None, None, None, None, :] < SHAPE_5) + if (BLOCK_6 * BLOCK_7) > 1: + offsets = offsets[:, :, :, :, :, :, + None] + tl.arange(0, BLOCK_6)[None, None, None, None, None, None, :] * STRIDE_6 + masks = masks[:, :, :, :, :, :, None] & (tl.arange(0, BLOCK_6)[None, None, None, None, None, None, :] < SHAPE_6) + if BLOCK_7 > 1: + offsets = offsets[:, :, :, :, :, :, :, + None] + tl.arange(0, BLOCK_7)[None, None, None, None, None, None, None, :] * STRIDE_7 + masks = masks[:, :, :, :, :, :, :, None] & (tl.arange(0, BLOCK_7)[None, None, None, None, None, None, None, :] + < SHAPE_7) + + mask = tl.load(mask_ptr + offsets) != 0 + tmp_in = tl.load(in_ptr0 + offsets) + tmp_out = tmp_in + tl.store(out_ptr0 + offsets, tmp_out, mask=mask, cache_modifier=cache_modifier, eviction_policy=eviction_policy) + + +testlist = [ + (15, 2, 2, 2, 3, 2, 1), +] + +cache_modifier = [None, ".wb", ".cs", ".cg", ".wt"] +eviction_policy = [None, "evict_first", "evict_last"] + +seed = int(time.strftime('%Y%m%d%H')) +random.seed(seed) +all_back_combinations = list(itertools.product(cache_modifier, eviction_policy)) +testlist1 = [random.choice(all_back_combinations)] + + +@pytest.mark.parametrize('shape', testlist) +@pytest.mark.parametrize('dtype', ["bfloat16"]) +@pytest.mark.parametrize('cache_modifier, eviction_policy', testlist1) +def test_store_new_mask(shape, dtype, cache_modifier, eviction_policy): + mask = test_common.generate_tensor(shape, "bool").npu() + x0 = test_common.generate_tensor(shape, dtype).npu() + y_actual = test_common.generate_tensor(shape, dtype).npu() + blocks = list(x0.size()) + [1] * (8 - len(x0.size())) + triton_shape = list(shape) + [1] * (8 - len(shape)) + strides = list(x0.stride()) + [1] * (8 - len(x0.stride())) + triton_store_new_mask[(1, )](x0, y_actual, mask, cache_modifier, eviction_policy, *blocks, *triton_shape, *strides) diff --git a/third_party/ascend/unittest/pytest_ut/test_store_scalar.py b/third_party/ascend/unittest/pytest_ut/test_store_scalar.py new file mode 100644 index 0000000000..52be31e3e8 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_store_scalar.py @@ -0,0 +1,46 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl + + +# load with mask, store with scalar +@triton.jit +def sum_kernel_1(inp, mid, M, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(0) + # 0 / 1 / 2 * 4 + (0,1,2,3) + offset = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + inp_ptrs = inp + offset + mask = offset < M + inp_val = tl.load(inp_ptrs, mask=mask).to(tl.float32) + sum_val = tl.sum(inp_val) + mid_ptr = mid + pid + tl.store(mid_ptr, sum_val) + + +def test_case(): + inp = torch.ones(16, device="npu", dtype=torch.float32) + mid = torch.empty(4, device="npu", dtype=torch.float32) + sum_kernel_1[(4, 1, 1)](inp, mid, 16, 4) + ref = torch.tensor([4.0, 4.0, 4.0, 4.0], device="npu", dtype=torch.float32) + assert torch.allclose(mid, ref, rtol=1e-03, atol=1e-03, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_stride0_load.py b/third_party/ascend/unittest/pytest_ut/test_stride0_load.py new file mode 100644 index 0000000000..9c9295381d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_stride0_load.py @@ -0,0 +1,66 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl + + +def torch_expanddims_load(in0, in1, out0, YBLOCK, XBLOCK): + for i in range(0, YBLOCK): + tmp = in0[i] + 10 + res = in1[tmp] + out0[i, :] = res + + +@triton.jit +def triton_expanddims_load(in0, in1, out0, YBLOCK: tl.constexpr, XBLOCK: tl.constexpr): + base_y = tl.arange(0, YBLOCK) + y_idx = base_y[:, None] + y_mask = y_idx < YBLOCK + base_x = tl.arange(0, XBLOCK) + x_idx = base_x[None, :] + x_mask = x_idx < XBLOCK + + y = tl.load(in0 + y_idx, mask=y_mask) + tmp0 = tl.full([YBLOCK, XBLOCK], 10, tl.int32) + tmp1 = y + tmp0 + res = tl.load(in1 + tmp1, mask=y_mask) + tl.store(out0 + (y_idx * XBLOCK + x_idx), res, mask=y_mask & x_mask) + + +def test_case(): + YBLOCK = 4 + XBLOCK = 8 + in0 = torch.arange(0, YBLOCK, device="npu", dtype=torch.int32) + in1 = torch.arange(0, YBLOCK + 10, device="npu", dtype=torch.int32) + out0 = torch.empty((YBLOCK, XBLOCK), device="npu", dtype=torch.int32) + + torch_expanddims_load(in0, in1, out0, YBLOCK, XBLOCK) + + in0_triton = in0 + in1_triton = in1 + out0_triton = torch.empty((YBLOCK * XBLOCK, ), device="npu", dtype=torch.int32) + + triton_expanddims_load[(1, 1, 1)](in0_triton, in1_triton, out0_triton, YBLOCK, XBLOCK) + out0_triton = out0_triton.view(YBLOCK, XBLOCK) + + assert torch.allclose(out0, out0_triton, rtol=1e-03, atol=1e-03, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_strided_axis_coalescing_e2e.py b/third_party/ascend/unittest/pytest_ut/test_strided_axis_coalescing_e2e.py new file mode 100644 index 0000000000..3ca83712ce --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_strided_axis_coalescing_e2e.py @@ -0,0 +1,164 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +"""Native gate-on regression for ``StridedAxisCoalescing``. + +This intentionally uses the original T2L eligibility shape instead of forcing +an option in the test: a one-dimensional block pointer has ``stride=S`` and a +base of ``ptr + (pid % S)``; ``pid // S`` selects the T tile. The default NPU +compile mode is ``simd_simt_template`` (also called ``unstructured_in_simt``), +which supplies the historical template-SIMT half of the T2L gate on a real +Ascend 950 target. + +Do not run this test on a generic/B4 machine. Pretending that such a machine +is 910_95 would exercise a different compiler/toolchain contract and would not +validate this pass's original gate. +""" + +import re +import pytest +import torch +import triton +import triton.language as tl + +try: + from triton.backends.ascend.utils import is_compile_on_910_95 +except Exception: + # Keep collection safe in generic CI images where the Ascend device helper + # is unavailable. The actual test must never run without the real gate. + def is_compile_on_910_95(_arch): + return False + + +pytestmark = pytest.mark.skipif( + not is_compile_on_910_95(triton.runtime.driver.active.get_current_target().arch), + reason="StridedAxisCoalescing native validation requires an Ascend 950 toolchain", +) + + +@triton.jit +def strided_axis_coalescing_copy(src, dst, T: tl.constexpr, S: tl.constexpr, BLOCK: tl.constexpr): + # Keep both pieces of the canonical FLA split alive in TTIR. The pass + # folds the S head programs into one [BLOCK, S] tile and redirects `tile` + # to the post-shrink program id. + pid = tl.program_id(0) + head = pid % S + tile = pid // S + + src_block = tl.make_block_ptr( + base=src + head, + shape=(T, ), + strides=(S, ), + offsets=(tile * BLOCK, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + dst_block = tl.make_block_ptr( + base=dst + head, + shape=(T, ), + strides=(S, ), + offsets=(tile * BLOCK, ), + block_shape=(BLOCK, ), + order=(0, ), + ) + value = tl.load(src_block) + tl.store(dst_block, value) + + +def _launcher_source_from_compiled_metadata(metadata): + """Render the launch-grid section from actual compiler metadata. + + Grid coalescing is entirely metadata-driven. A minimal two-pointer + signature therefore keeps this assertion independent of the incidental + representation of ``CompiledKernel.src`` while still exercising the real + driver implementation and the metadata emitted by this compilation. + """ + if metadata is None: + return None, "CompiledKernel no longer exposes metadata" + try: + from triton.backends.ascend import driver as ascend_driver + + return ( + ascend_driver.make_launcher( + constants={}, + signature={0: "*fp32", 1: "*fp32"}, + metadata=metadata, + ), + None, + ) + except Exception as error: + # This is intentionally a safe degradation for launcher *source* + # introspection only. Do not hide a real compile/run/metadata error: + # the actual launch above has already built and used its launcher. + return None, f"{type(error).__name__}: {error}" + + +def test_strided_axis_coalescing_gate_on_e2e(): + """Compile and run the canonical S=4 axis fold through the real T2L gate.""" + s = 4 + block = 16 + t = 64 + grid_x = (t // block) * s + assert grid_x % s == 0 + + # Logical layout is [T, S], while each block pointer sees one strided + # head column. A successful rewrite has to launch only grid_x / S + # programs and still copy every logical element exactly once. + src = torch.arange(t * s, dtype=torch.float32, device="npu").reshape(t, s) + dst = torch.empty_like(src) + + # Compile first through the normal JIT API, then retain its real metadata + # for the ABI checks below. Use the canonical public template-SIMT mode. + kernel = strided_axis_coalescing_copy.warmup( + src, + dst, + grid=(grid_x, ), + T=t, + S=s, + BLOCK=block, + compile_mode="simd_simt_template", + ) + assert kernel is not None + strided_axis_coalescing_copy[(grid_x, )]( + src, + dst, + T=t, + S=s, + BLOCK=block, + compile_mode="simd_simt_template", + ) + torch.npu.synchronize() + + assert torch.equal(dst.cpu(), src.cpu()) + + # These fields are compiler output, not test-supplied options: together + # they prove the compile_on_910_95 && compile_mode="simd_simt_template" + # condition actually ran StridedAxisCoalescing and exported the launch contract. + metadata = kernel.metadata + assert metadata.compile_mode == "simd_simt_template" + assert metadata.compile_on_910_95 is True + assert metadata.coalesce_factor == s + assert metadata.coalesce_axis == 0 + assert metadata.coalesce_grid_ceil_div is False + + launcher_source, launcher_reason = _launcher_source_from_compiled_metadata(metadata) + # This is an acceptance test for the compiler-to-launcher handoff, not a + # value-only smoke test. A missing launcher representation would leave + # Axis's floor-div grid contract unverified, so it must fail rather than + # downgrade the native gate-on result to a warning. + assert launcher_source is not None, ("StridedAxisCoalescing launcher source introspection is unavailable: " + f"{launcher_reason}") + + # make_launcher emits both the C ABI and local C++ launch paths. Check + # both, including Axis's floor-divisibility contract (unlike Row's + # ceil-div handling). + assert len(re.findall(r"assert\(gridX % 4 == 0", launcher_source)) == 2 + assert launcher_source.count("gridX = gridX / 4;") == 2 diff --git a/third_party/ascend/unittest/pytest_ut/test_strides.py b/third_party/ascend/unittest/pytest_ut/test_strides.py new file mode 100644 index 0000000000..b2522ebe4d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_strides.py @@ -0,0 +1,140 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging +import pytest +import torch +import triton +import triton.language as tl + + +@triton.jit +def mv_kernel( + A, + B, + C, + N, + M, + stride_an, + stride_am, + stride_bm, + stride_cn, + BLOCK_N: tl.constexpr, + BLOCK_M: tl.constexpr, +): + pid = tl.program_id(0).to(tl.int64) + offset_n = pid * BLOCK_N + tl.arange(0, BLOCK_N)[:, None] + offset_m = tl.arange(0, BLOCK_M)[None, :] + n_mask = offset_n < N + A_ptrs = A + offset_n * stride_an + offset_m * stride_am + B_ptrs = B + offset_m * stride_bm + acc = tl.zeros((BLOCK_N, BLOCK_M), dtype=tl.float32) + for m in range(0, M, BLOCK_M): + m_mask = m + offset_m < M + a = tl.load(A_ptrs, mask=n_mask & m_mask, other=0.0).to(tl.float32) + b = tl.load(B_ptrs, mask=m_mask, other=0.0).to(tl.float32) + acc += a * b + A_ptrs += BLOCK_M * stride_am + B_ptrs += BLOCK_M * stride_bm + + acc = tl.sum(acc, axis=1) + C_ptrs = C + offset_n * stride_cn + tl.store(C_ptrs, acc[:, None], mask=n_mask) + + +def mv(inp, vec): + assert inp.shape[1] == vec.shape[0], "incompatible dimensions" + N, M = inp.shape + out = torch.empty((N, ), device=inp.device, dtype=inp.dtype) + + def grid(META): + return (triton.cdiv(N, META["BLOCK_N"]), ) + + mv_kernel[grid]( + inp, + vec, + out, + N, + M, + inp.stride(0), + inp.stride(1), + vec.stride(0), + out.stride(0), + 32, + 4, + ) + return out + + +def to_reference(inp, upcast=False): + if inp is None: + return None + ref_inp = inp + ref_inp = ref_inp.to("cpu") + if upcast: + if ref_inp.is_complex(): + ref_inp = ref_inp.to(torch.complex128) + else: + ref_inp = ref_inp.to(torch.float64) + return ref_inp + + +RESOLUTION = { + torch.bool: 0, + torch.int16: 0, + torch.int32: 0, + torch.int64: 0, + torch.float16: 1e-3, + torch.float32: 1.3e-6, + torch.bfloat16: 0.016, + torch.float64: 1e-7, + torch.complex32: 1e-3, + torch.complex64: 1.3e-6, +} + + +def assert_close(res, ref, dtype, equal_nan=False, reduce_dim=1): + assert res.dtype == dtype + ref = ref.to(dtype) + atol = 1e-4 * reduce_dim + rtol = RESOLUTION[dtype] + torch.testing.assert_close(res, ref, atol=atol, rtol=rtol, equal_nan=equal_nan) + + +def gems_assert_close(res, ref, dtype, equal_nan=False, reduce_dim=1): + res = res.to("cpu") + assert ref.device == torch.device("cpu") + assert_close(res, ref, dtype, equal_nan=equal_nan, reduce_dim=reduce_dim) + + +@pytest.mark.mv +@pytest.mark.parametrize("M, N", [(1, 32), (160, 1024), (5333, 497)]) +@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32]) +def test_accuracy_mv(M, N, dtype): + matrix = torch.randn((N, M), dtype=dtype, device="npu") + vector = torch.randn((M, ), dtype=dtype, device="npu") + ref_matrix = to_reference(matrix, True) + ref_vector = to_reference(vector, True) + + ref_out = torch.mv(ref_matrix, ref_vector) + + res_out = mv(matrix, vector) + + gems_assert_close(res_out, ref_out, dtype, reduce_dim=M) diff --git a/third_party/ascend/unittest/pytest_ut/test_sub.py b/third_party/ascend/unittest/pytest_ut/test_sub.py new file mode 100644 index 0000000000..c799a48c5b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sub.py @@ -0,0 +1,62 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import numpy as np +import torch +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 - x1 + return res + + +@triton.jit +def triton_sub(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 - tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int32', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], + ['uint8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_sub[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_sub_vec_id.py b/third_party/ascend/unittest/pytest_ut/test_sub_vec_id.py new file mode 100644 index 0000000000..3fa24e76ed --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sub_vec_id.py @@ -0,0 +1,132 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import triton.language.extra.cann.extension as al + + +@triton.jit +def triton_matmul_exp( + A_ptr, + B_ptr, + C_ptr, + TBuff_ptr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, + sub_M: tl.constexpr, +): + """function: + 1) The matrix matmul + 2) The matrix exp computation + For example, + 1) [2, 3] @ [3, 5] -> [2, 5] + + [[-1.4310, 0.3144, 0.1952], [[-0.1099, 0.7062, 0.6576, 1.3056, 0.3783], + [ 1.6719, -0.2581, -1.0243]] @ [ 0.9769, -0.6924, 0.4765, 1.1012, 0.3814] + [-1.4598, -0.5444, 0.5582, -2.0959, -0.0568]] + -> + [[ 0.1795, -1.3346, -0.6822, -1.9311, -0.4324], + [ 1.0593, 1.9171, 0.4047, 4.0454, 0.5921]] + + 2) exp([2, 5]) + exp([[ 0.1795, -1.3346, -0.6822, -1.9311, -0.4324], -> [[ 1.1966, 0.2633, 0.5055, 0.1450, 0.6489], + [ 1.0593, 1.9171, 0.4047, 4.0454, 0.5921]]) [ 2.8845, 6.8013, 1.4988, 57.1358, 1.8078]] + """ + # Each program computes one element C[row, col] using 2D tl.dot + row_matmul = tl.program_id(0) + col = tl.program_id(1) + + # Build small 2D grids so tl.dot sees [M,K] x [K,N] + offs_i = tl.arange(0, tl.constexpr(M))[:, None] # [M,1] (row axis) + offs_j = tl.arange(0, N)[None, :] # [1,N] (col axis) + offs_k = tl.arange(0, K) # [K] + + # A row: [M, K] + a_ptrs = A_ptr + (row_matmul + offs_i) * K + offs_k[None, :] + a_vals = tl.load(a_ptrs) # [M, K] + + # B column: [K, N] + b_ptrs = B_ptr + offs_k[:, None] * N + (col + offs_j) + b_vals = tl.load(b_ptrs) # [K, N] + + tbuff_ptrs = TBuff_ptr + (row_matmul + offs_i) * N + (col + offs_j) + + # Dot: [M, K] @ [K, N] -> [M, N] + acc_11 = tl.dot(a_vals, b_vals) # [M, N] + tl.store(tbuff_ptrs, acc_11) + + # Load Matrix [M/2, N] + sub_vec_id = al.sub_vec_id() + row_exp = row_matmul + sub_M * sub_vec_id + offs_exp_i = tl.arange(0, tl.constexpr(sub_M))[:, None] # [M/2, 1] (row axis) + tbuff_exp_ptrs = TBuff_ptr + (row_exp + offs_exp_i) * N + (col + offs_j) + acc_11_reload = tl.load(tbuff_exp_ptrs) + # Pointer grid for the single output element: shape [M/2, N] + c_ptrs = C_ptr + (row_exp + offs_exp_i) * N + (col + offs_j) + + # Store exp(acc) without scalar indexing + tl.store(c_ptrs, tl.exp(acc_11_reload)) + + +@pytest.mark.parametrize( + "dtype, ashape, bshape", + [ + # dtype, A-shape, B-shape + ["float32", (2, 3), (3, 5)], + ["float32", (2, 1), (1, 5)], + ], +) +def test_sub_vec_id_1to2(dtype, ashape, bshape): + """function: + A 1:2 demo using sub_vec_id. + 1. The matrix computation and the vector computation unit each have their own independent Scalar scheduler units, + deploying separately on cube core and vector core. + 2. Combine cube core and vector core in a certain ratio (1:2) + + For example, [2, 3] @ [3, 5] -> [2, 5] matrix matmul computation and matrix exp([2, 5]) computation + using sub_vec_id was used during the matrix exp. + """ + M, K = ashape + K2, N = bshape + assert K == K2, "Inner dimensions must match" + assert M % 2 == 0, "M dimensions must be divisible by 2" + sub_M = int(M / 2) + + # Generate input tensors + A = test_common.generate_tensor(ashape, dtype).npu() + B = test_common.generate_tensor(bshape, dtype).npu() + C = test_common.generate_tensor((M, N), dtype).npu() + TBuff = test_common.generate_tensor((M, N), dtype).npu() + + # Run + grid_matmul_exp = (1, ) # grid + triton_matmul_exp[grid_matmul_exp](A, B, C, TBuff, M, N, K, sub_M) + + # Reference result + C_ref = (A @ B).exp() + test_common.validate_cmp(dtype, C, C_ref) + + +if __name__ == "__main__": + test_sub_vec_id_1to2("float32", (2, 3), (3, 5)) diff --git a/third_party/ascend/unittest/pytest_ut/test_sub_vec_num.py b/third_party/ascend/unittest/pytest_ut/test_sub_vec_num.py new file mode 100644 index 0000000000..b46302836b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sub_vec_num.py @@ -0,0 +1,130 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import pytest +import test_common +import triton.language.extra.cann.extension as al + + +@triton.jit +def triton_matmul_exp( + A_ptr, + B_ptr, + C_ptr, + TBuff_ptr, + M: tl.constexpr, + N: tl.constexpr, + K: tl.constexpr, +): + """function: + 1) The matrix matmul + 2) The matrix exp computation + For example, + 1) [2, 3] @ [3, 5] -> [2, 5] + + [[-1.4310, 0.3144, 0.1952], [[-0.1099, 0.7062, 0.6576, 1.3056, 0.3783], + [ 1.6719, -0.2581, -1.0243]] @ [ 0.9769, -0.6924, 0.4765, 1.1012, 0.3814] + [-1.4598, -0.5444, 0.5582, -2.0959, -0.0568]] + -> + [[ 0.1795, -1.3346, -0.6822, -1.9311, -0.4324], + [ 1.0593, 1.9171, 0.4047, 4.0454, 0.5921]] + + 2) exp([2, 5]) + exp([[ 0.1795, -1.3346, -0.6822, -1.9311, -0.4324], -> [[ 1.1966, 0.2633, 0.5055, 0.1450, 0.6489], + [ 1.0593, 1.9171, 0.4047, 4.0454, 0.5921]]) [ 2.8845, 6.8013, 1.4988, 57.1358, 1.8078]] + """ + # Each program computes one element C[row, col] using 2D tl.dot + row_matmul = tl.program_id(0) + col = tl.program_id(1) + + # Build small 2D grids so tl.dot sees [M,K] x [K,N] + offs_i = tl.arange(0, tl.constexpr(M))[:, None] # [M,1] (row axis) + offs_j = tl.arange(0, N)[None, :] # [1,N] (col axis) + offs_k = tl.arange(0, K) # [K] + + # A row: [M, K] + a_ptrs = A_ptr + (row_matmul + offs_i) * K + offs_k[None, :] + a_vals = tl.load(a_ptrs) # [M, K] + + # B column: [K, N] + b_ptrs = B_ptr + offs_k[:, None] * N + (col + offs_j) + b_vals = tl.load(b_ptrs) # [K, N] + + tbuff_ptrs = TBuff_ptr + (row_matmul + offs_i) * N + (col + offs_j) + + # Dot: [M, K] @ [K, N] -> [M, N] + acc_11 = tl.dot(a_vals, b_vals) # [M, N] + tl.store(tbuff_ptrs, acc_11) + + # Load Matrix [M/2, N] + sub_vec_id = al.sub_vec_id() + row_exp = row_matmul + (M // al.sub_vec_num()) * sub_vec_id + offs_exp_i = tl.arange(0, M // al.sub_vec_num())[:, None] # [M/2, 1] (row axis) + tbuff_exp_ptrs = TBuff_ptr + (row_exp + offs_exp_i) * N + (col + offs_j) + acc_11_reload = tl.load(tbuff_exp_ptrs) + # Pointer grid for the single output element: shape [M/2, N] + c_ptrs = C_ptr + (row_exp + offs_exp_i) * N + (col + offs_j) + + # Store exp(acc) without scalar indexing + tl.store(c_ptrs, tl.exp(acc_11_reload)) + + +@pytest.mark.parametrize( + "dtype, ashape, bshape", + [ + # dtype, A-shape, B-shape + ["float32", (2, 3), (3, 5)], + ["float32", (2, 1), (1, 5)], + ], +) +def test_sub_vec_num_1to2(dtype, ashape, bshape): + """function: + A 1:2 demo using sub_vec_id. + 1. The matrix computation and the vector computation unit each have their own independent Scalar scheduler units, + deploying separately on cube core and vector core. + 2. Combine cube core and vector core in a certain ratio (1:2) + + For example, [2, 3] @ [3, 5] -> [2, 5] matrix matmul computation and matrix exp([2, 5]) computation + using sub_vec_id was used during the matrix exp. + """ + M, K = ashape + K2, N = bshape + assert K == K2, "Inner dimensions must match" + assert M % 2 == 0, "M dimensions must be divisible by 2" + + # Generate input tensors + A = test_common.generate_tensor(ashape, dtype).npu() + B = test_common.generate_tensor(bshape, dtype).npu() + C = test_common.generate_tensor((M, N), dtype).npu() + TBuff = test_common.generate_tensor((M, N), dtype).npu() + + # Run + grid_matmul_exp = (1, ) # grid + triton_matmul_exp[grid_matmul_exp](A, B, C, TBuff, M, N, K) + + # Reference result + C_ref = (A @ B).exp() + test_common.validate_cmp(dtype, C, C_ref) + + +if __name__ == "__main__": + test_sub_vec_num_1to2("float32", (2, 3), (3, 5)) diff --git a/third_party/ascend/unittest/pytest_ut/test_subview.py b/third_party/ascend/unittest/pytest_ut/test_subview.py new file mode 100644 index 0000000000..3137d5f624 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_subview.py @@ -0,0 +1,90 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os + +import pytest +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {"create_address_space": al.semantic.create_address_space}, + {}) + return str(module) + + +# ============== Kernel definitions ============== + + +@triton.jit +def test_subview_kernel1(XBLOCK: tl.constexpr): + # 1. Allocate a local buffer + src_buffer = bl.alloc(tl.float32, [XBLOCK, XBLOCK]) + + result_buffer = bl.subview(src_buffer, offsets=[1, 1], sizes=[XBLOCK - 2, XBLOCK - 2], strides=[1, 1]) + + +@triton.jit +def test_subview_kernel2(XBLOCK: tl.constexpr, offsets: tl.constexpr, sizes: tl.constexpr, strides: tl.constexpr): + # this statement has no effect, just to test the builder + bl.alloc(tl.float32, [XBLOCK]).subview([offsets], [sizes], [strides]) + + +# ============== Main for manual testing ============== + +if __name__ == "__main__": + print("=" * 60) + print("Test 1: test_subview_function") + print("=" * 60) + mlir = compile_kernel(test_subview_kernel1, {}, {"XBLOCK": 8}) + print(f"Generated MLIR ({len(mlir)} chars):\n") + print(mlir) + + print("\n" + "=" * 60) + print("Test 2: test_subview_constructor") + print("=" * 60) + mlir = compile_kernel(test_subview_kernel2, {}, {"XBLOCK": 32, "offsets": 1, "sizes": 30, "strides": 1}) + print(f"Generated MLIR ({len(mlir)} chars):\n") + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_sum.py b/third_party/ascend/unittest/pytest_ut/test_sum.py new file mode 100644 index 0000000000..9e3ffeb76a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sum.py @@ -0,0 +1,124 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest +import test_common +import time + + +@triton.jit +def sum_loop_high(in_ptr0, in_ptr1, in_ptr2, out_ptr0, rnumel, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr, + RBLOCK: tl.constexpr): + R = rnumel + X = xnumel + xoffset = tl.program_id(0) * XBLOCK + xbase = tl.arange(0, XBLOCK_SUB) + rbase = tl.arange(0, RBLOCK) + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + xbase + x0 = xindex[None, :] + _tmp6 = tl.full([RBLOCK, XBLOCK_SUB], 0, tl.float32) + for roffset in range(0, rnumel, RBLOCK): + rindex = roffset + rbase + rmask = None + r1 = rindex[:, None] + tmp0 = tl.load(in_ptr0 + (X * r1 + (x0)), rmask) + tmp1 = tl.load(in_ptr1 + (X * r1 + (x0)), rmask) + tmp3 = tl.load(in_ptr2 + (X * r1 + (x0)), rmask) + tmp2 = tmp0 + tmp1 + tmp4 = tmp2 + tmp3 + _tmp6 = _tmp6 + tmp4 + tmp6 = tl.sum(_tmp6, 0) + tl.store(out_ptr0 + (xindex), tmp6, None) + + +@triton.jit +def sum_loop_low(in_ptr0, in_ptr1, in_ptr2, out_ptr0, xnumel, ynumel, XBLOCK: tl.constexpr, RBLOCK: tl.constexpr): + X = xnumel + Y = ynumel + xoffset = tl.program_id(0) * XBLOCK + xindex = xoffset + tl.arange(0, XBLOCK) + + x0 = xindex[:, None] + rbase = tl.arange(0, RBLOCK) + _tmp6 = tl.full([XBLOCK, RBLOCK], 0, tl.float32) + for roffset in range(0, ynumel, RBLOCK): + rindex = roffset + rbase + rmask = None + r1 = rindex[None, :] + tmp0 = tl.load(in_ptr0 + (r1 + (Y * x0)), rmask) + tmp1 = tl.load(in_ptr1 + (r1 + (Y * x0)), rmask) + tmp3 = tl.load(in_ptr2 + (r1 + (Y * x0)), rmask) + tmp2 = tmp0 + tmp1 + tmp4 = tmp2 + tmp3 + _tmp6 = _tmp6 + tmp4 + tmp6 = tl.sum(_tmp6, 1) + + tl.store(out_ptr0 + (xindex), tmp6, None) + + +def foo(a, b, c): + y = a + b + c + y = y.sum(0) + return y + + +def bar(a, b, c): + y = a + b + c + y = y.sum(1) + return y + + +@pytest.mark.parametrize('param_list', [ + ['float32', (64, 8192), 1, 2, 256, 16], +]) +def test_case_1(param_list): + dtype, shape, ncore, XB, YB, ZB = param_list + a = test_common.generate_tensor(shape, dtype).npu() + b = test_common.generate_tensor(shape, dtype).npu() + c = test_common.generate_tensor(shape, dtype).npu() + value = torch.empty_strided((a.shape[0], ), (1, )).npu() + + std_low_ret = bar(a, b, c) + print(f"std_low_ret = {std_low_ret[0:8]}") + XBLOCK = 64 + RBLOCK = 32 + NBLOCKS = a.shape[0] // XBLOCK + sum_loop_low[NBLOCKS, 1, 1](a, b, c, value, a.shape[0], a.shape[1], XBLOCK, RBLOCK) + triton_low_ret = value + print(f"triton_low_ret = {triton_low_ret[0:8]}") + torch.testing.assert_close(std_low_ret, triton_low_ret, rtol=1e-3, atol=1e-3) + + std_ret2 = foo(a, b, c) + print(f"std_ret2 = {std_ret2[0:8]}") + NBLOCKS = 32 + XBLOCK = a.shape[1] // NBLOCKS + XBLOCK_SUB = min(64, max(XBLOCK // 2, 32)) + RBLOCK = 64 + + value2 = torch.empty_strided((a.shape[1], ), (1, )).npu() + sum_loop_high[NBLOCKS, 1, 1](a, b, c, value2, a.shape[0], a.shape[1], XBLOCK, XBLOCK_SUB, RBLOCK) + triton_ret2 = value2 + print(f"triton_ret2 = {triton_ret2[0:8]}") + torch.testing.assert_close(std_ret2, triton_ret2, rtol=1e-3, atol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_sum_dim0.py b/third_party/ascend/unittest/pytest_ut/test_sum_dim0.py new file mode 100644 index 0000000000..93b107f839 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sum_dim0.py @@ -0,0 +1,133 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def standard_sum(x0, dim, dtype): + res = torch.sum(x0, dim, dtype=dtype) + return res + + +@triton.jit +def triton_sum_dim0(in_ptr0, out_ptr0, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + + mmask = mblk_idx < M + nmask = nblk_idx < N + + mask = (mmask[:, None]) & (nmask[None, :]) + + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + + x = tl.load(in_ptr0 + idx, mask=mask, other=0) + + ret = tl.sum(x, 0) + + tl.store(out_ptr0 + nblk_idx, ret, mask=nmask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16,'bfloat16'), TODO: waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), TODO: waiting for supporting or testing + # (torch.int32,'int32'), TODO: waiting for supporting or testing + # (torch.int64,'int64'), TODO: waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (57, 3, 64, 16), + (57, -32, 64, 32), + (57, 37, 64, 64), + (57, -256, 64, 256), + (57, 263, 64, 512), + (64, 3, 64, 16), + (64, -32, 64, 32), + (64, 37, 64, 64), + (64, -256, 64, 256), + (64, 263, 64, 512), + (3, 3, 8, 8), + (-32, 3, 32, 8), + (37, 3, 64, 8), + (-256, 3, 256, 8), + (263, 3, 512, 8), + (3, 1, 8, 8), + (-32, 1, 32, 8), + (37, 1, 64, 8), + (-256, 1, 256, 8), + (263, 1, 512, 8), +] + +map_for_64_t = {37: (31, 32), 263: (107, 128)} +map_for_32_t = {263: (137, 256)} + + +# @pytest.mark.parametrize('dtype, sigtype',[(torch.float32,'float32'),]) +@pytest.mark.parametrize('M, N, MNUMEL, NNUMEL', [ + (57, 3, 64, 16), + (64, -32, 64, 32), + (37, 3, 64, 8), + (263, 1, 512, 8), + (-256, 3, 256, 8), +]) +@pytest.mark.parametrize('dtype, sigtype', types) +# @pytest.mark.parametrize('M, N, MNUMEL, NNUMEL',shapes) +def test_sum_dim0(dtype, sigtype, M, N, MNUMEL, NNUMEL): + + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M][0] if M in map_for_64_t else M + MNUMEL = map_for_64_t[M][1] if M in map_for_64_t else MNUMEL + N = map_for_64_t[N][0] if N in map_for_64_t else N + NNUMEL = map_for_64_t[N][1] if N in map_for_64_t else NNUMEL + + elif sigtype == 'float32' or sigtype == 'bfloat16' or sigtype == 'int32': + M = map_for_32_t[M][0] if M in map_for_32_t else M + MNUMEL = map_for_32_t[M][1] if M in map_for_32_t else MNUMEL + N = map_for_32_t[N][0] if N in map_for_32_t else N + NNUMEL = map_for_32_t[N][1] if N in map_for_32_t else NNUMEL + + print(f"sum : ({M}, {N}) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(M, N), dtype=sigtype) + + ans = standard_sum(x0, 0, dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((N, ), dtype=dtype).npu() + triton_sum_dim0[1, 1, 1](x0, output, M=M, N=N, MNUMEL=MNUMEL, NNUMEL=NNUMEL, debug=True) + print(output) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_sum_dim1.py b/third_party/ascend/unittest/pytest_ut/test_sum_dim1.py new file mode 100644 index 0000000000..f7a3e27d5f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sum_dim1.py @@ -0,0 +1,133 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import time + +import torch +import torch_npu +import test_common + + +def standard_sum(x0, dim, dtype): + res = torch.sum(x0, dim, dtype=dtype) + return res + + +@triton.jit +def triton_sum_dim1(in_ptr0, out_ptr0, M: tl.constexpr, N: tl.constexpr, MNUMEL: tl.constexpr, NNUMEL: tl.constexpr): + mblk_idx = tl.arange(0, MNUMEL) + nblk_idx = tl.arange(0, NNUMEL) + + mmask = mblk_idx < M + nmask = nblk_idx < N + + mask = (mmask[:, None]) & (nmask[None, :]) + + idx = mblk_idx[:, None] * N + nblk_idx[None, :] + + x = tl.load(in_ptr0 + idx, mask=mask, other=0) + + ret = tl.sum(x, 1) + + tl.store(out_ptr0 + mblk_idx, ret, mask=mmask) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16,'bfloat16'), waiting for supporting or testing + (torch.int8, 'int8'), + # (torch.int16,'int16'), waiting for supporting or testing + # (torch.int32,'int32'), waiting for supporting or testing + # (torch.int64,'int64'), waiting for supporting or testing +] + +# if shape axis = 32/256 , then actual shape = axis/element_size() +shapes = [ + (57, 3, 64, 16), + (57, -32, 64, 32), + (57, 37, 64, 64), + (57, -256, 64, 256), + (57, 263, 64, 512), + (64, 3, 64, 16), + (64, -32, 64, 32), + (64, 37, 64, 64), + (64, -256, 64, 256), + (64, 263, 64, 512), + (3, 3, 8, 8), + (-32, 3, 32, 8), + (37, 3, 64, 8), + (-256, 3, 256, 8), + (263, 3, 512, 8), + (3, 1, 8, 8), + (-32, 1, 32, 8), + (37, 1, 64, 8), + (-256, 1, 256, 8), + (263, 1, 512, 8), +] + +map_for_64_t = {37: (31, 32), 263: (107, 128)} +map_for_32_t = {263: (137, 256)} + + +@pytest.mark.parametrize('M, N, MNUMEL, NNUMEL', [ + (57, 3, 64, 16), + (64, -32, 64, 32), + (37, 3, 64, 8), + (263, 1, 512, 8), + (-256, 3, 256, 8), +]) +# @pytest.mark.parametrize('M, N',[(263,3),(-256,3)]) +@pytest.mark.parametrize('dtype, sigtype', types) +# @pytest.mark.parametrize('M, N, MNUMEL, NNUMEL',shapes) +def test_sum_dim1(dtype, sigtype, M, N, MNUMEL, NNUMEL): + + M = (-M) // torch.tensor(0, dtype=dtype).element_size() if M < 0 else M + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == 'int64': + M = map_for_64_t[M][0] if M in map_for_64_t else M + MNUMEL = map_for_64_t[M][1] if M in map_for_64_t else MNUMEL + N = map_for_64_t[N][0] if N in map_for_64_t else N + NNUMEL = map_for_64_t[N][1] if N in map_for_64_t else NNUMEL + + elif sigtype == 'float32' or sigtype == 'bfloat16' or sigtype == 'int32': + M = map_for_32_t[M][0] if M in map_for_32_t else M + MNUMEL = map_for_32_t[M][1] if M in map_for_32_t else MNUMEL + N = map_for_32_t[N][0] if N in map_for_32_t else N + NNUMEL = map_for_32_t[N][1] if N in map_for_32_t else NNUMEL + + print(f"sum : ({M}, {N}) {dtype} {sigtype}") + x0 = test_common.generate_tensor(shape=(M, N), dtype=sigtype) + + ans = standard_sum(x0, 1, dtype) + + x0 = x0.npu() + print(ans) + + output = torch.zeros((M, ), dtype=dtype).npu() + triton_sum_dim1[1, 1, 1](x0, output, M=M, N=N, MNUMEL=MNUMEL, NNUMEL=NNUMEL, debug=True) + print(output) + + test_common.validate_cmp(sigtype, output, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_sum_vector.py b/third_party/ascend/unittest/pytest_ut/test_sum_vector.py new file mode 100644 index 0000000000..efa3dcb70c --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sum_vector.py @@ -0,0 +1,82 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +from triton.runtime.libentry import libentry +import pytest +from test_common import generate_tensor, validate_cmp, _32bit_dtypes, _16bit_dtypes + + +def torch_func(x0): + return torch.sum(x0) + + +@pytest.mark.parametrize("dtype", _32bit_dtypes) +@pytest.mark.parametrize("shape", [(1, ), (3, ), (8, ), (37, ), (64, ), (781, )]) +def test_sum(dtype, shape): + + @libentry() + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, XBLOCK: tl.constexpr): + idx = tl.arange(0, XBLOCK) + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.sum(tmp0) + tl.store(out_ptr0 + idx, tmp1) + + def triton_func(x0): + out = x0[0] + triton_kernel[1, 1, 1](out, x0, x0.numel()) + return out + + x0 = generate_tensor(shape=shape, dtype=dtype).npu() + torch_ref = torch_func(x0) + triton_cal = triton_func(x0) + validate_cmp(dtype, torch_ref, triton_cal) + + +@triton.jit +def _reduce_combine(a, b): + return a + b + + +@pytest.mark.parametrize("dtype", _32bit_dtypes) +@pytest.mark.parametrize("shape", [(1, ), (3, ), (8, ), (37, ), (64, ), (781, )]) +def test_reduce_sum(dtype, shape): + + @libentry() + @triton.jit + def triton_kernel(out_ptr0, in_ptr0, XBLOCK: tl.constexpr): + idx = tl.arange(0, XBLOCK) + tmp0 = tl.load(in_ptr0 + idx) + tmp1 = tl.reduce(tmp0, 0, _reduce_combine) + tl.store(out_ptr0 + idx, tmp1) + + def triton_func(x0): + out = x0[0] + triton_kernel[1, 1, 1](out, x0, x0.numel()) + return out + + x0 = generate_tensor(shape=shape, dtype=dtype).npu() + torch_ref = torch_func(x0) + triton_cal = triton_func(x0) + validate_cmp(dtype, torch_ref, triton_cal) diff --git a/third_party/ascend/unittest/pytest_ut/test_swap.py b/third_party/ascend/unittest/pytest_ut/test_swap.py new file mode 100644 index 0000000000..589def92e0 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_swap.py @@ -0,0 +1,58 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import triton +import triton.language as tl +import torch_npu +import pytest + + +@triton.jit +def swap_kernel(x_ptr, # *Pointer* to first inout vector. + y_ptr, # *Pointer* to second inout vector. + BLOCK_SIZE: tl.constexpr, # Number of elements each program should process. + # NOTE: `constexpr` so it can be used as a shape value. + ): + pid = tl.program_id(axis=0) # We use a 1D launch grid so axis is 0. + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + x = tl.load(x_ptr + offsets) + y = tl.load(y_ptr + offsets) + tl.store(x_ptr + offsets, y) + tl.store(y_ptr + offsets, x) + + +def swap(x: torch.Tensor, y: torch.Tensor, size): + n_elements = x.numel() + grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]), ) + swap_kernel[grid](x, y, BLOCK_SIZE=size) + + +@pytest.mark.parametrize('shape', [(1, ), (3, ), (4, ), (7, ), (8, ), (11, ), (16, ), (512, ), (1024, )]) +def test(shape): + x = torch.rand(shape).npu() + y = torch.rand(shape).npu() + assert not torch.equal(x, y) + x_ = x.clone() + y_ = y.clone() + swap(x, y, shape[0]) + torch.testing.assert_close(x, y_, rtol=1e-04, atol=1e-04, equal_nan=True) + torch.testing.assert_close(y, x_, rtol=1e-04, atol=1e-04, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_swiglu.py b/third_party/ascend/unittest/pytest_ut/test_swiglu.py new file mode 100644 index 0000000000..42d18ddb9d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_swiglu.py @@ -0,0 +1,100 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +# from .utils import calculate_settings +def standard_binary(e, g): + ee = e.to(torch.float32) + f = ee * torch.sigmoid(ee) + h = (f * g).to(g.dtype) + return h + + +@triton.jit +def _fg_kernel( + e, + g, + h, + n_elements, + BLOCK_SIZE: tl.constexpr, +): + block_idx = tl.program_id(0) + offsets = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + + e_row = tl.load(e + offsets, mask=mask, other=0).to(tl.float32) + g_row = tl.load(g + offsets, mask=mask, other=0) #.to(tl.float32) + + # f = e * sigmoid(e) + f_row = e_row * tl.sigmoid(e_row) # e_row / (1 + tl.exp(-e_row)) + # f_row = f_row.to(g_row.dtype) # bf16 should always cast to fp32 when calculating + # h = f * g + h_row = (f_row * g_row).to(g_row.dtype) + # Store h + tl.store(h + offsets, h_row, mask=mask) + + +pass + + +def swiglu_fg_kernel(e, g): + batch, seq_len, hd = e.shape + n_elements = e.numel() + h = torch.empty((batch, seq_len, hd), dtype=e.dtype, device="npu") + grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + kk = _fg_kernel[grid]( + e, + g, + h, + n_elements, + BLOCK_SIZE=1024, + ) + print(kk.asm['ttir']) + return h + + +pass + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 128, 128)], + ['float16', (2, 128, 128)], + ['bfloat16', (2, 128, 128)], +]) +def test_case(param_list): + dtype, size = param_list + torch.manual_seed(0) + x = torch.rand(size, device='npu', dtype=eval('torch.' + dtype)) + y = torch.rand(size, device='npu', dtype=eval('torch.' + dtype)) + std_ret = standard_binary(x, y) + print(f"std_ret= {std_ret}") + ret = swiglu_fg_kernel(x, y) + print(f"ret= {ret}") + test_common.validate_cmp(dtype, std_ret, ret) + + +pass diff --git a/third_party/ascend/unittest/pytest_ut/test_swizzle2d.py b/third_party/ascend/unittest/pytest_ut/test_swizzle2d.py new file mode 100644 index 0000000000..49f2dbab4b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_swizzle2d.py @@ -0,0 +1,55 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + i = tl.arange(0, 4) + j = tl.arange(0, 4) + xx, yy = tl.swizzle2d(i, j, size_i=4, size_j=4, size_g=2) + + tl.store(output_ptr + tl.arange(0, 4), xx) + tl.store(x_ptr + tl.arange(0, 4), yy) + + +@pytest.mark.parametrize('param_list', [ + ['int32', (2, 256, 16), 1, 2, 256, 16], +]) +def test_case(param_list): + dtype, shape, ncore, XB, YB, ZB = param_list + x = test_common.generate_tensor((4, ), dtype).npu() + a = torch.tensor([[0, 2, 1, 3], [4, 6, 5, 7], [8, 10, 9, 11], [12, 14, 13, 15]], dtype=eval('torch.' + dtype)).npu() + output = torch.randint(1, (4, ), dtype=eval('torch.' + dtype)).npu() + fn_npu_[ncore, 1, 1](output, x, XB, YB, ZB) + print(f"output={output}") + triton_ret = output[:, None] * 4 + x[None, :] + print(f"triton_ret={triton_ret}") + torch.testing.assert_close(triton_ret, a) diff --git a/third_party/ascend/unittest/pytest_ut/test_sync_block.py b/third_party/ascend/unittest/pytest_ut/test_sync_block.py new file mode 100755 index 0000000000..86392a8eca --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sync_block.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import torch + +import triton +import triton.language as tl +import pytest +import test_common + +import triton.language.extra.cann.extension as extension + +pipe = extension.PIPE + +# eg: pytest -v test_matmul_exp.py::test_matmul_exp +############################# + + +@triton.jit +def triton_matmul_exp(A_ptr, B_ptr, C_ptr, TBuff_ptr, M, N, K: tl.constexpr): + # Each program computes one element C[row, col] using 2D tl.dot + row = tl.program_id(0) + col = tl.program_id(1) + + # Build small 2D grids so tl.dot sees [1,K] x [K,1] + offs_i = tl.arange(0, 1)[:, None] # [1,1] (row axis) + offs_j = tl.arange(0, 1)[None, :] # [1,1] (col axis) + offs_k = tl.arange(0, K) # [K] + + # A row: [1, K] + a_ptrs = A_ptr + (row + offs_i) * K + offs_k[None, :] + a_vals = tl.load(a_ptrs) # [1, K] + + # B column: [K, 1] + b_ptrs = B_ptr + offs_k[:, None] * N + (col + offs_j) + b_vals = tl.load(b_ptrs) # [K, 1] + + tbuff_ptrs = TBuff_ptr + (row + offs_i) * N + (col + offs_j) + + # Dot: [1, K] @ [K, 1] -> [1, 1] + acc_11 = tl.dot(a_vals, b_vals) # [1, 1] + tl.store(tbuff_ptrs, acc_11) + + extension.sync_block_set("cube", "vector", 5, pipe.PIPE_MTE1, pipe.PIPE_MTE3) + extension.sync_block_wait("cube", "vector", 5, pipe.PIPE_MTE1, pipe.PIPE_MTE3) + + acc_11_reload = tl.load(tbuff_ptrs) + # Pointer grid for the single output element: shape [1,1] + c_ptrs = C_ptr + (row + offs_i) * N + (col + offs_j) + + # Store exp(acc) without scalar indexing + tl.store(c_ptrs, tl.exp(acc_11_reload)) + + +@pytest.mark.parametrize('dtype, ashape, bshape', [ + # dtype, A-shape, B-shape + ['float32', (4, 4), (4, 4)], + ['float32', (2, 3), (3, 5)], +]) +def test_matmul_exp(dtype, ashape, bshape): + M, K = ashape + K2, N = bshape + assert K == K2, "Inner dimensions must match" + + # generate input tensors + A = test_common.generate_tensor(ashape, dtype).npu() + B = test_common.generate_tensor(bshape, dtype).npu() + C = test_common.generate_tensor((M, N), dtype).npu() + TBuff = test_common.generate_tensor((M, N), dtype).npu() + + # run kernel + grid = (M, N) # one program per output element + triton_matmul_exp[grid](A, B, C, TBuff, M, N, K) + + # reference result + C_ref = (A @ B).exp() + + # compare + torch.testing.assert_close(C_ref, C, rtol=3e-2, atol=3e-2, equal_nan=True) + + +if __name__ == "__main__": + test_matmul_exp('float32', (4, 4), (4, 4)) diff --git a/third_party/ascend/unittest/pytest_ut/test_sync_block_all.py b/third_party/ascend/unittest/pytest_ut/test_sync_block_all.py new file mode 100755 index 0000000000..d5459aa788 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_sync_block_all.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +import os +import pytest +import triton +import triton.language as tl +import triton.language.extra.cann.extension as al +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +from triton._C.libtriton import ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {}, {}) + return str(module) + + +# ============== Kernel definitions ============== + + +@triton.jit +def test_sync_block_all(): + al.sync_block_all("all_cube", 8) + al.sync_block_all("all_vector", 9) + al.sync_block_all("all", 10) + al.sync_block_all("all_sub_vector", 11) + + +# ============== Main for manual testing ============== + +if __name__ == "__main__": + print("=" * 60) + print("Test 1: test_sync_block_all") + print("=" * 60) + mlir = compile_kernel(test_sync_block_all, {}, {}) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_tan.py b/third_party/ascend/unittest/pytest_ut/test_tan.py new file mode 100644 index 0000000000..13f63f51ab --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_tan.py @@ -0,0 +1,102 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import test_common + +import torch +import torch_npu + +import triton.language.extra.cann.libdevice as libdevice + + +def standard_unary(x0, dtype): + res = torch.tan(x0) + return res + + +def standard_binary(x0, y0, dtype): + res = x0 + y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = libdevice.tan(x) + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x + y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + (torch.float32, 'float32'), + (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + # (torch.int8, 'int8'), + # (torch.int16, 'int16'), + # (torch.int32, 'int32'), + # (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.skip(reason="randomly failed accuracy test") +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype) + + ans = standard_unary(x0, dtype) + x0 = x0.npu() + print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_unary[1, 1, 1](x0, out, N=N, NUMEL=NUMEL, debug=True) + print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_tanh.py b/third_party/ascend/unittest/pytest_ut/test_tanh.py new file mode 100644 index 0000000000..b80eac61a3 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_tanh.py @@ -0,0 +1,59 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +def torch_tanh(x0): + return torch.tanh(x0) + + +@triton.jit +def triton_tanh(in_ptr0, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = libdevice.tanh(tmp0) + tl.store(out_ptr0 + (x0), tmp1, None) + + +@pytest.mark.parametrize('param_list', [ + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['bfloat16', (2, 4096, 8), 2, 32768, 1024], + ['float32', (2, 4096, 8), 2, 32768, 1024], +]) +def test_tanh(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_tanh(x0) + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + triton_tanh[ncore, 1, 1](x0, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_template.py b/third_party/ascend/unittest/pytest_ut/test_template.py new file mode 100644 index 0000000000..2b1c5a2262 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_template.py @@ -0,0 +1,58 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl + +import time +import torch +import torch_npu +import test_common + +NBLOCKS = 1 +X_SIZE = tl.constexpr(4) +Y_SIZE = tl.constexpr(64) +Z_SIZE = tl.constexpr(32) +NUMEL = X_SIZE * Y_SIZE * Z_SIZE + + +def fn(input): + output = input.reshape((X_SIZE, Y_SIZE, Z_SIZE)).permute((1, 0, 2)).reshape((X_SIZE * Y_SIZE * Z_SIZE)) + return output + + +@triton.jit +def fn_kernel(output_ptr, input_ptr): + col_offsets = tl.arange(0, X_SIZE * Y_SIZE * Z_SIZE) + input_local = tl.load(input_ptr + col_offsets) + input_local = input_local.reshape((X_SIZE, Y_SIZE, Z_SIZE)).permute((1, 0, 2)).reshape((X_SIZE * Y_SIZE * Z_SIZE)) + tl.store(output_ptr + col_offsets, input_local) + + +def test_cases(): + input = torch.randn(NUMEL, dtype=torch.float16).npu() + output = torch.randn(NUMEL, dtype=torch.float16).npu() + output2 = torch.randn(NUMEL, dtype=torch.float16).npu() + fn_kernel[1, 1, 1](output, input) + output2 = fn(input) + test_common.validate_cmp('float16', output, output2) + print("data validation passed") diff --git a/third_party/ascend/unittest/pytest_ut/test_tensor_descriptor.py b/third_party/ascend/unittest/pytest_ut/test_tensor_descriptor.py new file mode 100644 index 0000000000..a4dc2cbfc5 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_tensor_descriptor.py @@ -0,0 +1,433 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import math +import pytest +import torch +import triton +import triton.language as tl +import test_common + + +@pytest.mark.parametrize("dtype", ['float32', 'int32']) +@pytest.mark.parametrize("M_BLOCK,N_BLOCK", [(2, 16), (8, 16)]) +def test_tensor_descriptor_load_store(dtype, M_BLOCK, N_BLOCK): + + @triton.jit + def kernel(out_ptr, a_ptr, M, N, M_BLOCK: tl.constexpr, N_BLOCK: tl.constexpr): + in_desc = tl.make_tensor_descriptor( + a_ptr, + shape=[M, N], + strides=[N, 1], + block_shape=[M_BLOCK, N_BLOCK], + ) + out_desc = tl.make_tensor_descriptor( + out_ptr, + shape=[M, N], + strides=[N, 1], + block_shape=[M_BLOCK, N_BLOCK], + ) + moffset = tl.program_id(0) * M_BLOCK + noffset = tl.program_id(1) * N_BLOCK + block = in_desc.load([moffset, noffset]) + out_desc.store([moffset, noffset], block) + + M, N = M_BLOCK * 2, N_BLOCK * 2 + inp = test_common.generate_tensor((M, N), dtype).npu() + out = inp.new_empty((M, N)) + + grid_m = M // M_BLOCK + grid_n = N // N_BLOCK + + kernel[(grid_m, grid_n)](out, inp, M, N, M_BLOCK, N_BLOCK) + torch.testing.assert_close(inp, out) + + +@pytest.mark.parametrize("dtype", ['float32', 'int32']) +def test_tensor_descriptor_load_store3d(dtype): + + @triton.jit + def kernel(out_ptr, a_ptr, M, N, K, stride_m, stride_n, stride_k, M_BLOCK: tl.constexpr, N_BLOCK: tl.constexpr, + K_BLOCK: tl.constexpr): + in_desc = tl.make_tensor_descriptor( + a_ptr, + shape=[M, N, K], + strides=[stride_m, stride_n, stride_k], + block_shape=[M_BLOCK, N_BLOCK, K_BLOCK], + ) + out_desc = tl.make_tensor_descriptor( + out_ptr, + shape=[M, N, K], + strides=[stride_m, stride_n, stride_k], + block_shape=[M_BLOCK, N_BLOCK, K_BLOCK], + ) + moffset = tl.program_id(0) * M_BLOCK + noffset = tl.program_id(1) * N_BLOCK + koffset = tl.program_id(2) * K_BLOCK + block = in_desc.load([moffset, noffset, koffset]) + out_desc.store([moffset, noffset, koffset], block) + + M, N, K = 8, 16, 32 + inp = test_common.generate_tensor((M, N, K), dtype).npu() + out = inp.new_empty((M, N, K)) + + M_BLOCK = 2 + N_BLOCK = 4 + + # 自动调整 K_BLOCK,保证最后一维 block 至少 16 字节 + dtype = getattr(inp, "dtype", None) + itemsize = torch.tensor([], dtype=inp.dtype).element_size() + min_k_block = max(16 // itemsize, 1) + K_BLOCK = max(8, min_k_block) + + grid_m = M // M_BLOCK + grid_n = N // N_BLOCK + grid_k = K // K_BLOCK + + kernel[(grid_m, grid_n, grid_k)](out, inp, *inp.shape, *out.stride(), M_BLOCK, N_BLOCK, K_BLOCK) + torch.testing.assert_close(inp.reshape(M * N * K), out.reshape(M * N * K)) + + +# Exercise the functional load/store builtins once to ensure they map through. +@pytest.mark.parametrize("dtype", ["float32", "uint8"]) +def test_tensor_descriptor_functional_interface(dtype): + """Copies an entire tensor blockwise using the descriptor builtins.""" + + @triton.jit + def kernel(out_ptr, a_ptr, M, N, M_BLOCK: tl.constexpr, N_BLOCK: tl.constexpr): + in_desc = tl.make_tensor_descriptor( + a_ptr, + shape=[M, N], + strides=[N, 1], + block_shape=[M_BLOCK, N_BLOCK], + ) + out_desc = tl.make_tensor_descriptor( + out_ptr, + shape=[M, N], + strides=[N, 1], + block_shape=[M_BLOCK, N_BLOCK], + ) + moffset = tl.program_id(0) * M_BLOCK + noffset = tl.program_id(1) * N_BLOCK + block = tl.load_tensor_descriptor(in_desc, [moffset, noffset]) + tl.store_tensor_descriptor(out_desc, [moffset, noffset], block) + + M, N = 32, 128 + inp = test_common.generate_tensor((M, N), dtype).npu() + + M_BLOCK = 8 + N_BLOCK = 32 + out = inp.new_empty((M, N)) + + grid_m = M // M_BLOCK + grid_n = N // N_BLOCK + + kernel[(grid_m, grid_n)](out, inp, M, N, M_BLOCK, N_BLOCK) + torch.testing.assert_close(inp, out) + + +@pytest.mark.parametrize("dtype_str", ["int32"]) +@pytest.mark.parametrize("shape", [(128, 2, 4), (2, 4, 2)]) +@pytest.mark.parametrize("axis", [0, 1, 2]) +@pytest.mark.parametrize("device", ["npu"]) +def test_reduce_max(dtype_str, shape, axis, device): + + @triton.jit + def kernel( + In, + Out, + in_shape1: tl.constexpr, + in_shape2: tl.constexpr, + in_shape3: tl.constexpr, + ou_shape1: tl.constexpr, + ou_shape2: tl.constexpr, + axis: tl.constexpr, + ): + in_desc = tl.make_tensor_descriptor( + base=In, + shape=[in_shape1 * in_shape2 * in_shape3], + strides=[1], + block_shape=[in_shape1 * in_shape2 * in_shape3], + ) + out_desc = tl.make_tensor_descriptor( + base=Out, + shape=[ou_shape1 * ou_shape2], + strides=[1], + block_shape=[ou_shape1 * ou_shape2], + ) + val = in_desc.load([0]).reshape(in_shape1, in_shape2, in_shape3) + output = tl.max(val, axis=axis) + out_desc.store([0], output.reshape(out_desc.block_shape)) + + inp = torch.arange(math.prod(shape), dtype=getattr(torch, dtype_str), device=device).reshape(shape) + expected, indices = torch.max(inp.to(torch.int64), dim=axis) + expected = expected.to(torch.int32) + actual = torch.zeros(expected.shape, dtype=getattr(torch, dtype_str), device=device) + kernel[(1, )](inp, actual, *shape, *expected.shape, axis=axis) + assert torch.equal(expected, actual) + + +@pytest.mark.parametrize("dtype", ['float32', 'float16', 'bfloat16']) +@pytest.mark.parametrize("padding", ["zero", "nan"]) +def test_tensor_descriptor_padding(dtype, padding): + + @triton.jit + def device_tma_load(in_ptr, out_ptr, IM, IN, YM, YN, M_BLOCK: tl.constexpr, N_BLOCK: tl.constexpr, + padding: tl.constexpr): + x_desc = tl.make_tensor_descriptor(in_ptr, shape=[IM, IN], strides=[IN, 1], block_shape=[M_BLOCK, N_BLOCK], + padding_option=padding) + out_desc = tl.make_tensor_descriptor(out_ptr, shape=[YM, YN], strides=[YN, 1], block_shape=[M_BLOCK, N_BLOCK]) + + moffset = tl.program_id(0) * M_BLOCK + noffset = tl.program_id(1) * N_BLOCK + + value = x_desc.load([moffset, noffset]) + + out_desc.store([moffset, noffset], value) + + def alloc_fn(size: int, alignment: float, stream: float): + return torch.ones(size, device="npu", dtype=torch.float32) + + triton.set_allocator(alloc_fn) + + IM, IN = 48, 48 + OM, ON = 64, 64 + M_BLOCK = 32 + N_BLOCK = 32 + torch_dtype = getattr(torch, dtype) + input_tensor = torch.arange(IM * IN, device="npu", dtype=torch_dtype).reshape(IM, IN) + out_device_tma = torch.zeros((OM, ON), device="npu", dtype=torch_dtype) + grid = (triton.cdiv(OM, M_BLOCK), triton.cdiv(ON, N_BLOCK)) + device_tma_load[grid](input_tensor, out_device_tma, IM, IN, OM, ON, M_BLOCK, N_BLOCK, padding) + expected = torch.zeros((OM, ON), device="npu", dtype=torch_dtype) + expected[0:IN, 0:IM] = input_tensor + if padding == "nan": + expected[:, IN:ON] = float('nan') + expected[IM:OM, :] = float('nan') + + torch.testing.assert_close(expected, out_device_tma, equal_nan=True) + + +@pytest.mark.parametrize("X, Y", [(128, 128)]) +@pytest.mark.parametrize("BLOCK_X, BLOCK_Y", [(32, 32)]) +@pytest.mark.parametrize("dtype", ['float32', 'int32']) +@pytest.mark.parametrize("y", [0, 32]) +def test_tensor_descriptor_scatter(X, Y, BLOCK_X, BLOCK_Y, dtype, y): + + def torch_scatter_rows(input, idx, y, block_y, X, Y): + out = torch.zeros((X, Y), dtype=input.dtype, device=input.device) + for i, j in enumerate(idx): + out[j][y:y + block_y] = input[i] + return out + + @triton.jit + def tensor_descriptor_scatter_rows_kernel(out_ptr, in_ptr, idx_ptr, y, X: tl.constexpr, Y: tl.constexpr, + BLOCK_X: tl.constexpr, BLOCK_Y: tl.constexpr): + idx = tl.load(idx_ptr + tl.arange(0, BLOCK_X)) + data = tl.load(in_ptr + tl.arange(0, BLOCK_X)[:, None] * BLOCK_Y + tl.arange(0, BLOCK_Y)[None, :]) + desc = tl.make_tensor_descriptor(out_ptr, [X, Y], [Y, 1], [1, BLOCK_Y]) + desc.scatter(data, idx, y) + + device = 'npu' + if BLOCK_X > X or y + BLOCK_Y > Y: + pytest.skip() + + torch.manual_seed(42) + torch_dtype = getattr(torch, dtype) + input_tensor = torch.arange(BLOCK_X * BLOCK_Y, dtype=torch_dtype, device=device).reshape(BLOCK_X, BLOCK_Y) + output = torch.zeros((X, Y), dtype=torch_dtype, device=device) + + idx = torch.randperm(BLOCK_X, dtype=torch.int32, device=device) + + def alloc_fn(size: int, align: int, stream): + return torch.empty(size, dtype=torch.int8, device=device) + + triton.set_allocator(alloc_fn) + + tensor_descriptor_scatter_rows_kernel[(1, )](output, input_tensor, idx, y, X, Y, BLOCK_X, BLOCK_Y) + + ref = torch_scatter_rows(input_tensor, idx, y, BLOCK_Y, X, Y) + torch.testing.assert_close(ref, output, atol=0, rtol=0) + + +@pytest.mark.parametrize("X, Y", [(128, 128)]) +@pytest.mark.parametrize("BLOCK_X, BLOCK_Y", [(32, 32)]) +@pytest.mark.parametrize("dtype", ['float32', 'int32']) +@pytest.mark.parametrize("y", [0, 32]) +def test_tensor_descriptor_gather(X, Y, BLOCK_X, BLOCK_Y, dtype, y): + + @triton.jit + def tensor_descriptor_gather_rows_kernel(out_ptr, in_ptr, idx_ptr, y, X: tl.constexpr, Y: tl.constexpr, + BLOCK_X: tl.constexpr, BLOCK_Y: tl.constexpr): + idx = tl.load(idx_ptr + tl.arange(0, BLOCK_X)) + desc = tl.make_tensor_descriptor(in_ptr, [X, Y], [Y, 1], [1, BLOCK_Y]) + out = desc.gather(idx, y) + tl.store(out_ptr + tl.arange(0, BLOCK_X)[:, None] * BLOCK_Y + tl.arange(0, BLOCK_Y)[None, :], out) + + def torch_gather_rows(input, idx, y, block_y): + return input[idx.long(), y:y + block_y] + + device = 'npu' + if BLOCK_X > X or y + BLOCK_Y > Y: + pytest.skip() + + torch.manual_seed(42) + torch_dtype = getattr(torch, dtype) + input_tensor = test_common.generate_tensor((X, Y), dtype).npu() + output = torch.empty((BLOCK_X, BLOCK_Y), dtype=torch_dtype, device=device) + + idx = torch.randint(BLOCK_X, (BLOCK_X, ), dtype=torch.int32, device=device) + + def alloc_fn(size: int, align: int, steam): + return torch.empty(size, dtype=torch.int8, device=device) + + triton.set_allocator(alloc_fn) + + tensor_descriptor_gather_rows_kernel[(1, )](output, input_tensor, idx, y, X, Y, BLOCK_X, BLOCK_Y) + + ref = torch_gather_rows(input_tensor, idx, y, BLOCK_Y) + torch.testing.assert_close(ref, output, atol=0, rtol=0) + + +REDUCE_OP = { + "add": lambda a, b: a + b, + "min": lambda a, b: torch.minimum(a, b), + "max": lambda a, b: torch.maximum(a, b), + "and": lambda a, b: torch.bitwise_and(a, b), + "or": lambda a, b: torch.bitwise_or(a, b), + "xor": lambda a, b: torch.bitwise_xor(a, b), +} + +SKIP_KINDS = {"and", "or", "xor"} +all_kinds = ["add", "min", "max", "and", "or", "xor"] +kind_parms = [ + pytest.param(k, marks=pytest.mark.skip( + reason=f"skip for bishengir compile failed on a2,succeed on a5")) if k in SKIP_KINDS else k for k in all_kinds +] + + +@pytest.mark.parametrize("kind", kind_parms) +@pytest.mark.parametrize("dtype", ['int32']) +@pytest.mark.parametrize("M_BLOCK,N_BLOCK", [(2, 16)]) +def test_tensor_descriptor_reduce(kind, dtype, M_BLOCK, N_BLOCK): + + @triton.jit(debug=True) + def kernel(a_ptr, out_ptr, M, N, M_BLOCK: tl.constexpr, N_BLOCK: tl.constexpr, kind: tl.constexpr): + moffset = tl.program_id(0) * M_BLOCK + noffset = tl.program_id(1) * N_BLOCK + + midx = moffset + tl.arange(0, M_BLOCK)[:, None] + nidx = noffset + tl.arange(0, N_BLOCK)[None, :] + idx = midx * N + nidx + val = tl.load(a_ptr + idx) + + desc = tl.make_tensor_descriptor( + out_ptr, + shape=[M, N], + strides=[N, 1], + block_shape=[M_BLOCK, N_BLOCK], + ) + + if kind == "add": + desc.atomic_add([moffset, noffset], val) + elif kind == "min": + desc.atomic_min([moffset, noffset], val) + elif kind == "max": + desc.atomic_max([moffset, noffset], val) + elif kind == "and": + desc.atomic_and([moffset, noffset], val) + elif kind == "or": + desc.atomic_or([moffset, noffset], val) + else: + tl.static_assert(kind == "xor") + desc.atomic_xor([moffset, noffset], val) + + M, N = M_BLOCK * 2, N_BLOCK * 2 + inp = test_common.generate_tensor((M, N), dtype).npu() + out = test_common.generate_tensor((M, N), dtype).npu() + + grid_m = M // M_BLOCK + grid_n = N // N_BLOCK + + expect = REDUCE_OP[kind](inp, out) + kernel[(grid_m, grid_n)](inp, out, M, N, M_BLOCK, N_BLOCK, kind) + torch.testing.assert_close(expect, out) + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +@pytest.mark.parametrize("dtype", ["float32"]) +@pytest.mark.parametrize("M,N,BLOCK_M,BLOCK_N", [(64, 128, 16, 32)]) +def test_host_tensor_descriptor_args(dtype, M, N, BLOCK_M, BLOCK_N): + from triton.tools.tensor_descriptor import TensorDescriptor + + @triton.jit + def kernel(in_desc, out_desc, M, N, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr): + pid = tl.program_id(0) + grid_n = tl.cdiv(N, BLOCK_N) + pid_m = pid // grid_n + pid_n = pid % grid_n + off_m = (pid_m * BLOCK_M).to(tl.int32) + off_n = (pid_n * BLOCK_N).to(tl.int32) + tile = in_desc.load([off_m, off_n]) + out_desc.store([off_m, off_n], tile) + + torch_dtype = getattr(torch, dtype) + inp = torch.randn((M, N), dtype=torch_dtype, device="npu") + out = torch.empty((M, N), dtype=torch_dtype, device="npu") + in_desc = TensorDescriptor(inp, list(inp.shape), list(inp.stride()), [BLOCK_M, BLOCK_N]) + out_desc = TensorDescriptor(out, list(out.shape), list(out.stride()), [BLOCK_M, BLOCK_N]) + + grid = (triton.cdiv(M, BLOCK_M) * triton.cdiv(N, BLOCK_N), ) + kernel[grid](in_desc, out_desc, M, N, BLOCK_M, BLOCK_N) + torch.testing.assert_close(inp, out) + + +@pytest.mark.skip(reason="The case is not supported on Ascend 950, skipping for now. Will be fixed in future.") +@pytest.mark.parametrize("padding", ["zero", "nan"]) +def test_host_tensor_descriptor_padding(padding): + from triton.tools.tensor_descriptor import TensorDescriptor + + @triton.jit + def kernel(in_desc, out_desc, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr): + moffset = (tl.program_id(0) * BLOCK_M).to(tl.int32) + noffset = (tl.program_id(1) * BLOCK_N).to(tl.int32) + value = in_desc.load([moffset, noffset]) + out_desc.store([moffset, noffset], value) + + IM, IN = 48, 48 + OM, ON = 64, 64 + BLOCK_M, BLOCK_N = 32, 32 + inp = torch.arange(IM * IN, device="npu", dtype=torch.float32).reshape(IM, IN) + out = torch.zeros((OM, ON), device="npu", dtype=torch.float32) + + in_desc = TensorDescriptor(inp, list(inp.shape), list(inp.stride()), [BLOCK_M, BLOCK_N], padding=padding) + out_desc = TensorDescriptor(out, list(out.shape), list(out.stride()), [BLOCK_M, BLOCK_N]) + + grid = (triton.cdiv(OM, BLOCK_M), triton.cdiv(ON, BLOCK_N)) + kernel[grid](in_desc, out_desc, BLOCK_M, BLOCK_N) + + expected = torch.zeros((OM, ON), device="npu", dtype=torch.float32) + expected[0:IM, 0:IN] = inp + if padding == "nan": + expected[IM:OM, :] = float("nan") + expected[:, IN:ON] = float("nan") + # corner already nan from both; keep as nan + expected[IM:OM, IN:ON] = float("nan") + torch.testing.assert_close(expected, out, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_to_buffer.py b/third_party/ascend/unittest/pytest_ut/test_to_buffer.py new file mode 100644 index 0000000000..2053bc7944 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_to_buffer.py @@ -0,0 +1,54 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch + +import triton +import triton.language as tl +from triton.compiler import ASTSource +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al + +target = triton.runtime.driver.active.get_current_target() + + +@triton.jit +def to_buffer(): + a = tl.full((32, 2, 4), 0, dtype=tl.int64) + a_buf = bl.to_buffer(a) + b = tl.full((32, 2, 4), 0, dtype=tl.int64) + b_buf = bl.to_buffer(b, al.ascend_address_space.UB) + c = tl.full((32, 2, 4), 0, dtype=tl.int64) + c_buf = bl.to_buffer(c, al.ascend_address_space.L1) + d = tl.full((32, 2, 4), 0, dtype=tl.int64) + d_buf = bl.to_buffer(d, al.ascend_address_space.L0A) + e = tl.full((32, 2, 4), 0, dtype=tl.int64) + e_buf = bl.to_buffer(e, al.ascend_address_space.L0B) + f = tl.full((32, 2, 4), 0, dtype=tl.int64) + f_buf = bl.to_buffer(f, al.ascend_address_space.L0C) + + +def test_to_buffer(): + src = ASTSource( + fn=to_buffer, + constexprs={}, + signature={}, + ) + triton.compile(src=src, target=target) diff --git a/third_party/ascend/unittest/pytest_ut/test_to_tensor.py b/third_party/ascend/unittest/pytest_ut/test_to_tensor.py new file mode 100644 index 0000000000..7bc826671a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_to_tensor.py @@ -0,0 +1,76 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os + +import triton +import triton.language as tl +from triton.compiler.compiler import ASTSource +from triton.compiler.code_generator import ast_to_ttir +import triton.extension.buffer.language as bl +import triton.language.extra.cann.extension as al +from triton._C.libtriton import ir, buffer_ir +from triton._C.libtriton.ascend import ir as ascend_ir + +os.environ["TORCH_DEVICE_BACKEND_AUTOLOAD"] = "0" + + +class Options: + num_warps = 4 + num_stages = 3 + num_ctas = 1 + cluster_dims = (1, 1, 1) + enable_fp_fusion = True + debug = False + sanitize_overflow = True + + +def compile_kernel(kernel, signature, constants): + """Helper to compile a kernel to MLIR.""" + src = ASTSource(kernel, signature, constants) + context = ir.context() + ir.load_dialects(context) + buffer_ir.load_dialects(context) + ascend_ir.load_dialects(context) + module = ast_to_ttir(kernel, src, context, Options(), {"create_address_space": al.semantic.create_address_space}, + {}) + return str(module) + + +# ============== Kernel definitions ============== + + +@triton.jit +def kernel_func(XBLOCK: tl.constexpr): + buffer1 = bl.alloc(tl.float32, [XBLOCK]) + buffer1.to_tensor(writable=True) + buffer2 = bl.alloc(tl.float32, [XBLOCK]) + bl.to_tensor(buffer2, writable=True) + + +# ============== Main for manual testing ============== + +if __name__ == "__main__": + print("=" * 60) + print("Test 1: Nested Scopes") + print("=" * 60) + mlir = compile_kernel(kernel_func, {}, {"XBLOCK": 256}) + print(f"✅ Generated MLIR ({len(mlir)} chars):\n") + print(mlir) diff --git a/third_party/ascend/unittest/pytest_ut/test_top2gating_argmax.py b/third_party/ascend/unittest/pytest_ut/test_top2gating_argmax.py new file mode 100644 index 0000000000..38f31b00bd --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_top2gating_argmax.py @@ -0,0 +1,97 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest +import test_common + + +@triton.jit +def promote_to_tensor(x): + # Addition promotes to tensor for us + return x + tl.zeros((1, ), tl.int1) + + +@triton.jit +def is_floating(x): + return promote_to_tensor(x).dtype.is_floating() + + +@triton.jit +def maximum_with_index(a_value, a_index, b_value, b_index): + mask = a_value > b_value + equal = a_value == b_value + if is_floating(a_value): + a_isnan = a_value != a_value + b_isnan = b_value != b_value + mask |= a_isnan and not b_isnan + # Consider NaNs as equal + equal |= a_isnan and b_isnan + + # Prefer lowest index if values are equal + mask |= equal & (a_index < b_index) + return tl.where(mask, a_value, b_value), tl.where(mask, a_index, b_index) + + +@triton.jit +def max_with_index(value, index, dim): + return tl.reduce((value, index), dim, maximum_with_index) + + +@triton.jit +def triton_4(in_ptr2, in_ptr4, out_ptr10, x0_numel, r1_numel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + RBLOCK: tl.constexpr = 4 + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = XBLOCK // XBLOCK_SUB + base2 = tl.arange(0, RBLOCK) + loops2: tl.constexpr = r1_numel // RBLOCK + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1[:, None] + r1 = base2[None, :] + tmp15 = tl.load(in_ptr2 + (r1 + (8 * x0)), None) + tmp25 = tl.load(in_ptr4 + (r1 + (4 * x0)), None) + tmp21 = tmp15.to(tl.float32) + tmp26 = tmp25 * tmp21 + tmp27 = tl.reshape(tmp26, [XBLOCK_SUB, RBLOCK]) + tmp31 = tl.broadcast_to(r1.reshape(1, RBLOCK), tmp27.shape) + _, tmp30_tmp = max_with_index(tmp27, tmp31, 1) + tmp30 = tmp30_tmp.reshape(XBLOCK_SUB, 1) + tmp43 = tmp30.to(tl.int32) + tmp46 = tmp43 + tl.store(out_ptr10 + (x0), tmp46, None) + + +def test_max_with_index_dim0(): + mask = torch.randint(low=0, high=2, size=(512, 2, 4), dtype=torch.int32).npu() + weights = torch.randn((512, 4), device='npu', dtype=torch.float32) + buf32 = torch.randint(low=0, high=8, size=(512, ), dtype=torch.int32).npu() + XBLOCK = 32 + XBLOCK_SUB = 32 + triton_4[16, 1, 1](mask, weights, buf32, 512, 4, XBLOCK, XBLOCK_SUB) + + _, first_idx = torch.max(weights * mask[:, 0, :], dim=1) + print(f"first_idx: {first_idx[0:8]}") + print(f"triton_idx: {buf32[0:8]}") + + assert torch.all(torch.eq(first_idx, buf32.to(first_idx.dtype))) diff --git a/third_party/ascend/unittest/pytest_ut/test_topk.py b/third_party/ascend/unittest/pytest_ut/test_topk.py new file mode 100644 index 0000000000..b532418e61 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_topk.py @@ -0,0 +1,74 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging +import pytest + +import triton +import triton.language as tl + +import torch +import torch_npu +import test_common + + +@pytest.mark.interpreter +@pytest.mark.parametrize("shape", [ + (16, 64), + (16, 8, 8), + (2, 8, 8, 8), +]) +@pytest.mark.parametrize("k", [4]) +@pytest.mark.parametrize("dtype", ["float32"]) +def test_topk_nd(shape, k, dtype): + + numel = 1 + for d in shape: + numel *= d + base = torch.arange(numel, dtype=torch.float32).view(shape) / 10.0 + if dtype == "float16": + base = base.half() + elif dtype == "int32": + base = torch.arange(numel, dtype=torch.int32).view(shape) + x = base.npu() + + y = torch.topk(x.cpu(), k=k, dim=-1).values + + M = int(torch.tensor(shape[:-1]).prod().item()) if len(shape) > 1 else 1 + N = shape[-1] + + x_2d = x.view(M, N) + z = torch.empty((M, k), dtype=x_2d.dtype, device=x_2d.device) + + @triton.jit + def topk_kernel_nd(X, stride_xm, Z, stride_zm, M: tl.constexpr, N: tl.constexpr, k: tl.constexpr): + offs_m = tl.arange(0, M) + offs_x_n = tl.arange(0, N) + offs_z_n = tl.arange(0, k) + offs_x = offs_m[:, None] * stride_xm + offs_x_n[None, :] + x_val = tl.load(X + offs_x) + z_val = tl.topk(x_val, k) + offs_z = offs_m[:, None] * stride_zm + offs_z_n[None, :] + tl.store(Z + offs_z, z_val) + + topk_kernel_nd[(1, )](x_2d, x_2d.stride(0), z, z.stride(0), M, N, k, num_warps=8) + + z_view = z.view(*shape[:-1], k) if len(shape) > 1 else z.view(k) + test_common.validate_cmp(dtype, z_view.cpu(), y) diff --git a/third_party/ascend/unittest/pytest_ut/test_trans_3d.py b/third_party/ascend/unittest/pytest_ut/test_trans_3d.py new file mode 100644 index 0000000000..fde7857774 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_trans_3d.py @@ -0,0 +1,80 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging +import math + +import pytest +import test_common +import torch + +import triton +import triton.language as tl + + +@triton.jit +def fn_npu_102(output_ptr, x_ptr, YB: tl.constexpr, ZB: tl.constexpr, KB: tl.constexpr): + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + kidx = tl.arange(0, KB) + idx = yidx[:, None, None] * ZB * KB + zidx[None, :, None] * KB + kidx[None, None, :] + + X = tl.load(x_ptr + idx) + + ret = tl.trans(X, 1, 0, 2) + + oidx = (zidx[:, None, None] * YB * KB + yidx[None, :, None] * KB + kidx[None, None, :]) + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_021(output_ptr, x_ptr, YB: tl.constexpr, ZB: tl.constexpr, KB: tl.constexpr): + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + kidx = tl.arange(0, KB) + idx = yidx[:, None, None] * ZB * KB + zidx[None, :, None] * KB + kidx[None, None, :] + + X = tl.load(x_ptr + idx) + + ret = tl.trans(X, (0, 2, 1)) + + oidx = (yidx[:, None, None] * ZB * KB + kidx[None, :, None] * ZB + zidx[None, None, :]) + + tl.store(output_ptr + oidx, ret) + + +@pytest.mark.parametrize("shape", [(23, 5, 31)]) +@pytest.mark.parametrize("dtype", ["float32"]) +def test_permute_3d(shape, dtype): + logging.debug(f"dtype:{dtype} shape:{shape}") + + data_type = eval("torch." + dtype) + x = torch.randint(low=0, high=2, size=shape, dtype=data_type).npu() + + triton_res = torch.empty((shape[1], shape[0], shape[2]), dtype=data_type).npu() + torch_res = torch.permute(x, (1, 0, 2)) + fn_npu_102[1, 1, 1](triton_res, x, shape[0], shape[1], shape[2]) + test_common.validate_cmp(dtype, triton_res, torch_res) + + triton_res = torch.empty((shape[0], shape[2], shape[1]), dtype=data_type).npu() + torch_res = torch.permute(x, (0, 2, 1)) + fn_npu_021[1, 1, 1](triton_res, x, shape[0], shape[1], shape[2]) + test_common.validate_cmp(dtype, triton_res, torch_res) diff --git a/third_party/ascend/unittest/pytest_ut/test_triton_eq.py b/third_party/ascend/unittest/pytest_ut/test_triton_eq.py new file mode 100644 index 0000000000..6bb1e09096 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_triton_eq.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 == x1 + return res + + +@triton.jit +def triton_test(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 == tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + 'bool')).npu() + triton_test[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_triton_le.py b/third_party/ascend/unittest/pytest_ut/test_triton_le.py new file mode 100644 index 0000000000..21e7f72dcd --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_triton_le.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 <= x1 + return res + + +@triton.jit +def triton_le(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 <= tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + 'bool')).npu() + triton_le[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_triton_lt.py b/third_party/ascend/unittest/pytest_ut/test_triton_lt.py new file mode 100644 index 0000000000..1789bbf9b8 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_triton_lt.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 < x1 + return res + + +@triton.jit +def triton_lt(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 < tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + 'bool')).npu() + triton_lt[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_triton_neq.py b/third_party/ascend/unittest/pytest_ut/test_triton_neq.py new file mode 100644 index 0000000000..a2fbde4242 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_triton_neq.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_pointwise(x0, x1): + res = x0 != x1 + return res + + +@triton.jit +def triton_neq(in_ptr0, in_ptr1, out_ptr0, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + for loop1 in range(loops1): + x0_prime = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1 + tmp0 = tl.load(in_ptr0 + (x0), None) + tmp1 = tl.load(in_ptr1 + (x0), None) + tmp2 = tmp0 != tmp1 + tl.store(out_ptr0 + (x0), tmp2, None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 4096, 8), 2, 32768, 1024], + ['float16', (2, 4096, 8), 2, 32768, 1024], + ['int8', (2, 4096, 8), 2, 32768, 1024], +]) +def test_case(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_pointwise(x0, x1) + y_cal = torch.zeros(shape, dtype=eval('torch.' + 'bool')).npu() + triton_neq[ncore, 1, 1](x0, x1, y_cal, xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_triton_unified_attention.py b/third_party/ascend/unittest/pytest_ut/test_triton_unified_attention.py new file mode 100644 index 0000000000..5948881681 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_triton_unified_attention.py @@ -0,0 +1,472 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from typing import Optional + +import pytest +import torch +import triton +import triton.language as tl + +NUM_HEADS = [(8, 2), (16, 2)] +HEAD_SIZES = [128] +# only support 32 currently +BLOCK_SIZES = [32] + +DTYPES = [torch.float16] +QDTYPES = [None] +# one value large enough to test overflow in index calculation. +# one value small enough to test the schema op check +NUM_BLOCKS = [32768, 2048] + + +@triton.jit +def cdiv_fn(x, y): + return (x + y - 1) // y + + +@triton.jit +def apply_softcap(S, x): + Sdiv = S / x + p1 = tl.exp(Sdiv) + p2 = tl.exp(-Sdiv) + return x * (p1 - p2) / (p1 + p2) + + +@triton.jit +def kernel_unified_attention_2d(output_ptr, # [num_tokens, num_query_heads, head_size] + query_ptr, # [num_tokens, num_query_heads, head_size] + key_cache_ptr, # [num_blks, blk_size, num_kv_heads, head_size] + value_cache_ptr, # [num_blks, blk_size, num_kv_heads, head_size] + block_tables_ptr, # [num_seqs, max_num_blocks_per_seq] + seq_lens_ptr, # [num_seqs] + alibi_slopes_ptr, # [num_query_heads] + scale, # float32 + k_scale, # float32 + v_scale, # float32 + softcap, # float32 + num_query_heads: tl.constexpr, # int + num_queries_per_kv: tl.constexpr, # int + block_table_stride: tl.int64, # int + query_stride_0: tl.int64, # int + query_stride_1: tl.int64, # int, should be equal to head_size + output_stride_0: tl.int64, # int + output_stride_1: tl.int64, # int, should be equal to head_size + BLOCK_SIZE: tl.constexpr, # int + HEAD_SIZE: tl.constexpr, # int + HEAD_SIZE_PADDED: tl.constexpr, # int, must be power of 2 + USE_ALIBI_SLOPES: tl.constexpr, # bool + USE_SOFTCAP: tl.constexpr, # bool + SLIDING_WINDOW: tl.constexpr, # int + stride_k_cache_0: tl.int64, # int + stride_k_cache_1: tl.int64, # int + stride_k_cache_2: tl.int64, # int + stride_k_cache_3: tl.constexpr, # int + stride_v_cache_0: tl.int64, # int + stride_v_cache_1: tl.int64, # int + stride_v_cache_2: tl.int64, # int + stride_v_cache_3: tl.constexpr, # int + query_start_len_ptr, # [num_seqs+1] + BLOCK_Q: tl.constexpr, # int + num_seqs: tl.int32, BLOCK_M: tl.constexpr, # int + ): + + q_block_global_idx = tl.program_id(0) + kv_head_idx = tl.program_id(1) + + left: tl.int32 = 0 + right = num_seqs + while left < right: + mid = (left + right) // 2 + mid_val = tl.load(query_start_len_ptr + mid) // BLOCK_Q + mid + if mid_val <= q_block_global_idx: + left = mid + 1 + else: + right = mid + + seq_idx = left - 1 + q_block_start_idx = tl.load(query_start_len_ptr + seq_idx) // BLOCK_Q + seq_idx + + q_block_local_idx = q_block_global_idx - q_block_start_idx + + cur_batch_in_all_start_index = tl.load(query_start_len_ptr + seq_idx) + cur_batch_in_all_stop_index = tl.load(query_start_len_ptr + seq_idx + 1) + + cur_batch_query_len = cur_batch_in_all_stop_index \ + - cur_batch_in_all_start_index + + if q_block_local_idx * BLOCK_Q >= cur_batch_query_len: + return + + offs_m = tl.arange(0, BLOCK_M) + offs_d = tl.arange(0, HEAD_SIZE_PADDED) + query_pos = q_block_local_idx * BLOCK_Q + offs_m // num_queries_per_kv + + query_offset_0 = cur_batch_in_all_start_index + query_pos + query_offset_1 = kv_head_idx * num_queries_per_kv + \ + offs_m % num_queries_per_kv + query_offset = (query_offset_0[:, None] * query_stride_0 + query_offset_1[:, None] * query_stride_1 + + offs_d[None, :]) + + dim_mask = tl.where(offs_d < HEAD_SIZE, 1, 0).to(tl.int1) + query_mask_0 = tl.where(query_pos < cur_batch_query_len, 1, 0).to(tl.int1) + query_mask_1 = tl.where(query_offset_1 < num_query_heads, 1, 0).to(tl.int1) + + Q = tl.load( + query_ptr + query_offset, + mask=dim_mask[None, :] & query_mask_0[:, None] & query_mask_1[:, None], + other=0.0, + ) + + block_table_offset = seq_idx * block_table_stride + + M = tl.full([BLOCK_M], float("-inf"), dtype=tl.float32) + L = tl.full([BLOCK_M], 1.0, dtype=tl.float32) + acc = tl.zeros([BLOCK_M, HEAD_SIZE_PADDED], dtype=tl.float32) + + # sequence len for this particular sequence + seq_len = tl.load(seq_lens_ptr + seq_idx) + + # context length for this particular sequences + context_len = seq_len - cur_batch_query_len + + # alibi slope for this head + if USE_ALIBI_SLOPES: + alibi_slope = tl.load(alibi_slopes_ptr + query_offset_1, mask=query_mask_1, other=0.0) + + num_blocks = cdiv_fn(seq_len, BLOCK_SIZE) + + # iterate through tiles + for j in range(0, num_blocks): + + physical_block_idx = tl.load(block_tables_ptr + block_table_offset + j) + + offs_n = tl.arange(0, BLOCK_SIZE) + + v_offset = (physical_block_idx * stride_v_cache_0 + kv_head_idx * stride_v_cache_2 + + offs_d[None, :] * stride_v_cache_3 + offs_n[:, None] * stride_v_cache_1) + + k_offset = (physical_block_idx * stride_k_cache_0 + kv_head_idx * stride_k_cache_2 + + offs_d[:, None] * stride_k_cache_3 + offs_n[None, :] * stride_k_cache_1) + + K_load = tl.load(key_cache_ptr + k_offset, mask=dim_mask[:, None], other=0.0) + + if K_load.dtype.is_fp8(): + if Q.dtype.is_fp8(): + K = K_load + else: + K = (K_load.to(tl.float32) * tl.load(k_scale)).to(Q.dtype) + else: + K = K_load + + V_load = tl.load(value_cache_ptr + v_offset, mask=dim_mask[None, :], other=0.0) + + if V_load.dtype.is_fp8(): + if Q.dtype.is_fp8(): + V = V_load + else: + V = (V_load.to(tl.float32) * tl.load(v_scale)).to(Q.dtype) + else: + V = V_load + + seq_offset = j * BLOCK_SIZE + offs_n + + seq_mask = seq_offset[None, :] < context_len + query_pos[:, None] + 1 + + S = tl.zeros(shape=(BLOCK_M, BLOCK_SIZE), dtype=tl.float32) + + S += scale * tl.dot(Q, K) + + if USE_SOFTCAP: + S = apply_softcap(S, softcap) + + S = tl.where(query_mask_1[:, None] & query_mask_0[:, None] & seq_mask, S, float("-inf")) + + if SLIDING_WINDOW > 0: + S = tl.where((context_len + query_pos[:, None] - seq_offset) < SLIDING_WINDOW, S, float("-inf")) + + if USE_ALIBI_SLOPES: + S += alibi_slope[:, None] * (seq_offset - context_len) + + # compute running maximum + m_j = tl.maximum(M, tl.max(S, axis=1)) + # For sliding window there's a chance the max is -inf due to masking of + # the entire row. In this case we need to set m_j 0 to avoid NaN + m_j = tl.where(m_j > float("-inf"), m_j, 0.0) + + P = tl.exp(S - m_j[:, None]) + + l_j = tl.sum(P, axis=1) + + alpha = tl.exp(M - m_j) + + acc = acc * alpha[:, None] + + # update constants + L = L * alpha + l_j + M = m_j + + acc += tl.dot(P.to(V.dtype), V) + + # epilogue + acc = acc / L[:, None] + + output_offset = (query_offset_0[:, None] * output_stride_0 + query_offset_1[:, None] * output_stride_1 + + offs_d[None, :]) + + tl.store( + output_ptr + output_offset, + acc, + mask=dim_mask[None, :] & query_mask_0[:, None] & query_mask_1[:, None], + ) + + +def unified_attention( + q, + k, + v, + out, + cu_seqlens_q, + max_seqlen_q, + seqused_k, + max_seqlen_k, + softmax_scale, + causal, + window_size, + block_table, + softcap, + q_descale, + k_descale, + v_descale, + alibi_slopes=None, +): + assert causal, "Only causal attention is supported" + assert q_descale is None, "Q scales not supported" + + block_size = v.shape[1] + assert q.element_size() >= 2 or block_size >= 32, \ + "Block size must be at least 32 for fp8" + + use_alibi_slopes = alibi_slopes is not None + + block_size = v.shape[1] + num_seqs = len(seqused_k) + num_query_heads = q.shape[1] + num_kv_heads = k.shape[2] + num_queries_per_kv = num_query_heads // num_kv_heads + head_size = q.shape[2] + + BLOCK_M = 16 + BLOCK_Q = BLOCK_M // num_queries_per_kv + + # Ideally we would launch with kernel with: + # \sum_i[ceil(query_len[i] / BLOCK_Q)] blocks. + # However, it is slow to realize the query_lens on cpu. + # Instead we use upper-bound: + # \sum_i[ceil(query_len[i] / BLOCK_Q)] + # <= \sum_i[floor(query_len[i] / BLOCK_Q) + 1] + # = \sum_i[floor(query_len[i] / BLOCK_Q)] + num_seqs + # <= floor(\sum_i(query_len[i]) / BLOCK_Q) + num_seqs + # = floor(q.shape[0] / BLOCK_Q) + num_seqs + total_num_q_blocks = q.shape[0] // BLOCK_Q + num_seqs + + kernel_unified_attention_2d[( + total_num_q_blocks, + num_kv_heads, + )]( + output_ptr=out, + query_ptr=q, + key_cache_ptr=k, + value_cache_ptr=v, + block_tables_ptr=block_table, + seq_lens_ptr=seqused_k, + alibi_slopes_ptr=alibi_slopes, + scale=softmax_scale, + k_scale=k_descale, + v_scale=v_descale, + softcap=softcap, + num_query_heads=num_query_heads, + num_queries_per_kv=num_queries_per_kv, + block_table_stride=block_table.stride(0), + query_stride_0=q.stride(0), + query_stride_1=q.stride(1), + output_stride_0=out.stride(0), + output_stride_1=out.stride(1), + BLOCK_SIZE=block_size, + HEAD_SIZE=head_size, + HEAD_SIZE_PADDED=triton.next_power_of_2(head_size), + USE_ALIBI_SLOPES=use_alibi_slopes, + USE_SOFTCAP=(softcap > 0), + SLIDING_WINDOW=(1 + window_size[0]), + stride_k_cache_0=k.stride(0), + stride_k_cache_1=k.stride(1), + stride_k_cache_2=k.stride(2), + stride_k_cache_3=k.stride(3), + stride_v_cache_0=v.stride(0), + stride_v_cache_1=v.stride(1), + stride_v_cache_2=v.stride(2), + stride_v_cache_3=v.stride(3), + query_start_len_ptr=cu_seqlens_q, + BLOCK_Q=BLOCK_Q, + num_seqs=num_seqs, + BLOCK_M=BLOCK_M, + ) + + +def ref_paged_attn( + query: torch.Tensor, + key_cache: torch.Tensor, + value_cache: torch.Tensor, + query_lens: list[int], + kv_lens: list[int], + block_tables: torch.Tensor, + scale: float, + sliding_window: Optional[int] = None, + soft_cap: Optional[float] = None, +) -> torch.Tensor: + num_seqs = len(query_lens) + block_tables = block_tables.cpu().numpy() + _, block_size, num_kv_heads, head_size = key_cache.shape + + outputs: list[torch.Tensor] = [] + start_idx = 0 + for i in range(num_seqs): + query_len = query_lens[i] + kv_len = kv_lens[i] + q = query[start_idx:start_idx + query_len] + q *= scale + + num_kv_blocks = (kv_len + block_size - 1) // block_size + block_indices = block_tables[i, :num_kv_blocks] + + k = key_cache[block_indices].view(-1, num_kv_heads, head_size) + k = k[:kv_len] + v = value_cache[block_indices].view(-1, num_kv_heads, head_size) + v = v[:kv_len] + + if q.shape[1] != k.shape[1]: + k = torch.repeat_interleave(k, q.shape[1] // k.shape[1], dim=1) + v = torch.repeat_interleave(v, q.shape[1] // v.shape[1], dim=1) + attn = torch.einsum("qhd,khd->hqk", q, k).float() + empty_mask = torch.ones(query_len, kv_len) + mask = torch.triu(empty_mask, diagonal=kv_len - query_len + 1).bool() + if sliding_window is not None: + sliding_window_mask = torch.triu(empty_mask, + diagonal=kv_len - (query_len + sliding_window) + 1).bool().logical_not() + mask |= sliding_window_mask + if soft_cap is not None and soft_cap > 0: + attn = soft_cap * torch.tanh(attn / soft_cap) + attn.masked_fill_(mask, float("-inf")) + attn = torch.softmax(attn, dim=-1).to(v.dtype) + out = torch.einsum("hqk,khd->qhd", attn, v) + + outputs.append(out) + start_idx += query_len + + return torch.cat(outputs, dim=0) + + +@pytest.mark.perf(repeat=17) +@pytest.mark.parametrize("seq_lens", [[(1, 1328), (5, 18), (129, 463)], [(1, 523), (1, 37), (1, 2011)]]) +@pytest.mark.parametrize("num_heads", NUM_HEADS) +@pytest.mark.parametrize("head_size", HEAD_SIZES) +@pytest.mark.parametrize("block_size", BLOCK_SIZES) +@pytest.mark.parametrize("sliding_window", [None]) +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("soft_cap", [None]) +@pytest.mark.parametrize("num_blocks", NUM_BLOCKS) +@pytest.mark.parametrize("q_dtype", QDTYPES) +@torch.inference_mode() +def test_triton_unified_attn( + seq_lens: list[tuple[int, int]], + num_heads: tuple[int, int], + head_size: int, + sliding_window: Optional[int], + dtype: torch.dtype, + block_size: int, + soft_cap: Optional[float], + num_blocks: int, + q_dtype: Optional[torch.dtype], + request: pytest.FixtureRequest, +) -> None: + previous_default_device = torch.get_default_device() + request.addfinalizer(lambda: torch.set_default_device(previous_default_device)) + torch.set_default_device("npu") + + if q_dtype is not None and q_dtype.itemsize < 2 and block_size < 32: + pytest.skip("block size must be at least 32 for fp8") + + num_seqs = len(seq_lens) + query_lens = [x[0] for x in seq_lens] + kv_lens = [x[1] for x in seq_lens] + num_query_heads = num_heads[0] + num_kv_heads = num_heads[1] + assert num_query_heads % num_kv_heads == 0 + max_query_len = max(query_lens) + max_kv_len = max(kv_lens) + window_size = ((sliding_window - 1, 0) if sliding_window is not None else (-1, -1)) + scale = head_size**-0.5 + + query = torch.randn(sum(query_lens), num_query_heads, head_size, dtype=dtype) + key_cache = torch.randn(num_blocks, block_size, num_kv_heads, head_size, dtype=dtype) + value_cache = torch.randn_like(key_cache) + cu_query_lens = torch.tensor([0] + query_lens, dtype=torch.int32).cumsum(dim=0, dtype=torch.int32) + kv_lens = torch.tensor(kv_lens, dtype=torch.int32) + + max_num_blocks_per_seq = (max_kv_len + block_size - 1) // block_size + block_tables = torch.randint(0, num_blocks, (num_seqs, max_num_blocks_per_seq), dtype=torch.int32) + + output = torch.empty_like(query) + + maybe_quantized_query = query + maybe_quantized_key_cache = key_cache + maybe_quantized_value_cache = value_cache + q_descale = None + k_descale = None + v_descale = None + if q_dtype is not None: + # QKV are drawn from N(0, 1): no need for a fp8 scaling factor + maybe_quantized_query = query.to(q_dtype) + maybe_quantized_key_cache = key_cache.to(q_dtype) + maybe_quantized_value_cache = value_cache.to(q_dtype) + + scale_shape = (num_seqs, num_kv_heads) + q_descale = None # Not yet supported + k_descale = torch.rand(scale_shape, dtype=torch.float32) + v_descale = torch.rand(scale_shape, dtype=torch.float32) + + unified_attention( + q=maybe_quantized_query, + k=maybe_quantized_key_cache, + v=maybe_quantized_value_cache, + out=output, + cu_seqlens_q=cu_query_lens, + seqused_k=kv_lens, + max_seqlen_q=max_query_len, + max_seqlen_k=max_kv_len, + softmax_scale=scale, + causal=True, + window_size=window_size, + block_table=block_tables, + softcap=soft_cap if soft_cap is not None else 0, + q_descale=q_descale, + k_descale=k_descale, + v_descale=v_descale, + ) + + ref_output = ref_paged_attn( + query=query, + key_cache=key_cache, + value_cache=value_cache, + query_lens=query_lens, + kv_lens=kv_lens, + block_tables=block_tables, + scale=scale, + sliding_window=sliding_window, + soft_cap=soft_cap, + ) + atol, rtol = 1.5e-2, 1e-2 + if q_dtype is not None: + atol, rtol = 1.5e-1, 1.5e-1 + torch.testing.assert_close(output, ref_output, atol=atol, rtol=rtol), \ + f"{torch.max(torch.abs(output - ref_output))}" diff --git a/third_party/ascend/unittest/pytest_ut/test_trunc.py b/third_party/ascend/unittest/pytest_ut/test_trunc.py new file mode 100644 index 0000000000..ec02d678b1 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_trunc.py @@ -0,0 +1,68 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import torch +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + + +@triton.jit +def trunc_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(axis=0) + block_start = pid * BLOCK_SIZE + offsets = block_start + tl.arange(0, BLOCK_SIZE) + + mask = offsets < n_elements + + x = tl.load(x_ptr + offsets, mask=mask) + + y = libdevice.trunc(x) + + tl.store(y_ptr + offsets, y, mask=mask) + + +@pytest.mark.parametrize('shape', [ + (12, 16), +]) +@pytest.mark.parametrize('dtype', ['float32']) +def test_cases(shape, dtype): + n_elements = shape[0] * shape[1] + x = test_common.generate_tensor(shape, dtype).npu() + + # Make sure to include some edge cases. + x[0, 0] = 0.0 + x[0, 1] = 3.14 + x[0, 2] = -2.71 + x[0, 3] = 5.0 + x[0, 4] = -3.0 + + y = torch.empty_like(x) + + BLOCK_SIZE = 192 + grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), ) + + trunc_kernel[grid](x, y, n_elements, BLOCK_SIZE=BLOCK_SIZE) + + expected = torch.trunc(x) + + torch.testing.assert_close(y, expected, rtol=1e-3, atol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_umulhi.py b/third_party/ascend/unittest/pytest_ut/test_umulhi.py new file mode 100644 index 0000000000..3f4617bdeb --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_umulhi.py @@ -0,0 +1,62 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import numpy as np +import triton +import triton.language as tl +from numpy.random import RandomState + + +# inp the two 32 bit signed integers. +@triton.jit +def umulhi_kernel(X, Y, Z, N: tl.constexpr): + offs = tl.arange(0, N) + x = tl.load(X + offs) + y = tl.load(Y + offs) + z = tl.umulhi(x, y) + tl.store(Z + tl.arange(0, N), z) + + +# accuracy reference +def umulhi32(a, b): + a_64 = a.astype(np.int64) + b_64 = b.astype(np.int64) + product_64 = a_64 * b_64 + # get the high part + result_high_32 = product_64 >> 32 + return result_high_32.astype(np.int32) + + +def test_umulhi(): + N = 128 + x = torch.randint(low=0, high=2000, size=(N, ), dtype=torch.int32) + y = torch.randint(low=0, high=2000, size=(N, ), dtype=torch.int32) + xx = x.npu() + yy = y.npu() + z_tri = torch.zeros(size=(N, ), dtype=torch.int32).npu() + umulhi_kernel[(1, )](xx, yy, z_tri, N=N) + + xxx = x.cpu().numpy() + yyy = y.cpu().numpy() + z_ref = umulhi32(xxx, yyy) + z_ref1 = torch.from_numpy(z_ref).npu() + torch.equal(z_tri, z_ref1) diff --git a/third_party/ascend/unittest/pytest_ut/test_unlign_max_with_index.py b/third_party/ascend/unittest/pytest_ut/test_unlign_max_with_index.py new file mode 100644 index 0000000000..74e96e5e45 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_unlign_max_with_index.py @@ -0,0 +1,115 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import pytest +import test_common + + +@triton.jit +def promote_to_tensor(x): + # Addition promotes to tensor for us + return x + tl.zeros((1, ), tl.int1) + + +@triton.jit +def is_floating(x): + return promote_to_tensor(x).dtype.is_floating() + + +@triton.jit +def maximum_with_index(a_value, a_index, b_value, b_index): + mask = a_value > b_value + equal = a_value == b_value + if is_floating(a_value): + a_isnan = a_value != a_value + b_isnan = b_value != b_value + mask |= a_isnan and not b_isnan + # Consider NaNs as equal + equal |= a_isnan and b_isnan + + # Prefer lowest index if values are equal + mask |= equal & (a_index < b_index) + return tl.where(mask, a_value, b_value), tl.where(mask, a_index, b_index) + + +@triton.jit +def max_with_index(value, index, dim): + return tl.reduce((value, index), dim, maximum_with_index) + + +def torch_max_pr_index(x0): + res = torch.max(x0, 0) + return res + + +# x0_numel 128, r1_numel 65, 128, 128, 32 +@triton.jit +def triton_kernel(in_ptr0, in_ptr1, out_ptr0, out_ptr1, x0_numel, r1_numel, XBLOCK: tl.constexpr, + XBLOCK_SUB: tl.constexpr, RBLOCK: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + base2 = tl.arange(0, RBLOCK) + loops2: tl.constexpr = (r1_numel + RBLOCK - 1) // RBLOCK + for loop1 in range(loops1): + x0 = offset + (loop1 * XBLOCK_SUB) + base1[:, None] + xmask = x0 < x0_numel + for loop2 in range(loops2): + r1 = loop2 * RBLOCK + base2[None, :] + rmask = r1 < r1_numel + tmp0 = tl.load(in_ptr0 + (r1 + (65 * x0)), rmask & xmask, other=0) + tmp2 = tl.load(in_ptr1 + (r1 + (65 * x0)), rmask & xmask, other=0) + tmp11 = tl.reshape(tmp0, [XBLOCK_SUB, RBLOCK]) + tmp22 = tl.reshape(tmp2, [XBLOCK_SUB, RBLOCK]) + tmp4, tmp5 = max_with_index(tmp11, tmp22, 0) + tl.store(out_ptr0 + r1, tmp4[None, :], None) + tl.store(out_ptr1 + r1, tmp5[None, :], None) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (128, 65), 1, 128, 65, 32], +]) +def test_max_with_index_dim0(param_list): + dtype, shape, ncore, XB, YB, RBLOCK = param_list + import math + numel = math.prod(shape) + xblock = numel // shape[-1] // ncore + assert (ncore * xblock * shape[-1] == numel) + + b = test_common.generate_tensor(shape, dtype).npu() + idx = torch.zeros((shape), device='npu', dtype=torch.int32) + c, d = torch.max(b, dim=0) + + print(f"std_ret = {c[0:4]}") + + ret = torch.empty((YB), device='npu', dtype=torch.float32) + ret1 = torch.zeros((YB), device='npu', dtype=torch.int32) + triton_kernel[ncore, 1, 1](b, idx, ret, ret1, shape[0], shape[1], shape[0], shape[0], RBLOCK) + print(f"triton_ret = {ret[0:4]}") + d = d.to(torch.int32) + ret1 = ret1.to(torch.int32) + print(f"d = {d[0:4]}") + print(f"triton_ret1 = {ret1[0:4]}") + assert torch.allclose(c, ret, rtol=1e-03, atol=1e-03, equal_nan=True) + assert torch.allclose(d, ret1, rtol=1e-03, atol=1e-03, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_unlign_sum.py b/third_party/ascend/unittest/pytest_ut/test_unlign_sum.py new file mode 100644 index 0000000000..31a34f74df --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_unlign_sum.py @@ -0,0 +1,64 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import torch +import torch_npu +import triton +import triton.language as tl +import triton +import triton.language as tl + +# [128,65] -> [128,128] +# [128,128] -> [128,65] mask 作用 +# [128,65] -> [128,64] [128,1] mask 作用 + + +@triton.jit +def triton_unlign(in_ptr0, out_ptr0, x0_numel, r1_numel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr, + RBLOCK: tl.constexpr): + offset = tl.program_id(0) * XBLOCK + base1 = tl.arange(0, XBLOCK_SUB) + loops1: tl.constexpr = (XBLOCK + XBLOCK_SUB - 1) // XBLOCK_SUB + base2 = tl.arange(0, RBLOCK) + loops2: tl.constexpr = (r1_numel + RBLOCK - 1) // RBLOCK + for loop1 in range(loops1): + x = offset + (loop1 * XBLOCK_SUB) + base1 + x0 = offset + (loop1 * XBLOCK_SUB) + base1[:, None] + xmask = x0 < x0_numel + _tmp2 = tl.full([XBLOCK_SUB, RBLOCK], 0, tl.float32) + for loop2 in range(loops2): + r1_prime = loop2 * RBLOCK + base2[:, None] + r1 = loop2 * RBLOCK + base2[None, :] + rmask = r1 < r1_numel + tmp0 = tl.load(in_ptr0 + (r1 + (65 * x0)), rmask & xmask, eviction_policy='evict_first', other=0.0) + tmp1 = tl.reshape(tmp0, [XBLOCK_SUB, RBLOCK]) + tmp3 = _tmp2 + tmp1 + _tmp2 = tmp3 + tmp2 = tl.sum(_tmp2, 1).reshape(XBLOCK_SUB, 1) + tl.store(out_ptr0 + (x0), tmp2, xmask) + + +def test_cases(): + size = (128, 65) + b = weights = torch.randn((size), dtype=torch.float32).npu() + c = torch.sum(b, dim=1) + ret = torch.randn((size[0]), device='npu', dtype=torch.float32).npu().reshape(size[0]) + triton_unlign[1, 1, 1](b, ret, size[0], size[1], size[0], size[0], 32) + assert torch.allclose(c, ret, rtol=1e-03, atol=1e-03, equal_nan=True) diff --git a/third_party/ascend/unittest/pytest_ut/test_unorder_block_lock.py b/third_party/ascend/unittest/pytest_ut/test_unorder_block_lock.py new file mode 100644 index 0000000000..168b75c44b --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_unorder_block_lock.py @@ -0,0 +1,360 @@ +import pytest +import torch +import triton +import triton.language as tl + +import test_common +from triton.backends.ascend.utils import is_compile_on_910_95 + + +@triton.jit +def store_mask_split_dim0( + in_ptr, + out_ptr, + mask_ptr, + cache_modifier: tl.constexpr, + eviction_policy: tl.constexpr, + BLOCK_1: tl.constexpr, + BLOCK_2: tl.constexpr, + BLOCK_3: tl.constexpr, + BLOCK_4: tl.constexpr, + BLOCK_5: tl.constexpr, + BLOCK_6: tl.constexpr, + BLOCK_7: tl.constexpr, + STRIDE_0: tl.constexpr, + STRIDE_1: tl.constexpr, + STRIDE_2: tl.constexpr, + STRIDE_3: tl.constexpr, + STRIDE_4: tl.constexpr, + STRIDE_5: tl.constexpr, + STRIDE_6: tl.constexpr, + STRIDE_7: tl.constexpr, +): + # Each program processes one complete slice of dimension 0. + offsets = tl.program_id(0) * STRIDE_0 + offsets = offsets + tl.arange(0, BLOCK_1) * STRIDE_1 + offsets = offsets[:, None] + tl.arange(0, BLOCK_2)[None, :] * STRIDE_2 + offsets = offsets[:, :, None] + tl.arange(0, BLOCK_3)[None, None, :] * STRIDE_3 + offsets = offsets[:, :, :, None] + tl.arange(0, BLOCK_4)[None, None, None, :] * STRIDE_4 + offsets = offsets[:, :, :, :, None] + tl.arange(0, BLOCK_5)[None, None, None, None, :] * STRIDE_5 + offsets = offsets[:, :, :, :, :, None] + tl.arange(0, BLOCK_6)[None, None, None, None, None, :] * STRIDE_6 + offsets = offsets[:, :, :, :, :, :, None] + tl.arange(0, BLOCK_7)[None, None, None, None, None, None, :] * STRIDE_7 + + mask = tl.load(mask_ptr + offsets) != 0 + value = tl.load(in_ptr + offsets) + tl.store( + out_ptr + offsets, + value, + mask=mask, + cache_modifier=cache_modifier, + eviction_policy=eviction_policy, + ) + + +@pytest.mark.parametrize("shape", [(15, 2, 2, 2, 3, 2, 2, 2)]) +@pytest.mark.parametrize("dtype", ["bfloat16"]) +def test_unorder_block_lock(shape, dtype): + mask = test_common.generate_tensor(shape, "bool").npu() + value = test_common.generate_tensor(shape, dtype).npu() + actual = test_common.generate_tensor(shape, dtype).npu() + original = actual.clone() + + blocks = list(value.size()) + strides = list(value.stride()) + grid = (shape[0], ) + store_mask_split_dim0[grid]( + value, + actual, + mask, + None, + None, + *blocks[1:], + *strides, + ) + torch.npu.synchronize() + + expected = torch.where(mask, value, original) + torch.testing.assert_close(actual.cpu(), expected.cpu()) + + +CV_BLOCK_M = 64 +CV_BLOCK_N = 64 +CV_BLOCK_K = 64 + + +@triton.jit +def simd_unorder_block_lock_a5( + x_ptr, + group_id_ptr, + row_index_ptr, + tile_start_ptr, + weight_ptr, + out_ptr, + stride_xm: tl.constexpr, + stride_xk: tl.constexpr, + stride_wg: tl.constexpr, + stride_wk: tl.constexpr, + stride_wn: tl.constexpr, + stride_om: tl.constexpr, + stride_on: tl.constexpr, + M: tl.constexpr, + GROUPS: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, +): + n_tile_id = tl.program_id(0) + m_tile_id = tl.program_id(1) + tile_start = tl.load(tile_start_ptr + m_tile_id) + # Some programs skip the guarded region, so lock participation is unordered. + if tile_start >= M: + return + + m_offsets = tile_start + tl.arange(0, BLOCK_M) + group_ids = tl.load(group_id_ptr + m_offsets, mask=m_offsets < M, other=GROUPS) + group_id = tl.min(group_ids) + row_mask = group_ids == group_id + # Indirect loads make the AIV path contain SIMT work. + row_indices = tl.load(row_index_ptr + m_offsets, mask=row_mask, other=0) + + k_offsets = tl.arange(0, BLOCK_K) + n_offsets = n_tile_id * BLOCK_N + tl.arange(0, BLOCK_N) + x_ptrs = x_ptr + row_indices[:, None] * stride_xm + k_offsets[None, :] * stride_xk + weight_ptrs = (weight_ptr + group_id * stride_wg + k_offsets[:, None] * stride_wk + n_offsets[None, :] * stride_wn) + x = tl.load(x_ptrs, mask=row_mask[:, None], other=0.0) + weight = tl.load(weight_ptrs) + # Dot adds Cube work and makes this a CV mixed kernel. + acc = tl.dot(x, weight, out_dtype=tl.float32) + + # Build rank-6 output pointers and a non-contiguous mask for the discrete + # mask store below. Discrete-mask rank > 5 selects the SIMD lowering path. + m_store_offsets = tile_start + m_store_offsets = m_store_offsets + tl.arange(0, 2)[:, None, None, None, None, None] * 32 + m_store_offsets = m_store_offsets + tl.arange(0, 2)[None, :, None, None, None, None] * 16 + m_store_offsets = m_store_offsets + tl.arange(0, 2)[None, None, :, None, None, None] * 8 + m_store_offsets = m_store_offsets + tl.arange(0, 2)[None, None, None, :, None, None] * 4 + m_store_offsets = m_store_offsets + tl.arange(0, 4)[None, None, None, None, :, None] + n_store_offsets = n_tile_id * BLOCK_N + tl.arange(0, BLOCK_N)[None, None, None, None, None, :] + out_ptrs = out_ptr + m_store_offsets * stride_om + n_store_offsets * stride_on + store_group_ids = tl.load( + group_id_ptr + m_store_offsets, + mask=m_store_offsets < M, + other=GROUPS, + ) + store_mask = (m_store_offsets < M) & (store_group_ids == group_id) & (n_store_offsets < BLOCK_N) + simd_shape: tl.constexpr = (2, 2, 2, 2, 4, 64) + acc = tl.reshape(acc.to(out_ptr.dtype.element_ty), simd_shape) + # This rank-6 masked store triggers SIMD discrete-mask conversion, which + # inserts the unordered block lock around the generated vector operation. + tl.store(out_ptrs, acc, mask=store_mask) + + +def test_cv_simd_unorder_block_lock_a5(): + torch.manual_seed(0) + device = torch.device("npu") + dtype = torch.bfloat16 + m, k, n, groups = 96, CV_BLOCK_K, CV_BLOCK_N, 2 + + x = torch.randn((m, k), device=device, dtype=dtype) + weight = torch.randn((groups, k, n), device=device, dtype=dtype) + group_ids = torch.cat(( + torch.zeros(48, device=device, dtype=torch.int64), + torch.ones(48, device=device, dtype=torch.int64), + )) + row_indices = torch.arange(m, device=device, dtype=torch.int64) + # The last two programs intentionally skip the guarded region. + tile_starts = torch.tensor([0, 48, m, m], device=device, dtype=torch.int64) + actual = torch.full((m, n), float("nan"), device=device, dtype=dtype) + + simd_unorder_block_lock_a5[(1, tile_starts.numel())]( + x, + group_ids, + row_indices, + tile_starts, + weight, + actual, + x.stride(0), + x.stride(1), + weight.stride(0), + weight.stride(1), + weight.stride(2), + actual.stride(0), + actual.stride(1), + M=m, + GROUPS=groups, + BLOCK_M=CV_BLOCK_M, + BLOCK_N=CV_BLOCK_N, + BLOCK_K=CV_BLOCK_K, + ) + torch.npu.synchronize() + + expected = torch.empty((m, n), device=device, dtype=dtype) + expected[:48] = torch.matmul(x[:48].float(), weight[0].float()).to(dtype) + expected[48:] = torch.matmul(x[48:].float(), weight[1].float()).to(dtype) + torch.testing.assert_close(actual.cpu(), expected.cpu(), rtol=2e-2, atol=2e-2) + + +@triton.jit +def simd_three_unorder_block_locks( + in_ptr, + out0_ptr, + out1_ptr, + out2_ptr, + mask0_ptr, + mask1_ptr, + mask2_ptr, + BLOCK_1: tl.constexpr, + BLOCK_2: tl.constexpr, + BLOCK_3: tl.constexpr, + BLOCK_4: tl.constexpr, + BLOCK_5: tl.constexpr, + BLOCK_6: tl.constexpr, + BLOCK_7: tl.constexpr, + STRIDE_0: tl.constexpr, + STRIDE_1: tl.constexpr, + STRIDE_2: tl.constexpr, + STRIDE_3: tl.constexpr, + STRIDE_4: tl.constexpr, + STRIDE_5: tl.constexpr, + STRIDE_6: tl.constexpr, + STRIDE_7: tl.constexpr, +): + offsets = tl.program_id(0) * STRIDE_0 + offsets = offsets + tl.arange(0, BLOCK_1) * STRIDE_1 + offsets = offsets[:, None] + tl.arange(0, BLOCK_2)[None, :] * STRIDE_2 + offsets = offsets[:, :, None] + tl.arange(0, BLOCK_3)[None, None, :] * STRIDE_3 + offsets = offsets[:, :, :, None] + tl.arange(0, BLOCK_4)[None, None, None, :] * STRIDE_4 + offsets = offsets[:, :, :, :, None] + tl.arange(0, BLOCK_5)[None, None, None, None, :] * STRIDE_5 + offsets = offsets[:, :, :, :, :, None] + tl.arange(0, BLOCK_6)[None, None, None, None, None, :] * STRIDE_6 + offsets = offsets[:, :, :, :, :, :, None] + tl.arange(0, BLOCK_7)[None, None, None, None, None, None, :] * STRIDE_7 + + value = tl.load(in_ptr + offsets) + mask0 = tl.load(mask0_ptr + offsets) != 0 + mask1 = tl.load(mask1_ptr + offsets) != 0 + mask2 = tl.load(mask2_ptr + offsets) != 0 + + # Each rank-7 discrete mask below selects SIMD masked-store lowering and + # therefore creates an independent unordered block lock. + tl.store(out0_ptr + offsets, value, mask=mask0) + tl.store(out1_ptr + offsets, value + 1.0, mask=mask1) + tl.store(out2_ptr + offsets, value * 2.0, mask=mask2) + + +@pytest.mark.parametrize('shape', [(15, 2, 2, 2, 3, 2, 2, 2)]) +@pytest.mark.parametrize('dtype', ['bfloat16']) +def test_simd_three_unorder_block_locks_a3_a5(shape, dtype): + torch.manual_seed(0) + value = test_common.generate_tensor(shape, dtype).npu() + masks = [test_common.generate_tensor(shape, 'bool').npu() for _ in range(3)] + actuals = [test_common.generate_tensor(shape, dtype).npu() for _ in range(3)] + originals = [actual.clone() for actual in actuals] + + blocks = list(value.size()) + strides = list(value.stride()) + simd_three_unorder_block_locks[(shape[0], )]( + value, + *actuals, + *masks, + *blocks[1:], + *strides, + ) + torch.npu.synchronize() + + stored_values = (value, value + 1.0, value * 2.0) + for actual, original, mask, stored_value in zip(actuals, originals, masks, stored_values): + expected = torch.where(mask, stored_value, original) + torch.testing.assert_close(actual.cpu(), expected.cpu()) + + +@triton.jit +def simd_unorder_block_lock_a3( + x_ptr, + group_id_ptr, + row_index_ptr, + tile_start_ptr, + weight_ptr, + out_ptr, + stride_xm: tl.constexpr, + stride_xk: tl.constexpr, + stride_wg: tl.constexpr, + stride_wk: tl.constexpr, + stride_wn: tl.constexpr, + stride_om: tl.constexpr, + stride_on: tl.constexpr, + M: tl.constexpr, + GROUPS: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, +): + n_tile_id = tl.program_id(0) + m_tile_id = tl.program_id(1) + tile_start = tl.load(tile_start_ptr + m_tile_id) + # Some programs skip the guarded region, so lock participation is unordered. + if tile_start >= M: + return + + m_offsets = tile_start + tl.arange(0, BLOCK_M) + group_ids = tl.load(group_id_ptr + m_offsets, mask=m_offsets < M, other=GROUPS) + group_id = tl.min(group_ids) + row_mask = group_ids == group_id + row_indices = tl.load(row_index_ptr + m_offsets, mask=row_mask, other=0) + + k_offsets = tl.arange(0, BLOCK_K) + n_offsets = n_tile_id * BLOCK_N + tl.arange(0, BLOCK_N) + x_ptrs = x_ptr + row_indices[:, None] * stride_xm + k_offsets[None, :] * stride_xk + weight_ptrs = (weight_ptr + group_id * stride_wg + k_offsets[:, None] * stride_wk + n_offsets[None, :] * stride_wn) + x = tl.load(x_ptrs, mask=row_mask[:, None], other=0.0) + weight = tl.load(weight_ptrs) + acc = tl.dot(x, weight, out_dtype=tl.float32) + + out_ptrs = out_ptr + m_offsets[:, None] * stride_om + n_offsets[None, :] * stride_on + store_mask = row_mask[:, None] & (n_offsets[None, :] < BLOCK_N) + # On A3 this original rank-2 discrete masked store selects SIMD lowering + # and inserts the unordered block lock around the vector operation. + tl.store(out_ptrs, acc.to(out_ptr.dtype.element_ty), mask=store_mask) + + +@pytest.mark.skipif(is_compile_on_910_95(), reason="requires A3") +def test_cv_simd_unorder_block_lock_a3(): + torch.manual_seed(0) + device = torch.device("npu") + dtype = torch.bfloat16 + m, k, n, groups = 96, CV_BLOCK_K, CV_BLOCK_N, 2 + + x = torch.randn((m, k), device=device, dtype=dtype) + weight = torch.randn((groups, k, n), device=device, dtype=dtype) + group_ids = torch.cat(( + torch.zeros(48, device=device, dtype=torch.int64), + torch.ones(48, device=device, dtype=torch.int64), + )) + row_indices = torch.arange(m, device=device, dtype=torch.int64) + tile_starts = torch.tensor([0, 48, m, m], device=device, dtype=torch.int64) + actual = torch.full((m, n), float("nan"), device=device, dtype=dtype) + + simd_unorder_block_lock_a3[(1, tile_starts.numel())]( + x, + group_ids, + row_indices, + tile_starts, + weight, + actual, + x.stride(0), + x.stride(1), + weight.stride(0), + weight.stride(1), + weight.stride(2), + actual.stride(0), + actual.stride(1), + M=m, + GROUPS=groups, + BLOCK_M=CV_BLOCK_M, + BLOCK_N=CV_BLOCK_N, + BLOCK_K=CV_BLOCK_K, + ) + torch.npu.synchronize() + + expected = torch.empty((m, n), device=device, dtype=dtype) + expected[:48] = torch.matmul(x[:48].float(), weight[0].float()).to(dtype) + expected[48:] = torch.matmul(x[48:].float(), weight[1].float()).to(dtype) + torch.testing.assert_close(actual.cpu(), expected.cpu(), rtol=2e-2, atol=2e-2) diff --git a/third_party/ascend/unittest/pytest_ut/test_unused_func_arg.py b/third_party/ascend/unittest/pytest_ut/test_unused_func_arg.py new file mode 100644 index 0000000000..3e7812de86 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_unused_func_arg.py @@ -0,0 +1,82 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import test_common +import torch +import torch_npu +import pytest +import math + + +def expand_to_next_power_of_two(a): + if a <= 0: + raise ValueError("must >0") + if (math.log2(a)).is_integer(): + return a + return 2**math.ceil(math.log2(a)) + + +@triton.jit +def triton_unused_func_arg_kernel(output_ptr, x_ptr, X: tl.constexpr, Y: tl.constexpr, Z: tl.constexpr, + XNUMEL: tl.constexpr, YNUMEL: tl.constexpr, ZNUMEL: tl.constexpr): + xidx = tl.arange(0, XNUMEL) + yidx = tl.arange(0, YNUMEL) + zidx = tl.arange(0, ZNUMEL) + Xmask = xidx < X + Ymask = yidx < Y + Zmask = zidx < Z + oidx = xidx[:, None, None] * Y * Z + yidx[None, :, None] * Z + zidx[None, None, :] + mask = (Xmask[:, None, None]) & (Ymask[None, :, None]) & (Zmask[None, None, :]) + abc = tl.load(x_ptr + oidx, mask=mask) + ret = tl.zeros_like(abc) + tl.store(output_ptr + oidx, ret, mask=mask) + + +testlist = [ + (triton_unused_func_arg_kernel, 'int8', torch.int8, 2, 255, 9), + (triton_unused_func_arg_kernel, 'int16', torch.int16, 3, 5, 3), + (triton_unused_func_arg_kernel, 'int32', torch.int32, 2, 255, 9), + (triton_unused_func_arg_kernel, 'int64', torch.int64, 2, 5, 3), + (triton_unused_func_arg_kernel, 'float16', torch.float16, 55, 5, 16), + (triton_unused_func_arg_kernel, 'float16', torch.float16, 4, 5, 17), + (triton_unused_func_arg_kernel, 'float16', torch.float16, 6, 5, 15), + (triton_unused_func_arg_kernel, 'float16', torch.float16, 2, 1928, 3), + (triton_unused_func_arg_kernel, 'float32', torch.float32, 2, 255, 9), + (triton_unused_func_arg_kernel, 'bfloat16', torch.bfloat16, 3, 5, 3), + (triton_unused_func_arg_kernel, 'bool', torch.bool, 3, 5, 3), +] + + +@pytest.mark.parametrize('testfunc, sigtype, dtype, X, Y, Z', testlist) +def test_npu(testfunc, sigtype, dtype, X, Y, Z): + XNUMEL = expand_to_next_power_of_two(X) + YNUMEL = expand_to_next_power_of_two(Y) + ZNUMEL = expand_to_next_power_of_two(Z) + x = torch.full((X, Y, Z), 10, dtype=dtype).npu() + y = torch.full((X, Y, Z), 0, dtype=dtype).npu() + output = torch.full((X, Y, Z), 5, dtype=dtype).npu() + testfunc[1, 1, 1](output, x, X, Y, Z, XNUMEL, YNUMEL, ZNUMEL) + test_common.validate_cmp(sigtype, output, y) + + +if __name__ == "__main__": + test_npu(triton_unused_func_arg_kernel, "bool", torch.bool, 3, 5, 3) diff --git a/third_party/ascend/unittest/pytest_ut/test_use_analysis.py b/third_party/ascend/unittest/pytest_ut/test_use_analysis.py new file mode 100644 index 0000000000..06ce0a2470 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_use_analysis.py @@ -0,0 +1,74 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest + +import triton +import triton.language as tl +import triton.language.extra.cann.libdevice as libdevice +import test_common + +import torch +import torch_npu + + +@triton.jit +def triton_reduce_deadcode(v_ptr, in_ptr0, in_ptr1, out_ptr0, VBLOCK: tl.constexpr, XBLOCK: tl.constexpr, + YBLOCK: tl.constexpr): + v_idx = tl.arange(0, VBLOCK) + v = tl.load(v_ptr + v_idx) + v_ret = tl.argmax(v, 0) + if v_ret < v_ret + 1: + for _ in range(v_ret, v_ret + 1): + cube_idx = tl.arange(0, XBLOCK)[:, None] * YBLOCK + tl.arange(0, YBLOCK)[None, :] + c0 = tl.load(in_ptr0 + cube_idx) + c1 = tl.load(in_ptr1 + cube_idx) + ret = tl.dot(c0, c1) + 1 + tl.store(out_ptr0 + cube_idx, ret) + else: + for _ in range(v_ret - 1, v_ret): + cube_idx = tl.arange(0, XBLOCK)[:, None] * YBLOCK + tl.arange(0, YBLOCK)[None, :] + c0 = tl.load(in_ptr0 + cube_idx) + c1 = tl.load(in_ptr1 + cube_idx) + ret = tl.dot(c0, c1) + 1 + tl.store(out_ptr0 + cube_idx, ret) + + +def torch_reduce_deadcode(in0, in1, v): + v_ret = torch.argmax(v) + if v_ret < v_ret + 1: + ret = torch.matmul(in0, in1) + 1 + else: + ret = torch.matmul(in0, in1) + 1 + return ret + + +def test_reduce_deadcode(): + VBLOCK, XBLOCK, YBLOCK = 16, 16, 16 + sigtype = 'float32' + dtype = torch.float32 + in0 = torch.randn((XBLOCK, YBLOCK), dtype=dtype, device='npu') + in1 = torch.randn((XBLOCK, YBLOCK), dtype=dtype, device='npu') + v = torch.randn((VBLOCK, ), dtype=dtype, device='npu') + out = torch.zeros((XBLOCK, YBLOCK), dtype=dtype, device='npu') + + triton_reduce_deadcode[(1, )](v, in0, in1, out, VBLOCK=VBLOCK, XBLOCK=XBLOCK, YBLOCK=YBLOCK) + expected = torch_reduce_deadcode(in0, in1, v) + test_common.validate_cmp(sigtype, out, expected) diff --git a/third_party/ascend/unittest/pytest_ut/test_utils.py b/third_party/ascend/unittest/pytest_ut/test_utils.py new file mode 100644 index 0000000000..ee8ec1b28f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_utils.py @@ -0,0 +1,48 @@ +import os +import platform +import hashlib +from unittest.mock import patch, mock_open +import unittest +import pytest +from triton.backends.ascend.utils import ( + get_cann_version_file_hash, + get_machine_arch, +) + + +@patch.object(platform, "machine") +def test_get_machine_arch_normal_case(mock_machine): + mock_machine.return_value = "x86_64" + result = get_machine_arch() + assert result == "x86_64" + + +@patch.object(platform, "machine") +def test_get_machine_arch_arm_case(mock_machine): + mock_machine.return_value = "arm64" + result = get_machine_arch() + assert result == "aarch64" + + +@patch.object(platform, "machine") +def test_get_machine_arch_unknown_case(mock_machine): + mock_machine.return_value = "unknown" + with pytest.raises(KeyError): + get_machine_arch() + + +def test_get_cann_version_file_hash_normal_case(): + test_version = "9.0.0" + test_inner_version = "v100rc00" + test_file_context = f"version={test_version}\ninnerversion={test_inner_version}".encode("utf-8") + with patch("builtins.open", mock_open(read_data=test_file_context)): + result = get_cann_version_file_hash() + excepted_hash = hashlib.sha256(test_file_context).hexdigest() + assert result == excepted_hash + + +def test_get_cann_version_file_hash_file_not_find_case(): + with patch("builtins.open") as mock_open_file: + mock_open_file.side_effect = FileNotFoundError + with pytest.raises(FileNotFoundError): + get_cann_version_file_hash() diff --git a/third_party/ascend/unittest/pytest_ut/test_view.py b/third_party/ascend/unittest/pytest_ut/test_view.py new file mode 100644 index 0000000000..988773b2c3 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_view.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + + ret = tl.view(X, (ZB, XB * YB)) + + oidx = tl.arange(0, ZB)[:, None] * XB * YB + tl.arange(0, XB * YB)[None, :] + + tl.store(output_ptr + oidx, ret) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 256, 16), 1, 2, 256, 16], + ['float32', (8, 8, 4), 1, 8, 8, 4], + ['float16', (2, 256, 16), 1, 2, 256, 16], + ['float16', (8, 8, 4), 1, 8, 8, 4], + ['int8', (2, 256, 16), 1, 2, 256, 16], + ['int8', (8, 8, 4), 1, 8, 8, 4], +]) +def test_case(param_list): + dtype, shape, ncore, XB, YB, ZB = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = x0.view(ZB, XB * YB).npu() + print(f"y_ref = {y_ref[0, 0:4]}") + y_cal = torch.empty((ZB, XB * YB), dtype=eval('torch.' + dtype)).npu() + + fn_npu_[ncore, 1, 1](y_cal, x0, XB, YB, ZB) + print(f"y_cal = {y_cal[0, 0:4]}") + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_where_lt.py b/third_party/ascend/unittest/pytest_ut/test_where_lt.py new file mode 100644 index 0000000000..26158b97de --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_where_lt.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_where_lt_case1(x0, x1): + res = torch.where(x0 < x1, x0, 1) + return res + + +@triton.jit +def triton_where_lt_case1(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.load(in_ptr1 + (x0), xmask) + tmp2 = tmp0 < tmp1 + tmp3 = tl.where(tmp2, tmp0, 1) + tl.store(out_ptr0 + (xindex), tmp3, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 1024, 8), 2, 8192, 1024], + ['float16', (2, 1024, 8), 2, 8192, 1024], + ['int8', (2, 1024, 8), 2, 8192, 1024], +]) +def test_where_lt_case1(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_where_lt_case1(x0, x1) + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_where_lt_case1[ncore, 1, 1](x0, x1, y_cal, x0.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_where_mask.py b/third_party/ascend/unittest/pytest_ut/test_where_mask.py new file mode 100644 index 0000000000..fb1474792a --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_where_mask.py @@ -0,0 +1,60 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +def torch_where_lt_case2(x0, x1): + res = torch.where(x0 < x1, x0, x1) + return res + + +@triton.jit +def triton_where_lt_case2(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.load(in_ptr1 + (x0), xmask) + tmp2 = tmp0 < tmp1 + tmp3 = tl.where(tmp2, tmp0, tmp1) + tl.store(out_ptr0 + (xindex), tmp3, xmask) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 1024, 8), 2, 8192, 1024], + ['float16', (2, 1024, 8), 2, 8192, 1024], + ['int8', (2, 1024, 8), 2, 8192, 1024], +]) +def test_where_lt_case2(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_where_lt_case2(x0, x1) + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_where_lt_case2[ncore, 1, 1](x0, x1, y_cal, x0.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_where_var.py b/third_party/ascend/unittest/pytest_ut/test_where_var.py new file mode 100644 index 0000000000..82adaf50ce --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_where_var.py @@ -0,0 +1,62 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common +# from test_common import pytest_runonce + + +def torch_where_lt_case2(x0, x1): + res = torch.where(x0 < x1, x0, x1) + return res + + +@triton.jit +def triton_where_lt_case2(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK: tl.constexpr, XBLOCK_SUB: tl.constexpr): + xoffset = tl.program_id(0) * XBLOCK + for xoffset_sub in range(0, XBLOCK, XBLOCK_SUB): + xindex = xoffset + xoffset_sub + tl.arange(0, XBLOCK_SUB)[:] + xmask = xindex < xnumel + x0 = xindex + tmp0 = tl.load(in_ptr0 + (x0), xmask) + tmp1 = tl.load(in_ptr1 + (x0), xmask) + tmp2 = tmp0 < tmp1 + tmp3 = tl.where(tmp2, tmp0, tmp1) + tl.store(out_ptr0 + (xindex), tmp3, xmask) + + +# @pytest.mark.usefixtures("pytest_runonce") +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 1024, 8), 2, 8192, 1024], + ['float16', (2, 1024, 8), 2, 8192, 1024], + ['int8', (2, 1024, 8), 2, 8192, 1024], +]) +def test_where_lt_case2(param_list): + dtype, shape, ncore, xblock, xblock_sub = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + x1 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch_where_lt_case2(x0, x1) + y_cal = test_common.generate_tensor(shape, dtype).npu() + triton_where_lt_case2[ncore, 1, 1](x0, x1, y_cal, x0.numel(), xblock, xblock_sub) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_workspace_usage.py b/third_party/ascend/unittest/pytest_ut/test_workspace_usage.py new file mode 100644 index 0000000000..bd3905d244 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_workspace_usage.py @@ -0,0 +1,119 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch + + +@triton.jit +def matmul_mul_kernel( + A_ptr, + B_ptr, + C_ptr, + Out_ptr, + M, + N, + K, + stride_am, + stride_ak, + stride_bk, + stride_bn, + stride_cm, + stride_cn, + stride_om, + stride_on, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_K: tl.constexpr, +): + pid_m = tl.program_id(0) + pid_n = tl.program_id(1) + + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) + rk = tl.arange(0, BLOCK_K) + + m_mask = rm[:, None] < M + n_mask = rn[None, :] < N + full_mask = m_mask & n_mask + + A = tl.load(A_ptr + rm[:, None] * stride_am + rk[None, :] * stride_ak, mask=m_mask, other=0.0) + + B = tl.load(B_ptr + rk[:, None] * stride_bk + rn[None, :] * stride_bn, mask=n_mask, other=0.0) + + C = tl.load(C_ptr + rm[:, None] * stride_cm + rn[None, :] * stride_cn, mask=full_mask, other=0.0) + + AB = tl.dot(A, B) + Out = AB * C + + tl.store(Out_ptr + rm[:, None] * stride_om + rn[None, :] * stride_on, Out, mask=full_mask) + + +def _make_inputs(M: int, N: int, K: int, dtype=torch.float16, device="cpu"): + """Build a packed set of A, B, C, Out on the given device.""" + torch.manual_seed(0) + A = torch.randn(M, K, dtype=dtype, device=device) + B = torch.randn(K, N, dtype=dtype, device=device) + C = torch.randn(M, N, dtype=dtype, device=device) + Out = torch.empty(M, N, dtype=dtype, device=device) + return A, B, C, Out + + +def _ref(A, B, C): + return torch.matmul(A, B) * C + + +def _launch_kernel(A, B, C, Out, BLOCK_M=32, BLOCK_N=32, BLOCK_K=32): + M, K = A.shape + _, N = B.shape + grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(N, BLOCK_N)) + matmul_mul_kernel[grid]( + A, + B, + C, + Out, + M, + N, + K, + A.stride(0), + A.stride(1), + B.stride(0), + B.stride(1), + C.stride(0), + C.stride(1), + Out.stride(0), + Out.stride(1), + BLOCK_M=BLOCK_M, + BLOCK_N=BLOCK_N, + BLOCK_K=BLOCK_K, + ) + + +def test_workspace(): + M = 256 * 64 + N = 256 * 64 + K = 32 + + A, B, C, Out = _make_inputs(M, N, K, device="npu") + # cv pipeline workspace * 4 + _launch_kernel(A, B, C, Out, BLOCK_M=64, BLOCK_N=64, BLOCK_K=32) + ref = _ref(A, B, C) + torch.testing.assert_close(Out, ref, atol=1e-3, rtol=1e-3) diff --git a/third_party/ascend/unittest/pytest_ut/test_xor.py b/third_party/ascend/unittest/pytest_ut/test_xor.py new file mode 100644 index 0000000000..f10caf4536 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_xor.py @@ -0,0 +1,97 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import pytest +import triton +import triton.language as tl +import time +import test_common +import torch +import torch_npu + + +def standard_unary(x0, dtype): + res = x0 + return res + + +def standard_binary(x0, y0, dtype): + res = x0 ^ y0 + return res + + +@triton.jit +def triton_elementwise_unary(in_ptr0, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + ret = x + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +@triton.jit +def triton_elementwise_binary(in_ptr0, in_ptr1, out_ptr0, N: tl.constexpr, NUMEL: tl.constexpr): + idx_block = tl.arange(0, NUMEL) + x = tl.load(in_ptr0 + idx_block, mask=idx_block < N) + y = tl.load(in_ptr1 + idx_block, mask=idx_block < N) + ret = x ^ y + tl.store(out_ptr0 + idx_block, ret, mask=idx_block < N) + + +types = [ + # (torch.float32, 'float32'), + # (torch.float16, 'float16'), + # (torch.bfloat16, 'bfloat16'), + (torch.int8, 'int8'), + # (torch.int16, 'int16'), + # (torch.int32, 'int32'), + # (torch.int64, 'int64'), +] + +shapes = [ + (3, 32), + (-32, 32), + (37, 64), + (-256, 256), + (781, 1024), +] + +map_for_64_t = {37: 31} + + +@pytest.mark.parametrize('dtype,sigtype', types) +@pytest.mark.parametrize('N,NUMEL', shapes) +def test_elementwsie_common(dtype, sigtype, N, NUMEL): + N = (-N) // torch.tensor(0, dtype=dtype).element_size() if N < 0 else N + + if sigtype == "int64": + N = map_for_64_t[N] if N in map_for_64_t else N + + print(f"elementwise : ({N},) {dtype} {sigtype}") + + x0 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + x1 = test_common.generate_tensor(shape=(N, ), dtype=sigtype).npu() + ans = standard_binary(x0, x1, dtype) + print(ans) + + out = torch.zeros((N, ), dtype=dtype).npu() + triton_elementwise_binary[1, 1, 1](x0, x1, out, N=N, NUMEL=NUMEL, debug=True) + print(out) + + test_common.validate_cmp(sigtype, out, ans) diff --git a/third_party/ascend/unittest/pytest_ut/test_xor_sum.py b/third_party/ascend/unittest/pytest_ut/test_xor_sum.py new file mode 100644 index 0000000000..218ef6a79d --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_xor_sum.py @@ -0,0 +1,78 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(in_ptr0, out_ptr0, xnumel, ynumel, XBLOCK: tl.constexpr, RBLOCK: tl.constexpr): + X = xnumel + Y = ynumel + xoffset = tl.program_id(0) * XBLOCK + xindex = xoffset + tl.arange(0, XBLOCK) + + x0 = xindex[:, None] + rbase = tl.arange(0, RBLOCK) + _tmp6 = tl.full([XBLOCK, RBLOCK], 0, tl.int8) + for roffset in range(0, ynumel, RBLOCK): + rindex = roffset + rbase + rmask = None + r1 = rindex[None, :] + tmp0 = tl.load(in_ptr0 + (r1 + (Y * x0)), rmask) + _tmp6 = _tmp6 ^ tmp0 + tmp6 = tl.xor_sum(_tmp6, 1) + + tl.store(out_ptr0 + (xindex), tmp6, None) + + +def bar(tensor): + N, M = tensor.shape + result = torch.zeros(N, dtype=tensor.dtype, device=tensor.device) + for i in range(N): + row_xor_sum = 0 + for j in range(M): + row_xor_sum ^= tensor[i, j].item() + result[i] = row_xor_sum + return result + + +@pytest.mark.parametrize('param_list', [ + ['int8', (64, 32), 64, 32], +]) +def test_case(param_list): + dtype, shape, xblock, rblock = param_list + a = test_common.generate_tensor(shape, dtype).npu() + + std_ret = bar(a) + print(f"std_ret={std_ret}") + + value = torch.empty_strided((a.shape[0], ), (1, ), dtype=eval('torch.' + dtype)).npu() + XBLOCK = xblock + RBLOCK = rblock + NBLOCK = a.shape[0] // XBLOCK + fn_npu_[NBLOCK, 1, 1](a, value, a.shape[0], a.shape[1], XBLOCK, RBLOCK) + print(f"triton_ret={value}") + + torch.testing.assert_close(value, std_ret) diff --git a/third_party/ascend/unittest/pytest_ut/test_zeros.py b/third_party/ascend/unittest/pytest_ut/test_zeros.py new file mode 100644 index 0000000000..3dab18af39 --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_zeros.py @@ -0,0 +1,88 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_f32(output_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + ret = tl.zeros((XB, YB, ZB), dtype=tl.float32) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_f16(output_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + ret = tl.zeros((XB, YB, ZB), dtype=tl.float16) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@triton.jit +def fn_npu_i8(output_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + ret = tl.zeros((XB, YB, ZB), dtype=tl.int8) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 256, 16), 1, 2, 256, 16], + ['float32', (8, 8, 4), 1, 8, 8, 4], + ['float16', (2, 256, 16), 1, 2, 256, 16], + ['float16', (8, 8, 4), 1, 8, 8, 4], + ['int8', (2, 256, 16), 1, 2, 256, 16], + ['int8', (8, 8, 4), 1, 8, 8, 4], +]) +def test_case(param_list): + dtype, shape, ncore, XB, YB, ZB = param_list + + y_ref = torch.full((XB, YB, ZB), 0, dtype=eval('torch.' + dtype)).npu() + + y_cal = torch.randint(1, (XB, YB, ZB), dtype=eval('torch.' + dtype)).npu() + if dtype == "float32": + fn_npu_f32[ncore, 1, 1](y_cal, XB, YB, ZB) + elif dtype == "float16": + fn_npu_f16[ncore, 1, 1](y_cal, XB, YB, ZB) + else: + fn_npu_i8[ncore, 1, 1](y_cal, XB, YB, ZB) + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/pytest_ut/test_zeroslike.py b/third_party/ascend/unittest/pytest_ut/test_zeroslike.py new file mode 100644 index 0000000000..6d6b4a822f --- /dev/null +++ b/third_party/ascend/unittest/pytest_ut/test_zeroslike.py @@ -0,0 +1,63 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import triton +import triton.language as tl +import torch +import torch_npu +import pytest +import test_common + + +@triton.jit +def fn_npu_(output_ptr, x_ptr, XB: tl.constexpr, YB: tl.constexpr, ZB: tl.constexpr): + xidx = tl.arange(0, XB) + yidx = tl.arange(0, YB) + zidx = tl.arange(0, ZB) + + idx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + X = tl.load(x_ptr + idx) + + ret = tl.zeros_like(X) + + oidx = xidx[:, None, None] * YB * ZB + yidx[None, :, None] * ZB + zidx[None, None, :] + + tl.store(output_ptr + oidx, ret) + + +@pytest.mark.parametrize('param_list', [ + ['float32', (2, 256, 16), 1, 2, 256, 16], + ['float32', (8, 8, 4), 1, 8, 8, 4], + ['float16', (2, 256, 16), 1, 2, 256, 16], + ['float16', (8, 8, 4), 1, 8, 8, 4], + ['int8', (2, 256, 16), 1, 2, 256, 16], + ['int8', (8, 8, 4), 1, 8, 8, 4], +]) +def test_case(param_list): + dtype, shape, ncore, XB, YB, ZB = param_list + x0 = test_common.generate_tensor(shape, dtype).npu() + y_ref = torch.zeros_like(x0, dtype=eval('torch.' + dtype)).npu() + print(f"y_ref = {y_ref[0, 0, 0:4]}") + y_cal = torch.zeros(shape, dtype=eval('torch.' + dtype)).npu() + + fn_npu_[ncore, 1, 1](y_cal, x0, XB, YB, ZB) + print(f"y_cal = {y_cal[0, 0, 0:4]}") + test_common.validate_cmp(dtype, y_cal, y_ref) diff --git a/third_party/ascend/unittest/run_prtest.sh b/third_party/ascend/unittest/run_prtest.sh new file mode 100755 index 0000000000..cbff6889be --- /dev/null +++ b/third_party/ascend/unittest/run_prtest.sh @@ -0,0 +1,170 @@ +#!/bin/bash +# notice: this script supports python3.11.x + +set -ex + +script=$(readlink -f "$0") +script_dir=$(dirname "$script") + +# clean old logs +mkdir -p /home/pr_test_log +UNITTEST_DIR="triton-ascend/third_party/ascend/unittest" + +# define summary file path +SUMMARY_FILE="${WORKSPACE}/${UNITTEST_DIR}/summary.txt" + +function clean_cache() { + if [ -d /tmp/torchinductor_* ];then + rm -rf /tmp/torchinductor_* + fi + if [ -d ${HOME}/.triton/dump ];then + rm -rf ${HOME}/.triton/dump + fi + + if [ -d ${HOME}/.triton/cache ];then + rm -rf ${HOME}/.triton/cache + fi +} + + +function run_case_by_multi_card() { + NPU_DEVICES=$(ls /dev/davinci[0-9]* 2>/dev/null | wc -l) + [ $NPU_DEVICES -eq 0 ] && { + echo "No Ascend devices found!" + exit 1 + } + + echo "Detected $NPU_DEVICES Ascend devices" + + test_dir=$1 + cd ${test_dir} + + # clean logs + rm -rf logs && mkdir logs + + # record start time + start_time=$(date +"%Y-%m-%d %H:%M:%S") + echo "===== Test Start Time: ${start_time} =====" + + # run tests and capture exit status + set +e + python -m pytest ${test_dir} -n auto --dist=loadfile -v --junitxml=logs/results.xml | tee logs/raw_output.log + pytest_exit=$? + set -e + + # process logs (add device tags) + awk ' + />> Worker gw[0-9]+ using NPU device/ { + split($0, parts, / /) + dev_id = parts[NF] + worker = parts[3] + print "[" strftime("%Y-%m-%d %H:%M:%S") "| DEV-" dev_id "] " $0 + next + } + { print "[" strftime("%Y-%m-%d %H:%M:%S") "| DEV-" dev_id "] " $0 } + ' logs/raw_output.log > logs/combined.log + + # parse test result statistics + total_tests=0 + passed_tests=0 + failed_tests=0 + skipped_tests=0 + error_tests=0 + + # use Python to parse JUnit XML report + python3 -c " +import xml.etree.ElementTree as ET +import os + +xml_file = os.path.join('logs', 'results.xml') +if not os.path.exists(xml_file): + print('JUnitXML report not found:', xml_file) + exit(1) + +tree = ET.parse(xml_file) +root = tree.getroot() + +total = 0 +passed = 0 +failed = 0 +skipped = 0 +errors = 0 + +# 遍历所有testsuite +for testsuite in root.findall('testsuite'): + total += int(testsuite.get('tests', 0)) + passed += int(testsuite.get('tests', 0)) - int(testsuite.get('errors', 0)) - int(testsuite.get('failures', 0)) - int(testsuite.get('skipped', 0)) + failed += int(testsuite.get('failures', 0)) + skipped += int(testsuite.get('skipped', 0)) + errors += int(testsuite.get('errors', 0)) + +print(f'total_tests={total}') +print(f'passed_tests={passed}') +print(f'failed_tests={failed}') +print(f'skipped_tests={skipped}') +print(f'error_tests={errors}') +" > logs/stats.tmp + + # load stats + source logs/stats.tmp + rm logs/stats.tmp + + # record end time + end_time=$(date +"%Y-%m-%d %H:%M:%S") + duration=$(( $(date -d "$end_time" +%s) - $(date -d "$start_time" +%s) )) + duration_str=$(printf "%02dh %02dm %02ds" $((duration/3600)) $(((duration%3600)/60)) $((duration%60))) + + # generate summary + stats_summary=" +===== Test Summary - [generalization_cases] ===== +Test Directory: $(basename ${test_dir}) +Test Start Time: ${start_time} +Test End Time: ${end_time} +Total Duration: ${duration_str} +------------------------ +Total Tests: ${total_tests} +Passed Tests: ${passed_tests} +Failed Tests: ${failed_tests} +Skipped Tests: ${skipped_tests} +Error Tests: ${error_tests} +Success Rate: $(( passed_tests * 100 / total_tests ))% (Passed/Total) +NPU Devices: ${NPU_DEVICES} +======================== +" + + # output stats summary to console + echo "${stats_summary}" + + # append stats summary to summary.txt + echo "${stats_summary}" >> ${SUMMARY_FILE} + + echo "========================================" + echo "All tests completed!" + echo "JUnit Report: logs/results.xml" + echo "Combined Log: logs/combined.log" + echo "Stats Summary has been appended to: ${SUMMARY_FILE}" + echo "========================================" + + zip_file=$2 + cd ${test_dir}/logs + zip ${zip_file} combined.log + cp ${zip_file} "/home/pr_test_log" + + # return pytest exit status + return $pytest_exit +} + +# initialize stats file +echo "Generate Time: $(date +"%Y-%m-%d %H:%M:%S")" >> ${SUMMARY_FILE} +echo "========================================" >> ${SUMMARY_FILE} + +# run gene case +zip_file="test_generalization_case_$(date +%Y%m%d).zip" +TEST_generalization="${WORKSPACE}/${UNITTEST_DIR}/generalization_cases" +clean_cache +run_case_by_multi_card ${TEST_generalization} ${zip_file} + +echo "========================================" >> ${SUMMARY_FILE} + +# copy summary.txt to /home/pr_test_log +cp ${SUMMARY_FILE} /home/pr_test_log diff --git a/third_party/ascend/unittest/ubtuner_ut/test_ubtuner.py b/third_party/ascend/unittest/ubtuner_ut/test_ubtuner.py new file mode 100755 index 0000000000..8b15508903 --- /dev/null +++ b/third_party/ascend/unittest/ubtuner_ut/test_ubtuner.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Pytest tests for ubtuner decorator. + +Tests cover: +1. UBTuner decorator detection and error handling +2. Decorator order validation (autotune outside ubtuner) +3. Two-chain kernel UB overflow scenarios +""" +import os +import shutil +import sys +import tempfile + +import pytest + +import torch +try: + import torch_npu # noqa: F401 +except ImportError: + torch_npu = None + +import triton +import triton.language as tl + +from triton.backends.ascend.runtime.ubtuner import ubtuner, get_origin_fn + +# Set environment before imports +os.environ.setdefault("TORCH_DEVICE_BACKEND_AUTOLOAD", "0") + +# Skip all tests if NPU is not available +pytestmark = pytest.mark.skipif(torch_npu is None or not (hasattr(torch, "npu") and torch.npu.is_available()), + reason="requires torch_npu runtime") + +BLOCK = int(os.environ.get("BLOCK", "20480")) + + +# ============================================================================= +# Test: Decorator order validation +# ============================================================================= +class TestDecoratorOrder: + """Test that decorator order is correctly validated.""" + + def test_autotune_outside_ubtuner_allowed(self): + """Test: @autotune outside @ubtuner is allowed.""" + + @triton.autotune( + configs=[triton.Config(kwargs={'N': 512})], + key=['N'], + ) + @ubtuner(key=['N']) + @triton.jit + def kernel_allowed(a_ptr, b_ptr, out_ptr, N: tl.constexpr): + offs = tl.arange(0, N) + a = tl.load(a_ptr + offs) + b = tl.load(b_ptr + offs) + r = a * b + tl.store(out_ptr + offs, r) + + # Should not raise any error when defining + assert kernel_allowed is not None + + def test_ubtuner_outside_autotune_raises_error(self): + """Test: @ubtuner outside @autotune should raise ValueError.""" + + with pytest.raises(ValueError) as exc_info: + + @ubtuner(key=['N']) + @triton.autotune( + configs=[triton.Config(kwargs={'N': 512})], + key=['N'], + ) + @triton.jit + def kernel_disallowed(a_ptr, b_ptr, out_ptr, N: tl.constexpr): + offs = tl.arange(0, N) + a = tl.load(a_ptr + offs) + b = tl.load(b_ptr + offs) + r = a * b + tl.store(out_ptr + offs, r) + + assert "Cannot apply @ubtuner decorator" in str(exc_info.value) + assert "@autotune" in str(exc_info.value) + + def test_ubtuner_with_intermediate_decorator(self): + """Test: @ubtuner with intermediate decorator should work when autotune is outermost.""" + + def some_decorator(fn): + return fn + + @triton.autotune( + configs=[triton.Config(kwargs={'N': 512})], + key=['N'], + ) + @some_decorator + @ubtuner(key=['N']) + @triton.jit + def kernel_with_intermediate(a_ptr, b_ptr, out_ptr, N: tl.constexpr): + offs = tl.arange(0, N) + a = tl.load(a_ptr + offs) + b = tl.load(b_ptr + offs) + r = a * b + tl.store(out_ptr + offs, r) + + assert kernel_with_intermediate is not None + + +# ============================================================================= +# Base kernel for TestTwoChainKernel - defined once, decorated per test +# ============================================================================= +@triton.jit +def _add_kernel_base( + x_ptr, + y_ptr, + output_ptr, + n_elements, + BLOCK_SIZE: tl.constexpr, + ITERATIONS: tl.constexpr, +): + pid = tl.program_id(0) + combined_block_start = pid * BLOCK_SIZE * ITERATIONS + + for i in range(0, ITERATIONS): + curr_block_start = combined_block_start + i * BLOCK_SIZE + offsets = curr_block_start + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + y = tl.load(y_ptr + offsets, mask=mask) + output = x + y + tl.store(output_ptr + offsets, output, mask=mask) + + +# ============================================================================= +# Test: Two-chain kernel scenarios +# ============================================================================= +class TestTwoChainKernel: + """Test two-chain kernel UB overflow scenarios using add_kernel.""" + + @pytest.fixture(autouse=True) + def setup(self): + """Setup test environment.""" + with tempfile.TemporaryDirectory() as tmpdir: + os.environ["TRITON_CACHE_DIR"] = tmpdir + os.environ["TRITON_ENABLE_UBTUNER"] = "compile" + shutil.rmtree(tmpdir, ignore_errors=True) + yield + # Cleanup after test + shutil.rmtree(tmpdir, ignore_errors=True) + os.environ.pop("TRITON_ENABLE_UBTUNER", None) + os.environ.pop("TRITON_CACHE_DIR", None) + + def _create_tensors(self, size=BLOCK): + """Create input and output tensors for add_kernel.""" + x = torch.rand(size, dtype=torch.float32).npu() + y = torch.rand(size, dtype=torch.float32).npu() + output = torch.empty(size, dtype=torch.float32).npu() + expected = x.cpu() + y.cpu() + return x, y, output, expected + + def _make_kernel(self, decorators): + """Apply decorators to base kernel.""" + fn = get_origin_fn(_add_kernel_base) + setattr(fn, '_ubtuned', False) # 重置_add_kernel_base属性 + kernel = _add_kernel_base + for dec in decorators: + kernel = dec(kernel) + return kernel + + def test_ubtuner_only_no_overflow(self): + """Test: @ubtuner only - should not overflow (with memory scheduler).""" + kernel = self._make_kernel([ + lambda fn: ubtuner(key=['BLOCK_SIZE'])(fn), + ]) + x, y, output, expected = self._create_tensors() + try: + kernel[(1, )](x, y, output, output.numel(), BLOCK_SIZE=BLOCK, ITERATIONS=8, multibuffer=True) + torch.npu.synchronize() + except Exception as e: + if "ub overflow" in str(e).lower(): + pytest.fail(f"UB overflow occurred with ubtuner: {e}") + raise + torch.testing.assert_close(output.cpu(), expected, rtol=1e-5, atol=1e-5) + + def test_autotune_with_ubtuner_inner_no_overflow(self): + """Test: @autotune (outer) + @ubtuner (inner) - should not overflow.""" + kernel = self._make_kernel([ + lambda fn: ubtuner(key=['BLOCK_SIZE'])(fn), + lambda fn: triton.autotune(configs=[triton.Config(kwargs={'BLOCK_SIZE': BLOCK})], key=['BLOCK_SIZE'])(fn), + ]) + x, y, output, expected = self._create_tensors() + try: + kernel[(1, )](x, y, output, output.numel(), ITERATIONS=8, multibuffer=True) + torch.npu.synchronize() + except Exception as e: + if "ub overflow" in str(e).lower(): + pytest.fail(f"UB overflow occurred: {e}") + raise + torch.testing.assert_close(output.cpu(), expected, rtol=1e-5, atol=1e-5) + + def test_autotune_only_overflow(self): + """Test: @autotune only (without TRITON_ENABLE_UBTUNER) - should overflow.""" + os.environ.pop("TRITON_ENABLE_UBTUNER", None) + kernel = self._make_kernel([ + lambda fn: triton.autotune(configs=[triton.Config(kwargs={'BLOCK_SIZE': BLOCK})], key=['BLOCK_SIZE'])(fn), + ]) + x, y, output, _ = self._create_tensors() + with pytest.raises(Exception) as exc_info: + kernel[(1, )](x, y, output, output.numel(), ITERATIONS=8, multibuffer=True) + torch.npu.synchronize() + assert "ub overflow" in str(exc_info.value).lower() + + def test_autotune_with_triton_enable_ubtuner(self): + """Test: @autotune with TRITON_ENABLE_UBTUNER=compile - should not overflow.""" + kernel = self._make_kernel([ + lambda fn: triton.autotune(configs=[triton.Config(kwargs={'BLOCK_SIZE': BLOCK})], key=['BLOCK_SIZE'])(fn), + ]) + x, y, output, expected = self._create_tensors() + try: + kernel[(1, )](x, y, output, output.numel(), ITERATIONS=8, multibuffer=True) + torch.npu.synchronize() + except Exception as e: + if "ub overflow" in str(e).lower(): + pytest.fail(f"UB overflow occurred with TRITON_ENABLE_UBTUNER=compile: {e}") + raise + torch.testing.assert_close(output.cpu(), expected, rtol=1e-5, atol=1e-5) + + +if __name__ == "__main__": + # Run pytest when executed directly + sys.exit(pytest.main([__file__, "-v"]))